3. Cortex-M基础架构:寄存器模型、存储器映射、中断与异常处理机制
好,咱们今天聊聊Cortex-M的“骨架”。
很多人一上来就撸代码,结果遇到奇怪的问题——比如中断进不去、变量莫名其妙被改、程序跑飞。说白了,都是对底层架构不熟。我当年刚转TinyML时也踩过这些坑,后来花了一周时间把架构啃透,才真正“开窍”。
这一章,咱们就把它掰开揉碎。你想想看,搞懂了寄存器模型、存储器映射、中断异常这三大块,后面移植模型、优化推理,心里就有底了。
3.1 寄存器模型:CPU的“工作台”
Cortex-M的寄存器,你可以理解成CPU干活时用的“临时工作台”。
它不像我们写代码用的变量,存在内存里。寄存器就在CPU内部,读写速度极快。我习惯把它们分成两类:通用寄存器和特殊功能寄存器。
3.4.1 通用寄存器组(R0-R15)
一共16个,编号从R0到R15。其中:
- R0-R12:随便用。编译器分配,存临时变量、函数参数。我在项目中调试时,经常盯着R0看,它往往藏着关键数据。
- R13(SP):栈指针。分主栈指针(MSP)和进程栈指针(PSP)。中断里默认用MSP,任务里用PSP。嗯,这里要注意,搞RTOS时别搞混了。
- R14(LR):链接寄存器。存函数返回地址。我曾经在中断里直接修改LR,结果返回时程序飞了……后来再也不敢乱动它。
- R15(PC):程序计数器。指向当前执行的指令。你没法直接写它,但可以通过跳转指令间接改。
重要:Cortex-M的寄存器是“银行切换”的。进入异常时,硬件自动压栈R0-R3、R12、LR、PC、xPSR。退出时自动出栈。这个机制让中断响应极快,但也意味着——别指望在中断里用R4-R11,它们不会被自动保存。
3.4.2 特殊功能寄存器
这些寄存器控制CPU的核心行为。我挑几个最常用的:
| 寄存器 | 全称 | 作用 |
|---|---|---|
| xPSR | 程序状态寄存器 | 记录运算结果标志(零、进位、溢出等) |
| PRIMASK | 优先级屏蔽寄存器 | 置1后屏蔽所有可屏蔽中断(除了NMI和HardFault) |
| FAULTMASK | 错误屏蔽寄存器 | 置1后连HardFault都屏蔽(慎用!) |
| BASEPRI | 基础优先级寄存器 | 屏蔽低于某优先级的所有中断 |
| CONTROL | 控制寄存器 | 选择栈指针(MSP/PSP)、决定特权级别 |
个人习惯:在TinyML的推理循环中,我会用BASEPRI临时屏蔽低优先级中断,防止关键计算被频繁打断。但记得用完立刻恢复,否则系统会“变聋”。
3.2 存储器映射:地址空间的“地图”
Cortex-M的地址空间是4GB(32位)。但芯片厂商不会全用上。他们按固定“区域”分配功能。你想想看,这就像一张城市地图——每个区干不同的事。
3.2.1 标准映射布局
ARM规定了一个“推荐”布局,大部分芯片都遵守:
| 地址范围 | 区域名称 | 典型用途 |
|---|---|---|
| 0x00000000 - 0x1FFFFFFF | Code区 | 存放程序代码、向量表。通常映射到Flash |
| 0x20000000 - 0x3FFFFFFF | SRAM区 | 存放变量、堆栈。掉电丢失 |
| 0x40000000 - 0x5FFFFFFF | 外设区 | GPIO、UART、TIM等寄存器都在这 |
| 0x60000000 - 0x9FFFFFFF | 外部RAM区 | 扩展内存(如SDRAM) |
| 0xE0000000 - 0xE00FFFFF | 系统控制区 | NVIC、SysTick、MPU等内核外设 |
避坑指南:我曾经在STM32上调试一个TinyML模型,发现推理结果总是不对。查了两天,最后发现是数组定义在了0x20000000之后的SRAM里,但编译器默认把未初始化变量放在了0x10000000的“备用SRAM”区域——那个区域没被MPU正确配置。所以,一定要看芯片手册的“Memory Map”章节,别想当然。
3.2.2 位带操作(Bit-Banding)
Cortex-M3/M4有个“黑科技”——位带区。说白了,就是把一个32位地址的某一位,映射到另一个地址空间的一个字。你写那个字,就等于改那个位。
为什么需要这个?因为很多外设寄存器需要“原子操作”。比如置位一个GPIO引脚,如果用“读-改-写”,中间可能被中断打断。位带操作是单次写,天然原子。
// 位带操作示例:直接置位GPIOA的第5位
// 假设GPIOA的ODR寄存器地址是0x40020014
// 位带别名地址 = 0x42000000 + (0x40020014 - 0x40000000)*32 + 5*4
#define BITBAND_SRAM(addr, bit) ((volatile uint32_t *)(0x22000000 + ((uint32_t)(addr) - 0x20000000)*32 + (bit)*4))
#define BITBAND_PERIPH(addr, bit) ((volatile uint32_t *)(0x42000000 + ((uint32_t)(addr) - 0x40000000)*32 + (bit)*4))
// 使用
*BITBAND_PERIPH(0x40020014, 5) = 1; // 置位
*BITBAND_PERIPH(0x40020014, 5) = 0; // 清零
我建议:在TinyML中,如果你需要频繁操作某个状态位(比如推理完成标志),用位带操作代替“读-改-写”,能省下好几个指令周期。积少成多,推理速度就上去了。
3.3 中断与异常处理机制:CPU的“应急响应系统”
中断和异常,是Cortex-M的“灵魂”。没有它们,你只能轮询——CPU啥也别干了,就盯着一个引脚看。
3.3.1 异常类型与优先级
Cortex-M把“突发事件”分成两类:
- 异常(Exception):CPU内部产生。比如HardFault(硬件错误)、SVCall(系统调用)、PendSV(可挂起系统调用)。
- 中断(Interrupt):外部设备产生。比如定时器溢出、UART收到数据、GPIO电平变化。
它们都有固定的编号。编号越小,优先级越高(数字越小越紧急)。
| 编号 | 名称 | 类型 | 说明 |
|---|---|---|---|
| 1 | Reset | 异常 | 复位,优先级最高(-3) |
| 2 | NMI | 异常 | 不可屏蔽中断(-2) |
| 3 | HardFault | 异常 | 硬件错误(-1) |
| 4-10 | MemManage/BusFault/UsageFault | 异常 | 存储器管理、总线、用法错误(可配置优先级) |
| 11 | SVCall | 异常 | 系统调用(SVC指令触发) |
| 12 | DebugMonitor | 异常 | 调试监控 |
| 14 | PendSV | 异常 | 可挂起系统调用(RTOS任务切换用) |
| 15 | SysTick | 异常 | 系统滴答定时器 |
| 16+ | IRQ0 - IRQn | 中断 | 芯片厂商定义的外部中断 |
关键点:优先级数值越小,优先级越高。但Cortex-M支持“抢占优先级”和“子优先级”的配置。通过SCB->AIRCR寄存器设置。我一般习惯用4位抢占优先级、0位子优先级——简单粗暴,不容易乱。
3.3.2 中断响应流程
当一个中断发生时,CPU会做这几件事:
- 入栈:硬件自动把R0-R3、R12、LR、PC、xPSR压入当前栈。
- 取向量:从向量表(通常在0x00000000)取出中断服务函数地址。
- 更新寄存器:PC指向中断服务函数,LR被设置成EXC_RETURN(一个特殊值,告诉CPU返回时用哪个栈)。
- 执行ISR:运行你的中断处理代码。
- 出栈:执行完ISR后,硬件自动出栈,恢复现场。
我曾经:在ISR里写了一个很长的循环,结果导致其他低优先级中断响应延迟。后来才意识到,ISR应该“短平快”——只做最必要的事(比如置一个标志位),具体处理放到主循环或任务里。这在TinyML中尤其重要,因为推理计算本身就很耗时,别让中断再添乱。
3.3.3 向量表与中断配置
向量表是中断的“通讯录”。每个异常/中断对应一个32位地址,指向对应的处理函数。
默认向量表在Flash起始地址(0x00000000)。但你可以通过SCB->VTOR寄存器重映射到SRAM——这在动态更新中断处理函数时很有用。
// 中断配置示例:使能一个外部中断(以STM32为例)
// 假设我们要使能EXTI0(PA0引脚)
// 1. 配置GPIO
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // 使能GPIOA时钟
GPIOA->MODER &= ~GPIO_MODER_MODER0; // 输入模式
// 2. 配置SYSCFG
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; // 使能SYSCFG时钟
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0; // 选择PA0
// 3. 配置EXTI
EXTI->IMR |= EXTI_IMR_IM0; // 不屏蔽中断
EXTI->RTSR |= EXTI_RTSR_TR0; // 上升沿触发
// 4. 配置NVIC
NVIC_SetPriority(EXTI0_IRQn, 2); // 设置优先级(数值越小越高)
NVIC_EnableIRQ(EXTI0_IRQn); // 使能中断
// 5. 编写ISR
void EXTI0_IRQHandler(void) {
if(EXTI->PR & EXTI_PR_PR0) { // 检查中断标志
// 处理中断
EXTI->PR |= EXTI_PR_PR0; // 清除标志
}
}
我建议:在TinyML项目中,把SysTick中断优先级设成最高(数值最小),因为它负责系统心跳。把推理相关的计算任务放在PendSV里,优先级设成最低。这样既能保证时间基准准确,又不会让推理被频繁打断。
3.4 小结:架构是地基,别偷懒
嗯,这一章内容不少。但说实话,搞TinyML的人最容易忽略的就是架构。你想想看,模型再牛,跑在错误的地址上、被中断频繁打断、或者寄存器配置错了,结果都是零。
我个人习惯是:拿到一块新芯片,第一件事不是跑Hello World,而是看它的Memory Map和中断向量表。花半小时搞清楚这些,后面能省下几天调试时间。
下一章,咱们聊聊Cortex-M的指令集和Thumb-2技术。到时候你会发现,很多TinyML的优化技巧,其实就藏在指令里。
核心要点回顾:
- 寄存器模型:R0-R15是工作台,特殊寄存器控制CPU行为
- 存储器映射:4GB空间分区域,Code、SRAM、外设、系统控制各司其职
- 中断异常:优先级数值越小越紧急,ISR要短平快
- 位带操作:原子操作利器,TinyML中能省指令周期
公众号:蓝海资料掘金营,微信deep3321