FreeRTOS启动第一个任务和任务的切换实现过程
此篇文章主要参考了野火的《FreeRTOS内核实现与应用开发指南》,和其他博主的一些资料并加入了一些个人理解,作为学习笔记,在此感谢火哥和其他博主;
第一个任务的启动
vPortSVCHandler函数开始真正启动第一个任务;
上代码:
__asm void vPortSVCHandler( void )
{
extern pxCurrentTCB; (1)
PRESERVE8
ldr r3, =pxCurrentTCB (2)
ldr r1, [r3] (3)
ldr r0, [r1] (4)
ldmia r0!, {r4-r11} (5)
msr psp, r0 (6)
isb
mov r0, #0 (7)
msr basepri, r0 (8)
orr r14, #0xd (9)
bx r14 (10)
}
(1) 是声明外部变量pxCurrentTCB,pxCurrentTCB是一个全局变量指针,用来指向当前正在运行或者即将运行任务的任务控制块。
(2) 加载pxCurrentTCB任务控制块的地址到R3中;
(3) 加载pxCurrentTCB到R1
(4) 加载pxCurrentTCB指向的任务控制块到R0中,因为任务控制块中的第一个成员变量就是任务的栈顶指针: pxTopOfStack,所以,R0就等于下图中的pxTopOfStack。
:-:
图1任务栈初始化完成后的栈空间分布图
(5) 以r0为基地址(指针先加后操作)将栈中的8个字的数据加载到CPU中的R4-R11中。这时候的R0中的地址见图2;
(6) 将操作5后新的栈顶指针R0更新到PSP中(注意:在执行异常的时候,SP以MSP为栈指针,在执行任务时SP以PSP为栈指针);
(7) 执行7之前清流水线,确保操作6指令执行完毕。清除R0;
(8) 设置BASEPR寄存器为0 即打开所有中断;
(9) (10)两个操作具体解释如下
当r14为0xFFFFFFFX,执行是中断返回指令,
cortext-m3的做法,X的bit0为1表示返回thumb状态,bit1和bit2分别表示返回后sp用msp还是psp、以及返回到特权模式还是用户模式;
异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)同时PSP的值也将更新,即指向任务栈的栈顶 。
0x0D 表示返回后为thumb状态,且指定SP出栈指针使用PSP作为出栈指针,这里的栈指针指向的准备运行的栈,所有在执行BX LR后,在恢复现场时会以当前的PSP为基地址,把栈中的剩下内容将会自动加载到CPU寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)中,又因为跳转是BX 无链接,所以跳转后的R15 PC会被从新的PC从堆栈中恢复到R15(PC)寄存器中,所以程序会从该新任务入口函数开始执行。
(注意:)发生异常跳转到异常处理服务前,自动执行的现场保护会保留返回模式。
图2第一个任务启动后PSP的指向位置
任务的切换
在FreeROTS任务的切换实际由xPortPendSVHandler函数完成,主要完成上下文切换即保存上文,切换下文两个主要目的操作;
切换前我们需要了解上文需要保存什么,下文切换的又是什么;
上文中保存的主要内容是:
(1).寄存器中的R4-R11数据(其余的进入异常前CPU自动保存);
(2).当前任务的栈顶指针;
(3).全局TCB地址;
下文切换需要切换的内容主要是:
(1).最新任务的TCB;
(2).最新任务中栈中的r4-r11数据到CPU R4-R11;
(3)新任务栈顶存入PSP,用来出栈根据PSP调用新任务;
主要流程如下图所示:
上代码:
__asm void xPortPendSVHandler( void )
{
extern pxCurrentTCB; (1)
extern vTaskSwitchContext; (2)
PRESERVE8 (3)
mrs r0, psp (4)
isb
ldr r3, =pxCurrentTCB (5)
ldr r2, [r3] (6)
stmdb r0!, {r4-r11} (7)
str r0, [r2] (8)
stmdb sp!, {r3, r14} (9)
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY (10)
msr basepri, r0 (11)
dsb
isb
bl vTaskSwitchContext (12)
mov r0, #0 (13)
msr basepri, r0
ldmia sp!, {r3, r14} (14)
ldr r1, [r3] (15)
ldr r0, [r1] (16)
ldmia r0!, {r4-r11} (17)
msr psp, r0 (18)
isb
bx r14 (19)
nop
}
说明;
上文的保存
注意:在进入该函数前,系统(CPU)会自动的将上一个任务的运行的环境即: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)这些寄存器的值会存储到任务的栈中,是自动完成的。
(4) 进入到xPortPendSVHandler函数中第一步要做的是将剩下的 R4~R11手动保存入栈,同时PSP 会自动更新(在更新之前 PSP 指向任务栈的栈顶)。
这时当前PSP指向见图3
图3上文环境自动保存到任务栈后PSP的指向
(5)将pxCurrentTCB的地址加载到R3中;
(6)将R3中指向的内容加载到R2中R2 =pxCurrentTCB ;
(7)先将R0指向地址递减在以新R0为及地址将CPU中的R4-R11手动入栈,这时的R0的具体指向见图4;
图4上一个任务的运行环境手动入栈后,R0的指向
(8)将R0的值存储到R2指向的内容,因为R2等于pxCurrentTCB,而pxCurrentTCB的第一个成员为pxTopOfStack,所以就是将R0存储到的上一个任务的pxTopOfStack任务栈顶指针中。完成了上文的保存;
切换下文
(9)把R14(LR)寄存器入栈保存,是因为调用vTaskSwitchContext函数返回时,CPU会把返回地址自动保存到LR寄存器中,R14(LR)寄存器的值被覆盖故需要进入入栈保护处理。把R3一并入栈保存是因为下面要调用vTaskSwitchContext函数来切换任务控制块,可能会用到R3,把R3的值覆盖,而我们还需要原R3来找到当操作新的pxCurrentTCB,所以保险起见一并把R3也保存了起来;
(10)把configMAX_SYSCALL_INTERRUPT_PRIORITY值存到R0中,用来屏蔽BASEPRI的值;
(11)关闭部分高优先级中断;
(12)调用vTaskSwitchContext函数用来更新下一个需要运行任务的任务控制块pxCurrentTCB;
(13)退出临界段,开中断,直接往 BASEPRI写 0;
(14)从堆栈中恢复R3,R14的值,此时SP使用的时MSP;
(15)将将要运行任务的任务控制块pxCurrentTCB的地址指向的内容即任务控制块本身加载到R1中;
(16)加载R1中指向的内容即将要运行的任务的栈顶指针到R0中;
(17)把R0为基地址,将下一个将要运行任务的栈加载到R4-R11寄存器中,此时的R0的指向已经变化;
(18)将R0的值更新到PSP中,用来退出异常后以PSP为及地址,将新任务栈中剩下的内容加载到CPU寄存器中,然后清流水线,保证此操作完成后在执行下一条指令。
(19) 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回, 然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址及参数r0,r1,r2,r3,r12,r14,r15,xpsr自动加载到R0,R1,R2,R3,R12,R14,R15(PC)以及xPSR寄存器后,CPU根据被出栈到PC寄存器的值,执行新的任务指令。
刚入门FreeRTOS系统实现,此文章叙述任务的调度,后期把任务的创建机制及管理方式实现补上来。看了几天比较吃力,可能以上理解稍有偏差,仅供个人笔记和学习参考使用。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)