1. 前后台系统结构
1.1 概述
说明1:前后台结构实现为主循环 + ISR,我们需要自己实现主循环并处理ISR与主循环之间的交互
说明2:如果中断要处理的事情很简短,可以在ISR中完成;如果时间要处理的事情较多,则返回后台程序处理
1.2 前后台系统存在的问题
1.2.1 实时性不能保证
实时性不能保证,事件可能无法得到及时处理
示例中在处理flag1事件时延时2秒,会影响对flag2事件的处理
1.2.2 CPU利用率不高
存在CPU空转的情况,CPU利用率不高
说明:上述CPU空转就是因为当前要操作的资源尚未准备好,但是前后台系统又无法去处理其他事务
1.2.3 编程思维不自然
强迫人按照机器的顺序工作方式思考编码。当执行的任务越多,代码结构越复杂,编码难度越大。
不能并行处理,只能顺序处理
2. RTOS原理及功能简介
关键:为什么RTOS能解决前后台系统存在的问题
2.1 概述
RTOS是一种通用的任务管理框架,用于控制任务的运行和任务之间的交互,保证事件得到实时处理。
RTOS的三要素:实时性 + 操作系统 + 嵌入式
2.2 RTOS与前后台结构的比较
① RTOS相当于实现了后台的主循环,并能够处理ISR与主循环的交互
② 使得用户可以只考虑任务的设计
③ RTOS还提供了各种组件用于实现任务间交互及其他控制管理功能(e.g. 存储管理)
2.3 工作原理简介
2.3.1 提供任务概念
提供多个执行流,虽然实际只有一颗CPU,但通过"虚拟化",每个Task好像独占CPU
此处"虚拟"的CPU并非完全的虚拟,"独占"也并不是真正独占,而只是任务认为自己独占
任务认为自己独占的理解:task可以认为自己独占一颗CPU,所以可以实现为一个死循环。而CPU上实际的执行流,会由于操作系统的控制,在不同task之间进行切换。这样既简化了task的设计,也充分利用了CPU
这就引入了一个问题,即"操作系统的控制"本身在实现上也是一段代码,那么这段代码在什么时机运行呢 ? 其实就是在中断或者task主动交出CPU控制权的时候(e.g. task调用可能导致阻塞的函数)
2.3.2 提供任务调度机制
通过RTOS控制任务的运行时机,事件处理的实时性得到有效保证
说明1:一般中断ISR只会进行简短的预处理,而将耗时的操作交给task来执行。RTOS可以确保在ISR执行完成后,立即调度ISR的后续task执行(当然,这需要task优先级的保障)
说明2:ISR后续task在运行时,如果需要等待资源,RTOS会调度其他task执行
说明3:高优先级task可以抢占低优先级task
2.3.3 提供资源管理与通信组件
提供一些组件用于简化任务对资源的访问,事件的处理,以及任务之间的通信,有效降低任务之间的代码耦合
2.4 总结
下图体现了RTOS相对于前后台系统的优势
3. 调试工具使用
4. 芯片内核简介
4.1 为什么要了解硬件特性
RTOS作为系统软件,运行时必然与硬件相关,e.g.
① 任务切换时寄存器的保存
② 异常处理
③ 内核时钟节拍来自硬件定时器
4.2 内核概述
Cortex-M3内核是ARM公司开发的CPU内核。完整的MCU芯片集成了Cortex-M3内核及其他组件
4.3 内核特性介绍
4.3.1 工作模式及特权级别
① 前台程序(中断服务程序)只能在特权级运行
② 后台程序可以根据需要切换权限级别
说明:特权级的不同通常体现在栈指针的使用上,用户级使用PSP;特权级使用MSP
4.3.2 寄存器组
① 只有R13为Banked register,代码中均为R13,运行时根据当前运行的特权级确定使用的是MSP还是PSP
② 之所以区分低组和高组寄存器,是因为大部分16-bit Thumb-2指令只能访问低组寄存器(本质原因:16-bit指令编码长度限制)
注意:Cortex-M3有三个程序状态寄存器,分别是APSR(应用PSR)/ IPSR(中断PSR)/ EPSR(执行PSR)
上述三个程序状态寄存器其实是一个三合一寄存器,即可以单独访问,也可以组合访问
4.3.3 Cortex-M3预定义的存储器映射
说明:Cortex-M3此处的地址空间映射由处理器内核设置,而非芯片厂商设置,这点有利于简化系统移植
4.3.4 堆栈
Cortex-M3使用满递减栈,采用双堆栈机制
4.3.5 系统异常
4.3.5.1 系统异常列表
需要注意如下3种异常,
① 复位
② PendSV
③ SysTick
4.3.5.2 进入异常
说明1:此处要区分哪些寄存器由硬件保存,哪些寄存器由软件保存,由软件保存的寄存器需要软件自己恢复
特别注意:由硬件保存的寄存器也是保存在当前栈中
说明2:注意异常向量表的前2个成员,分别是MSP初始值和复位异常入口地址
注意1:Cortex-M3中需要保存LR,是因为LR只有一个,并非banked register(在Cortex-A系列中,LR为banked register)
而且保存后LR寄存器还要在异常返回时起特殊作用,这点和Cortex-A系列非常不同。Cortex-A系列异常处理的思路是通过banked LR保存返回地址,然后用LR恢复PC
注意2:进入异常时,LR寄存器值在入栈后,会被设置为特殊的EXC_RETURN值,这个值在异常退出时影响返回动作
经过仿真,Cortex-M3在首次进入PendSV异常时,LR值为0xFFFFFFF9,即退出异常时会返回线程模式,并使用MSP
4.3.5.3 退出异常
说明:将进入异常时设置的特殊LR(即EXC_RETURN值)写入PC,就会进入异常返回流程
在Cortex M3中,只有bit 3 & bit 2是可变动的,各种组合情况如下,
bit 3
bit 2
含义
EXC_RETURN数值
0
0
返回handler模式,因为handler模式只能运行在特权级,所以只能使用MSP
0xFFFFFFF1
0
1
错误组合,handler模式无法使用PSP
1
0
返回thread模式,且使用MSP,即仍在特权级运行
0xFFFFFFF9
1
1
返回thread模式,且使用PSP,即在用户级运行
0xFFFFFFFD
4.3.5.4 复位异常的响应
说明:复位异常发生后,CPU将0x00000000和0x00000004中的内容(即异常向量表的前2项)分别加载MSP和PC,即可开始执行
右边的图画错了,赋值给PC的应该是0x00000101,即启动引导代码的位置;赋值给MSP的应该是0x20008000,即MSP初始值
4.3.5.5 PendSV异常的响应
作用:在PendSV中执行RTOS上下文切换(即不同任务间的切换)
工作原理:配置为最低优先级,上下文切换的请求将自动延迟到其他的ISR都完成后才处理,并且可以被其他异常 / 中断抢占。
说明:如果在SysTick中断中发现需要进行任务切换,则只是标记PendSV异常,SytTick中断处理结束时仍然返回之前的ISR。最后当没有比PendSV优先级更高的异常/中断时,才进行任务切换
注意:这一流程的实现需要依赖Cortex-M3提供的NVIC硬件支持,这种可抢占的异常也体现了RTOS的实时特性
4.3.6 指令系统
Cortex-M3使用Thumb-2指令集,长度可为16位或32位。指令可以携带后缀,如有条件执行。
下面仅介绍后续汇编代码中会使用到的指令。
5. 内核编程实践
5.1 需求说明
触发PendSV异常,在异常处理函数中,保存R4 ~ R11寄存器到缓冲区,再恢复R4 ~ R11寄存器,以模拟任务切换时的寄存器保存与恢复
5.2 代码说明
5.2.1 寄存器操作
#define NVIC_INT_CTRL (0xE000ED04) //中断控制状态寄存器
#define NVIC_PENDSVSET (0x10000000) //设置挂起pendSV位,将bit[28]置1
#define NVIC_SYSPRI2 (0xE000ED22) //系统处理器优先级寄存器,按字节访问
#define NVIC_PENDSV_PRI (0x000000FF) //设置pendSV优先级为最低
说明1:中断控制状态寄存器
设置该寄存器的bit 28即可将pendSV中断挂起,当没有更高优先级的中断需要处理时,将进入pendSV中断的ISR运行
说明2:系统处理器优先级寄存器
pendSV中断为第14号中断,由于可以按字节访问NVIC寄存器,所以将0xE000ED22设置为0xFF即可将pendSV中断设置为最低优先级
5.2.2 triggerSV函数说明
void triggerPendSV(void)
{
// 设置pendSV为最低优先级
MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;
// 设置挂起pendSV位
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;
}
triggerPendSV函数的作用就是先将pendSV中断的优先级设为最低,然后将该中断挂起,用于模拟请求任务切换的场景
5.2.3 PendSV_Handler函数说明
__asm void PendSV_Handler(void)
{
// 汇编中使用C变量
IMPORT blockPtr
// 加载缓冲区地址
LDR R0, =blockPtr
LDR R0, [R0]
LDR R0, [R0]
// 保存寄存器
STMDB R0!, {R4-R11}
// 更新缓冲区指针
LDR R1, =blockPtr
LDR R1, [R1]
STR R0, [R1]
// 修改部分寄存器,用于测试
ADD R4, R4, #1
ADD R5, R5, #1
// 恢复寄存器
LDMIA R0!, {R4-R11}
// 更新缓冲区指针
STR R0, [R1]
// 异常返回
BX LR
}
说明1:PendSV_Handler函数名
该函数为pendSV异常的ISR,之所以使用该函数名,是因为在Keil初始化环境中将pendSV的异常向量设置为PendSV_Handler
说明2:缓冲区指针的维护
先来说明一下缓冲区的定义方式,
typedef struct _BlockType_t {
unsigned long *stackPtr;
} BlockType_t;
BlockType_t block;
BlockType_t *blockPtr = NULL;
unsigned long stackBuffer[1024]; // 缓冲区
int main(void)
{
block.stackPtr = stackBuffer + 1024;
blockPtr = █
....
}
如此便可理解加载缓冲区地址的操作了,
LDR R0, =blockPtr // 获取blockPtr变量的地址(blockPtr标号的地址)
// 变量名就是符号地址
LDR R0, [R0] // 获取block的地址(也就是blockPtr指针变量的值)
LDR R0, [R0] // 获取stackPtr指针变量的值(也就是缓冲区的地址)
之所以可以通过第2次{ LDR R0, [R0] }可以获取stackPtr指针变量的值,是因为stackPtr是BlockType_t结构的第1个成员,而C语言要求结构体变量的首地址与其第1个成员的首地址相同
说明3:EXC_RETURN值的设置
响应pendSV异常时,LR的值被设置为0xFFFFFFF9,即异常返回时进入线程模式且使用MSP(线程模式 + 特权级)
这是因为系统启动时默认为线程模式 + MSP,而中断返回是默认返回中断触发前的运行状态
版权归原作者 麦兜的学习笔记 所有, 如有侵权,请联系我们删除。