位级探针:从 RawSwdIo 到 DAP 事务
主线位置:访问主线的最底层(位级探针那一支)。上一层交下来的是「读 AP 的 DRW」这样一次寄存器访问,本页讲 probe-rs 怎么把它拼成 SWDIO 线上的 46 个时钟周期,再从探针采回的位里解出 ACK 和数据。
flowchart TB dap["RawDapAccess::raw_read_register"] --> pt["perform_transfers:补 RDBUFF、加空闲周期"] pt --> retry["perform_raw_transfers_retry:WAIT 时退避重试"] retry --> swd["perform_swd_transfers:每个传输拼成 IoSequence"] swd --> build["build_swd_transfer:请求 8 位 加 输入槽"] build --> io["RawSwdIo::swd_io:探针翻转 SWCLK、SWDIO"] io --> parse["parse_swd_response:ACK、数据、奇偶校验"]
两个 trait 和一个 blanket impl
位级探针只需要实现 RawSwdIo:
// probe-rs/src/probe.rs
pub trait RawSwdIo: DebugProbe {
/// Drive a sequence of SWD I/O items and return the sampled bits.
fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
where
S: IntoIterator<Item = IoSequenceItem>;
fn swj_pins(&mut self, pin_out: u32, pin_select: u32, pin_wait: u32)
-> Result<u32, DebugProbeError>;
fn swd_settings(&self) -> &SwdSettings;
}
IoSequenceItem 只有两种:Output(bool)(这个时钟周期主机驱动 SWDIO 为 0 或 1)和 Input(这个周期采样 SWDIO)。swd_io 对每一项返回一个 bool,输出项的返回值没有意义。JTAG 一侧对应的是 RawJtagIo:一次移一位 TMS / TDI,采 TDO;再加一个标记 trait AutoImplementJtagAccess,probe/common.rs 就会给它一份完整的 JtagAccess:TAP 状态机、扫描链、IR / DR 移位(common.rs:341 起的 JtagState,common.rs:570 的 blanket impl)。
有了这两样,polyfill.rs:941 的 blanket impl 就给它补上 RawDapAccess:
// probe-rs/src/architecture/arm/traits/polyfill.rs
/// RawDapAccess implementation for probes that implement RawProtocolIo.
// TODO: JTAG shouldn't be required, but an option - maybe via trait downcasting?
impl<Probe: DebugProbe + RawSwdIo + JtagAccess + 'static> RawDapAccess for Probe {
从这里往上,J-Link 和 CMSIS-DAP 走的是同一条路(DebugProbe 与驱动注册表)。TODO 说的是一个现状:只会 SWD 的探针也得实现 RawJtagIo,哪怕所有方法都返回错误;本卷的模拟目标就是这么做的。
一个 SWD 包
build_swd_transfer 把一次访问拼成一串 IoSequenceItem,注释写得很直白:
// probe-rs/src/architecture/arm/traits/polyfill.rs
// Start bit (always 1).
sequence.add_output(true);
// APnDP (0 for DP, 1 for AP).
sequence.add_output(ap_n_dp);
// RnW (0 for Write, 1 for Read).
sequence.add_output(direction_bit);
// Address bits
sequence.add_output(a2);
sequence.add_output(a3);
// Odd parity bit over APnDP, RnW a2 and a3
sequence.add_output(ap_n_dp ^ direction_bit ^ a2 ^ a3);
// Stop bit (always 0).
sequence.add_output(false);
// Park bit (always 1).
sequence.add_output(true);
// Turnaround bit.
sequence.add_input();
// ACK bits.
sequence.add_input_sequence(3);
if let TransferType::Write(value) = direction {
// For writes, we need to a turnaround bit.
sequence.add_input();
// Now we add all the data bits to the sequence.
for i in 0..32 {
sequence.add_output(value & (1 << i) != 0);
}
// Add the parity of the data bits.
sequence.add_output(value.count_ones() % 2 == 1);
} else {
// ... 32 个输入槽、1 个奇偶校验输入、1 个 turnaround
}
| 段 | 位数 | 谁驱动 | 内容 |
|---|---|---|---|
| 请求 | 8 | 主机 | start=1、APnDP、RnW、A[2]、A[3]、奇偶校验、stop=0、park=1 |
| turnaround | 1 | 无人 | 线的方向从主机切到目标 |
| ACK | 3 | 目标 | 001 OK、010 WAIT、100 FAULT(按发送顺序,低位在前) |
| 数据(读) | 32 + 1 | 目标 | 低位在前,最后一位是奇偶校验;再一个 turnaround 把线还给主机 |
| 数据(写) | 1 + 32 + 1 | 主机 | 先一个 turnaround,再 32 位数据和奇偶校验 |
读和写都是 46 个周期(swd_response_length)。请求里只有 A[3:2] 两位地址,所以 DP 和 AP 各自只能直接寻址 4 个寄存器;AP 的其他寄存器要靠 DP 的 SELECT 切 bank,这是下一部分的主题。
把 8 个请求位按低位在前读成一个字节,就是常见调试器日志里的那个「请求字节」。下面是模拟目标实测到的(AP0 指 APSEL=0 的 AHB-AP):
| 访问 | 线上的 8 位 | 字节 |
|---|---|---|
| DP 读 DPIDR | 10100101 | 0xA5 |
| DP 写 SELECT | 10001101 | 0xB1 |
| DP 读 RDBUFF | 10111101 | 0xBD |
| AP 读 CSW | 11100001 | 0x87 |
| AP 写 CSW | 11000101 | 0xA3 |
| AP 写 TAR | 11010001 | 0x8B |
| AP 读 DRW(或同一位置上 bank 0xF 的 IDR) | 11111001 | 0x9F |
采样相位
parse_swd_response 从请求之后的第一个输入槽开始解析,也就是 turnaround 那一位:
// probe-rs/src/architecture/arm/traits/polyfill.rs
// It appears that the JLink probe samples this line with the falling edge of
// the clock. Therefore, the whole sequence seems to be leading by one bit,
// which is why we don't discard the turnaround bit. It actually contains the
// first ack bit.
let (ack, response) = resp.split_at(3);
也就是说,polyfill 默认探针在下降沿采样,采回来的每一位都比「名义上」的位置提前一个周期:turnaround 槽里已经是 ACK 的第 0 位,最后一个数据槽里是奇偶校验。所有走 polyfill 的位级驱动都必须按这个相位返回采样结果。本卷的模拟目标照这个约定实现;某个具体探针的实际相位待实测。
一次访问不止一个包
perform_transfers 在真正发出去之前,会给一批传输补上额外的包。源码开头的注释把规则列全了:
// probe-rs/src/architecture/arm/traits/polyfill.rs
// Read from DebugPort -> Nothing special needed
// Read from AccessPort -> Response is returned in next read
// -> The next transfer must be a AP Read, otherwise we need to insert a read from the RDBUFF register
// Write to any port -> Status is reported in next transfer
// Write to any port -> Writes can be buffered, so certain transfers have to be avoided until a instruction which can be stalled is performed
- AP 读是「posted」的:一次 AP 读的数据阶段里送回来的是上一次 AP 读的结果,这一次的结果要等下一次 AP 读或读 DP 的 RDBUFF 才拿到。所以 AP 读后面如果不紧跟另一个 AP 读,就补一个 RDBUFF 读。
- 写要确认:除了写 ABORT,每个写后面都要再来一个传输,才能知道写有没有出错;一批的最后一个是写时,补一个 RDBUFF 读,DP 会在写缓冲排空之前对它回 WAIT。
- 空闲周期:每个写后面加
num_idle_cycles_between_writes个空闲周期,补的确认读前面再加idle_cycles_before_write_verify个,整批最后加idle_cycles_after_transfer个。SwdSettings的默认值分别是 2、8、8。
问题在于:ArmCommunicationInterface 交下来的单个寄存器访问(raw_read_register / raw_write_register)是一个一个来的,polyfill 也就一个一个处理,没有机会把写 TAR 和读 DRW 合成一批。于是在位级探针上,几乎每个单独的访问都变成两个包。只有块读写(raw_read_block / raw_write_block,MEM-AP 连续读写 DRW 时用)会把同一个寄存器的 N 次访问放进一批:N 个 AP 读后面只补一个 RDBUFF。
实验:一次 read_word_32 在线上
模拟目标这次实现的是 RawSwdIo:swd_io 收到一串 IoSequenceItem,从输出位里解出请求,按 AP 读的 posted 语义和 RDBUFF 应答,再按上面的相位填好输入位。probe-rs 自己的 polyfill 在上面跑完了整个 attach。之后执行 core.read_word_32(0x2000_0000)(模拟内存里放的是 0xCAFEF00D),记下真正访问内存的那两步(H / L 是采回来的高低电平,为了阅读在段之间加了空格):
W AP0 TAR = 0x20000000 11010001 H LL L L 00000000000000000000000000000100 1 +10 idle
R DP RDBUFF 10111101 H LL LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL L LL +8 idle
R AP0 DRW 11111001 H LL LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL L LL +0 idle
R DP RDBUFF 10111101 H LL HLHHLLLLLLLLHHHHLHHHHHHHLHLHLLHH L LL +8 idle
每行依次是:8 位请求(按线上顺序)、turnaround 槽、两个 ACK 槽,然后读是 32 位数据、奇偶校验、两个尾槽,写是第三个 ACK 槽、turnaround、主机驱动的 32 位数据和奇偶校验。第一行是写 TAR:请求 0x8B,ACK 采回 H L L(因为提前一位,turnaround 槽里就是 ACK 的第 0 位,合起来是 OK),然后是地址 0x20000000(低位在前,所以 1 出现在第 29 位,从 0 数)和奇偶校验 1。第二行是 polyfill 补的确认读,数据阶段是上一次 AP 读留下的 0,丢掉。第三行读 DRW,数据阶段同样是旧值。第四行读 RDBUFF 才拿到 0xCAFEF00D:低位在前是 1011 0000 0000 1111 0111 1111 0101 0011,正好是 HLHH LLLL LLLL HHHH LHHH HHHH LHLH LLHH;它有 18 个 1,偶数,所以奇偶校验位是 L。
把每一步的周期数和包数汇总(模拟目标,从不回 WAIT):
| 操作 | SWCLK 周期 | SWD 包 | 同一操作在 DAP 层的访问数 |
|---|---|---|---|
attach STM32F103C8 | 14 735 | 274 | 142 |
session.core(0)(重建 MEM-AP 状态) | 850 | 16 | 8 |
read_word_32(含上一行的 850 周期) | 1 060 | 20 | 10 |
read_32,4 个字,跨过 1 KiB 边界 | 1 362 | 26 | 14 |
halt(含读 PC) | 2 350 | 44 | 22 |
write_core_reg(R0) | 1 500 | 28 | 14 |
set_hw_breakpoint(6 个比较器) | 4 230 | 80 | 40 |
| 遍历 ROM table(4 个组件) | 10 060 | 197 | 121 |
两个观察:
- 真正读那个字只要 4 个包、210 个周期(写 TAR 110、读 DRW 加 RDBUFF 100);其余 850 个周期花在每次
session.core(0)都要重新读一遍 MEM-AP 的 IDR、CSW、CFG 上,原因在 MEM-AP 与 ROM table。 - 「SWD 包」约等于「DAP 层访问数」的两倍,就是上一节说的每个访问单独成批、各补一个 RDBUFF。按 1 MHz 的 SWCLK 折算,210 个周期是 210 µs 的线上时间;探针的 USB 往返另算,实际耗时待实测。
WAIT 和 FAULT
目标忙(比如总线上的访问还没完成)时回 WAIT。perform_raw_transfers_retry 从第一个没成功的传输开始整段重发,最多 num_retries_after_wait 次(默认 1000);每次重发前先写 ABORT 清掉粘滞错误位,并给这一段里的每个写追加空闲周期,追加量从 2 开始翻倍,封顶 128。重试用完还是 WAIT,就写 ABORT 的 DAPABORT 位放弃这次 AP 事务。
FAULT 表示上一次访问出了错,DP 的 CTRL/STAT 里有粘滞错误位。raw_read_register 收到 FAULT 时先读 CTRL/STAT 看原因(溢出还是写错误),清掉粘滞位,再把 FAULT 报上去;如果出错的正好是读 CTRL/STAT 本身,就不再读它,避免无限递归。NO ACK(三位全 1)、奇偶校验错、不合规范的 ACK 不做恢复,直接返回错误:注释说「There's no guaranteed correct way to recover, so don't.」
WAIT 的实际频率和重试次数取决于目标和 SWCLK 速度,待实测。
JTAG 一支
同一个 blanket impl 在 JTAG 模式下走 perform_jtag_transfers:DP 和 AP 访问分别是向 IR 写 0xA(DPACC)、0xB(APACC)后移一个 35 位的 DR,DR 里是 32 位数据、A[3:2] 和 RnW,返回的低 3 位是状态(0b010 表示 OK 或 FAULT,0b001 表示 WAIT)。perform_jtag_transfers 的注释写明了两点:每个传输的结果在下一次移位时才移出来(「Each response is read in the next transaction, so skip 1」),所以一批的末尾要补一次读;JTAG 的应答分不出 OK 和 FAULT,所以还要读一次 CTRL/STAT 看有没有粘滞错误。TAP 状态机、扫描链和多 TAP 的 IR 长度都在 probe/common.rs 里,本卷不再展开。
小结
- 位级探针只实现
RawSwdIo(加上RawJtagIo和标记 trait),polyfill.rs的 blanket impl 补出RawDapAccess,之后和 CMSIS-DAP 走同一套 DP / AP 代码。 - 一个 SWD 包 46 个周期:8 位请求、turnaround、3 位 ACK、32 位数据加奇偶校验;polyfill 默认探针下降沿采样,采回的位提前一个周期。
- AP 读是 posted 的,写需要确认,所以位级探针上几乎每个单独的访问都要补一个 RDBUFF 读;只有对 DRW 的块读写会合成一批。
- 模拟目标上,一次
read_word_32的内存访问本身是 4 个包、210 个周期,而每次session.core(0)重建 MEM-AP 状态要 16 个包、850 个周期。 - WAIT 时整段重发、空闲周期翻倍退避,最多 1000 次;FAULT 时读 CTRL/STAT 清粘滞位后报错;其他协议错误不恢复。