除本章中讨论的问题外,还有其他更妙的问题 第1部分来自硬件和软件的交互。这些可能不符合折返的经典定义,但是会带来类似的风险,并且需要类似的解决方案。
我们在嵌入式硬件和软件之间的模糊接口上工作,由于我们的代码和设备之间的相互作用,这还会产生其他问题。有些导致不稳定的故障,而且几乎无法诊断,导致我们的客户大为恼火。
所有最严重的错误是很少出现的错误,无法复制。但是,可靠的系统不能容忍任何类型的缺陷,尤其是通过我们测试的随机缺陷,也许被“啊,这只是小故障”行为所忽略。
每当硬件和软件异步交互时,潜在的恶魔就会潜伏。也就是说,当某些物理设备以自己的速率运行时,将以以不同速度运行的固件采样。
我浏览了一些开源代码,并遇到了异步交互的典型示例。OAR Corporation提供的RTEMS实时操作系统是经过精心编写的,结构合理的产品,具有许多简洁的功能。
但是,至少对于68302发行版而言,计时器处理例程存在某种缺陷,这种缺陷很少会发生,但可能会造成灾难性的后果。这只是我经常看到的埋在专有固件中的问题的一个非常公开的例子。
该代码简单明了,看起来与其他计时器处理程序非常相似。
inttimer_hi;
中断计时器(){ ++ timer_hi;}
长read_timer(无效){ unsigned int低,高;
低= inword(硬件寄存器);
高= timer_hi; return(high << 16 + low);}
16位硬件计时器溢出时,将调用中断服务程序。ISR为硬件提供服务,递增名为timer_hi的全局变量,然后返回。
因此,timer_hi会将硬件计数的次数保持为65536。函数read_timer返回当前的“时间”(ISR和硬件计时器所跟踪的经过时间,以微秒为单位)。它也令人愉快地没有并发症。
像大多数此类例程一样,它读取硬件定时器寄存器的当前内容,将Timer_hi左移16位,并添加从定时器读取的值。也就是说,当前时间是计时器的当前值和溢出次数的串联。
假设硬件滚动了5次,创建了五个中断。timer_hi等于5。当我们调用read_timer时,内部寄存器可能是0x1000。该例程返回值0x51000。简单,看似没有问题。
比赛条件
但是,让我们仔细考虑一下。确实有两件事同时发生。与RTOS分配CPU资源的多任务环境不同,因此所有任务似乎在同时运行,因此不是同时存在,这意味着“显然是在同一时间”。
否,在这种情况下,只要调用read_timer中的代码便会执行,并且时钟计数计时器以其自己的速率运行。这两个区域是同步的。
硬件设计的基本规则是,每当异步事件突然同步时,就会惊慌失措。例如,当两个不同的处理器共享一个内存阵列时,需要大量的卷积逻辑,以确保任何时候都只能访问一个。如果CPU使用不同的时钟,则问题将更加棘手,因为设计人员可能会发现这两个请求独占内存访问的时间彼此之间相差不超过十亿分之一秒。
这就是所谓的“种族”的条件,是许多grayhairs和戏剧性failures.One源read_time r'srace条件可能包括:
它读取硬件,并获得0xffff的值。
在有机会从变量timer_hi检索大部分时间之前,硬件再次递增至0x0000。
溢出触发中断。ISR运行timer_hi现在为0x0001,而不是0,因为它之前只有10 ns。
ISR返回了我们无所畏惧的read_timer例程,没有中断发生的想法,将新的0x0001与先前读取的计时器值0xffff巧妙地连接起来,并返回了0x1ffff(一个非常不正确的值)。
或者,假设在禁用中断的时间(例如,是否有其他ISR需要时间)期间调用read_timer。编写封装的代码和驱动程序的少数危险之一是,您完全确定例程被调用时系统所处的状态。在这种情况下:
read_timer启动。计时器为0xffff,无溢出。
在发生其他事情之前,它计数为0x0000。中断关闭时,挂起的中断会延迟。
read_timer 返回值0x0000,而不是正确的0x10000或合理的0xffff。
因此,看起来如此简单的算法存在相当细微的问题,因此需要一种更复杂的方法。RTEMS RTOS,至少在其68 k分布中,可能会产生偶发但严重的错误。
当然,被误读的可能性很小。实际上,随着我们称为read_timer的频率降低,出现错误的机率直线下降。种族状况多久出现一次?每周一次?
许多嵌入式系统运行了好几年却没有重新启动。可靠的产品不得包含易碎的代码。作为健壮的系统设计者,我们面临的挑战是识别此类问题并创建每次都能正常工作的替代解决方案。
有哪些选项可用?
幸运的是,确实存在许多解决方案。最简单的方法是在尝试读取计时器之前先停止它。这是一个简单且有保证的解决方案,不会有溢出的可能使上半部分和下半部分数据不同步。我们会浪费时间。由于硬件通常会对处理器的时钟进行计数,或者将时钟除以一个小数,因此在执行读取操作的少数指令期间,它可能会丢失很多滴答声。
如果中断在禁用计数后导致上下文切换,则问题将更加严重。在此期间关闭中断将消除不必要的任务,但会增加系统延迟和复杂性。
我只是讨厌禁用中断,系统延迟增加,有时调试工具有点时髦。如果我看到很多禁用中断指令,那么在阅读代码时会出现一个红旗。尽管不一定很糟糕,但这通常表明该代码被强行提交(通过英勇的调试工作,而不是精心设计),或者环境有些困难和奇怪。
另一种解决方案是先读取timer_hi变量,然后读取hardwaretimer,然后重新读取timer_hi。如果两个变量值都不相同,则会发生中断。迭代直到两个变量读取相等。好处是:正确的数据,中断持续存在,并且系统不会丢失计数。
缺点:在负载重的多任务环境中,例程可能在获取两次相同的读取之前可能循环很长时间。函数的执行时间是不确定的。我们已经从一个非常简单的计时器读取器变成了可以运行几毫秒而不是几微秒的更复杂的代码。
另一个选择可能是简单地禁用读取周围的中断。在我们已经阅读完ISR之后,它会阻止ISR获得控制权和changetimer_hi,但会带来另一个问题。
我们输入read_timer并立即关闭中断。假设硬件计时器在我们臭名昭著的0xffff处,并且timer_hi为零。现在,在代码有机会做其他事情之前,就会发生溢出。随着上下文切换的关闭,我们错过了过渡。
代码从定时器寄存器和fromtimer_hi读取零,返回零,而不是正确的0x10000,甚至返回0x0ffff区域。尽管我反对这种做法,但是禁用中断可能确实是一件好事。
有了它们,我们的阅读程序总是有可能在很长一段时间内被较高优先级的任务和其他ISR暂停。也许足够长的时间让计时器轮流滚动几次,所以让我们尝试修复代码。考虑以下:
longRead_timer(void){
unsigned int low,high;
push_interrupt_state;
disable_interrupts;
low = inword(Timer_register);
高= timer_hi; if(inword(timer_overflow))
{++高;
low = inword(timer_register);}
pop_interrupt_state;
return(((ulong)high)<< 16 +(ulong)low);
}
我们对RTEMS代码进行了三处更改。首先,中断已关闭,如所述。其次,您会注意到没有显式的中断重新启用。出现了两个新的伪C语句,它们推动并弹出中断状态。请片刻,这是管理系统中断状态的一种更复杂的方法。
第三个变化是一个新测试,该测试针对称为“ timer_overflow”的东西,它是硬件的一部分的输入端口。大多数定时器都有一个可测试的位,表明发生了溢出。我们检查此项以查看在关闭中断和从设备中读取时间的较低部分之间是否发生了溢出。如果ISR变量无效,则timer_hi无法正确反映此类溢出。
如果发生溢出,我们将测试状态位并重新读取硬件计数。手动增加高电平部分可纠正悬浮的ISR。然后,代码将两个固定值连接起来,每次都返回正确的结果。随着中断的关闭,我们增加了延迟。但是,没有循环。代码的执行时间完全是确定性的。
其他RTOS
不幸的是,只要我们需要多个读取来访问与软件异步更改的数据,就会发生争用情况。如果要从移动的机器中读取X和Y坐标(即使只有8位分辨率),则存在危险,如果需要两次读取,它们可能会严重失步。通过字节宽端口管理的十位编码器可能会产生类似的风险。
多年来,在处理了许多嵌入式系统中的这个问题后,我对在RTEMS RTOS中看到它并不感到震惊。毕竟,这是一个相当晦涩的问题,尽管这是非常现实的,甚至可能是致命的。
为了好玩,我浏览了uC / OS的源代码,这是另一个非常流行的操作系统,其源代码在网上(请参阅www.ucos-ii.com)。uC / OS从不读取计时器的硬件。它只计算ISR检测到的溢出,因为不需要更高分辨率。不可能有不正确的值。
你们中的某些人,尤其是那些具有硬件背景的人,可能对我尚未提及的显而易见的解决方案不屑一顾。在计时器和系统之间添加一个输入捕获寄存器;该代码将“将值锁定到锁存器”位置1,然后安全地读取此不变数据。该寄存器无非是一个并行锁存器,与输入数据一样宽。
当strobedit将数据锁定到寄存器中时,一条时钟线驱动锁存器中的每个触发器。输出被馈送到一对处理器输入端口。
当需要读取一个安全的,不变的值时,代码会发出“立即保留数据”命令,该命令将编码器值选通。因此,所有位都可以存储,并且可以随时由软件读取,而不必担心读取之间会发生变化。一些设计人员将寄存器的时钟输入绑定到端口控制线之一。
I / O读指令然后自动将数据选通到锁存器中,前提是它足以确保寄存器在时钟的前沿锁存数据。
输入捕获寄存器是在几次读取期间暂停移动数据的非常简单的方法。乍看之下,它似乎是完全安全的。但是,一些分析表明,对于异步输入来说,这是不可靠的。我们正在使用硬件来解决软件问题,因此我们必须意识到物理逻辑设备的局限性。
为了简化操作,让我们放大该输入CaptureRegister并仅检查其其中之一。每个逻辑块都存储在触发器中,这有点逻辑,可能只有三个连接:数据输入,数据输出和时钟。当输入为1时,选通时钟将输出置1。