4. Cortex-M3/M4架构基础:寄存器组与堆栈指针、异常与中断向量表、PendSV与SVC异常机制、双堆栈指针(MSP/PSP)切换
好,咱们今天来啃一块硬骨头——Cortex-M3/M4的架构基础。说实话,这部分内容在RTOS移植中属于“绕不过去的坎”。你如果不理解寄存器组和堆栈指针,后面写任务切换代码时就会一头雾水。我个人习惯是先把硬件架构摸透,再动手写代码,这样心里有底。
4.1 寄存器组:CPU的“工作台”
Cortex-M3/M4的寄存器组,说白了就是CPU干活时放工具的地方。它包含16个通用寄存器(R0-R15)和几个特殊功能寄存器。我刚开始学的时候,总觉得寄存器太多记不住,后来发现只要抓住核心的几个就行。
| 寄存器 | 名称 | 作用 |
|---|---|---|
| R0-R12 | 通用寄存器 | 存数据、地址,随便用 |
| R13 (SP) | 堆栈指针 | 指向当前堆栈顶部 |
| R14 (LR) | 链接寄存器 | 保存函数返回地址 |
| R15 (PC) | 程序计数器 | 指向当前执行的指令 |
| xPSR | 程序状态寄存器 | 记录运算结果标志 |
| PRIMASK | 中断屏蔽寄存器 | 全局关中断 |
| CONTROL | 控制寄存器 | 选择堆栈指针、特权级 |
这里我要特别提一下R14(LR)。你在函数调用时,硬件会自动把返回地址存到LR里。但RTOS任务切换时,LR会被我们拿来干别的事——比如触发PendSV异常。嗯,这里先留个悬念。
关键点:R0-R3通常用于传参,R4-R11在函数调用时必须保存。RTOS任务切换时,这些寄存器都要压栈保存。
4.2 堆栈指针:MSP与PSP的双重人格
Cortex-M3/M4有两个堆栈指针:MSP(主堆栈指针)和PSP(进程堆栈指针)。为什么搞两个?说白了就是为了隔离——操作系统内核用一个栈,用户任务用另一个栈。
- MSP(主堆栈指针):复位后默认使用。处理异常和中断时自动切换到这里。
- PSP(进程堆栈指针):跑用户任务时用。任务切换时,当前任务的上下文就保存在PSP指向的栈里。
我记得第一次移植FreeRTOS时,死活跑不起来。查了两天,发现是CONTROL寄存器没设置对,PSP根本没生效。你想想看,任务切换时CPU从PSP取数据,结果PSP还是默认值,那不乱套了吗?
我的习惯:在启动代码里,先把MSP初始化好。等创建第一个任务时,再通过修改CONTROL寄存器切换到PSP。这样内核和任务各用各的栈,互不干扰。
4.3 异常与中断向量表:CPU的“电话本”
异常和中断,本质上是一回事——都是CPU停下当前工作,去处理紧急事件。Cortex-M3/M4把异常和中断统一编号,放在一张表里,这就是中断向量表。
这张表从地址0x00000000开始,每个表项占4字节,存的是对应异常/中断的服务函数地址。前16个是系统异常,后面的是外部中断。我见过不少新手把中断向量表放错位置,结果一触发中断就跑飞。
; 中断向量表示例(汇编)
__Vectors DCD __initial_sp ; 0x00: 栈顶地址
DCD Reset_Handler ; 0x04: 复位异常
DCD NMI_Handler ; 0x08: NMI
DCD HardFault_Handler ; 0x0C: 硬错误
DCD MemManage_Handler ; 0x10: 内存管理
DCD BusFault_Handler ; 0x14: 总线错误
DCD UsageFault_Handler ; 0x18: 用法错误
; ... 后面还有SVC、PendSV、SysTick等
避坑指南:我曾经把中断向量表放在0x08000000(Flash起始地址),但忘了修改SCB->VTOR寄存器。结果CPU还是从0x00000000找向量表,那里全是0,一触发中断就进HardFault。这个坑我踩了整整一个下午。
4.4 PendSV与SVC:RTOS的“左右手”
SVC(系统服务调用)和PendSV(可挂起系统调用)是RTOS里最常用的两个异常。它们分工明确:SVC用于主动请求,PendSV用于被动切换。
SVC:主动请求内核服务
用户任务想进入临界区、发送消息、或者创建任务时,会执行一条SVC指令。这条指令会触发SVC异常,CPU自动切换到MSP,然后执行SVC_Handler。我在项目中常用SVC来实现系统调用接口,用户任务通过它来请求内核服务,安全又规范。
// 触发SVC异常
__asm void SVC_CreateTask(void *task_func, void *stack)
{
SVC 0x01 ; 服务号1:创建任务
BX LR
}
void SVC_Handler(void)
{
// 从堆栈中取出服务号
// 根据服务号执行对应操作
}
PendSV:延迟执行的任务切换
PendSV的特点是“可挂起”——你可以随时触发它,但CPU会等到当前中断处理完才执行它。这有什么用?你想想看,如果在中断里直接做任务切换,万一中断嵌套了怎么办?PendSV就是用来解决这个问题的。
RTOS通常的做法是:在SysTick中断里检查是否需要切换任务,如果需要,就触发PendSV。等SysTick退出后,PendSV才真正执行任务切换。这样保证了中断响应的实时性。
核心逻辑:PendSV_Handler里做三件事:保存当前任务上下文(压栈)、找到下一个任务、恢复新任务上下文(出栈)。说白了就是“换人”——把当前任务的寄存器存起来,把新任务的寄存器恢复出来。
4.5 双堆栈指针切换:任务切换的“幕后黑手”
任务切换时,堆栈指针的切换是关键。每个任务都有自己的栈,PSP指向当前任务的栈顶。切换时,先把当前任务的寄存器压到PSP指向的栈里,然后修改PSP指向新任务的栈,最后从新栈里弹出寄存器。
这里有个细节:进入PendSV时,CPU已经自动把xPSR、PC、LR、R12、R0-R3压栈了。我们只需要手动压栈R4-R11。退出时,先手动弹出R4-R11,再让CPU自动弹出剩下的寄存器。
; PendSV_Handler 核心代码(简化版)
PendSV_Handler
; 保存当前任务上下文
MRS R0, PSP ; 获取当前PSP
STMDB R0!, {R4-R11} ; 压栈R4-R11
; 更新当前任务的栈顶指针
LDR R1, =CurrentTask
STR R0, [R1]
; 切换到下一个任务
LDR R1, =NextTask
LDR R0, [R1]
LDR R0, [R0] ; 获取新任务的栈顶
; 恢复新任务上下文
LDMIA R0!, {R4-R11} ; 弹出R4-R11
MSR PSP, R0 ; 更新PSP
ORR LR, LR, #0x04 ; 确保返回后使用PSP
BX LR ; 异常返回,自动弹出剩余寄存器
个人经验:写这段代码时,最容易出错的是LR的设置。LR的bit2决定返回后使用哪个堆栈指针:0用MSP,1用PSP。我刚开始总是忘记设置这个位,结果任务切换后CPU还在用MSP,新任务的栈根本没用上。调试时发现堆栈数据对不上,查了好久才找到原因。
好了,Cortex-M3/M4的架构基础就讲到这里。这些内容看起来枯燥,但确实是RTOS移植的基石。下一章我们会把这些知识串起来,真正动手写一个任务切换的Demo。到时候你会发现,理解了这些寄存器怎么用,写代码就像搭积木一样简单。