Rust in Everything
基于 probe-rs 0.32.0

位级探针:从 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
turnaround1无人线的方向从主机切到目标
ACK3目标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 读 DPIDR101001010xA5
DP 写 SELECT100011010xB1
DP 读 RDBUFF101111010xBD
AP 读 CSW111000010x87
AP 写 CSW110001010xA3
AP 写 TAR110100010x8B
AP 读 DRW(或同一位置上 bank 0xF 的 IDR)111110010x9F

采样相位

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 STM32F103C814 735274142
session.core(0)(重建 MEM-AP 状态)850168
read_word_32(含上一行的 850 周期)1 0602010
read_32,4 个字,跨过 1 KiB 边界1 3622614
halt(含读 PC)2 3504422
write_core_reg(R0)1 5002814
set_hw_breakpoint(6 个比较器)4 2308040
遍历 ROM table(4 个组件)10 060197121

两个观察:

  • 真正读那个字只要 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 清粘滞位后报错;其他协议错误不恢复。

💬 关联讨论

发起讨论
加载中...