µCOS-III从入门到精通 第七章(任务调度)
参考教程:【正点原子】手把手教你学UCOS-III实时操作系统_哔哩哔哩_bilibili
一、µC/OS-III的初始化
1、OSInit函数简介
(1)OSInit函数用于初始化µC/OS-III,必须在调用任何其它µC/OS-III函数之前调用它,仅调用一次即可。
(2)该函数的执行内容:
①对一些全局变量赋初始值。
②初始化就绪列表以及Tick列表等。
③创建三个任务:空闲任务(必须创建),统计任务(条件创建),软件定时器任务(条件创建)。
[1]空闲任务:任务优先级最低31,当系统无其它就绪任务,那么空闲任务将会执行(空闲任务不能被阻塞)。
[2]统计任务:任务优先级为30,用来统计CPU使用率和各个任务的堆栈使用量。
[3]软件定时器任务:任务优先级为29,主要用于在特定的时间段内处理单次或周期性的软件定时器。
2、OSInit函数源码概览
void OSInit (OS_ERR *p_err)
{
#if (OS_CFG_ISR_STK_SIZE > 0u)
CPU_STK *p_stk;
CPU_STK_SIZE size;
#endif
#ifdef OS_SAFETY_CRITICAL
if (p_err == (OS_ERR *)0) {
OS_SAFETY_CRITICAL_EXCEPTION();
return;
}
#endif
OSInitHook(); //调用端口特定初始化代码
OSIntNestingCtr = 0u; //中断嵌套计数器初始化为0
OSRunning = OS_STATE_OS_STOPPED; //多任务调度初始化为未开启
OSSchedLockNestingCtr = 0u; //任务调度锁初始化为关闭
OSTCBCurPtr = (OS_TCB *)0; //当前执行任务的任务控制块初始化为空
OSTCBHighRdyPtr = (OS_TCB *)0;//当前优先级最高的任务的任务控制块初始化为空
OSPrioCur = 0u; //当前运行的任务的优先级初始化为0
OSPrioHighRdy = 0u; //当前优先级最高的任务的优先级初始化为0
#if (OS_CFG_SCHED_LOCK_TIME_MEAS_EN > 0u)
OSSchedLockTimeBegin = 0u;
OSSchedLockTimeMax = 0u;
OSSchedLockTimeMaxCur = 0u;
#endif
#ifdef OS_SAFETY_CRITICAL_IEC61508
OSSafetyCriticalStartFlag = OS_FALSE;
#endif
#if (OS_CFG_SCHED_ROUND_ROBIN_EN > 0u)
OSSchedRoundRobinEn = OS_FALSE;
OSSchedRoundRobinDfltTimeQuanta = OSCfg_TickRate_Hz / 10u;
#endif
#if (OS_CFG_ISR_STK_SIZE > 0u)
p_stk = OSCfg_ISRStkBasePtr;
if (p_stk != (CPU_STK *)0) {
size = OSCfg_ISRStkSize;
while (size > 0u) {
size--;
*p_stk = 0u;
p_stk++;
}
}
#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)
OS_TaskStkRedzoneInit(OSCfg_ISRStkBasePtr, OSCfg_ISRStkSize);
#endif
#endif
#if (OS_CFG_APP_HOOKS_EN > 0u)
#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)
OS_AppRedzoneHitHookPtr = (OS_APP_HOOK_TCB )0;
#endif
OS_AppTaskCreateHookPtr = (OS_APP_HOOK_TCB )0;
OS_AppTaskDelHookPtr = (OS_APP_HOOK_TCB )0;
OS_AppTaskReturnHookPtr = (OS_APP_HOOK_TCB )0;
OS_AppIdleTaskHookPtr = (OS_APP_HOOK_VOID)0;
OS_AppStatTaskHookPtr = (OS_APP_HOOK_VOID)0;
OS_AppTaskSwHookPtr = (OS_APP_HOOK_VOID)0;
OS_AppTimeTickHookPtr = (OS_APP_HOOK_VOID)0;
#endif
#if (OS_CFG_TASK_REG_TBL_SIZE > 0u)
OSTaskRegNextAvailID = 0u;
#endif
OS_PrioInit(); //就绪列表中每个优先级的任务列表全初始化为空
OS_RdyListInit(); //就绪列表初始化
#if (OS_CFG_FLAG_EN > 0u)
#if (OS_CFG_DBG_EN > 0u)
OSFlagDbgListPtr = (OS_FLAG_GRP *)0;
OSFlagQty = 0u;
#endif
#endif
#if (OS_CFG_MEM_EN > 0u)
OS_MemInit(p_err);
if (*p_err != OS_ERR_NONE)
return;
#endif
#if (OS_MSG_EN > 0u)
OS_MsgPoolInit(p_err);
if (*p_err != OS_ERR_NONE)
return;
#endif
#if (OS_CFG_MUTEX_EN > 0u)
#if (OS_CFG_DBG_EN > 0u)
OSMutexDbgListPtr = (OS_MUTEX *)0;
OSMutexQty = 0u;
#endif
#endif
#if (OS_CFG_Q_EN > 0u)
#if (OS_CFG_DBG_EN > 0u)
OSQDbgListPtr = (OS_Q *)0;
OSQQty = 0u;
#endif
#endif
#if (OS_CFG_SEM_EN > 0u)
#if (OS_CFG_DBG_EN > 0u)
OSSemDbgListPtr = (OS_SEM *)0;
OSSemQty = 0u;
#endif
#endif
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
OS_TLS_Init(p_err);
if (*p_err != OS_ERR_NONE)
return;
#endif
OS_TaskInit(p_err); //任务数、任务切换次数均初始化为0
if (*p_err != OS_ERR_NONE)
return;
#if (OS_CFG_TASK_IDLE_EN > 0u)
OS_IdleTaskInit(p_err); //创建空闲任务
if (*p_err != OS_ERR_NONE)
return;
#endif
#if (OS_CFG_TICK_EN > 0u)
OS_TickInit(p_err); //Tick列表初始化(包括但不限于系统节拍初始化为0)
if (*p_err != OS_ERR_NONE)
return;
#endif
#if (OS_CFG_STAT_TASK_EN > 0u)
OS_StatTaskInit(p_err); //创建统计任务
if (*p_err != OS_ERR_NONE)
return;
#endif
#if (OS_CFG_TMR_EN > 0u)
OS_TmrInit(p_err); //创建软件定时器任务
if (*p_err != OS_ERR_NONE)
return;
#endif
#if (OS_CFG_DBG_EN > 0u)
OS_Dbg_Init();
#endif
OSCfg_Init();
OSInitialized = OS_TRUE;
}
二、开启任务调度器
1、OSStart函数简介
(1)OSStart函数用于启动任务调度器,任务调度器启动后,µC/OS-III便会开始进行任务调度。
(2)该函数的执行内容:
①进行一些安全关键代码判断。
②获取当前最高优先级任务。
③将调度器运行状态标志设置为开启状态。
④获取最高优先级任务的任务控制块。
⑤调用函数OSStartHighRdy,启动第一个任务。
(3)前导置零指令:CPU_CntLeadZeros(x),其中x为32位的变量,它能返回一个32位二进制数头部0的个数。
2、OSStart函数源码概览
void OSStart (OS_ERR *p_err)
{
OS_OBJ_QTY kernel_task_cnt; //用于当前任务总数统计
#ifdef OS_SAFETY_CRITICAL
if (p_err == (OS_ERR *)0) {
OS_SAFETY_CRITICAL_EXCEPTION();
return;
}
#endif
if (OSInitialized != OS_TRUE) {
*p_err = OS_ERR_OS_NOT_INIT;
return;
}
kernel_task_cnt = 0u;
#if (OS_CFG_STAT_TASK_EN > 0u) //如果创建了统计任务,需要计入任务总数
kernel_task_cnt++;
#endif
#if (OS_CFG_TMR_EN > 0u) //如果创建了软件定时器任务,需要计入任务总数
kernel_task_cnt++;
#endif
#if (OS_CFG_TASK_IDLE_EN > 0u) //如果创建了空闲任务,需要计入任务总数
kernel_task_cnt++;
#endif
if (OSTaskQty <= kernel_task_cnt) //如果没有创建应用任务,将无法开启任务调度器
{
*p_err = OS_ERR_OS_NO_APP_TASK;
return;
}
if (OSRunning == OS_STATE_OS_STOPPED) //调度器仅开启一次即可
{
OSPrioHighRdy = OS_PrioGetHighest(); //通过前导置零指令获取当前最高优先级任务的优先级
OSPrioCur = OSPrioHighRdy; //更改当前执行任务的优先级为当前最高优先级任务的优先级
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; //获取最高优先级就绪列表第一个任务的任务控制块
OSTCBCurPtr = OSTCBHighRdyPtr; //更改任务调度器启动后运行的任务为最高优先级就绪列表的第一个任务
OSRunning = OS_STATE_OS_RUNNING; //调度器运行状态置为运行中
OSStartHighRdy(); //启动第一个任务
*p_err = OS_ERR_FATAL_RETURN;
}
else
{
*p_err = OS_ERR_OS_RUNNING;
}
}
三、启动第一个任务
1、任务的栈
(1)要想启动第一个任务,就需要将该任务的值恢复到CPU的寄存器中,而任务的值在创建任务时已经保存到任务堆栈中。
①中断产生时,硬件自动将xPSR、PC(R15)、LR(R14)、R12、R3-R0保存和恢复,而R4-R11需要手动保存和恢复。
②进入中断后硬件会强制使用MSP指针,此时LR(R14)的值将会被自动被更新为特殊的EXC_RETURN。
(2)任务在被创建时,系统会调用OSTaskStkInit函数初始化任务的堆栈,其源码如下:
CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK *p_stk_limit,
CPU_STK_SIZE stk_size,
OS_OPT opt)
{
CPU_STK *p_stk;
(void)opt; /* 'opt' is not used, prevent warning */
p_stk = &p_stk_base[stk_size]; //获取任务堆栈末尾地址
p_stk = (CPU_STK *)((CPU_STK)(p_stk) & 0xFFFFFFF8u); //8字节对齐
/* 任务相关内容入栈 */
*(--p_stk) = (CPU_STK)0x01000000u; /* xPSR */
//寄存器xPSR被初始为0x01000000,其中bit24被置1,表示使用Thumb指令
*(--p_stk) = (CPU_STK)p_task; /* Entry Point */
//寄存器PC被初始化为任务函数指针vTask_x,这样当某次任务切换后,任务x获得CPU控制权,任务函数vTask_x被出栈到PC寄存器,之后会执行任务x的代码
*(--p_stk) = (CPU_STK)OS_TaskReturn; /* R14 (LR) */
//LR寄存器初始化为函数指针OS_TaskReturn,这是由移植层提供的一个出错处理函数
*(--p_stk) = (CPU_STK)0x12121212u; /* R12 */
//子函数的调用通过寄存器R0~R3传递参数,创建任务时,传入的参数被保存到R0中,用来向任务函数传递参数
*(--p_stk) = (CPU_STK)0x03030303u; /* R3 */
*(--p_stk) = (CPU_STK)0x02020202u; /* R2 */
*(--p_stk) = (CPU_STK)p_stk_limit; /* R1 */
*(--p_stk) = (CPU_STK)p_arg; /* R0 : argument */
*(--p_stk) = (CPU_STK)0xFFFFFFFDuL; /* R14: EXEC_RETURN; See Note 5 */
/* Remaining registers saved on process stack */
*(--p_stk) = (CPU_STK)0x11111111uL; /* R11 */
*(--p_stk) = (CPU_STK)0x10101010uL; /* R10 */
*(--p_stk) = (CPU_STK)0x09090909uL; /* R9 */
*(--p_stk) = (CPU_STK)0x08080808uL; /* R8 */
*(--p_stk) = (CPU_STK)0x07070707uL; /* R7 */
*(--p_stk) = (CPU_STK)0x06060606uL; /* R6 */
*(--p_stk) = (CPU_STK)0x05050505uL; /* R5 */
*(--p_stk) = (CPU_STK)0x04040404uL; /* R4 */
return (p_stk);
}
(3)出栈/压栈汇编指令详解:
2、OSStartHighRdy函数简介
(1)OSStartHighRdy函数用于启动第一个任务。
(2)该函数的执行内容:
①屏蔽中断,防止在启动第一个任务时被打断。
②将PendSV设置为最低优先级。
③将PSP清0,PSP将用于任务栈。
④将MSP设置为OS_CPU_ExceptStkBas,指向异常栈的栈底(按8字节对齐)。
⑤获取当前最高优先级的任务的任务优先级,以及任务控制块。
⑥将最高优先级任务的任务栈出栈。
⑦开中断,并跳转到第一个运行任务的任务函数执行。
(3)程序在运行过程中需要一定的栈空间来保存局部变量等一些信息,当有信息保存到栈中时,MCU会自动更新SP指针,使SP指针指向最后一个入栈的元素,那么程序就可以根据SP指针来从栈中存取信息。
ARMCortex-M提供了两个栈空间,这两个栈空间的堆栈指针分别是MSP(主堆栈指针)和PSP(进程堆指针)。
①MSP由OS内核、异常服务例程以及所有需要特权访问的应用程序代码来使用,在µC/OS-III中,中断使用MSP。
②PSP用于常规的应用程序代码(不处于异常服务例程中时),在µC/OS-III中,中断以外使用PSP。
当使用不同的堆栈指针时,SP会等于当前使用的堆栈指针。
3、OSStartHighRdy函数源码概览
OSStartHighRdy
CPSID I ; 屏蔽中断,防止在启动第一个任务时被打断
MOV32 R0, NVIC_SYSPRI14
MOV32 R1, NVIC_PENDSV_PRI
STRB R1, [R0] ; 设置PENDSV的中断优先级为最低
MOVS R0, #0
MSR PSP, R0 ; 将PSP清0,PSP将用于任务栈
MOV32 R0, OS_CPU_ExceptStkBase
LDR R1, [R0]
MSR MSP, R1 ; 将MSP设置为OS_CPU_ExceptStkBas,指向异常栈的栈底(按8字节对齐)
BL OSTaskSwHook
MOV32 R0, OSPrioCur
MOV32 R1, OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0] ; 获取当前最高优先级的任务的任务优先级
MOV32 R0, OSTCBCurPtr
MOV32 R1, OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0] ; 获取当前最高优先级的任务的任务控制块
LDR R0, [R2]
MSR PSP, R0 ; PSP指向最高优先级任务的任务栈栈底
MRS R0, CONTROL
ORR R0, R0, #2 ; CONTROL寄存器的bit1置1,选择PSP指针
BIC R0, R0, #4 ; 如果使用了FPU,CONTROL寄存器的bit2会置1,需要清零
MSR CONTROL, R0
ISB
; SP此时指向任务栈栈底
LDMFD SP!, {R4-R11, LR} ; 将SP存储地址里面的内容手动加载到CPU寄存器r4-r11以及LR,过程中SP地址上移
; 以下出栈操作也需手动,因为这里不是中断
LDMFD SP!, {R0-R3} ; 将SP存储地址里面的内容手动加载到CPU寄存器r0-r3,过程中SP地址上移
LDMFD SP!, {R12, LR} ; 将SP存储地址里面的内容手动加载到CPU寄存器r12以及LR,过程中SP地址上移
LDMFD SP!, {R1, R2} ; 将SP存储地址里面的内容手动加载到CPU寄存器r1-r2,过程中SP地址上移
CPSIE I ; 开中断
BX R1 ; 返回任务函数地址(此时r1中的内容就是任务函数地址)
四、任务切换
1、任务切换概述
(1)任务切换的本质其实就是CPU寄存器的切换(又叫上下文切换),这个过程在PendSV中断服务函数里完成。
(2)假设由任务A切换到任务B,这个过程主要分为两步:
①需暂停任务A的执行,并将此时任务A的寄存器值保存到任务堆栈,这个过程叫做保存现场。
②将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场。
2、PendSV中断
(1)执行µC/OS-III提供的API函数——OSCtxSw和OSIntCtxSw,可以触发PendSV中断。
函数 | 描述 |
OSSched | 任务中使用 |
OSIntExit | 中断中使用 |
(2)启动PendSV中断的底层逻辑:通过向中断控制和状态寄存器ICSR的bit28写入1,挂起PendSV,以启动PendSV中断。
(3)触发任务切换(PendSV中断)的时机:
①任务向另外一个任务发送信号或消息。
②任务调用延时函数(OSTimeDly或OSTimeDlyHMSM)。
③任务等待一个还未发生的事件发生。
④任务的挂起状态被终止。
⑤有任务被创建或被删除。
⑥内核对象被删除。
⑦中断嵌套结束。
⑧任务修改了自身或其它任务的任务优先级。
⑨任务调用函数OSTaskSuspend挂起自身,或者任务调用函数OSTaskResue恢复其它被函数OSTaskSuspend挂起的任务。
⑩使用函数OSSchedUnblock解锁任务调度器。
⑪任务调用函数OSSchedRoundRobinYield弃用剩余的时间片。
⑫在应用程序中调用函数OSSched。
(4)PendSV中断服务函数源码:
OS_CPU_PendSVHandler
CPSID I ; 关中断(Cortex-M7写入BASEPRI后不能立即生效,需要先关中断)
MOV32 R2, OS_KA_BASEPRI_Boundary
LDR R1, [R2]
MSR BASEPRI, R1 ; 屏蔽μC/OS-III可管理的全部中断(优先级4-15的中断)
DSB
ISB
CPSIE I ; 开中断
; 硬件已自动压栈,现PSP指向任务栈的R14,需说明Cortex-M3压栈(以及出栈)的过程在中断以外,使用的是PSP指针
MRS R0, PSP ; 获取PSP的指向地址
IF {FPU} != "SoftVFP"
TST R14, #0x10
IT EQ
VSTMDBEQ R0!, {S16-S31}
ENDIF
STMFD R0!, {R4-R11, R14} ; 从R14开始压栈,将CPU寄存器的内容压回任务栈
MOV32 R5, OSTCBCurPtr ; 获取任务控制块的地址
LDR R1, [R5]
STR R0, [R1] ; 将任务栈顶指针保存到任务控制块的成员中
MOV R4, LR
BL OSTaskSwHook
MOV32 R0, OSPrioCur
MOV32 R1, OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0] ; 获取当前最高优先级任务的最高优先级
MOV32 R1, OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R5] ; 获取当前最高优先级任务的任务控制块
ORR LR, R4, #0x04 ; R14恢复特殊值,bit2要置1,表示从进程堆栈中做出栈操作,返回后使用PSP
LDR R0, [R2] ; 获取切换后任务的任务栈栈底地址
LDMFD R0!, {R4-R11, R14} ; 将任务栈中的部分内容手动恢复至CPU寄存器(剩下的内容由硬件自动恢复)
IF {FPU} != "SoftVFP"
TST R14, #0x10
IT EQ
VLDMIAEQ R0!, {S16-S31}
ENDIF
MSR PSP, R0 ; 使PSP指向任务栈的R0
MOV32 R2, #0
CPSID I ; 关中断(Cortex-M7写入BASEPRI后不能立即生效,需要先关中断)
MSR BASEPRI, R2 ; 打开μC/OS-III可管理的全部中断(优先级4-15的中断)
DSB
ISB
CPSIE I ; 开中断
BX LR
ALIGN
END