☰
STM32调试实战:RTT与串口日志系统的高效组合
2026/10/23 8:42:35 网站建设 项目流程

做嵌入式这些年,我一直在跟 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 调节这类实时性要求高的场景,系统必须一直跑着,你只能在旁边看数据变化。这时候 RTT(Real-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 库工程,操作过程如下:

  1. 从 SEGGER 官网下载 J-Link 软件包,里面有个 RTT 文件夹(通常在 SEGGER_JLink_Windows 安装目录下的 SEGGER/RTT 路径),里面有几个关键文件:SEGGER_RTT.c、SEGGER_RTT.h、SEGGER_RTT_Conf.h、SEGGER_RTT_printf.c。把这几个文件拷贝到工程目录,加进 Keil 工程。

  2. 在主程序初始化阶段调用 SEGGER_RTT_Init(),建议放在系统时钟初始化之后、外设初始化之前。这个函数会清空缓冲区、配置通道,不调用的话默认配置也能跑,但保险起见还是显式初始化一下。

  3. 写日志用 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 和 App,RTT 的 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 项目上浪费的时间会大幅度缩减,这也是这篇文章我最想传递的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询