Hi,欢迎回来!从本课时开始,你需要动手开始编程!

在这里,我们通过一个小的编程代码熟悉前一节课时中介绍的《芯片内核介绍》,同时也会为下一步讲解任务切换原理做一个铺垫特别注意:本课时难度较大,需要花很多时间学习!

主要内容

课时中主要通过编写代码、编译、调试来完成一个功能流程。这个流程接下来的《任务切换的实现》非常相似。所以,如果你现在对这个流程搞不明白其具体为什么是这样,没有关系,就当其是个要求实现的流程即可。
这个流程如下:

  • 初始化时,使用blockptr指针变量指向了一个缓冲区空间
  • 在主循环中,不断地对flag进行翻转。同时手动触发pendsvc异常
  • 在pendsvc异常处理函数中
    • 通过stmdb将r4-r11寄存器的内容压到缓冲区中,保存的起始位置为blockptr->stackPtr;
    • 再将最后缓冲区的地址写回到blockptr->stackPtr中
    • 最后通过ldmia将刚才保存到缓冲区r4-r11值恢复到r4-r11寄存器

在后面的课时你会看到,这个流程跟任务切换流程是非常相似的。对比如下:

  • 初始化时,我们的任务初始化一个堆栈空间
  • 在需要任务切换时,手动触发pendsvc异常
  • 在pendsvc异常处理函数中

    • 通过stmdb将r4-r11寄存器的内容压到当前任务栈中,保存的起始位置为PSP指向的位置;
    • 再将最后PSP的值写回到task->Stack中
    • 最后通过ldmia将下一任务的堆栈中保存的r4-r11值恢复到r4-r11寄存器
      对于这个流程的对比,请在学习下一节课《任务切换的实现》时,再回顾。

      重点难点

      本课时的难点在于理解从触发pendsvc异常,进入pendsvc,各指令的意义和功能,到最后退出异常整个流程中的软硬件工作过程。具体来说有以下几点:
  • 触发pendsvc异常。请参考上一节课时《芯片内核简介》

  • 进入pendsvc异常后的工作流程。硬件在进入/退出异常时,会自动保存恢复一部分寄存器(请参考上一节课时《芯片内核简介》)
  • 具体的指令含义。主要是怎样通过LDR/STR指令对指针及其指向的空间进行读写操作。

据以往同学反馈,问题多出现在pendsvc异常处理函数中的汇编指令理解上,此部分请参考下方的PendSV_Handler代码详解。
如有其它疑问,请见下方的常见问题

注意事项

本课时很难但是很重要,是理解任务切换实现的必备硬件基础,必须掌握。
虽然要花很多时间,但是值得付出!加油!
以下是学员最常问的问题:
Q:这样写不怕定义其他变量后把stackBuffer[1024]地址里的值改了吗,还是因为这个地址存的值没用所以不用管?

A:首先要注意的是,数组stackBuffer有1024个单元。按正常的理解,对数组单元访问,可以使用stackBuffer[0~1023]。
但是假设我们要获取stackBuffer数组的结束地址(最后一个单元的下一地址),那么应该用什么表示方法?可以使用stackBuffer + 1024,与stackBuffer[1024]一个意思。
有同学担心这里越界!其实不必。
所谓的越界,最终要看是不是读写越过数组区域之外的单元,超过并读写才算真正越界。而这里1024只是获取数组的末端地址,并没有读写。所以这并不算越界。
事实上,数组下标可以随意多长的,2048都可以,语法上并没有问题。如果觉得还是难以理解,写成stackBuffer + 1024。
在后面,汇编指令中用的是stmdb,是先递减堆栈地址,-4,再写入寄存器,所以没有用到stackBuffer[1024]单元,而是先写1023单元,再写1022单元,。。。
在后面的《任务定义与切换原理》课时中,会再提及这个问题。

PendSV_Handler代码详解

有同学在看完视频后,对PendSV_Handler中的实现代码仍有很多不解,所以这里用文档再详细标注。如果你在学习视频时遇到同样的问题,请先参考此文档。
该课时中的PendSV_Handler实现,主要用于模拟任务切换时的CPU内核寄存器的保存与恢复。这段代码非常重要,请务必弄懂。主要流程如下:
进入异常硬件自动保存部分寄存器到当前堆栈中

  • 执行STM指令将其余未自动保存的寄存器R4~R11保存到自定义的缓冲区中
  • 执行LDM指令将从自定义缓冲区中恢复之前保存的寄存器值到R4~R11
  • 执行BX LR,硬件自动从当前堆栈中恢复之前保存的寄存器值

源码详解:

__asm void PendSV_Handler () { // blockPtr在main.c中定义,定义为:BlockType_t * blockPtr。 // 在汇编代码中,如要引用C中的符号,必 须先用IMPORT导入。有些类似于在C语言中的extern IMPORT blockPtr

  1. // 加载寄存器存储地址
  2. // LDR R0, =blockPtr ; 将blockPtr变量的地址加载到R0中
  3. // LDR R0, [R0] ; 再从该地址加载32位数据值,也就是将blockPtr的值加载到R0中。在main()中,我们已经设置:
  4. // BlockType_t block;
  5. // block.stackPtr = &stackBuffer[1024];
  6. // blockPtr = █
  7. // 所以,此时R0的值将会是block结构的起始地址。
  8. // LDR R0, [R0] ; 再次加载32位数据,显示此次就是从block结构开始处加载32位值,根据block结构的定义:
  9. // typedef struct _BlockType_t {
  10. // unsigned long * stackPtr;
  11. // }BlockType_t
  12. // 显然,此时R0的值就是stackPtr的值,也就是&stackBuffer[1024];
  13. LDR R0, =blockPtr
  14. LDR R0, [R0]
  15. LDR R0, [R0]
  16. // 保存寄存器

// 此时R0的值就是&stackBuffer[1024]。然后,将R4, R5, .. R11写入到stackBuff缓冲区中,顺序不重要,你只需知道其与下面LDMIA的加载次序正好相反即可。写入前,以R0中地址为基准,每写入一个寄存器前,先对地址减去4,然后再写入寄存器值。完成所有写入之后,将最后的地址覆盖到R0寄存器中 STMDB R0!, {R4-R11}

  1. // 将最后的地址写入到blockPtr中
  2. // LDR R1, =blockPtr ; 将blockPtr变量的地址加载到R1中
  3. // LDR R1, [R1] ; 再从该地址加载32位数据值,也就是将blockPtr的值加载到R1中。在main()中,我们已经设置:
  4. // BlockType_t block;
  5. // block.stackPtr = &stackBuffer[1024];
  6. // blockPtr = █
  7. // 所以,此时R1的值将会是block结构的起始地址。
  8. // STR R0, [R1] ; 将前面执行STM执行后,R0的值也就是最后写入的地址写入R1中地址指向的位置。
  9. // typedef struct _BlockType_t {
  10. // unsigned long * stackPtr;
  11. // }BlockType_t
  12. // 显然,此时R1的值就是block结构的地址,也就是向将R0的值写入到stackPtr。最终实现将最后的写stackBuff的 地址保存到block.stackPtr
  13. LDR R1, =blockPtr
  14. LDR R1, [R1]
  15. STR R0, [R1]
  16. // 修改部分寄存器,用于测试

// 测试代码。用于检查前面的保存(STMDB),以及后面的恢复(LDMIA)是否正确。如果保存和恢复正确,那么执行LDMIA后,R4,R5的值应为恢复之前的值。 // 如果需要,可在此添加对其它寄存器的修改测试代码。 ADD R4, R4, #1 ADD R5, R5, #1

  1. // 恢复寄存器
  2. // 与STMDB正好相反,从R0地址对应的存储单元中读取多个32位的单元,恢复到R4~R11寄存器。
  3. // 注意到,此时,R0为之前向存储单元写R4~R11的最后单元的地址,所以此时可以正确恢复。
  4. // 具体执行时,首先从当前地址对应的单元处加载数据恢复到寄存器,再将地址+4,如此反复,直到所有寄存器恢复完毕。
  5. LDMIA R0!, {R4-R11}
  6. // 异常返回
  7. // 异常返回指令,不同于从函数调用。此时LR的值不是函数的返回地址,而是一串特定的值,如0xFFFF_FFFx,见《芯片内核简介》中退出异常小节。
  8. BX LR

}

常见问题

LDR R0,=blockPtr中的blockPtr算立即数/符号?

Q:LDR R0,=blockPtr 这里的blockPtr算立即数吗?还是表示符?
A:是符号。立即数是LDR R0,=0x12345678这种。这个准确来说是伪指令。LDR R0,=blockPtr 这里是把blockPtr符号本身的地址给了RO,而不是加载blockPtr的内容。
内核编程实践 - 图1

在C文件中使用EQU出错

Q:为什么我在C文件中使用 EQU 会报错
A:你不能在c文件中这样用,只能用在汇编文件中。

LDR R0, [R0]指令的功能

Q:LDR R0, [R0]中[]的作用是什么
A:这条指令的含义表示,将R0寄存器中的内容作为地址,从该地址中读取32位数值并加载到R0寄存器中。类似的,还有其它指令,如LDRH取16位,LDRB取8位。

ADD R4, R4, #1指令含义

Q:请问 ADD R4, R4, #1指令含义
A:将R4=R4+1

LDR R0, =&stackBuffer[15]编译报错?

Q:为什么 LDR R0, =&stackBuffer[15]编译报错 这种写法会报错?
A:汇编不支持这种语法,只能写作LDR Rx, =符号名。汇编语法和C语法差异较大,不要将C中的语法用到汇编中。
如果需要了解完整的汇编指令,请阅读《Cortex-M3权威指南》或者Cortex-M3的技术手册。

异常返回时LR的值含义

Q:程序执行switch函数PendSVHandler_nosave。我对LR的值很不懂,它指向的值是程序返回地址?

A:不是程序入口地址。

内核编程实践 - 图2

Unknown opcode PendSVHandler_nosave, expecting opcode or Macro

Q:source\switch.c(26): error: A1163E: Unknown opcode PendSVHandler_nosave , expecting opcode or Macro。
A:标号PendSVHandler_nosave之前,不要缩进,前面不要留空。ARM汇编器规定,符号不能空格写。

Relocation #REL:1 in switch.o(.emb_text) with respect to NextTask. Value(0x1ffffe98) out of range(0 - 0xfff) for (R_ARM_THM_PC12)

Q:.\Objects\tinyos.axf: Error: L6286E: Relocation #REL:1 in switch.o(.emb_text) with respect to NextTask. Value(0x1ffffe98) out of range(0 - 0xfff) for (R_ARM_THM_PC12)
A:注意那个LDR,如果你是要加载符号的地址话,注意写成LDR Rx, =符号名的形式,

为什么要在BlockType中包含一个指针?

Q:下图中这样的好处是什么?为什么不直接用一个指针,还用结构体里面包含指针

内核编程实践 - 图3

A:这个是为后面讲《任务切换的实现》刻意准备的,就这样设计。后面我们可以看到,我们会为任务定义一个tTask结构,然后在开头包含一个堆栈指针。所以暂时不用考虑为什么这么做,纯粹是为了方便以后的学习。这个课时主要是让你理解下主要的汇编代码怎么写,异常处理函数,以及寄存器的保存与恢复。

IMPORT是不是相当于C中的extern

Q:IMPORT是不是相当于C中的extern
A:是的,其含义就是导入外部符号。

为什么异常处理函数是PendSV_Handler

Q:pendsvc-handler相当于51里面中断的中断处理程序吗?
A:差不多。区别在于51等单片机只有中断的说法。而Cortex-M3中细分为中断和异常。

  • 中断用于响应外部事件,比如IO引脚中断;
  • 异常用于响应内部事件,比如除0异常,系统请求调用,遇到未定义指令等。

Pendsvc属于异常,相应的pendsvc-handler即为异常处理程序。

PendSVC为什么要把地址写入到blockPtr

感谢同学 @小千 的提问
Q:PendSVC-Handler中为什么要把最后的地址写入到blockPtr?后面并没有用到这个地址?
A:这三条指令,是告诉你怎么用汇编实现这个地址的保存,至于用不用在这个课时中不重要。
LDR R1, =currentTask
LDR R1, [R1]
STR R0, [R1]
理解了怎么保存后,在任务切换实现课时中,我就不必再在视频中讲这个功能用汇编代码怎么实现了
课时是循环渐进的,前面的知识点会在后面用到。
所以,你现在只需要学习好怎么实现这个功能。

为什么R4-R11在缓存保存位置会不断增加?

Q:在调试过程中发现,多次运行时,R4-R11在缓存中保存位置不断增加,这是因为SP指针的自动移动导致的吗?
内核编程实践 - 图4
内核编程实践 - 图5
A:不是因为sp的变化,是结构体内的那个指针变量一直在变化。每次进去异常,都取指针值,相应的区域压入r4-11。压入后再将最后地址写回,这样每次写入地址都变化。stmdb表示压入前减地址,所以往低地址变化。关于这段代码的具体含义:请参考PendSV_Handler代码详解注释,看不懂的请见这里)

程序为什么会自动跳转到PendSV_Handler函数

Q:程序为什么会跳转到PendSV_Handler函数,我看main.c没有什么地方设置跳转这个函数的语句?
A:异常发生时,硬件会自动从中断向量表中找入口地址。关于向量表和PendSV_Handler的关系,可以看这里:
为什么pendsvc-handler是异常处理函数
我在视频课时中提过,不需要关心这个细节,只需要知道这么写就可以了。不要在硬件细节上花太多时间,因为课程主要还是学rtos工作原理。换一颗芯片,可能工作方式又有些不太一样。甚至于换用不同的IDE,用不同的工程模板,PendSV_Handler的写法也不同

触发PendSV异常和设置优先级的介绍资料

Q:老师,触发PendSV异常和设置优先级,这部分的内容那个资料上有?我看芯片手册上没有这方面的介绍。
A:请看课程资源的《cortex m3权威指南》或者Cortex-M3内核参考手册

LDR Rd,[Rs]含义

Q:LDR Rd,[Rs];这行代码里,存到Rd里的是Rs本身的地址还是Rs地址内存的数据?
A:感谢同学16+腾威_99f 的回答。 数据,你可以用调试来验证。

STMDB和LDMIA的{R4-R11}的加载顺序

Q:PendSV_Handler里R4-R11压栈和出栈顺序要相反,STMDB是{R4-R11},那LDMIA,不应该是{R11-R4}吗?
A:就这么写,cpu自动处理了,不用管。

Branch Offset 0xFFFFFFE out of Range

Q:c2.02第十课中关于PendSV_Handler中断,这个跳转指令怎么编译不过啊。老是提示超出那个范围了。

内核编程实践 - 图6

A:下面加个NOP指令。这个错误是汇编器要求的,cbz不能这样直接跳