使用定时器保持时间会中断嵌入式微控制器

时间:2009-05-21 12:19:39

标签: embedded interrupt microchip pic18

这个问题是关于编写没有操作系统的小型微控制器。特别是,我现在对PIC感兴趣,但问题很笼罩。

为了节省时间,我多次看到以下模式:

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

...
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秒的点火计时器也可以用于其他目的)。

还有其他想法吗?

11 个答案:

答案 0 :(得分:2)

PIC架构是原子的。它确保对内存文件的所有读 - 修改 - 写操作都是“原子的”。虽然执行整个读 - 修改 - 写操作需要4个时钟,但所有4个时钟在单个指令中消耗,下一个指令使用下一个4个时钟周期。这是管道工作的方式。在8个时钟周期中,有两条指令正在进行中。

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

答案 1 :(得分:1)

写下该值,然后检查它是否是所需的值似乎是最简单的替代方法。

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

顺便说一句,如果使用C,你应该使变量变为volatile。

如果您需要阅读该值,则可以阅读两次。

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

答案 2 :(得分: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;
}

这样你就抽象了起始方法(并在需要时隐藏丑陋)。例如,您可以轻松更改它以启动硬件计时器,而不会影响方法的调用者。

答案 3 :(得分:1)

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

答案 4 :(得分:1)

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

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

答案 5 :(得分:1)

对原子中少于256次的移动并不是那么好奇 - 移动8位值是一个操作码,因此它就像你得到的原子一样。

PIC这样的微控制器的最佳解决方案是在更改定时器值之前禁用中断。您甚至可以在更改主循环中的变量时检查中断标志的值,并根据需要进行处理。使它成为一个改变变量值的函数,你甚至可以从ISR中调用它。

答案 6 :(得分:0)

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

考虑到它向下计数,并且它只是一个零比较,如果它首先检查MSB,那么它应该是安全的,然后是LSB。可能存在损坏,但如果它位于0x100和0xff之间并且损坏的比较值为0x1ff则无关紧要。

答案 7 :(得分:0)

现在使用计时器的方式,无论如何都不会计算整秒,因为您可能会在周期中间更改它。 所以,如果你不关心它。在我看来,最好的方法是读取值,然后比较差异。它需要更多的OP,但没有多线程问题。(由于计时器具有优先权)

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

答案 8 :(得分:0)

将main()上的代码部分移动到正确的函数,并由ISR有条件地调用它。

另外,为了避免任何类型的延迟或丢失滴答,请选择此定时器ISR作为高prio中断(PIC18有两个级别)。

答案 9 :(得分: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);
}

答案 10 :(得分:0)

没有人解决读取多字节硬件寄存器(例如定时器)的问题。 当你正在阅读时,计时器可以翻转并递增第二个字节。

说它是0x0001ffff并且你读了它。您可能会得到0x0010ffff或0x00010000。

16位外设寄存器 volatile 代码。

对于任何 volatile “变量”,我使用双读技术。

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