STM32 HAL库串口接收不定长数据与DMA传输故障定位 - 第3节
3.1 NVIC中断控制器——CPU的“紧急事务处理中心”
聊到UART中断接收,绕不开NVIC。NVIC是什么?说白了就是Cortex-M内核里的一个中断管家。它负责决定:哪个中断来了、该不该响应、响应完要不要切去更高优先级的活儿。
我刚开始用STM32时,总觉得NVIC就是个“配优先级的地方”,配完就忘了。直到有一次,两个外设同时触发中断,系统直接卡死——我才意识到,NVIC的优先级分组、抢占优先级和子优先级,不是随便填的。
3.1.1 NVIC核心概念速览
| 概念 | 说明 | 我的经验 |
|---|---|---|
| 中断通道 | 每个外设中断对应一个通道号,如USART1_IRQn | 查手册时注意通道号别写错,我吃过这个亏 |
| 抢占优先级 | 数值越小,优先级越高,可打断低优先级中断 | 一般把UART中断设高一点,避免丢字节 |
| 子优先级 | 同抢占优先级下,数值越小越先响应 | 同组中断用子优先级区分,够用就行 |
| 优先级分组 | 决定抢占位和子优先位的分配比例 | 推荐用NVIC_PriorityGroup_2,4位抢占+4位子优先 |
关键点:NVIC的优先级分组必须在系统初始化时设置一次,中途改会出问题。我习惯在HAL_Init()之后立刻调用HAL_NVIC_SetPriorityGrouping()。
3.1.2 NVIC配置三步走
- 设置优先级分组——HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2)
- 配置具体中断通道——HAL_NVIC_SetPriority(USART1_IRQn, 0, 0)
- 使能中断通道——HAL_NVIC_EnableIRQ(USART1_IRQn)
嗯,就这么简单。但简单归简单,顺序别搞反。先设分组,再配通道,最后使能。我曾经在项目里把使能写在了分组前面,结果中断响应乱套,查了半天才发现。
3.2 HAL_UART_Receive_IT函数原理——启动中断接收的“开关”
这个函数,是HAL库提供的“一键开启中断接收”接口。你调用它,库就会帮你配好UART的接收中断,并准备好缓冲区。
3.2.1 函数原型与参数
HAL_StatusTypeDef HAL_UART_Receive_IT(
UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size
);
| 参数 | 含义 | 注意 |
|---|---|---|
| huart | UART句柄指针 | 确保句柄已初始化 |
| pData | 接收缓冲区地址 | 缓冲区要足够大,别溢出 |
| Size | 期望接收的字节数 | 收到Size个字节后触发回调 |
小技巧:如果你只想每次收1个字节,就把Size设为1。这样每来一个字节,回调函数就执行一次。我常用这种方式做“单字节中断接收”。
3.2.2 函数内部干了什么
说白了,HAL_UART_Receive_IT做了三件事:
- 把pData和Size存到huart的成员变量里
- 使能UART的接收非空中断(RXNEIE)
- 返回HAL_OK
你想想看,它其实没真正“收”数据,只是把“收数据”的准备工作做好了。真正收数据,是在中断服务函数里完成的。
3.3 中断回调函数HAL_UART_RxCpltCallback——数据到手的“通知铃”
这个回调函数,是HAL库留给用户的“钩子”。当UART接收完指定数量的字节后,库会自动调用它。你只需要在这个函数里写“数据到了,接下来怎么办”的逻辑。
3.3.1 回调函数的典型写法
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
// 收到一个字节,存到环形缓冲区
rx_buffer[rx_index++] = rx_byte;
// 重新启动接收,准备收下一个字节
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
注意:回调函数是在中断上下文执行的。不要在里边做耗时操作,比如printf、延时、复杂计算。我见过有人直接在回调里写LCD刷新,结果系统卡成PPT。
3.3.2 回调函数的触发条件
触发条件只有一个:UART接收到了你设定的Size个字节。如果你设Size=1,那每来一个字节就触发一次。如果你设Size=10,那收满10个字节才触发一次。
我个人习惯用Size=1做单字节接收,这样灵活性最高。你可以根据协议头判断数据长度,再动态调整接收策略。
3.4 单字节中断接收实战——从零搭一个能跑的例子
光说不练假把式。咱们直接上手,写一个完整的单字节中断接收程序。
3.4.1 硬件准备
- STM32F103C8T6开发板(其他型号同理)
- USB转串口模块(CH340或CP2102)
- 杜邦线若干
3.4.2 代码实现
第一步:定义全局变量
uint8_t rx_byte; // 接收缓冲区(单字节)
uint8_t rx_flag = 0; // 接收完成标志
第二步:初始化UART和NVIC
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
// NVIC配置
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
第三步:启动中断接收
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
第四步:编写回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
rx_flag = 1; // 标记收到数据
// 可以在这里处理数据,或者存到缓冲区
HAL_UART_Receive_IT(&huart1, &rx_byte, 1); // 重新启动
}
}
第五步:在主循环中处理
while(1)
{
if(rx_flag)
{
rx_flag = 0;
// 处理收到的rx_byte
// 比如:HAL_UART_Transmit(&huart1, &rx_byte, 1, 100);
}
}
避坑指南:我曾经在回调函数里直接处理数据,没重新调用HAL_UART_Receive_IT。结果只收到第一个字节,后面的全丢了。记住:回调里一定要重新启动接收,否则中断只触发一次。
3.5 本章知识体系图
3.6 本章小结
这一章我们走通了UART中断接收的完整链路:从NVIC的优先级配置,到HAL_UART_Receive_IT的启动原理,再到回调函数的数据处理。单字节中断接收是最基础、最常用的方式,也是后续实现不定长接收的基石。
我个人建议,初学者先把单字节中断跑通,再考虑更复杂的DMA或空闲中断。基础打牢了,后面遇到问题才能快速定位。
最后提醒一句:调试中断程序时,多用串口打印关键变量的值。我每次调试都会在回调里加一个GPIO翻转,用示波器看中断响应频率,比猜靠谱多了。