4. 目标端初始化与配置

RTT 能在目标端跑起来,靠的不是“自动魔法”,而是目标固件里的一段初始化代码。这段代码把控制块(Control Block)和上行/下行缓冲区放进 RAM,并让调试器通过内存扫描找到它。理解初始化的三个关键点——调用时机、缓冲区尺寸、通道与模式——决定了 RTT 是“开机即用”还是“日志丢失、卡顿甚至启动异常”。

一、SEGGER_RTT_Init 到底做了什么

在 RTT 源码中,SEGGER_RTT_Init() 的核心工作是初始化全局控制块 _SEGGER_RTT,其中包含标识字符串 "SEGGER RTT"、通道数量、每个通道的上行/下行缓冲区指针与大小、读写索引等字段。调试器(J-Link / OpenSDA / 支持 RTT 的调试探针)在连接后会扫描目标 RAM,寻找这段标识字符串,从而定位控制块并建立数据通道。

因此,SEGGER_RTT_Init() 必须在第一次调用任何 RTT 输出函数之前执行。若使用 SEGGER_RTT_printf()、SEGGER_RTT_Write() 等 API 而未初始化,控制块中的缓冲区指针为空或未定义,写入行为不可预期,常见表现是日志完全无输出或调试器无法识别 RTT 控制块。

在多数工程中,RTT 的初始化有两种典型方式:

  • 显式调用:在 main() 开头、时钟与外设初始化之后调用 SEGGER_RTT_Init()。
  • 自动初始化:使用 SEGGER_RTT_Conf.h 中的 SEGGER_RTT_INIT_AUTO(部分版本提供),由启动代码或构造函数自动完成初始化。

显式调用更可控,推荐在需要精确掌握初始化时机的项目中使用。

二、缓冲区大小:决定“能缓存多少日志”

RTT 的上行缓冲区是目标端写入、调试器读取的环形缓冲区。缓冲区越大,目标端在调试器未及时读取时能缓存的日志越多,越不容易因缓冲区满而丢日志;但代价是占用更多 RAM。下行缓冲区用于主机向目标发送数据,通常可以设得较小。

在 SEGGER_RTT_Conf.h 中,常用配置项如下:

#define BUFFER_SIZE_UP      (1024)   // 上行缓冲区,默认 1KB
#define BUFFER_SIZE_DOWN    (16)     // 下行缓冲区,默认 16B
#define SEGGER_RTT_MAX_NUM_UP_BUFFERS   (3)  // 上行通道数
#define SEGGER_RTT_MAX_NUM_DOWN_BUFFERS (3)  // 下行通道数
#define SEGGER_RTT_MODE_DEFAULT  SEGGER_RTT_MODE_NO_BLOCK_SKIP

选择缓冲区大小时,可参考以下经验:

  • 低频日志(每秒几十条):512 B ~ 1 KB 足够。
  • 中频日志(每秒几百条):2 KB ~ 4 KB,避免调试器轮询间隙丢数据。
  • 高频或突发日志:8 KB 以上,或改用多通道分流。

注意:缓冲区是静态分配的全局数组,增大缓冲区会直接增加 RAM 占用。在 RAM 紧张的 MCU 上,应结合日志频率和调试器读取速度权衡。

三、通道数量与模式选择

RTT 支持多个上行/下行通道,每个通道有独立的缓冲区。典型用法是:

  • 通道 0:终端日志,配合 SEGGER_RTT_printf() 使用。
  • 通道 1:二进制数据流,如传感器采样、波形数据。
  • 通道 2:控制命令或状态回传。

通道数量由 SEGGER_RTT_MAX_NUM_UP_BUFFERS 和 SEGGER_RTT_MAX_NUM_DOWN_BUFFERS 决定。增加通道会略微增加控制块大小和初始化开销,但能有效隔离不同数据流,避免日志与高速数据互相阻塞。

模式选择通过 SEGGER_RTT_MODE_DEFAULT 或运行时 SEGGER_RTT_SetFlagsUpBuffer() 设置,常见模式有:

模式行为适用场景
NO_BLOCK_SKIP缓冲区满时跳过本次写入,不阻塞实时性要求高,允许丢日志
NO_BLOCK_TRIM缓冲区满时截断本次写入希望保留部分数据
BLOCK_IF_FIFO_FULL缓冲区满时阻塞等待不允许丢日志,可接受阻塞

默认推荐 NO_BLOCK_SKIP,因为它不会让目标程序因日志而卡死。若日志用于故障复现且不能丢,可改用 BLOCK_IF_FIFO_FULL,但要评估阻塞对实时任务的影响。

四、初始化时机对系统启动的影响

初始化时机是本节最容易被忽视、却最容易出问题的地方。常见错误包括:

  • 在时钟未稳定时初始化:若 RTT 初始化依赖系统时钟或调试接口时钟,过早调用可能导致控制块未正确建立。
  • 在 RTOS 调度器启动前初始化:通常没问题,但若在中断中首次调用 RTT 输出,需确保控制块已就绪。
  • 在低功耗模式下未重新初始化:部分 MCU 进入低功耗后 RAM 保持,但调试接口可能断开,唤醒后需重新连接调试器,RTT 控制块通常无需重建。

推荐做法:在 main() 中完成系统时钟、GPIO、调试接口初始化后,紧接着调用 SEGGER_RTT_Init(),然后再初始化外设和 RTOS。这样既能保证 RTT 尽早可用,又不会因时钟未就绪而失败。

如果使用 RTOS,建议在创建任务之前完成 RTT 初始化,这样所有任务都可以直接使用 RTT 输出,无需额外同步。

五、配置检查与验证

完成配置后,可通过以下方式验证:

  1. 编译下载后,用 J-Link RTT Viewer 连接目标,查看是否出现 RTT 控制块标识。
  2. 调用 SEGGER_RTT_WriteString(0, "RTT OK\n"),观察终端是否输出。
  3. 若使用多通道,分别向通道 0 和通道 1 写入数据,确认两个通道独立工作。
  4. 在缓冲区满的情况下观察是否按所选模式丢数据或阻塞。

下面这张图展示了 RTT 目标端初始化的关键路径:从启动代码到控制块建立,再到调试器扫描识别,以及不同初始化时机对系统启动的影响。

RTT 目标端初始化路径与时机影响 启动代码 / Reset 时钟、RAM 初始化 SEGGER_RTT_Init() 建立控制块 _SEGGER_RTT RTT 控制块(RAM 中) 标识 "SEGGER RTT" + 通道表 上行/下行缓冲区指针与大小 读写索引、模式标志 调试器扫描 RAM 找到 "SEGGER RTT" → 建立通道 RTT 数据通道 上行日志 / 下行命令 初始化时机对系统启动的影响 ✓ 推荐:时钟就绪后、外设初始化前 RTT 尽早可用,不影响后续初始化 ✗ 过早:时钟未稳定时调用 控制块可能未正确建立,调试器无法识别 △ 过晚:RTOS 启动后才初始化 早期任务日志丢失,需额外同步 ⚠ 中断中首次调用:需确保控制块已就绪 否则可能写入未初始化缓冲区

从图中可以看到,RTT 初始化的本质是“在正确的时间把控制块准备好”。只要控制块建立成功,调试器就能通过内存扫描找到它,后续的日志和数据传输就水到渠成。反之,时机不对或配置不当,就会出现“连上了但没输出”的典型问题。

修改 SEGGER_RTT_Conf.h 后必须重新编译整个工程,因为缓冲区大小和通道数量会影响控制块结构体布局。只重新编译部分文件可能导致控制块与调试器预期不一致。

最后给出一个可直接参考的配置示例,适用于大多数 Cortex-M 项目:上行 2 KB、下行 16 B、3 个上行通道、默认 NO_BLOCK_SKIP 模式,并在 main() 中时钟初始化后立即调用 SEGGER_RTT_Init()。这套配置在日志量与 RAM 占用之间取得了较好的平衡,也便于后续按需调整。