任务的创建函数
ucosii之前的版本都只支持64个任务,但是V2.90版本支持的任务数量达到了256。
用法和原理都差不多。我这里就只介绍任务数不大于64的情况。当任务大于64的时候,只需要配置相关的宏就可。
在之前的统计任务和空闲任务的初始化函数中使用到了任务创建函数OSTaskCreate、OSTaskCreateExt。
UCOSII任务创建函数有两个OSTaskCreate和OSTaskCreateExt。
在空闲任务初始化OS_InitTaskIdle()和统计任务
OS_InitTaskStat(),是根据OS_TASK_CREATE_EXT_EN宏是否大于0来决定是在编译的时候是用OSTaskCreate还是OSTaskCreateExt。
OSTaskCreate提供任务的基本创建。OSTaskCreateExt功能更强,是OSTaskCreate功能的拓展,带有很多的附加的功能。若是不用这些附加功能。
一般的情况下,若是不用到这些附加功能,OSTaskCreate基本能够满足要求。这两个函数都在os_task.c中定义。
1:OSTaskCreate任务创建函数
该函数的原型为:INT8U OSTaskCreate (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT8U prio)
传递的参数分别为任务函数指针,注意任务函数的原型必须是位 void function(void *p);
空指针,任务堆栈指针,以及需要创建的任务优先级prio。
任务创建函数不要在中断中调用,否则会任务创建不成功。
如下代码,若是程序不在
中断环境下运行,
OSIntNesting全局变量为0,否则大于1。
OS_ENTER_CRITICAL();
if (OSIntNesting > 0u) { /* Make sure we don't create the task from within an ISR */
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_CREATE_ISR);
if (OSIntNesting > 0u) { /* Make sure we don't create the task from within an ISR */
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_CREATE_ISR);
}
下面的代码主要是做一些任务相关的检查以及数据初始化工作。
if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { //检查相应优先级的优先级指针是否为空。若不为空,则说明该任务优先级指针已经指向了任务控制块,也就是相应优先级任务已经被其他任务占用,该任务创建也就会失败。
OSTCBPrioTbl[prio] = OS_TCB_RESERVED;//将相应的优先级的指针赋值为OS_TCB_RESERVED,这样的目的是在OS_EXIT_CRITICAL()后,在任务完全创建好之前。系统调度,其他进程运行创建任务时,若是跟该优先级有冲突的时候,判断该任务优先级指针不为空,不会进行该任务的创建了。
OS_EXIT_CRITICAL();
psp = OSTaskStkInit(task, p_arg, ptos, 0u); //初始化任务的堆栈,具体实现,在后面分析。
err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u); //初始化任务控制块的初始化,在后面的分析
if (err == OS_ERR_NONE) {
if (OSRunning == OS_TRUE) { /* Find highest priority task if multitasking has started */
OS_Sched(); //任务创建成功,若任务是在系统创建实在系统内核已经启动值周,则启动调度程序,执行就绪任务优先级最高的任务
}
} else {
} else {
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = (OS_TCB *)0;//若是在这里任务创建不成功之后,注意要讲任务优先级指针清零,否则该优先级指针没有指向具体的任务,指针却被占用着,由前面的代码克制,要再创建该优先级的任务,就不会成功。
OS_EXIT_CRITICAL();
}
return (err);
}
return (err);
}
return (OS_ERR_PRIO_EXIST);
1.1:任务堆栈初始化函数OSTaskStkInit
OSTaskStkInit()的作用,它被任务创建函数调用,所以要在开始时,在栈中作出该任务好像刚被中断一样的假象。那么对应任务的堆栈中,也就要保存该任务的返回现场,但是由于我们这是刚刚创建任务,该任务还没有执行,所以这里堆栈初始保存的是任务入口。从而使在创建成功,在调度程序第一次调用该任务的时候,可以顺利找到该任务的入口,将对应堆栈中的值刷新相对应的各个寄存器。从而进行任务的切换。
中断之后,xPSR,PC,LR,R12,R3-R0被自动保存到栈中的,R11-R4如果需要保存,只能手工保存。
OSTaskStkInit()的工作就是在任务自己的栈中保存cpu的所有寄存器。但是任务在刚刚创建的时候,这些值都只有手工将值写进堆栈中相应的位置了。这些值里R1-R12都没什么意义,这里用相应的数字代号(如R1用0x01010101)主要是方便调试。
其他几个: xPSR = 0x01000000L,xPSR T位(第24位)置1,否则第一次执行任务时Fault, PC肯定得指向任务入口, R14 = 0xFFFFFFFEL,最低4位为E,是一个非法值,主要目的是不让使用R14,即任务是不能返回的。 R0用于传递任务函数的参数,因此等于p_arg。
这里为什么--stk,是因为stm32的堆栈是向下生长的。
OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK *stk;
(void)opt; /* 'opt' is not used, prevent warning */
stk = ptos; /* Load stack pointer */
/* Registers stacked as if auto-saved on exception */
*(stk) = (INT32U)0x01000000L; /* xPSR */
*(--stk) = (INT32U)task; /* Entry Point */
*(--stk) = (INT32U)0xFFFFFFFEL; /* R14 (LR) (init value will cause fault if ever used)*/
*(--stk) = (INT32U)0x12121212L; /* R12 */
*(--stk) = (INT32U)0x03030303L; /* R3 */
*(--stk) = (INT32U)0x02020202L; /* R2 */
*(--stk) = (INT32U)0x01010101L; /* R1 */
*(--stk) = (INT32U)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*(--stk) = (INT32U)0x11111111L; /* R11 */
*(--stk) = (INT32U)0x10101010L; /* R10 */
*(--stk) = (INT32U)0x09090909L; /* R9 */
*(--stk) = (INT32U)0x08080808L; /* R8 */
*(--stk) = (INT32U)0x07070707L; /* R7 */
*(--stk) = (INT32U)0x06060606L; /* R6 */
*(--stk) = (INT32U)0x05050505L; /* R5 */
*(--stk) = (INT32U)0x04040404L; /* R4 */
return (stk);
{
OS_STK *stk;
(void)opt; /* 'opt' is not used, prevent warning */
stk = ptos; /* Load stack pointer */
/* Registers stacked as if auto-saved on exception */
*(stk) = (INT32U)0x01000000L; /* xPSR */
*(--stk) = (INT32U)task; /* Entry Point */
*(--stk) = (INT32U)0xFFFFFFFEL; /* R14 (LR) (init value will cause fault if ever used)*/
*(--stk) = (INT32U)0x12121212L; /* R12 */
*(--stk) = (INT32U)0x03030303L; /* R3 */
*(--stk) = (INT32U)0x02020202L; /* R2 */
*(--stk) = (INT32U)0x01010101L; /* R1 */
*(--stk) = (INT32U)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*(--stk) = (INT32U)0x11111111L; /* R11 */
*(--stk) = (INT32U)0x10101010L; /* R10 */
*(--stk) = (INT32U)0x09090909L; /* R9 */
*(--stk) = (INT32U)0x08080808L; /* R8 */
*(--stk) = (INT32U)0x07070707L; /* R7 */
*(--stk) = (INT32U)0x06060606L; /* R6 */
*(--stk) = (INT32U)0x05050505L; /* R5 */
*(--stk) = (INT32U)0x04040404L; /* R4 */
return (stk);
}
1.2:任务控制块(TCB)初始化函数OS_TCBInit
该函数的原型:NT8U OS_TCBInit (INT8U prio,OS_STK *ptos,OS_STK *pbos,INT16U id,INT32U stk_size,void *pext,INT16U opt)
该函数主要功能是从空闲指针链表中OSTCBFreeList中取出空闲任务控制块,然后取出的任务控制块内的数据进行初始化,这个没有什么太多可以讲的。
OSTCBY、OSTCBX 、ptcb->OSTCBBitY 、ptcb->OSTCBBitX 在之前的就绪表和就绪数组中有讲过。
将任务优先级数组OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u] 中对应的指针元素指向该任务控制块,然后将该控制块放到就绪任务控制块链表OSTCBList中。
最后更新系统的就绪表和就绪数组。
1.3:OS_Sched任务调度函数(os_core.c)
任务调度函数就是找到就绪的任务的最高优先级,并将该任务投入运行,将任务切换相关的数据OSCtxSwCtr等进行刷新,保存将要切换掉任务的上下文环境到任务的堆栈中,同时将要执行的任务的堆栈的值恢复到寄存器中。若是在创建任务的时候,系统多任务没有启动,那么就不会执行调度。若是在系统多任务启动之后,创建任务就会执行调度。
注意该函数不能再中断运行环境下调用。全局OSLockNesting若大于1,调度函数将被锁定,不能进行任务调度。
调用OS_SchedNew()先计算出就绪任务的最高优先级,若当前运行任务的优先级与就绪任务的最高优先级相等,则不调用OS_TASK_SW函数切换。否则调用OS_TASK_SW函数,运行最高优先级任务。
1.3.1:OS_SchedNew函数
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
得到就绪任务中最高优先级如上代码,
具体实现分析在就绪任务和就绪任务组处有说明。(v2.9版本的支持任务已经大于64个任务了,最多可以多达256个)
1.3.2:OS_TASK_SW函数
#define OS_TASK_SW() OSCtxSw()
OSCtxSw()函数跟硬件平台密切相关。该函数的作用就是执行任务的切换。该函数在os_cpu_a.asm中定义,用汇编写的。
NVIC_INT_CTRL EQU 0xE000ED04
NVIC_PENDSVSET EQU 0x10000000
OSCtxSw
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
STR R1, [R0]
BX LR
0xE000ED04是 中断控制及状态寄存器ICSR地址(详见《CM3权威指南》表8.2),将该寄存器赋值为0x10000000,即向该寄存器第28位写1,即悬起 PendSV,当没有更高优先级的中断悬起的时,执行pendsv中断程序。
PENDSV中断程序OS_CPU_PendSVHandler(os_cpu_a.asm )的功能是执行任务上下文切换。
程序如下:
OS_CPU_PendSVHandler
CPSID I ;在任务切换之前,关掉总中断,(CPSID I ;关中断 CPSIE I ;开中断 CPSID F ;关异常 CPSIE F ;开异常 )
MRS R0, PSP ;将PSP堆栈指针的值赋给寄存器R0
CBZ R0, OS_CPU_PendSVHandler_nosave ;R0( 即PSP)为零的时候,说明这是第一次任务切换,在之前都没有进行过任务切换,因此没有需要保存的上下文,这个时候就直接跳到(CBZ为条件转移指令)OS_CPU_PendSVHandler_nosave执行。
SUBS R0, R0, #0x20 ; R0自减#0x20(十进制为32),即向低地址移动32个字节。然后将R4-R11压入堆栈当中(xPSR,PC,LR,R12,R3-R0被自动保存到栈中的
),一个32位寄存器为4个字节,8个寄存器,刚好为32个字节。
STM R0, {R4-R11}
LDR R1, =OSTCBCur
LDR R1, [R1] ;将OSTCBCur的值赋给R1
STR R0, [R1] ; 将R0赋值给*OSTCBCur。TCB中的第一项为任务堆栈的地址,因此这一句改写为新的栈顶地址,改为R0的值。即该任务新SP的地址
之前的为保存上下文环境,至此已经全部完成,由于程序的顺序执行,接下来我们进行需要切换任务的现场恢复OS_CPU_PendSVHandler_nosave 。
OS_CPU_PendSVHandler_nosave
PUSH {R14} ; 保存LR的值,记住这里使用的是中断的堆栈
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0 ;执行OSTaskSwHook
POP {R14} ;恢复LR的值
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCur ; OSTCBCur = OSTCBHighRdy;
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 为新任务的SP; 因为OSTCB的第一个成员为OSTCBStkPtr ,所以R0=SP=*OSTCBHighRdy =OSTCBHighRdy->OSTCBStkPtr,
LDM R0, {R4-R11} ; 从堆栈中取出数据,放到相对应的寄存器中,其他寄存器,xPSR,PC,LR,R12,R3-R0会根据中断退出时PSP的值会将堆栈中的值取出的。
ADDS R0, R0, #0x20 ;在堆栈取出数据之后,R0自增32,向高地址自增32个字节。
MSR PSP, R0 ; 更新PSP寄存器
ORR LR, LR, #0x04 ; 这条语句是确保在中断退出的时候,会将xPSR,PC,LR,R12,R3-R0这8个寄存器从堆栈中自动恢复过来。LR = LR |0X04将LR寄存器的后两位强行置1,在中断服务程序中LR的用法被重新解释,其值也会被更新成一种特殊的值,称为“EXC_RETURN”,并且在返回时候使用LR,返回后会使用PSP。
CPSIE ;开中断
BX LR ; 将LR(EXC_RETURN )送给PC,CM3会识别,然后执行一系列返回动作。将xPSR,PC,LR,R12,R3-R0这8个寄存器从堆栈中自动恢复过来,然后会更新NVIC寄存器。然后执行就绪任务中最高优先级的任务的任务。
END
2
: OSTaskCreateExt任务创建函数
INT8U OSTaskCreateExt (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos,INT8U prio, INT16U id,OS_STK *pbos,INT32U stk_size, void *pext, INT16U opt)
该函数是OSTaskCreate的加强版本。从函数的参数上就可以看出来。多了INT16U id, OS_STK *pbos, INT32U stk_size, void *pext, INT16U opt这5个参数。
其中pbos是任务堆栈底的地址,stk_size为堆栈的大小。pnext为拓展块的地址,如果使用TCB拓展块,将该拓展块的地址作为参数地址传递给任务创建函数,opt为包含任务的附加信息。
函数的主体部分跟OSTaskCreate函数差别不大,其区别为在使用以下两个函数时传递的参数不一样:
psp = OSTaskStkInit(task, p_arg, ptos, opt); /* Initialize the task's stack */
err = OS_TCBInit(prio, psp, pbos, id, stk_size, pext, opt);
以及在如果配置了堆栈的检查,需要对堆栈进行清空。
#if (OS_TASK_STAT_STK_CHK_EN > 0u)
OS_TaskStkClr(pbos, stk_size, opt); /* Clear the task stack (if needed) */
#endif
该函数的主要功能就是将整个任务对应的栈空间全部清零。这样做是为了符合以后能够进行堆栈检查的需要。