1. 为什么我们需要SEGGER RTT的printf功能如果你在玩嵌入式开发尤其是用ARM Cortex-M这类资源紧张的MCU调试绝对是个绕不开的“坎”。传统的调试方式比如串口打印UART大家肯定都用过。接上几根线打开串口助手就能看到程序里printf出来的信息确实方便。但用久了你会发现几个痛点首先它得占用一个硬件串口对于引脚资源本就紧张的芯片来说有点奢侈其次波特率设高了怕丢数据设低了打印慢看着信息一条条“挤”出来调试效率大打折扣最关键的是在一些低功耗场景下频繁唤醒串口外设来打印日志本身就会干扰你对真实功耗的测量。这时候SEGGER RTTReal Time Transfer就像个“救星”。它本质上是一种利用调试接口比如J-Link、ST-Link等进行高速数据通信的技术。你不需要占用任何额外的硬件外设只需要你的调试器连接着目标板就能在IDE和MCU之间开辟一条双向的“高速公路”。数据吞吐量远高于普通串口而且几乎是实时的延迟极低。但是SEGGER官方提供的RTT基础实现其SEGGER_RTT_printf函数有一个让很多人头疼的“阉割”默认不支持浮点数打印。你试一下就知道当你尝试用SEGGER_RTT_printf(“value %f”, 3.14)时输出很可能是一堆乱码或者干脆什么都没有。这是因为在嵌入式领域为了极致地压缩代码体积Flash占用和减少运行时开销RAM和CPU很多库的printf实现都会默认裁剪掉对浮点数这种“重量级”数据类型的支持。浮点数的格式化输出涉及到乘除运算、四舍五入等确实比整数复杂得多。所以我们的目标就很明确了把RTT这套好用的工具移植到自己的项目里并且亲手给它“补上”浮点数打印这个关键功能。这个过程不仅能让你用上高效的调试手段更能让你深入理解一个简化版printf的内部运作机制绝对是嵌入式工程师的一项实用技能。下面我就结合自己多次移植和优化的经验带你一步步搞定它。2. 手把手移植SEGGER RTT源码到你的工程移植的第一步是找到源码。如果你安装了SEGGER J-Link软件包源码通常就在安装目录下。比如在Windows上路径可能是C:\Program Files\SEGGER\JLink\Samples\RTT。你会看到一个压缩包解压后核心文件就展现在眼前了。对于大多数项目我们只需要关注其中几个文件SEGGER_RTT.h 头文件包含了所有的API函数声明和数据结构定义。SEGGER_RTT.c 核心实现文件实现了RTT的上下行通道管理、缓冲区操作等。SEGGER_RTT_Conf.h这是关键配置文件你可以根据你的芯片资源情况对RTT进行裁剪和配置。SEGGER_RTT_printf.c 这就是我们这次要动“大手术”的文件它实现了printf的格式化功能但默认是“精简版”。我的习惯是在项目的Drivers或Middlewares目录下新建一个SEGGER_RTT文件夹把上面这几个.c和.h文件直接拷贝进去。然后在你的IDE比如Keil、IAR或者STM32CubeIDE中将这个文件夹路径添加到项目的头文件包含路径Include Paths里并把.c文件添加到项目的编译源文件中。接下来打开SEGGER_RTT_Conf.h文件这里就是决定RTT占用多少内存的地方。你会看到类似下面的配置#define BUFFER_SIZE_UP (1024) // 上行缓冲区大小MCU-PC #define BUFFER_SIZE_DOWN (16) // 下行缓冲区大小PC-MCU #define SEGGER_RTT_MAX_NUM_UP_BUFFERS (2) // 最大上行缓冲区数量 #define SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (2) // 最大下行缓冲区数量对于调试日志输出我们主要关心上行缓冲区BUFFER_SIZE_UP。1024字节对于大多数调试场景已经足够。如果你的日志非常频繁或者单条日志很长可以适当增大。反之如果你的芯片RAM极其紧张比如只有几KB可以减小到512甚至256。但要小心缓冲区太小容易导致数据被覆盖丢失。BUFFER_SIZE_DOWN通常用于接收PC端发来的命令16字节一般够用。另一个重要配置是锁机制SEGGER_RTT_LOCK和SEGGER_RTT_UNLOCK。当你的系统在中断上下文和任务上下文都可能调用RTT_printf时为了确保输出不会错乱需要启用锁。默认可能是用中断禁用/使能来实现的。你需要根据你的RTOS如FreeRTOS或裸机系统环境实现这两个宏用信号量或关中断等方式来保护缓冲区。如果只是单线程或简单的前后台系统可以先关闭锁功能以节省代码空间。做完这些编译一下工程如果没有错误最基本的RTT通信框架就搭好了。你可以调用SEGGER_RTT_Init()初始化实际上这个函数体通常是空的但保留调用是个好习惯然后用SEGGER_RTT_WriteString()写个字符串试试。如果连接了J-Link打开J-Link RTT Viewer工具理论上应该能看到输出的字符串了。不过我们的重头戏——功能强大的printf特别是支持浮点数的printf还没开始呢。3. 攻克核心难题为RTT printf添加浮点数支持现在来到最核心的部分。打开SEGGER_RTT_printf.c文件你会发现它的实现非常紧凑。函数调用链是SEGGER_RTT_printf-SEGGER_RTT_vprintf。我们需要修改的正是SEGGER_RTT_vprintf这个函数。首先我们需要一个“开关”来控制是否编译浮点数支持代码。最好的地方就是在SEGGER_RTT_Conf.h里添加一个宏定义。这样项目里所有用到RTT的文件都能看到这个配置。// SEGGER_RTT_Conf.h // ... //! 启用浮点数打印支持 (注意这会增加约420字节的Flash占用) #ifndef SEGGER_RTT_PRINTF_FLOAT_ENABLE #define SEGGER_RTT_PRINTF_FLOAT_ENABLE 1 #endif我实测下来开启这个功能后代码体积大概会增加400-500字节对于现在动辄几百KB甚至上MB Flash的芯片来说完全可以接受。但对于只有64KB或更少Flash的极致成本型芯片你就需要权衡一下了。接着我们打开SEGGER_RTT_vprintf函数。这个函数的主体是一个大的do-while循环逐个解析格式字符串%后面的那些格式化说明符比如%d%x%s等。我们需要在switch (c)语句中找到处理完%u%s等标准格式的地方在default:分支之前加入我们对%f和%F的处理。这里我分享一个我优化过的实现思路。原始文章里给出的方法是将浮点数乘以1000取整然后分别打印整数部分和小数部分。这个方法简单但有个明显问题它固定打印3位小数而且对于负数小数的处理需要格外小心原文后面也提到了负号丢失的bug。我们来写一个更通用、更健壮一点的版本// 在 SEGGER_RTT_vprintf 函数的 switch (c) 语句块内添加 #if (SEGGER_RTT_PRINTF_FLOAT_ENABLE ! 0) case f: case F: { double f_arg; long int_part; double frac_part; int decimals 6; // 默认打印6位小数可以从NumDigits获取精度 int i; // 从参数列表中取出double转为float处理如果传参是float会提升为double f_arg va_arg(*pParamList, double); // 处理负数如果为负先输出负号然后取绝对值 if (f_arg 0.0) { _StoreChar(BufferDesc, -); f_arg -f_arg; } // 分离整数部分和小数部分 int_part (long)f_arg; frac_part f_arg - (double)int_part; // 打印整数部分 _PrintInt(BufferDesc, int_part, 10u, 0, FieldWidth, FormatFlags); // 如果需要打印小数精度NumDigits 0 if (NumDigits 0) { _StoreChar(BufferDesc, .); // 输出小数点 // 根据指定精度NumDigits打印小数部分 for (i 0; i NumDigits; i) { frac_part * 10.0; } // 四舍五入 long frac_int (long)(frac_part 0.5); // 处理因为四舍五入可能产生的进位例如 1.999 打印两位小数应为 2.00 if (frac_int (long)pow(10, NumDigits)) { int_part; frac_int - (long)pow(10, NumDigits); // 这里需要重新打印整数部分为了简化我们假设FieldWidth足够实际项目可能需要更复杂的处理 } // 打印小数部分需要补前导零 char fmt_buf[10]; snprintf(fmt_buf, sizeof(fmt_buf), %%0%ulu, NumDigits); // 生成如 %03lu 的格式 // 注意这里snprintf本身需要支持我们可以用更底层的方法 _PrintInt(BufferDesc, frac_int, 10u, NumDigits, NumDigits, FORMAT_FLAG_PAD_ZERO); } // 如果精度为0按照标准不打印小数点和小数部分相当于强制取整 // 但通常%f默认打印6位小数这里我们按NumDigits处理如果用户没指定.精度则NumDigits为0我们可以给个默认值6 // 更完整的实现需要区分用户是否显式指定了精度。为了清晰我们先按这个简化版来。 } break; #endif这段代码做了几件事正确处理正负号先判断正负输出负号并取绝对值避免了原文中后续计算可能出现的符号问题。支持精度控制利用格式字符串中%.3f的精度部分存储在NumDigits变量中。用户可以自由控制打印几位小数。四舍五入这是浮点数打印的基本要求让显示更符合数学直觉。处理进位考虑了小数部分四舍五入后可能向整数部分进位的情况比如9.995保留两位小数应该是10.00。当然这个实现还不是工业级的比如它使用了pow函数这可能会引入额外的库依赖和开销。在极其资源受限的环境下我们可以用一个循环乘以10来代替pow或者像最初的方法那样写死一个精度比如3位。这里的关键是你理解了原理就可以根据自己项目的实际需求代码大小 vs 功能完整性来做取舍。我建议第一次实现时可以先做一个固定3位小数、无四舍五入的简单版本确保通路跑通。等基本功能稳定后再逐步添加精度控制和四舍五入等高级特性。修改完代码后强烈建议你写几个简单的测试用例在main函数里调用SEGGER_RTT_printf打印正浮点数、负浮点数、大于1的数、纯小数等等验证输出是否正确。4. 打造好用的日志输出模块直接使用原始的SEGGER_RTT_printf函数虽然可以工作但用起来不够“顺手”。我们通常需要不同等级的日志调试、信息、警告、错误最好还能带点颜色在RTT Viewer里一目了然。另外我们可能希望在发布版本中完全关闭日志输出以减少开销。这就需要我们在用户层做一个漂亮的封装。我在项目里通常会创建一个log.h或者debug.h文件里面实现一套宏定义。下面是我常用的一个版本// log.h #ifndef __LOG_H #define __LOG_H #include SEGGER_RTT.h // 日志总开关发布版本可定义为0 #define LOG_ENABLE 1 // RTT通道选择通常用0号上行通道 #define LOG_RTT_CHANNEL 0 // 定义一些颜色控制码RTT Viewer支持ANSI颜色码 #define LOG_COLOR_RED RTT_CTRL_TEXT_BRIGHT_RED #define LOG_COLOR_GREEN RTT_CTRL_TEXT_BRIGHT_GREEN #define LOG_COLOR_YELLOW RTT_CTRL_TEXT_BRIGHT_YELLOW #define LOG_COLOR_BLUE RTT_CTRL_TEXT_BRIGHT_BLUE #define LOG_COLOR_MAGENTA RTT_CTRL_TEXT_BRIGHT_MAGENTA #define LOG_COLOR_CYAN RTT_CTRL_TEXT_BRIGHT_CYAN #define LOG_COLOR_WHITE RTT_CTRL_TEXT_WHITE #define LOG_COLOR_RESET RTT_CTRL_RESET // 根据总开关定义日志宏 #if LOG_ENABLE // 带颜色和标签的日志输出基础宏 #define LOG_PRINT(level, color, format, ...) \ SEGGER_RTT_printf(LOG_RTT_CHANNEL, \ %s[%s]%s format \r\n, \ color, level, LOG_COLOR_RESET, ##__VA_ARGS__) // 各等级日志定义 #define LOG_DEBUG(format, ...) LOG_PRINT(DBG, LOG_COLOR_WHITE, format, ##__VA_ARGS__) #define LOG_INFO(format, ...) LOG_PRINT(INF, LOG_COLOR_GREEN, format, ##__VA_ARGS__) #define LOG_WARN(format, ...) LOG_PRINT(WRN, LOG_COLOR_YELLOW, format, ##__VA_ARGS__) #define LOG_ERROR(format, ...) LOG_PRINT(ERR, LOG_COLOR_RED, format, ##__VA_ARGS__) // 纯数据打印不带装饰 #define LOG_RAW(format, ...) SEGGER_RTT_printf(LOG_RTT_CHANNEL, format, ##__VA_ARGS__) // 清屏和初始化按需使用 #define LOG_CLEAR() SEGGER_RTT_WriteString(LOG_RTT_CHANNEL, RTT_CTRL_CLEAR) #define LOG_INIT() SEGGER_RTT_Init() #else // 当LOG_ENABLE为0时将这些宏定义为空编译器会优化掉不产生任何代码 #define LOG_DEBUG(format, ...) #define LOG_INFO(format, ...) #define LOG_WARN(format, ...) #define LOG_ERROR(format, ...) #define LOG_RAW(format, ...) #define LOG_CLEAR() #define LOG_INIT() #endif // LOG_ENABLE #endif // __LOG_H这样封装之后在你的业务代码里就可以非常优雅地打日志了#include log.h void some_function(float sensor_value) { LOG_INIT(); // 通常只在main里调用一次 LOG_INFO(系统启动完成准备读取传感器...); // 直接使用 %f 打印浮点数因为我们已添加支持 LOG_DEBUG(传感器原始值: %.3f, sensor_value); if (sensor_value 100.0f) { LOG_WARN(传感器值偏高: %.2f, sensor_value); } if (some_error_condition) { LOG_ERROR(发生错误错误码: %d, error_code); // 错误信息用红色显示非常醒目 } }这样的日志系统在调试时不同等级和颜色的信息能帮你快速定位问题。在发布最终产品时只需要把LOG_ENABLE改成0重新编译所有日志代码都会被预处理器清除不影响性能。这个封装虽然简单但在我经历过的多个项目中极大地提升了调试效率和代码的可维护性。5. 实战测试与常见问题排坑指南代码写好了也封装完了接下来就是激动人心的测试环节。但现实往往是你满怀期待地编译、下载、运行然后打开RTT Viewer却发现一片空白或者输出些乱码。别急这都是正常现象我踩过的坑可能比这还多。第一步基础连接测试。先别急着用printf用最基础的SEGGER_RTT_WriteString()写一个固定的字符串比如Hello RTT!\n。如果这个都看不到说明RTT的基础通信没建立起来。检查以下几点调试器连接确保你的J-Link或其他支持RTT的调试器正确连接并且驱动正常。目标芯片支持RTT需要芯片的调试模块支持主流的Cortex-M0/M3/M4/M7都没问题。RTT Viewer配置确认RTT Viewer里选择的设备型号和你的芯片一致或者选择“Auto Detection”。如果自动检测不到可能需要手动输入_SEGGER_RTT结构体的地址这个地址可以在编译生成的map文件里搜索到。第二步浮点数功能测试。如果基础字符串能输出但浮点数不行那问题就出在我们修改的SEGGER_RTT_vprintf函数上。我的建议是写一个简单的测试函数把所有边界情况都测一遍void test_rtt_float(void) { SEGGER_RTT_printf(0, 正浮点数: %f\n, 3.1415926); SEGGER_RTT_printf(0, 负浮点数: %f\n, -2.71828); SEGGER_RTT_printf(0, 零: %f\n, 0.0); SEGGER_RTT_printf(0, 大数: %.2f\n, 123456.789); SEGGER_RTT_printf(0, 小数: %.4f\n, 0.0005); SEGGER_RTT_printf(0, 精度控制3位: %.3f\n, 1.2345); SEGGER_RTT_printf(0, 精度控制0位: %.0f\n, 1.6); // 应输出2四舍五入 }观察输出是否符合预期。特别是负数和四舍五入最容易出问题。如果输出不对就单步调试进入SEGGER_RTT_vprintf函数看看在处理%f时变量的值是如何变化的。我遇到的一个典型问题缓冲区溢出。SEGGER_RTT_printf内部使用了一个静态缓冲区acBuffer其大小由SEGGER_RTT_PRINTF_BUFFER_SIZE定义在SEGGER_RTT_Conf.h或SEGGER_RTT_printf.c中。默认值可能是128或256字节。如果你一次性打印一个很长的字符串比如打印一个很长的JSON格式的浮点数组就可能超出这个缓冲区导致输出截断甚至程序异常。解决方法就是适当增大这个缓冲区大小或者分多次打印。另一个关于RTT Viewer版本的问题。就像原始文章末尾提到的不同版本的J-Link软件和RTT Viewer兼容性有差异。我手头有6.x和7.x两个大版本。确实发现在某些新款芯片上老版本的RTT Viewer如6.98e能自动识别到RTT缓冲区的地址而新版本如7.88e反而需要手动配置。如果你的芯片比较新或比较冷门遇到自动检测失败别怀疑自己先去SEGGER官网下载最新版的J-Link软件包试试。如果还不行就老老实实从map文件里找到_SEGGER_RTT的地址在RTT Viewer里手动填入“RTT Control Block”地址栏。这个过程虽然麻烦点但一旦配通就一劳永逸了。最后别忘了性能影响。虽然RTT本身很快但格式化输出尤其是浮点数的格式化仍然是比较耗CPU的操作。避免在高速中断服务程序ISR中频繁调用RTT_printf来打印浮点数这可能会影响系统的实时性。如果确实需要在ISR中调试可以考虑只打印简短的标识符或整数或者先将数据存入循环缓冲区在低优先级的任务中统一格式化输出。移植和优化SEGGER RTT的printf功能尤其是搞定浮点数打印是一个典型的“麻雀虽小五脏俱全”的嵌入式工程实践。它涉及源码获取、工程配置、算法修改、模块封装、调试测试等多个环节。当你成功地在RTT Viewer里看到第一行带颜色的、格式正确的浮点数日志时那种成就感会让你觉得这一切的折腾都是值得的。这套工具链一旦搭建完成会成为你日后嵌入式开发中最得力的调试助手之一。