1. ARM Cortex-M0+ 内核架构速览:寄存器组、工作模式、异常处理与向量表、存储器映射

各位同学,咱们今天正式开讲。

做嵌入式开发,说白了就是跟芯片内核打交道。你写再多应用层代码,最后都得落到寄存器上。Cortex-M0+ 这个内核,我用了好多年,从早期的智能家居项目到后来的医疗设备,它一直是我的首选。为什么?因为它够简单、够省电、够便宜。

嗯,咱们今天就把它的底裤扒开看看。

1.1 寄存器组:你手头的工具

Cortex-M0+ 的寄存器组,其实没几个。比起那些动辄几十个寄存器的大家伙,它清爽得很。我个人习惯,先把寄存器分成两类:通用寄存器和特殊寄存器。

1.1.1 通用寄存器(R0-R12)

R0 到 R12,一共13个。32位宽,干啥都行。你可以把它们想象成你桌上的13个便签纸,临时记点数据、算点东西,都用它们。

  • R0-R7:低组寄存器。所有指令都能访问,最常用。
  • R8-R12:高组寄存器。部分指令(比如16位Thumb指令)访问不了,得小心。
我的经验: 写中断服务函数时,我习惯把频繁使用的变量放在 R4-R7 里。为什么?因为编译器压栈时,R0-R3 是自动保存的,R4-R11 得手动来。你想想看,省一次压栈,中断响应就快一截。

1.1.2 堆栈指针(R13/SP)

两个堆栈指针:主堆栈指针(MSP)和进程堆栈指针(PSP)。

复位后默认用 MSP。我刚开始做项目时,一直搞不懂为啥要两个。后来做 RTOS 移植,才明白——MSP 给异常处理用,PSP 给任务用。这样任务栈崩了,异常还能跑,不至于死得不明不白。

1.1.3 链接寄存器(R14/LR)

函数调用时,LR 存返回地址。这个好理解。但有个坑——异常发生时,LR 会被硬件自动修改成一个特殊值(EXC_RETURN)。

注意: 我曾经在调试一个中断嵌套的 bug 时,发现 LR 的值变成了 0xFFFFFFF9。查了半天手册才明白,这是指示「返回后使用 MSP,并回到线程模式」。如果你手动改了 LR,中断返回就乱套了。

1.1.4 程序计数器(R15/PC)

指向当前执行的指令地址。Cortex-M0+ 有个特点:PC 的最低位必须是 1,表示 Thumb 模式。如果你不小心写了个 0,硬件直接进 fault。

1.2 工作模式:两种模式,两种人生

Cortex-M0+ 只有两种模式:线程模式处理模式

  • 线程模式:跑普通代码的地方。可以运行在特权级或非特权级。
  • 处理模式:跑异常/中断的地方。永远在特权级。

说白了,处理模式就是「领导模式」——只有它才能碰系统控制寄存器。线程模式是「员工模式」,你给它非特权级,它连改个中断优先级都做不到。

我做过一个安全要求很高的血压计项目,就把用户代码放在非特权级线程模式,系统服务放在处理模式。这样用户代码再怎么乱搞,也动不了系统配置。

1.3 异常处理与向量表:中断的交通指挥

1.3.1 异常类型

Cortex-M0+ 有 32 个中断源(IRQ 0-31),加上系统异常,总共 48 个。常用的系统异常就这几个:

异常编号 名称 优先级 说明
1 Reset -3(最高) 上电复位,没得商量
2 NMI -2 不可屏蔽中断,比如看门狗
3 HardFault -1 所有 fault 的最终归宿
11 SVCall 可编程 系统调用,RTOS 常用
14 PendSV 可编程 可挂起的中断,任务切换用
15 SysTick 可编程 系统滴答定时器

1.3.2 向量表:中断的通讯录

向量表就是一张地址表,每个异常对应一个入口地址。复位后,硬件从 0x00000000 取栈顶指针,从 0x00000004 取 Reset_Handler 地址。

我见过不少新手,写启动文件时把向量表放错了位置,结果一上电就跑飞。嗯,这里要注意:向量表可以重定位,通过 SCB->VTOR 寄存器设置新地址。但 M0+ 的 VTOR 寄存器只在特权级可写,而且要求地址必须 256 字节对齐。

// 向量表重定位示例
#define VECTOR_TABLE_ADDR 0x20000000  // 放在 SRAM 里

void vector_table_relocate(void)
{
    // 先复制向量表到 SRAM
    memcpy((void*)VECTOR_TABLE_ADDR, (void*)0x00000000, 256);
    
    // 设置 VTOR 寄存器
    SCB->VTOR = VECTOR_TABLE_ADDR;
    
    // 别忘了开内存的写缓冲,不然速度慢得你想哭
    __DSB();
    __ISB();
}
避坑指南: 我曾经在一个量产项目中,因为向量表没对齐,导致中断偶尔响应错乱。查了三天,最后发现是 VTOR 写了个非 256 对齐的地址。从那以后,我每次写 VTOR 都要用宏检查一下:(addr & 0xFF) == 0。

1.4 存储器映射:你的地盘你做主

Cortex-M0+ 的存储器映射是固定的,4GB 地址空间分成了几个大区:

  • 0x00000000 - 0x1FFFFFFF:代码区。通常放 Flash,也可以放 SRAM(如果支持)。
  • 0x20000000 - 0x3FFFFFFF:SRAM 区。位带(Bit-banding)功能就在这里。
  • 0x40000000 - 0x5FFFFFFF:外设区。GPIO、UART、TIM 都在这。
  • 0xE0000000 - 0xE00FFFFF:系统控制区。NVIC、SCB、SysTick 等内核外设。

你想想看,这个映射是 ARM 定死的,芯片厂商只能在外设区里自由发挥。所以,你换不同厂家的 M0+ 芯片,内核寄存器地址都一样,变的只是外设地址。

1.4.1 位带操作:用内存地址操作单个位

M0+ 支持位带操作。说白了,就是把一个 32 位字的某一位,映射到一个单独的地址上。你读写这个地址,就等于读写那个位。

我当年做 LED 点阵屏驱动时,用位带操作控制 GPIO 输出,速度比读-改-写快了一倍。代码也清爽:

// 位带地址计算宏
#define BITBAND_SRAM(addr, bit)  ((0x22000000) + (((uint32_t)(addr) - 0x20000000) * 32) + (bit * 4))
#define BITBAND_PERIPH(addr, bit) ((0x42000000) + (((uint32_t)(addr) - 0x40000000) * 32) + (bit * 4))

// 使用示例:直接操作 GPIOA 的 Pin 5
#define GPIOA_ODR_ADDR  0x40020014  // 假设 GPIOA 基址
#define PA5             BITBAND_PERIPH(GPIOA_ODR_ADDR, 5)

void set_pa5_high(void)
{
    *(volatile uint32_t*)PA5 = 1;  // 直接写位带地址
}
小技巧: 位带操作虽然快,但别滥用。我见过有人把整个外设寄存器都用位带访问,结果代码可读性极差。我的原则是:只有对时间要求苛刻的位操作才用位带,比如中断标志清除、GPIO 翻转。

1.5 小结:内核架构的骨架

好了,咱们把 Cortex-M0+ 的骨架搭起来了。寄存器组是你的工具箱,工作模式是你的权限等级,异常处理是你的应急机制,存储器映射是你的地盘划分。

我个人觉得,学内核架构最忌讳的就是死记硬背。你只要记住:寄存器是干活的手,异常是处理意外的脑,映射是划分地盘的墙。剩下的细节,用到的时候查手册就行。

下一章,咱们会深入启动流程,看看芯片上电后到底干了哪些事。到时候,我会拿一个真实的血压计项目启动代码来拆解。敬请期待。