首页 / 资讯中心 / 文章详情

CMSIS-DAP协议核心命令数据包逐字节拆解与调试实战

CMSIS-DAP协议核心命令数据包逐字节拆解与调试实战 ★ FEATURED ARTICLE
平时我自己做调试器固件和工具链适配的时候最常干的一件事就是对着CMSIS-DAP协议的数据包发呆。命令手册翻过很多遍但真正到了要自己组包、抓包、定位“为什么上位机连不上”的时候需要的不是“知道有个命令叫DAP_Transfer”而是知道这个命令发出去之后每一个字节应该怎么摆、响应回来之后又该怎么解析。这篇内容就是把CMSIS-DAP协议里最关键的命令用完整的hex数据包示例逐个拆开。读者主要面向三类人正在写自研调试器/烧录器固件的人、在移植某个调试栈时被USB HID报文折磨的人、以及纯粹想搞明白调试器底层在干什么的嵌入式开发者。下面的报文示例统一按CMSIS-DAP v2.x的包格式来写也就是“1字节命令ID 2字节小端长度字段 payload”。如果你还在用老的v1协议只需要把长度字段从两字节换成一字节命令ID和payload的摆放完全一样后面我会专门讲这个差异。1. 包级认知命令地图、头部格式与v1/v2差异1.1 CMSIS-DAP命令地图CMSIS-DAP协议的命令按功能可以分成三组枚举与连接、寄存器传输访问、以及JTAG/SWD线路控制。先把最常用的命令ID列出来后面所有报文都是围绕它们展开的。命令ID用途DAP_Info0x00查询调试器固件信息、版本、能力位DAP_Connect0x02选择SWD或JTAG接口DAP_Disconnect0x03断开调试接口DAP_Transfer0x05执行1到n个DP/AP读写DAP_TransferBlock0x06对同一寄存器连续读写批处理DAP_TransferAbort0x07中止当前传输操作DAP_WriteABORT0x08向目标ABORT寄存器写值DAP_Delay0x09延时指定毫秒数DAP_ResetTarget0x0A复位目标芯片DAP_SWJ_Pins0x10直接控制SWJ引脚电平DAP_SWJ_Clock0x11设置SWJ时钟频率DAP_SWJ_Sequence0x12输出一段自定义SWJ序列DAP_SWD_Configure0x13配置SWD参数如turnaround周期DAP_SWD_Sequence0x14输出一段自定义SWD序列DAP_JTAG_Sequence0x15输出一段自定义JTAG序列这张表不需要背但建议把它贴在工位旁边。因为后续无论你用逻辑分析仪抓包还是自己组包本质上都是在和这十几个ID打交道。1.2 头部结构和Length字段的坑CMSIS-DAP v2的头部非常简洁第一个字节是命令ID第二和第三字节是小端表示的payload长度从第四个字节开始才是真正的数据。所有命令的响应也遵循同样的规则也就是说一个完整包的最小长度是3字节ID Length低字节 Length高字节。v1和v2最大的区别就在这个长度字段上。v1协议里长度就是一个8位字节因此单个命令的payload理论上限是255字节再加上早期很多实现挂在USB HID Report上、报告大小往往只有64字节所以v1时代大家都习惯了把小包拼来拼去。到了v2长度变成16位单个包的payload上限大幅提高配合USB bulk传输批量命令才能放开手脚。这里有一个实际中很容易踩的坑USB HID的Report大小和CMSIS-DAP协议包大小是两回事。有些固件收到PC发来的一个64字节HID Report里面只装了一个12字节的CMSIS-DAP命令剩下的字节会填充0x00或0xFF。解析的时候不要拿填充字节去凑长度字段也不要因为“payload没填满”就认为协议错了。我见过不止一个新人在抓包时对着满屏的00纠结半天其实填充就是填充。2. 握手阶段的数据包DAP_Info枚举与DAP_Connect2.1 DAP_Info怎么问出版本和能力上位机连接调试器之后干的第一件事几乎都是枚举。DAP_Info是所有命令里最简单的请求payload只有一个字节这个字节就是要查询的信息项ID。常用的信息项如下Info ID含义0x01厂商名称字符串0x02产品名称字符串0x03序列号字符串0xF0CMSIS-DAP协议版本0xF1目标接口类型0JTAG1SWD0xF2最大packet count0xF3最大packet size0xF4能力位先看一个查询协议版本的完整对话。请求要查询0xF0请求: 00 01 00 F0逐字节解释00是DAP_Info命令ID01 00是长度字段表示payload有1个字节F0就是要查询的信息项ID。大多数v2固件会返回一个32位小端版本号假设固件报告版本为2.0.0响应可能是响应: 00 04 00 00 02 00 00其中00是响应命令ID04 00是长度4后面的00 02 00 00就是小端的0x00000200。注意也有一部分固件只返回两个字节的版本号00 02 00这是因为实现者把版本号定义成了uint16。所以解析DAP_Info时一定不要按固定长度写死要以响应里的Length字段为准。再查接口类型0xF1请求: 00 01 00 F1 响应: 00 01 00 01响应里的最后一个01表示当前固件支持并配置为SWD模式。如果固件只支持JTAG这里会返回00。很多复位时序错误、IDCODE读不到的怪问题第一步就应该看这个值是不是自己想要的。2.2 DAP_Connect选SWD还是JTAGDAP_Connect的作用和名字完全一致就是通知调试器固件“接下来用SWD还是JTAG”。请求payload一个字节00表示保持默认01表示SWD02表示JTAG。请求: 02 01 00 01 响应: 02 01 00 00请求解析02是命令ID01 00是长度101表示选择SWD。响应解析02是响应ID01 00是长度1最后一个00表示成功。如果返回值非0一般是0xFF这种错误码说明目标接口初始化失败这时候要回头检查硬件接线或者前面的枚举流程。2.3 推荐握手顺序和失败排查我自己的调试栈里标准握手顺序是固定的DAP_Info查询0xF1确认接口类型。DAP_Info查询0xF2和0xF3拿到固件支持的最大包数量和包大小。DAP_Connect选择SWD或JTAG。DAP_SWD_Configure配置SWD参数。DAP_SWJ_Clock设置初始时钟频率。读一次DPIDR验证链路。这个顺序不是绝对的但“先Connect再配置时钟”对绝大多数固件都成立。如果你先配了时钟再Connect有些固件会在Connect时重置时钟配置导致刚才设的频率白设。握手失败的排查思路也分享一个实战经验。之前某项目里PC端反复发送DAP_Connect都返回错误抓包看固件收到的报文完全正常。后来定位到是DAP_Info查询0xF2返回的packet count和实际固件实现不一致上位机按照返回的count一次性提交了过多命令固件缓冲区直接溢出。所以枚举阶段拿到的能力参数尽量要信但也要做好降级重试。3. DAP_Transfer读写DP/AP寄存器的逐字节拆解3.1 Transfer Request位域怎么编码DAP_Transfer是CMSIS-DAP协议里最核心的命令没有之一。SWD和JTAG协议栈的DP/AP寄存器读写最终都是通过它来完成的。每个传输请求是一个32位字段标准CMSIS-DAP v2规范里它低位的布局如下位域含义bit00DP访问1AP访问bit10写1读bit[6:2]寄存器地址编码这里有一个经常被混淆的点。SWD协议里DP寄存器只有4个地址位A[3:2]的范围就是0到3可以直接放进bit[6:2]的低两位。AP寄存器的地址则是先右移2位得到寄存器编号比如AP的DRW寄存器偏移0x0C右移2位得到3再放进bit[6:2]。所以同样是“地址3”DP访问和AP访问的request值可能会差出很多组包的时候一定要分清楚APnDP位。3.2 读DPIDR的完整报文示例读DPIDR是所有调试会话建立之后的第一件事相当于探路的“ping”。DPIDR在DP地址0处读操作所以Transfer Request编码为APnDP0RnW1地址0得到0x00000002。完整请求如下请求: 05 09 00 00 02 00 00 00 00 00 00 00逐字节拆开05DAP_Transfer命令ID09 00payload长度9即DAP index占1字节 Transfer Request占4字节 Transfer Data占4字节00DAP index多核调试器才可能非0一般固定为002 00 00 00Transfer Request读DP地址000 00 00 00Transfer Data读操作时这个字段必须占位读操作的Data字段是占位符但绝不能省略。曾经有人图省事读操作请求里只发了index和request长度字段写成5结果后面所有字节全部错位固件把响应当垃圾丢掉。这个问题我在好几个自己组包的上位机里都见过。假设目标返回的IDCODE是0x12345678ACK为OK响应如下响应: 05 05 00 01 78 56 34 1205响应命令ID05 00payload长度5即Transfer Response占1字节 数据占4字节01Transfer Response低三位是ACK状态0b001表示ACK_OK78 56 34 12DPIDR的值小端读到78 56 34 12而不是12 34 56 78是正常的因为ARM调试总线本身就是小端。很多刚开始接触的人会怀疑是字节序反转了其实是目标端就是这么吐数据的。3.3 连续访问AP寄存器CSW/TAR/DRW组合示例读内存不是直接读地址而是要通过AP寄存器间接访问。最经典的流程是写CSW配置访问宽度写TAR设置目标地址然后读DRW。这三步在CMSIS-DAP协议里就是三个DAP_Transfer。写AP CSW寄存器。CSW地址偏移0x00编号0写操作Request为0x00000001写入值0x23000002请求: 05 09 00 00 01 00 00 00 02 00 00 23写AP TAR寄存器。TAR地址偏移0x04编号1写操作Request为0x00000005写入目标地址0x20000000请求: 05 09 00 00 05 00 00 00 00 00 00 20读AP DRW寄存器。DRW地址偏移0x0C编号3读操作Request为0x0000000F请求: 05 09 00 00 0F 00 00 00 00 00 00 00 响应: 05 05 00 01 78 56 34 12注意这里Request从0x05变成了0x0F就是因为APnDP位从0变成了1同时读位也置1地址编号3占到了bit[3:2]。不熟悉位域的话建议每次组包前都把这三个维度DP/AP、读/写、寄存器编号单独算一遍再拼到一起不要凭感觉写。3.4 响应里的ACK位和重试逻辑Transfer Response这个字节核心就是低三位的ACK状态。SWD协议里ACK_OK是0b001ACK_WAIT是0b010ACK_FAULT是0b100。当你看到响应字节是0x02时说明目标忙这时候正确的做法不是立刻放弃而是等待几个周期后重试同一个请求。当看到0x04时说明目标调试逻辑报错这时候需要查看目标调试寄存器的错误位并通常要写ABORT寄存器清掉sticky错误而不是无脑重发。批量连续读写时还要注意DAP_Transfer是支持一次请求里带多个Transfer Request的。比如一次读DPIDR又读CTRL/STAT可以把两个request拼在一个包里减少USB往返。但多请求模式下响应里的Transfer Response和数据是交错排列的解析时一定要按照请求顺序逐个取。4. DAP_TransferBlock批量报文的边界与拆包4.1 连续读内存请求不带数据区响应带DAP_TransferBlock和DAP_Transfer的核心区别在于Block会对同一个Transfer Request连续执行count次。这个特性特别适合读内存先写CSW和TAR然后连续读DRW多次因为AP的TAR在每次访问后自增所以能读出一段连续地址。假设已经配置好CSW和TAR现在要连续读4个word。请求如下请求: 06 09 00 00 0F 00 00 00 04 00 00 00逐字节拆开06DAP_TransferBlock命令ID09 00payload长度9即index 1字节 Request 4字节 Count 4字节00DAP index0F 00 00 00Transfer Request读AP DRW04 00 00 00Count连续读4次读操作的Block请求里没有Data区因为数据是目标返回的。响应如下响应: 06 11 00 01 AA BB CC DD 11 22 33 44 55 66 77 88 99 00 AA BB06是响应ID11 00是长度17也就是1字节Transfer Response加16字节数据01是ACK_OK后面16字节就是4次DRW读到的值按顺序排列。这里有一个实战技巧连续读内存时如果目标端速度跟不上Block中间某一次访问返回ACK_WAIT整个Block响应的处理逻辑会变得很复杂。我的做法是Count不要一次贪多先小批量验证跑通后再调大。4.2 连续写数据区怎么排、响应为什么短连续写4个word到同一寄存器请求里就要带上完整的写数据区。请求如下请求: 06 19 00 00 01 00 00 00 04 00 00 00 78 56 34 12 AA BB CC DD 11 22 33 44 55 66 77 8806是命令ID19 00是长度25也就是index 1字节 Request 4字节 Count 4字节 Data 16字节后面16字节就是4个要写入的值按访问顺序排。写Block的响应一般比读Block短常见是只回一个Transfer Response响应: 06 01 00 01Length为1数据只有01这一个ACK状态。不同固件对写Block的响应实现略有差异有些会回长度0有些会回长度1解析时不要对响应长度做过于严格的假设。4.3 包大小限制和拆包策略Block命令最大的坑在于包大小。虽然CMSIS-DAP v2的长度字段是16位看起来能塞很多数据但固件实际能处理的包大小是有限的这个限制可以通过DAP_Info的0xF2和0xF3查询到。0xF2是固件支持的最大packet数量0xF3是最大packet size单位是字节。比如0xF3返回512那你组一个写100个word的Block请求payload是144400409字节没超512可以直接发。如果返回64那它只能容纳一次写十几个word超了就必然出错。拆包策略的核心就是“监测目标内存地址的边界 按packet size计算每次最多能发多少个word”。我习惯按比上限略小的整数值切块比如上限是512我就按每个Block最多64个word切给固件留足处理富余。另外一个容易被忽略的是DAP_Info的0xF2和0xF3返回值在上电后不是一成不变的。某些调试器固件在切换JTAG/SWD模式后会重新上报不同的packet能力所以最好在每次Connect完成后重新查询一次。5. 控制类命令实测时钟、引脚、复位、延时5.1 DAP_SWJ_Clock32位kHz参数DAP_SWJ_Clock用于设置SWD和JTAG时钟频率请求payload是一个32位小端的kHz值。比如设置1000kHz请求: 11 04 00 E8 03 00 00 响应: 11 00 0011是命令ID04 00是长度4E8 03 00 00是1000的32位小端表示。响应11 00 00表示成功且没有额外数据。时钟设置的核心经验是“启动时低稳定后高”。目标芯片刚从复位里出来时SRST可能还拉着这时候用太高频率去访问DP很容易遇到ACK_WAIT甚至无响应。稳妥做法是先设一个低频比如100kHz完成握手和IDCODE读取后再把频率拉高。5.2 DAP_SWJ_Pins一次拉低nRESET并保持的报文DAP_SWJ_Pins允许上位机直接控制SWJ接口的引脚。SWJ引脚的bit定义如下bit引脚bit0TCK / SWCLKbit1TMS / SWDIObit2TDIbit3TDObit4nTRSTbit5nRESET请求payload有三个字节output、select、wait。output是要输出的电平select是选择哪些引脚生效wait是保持时间单位毫秒。一个典型场景是手动拉低nRESET 100ms再释放。拉低nRESET也就是output的bit5为0select的bit5为1请求: 10 03 00 00 20 6410是命令ID03 00是长度300是output20是select64是100ms。响应会带一个当前pin输入状态的字节响应: 10 01 00 20响应里的20代表读到的实际引脚电平bit5还是1表示nRESET当前仍为低。要注意这个wait并不是绝对精确的毫秒定时很多固件实现用的是简单软件延时误差会比较大。如果你需要精确的复位时序建议在上位机侧控制延时而不是依赖DAP_SWJ_Pins的wait。5.3 DAP_ResetTarget与DAP_Delay组合DAP_ResetTarget是让调试器固件去做一次目标复位请求和响应都非常简单请求: 0A 00 00 响应: 0A 00 00之所以要把它和DAP_Delay放在一起说是因为实际复位时序里单纯发一个ResetTarget往往不够。很多目标芯片要求复位释放后保持一段时间才能稳定进入调试状态。组合方式通常是发DAP_ResetTarget让硬件复位引脚动作。发DAP_Delay等待几十毫秒。发DAP_SWJ_Pins手动拉低/释放nRESET做二次确认。发DAP_SWJ_Clock设低频再读DPIDR验证。DAP_Delay的payload是16位小端毫秒数示例延时320ms请求: 09 02 00 40 01 响应: 09 00 00注意DAP_Delay虽然叫Delay但它的语义是“发送方在继续下一步前应该等待这么久”固件实现时通常会阻塞所以不要在一个需要高响应性的会话里频繁调用。5.4 DAP_TransferAbort和DAP_WriteABORT的区别这两个命令名字很像但作用对象完全不同。DAP_TransferAbort是上位机通知调试器固件“放弃当前正在执行的Transfer序列”它的请求payload只有一个DAP index请求: 07 01 00 00一般用于上位机发现目标长时间ACK_WAIT已经超过了自己设定的超时时间不想继续傻等。而DAP_WriteABORT是直接对目标芯片的ABORT寄存器做一次写操作。目标端的SWD DP控制逻辑里有一堆sticky错误标志比如STICKYERR、STICKYCMP等出错之后不清掉后续访问就会一直被FAULT挡住。这时候需要发DAP_WriteABORT写入对应的清除位请求: 08 05 00 00 1E 00 00 00 响应: 08 00 0008是命令ID05 00是长度500是DAP index1E 00 00 00是写入ABORT寄存器的值。这里我写0x1E是按常见方式来清除主要sticky错误位具体取值取决于你实际遭遇的错误位。实战中看到连续ACK_FAULT时第一反应不应该是反复重发DAP_Transfer而是先写一次ABORT把目标端的错误状态清干净再继续。6. 序列命令进阶DAP_SWJ_Sequence与DAP_SWD_Sequence6.1 info字节“count-1”的来历CMSIS-DAP有一类序列命令允许上位机绕过标准协议限制直接往SWJ/SWD线路上吐自定义波形。它们的请求格式非常相似命令ID Length info字节 数据字节。最让人困惑的就是这个info字节。info字节的低7位表示“本次要发送的bit数减1”。也就是说如果info低7位是0x07实际发送8个bit是0x0F实际发送16个bit。设计成减1是为了让1到128个bit都能用一个7位字段表达。第一次自己组包的人十有八九会把0x07看成“发送7个bit”然后盯着波形怎么少了一位。记住一个口诀info永远是bit数减1。数据字节则是按照bit序列打包的默认按LSB first的顺序排列。比如要发送8个bitbit0到bit7依次对应数据字节的第0位到第7位。6.2 输出8bits序列的完整报文用DAP_SWJ_Sequence输出8个bit序列值为0xA5也就是1010 0101请求: 12 02 00 07 A5 响应: 12 00 0012是命令ID02 00是长度207是info字节低7位为7表示8个bitA5是要输出的一字节数据。如果序列长度超过8bit数据字段就要相应增加字节数。例如发送17个bitinfo为0x10数据字段就需要3个字节。一定要确保数据字节数足以容纳指定的bit数否则固件会读到缺失的数据行为不可预知。DAP_SWD_Sequence的报文结构完全一样只是命令ID换成0x14请求: 14 02 00 07 A5 响应: 14 00 00二者在物理层的作用域不同。DAP_SWJ_Sequence是通用的SWJ线控制适合JTAG模式或者切换时序DAP_SWD_Sequence是专门给SWD模式下的线路序列用的会按照当前SWD参数的turnaround周期来调整采样点。6.3 序列命令帮你绕过标准命令的局限序列命令最大的价值在于它能让上位机实现标准命令集之外的时序。比如某些芯片有特殊的SWD切换序列标准的DAP_Connect不一定能触发再比如一些非标准调试接口需要在TMS线上精确拉特定周期的高电平这时候DAP_SWD_Sequence就是唯一解。但序列命令的滥用也会带来问题。用序列命令纯软搓一个完整的DPIDR读取时序响应延迟会比标准DAP_Transfer高一个数量级因为每一bit的翻转都可能是USB往返里的一条指令。我见过有人为了“灵活”把整个调试时序全用序列命令实现结果调试速度慢得没法用。正确姿势是标准命令能做的用标准命令序列命令只做标准命令覆盖不到的边角时序。还有一点不同调试器固件对序列命令的支持深度差异很大。有的固件对info字节的bit7有特殊定义比如表示最后一段序列有的固件完全不支持DAP_SWJ_Sequence。所以在自己的调试栈里用序列命令之前一定要通过DAP_Info先确认固件能力不能默认所有CMSIS-DAP调试器都支持全套命令。最后分享一个我自己调试时的习惯。拿到一个新的CMSIS-DAP调试器我先不用PC上位机连接而是用USB抓包工具记录PC端正常连接时的完整报文流把DAP_Info、DAP_Connect、读DPIDR这几段留档当作“标准答案”。之后自己写的固件或者上位机所有组包都以这批报文为基准对照。绝大多数所谓“协议不兼容”问题最后发现都是长度字段算错、读操作数据区没占位、或者Response里多个请求的数据没按顺序解析。把这几个基础错误避开CMSIS-DAP协议这条路基本就走顺了。
阅读完成 · 觉得有帮助?
咨询建站