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
实验流程:
- 主循环触发EXTI0中断(C)
- C执行到一半,USART1收到数据,触发中断B
- B执行到一半,TIM2溢出,触发中断A
- 观察中断执行顺序
// 配置优先级分组
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 中断嵌套流程图
下面这张图,把中断嵌套的流程画清楚了。你一看就明白。
4.5 避坑指南
做中断实验,有几个坑我替你们踩过了:
- 中断服务函数别写太长:我曾经在串口中断里做数据解析,结果数据量一大,其他中断全被堵死了。后来改成只收数据,解析放主循环。
- 优先级分组全局只设一次:NVIC_PriorityGroupConfig只能调用一次,否则会出问题。我见过有人每个外设初始化都调一次,结果系统跑飞了。
- 中断标志位必须清除:忘了清标志位,中断会一直触发。嗯,这个坑我至少踩过三次。
- 抢占优先级数值越小优先级越高:0最高,15最低。这个和直觉相反,我刚学时总搞反。
调试技巧:用逻辑分析仪抓GPIO电平变化,可以直观看到中断嵌套的执行时序。比printf靠谱多了,printf本身也会触发中断,干扰实验结果。
4.6 本章小结
三个实验做下来,你应该对STM32的中断机制有了直观认识。串口中断让你知道怎么响应外部事件,定时器中断让你掌握周期性任务,中断嵌套实验则让你理解优先级的重要性。
记住一句话:中断是嵌入式系统的灵魂,优先级是中断的灵魂。配置不好,系统就乱套了。