4. 中断系统与NVIC:中断优先级分组、外部中断EXTI、模块化封装中断驱动、实战:按键中断控制LED
中断,是嵌入式系统的灵魂。没有中断的MCU,就像一个人只能排队做事情,效率极低。我刚开始学STM32时,对中断的理解就是“一个函数被自动调用”。后来踩了不少坑才明白——中断的配置、优先级管理、响应时序,每一个细节都藏着雷。
这一章,我们就把中断系统彻底讲透。从NVIC的优先级分组,到外部中断EXTI的配置,再到如何写出可复用的中断驱动模块。最后,用一个按键中断控制LED的实战,把知识点串起来。
4.1 NVIC与中断优先级分组
NVIC(Nested Vectored Interrupt Controller),嵌套向量中断控制器。它是Cortex-M3内核的中断管家。STM32的所有中断,最终都由NVIC来调度。
优先级分组,是NVIC最核心的概念。说白了,就是决定“抢占优先级”和“子优先级”各占多少位。
| 分组编号 | 抢占优先级位数 | 子优先级位数 | 优先级总数 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 bit | 4 bits | 16级子优先级 |
| NVIC_PriorityGroup_1 | 1 bit | 3 bits | 2级抢占 × 8级子优先级 |
| NVIC_PriorityGroup_2 | 2 bits | 2 bits | 4级抢占 × 4级子优先级 |
| NVIC_PriorityGroup_3 | 3 bits | 1 bit | 8级抢占 × 2级子优先级 |
| NVIC_PriorityGroup_4 | 4 bits | 0 bit | 16级抢占优先级 |
我个人习惯使用NVIC_PriorityGroup_2。4级抢占优先级,4级子优先级,够用且灵活。高实时性的中断(比如定时器、通信)给高抢占优先级,普通中断给低抢占优先级。
4.2 外部中断EXTI详解
EXTI(External Interrupt/Event Controller),外部中断/事件控制器。它负责检测GPIO引脚上的信号变化,并产生中断请求。
EXTI的配置流程,其实不复杂。但有几个关键点,容易出错。
- 选择中断线:EXTI有16条中断线(EXTI0~EXTI15),每条线对应一组GPIO引脚。比如PA0、PB0、PC0都映射到EXTI0。同一时间,只能有一个引脚连接到某条中断线。
- 配置触发方式:上升沿触发、下降沿触发、或者双边沿触发。
- 使能中断线:在EXTI寄存器中使能对应的中断线。
- 配置NVIC:使能对应的中断通道,设置优先级。
关键点:GPIO的AFIO时钟必须使能!我见过有人配置了EXTI,但忘了开AFIO时钟,中断死活不触发。AFIO时钟在RCC_APB2PeriphClockCmd()中使能。
举个例子,配置PA0为下降沿触发的外部中断:
// 1. 使能GPIOA和AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置PA0为输入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 连接EXTI线到PA0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// 4. 配置EXTI
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 5. 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
4.3 模块化封装中断驱动
裸机编程最容易出现的问题,就是中断服务函数里塞了太多逻辑。你想想看,中断里做延时、做复杂计算、甚至调用printf——系统不崩才怪。
我的做法是:中断服务函数只做一件事——设置一个标志位,或者把数据放到一个环形缓冲区。真正的处理逻辑,放在主循环里。
下面是一个模块化的按键中断驱动示例:
// key_interrupt.h
#ifndef __KEY_INTERRUPT_H
#define __KEY_INTERRUPT_H
#include "stm32f10x.h"
// 按键中断回调函数类型
typedef void (*KeyInterruptCallback)(void);
// 初始化按键中断
void KeyInterrupt_Init(void);
// 注册按键按下回调函数
void KeyInterrupt_RegisterCallback(KeyInterruptCallback callback);
#endif
// key_interrupt.c
#include "key_interrupt.h"
static KeyInterruptCallback key_press_callback = NULL;
void KeyInterrupt_Init(void)
{
// 配置GPIO、EXTI、NVIC(代码同上)
// ...
}
void KeyInterrupt_RegisterCallback(KeyInterruptCallback callback)
{
key_press_callback = callback;
}
// 中断服务函数
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line0);
// 调用回调函数
if (key_press_callback != NULL)
{
key_press_callback();
}
}
}
我的习惯:回调函数里不要做耗时操作。如果需要在中断里处理数据,用环形缓冲区。主循环定期检查缓冲区,处理数据。这样既保证了中断的实时性,又避免了中断嵌套过深。
4.4 实战:按键中断控制LED
理论说完了,我们来写一个完整的例子。按下按键,LED状态翻转。
硬件连接:
- 按键:PA0,外部中断下降沿触发
- LED:PC13,低电平点亮
// main.c
#include "stm32f10x.h"
#include "key_interrupt.h"
// LED控制函数
void LED_Toggle(void)
{
static uint8_t led_state = 0;
if (led_state == 0)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
led_state = 1;
}
else
{
GPIO_SetBits(GPIOC, GPIO_Pin_13);
led_state = 0;
}
}
int main(void)
{
// 配置LED引脚
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
// 初始化按键中断
KeyInterrupt_Init();
// 注册回调函数
KeyInterrupt_RegisterCallback(LED_Toggle);
while (1)
{
// 主循环可以干其他事情
}
}
这个例子看起来简单,但背后涉及了中断配置、回调机制、模块化设计。你想想看,如果以后要增加第二个按键,只需要再注册一个回调函数,完全不需要修改中断服务函数。
注意:按键存在机械抖动。按下和释放的瞬间,会产生多个边沿跳变。如果直接触发中断,LED可能会闪烁多次。解决办法:硬件上并联一个100nF电容,或者软件上做10~20ms的延时消抖。我个人推荐硬件消抖,简单可靠。
4.5 中断系统的知识结构
为了让你更直观地理解中断系统的整体架构,我画了一张图:
从这张图可以看得很清楚:中断源产生请求,NVIC根据优先级决定是否响应,CPU执行对应的ISR。ISR里只做最轻量级的操作,真正的业务逻辑在主循环中处理。这个架构,我在多个项目中验证过,稳定可靠。
4.6 中断调试的常见问题
最后,分享几个我在调试中断时遇到的典型问题:
- 中断不触发:检查AFIO时钟是否使能,检查NVIC是否使能,检查中断线是否被其他引脚占用。
- 中断频繁触发:检查中断标志位是否在ISR中清除,检查外部信号是否有抖动。
- 中断响应慢:检查是否有更高优先级的中断在频繁执行,检查ISR中是否有耗时操作。
- 中断嵌套导致栈溢出:检查中断嵌套深度,适当增大栈空间。
调试技巧:在ISR中设置一个GPIO引脚,用示波器观察引脚电平变化。如果引脚电平变化频率异常,说明中断触发有问题。这个方法比printf高效得多。
中断系统是STM32开发的基础,也是难点。把这一章的内容吃透,后面的定时器、通信、ADC等外设驱动,都会轻松很多。