利用hal_uart_transmit构建高效UART驱动的实战教程
深入讲解如何利用hal_uart_transmit实现稳定高效的UART通信,结合实际开发场景优化驱动设计,提升嵌入式系统数据传输性能与响应速度。
用好 HAL_UART_Transmit ,让串口通信不再“卡住”你的系统
你有没有遇到过这种情况:主控在发一条日志时,整个系统像被“冻结”了一样?定时器不准了、按键没反应、传感器数据也丢了……排查半天,最后发现罪魁祸首竟是那句看似无害的:
HAL_UART_Transmit(&huart1, buffer, len, 100);
没错,就是这个 “基础中的基础”函数 —— HAL_UART_Transmit 。它简单易用,几乎每个STM32项目都会用到;但它也暗藏陷阱,稍不注意就会拖垮系统的实时性和响应能力。
今天我们就来彻底拆解 HAL_UART_Transmit ,从它的底层机制讲起,一步步带你走出“轮询阻塞”的舒适区,构建一个真正 高效、稳定、可复用 的UART驱动架构。
为什么 HAL_UART_Transmit 会让CPU“忙死”?
先来看一眼这个函数的标准原型:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
参数很清晰:
- huart :UART句柄
- pData :要发送的数据缓冲区
- Size :数据长度
- Timeout :最大等待时间(毫秒)
表面上看一切正常。但问题就出在它的 工作方式 上。
它干了什么?
当你调用这个函数后,HAL库会做这几件事:
- 检查参数合法性;
- 设置UART状态为“BUSY”;
- 把第一个字节写进
USART_DR寄存器; - 然后——开始 轮询等待 !
它不断读取状态寄存器( SR ),检查两个标志位:
- TXE :发送数据寄存器空(可以写下一个字节)
- TC :传输完成(所有字节都已移出)
直到所有数据发完或超时为止。
🔍 关键点:这整个过程是 完全由CPU主动轮询完成的 ,期间不能做任何其他事。
这意味着:如果你要发512字节的数据,波特率是115200,理论上需要约44ms。在这44ms里,你的主循环停摆,中断虽然还能响应,但高频率任务可能已经被打乱节奏。
这不是“通信”,这是“自锁”。
那怎么办?别急,HAL库早就准备了后手
好消息是,ST并没有让我们一直困在轮询里。除了 HAL_UART_Transmit ,还有两个更高级的兄弟:
| 函数 | 类型 | CPU占用 | 适用场景 |
|---|---|---|---|
HAL_UART_Transmit |
阻塞式轮询 | 高 | 调试打印、极小数据 |
HAL_UART_Transmit_IT |
中断驱动 | 低 | 周期性小包、中频通信 |
HAL_UART_Transmit_DMA |
DMA驱动 | 极低 | 大数据流、固件升级 |
我们一个个来看怎么用。
方案一:用中断解放CPU —— HAL_UART_Transmit_IT
它是怎么工作的?
调用 HAL_UART_Transmit_IT 后:
1. HAL把第一字节写入DR;
2. 开启 TXE 中断;
3. 函数立即返回,CPU继续执行其他任务;
4. 每当硬件发送完一字节,触发中断;
5. 在中断服务程序中填入下一字节;
6. 全部发完后,调用回调函数 HAL_UART_TxCpltCallback() 。
实战代码示例
uint8_t tx_buf[] = "Sensor: OK\r\n";
// 启动中断发送
if (HAL_UART_Transmit_IT(&huart1, tx_buf, sizeof(tx_buf)-1) != HAL_OK) {
Error_Handler();
}
// 回调函数必须自己实现
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 可以在这里置标志、点亮LED、启动下一次发送等
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
}
}
注意事项 ⚠️
- 中断频率不能太高 :比如921600波特率下连续发数据,中断太频繁会影响系统性能;
- 缓冲区不能改 :发送过程中千万别去修改
tx_buf的内容; - 不能并发调用 :同一UART实例不支持同时发起两次IT发送,否则会报错
HAL_BUSY。
所以, 适合每秒几次到几十次的小数据包发送 ,比如传感器上报、心跳包、状态通知等。
方案二:彻底放手给硬件 —— HAL_UART_Transmit_DMA
这才是真正的“零打扰”方案。
它的核心思想是什么?
DMA(Direct Memory Access)是一个独立于CPU的控制器,专门负责内存和外设之间的数据搬运。
当我们调用 HAL_UART_Transmit_DMA 时:
1. HAL配置DMA通道:源地址 = 数据缓冲区,目标地址 = USART_DR;
2. 启动DMA传输;
3. 此后无需CPU干预 ,DMA自动将每个字节送入UART;
4. 传完一半时可选触发半完成回调;
5. 全部完成后触发 HAL_UART_TxCpltCallback() 。
整个过程CPU只花了几个微秒做初始化,剩下的全交给硬件。
如何配置DMA?(基于STM32CubeMX)
在 .ioc 文件中打开UART配置 → 找到 DMA Settings → 添加一条新通道:
| 参数 | 推荐设置 |
|---|---|
| Mode | Normal 或 Circular |
| Direction | Memory to Peripheral |
| Data Width | Byte |
| Increment | Memory: Yes / Peripheral: No |
| Priority | Medium |
✅ 特别提醒:确保DMA请求映射正确!例如USART1_TX通常对应DMA1_Stream7_Channel4(具体查芯片手册RM0090)。
实际应用场景
- 固件升级(FOTA)时上传大量日志或接收固件块;
- 波形数据实时上传PC分析;
- RS485网络广播多设备同步消息;
- 音频调试信息流输出。
示例代码
uint8_t big_data[1024];
// ...填充数据...
// 发起DMA发送
HAL_UART_Transmit_DMA(&huart1, big_data, 1024);
// 可选进度监控
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 已发送512字节,可用于更新UI或心跳
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 发送完毕,可以关闭电源域或进入低功耗模式
}
}
多任务环境下如何避免“抢串口”?
在FreeRTOS或其他RTOS环境中,多个任务都想通过UART发数据怎么办?
直接调用 HAL_UART_Transmit_DMA 很容易导致冲突:A任务刚启动发送,B任务又来了,结果A的数据还没发完就被覆盖了。
解法:加互斥锁(Mutex)
osMutexId_t uart_mutex;
// 初始化时创建互斥量
uart_mutex = osMutexNew(NULL);
// 封装安全发送函数
HAL_StatusTypeDef SafeUartSend(uint8_t *data, uint16_t len) {
osStatus_t status = osMutexAcquire(uart_mutex, osWaitForever);
if (status != osOK) return HAL_ERROR;
HAL_StatusTypeDef result = HAL_UART_Transmit_DMA(&huart1, data, len);
// 不在这里释放!要在回调里释放
return result;
}
// 在回调中释放锁
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
osMutexRelease(uart_mutex);
}
}
这样就能保证: 只有当前发送完全结束,下一个任务才能拿到资源 。
还有哪些坑?这些经验帮你避雷
❌ 坑1:DMA发送时修改缓冲区内容
常见错误写法:
uint8_t temp[32];
sprintf(temp, "Count: %d\r\n", i++);
HAL_UART_Transmit_DMA(&huart1, temp, strlen(temp)); // 危险!
问题在哪? temp 是局部变量,函数退出后栈空间可能被覆盖;而且DMA还没发完,下次循环又改了 temp 内容。
✅ 正确做法:
- 使用静态缓冲区;
- 或动态分配并确保生命周期覆盖整个DMA过程;
- 或使用双缓冲机制轮流发送。
❌ 坑2:忘记处理错误回调
UART通信不是总成功的。可能会遇到帧错误、噪声干扰、溢出等问题。
记得实现错误回调:
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 记录错误类型
uint32_t error = huart->ErrorCode;
// 复位UART
__HAL_UART_DISABLE(huart);
__HAL_UART_ENABLE(huart);
// 释放互斥锁(如果有)
osMutexRelease(uart_mutex);
}
}
❌ 坑3:超时不设或设得太长
即使是阻塞式发送,也别偷懒写成:
HAL_UART_Transmit(&huart1, buf, len, HAL_MAX_DELAY); // 绝对禁止!
一旦物理链路断开,系统就永远卡住了。
✅ 建议策略:
- 小数据包:50~100ms
- 大数据包:按波特率估算 + 一定余量(如 (size * 10) / baud_rate_second + 50 ms)
最佳实践总结:什么时候该用哪种方式?
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 调试打印、偶尔发个命令 | HAL_UART_Transmit |
简单直接,不怕短暂阻塞 |
| 传感器周期上报(每秒几次) | HAL_UART_Transmit_IT |
CPU释放,响应及时 |
| 固件升级、大数据上传 | HAL_UART_Transmit_DMA |
零负载,高吞吐 |
| RTOS多任务共享UART | + Mutex互斥锁 | 防止资源竞争 |
| 高可靠性要求 | + 超时 + 重试 + 错误恢复 | 提升鲁棒性 |
写在最后:理解 HAL_UART_Transmit ,其实是理解嵌入式通信的本质
很多人觉得“用了HAL库就不需要懂寄存器”,其实恰恰相反。
正是因为你用了 HAL_UART_Transmit ,才更需要明白它背后发生了什么。否则你永远只能停留在“能跑就行”的阶段,一旦出问题就束手无策。
而当你掌握了从 轮询 → 中断 → DMA 这条演进路径,你就不仅学会了UART,也为掌握SPI、I2C、ADC等其他外设打下了坚实基础。
毕竟,所有的高效嵌入式系统,都不是靠“卡住CPU”来实现的。
它们靠的是—— 让每个部件各司其职,协同运转 。
如果你正在做一个需要稳定串口通信的项目,不妨回头看看你的 HAL_UART_Transmit 是不是还在“霸占”主循环?也许只需一次小小的重构,就能换来整个系统流畅度的飞跃。
欢迎在评论区分享你的UART优化经验,我们一起打造更强的嵌入式系统!
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)