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):
| v1 | v2 | |
|---|---|---|
| USB 传输 | HID report | bulk 端点 |
| 每次发送 | 必须发满一个 report(加 1 字节 report ID) | 只发实际长度 |
| 依赖 | hidapi,在 cmsisdap_v1 feature 后面(0.30 起,#3473) | nusb |
| 0.32 的变化 | 无 | 端点句柄在打开时申请一次、之后复用(#4099) |
每条命令都是同步的一来一回:send_command_inner 把命令号写进 buffer[1],写出去,再读一个应答,检查应答的第一个字节是不是同一个命令号。
用到的主要命令(CommandId):
| 命令 | 号 | 用在哪 |
|---|---|---|
DAP_Info | 0x00 | 包长、能力、版本 |
DAP_Connect / DAP_Disconnect | 0x02 / 0x03 | 选 SWD 或 JTAG |
DAP_TransferConfigure | 0x04 | 空闲周期、WAIT 重试次数 |
DAP_Transfer | 0x05 | 一串 DP / AP 读写,本页的主角 |
DAP_TransferBlock | 0x06 | 对同一个寄存器连续读写多次(MEM-AP 的 DRW) |
DAP_SWJ_Pins / DAP_SWJ_Clock / DAP_SWJ_Sequence | 0x10 / 0x11 / 0x12 | 引脚、时钟、线复位与切换序列 |
DAP_JTAG_Sequence | 0x14 | JTAG 移位 |
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 STM32F103C8 | 142 | 62 |
session.core(0) | 8 | 3 |
read_word_32(含 core(0)) | 10 | 4 |
read_32,4 个字,跨 1 KiB 边界 | 14 | 7 |
halt(含读 PC) | 22 | 7 |
set_hw_breakpoint(6 个比较器) | 40 | 18 |
| 遍历 ROM table | 121 | 52 |
把包长换成 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_324 条,包长从 64 换到 512 不影响这些数字。