STM32 Hal库的Uart串口接受

每次编写相关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 官方推荐的标准做法,也是所有实际项目里最常用的方案。

逻辑非常简单:

  1. 开 DMA 循环接收一个大缓冲区(比如 256 字节)

  2. 开 IDLE 中断

  3. 只要串口超过一个字符时间没有数据进来(出现"空闲"),

    → 触发 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。

所以我只在一些非常简单、低速场景下用这种方式。

相关推荐
松涛和鸣4 小时前
DAY20 Optimizing VS Code for C/C++ Development on Ubuntu
linux·c语言·开发语言·c++·嵌入式硬件·ubuntu
罗汉松(山水白河)5 小时前
STM32F407核心板
stm32·单片机·嵌入式硬件
DIY机器人工房5 小时前
简单理解:什么是GSM?
stm32·单片机·嵌入式硬件·gsm·diy机器人工房
hazy1k5 小时前
RA6E2基础-RTC时钟与日历介绍及使用
stm32·单片机·嵌入式硬件·esp32·实时音视频·ra
2401_853448235 小时前
FreeRTOS项目---WiFi模块(2)
stm32·单片机·freertos·esp8266·通信协议
DIY机器人工房6 小时前
简单理解:什么是运放?
单片机·嵌入式硬件·运放·diy机器人工房
DIY机器人工房6 小时前
简单理解:反相比例放大器、同相比例放大器、比较器
单片机·嵌入式硬件·比较器·diy机器人工房·嵌入式面试题·同相比例放大器·反相比例放大器
DIY机器人工房6 小时前
简单理解:什么是施密特触发器?
stm32·单片机·嵌入式硬件·diy机器人工房·施密特触发器
非凡自我_成功7 小时前
寄存器开发控制LED
单片机·嵌入式硬件