4

这个问题是关于在没有操作系统的情况下对小型微控制器进行编程。特别是,我目前对图片感兴趣,但问题很笼统。

我已经多次看到以下模式来保持时间:

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

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

还有其他想法吗?

4

11 回答 11

2

在设置计数器之前,您肯定需要禁用中断。尽管它可能很丑陋,但它是必要的。在配置影响 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;
}

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

于 2009-05-21T12:59:32.157 回答
2

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

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

于 2009-05-21T13:33:24.990 回答
1

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

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

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

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

do {
    value = sec_counter;
} while (value != sec_counter);
于 2009-05-21T12:31:17.160 回答
1

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

于 2009-05-21T14:33:04.153 回答
1

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

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

于 2009-05-21T19:48:53.237 回答
1

少于 256 次移动是原子的并不那么好奇——移动一个 8 位的值是一个操作码,所以这就像你得到的那样是原子的。

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

于 2009-05-23T15:35:58.650 回答
0

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

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

于 2009-05-21T12:47:36.613 回答
0

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

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

于 2009-05-21T13:15:15.853 回答
0

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

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

于 2009-05-21T14:05:23.897 回答
0

一种方法是让中断保留一个字节变量,并让其他东西至少每 256 次计数器被调用一次;做类似的事情:

// ub==无符号字符;ui==无符号整数;ul==无符号长
ub now_ctr; // 这个被中断击中
ub prev_ctr;
ul big_ctr;

无效轮询计数器(无效)
{
  ub delta_ctr;

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

如果您不介意强制中断计数器与大计数器的 LSB 保持同步,则略有不同:

ul big_ctr;
无效轮询计数器(无效)
{
  big_ctr += (ub)(now_ctr - big_ctr);
}
于 2010-08-19T18:42:33.877 回答
0

没有人解决读取多字节硬件寄存器的问题(例如计时器。计时器可能会在您读取它时翻转并增加其第二个字节。

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

16 位外设寄存器对您的代码是易失的。

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

do {
       t = timer;
 } while (t != timer);
于 2017-06-02T08:41:27.940 回答