FreeRTOS任务切换详解
概要 :在使用FreeRTOS的时候,我们可以很容易在网上找到移植教程,并成功地运行到我们的单片机上。很多时候可能我们只需要去使用RTOS给到的接口,很少涉及到其运行的原理,本篇就是为了揭开其中最为关键(自认为)的一部分的运行原理,基本上是针对port.c这个文件上的内容进行讲解。
需要理解的前置知识
PenSV和SVC系统服务调用
SVC(Supervisor Call,系统服务调用):可以使单片机进入特权模式创建第一个任务,FreeRTOS v9.0.0 版本之前是使用这个系统服务去创建开始第一个任务,后续版本则都是仅使用PenSV系统服务。
PendSV(Pendable Service Call,可悬起系统调用): 该系统服务可以设置很低的优先级(任务切换,延迟处理事件),确保其他中断处理完成之后才调用该中断,且会自动保存当前寄存器上下文(xPSR、R0-R15)到栈中。
PendSV 异常发生时的寄存器入栈顺序
- xPSR(程序状态寄存器)
- PC(R15)(程序计数器,保存下一条要执行的指令地址)
- LR(R14)(链接寄存器,保存返回地址)
- R12(通用寄存器)
- R3(通用寄存器)
- R2(通用寄存器)
- R1(通用寄存器)
- R0(通用寄存器)
单片机会依次将这些寄存器压入堆栈,而R4-R11则需要我们手动保存,理解这点很重要。
ARM汇编
MRS R0, CPSR ; R0 = CPSR 将特殊寄存器的值传送到通用寄存器
MRS R0, CPSR ; 读取CPSR到R0
ORR R0, R0, #0x80 ; 设置I位(bit7)
MSR CPSR_c, R0 ; 将修改后的值写回CPSR控制域 将通用寄存器的值传送到特殊寄存器
STMIA SP!, {R0-R3} ; [SP] = R0, SP+4; [SP] = R1, SP+4; ... 批量存储寄存器到内存(地址自增)
LDR R0, =0x20000000 ; R0 = 0x20000000
LDR R1, [R0] ; R1 = [0x20000000] 从内存加载数据到寄存器
ADDS R0, R1, R2 ; R0 = R1 + R2 带进位加法并更新标志位
MOVS R0, #0x5 ; R0 = 5, Z=0 (因结果非零) 数据传送并更新标志位
POP {R0-R3, PC} ; 从栈中依次恢复R0-R3,最后恢复PC 从栈顶弹出数据到寄存器
PUSH {R4-R7, LR} ; SP先减4*5=20,再依次存入R4-R7、LR 将寄存器值压入栈顶
源码讲解
新增任务初始化堆栈
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; /* PC */pxTopOfStack--;*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */pxTopOfStack -= 5; /* R12, R3, R2 and R1. */*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */pxTopOfStack -= 8; /* R11..R4. */return pxTopOfStack;}
理解这个函数 是理解整个FreeRTOS任务切换的关键,上面这个函数在调用创建一个新任务的函数(xTaskCreateStatic)会被调用到,篇幅有限具体可看源码,这里对里面的代码逐步进行讲解。以上函数初始化的其实对应的是当单片机触发了PenSV异常后,单片机CPU内寄存器出入栈的动作。
**代码逐行讲解
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
这个操作是将xPSR的bit24位置一,在 ARM Cortex-M 架构中,xPSR 寄存器的 bit24 是 T 位(Thumb 状态位),T=1:表示处理器处于 Thumb 指令集模式(Cortex-M 架构只支持 Thumb 模式,不支持传统的 ARM 32 位指令集)。
*pxTopOfStack = ( StackType_t ) pxCode; /* PC */
这个是将R15 也就是 PC寄存器的值设置为任务函数的入口位置,当触发PenSV出栈的时候会去运行到这个函数(任务)。
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
将R14寄存器(LR)的值填上返回函数,任务函数的返回值,我们都知道FreeRTOS在运行的时候一般不会返回,一般有返回了可能是出了点错误。
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
R0寄存器的值,也就是我们的任务的调用参数。
pxTopOfStack -= 8; /* R11..R4. */
这个是R4-R11寄存器的值,需要我们手动保存,这个会在任务切换里面实现。
开启第一个任务
__asm void prvPortStartFirstTask( void ){extern pxCurrentTCB;PRESERVE8/* The MSP stack is not reset as, unlike on M3/4 parts, there is no vector* table offset register that can be used to locate the initial stack value.* Not all M0 parts have the application vector table at address 0. *//* *INDENT-OFF* */ldr r3, = pxCurrentTCB /* Obtain location of pxCurrentTCB. */ldr r1, [ r3 ]ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */adds r0, # 32 /* Discard everything up to r0. */msr psp, r0 /* This is now the new top of stack to use in the task. */movs r0, # 2 /* Switch to the psp stack. */msr CONTROL, r0isbpop { r0 - r5 } /* Pop the registers that are saved automatically. */mov lr, r5 /* lr is now in r5. */pop { r3 } /* The return address is now in r3. */pop { r2 } /* Pop and discard the XPSR. */cpsie i /* The first task has its context and interrupts can be enabled. */bx r3 /* Finally, jump to the user defined task code. */ALIGN/* *INDENT-ON* */}
上面是FreeRTOS官方源码部分,现在使用的是FreeRTOS V9.0.0版本,所以没有使用SVC进行启动第一个任务。
**代码逐步讲解
ldr r3, =pxCurrentTCB 获取<font color = red>pxCurrentTCB的地址</font> 相当于
&pxCurrentTCB`` 保存到r3
``ldr r1, [r3]
获取pxCurrentTCB的值,保存在r1 里面
ldr r0, [r1] 因为pxCurrentTCB也是指针 所以这一步是获取pxCurrentTCB第一个指向的位置,也就是
volatile StackType_t *pxTopOfStack; ``任务块栈顶的位置
adds r0, #32
msr psp, r0
因为之前在初始化的时候我们手动设置了R4-R11的信息
现在R0的地址加上32个字节,所以R0现在指向的是上面那个位置,然后更新到PSP中去。
movs r0, #2
msr CONTROL, r0
isb
这三步是切换栈指针 将栈指针从MSP切换到PSP,并等待指令完成。
pop {r0-r5}
mov lr, r5
这两步是将刚刚我们获取TCB块指针中的内容弹出,依次对应的是
获取值 | 对应之前压入堆栈的值 |
---|---|
R0 | R0 |
R1 | R1 |
R2 | R2 |
R3 | R3 |
R4 | R12 |
R5 | R14(LR) |
然后又继续将r5的值复制到lr 和之前我们设置的是一致的
pop {r3}
pop {r2}
继续弹出r3 此时对应的是之前 存入TCB块的 R15的值也就是 任务函数入口
R2 对应的是 xPSR的值
cpsie i
bx r3
这两步的作用是 开中断 然后进行跳转,跳转的就是pxCurrentTCB 指向的那个任务函数入口。
PenSV任务切换
__asm void xPortPendSVHandler( void )
{extern vTaskSwitchContextextern pxCurrentTCB/* *INDENT-OFF* */PRESERVE8mrs r0, pspldr r3, = pxCurrentTCB /* Get the location of the current TCB. */ldr r2, [ r3 ]subs r0, # 32 /* Make space for the remaining low registers. */str r0, [ r2 ] /* Save the new top of stack. */stmia r0 !, { r4 - r7 } /* Store the low registers that are not saved automatically. */mov r4, r8 /* Store the high registers. */mov r5, r9mov r6, r10mov r7, r11stmia r0 !, { r4 - r7 }push { r3, r14 }cpsid ibl vTaskSwitchContextcpsie ipop { r2, r3 } /* lr goes in r3. r2 now holds tcb pointer. */ldr r1, [ r2 ]ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */adds r0, # 16 /* Move to the high registers. */ldmia r0 !, { r4 - r7 } /* Pop the high registers. */mov r8, r4mov r9, r5mov r10, r6mov r11, r7msr psp, r0 /* Remember the new top of stack for the task. */subs r0, # 32 /* Go back for the low registers that are not automatically restored. */ldmia r0 !, { r4 - r7 } /* Pop low registers. */bx r3ALIGN/* *INDENT-ON* */}
mrs r0, psp
ldr r3, =pxCurrentTCB
ldr r2, [r3]
将psp 进程栈指针的值存入r0,r2为获取pxCurrentTCB当前任务的栈顶指针
stmia r0!, {r4-r7}
mov r4, r8
mov r5, r9
mov r6, r10
mov r7, r11
stmia r0!, {r4-r7}
这几步是保存上下当前任务的上下文,正如前面提到的r4-r11的值在中断发生的时候不会自动保存,至于为什么不直接使用stmia r0!, {r4-r11},这是因为现在使用的是M0架构的单片机,不允许直接同时操作高低寄存器,r4-r7属于低寄存器 r8-r11是属于高寄存器
push {r3, r14}
cpsid i
bl vTaskSwitchContext
cpsie i
将r3 r14的值压入堆栈 r3是pxCurrentTCB的指针(经过vTaskSwitchContext 函数后会切换到下一个指针 r14就是跳转错误链接)
ldr r1, [r2]
ldr r0, [r1]
adds r0, r0, #16
ldmia r0!, {r4-r7}
mov r8, r4
mov r9, r5
mov r10, r6
mov r11, r7
r0获取任务切换后的pxCurrentTCB栈顶指针,adds r0, r0, #16
,是为了先恢复高寄存器,
msr psp, r0
subs r0, r0, #32
ldmia r0!, {r4-r7}
将r0的值赋值给psp 此时r0指向的是栈顶指针
然后subs r0, r0, #32 是为了恢复低寄存器也就是r4-r7
结尾
一开始学习的时候可能是会有点绕,多看几遍基本上就可以很清晰地了解整个过程了,加油共勉。