4. 中断配置实战:GPIO外部中断、定时器中断、串口中断
中断配置,说白了就是教会CPU什么时候该放下手里的活,去处理更紧急的事。我刚开始用STM32时,总觉得中断配置就是填几个寄存器,后来发现——填错一个位,系统就可能跑飞。今天咱们就拿STM32F4系列为例,把三种最常用的中断配置掰开揉碎讲清楚。
4.1 GPIO外部中断配置
外部中断是嵌入式系统里最基础、也最常用的中断类型。按键触发、传感器信号跳变、编码器脉冲捕获,都离不开它。
4.1.1 配置流程
- 使能GPIO时钟和SYSCFG时钟
- 配置GPIO为输入模式(通常带上拉或下拉)
- 将GPIO引脚连接到EXTI线(通过SYSCFG_EXTICR寄存器)
- 配置EXTI触发边沿(上升沿、下降沿或双边沿)
- 使能EXTI中断线(取消屏蔽)
- 配置NVIC优先级并使能中断
- 编写中断服务函数
核心要点:GPIO引脚与EXTI线的映射关系是固定的——PA0映射到EXTI0,PB0也映射到EXTI0。同一时刻只能有一个引脚连接到某条EXTI线。我在项目中遇到过两个按键分别挂在PA0和PB0上,结果只有一个能触发中断,查了半天才发现是映射冲突。
4.1.2 代码示例
// 配置PA0为外部中断(上升沿触发)
void EXTI_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
EXTI_InitTypeDef EXTI_InitStruct = {0};
NVIC_InitTypeDef NVIC_InitStruct = {0};
// 1. 使能时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
// 2. 配置GPIO
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_DOWN; // 下拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 连接EXTI线
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
// 4. 配置EXTI
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 5. 配置NVIC
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
// 中断服务函数
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 处理中断事件
// ... 你的业务逻辑
// 清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
注意:中断服务函数里一定要清除中断标志位,否则会一直进中断。我曾经见过一个同事忘了清标志位,结果系统卡死在中断里,看门狗都救不回来。
4.2 定时器中断配置
定时器中断是嵌入式系统的心跳。周期性采样、PWM生成、延时控制、系统调度,都靠它。STM32的定时器资源丰富,从基本定时器到高级定时器,功能逐级增强。
4.2.1 定时器中断配置步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 使能定时器时钟 | 根据定时器挂载的APB总线使能 |
| 2 | 配置预分频器(PSC) | 决定计数时钟频率 |
| 3 | 配置自动重装载值(ARR) | 决定中断周期 |
| 4 | 使能更新中断 | 允许定时器产生中断 |
| 5 | 配置NVIC | 设置优先级并使能 |
| 6 | 使能定时器 | 开始计数 |
4.2.2 代码示例
// 配置TIM3,产生1ms定时中断(假设系统时钟168MHz,APB1时钟84MHz)
void TIM3_Int_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct = {0};
NVIC_InitTypeDef NVIC_InitStruct = {0};
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 2. 配置定时器基础参数
TIM_TimeBaseStruct.TIM_Prescaler = 84 - 1; // 84MHz / 84 = 1MHz (1us计数一次)
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStruct.TIM_Period = 1000 - 1; // 1000次计数 = 1ms
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct);
// 3. 使能更新中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
// 4. 配置NVIC
NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 5. 使能定时器
TIM_Cmd(TIM3, ENABLE);
}
// 中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
// 1ms定时任务
// ... 你的周期性处理代码
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
经验之谈:计算定时器周期时,我习惯先确定计数频率,再算重装载值。比如要1ms中断,先让计数器以1MHz跑(1us计数一次),然后ARR设999,这样刚好1000次溢出一次。你想想看,如果直接拿84MHz去算,很容易算错。
4.3 串口中断配置
串口中断是通信场景下的主力。接收不定长数据、处理高波特率数据流、实现应答协议,都依赖串口中断。相比轮询方式,中断方式能大幅提升CPU利用率。
4.3.1 串口中断类型
| 中断类型 | 触发条件 | 典型用途 |
|---|---|---|
| RXNE(接收非空) | 接收到一个字节 | 逐字节接收数据 |
| TC(发送完成) | 数据移位寄存器发送完毕 | 发送完成后释放缓冲区 |
| TXE(发送寄存器空) | 数据寄存器已空 | 连续发送多字节 |
| IDLE(总线空闲) | 接收总线空闲超过1帧时间 | 判断一帧数据接收完毕 |
4.3.2 代码示例
// 配置USART1,115200波特率,使能接收中断
void USART1_Int_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
USART_InitTypeDef USART_InitStruct = {0};
NVIC_InitTypeDef NVIC_InitStruct = {0};
// 1. 使能时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// 2. 配置GPIO复用功能
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); // TX
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); // RX
// 3. 配置USART
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStruct);
// 4. 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 5. 配置NVIC
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 6. 使能USART
USART_Cmd(USART1, ENABLE);
}
// 中断服务函数
void USART1_IRQHandler(void)
{
uint8_t data;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
data = USART_ReceiveData(USART1);
// 处理接收到的字节
// ... 放入环形缓冲区或直接处理
// RXNE标志位由读取数据寄存器自动清除
}
}
避坑指南:我曾经在调试串口中断时,发现接收数据偶尔会丢字节。排查了半天,原来是中断服务函数里做了太多处理,导致下一个字节到达时还没退出中断。记住:串口中断服务函数里只做最轻量的操作——把数据扔进缓冲区就退出,解析处理放到主循环里做。
4.4 三种中断配置对比
| 对比维度 | GPIO外部中断 | 定时器中断 | 串口中断 |
|---|---|---|---|
| 触发源 | 外部引脚电平变化 | 定时器计数溢出 | 串口数据收发事件 |
| 中断频率 | 由外部事件决定 | 固定周期(可配置) | 由通信速率决定 |
| 配置复杂度 | 中等(需映射EXTI线) | 低(PSC+ARR即可) | 中等(需配置GPIO复用) |
| 典型应用 | 按键、传感器、编码器 | 周期性采样、PWM、延时 | 串口通信、AT指令解析 |
| 中断服务函数耗时要求 | 极短(避免丢失边沿) | 必须小于中断周期 | 极短(避免溢出丢数据) |
4.5 中断配置核心流程
下面这张图总结了三种中断配置的共性流程,你配置任何一种中断时都可以参考这个框架。
4.6 中断配置避坑指南
- 时钟使能顺序:必须先使能外设时钟,再配置寄存器。我曾经在调试时先配了GPIO再开时钟,结果寄存器写不进去,查了半天才发现是顺序反了。
- 中断标志位清除:不同外设清除方式不同——EXTI需要软件写1清除,串口RXNE由读数据自动清除,定时器需要软件清除。一定要查手册确认。
- NVIC优先级分组:整个系统只用一种优先级分组(通常用NVIC_PriorityGroup_2或Group_4),混用会导致优先级混乱。
- 中断服务函数命名:必须使用启动文件中定义的中断向量名,比如USART1_IRQHandler,不能自己随便起名,否则链接不到。
- 中断嵌套深度:不要把所有中断优先级都设成一样,至少留一个高优先级给时间敏感的中断(比如电机控制)。
个人习惯:我每次写完中断配置,都会先跑一个最小测试——在中断服务函数里翻转一个GPIO,用示波器看波形。这样能快速确认中断是否触发、频率是否正确、有没有误触发。等波形对了,再往里填业务逻辑。