使用定时器计时会中断嵌入式微控制器
题
这个问题是关于在没有操作系统的情况下对小型微控制器进行编程的问题。我目前对 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 秒触发计时器也可用于其他目的)。
还有其他想法吗?
解决方案
PIC 架构是最原子的。它确保对内存文件的所有读取-修改-写入操作都是“原子的”。虽然执行整个读取-修改-写入需要 4 个时钟周期,但所有 4 个时钟周期都消耗在一条指令中,并且下一条指令使用下一个 4 个时钟周期。这就是管道的工作方式。在 8 个时钟周期中,有两条指令在流水线中。
如果该值大于 8 位,则会出现问题,因为 PIC 是 8 位机器,并且较大的操作数在多个指令中处理。这将引入原子问题。
其他提示
写入值然后检查它是否是所需的值似乎是最简单的选择。
do {
sec_counter = value;
} while (sec_counter != value);
顺便说一句,如果使用 C,你应该使变量可变。
如果您需要读取该值,则可以读取两次。
do {
value = sec_counter;
} while (value != sec_counter);
在设置计数器之前,您肯定需要禁用中断。尽管它可能很丑陋,但这是必要的。在配置影响 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;
}
通过这种方式,您可以抽象启动方法(并在需要时隐藏丑陋之处)。例如,您可以轻松更改它以启动硬件计时器,而不会影响该方法的调用者。
由于对 sec_counter 变量的访问不是原子的,因此如果您想要确定性行为,实际上没有办法避免在主线代码中访问该变量之前禁用中断并在访问后恢复中断状态。这可能是比专用硬件计时器来完成此任务更好的选择(除非您有多余的计时器,在这种情况下您最好使用一个)。
如果您下载了 Microchip 的免费 TCP/IP 堆栈,其中有一些例程使用计时器溢出来跟踪经过的时间。特别是“tick.c”和“tick.h”。只需将这些文件复制到您的项目中即可。
在这些文件中,您可以看到它们是如何做到的。
对于少于 256 个移动是原子的这一点并不奇怪 - 移动 8 位值是一个操作码,因此它是原子的。
对于像 PIC 这样的微控制器,最好的解决方案是在更改定时器值之前禁用中断。您甚至可以在更改主循环中的变量时检查中断标志的值,并根据需要进行处理。让它成为一个改变变量值的函数,你甚至可以从 ISR 中调用它。
那么,比较汇编代码是什么样的呢?
考虑到它是向下计数的,并且它只是一个零比较,如果它首先检查 MSB,然后检查 LSB,应该是安全的。可能存在损坏,但如果它位于 0x100 和 0xff 之间并且损坏的比较值是 0x1ff,则并不重要。
按照您现在使用计时器的方式,无论如何它都不会计算整秒,因为您可能会在一个周期的中间更改它。所以,如果你不关心的话。在我看来,最好的方法是读取值,然后比较差异。它需要多一些OP,但没有多线程问题。(因为计时器具有优先级)
如果你对时间值更严格,我会在倒数到0时自动禁用计时器,并清除计时器的内部计数器并在需要时激活。
将 main() 上的代码部分移至适当的函数,并由 ISR 有条件地调用它。
此外,为了避免任何类型的延迟或丢失滴答声,请选择此定时器 ISR 作为高优先级中断(PIC18 有两个级别)。
一种方法是让中断保留一个字节变量,并设置其他东西,至少每 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); }
没有人解决这个问题 阅读 多字节硬件寄存器(例如定时器。当您读取计时器时,计时器可能会翻转并增加其第二个字节。
假设它是 0x0001ffff,你就读了它。您可能会得到 0x0010ffff 或 0x00010000。
16位外设寄存器是 易挥发的 到你的代码。
对于任何 易挥发的 “变量”,我使用双重读取技术。
do {
t = timer;
} while (t != timer);