Rust in Everything
基于 probe-rs 0.32.0

CMSIS-DAP 驱动

主线位置:访问主线的最底层(CMSIS-DAP 那一支)。上一层交下来的是一个个 DP / AP 寄存器访问,本页讲驱动怎么把它们攒成 DAP_Transfer 命令、什么时候真正发出 USB 包。

sequenceDiagram
  participant A as ArmCommunicationInterface
  participant D as CmsisDap
  participant U as USB 探针固件
  A->>D: raw_write_register SELECT
  D->>D: batch 里放一个写
  A->>D: raw_write_register TAR
  D->>D: batch 里再放一个写
  A->>D: raw_read_register DRW
  D->>U: DAP_Transfer 两个写一个读
  U-->>D: 执行个数 ACK 读到的数据
  D-->>A: DRW 的值

CMSIS-DAP 是 Arm 定义的一套探针固件协议:主机发一条命令(第一个字节是命令号),探针执行完回一条应答。探针固件自己负责 SWD / JTAG 的位时序、posted 读和 WAIT 重试,主机只说「读 AP 的这个寄存器」。DAPLink、Raspberry Pi Debug Probe 等都实现了它。probe-rs 的驱动在 probe/cmsisdap/,mod.rs 1417 行,命令编码在 commands/ 下。

打开设备

CmsisDapFactory::open 先按 selector 找到 USB 设备(tools.rs 列举时把 v2 接口排在 v1 前面),再进 new_from_device:清空设备里残留的应答,问包长(DAP_Info 的 packet size,最多试 16 次,每次 50 ms 超时)、问能同时排队几个包、问能力(SWD、JTAG、SWO……),默认速度 1000 kHz。

设备有两种(CmsisDapDevice):

v1v2
USB 传输HID reportbulk 端点
每次发送必须发满一个 report(加 1 字节 report ID)只发实际长度
依赖hidapi,在 cmsisdap_v1 feature 后面(0.30 起,#3473)nusb
0.32 的变化无端点句柄在打开时申请一次、之后复用(#4099)

每条命令都是同步的一来一回:send_command_inner 把命令号写进 buffer[1],写出去,再读一个应答,检查应答的第一个字节是不是同一个命令号。

用到的主要命令(CommandId):

命令号用在哪
DAP_Info0x00包长、能力、版本
DAP_Connect / DAP_Disconnect0x02 / 0x03选 SWD 或 JTAG
DAP_TransferConfigure0x04空闲周期、WAIT 重试次数
DAP_Transfer0x05一串 DP / AP 读写,本页的主角
DAP_TransferBlock0x06对同一个寄存器连续读写多次(MEM-AP 的 DRW)
DAP_SWJ_Pins / DAP_SWJ_Clock / DAP_SWJ_Sequence0x10 / 0x11 / 0x12引脚、时钟、线复位与切换序列
DAP_JTAG_Sequence0x14JTAG 移位

batch_add:只攒写

驱动对上层的接口就是 RawDapAccess,读写都进 batch_add:

// probe-rs/src/probe/cmsisdap/mod.rs
    fn batch_add(&mut self, command: BatchCommand) -> Result<Option<u32>, ArmError> {
        tracing::debug!("Adding command to batch: {}", command);

        let command_is_read = matches!(command, BatchCommand::Read(_));
        self.batch.push(command);

        // We always immediately process any reads, which means there will never
        // be more than one read in a batch. We also process whenever the batch
        // is as long as can fit in one packet.
        let max_writes = (self.packet_size as usize - 3) / (1 + 4);
        if command_is_read || self.batch.len() == max_writes {
            self.process_batch()
        } else {
            Ok(None)
        }
    }

写先放进 self.batch,不发;遇到读,连同前面攒下的写一起发出去,因为调用方马上要这个读的值。所以一批里最多一个读,而且总在最后。每个写在包里占 5 个字节(1 字节请求加 4 字节数据),包头 3 个字节,64 字节的包最多攒 12 个写。写的错误要等这一批发出去才知道:raw_write_register 返回 Ok 只表示「放进了 batch」。需要确认写已生效的地方(比如上电序列)会显式调 raw_flush。

process_batch 把 batch 编成一个 TransferRequest,发出去,看应答。一个 DAP_Transfer 的布局由 TransferRequest::to_bytes 和 InnerTransferRequest::to_bytes 决定:命令号、DAP 索引(JTAG 链上的第几个 TAP,SWD 下忽略)、传输个数,然后每个传输一个请求字节(bit0 APnDP、bit1 RnW、bit2 A2、bit3 A3),写再跟 4 字节小端数据。

按这个规则,访问主线最后那一批(模拟目标的日志:写 SELECT、写 CSW、写 TAR、读 DRW)编出来是:

05 00 04 | 08 00 00 00 00 | 01 52 00 00 EB | 05 00 00 00 20 | 0F
命令 索引 个数  W DP SELECT=0   W AP CSW        W AP TAR         R AP DRW

19 个字节(v1 还要加 1 字节 report ID 并补满 report)。探针按顺序执行这 4 个传输,回 05 04 01 加读到的 4 字节:执行了 4 个、最后一个的 ACK 是 OK、数据。这一段字节是按源码的编码规则手工推出来的,和真实探针的 USB 抓包对照待实测。

应答与错误

process_batch 只看最后一个传输的 ACK:

ACK处理
OK如果执行的个数少于请求的个数,按 CMSIS-DAP 规范这不该发生,但「Still, this seems to happen in practice」,报错;否则返回最后一个读的值
FAULT调 handle_sticky_err:不经过 batch 直接读 CTRL/STAT,有粘滞错误就写 ABORT 清掉,然后报 FAULT
WAIT写 ABORT 的 DAPABORT 位放弃当前事务,报 WAIT
NO ACK报错;源码在这里留了一句 // TODO: Try a reset?

注意 WAIT 在这里不重试:重试已经由探针固件按 DAP_TransferConfigure 的参数做过了,能走到主机说明固件放弃了。0.30 之前驱动在传输出错时还会自己重试,0.30 去掉了(#3466),CHANGELOG 给的理由是修复 i.MX RT6xx 等芯片在复位期间的异常行为。0.32 又改了两处:块传输里的 FAULT 和单次传输一样走 handle_sticky_err(#3806);读请求的应答里没有数据时返回错误而不是 panic(#4130),后者就是 raw_read_register 里那个 ok_or_else。

块传输的错误处理 handle_transfer_block_response 至今是这一片 TODO 最密的地方,FAULT、NACK、WAIT 三个分支各有一句「TODO: Perform error handling -> clear fault, etc.」和「TODO: Properly track what exactly failed」:出错时知道执行到了第几个字,但不往上报。

块传输

MEM-AP 读一段内存时,会对 DRW 连续读 N 次(MEM-AP 与 ROM table),这在 RawDapAccess 里是 raw_read_block / raw_write_block。CMSIS-DAP 用 DAP_TransferBlock 实现(mod.rs:1115):先 process_batch 把攒着的写发掉,再按「包长减 6 字节头,除以 4」切片,64 字节的包每片 14 个字,每片一来一回。

两处值得留意的细节

  • SWJ 序列不冲刷 batch。 send_swj_sequences 开头那行 //self.process_batch()?; 被注释掉了。如果 batch 里还攒着写,再发一个线复位,线上的实际顺序是「先复位、后写」。probe-rs 自己的调试序列在需要的地方显式调了 raw_flush(比如 debug_port_connect 写完 ABORT 之后),模拟目标的 attach 日志里也没有出现「batch 里有写时发 SWJ 序列」的情况;自定义序列是否会踩到,未核实。
  • 溢出检测关着。 try_get_arm_debug_interface 用 ArmCommunicationInterface::create(self, sequence, false),CTRL/STAT 的 ORUNDETECT 不打开;位级探针传的是 true(DebugProbe 与驱动注册表)。

实验:数一数命令

模拟目标的 DapSim 在 RawDapAccess 这一层记账,按上面的规则推算 CMSIS-DAP 驱动会发多少条命令:读一次算一条(连同之前攒的写);攒满 (包长 - 3) / 5 个写算一条;块传输先冲刷,再按每片 (包长 - 6) / 4 个字算;每个 SWJ 序列算一条。结果(模拟目标,64 字节包):

操作DP / AP 访问推算的命令条数
attach STM32F103C814262
session.core(0)83
read_word_32(含 core(0))104
read_32,4 个字,跨 1 KiB 边界147
halt(含读 PC)227
set_hw_breakpoint(6 个比较器)4018
遍历 ROM table12152

把包长换成 512 字节(高速 USB 的 v2 探针常见的值)重跑,每一行的数字都不变:这些操作里连续的写从来不超过 12 个,命令条数完全由「读了几次」决定。和位级探针对照,同一个 attach 在 polyfill 上是 274 个 SWD 包,而 CMSIS-DAP 只要 62 次 USB 往返;但对 USB 来说,往返次数才是代价的大头。每条命令的往返时间、实际的 SWD 时钟速度和 probe-rs benchmark 的吞吐都待实测。

小结

  • CMSIS-DAP 探针的固件做 DAP 事务,驱动只发命令:DAP_Transfer 装一串 DP / AP 读写,DAP_TransferBlock 对一个寄存器连续读写。
  • batch_add 只攒写,遇到读立刻连同前面的写一起发,所以一批最多一个读;64 字节的包最多 12 个写,块传输每片 14 个字。
  • FAULT 时读 CTRL/STAT 清粘滞位,WAIT 时写 DAPABORT,驱动自己不重试(0.30 去掉了重试);0.32 统一了块传输的 FAULT 处理,并把「读没拿到数据」从 panic 改成错误。
  • v1 走 HID、每次发满一个 report,v2 走 bulk;SWJ 序列不冲刷 batch;CMSIS-DAP 不开溢出检测。
  • 模拟目标上推算:attach 一次约 62 条命令,一次 read_word_32 4 条,包长从 64 换到 512 不影响这些数字。

💬 关联讨论

发起讨论
加载中...