1. 课程导入
做嵌入式开发这么多年,我接触过不少内核架构。从早期的 8051 到后来的 ARM7、ARM9,再到现在的 Cortex-M 系列。说实话,Cortex-M 是我个人最喜欢的一套内核。为什么?因为它把「实时性」和「低功耗」这两件事做到了极致。今天咱们就来扒一扒它的内核架构。
1.1 寄存器模型:程序员的第一张地图
Cortex-M 的寄存器模型,说白了就是一张「程序员可见的硬件资源地图」。你写的每一行 C 代码,最终都要落到这些寄存器上。
Cortex-M 系列(M0/M3/M4/M7)的寄存器组基本一致,主要分为以下几类:
| 寄存器组 | 寄存器名称 | 用途说明 |
|---|---|---|
| 通用寄存器 | R0-R12 | 数据运算、地址暂存,R0-R3 常用于传参 |
| 堆栈指针 | MSP / PSP | 主堆栈指针 / 进程堆栈指针,异常时自动切换 |
| 链接寄存器 | LR (R14) | 保存函数返回地址,异常时保存 EXC_RETURN |
| 程序计数器 | PC (R15) | 指向当前执行指令地址 |
| 特殊寄存器 | xPSR, PRIMASK, BASEPRI, FAULTMASK, CONTROL | 状态标志、中断屏蔽、特权等级控制 |
1.2 工作模式与特权等级:谁说了算?
Cortex-M 有两种工作模式:线程模式(Thread Mode)和处理模式(Handler Mode)。同时,还有两个特权等级:特权级(Privileged)和非特权级(Unprivileged)。
你想想看,为什么要有这些区分?
举个例子。我在做一款工业控制器时,客户要求系统必须足够安全——应用程序不能随便改系统配置。这时候,我就把 RTOS 内核跑在特权级,用户任务跑在非特权级。用户任务想访问系统寄存器?必须通过系统调用(SVC 异常)来请求。这样,即使某个任务崩了,也搞不乱整个系统。
关键点:
- 复位后默认进入线程模式 + 特权级
- 发生异常/中断时自动切换到处理模式 + 特权级
- 通过 CONTROL 寄存器可以切换到非特权级(一旦切下去,只能通过异常返回特权级)
1.3 异常处理机制:Cortex-M 的杀手锏
说到异常处理,Cortex-M 有一个非常巧妙的设计——硬件自动压栈。这一点跟传统的 ARM7 完全不同。ARM7 进中断要手动压栈,写汇编代码时稍不留神就漏掉一个寄存器。Cortex-M 呢?硬件帮你把 R0-R3、R12、LR、PC、xPSR 这 8 个寄存器自动压到栈上。中断返回时,硬件再自动弹栈。
为什么会这样设计?说白了,就是为了「确定性」。实时系统最怕的就是中断延迟不可控。硬件自动压栈把延迟时间固定下来了,你不需要在中断服务函数里写一堆 push/pop。
1.3.1 异常类型与优先级
Cortex-M 的异常分为系统异常和外部中断两大类。系统异常有固定的编号,外部中断的编号由芯片厂商定义。
| 异常编号 | 异常名称 | 优先级 | 说明 |
|---|---|---|---|
| 1 | Reset | -3(最高) | 系统复位,不可屏蔽 |
| 2 | NMI | -2 | 不可屏蔽中断,常用于看门狗 |
| 3 | HardFault | -1 | 硬错误,其他故障处理不了的归它管 |
| 4 | MemManage | 可编程 | 内存管理故障(MPU 触发) |
| 5 | BusFault | 可编程 | 总线错误,访问非法地址 |
| 6 | UsageFault | 可编程 | 用法错误,比如除零、未对齐访问 |
| 11 | SVC | 可编程 | 系统调用,用户任务切到内核的入口 |
| 12 | DebugMon | 可编程 | 调试监视器 |
| 14 | PendSV | 可编程 | 可挂起系统调用,RTOS 上下文切换的利器 |
| 15 | SysTick | 可编程 | 系统滴答定时器,RTOS 的心跳 |
| 16+ | 外部中断 IRQ | 可编程 | 芯片厂商定义的外设中断 |
1.3.2 中断嵌套与咬尾优化
Cortex-M 支持中断嵌套——高优先级中断可以打断低优先级中断。但这里有一个很多人忽略的细节:咬尾中断(Tail-Chaining)。
什么意思呢?假设当前正在处理中断 A,此时中断 B 来了。如果 B 的优先级比 A 高,硬件会暂停 A,压栈,然后处理 B。等 B 处理完,硬件发现 A 还在挂起状态,它不会做弹栈再压栈这种傻事,而是直接跳转到 A 的入口。这就是咬尾优化。省掉了两次栈操作,延迟能减少 12 个时钟周期左右。
我在做电机控制项目时,PWM 中断和编码器中断经常前后脚触发。咬尾优化帮了大忙,让控制环路的抖动控制在 100ns 以内。
1.4 知识结构总览
下面这张图把 Cortex-M 内核的核心要素串起来了。你可以把它当作本章的「导航图」。
1.5 实战要点:启动代码中的内核初始化
说了这么多理论,咱们来点实际的。Cortex-M 的启动代码(startup.s)里,有几个关键步骤跟内核架构直接相关:
- 设置栈顶指针:向量表第一个字就是栈顶地址,复位后硬件自动加载到 MSP。
- 设置向量表偏移:如果代码跑在 RAM 里(比如 IAP 升级),需要修改 SCB->VTOR 寄存器。
- 使能 Fault 异常:开发阶段建议把 MemManage、BusFault、UsageFault 都使能,别等出问题了再抓瞎。
- 配置 SysTick:RTOS 的心跳,通常设置为 1ms 或 10ms 中断一次。
一个实用的启动代码片段(C 语言风格):
// 使能 UsageFault、BusFault、MemManage 异常
SCB->SHCSR |= (SCB_SHCSR_USGFAULTENA_Msk |
SCB_SHCSR_BUSFAULTENA_Msk |
SCB_SHCSR_MEMFAULTENA_Msk);
// 设置 SysTick 为 1ms 中断(假设系统时钟 72MHz)
SysTick->LOAD = 72000 - 1;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_ENABLE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_CLKSOURCE_Msk;
好了,这一章的内容就到这里。Cortex-M 的寄存器模型、工作模式和异常处理机制,是后面所有章节的基础。把这些搞透了,后面讲低功耗模式、外设接口开发,你才能理解为什么 Cortex-M 能做得那么「聪明」。