多任务调度
1. 多任务启动流程
多任务启动流程如下表所示
启动后以下各函数由上至下依次执行 | 含义 |
---|---|
osKernelStart() | 启动内核 |
vTaskStartScheduler() | 启动任务调度器 |
xPortStartScheduler() | 启动调度器 |
prvStartFirstTask() | 启动第一个任务 |
SVC | 调用SVC中断 |
2. 源码分析
- 启动任务调度器
void vTaskStartScheduler( void ){
BaseType_t xReturn;
/* Add the idle task at the lowest priority. */
#if(configSUPPORT_STATIC_ALLOCATION == 1){
}
#else{
/* 动态创建空闲任务 */
xReturn = xTaskCreate(prvIdleTask,
"IDLE", configMINIMAL_STACK_SIZE,
(void *) NULL,
(tskIDLE_PRIORITY|portPRIVILEGE_BIT),
&xIdleTaskHandle);
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
if(xReturn == pdPASS){
/* 关闭中断 */
portDISABLE_INTERRUPTS();
/* 下一个任务锁定时间赋值为最大,其实就是时间片调度,不让其进行调度 */
#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
xNextTaskUnblockTime = portMAX_DELAY;
/* 调度器的运行状态置位,标记开始运行了 */
xSchedulerRunning = pdTRUE;
/* 初始化系统的节拍值为0 */
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if(xPortStartScheduler() != pdFALSE){
//如果调度器启动成功就不会执行到这里,所以没有代码
}
else{
//不会执行到这里,所以没有代码
}
}
else{
//运行到这里说明系统内核没有启动成功,空闲任务创建失败
}
}
- 启动调度器:FreeRTOS系统时钟是由滴答定时器来提供,任务切换也会用到PendSV中断,这些硬件的初始化在这里完成
BaseType_t xPortStartScheduler( void ){
/* 为了保证系统的实时性,配置systick和pendsv为最低的优先级 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 配置滴答定时器systick的定时周期,并开启systick中断 */
vPortSetupTimerInterrupt();
/* 初始化临界段嵌套计数器 */
uxCriticalNesting = 0;
/* 启动第一个任务 */
prvStartFirstTask();
/* 代码正常执行的话不会到这里! */
return 0;
}
- 启动第一个任务:用于启动第一个任务,是一个汇编函数
__asm void prvStartFirstTask( void ){
PRESERVE8 //8字节对齐,AAPCS的标准,ARM特有
/* 将0xE000ED08保存在寄存器R0中;它是中断向量表的一个地址,
存储的是MSP的指针,最终获取到MSP的RAM的地址 */
ldr r0, =0xE000ED08
ldr r0, [r0] //取R0保存的地址处的值赋给R0
ldr r0, [r0] //获取MSP初始值
/* 重新把MSP的地址,赋值为MSP,相当于复位MSP */
msr msp, r0
/* 开启全局中断 */
cpsie i //使能中断
cpsie f //使能中断
dsb //数据同步屏障
isb //指令同步屏障
/* 调用SVC */
svc 0
nop
nop
}
- 调用SVC中断:在prvStartFirstTask()中通过调用SVC指令触发了SVC中断,而第一个任务的启动就是在SVC中断服务函数中完成的
__asm void vPortSVCHandler(void){
PRESERVE8//8字节对齐
/* 获取当前任务控制块 */
ldr r3, =pxCurrentTCB
ldr r1, [r3] //
ldr r0, [r1] //
/* 出栈内核寄存器,R14其实就是异常返回值 */
ldmia r0!, {
r4-r11, r14}
/* 进程栈指针PSP设置为任务的堆栈 */
msr psp, r0
isb //指令同步屏障
/* 把basepri赋值为0,即打开屏蔽中断 */
mov r0, #0
msr basepri, r0
/* 异常退出 */
bx r14
}
3. 任务切换
3.1 任务切换场合
RTOS系统的核心是任务管理,而任务管理的核心是任务切换,任务切换决定了任务的执行顺序。上下文(任务)切换被触发的场合可以是
- 系统滴答定时器(SysTick)中断
- 执行一个系统调用
典型的嵌入式OS系统中,处理器被划分为多个时间片。若系统中只有两个任务,这两个任务会交替执行,任务切换都是在SysTick中断中执行,如下图示:
在一些OS设计中,为了解决SysTick和IRQ的冲突问题,PendSV异常将上下文切换请求延迟到所有其他IRQ处理都已经完成后,在PendSV异常内执行上下文切换。如下图示:
PendSV(可挂起的系统调用)异常对OS操作非常重要,其优先级可通过编程设置。可通过将中断控制和状态寄存器ICSR的bit28(挂起位)置1来触发PendSV中断。上面提到过上下文切换被触发的两个场合:SysTick中断和执行一个系统调用,其源码分析如下:
- SysTick中断
//滴答定时器中断服务函数
void SysTick_Handler(void){
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED){
//系统已经运行
xPortSysTickHandler();
}
}
void xPortSysTickHandler( void ){
vPortRaiseBASEPRI(); //关闭中断
{
if( xTaskIncrementTick() != pdFALSE ){
//增加时钟计数器xTickCount的值
/* 通过向中断控制和状态寄存器的bit28位写入1挂起PendSV来启动PendSV中断 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR(); //打开中断
}
- 执行一个系统调用
//以任务切换函数taskYIELD()为例
#define taskYIELD() portYIELD()
#define portYIELD()
{
/* 通过向中断控制和状态寄存器的bit28位写入1挂起PendSV来启动PendSV中断 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
}
3.2 PendSV中断服务函数
FreeRTOS任务切换的具体过程是在PendSV中断服务函数中完成的,下面分析PendSV中断服务函数源码,看看切换过程是如何进行的
__asm void xPortPendSVHandler( void ){
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp
isb
/* 获取当前任务控制块,其实就获取任务栈顶 */
ldr r3, =pxCurrentTCB
ldr r2, [r3]
/* 浮点数处理,如果使能浮点数,就需要入栈 */
tst r14, #0x10
it eq
vstmdbeq r0!, {
s16-s31}
/* 保存内核寄存器---调用者需要做的 */
stmdb r0!, {
r4-r11, r14}
/* 保存当前任务栈顶,把栈顶指针入栈 */
str r0, [r2]
stmdb sp!, {
r3}
/* 使能可屏蔽的中断-----临界段 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/* 执行上线文切换 */
bl vTaskSwitchContext
/* 使能可屏蔽的中断 */
mov r0, #0
msr basepri, r0
/* 恢复任务控制块指向的栈顶 */
ldmia sp!, {
r3}
/* 获取当前栈顶 */
ldr r1, [r3]
ldr r0, [r1]
/* 出栈*/
ldmia r0!, {
r4-r11, r14}
/* 出栈*/
tst r14, #0x10
it eq
vldmiaeq r0!, {
s16-s31}
/* 更新PSP指针 */
msr psp, r0
isb
/* 异常返回,下面要执行的代码,就是要切换的任务代码了 */
bx r14
nop
nop
}
在PendSV中断服务函数中有调用函数vTaskSwitchContext来获取下一个要运行的任务,其源码如下
void vTaskSwitchContext( void ){
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){
/* 标记调度器状态*/
xYieldPending = pdTRUE;
}
else{
/* 标记调度器状态*/
xYieldPending = pdFALSE;
/* 检查任务栈是否溢出 */
taskCHECK_FOR_STACK_OVERFLOW();
/* 选择优先级最高的任务,把当前的任务控制块进行赋值 */
taskSELECT_HIGHEST_PRIORITY_TASK();
}
}