ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SEGGER RTT嵌入式调试技术:原理、集成与实战应用指南

SEGGER RTT嵌入式调试技术:原理、集成与实战应用指南 1. 项目概述SEGGER RTT到底是什么如果你在嵌入式开发领域摸爬滚打了一段时间尤其是在使用ARM Cortex-M这类MCU时调试和日志输出绝对是你绕不开的“日常”。传统的调试方式比如串口打印UART大家肯定都用过。接上串口线打开串口助手设置好波特率然后就能看到程序里printf出来的信息了。这个方法经典、可靠但缺点也很明显它需要占用一个硬件UART外设需要额外的连接线波特率设置不当还会乱码最关键的是打印速度受限于波特率当你想看大量、快速的调试信息时那种“涓涓细流”的感觉实在让人着急。这时候SEGGER RTTReal Time Transfer实时传输技术就该登场了。我第一次接触RTT时感觉就像发现了一个“作弊器”。简单来说RTT是一种通过调试接口比如J-Link、DAP-Link在目标MCU和PC主机之间进行高速数据交换的技术。它不需要占用任何额外的硬件外设如UART、SPI完全依托于你已经连接好的调试探头。你可以在代码中像使用printf一样方便地输出日志而PC端通过J-Link RTT Viewer、J-Link RTT Client或者Telnet等工具就能近乎实时地看到这些信息速度远超串口。它的核心价值在于“实时”和“非侵入”。实时意味着数据传输延迟极低几乎可以跟上代码的执行速度非常适合观察中断、时序等关键事件。非侵入指的是它几乎不影响目标系统的正常运行不占用宝贵的硬件资源也不需要为了调试而改动硬件连接。对于资源紧张、引脚有限的嵌入式设备或者那些已经定型、没有预留调试串口的量产产品RTT提供了一种极其优雅的后门。2. RTT的工作原理与架构拆解要玩转RTT不能只停留在“会用”的层面理解其背后的工作机制能帮助你在遇到问题时快速定位甚至进行一些高级定制。2.1 核心组件上行通道与下行通道RTT的通信是双向的这比单向的串口打印强大得多。其核心架构围绕一个位于目标MCU RAM中的特殊数据结构——控制块Control Block展开。上行通道Up Buffer这是最常用的功能即从MCU发送数据到PC。你在代码中调用SEGGER_RTT_printf()数据并不会立刻通过调试接口发送出去而是被写入到控制块中预先定义好的一个环形缓冲区Ring Buffer里。PC端的RTT客户端工具如RTT Viewer会通过调试接口周期性地主动去读取这个缓冲区的内容并将其显示出来。这个过程是查询式的而非中断驱动因此对MCU的性能影响极小。下行通道Down Buffer这个功能常被忽略但非常有用。它允许从PC端向MCU发送数据比如模拟一个终端输入。你可以在RTT Viewer里输入命令数据会通过调试接口写入到下行缓冲区MCU端的代码可以周期性地调用SEGGER_RTT_GetKey()或SEGGER_RTT_Read()来读取这些数据实现一个简单的命令行交互界面。这对于产品测试、参数配置来说是个轻量级且好用的方案。2.2 控制块与缓冲区管理控制块是RTT的灵魂它是一个固定的数据结构包含了RTT的标识符、通道数量、每个通道缓冲区的地址和大小等信息。SEGGER的库在初始化时会在RAM中创建这个控制块。这里有一个关键细节缓冲区的地址必须在调试器和RTT客户端可知的范围内。通常我们将其放在全局变量区确保其地址是确定的。RTT客户端通过扫描目标内存寻找控制块的特定标识符“SEGGER RTT”来定位它从而建立起通信。缓冲区的管理采用环形队列这意味着当缓冲区写满时新的数据会覆盖旧的数据除非你配置了阻塞模式。这种设计保证了在高速数据输出时不会因为PC端读取慢而导致MCU卡死但代价是可能会丢失一部分历史日志。在实际项目中你需要根据日志量和重要性来权衡缓冲区的大小。注意缓冲区不宜过大。虽然大的缓冲区可以存储更多历史信息但它会永久占用宝贵的RAM。对于只有几十KB RAM的MCU分配一个10KB的RTT缓冲区可能就过于奢侈了。通常1-2KB的缓冲区对于大多数调试场景已经足够。2.3 与调试接口的协作J-Link与DAP-LinkRTT需要调试探针的支持。SEGGER自家的J-Link是“亲儿子”支持得最完善性能也最好。但好消息是开源的CMSIS-DAP协议调试器如DAPLink常见于很多国产调试器和开发板也支持RTT功能。其底层原理是调试器除了提供常规的下载、单步、断点功能外还开放了一个用于高速数据访问的接口。RTT客户端通过这个接口以非常高的频率远高于串口波特率去读取或写入目标MCU RAM中指定地址即RTT控制块和缓冲区的数据。这个过程完全在调试会话的背景下进行因此你甚至可以在MCU运行时Run状态下进行数据交换而无需暂停程序这才是“实时”二字的精髓。3. 在项目中集成与使用SEGGER RTT理论讲完我们进入实战环节。如何把一个裸机工程或者RTOS工程快速武装上RTT能力3.1 获取与集成RTT库SEGGER非常慷慨RTT的实现代码是随其J-Link软件包一起提供的并且可以免费用于商业项目。你通常可以在以下路径找到它Windows:C:\Program Files\SEGGER\JLink\Samples\RTT或者从SEGGER官网下载J-Link软件包里面包含RTT压缩包。核心文件就几个SEGGER_RTT.cRTT的实现源码。SEGGER_RTT.h头文件。SEGGER_RTT_Conf.h配置文件这是你需要重点修改和关注的。SEGGER_RTT_printf.c如果需要使用printf格式化功能需要这个文件它依赖标准库的vsnprintf。集成步骤将上述源文件.c和头文件.h复制到你的项目目录中例如Middlewares/SEGGER/RTT/。在IDE如Keil MDK、IAR或SEGGER Embedded Studio中将这些源文件添加到你的工程。在你的主程序或初始化代码中无需显式调用初始化函数。RTT库在第一次被调用时如SEGGER_RTT_WriteString会自动初始化控制块。你只需要确保在调用任何RTT函数之前系统时钟和RAM已经正常初始化即可。如果你想替换标准库的printf到RTT通常需要重写_write或fputc等底层函数。SEGGER提供了一个示例SEGGER_RTT_Syscalls_*.c你可以参考。3.2 关键配置SEGGER_RTT_Conf.h详解这个配置文件决定了RTT的行为和资源占用。打开它你会看到一系列#define。下面我挑几个最关键的讲BUFFER_SIZE_UP和BUFFER_SIZE_DOWN定义上行和下行缓冲区的大小单位是字节。如前所述根据需求调整。默认的1KB1024是个不错的起点。#define BUFFER_SIZE_UP (1024) // 上行缓冲区存储MCU-PC的数据 #define BUFFER_SIZE_DOWN (16) // 下行缓冲区通常可以设置小一些SEGGER_RTT_MAX_NUM_UP_BUFFERS和SEGGER_RTT_MAX_NUM_DOWN_BUFFERS定义最大上行/下行通道数。默认都是2。通道0是默认通道你还可以创建通道1、2等用于对不同模块、不同等级的日志进行分类输出然后在PC端选择查看哪个通道非常方便。#define SEGGER_RTT_MAX_NUM_UP_BUFFERS (3) // 例如0-默认1-错误2-网络调试SEGGER_RTT_MODE_DEFAULT设置默认通道的阻塞模式。这是极易踩坑的地方。SEGGER_RTT_MODE_NO_BLOCK_SKIP默认缓冲区满时丢弃新数据。性能最好但会丢数据。SEGGER_RTT_MODE_NO_BLOCK_TRIM缓冲区满时丢弃最老的数据以容纳新数据。SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL缓冲区满时发送方MCU阻塞等待直到有空间。这可能导致程序卡死除非PC端读取非常及时否则慎用 我的经验是对于调试日志使用NO_BLOCK_SKIP即可接受偶尔丢失一些非关键信息换取系统的流畅运行。如果需要保证关键信息不丢失可以单独为其分配一个缓冲区并设置为阻塞模式或者增加缓冲区大小。SEGGER_RTT_PRINTF_BUFFER_SIZE使用SEGGER_RTT_printf时的内部格式化缓冲区大小。如果打印的字符串很长需要调大这个值否则长字符串会被截断。通常256或512足够。3.3 基础API使用与格式化输出集成配置好后使用起来非常简单。基本输出#include SEGGER_RTT.h void main(void) { // ... 系统初始化 SEGGER_RTT_WriteString(0, Hello, RTT! System Started.\n); // 向通道0写入字符串 SEGGER_RTT_printf(0, System Clock: %d Hz\n, SystemCoreClock); // 格式化输出到通道0 }多通道应用// 初始化时可以配置多个上行缓冲区在Conf.h中需先定义最大数量 SEGGER_RTT_ConfigUpBuffer(1, ErrorLog, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP); SEGGER_RTT_ConfigUpBuffer(2, NetTrace, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP); // 使用时指定通道号 if (error_occurred) { SEGGER_RTT_printf(1, [ERR] Code: 0x%X at %s:%d\n, err_code, __FILE__, __LINE__); } SEGGER_RTT_printf(2, [NET] Packet sent, len%d\n, pkt_len);终端输入下行通道int key; key SEGGER_RTT_GetKey(); // 从下行缓冲区读取一个字符非阻塞无数据返回-1 if (key 0) { if (key r) { SEGGER_RTT_WriteString(0, Reset command received.\n); // ... 执行复位操作 } }4. PC端工具链如何查看RTT数据MCU端准备就绪PC端也需要相应的工具来接收和显示数据。这里有几个主流选择。4.1 J-Link RTT Viewer官方主力工具这是SEGGER官方的图形化工具功能最全集成在J-Link软件包中。使用起来非常直观连接好J-Link调试器和目标板给板上电。打开J-Link RTT Viewer。在配置中选择你的J-Link型号和目标芯片型号。点击“Connect”。如果一切正常你会看到“Found RTT control block at 0xXXXXXX”的提示并且下方的终端窗口开始显示MCU发送过来的日志。它的高级功能很好用多终端标签每个上行通道Channel对应一个标签页你可以同时查看“All Output”、“通道0”、“通道1”等方便分类筛选。下行输入在底部的输入框你可以直接输入字符或字符串发送到MCU的下行通道。数据可视化它甚至支持将接收到的数据以图形化方式显示如绘制波形虽然这个功能用得不多。日志保存可以将终端内容保存为文本文件。4.2 J-Link RTT Client轻量级命令行工具这是一个命令行工具JLinkRTTClient.exe适合集成到自动化测试脚本中或者在你偏爱命令行环境时使用。它的输出是纯文本可以直接重定向到文件。JLinkRTTClient -device Cortex-M4 -if SWD -speed 4000连接成功后MCU的RTT输出就会打印在命令行窗口里。你可以用管道或重定向来保存日志。4.3 使用Telnet连接这是我最喜欢也最灵活的方式之一。J-Link RTT Server可以作为一个本地服务器将RTT数据映射到本地的某个TCP端口默认19021。然后你可以用任何支持Telnet或Raw TCP的工具来连接比如系统自带的Telnet客户端Windows需要在“启用或关闭Windows功能”里安装telnet localhost 19021PuTTY选择连接类型为“Raw”主机名填localhost端口填19021。MobaXterm、SecureCRT等高级终端工具。甚至是你自己写的Python/Node.js脚本用socket连接localhost:19021就能实时获取日志流进行自定义解析、过滤或转发到云端。这种方式的优势是跨平台和可编程性。你可以在Linux/Mac下开发同样用telnet连接。你的自动化测试框架可以轻松地通过TCP连接获取设备日志并进行分析判断。启动RTT Server的方法在J-Link RTT Viewer中有“Start Server”按钮。或者使用命令行JLinkRTTLogger -device Device -if Interface -speed Speed -RTTTelnetPort 190214.4 在IDE中集成SEGGER Embedded Studio (SES)如果你使用SEGGER自家的IDE——Embedded Studio那么RTT的支持是内置且无缝的。在Debug模式下SES的“Debug Terminal”窗口会自动识别并连接RTT你无需任何额外配置就能看到printf输出。这对于使用SES作为开发环境的用户来说体验是最佳的。5. 实战技巧与高级应用掌握了基础我们来看看如何把RTT用得更好解决一些实际开发中的痛点。5.1 替换标准库printf实现无硬件串口调试这是RTT最经典的应用。以ARMCCKeil为例你需要重定向fputc或__stdout。方法一重写fputc简单直接#include stdio.h #include SEGGER_RTT.h #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { SEGGER_RTT_Write(0, (char *)ch, 1); // 向RTT通道0写入一个字符 return ch; } // 对于GCC可能还需要重写_write函数然后在代码中就可以直接使用printf了输出会自动重定向到RTT。实操心得重定向后printf会变得非常快但也要注意频繁调用printf进行复杂格式化尤其是浮点数%f仍然会消耗可观的CPU时间和栈空间。在实时性要求高的中断服务程序里要避免使用printf可以用SEGGER_RTT_WriteString直接输出预格式化的字符串。5.2 在RTOS环境下的线程安全输出如果你在FreeRTOS、RT-Thread等RTOS中使用RTT多个任务同时调用RTT输出函数可能会造成输出内容交错因为RTT的写操作不是原子的。虽然RTT库内部有一些简单的锁机制通过SEGGER_RTT_LOCK和SEGGER_RTT_UNLOCK宏但在RTOS中最好使用信号量Semaphore或互斥量Mutex来实现更严格的保护。一个常见的做法是创建一个专用的日志任务Logger Task和一个消息队列。其他任务需要输出日志时不直接调用RTT而是将格式化好的日志字符串或包含格式化信息的结构体发送到这个消息队列。日志任务负责从队列中取出消息并调用SEGGER_RTT_WriteString进行输出。这样既保证了线程安全也避免了输出操作阻塞高优先级任务。5.3 性能分析与时间测量RTT可以用来做简单的性能分析。SEGGER提供了一个SEGGER_RTT_GetKey的“兄弟”函数——SEGGER_SYSVIEW_*系列函数是更专业的系统视图工具但RTT本身也能完成一些工作。例如你可以利用系统滴答定时器SysTick和RTT来测量代码段执行时间#include SEGGER_RTT.h void measure_function(void) { uint32_t start_ticks, end_ticks, elapsed_us; start_ticks SysTick-VAL; // 获取当前SysTick计数器的值递减计数器 // ... 这里是你要测量的代码段 ... end_ticks SysTick-VAL; // 计算耗时假设系统时钟频率已知SysTick重装载值已知 // 注意处理计数器溢出的情况 elapsed_us calculate_elapsed_time(start_ticks, end_ticks); SEGGER_RTT_printf(0, [Profiling] Function XXX took %lu us\n, elapsed_us); }更精确的做法是使用DWTData Watchpoint and Trace单元中的CYCCNT计数器如果芯片支持它能提供时钟周期级别的精度。5.4 实现简单的命令行交互界面CLI结合下行通道你可以用RTT实现一个轻量级的CLI用于产品测试或配置。void cli_task(void) { char cmd_buf[128]; int idx 0; int ch; SEGGER_RTT_WriteString(0, \nRTT CLI Ready. Type help\n ); while(1) { ch SEGGER_RTT_GetKey(); if (ch 0) { if (ch \r || ch \n) { // 回车键 cmd_buf[idx] \0; process_command(cmd_buf); // 处理命令 idx 0; SEGGER_RTT_WriteString(0, \n ); } else if (ch \b) { // 退格键 if (idx 0) { idx--; SEGGER_RTT_WriteString(0, \b \b); // 回显退格 } } else if (idx sizeof(cmd_buf)-1) { cmd_buf[idx] (char)ch; SEGGER_RTT_Write(0, (char*)ch, 1); // 回显字符 } } // ... 其他任务逻辑 } }在process_command函数里你可以解析cmd_buf实现诸如get voltage、set led on、reset等命令。6. 常见问题排查与解决方案实录即使RTT设计得很优雅在实际使用中还是会遇到各种问题。下面是我和同事们踩过的一些坑以及解决办法。6.1 连接失败找不到RTT控制块这是最常遇到的问题。PC端工具提示“RTT control block not found”或一直连接不上。可能原因及排查步骤RTT库未成功链接或初始化检查确认SEGGER_RTT.c已正确添加到工程并参与编译链接。查看map文件确认_SEGGER_RTT等符号存在。检查确保在调用任何RTT函数前MCU的时钟和内存尤其是RTT控制块所在的RAM区域已经初始化完成。对于某些在启动早期就打印日志的情况可能需要将RTT初始化提前。缓冲区地址不可访问检查调试器连接正常吗能正常读写内存吗尝试在调试器中手动读取你定义的缓冲区地址在SEGGER_RTT_Conf.h中SEGGER_RTT_CB的地址或者查看_SEGGER_RTT符号的地址。检查是否启用了内存保护单元MPU或缓存Cache导致调试器无法直接访问RAM有时需要配置MPU区域或禁用Cache来确保调试接口能访问RTT缓冲区。目标芯片进入低功耗模式现象程序运行正常时RTT有输出一旦进入睡眠Sleep/Stop模式RTT就断了。原因调试接口如SWD在芯片深度睡眠时可能被关闭导致调试器无法访问内存。解决在进入低功耗前刷新RTT缓冲区调用SEGGER_RTT_Flush可能没用因为问题在访问路径。更好的办法是避免在低功耗模式下进行大量日志输出或者使用一种在唤醒瞬间能快速传输的日志缓冲机制。J-Link驱动或固件过旧解决更新到最新版本的J-Link驱动和软件包。SEGGER会持续修复问题和提升兼容性。6.2 RTT输出速度慢、卡顿或丢失数据这通常与缓冲区设置和阻塞模式有关。症状输出断断续续有时一大段有时停顿很久。分析PC端RTT客户端如RTT Viewer读取缓冲区有间隔。如果MCU端输出速度远超客户端读取速度缓冲区会很快写满。解决增大上行缓冲区BUFFER_SIZE_UP给数据更多的缓冲空间。优化PC端尝试使用J-Link RTT Viewer的“高速模式”设置如果有或者尝试用Telnet连接有时命令行工具的效率更高。减少MCU端输出频率和长度检查代码中是否有不必要的、过于频繁的printf。在循环中打印调试信息时考虑增加条件或降低频率。症状程序运行明显变慢甚至卡死。分析很可能将RTT通道模式设置为了SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL阻塞模式。当缓冲区满而PC端未及时读取时MCU会一直等待导致程序阻塞。解决强烈建议调试时使用SEGGER_RTT_MODE_NO_BLOCK_SKIP或SEGGER_RTT_MODE_NO_BLOCK_TRIM。接受丢失部分非关键日志换取程序的实时性。6.3 多通道配置不生效你按照文档配置了多个上行缓冲区但在RTT Viewer里只看到“通道0”。检查确认SEGGER_RTT_MAX_NUM_UP_BUFFERS的值大于你配置的通道索引号。如果你想用通道1和2这个值至少需要设置为3。检查是否在代码中正确调用了SEGGER_RTT_ConfigUpBuffer来初始化额外的通道这个调用需要在输出到该通道之前执行通常放在main函数的初始化部分。操作在J-Link RTT Viewer中连接后需要手动在下拉框或标签页中选择“Terminal 1”、“Terminal 2”才能看到对应通道的输出。6.4 与调试功能冲突在某些极端情况下RTT可能会与调试器的其他功能如实时变量查看、断点产生轻微干扰因为它们在共享调试接口的带宽。如果遇到断点后程序行为异常或者变量更新不及时可以尝试暂时禁用RTT输出进行对比测试。不过在绝大多数应用中这种干扰可以忽略不计。6.5 使用DAPLink时的注意事项越来越多的开发板使用基于CMSIS-DAP的DAPLink调试器。好消息是新版本的DAPLink固件也支持RTT。确保固件版本你的DAPLink固件需要是较新的版本通常2020年后的版本都支持。可以查看DAPLink的版本号或尝试连接RTT如果支持一般能成功。使用兼容的工具J-Link RTT Viewer可能无法直接连接DAPLink。你需要使用pyOCDRTT插件或者使用OpenOCD配合rtt命令来连接。也有第三方开发的DAPLink RTT Client工具。速度可能稍慢由于DAPLink是开源实现其RTT数据传输效率可能不及原厂J-Link但对于日常调试来说完全足够。一个快速验证DAPLink RTT的方法是使用pyOCD# 安装pyOCD和rtt插件 pip install pyocd pyocd rtt如果连接成功它会自动识别RTT控制块并开始输出日志。最后我个人最深的体会是SEGGER RTT彻底改变了我调试嵌入式系统的习惯。它把调试从“硬件接线”的束缚中解放出来让日志输出变得像在PC上写程序一样自然流畅。尤其是在项目后期硬件接口已经用尽或者产品外壳封死没有预留调试口时RTT通过那根仅有的SWD调试线就成了连接你和设备内部世界的唯一高速桥梁。花一点时间把它集成到你的项目框架中绝对是笔一劳永逸的投资。当你习惯了在代码里随手printf并在屏幕上即时看到结果时就很难再回去忍受串口的“慢速”和“占端口”了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进