SEGGER RTT 日志:原理、配置与实战对比

调试嵌入式系统,日志输出是基本功。但很多工程师还在用串口打印,速度慢、占引脚、还影响实时性。今天我要聊的 SEGGER RTT,是 J-Link 调试器自带的一种「黑科技」日志方案。我个人用了快十年,从早期的 Cortex-M3 到现在的 RISC-V,几乎每个项目都会把它集成进去。

RTT 原理与优势

RTT 的全称是 Real-Time Transfer,实时传输。它不像串口那样需要硬件外设,而是直接在 RAM 里划出一块缓冲区,配合调试器的 SWD/JTAG 接口,实现双向数据交换。

说白了,就是你的目标芯片在 RAM 里开了一个「信箱」。调试器通过调试接口,随时来取信、放信。整个过程不占用串口、不占用定时器、不产生中断。

核心优势:

  • 极低延迟:写入缓冲区只需几条内存操作指令,微秒级完成
  • 零中断开销:不触发任何硬件中断,不影响实时任务
  • 双向通信:上行(目标→主机)和下行(主机→目标)同时工作
  • 带宽高:典型场景下可达 1-2 MB/s,远超串口的 115200 bps
  • 不占引脚:复用 SWD 接口,不需要额外 UART 引脚

我记得第一次在项目中用 RTT 替换串口日志,系统响应时间从原来的 200 微秒降到了 5 微秒。当时团队里还有人怀疑是不是测量错了,反复测了三遍才信。

RTT 配置与初始化

配置 RTT 其实很简单。SEGGER 官方提供了两个核心文件:SEGGER_RTT.c 和 SEGGER_RTT.h。把它们加到工程里,再调用初始化函数就行。

下面是一个典型的初始化流程:

/* 1. 包含头文件 */
#include "SEGGER_RTT.h"

/* 2. 配置缓冲区大小(在 SEGGER_RTT_Conf.h 中) */
#define BUFFER_SIZE_UP   (1024)   /* 上行缓冲区,目标→主机 */
#define BUFFER_SIZE_DOWN (64)     /* 下行缓冲区,主机→目标 */

/* 3. 初始化 RTT(在 main 函数开头调用) */
int main(void)
{
    /* 硬件初始化 */
    SystemInit();
    
    /* RTT 初始化 —— 其实就是清空缓冲区 */
    SEGGER_RTT_Init();
    
    /* 测试输出 */
    SEGGER_RTT_WriteString(0, "RTT 日志已启动\n");
    
    while(1)
    {
        /* 主循环 */
        SEGGER_RTT_printf(0, "系统运行中,时间戳: %u\n", GetTick());
        Delay_ms(1000);
    }
}

我的习惯:我会在 SEGGER_RTT_Conf.h 里把上行缓冲区设到 2KB 以上。因为日志量大的时候,缓冲区太小会导致数据丢失。下行缓冲区 64 字节通常够用,除非你要传大量命令。

上行通道与下行通道

RTT 支持多个通道,每个通道都是独立的缓冲区。通道 0 默认用于标准日志,通道 1 可以用于调试命令,通道 2 可以用于数据采集。

通道 方向 典型用途 缓冲区大小建议
0 上行 标准日志输出 1024-4096 字节
1 下行 调试命令输入 64-256 字节
2 上行 数据采集(如传感器数据) 4096-16384 字节

上行通道是目标芯片往主机发数据。你调用 SEGGER_RTT_WriteString() 或 SEGGER_RTT_printf(),数据就写进缓冲区。调试器通过 SWD 接口定期轮询,把数据读走。

下行通道是主机往目标芯片发数据。比如你在 J-Link RTT Viewer 里输入一条命令,数据会先写到下行缓冲区,目标芯片再通过 SEGGER_RTT_Read() 读取。

我曾经踩过的坑:下行通道的读取必须在主循环或任务中主动调用 SEGGER_RTT_Read()。如果你只写上行日志,忘了处理下行,主机发过来的命令就会一直积压在缓冲区里。有一次我调试一个电机控制程序,命令延迟了 2 秒才响应,差点以为是 PID 参数出了问题。

与串口日志的对比

很多工程师习惯用串口打印日志,觉得简单可靠。但 RTT 在多个维度上完胜串口。我整理了一张对比表:

对比项 串口日志 RTT 日志
硬件依赖 需要 UART 外设 + 引脚 仅需 SWD/JTAG 接口
传输速度 通常 115200 bps(约 11.5 KB/s) 1-2 MB/s(快 100 倍以上)
中断开销 每个字符触发中断 零中断,纯内存操作
实时性影响 高,尤其在高波特率下 极低,微秒级写入
双向通信 需要额外引脚和协议 原生支持,通道分离
调试器依赖 不需要调试器 需要 J-Link 或兼容调试器
数据完整性 可能丢数据(缓冲区溢出) 缓冲区机制,可配置大小

你想想看,串口每发一个字节都要进中断,CPU 要保存现场、处理数据、恢复现场。如果日志量大,中断频繁,系统的实时性就崩了。RTT 呢?就是几条内存写指令,连缓存都不用刷。

什么时候用串口?

  • 没有调试器,只有串口线
  • 需要与外部设备通信(如蓝牙模块、传感器)
  • 日志量极小,实时性要求不高

什么时候用 RTT?

  • 有 J-Link 调试器
  • 需要高频日志(如 1ms 一次的任务调度记录)
  • 系统对实时性敏感
  • 引脚资源紧张

RTT 工作流程示意图

下面这张图展示了 RTT 的完整数据流。从目标芯片的 RAM 缓冲区,到调试器的 SWD 接口,再到 PC 端的 RTT Viewer 软件。

RTT 工作流程示意图 RTT 日志数据流 目标芯片 RAM 缓冲区 上行 / 下行通道 应用程序 SEGGER_RTT_Write() J-Link 调试器 SWD 接口 轮询读取 / 写入 固件处理 解析 / 转发 PC 端 RTT Viewer 终端显示 SWD 协议 USB 说明:数据从应用程序写入 RAM 缓冲区,调试器通过 SWD 轮询读取, 再通过 USB 传给 PC 端的 RTT Viewer 显示。整个过程不产生中断。

实际项目中的选择建议

我个人的经验是:只要手头有 J-Link,就优先用 RTT。串口留给真正需要物理连接的外设。比如蓝牙模块、GPS 模块、RS485 总线这些,串口是必须的。但调试日志?RTT 完胜。

有一次我在一个 RTOS 项目里调试任务切换。用串口打印任务 ID,结果系统跑起来后,日志输出占用了 30% 的 CPU 时间。换成 RTT 后,CPU 占用降到了 0.5% 以下。你想想看,这差距有多大。

小技巧:如果你用 RTT 打印浮点数,记得用 SEGGER_RTT_printf() 的 %f 格式。但要注意,有些编译器默认不开启浮点支持,需要在链接选项中加上 -u _printf_float。