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

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

主要内容

本课时以实现为主,其主要内容就是实现任务切换的过程。具体用图来表示如下:
任务切换的实现 - 图1
整个程序的工作流程和《内核实践编程》有些类似。只不过我们做了一些修改。建议的学习步骤如下:

切换到首个任务

在视频中,我们可以看到演示过程及讲解,这里再总结说明。
在系统初次启动时,没有任务运行过,所以第一个任务运行时,我们首先使用tTaskInit()对任务的堆栈进行初始化,即设置一个任务的初始状态。这里就包含了设置PC(R15)为函数的入口地址。
任务切换的实现 - 图2
然后tTaskRunFirst()触发PendSVC进入第异常处理函数中。在PendSVC异常处理函数,判断发现PSP=0(tTaskRunFirst中设置),所以只执行任务的状态恢复,通过LDMIA指令恢复R4-R11,退出异常时硬件自动从栈中恢复R0~R3等寄存器。这其中就包含恢复R15,一旦恢复程序就从任务的入口函数开始运行。

任务间切换

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

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

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

重点难点

堆栈加载顺序

有同学不理解taskInit中的堆栈初始化顺序,请看下图。
任务切换的实现 - 图3
图中分两块,上半部分的值会在任务恢复运行时,在退出pendsvc时弹出。这个顺序要和退出异常时硬件自动弹栈的顺序一致,否则将相应的值恢复到错误的寄存器。具体顺序,请看上图,或者《芯片内核简介》课时
下半部分则与PendSVC中所用的LDMIA指令有关,使用LDMIA R0!, {R4R11。所以为了保正加载正确,顺序要一致。
关于具体的顺序,如不想深究也可以,只需要相互之间配合即可。

PendSVC代码解析

挺多同学问这块代码的问题,所以总结一下。对这块不清楚的,请仔细阅读下面的说明。
首先,这块代码要对照课件中的图。
任务切换的实现 - 图4
源码详解:

  1. __asm void PendSV_Handler ()
  2. {
  3. IMPORT currentTask // 使用import导入C文件中声明的全局变量
  4. IMPORT nextTask // 类似于在C文文件中使用extern int variable
  5. // MRS Rx, PSP --- 将PSP堆栈寄存器的值传送给Rx寄存器
  6. MRS R0, PSP // 获取当前任务的堆栈指针
  7. // CBZ Rx, label -- 判断Rx的值是否为0,如果为0则跳转到指定标号处运行。否则继续往下运行
  8. CBZ R0, PendSVHandler_nosave // if 这是由tTaskSwitch触发的(此时,PSP肯定不会是0了,0的话必定是tTaskRunFirst)触发
  9. // 不清楚的话,可以先看tTaskRunFirst和tTaskSwitch的实现
  10. // STMDB Rx!, {Rm-Rn} -- 将Rm-Rn之间的一堆寄存器写到Rx中地址对应的内存处。每写一个单元前,地址先自减4再写,先写Rm,最后写Rn。写完后将最后的地址保存到Rx寄存器中。
  11. STMDB R0!, {R4-R11} // 那么,我们需要将除异常自动保存的寄存器这外的其它寄存器自动保存起来{R4, R11}
  12. // 保存的地址是当前任务的PSP堆栈中,这样就完整的保存了必要的CPU寄存器,便于下次恢复
  13. // 取currentTask这个变量符号的地址写到R1!注意,不是取currentTask的值
  14. LDR R1, =currentTask // 保存好后,将最后的堆栈顶位置,保存到currentTask->stack处
  15. // LDR R1, [R1] -- 从R1中的地址处,取32位,再写到R1。也就是从currentTask的地址处,取32位值。由于currenTask是指针,这个操作也就是取currentTask的值到R1。由于currentTask指向了某个tTask结构,也就是说此时R1的值是某个tTask结构变量的起始地址。而由于stack位于tTask的起始处,所以tTask.stack的地址与tTask相同。此时R1就是currentTask中stack的地址
  16. LDR R1, [R1] // 由于stack处在结构体stack处的开始位置处,显然currentTask和stack在内存中的起始
  17. // STR R0, [R1] -- 将R0的值写到R1中地址处。也就是将STMDB最后的地址,写到currentTask->stack处
  18. STR R0, [R1] // 地址是一样的,这么做不会有任何问题
  19. PendSVHandler_nosave // 无论是tTaskSwitch和tTaskSwitch触发的,最后都要从下一个要运行的任务的堆栈中恢复
  20. // CPU寄存器,然后切换至该任务中运行
  21. // 取currentTask的地址到R0
  22. LDR R0, =currentTask // 好了,准备切换了
  23. // 取nextTask的地址到R1
  24. LDR R1, =nextTask
  25. // 从nextTask的地址处取32位值,也就是R2 <= nextTask的值。
  26. LDR R2, [R1]
  27. // 向currentTask的地址处写nextTask的值,也就是实现currentTask <= nextTask
  28. STR R2, [R0] // 先将currentTask设置为nextTask,也就是下一任务变成了当前任务
  29. // 从currentTask指向的结构起始地址中取32位,由于stack成员位于结构体开始处,也就是R0 <= currentTask.stack
  30. LDR R0, [R2] // 然后,从currentTask中加载stack,这样好知道从哪个位置取出CPU寄存器恢复运行
  31. // 前面取了堆栈地址currentTask.stack。下面就是从该地址(R0中的值)连续取若干个32位单元,恢复到R4~R11。这个顺序和前面的STMDB恰好相反。
  32. LDMIA R0!, {R4-R11} // 恢复{R4, R11}。为什么只恢复了这么点,因为其余在退出PendSV时,硬件自动恢复
  33. // 恢复R4~R11后,我们需要切换到这个堆栈。所以将最后的R0地址,写到PSP堆栈寄存器中
  34. MSR PSP, R0 // 最后,恢复真正的堆栈指针到PSP
  35. // 下面的代码,如果不懂,请忽略。只要知道是切换到PSP堆栈中。
  36. ORR LR, LR, #0x04 // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP)
  37. BX LR // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置
  38. }

注意事项

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

课程源码的另一版本实现

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

不再将PSP=0

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

不再判断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 ?的问题

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

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

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

常见问题

Undefined Sysmbol _set_PSP

Q:编译遇到错误: Undefined Sysmbol _set_PSP
A:写法错误,注意有开头有两个下划线:__set_PSP

Keil窗口中观察currentTask结构的值

Q: 从上到下三个值分别是什么?哪个是地址,哪个是值。如果current->stack指向的是task1Env,那这个第0个单元,应该是指的task1Env[?]

任务切换的实现 - 图5

A:currentTask的值,currentask->stack的值,currentTask->stack[0]的值。stack指向哪儿,第stack[0]就是这个地址的第0个位置。

执行 BX LR之后,为什么会直接跳转到任务

首次进入PendSV,PC为什么恢复的是task1Entry的地址,又不是从这里进入中断的。

Q:跳转到BX LR之后,为什么会直接跳转到这个task1
A:BX LR时退出异常时,会自动从PSP指向的栈中弹出很多寄存器值,包含PC。由于最开始有初始化了堆栈,所以弹出后会弹出到相应位置。具体过程请再看下《任务切换的实现》理论部分。执行bx lr之前,你在观察窗口里看下相应任务的栈空间里面的值,然后再执行bx lr之后,对照一下是将哪些值给弹出到寄存器里。

如果任务中定义了局部变量,需要在任务切换的时候保存局部变量么

Q:刚看完第十课,想请问一下,如果任务中定义了局部变量,需要在任务切换的时候保存局部变量么?
A:不用,因为局部变量一般是放在内核寄存器中,或者当前任务的堆栈里面

调用子函数时,堆栈保存哪些参数?

Q:调用子函数时,SP堆栈保存哪些参数?
A:看你的子程序怎么写。具体要看arm AAPCS,也可以直接看keil帮助文档,里面有关于怎样传递参数给子程序的说明。

执行PendSVC进入HardFault

Q:我在看第十讲内容,然后调试并没有进入任务,而是进入HardFault
A:经查是执行pendsv_handler里面的BX LR后进入HardFault。你检查单步调试看看,我觉得问题主要是Pendsv_handler这里没处理好 ,PC,或者xPSR,SP,LR,某一个寄存器出错了。
有几个地方要特别注意:

  • 调用tTaskInit()时,各个参数要正确,特别是传递的堆栈配置参数
  • tTaskInit()中有关堆栈初始化部分。堆栈的初始化顺序一定要正确,各个初始化值要正确,绝对不能多也不能少。
  • PendSVC_Handler中各个汇编指令中涉及堆栈保存和恢复的代码绝对要正确。

如遇到问题,强烈建议拿课程源码对照着看,务必要仔细对照。因为有些同学犯了小错误,对照时又没对照仔细,最终查出来了少写了或者错写。