4. 中断配置实战:GPIO外部中断、定时器中断、串口中断

中断配置,说白了就是教会CPU什么时候该放下手里的活,去处理更紧急的事。我刚开始用STM32时,总觉得中断配置就是填几个寄存器,后来发现——填错一个位,系统就可能跑飞。今天咱们就拿STM32F4系列为例,把三种最常用的中断配置掰开揉碎讲清楚。

4.1 GPIO外部中断配置

外部中断是嵌入式系统里最基础、也最常用的中断类型。按键触发、传感器信号跳变、编码器脉冲捕获,都离不开它。

4.1.1 配置流程

  1. 使能GPIO时钟和SYSCFG时钟
  2. 配置GPIO为输入模式(通常带上拉或下拉)
  3. 将GPIO引脚连接到EXTI线(通过SYSCFG_EXTICR寄存器)
  4. 配置EXTI触发边沿(上升沿、下降沿或双边沿)
  5. 使能EXTI中断线(取消屏蔽)
  6. 配置NVIC优先级并使能中断
  7. 编写中断服务函数

核心要点: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 中断配置核心流程

下面这张图总结了三种中断配置的共性流程,你配置任何一种中断时都可以参考这个框架。

中断配置核心流程 ① 使能外设时钟 ② 配置GPIO/外设参数 ③ 配置中断触发条件 ④ 配置NVIC优先级 ⑤ 使能中断并启动外设 ⑥ 编写中断服务函数 关键提醒 ISR中必须清除中断标志 ISR执行时间要尽量短 注意中断优先级分组 常见错误 忘记使能外设时钟 NVIC未使能中断通道 中断优先级配置错误

4.6 中断配置避坑指南

  • 时钟使能顺序:必须先使能外设时钟,再配置寄存器。我曾经在调试时先配了GPIO再开时钟,结果寄存器写不进去,查了半天才发现是顺序反了。
  • 中断标志位清除:不同外设清除方式不同——EXTI需要软件写1清除,串口RXNE由读数据自动清除,定时器需要软件清除。一定要查手册确认。
  • NVIC优先级分组:整个系统只用一种优先级分组(通常用NVIC_PriorityGroup_2或Group_4),混用会导致优先级混乱。
  • 中断服务函数命名:必须使用启动文件中定义的中断向量名,比如USART1_IRQHandler,不能自己随便起名,否则链接不到。
  • 中断嵌套深度:不要把所有中断优先级都设成一样,至少留一个高优先级给时间敏感的中断(比如电机控制)。

个人习惯:我每次写完中断配置,都会先跑一个最小测试——在中断服务函数里翻转一个GPIO,用示波器看波形。这样能快速确认中断是否触发、频率是否正确、有没有误触发。等波形对了,再往里填业务逻辑。