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 状态标志、中断屏蔽、特权等级控制
我的习惯: 调试时我总爱盯着 R13(SP)和 R14(LR)看。有一次排查栈溢出,就是发现 MSP 的值跑到了 SRAM 边界之外。嗯,从那以后我写启动代码都会在栈底放一个魔术字,用来检测栈溢出。

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 可编程 芯片厂商定义的外设中断
我曾经踩过的坑: 有一次调试一个 HardFault,查了两天才发现是中断服务函数里用了一个未对齐的指针做 32 位访问。Cortex-M4 默认是允许未对齐访问的,但如果你使能了 UsageFault,它就会触发异常。所以我的建议是:开发阶段把所有 Fault 异常都使能,别偷懒。

1.3.2 中断嵌套与咬尾优化

Cortex-M 支持中断嵌套——高优先级中断可以打断低优先级中断。但这里有一个很多人忽略的细节:咬尾中断(Tail-Chaining)。

什么意思呢?假设当前正在处理中断 A,此时中断 B 来了。如果 B 的优先级比 A 高,硬件会暂停 A,压栈,然后处理 B。等 B 处理完,硬件发现 A 还在挂起状态,它不会做弹栈再压栈这种傻事,而是直接跳转到 A 的入口。这就是咬尾优化。省掉了两次栈操作,延迟能减少 12 个时钟周期左右。

我在做电机控制项目时,PWM 中断和编码器中断经常前后脚触发。咬尾优化帮了大忙,让控制环路的抖动控制在 100ns 以内。

1.4 知识结构总览

下面这张图把 Cortex-M 内核的核心要素串起来了。你可以把它当作本章的「导航图」。

ARM Cortex-M 内核架构知识结构 Cortex-M 内核 寄存器模型 工作模式与特权 异常处理机制 通用寄存器 R0-R12 堆栈指针 MSP / PSP 特殊寄存器组 线程模式 处理模式 特权级 / 非特权级 系统异常 + 外部中断 硬件自动压栈 咬尾 / 晚到优化 三大模块相互配合,构成了 Cortex-M 高效、确定性的实时响应能力

1.5 实战要点:启动代码中的内核初始化

说了这么多理论,咱们来点实际的。Cortex-M 的启动代码(startup.s)里,有几个关键步骤跟内核架构直接相关:

  1. 设置栈顶指针:向量表第一个字就是栈顶地址,复位后硬件自动加载到 MSP。
  2. 设置向量表偏移:如果代码跑在 RAM 里(比如 IAP 升级),需要修改 SCB->VTOR 寄存器。
  3. 使能 Fault 异常:开发阶段建议把 MemManage、BusFault、UsageFault 都使能,别等出问题了再抓瞎。
  4. 配置 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;
我的建议: 别直接用厂商的启动代码就完事了。花 10 分钟读一遍 startup.s,把向量表、堆栈初始化、异常入口这些搞明白。我见过太多人遇到 HardFault 就懵了,其实只要看一眼 LR 里的 EXC_RETURN 值,就能判断出是从 MSP 还是 PSP 栈空间出的问题。

好了,这一章的内容就到这里。Cortex-M 的寄存器模型、工作模式和异常处理机制,是后面所有章节的基础。把这些搞透了,后面讲低功耗模式、外设接口开发,你才能理解为什么 Cortex-M 能做得那么「聪明」。