HAL库串口
串口中断模式
1. CubeMX 必须配置(2 步)
- 开启串口外设:USART1/2/3 选异步通信
- NVIC 中断使能:
- 勾选
USARTx global interrupt - 配置抢占优先级 / 子优先级(别和定时器 / EXTI 冲突)
- 勾选
2. 核心原理(中断发送)
HAL 串口发送分三种:
- 阻塞:
HAL_UART_Transmit()卡死等发完 - 中断:
HAL_UART_Transmit_IT()主推 - DMA:
HAL_UART_Transmit_DMA()
中断发送:调用 IT 函数 → 触发发送中断 → 自动发数据 → 发完进发送完成回调
3. 用到的关键函数
① 触发中断发送(主函数 / 自己逻辑里调用)
// huart1 是你的串口句柄;pData数据首地址;Size长度 HAL_UART_Transmit_IT(&huart1, (uint8_t*)"Hello\r\n", 6);② 发送完成回调(必须自己写,发完自动进)
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1)// 判断是哪个串口发完了 { // 串口1发送完成后,在这里写后续逻辑 // 比如:标记发送完成、触发下一包、翻转LED HAL_UART_Receive_IT(&huart2, rx_data, 2);// 重新开启中断接收 } }最容易犯的 2 个错误
接收中断只收一次原因:回调里没重新调用
HAL_UART_Receive_IT连续发送死机原因:上一次还没发完,又调用发送函数解决:判断
huart->gState == HAL_UART_STATE_READY
③ 底层中断入口(不用自己改,CubeMX 自动生成)
void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); // HAL清标志、调度回调 }发送流程
你调用Transmit_ITHAL_UART_Transmit_IT(&huart1, data, len);
- 使能TXE 发送寄存器空中断
- 使能TC 发送完成中断
- 把数据、长度存入句柄
huart - 函数立刻返回,不阻塞主程序
→ 硬件发数据
每发完一个字节 触发TXE 中断全部发完 触发TC 中断
→ 进USARTx_IRQHandler中断服务函数
void USART1_IRQHandler(void)
{ HAL_UART_IRQHandler(&huart1); // 库函数处理 }
→ 库处理
- 清除标志位
- 自动发送下一个字节
- 发完所有数据后
→ 发完 →TxCpltCallback串口【发送完成中断回调函数】
当串口用中断方式(HAL_UART_Transmit_IT)把数据全部发完后,系统会自动调用这个函数,告诉你:发送结束了!
你调用HAL_UART_Transmit_IT发数据数据在后台自动发,不卡主程序发完那一刻,CPU 自动跳进这个函数你在这里写:发送完成后要做的事
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { // 发送完成!你在这里写逻辑 }接收流程
先开Receive_IT→ 收到数据→ 进USARTx_IRQHandler→ 库处理→ 收完 →RxCpltCallback→重新开接收
串口DMA模式
DMA = 不需要 CPU 参与,硬件自己搬运数据发串口 / 收串口
中断发送:CPU 每发一个字节都要进一次中断
DMA 发送:CPU 说 “开始发”,然后硬件自己发,发完通知一声CPU 完全解放!
- IDLE 空闲中断:串口检测到「一段时间没数据」,判定一帧报文收完
- 组合实现:随便发多长,自动识别一帧数据,不用定死长度
二、DMA 需要配置什么(CubeMX)
1. 串口配置
- 选 Async 异步模式
- 波特率等正常设置
2. DMA Settings 点进去
发送 TX
- Add →USART1_TX
- 模式:Normal正常模式 /Circular循环模式
- 数据宽度:Byte
接收 RX(可选)
- Add →USART1_RX
3. 必须开 NVIC 中断
- 开启 USART1 global interrupt
- 开启 DMA 中断(默认会自动开)
三、DMA 发送用什么函数
启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, (uint8_t*)"Hello DMA\r\n", 11);发送完成后自动进的函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { // DMA 发完也进这里!和 IT 共用同一个回调! }DMA 和中断发送共用同一个完成回调!
四、DMA 接收
开启 DMA 接收(通常初始化时开一次)
uint8_t rx_buf[100]; HAL_UART_Receive_DMA(&huart1, rx_buf, 100);接收完成自动进
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 收满 100 字节进来这里 }五、完整执行流程(DMA 发送)
- 你调用
HAL_UART_Transmit_DMA - CPU 立刻返回,去干别的
- DMA 硬件自动把数据搬到串口
- 发完 → 触发中断
- 进入
HAL_UART_TxCpltCallback告诉你发完了
【DMA+IDLE 空闲中断】不定长接收
一、核心原理
- DMA:硬件搬运数据,收数据全程不占 CPU
- IDLE 空闲中断:串口检测到「一段时间没数据」,判定一帧报文收完
- 组合实现:随便发多长,自动识别一帧数据,不用定死长度
二、CubeMX 必配步骤
- 串口异步、配波特率
- DMA 设置:
- USARTx_RX 新增 DMA,模式:Normal
- 字节 8 位
- NVIC:
- 开 USARTx 全局中断
- 开 DMA 中断
- 开启:串口 IDLE 空闲中断(代码里初始化开启)
三.流程
全局变量定义(main.h或usart.c里)
// 定义接收缓冲区,大小根据你的最大一帧数据长度设置 #define UART2_RX_BUF_SIZE 256 // 一帧最多收256字节,可改 uint8_t uart2_rx_buf[UART2_RX_BUF_SIZE]; // DMA接收缓存 uint16_t uart2_rx_len = 0; // 一帧实际收到的长度 uint8_t uart2_rx_flag = 0; // 接收完成标志位3. 初始化开启(main.c里MX_USART2_UART_Init()之后调用)
// 开启串口2的DMA+IDLE不定长接收,只需要调用这一行! HAL_UARTEx_ReceiveToIdle_DMA(&huart2, uart2_rx_buf, UART2_RX_BUF_SIZE);4. 重写接收完成回调函数(核心!)
这个函数会在两种情况下自动调用:
- 串口检测到空闲(IDLE),一帧数据收完
- DMA 缓冲区满(收到了
UART2_RX_BUF_SIZE字节)
/* USER CODE BEGIN 1 */ // 串口IDLE+DMA接收完成回调函数 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if(huart->Instance == USART2) // 判断是串口2触发 { uart2_rx_len = Size; // Size就是这一帧的实际长度! uart2_rx_flag = 1; // 标记一帧接收完成 //关闭 “传了一半” 的提醒 HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); // 重新开启接收,否则只能收一帧! HAL_UARTEx_ReceiveToIdle_DMA(&huart2, uart2_rx_buf, UART2_RX_BUF_SIZE); } } /* USER CODE END 1 */5. 主循环处理数据(业务逻辑放这里)
while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if(uart2_rx_flag == 1) { uart2_rx_flag = 0; // 清零标志位 // ✅ 这里就是你收到的不定长数据! // uart2_rx_buf[0...uart2_rx_len-1] 是有效数据 // uart2_rx_len 是这一帧的实际长度 // 示例1:把收到的数据原样发回去(回显测试) HAL_UART_Transmit_DMA(&huart2, uart2_rx_buf, uart2_rx_len); // 示例2:协议解析(比如Modbus、自定义协议) // uart_protocol_parse(uart2_rx_buf, uart2_rx_len); } /* USER CODE END 3 */ }四、关键原理 & 避坑指南
1. 执行流程(一目了然)
- 你调用
HAL_UARTEx_ReceiveToIdle_DMA→ 开启 DMA+IDLE - 上位机发数据 → DMA 自动把数据搬到
uart2_rx_buf - 串口空闲(没数据了)→ 触发 IDLE 中断
- HAL 库自动调用
HAL_UARTEx_RxEventCallback,传入实际长度Size - 你在回调里标记完成,然后必须重新开启接收,等待下一帧
2. 必须注意的 3 个坑
回调函数是中断上下文!
- 绝对不能写
HAL_Delay()、while(1)等阻塞 / 延时函数 - 不能做耗时操作,只做标志位标记、长度保存
- 业务逻辑(协议解析、数据处理)必须放到主循环里
- 绝对不能写
必须重新开启接收!
- 每次回调里都要再调用一次
HAL_UARTEx_ReceiveToIdle_DMA - 否则只会收第一帧,之后就再也收不到了
- 每次回调里都要再调用一次
缓冲区大小要合理
UART2_RX_BUF_SIZE要大于你最大一帧数据的长度- 否则会触发缓冲区满回调,数据被截断
