news 2026/10/29 21:59:35

STM32嵌入式调试:用SEGGER RTT替代串口日志实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32嵌入式调试:用SEGGER RTT替代串口日志实践

调试一块跑着实时任务的板子,最难受的不是写不出逻辑,而是不知道它到底跑到哪一步了。早期我一直靠串口打日志,115200 波特率下打一行三十来个字节的调试信息,光传输就要两毫秒多,主循环里放三五行打印,实时性直接被拖垮;更别说有些板子调试串口和业务串口抢同一个外设,接上串口线程序就跑飞。后来我把这套东西整体换成了 RTT 日志,STM32 上用 SEGGER RTT 打日志这个方案,本质上就是借调试器的那根 SWD 线,把日志塞进芯片 RAM 里的一块环形缓冲区,上位机通过调试接口去“偷看”这块内存。它不占用任何串口外设,不需要额外接线,对 CPU 的打扰极小,打印速度比串口快两个数量级。这篇内容适合三类人看:正在被串口打印拖慢实时性的嵌入式开发者、手上有调试器但只会用来单步的工程师、以及想给自家产品做一套轻量日志系统的同学。下面我把从移植、配置、封装到踩坑的完整过程摊开讲一遍。

1. 我为什么把串口日志换成了RTT

1.1 串口打印在真实项目里的四个硬伤

先说清楚痛点在哪,不然你不会有动力换。我做过一个电机控制的项目,主控跑 FOC,电流环 20kHz 中断,一开始为了看环路中间变量,在中断里加了printf,现象是电机直接啸叫。后来算了一下,printf走半主机或者重定向到串口,一行二十字节在 115200 波特率下需要大约 1.7 毫秒,而 20kHz 中断周期只有 50 微秒,一次打印直接把后面三十多次中断全部堵死,环路不炸才怪。

第二个硬伤是外设冲突。很多小板子只有一个 USART 引出来做调试口,业务上又要接一个模块通信,两个功能抢一个外设,只能在代码里写一堆#ifdef DEBUG来切换,改一次配置烧一次程序,效率极低。第三个是接线麻烦,产品装进壳子里以后,串口线根本插不进去,只能拆壳。第四个是阻塞性,标准库的printf在发送缓冲满的时候会死等,主循环里一个没注意就卡住了,而且卡住的地方往往跟日志本身毫无关系,排查方向直接被带偏。

RTT 把这四个问题一次性绕开了。它不需要串口外设,不需要额外引脚,日志走的是调试器那条 SWD 通道,写缓冲区是纯内存操作,即使上位机没开、没人看,目标端也只是往 RAM 里拷一段数据然后指针前移,不会阻塞业务逻辑。

1.2 RTT到底是怎么工作的

很多人用了一年 RTT 也没搞明白它的原理,遇到“找不到控制块”就抓瞎。其实机制非常简单,一句话概括:目标芯片在自己 RAM 里开一块带描述头(header)的缓冲区,主机通过调试接口单向读取这块内存。

具体来说,目标端会定义一个全局结构体,也就是所谓的控制块,它的开头十六个字节是固定的标识字符串,后面跟着最多三个上行通道和三个下行通道的描述符。每个通道描述符里记录了缓冲区首地址、缓冲区大小、当前写指针、当前读指针和标志位。目标端代码只负责往缓冲区里写数据并更新写指针,主机端则周期性地通过 SWD 读取写指针和缓冲区内容,读完之后它自己维护一个读指针副本,不回写目标内存。

这个“只读不回写”的设计是关键。它意味着主机端的读取行为对目标芯片零干扰,不需要目标端配合中断,也不需要在目标端跑任何服务程序。哪怕你的程序卡在死循环里、卡在 HardFault 里,只要 CPU 还在跑,RAM 还有电,上位机就能把日志读出来。这一点在排查死机问题时价值巨大,串口打印在死机后基本就哑了,而 RTT 还能把死机前最后那几行吐出来。

还有个细节值得说:控制块在 RAM 里的地址不是固定的,跟链接脚本、变量顺序、编译选项都有关系。所以上位机一般用“自动搜索”的方式,在指定的 RAM 范围内按对齐粒度扫描那块固定的标识字符串。搜索耗时通常在一秒以内,但如果你的 RAM 开了很多段、或者程序把这块内存清零了、或者标识被链接器优化掉了,就会搜不到。

1.3 哪些场景该用,哪些场景别硬上

RTT 不是万能药,我得把边界说清楚。

适合用的场景很明确:开发调试阶段需要高频打印、需要看中断里的变量、板子封在壳里没法接线、串口已经被业务占用、需要在不打断程序的情况下长时间记录日志。特别是那种需要连续跑几个小时观察状态机的场景,RTT 配合上位机的日志落盘功能,比串口稳得多,因为串口会受波特率误差和干扰影响丢字节,而 RTT 走的是数字调试通道。

不太适合的场景也得讲。第一,量产固件里如果完全删掉调试器支持,RTT 就没意义了,它依赖调试接口活着。第二,如果你的日志量极大,比如每毫秒打几百字节,而主机端轮询频率跟不上,缓冲区会溢出,默认策略是整条丢弃,你会丢日志而不是卡程序,这时候要么加大缓冲区要么降低打印频率。第三,低功耗休眠场景要小心,芯片进 STOP 模式后 CPU 停了,自然不会写缓冲区,上位机读到的是旧数据,容易误判。第四,如果手头只有一个最便宜的调试器且不支持内存读取,那也别折腾了。

2. 把RTT源码塞进STM32工程的完整步骤

2.1 动手前要准备的东西

先说清楚清单,免得你中途发现缺东西。硬件上需要一块能正常下载调试的 STM32 板子、一个支持 SWD 的调试器、一根连接线。软件上需要目标端源码,也就是那几个 C 文件和头文件,一般随调试器厂商的软件包一起提供,也可以在它的安装目录里找到,通常包含SEGGER_RTT.c、SEGGER_RTT.h、SEGGER_RTT_printf.c、SEGGER_RTT_Conf.h这几个文件,另外还有一份可选的SEGGER_RTT_Syscalls_*.c用来重定向标准库的输出。上位机工具用调试器厂商提供的 RTT 查看器就行。

版本选择上我建议用近两三年的版本,老版本在 Cortex-M0 和小内存芯片上有些边界问题,新版本对SEGGER_RTT_Conf.h的配置项划分更清晰,缓冲区大小、模式、锁机制都能在头文件里改,不用去动 C 文件。我见过有人在 C 文件里手改宏定义,升级一次全丢了,非常难受。

还有一点容易被忽略:确认你的调试器固件版本和上位机版本匹配。版本差距太大时,上位机能连上芯片但搜不到控制块,你会白白怀疑半天代码。

2.2 源码目录怎么放、文件怎么加

我习惯在工程根目录下建一个Middlewares/RTT目录,把四个核心文件放进去,然后在 IDE 里新建一个分组把这些文件加进去,头文件路径加一条。这里有个坑要提前说:SEGGER_RTT_printf.c里实现了格式化输出,它内部是一套精简的vsnprintf,如果你工程里已经用了标准库的printf,两者并不冲突,但会多占一点 Flash,大概一两 K。如果 Flash 紧张,可以只保留SEGGER_RTT.c,用SEGGER_RTT_Write和SEGGER_RTT_WriteString打日志,自己拼字符串。

SEGGER_RTT_Conf.h是配置入口,重点改三个地方:上行缓冲区默认大小BUFFER_SIZE_UP、下行缓冲区大小BUFFER_SIZE_DOWN、默认写入模式SEGGER_RTT_MODE_DEFAULT。下行缓冲区只在主机往目标发数据时才用,平时打日志用不到,保持 16 字节即可,不用浪费 RAM。

加文件的时候注意别把SEGGER_RTT_Syscalls_GCC.c之类的系统调用重定向文件一起加进去,除非你确实想用printf直接输出到 RTT 通道。那个文件会覆盖_write,跟你的串口重定向函数冲突,链接时会报重复定义,这个错误信息很不直观,会指向 newlib,第一次遇到容易懵。

2.3 三种工具链的链接配置差异

这一步是最容易出错的地方,必须单独讲。RTT 的控制块必须一直存在于 RAM 中,且不能被链接器当作未引用符号回收。不同工具链的处理方式不一样,我整理成表。

工具链关键处理典型写法或配置
ARMCC / AC6需要used属性防止被删,段名可自定义源文件内已有__attribute__((used)),链接脚本保留.bss.SEGGER_RTT段
GCC需要防止--gc-sections回收用KEEP(*(.bss.SEGGER_RTT))或源文件内加__attribute__((used))
IAR需要__root或者 keep 指令在.icf里加keep { section .bss.SEGGER_RTT };

我实际踩过的坑是 GCC 下开了-ffunction-sections -fdata-sections -Wl,--gc-sections三个选项做体积优化,结果控制块被整个回收,程序里所有 RTT 调用都变成往一个已经被释放的地址写数据,现象是打印没输出但不报错,串口那边一切正常,非常隐蔽。后来在链接脚本的.bss段里加了KEEP才解决。

还有一个细节:控制块里的缓冲区应该放在普通 RAM 段,别放到会被启动代码清零之外的保留区,也别放到需要掉电保持的备份 SRAM 里。如果你用了内存保护单元或者把 RAM 分了块,要确认这块内存是可写的、且不与其他功能重叠。我见过有人把 RTT 缓冲区地址手动指定到了栈区附近,跑一段时间后栈长上来把控制块覆盖了,日志开始输出乱码,查了两天才定位到。

2.4 初始化放在哪里最稳妥

SEGGER_RTT.c里有个自动初始化机制,第一次调用任何 RTT 函数时它会检查控制块是否已初始化,没初始化就现场初始化。这个机制很方便,但有两个隐患:一是在多任务环境下,两个任务同时首次调用可能产生竞态;二是在低功耗唤醒后,如果 RAM 内容异常,自动判断可能出错。

我的做法是在main函数里、外设初始化之前显式调用一次初始化,然后再显式配置一次上行缓冲区。这样控制块的地址和内容在系统启动阶段就确定了,后面不管从哪里调用都稳定。对于跑 RTOS 的工程,把初始化放在调度器启动之前,别放在某个任务里,否则别的任务抢在前面打印就会出现缓冲区还没准备好的情况。

#include "SEGGER_RTT.h" static uint8_t s_log_buf[2048]; void log_init(void) { SEGGER_RTT_Init(); SEGGER_RTT_ConfigUpBuffer(0, "LOG", s_log_buf, sizeof(s_log_buf), SEGGER_RTT_MODE_NO_BLOCK_SKIP); }

这里我用自己定义的静态数组代替默认缓冲区,好处是缓冲区大小一目了然、可以按需调整,而且编译器会帮你检查是否超了 RAM 预算。名字参数填一个短字符串就行,上位机会把它显示在通道名上,多通道时方便区分。

3. 核心API与缓冲区参数的取舍

3.1 三个必须记住的API

RTT 的接口不多,真正天天用的就三个。

SEGGER_RTT_Write(chn, buf, len)最底层,往里拷一段二进制数据,不解析格式、不加换行,适合打二进制采样数据,比如把 ADC 原始值直接怼出去。

SEGGER_RTT_WriteString(chn, str)打一个以零结尾的字符串,不做任何格式化,速度最快。打固定提示信息就该用它,比如状态机切换、初始化完成这类不需要变量的日志。我做过对比,同样二十个字符,WriteString比printf快大概三到五倍,因为跳过了格式解析。

SEGGER_RTT_printf(chn, fmt, ...)最常用,支持%d %x %s %c %u %p这些常见格式。有个细节要注意,它不支持浮点。想打浮点有两种办法:手动拆成整数部分和小数部分分别打,或者把浮点数乘 1000 取整后按整数打。后者在绝大多数调试场景够用了,别为了打个温度值去开printf的浮点支持,那会多吃好几 K 的 Flash。

另外还有个SEGGER_RTT_HasKey()和SEGGER_RTT_GetKey(),用于主机往目标发数据,做交互式调试时有用,比如你可以在上位机里敲个字符让程序切换日志级别,不用重新烧录。

3.2 UP缓冲区大小到底该开多大

这是最需要算一算的地方。缓冲区太小会丢日志,太大会白占 RAM,得按实际情况估。

计算逻辑是这样的:先估算你的峰值日志速率,也就是单位时间内写入的字节数,再估算主机端的轮询间隔,两者相乘就是缓冲区的最低需求。举个例子,假设你的日志平均每行 60 字节,峰值每秒 200 行,那么峰值写入速率是 12000 字节每秒。上位机的轮询周期保守按 20 毫秒算(实际更快,但考虑系统卡顿留余量),那么两次读取之间最多堆积 240 字节。看起来 256 字节就够,但这是平均情况,实际日志经常是突发的,比如初始化阶段一口气打几百行,或者某个异常分支里连续打印。所以我一般按峰值的五到十倍留余量。

场景峰值写入速率建议缓冲区
偶尔打几条状态低于 1KB/s512 字节
主循环常规打印5–15KB/s1–2KB
中断内高频打印30KB/s 以上4–8KB
连续波形数据采集百 KB/s 级结合降采样,或改用二进制通道

还有一点:SEGGER_RTT_MODE_NO_BLOCK_SKIP模式下,如果剩余空间放不下一整条日志,它会整条丢弃,而不是写一半。所以缓冲区最好能容纳单条日志的整数倍,避免出现“总是差几个字节写不进去”的尴尬。我曾经把缓冲区设成 1020 字节,结果每次写到接近满的时候就频繁丢整行,改成 1024 就好了。这不是玄学,是环形缓冲区的边界处理逻辑决定的。

3.3 三种写入模式的真实区别

模式这个参数很多人随手填默认值,其实三种模式的差别直接决定程序会不会卡死。

跳过模式(默认)是最常用的。缓冲区空间不够时,整条日志直接丢弃,函数立刻返回,写入失败会给一个负的返回值。它永远不会阻塞,所以中断里、临界区里都能用。缺点是丢日志的时候你不知道丢了什么,只能看到中间断了一截。

裁剪模式会把日志按剩余空间截断写入,写一半丢一半。这个模式适合打那种超长数据块,比如一次输出一整个结构体,宁可看到残缺内容也别全丢。但日志行会被截得乱七八糟,可读性差,我一般不用。

阻塞模式最危险。缓冲区满的时候它会原地死等,等主机把数据读走腾出空间。这在主循环里勉强能用,但你能想象在 20kHz 中断里调用它会发生什么:主机端只要没开或者卡了一下,中断就再也不返回,整个系统挂死。而且这个挂死很隐蔽,因为看起来程序在跑(调试器能暂停、能单步),但业务逻辑一动不动。

我的原则很简单:中断和临界区里只用跳过模式,主循环里也用跳过模式,如果需要保证日志不丢,那说明你的日志量已经超出了调试需求,应该改成采样打印,而不是靠阻塞来保证。

3.4 封装一套能打级别的日志宏

裸调SEGGER_RTT_printf写日志,写到后面你一定会想要级别、时间戳和模块名。这三个东西能让几十万行日志变得可检索,值得花半小时封装一次。

/* 需要先在 main 里使能 DWT 周期计数器 */ static inline uint32_t log_ts_us(void) { return DWT->CYCCNT / (SystemCoreClock / 1000000U); }

DWT 是 Cortex-M3 及以上内核自带的周期计数器,开了以后几乎零开销。使能代码就三行,放在时钟初始化之后:

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

注意 Cortex-M0 和 M0+ 没有 DWT,这两颗核上就只能用 SysTick 计数做粗粒度时间戳,精度到毫秒级,够用但别指望微秒。

日志宏可以这么写,GCC 和 ARMCC 对新版 C 的可变参数宏支持都还行:

#define LOG_TAG "MAIN" #define LOGI(fmt, ...) \ SEGGER_RTT_printf(0, "[%lu][I][%s] " fmt "\r\n", \ (unsigned long)log_ts_us(), LOG_TAG, ##__VA_ARGS__) #define LOGE(fmt, ...) \ SEGGER_RTT_printf(0, "[%lu][E][%s] " fmt "\r\n", \ (unsigned long)log_ts_us(), LOG_TAG, ##__VA_ARGS__)

模块名用#define LOG_TAG在每个文件开头重定义,编译器在包含头文件顺序上会有讲究,建议在头文件里先#undef LOG_TAG再给默认值,避免互相污染。这个技巧不新鲜,但确实省事。

这里提醒一句:日志级别不要做太细,INFO/WARN/ERROR 三级足够了。我见过有人分了七级,结果实际用起来永远是全开,分级形同虚设。真要做运行时开关,不如用全局变量控制,配合HasKey从上位机动态切换。

4. 上位机连接与第一次跑通的现场记录

4.1 上位机怎么配才能连上

第一次跑通的过程我记录一下,很多细节官方文档不会写。打开上位机工具后,需要配三样东西:连接方式选 USB(如果你的调试器是网络版就选 IP)、目标芯片型号选对、控制块位置选自动搜索。

芯片型号这一步必须选准。选错了不一定会报错,但内存地址映射不对,搜索范围就偏了,结果就是一直转圈找不到。有些型号有多个变体,比如 Flash 大小不同,选哪个影响不大,但内核类型必须对,因为搜索范围是按内核的地址空间来的。

自动搜索的等待时间一般设成几秒就够。如果超过五秒还没搜到,别傻等,直接去排查,八成是程序没跑起来或者控制块被优化了。这时候你可以先在代码里确认 RTT 初始化确实执行了,用一个 GPIO 翻转或者断点验证一下。

搜到之后通道列表会出现你配置的通道名,选中通道 0,日志就会滚出来。如果日志出来是乱码,先检查字符编码,再检查是不是把二进制数据打到了文本通道,最后检查波特率概念在这里不适用,RTT 没有波特率,别去调那个。

4.2 找不到控制块的那几分钟

这个提示大概是 RTT 使用中最高频的错误。我把排查顺序写下来,按这个走基本五六分钟能定位。

第一,确认程序真的在跑。用调试器暂停一下,看 PC 是不是停在正常位置,别是 HardFault 或者停在启动文件的死循环里。第二,确认初始化函数被调用了。可以在初始化后面加一句打固定字符串的代码,如果这句都出不来,说明根本没走到。第三,确认控制块没有被链接器删掉。打开 map 文件搜控制块的符号名,如果没有,回到 2.3 节改链接配置。第四,确认搜索范围覆盖了控制块所在地址。map 文件里能看到地址,手动填进去试试。第五,确认 RAM 内容没被清掉。有些工程在启动时会把整片 RAM 清零,如果你把控制块放到了某个被特殊处理的段里,清完就没了。

还有一种很隐蔽的情况:程序跑起来后进入了低功耗模式,CPU 停了,上位机搜不到。这时候要让芯片保持唤醒状态再连。

4.3 多通道分离输出

通道 0 是默认通道,所有日志混在一起。当你的系统有多个模块、多个任务时,全混在一个通道里根本没法看。RTT 支持最多三个上行通道,可以按模块分。

static uint8_t s_can_buf[512]; static uint8_t s_motor_buf[1024]; void log_channels_init(void) { SEGGER_RTT_Init(); SEGGER_RTT_ConfigUpBuffer(0, "SYS", NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP); SEGGER_RTT_ConfigUpBuffer(1, "MOTOR", s_motor_buf, sizeof(s_motor_buf), SEGGER_RTT_MODE_NO_BLOCK_SKIP); SEGGER_RTT_ConfigUpBuffer(2, "CAN", s_can_buf, sizeof(s_can_buf), SEGGER_RTT_MODE_NO_BLOCK_SKIP); }

注意通道 0 我传了空指针和零长度,意思是使用默认缓冲区,这样不用额外分配。上位机里会显示三个独立的窗口,你可以只盯电机那个窗口,也可以把三个窗口的日志同时落盘到不同文件,事后按时间对齐分析。这个功能在查“CAN 收包和电机响应时序对不上”这类问题时特别好用,因为三个通道的数据在时间轴上是连续的。

三个通道的上限是硬限制,想要更多通道就得自己改源码里的通道数宏,但我不建议,通道多了反而眼乱。真需要细分,用日志里的模块名筛选更灵活。

5. 实测性能与几个容易翻车的场景

5.1 串口和RTT的耗时对比

我做过一组对比测试,条件是同样的芯片、同样的主频、打同样的一行四十字符日志,用 GPIO 翻转配合逻辑分析仪测函数执行时间。

输出方式单次耗时(约)说明
串口 115200 轮询发送3400 微秒阻塞等发送完成
串口 115200 + DMA30 微秒(返回时间)但受总线占用影响
RTT 跳过模式8 微秒纯内存拷贝
RTT 跳过模式(缓冲区满)2 微秒直接丢弃返回
RTT 阻塞模式(空间充足)10 微秒拷贝完成即返回

差距是数量级的。串口那一栏的 3.4 毫秒意味着在 1kHz 的控制循环里,一次打印就吃掉三个周期,实时性无从谈起。而 RTT 的八微秒对 1kHz 循环来说只占不到百分之一,勉强可以接受;如果是 20kHz 那种,我还是建议只打采样后的日志,别每次都打。

缓冲区满时反而更快这个现象值得解释一下:跳过模式发现放不下就直接返回,连拷贝都省了。所以你不会因为日志太多而变慢,只会因为日志太多而丢日志,这个设计取舍我认为是对的。

5.2 中断里打印的红线

在中断里用 RTT 有两条红线。

第一条,绝对不能用阻塞模式,理由前面说过。第二条,即使是用跳过模式,也要注意中断优先级和嵌套。RTT 内部其实没有加锁,同一个通道被两个不同优先级的中断同时写入时,写指针的更新可能出现竞争,表现是偶尔丢一行或者出现半行乱码。概率不高但确实存在。

解决办法有两个:一是每个中断用独立的通道,互不干扰;二是在写之前短暂关中断,代价是几十个时钟周期,一般能接受。我一般不关中断,因为 RTT 的写入窗口本来就极短,竞争概率在千分之一以下,调试日志丢一行不影响判断。但如果你在做一个需要严格日志完整性的场景,那就老实加保护。

还有一个实践建议:中断里别做格式化。SEGGER_RTT_printf内部的格式解析要几百个周期,在 20kHz 中断里依然是负担。我的做法是在中断里把关键变量存到全局数组,然后在主循环里批量打出来,这样既能看到数据,又不影响实时性。

5.3 和RTOS一起跑要注意的事

跑 FreeRTOS 的时候用 RTT,最容易出的问题是日志交错。两个任务同时调用printf,格式化过程是可重入的吗?SEGGER_RTT_printf内部用了一个静态的格式化缓冲区(或者在栈上,取决于版本和配置),如果是静态的,多任务并发就会出现一个任务的日志被另一个任务覆盖,打出奇怪的内容。

稳妥的做法是给日志加一把互斥锁,或者干脆在日志宏里做临界区保护。用 FreeRTOS 的话,taskENTER_CRITICAL包住整个打印调用就行,但要注意别在临界区里做阻塞操作,跳过模式本来也不阻塞,问题不大。

另一个坑是优先级反转。如果你的日志宏里用了互斥量,高优先级任务等低优先级任务释放锁,而低优先级任务被中优先级任务抢占,就会反转。日志这种场景不值得引入互斥量,用临界区更简单。

还有个更简单的方案:每个任务用不同的 RTT 通道。任务数量不多的时候这个方案最干净,完全没有竞争,上位机按窗口分开展示,看日志就像看多个独立终端。

5.4 低功耗与复位后的表现

这一块很少有人讲,但实际项目一定会遇到。

芯片进入 STOP 或 STANDBY 模式后,CPU 停止运行,自然就不再写缓冲区。上位机那边看到的是 “连接正常但没数据”,容易误以为是程序卡死。这时候可以在进入低功耗前打一条明确标记,比如LOGI("enter stop mode"),然后上位机就停在那一行不动,你一看就知道是进休眠了而不是死机。

唤醒之后有个隐患:如果用的是 STANDBY 模式,RAM 内容会丢失,控制块里的标识符、指针全部变成垃圾数据,自动初始化机制可能判断失误。这种情况下必须把初始化重新做一遍,我的做法是在唤醒后的启动代码路径里显式重新调用log_init()。如果是 STOP 模式,RAM 保持,一般不用重初始化,但缓冲区里残留的数据可能被主机读出来,看起来像“醒来后打了一堆乱码”,其实是休眠前没写完的内容,属正常现象。

复位是另一种情况。软件复位或者看门狗复位后,控制块会被启动代码重新初始化,但上位机可能还保持着原来的连接,这时候需要手动重新搜索控制块,否则会读到旧地址的无效数据。上位机一般有自动重连选项,勾上会省事很多。

6. 常见问题速查与踩坑清单

6.1 问题速查表

下面这张表是我和同事这些年攒下来的,遇到问题先对一遍,能省不少时间。

现象最可能原因处理方式
上位机提示找不到控制块程序没跑、控制块被优化、搜索范围不对查 map 文件、加 KEEP、手动填地址
能连上但没日志通道配错、初始化没执行、模式填错确认通道号,确认初始化顺序
日志偶尔丢失整行缓冲区太小、跳过模式丢包加大缓冲区或降频打印
日志出现半行或乱码多任务或多中断并发写同一通道分通道或加临界区保护
打印浮点数输出错误RTT 的 printf 不支持浮点拆整数小数分别打
程序偶尔卡死用了阻塞模式且在中断里调用全部改成跳过模式
换台电脑连不上调试器驱动或固件版本不匹配统一上位机和驱动版本
一加日志程序就变慢打印频率过高、格式化开销大改采样打印、用 WriteString 打常量
复位后日志错乱上位机未重新搜索控制块开启自动重连,或手动刷新
低功耗后无输出CPU 停止、RAM 掉电休眠前打标记,唤醒后重初始化

6.2 几条用血换来的经验

第一条,别在中断里用阻塞模式。这句话我说了三遍,因为每年都能见到有人栽在这上面,而且现象是“程序跑着跑着就不动了,重启就好”,这种偶发问题最难查,往往查两天最后发现是日志本身惹的祸。

第二条,缓冲区不要放在栈上或者局部作用域。SEGGER_RTT_ConfigUpBuffer只保存指针不拷贝内容,如果你的数组是函数内的局部变量,函数返回后那块内存就被复用了,日志会写入一片不属于你的区域,轻则乱码,重则改掉别的变量。必须用静态数组或者全局数组。

第三条,日志字符串尽量短。一条日志写几十个字符,几秒钟就能刷满屏幕,真正有用的信息被淹没。我现在的习惯是,正常流程只打状态切换和关键参数,异常路径才打详细信息,而且异常路径本身就该少发生。有人喜欢把每个函数的进出都打一遍,那种日志在 RTT 里跑起来就是灾难,屏幕上什么都看不清。

第四条,保留一份“关闭日志”的编译开关。发布版本里把所有日志宏定义成空,既省 Flash 又省 CPU。宏定义成空有个小技巧,写成do { } while (0)而不是直接空,这样if (x) LOGI(...); else ...这种写法才不会出编译错误。

#ifdef ENABLE_LOG #define LOGI(fmt, ...) SEGGER_RTT_printf(0, fmt "\r\n", ##__VA_ARGS__) #else #define LOGI(fmt, ...) do { } while (0) #endif

第五条,多准备一个“裸打”通道。有时候怀疑格式化函数本身出问题,直接在异常处理里用SEGGER_RTT_WriteString(0, "boom\n")打个裸字符串,能出来就说明 RTT 通道是通的,问题在别处;出不来才说明是 RTT 配置问题。这个二分法很好用。

我个人在实际项目中的体会是,RTT 这套东西真正的价值不在于“比串口快”,而在于它把日志从“占用硬件资源的功能”变成了“几乎零成本的观察手段”。当你不再需要为了一行打印去权衡实时性、不再需要为了接线去拆机壳,你调试时的心态会完全不一样,会更愿意去看、去验证,而不是凭感觉猜。这个心态上的变化,比任何具体参数的调优都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 21:57:06

企业数字化转型:供应商协同平台的高效实践

1. 企业数字化转型的切入点选择企业数字化转型不是一蹴而就的过程,需要找到合适的切入点。根据我十多年为企业提供数字化咨询的经验,一个好的切入点应该具备三个特征:见效快、投入可控、可扩展性强。很多企业数字化转型失败,往往是…

作者头像 李华
网站建设 2026/10/29 21:56:56

impeccable:从词源到实战,彻底掌握这个高级评价词

说完美,别只盯着 nice、good、wonderful 这些幼儿园词汇了。我想认真聊聊 impeccable 这个词。大概五年前我在给一个客户写产品文案时,为了描述一款护手霜的质地,我用了 smooth、silky、velvety,最后在打印预览前一刻,…

作者头像 李华
网站建设 2026/10/29 21:56:54

AI+BI智能分析平台:自然语言驱动业务决策

1. 项目背景与核心价值在数字化转型浪潮中,数据分析能力正从专业岗位向一线业务人员下沉。传统BI工具虽然降低了技术门槛,但面对复杂业务场景时,业务人员仍需要掌握数据建模、公式编写等技能。我们团队通过整合AI能力与BI平台,实现…

作者头像 李华
网站建设 2026/10/29 21:53:19

营销技能的本质与实战:从用户洞察到数据驱动的内容营销体系

1. 营销技能的本质拆解:先别急着学更多,先建立坐标系在互联网行业里,营销技能这四个字被严重低估了。很多人以为营销技能就是会写文案、会投广告、会做短视频,结果真到了做增长的时候,发现策划案写了一堆,预…

作者头像 李华
网站建设 2026/10/29 21:51:39

Superpowers技能体系全解析:从概念到实操搭建个人能力增强系统

1. 从“superpowers”这个热词说起:它到底指什么最近一段时间,“superpowers”这个词在技术社区和效率工具圈子里被反复提起。很多人第一次看到它,会以为是某个新出的超级英雄电影,或者某个游戏里的技能系统。但如果你在开发者社区…

作者头像 李华
网站建设 2026/10/29 21:49:07

Hyperframes超帧结构:从组件化到帧化的架构设计与调度实践

1. 拆解 hyperframes:它到底是什么,能解决什么问题第一次看到“hyperframes”这个词,很多人会下意识地把它和“超高速”“超频”“超媒体”这类概念挂钩。我最初接触这个方向时也有同样的困惑,直到真正把它放进实际项目里跑了一遍…

作者头像 李华