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会做这几件事:

  1. 入栈:硬件自动把R0-R3、R12、LR、PC、xPSR压入当前栈。
  2. 取向量:从向量表(通常在0x00000000)取出中断服务函数地址。
  3. 更新寄存器:PC指向中断服务函数,LR被设置成EXC_RETURN(一个特殊值,告诉CPU返回时用哪个栈)。
  4. 执行ISR:运行你的中断处理代码。
  5. 出栈:执行完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