4、中断实战:串口中断收发、定时器中断、中断嵌套与抢占优先级实验

中断,说白了就是CPU正在干一件事,突然被更紧急的事打断,处理完再回来接着干。我刚开始学STM32时,觉得中断就是个“回调函数”,后来踩了不少坑才明白——中断的优先级配置、嵌套逻辑、抢占与响应的区别,才是真正决定系统稳定性的关键。

这一章,我们直接动手。通过三个实验,把串口中断、定时器中断、中断嵌套和抢占优先级彻底搞明白。

4.1 实验一:串口中断收发

串口中断是最基础、最常用的中断场景。你想想看,如果CPU一直轮询等待串口数据,那啥也别干了。用中断,数据来了再处理,效率高得多。

4.1.1 硬件准备

  • STM32F103C8T6 开发板(我用的是最小系统板)
  • USB转TTL模块(CH340G,便宜好用)
  • 杜邦线若干
  • 串口调试助手(随便哪个都行)

4.1.2 代码实现

先配置USART1,开启接收中断。我个人习惯用HAL库,但底层逻辑必须清楚。

// 串口初始化
void USART1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    USART_InitTypeDef USART_InitStruct = {0};
    NVIC_InitTypeDef NVIC_InitStruct = {0};

    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

    // TX - PA9, RX - PA10
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 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);

    // 开启接收中断
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    // NVIC配置
    NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    USART_Cmd(USART1, ENABLE);
}

// 中断服务函数
void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        uint8_t data = USART_ReceiveData(USART1);
        // 这里处理接收到的数据,比如回显
        USART_SendData(USART1, data);
        while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    }
}
小技巧:串口中断里尽量少做事。我一般只把数据丢进环形缓冲区,主循环再处理。否则数据量一大,中断服务函数执行时间过长,会丢包。

4.2 实验二:定时器中断

定时器中断,说白了就是让CPU每隔固定时间干一件事。比如LED闪烁、数据采集、任务调度。

4.2.1 配置TIM2

我用TIM2做1ms中断。为什么是1ms?因为很多实时任务都需要这个时间粒度。

void TIM2_Init(void)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct = {0};
    NVIC_InitTypeDef NVIC_InitStruct = {0};

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 72MHz / 72 = 1MHz, 计数1000次 = 1ms
    TIM_TimeBaseStruct.TIM_Prescaler = 72 - 1;
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStruct.TIM_Period = 1000 - 1;
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);

    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    TIM_Cmd(TIM2, ENABLE);
}

void TIM2_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        // 每1ms执行一次
        // 比如翻转LED
        GPIO_ToggleBits(GPIOC, GPIO_Pin_13);
    }
}
注意:定时器中断频率别设太高。我曾经为了追求精度,把中断设到10μs一次,结果CPU 90%的时间都在进中断,主循环几乎跑不动。后来改成1ms,配合DMA,效果反而更好。

4.3 实验三:中断嵌套与抢占优先级

这才是本章的重头戏。中断嵌套,说白了就是高优先级中断可以打断低优先级中断。抢占优先级决定谁可以打断谁,响应优先级决定同抢占优先级时的处理顺序。

4.3.1 优先级分组

STM32的NVIC支持4位优先级,可以分成抢占优先级和响应优先级。分组方式如下:

分组 抢占优先级位数 响应优先级位数 说明
NVIC_PriorityGroup_0 0 4 无抢占,只有响应
NVIC_PriorityGroup_1 1 3 2级抢占,8级响应
NVIC_PriorityGroup_2 2 2 4级抢占,4级响应
NVIC_PriorityGroup_3 3 1 8级抢占,2级响应
NVIC_PriorityGroup_4 4 0 16级抢占,无响应

我个人习惯用Group_2,4级抢占够用,4级响应也灵活。你想想看,如果抢占位数太多,配置起来容易乱;太少,又不够用。

4.3.2 嵌套实验设计

我们设计三个中断:

  • 中断A(高抢占优先级):TIM2中断,抢占优先级0,响应优先级0
  • 中断B(中抢占优先级):USART1中断,抢占优先级1,响应优先级0
  • 中断C(低抢占优先级):EXTI0中断,抢占优先级2,响应优先级0

实验流程:

  1. 主循环触发EXTI0中断(C)
  2. C执行到一半,USART1收到数据,触发中断B
  3. B执行到一半,TIM2溢出,触发中断A
  4. 观察中断执行顺序
// 配置优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// 中断A:TIM2 - 抢占0,响应0
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);

// 中断B:USART1 - 抢占1,响应0
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);

// 中断C:EXTI0 - 抢占2,响应0
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);
实验结果:执行顺序是C→B→A→A结束→B结束→C结束。高抢占优先级的中断A会打断B和C,B会打断C。这就是中断嵌套。

4.4 中断嵌套流程图

下面这张图,把中断嵌套的流程画清楚了。你一看就明白。

中断嵌套流程图 中断嵌套执行流程 主循环 中断C (抢占2) 中断B (抢占1) 中断A (抢占0) 执行顺序:主循环 → 中断C → 中断B → 中断A → 中断A结束 → 中断B结束 → 中断C结束 → 主循环 高抢占优先级的中断可以打断低抢占优先级的中断,形成嵌套

4.5 避坑指南

做中断实验,有几个坑我替你们踩过了:

  • 中断服务函数别写太长:我曾经在串口中断里做数据解析,结果数据量一大,其他中断全被堵死了。后来改成只收数据,解析放主循环。
  • 优先级分组全局只设一次:NVIC_PriorityGroupConfig只能调用一次,否则会出问题。我见过有人每个外设初始化都调一次,结果系统跑飞了。
  • 中断标志位必须清除:忘了清标志位,中断会一直触发。嗯,这个坑我至少踩过三次。
  • 抢占优先级数值越小优先级越高:0最高,15最低。这个和直觉相反,我刚学时总搞反。
调试技巧:用逻辑分析仪抓GPIO电平变化,可以直观看到中断嵌套的执行时序。比printf靠谱多了,printf本身也会触发中断,干扰实验结果。

4.6 本章小结

三个实验做下来,你应该对STM32的中断机制有了直观认识。串口中断让你知道怎么响应外部事件,定时器中断让你掌握周期性任务,中断嵌套实验则让你理解优先级的重要性。

记住一句话:中断是嵌入式系统的灵魂,优先级是中断的灵魂。配置不好,系统就乱套了。