我目前正在编写一个与时间相关的嵌入式系统应用程序,特别是在ARM Cortex M4上。它有一系列以不同频率发生的任务,因此,我们使用ARM SysTick定时器来跟踪1秒帧内的时间。主忙循环等待直到定时器递增并启动任务,如下所示:
void main(){
while(1){
if(!timerTicked)
continue; //timer hasn't updated since last loop, do nothing.
if(time==0)
toggleStatusLED();
if(time==0 || time==500){
twoHzTask(); //some task that must run twice a second
}
if(time%300==5){ //run at ms=5, 305 and 605
threeHzTask(); //some task that must run three times a second
}
}
}
void SysTick_increment(){ //Interrupt Service Routine for SysTick timer
time=(time+1)%1000;
}
这适用于无法挂起的高优先级任务。此外,只要需要完成高优先级任务就可以使用,但是随着时间的推移,可能会导致系统进入故障安全状态,具体取决于任务。但是,我想添加一个简单的机制来中止非任务关键代码,这需要太长时间,如下所示:
void main(){
while(1){
if(time==500){ //run at ms=500
savingWorld=1;
saveTheWorld(); //super important task
savingWorld=0;
}
else if(time==750){
spinningWheels=1;
spinWheels(); //not very important task. Code for this function is immutable, can not alter it to support task cancellation.
spinningWheels=0;
}
}
}
void SysTick_increment(){ //Interrupt Service Routine for SysTick timer
time=(time+1)%1000;
if(time==900 && spinningWheels){
spinningWheels_cancel(); //we have more important stuff to do
}
}
由于这是一个基本的嵌入式系统,我不想介绍操作系统或多线程机制的复杂性。此外,在调用spinningWheels_cancel()
之后,系统的其余部分应处于稳定状态。也就是说,saveTheWorld()
仍应按预期工作。最简单的方法是什么?
答案 0 :(得分:2)
您已实施了一系列合作任务。此机制的一个关键属性是,多任务仅在所有任务都遵循规则且不会超出其插槽时才有效。保证这种方法的一种方法是将spinningWheels()
这样的长,慢,低优先级任务的实现分解为各自满足时间约束的段。
然后spinningWheels()
将使用私有状态变量来记住它停止的位置并继续旋转一段时间。根据其性质,可能是内循环的调整迭代次数,或者可能意味着采样time
并且当它太大时放弃。
在我构建的嵌入式系统中,我经常最终使用定期调用的有限状态机来实现类似的长时间运行任务。这通常非常适合于通信协议的实现,通信协议通常需要在线路另一端的意外延迟时具有弹性。
仅考虑合作任务,请考虑您提议的函数spinningWheels_cancel()
必须是什么。它是从定时器节拍中断调用的,所以它仅限于已知可重入的函数。由于您没有正式的调度程序和单独的任务,因此实际上并不能杀死"一个任务。因此,它的实现只能设置一个原子标志,该标志由spinningWheels()
中的所有内部循环测试,并用于表示提前退出。
或者,此要求可能是转移到某些RTOS内核的动机。通过抢占式调度,缓慢的任务会超出其时间段,但自然会比更重要的任务更低优先级,并且只要有足够的总周期可用于平均完成其工作,那么溢出可能不那么重要。
野外有不少RTOS内核,重量轻,提供抢占式调度,ARM支持。商业(具有广泛的许可模式)和免费候选人都在那里,但期望选择过程是非平凡的。
答案 1 :(得分:2)
我认为RTOS调度程序会引入简单性而不是引入复杂性(您可以在目标上运行它)。独立的可同时调度的任务可以增强内聚并减少耦合 - 这是任何系统中的理想目标。
然而,另一种架构是改变你所有的任务"从 run-to-task-completion 到 state-machines ,可以重复调用并处理自己的时间并在每次调用中只执行任务的一部分,维护& #34;状态"确定应该做什么,避免忙碌等待"。
如果您运行系统的速度要快得多,而不是调用您的"任务"在所需的时间,您只需在主循环中尽可能快地调用它们,并使它们单独负责其触发时间或状态切换,这将为您提供更大的灵活性并允许您的处理器做一些有用的事情"后台任务"而不只是等待一秒钟的滴答声。这样可以更有效地使用处理器,提供更高的响应速度,并允许工作异步完成任意时间滴答。
例如,根据您的大纲,但为了说明和缺乏信息,可能有点做作:
#include <stdio.h>
#define TICKS_PER_SECOND = 100u ; // for example
volatile uint32_t tick ;
void SysTick_increment()
{
tick++ ;
}
void main()
{
for(;;)
{
LedUpdate() ;
TwoHzTask() ;
ThreeHzTask() ;
SpinningWheels() ;
}
}
void LedUpdate()
{
static uint32_t last_event_timestamp = tick ;
uint32_t now = tick ;
if( now - last_event_timestamp >= TICKS_PER_SECOND )
{
last_event_timestamp = now ;
toggleStatusLED() ;
}
}
void TwoHzTask()
{
static uint32_t last_event_timestamp = tick ;
uint32_t now = tick ;
if( now - last_event_timestamp >= 2 * TICKS_PER_SECOND )
{
last_event_timestamp = now ;
// Do 2Hz task...
}
}
void ThreeHzTask()
{
static uint32_t last_event_timestamp = tick ;
uint32_t now = tick ;
if( now - last_event_timestamp >= 3 * TICKS_PER_SECOND )
{
last_event_timestamp = now ;
// Do 3Hz task...
}
}
void SpinningWheels()
{
static enum
{
IDLE, SPINNING;
} state = IDLE ;
switch( state )
{
case IDLE ;
{
if( startSpinning() )
{
state = SPINNING ;
}
}
break ;
case SPINNING ;
{
bool keep_spinning = doOneSpin() ;
if( !keep_spinning )
{
state = IDLE ;
}
}
break ;
}
}
需要注意的重要一点是,四个任务中没有一个&#34;功能取决于其他功能,以确定它们何时或是否运行,除了表现良好&#34;任务不会阻塞以等待任何事件 - 它只是设置其状态以指示它正在等待该事件,并且切换状态和/或在该事件发生时执行某些动作。 A&#34;任务&#34;不能忙 - 等待或执行任何冗长的处理,不会影响其他任务的实时要求。因此,例如,如果您可能有一个长度或非确定性循环,您可以将其重新计算为#34;迭代状态&#34;,其中每次调用执行一次迭代(如doOneSpin()
上例中的函数。)
请注意,在原始的systick中,表达式time=(time+1)%1000
是一个非常糟糕的主意,应该避免使用。如果强制它包装每1000个滴答,它会使处理时间的回绕成问题。理想情况下它应该包含2的幂,并且由于所有整数类型在任何情况下都具有两个范围的幂,所以您可以像我一样简单地递增它。如果你这样做,now - last_event_timestamp
之类的表达式即使在计数器已经缠绕时也给出了正确的时间差(只要它在那个时间内没有缠绕两次),例如1 - 999就会给出答案 - 998(如果是无符号则为4294966298),而1 - 0xffffffff = 2。
避免在ISR中表达式的另一个原因是%运算符具有隐式除法运算,这种运算相对昂贵并且需要可变数量的时钟周期。我们当然会谈论很少的时间,但是在一些硬实时应用程序中,引起的抖动可能是不可取的。