STM32 USART 中断模式数据收发:从原理到源码级解析
一、为什么用中断模式?
串口数据收发常见三种模式:轮询(阻塞)、中断(非阻塞)、DMA。
对比维度 轮询模式 中断模式 CPU 参与方式 死等标志位,期间 CPU 被占用 硬件事件触发,CPU 空闲时干别的 实时性 依赖轮询周期,可能丢数据 字节到达立刻响应 代码复杂度 简单但低效 需要回调函数,稍复杂 适用场景 简单单字节交互 高频、不定长数据收发 核心结论:
中断模式比轮询高效得多,是非阻塞收发的标准做法。
本例用 UART 中断模式完成数据的接收、处理(大小写互换)、回传,并用 LED 指示传输状态。
二、核心机制:USART 如何连上 NVIC?
2.1 中断源的两种类型
- 外部中断(EXTI):处理来自 GPIO 引脚的外部信号,需经过 EXTI 外设。
- 片上外设中断:USART、定时器、I2C、SPI 等外设自带中断模块,直接接到 Cortex-M3 的 NVIC。
关键认知:USART 这类片上外设并不需要经过 EXTI,EXTI 是专门处理外部中断的。
2.2 标志位 → 中断请求 → NVIC
USART 内部有
USART 中断控制单元:当 状态寄存器(SR)中对应标志位被置位(收到数据 RXNE=1、发送寄存器空 TXE=1),且 USART 中断被使能,USART 就会向 NVIC 发起中断请求。
中断号:
USART1_IRQn = 37(stm32f1xx.h 中枚举定义)。2.3 两个开关(最重要的结论)
让 USART 以中断模式工作,必须开启两个开关:
开关 内容 在哪里开 ① USART 内部中断 CR1 中的 RXNEIE / TXEIE 等中断使能位 HAL 库( __HAL_UART_ENABLE_IT)② NVIC 中断 USART1 的全局中断使能 CubeMX 配置 / HAL_NVIC_EnableIRQ缺一不可!CubeMX 勾选 "USART1 global interrupt" 后,NVIC 会自动生成使能代码。
三、实验目标与硬件分析
3.1 实验目标
- 串口配置:波特率 115200bps、8 位数据、奇校验(Odd)、停止位 1、无流控。
- 用串口中断模式接收上位机发来的测试数据。
- 用串口中断模式把数据回传到上位机。
- 用 LED 指示灯闪烁表示串口正在进行数据传输。
3.2 原理图:为什么需要 CH340K?
开发板串口电路使用 USB 转 USART 芯片 CH340K(等效"USB 转 UART TTL"模块):
笔记本电脑(USB 口)──USB──> CH340K ──TTL──> STM32(USART1) Tx ───────> Rx Rx <─────── Tx GND ────── GND(共地)原因:
现代电脑(尤其笔记本)没有原生串口;
MCU 只认识 TTL 电平串口信号,二者无法直接对话,需要 CH340K 这个"翻译官"。对软件应用来讲,电脑看到的是"串口",其实是 USB 转出来的。
四、CubeMX 工程配置(逐步)
4.1 仿真器接口:SWD
SYS → Debug 选择
Serial Wire。⚠️ 若 CubeMX 错误配置或禁止了 debug 接口(JTAG/SWD),烧录完程序后将无法再通过调试器连接目标 STM32。
4.2 时钟:8MHz HSE → 72MHz
RCC 的 HSE 选
Crystal / Ceramic Resonator(外部晶振);开发板是 8MHz 晶振,配置到系统最大频率 72MHz:
时钟路径 配置 结果 HSE 8 MHz 8 MHz PLL 倍频 ×9 PLLCLK = 72 MHz SYSCLK 选 PLLCLK 72 MHz AHB 预分频(HCLK) /1 72 MHz APB1 预分频(PCLK1) /2 36 MHz APB2 预分频(PCLK2) /1 72 MHz USART1 挂在 APB2 总线上,时钟源 72MHz。
4.3 USART1 基本参数
参数 配置值 说明 Mode Asynchronous 异步通信模式 Hardware Flow Control Disable 不用硬件流控,只用 RX/TX Baud Rate 115200 Bits/s 常见波特率 Word Length 9 Bits (including Parity) 8 位数据 + 1 位校验 Parity Odd 奇校验 Stop Bits 1 1 位停止位 Data Direction Receive and Transmit 收发模式 Over Sampling 16 Samples 过采样,默认即可 过采样(OverSampling):
USART 接收器在一个位周期内对信号采样的次数,用于判断该位的逻辑状态。
4.4 引脚复用(使能串口后自动配置)
引脚 信号 GPIO 模式 说明 PA9 USART1_TX Alternate Func.(复用推挽) 发送 PA10 USART1_RX Input mode(下拉) 接收 4.5 中断配置(NVIC)
USART1 的 NVIC Settings 勾选
USART1 global interrupt,抢占优先级 0、子优先级 0。系统 NVIC 页面会自动出现 USART1 的中断信息。4.6 LED 指示(PF8 / PF9)
PF8、PF9 配置为 GPIO Output Push Pull(推挽输出),默认输出 High(灯熄灭,低电平点亮)。
原理图中推挽、开漏都行。
本例:PF8 绿灯指示收发状态,PF9 红灯指示接收溢出异常。
4.7 项目配置与生成
Project Name 自定义;Toolchain/IDE 选
MDK-ARM(V5.32)。点击 GENERATE CODE 生成工程。
五、两个核心 API:函数签名与行为
5.1
HAL_UART_Receive_IT—— 中断模式接收HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
参数 含义 huart UART 句柄,需处于接收就绪状态 pData 接收缓冲区起始地址(9 位无校验按 uint16_t*,其余按uint8_t*)Size 接收的数据元素个数(不是字节数)
返回值 含义 HAL_OK 成功启动中断接收 HAL_BUSY 当前有接收正在进行(RxState != READY) HAL_ERROR 入参非法(pData==NULL 或 Size==0) 这个函数做了什么?
核心是"设定一个接收计划,然后使能中断,让硬件在后台完成接收":
- 设定目标:告诉句柄数据放哪(pData)、收多少(Size);
- 开启中断:使能接收中断 RXNE,硬件随时响应新数据;
- 等待完成:每收到一字节,自动进入中断服务程序,HAL 库内部存入缓冲区并递减剩余计数,收满后触发用户层回调。
5.2
HAL_UART_Transmit_IT—— 中断模式发送HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);
- huart:UART 句柄;pData:待发送缓冲区指针;Size:待发送数据元素数量。
- 字长与校验说明:Parity 无校验且 WordLength 为 9 位时按 u16 元素处理;其他情形按 u8。
做了什么?
- 设定计划:把 pData 和 Size 存入句柄内部变量;
- 开启中断:使能发送数据寄存器空中断 TXEIE;
- 中断触发:TDR 空则立即触发 TXE 中断,CPU 在 HAL 库中逐字节搬运,全部发完触发用户层回调。
5.3 关键认知(务必记住)
上面的"发送"和"接收"并不是真的在收发数据,只是设定了计划、开启了中断。
真正的收发发生在 ISR 被调用、回调执行时——数据流先经过 HAL 库内部,再到你的应用层回调函数。
六、完整工程代码(含详细注释)
6.1 全局变量与缓冲区
#include <stdbool.h> /* USER CODE BEGIN PM */ #define BUFFER_SIZE 128 /* USER CODE END PM */ /* USER CODE BEGIN PV */ volatile static uint16_t rx_index = 0; // 接收缓冲区当前索引位置 static uint8_t rx_buffer[BUFFER_SIZE]; // 接收缓冲区 static uint8_t tx_buffer[BUFFER_SIZE]; // 发送缓冲区(为可读性用两个) /* USER CODE END PV */6.2 main:初始化并启动第一次接收
int main(void) { HAL_Init(); SystemClock_Config(); // CubeMX 生成:时钟配置 MX_GPIO_Init(); // CubeMX 生成:GPIO 配置 MX_USART1_UART_Init(); // CubeMX 生成:USART1 配置(含中断使能) /* 关键:启动第一次中断接收,收 1 个字节到 rx_buffer[0] */ HAL_UART_Receive_IT(&huart1, &rx_buffer[0], 1); while (1) { /* 主循环空闲,CPU 可做其他事 */ } }课件中 main 主体省略为
......,但第一次接收必须在 main 中启动,否则永远收不到数据。每收到一字节后,回调里会再次调用HAL_UART_Receive_IT形成接收闭环。6.3 数据处理:大小写互换
// 把收到的行数据,处理完写入到发送缓冲区 static void Handler_Data(void) { HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_RESET); // 点亮绿灯:处理数据中 // 做测试用(模拟耗时,让闪灯效果更明显) // volatile int cnt = 5000000; // while (cnt--); uint16_t rx_total = rx_index + 1; // 含换行符在内的整行长度 for (int i = 0; i < rx_total; i++) { if (rx_buffer[i] >= 'a' && rx_buffer[i] <= 'z') // 小写 → 大写 { tx_buffer[i] = rx_buffer[i] - 32; } else if (rx_buffer[i] >= 'A' && rx_buffer[i] <= 'Z') // 大写 → 小写 { tx_buffer[i] = rx_buffer[i] + 32; } else { tx_buffer[i] = rx_buffer[i]; // 其他字符原样保留 } } }6.4 接收完成回调
HAL_UART_RxCpltCallback/* USER CODE BEGIN 4 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // ① 溢出检查:接收超过缓冲区大小 if (rx_index > BUFFER_SIZE - 1) { rx_index = 0; // 恢复初始 HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_RESET); // 点亮红灯:溢出异常 return; // 直接返回,不再开启读中断 } // ② 收到换行符:一行结束 if (rx_buffer[rx_index] == '\n') { Handler_Data(); // 处理数据(大小写转换) HAL_UART_Transmit_IT(&huart1, tx_buffer, rx_index + 1); // 开启中断发送,回传整行 rx_index = 0; // 复位,重新接收 } else { rx_index++; // 一次收一个字节,索引后移 } // ③ 继续开启读中断(接收闭环) HAL_UART_Receive_IT(huart, &rx_buffer[rx_index], 1); } }6.5 发送完成回调
HAL_UART_TxCpltCallback// 发送可是一次发多字节,底层全部发完才会触发该回调 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_SET); // 熄灭绿灯 } }6.6 数据流逻辑小结
上位机发送一行(如 abcdEFG1234\n) ↓ 逐字节中断接收(每 1 字节触发一次 USART1 中断) ↓ 存入 rx_buffer,rx_index 递增 ↓ 遇到 '\n' → Handler_Data() 大小写互换写入 tx_buffer ↓ → HAL_UART_Transmit_IT() 开启中断发送 ↓ → rx_index = 0 复位 逐字节 TXE 中断发送 → 发完触发 HAL_UART_TxCpltCallback → 熄灭绿灯 上位机收到:ABCDefg1234\n(完成一次回环)
七、源码级解析:两条线
核心就两条线:调用线(软件使能) 和 回调线(中断驱动)。
7.1 调用线:HAL_UART_Transmit_IT 内部实现
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size) { /* 检查发送进程是否已在进行中 */ if (huart->gState == HAL_UART_STATE_READY) // gState = 空闲状态 { /* 参数校验:指针不能为空,长度不能为 0 */ if ((pData == NULL) || (Size == 0U)) { return HAL_ERROR; } /* 保存发送参数到句柄中 */ huart->pTxBuffPtr = pData; // 指向要发送的数据 huart->TxXferSize = Size; // 总发送长度 huart->TxXferCount = Size; // 剩余待发送字节数(中断里递减) /* 清除错误码,标记状态为"发送忙碌" */ huart->ErrorCode = HAL_UART_ERROR_NONE; huart->gState = HAL_UART_STATE_BUSY_TX; /* 使能发送数据寄存器空中断(TXE)——一旦使能,TDR 为空立即触发中断 */ __HAL_UART_ENABLE_IT(huart, UART_IT_TXE); return HAL_OK; } else { return HAL_BUSY; // 串口正忙(可能正在发送或接收) } }
__HAL_UART_ENABLE_IT宏:三选一写寄存器#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__) \ ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX) ? ((__HANDLE__)->Instance->CR1 |= ((__INTERRUPT__) & UART_IT_MASK)) : \ ((((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX) ? ((__HANDLE__)->Instance->CR2 |= ((__INTERRUPT__) & UART_IT_MASK)) : \ ((__HANDLE__)->Instance->CR3 |= ((__INTERRUPT__) & UART_IT_MASK))))原理:
每个
UART_IT_XXX是 32 位值——高 4 位(>>28)标识属于 CR1/CR2/CR3 哪个寄存器,低 16 位(& UART_IT_MASK)是寄存器内的对应位。宏按高 4 位选择寄存器并置位。
#define UART_IT_TXE ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_TXEIE)) // TXE 中断 #define USART_CR1_TXEIE_Pos (7U) #define USART_CR1_TXEIE_Msk (0x1UL << USART_CR1_TXEIE_Pos) // 0x00000080 #define USART_CR1_TXEIE USART_CR1_TXEIE_Msk
7.2 调用线:HAL_UART_Receive_IT → UART_Start_Receive_IT
HAL_StatusTypeDef UART_Start_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { /* 保存接收参数(供中断服务函数使用) */ huart->pRxBuffPtr = pData; // 指向接收缓冲区 huart->RxXferSize = Size; // 总共要接收的字节数 huart->RxXferCount = Size; // 剩余待接收字节数(递减到 0 表示收完) /* 清除错误码,状态改为"接收忙碌" */ huart->ErrorCode = HAL_UART_ERROR_NONE; huart->RxState = HAL_UART_STATE_BUSY_RX; /* 开启了奇偶校验 → 使能奇偶校验错误中断(PE) */ if (huart->Init.Parity != UART_PARITY_NONE) { __HAL_UART_ENABLE_IT(huart, UART_IT_PE); } /* 使能错误中断(帧错误、噪声错误、溢出错误 ORE/NE/FE) */ __HAL_UART_ENABLE_IT(huart, UART_IT_ERR); /* 使能接收数据寄存器非空中断(RXNE)——最关键的一步! */ __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); return HAL_OK; }#define UART_IT_RXNE ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_RXNEIE)) #define USART_CR1_RXNEIE_Pos (5U) #define USART_CR1_RXNEIE_Msk (0x1UL << USART_CR1_RXNEIE_Pos) // 0x00000020 #define USART_CR1_RXNEIE USART_CR1_RXNEIE_Msk
7.3 USART_CR1 关键中断使能位
位 名称 含义 相关中断宏 bit5 RXNEIE 接收数据寄存器非空中断使能 UART_IT_RXNE bit6 TCIE 发送完成中断使能 UART_IT_TC bit7 TXEIE 发送数据寄存器空中断使能 UART_IT_TXE bit8 PEIE 奇偶校验错误中断使能 UART_IT_PE
7.4 MspInit:引脚 + NVIC(CubeMX 生成)
void HAL_UART_MspInit(UART_HandleTypeDef* huart) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if (huart->Instance == USART1) { __HAL_RCC_USART1_CLK_ENABLE(); // 使能 USART1 时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟 /* PA9 ------> USART1_TX */ GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* PA10 ------> USART1_RX */ GPIO_InitStruct.Pin = GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式 GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* USART1 interrupt Init */ HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); // 优先级 0/0 HAL_NVIC_EnableIRQ(USART1_IRQn); // 开关②:使能 NVIC 中断 } } // USART1_IRQn = 377.5 调用线结论
- 中断方式收发函数,并不是收发数据,而是使能 USART 内部的中断标志位;
- 让 USART 以中断模式工作需要两个开关:USART 自身中断开关(CR1 使能位)+ NVIC 的 USART 中断开关。
7.6 回调线:中断服务例程链路
- USART 的收发寄存器各自只能收发一个字节,所以中断触发的单位就是字节;
- 无论是收还是发,中断号一样、优先级一样、执行的中断服务程序一样——收发回调的是同一个中断服务例程
USART1_IRQHandler;- 收发共用同一个 ISR、优先级相同,中断处理函数之间不会互相抢占嵌套,只会交叉执行。
启动文件 startup_stm32f103xe.s 中:
DCD USART1_IRQHandler ; USART1中断服务函数(stm32f1xx_it.c):
void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); // HAL 库统一处理:读标志、搬数据、调回调 }
HAL_UART_IRQHandler内部会检查 RXNE、TXE、ORE、NE、FE、PE 等标志,执行数据搬运(DR 读到缓冲区 / 缓冲区字节写入 DR),收满/发完再调用用户回调(HAL_UART_RxCpltCallback/HAL_UART_TxCpltCallback)。中断里先过 HAL 库,再到应用层回调。
7.7 完整调用链总结
【调用线】应用层 HAL_UART_Receive_IT / Transmit_IT → HAL 库保存缓冲区指针 + 计数 + 状态置 BUSY → 使能 USART 中断位(开关①:RXNEIE/TXEIE...) →(开关② NVIC 使能已由 CubeMX 配置好) 【回调线】硬件事件(收到字节 RXNE=1 / TDR 空 TXE=1) → USART 请求 NVIC → Cortex-M3 响应 → USART1_IRQHandler → HAL_UART_IRQHandler → 数据搬运 → 用户回调 RxCpltCallback / TxCpltCallback → 应用层处理 / 继续调用 IT 函数形成闭环八、核心知识点总结(面试/考试考点)
- USART 是片上外设中断,直接连 NVIC,不经过 EXTI。
- 中断模式两个开关:USART 内部中断使能(CR1 位)+ NVIC 全局中断使能,缺一不可。
- HAL_UART_Receive_IT / Transmit_IT 只是"设定计划 + 使能中断",不真正搬数据;真正的收发发生在 ISR 回调链路。
- 收发共用同一个中断服务例程(USART1_IRQHandler),中断号 37,按字节触发。
- 回调执行顺序:硬件 → NVIC → ISR → HAL_UART_IRQHandler → 应用层回调。
- 中断内部不要调用 HAL_Delay();闪灯效果不明显可用
while(x)模拟耗时观察。易踩坑提醒
- CubeMX 禁掉 SWD → 芯片无法再次烧录;
- 忘记在 main 里启动第一次
HAL_UART_Receive_IT→ 永远收不到数据;- 缓冲区溢出未处理 → 数据错乱;
- 中断里做耗时操作 → 丢失后续字节(本例每字节都触发一次中断)。
面试题
一、基础概念类
1. 轮询、中断、DMA 三种收发模式有什么区别?
模式 CPU 参与 效率 适用 轮询 死等标志位,阻塞 低 简单单字节交互 中断 事件触发,空闲可做别的 高 高频 / 不定长数据 DMA 硬件搬运,CPU 完全释放 最高 大批量连续数据
2. USART 属于哪一类中断源?为什么不经过 EXTI?
- 属于片上外设中断(自带中断模块,直接连 Cortex-M3 的 NVIC)。
- EXTI 只负责外部引脚信号的中断;USART 是芯片内部外设,信号在片内走,无需 EXTI 中转。
3. 让 USART 以中断模式工作需要哪 "两个开关"?缺一个会怎样?
- ① USART 内部中断开关:CR1 寄存器的 RXNEIE/TXEIE 等使能位(HAL 库
__HAL_UART_ENABLE_IT设置)。- ② NVIC 中断开关:
USART1_IRQn(=37)全局使能(CubeMX 勾选 /HAL_NVIC_EnableIRQ)。- 缺①:标志位置位但不产生中断请求;
- 缺②:请求到了 NVIC 也不响应 → 中断永远不执行。
4. 为什么说中断触发的单位是字节?
USART 的发送 / 接收数据寄存器(TDR/RDR)各只能存 1 个字节,
所以每收 / 发一个字节就会置位一次 RXNE/TXE 并触发一次中断。
5. HAL_UART_Receive_IT 真的在接收数据吗?
- 不是。它只做三件事:保存缓冲区指针和计数 → 状态置 BUSY → 使能 RXNE 中断。
- 真正收数据发生在 ISR 回调链路:硬件收到字节 → NVIC →
USART1_IRQHandler→HAL_UART_IRQHandler→ 应用层回调。
6. 接收和发送共用中断服务函数吗?
- 共用。收发的中断号一样(USART1_IRQn=37)、优先级一样,都进
USART1_IRQHandler。- 因为共用且同级,中断处理函数之间不会抢占嵌套,只会交叉执行。
7. 开发板为什么需要 CH340K 芯片?
- 电脑(尤其笔记本)没有原生串口,而 STM32 只认 TTL 电平串口信号,二者无法直接通信。
- CH340K 做 USB ↔ UART TTL 转换 "翻译官";对软件来讲电脑看到的是 "串口"。
8. 为什么中断回调里不能调用 HAL_Delay ()?
- HAL_Delay 依赖 SysTick 中断,在串口中断里调用会引入嵌套 / 优先级问题。
- 阻塞式延时期间,同优先级的串口中断无法及时响应,后续字节会丢失。
二、源码 / 寄存器类
9.
__HAL_UART_ENABLE_IT宏的工作原理?
- 每个
UART_IT_XXX中断号是 32 位值:高 4 位(>>28)标识该位属于 CR1/CR2/CR3 哪个寄存器,低 16 位(&UART_IT_MASK)是寄存器内对应位。- 宏用三目运算符按高 4 位选寄存器,再对选中寄存器做
|=置位。
10. 为什么
UART_IT_TXE = (1<<28) | 0x80?
- 高 4 位
1=UART_CR1_REG_INDEX,表示 TXE 使能位在 CR1;- 低 16 位
0x80=USART_CR1_TXEIE(CR1 的 bit7,发送数据寄存器空中断使能)。
11.
UART_Start_Receive_IT做了哪些事?
- 保存
pRxBuffPtr/RxXferSize/RxXferCount(剩余计数,中断里递减);RxState = BUSY_RX;- 开了奇偶校验 → 使能 PE 中断;
- 使能 ERR 错误中断(ORE/NE/FE);
- 使能 RXNE 接收中断(最关键)。
12. 一次完整中断回调的执行顺序?
硬件收到字节 → RXNE=1 → USART 请求 NVIC → Cortex-M3 响应 → USART1_IRQHandler → HAL_UART_IRQHandler(读标志、搬数据) → 数据搬完 → HAL_UART_RxCpltCallback(应用层回调)口诀:硬件 → NVIC → ISR → HAL 库 → 应用回调。
13. gState 和 RxState 是什么?
- 句柄里的两个独立状态机:
gState管发送、RxState管接收,初始 READY,启动后置 BUSY_TX / BUSY_RX。- 所以收发可以同时进行,互不干扰;忙时再调用返回 HAL_BUSY。
14. 接收缓冲区索引
rx_index为什么要加 volatile?它在中断回调里被修改,而主循环 / 其他代码可能读取它。
volatile防止编译器优化时把它缓存到寄存器,保证每次访问都读内存最新值。
三、工程实践类
15. 如果 main 里忘记调用
HAL_UART_Receive_IT(&huart1, &rx_buffer[0], 1)会发生什么?
- 永远收不到数据。因为回调里每次只重新使能 "下一次" 接收,第一次启动必须在 main 完成 —— 否则接收中断根本没被打开。
16. 缓冲区溢出如何处理?(本示例的做法)
- 回调开头检查
rx_index > BUFFER_SIZE - 1:溢出则复位索引、点亮红灯(PF9)指示异常、直接 return 不再开启读中断,防止越界写。
17. 本例配置 "9 位数据 + 奇校验" 怎么理解?Size 参数是什么单位?
- 9 Bits = 8 位数据 + 1 位校验(Word Length 含校验位);
- 校验开启时按 u8 元素计数;若 "无校验 + 9 位字长",则按 u16 元素计数,
pData要按uint16_t*用。
18. 收到什么信号判定 "一行数据" 结束?
- 收到
'\n'(换行符)。回调里判断rx_buffer[rx_index] == '\n',成立则说明整行收完 → 处理数据(大小写互换)→HAL_UART_Transmit_IT回传 →rx_index = 0复位。
19. 两个 LED 分别干什么?
- PF8 绿灯:数据处理中点亮、发送完成熄灭(指示收发进行中);
- PF9 红灯:接收溢出异常时点亮告警。
四、进阶拓展类
20. 如果上位机一次发 100 字节,中断会触发几次?
- 触发 100 次(每字节一次 RXNE 中断),回调里逐字节存缓冲区。
- 这正是字节级中断的特点:代码简单,但高频时 CPU 开销大;大批量场景应换 DMA + 空闲中断。
21. 中断模式下如何避免丢字节?(要点)
- 回调里别做耗时操作(别 HAL_Delay、别大循环),尽快重新使能下一次接收;
- 缓冲区要够大并做溢出保护;
- 提高 NVIC 抢占优先级,避免被低优先级中断长期打断。
22. NVIC 抢占优先级和子优先级怎么理解?
- 抢占优先级:能打断低抢占级的中断(本例分组 4 位全给抢占,USART1 为 0,最高);
- 子优先级:同抢占级时决定响应先后,不能打断同级。
- 本例 USART1 配置抢占 0、子 0,即最高优先级,保证串口响应及时。
23. 串口参数 "115200, 8O1" 是什么意思?
- 波特率 115200bps;8 位数据;O = Odd 奇校验(9 位帧含校验位);1 位停止位。与 "8N1"(无校验)的帧格式不同,两端必须一致才能通信。
24. 如果把中断收发改成 DMA 收发,核心差别在哪?
- 中断:每字节触发一次 ISR,CPU 逐字节搬运;
- DMA:配置好源 / 目的 / 长度后,硬件自动搬运,搬完只触发一次完成中断,CPU 开销最小,适合大数据量连续传输。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2602_95649725/article/details/166738804







