2. RTT核心原理

RTT(Real Time Transfer)之所以能做到“几乎不打断 CPU 运行”的实时日志输出,根本原因在于它并不依赖目标芯片的串口外设,而是把目标端 RAM 中的一块普通内存当作数据交换区,由调试探针(J-Link)通过调试接口直接读写这块内存。目标程序只做“往内存里写数据”这一件事,真正的数据搬运由 J-Link 在后台完成。理解这一点,就理解了 RTT 的全部核心。

一、控制块:RTT 的“总目录”

RTT 在目标内存中首先放置一个结构体,称为控制块(Control Block),它是整个 RTT 机制的入口。控制块本身不存放日志数据,而是存放“去哪里找数据”的描述信息。其关键字段包括:

  • ID 字符串:固定为 "SEGGER RTT",J-Link 连接后会在目标 RAM 中搜索这个字符串,从而定位控制块。这就是“自动识别”的基础。
  • 上行缓冲区描述数组:每个上行通道对应一个 {缓冲区指针, 缓冲区大小, 写指针, 读指针, 标志} 的描述项。
  • 下行缓冲区描述数组:结构相同,方向相反,供主机向目标发送数据。
  • 通道数量:上行、下行各有多少个通道(默认各 1 个,最多可配置到 16 个)。

控制块通常由 SEGGER_RTT_Init() 在启动时初始化,也可以放在固定链接段中,使 J-Link 无需运行目标代码即可找到它。

二、环形缓冲区:无锁通信的关键

每个通道的核心是一个环形缓冲区(Ring Buffer),由一块连续内存、一个写指针 WrOff 和一个读指针 RdOff 组成。两个指针都只在 0 ~ Size-1 范围内循环回绕,因此称为“环形”。

上行通道的工作方式如下:

  1. 目标程序调用 SEGGER_RTT_Write(),把数据拷贝到 WrOff 指向的位置,然后前移 WrOff。
  2. J-Link 通过调试接口读取 RdOff 和 WrOff,把两者之间的数据取走,然后更新 RdOff。
  3. 只要 WrOff != RdOff,就说明缓冲区里有待取数据;当 WrOff == RdOff 时表示缓冲区为空。

这里有一个非常重要的设计:目标端只写 WrOff,主机端只写 RdOff。两个指针由不同的一方单独修改,因此不需要互斥锁,也不会出现“目标在写、主机在读”的竞争。这正是 RTT 能做到极低开销的原因。

缓冲区满时,SEGGER_RTT_Write() 默认采用“非阻塞”模式:能写多少写多少,剩余数据被丢弃并返回实际写入长度。若使用阻塞模式,则目标会等待主机取走数据,可能拖慢实时性,需谨慎使用。

三、上行与下行:双向通道

RTT 的“双向”体现在两套独立的环形缓冲区上:

  • 上行通道(Up Buffer):目标 → 主机,用于日志、printf 输出、变量跟踪数据。目标写、主机读。
  • 下行通道(Down Buffer):主机 → 目标,用于向目标发送命令、参数、交互输入。主机写、目标读。

两者共用同一套控制块和指针协议,只是读写角色互换。因此 RTT 不仅能“看日志”,还能实现主机与目标之间的双向交互,例如通过 J-Link 向目标下发控制指令。

四、自动识别过程

当 J-Link 连接目标并启动 RTT 时,会执行以下识别流程:

  1. 在目标 RAM 的已知搜索范围内扫描 "SEGGER RTT" 字符串。
  2. 找到后,以该位置为起点解析控制块,读取通道数量和各缓冲区描述。
  3. 根据描述中的指针和大小,建立主机端与目标端缓冲区的映射关系。
  4. 此后主机周期性轮询 WrOff,一旦发现新数据就取走并更新 RdOff。

整个过程不需要目标程序主动“通知”主机,也不需要串口中断,因此对目标实时性的影响极小。

RTT 双向通信机制:目标内存环形缓冲区 ↔ 调试探针 目标 MCU(RAM) 控制块 Control Block ID: "SEGGER RTT" | 上行/下行通道数 各通道:缓冲区指针 / 大小 / WrOff / RdOff 上行环形缓冲区(目标 → 主机) 数据区(连续内存,循环回绕) WrOff ▲(目标写) RdOff ▲(主机写) WrOff == RdOff → 空;WrOff != RdOff → 有数据 下行环形缓冲区(主机 → 目标) 数据区(连续内存,循环回绕) RdOff ▲(目标读) WrOff ▲(主机写) 调试探针 J-Link + 主机 RTT 自动识别 扫描 "SEGGER RTT" → 定位控制块 解析通道描述 → 建立缓冲区映射 主机读取上行数据 轮询 WrOff → 读取 RdOff~WrOff 数据 更新 RdOff → 释放缓冲区空间 输出到 RTT Viewer / 日志文件 主机写入下行数据 主机写 WrOff → 数据进入下行缓冲区 目标调用 SEGGER_RTT_Read() 读取 目标更新 RdOff → 完成一次交互 上行数据流 下行数据流 调试接口(SWD/JTAG)直接访问 RAM 目标端只写 WrOff,主机端只写 RdOff → 无需互斥锁,实现无锁双向通信

五、关键参数与配置示例

在 SEGGER_RTT_Conf.h 中,最常调整的参数是缓冲区大小和通道数量。以下是一组典型配置示例(属于教学示例值,实际应按项目需求调整):

#define BUFFER_SIZE_UP      (1024)   // 上行缓冲区 1KB
#define BUFFER_SIZE_DOWN    (16)     // 下行缓冲区 16B
#define SEGGER_RTT_MAX_NUM_UP_BUFFERS   (2)  // 2 个上行通道
#define SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (1)  // 1 个下行通道
#define SEGGER_RTT_MODE_DEFAULT  SEGGER_RTT_MODE_NO_BLOCK_SKIP

缓冲区越大,能缓存的日志越多,但占用 RAM 也越多;缓冲区越小,越容易在高速打印时丢数据。判断是否丢数据的方法是:观察 SEGGER_RTT_Write() 的返回值是否小于请求写入长度,或使用 SEGGER_RTT_HasDataUp() 检查是否有未取走的数据。

若目标在写入时被调试器暂停(Halt),主机仍可读取已写入的数据;但若目标在写入过程中被暂停,指针可能处于中间状态,因此 RTT 的指针更新应尽量保持原子性。SEGGER 官方实现已对此做了处理,自行移植时需注意。

六、小结与判断依据

RTT 的核心可以概括为三句话:控制块是入口,环形缓冲区是载体,双指针分离是无锁的关键。上行通道负责目标到主机的日志输出,下行通道负责主机到目标的命令下发,两者共用同一套指针协议。自动识别则依赖 "SEGGER RTT" 字符串在 RAM 中的定位。

在实际使用中,判断 RTT 是否正常工作的依据包括:J-Link 能否在目标 RAM 中找到控制块、RTT Viewer 是否显示通道列表、SEGGER_RTT_Write() 返回值是否等于写入长度、以及日志是否连续无丢失。掌握这些,就为后续的 RTT 配置、性能调优和故障排查打下了基础。