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。到时候你会发现,理解了这些寄存器怎么用,写代码就像搭积木一样简单。