硬件基础:Cortex-M系列内核架构、NVIC中断控制器、SysTick定时器深度解析

做嵌入式低功耗实时系统,绕不开Cortex-M内核。我这些年调过的板子,从M0到M7几乎都碰过。说实话,很多性能问题、功耗问题,根子都在你对硬件理解得够不够深。这一章,咱们就把Cortex-M的底牌翻出来看看。

Cortex-M系列内核架构概览

Cortex-M系列是ARM专门为微控制器设计的。它跟Cortex-A(应用处理器)最大的区别是什么?说白了,M系列没有MMU,跑不了Linux,但它有极低的中断延迟和极简的编程模型。我刚开始从8051转到ARM时,最不习惯的就是它的寄存器组——只有R0-R15,加上特殊寄存器,清爽得很。

咱们先看一张整体架构图,把核心组件串起来:

Cortex-M 内核架构核心组件 处理器内核 ARMv7-M / ARMv8-M R0-R15 寄存器组 xPSR / PRIMASK / BASEPRI FAULTMASK / CONTROL 单周期乘/硬件除法 (M4/M7: FPU + DSP) NVIC 中断控制器 1-240 个外部中断 8-256 级可编程优先级 尾链技术 (Tail-Chaining) 咬尾中断 (Late-Arriving) 向量表重定位 中断优先级分组 SysTick 定时器 24 位递减计数器 可配置时钟源 自动重装载 中断异常 #15 OS Tick 基准 低功耗定时唤醒 AHB-Lite / APB 总线矩阵 哈佛架构:指令总线 (I-Code) | 数据总线 (D-Code) | 系统总线 (System) | 外设总线 (PPB) 存储器系统 Flash / SRAM MPU 内存保护单元 Cache (M7/M55) 调试与跟踪 SWD / JTAG 调试接口 ETM / ITM 跟踪 DWT 数据观察点 功耗管理 WFI / WFE 休眠指令 Sleep / DeepSleep 模式 唤醒中断控制器 (WIC) 核心要点:NVIC 和 SysTick 是 RTOS 调度的硬件基石,理解它们才能做好低功耗实时系统

NVIC:嵌套向量中断控制器

NVIC 是 Cortex-M 系列最亮眼的设计之一。我见过不少工程师在配置中断优先级时随便填,结果系统跑起来莫名其妙卡死——其实就是优先级嵌套出了问题。

NVIC 的核心能力

  • 中断数量:最多支持 240 个外部中断,具体看芯片厂商实现。比如 STM32F103 只用了 60 个。
  • 优先级位数:M3/M4/M7 支持 8 位优先级寄存器,但实际只用了高 4-8 位。M0/M0+ 只有 2 位,也就是 4 级优先级。
  • 优先级分组:通过 SCB->AIRCR 寄存器,把优先级拆成抢占优先级和子优先级。这个分组一旦定下来,整个系统的中断行为就定了。

优先级分组配置示例

// 设置优先级分组:3位抢占优先级 + 1位子优先级
// 即抢占优先级 0-7,子优先级 0-1
SCB->AIRCR = (0x5FA << 16) | (0x400 << 0);  // PRIGROUP = 4

// 使能 USART1 中断,优先级设为 3
NVIC_SetPriority(USART1_IRQn, 3);
NVIC_EnableIRQ(USART1_IRQn);

我的经验:我习惯把实时性要求高的中断(如定时器、DMA)放在抢占优先级 0-1,普通外设放在 2-3,非关键中断放在 4-7。这样高优先级中断不会被低优先级打断,延迟可控。

尾链技术(Tail-Chaining)

这是 NVIC 的杀手锏。传统 ARM7/9 处理中断时,每次都要压栈、弹栈。Cortex-M 的尾链技术,说白了就是:如果退出一个中断时,另一个中断已经在 pending,那就直接跳过去,省掉两次栈操作。我实测过,尾链能省下 12-16 个时钟周期。在高速中断场景下,这个优化非常可观。

咬尾中断(Late-Arriving)

这个更妙。当一个高优先级中断在低优先级中断的压栈过程中到达,NVIC 会直接放弃低优先级中断的压栈,转而处理高优先级中断。嗯,这里要注意:这个机制是硬件自动完成的,软件不需要任何干预。但如果你在中断里做了太多操作,这个优势就被浪费了。

SysTick 定时器:RTOS 的心跳

SysTick 是 Cortex-M 内核自带的 24 位递减定时器。为什么说它是 RTOS 的心跳?因为几乎所有的 RTOS——FreeRTOS、uC/OS、RT-Thread——都用它来产生系统 Tick 中断。

SysTick 寄存器

寄存器 地址 功能
SYST_CSR 0xE000E010 控制与状态寄存器:使能、时钟源选择、中断使能、计数标志
SYST_RVR 0xE000E014 重装载值寄存器:24 位有效,决定 Tick 周期
SYST_CVR 0xE000E018 当前值寄存器:读当前计数值,写清零
SYST_CALIB 0xE000E01C 校准寄存器:厂商提供的校准值,一般不用

我曾经踩过的坑:SysTick 的时钟源有两种选择——内核时钟(AHB)或 AHB/8。如果你用 AHB/8,而 AHB 频率是 72MHz,那 SysTick 就是 9MHz。这时候重装载值设成 9000,Tick 周期就是 1ms。但如果你切换了时钟源没改重装载值,Tick 周期就变了,RTOS 的时间片调度会直接乱掉。我调试过一个项目,系统跑着跑着就死机,查了两天才发现是时钟源配置问题。

SysTick 配置实战

// 假设系统时钟 72MHz,使用 AHB/8 = 9MHz
// 目标 Tick 周期 1ms,重装载值 = 9000 - 1 = 8999

void SysTick_Init(void)
{
    // 1. 关闭 SysTick 进行配置
    SysTick->CTRL = 0;
    
    // 2. 设置重装载值
    SysTick->LOAD = 8999;
    
    // 3. 清零当前值
    SysTick->VAL = 0;
    
    // 4. 配置并启动:使用 AHB/8 时钟源,使能中断,使能计数器
    SysTick->CTRL = (1 << 2) |  // CLKSOURCE = AHB/8
                    (1 << 1) |  // TICKINT = 使能中断
                    (1 << 0);   // ENABLE = 使能
}

// SysTick 中断处理函数
void SysTick_Handler(void)
{
    // RTOS 的 Tick 处理
    // 比如:任务时间片递减、定时器管理、任务调度
    xPortSysTickHandler();  // FreeRTOS 示例
}

中断延迟:实时系统的生命线

中断延迟,就是从硬件触发中断到 CPU 执行第一条中断服务程序指令的时间。Cortex-M 的中断延迟有多快?M3/M4 典型值是 12 个时钟周期,M0 是 16 个。相比之下,传统 ARM7 需要 24-42 个周期。这就是为什么 Cortex-M 能成为实时控制的首选。

影响中断延迟的因素有哪些?我列个清单:

  1. 优先级屏蔽:如果当前 CPU 在更高优先级中断里,或者 PRIMASK/BASEPRI 被设置,中断会被阻塞。
  2. 内存访问等待:如果 Flash 有等待周期,取向量表时会多花时间。建议把向量表放在 SRAM 里。
  3. 中断嵌套深度:嵌套越深,压栈次数越多,延迟越大。尾链技术能缓解这个问题。
  4. 中断服务程序长度:ISR 越长,阻塞其他中断的时间越长。我建议 ISR 只做标志位设置和数据搬运,复杂逻辑放任务里。

优化中断延迟的实用技巧

  • 向量表重定位到 SRAM:SCB->VTOR = (uint32_t)&__Vectors;
  • 使用 BASEPRI 代替 PRIMASK:BASEPRI 可以屏蔽低于某个优先级的所有中断,而不影响高优先级中断。
  • 中断服务程序用 __RAM_FUNC 属性放到 SRAM 执行,避免 Flash 等待。

低功耗模式与中断唤醒

Cortex-M 支持多种低功耗模式。最常用的是 WFI(Wait For Interrupt)和 WFE(Wait For Event)。WFI 会让 CPU 进入休眠,直到任意中断唤醒。WFE 则可以被事件唤醒,不一定要中断。

在低功耗设计中,SysTick 的角色很关键。很多 RTOS 在空闲任务里执行 WFI,然后靠 SysTick 中断唤醒。但问题来了:如果 SysTick 中断频率太高(比如 1ms),CPU 频繁唤醒,功耗反而降不下去。我做过一个项目,把 SysTick 从 1ms 改成 10ms,待机电流从 2mA 降到了 0.3mA。代价是任务调度精度降低了,但很多应用场景完全能接受。

我的建议:在低功耗场景下,可以考虑动态调整 SysTick 周期。系统忙时用 1ms,空闲时切换到 10ms 甚至 100ms。配合 WFI 和深度睡眠模式,能把功耗压到微安级别。

小结