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指令)访问不了,得小心。
1.1.2 堆栈指针(R13/SP)
两个堆栈指针:主堆栈指针(MSP)和进程堆栈指针(PSP)。
复位后默认用 MSP。我刚开始做项目时,一直搞不懂为啥要两个。后来做 RTOS 移植,才明白——MSP 给异常处理用,PSP 给任务用。这样任务栈崩了,异常还能跑,不至于死得不明不白。
1.1.3 链接寄存器(R14/LR)
函数调用时,LR 存返回地址。这个好理解。但有个坑——异常发生时,LR 会被硬件自动修改成一个特殊值(EXC_RETURN)。
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();
}
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; // 直接写位带地址
}
1.5 小结:内核架构的骨架
好了,咱们把 Cortex-M0+ 的骨架搭起来了。寄存器组是你的工具箱,工作模式是你的权限等级,异常处理是你的应急机制,存储器映射是你的地盘划分。
我个人觉得,学内核架构最忌讳的就是死记硬背。你只要记住:寄存器是干活的手,异常是处理意外的脑,映射是划分地盘的墙。剩下的细节,用到的时候查手册就行。
下一章,咱们会深入启动流程,看看芯片上电后到底干了哪些事。到时候,我会拿一个真实的血压计项目启动代码来拆解。敬请期待。