章节列表

任务切换的实现

2018-01-12 09:42:05 +0000 李述铜

Hi,恭喜你顺利理解前面的内容!但是接下来的这节课时,更有难度。单从时长来看,就已经有1个小时!

本课时是整个课程是的精华所在!可能你要付出好几个小时的努力。但是相信我,绝对值得付出!

注意,不少同学在学习时卡在汇编上,因此后面我将PendSVC_Handler的源码重写了一遍。大幅减少了汇编语句数!请在学习的时候注意参考对比!源码具体说明见文末!

主要内容

本课时以实现为主,其主要内容就是实现任务切换的过程。具体用图来表示如下:

整个程序的工作流程和《内核实践编程》有些类似。只不过我们做了一些修改。建议的学习步骤如下:

切换到首个任务

在视频中,我们可以看到演示过程及讲解,这里再总结说明。

在系统初次启动时,没有任务运行过,所以第一个任务运行时,我们首先使用tTaskInit()对任务的堆栈进行初始化,即设置一个任务的初始状态。这里就包含了设置PC(R15)为函数的入口地址。

然后tTaskRunFirst()触发PendSVC进入第异常处理函数中。在PendSVC异常处理函数,判断发现PSP=0(tTaskRunFirst中设置),所以只执行任务的状态恢复,通过LDMIA指令恢复R4-R11,退出异常时硬件自动从栈中恢复R0~R3等寄存器。这其中就包含恢复R15,一旦恢复程序就从任务的入口函数开始运行。

任务间切换

如果理解上面小节,则理解任务间切换就会容易很多。我们要理解的是怎样保存任务的状态。

当我们要想从一个任务切换到另一个任务时,首先发起PendSVC请求,然后在PendSVC异常中首先由硬件自动保存R0~R3等寄存器,再通过STMDB保存R4~R11寄存器到当前堆栈中,这样就相当于保存了当前任务的状态。

如此一来,每个任务如果没运行过则给其配备初始状态,如果运行过则保存当前工作状态。当要进行任务切换时,任务切换实现过程就简单的实现为前一任务状态的保存和后一任务状态的恢复。

重点难点

堆栈加载顺序

有同学不理解taskInit中的堆栈初始化顺序,请看下图。

图中分两块,上半部分的值会在任务恢复运行时,在退出pendsvc时弹出。这个顺序要和退出异常时硬件自动弹栈的顺序一致,否则将相应的值恢复到错误的寄存器。具体顺序,请看上图,或者《芯片内核简介》课时

下半部分则与PendSVC中所用的LDMIA指令有关,使用LDMIA R0!, {R4~R11}时会自动按一定的顺序从内存中加载值到R4~R11。所以为了保正加载正确,顺序要一致。

关于具体的顺序,如不想深究也可以,只需要相互之间配合即可。

PendSVC代码解析

关于其具体的代码的说明,请见:PendSVC_Handler代码详细注释

注意事项

学习本课时,需要同时结合课时《芯片内核简介》、《内核编程实践》。

课程源码的另一版本实现

基本流程是一样的,只是做了一些改动,主要目标是尽量用C来替代汇编。源码请见目录下的:《C2.02 任务切换的实现-另一种实现》。

不再将PSP=0

芯片在上电启动后,默认使用MSP作为堆栈的指针, 在这里我们直接将PSP = MSP。

void tTaskRunFirst () {
    // 这里设置了一个标记,PSP = MSP, 用于与tTaskSwitch()区分,用于在PEND_SV
    // 中判断当前切换是tinyOS启动时切换至第1个任务,还是多任务已经跑起来后执行的切换
    __set_PSP(__get_MSP());

    MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;   // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级

    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;    // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV

    // 可以看到,这个函数是没有返回
    // 这是因为,一旦触发PendSV后,将会在PendSV后立即进行任务切换,切换至第1个任务运行
    // 此后,tinyOS将负责管理所有任务的运行,永远不会返回到该函数运行
}

不再判断PSP=0?

上面提到过,在芯片启动时,默认使用MSP堆栈。然后在tTaskRunFirst()中,我们设置了PSP=MSP。

第一次进入PendSV_Handler()时,已经设置了PSP=MSP,所以硬件自动将R0~R3等压入MSP堆栈。同时STMDB R0!, {R4-R11}会将R4~R11压入到MSP。

系统跑起来之后,进入PendSV_Handler()前一直用的是PSP堆栈,所以硬件自动保存及STMDB保存的也是在PSP堆栈中。

这样一来就不用再判断PSP = 0 ?的问题

__asm void PendSV_Handler (void) { 
    IMPORT saveAndLoadStackAddr
    
    // 切换第一个任务时,由于设置了PSP=MSP,所以下面的STMDB保存会将R4~R11
    // 保存到系统启动时默认的堆栈中,而不是某个任务
    MRS     R0, PSP                 
    STMDB   R0!, {R4-R11}               // 将R4~R11保存到当前任务栈,也就是PSP指向的堆栈
    BL      saveAndLoadStackAddr        // 调用函数:参数通过R0传递,返回值也通过R0传递 
    LDMIA   R0!, {R4-R11}               // 从下一任务的堆栈中,恢复R4~R11
    MSR     PSP, R0
    
    MOV     LR, #0xFFFFFFFD             // 指明返回异常时使用PSP。注意,这时LR不是程序返回地址
    BX      LR
}

下面的C代码,用于替换原来的汇编代码,更容易理解。

uint32_t saveAndLoadStackAddr (uint32_t stackAddr) {
    if (currentTask != (tTask *)0) {                    // 第一次切换时,当前任务为0
        currentTask->stack = (uint32_t *)stackAddr;     // 所以不会保存
    }
    currentTask = nextTask;                     
    return (uint32_t)currentTask->stack;                // 取下一任务堆栈地址
}

常见问题