我在初学uC/OS-III的时候,时基产生后到底是如何去驱动操作系统运转的,对于这个问题一直有很多疑问,最后读了手册并且仔细推敲源码后终于理解,这里记录下来。
关于时钟节拍的意义,这里就不再赘述。我们只要知道操作系统依赖于时钟节拍推动 CPU 去执行指令就行了。下文的概述主要是针对STM32系列单片机。
如何产生时基信号
时钟节拍需要依赖于硬件定时器,STM32 通常使用内核定时器 SystemTick时钟作为时钟节拍的产生。因此我们在初始化代码的时候就需要配置SystemTick定时器。贴出部分代码如下:
CPU_INT32U cpu_clk_freq;
CPU_INT32U cnts;
cpu_clk_freq = BSP_CPU_ClkFreq(); //获取 CPU 内核时钟频率(SysTick 工作时钟)
cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz; //根据用户设定的时钟节拍频率计算 SysTick 定时器的计数值
OS_CPU_SysTickInit(cnts); //调用 SysTick 初始化函数,设置定时器计数值和启动定时器
其中OSCfg_TickRate_Hz的值决定了UCOS-III的时钟节拍频率,在文件os_cfg_app.c中被如下定义:
OS_RATE_HZ const OSCfg_TickRate_Hz = (OS_RATE_HZ )OS_CFG_TICK_RATE_HZ;
然后我们再看OS_CFG_TICK_RATE_HZ的值,最后定义到文件os_cfg_app.h中
#define OS_CFG_TICK_RATE_HZ 1000u /* Tick rate in Hertz (10 to 1000 Hz) */
也就是说,uCOS-III默认时钟节拍为1000Hz。这样就完成了对SystemTick的初始化,初始化后我们关心的就是SystemTick的中断服务函数了。
系统时钟中断管理
在uCOS-III中,SystemTick的中断服务函数由SysTick_Handler改为了OS_CPU_SysTickHandler,然后我们找到OS_CPU_SysTickHandler函数后可以看到如下代码:
/*$PAGE*/
/*
*********************************************************************************************************
* SYS TICK HANDLER
*
* Description: Handle the system tick (SysTick) interrupt, which is used to generate the uC/OS-II tick
* interrupt.
*
* Arguments : None.
*
* Note(s) : 1) This function MUST be placed on entry 15 of the Cortex-M3 vector table.
*********************************************************************************************************
*/
void OS_CPU_SysTickHandler (void)
{
CPU_SR_ALLOC();
CPU_CRITICAL_ENTER();
OSIntNestingCtr++; /* Tell uC/OS-III that we are starting an ISR */
CPU_CRITICAL_EXIT();
OSTimeTick(); /* Call uC/OS-III's OSTimeTick() */
OSIntExit(); /* Tell uC/OS-III that we are leaving the ISR */
}
这里我们最关心的也就是OSTimeTick函数了,我们转到定义查看一下代码,如下:
/*$PAGE*/
/*
************************************************************************************************************************
* PROCESS SYSTEM TICK
*
* Description: This function is used to signal to uC/OS-III the occurrence of a 'system tick' (also known as a
* 'clock tick'). This function should be called by the tick ISR.
*
* Arguments : none
*
* Returns : none
************************************************************************************************************************
*/
void OSTimeTick (void)
{
OS_ERR err;
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u
CPU_TS ts;
#endif
OSTimeTickHook(); /* Call user definable hook */
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u
ts = OS_TS_GET(); /* Get timestamp */
OS_IntQPost((OS_OBJ_TYPE) OS_OBJ_TYPE_TICK, /* Post to ISR queue */
(void *)&OSRdyList[OSPrioCur],
(void *) 0,
(OS_MSG_SIZE) 0u,
(OS_FLAGS ) 0u,
(OS_OPT ) 0u,
(CPU_TS ) ts,
(OS_ERR *)&err);
#else
(void)OSTaskSemPost((OS_TCB *)&OSTickTaskTCB, /* Signal tick task */
(OS_OPT ) OS_OPT_POST_NONE,
(OS_ERR *)&err);
#if OS_CFG_SCHED_ROUND_ROBIN_EN > 0u
OS_SchedRoundRobin(&OSRdyList[OSPrioCur]);
#endif
#if OS_CFG_TMR_EN > 0u
OSTmrUpdateCtr--;
if (OSTmrUpdateCtr == (OS_CTR)0u) {
OSTmrUpdateCtr = OSTmrUpdateCnt;
OSTaskSemPost((OS_TCB *)&OSTmrTaskTCB, /* Signal timer task */
(OS_OPT ) OS_OPT_POST_NONE,
(OS_ERR *)&err);
}
#endif
#endif
}
此函数看起来很复杂,但是仔细分析后会发现,OS_CFG_ISR_POST_DEFERRED_EN这个宏是被默认设置为1的,所以整个代码条件编译完后关注的函数只有两个,第一个是OSTimeTickHook(); ,这是时基任务的钩子函数,里面打开后可以看见没有操作,第二个就只剩下这个函数了:
OS_IntQPost((OS_OBJ_TYPE) OS_OBJ_TYPE_TICK, /* Post to ISR queue */
(void *)&OSRdyList[OSPrioCur],
(void *) 0,
(OS_MSG_SIZE) 0u,
(OS_FLAGS ) 0u,
(OS_OPT ) 0u,
(CPU_TS ) ts,
(OS_ERR *)&err);
这个,然后我们会奇怪,这里面并没有进行任务调度的代码啊,这里也是初学者最容易发生疑惑的地方,就是并没有出现关于时基更新的函数啊,然后中断就结束了。
时基任务
其实这里用到了UCOS-III中消息传递方面的知识。我们知道,上面的函数都是在OS_CPU_SysTickHandler中执行了,而这又是一个中断服务函数,理论上中断服务函数要快进快出,于是UCOS-III为此,使用了一种叫做中断延迟发布的方法,让中断中需要发送的信息先保存在一个特殊的队列中,然后等到中断服务函数执行完后再去发布这个信号或者消息。
我们先转到下面这段程序,来看一下:
/*$PAGE*/
/*
************************************************************************************************************************
* INITIALIZE TICK TASK
*
* Description: This function is called by OSInit() to create the tick task.
*
* Arguments : p_err is a pointer to a variable that will hold the value of an error code:
*
* OS_ERR_TICK_STK_INVALID if the pointer to the tick task stack is a NULL pointer
* OS_ERR_TICK_STK_SIZE indicates that the specified stack size
* OS_ERR_PRIO_INVALID if the priority you specified in the configuration is invalid
* (There could be only one task at the Idle Task priority)
* (Maybe the priority you specified is higher than OS_CFG_PRIO_MAX-1
* OS_ERR_?? other error code returned by OSTaskCreate()
*
* Returns : none
*
* Note(s) : This function is INTERNAL to uC/OS-III and your application should not call it.
************************************************************************************************************************
*/
void OS_TickTaskInit (OS_ERR *p_err)
{
#ifdef OS_SAFETY_CRITICAL
if (p_err == (OS_ERR *)0) {
OS_SAFETY_CRITICAL_EXCEPTION();
return;
}
#endif
OSTickCtr = (OS_TICK)0u; /* Clear the tick counter */
OSTickTaskTimeMax = (CPU_TS)0u;
OS_TickListInit(); /* Initialize the tick list data structures */
/* ---------------- CREATE THE TICK TASK ---------------- */
if (OSCfg_TickTaskStkBasePtr == (CPU_STK *)0) {
*p_err = OS_ERR_TICK_STK_INVALID;
return;
}
if (OSCfg_TickTaskStkSize < OSCfg_StkSizeMin) {
*p_err = OS_ERR_TICK_STK_SIZE_INVALID;
return;
}
if (OSCfg_TickTaskPrio >= (OS_CFG_PRIO_MAX - 1u)) { /* Only one task at the 'Idle Task' priority */
*p_err = OS_ERR_TICK_PRIO_INVALID;
return;
}
OSTaskCreate((OS_TCB *)&OSTickTaskTCB,
(CPU_CHAR *)((void *)"uC/OS-III Tick Task"),
(OS_TASK_PTR )OS_TickTask,
(void *)0,
(OS_PRIO )OSCfg_TickTaskPrio,
(CPU_STK *)OSCfg_TickTaskStkBasePtr,
(CPU_STK_SIZE)OSCfg_TickTaskStkLimit,
(CPU_STK_SIZE)OSCfg_TickTaskStkSize,
(OS_MSG_QTY )0u,
(OS_TICK )0u,
(void *)0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR | OS_OPT_TASK_NO_TLS),
(OS_ERR *)p_err);
}
这就是UCOS-III的时基任务的初始化,在我们对UCOS-III初始化的时候就已经被调用了,属于系统自己创建的任务,然后我们转到OS_TickTask的定义看一下:
/*
************************************************************************************************************************
* TICK TASK
*
* Description: This task is internal to uC/OS-III and is triggered by the tick interrupt.
*
* Arguments : p_arg is an argument passed to the task when the task is created (unused).
*
* Returns : none
*
* Note(s) : This function is INTERNAL to uC/OS-III and your application should not call it.
************************************************************************************************************************
*/
void OS_TickTask (void *p_arg)
{
OS_ERR err;
CPU_TS ts;
p_arg = p_arg; /* Prevent compiler warning */
while (DEF_ON) {
(void)OSTaskSemPend((OS_TICK )0,
(OS_OPT )OS_OPT_PEND_BLOCKING,
(CPU_TS *)&ts,
(OS_ERR *)&err); /* Wait for signal from tick interrupt */
if (err == OS_ERR_NONE) {
if (OSRunning == OS_STATE_OS_RUNNING) {
OS_TickListUpdate(); /* Update all tasks waiting for time */
}
}
}
}
这里其实我们就找到了真正完成UCOS-III系统时基更新的函数OS_TickListUpdate(),在查看这个函数之前,我们可以看到在这个while死循环中,一直在调用一个函数OSTaskSemPend,并且当返回的err是无错误,并且OS已经开始跑了,才会执行这个OS_TickListUpdate(),这里刚好就和上文一直留下的一个伏笔相呼应,它就是OS_IntQPost。
OSTaskSemPend会将OS_TickTask 挂起等待一个信号的到来才能继续执行,而OS_IntQPost就是提供这个信号的函数,这两者配合后,就完成了OS_CPU_SysTickHandler的任务,来引起时基列表的更新,也就是OS_TickListUpdate()。
时基列表更新
先列出代码:
/*$PAGE*/
/*
************************************************************************************************************************
* UPDATE THE TICK LIST
*
* Description: This function is called when a tick occurs and determines if the timeout waiting for a kernel object has
* expired or a delay has expired.
*
* Arguments : non
*
* Returns : none
*
* Note(s) : 1) This function is INTERNAL to uC/OS-III and your application MUST NOT call it.
************************************************************************************************************************
*/
void OS_TickListUpdate (void)
{
CPU_BOOLEAN done;
OS_TICK_SPOKE *p_spoke;
OS_TCB *p_tcb;
OS_TCB *p_tcb_next;
OS_TICK_SPOKE_IX spoke;
CPU_TS ts_start;
CPU_TS ts_end;
CPU_SR_ALLOC();
OS_CRITICAL_ENTER();
ts_start = OS_TS_GET();
OSTickCtr++; /* Keep track of the number of ticks */
spoke = (OS_TICK_SPOKE_IX)(OSTickCtr % OSCfg_TickWheelSize);
p_spoke = &OSCfg_TickWheel[spoke];
p_tcb = p_spoke->FirstPtr;
done = DEF_FALSE;
while (done == DEF_FALSE) {
if (p_tcb != (OS_TCB *)0) {
p_tcb_next = p_tcb->TickNextPtr; /* Point to next TCB to update */
switch (p_tcb->TaskState) {
case OS_TASK_STATE_RDY:
case OS_TASK_STATE_PEND:
case OS_TASK_STATE_SUSPENDED:
case OS_TASK_STATE_PEND_SUSPENDED:
break;
case OS_TASK_STATE_DLY:
p_tcb->TickRemain = p_tcb->TickCtrMatch /* Compute time remaining of current TCB */
- OSTickCtr;
if (OSTickCtr == p_tcb->TickCtrMatch) { /* Process each TCB that expires */
p_tcb->TaskState = OS_TASK_STATE_RDY;
OS_TaskRdy(p_tcb); /* Make task ready to run */
} else {
done = DEF_TRUE; /* Don't find a match, we're done! */
}
break;
case OS_TASK_STATE_PEND_TIMEOUT:
p_tcb->TickRemain = p_tcb->TickCtrMatch /* Compute time remaining of current TCB */
- OSTickCtr;
if (OSTickCtr == p_tcb->TickCtrMatch) { /* Process each TCB that expires */
#if (OS_MSG_EN > 0u)
p_tcb->MsgPtr = (void *)0;
p_tcb->MsgSize = (OS_MSG_SIZE)0u;
#endif
p_tcb->TS = OS_TS_GET();
OS_PendListRemove(p_tcb); /* Remove from wait list */
OS_TaskRdy(p_tcb);
p_tcb->TaskState = OS_TASK_STATE_RDY;
p_tcb->PendStatus = OS_STATUS_PEND_TIMEOUT; /* Indicate pend timed out */
p_tcb->PendOn = OS_TASK_PEND_ON_NOTHING; /* Indicate no longer pending */
} else {
done = DEF_TRUE; /* Don't find a match, we're done! */
}
break;
case OS_TASK_STATE_DLY_SUSPENDED:
p_tcb->TickRemain = p_tcb->TickCtrMatch /* Compute time remaining of current TCB */
- OSTickCtr;
if (OSTickCtr == p_tcb->TickCtrMatch) { /* Process each TCB that expires */
p_tcb->TaskState = OS_TASK_STATE_SUSPENDED;
OS_TickListRemove(p_tcb); /* Remove from current wheel spoke */
} else {
done = DEF_TRUE; /* Don't find a match, we're done! */
}
break;
case OS_TASK_STATE_PEND_TIMEOUT_SUSPENDED:
p_tcb->TickRemain = p_tcb->TickCtrMatch /* Compute time remaining of current TCB */
- OSTickCtr;
if (OSTickCtr == p_tcb->TickCtrMatch) { /* Process each TCB that expires */
#if (OS_MSG_EN > 0u)
p_tcb->MsgPtr = (void *)0;
p_tcb->MsgSize = (OS_MSG_SIZE)0u;
#endif
p_tcb->TS = OS_TS_GET();
OS_PendListRemove(p_tcb); /* Remove from wait list */
OS_TickListRemove(p_tcb); /* Remove from current wheel spoke */
p_tcb->TaskState = OS_TASK_STATE_SUSPENDED;
p_tcb->PendStatus = OS_STATUS_PEND_TIMEOUT; /* Indicate pend timed out */
p_tcb->PendOn = OS_TASK_PEND_ON_NOTHING; /* Indicate no longer pending */
} else {
done = DEF_TRUE; /* Don't find a match, we're done! */
}
break;
default:
break;
}
p_tcb = p_tcb_next;
} else {
done = DEF_TRUE;
}
}
ts_end = OS_TS_GET() - ts_start; /* Measure execution time of tick task */
if (OSTickTaskTimeMax < ts_end) {
OSTickTaskTimeMax = ts_end;
}
OS_CRITICAL_EXIT();
}
这里代码很长,可以 下去后慢慢分析,主要是关于时钟节拍列表的,这里不赘述只提一点,就是第31行的OSTickCtr++;,如下:
OSTickCtr++; /* Keep track of the number of ticks */
OSTickCtr变量是由系统创建的一个32bit无符号整型的全局变量,用来记录时钟节拍数,简单点理解就是,每当 OS_CPU_SysTickHandler 发生中断后,会给OS_TickTask 函数发送一个信号,收到信号后会调用 OS_TickListUpdate 来更新时钟节拍列表,并且使OSTickCtr加一,来驱动任务调度。
写在最后
uCOS-III作为一个能够上火星的操作系统,一定程度上反映了它的优越性,但是越是成熟的操作系统,就越会有很多难以理解或者不容易理解的地方,我作为一个初学者,会时常将遇到的一些问题或者难以理解的东西,这里解决后记录下来,希望可以给其他也遇到这个问题的同学提供一点帮助。