時鍾節拍可謂是 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(); //退出臨界段 }