RTT 控制块与通道
主线位置:命令主线第五步「轮询」里的「读 RTT」。固件把日志写进自己 RAM 里的环形缓冲区,主机经调试端口去读:没有中断、没有额外的线,只有访问主线上的内存读写。本页讲内存里的布局、probe-rs 怎么找到它、读写各动哪几个字。
flowchart TB cb["_SEGGER_RTT:id 16 字节,max_up,max_down"] --> up0["up[0]:name、buffer、size、WrOff、RdOff、flags"] cb --> up1["up[1] ..."] cb --> down0["down[0]:同样 6 个字段"] up0 --> ubuf["环形缓冲区:固件写 WrOff,主机读后写 RdOff"] down0 --> dbuf["环形缓冲区:主机写 WrOff,固件读后写 RdOff"]
内存布局
RTT 是 SEGGER 的协议,probe-rs 实现了主机这一侧,布局和 SEGGER 的 C 实现兼容(rtt.rs:157、channel.rs:24):
// probe-rs/src/rtt.rs
struct RttControlBlockHeaderInner {
id: [u8; 16],
max_up_channels: u32,
max_down_channels: u32,
}
// probe-rs/src/rtt/channel.rs
pub(crate) struct RttChannelBufferInner<T> {
standard_name_pointer: T,
buffer_start_pointer: T,
size_of_buffer: u32,
write_offset: u32,
read_offset: u32,
flags: u32,
}
控制块是 24 字节的头,后面紧跟 max_up_channels 个 up 通道描述和 max_down_channels 个 down 通道描述,32 位目标上每个 24 字节。魔数是 "SEGGER RTT\0\0\0\0\0\0"。
谁写哪个字是 RTT 不需要锁的原因:
写 write_offset | 写 read_offset | |
|---|---|---|
| up 通道(目标 → 主机) | 固件 | 主机 |
| down 通道(主机 → 目标) | 主机 | 固件 |
每个偏移只有一方写,另一方只读;write == read 表示空,write + 1 == read(取模)表示满,所以一个 n 字节的缓冲区最多存 n − 1 字节。flags 的低两位是模式(ChannelMode):缓冲区满时固件丢掉整条、截断,还是阻塞等主机读。
找到控制块
先要知道它在哪。命令行(parse_metadata)先在 ELF 的符号表里找 _SEGGER_RTT(find_rtt_control_block_in_file),找到就是 ScanRegion::Exact(地址);找不到才用 --scan-region。这个参数的默认值是空字符串,被解析成「空的范围列表」,结果是 NoControlBlockLocation:没有符号、又没给 --scan-region 时,命令行根本不扫描,也不轮询 RTT。0.32 用 --scan-region(接受 ram、一个地址或一个范围)替换了原来的 --rtt-scan-memory(#3747)。库的 Rtt::attach 默认则是 ScanRegion::Ram。
find_control_block 对 Ram 和 Ranges 的做法很直接:把每个范围整段读出来,在里面找魔数;找到一个就用它,零个报 ControlBlockNotFound,多个报 MultipleControlBlocksFound。Ram 是目标描述里所有非别名的 RAM 区,大 RAM 的芯片扫一遍要读几百 KiB,这也是命令行默认不扫的原因。
attach_at 在给定地址读 24 字节的头,魔数不对就是 ControlBlockNotFound;通道数超过 255 认为控制块坏了(ControlBlockCorrupted);然后一次读出所有通道描述,逐个建 Channel:缓冲区指针为 0 的通道当作没初始化,跳过并打一条警告;有名字指针的读最多 128 字节的 C 字符串作为通道名。
实验:在模拟目标里放一个控制块
本卷的模拟 SWD 目标有一块会保存写入的 RAM。attach STM32F103C8(RAM 0x20000000..0x20005000)后,用 core.write 像固件那样摆一个控制块:0x20000400 处,2 个 up 通道(第二个不初始化)、1 个 down 通道;up 0 的缓冲区 64 字节,放一条 12 字节的消息,故意跨过缓冲区末尾(RdOff = 58、WrOff = 6);down 0 的缓冲区 16 字节。
== control block: 96 bytes at 0x20000400
== find_control_block(Ram) -> Ok(20000400); DP/AP transfers: 5149
== attach_at(0x20000400) -> ptr=0x20000400; DP/AP transfers: 111
up #0 name=Some("Terminal") size=64
down #0 name=Some("Terminal") size=16
(core() + read with data: 19 DP/AP transfers)
(core() + empty read: 11 DP/AP transfers)
== up0 read 12 bytes: "hello, rtt!\n"
read offset in target memory now: 6
== up0 read again: 0 bytes
== up0 read after read pointer changed: Err("The read pointer changed unexpectedly.")
== down0 write -> 5 bytes; target buffer "ping\n", write offset 5
== down0 write 16 more bytes -> 10 accepted (16-byte ring, firmware has not read yet)
== attach_at(wrong address) -> Err("RTT control block not found in target memory.\n- Make sure RTT is initialized on the target, AND that there are NO target breakpoints before RTT initialization.\n- For VSCode and probe-rs-debugger users, using `halt_after_reset:true` in your `launch.json` file will prevent RTT\ninitialization from happening on time.\n- Depending on the target, sleep modes can interfere with RTT.")
== attach_at(max_up_channels=300) -> Err("The control block has been corrupted: Unexpected array sizes at 0x20000400: max_up_channels=300 max_down_channels=1")
逐条看:
- 扫描很贵:扫 20 KiB 的 RAM 是 5149 次 DP / AP 访问(5120 个字的块读加上开销);已知地址的
attach_at是 111 次(头、通道描述、两个通道名)。有_SEGGER_RTT符号就别扫。 - 一次轮询的代价:没数据时一次
read是 11 次访问,其中 8 次是session.core(0)重建 MEM-AP 状态(Session、Core 与 MemoryInterface),真正读两个偏移只有 3 次;有数据时 19 次。命令行有数据时立刻再读、没数据时等 100 ms(一条 probe-rs run)。真实探针上的吞吐和丢失率待实测。 - 回绕:12 字节分成两段读,
read_core把两段读和「写回读指针」排成一组Operation交给execute_memory_operations(channel.rs:350),读完目标内存里的RdOff变成 6。 - 读指针被别人改了:主机记着上次写回的读指针,下次读之前发现目标里的值变了,就报
ReadPointerChanged,而不是猜测发生了什么。两个主机程序同时读同一个 RTT(比如probe-rs run和另一个调试器)会撞上这个错误。 - down 通道:
DownChannel::write写数据、再写WrOff;固件没读时 16 字节的缓冲区最多再收 10 字节(已有 5 字节,留 1 字节区分空和满),多出来的不阻塞、直接返回实际写入数。0.32 起probe-rs run/attach发现 down 通道时会给一个输入提示,ctrl-d切换通道(#3746)。 - 两种错误:魔数不对报
ControlBlockNotFound,那段很长的提示就是它的Display;通道数离谱报ControlBlockCorrupted。
为什么 FakeProbe 上找不到
nRF52833 夹具的 ELF 里有 _SEGGER_RTT,地址 0x20000074,可在 FakeProbe 上 attach_at 得到 ControlBlockNotFound。两层原因:
0x20000070起是.bss(filesz = 0),控制块在固件启动、初始化 RTT 之后才写进 RAM,ELF 里没有它的初值;- 就算是
.data里的静态控制块(defmt-rtt就是这样,_SEGGER_RTT是一个带初值的static),FakeProbe 也是按加载地址返回 ELF 内容,读 RAM 运行地址永远得 0。
真芯片上同样的现象也会出现:固件停在 RTT 初始化之前(比如复位后停在 main 入口),控制块还不存在。命令行的 RttClient 因此不是只试一次,而是每轮轮询都 try_attach,直到找到为止(monitor.rs:258)。为了避免连上上一次运行留下的旧控制块,监控开始时(刚烧录或复位、核还没跑到 RTT 初始化)会先把它清掉(clear_control_block:已知确切地址时把 24 字节的头写成 0,靠扫描找到的只清魔数);但如果控制块地址本身就在这次要下载的数据里(固件直接下载到 RAM 运行),清掉就等于破坏它,configure_from_loader 会关掉清除。常见坑把 ControlBlockNotFound 当作一个坑再讲。
小结
- 控制块是 24 字节的头加若干通道描述;每个读写偏移只有一方写,所以主机和固件不需要锁。
- 命令行优先用 ELF 里的
_SEGGER_RTT;没有符号又没给--scan-region时不扫描、不轮询。扫描就是把整段 RAM 读出来找魔数,模拟目标上 20 KiB 是 5149 次访问。 - 一次空轮询在模拟目标上是 11 次访问,其中 8 次花在重建
Core;读回绕的数据和写回读指针排成一组操作。 - 读指针被别人动过时报
ReadPointerChanged;down 通道写满就截断,不阻塞。 FakeProbe找不到控制块,是因为它按加载地址返回 ELF 内容,而控制块在 RAM 的运行地址上,还常常在.bss里。