1.前后台系统与多任务系统
熟悉嵌入式开发的同学都知道,一般没有操作系统的程序都是在main函数有一个死循环来完成相关任务,一些紧急的操作放在中断里来完成,通常称作前后台系统,如下图所示:
对于业务逻辑简单的程序,这么做没什么不好的。但是代码复杂后,很多个中断包含嵌套中断会使复杂性急剧膨胀,中断间的交互将会变得十分困难,可维护性差,增加一个新功能对代码的改动较大,如果中断函数执行时间太长,同级中断将会受到影响。所以为了减少复杂性,通过在中断里置标志位,在主循环里查询,但是这样查询又是按照顺序来的,后面的任务实时性将会降低,而且如果每个任务的执行周期不一样,又会额外增加很多工作量。另外,这种系统的应用层代码和硬件代码的耦合性较高也带来了移植的困难。
实时操作系统(RTOS)正是为了解决前后台系统应对复杂应用的不足,通过芯片内核的软件中断来实现多任务系统。多任务系统如下图所示
每个模块的任务保持独立,任务可以自行设置执行周期而不受其他任务的影响。高优先级的任务能够抢占低优先级的任务,保证了系统的实时性,当任务优先级相同的时候还可以按照时间片的方式运行,看起来就像在同时运行多个任务一样。任务由系统调度器统一管理,通过信号量、消息、事件标志等机制使任务和任务、任务和中断的交互变得更加容易,但又保持了各模块的独立性,总之引入实时操作系统使程序处理复杂逻辑业务变得更加容易,很好地隔离了应用层和硬件驱动层,极大地提高了系统的可扩展性。
接下来就来介绍FreeRTOS和µC/OS-III是如何管理任务运行的,主要从任务启动和任务切换2个方面来分析。
2.FreeRTOS的任务启动和切换
2.1堆栈初始化
每一个任务都会有一个任务控制块(TCB),TCB是一个非常复杂的结构体,包含了大量任务的重要信息如任务堆栈、任务运行状态、任务优先级等等,它与任务的调度息息相关,这里我们不细讲。
通过调用xTaskCreate()函数来创建一个任务,来初始化TCB相关信息,这里我们需要知道TCB的第一个变量为堆栈的栈顶指针,在任务启动和切换时会用到,新建任务时会初始化堆栈,要注意堆栈的偏移地址与Cortex-M3的内核寄存器存在对应关系,之后在任务启动后会与Cortex-M3的线程堆栈指针挂接在一起,任务切换后堆栈的内容会pop到内核寄存器,CPU从而开始执行任务代码。
//更新任务控制块的栈顶指针
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
//初始化堆栈
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
interrupt. */
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
入栈时堆栈地址从高地址到低地址增长,xPSR、PC、LR、R12、R3~R0这8个寄存器会由CPU自动入栈,入栈顺序如下图所示:
其他寄存器R11~R4需要手工入栈
最后在FreeRTOS里通过全局变量pxCurrentTCB指向当前优先级最高的第一个就绪任务控制块,任务切换时只需改变pxCurrentTCB的指向即可
2.2任务启动
通过调用vTaskStartScheduler()来启动任务,此时会创建一个空闲任务
/* The Idle task is being created using dynamically allocated RAM. */
xReturn = xTaskCreate( prvIdleTask, "IDLE", configMINIMAL_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
之后根据不同的硬件平台会调用xPortStartScheduler()来启动任务,此时会先设置系统定时器周期,再调用vPortStartFirstTask()来启动第一个任务,这是一个汇编函数
vPortStartFirstTask
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08 ;中断向量表的第一个地址
ldr r0, [r0] ;获取第一个中断地址
ldr r0, [r0] ;获取栈顶地址
/* Set the msp back to the start of the stack. */
msr msp, r0
/* Call SVC to start the first task, ensuring interrupts are enabled. */
cpsie i ;开中断
cpsie f ;开异常
dsb ;数据隔离
isb ;指令隔离,保证前面的指令先执行完
svc 0 ;跳转到SVC中断
END
进到这里之后,以后不会再返回主循环了,所以代码先复位主堆栈指针,然后跳转到SVC中断
vPortSVCHandler:
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1] ;获取栈顶地址
/* Pop the core registers. */
ldmia r0!, {r4-r11} ;手工将寄存器r4-r11出栈
/*让线程堆栈指针PSP指向pxCurrentTCB->pxTopOfStack
此后上面提到的8个寄存器就会由M3内核自动出栈*/
msr psp, r0
isb
mov r0, #0
msr basepri, r0 ;不屏蔽任何中断
orr r14, r14, #13
bx r14 ;跳转到任务代码,任务地址将会从PSP自动出栈到PC指针
上面有一句话是orr r14, r14, #13表示返回线程模式,使用的是线程堆栈PSP,没有这一句则会使用主堆栈MSP,r14即为LR寄存器,在调用子函数时会保存返回地址,而在进入中断时只有3个合法值,这个值是CPU自动设置的,意义如下
0xFFFFFFF1
表示中断返回时从MSP堆栈恢复寄存器值,中断返回后进入Handler模式,使用MSP堆栈,(相当于从中断返回到另一个中断)。0xFFFFFFF9
表示中断返回时从MSP堆栈恢复寄存器值,中断返回后进入线程模式,使用MSP堆栈(这种用于不使用PSP只使用MSP堆栈的情况)。0xFFFFFFFD
表示中断返回时从PSP堆栈恢复寄存器值,中断返回后进入线程模式,使用PSP堆栈(这是常见的,OS处理完中断后返回用户程序)。
这里需要返回任务代码,并使用PSP堆栈,所以使用0xFFFFFFFD作为返回值
2.3任务切换
任务切换和任务启动的代码其实是类似的,只不过多了2个步骤:
- 在当前任务堆栈中,手动将R4~R11寄存器入栈
- 调用vTaskSwitchContext寻找当前最高优先级任务TCB,并保存在pxCurrentTCB
切换操作在xPortPendSVHandler中断里进行
xPortPendSVHandler:
mrs r0, psp
isb
ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
ldr r2, [r3]
stmdb r0!, {r4-r11} /* Save the remaining registers. */
str r0, [r2] /* Save the new top of stack into the first member of the TCB. */
stmdb sp!, {r3, r14}
/*关中断,防止全局变量pxCurrentTCB被外部修改*/
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}
ldr r1, [r3]
ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0!, {r4-r11} /* Pop the registers. */
/*将新的堆栈挂接到PSP,PC等8个寄存器会自动出栈,跳转到新任务代码执行任务*/
msr psp, r0
isb
bx r14
在系统心跳或发送信号量时都会触发一次请求任务切换,即进入xPortPendSVHandler中断,都会调用portYIELD,这个函数直接修改寄存器的相应bit来进入中断
/* Scheduler utilities. */
#define portYIELD() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__DSB(); \
__ISB(); \
}
3.µC/OS-III的任务启动和切换
同样的µC/OS-III也有一个任务TCB,TCB的第一个成员就是任务堆栈指针,新建任务也要初始化任务堆栈
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; /* Prevent compiler warning */
p_stk = &p_stk_base[stk_size]; /* Load stack pointer */
/* Registers stacked as if auto-saved on exception */
*--p_stk = (CPU_STK)0x01000000u; /* xPSR */
*--p_stk = (CPU_STK)p_task; /* Entry Point */
*--p_stk = (CPU_STK)OS_TaskReturn; /* R14 (LR) */
*--p_stk = (CPU_STK)0x12121212u; /* R12 */
*--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 */
/* Remaining registers saved on process stack */
*--p_stk = (CPU_STK)0x11111111u; /* R11 */
*--p_stk = (CPU_STK)0x10101010u; /* R10 */
*--p_stk = (CPU_STK)0x09090909u; /* R9 */
*--p_stk = (CPU_STK)0x08080808u; /* R8 */
*--p_stk = (CPU_STK)0x07070707u; /* R7 */
*--p_stk = (CPU_STK)0x06060606u; /* R6 */
*--p_stk = (CPU_STK)0x05050505u; /* R5 */
*--p_stk = (CPU_STK)0x04040404u; /* R4 */
return (p_stk);
}
在启动第一个任务前会调用OSStart()启动调度器,在这里会调用 OSStartHighRdy()来启动第一个任务,这个函数是用汇编写的
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority
LDR R1, =NVIC_PENDSV_PRI ;设置最低优先级中断
STRB R1, [R0]
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call
MSR PSP, R0
LDR R0, =OS_CPU_ExceptStkBase ; Initialize the MSP to the OS_CPU_ExceptStkBase
LDR R1, [R0] ;设置主堆栈指针为OS_CPU_ExceptStkBase
MSR MSP, R1
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET ;跳转到OS_CPU_PendSVHandr中断
STR R1, [R0]
CPSIE I ; Enable interrupts at processor level
OSStartHang
B OSStartHang ; Should never get here
由代码知道,首先设置OS_CPU_PendSVHandr中断为最低优先级,并初始PSP指针为0,表示这是第一次中断,不同于FreeRTOS,µC/OS-III并没有使用SVC中断,第一次启动任务和任务切换使用同一个中断。之后把主堆栈指针设为指定值,而FreeRTOS中是设为复位值。接着就进入OS_CPU_PendSVHandr切换任务,代码如下:
OS_CPU_PendSVHandler
CPSID I ; Prevent interruption during context switch
MRS R0, PSP ; PSP is process stack pointer
CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11}
LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosave
PUSH {R14} ; Save LR exc_return value
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R1, =OSTCBHighRdyPtr
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
ADDS R0, R0, #0x20
MSR PSP, R0 ; Load PSP with new process SP
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR ; Exception return will restore remaining context
END
如果PSP为0则表示是第一次启动任务,则直接进入OS_CPU_PendSVHandler_nosave切换任务,否则需要事先把当前的寄存器R4~R11手工压入当前任务堆栈,再进行切换。
在切换前会调用OSTaskSwHook对原来的任务做一些统计工作,之后就会更新OSPrioCur为新任务优先级,OSTCBCurPtr为新任务TCB,最后将新任务堆栈出栈到R4-R11寄存器,剩余的寄存器在绑定PSP后由M3内核自动出栈。
µC/OS-III中通过调用OS_TASK_SW() 或OSIntCtxSw()触发OS_CPU_PendSVHandler来切换任务,在此前已经获取最高优先级的就绪任务,所以在切换时直接把OSTCBCurPtr更新为OSTCBHighRdyPtr即可
void OSSched (void)
{
......
CPU_INT_DIS();
//此处获取最高优先级就绪任务
OSPrioHighRdy = OS_PrioGetHighest(); /* Find the highest priority ready */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* Current task is still highest priority task? */
CPU_INT_EN(); /* Yes ... no need to context switch */
return;
}
......
//此处触发切换中断
OS_TASK_SW(); /* Perform a task level context switch */
CPU_INT_EN();
}