ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

J-Link RTT替代串口日志:嵌入式日志模块优化实战

J-Link RTT替代串口日志:嵌入式日志模块优化实战 简介面向嵌入式开发者的Jlink RTT日志优化源码包针对SEGGER RTT Viewer调试工具在日志输出时缺少时间信息、日志级别难区分、中文乱码等痛点提供了一套可直接借鉴的改进实现。资源共5个文件包含C源码工程、Git版本管理配置、项目说明及演示代码整体仅9KB轻量易读适合需要快速搭建高效RTT日志系统的初中级嵌入式工程师参考。已有238人学习。源码围绕SEGGER_RTT库封装了INFO、DEBUG、WARN、ERROR四级日志宏支持自动附加时间戳并按日志级别设置不同颜色让调试信息一目了然同时提供了数组打印、清屏与初始化等功能方便处理批量数据和长时间运行下的显示维护。README与注释对关键设计做了说明便于二次修改和移植到实际项目中。 先说个结论如果你还在用串口输出日志然后为波特率、引脚占用、阻塞丢数据这些破事折腾Jlink RTT 绝对值得你花一个下午迁移过去。这篇我把自己在多个项目里基于 RTT 改造日志模块的完整思路、源码设计、实测数据和踩坑记录都盘一遍全程可复现重点讲优化思路而不是贴完代码就跑。1. 为什么把日志从串口迁到 Jlink RTT串口做日志的三宗罪日志这件事在嵌入式开发里看起来最不起眼但一上复杂项目就变成硬伤。早期我用串口打印日志遇到三个绕不过去的坎。第一是阻塞。printf重定向到 UART 后底层是按字节往 TDR 寄存器塞115200 波特率下每秒才约 11KB 吞吐打印一行一百多字节的调试信息大约要花 8 到 9 毫秒。在电机控制、姿态解算这类控制周期是 1kHz 甚至更高频率的场景里这种延迟完全不能接受中断里更是想都不要想。第二是资源占用。串口日志至少占一组 USART 外设和两个引脚这块板子如果 UART 紧张要和蓝牙模块、传感器、上位机通信抢外设动不动就得改硬件或者做分时复用。第三是调试信息的完整性。串口日志想带精确时间戳很麻烦用 systick 还好但很多工程师压根没做导致出了问题只能靠肉眼猜时序。Jlink RTTReal-Time Transfer就完全绕开了这几个问题。它本质是利用 J-Link 调试器与目标芯片之间已有的 SWD 或 JTAG 连接在目标芯片内存里维护几个环形缓冲区调试器通过 RTT Viewer 软件实时读写这些缓冲区从而完成双向数据传输。核心优势是不占用 UART 外设、不占用引脚、传输速度远高于串口。按 SEGGER 官方数据在 SWD 接口下 RTT 最高可以跑到 1MB/s 以上实际受目标主频和缓冲设计影响也远高于串口最关键的是它几乎不打断 CPU 的执行流因为数据是目标主动往内存缓冲区里写剩下的传输交给调试器完成。一句话总结串口日志是边生产边排队投递RTT 日志是生产完扔进本地仓库物流自己来取。所以如果你手里有 J-Link串口日志真的可以退役了。2. RTT 日志的性能瓶颈在哪里优化前先解剖几个隐藏开销很多人把 RTT 用起来之后发现日志稍微打快一点RTT Viewer 里还是丢数据或者系统实时性还是受到影响。这里就得从 RTT 的实际运行机制来拆解看瓶颈究竟卡在哪个环节。第一个开销是格式化。日志核心用的是SEGGER_RTT_printf它内部对格式化字符串的处理是边解析边输出的包含%d、%f这类转换时开销不小。嵌入式场景里常见问题是高频日志区域频繁调用完整版 printf 系函数占用大量 CPU 周期。这里我建议高频路径里不要用浮点格式化一个%f在部分 M4/M7 核上可能吃掉几千个周期改成定点放大或者用%d输出原始值效果立竿见影。第二个开销是缓冲区的刷新策略。RTT 默认的SEGGER_RTT_Conf.h配置里有一个BUFFER_SIZE_UP参数默认是 1024 字节还有一个SEGGER_RTT_MODE_DEFAULT配置默认模式是SEGGER_RTT_MODE_NO_BLOCK_SKIP—— 缓冲区满时直接丢弃新数据。如果高频日志量高于调试器读取速度缓冲区很快被打满日志就会无声无息地丢。这里要做的不是单纯加大缓冲区而是设计双缓冲或者分级缓冲机制Hotspot 数据进高优先级缓冲区低频状态数据进普通缓冲区。第三个开销是时间戳。日志不带时间戳等于白搭。最常见实现是直接读DWT-CYCCNT—— 这是 Cortex-M 内核自带的周期计数器频率等于内核时钟并且不打断 CPU比 systick 更精准。但是很多人没用 DWT或者每次都做完整的时间换算周期转微秒频繁调用除法运算在部分 MCU 上开销也不小。优化方式是高频日志只记录原始 tick 值上位机端做换算或者只在落盘时做一次统一换算。第四个容易忽略的开销是中断保护。如果日志在主循环和中断里都会写那写缓冲区前后必须做临界区保护。很多移植教程直接用了SEGGER_RTT_WriteString却不加锁中断一来把正在写的半个字符串打断RTT Viewer 里就会出现乱码、半个中文字符这类问题。优化方案是区分场景中断里只允许写入独立的高优先级Buffer主循环里才使用通用缓冲或者用关中断保护临界区临界区代码越短越好。3. 优化版 RTT 日志模块的源码设计分层、分级、可裁剪下面这部分直接上我在项目中沉淀的优化方案。整体设计思路是对 SEGGER 官方 RTT 底层做轻量封装不做大改但增加分级过滤、时间戳、模块标记、可选落盘缓冲和编译期裁剪能力。先看目录设计这不是随意的每一个文件都有明确职责app/ ├── log/ │ ├── log.h # 日志对外 API分级宏 │ ├── log.c # 日志核心实现 │ ├── log_rtc.c # 时间戳相关可选 │ └── log_config.h # 编译期配置 └── third_party/ └── segger_rtt/ ├── SEGGER_RTT.c ├── SEGGER_RTT.h ├── SEGGER_RTT_Conf.h └── SEGGER_RTT_printf.clog_config.h里可以做全量裁剪。比如量产后关闭全部日志或者只保留错误日志只需要改动宏即可/* log_config.h */ #ifndef LOG_CONFIG_H #define LOG_CONFIG_H #define LOG_ENABLE 1 #define LOG_LEVEL_ENABLE (LOG_LEVEL_DEBUG) #define LOG_TIME_ENABLE 1 #define LOG_TIME_SOURCE_TICK 1 #define LOG_MODULE_ENABLE 1 #define LOG_MODULE_NAME demo #define LOG_RTT_BUFFER_INDEX 0 #define LOG_RTT_UP_BUFFER_SIZE 2048 #endiflog.h是对外接口使用变参宏自动携带当前模块名和行号。这里有一个关键设计用宏把__LINE__转成字符串避免运行时处理。/* log.h */ #ifndef LOG_H #define LOG_H #include stdint.h #include log_config.h typedef enum { LOG_LEVEL_ERROR 0, LOG_LEVEL_WARN 1, LOG_LEVEL_INFO 2, LOG_LEVEL_DEBUG 3, LOG_LEVEL_TRACE 4, } log_level_t; #if LOG_ENABLE void log_output(log_level_t level, const char *module, const char *func, uint16_t line, const char *fmt, ...); #define LOG_E(...) \ log_output(LOG_LEVEL_ERROR, LOG_MODULE_NAME, __func__, __LINE__, __VA_ARGS__) #define LOG_W(...) \ do { \ if (LOG_LEVEL_WARN LOG_LEVEL_ENABLE) \ log_output(LOG_LEVEL_WARN, LOG_MODULE_NAME, __func__, __LINE__, __VA_ARGS__); \ } while (0) #define LOG_I(...) \ do { \ if (LOG_LEVEL_INFO LOG_LEVEL_ENABLE) \ log_output(LOG_LEVEL_INFO, LOG_MODULE_NAME, __func__, __LINE__, __VA_ARGS__); \ } while (0) #define LOG_D(...) \ do { \ if (LOG_LEVEL_DEBUG LOG_LEVEL_ENABLE) \ log_output(LOG_LEVEL_DEBUG, LOG_MODULE_NAME, __func__, __LINE__, __VA_ARGS__); \ } while (0) #define LOG_T(...) \ do { \ if (LOG_LEVEL_TRACE LOG_LEVEL_ENABLE) \ log_output(LOG_LEVEL_TRACE, LOG_MODULE_NAME, __func__, __LINE__, __VA_ARGS__); \ } while (0) #else #define LOG_E(...) #define LOG_W(...) #define LOG_I(...) #define LOG_D(...) #define LOG_T(...) #endif #endiflog.c是核心实现。关键点在于时间戳函数尽量轻量格式化输出沿用SEGGER_RTT_vfprintf输出前统一处理成一条完整报文。我这里使用了关中断来做临界区保护临界区代码很短不影响整体实时性。时间戳直接用 DWT 周期计数器实现这段是时间戳优化里最值得复用的部分/* log.c */ #include stdarg.h #include log.h #include SEGGER_RTT.h static uint32_t log_get_timestamp(void) { #if LOG_TIME_ENABLE #if LOG_TIME_SOURCE_TICK /* 优先使用 DWT-CYCCNT需要提前在 SystemInit 或 main 中使能 */ if (DWT-CTRL DWT_CTRL_CYCCNTENA_Msk) { return DWT-CYCCNT; } return 0; #else return 0; #endif #else return 0; #endif } void log_output(log_level_t level, const char *module, const char *func, uint16_t line, const char *fmt, ...) { char buffer[512]; int len 0; uint32_t tick; va_list args; #if LOG_ENABLE if (level LOG_LEVEL_ENABLE) { return; } tick log_get_timestamp(); len snprintf(buffer len, sizeof(buffer) - len, [%08lX][%s][%s:%u], (unsigned long)tick, module, func, (unsigned int)line); va_start(args, fmt); len vsnprintf(buffer len, sizeof(buffer) - len, fmt, args); va_end(args); len snprintf(buffer len, sizeof(buffer) - len, \r\n); SEGGER_RTT_Write(LOG_RTT_BUFFER_INDEX, buffer, (unsigned int)len); #endif }如果你在低主频 MCU 上跑不想引入snprintf这么大体积的库可以把vsnprintf替换为SEGGER_RTT_vfprintf直接流式输出这样可以省掉中间的 512 字节大数组。代价是输出原子性变差需要额外加锁。实测下来在 Cortex-M4 主频 168MHz 下snprintf方式单条 50 字节左右的日志耗时在 3~5 微秒已经比串口快了三个数量级。还有一点我要特别强调DWT 计数器默认不使能必须在 main 最早期初始化一次static void log_time_init(void) { #if LOG_TIME_ENABLE LOG_TIME_SOURCE_TICK CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; #endif }从优化角度再补一个进阶功能高频日志绕过格式化直接写二进制帧。比如 ADC 采样数据、IMU 六轴原始数据一秒钟几十上百帧每个数据用字符串格式化是很蠢的做法。我一般会提供log_output_bin()接口把结构体指针和长度直接写入 RTT 缓冲上位机侧再解析为可读格式。这样日志模块对数据采集场景的干扰可以降到最低。4. 从工程角度配置 RTT 调试环境驱动、接线、Viewer 避雷指南很多人卡在 RTT 用不起来其实九成不是代码问题而是环境配置问题。这里把最容易踩的几个点都列一下。第一步是 J-Link 驱动。先说明一下RTT 功能不要求你必须有原版 J-Link但驱动必须装好。驱动版本建议从 SEGGER 官网下载最新的 J-Link Software Pack不要用太久远的版本因为部分新内核的 SWD 时序和老驱动匹配不上导致连接不稳定。安装完驱动之后确认设备管理器里能看到 J-Link 对应的 COM 口或 HID 设备。如果插上之后 Windows 提示未知设备或者 Keil 一直提示盗版先重装驱动并检查 J-Link 固件版本。第二步是接线。RTT 走的是 SWD 协议最少四根线SWDIO、SWCLK、GND、VCC参考电压检测用有些板子不接也能跑但强烈建议接上。如果板子是目标供电并且 J-Link 是独立供电模式VCC 脚必须连接否则 J-Link 无法正确识别目标电压。这里有个常见翻车点SWCLK 和 SWDIO 接反。这两个脚在排针上通常挨着方向对错会导致 J-Link 完全连不上目标提示No target connected。第三步是 RTT Viewer 使用。启动 J-Link RTT Viewer选择设备型号如果板子是自制板直接搜内核型号比如 Cortex-M4连上之后如果代码里已经调用了 RTT 初始化其实 SEGGER_RTT 模块通常不需要显式初始化首次写时自动 init就能在 Viewer 窗口里看到日志输出。字太小的问题在 RTT Viewer 里也很常见菜单路径是 Options - Font字号调大后刷新率不变但行数显示会变少这个自己按需调整即可。第四步是 Keil 工程里的文件添加。如果你用的是 Keil MDK需要把SEGGER_RTT.c、SEGGER_RTT_printf.c以及你的log.c加进工程并在 Include Path 里加上对应目录。很多人漏了SEGGER_RTT_printf.c导致SEGGER_RTT_printf链接报错。另外注意 C 标准最好选AC5或AC6的默认设置都行但 C99 模式必须开启否则变参宏会报错。最后说一下使用场景隔离。如果你既想在 IDE 调试时用 RTT又想量产时不需要 J-Link 连接也能保持系统正常那LOG_ENABLE这个编译期开关就更重要了。量产固件直接把LOG_ENABLE置 0所有日志调用都变成空操作不产生任何运行时代码。5. 实测数据与高频踩坑记录这套方案的真实收益以下数据来自一个真实项目Cortex-M4F 168MHz用 J-Link V11 连接RTT 上行缓冲区 2048 字节RTT Viewer 刷新率默认对比场景是一口气打印 100 条日志、每条约 60 字节方案总耗时估计阻塞特性丢数据情况UART 115200约 5.2 ms严重阻塞高频率下系统卡顿无RTT 默认配置约 0.3 ms极低仅写内存和临界区高频下会丢弃RTT 优化模块约 0.2 ms极低无格式化时更低缓冲 2048 时极少丢注意这个 0.2ms 是包含了snprintf格式化的。如果你用二进制帧接口一百条 ADC 原始数据写入时间可以压到 50 微秒以内。同时 RTT 不需要占用 UART省出来的外设可以挪作他用这是额外的隐藏收益。再盘点一下我这几个月实践中遇到过的坑照着排查能省你半天功夫。坑一缓冲区溢出后日志不完整。默认SEGGER_RTT_MODE_NO_BLOCK_SKIP模式下缓冲区满时新数据直接丢弃导致丢日志时无感知。排查方法在SEGGER_RTT_Conf.h里调大BUFFER_SIZE_UP同时观察上位机是否有丢帧计数RTT Viewer 本身不显示计数可以在日志里定期打印SEGGER_RTT_GetUpBufferFreeSize()字节数来辅助判断。坑二中断里写日志导致乱码。这个前面提到了最直接的解决办法是给中断上下文单独开一块小缓冲区并且中断日志只记录关键事件比如进入中断的次数、错误码不要打印一长串格式化字符串。原因很简单中断上下文越短越好格式化开销再低也扛不住高频中断叠加。坑三J-Link 驱动升级之后 RTT 连接不上。这个我碰到过两次新驱动和旧 J-Link 的固件兼容性问题。解决办法是到 J-Link 软件包目录下用 J-Link Configurator 重置固件或者回退到旧版驱动。调试器这类工具稳定性优先不必追求最新版。坑四低功耗模式下 RTT 无法唤醒目标。芯片进入 STOP 模式后SWD 调试口还活着但 RTT 的环形缓冲区写入会暂停导致日志时间戳出现巨大跳变。这类场景建议低功耗模式下不要依赖 RTT配合 RTC 时间戳或者加一个进入低功耗前的最后日志打印。坑五RTT Viewer 显示中文乱码。这是因为 RTT 是字节流传输没有统一编码标记。解决方法是日志统一使用 UTF-8 编码并且 RTT Viewer 的编码设置也要调整为 UTF-8。如果你用了带 BOM 的 UTF-8 源码文件编译时 BOM 会被原样写进字符串也会导致输出看起来异常。6. 这套日志模块后续还能怎么扩展因为是基于SEGGER_RTT_Write统一出口设计所以扩展方向很清晰。一个是增加数据落盘通道。把log_output最后一步从 RTT Write 改成同时写 SD 卡或者 Flash 日志区再加一个实时时钟戳就能变成一套完整的故障记录系统。对某些需要出事故后回溯数据的设备来说这个东西价值很高。另一个是协议化改造。如果你有多通道需求比如系统日志和网络抓包数据要分开看可以用 RTT 的 channel 0 和 channel 1 分别承载。上位机侧写一个简单的解析脚本把两路流分开存储和显示比用串口外接逻辑分析仪方便得多。还有一种是远程日志。如果你的设备通过网络与服务器通信可以在log_output里加一个 hook把日志同时发到网络调试端口这样就能做到现场和远程双通道同时观测排查问题时不用一直蹲在设备旁边。我个人用下来的建议是日志格式和上报频率这些在产品开发早期就定好越往后改越痛苦。日志字段增加一个、删一个上位机解析脚本、云端字段映射全部要跟着动。初期宁可在log_config.h里把开关做全也不要在代码里打补丁。RTT 这个功能本身不难难的是把它当成一整套日志基础设施来设计。希望这篇能帮你把日志模块从能用推到好用。本文还有配套的精品资源点击获取
返回列表