Skip to content

第43篇:常见坑位与实战练习 —— 把 UART 玩出花样来

UART 教程的最后一篇。坑位排雷 + 三个练习,帮你把学到的知识真正变成自己的。


常见坑位

坑位 1:TX/RX 交叉接线

这是 UART 调试中排名第一的问题,没有之一。

症状:终端什么都收不到,或者收不到发送的数据。

原因:把适配器的 TX 接到了 Blue Pill 的 TX(PA9),适配器的 RX 接到了 Blue Pill 的 RX(PA10)。TX 接 TX,两边都在发、没人收——当然什么都收不到。

解决:记住"交叉连接"——适配器 TX 接 Blue Pill RX(PA10),适配器 RX 接 Blue Pill TX(PA9)。如果不确定哪根线是 TX 哪根是 RX,交换一下试试——不会烧坏东西,只是不工作。

坑位 2:波特率不匹配

症状:终端显示乱码——看起来像是随机字符。

原因:代码中设置的波特率和终端软件的波特率不一致。比如代码里是 115200,终端设的是 9600。UART 是异步协议,双方必须以完全相同的速率工作,否则采样点全部错位,读出来的数据全是错的。

解决:确认代码中的 UartConfig{.baud_rate = ...} 和终端软件的波特率设置完全一致。不只是波特率——数据位、校验位、停止位也必须匹配(标准配置是 8N1)。

坑位 3:环形缓冲区溢出

症状:长字符串传输时后半部分丢失,或者命令解析偶尔出错。

原因:ISR push 字节的速度超过了主循环 pop 的速度。128 字节的缓冲区满了之后,push() 返回 false,字节被丢弃。当 PC 端快速发送大量数据(比如粘贴一段长文本),而主循环正好在处理其他事情(比如按钮消抖或发送响应),就会发生这种情况。

解决:增加缓冲区大小是最直接的方法——把 CircularBuffer<128> 改成 CircularBuffer<256>CircularBuffer<512>。另外,确保主循环中不要有长时间阻塞的操作——每个循环迭代都应该尽快处理完所有挂起的数据。

坑位 4:环形缓冲区忘记 volatile

症状:看起来工作正常,但偶尔丢失数据。加优化级别(-O2)后更频繁。

原因CircularBufferhead_tail_ 没有声明为 volatile。编译器优化时把主循环中的 head_ 读取缓存到了寄存器中,后续循环不再从内存重新读取——ISR 的 push 操作对主循环不可见。

解决:确保 head_tail_ 声明为 volatile size_t。我们的代码中已经正确使用了 volatile——但如果你自己写一个环形缓冲区,别忘了这一点。

坑位 5:printf 浮点数 vs nano.specs

症状printf("%f", 3.14) 输出乱码或什么都不输出。

原因:我们的 CMakeLists.txt 使用了 -specs=nano.specs 链接器选项,它链接的是精简版 C 库(nano newlib)。精简版不支持浮点数的 printf 格式化——%f%g 等格式化符不工作。

解决:用整数模拟浮点输出:printf("%d.%02d", (int)(value * 100) / 100, (int)(value * 100) % 100)。或者如果 Flash 空间足够,去掉 -specs=nano.specs 链接完整版 C 库(Flash 占用会增加约 10-20 KB)。

坑位 6:回调中忘记重启接收

症状:能收到第一个字节,之后再也收不到任何数据。

原因HAL_UART_RxCpltCallback() 中忘记调用 restart_receive()。HAL 完成一次单字节接收后不会再自动启动下一轮——你必须手动调用 HAL_UART_Receive_IT() 来重新使能接收。如果忘了,RXNEIE 就没有被重新使能,下一个字节到达时不会触发中断。

解决:确保回调中的最后一行是 restart_receive()。这是中断接收中最容易遗漏的一步——不报错、不崩溃,只是"静默失灵"。


练习

练习 1:添加 STATUS 命令(简单)

handle_command() 中添加一个新命令 STATUS,返回当前 LED 的状态(ON 或 OFF)。

提示:你需要一种方式来追踪 LED 的当前状态。最简单的方法是用一个 bool 变量,每次调用 led.on()led.off() 时更新它。或者,你可以读取 PC13 的实际电平——但要注意 PC13 是低电平点亮(Blue Pill 的板载 LED 是低有效)。

目标:在终端中输入 "STATUS",芯片返回 "LED is ON" 或 "LED is OFF"。理解如何扩展现有的命令处理框架。

练习 2:ECHO 模式切换(中等)

实现一个 ECHO 模式:启用后,每个接收到的字节都立即原样发回。添加 "ECHO ON" 和 "ECHO OFF" 命令来切换模式。

提示:在主循环的 UART 接收部分,增加一个 bool echo_mode = false 标志。当 echo_mode 为 true 时,每弹出一个字节就立即 send_string() 发回去。注意:echo 应该发生在行解析之前——字节弹出后先 echo,再拼入行缓冲。

目标:输入 "ECHO ON" 后,你在终端中打的每一个字符都会回显(你自己能看到自己打的内容)。输入 "ECHO OFF" 后停止回显。理解如何在中断接收 + 主循环消费的框架中添加实时响应逻辑。

练习 3:中断发送 + 发送环形缓冲区(挑战)

我们的代码中接收是中断驱动的,但发送仍然是阻塞式的。这个练习要求你实现中断驱动的发送。

提示:你需要:

  1. 一个发送方向的环形缓冲区(CircularBuffer<256> tx_ring
  2. 主循环中,需要发送数据时 push 到 tx_ring 而不是直接调 HAL_UART_Transmit
  3. 启动中断发送:HAL_UART_Transmit_IT(&huart, &byte, 1)
  4. HAL_UART_TxCpltCallback() 中检查 tx_ring 是否还有数据——有就继续发,没有就停止
  5. 注意 TXEIE(发送中断使能)的管理——只在有待发数据时使能,发完后关闭

这个练习的挑战在于:发送是"按需启动"的——不像接收那样始终运行。你需要处理"环形缓冲区为空时怎么停止中断"、"第一个字节怎么启动发送"等边界情况。

目标:理解中断发送和中断接收的对称性,掌握双环形缓冲区的完整中断驱动 UART 架构。


UART 教程回顾

13 篇文章走完了。回顾一下我们的学习路径:

阶段一:动机(第 31 篇)

  • 从 LED(输出)和 Button(输入)引出通信需求
  • UART 是什么,为什么选它
  • 最终效果预览和硬件准备

阶段二:硬件基础(第 32-33 篇)

  • UART 协议详解:起始位、数据位、校验位、停止位、波特率、过采样
  • STM32 USART 外设:三个实例、关键寄存器、GPIO 复用功能、NVIC 预览

阶段三:HAL + 阻塞 I/O(第 34-35 篇)

  • HAL 初始化和阻塞式发送
  • printf 重定向、阻塞式接收的致命问题

阶段四:中断驱动(第 36-38 篇)

  • Cortex-M3 中断机制和 NVIC
  • 无锁 SPSC 环形缓冲区
  • UART IRQ 处理和回调链

阶段五:C++ 抽象(第 39-42 篇)

  • std::expected 错误处理
  • UART 驱动模板:零大小抽象、if constexprstatic inline
  • Concepts 约束 + UartManager
  • 命令处理器与完整代码走读

阶段六:总结(第 43 篇)

  • 6 个常见坑位和 3 个递进练习

用到的 C++ 特性总结:

  • std::expected<T, E>(C++23)— 类型安全的错误处理
  • std::span(C++20)— 安全的连续内存视图
  • std::string_view(C++17)— 零拷贝字符串视图
  • consteval(C++20)— 编译时波特率校验
  • Concepts(C++20)— 约束回调签名
  • static inline 成员(C++17)— 模板单例
  • if constexpr(C++17)— 编译时硬件分发
  • enum class : uintptr_t — 基地址编码
  • volatile — ISR 可见性保证
  • extern "C" — ISR 和 printf 桥接
  • [[maybe_unused]](C++17)— 抑制未使用参数警告
  • 指定初始化器(C++20)— UartConfig{.baud_rate = 115200}

每个特性都在 UART 驱动的具体场景中解决了实际问题。从错误处理到类型约束,从编译时分发到 ISR 桥接——现代 C++ 在嵌入式领域不是"花拳绣腿",而是实打实地让代码更安全、更可维护、更高效。

到这里,UART 教程完结了。我们从协议原理讲到中断驱动,从 C 风格的 HAL 调用讲到 C++23 的模板和 Concepts。你的 STM32 现在不仅能自己亮灯和读按钮,还能和 PC 双向通信——这是一个质的飞跃。接下来无论你去做 SPI 驱传感器、I2C 读 EEPROM,还是搭一个完整的嵌入式 Web 服务器,UART 通信都是你调试和验证的基础工具。

基于 VitePress 构建