4. 中断系统与NVIC驱动工程
在轮询方式下,CPU 必须不断读取外设状态寄存器来判断事件是否发生,这种方式在按键、串口接收、定时器溢出等异步事件面前效率极低。STM32 的中断系统让外设能够在事件发生时主动“通知”内核,CPU 暂停当前任务、跳转到对应的中断服务函数处理事件,处理完再返回原位置继续执行。本节围绕 EXTI 外部中断、NVIC 优先级分组、中断服务函数编写规范,以及按键中断的 BSP 注册机制展开。
一、EXTI 与 NVIC 的协作关系
STM32 的中断链路可以概括为:外设或 GPIO 检测到事件 → EXTI 边沿检测电路捕获上升沿/下降沿 → 置位挂起寄存器 EXTI_PR → 向 NVIC 发出中断请求 → NVIC 根据优先级分组和使能状态决定是否提交给 Cortex-M 内核 → 内核响应中断,读取向量表跳转到 ISR。
EXTI 负责“事件检测”,NVIC 负责“优先级仲裁与使能”。EXTI 的每条线可独立配置触发边沿(上升沿、下降沿或双边沿)、屏蔽(IMR)和挂起(PR)。以 STM32F103 为例,EXTI0~EXTI15 分别对应 GPIO 的 Px0~Px15,同一编号的引脚(如 PA0、PB0、PC0)共享一条 EXTI 线,因此不能同时把 PA0 和 PB0 都配置为 EXTI0 中断源,这是初学者常见的配置冲突。
二、NVIC 优先级分组与配置
Cortex-M3/M4 的每个中断拥有 4 位优先级位,通过 NVIC 的 AIRCR 寄存器中的 PRIGROUP 字段把这 4 位划分为“抢占优先级”和“子优先级”两部分。抢占优先级高的中断可以打断正在执行的低抢占优先级 ISR,形成嵌套;子优先级只在两个中断同时挂起时决定谁先响应,不能嵌套。
常用的分组是 NVIC_PRIORITYGROUP_2,即 2 位抢占 + 2 位子优先级,抢占和子优先级各 0~3 共 4 级。配置时先调用 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2),再对每个中断设置优先级和使能:
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// EXTI0 中断:抢占 1,子优先级 0
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
// EXTI15_10 中断:抢占 2,子优先级 1
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 2, 1);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
数值越小优先级越高。上例中 EXTI0 抢占优先级为 1,高于 EXTI15_10 的 2,因此 EXTI0 的 ISR 可以打断 EXTI15_10 的 ISR。若两个中断抢占优先级相同,则不能互相打断,只能按子优先级顺序排队。
三、中断服务函数编写规范
ISR 是中断响应的入口,编写时有几条硬性规范:
- 函数名必须与启动文件中的向量表一致,例如
EXTI0_IRQHandler、EXTI15_10_IRQHandler,拼写错误会导致中断触发后跳转到默认死循环。 - 执行时间尽量短,只做标志清除、数据搬运、置位事件标志等必要动作,耗时逻辑放到主循环处理。
- 必须清除中断挂起标志,EXTI 通过写 1 到
EXTI_PR对应位清除,HAL 库中调用__HAL_GPIO_EXTI_CLEAR_IT()或HAL_GPIO_EXTI_IRQHandler()完成。 - 共享变量加 volatile,防止编译器优化导致主循环读不到 ISR 的修改。
- 避免在 ISR 中调用阻塞函数,如
HAL_Delay、带超时的阻塞发送等。
使用 HAL 库时,标准写法是在 ISR 中调用 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_x),它会自动清除标志并回调 HAL_GPIO_EXTI_Callback()。用户只需重写回调函数,把业务逻辑放在回调里,这样中断入口与业务处理解耦,便于 BSP 层统一管理。
四、按键中断触发事件处理
以 PC13 按键为例,配置流程如下:
- 使能 GPIOC 时钟,把 PC13 配置为输入模式,并开启上拉(按键另一端接 GND,按下为低电平)。
- 把 PC13 复用为 EXTI13,选择下降沿触发(按下瞬间产生下降沿)。
- 配置 NVIC:设置优先级分组、EXTI15_10_IRQn 的抢占/子优先级并使能。
- 在
EXTI15_10_IRQHandler中调用HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13)。 - 重写
HAL_GPIO_EXTI_Callback(),判断引脚号后置位按键事件标志或翻转 LED。
需要注意机械按键的抖动问题。按下和释放瞬间会产生 5~20 ms 的抖动,若不加处理,一次按键可能触发多次中断。常见做法是在回调中记录时间戳,与上次触发时间比较,小于 20 ms 则忽略;或者只置位标志,由主循环做软件消抖。前者响应快但占用中断时间,后者把消抖放到主循环,ISR 更短,工程中更常用。
EXTI0_IRQHandler;若接在 PC13 上,则使用 EXTI15_10_IRQn 和 EXTI15_10_IRQHandler。EXTI 线号由引脚号决定,与端口无关,但同一线号只能选一个端口。五、BSP 中断注册机制
在 BSP 工程中,不建议把每个按键的业务逻辑直接写死在 ISR 或回调里,而是建立一层“中断注册”机制:BSP 层维护一张回调表,按键初始化时把处理函数注册进去,ISR 触发后根据引脚号查表调用对应回调。这样新增按键只需注册,不必修改中断入口代码。
一种简洁的实现方式是定义结构体数组:
typedef void (*key_callback_t)(uint16_t pin);
typedef struct {
uint16_t pin;
key_callback_t cb;
} key_isr_item_t;
static key_isr_item_t key_isr_table[KEY_MAX];
void BSP_Key_Register(uint16_t pin, key_callback_t cb)
{
for (int i = 0; i < KEY_MAX; i++) {
if (key_isr_table[i].pin == 0) {
key_isr_table[i].pin = pin;
key_isr_table[i].cb = cb;
return;
}
}
}
void HAL_GPIO_EXTI_Callback(uint16_t pin)
{
for (int i = 0; i < KEY_MAX; i++) {
if (key_isr_table[i].pin == pin && key_isr_table[i].cb) {
key_isr_table[i].cb(pin);
return;
}
}
}
应用层只需调用 BSP_Key_Register(GPIO_PIN_13, Key_ShortPress_Handler) 即可完成绑定。回调中通常只做“置位事件标志 + 记录时间戳”,主循环轮询标志后执行具体业务,从而保证 ISR 短小、业务可扩展、中断与逻辑分层清晰。
掌握 EXTI 与 NVIC 的协作、优先级分组规则、ISR 编写规范和 BSP 注册机制后,按键、编码器、外部传感器等异步事件都能以统一、可维护的方式接入工程,为后续串口、定时器等中断外设打下基础。