每次编写相关uart相关模块的时候,总是因为未开启 HAL_UART_Receive_IT这个函数导致无法开启中断,中断接受数据。于是我去深入了解了一下它的设计工作逻辑。
工作逻辑
HAL 的UART逻辑是:"你必须先提交一个任务,我才帮你在中断里处理。"
这个接受的任务就是:
HAL_UART_Receive_IT(&huart, buffer, length);
HAL_UART_Receive(&huart, buffer, length, time);
HAL_UART_Receive_DMA(&huart, buffer, length);
......
HAL 才能知道:
-
你要接收多长?
-
接收的数据存哪里?
-
接收完要不要回调?
-
中断来了要怎么处理?
而 只开中断 并不会自动创建任务,所以 HAL 不知道你要干嘛。
为什么开启了中断依旧没有输入
cs
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
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);
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);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
以上的代码是cubemax生产的Uart初始化函数。函数中只包含了初始化和NVIC的开启。
但是 NVIC 只是"允许"中断发生,但不等于"告诉 HAL 要接收数据"!
HAL_USART_Receive_IT() 等任务函数 才是真正让 HAL 开始接收数据的"启动指令"。
说白话:
-
开中断 = 把门打开,但站台上没人。
-
HAL_USART_Receive_IT 等函数 = 派一个 HAL 的"接收任务员"站到门口等数据。
没有任务员,自然没人处理数据,即使门是开的。
NVIC 的作用只是当外设产生中断事件时,让 CPU 能响应。
但外设到底能不能产生中断,要看 USART 寄存器 本身有没有打开对应的中断源。
在 STM32 USART 里,中断能不能产生由 CR1, CR2, CR3 决定
例如:
| 中断类型 | 要打开的寄存器位 |
|---|---|
| 接收缓冲非空 RXNE | USART_CR1_RXNEIE |
| 空闲线路 IDLE | USART_CR1_IDLEIE |
| 发送完成 TC | USART_CR1_TCIE |
| 错误中断 | USART_CR3_EIE |
如果这些位都没开:
- USART 不会产生任何中断事件
- NVIC 永远不会触发
- 中断入口不会进
- "看起来"像完全没中断
那 HAL_USART_Receive_IT 做了什么关键动作?
它做了"最后一刀":
__HAL_USART_ENABLE_IT(huart, USART_IT_RXNE);
也就是:
✔ 打开 RXNEIE(接收数据中断源)
✔ (可能)打开错误中断
✔ 设置 HAL 自己内部状态机
✔ 设置缓冲区指针
如果你没调用 HAL_USART_Receive_IT:
-
RXNEIE = 0
-
外设根本不产生中断事件
-
NVIC 那边即使开着,也永远等不到信号
"只有外设中断源 + NVIC 都开了,才能进中断"
用图画一下:
USART 外设 (CR1/CR3)
↓
中断事件? NVIC
↓
CPU 是否响应? CPU → 进入中断函数
你这种情况下:
USART 外设:RXNEIE=0(没开中断源) → 不产生事件 NVIC:开了也没用 → 没有事件传上来 CPU:永远不会进中断
所以不是 "有中断但是没数据",
而是 压根没有中断事件产生。
如何接受不定长的数据?
HAL_USART_Receive_IT 不能天然处理"不定长接收"。
它必须要传入固定长度,所以不适合不定长协议。
但工程上常用的处理方法如下,我按从"最推荐"到"最不推荐"的顺序给你讲清楚。
方法 1(最推荐):用 UART + IDLE 中断 + DMA 实现不定长接收
这是 STM32 官方推荐的标准做法,也是所有实际项目里最常用的方案。
逻辑非常简单:
-
开 DMA 循环接收一个大缓冲区(比如 256 字节)
-
开 IDLE 中断
-
只要串口超过一个字符时间没有数据进来(出现"空闲"),
→ 触发 IDLE 中断
→ 数据帧结束
→ 你在 callback 里计算 DMA 已经收到多少字节,就是一帧长度
初始化:(记得初始化DMA)
......(省略了DMA初始化)
HAL_UARTEx_Receive_DMA(&huart, buffer, length);
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);
或者HAL_UARTEx_ReceiveToIdle_DMA(&huart1,u8_rxBuffer,COM_BUFFER_LENGTH);
在中断处理函数:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
uint8_t u8_dataLength;
uint8_t u8_flag;
/* Prevent unused argument(s) compilation warning */
if (huart->Instance == USART1)
{
HAL_UART_DMAStop(huart); //停止DMA接收
u8_dataLength = COM_BUFFER_LENGTH - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中传输的数据个数
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,u8_rxBuffer,COM_BUFFER_LENGTH);
HAL_UART_Transmit_DMA(&huart1,u8_rxBuffer,u8_dataLength);
}
/* NOTE : This function should not be modified, when the callback is needed,
the HAL_UARTEx_RxEventCallback can be implemented in the user file.
*/
}
为什么这是最佳方式?
因为:
-
DMA 自动搬运数据,无需每字节进一次中断,只有在接受结束,空闲中断触发时才进入中断
-
IDLE 中断可以"自然划分帧"
-
完美适配 不定长度协议(Modbus、ESP8266 AT 指令等)
-
性能最好、可靠性最高
HAL 最适合不定长数据的方法就是 DMA + IDLE 中断。
方法 2:用 HAL_UART_Receive_IT 只接收 1 个字节 + 自己处理帧
你让 HAL 每次只接收 1 字节:
HAL_UART_Receive_IT(&huart, buffer, 1);
然后在回调函数里继续调用:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 你自己把 rx_byte 加到自己的 FIFO 或 buffer 中
buffer[Index++] = rxByte;
// 再次启动下一次1字节接收
HAL_UART_Receive_IT(&huart, buffer, 1);
}
使用HAL_UART_Receive_IT(&huart, buffer, length);这个函数,在每个字节接受完成时,都会进入中断,即使length不为1 。意味着:
-
每个字节都进一次中断,中断频率高,效率最低
-
波特率高时 CPU 占用高
故该方法适合中低速串口(9600~115200)。
方法 3(不推荐):写一个非常大的 length,比如 255 或 1024
比如:
HAL_UART_Receive_IT(&huart1, RxBuffer, 1024);
然后依靠超时来判断帧是否结束。
但 HAL 的超时机制是 阻塞式 (是给阻塞函数用的),
而 IT 模式没有自动超时。
所以你:
需要自己加硬件定时器
每来一个字节重置超时计时器
超时到了认为一帧结束
这是可以实现的,但既不优雅,也容易出 bug。
所以我只在一些非常简单、低速场景下用这种方式。