Cortex-M 中断模型为什么好用
和老派的 8 位单片机比,Cortex-M 的中断设计干净得多:中断向量表在内存里硬件映射,响应时自动压栈 R0–R3、R12、LR、PC、xPSR 八个寄存器,退出时自动出栈,C 函数里写中断服务程序根本不用考虑寄存器保护。这就是你在 STM32 上写中断能像写普通函数一样轻松的底层原因。
Cortex-M 的中断延迟是硬件固定的:从中断触发到进入 ISR,典型 12 个周期。这个数字是设计芯片时就定死的,不是软件优化出来的——这就是实时性的根基。
理解中断模型,要先搞清楚两件事:谁在打断谁、优先级怎么排。硬件外设触发中断信号,NVIC 裁决后通知内核挂起当前代码、跳转到对应 ISR,执行完再回来。整个过程对你的主循环透明。
NVIC:中断优先级与嵌套
NVIC 是嵌套向量中断控制器的缩写,是 Cortex-M 内核的一部分。它干两件事:挂起中断请求、按优先级裁决。Cortex-M 的优先级数字越小优先级越高(和直觉相反,记牢),而且它把优先级分成“抢占优先级”和“子优先级”两层——抢占优先级高的 ISR 可以打断正在执行的低优先级 ISR,实现中断嵌套。
// NVIC 优先级分组(STM32 HAL 示例)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
// 全部 4 bit 给抢占优先级,共 16 级
// 配置外部中断线 0:抢占优先级 2,子优先级 0
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
- 紧急中断最高看门狗、硬件错误、紧急停机信号给最高优先级。
- 时间敏感次之编码器输入捕获、PWM 周期中断给次高。
- 普通外设最低串口接收、按键消抖给最低优先级。
- 子优先级管并列两个同抢占优先级中断同时到,子优先级高的先执行。
外部中断 EXTI 实战
EXTI 是 STM32 的外部中断控制器,把 GPIO 引脚的电平变化转化成中断信号。最常见的用法是按键唤醒:平时 MCU 睡在低功耗模式,按键一按,EXTI 中断把它叫醒处理任务。
void EXTI0_IRQHandler(void) {
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
// 按键按下:加一个软消抖
if (HAL_GetTick() - lastKeyTick > 50) {
keyPressed = 1;
lastKeyTick = HAL_GetTick();
}
}
}
写 EXTI ISR 有两个铁律:进来第一件事是清中断标志位,不然退出后会立刻再次触发;ISR 里不要做耗时操作,按键检测这种事只设标志位,主循环里再处理。
定时器时基与 PWM 输出
定时器是 MCU 里最万能的外设:定时中断、PWM 输出、输入捕获、编码器解码全靠它。基础用法是让它按固定频率触发更新中断,当系统心跳用——比如每 1ms 进一次中断,给软件定时器做时基。
// 配置定时器 2:1MHz 计数频率,1000 分频 → 1ms 周期
htim2.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz
htim2.Init.Period = 1000 - 1; // 1MHz / 1000 = 1kHz
HAL_TIM_Base_Start_IT(&htim2);
// 更新中断回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
sysTickMs++; // 系统毫秒计数
if (sysTickMs % 1000 == 0) heartbeatToggle();
}
}
// PWM:驱动 LED 呼吸灯
htim3.Init.Period = 1000 - 1; // 1ms PWM 周期
htim3.Init.Prescaler = 72 - 1;
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
// 改 CCR1 寄存器就能调占空比,从 0 到 1000 线性扫
PWM 的本质就是:计数器从 0 数到 Period,数到 CCR 值时输出翻转,数到 Period 时复位。改 CCR 就改占空比,LED 呼吸灯、电机调速、舵机角度全是这个原理。
输入捕获与脉冲测量
输入捕获是定时器的另一个方向:外部信号引脚跳变时,硬件自动把当前定时器计数值锁存下来。用它可以测方波频率、脉冲宽度、超声波回波时间——CPU 完全不用干等。
- 测频率:两次上升沿的捕获值之差就是周期,倒数就是频率。
- 测脉宽:上升沿触发捕获一次,下降沿再捕获一次,差值就是高电平时长。
- 超声波:发出发射信号后开输入捕获,回波上升沿来时记录时间,乘声速除以 2 就是距离。
- 编码器模式:AB 两相正交信号接入两个捕获通道,硬件自动加减计数,直接读出转速和方向。
中断设计的常见陷阱
中断写得多了,坑都差不多。列几个最常见的。
- ISR 太长在中断里 delay、printf、浮点运算,会把高优先级中断饿死。
- 没清标志退出 ISR 前不清中断标志,直接进死循环反复触发。
- 共享变量没加 volatile主循环读的值被编译器优化成寄存器缓存,和 ISR 写的对不上。
- 优先级乱排低优先级 ISR 里调了高优先级才需要的函数,整个系统时序都乱。
- 中断里用 HAL_Delay那个函数靠 SysTick 中断递增,被当前 ISR 挡住就永远死等。
一套可复用的定时器中断模板
最后给一套在多个项目里都跑过的定时器中断模板:一个 1ms 硬件时基 + 多个软件定时器,主循环里跑任务,中断里只做计数。
volatile uint32_t tickMs = 0;
// 1ms 定时器更新中断
void TIM1_UP_IRQHandler(void) {
if (TIM1->SR & TIM_SR_UIF) {
TIM1->SR = ~TIM_SR_UIF; // 手动清标志
tickMs++;
}
}
// 软件定时器:每隔 ms 毫秒执行一次 callback
void schedulerRun(void) {
static uint32_t last10ms = 0, last100ms = 0, last1s = 0;
if (tickMs - last10ms >= 10) { last10ms = tickMs; task10ms(); }
if (tickMs - last100ms >= 100){ last100ms = tickMs; task100ms(); }
if (tickMs - last1s >= 1000){ last1s = tickMs; task1s(); }
}
中断快进快出、主循环慢工出细活——记住这八个字,Cortex-M 的实时编程就入门了。
建议的练习顺序:先用定时器中断让 LED 每 500ms 闪一下,再把它改成 PWM 呼吸灯,然后加一个外部中断按键,按下时改变呼吸节奏。三步走完,中断和定时器就算真用熟了。