ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32调试实战:RTT与串口日志系统的高效组合

2026/8/31 21:38:26 拓冰建站 浏览量
STM32调试实战:RTT与串口日志系统的高效组合 做嵌入式这些年我一直在跟 STM32 打交道。调试工具我试过一堆断点、串口打印、逻辑分析仪、RTT、CubeMonitor……如果你问我“哪个调试工具最省时间”我不会直接给出某个软件的名字而是会给你讲一套组合打法。核心答案其实是SEGGER RTT 配合 J-Link再加上一套自己搭的串口日志系统这两样东西合起来省掉的时间足够我再写两个完整的外设驱动。先说下我的背景方便你对号入座。我平时主要用 STM32F1/F4/H7 系列做电机控制、传感器采集和设备通信这类活开发环境从早期的 Keil MDK 换到后来的 VSCode CMake中间也折腾过 STM32CubeMX 自动生成代码不管环境怎么变调试工具始终是影响效率最大的变量。这篇文章我尽量用大白话把我踩过的坑、验证过好用的方案以及为什么有些工具“看着很强但实际鸡肋”的真实原因都摊开来讲希望能帮你少走弯路。1. 调试思路的转变从“打断点看寄存器”到“全链路观测”先说清楚一个观点很多人在 STM32 调试上浪费时间不是工具不好用而是思路没转过来。早期我用 Keil 调程序习惯就是打断点、单步执行、看 Watch 窗口里的寄存器。这套方法在逻辑简单的裸机程序里够用但一旦上了 FreeRTOS、加了电机 PID 控制、接了多个传感器就完全行不通了。1.1 传统断点调试为什么越调越慢打断点调试有个致命问题它会冻住整个系统。你停在断点上的时候电机 PWM 停了、通信超时了、看门狗可能已经准备复位了。尤其在跑 FreeRTOS 的时候你在某个任务里打断点其他任务的时序全乱了你看到的根本不是一个真实的运行状态而是“停在断点那一刻的尸体”。另一个坑是断点能看到的数据是静态的。你只能看到某个瞬间的变量值看不到变量是怎么变化的。ADC 采样值波动、PID 输出震荡、串口接收缓冲区的数据流这些最需要观察的动态信息断点根本给不了你。我早期调一个 ADC 多通道扫描循环采样 DMA 的程序就是靠打断点一个通道一个通道看值结果发现 DMA 搬运的地址计算错了光是定位这个问题就花了两天后来换了个思路五分钟就找到了。1.2 真正省时间的调试方式观测运行中的数据流后来我总结出一套适合自己的调试逻辑简单说就是三句话能不打断系统就绝不停下来能看数据流变化就不看静态快照能用低成本方式观测的就不用昂贵设备先说“不停下来”。对于电机控制、PID 调节这类实时性要求高的场景系统必须一直跑着你只能在旁边看数据变化。这时候 RTTReal-Time Transfer实时传输就比断点好用得多。RTT 是 SEGGER 搞出来的一种调试通道技术它利用 J-Link 调试器的内存访问能力在目标芯片 RAM 里划一块环形缓冲区程序往这个缓冲区写日志PC 端的软件实时读出来整个过程芯片不需要停下来也不会像串口那样占用额外的引脚。再说“看数据流”。串口调试是我们最熟悉的方式printf 打印大法确实好用但裸用串口有个问题你要处理波特率、引脚复用、DMA 配置、重定向 printf 这一堆事。而且串口打印本身会占用 CPU在某些时序敏感的场景下反而会把问题掩盖掉。后来我用 HAL 库的串口空闲中断加 DMA 接收不定长数据把串口接收也做成非阻塞模式这才算把串口调试这条路的性价比拉满。最后说“低成本观测”。逻辑分析仪我也有但说实话日常 90% 的问题用不上它。真正高频用的还是 RTT、串口日志、加上 STM32CubeMonitor 这类可视化工具成本低、上手快、信息量足够。2. 最省时间的三个核心工具RTT、串口日志、CubeMonitor下面重点拆解三个我实测下来最省时间的工具。每一个我都会说清楚它解决什么问题、怎么用、适合什么场景以及哪些情况下它其实不好用。2.1 SEGGER RTT实时日志领域的效率天花板SEGGER RTT 绝对是我调试 STM32 以来用过最值的工具没有之一。它的核心原理非常聪明J-Link 调试器支持通过 SWD 接口直接读写目标芯片的内存SEGGER 就在芯片 RAM 里划了一块区域作为环形缓冲区你的固件往这个缓冲区写数据PC 端的 J-Link RTT Viewer 通过调试口把数据读出来显示。整个过程不占用串口、不需要额外布线、不需要在目标板上引出 TX/RX 引脚。实际使用中RTT 给我省下的时间主要体现在三个方面第一日志输出的开销极低。普通的 printf 走串口每条日志可能耗时几十到几百微秒取决于波特率和字符串长度在中断里打印甚至可能引起时序问题。RTT 写日志本质上就是往内存里拷贝一段数据一次写入大概只需要几微秒即使在中断服务函数里用也没有问题。第二它天生支持双向通信。RTT 不只能输出日志还能从 PC 端向芯片发送数据。这意味着你可以在程序运行的时候通过 RTT Viewer 直接修改 PID 参数、切换运行模式、甚至手动控制电机输出整个过程不需要重新编译烧录。我调伺服电机 485 通信的时候就是靠 RTT 在运行中改速度指令、改 PID 的 Kp 和 Ki改完立刻看电机响应曲线来回调参的效率比“改代码-编译-烧录-跑一下-再改”快了何止十倍。第三它能把多个通道分开用。RTT 支持多个上行通道你可以把调试日志、变量监控数据、错误告警分别输出到不同通道在 RTT Viewer 里用不同颜色窗口显示信息分类一目了然。我习惯通道 0 放普通调试日志通道 1 放周期性变量快照通道 2 放错误事件。但 RTT 也有它的适用边界。最核心的依赖就是硬件你必须用 J-Link或者支持 RTT 的第三方调试器如某些 DAP-Link 魔改版用 ST-Link 是不行的。如果你手头只有 ST-Link那 RTT 这条路暂时走不通老老实实串口日志。另外RTT 缓冲区是占 RAM 的默认配置 1KB 上行缓冲区在 RAM 紧张的小容量芯片上需要考虑一下开销。我在 F103C8T6 这种 20KB RAM 的芯片上也跑过 RTT把缓冲区从默认的 1KB 压缩到 256 字节配合上行通道只保留必要的日志完全够用。所以不要拿“RAM 太小”当借口关键是日志内容要克制。2.2 STM32CubeMonitor变量可视化比看数字直观太多了STM32CubeMonitor 是 ST 官方出的一个调试工具它的玩法比 RTT Viewer 更高阶它可以从正在运行的 MCU 里持续读取变量然后在 PC 端以波形、仪表盘、折线图的形式实时展示出来。简单说RTT Viewer 是看文本日志CubeMonitor 是看曲线图表。这个工具在什么场景下最省时间我的回答是调 PID 和看传感器数据流的时候。举个例子我在调一个基于 STM32 的智能台灯项目需要实时观察环境光传感器的采样值变化。如果只用串口打印你会看到一串数字不断刷新但你很难从数字里直观看出“变化趋势”。用 CubeMonitor 把传感器变量拉成折线图开灯关灯、手遮挡传感器时曲线怎么变化一眼就能看清哪个时刻出现毛刺也一目了然。还有一次调两轮差速小车我需要同时观察左右轮编码器的速度反馈和 PID 输出。用 CubeMonitor 配了三个波形窗口把左右轮实际速度和目标速度放一起对比哪个轮子响应慢、哪个轮子超调大看曲线立刻就知道不用再靠猜。CubeMonitor 的配置过程也很简单图形化界面里选变量、拖控件、关联变量几分钟就能搭好一个监控面板。需要说明的是它默认的底层实现也是基于调试接口读取变量地址所以调试器这块需要选择支持的型号SWD 接口连接好就能用。但 CubeMonitor 的坑也有第一它读取变量是周期性轮询的频率太高会影响 CPU 性能我一般设置在 10-50Hz 左右就足够了第二它只能观察全局变量局部变量看不到所以有时候你需要为了调试临时把某个变量提升成全局变量第三它不方便输出文本日志和 RTT 的定位是互补关系而非替代关系。2.3 自建串口日志系统没有 J-Link 时的最佳替代如果你手头没有 J-Link那串口日志仍然是最高性价比的调试手段。但这里我强调一下裸用 printf 和搭一套好用的串口日志系统是两码事。好的串口日志系统应该具备这些特点非阻塞发送、支持不定长接收、有日志分级、带时间戳、数据不容易丢失。我在实际项目里自己封装了一套核心配置是“串口 DMA 发送 空闲中断 DMA 接收”这套组合在 STM32 HAL 库下非常好用。先说发送端。HAL 库的 HAL_UART_Transmit 是阻塞的在日志量大的时候会卡住主循环我的做法是重定向 printf 到 HAL_UART_Transmit_DMA发送过程不阻塞 CPU日志数据量大的时候最多 DMA 缓冲满了丢几笔但主循环的实时性保住了。再说接收端。很多调试场景需要在运行中接收 PC 端下发的命令比如修改参数、切换模式。用串口空闲中断IDLE Interrupt加 DMA 就可以实现不定长数据接收DMA 一直把串口接收数据寄存器里的数据搬到内存缓冲区当一帧数据发完、总线空闲时触发空闲中断在中断里判断这次的帧长度并置标志位主循环再解析处理。这套方案配置好之后调试体验非常接近 RTT 的“日志命令”模式。而且串口转 USB 模块很便宜手头没有 J-Link 也能有不错的调试体验。3. 实操手把手搭一套高效的 STM32 调试环境理论说完了下面讲点能直接照做的。我把自己现在用的调试环境完整梳理一遍包括 RTT 怎么接进工程、串口日志框架怎么设计、以及如何组合使用它们。3.1 在 Keil HAL 库工程里快速接入 RTT接入 RTT 其实不复杂核心就三步拷贝 RTT 源码、初始化、写日志。我用的是 Keil MDK 加 STM32CubeMX 生成的 HAL 库工程操作过程如下从 SEGGER 官网下载 J-Link 软件包里面有个 RTT 文件夹通常在 SEGGER_JLink_Windows 安装目录下的 SEGGER/RTT 路径里面有几个关键文件SEGGER_RTT.c、SEGGER_RTT.h、SEGGER_RTT_Conf.h、SEGGER_RTT_printf.c。把这几个文件拷贝到工程目录加进 Keil 工程。在主程序初始化阶段调用 SEGGER_RTT_Init()建议放在系统时钟初始化之后、外设初始化之前。这个函数会清空缓冲区、配置通道不调用的话默认配置也能跑但保险起见还是显式初始化一下。写日志用 SEGGER_RTT_printf() 函数用法和 printf 类似。比如 SEGGER_RTT_printf(0, ADC value: %d\n, adc_val); 表示往通道 0 输出。也可以在 RTT_Conf.h 里配置缓冲区大小、使能哪个通道、是否支持 printf 功能。我遇到过不少新手问为什么我加了 RTT 的源码编译报错说找不到 SEGGER_RTT_Conf.h这个文件通常和 SEGGER_RTT.c 在同一个目录需要在 Keil 的 Include Paths 里加上 RTT 源码所在目录。接入后的验证方法很简单编译烧录打开 J-Link RTT Viewer选择设备型号连接后如果程序运行正常Viewer 里就会实时滚出日志。不出日志时优先检查J-Link 是否连接了 SWDIO/SWCLK/GND有时候还需要 VTref 参考电压引脚芯片是否被调试器识别RTT Control Block 地址是否能被 Viewer 自动发现。我在实际使用中发现如果芯片里同时烧录了 Bootloader 和 AppRTT 的 Control Block 地址在 App 里J-Link 有时会找不到。解决办法是在 RTT Viewer 的连接设置里手动指定 Control Block 地址或者用 SEGGER_RTT_Init() 之后把 SEGGER_RTT_CB 这个结构体变量的地址打印出来然后在 Viewer 里填进去。3.2 设计一套可复用的串口日志框架RTT 好用但串口日志这套也得留着因为不是所有场合都有 J-Link。我把自己常用的串口日志框架梳理出来供你参考。我给它取名“SLog”核心能力有三个日志分级、超时重发、命令回调。日志分级是指 LOG_INFO、LOG_WARN、LOG_DEBUG、LOG_ERROR 这样的等级宏不同等级可以开关比如发布版本只保留 ERROR 等级调试版本全开。实现上就是一个宏定义加一个全局等级变量日志输出前判断等级低于当前等级的日志直接跳过这个机制能帮你减少刷屏让有效信息更突出。超时重发指的是 DMA 发送失败时的处理。DMA 发送环形缓冲时会遇到缓冲满的情况我的做法是记录日志丢弃计数在串口空闲时把没发出去的日志补发或丢弃。实际调试中大多数场景下日志丢失一两条无伤大雅但如果要用日志做时序分析就得保证不丢这时候可以把发送等级调低或者改用阻塞发送。命令回调是为了实现类似 RTT 的双向通信。我在主循环里检测“串口收到一帧完整数据”的标志位然后把帧内容解析成命令和参数调用注册好的命令处理函数。比如输入“SET_KP 12.5”解析后调用 set_kp(12.5)在不重新编译的情况下完成参数调整。这个能力在调 PID、调传感器阈值时非常实用。设计这套框架的时候有几个关节需要重点处理第一是 printf 重定向。在 HAL 库环境下写一个 int _write(int file, char *ptr, int len) 函数ARMCC 编译器内部调用 HAL_UART_Transmit_DMA这样 printf 的输出就自动走 DMA 了。用 GCC 工具链的话需要实现 _write rt 或者 _write 的弱符号重写格式略有不同原理一致。第二是串口空闲中断的配置。在 CubeMX 里开启串口的全局中断然后在 HAL_UART_IRQHandler() 之前或者在其回调里判断 UART_FLAG_IDLE。HAL 库对空闲中断的处理不是特别友好需要自己清标志位。标准做法是void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 此时 DMA 已经收到的数据长度是当前缓冲区指针位置 // 保存这一帧的长度置标志位重新启动下一次 DMA 接收 } HAL_UART_IRQHandler(huart1); }第三是 DMA 的不定长接收。CubeMX 配好串口 DMA 接收注意要在接收中断回调里重新调用 HAL_UART_Receive_DMA 来重新武装 DMA否则接收一次后就停了。这三块配好一套能用的串口日志框架就出来了。我实际用下来在 115200 波特率下即使日志量很大系统实时性也基本不受影响。3.3 进阶组合用调试工具解决具体场景问题光有工具不会用也不行我给你拆解三个典型场景看看这些工具组合起来怎么解决实际问题。场景一调 ADC 多通道扫描循环采样 DMA。这是我遇到的最常见又最容易出问题的需求。ADC 多通道配好之后DMA 会把所有通道的采样值按顺序搬到数组里但有一个经典坑DMA 传输完成中断和 ADC 转换完成事件之间的时序配合。用 RTT 打印 DMA 搬运完的数组内容你能直观看到每个通道的数据是否错位、采样率是否满足、第一轮数据是否为无效值。当时我定位到问题出在 DMA 循环模式和 ADC 扫描顺序的配置上RTT 日志直接把每一轮的数据快照打出来两三分钟就看明白了。场景二跑 FreeRTOS 多任务时排查任务优先级和中断设置问题。FreeRTOS 的坑往往出在中断优先级和任务切换上。比如某个中断里调用了不安全 API或者某个任务被更高优先级任务饿死。用 RTT 可以在每个任务里打日志输出任务名和当前时间戳跑起来之后在 RTT Viewer 里看日志的时间顺序哪个任务占用时间过长、哪个任务卡住了一目了然。我自己还习惯用 RTT 输出 vTaskList 获取的任务状态表定期快照观察栈溢出的先兆。场景三调伺服电机 485 通信。485 通信需要控制方向引脚用串口日志系统把发送请求和接收响应都打出来然后用逻辑分析仪对比总线上的电平波形定位方向引脚切换时序是否准确。这个场景中串口日志负责记录“软件视角”逻辑分析仪负责记录“总线视角”两个视角一对照问题出在发送端还是接收端立刻清楚。4. 常见问题与排查技巧实录工具用多了会遇到各种奇奇怪怪的问题。我挑几个高频的问题整理一下每个都是自己踩过坑之后总结出来的。4.1 那些年我踩过的调试工具坑第一个高频问题J-Link 连不上芯片。这个排在所有问题之首。排查思路基本是固定的先查接线SWDIO、SWCLK、GND 三根线是底线VTref 如果有的话也要接不然调试器检测不到目标电压再查供电目标板必须独立供电或由调试器稳定供电欠压会导致 SWD 不稳定最后查芯片状态如果芯片进了低功耗模式或者被配置成 SWD 引脚复用调试器可能连不上。说到 SWD 引脚复用这里有个很实用的经验CubeMX 默认把 SWDIO 和 SWCLK 作为调试功能保留但如果你不小心把 PA13/PA14 复用成了普通 GPIO烧录一次之后就再也连不上调试器了。解决办法是用串口 ISP 模式全片擦除或者按住复位键的同时点击下载、在芯片复位瞬间连接调试器。比较保险的做法是任何设计里都要预留 SWD 引脚不要轻易复用。系统初始化阶段先保留调试口等功能稳定后再考虑复用。第二个高频问题RTT Viewer 不输出日志。常见原因有三个调试器型号不对ST-Link 不支持RTT Control Block 地址没找到程序没有跑起来或者在初始化之前就死机了。遇到前两种检查硬件和手动指定地址遇到第三种把 RTT_Conf.h 里的缓冲区设为 0 地址让 Viewer 用搜索模式尝试。第三个高频问题printf 重定向到串口之后程序卡死了。这个坑非常经典。HAL_UART_Transmit 是阻塞发送如果在中断里或者高优先级任务里调用 printf会一直等待发送完成形成死锁。我自己的规范是中断服务函数里一律不用 printf把日志内容存到缓冲区回主循环再统一发送如果必须实时打印用 RTT 而不是串口。第四个高频问题串口 DMA 接收不定长数据时数据帧错位。这通常是因为空闲中断的判定时机和 DMA 缓冲处理没配合好。我的处理办法是在空闲中断里读取 DMA 的当前数据计数寄存器计算本次接收长度然后在主循环统一按帧解析避免在中断里做太多数据处理。4.2 调试工具的实战心得与避坑建议下面这些话基本是我摸索几年之后觉得最有价值的经验篇幅也不长但你如果听进去了能少走不少弯路。第一日志内容要有自己的“仪式感”。比如每条日志加时间戳、加函数名上线之后排查问题时特别有用。我在 RTT 里常年打印系统运行时长ms配合事件日志能精确还原崩溃前最后一刻发生了什么。第二调硬件问题的时候先确认软件视角再上波形视角。软件日志说“我已经发送了指令”总线波形看不看两者一对照就知道问题在哪。很多模棱两可的通信问题都是这样定位的。第三调试工具不要只用一个。RTT 是主调试通道串口日志是备选CubeMonitor 是变量可视化逻辑分析仪是底层波形确认。有些场合你明知道用波形仪器更合适就不要硬用日志去猜。第四用调试工具的时候一定要想清楚“我要回答什么问题”而不是“我能打什么日志”。我见过很多新手日志打了一堆但回答不了“PID 为什么震荡”这个问题。正确的做法是先想清楚你想要观测哪些变量、哪些事件、什么频率再决定用哪个工具日志才真正有用。5. 最后再分享一下我的调试习惯文章写到这里核心内容基本都讲完了。最后我不做什么总结归纳就分享一个我自己的小习惯每次接到新的 STM32 项目我做的第一件事不是写业务代码而是把调试基础设施搭好——RTT 或者串口日志框架必须就位然后写一个简单的自检程序把 CPU 频率、编译器版本、关键外设初始化状态、堆栈使用情况全部打印出来。这样每次程序跑起来第一眼就能看到系统的基本状态是否正常远比出了问题再手忙脚乱地加日志高效。我个人在 STM32 调试上最大的体会是调试工具的差距本质上不是贵和便宜的差距而是思路的差距。RTT 也好、串口日志也好都只是帮你“看见”系统运行状态的手段真正值钱的是你判断“该看哪里”的能力。把这个能力练出来配合一套顺手好用的工具组合你在 STM32 项目上浪费的时间会大幅度缩减这也是这篇文章我最想传递的东西。