2009-05-21 35 views
4

这个问题是关于编程没有OS的小型微控制器。特别是,我目前对PICs感兴趣,但问题是一般的。使用定时器保持时间中断嵌入式微控制器

我见过几次的保持时间以下模式:

定时器中断代码(比如定时器触发每秒):

... 
if (sec_counter > 0) 
    sec_counter--; 
... 

主线代码(非中断):

sec_counter = 500; // 500 seconds 

while (sec_counter) 
{ 
    // .. do stuff 
} 

主线代码可能会重复,将计数器设置为各种值(不仅仅是秒)等等。

在我看来,当主线代码中的sec_counter分配不是原子时,这里有一个竞争条件。例如,在PIC18中,分配被转换为4个ASM语句(在此之前加载每个字节并从存储体中选择正确的字节)。如果中断代码出现在中间,则最终值可能会损坏。

奇怪的是,如果赋值小于256,赋值原子,所以没有问题。

我对这个问题正确吗? 您使用什么模式来正确实现此类行为?我看到几个选项:

  • 禁止中断每个任务之前sec_counter并启用后 - 这是不是漂亮
  • 不要使用中断,但相应地开始,然后调查的一个单独的计时器。这是干净的,但用完了一个定时器(在前面的情况下,1秒定时器也可用于其他目的)。

还有其他想法吗?

回答

2

PIC体系结构尽可能原子化。它确保对存储器文件的所有读取 - 修改 - 写入操作都是“原子”的。尽管执行整个读取 - 修改 - 写入需要4个时钟,但是所有4个时钟都在单个指令中消耗,并且下一个指令使用下一个4个时钟周期。这是管道工作的方式。在8个时钟中,两条指令正在处理中。

如果该值大于8位,则它成为一个问题,因为PIC是一个8位机器,而较大的操作数在多个指令中处理。这将引入原子问题。

1

编写该值,然后检查它是否是所需的值似乎是最简单的选择。

do { 
sec_counter = value; 
} while (sec_counter != value); 

顺便说一句,你应该使用是否C.

如果你需要阅读的价值,那么你可以读了两遍使变量波动。

do { 
    value = sec_counter; 
} while (value != sec_counter); 
+0

有趣的方法,虽然怀疑比禁用中断更清洁。我不知道我需要volatile,因为无论如何编译器优化都是禁用的。 – 2009-05-21 13:08:05

+0

我希望这己技巧被注释掉,当你使用它,因为它要气色好怪异的人谁不立即意识到它的防护安剑铮,卓杰中断改变一个“C指令”中的值。 – Martin 2009-05-22 06:50:03

+0

从多个内存映射寄存器中读取禁用中断不起作用时,它也可以工作。 – Dipstick 2009-05-22 15:53:41

0

那么,比较汇编代码是什么样的?

考虑到它倒数,并且它只是一个零比较,它应该是安全的,如果它首先检查MSB,然后检查LSB。可能存在破坏,但它是否在0x100和0xff之间的中间并且损坏的比较值是0x1ff并不重要。

+0

这个问题与我的想法不符。这是主线代码和ISR写入相同的变量 – 2009-05-21 13:10:44

1

在设置计数器之前,您一定需要禁用中断。丑陋,因为它可能是必要的。在配置影响ISR方法的硬件寄存器或软件变量之前,始终禁用中断是一种很好的做法。如果您使用C编写,则应将所有操作视为非原子操作。如果你发现你必须多次查看生成的程序集,那么最好放弃C并在程序集中进行编程。根据我的经验,这种情况很少。

关于讨论过这个问题,这是我的建议:

ISR: 
if (countDownFlag) 
{ 
    sec_counter--; 
} 

,并设置计数器:

// make sure the countdown isn't running 
sec_counter = 500; 
countDownFlag = true; 

... 
// Countdown finished 
countDownFlag = false; 

你需要一个额外的变量,是更好的功能来包装的一切:

void startCountDown(int startValue) 
{ 
    sec_counter = 500; 
    countDownFlag = true; 
} 

这样你就可以抽象出起始方法(如果需要的话可以隐藏起来很丑)。例如,您可以轻松将其更改为启动硬件计时器,而不会影响方法的调用者。

0

你现在使用你的计时器的方式,它不会计算整秒,因为你可能会在一个周期中改变它。 所以,如果你不关心它。在我看来,最好的方法是阅读价值,然后比较差异。 (因为定时器有优先权)

如果你对时间值更严格,我会自动禁用定时器,一旦它减少到0,并清除计时器的内部计数器并在需要时激活。

0

将main()中的代码部分移动到适当的函数中,并让ISR有条件地调用它。另外,为了避免任何类型的延迟或丢失滴答,选择此定时器ISR为高级中断(PIC18有两个级别)。

1

由于对sec_counter变量的访问不是原子的,如果您需要确定性行为,在访问主线代码中的此变量并在访问后恢复中断状态之前,确实无法避免禁用中断。这可能是一个比为这个任务专门配置硬件定时器更好的选择(除非你有多余的定时器,在这种情况下你可以使用它)。

1

如果您下载Microchip免费的TCP/IP协议栈,那里有一些例程使用定时器溢出来跟踪经过的时间。特别是“tick.c”和“tick.h”。只需将这些文件复制到您的项目。

在这些文件中你可以看到他们是如何做到的。

1

对于小于256个原子的移动并不是很好奇 - 移动一个8位的值是一个操作码,这样就像原子一样。

PIC等微控制器的最佳解决方案是在更改定时器值之前禁用中断。当你在主循环中改变变量并且如果你想要的话,你可以检查中断标志的值。使它成为一个可以改变变量值的函数,你甚至可以从ISR调用它。

0

一种方法是有一个中断保持一个字节变量,还有别的都会调用至少每256次反被击中;这样做:

 
// ub==unsigned char; ui==unsigned int; ul==unsigned long 
ub now_ctr; // This one is hit by the interrupt 
ub prev_ctr; 
ul big_ctr; 

void poll_counter(void) 
{ 
    ub delta_ctr; 

    delta_ctr = (ub)(now_ctr-prev_ctr); 
    big_ctr += delta_ctr; 
    prev_ctr += delta_ctr; 
}

细微变化,如果你不介意迫使中断的计数器保持同步与你的大计数器的LSB:

 
ul big_ctr; 
void poll_counter(void) 
{ 
    big_ctr += (ub)(now_ctr - big_ctr); 
} 
0

没有一个解决的问题阅读多字节硬件寄存器(例如定时器。 定时器可以翻转,增加它的第二个字节,而你读它。

说这是0x0001ffff和你读它,你可能摹等于0x0010ffff或0x00010000。

16位外围寄存器是挥发性至代码。

对于任何挥发性“变量”,我用的是双重阅读技巧。

do { 
     t = timer; 
} while (t != timer);