这个问题是关于编写没有操作系统的小型微控制器。特别是,我现在对PIC感兴趣,但问题很笼罩。
为了节省时间,我多次看到以下模式:
定时器中断代码(比如定时器每秒触发):
...
if (sec_counter > 0)
sec_counter--;
...
主线代码(非中断):
sec_counter = 500; // 500 seconds
while (sec_counter)
{
// .. do stuff
}
主线代码可能重复,将计数器设置为各种值(不仅仅是秒)等等。
在我看来,当主线代码中sec_counter
的赋值不是原子时,就存在竞争条件。例如,在PIC18中,赋值转换为4个ASM语句(此时加载每个字节并在此之前从存储区中选择正确的字节)。如果中断代码位于此中间,则最终值可能已损坏。
奇怪的是,如果分配的值小于256,则赋值是原子,所以没有问题。
我对这个问题是对的吗? 您使用什么模式来正确实现此类行为?我看到了几个选项:
还有其他想法吗?
答案 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);