时钟节拍可谓是 uC/OS 操作系统的心脏,它若不跳动,整个系统都将会瘫痪。时钟节拍就是操作系统的时基,操作系统要实现时间上的管理,必须依赖于时基。
时钟节拍就是系统以固定的频率产生中断(时基中断),并在中断中处理与时间相关的事件,推动所有任务向前运行。时钟节拍需要依赖于硬件定时器,在 STM32 裸机程序中经常使用的 SysTick 时钟是 MCU的内核定时器,通常都使用该定时器产生操作系统的时钟节拍。
用户需要先在“os_cfg_app.h”中设定时钟节拍的频率,该频率越高,操作系统检测事件就越频繁,可以增强任务的实时性,但太频繁也会增加操作系统内核的负担加重,所以用户需要权衡该频率的设置。秉火在这里采用默认的 1000 Hz(本书之后若无特别声明,均采用 1000 Hz),也就是时钟节拍的周期为 1 ms。
设置时钟节拍的频率 :
/* ------------------------ TICKS ----------------------- */ #define OS_CFG_TICK_RATE_HZ 1000u // 时钟节拍频率 (10 to 1000 Hz) #define OS_CFG_TICK_TASK_PRIO 10u // 时钟节拍任务 OS_TickTask() 的优先级 #define OS_CFG_TICK_TASK_STK_SIZE 128u // 时钟节拍任务 OS_TickTask() 的栈空间大小 #define OS_CFG_TICK_WHEEL_SIZE 17u // OSCfg_TickWheel 数组的大小,推荐使用任务总数/4,且为质数
在app.c中的起始任务 AppTaskStart() 中初始化时钟节拍定时器,其实就是初始化 STM32 内核的 SysTick 时钟。
初始化 SysTick 时钟 :
cpu_clk_freq = BSP_CPU_ClkFreq(); //获取 CPU 内核时钟频率(SysTick 工作时钟) cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz; //根据用户设定的时钟节拍频率计算 SysTick 定时器的计数值 OS_CPU_SysTickInit(cnts); //调用 SysTick 初始化函数,设置定时器计数值和启动定时器
OS_CPU_SysTickInit() 函数的定义位于“os_cpu_c.c” :
void OS_CPU_SysTickInit (CPU_INT32U cnts) { CPU_INT32U prio; /* 填写 SysTick 的重载计数值 */ CPU_REG_NVIC_ST_RELOAD = cnts - 1u; // SysTick 以该计数值为周期循环计数定时 /* 设置 SysTick 中断优先级 */ prio = CPU_REG_NVIC_SHPRI3; prio &= DEF_BIT_FIELD(24, 0); prio |= DEF_BIT_MASK(OS_CPU_CFG_SYSTICK_PRIO, 24); //设置为默认的最高优先级0,在裸机例程中该优先级默认为最低 CPU_REG_NVIC_SHPRI3 = prio; /* 使能 SysTick 的时钟源和启动计数器 */ CPU_REG_NVIC_ST_CTRL |= CPU_REG_NVIC_ST_CTRL_CLKSOURCE | CPU_REG_NVIC_ST_CTRL_ENABLE; /* 使能 SysTick 的定时中断 */ CPU_REG_NVIC_ST_CTRL |= CPU_REG_NVIC_ST_CTRL_TICKINT; }
SysTick 定时中断函数 OS_CPU_SysTickHandler() 的定义也位于“os_cpu_c.c”,就毗邻 OS_CPU_SysTickInit() 函数定义体的上方。
void OS_CPU_SysTickHandler (void) { CPU_SR_ALLOC(); //分配保存中断状态的局部变量,后面关中断的时候可以保存中断状态 CPU_CRITICAL_ENTER(); // CPU_CRITICAL_ENTER() 和 CPU_CRITICAL_EXIT() 之间形成临界段,避免期间程序运行时受到干扰 OSIntNestingCtr++; //进入中断时中断嵌套数要加1 CPU_CRITICAL_EXIT(); OSTimeTick(); //调用 OSTimeTick() 函数 OSIntExit(); //退出中断,里面回家中断嵌套数减1 }
OSTimeTick ()的定义位于“os_time.c”。
void OSTimeTick (void) { OS_ERR err; #if OS_CFG_ISR_POST_DEFERRED_EN > 0u CPU_TS ts; #endif OSTimeTickHook(); //调用用户可自定义的钩子函数,可在此函数中定义在时钟节拍到来时的事件 #if OS_CFG_ISR_POST_DEFERRED_EN > 0u //如果使能(默认使能)了中断发送延迟 ts = OS_TS_GET(); //获取时间戳 OS_IntQPost((OS_OBJ_TYPE) OS_OBJ_TYPE_TICK, //任务信号量暂时发送到中断队列,退出中断后由优先级最高的延迟发布任务 (void *)&OSRdyList[OSPrioCur], //就绪发送给时钟节拍任务 OS_TickTask(),OS_TickTask() 接收到该信号量 (void *) 0, //就会继续执行。中断发送延迟可以减少中断时间,将中断级事件转为任务级 (OS_MSG_SIZE) 0u, //,提高了操作系统的实时性。 (OS_FLAGS ) 0u, (OS_OPT ) 0u, (CPU_TS ) ts, (OS_ERR *)&err); #else //如果禁用(默认使能)了中断发送延迟 (void)OSTaskSemPost((OS_TCB *)&OSTickTaskTCB, //直接发送信号量给时钟节拍任务 OS_TickTask() (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) { //如果软件定时器计数器减至0 OSTmrUpdateCtr = OSTmrUpdateCnt; //重载软件定时器计数器 OSTaskSemPost((OS_TCB *)&OSTmrTaskTCB, //发送信号量给软件定时器任务 OS_TmrTask() (OS_OPT ) OS_OPT_POST_NONE, (OS_ERR *)&err); } #endif #endif }
在函数 OSTimeTick () 会发送信号量给时基任务 OS_TickTask() ,任务 OS_TickTask() 接收到信号量后就会进入就绪状态,准备运行。
void OS_TickTask (void *p_arg) { OS_ERR err; CPU_TS ts; p_arg = p_arg; //预防编译警告,没有实际意义 while (DEF_ON) { //循环运行 (void)OSTaskSemPend((OS_TICK )0, //等待来自时基中断的信号量,接收到信号量后继续运行 (OS_OPT )OS_OPT_PEND_BLOCKING, (CPU_TS *)&ts, (OS_ERR *)&err); if (err == OS_ERR_NONE) { //如果上面接受的信号量没有错误 if (OSRunning == OS_STATE_OS_RUNNING) { //如果操作系统正在运行 OS_TickListUpdate(); //更新所有任务的时间等待时间(如延时、超时等) } } } }
OS_TickListUpdate() 函数的定义位于“os_tick.c”。在一个任务将要进行延时或超时检测的时候,内核会将这些任务插入 OSCfg_TickWheel 数组的不同元素(一个元素组织一个节拍列表)中。插入操作位于 OS_TickListInsert() 函数(函数的定义也位于“os_tick.c”,就在OS_TickListUpdate() 函数定义的上方),通过任务的 TickCtrMatch(TickCtrMatch=OSTickCtr 当前+需延时或超时节拍数)对 OSCfg_TickWheelSize 的取余(哈希算法)来决定将其插入OSCfg_TickWheel 数组的哪个元素(列表)。相对应的,在 OS_TickListUpdate() 函数中查找到
期任务时,为了能快速检测到到期的任务,通过 OSTickCtr 对 OSCfg_TickWheelSize 的取余来决定操作 OSCfg_TickWheel 数组的哪个元素(列表)。TickCtrMatch 不变,OSTickCtr 一直在计数(逢一个时钟节拍加 1),OSTickCtr 等于 TickCtrMatch 时,延时或超时完成,所以此时它
俩对 OSCfg_TickWheelSize 的取余肯定相等,也就找到了到期任务在 OSCfg_TickWheel 数组的哪个元素了。这样就大大缩小了查找范围了,不用遍历 OSCfg_TickWheel 整个数组,缩小为1/OSCfg_TickWheelSize。但是在代码中,OSTickCtr 和 TickCtrMatch 对 OSCfg_TickWheelSize 的
取余相等,不一定该两变量就相等,只是可能相等,所以还得进一步判断 OSTickCtr 和TickCtrMatch 是否相等,所以在代码中可以看到对查找到元素(列表)还进行了进一步的判断(遍历)。在一个节拍列表中,是 TickCtrMatch 从小到大排序的,所以当遍历到 OSTickCtr
和 TickCtrMatch 相等时,还要继续遍历,因为下一个 TickCtrMatch 可能和当前的 TickCtrMatch相等;如若当遍历到 OSTickCtr 和 TickCtrMatch 不相等时,后面的肯定也不相等,就无需继续遍历了。
OS_TickListInsert() 函数中将任务插入 OSCfg_TickWheel 数组
spoke = (OS_TICK_SPOKE_IX)(p_tcb->TickCtrMatch % OSCfg_TickWheelSize); //使用哈希算法(取余)来决定任务存于 OSCfg_TickWheel 数组 p_spoke = &OSCfg_TickWheel[spoke]; //的哪个元素(节拍列表),与查找到期任务时对应,可方便查找。
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(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和定义一个局部变 //量,用于保存关中断前的 CPU 状态寄存器 SR(临界段关中断只需保存SR) //,开中断时将该值还原。 OS_CRITICAL_ENTER(); //进入临界段 ts_start = OS_TS_GET(); //获取 OS_TickTask() 任务的起始时间戳 OSTickCtr++; //时钟节拍数自加 spoke = (OS_TICK_SPOKE_IX)(OSTickCtr % OSCfg_TickWheelSize); //使用哈希算法(取余)缩小查找到期任务位于 OSCfg_TickWheel 数组的 p_spoke = &OSCfg_TickWheel[spoke]; //哪个元素(一个节拍列表),与任务插入数组时对应,下面只操作该列表。 p_tcb = p_spoke->FirstPtr; //获取节拍列表的首个任务控制块的地址 done = DEF_FALSE; //使下面 while 体得到运行 while (done == DEF_FALSE) { if (p_tcb != (OS_TCB *)0) { //如果该任务不空(存在) p_tcb_next = p_tcb->TickNextPtr; //获取该列表中紧邻该任务的下一个任务控制块的地址 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 //计算延时的的剩余时间 - OSTickCtr; if (OSTickCtr == p_tcb->TickCtrMatch) { //如果任务期满 p_tcb->TaskState = OS_TASK_STATE_RDY; //修改任务状态量为就绪状态 OS_TaskRdy(p_tcb); //让任务就绪 } else { //如果任务未期满(由于升序排列,该列表后面的任务肯定也未期满) done = DEF_TRUE; //不再遍历该列表,退出 while 循环 } break; case OS_TASK_STATE_PEND_TIMEOUT: //如果是有期限等待状态 p_tcb->TickRemain = p_tcb->TickCtrMatch //计算期限的的剩余时间 - OSTickCtr; if (OSTickCtr == p_tcb->TickCtrMatch) { //如果任务期满 #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); //从等待列表移除该任务 OS_TaskRdy(p_tcb); //让任务就绪 p_tcb->TaskState = OS_TASK_STATE_RDY; //修改任务状态量为就绪状态 p_tcb->PendStatus = OS_STATUS_PEND_TIMEOUT; //记录等待状态为超时 p_tcb->PendOn = OS_TASK_PEND_ON_NOTHING; //记录等待内核对象变量为空 } else { //如果任务未期满(由于升序排列,该列表后面的任务肯定也未期满) done = DEF_TRUE; //不再遍历该列表,退出 while 循环 } break; case OS_TASK_STATE_DLY_SUSPENDED: //如果是延时中被挂起状态 p_tcb->TickRemain = p_tcb->TickCtrMatch //计算延时的的剩余时间 - OSTickCtr; if (OSTickCtr == p_tcb->TickCtrMatch) { //如果任务期满 p_tcb->TaskState = OS_TASK_STATE_SUSPENDED; //修改任务状态量为被挂起状态 OS_TickListRemove(p_tcb); //从节拍列表移除该任务 } else { //如果任务未期满(由于升序排列,该列表后面的任务肯定也未期满) done = DEF_TRUE; //不再遍历该列表,退出 while 循环 } break; case OS_TASK_STATE_PEND_TIMEOUT_SUSPENDED: //如果是有期限等待中被挂起状态 p_tcb->TickRemain = p_tcb->TickCtrMatch //计算期限的的剩余时间 - OSTickCtr; if (OSTickCtr == p_tcb->TickCtrMatch) { //如果任务期满 #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); //从等待列表移除该任务 OS_TickListRemove(p_tcb); //从节拍列表移除该任务 p_tcb->TaskState = OS_TASK_STATE_SUSPENDED; //修改任务状态量为被挂起状态 p_tcb->PendStatus = OS_STATUS_PEND_TIMEOUT; //记录等待状态为超时 p_tcb->PendOn = OS_TASK_PEND_ON_NOTHING; //记录等待内核对象变量为空 } else { //如果任务未期满(由于升序排列,该列表后面的任务肯定也未期满) done = DEF_TRUE; //不再遍历该列表,退出 while 循环 } break; default: break; } p_tcb = p_tcb_next; //遍历节拍列表的下一个任务 } else { //如果该任务为空(节拍列表后面肯定也都是空的) done = DEF_TRUE; //不再遍历该列表,退出 while 循环 } } ts_end = OS_TS_GET() - ts_start; //获取 OS_TickTask() 任务的结束时间戳,并计算其执行时间 if (OSTickTaskTimeMax < ts_end) { //更新 OS_TickTask() 任务的最大运行时间 OSTickTaskTimeMax = ts_end; } OS_CRITICAL_EXIT(); //退出临界段 }