一.前言
之前移植过freertos操作系统,涉及到计算机和操作系统的底层,特此详细记录下这些知识点。至于具体的详细步骤,就不给出了,网上有很多参考,这里只分析“重点”。笔者的cpu内核是cotex-M3.
二.3个重点函数
- vPortSVCHandler():加载第一个任务的中断处理函数。
- xPortPendSVHandler():实现任务切换的中断处理函数(保存和恢复任务的上下文)。
- xPortSysTickHandler():给操作系统提供滴答时钟。
三.vPortSVCHandler
FreeRTOS启动调度器时,调用prvStartFirstTask,再调用SVC中断来启动第一个任务
点击查看代码
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08 /* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,里面存放的是向量表的起始地址,即MSP的地址 */
ldr r0, [r0]
ldr r0, [r0]
/* Set the msp back to the start of the stack. */
msr msp, r0 /* 设置主堆栈指针msp的值 *
/* Globally enable interrupts. *//* 使能全局中断 */
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. *//* 调用SVC去启动第一个任务 */
svc 0/*产生系统调用,服务号 0表示 SVC 中断,接下来将会执行 SVC 中断服务函数*/
nop
nop
}
SVC中断被触发,PortSVCHandler加载第一个任务
点击查看代码
PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;/* 当前正在运行的任务的任务控制块指针,默认初始化为NULL,defined in task.c*/
__asm void vPortSVCHandler( void )
{
PRESERVE8
ldr r3, =pxCurrentTCB //r3=&pxCurrentTCB,即r3指向当前执行任务的TCB指针所在地址
ldr r1, [r3] //r1=*r3=pxCurrentTCB,既让r1指向当前任务的TCB
ldr r0, [r1] //r0=*r1=pxTopOfStack,即让r0执行当前任务栈顶
/* Pop the core registers. */ //将当前任务栈内容pop,保存入cpu寄存器,注意序号小的寄存器会先被pop,所以pop顺序:r4...r10,r11
ldmia r0!, {r4-r11} /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
msr psp, r0 /* Restore the task stack pointer. *// / 将当前任务栈顶赋给psp, 即psp = pxTopOfStack,执行后效果如图1所示
isb // 指令同步隔离,确保之前的指令都已执行完毕
mov r0, #0 // r0清0,用于关中断
msr basepri, r0 //设置 basepri 寄存器的值为 0,即关闭所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽,但如果设置成0,则不关闭任何中断
orr r14, #0xd
bx r14 ///任务上下文加载完毕,中断执行结束,返回用户线程,在ARM中,使用r14(LR寄存器)来保存子程序的返回地址(即上一个程序的地址)
}
四.xPortPendSVHandler
任务调度中断,被切换出去的任务,一部分参数由硬件自动保存,一部分手动保存
点击查看代码
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
/*r0=psp, 进入PendSV中断时,上个任务环境即
xPSR,PC,R14,R12,R3,R2,R1,R0这些将自动保存入任务栈,
剩下R4-R11需要手动保存,同时PSP将自动更新(在更新之前 PSP 指向任务栈的栈顶),
此时 PSP是"上文"任务的堆栈指针,具体指向见图3*/
mrs r0, psp
isb //确保之前指令已执行(为什么mrs或者msr执行完之后就要接一个isb或者dsb?不知道)
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB //r3=&pxCurrentTCB
ldr r2, [r3] //r2=*r3=pxCurrentTCB
/* Save the core registers. */
stmdb r0!, {r4-r11} //将cpu寄存器保存入"上文"任务栈,注意push总是先push序号大的,因此push顺序:r11,r10....r4
/* Save the new top of stack into the first member of the TCB. */
str r0, [r2] //*r2=r0 => pxTopOfStack=p0, 更新"上文"任务的栈顶
stmdb sp!, {r0, r3} //入栈栈顶指针和pxCurrentTCB,这个栈的指针是MSP,注意顺序:r3,r0
/* 至此,上下文切换的"上文"环境保存完成 */
//关中断,高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都将被屏蔽,configMAX_SYSCALL_INTERRUPT_PRIORITY的值在FreeRTOSConfig.h定义
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb //数据隔离,同步之前对msr的操作
isb //指令隔离,确保之前所有指令已执行完毕,之后的指令使用的是正确的basepri配置
bl vTaskSwitchContext //跳转到vTaskSwitchContext函数去执行,pxCurrentTCB将被更改指向下一个任务
//开中断
mov r0, #0
msr basepri, r0
ldmia sp!, {r0, r3} //从MSP栈加载r0和r3,此时r3已经指向新任务pxCurrentTCB的地址值,注意pop顺序:r0,r3
/* 以下为上下文切换的"下文"环境切换 */
/* The first item in pxCurrentTCB is the task top of stack. */
ldr r1, [r3] //r1=*r3=pxCurrentTCB,即新任务的TCB
ldr r0, [r1] //r0=*r1=pxTopOfStack,即新任务的栈顶指针
/* Pop the core registers. */
ldmia r0!, {r4-r11} //将新任务的任务栈数据加载入cpu寄存器r4-r11
/* 更新psp的值,等PendSV退出时,会以psp作为基地址,将任务栈中剩下的内容自动加载到CPU寄存器 */
/* 剩下的内容包括: xPSR、PC、LR、r12、r3、r2、r1、r0 */
msr psp, r0
isb
bx r14 //中断结束返回
}
五.xPortSysTickHandler
xPortSysTickHandler是一个定时中断服务函数,默认为1ms触发一次。在FreeRTOS中,它被用作触发PendSV中断,实际的任务切换在PendSV_Handler函数中执行。
移植参考如下:
点击查看代码
void SysTick_Handler(void)
{
lv_tick_inc(1);
swm_inctick();
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
}