uCOS-III 學習記錄(8)——支援多優先順序
參考內容:《[野火]uCOS-III核心實現與應用開發實戰指南——基於STM32》第 12 章。
目錄- 0 資料型別定義和巨集定義
- 1 系統初始化 OSInit()(os_core.c)
- 2 任務建立函式 OSTaskCreate()(os_task.c)
- 3 空閒任務初始化 OS_IdleTaskInit()(os_core.c)
- 4 系統啟動 OSStart()(os_core.c)
- 5 可懸起系統呼叫中斷服務程式 PendSV_Handler()(os_cpu_a.s)
- 6 阻塞延時 OSTimeDly()(os_time.c)
- 7 任務切換 OSSched()(os_core.c)
- 8 SysTick 發起中斷後呼叫 OSTimeTick()(os_time.c)
- 9 將之前所新增的內容進行運用
本篇內容主要是對過往函式的一些修改,因此,一些細節將不會贅述。
0 資料型別定義和巨集定義
0.1 臨界段巨集定義(os.h)
#define OS_CRITICAL_ENTER() CPU_CRITICAL_ENTER()
#define OS_CRITICAL_ENTER_CPU_CRITICAL_EXIT()
#define OS_CRITICAL_EXIT() CPU_CRITICAL_EXIT()
#define OS_CRITICAL_EXIT_NO_SCHED() CPU_CRITICAL_EXIT()
0.2 任務控制塊 TCB 定義(os.h)
型別定義:
/*----------------------TCB---------------------------*/
/* TCB 重新命名為大寫字母格式 */
typedef struct os_tcb OS_TCB;
/* TCB 資料型別宣告 */
struct os_tcb{
CPU_STK *StkPtr;
CPU_STK_SIZE StkSize;
OS_TICK TaskDelayTicks; /* 任務延時週期個數 */
OS_PRIO Prio; /* 任務優先順序(8位整型,最多支援255個優先順序) */
OS_TCB *NextPtr; /* 就緒列表雙向連結串列的下一個指標 */
OS_TCB *PrevPtr; /* 就緒列表雙向連結串列的前一個指標 */
};
全域性變數定義:
OS_EXT OS_TCB *OSTCBCurPtr;
OS_EXT OS_TCB *OSTCBHighRdyPtr;
0.3 任務就緒列表定義(os.h)
型別定義:
/*---------------------OS_RDY_LIST----------------------------*/
/* 就緒列表重新命名為大寫字母格式 */
typedef struct os_rdy_list OS_RDY_LIST;
/* 就緒列表資料型別宣告,將 TCB 串成雙向連結串列 */
struct os_rdy_list{
OS_TCB *HeadPtr;
OS_TCB *TailPtr;
OS_OBJ_QTY NbrEntries; /* 同一個索引下有多少個任務 */
};
全域性變數定義:
OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX];
0.4 優先順序相關變數定義(os.h)
OS_EXT OS_PRIO OSPrioCur; /* 當前優先順序 */
OS_EXT OS_PRIO OSPrioHighRdy; /* 最高優先順序 */
巨集定義:
#define OS_PRIO_INIT (OS_CFG_PRIO_MAX)
1 系統初始化 OSInit()(os_core.c)
該函式用於系統的初始化,說白了就是初始化各種全域性變數的地方。該函式完成的工作有:
- 初始化 TCB 相關的全域性變數。
- 初始化優先順序相關的全域性變數。
- 初始化優先順序表。
- 初始化就緒列表。
- 初始化空閒任務。
/* OS 系統初始化,用於初始化全域性變數 */
void OSInit (OS_ERR *p_err)
{
/* 系統用一個全域性變數 OSRunning 來指示系統的執行狀態。系統初始化時,預設為停止狀態,即 OS_STATE_OS_STOPPED */
OSRunning = OS_STATE_OS_STOPPED;
OSTCBCurPtr = (OS_TCB *) 0; /* 指向當前正在執行的任務的 TCB 指標 */
OSTCBHighRdyPtr = (OS_TCB *) 0; /* 指向就緒任務中優先順序最高的任務的 TCB */
OSPrioCur = (OS_PRIO)0; /* 初始化當前優先順序 */
OSPrioHighRdy = (OS_PRIO)0; /* 初始化最高優先順序 */
OS_PrioInit(); /* 初始化優先順序表 */
OS_RdyListInit(); /* 初始化就緒列表 */
OS_IdleTaskInit(p_err); /* 初始化空閒任務 */
if (*p_err != OS_ERR_NONE) {
return;
}
}
2 任務建立函式 OSTaskCreate()(os_task.c)
該函式完成的工作有:
- 初始化任務 TCB,將 TCB 初始化為預設值。
- 初始化任務棧。
- 在任務 TCB 中記錄任務的優先順序。
- 在任務 TCB 中記錄棧頂指標。
- 在任務 TCB 中記錄棧的大小。
- 在優先順序表中將對應的優先順序位置置 1。
- 將任務 TCB 加入就緒列表中。即:將任務 TCB 放到 OSRdyList[優先順序] 中,如果同一個優先順序有多個任務,那麼這些任務的 TCB 就會被放到 OSRdyList[優先順序] 串成一個雙向連結串列。
注意:
- 以上工作位於臨界段內。
/* 任務建立函式 */
void OSTaskCreate( OS_TCB *p_tcb, /* TCB指標 */
OS_TASK_PTR p_task, /* 任務函式名 */
void *p_arg, /* 任務的形參 */
OS_PRIO prio, /* 任務優先順序 */
CPU_STK *p_stk_base, /* 任務棧的起始地址 */
CPU_STK_SIZE stk_size, /* 任務棧大小 */
OS_ERR *p_err ) /* 錯誤碼 */
{
CPU_STK *p_sp;
CPU_SR_ALLOC();
OS_TaskInitTCB (p_tcb);
p_sp = OSTaskStkInit ( p_task,
p_arg,
p_stk_base,
stk_size ); /* 任務棧初始化函式 */
p_tcb->Prio = prio; /* 任務優先順序儲存在 TCB 的 prio 中*/
p_tcb->StkPtr = p_sp; /* 剩餘棧的棧頂指標 p_sp 儲存到任務控制塊 TCB 的 StkPtr 中 */
p_tcb->StkSize = stk_size; /* 將任務棧的大小儲存到任務控制塊 TCB 的成員 StkSize 中 */
OS_CRITICAL_ENTER(); /* 進入臨界段 */
/* 將任務新增到就緒列表 */
OS_PrioInsert (p_tcb->Prio);
OS_RdyListInsertTail (p_tcb);
OS_CRITICAL_EXIT(); /* 退出臨界段 */
*p_err = OS_ERR_NONE; /* 函式執行到這裡表示沒有錯誤 */
}
2.1 初始化任務控制塊 OS_TaskInitTCB()(os_task.c)
該函式完成的工作是將任務 TCB 的每一個成員都賦值為預設值。其中 OS_PRIO_INIT 是任務 TCB 初始化的時候給的預設的一個優先順序,
巨集展開等於 OS_CFG_PRIO_MAX,這是一個不會被 OS 使用到的優先順序。
/* 初始化任務 TCB */
void OS_TaskInitTCB (OS_TCB *p_tcb)
{
p_tcb->StkPtr = (CPU_STK *) 0;
p_tcb->StkSize = (CPU_STK_SIZE)0;
p_tcb->TaskDelayTicks = (OS_TICK ) 0;
p_tcb->Prio = (OS_PRIO ) OS_PRIO_INIT;
p_tcb->NextPtr = (OS_TCB *) 0;
p_tcb->PrevPtr = (OS_TCB *) 0;
}
3 空閒任務初始化 OS_IdleTaskInit()(os_core.c)
該函式為空閒任務建立了一個 TCB,並初始化了空閒任務的棧。注意,空閒任務的優先順序是最低的,等於 (OS_CFG_PRIO_MAX - 1u),這意味著在系統沒有任何使用者任務執行的情況下,空閒任務就會被執行。
/* 空閒任務初始化函式 */
void OS_IdleTaskInit (OS_ERR *p_err)
{
OSIdleTaskCtr = (OS_IDLE_CTR) 0; /* 計數器清零 */
OSTaskCreate ((OS_TCB*) &OSIdleTaskTCB,
(OS_TASK_PTR) OS_IdleTask,
(void *) 0,
(OS_PRIO) (OS_CFG_PRIO_MAX - 1u),
(CPU_STK *) OSCfg_IdleTaskStkBasePtr,
(CPU_STK_SIZE) OSCfg_IdleTaskStkSize,
(OS_ERR *) &p_err); /* 建立空閒任務 */
}
4 系統啟動 OSStart()(os_core.c)
該函式完成的工作有:
- 找到優先順序表的最高優先順序,並將其賦值給 OSPrioHighRdy,再賦值給 OSPrioCur。
- 根據最高優先順序,找到對應的任務 TCB 連結串列,將其頭指標賦值給 OSTCBHighRdyPtr,再賦值給 OSTCBCurPtr。
- 啟動任務切換。
/* 系統啟動函式 */
void OSStart (OS_ERR *p_err)
{
if ( OSRunning == OS_STATE_OS_STOPPED )
{
OSPrioHighRdy = OS_PrioGetHighest();
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
OSTCBCurPtr = OSTCBHighRdyPtr;
OSStartHighRdy(); /* 啟動任務切換,不會返回 */
*p_err = OS_ERR_FATAL_RETURN; /* 執行至此處,說明發生了致命錯誤 */
}
else{
*p_err = OS_STATE_OS_RUNNING;
}
}
5 可懸起系統呼叫中斷服務程式 PendSV_Handler()(os_cpu_a.s)
該函式完成的工作有:
- 儲存舊任務的暫存器狀態。
- 使 OSPrioCur = OSPrioHighRdy。
- 使 OSTCBCurPtr = OSTCBHighRdyPtr。
- 恢復新任務的暫存器狀態。
;**********PendSVHandler異常**********
PendSV_Handler PROC
CPSID I ; 關中斷,防止上下文切換
MRS R0, PSP ; 將 PSP 載入到 R0,MRS 是 ARM 32 位資料載入指令,
; 功能是載入特殊功能暫存器的值到通用暫存器
CBZ R0, OS_CPU_PendSVHandler_nosave ; 判斷 R0,如果值為 0 則跳轉到 OS_CPU_PendSVHandler_nosave
; 進行第一次任務切換的時候,R0 肯定為 0
STMDB R0!, {R4-R11} ; 手動儲存 R4-R11 暫存器到當前任務棧中,而其他暫存器會被 CPU 自動入棧
LDR R1, = OSTCBCurPtr ; 將 OSTCBCurPtr 指標的地址載入到 R1
LDR R1, [R1] ; 將 OSTCBCurPtr 指標載入到 R1
STR R0, [R1] ; 儲存 R0(任務棧棧頂)的值到 OSTCBCurPtr(->StkPtr)
OS_CPU_PendSVHandler_nosave
; 使 OSPrioCur = OSPrioHighRdy
LDR R0, = OSPrioCur ; 將 OSPrioCur 指標的地址載入到 R0
LDR R1, = OSPrioHighRdy ; 將 OSPrioHighRdy 指標的地址載入到 R1
LDR R2, [R1] ; 將 OSPrioCur 指標載入到 R2
STR R2, [R0] ; 將 OSPrioHighRdy(R2)存到 OSPrioCur(R0)
; 使 OSTCBCurPtr = OSTCBHighRdyPtr
LDR R0, = OSTCBCurPtr ; 將 OSTCBCurPtr 指標的地址載入到 R0
LDR R1, = OSTCBHighRdyPtr ; 將 OSTCBHighRdyPtr 指標的地址載入到 R1
LDR R2, [R1] ; 將 OSTCBCurPtr 指標載入到 R2
STR R2, [R0] ; 將 OSTCBHighRdyPtr(R2)存到 OSTCBCurPtr(R0)
LDR R0, [R2] ; 載入 OSTCBHighRdyPtr(->StkPtr) 到 R0
LDMIA R0!, {R4-R11} ; 載入需要手動儲存的資訊到 CPU 暫存器 R4-R11,其他暫存器將在返回後由 CPU 自動裝載
MSR PSP, R0 ; 更新PSP的值,這個時候PSP指向下一個要執行的任務的堆疊的棧底(這個棧底已經加上剛剛手動載入到CPU暫存器R4-R11的偏移)
ORR LR, LR, #0x04 ; 確保異常返回使用的堆疊指標是PSP,即LR暫存器的位2要為1
CPSIE I ; 開中斷
BX LR ; 異常返回,這個時候任務堆疊中的剩下內容將會自動載入到xPSR,PC(任務入口地址),R14,R12,R3,R2,R1,R0(任務的形參)
; 同時PSP的值也將更新,即指向任務堆疊的棧頂。在STM32中,堆疊是由高地址向低地址生長的。
NOP ; 為了彙編指令對齊,不然會有警告
ENDP
6 阻塞延時 OSTimeDly()(os_time.c)
任務呼叫 OSTimeDly() 函式之後,任務就處於阻塞態,需要將任務從就緒列表中移除(此處未實現)。因此需要完成的工作有:
- 任務 TCB 記錄好延時時間。
- 在優先順序表中清除相應的位,達到任務不處於就緒態的目的。
注意:
- 以上工作位於臨界段內。
/* 阻塞延時 */
void OSTimeDly (OS_TICK dly)
{
CPU_SR_ALLOC();
OS_CRITICAL_ENTER(); /* 進入臨界段 */
/* 延時時間 */
OSTCBCurPtr->TaskDelayTicks = dly;
OS_PrioRemove (OSTCBCurPtr->Prio);
OS_CRITICAL_EXIT(); /* 退出臨界段 */
/* 任務切換 */
OSSched();
}
7 任務切換 OSSched()(os_core.c)
任務排程函式根據優先順序進行排程。具體完成的工作如下:
- 查詢最高優先順序。
- 如果找到的最高優先順序是當前任務,則直接返回,不進行任務切換,否則進行任務切換。
注意:
- 以上工作位於臨界段內。
/* 任務排程 */
void OSSched (void)
{
CPU_SR_ALLOC();
OS_CRITICAL_ENTER(); /* 進入臨界段 */
OSPrioHighRdy = OS_PrioGetHighest();
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
/* 如果最高優先順序的任務是當前任務則直接返回,不進行任務切換 */
if (OSTCBHighRdyPtr == OSTCBCurPtr)
{
OS_CRITICAL_EXIT(); /* 退出臨界段 */
return;
}
OS_CRITICAL_EXIT(); /* 退出臨界段 */
OS_TASK_SW(); /* 任務切換 */
}
8 SysTick 發起中斷後呼叫 OSTimeTick()(os_time.c)
該函式完成的工作有:
- 遍歷整個就緒列表,發現有任務在延時,將其延時時間減一。
- 如果減一後發現任務已經延時結束了,將任務從阻塞態變為就緒態,即在優先順序表中的相應位置置位。
注意:
- 以上工作位於臨界段內。
void OSTimeTick (void)
{
OS_PRIO i;
CPU_SR_ALLOC();
OS_CRITICAL_ENTER(); /* 進入臨界段 */
/* 遍歷整個就緒列表 */
for ( i = 0u; i < OS_CFG_PRIO_MAX; i++)
{
if ( OSRdyList[i].HeadPtr->TaskDelayTicks > 0u ) /* 如果延時未到時,則減 1 */
{
OSRdyList[i].HeadPtr->TaskDelayTicks --;
if (OSRdyList[i].HeadPtr->TaskDelayTicks == 0u) /* 如果延時時間已到,讓任務就緒 */
{
OS_PrioInsert (i);
}
}
}
OS_CRITICAL_EXIT(); /* 退出臨界段 */
/* 任務排程 */
OSSched();
}
9 將之前所新增的內容進行運用
9.1 主函式 main()(app.c)
在 app.c 中,我們添加了 3 個任務。注意:
- 要將任務棧的大小設定得大些,不然可能不夠用。
- 3 個任務的優先順序分別是 1、2、3,空閒任務佔據了最後一個優先順序,0 優先順序不能使用。
#include "ARMCM3.h"
#include "os.h"
#define TASK1_STK_SIZE 128
#define TASK2_STK_SIZE 128
#define TASK3_STK_SIZE 128
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
static CPU_STK Task3Stk[TASK3_STK_SIZE];
static OS_TCB Task1TCB;
static OS_TCB Task2TCB;
static OS_TCB Task3TCB;
uint32_t flag1;
uint32_t flag2;
uint32_t flag3;
void Task1 (void *p_arg);
void Task2 (void *p_arg);
void Task3 (void *p_arg);
int main (void)
{
OS_ERR err;
/* 初始化相關的全域性變數,建立空閒任務 */
OSInit(&err);
/* CPU 初始化:初始化時間戳 */
CPU_Init();
/* 關中斷,因為此時 OS 未啟動,若開啟中斷,那麼 SysTick 將會引發中斷 */
CPU_IntDis();
/* 初始化 SysTick,配置 SysTick 為 10ms 中斷一次,Tick = 10ms */
OS_CPU_SysTickInit(10);
/* 建立任務 */
OSTaskCreate ((OS_TCB*) &Task1TCB,
(OS_TASK_PTR) Task1,
(void *) 0,
(OS_PRIO) 1,
(CPU_STK*) &Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task2TCB,
(OS_TASK_PTR) Task2,
(void *) 0,
(OS_PRIO) 2,
(CPU_STK*) &Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task3TCB,
(OS_TASK_PTR) Task3,
(void *) 0,
(OS_PRIO) 3,
(CPU_STK*) &Task3Stk[0],
(CPU_STK_SIZE) TASK3_STK_SIZE,
(OS_ERR *) &err);
/* 啟動OS,將不再返回 */
OSStart(&err);
}
void Task1 (void *p_arg)
{
for (;;)
{
flag1 = 1;
OSTimeDly (2);
flag1 = 0;
OSTimeDly (2);
}
}
void Task2 (void *p_arg)
{
for (;;)
{
flag2 = 1;
OSTimeDly (2);
flag2 = 0;
OSTimeDly (2);
}
}
void Task3 (void *p_arg)
{
for (;;)
{
flag3 = 1;
OSTimeDly (2);
flag3 = 0;
OSTimeDly (2);
}
}
現在我們來複原一下整個執行過程。
9.2 執行過程
9.2.1 在主函式中
- 系統初始化:初始化各種全域性變數,初始化優先順序表,初始化就緒列表,初始化空閒任務(包括初始化空閒任務棧和空閒任務 TCB)。
- CPU 初始化:暫為空。
- 關中斷:因為此時 OS 未啟動,若開啟中斷,那麼 SysTick 將會引發中斷,打斷初始化流程。
- 初始化 SysTick:配置 SysTick 為 10ms 中斷一次,Tick = 10ms。
- 建立任務:包括建立任務棧和任務 TCB,以及將 TCB 插入到就緒列表中,在優先順序表對應位置置位。
- 啟動系統:先找到最高優先順序,然後開始執行最高優先順序對應的任務(最高優先順序為 1,即為 Task1),啟動第一次任務切換(此時將完成最後的初始化流程,即有關 PendSV 的中斷優先順序配置,接著觸發 PendSV 異常,發起任務切換),將 CPU 佔有權交給任務 Task1。
9.2.2 在 Task1 中
- flag1 = 1。
- 執行到阻塞函式 OSTimeDly:在 Task1 的 TCB 中記錄好延時時間(為 2),同時在優先順序表中的相應位置(即優先順序 1 的位置)清零。最後啟動任務排程。
- 執行任務排程 OSSched:任務排程器先找到最高優先順序,然後再找到最高優先順序的任務 TCB。如果發現該任務就是當前任務,則不進行任務切換。在本案例中發現最高優先順序為 2,對應任務是 Task2,不是當前任務,則發起任務切換(發起 PendSV 異常)。
- PendSV 異常處理程式:儲存 Task1 的狀態,載入 Task2 的狀態,更新全域性變數的值。
9.2.3 在 Task2 中
- flag2 = 1。
- 執行到阻塞函式 OSTimeDly:在 Task2 的 TCB 中記錄好延時時間(為 2),同時在優先順序表中的相應位置(即優先順序 2 的位置)清零。最後啟動任務排程。
- 執行任務排程 OSSched:任務排程器先找到最高優先順序,然後再找到最高優先順序的任務 TCB。如果發現該任務就是當前任務,則不進行任務切換。在本案例中發現最高優先順序為 3,對應任務是 Task3,不是當前任務,則發起任務切換(發起 PendSV 異常)。
- PendSV 異常處理程式:儲存 Task2 的狀態,載入 Task3 的狀態,更新全域性變數的值。
9.2.4 在 Task3 中
- flag3 = 1。
- 執行到阻塞函式 OSTimeDly:在 Task3 的 TCB 中記錄好延時時間(為 2),同時在優先順序表中的相應位置(即優先順序 31 的位置)清零。最後啟動任務排程。
- 執行任務排程 OSSched:任務排程器先找到最高優先順序,然後再找到最高優先順序的任務 TCB。如果發現該任務就是當前任務,則不進行任務切換。在本案例中發現最高優先順序為 31,對應任務是 空閒任務,不是當前任務,則發起任務切換(發起 PendSV 異常)。
- PendSV 異常處理程式:儲存 Task3 的狀態,載入空閒任務的狀態,更新全域性變數的值。
請注意,每次任務切換的時間其實非常短,大約只有不到 0.1ms 的長度,不會對 2ms 的延時造成很大的影響。
9.2.5 在空閒任務中 SysTick 發起中斷
- 執行 OSTimeTick:遍歷整個就緒列表,發現各個任務的延時未到時(此時為 2),全部減一;減一後(此時為 1)發現延時也未到時,說明任務還未到進入就緒態的時候。最後發起任務排程,發現不用進行任務切換,空閒任務繼續執行。
- 再次發起中斷,執行 OSTimeTick:遍歷整個就緒列表,發現各個任務的延時未到時(此時為 1),全部減一;減一後(此時為 0)發現延時已到時,說明任務進入就緒態了。將 Task1、Task2、Task3 的優先順序在優先順序表中的相應位置重新置位。最後發起任務排程,發現最高優先順序為 1,對應的是 Task1,切換到 Task1 執行。
9.2.6 又在 Task1 中
- 將全域性變數 flag1 由 1 變成 0。
- 執行到阻塞函式 OSTimeDly:在 Task1 的 TCB 中記錄好延時時間(為 2),同時在優先順序表中的相應位置(即優先順序 1 的位置)清零。最後啟動任務排程。
- 執行任務排程 OSSched:任務排程器先找到最高優先順序,然後再找到最高優先順序的任務 TCB。如果發現該任務就是當前任務,則不進行任務切換。在本案例中發現最高優先順序為 2,對應任務是 Task2,不是當前任務,則發起任務切換(發起 PendSV 異常)。
- PendSV 異常處理程式:儲存 Task1 的狀態,載入 Task2 的狀態,更新全域性變數的值。
如此反覆,不再贅述。
9.3 實驗現象
這是執行的模擬結果,實驗現象符合以上分析,三個任務同時執行,就像多執行緒一樣:
看起來三個任務同步執行,效率很高。不過放大後發現,三個任務的上升時間並不是嚴格同時的,而是略有先後:先是 Task1 上升,然後是 Task2,最後是 Task3,這中間的時間差就是任務切換的花費時間,大約是 0.1ms。
同理,三個任務的下降時間也是略有先後的:先是 Task1 下降,然後是 Task2,最後是 Task3,這中間的時間差也是任務切換的花費時間,大約是 0.1ms。
嘗試:
- 如果將 Task1、Task2、Task3 的優先順序分別改為 3、2、1,那麼實驗現象是:先是 Task3 上升,然後是 Task2,最後是 Task1,對於下降同理。因此,優先順序影響著任務執行的先後順序:先執行 Task3,然後切換到 Task2,最後切換到 Task1。
- 如果將 Task3 的優先順序改為 2,那麼 Task3 將不會被執行,因為優先順序為 2 的任務有兩個,目前只能執行其中一個(位於頭指標的任務 TCB),我們還未實現任務輪轉的功能。