FreeRTOS调度器

产生调度的方法
FreeRTSO的任务调度只做一件事,就是在系统被要求任务切换时找到在就绪表中优先级最高的任务并将CPU执行权切给它。
有两种方法触发任务切换:执行系统呼叫taskYIELD(),以及系统时钟节拍。
1.执行系统呼叫

define taskYIELD portYIELD

define portYIELD \

{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
__DSB();
__ISB();
}

define portNVIC_INT_CTRL_REG (*((volatile uint32_t *)0xE000ED04))

define portNVIC_PENDSVSET_BIT (1UL << 28UL)

以上宏定义一看就很清晰了,通过写PendSV手动触发系统调用进入PendSV中断

2.系统时钟节拍
void SysTick_Handler(void)
{
portDISABLE_INTERRUPTS();
{
if(xTaskIncrementTick != pdFALSE)
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}

启动调度:
当任务创建好之后,调度器不会自己工作,需要调用系统接口(Task级)。在其中会创建一个空闲任务,主要工作是回收资源。
1.当创建好任务后,程序调用启动调度接口:vTaskStartScheduler();
创建空闲任务:xTaskCreate(prvIdleTask, “IDLE”, configMINIMAL_STACK_SIZE, (void *)NULL, (tskIDLE_PRIORITY | portPRIVILEGE_BIT), &xIdleTaskHandle);
如开启软定时器功能,则创建一个定时器任务:xTimerCreateTimerTask();
进入临界关中断
设置调度器运行:xSchedulerRunning = pdTRUE;
设置Tick计数器初值:xTickCount = 0;
调用启动调度接口(Port级):xPortStartScheduler(),跟CPU架构有关
配置PendSV和SYSTICK的中断优先级
配置和使能Systick
设置临界嵌套初值:uxCriticalNesting = 0;
启动第一个任务:
获取中断向量表地址
LDR R0, =0xE000ED08
取出向量表第一个word空间的值,是主堆栈地址
LDR R0, [R0]
LDR R0, [R0]
设置CPU的主堆栈地址:
MSR MSP, R0
使能外部中断和Fault中断
CPSIE i
CPSIE f
dsb
isb
系统呼叫0,进入SVC_Handler
SVC 0

2.进入SVC_Handler
获取当前运行任务的TCB,在任务创建时,已经将最高优先级的任务的TCB放进了pxCurrentTCB中
LDR R3, =pxCurrentTCB
LDR R1, [R3]
LDR R0, [R1]
此时R0中已经是pxCurrentTCB的第一个成员的地址,也就是该任务的堆栈地址
任务出栈
LDMIA R0!, {R4-R11}
设置线程堆栈
MSR PSP, R0
isb
打开中断
mov R0, #0
MSR BASEPRI, R0
退出异常前的设置
ORR R14, R14, #1101b
bit3:退出后使用线程堆栈PSP
bit2:退出后从线程堆栈出栈
bit0:推出后返回thumb状态,CM3只有Thumb状态,所以必须为1
退出异常,跳到R14
硬件自动从PSP中POP状态,也就跳去执行pxCurrentTCB的任务了。

3.当SysTick启动后,产生SysTick中断
将RTOS 系统Tick加1:xTaskIncrementTick
调度器未挂起
xTickCount加1,
如xTickCount等于0,则切换延时任务表:taskSWITCH_DELAYED_LISTS()
如xTickCount大于下一任务锁定时间,
检查延时表中是否为空,空则跳过
非空延时表则取出要解锁任务的TCB,并从状态链表中移除
将该任务添加到继续表中
如需产生调度则返回pdTRUE
调度器已挂起
将记录Pending的Tick计数加1;
如xTaskIncrementTick返回pdTRUE,表明当前需要产生一次调度,于是手动触发PendSV进如PendSV中断
手动触发PendSV:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;

4.PendSV中断处理
当触发PendSV之后,CortexM3硬件自动压栈[R0 – R3, R12, LR, PSR, PC],将进异常之前的CPU状态
保存到线程堆栈PSP中,假如跑任务A时进的异常,则将CPU状态压到任务A的堆栈中,然后进入中断,
此时CPU工作在特权模式下,使用的时主堆栈MSP。

当进入中断后,首先保存任务A的现场,硬件已经压了8个寄存器,剩下的[R4 - R11]需要手动压栈

1.首先获取当前运行任务(切换前)的TCB,和当前的线程栈
    MRS R0, PSP
    isb
    LDR R3,=pxCurrentTCB
    LDR R2, [r3]
2.手动压栈,已知硬件自动压了8个,剩下的手动压栈
    stmdb R0!, {R4 - R11}   ;R0中为旧任务的线程堆栈,硬件压栈也在里面,手动压的也压在里面
    str R0, [R2]            ;R2中为旧任务的TCB指针,也是任务的堆栈栈顶指针,R0为压栈后的栈顶指针值,更新到R2中
3.保存现场准备跳转,将R3和R14的值压栈,这里是在异常中用的是主堆栈
    stmdb sp!, {R3, R14}
4.屏蔽中断
    mov R0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    dsb
    isb
5.执行上下文切换
    bl  vTaskSwitchContext
6.打开中断
    mov R0, #0
    msr basepri, R0
7.按前面的压栈顺序出栈
    ldmia   sp!, {R3, R14}
8.新任务出栈
    ldr R1, [R3]    ;R3中是正在运行任务的TCB地址,实际上这个地址在第5步中已经替换成新任务的TCB了,
    ldr R0, [R1]    ;取出新TCB的地址中的值,也就是新任务的栈顶地址
    ldmia   R0!, {R4 - R11} ;只需要出栈手动压栈的8个,硬件压栈的硬件自己会出栈;
    msr PSP, R0             ;将出栈后的栈顶指针设置到CPU的PSP寄存器
    isb
9.中断执行完成,即将退出异常,这个时候PSP已经是新任务的堆栈地址了,那么硬件出栈也会从里面POP
    bx  R14
10.vTaskSwitchContext任务TCB的替换
    获取就绪表中最高的优先级:
        portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority)
            uxTopPriority = (31 - __clz(uxTopReadyPriority)); 计算前导0,__clz,从0开始的第几个bit为0,比如第一个0是在bit16,则运算结果为16
            pxCurrentTCB, &pxReadyTaskLists[uxTopPriority],获取就绪表中优先级最高的TCB,挂到pxCurrentTCB
    根据任务优先级找到该任务的TCB,并将其存放到 pxCurrentTCB 中
    退出,就这样实现pxCurrentTCB内容的切换
11.小结:任务切换跟函数切换有差异,函数跳转更多的关注是被硬件压栈的状态,但是任务切换时完整的
CPu状态替换,所以出了硬件压栈,还得软件压栈,恢复任务现场时也需要软件出栈。

通讯更新

请输入您的电子邮件地址进行订阅