AUTOSAR OS模块详解(三) Alarm
AUTOSAR OS模块详解(三) Alarm
本文主要介绍AUTOSAR OS的Alarm,并对基于英飞凌Aurix TC3XX系列芯片的Vector Microsar代码和配置进行部分讲解。
文章目录
- AUTOSAR OS模块详解(三) Alarm
- 1 简介
- 2 功能介绍
- 2.1 触发原理
- 2.2 工作类型
- 2.3 Alarm启动方式
- 2.4 Alarm配置
- 2.5 代码分析
- 2.5.1 主要数据结构
- 2.5.2 Alarm初始化阶段
- 2.5.3 Alarm启动阶段
- 2.5.4 Alarm运行阶段
- 3 小结
1 简介
在上一篇Autosar OS模块详解中,我们介绍了OS Counter,其作为OS时间Tick管理的重要元素,为OS提供了重要的时间基准,并驱动OS进行时间调度。
而Counter仅为OS注入时间驱动力,却不负责管理OS中各个任务或者其他定时事件,比如我们要设置某个Task的周期为10ms,或者有一些定时事件,比如100ms之后触发某个event,从概念上我们不能直接让Counter管理这些定时业务,毕竟它的职责仅仅是驱动OS,就像发动机仅仅负责动力,而不负责车辆的档位。
因此我们还需要一个中间层,来管理所有的定时业务。在Autosar OS中,这个元素称为Alarm,它就像车辆的变速器,来管理不同时间频率的定时事件。在Autosar OS中,Counter通过累加来驱动Alarm,Alarm根据自身的设定参数周期地激活Task或者触发事件。
对于不同的周期事件,我们只需要设置多个Alarm,每个Alarm驱动所关联的业务,这样的方式实现了代码的抽象和业务分层,具备良好的架构体系。
2 功能介绍
2.1 触发原理
在Autosar OS中,Alarm顾名思义,就是闹钟,用于实现定时业务。在前面的Counter模块中我们已经介绍了,Counter在每个Tick会累加所有关联的Alarm,然后判断Alarm的状态,如果Alarm到期,则会执行相应的Job,也就是下文会提到的工作类型。
2.2 工作类型
在Autosar OS中,Alarm有4种工作类型,用户可配置不同的类型,并关联不同的Task、Event等。
Os_AlarmActionSetEvent
该工作类型用来设置某个Task中的一个Event,关于Event我们后面会介绍到,它是扩展任务中的关键元素,Event触发后该任务进入就绪状态,并且不同的Event关联不同的Runable。
Os_AlarmActionActivateTask
该工作类型用于激活某个Task,用于基础任务。基础任务是一种不带Event的普通任务,该任务每次运行都需要被激活,运行完成之后会自动Terminate,也就是挂起。
Os_AlarmActionIncrementCounter
前文提到过,并非每个Counter都需要关联到独立的硬件,也可以使用软件Counter。使用软件Counter的时候就是通过其他硬件Counter中的Alarm进行累加,使软件Couter得以产生连续的Tick。
Os_AlarmActionCallback
该工作类型为调用相应的CallBack函数,需要注意的是,这种工作类型只能在SC1下使用。该工作类型使用不多,一般情况下很少会将业务逻辑的执行与OS内部Counter、Alarm等元素进行直接关联,而是通过Task或者Event激活等进行业务逻辑的设置。
2.3 Alarm启动方式
Alarm的启动有两种方式,一种是绝对启动(SetAbsAlarm),另一种是相对启动(SetRelAlarm)。
绝对启动
如果以Counter的值作为一个时间轴的话,绝对启动就是Alarm的触发在Counter的初始时刻,所有通过绝对启动的Alarm的启动时刻相等。在Vecotr Os中,所有的Alarm启动之后下一次Counter就会立即执行一次,因此所有的Alarm会在第一个Tick统一执行。
当存在1ms、5ms、10ms三个Alarm时,如果使用绝对启动,其在Counter Tick的时间轴上的触发关系如下图所示。可以看到当Alarm的最小公倍数Tick数出现时,所有的Alarm都会同时触发,此时系统的瞬态负载较高。
相对启动
相对启动在设置Alarm启动时,可以添加一个Offset,这个Offset的分辨率是Os的Tick。我们可以通过设置Alarm的Offset将Alarm的触发错开,相应的Task的执行也会因此错开。
同样是1ms、5ms、10ms三个Alarm,我们将10ms的Alarm添加一个2ms的Offset,将5ms的Alarm添加一个1ms的Offset,则其在Counter Tick时间轴上的触发如下图所示,我们可以看到,此时最多两两之间发生碰撞,系统的瞬态峰值负载得到了降低。
另外值得一提的是,如果Os Tick的分辨率是0.5ms,则可将这三个Alarm通过设置1.5ms、2.5ms的Offset的方式全部错开。但是分辨率增加也意味着Os运行开销的增大,需要根据项目实际情况进行斟酌。
2.4 Alarm配置
其实在Vector工具中,Alarm是不需要配置的,工具会根据Task内部配置的Rbl的周期,自适应生成对应的Alarm,相对启动的Offset也是通过配置Task中Rbl的Offset来实现的。这里我们以一个Bsw任务中的10ms Alarm为示例,介绍Alarm各配置的含义。
OsAlarmCounterRef
如上一章所述,Counter是Alarm的驱动源,因此每个Alarm都需要关联一个Counter,一般情况下是Alarm所在核内的Counter。
OsAlarmAction
如前所述,Alarm具有四种Action,一般最常使用的是OsAlarmSetEvent和OsAlarmActivateTask。这里由于Bsw Task分配了多个周期的Rbl,属性为Extended Task,因此需要使用SetEvent的形式进行调度。
OsAlarmTask
每个Alarm的动作都需要对应一个Task。
OsAlarmCallbackName
SC1类型的Os中可以使用Os_AlarmActionCallback类型,因此如果是该类型的Alarm这里可以配置Callback函数。
OsAlarmCounter
当Alarm的工作类型为Os_AlarmActionIncrementCounter时,这里就需要配置一个Counter,以实现软件Counter的激活。
OsAlarmEvent
当Alarm的工作类型为Os_AlarmActionSetEvent时,这里配置对应的需要设置的Event即可。
OsAlarmAlarmTime
Alarm的Offset,但是实际上Offset是根据对应Task中分配的Rbl的Offset来计算的,因此这里不用配置。
OsAlarmAutostartType
启动类型,绝对启动还是相对启动,这个一般由工具计算也不需要配置。
OsAlarmCycleTime
Alarm的周期,如果是一次性的Alarm,这里配置为0,但是这项配置其实也是工具自动计算的,不用配置。
OsAlarmAppModeRef
Os的启动是有启动模式的,这里可以配置为:在哪种启动模式下,该Alarm自动启动,即初始化时启动。这项配置一般不用配置,Rte启动时会统一启动所有Alarm。
OsAlarmAccessingApplication
Alarm的访问权限配置,一般情况下Alarm的激活都是由Os来执行,权限也是由Os自动分配,少数比较特别的Alarm用户或者运行时的RTE会操作,因此Os无法识别所有访问源,因此这里有时候需要手动配置权限。如果执行Os服务时报权限访问的错误,一般是这种权限配置的地方有缺失。
前文提到Alarm可以配置Offset以错开激活时机,在Vector工具中,这是通过Task中实现的。如上图中将所有5ms的Rbl都配置了1ms的Offset,则该5ms的Alarm就会由工具自动计算出Offset。
2.5 代码分析
此处以Rte_Al_TE2_Default_BSW_Async_Task_Core0_1_5ms为例,分析Vector Os中关于Alarm的代码执行逻辑。
2.5.1 主要数据结构
Alarm中的主要数据结构为Os_AlarmSetEventConfigType、Os_AlarmActivateTaskConfigType、Os_AlarmIncrementCounterConfigType和Os_AlarmCallbackConfigType,分别对应Alarm的四种工作类型,此处我们的Alarm工作类型为设置Event,因此我们重点介绍Os_AlarmSetEventConfigType类型。
在Os_Alarm_Lcfg.c文件中我们可以找到配置生成的每个Alarm的数据类型:
/*! Alarm configuration data: Rte_Al_TE2_Default_BSW_Async_Task_Core0_1_5ms */
CONST(Os_AlarmSetEventConfigType, OS_CONST) OsCfg_Alarm_Rte_Al_TE2_Default_BSW_Async_Task_Core0_1_5ms =
{
/* .Alarm = */
{
/* .Job = */
{
/* .Dyn = */ OS_ALARM_CASTDYN_ALARM_2_JOB(OsCfg_Alarm_Rte_Al_TE2_Default_BSW_Async_Task_Core0_1_5ms_Dyn),
/* .Counter = */ OS_COUNTER_CASTCONFIG_TIMERPFRT_2_COUNTER(OsCfg_Counter_SystemTimer),
/* .Callback = */ Os_AlarmActionSetEvent
},
/* .Autostart = */
{
/* .AlarmTime = */ 0uL, /* 0.0 sec */
/* .Cycle = */ 0uL, /* 0.0 sec */
/* .ApplicationModes = */ OS_APPMODE_NONE,
/* .AlarmMode = */ OS_ALARMMODE_ABSOLUTE
},
/* .AccessingApplications = */ (OS_APPID2MASK(OsApplication_NonTrusted_Core0) | OS_APPID2MASK(OsApplication_NonTrusted_Core1) | OS_APPID2MASK(OsApplication_NonTrusted_Core2) | OS_APPID2MASK(OsApplication_NonTrusted_Core3) | OS_APPID2MASK(SystemApplication_OsCore0) | OS_APPID2MASK(SystemApplication_OsCore1) | OS_APPID2MASK(SystemApplication_OsCore2) | OS_APPID2MASK(SystemApplication_OsCore3)), /* PRQA S 0410 */ /* MD_MSR_Dir1.1 */
/* .OwnerApplication = */ &OsCfg_App_OsApplication_NonTrusted_Core0
},
/* .Task = */ &OsCfg_Task_Default_BSW_Async_Task_Core0,
/* .Mask = */ Rte_Ev_Cyclic2_Default_BSW_Async_Task_Core0_1_5ms
};
我们可以看到,配置生成的数据为OsCfg_Alarm_前缀加上Alarm配置名。 这里我们可以看到所有的配置值都在这个结构体里,我们重点关注其中的.Alarm.job.Dyn,这个指针指向的结构体中有一个ExpirationTimestamp成员,是Alarm下一个需要被激活的Counter Tick值,也就是“闹钟”的设定值。
2.5.2 Alarm初始化阶段
关于Os的初始化,在本系列Counter章节中已经介绍过,这里就不赘述了,与Counter相同,Alarm的初始化也是在Os_AppInit中进行的,其调用栈如下图所示。
在初始化阶段,仅仅是将Alarm的状态切换为OS_ALARMSTATE_CANCELED,并没有其他操作。
2.5.3 Alarm启动阶段
Alarm的启动是在EcuM的StartupTwo阶段完成的,关于EcuM的启动时序,这里不做过多介绍,后续聊到EcuM模块时再做解读。
StartupTwo运行在Default_Init_Task中,这是一个系统自带的初始化Task,优先级较高。在StartupTwo阶段,有两个地方会启动Alarm,SchM_Init和Rte_Start。SchM_Init中主要初始化EcuM等Bsw模块相关联的Alarm,而Rte_Start则主要初始化用户定义的相关的Rbl的Alarm。其时序图如图所示。
我们从中可以看到,Os_AlarmSetRelAlarm是最终实施的主要服务函数,我们从该函数解读SetRelAlarm的实现,这里忽略掉其中的错误检查。
OS_FUNC_ATTRIBUTE_DEFINITION(OS_LOCAL_INLINE Os_StatusType, OS_CODE, OS_ALWAYS_INLINE, Os_AlarmSetRelAla
(
P2CONST(Os_AlarmConfigType, AUTOMATIC, OS_CONST) Alarm,
TickType Increment,
TickType Cycle
))
{
Os_StatusType status;
if(ErrorCheck...){}
else
{
P2VAR(Os_AlarmType, AUTOMATIC, OS_VAR_NOINIT) alarmDyn;
alarmDyn = Os_AlarmGetDyn(Alarm);
/* #20 Set alarm's state to SET. */
alarmDyn->State = OS_ALARMSTATE_SET;
/* #30 Set alarm's cycle time to Cycle. */
alarmDyn->Cycle = Cycle;
/* #40 Tell counter to execute alarm's job in Increment ticks. */
Os_JobAddRel(&Alarm->Job, Increment);
status = OS_STATUS_OK;
}
return status;
}
这里alarmDyn的数据指向的是OsCfg_Alarm_Rte_Al_TE2_Default_BSW_Async_Task_Core0_1_5ms_Dyn变量,大家能够很容易注意到其命名是有规律的。
首先,该函数将Alarm的状态切换到OS_ALARMSTATE_SET,表示该Alarm已经启动。
然后设置该Alarm的周期为Cycle,这里我们是5ms的Alarm,周期为5。
然后通过Os_JobAddRel将Alarm添加到其关联的Counter中,这里的Increment就是Offset,如果没有偏移的话Increment为1,表示第一次Counter Tick就会激活该Alarm,这里我们设置了1ms的Offset,因此Increment=2,表示第二次Counter Tick才会激活该Alarm。(Counter的逻辑是先累加再判断,因此第一次工作时Counter Tick值为1)
这里的Os_JobAddRel,也就是Os_CounterAddRelJob,是Counter内部的实现函数,我们观察其代码。
FUNC(void, OS_CODE) Os_CounterAddRelJob
(
P2CONST(Os_CounterConfigType, AUTOMATIC, OS_CONST) Counter,
P2CONST(Os_JobConfigType, AUTOMATIC, OS_CONST) Job,
Os_TickType Offset
)
{
Os_TickType now;
Os_TickType newExpTime;
P2CONST(Os_PriorityQueueConfigType, AUTOMATIC, OS_CONST) jobQueue;
jobQueue = &Counter->JobQueue;
/* #10 Get the current counter value (now). */
now = Os_CounterGetPhysicalValue(Counter);
/* #20 Set job's expiration time to mod(now + Offset). */
newExpTime = Os_TimerAdd(
Counter->Characteristics.MaxAllowedValue,
Counter->Characteristics.MaxCountingValue,
now,
Offset);
Os_JobSetExpirationTimestamp(Job, newExpTime);
/* #30 Enqueue the given job in counter's job queue. */
Os_PriorityQueueInsert(jobQueue, Job);
/* #40 If the top element of the queue has changed: */
if(Job == Os_PriorityQueueTopGet(jobQueue))
{
/* #50 Update timer's compare value. */
Os_CounterSetCompareValue(Counter, newExpTime);
}
}
首先通过Os_CounterGetPhysicalValue计算出Counter的当前值,一般启动时该值都是0。然后通过Os_TimerAdd结合当前值和Offset,计算“闹钟”的定时时刻ExpirationTimestamp,也就是时间轴上的下一个触发点。这里需要最大值来判断溢出。这里now=0,Offset=2,计算出结果为ExpirationTimestamp=2。
然后通过Os_JobSetExpirationTimestamp函数,将计算出的ExpirationTimestamp设置为超时值,在Counter的中断中,就是通过比较Counter的Tick值和ExpirationTimestamp,来判断该Alarm是否需要激活。
然后通过Os_PriorityQueueInsert来将Alarm插入到Counter的Job队列中,这里使用了完全二叉树进行优先级排序,时间轴上未来最近的一个Alarm具有最高优先级,感兴趣的读者可以自行研究,这里不进行算法层面的探讨。
最后一步是判断是否更新了待激活的Alarm,比如当前Counter待激活的时刻为Tick=5,现在插入的Alarm需要在Tick=2时激活,那么就需要更新Counter的Compare值,因为Counter并不是每一次Timer中断都会工作,而是按需执行。
到这里就完成了Alarm的启动,只需要等待Counter中断去执行了。
2.5.4 Alarm运行阶段
前篇我们提到,在Counter中的Os_CounterWorkJobs函数,会轮询任务队列中的所有Alarm,如果该Alarm的ExpirationTimestamp与Counter Tick的值吻合,则执行该Alarm的Callback,同时将该Alarm从Job队列中取出。这部分逻辑不再赘述,读者如果有疑问可以阅读前篇Counter详解。
这里我们在运行阶段重点介绍Alarm在的Callback中的动作,这是Counter激活Alarm之后由Alarm实施的主要业务。我们的Alarm选取的是5ms的SetEvent,因此其Callback是Os_AlarmActionSetEvent。
如果是周期Alarm,在Os_CounterReloadJob中,该Alarm会被重载,会根据Counter的当前Tick值加上周期,算出最新的ExpirationTimestamp,以设置下一个周期的触发;如果是一次性Alarm,则在Os_AlarmCancelOrReload中Alarm的状态会被设置为OS_ALARMSTATE_CANCELED,以表示关闭该Alarm。
然后执行Os_EventSetInternal以进行Event的设置,来触发对应Task中的Rbl执行。该部分我们在后面的Event文章内容中进行详细介绍,这里就一概而过。
3 小结
本文承接上篇Counter,介绍了Autosar Os中的Alarm机制,解释了Vector工具中的Alarm配置项,并根据Vector代码详细解读了其初始化、启动、运行等阶段,结合详细的时序图,介绍了其由Counter激活后的执行逻辑。