FreeRTOS任务切换涉及到芯片架构以及汇编代码,因此 这里将使用Contex_M3为例子,这里将从contex_M3寄存器组,汇编处理a=a+b流程,基本的汇编语句,PendSV,PendSV中断 源码,查找最高优先级任务共6个部分来介绍FreeRTOS的任务切换。
contex_M3寄存器组
如我们所见,CM3 拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器。R0‐R12 是最“通用目的”的,但是绝大多数的 16 位指令只能使用 R0‐R7(低组寄存器),而 32 位的 Thumb‐2指令则可以访问所有通用寄存器。特殊功能寄存器有预定义的功能,而且必须通过专用的指令来访问。
- 堆栈指针 R13
CM3拥有两个堆栈指针,分别是MSP(主堆栈指针),PSP(进程堆栈指针),MSP主要用于中断,异常和mian函数;PSP主要用于我们自己建立的任务使用,我们每次调用xTaskCreate函数时,都会传入一个申请空间大小,这里的大小就是任务栈大小,每一个任务都有自己独享的任务栈,每次进行任务切换得 时候,都会当前任务使用到的个R0-R15寄存器的值存入当前的任务栈。 - 连接寄存器 R14
连接寄存器 R14一般有两个用途:一是用来保存子程序返回地址;二是当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。
汇编处理a=a+b流程
Contex_M3本身使用精简指令,ARM指令有如下特点:
- 对内存只有读写指令,
- 对数据运算都是在CPU内部实现
- 精简指令的CPU比较容易设计
上图所示为c语言代码 a= a + b转换为汇编的情况,第1,2,4均为读写内存指令。
基本汇编语句
左对齐 | 右对齐 | 居中对齐 |
---|---|---|
mrs r0, psp | 读取特殊寄存器指令到寄存器 | 读取PSP寄存器的值到R0 |
msr psp, r0 | 写寄存器到特殊寄存器指令 | 写R0寄存器到PSP |
ldr r3, =pxCurrentTCB | 读取 pxCurrentTCB的地址到R3 | 类似c语言 R3 = &pxCurrentTCB |
ldr r3, [r2] | 对R2指向的地址取值 | 类似c语言 R3 = *R2 |
stmdb r0!, {r4-r11} | 压入堆栈 | 将R4-R11的值拷贝到R0的地址里面 |
ldmia sp!, {r3, r14} | 压出堆栈 | 将默认堆栈指针的数据拷贝到R3-R14寄存器 |
bl vTaskSwitchContext | 跳转指令 | |
mov r0, #0 | 写入立即数0 | 类似c语言包R0 = 0 |
PendSV中断
FreeRTOS任务切换是在PendSV里面执行的,PendSV主要特点是它是可以像普通的中断一样被悬起的。OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。悬 起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果PendSV优先级不够高,则将缓期等待执行。利用 PendSV执行上下文切换如下:
触发PendSV中断一般两种情况:
- sysTick到了任务切换得时间点,在sysTick中断中触发PendSV中断。
- 在函数中进行触发PendSV中断。
PendSV源码分析
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
//告诉编译器以精简指令运行
PRESERVE8
//读取任务栈指针到r0
mrs r0, psp
isb
//获取当前任务控制块的指针
ldr r3, =pxCurrentTCB
//获取当前任务控制块的指针 指向的第一个元素 及pxTopOfStack的值
ldr r2, [r3]
//将寄存器R3-R11的值压入当前任务栈
stmdb r0!, { r4-r11}
//将新的任务栈亚地址存入 当前TCB第一个地址
str r0, [r2]
//将r3 和r14压入主堆栈
stmdb sp!, { r3, r14}
//将configMAX_SYSCALL_INTERRUPT_PRIORITY写入r0
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
//将r0的值写入basepri 进入临界区
msr basepri, r0
dsb
isb
//跳转查找优先级最高的任务
bl vTaskSwitchContext
//r0写入立即数0
mov r0, #0
//将r0的0写入basepri 退出临界区
msr basepri, r0
//将r3和r14压出堆栈
ldmia sp!, { r3, r14}
//r3存入的是pxCurrentTCB 的指针地址 刚才已经查找到新的pxCurrentTCB ,此时将pxCurrentTCB 读入r1
ldr r1, [r3]
//得到新的TCB的第一个元素 及pxTopOfStack的 栈地址
ldr r0, [r1]
//出栈到新的任务栈
ldmia r0!, { r4-r11}
//将新的任务栈地址 写入PSP寄存器
msr psp, r0
isb
//退出挡墙中断
bx r14
nop
}
上面代码为一个任务切换过程,从中可以发现,只进行了R4-R11寄存器的压栈处理,并没有进行R0-R13压栈处理,那是因为CM3内核在进入或者退出中断时会自动将R0-R3 R12 LR PC等寄存器压入堆栈,然后在退出中断 自动恢复。
查找最高优先级任务
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
taskCHECK_FOR_STACK_OVERFLOW();
//查找最高优先级任务
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
}
}
#define taskSELECT_HIGHEST_PRIORITY_TASK() \ { \ UBaseType_t uxTopPriority; \ \ \
//查找当前最有就绪态的高优先级
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
//得到一个最高优先级下挂载的就绪任务
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \ { \ List_t * const pxConstList = ( pxList ); \ \
//得到上一个任务的链表节点 \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
}
//返回下一个链表的TCB \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
从上可至,每一个优先级下面有一个就绪态任务TCB一个链表,每次切换任务都会取一个任务的指针,如果同一个优先级下有多个就绪态的任务,则会依次调用。