Task Switch

作者:郑沛坤

RTOS中最重要的功能就是找到当前就绪任务中优先级最高的任务,然后将CPU运行权切换到此任务。任务切换是此过程的直接参与者,任务切换代码要求简洁、执行效率高,通常会使用到汇编指令,CPU硬件底层也会提供特殊的指令,以加快任务切换的速度。以Cortex-M3为例,分析RTOS的任务切换过程。

  1. 硬件层

    • Cortex-M3中,有”主堆栈指针MSP”和”进程堆栈指针PSP”。CPU上电后默认使用MSP指针,MSP指针用于处理中断服务程序,PSP指针用于处理用户任务。
    • 执行代码时用到的寄存器分为两类,一类由PSP指针自动将xPSR,PC,LR,R12,R3-R0寄存器直接保存到任务堆栈中,另一类R11-R4寄存器需要用户自动保存到堆栈中。
    • PendSV是专门设计为处理任务切换的中断,存储当前任务的寄存器数据到任务堆栈和从下一运行任务的任务堆栈中恢复信息到寄存器的功能在此中断中实现。
  2. 任务切换流程

    1
    2
    3
    4
    5
    6
    7
    graph TB
    A[发生PendSV中断,硬件自动将部分寄存器数值压入堆栈]-->B[进入中断服务程序,将剩下寄存器数值继续压入堆栈]
    B-->C[保存当前运行任务的栈顶地址]
    C-->D[切换到下一运行任务中]
    D-->E[从下一运行任务的堆栈中恢复寄存器数值]
    E-->F[恢复下一运行任务的指针到PSP,返回到上次运行的位置]
    F-->G[退出PendSV,硬件自动恢复其他寄存器数值,开始执行任务]
    图1 任务切换流程

    触发PendSV函数:

    1
    2
    3
    4
    5
    6
    // 向地址addr处写入某数值
    #define MEM32(addr) *(volatile unsigned long*)(addr)
    #define NVIC_INI_CTRL 0xE000ED04 //中断控制寄存器地址
    #define NVIC_PENDSVSET 0x10000000//触发PendSV中断的值

    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;

    PendSV中断函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    __asm void PendSV_Handler()
    {
    IMPORT currentTask//当前运行任务
    IMPORT nextTask//下一运行任务

    MRS RO,PSP//获取当前运行任务的堆栈指针
    STMDB R0!,{R4-R11}//分别向当前任务堆栈中压入R4-R11寄存器数值
    LDR R1,=currentTask//获取当前任务的地址
    LDR R1,[R1]//获取当前任务的堆栈指针的值
    STR R0,[R1]//保存此时堆栈指针的值,覆盖之前存储的值

    LDR R0,=currentTask//切换currentTask到nextTask
    LDR R1,=nextTask
    LDR R2,[R1]
    STR R2,[R0]

    LDR RO,[R2]//获取下一运行任务的堆栈指针
    LDMIA R0!,{R4-R11}//从下一运行任务的堆栈中恢复R4-R11寄存器

    MSR PSP,R0//将移动过的堆栈指针恢复到PSP
    ORR LR,LR,#0x04//指明在退出LR时,切换到PSP任务堆栈中
    BX LR//返回后硬件会从堆栈中自动取回LR值,恢复到上次运行的位置
    }

    PendSV的中断处理函数只处理了从currentTask到nextTask的切换,而如何找到就绪任务中,优先级最高的nextTask是RTOS的核心内容。在存储和恢复寄存器信息过程中,堆栈指针也进行了移动,我们需要保存的和恢复到PSP任务堆栈指针上的是经过移动后的最终值。