- 主要内容
- 重点难点
- 注意事项
- PendSV_Handler代码详解
- 常见问题
- LDR R0,=blockPtr中的blockPtr算立即数/符号?
- 在C文件中使用EQU出错
- LDR R0, [R0]指令的功能
- ADD R4, R4, #1指令含义
- LDR R0, =&stackBuffer[15]编译报错?
- 异常返回时LR的值含义
- Unknown opcode PendSVHandler_nosave, expecting opcode or Macro
- Relocation #REL:1 in switch.o(.emb_text) with respect to NextTask. Value(0x1ffffe98) out of range(0 - 0xfff) for (R_ARM_THM_PC12)
- 为什么要在BlockType中包含一个指针?
- IMPORT是不是相当于C中的extern
- 为什么异常处理函数是PendSV_Handler
- PendSVC为什么要把地址写入到blockPtr
- 为什么R4-R11在缓存保存位置会不断增加?
- 程序为什么会自动跳转到PendSV_Handler函数
- 触发PendSV异常和设置优先级的介绍资料
- LDR Rd,[Rs]含义
- STMDB和LDMIA的{R4-R11}的加载顺序
- Branch Offset 0xFFFFFFE out of Range
Hi,欢迎回来!从本课时开始,你需要动手开始编程!
在这里,我们通过一个小的编程代码熟悉前一节课时中介绍的《芯片内核介绍》,同时也会为下一步讲解任务切换原理做一个铺垫。 特别注意:本课时难度较大,需要花很多时间学习!
主要内容
课时中主要通过编写代码、编译、调试来完成一个功能流程。这个流程接下来的《任务切换的实现》非常相似。所以,如果你现在对这个流程搞不明白其具体为什么是这样,没有关系,就当其是个要求实现的流程即可。
这个流程如下:
- 初始化时,使用blockptr指针变量指向了一个缓冲区空间
- 在主循环中,不断地对flag进行翻转。同时手动触发pendsvc异常
- 在pendsvc异常处理函数中
- 通过stmdb将r4-r11寄存器的内容压到缓冲区中,保存的起始位置为blockptr->stackPtr;
- 再将最后缓冲区的地址写回到blockptr->stackPtr中
- 最后通过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
// 加载寄存器存储地址
// LDR R0, =blockPtr ; 将blockPtr变量的地址加载到R0中
// LDR R0, [R0] ; 再从该地址加载32位数据值,也就是将blockPtr的值加载到R0中。在main()中,我们已经设置:
// BlockType_t block;
// block.stackPtr = &stackBuffer[1024];
// blockPtr = █
// 所以,此时R0的值将会是block结构的起始地址。
// LDR R0, [R0] ; 再次加载32位数据,显示此次就是从block结构开始处加载32位值,根据block结构的定义:
// typedef struct _BlockType_t {
// unsigned long * stackPtr;
// }BlockType_t
// 显然,此时R0的值就是stackPtr的值,也就是&stackBuffer[1024];
LDR R0, =blockPtr
LDR R0, [R0]
LDR R0, [R0]
// 保存寄存器
// 此时R0的值就是&stackBuffer[1024]。然后,将R4, R5, .. R11写入到stackBuff缓冲区中,顺序不重要,你只需知道其与下面LDMIA的加载次序正好相反即可。写入前,以R0中地址为基准,每写入一个寄存器前,先对地址减去4,然后再写入寄存器值。完成所有写入之后,将最后的地址覆盖到R0寄存器中 STMDB R0!, {R4-R11}
// 将最后的地址写入到blockPtr中
// LDR R1, =blockPtr ; 将blockPtr变量的地址加载到R1中
// LDR R1, [R1] ; 再从该地址加载32位数据值,也就是将blockPtr的值加载到R1中。在main()中,我们已经设置:
// BlockType_t block;
// block.stackPtr = &stackBuffer[1024];
// blockPtr = █
// 所以,此时R1的值将会是block结构的起始地址。
// STR R0, [R1] ; 将前面执行STM执行后,R0的值也就是最后写入的地址写入R1中地址指向的位置。
// typedef struct _BlockType_t {
// unsigned long * stackPtr;
// }BlockType_t
// 显然,此时R1的值就是block结构的地址,也就是向将R0的值写入到stackPtr。最终实现将最后的写stackBuff的 地址保存到block.stackPtr
LDR R1, =blockPtr
LDR R1, [R1]
STR R0, [R1]
// 修改部分寄存器,用于测试
// 测试代码。用于检查前面的保存(STMDB),以及后面的恢复(LDMIA)是否正确。如果保存和恢复正确,那么执行LDMIA后,R4,R5的值应为恢复之前的值。 // 如果需要,可在此添加对其它寄存器的修改测试代码。 ADD R4, R4, #1 ADD R5, R5, #1
// 恢复寄存器
// 与STMDB正好相反,从R0地址对应的存储单元中读取多个32位的单元,恢复到R4~R11寄存器。
// 注意到,此时,R0为之前向存储单元写R4~R11的最后单元的地址,所以此时可以正确恢复。
// 具体执行时,首先从当前地址对应的单元处加载数据恢复到寄存器,再将地址+4,如此反复,直到所有寄存器恢复完毕。
LDMIA R0!, {R4-R11}
// 异常返回
// 异常返回指令,不同于从函数调用。此时LR的值不是函数的返回地址,而是一串特定的值,如0xFFFF_FFFx,见《芯片内核简介》中退出异常小节。
BX LR
}
常见问题
LDR R0,=blockPtr中的blockPtr算立即数/符号?
Q:LDR R0,=blockPtr 这里的blockPtr算立即数吗?还是表示符?
A:是符号。立即数是LDR R0,=0x12345678这种。这个准确来说是伪指令。LDR R0,=blockPtr 这里是把blockPtr符号本身的地址给了RO,而不是加载blockPtr的内容。
在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:不是程序入口地址。
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:下图中这样的好处是什么?为什么不直接用一个指针,还用结构体里面包含指针
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指针的自动移动导致的吗?
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中断,这个跳转指令怎么编译不过啊。老是提示超出那个范围了。
A:下面加个NOP指令。这个错误是汇编器要求的,cbz不能这样直接跳