平时我自己做调试器固件和工具链适配的时候,最常干的一件事就是对着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目标接口类型,0=JTAG,1=SWD0xF2最大packet count0xF3最大packet size0xF4能力位先看一个查询协议版本的完整对话。请求要查询0xF0:请求: 00 01 00 F0逐字节解释:00是DAP_Info命令ID,01 00是长度字段,表示payload有1个字节,F0就是要查询的信息项ID。大多数v2固件会返回一个32位小端版本号,假设固件报告版本为2.0.0,响应可能是:响应: 00 04 00 00 02 00 00其中00是响应命令ID,04 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表示SWD,02表示JTAG。请求: 02 01 00 01 响应: 02 01 00 00请求解析:02是命令ID,01 00是长度1,01表示选择SWD。响应解析:02是响应ID,01 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规范里,它低位的布局如下:位域含义bit00=DP访问,1=AP访问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编码为:APnDP=0,RnW=1,地址=0,得到0x00000002。完整请求如下:请求: 05 09 00 00 02 00 00 00 00 00 00 00逐字节拆开:05:DAP_Transfer命令ID09 00:payload长度9,即DAP index占1字节 + Transfer Request占4字节 + Transfer Data占4字节00:DAP index,多核调试器才可能非0,一般固定为002 00 00 00:Transfer Request,读DP地址000 00 00 00:Transfer Data,读操作时这个字段必须占位读操作的Data字段是占位符,但绝不能省略。曾经有人图省事,读操作请求里只发了index和request,长度字段写成5,结果后面所有字节全部错位,固件把响应当垃圾丢掉。这个问题我在好几个自己组包的上位机里都见过。假设目标返回的IDCODE是0x12345678,ACK为OK,响应如下:响应: 05 05 00 01 78 56 34 1205:响应命令ID05 00:payload长度5,即Transfer Response占1字节 + 数据占4字节01:Transfer Response,低三位是ACK状态,0b001表示ACK_OK78 56 34 12:DPIDR的值,小端读到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是0b001,ACK_WAIT是0b010,ACK_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逐字节拆开:06:DAP_TransferBlock命令ID09 00:payload长度9,即index 1字节 + Request 4字节 + Count 4字节00:DAP index0F 00 00 00:Transfer Request,读AP DRW04 00 00 00:Count,连续读4次读操作的Block请求里没有Data区,因为数据是目标返回的。响应如下:响应: 06 11 00 01 AA BB CC DD 11 22 33 44 55 66 77 88 99 00 AA BB06是响应ID,11 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是命令ID,19 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是1+4+4+400=409字节,没超512,可以直接发。如果返回64,那它只能容纳一次写十几个word,超了就必然出错。拆包策略的核心就是“监测目标内存地址的边界 + 按packet size计算每次最多能发多少个word”。我习惯按比上限略小的整数值切块,比如上限是512,我就按每个Block最多64个word切,给固件留足处理富余。另外一个容易被忽略的是:DAP_Info的0xF2和0xF3返回值,在上电后不是一成不变的。某些调试器固件在切换JTAG/SWD模式后会重新上报不同的packet能力,所以最好在每次Connect完成后重新查询一次。5. 控制类命令实测:时钟、引脚、复位、延时5.1 DAP_SWJ_Clock:32位kHz参数DAP_SWJ_Clock用于设置SWD和JTAG时钟频率,请求payload是一个32位小端的kHz值。比如设置1000kHz:请求: 11 04 00 E8 03 00 00 响应: 11 00 0011是命令ID,04 00是长度4,E8 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为0,select的bit5为1:请求: 10 03 00 00 20 6410是命令ID,03 00是长度3,00是output,20是select,64是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是命令ID,05 00是长度5,00是DAP index,1E 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个bit,bit0到bit7依次对应数据字节的第0位到第7位。6.2 输出8bits序列的完整报文用DAP_SWJ_Sequence输出8个bit,序列值为0xA5,也就是1010 0101:请求: 12 02 00 07 A5 响应: 12 00 0012是命令ID,02 00是长度2,07是info字节,低7位为7表示8个bit,A5是要输出的一字节数据。如果序列长度超过8bit,数据字段就要相应增加字节数。例如发送17个bit,info为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协议这条路基本就走顺了。
平时我自己做调试器固件和工具链适配的时候,最常干的一件事就是对着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目标接口类型,0=JTAG,1=SWD0xF2最大packet count0xF3最大packet size0xF4能力位先看一个查询协议版本的完整对话。请求要查询0xF0:请求: 00 01 00 F0逐字节解释:00是DAP_Info命令ID,01 00是长度字段,表示payload有1个字节,F0就是要查询的信息项ID。大多数v2固件会返回一个32位小端版本号,假设固件报告版本为2.0.0,响应可能是:响应: 00 04 00 00 02 00 00其中00是响应命令ID,04 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表示SWD,02表示JTAG。请求: 02 01 00 01 响应: 02 01 00 00请求解析:02是命令ID,01 00是长度1,01表示选择SWD。响应解析:02是响应ID,01 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规范里,它低位的布局如下:位域含义bit00=DP访问,1=AP访问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编码为:APnDP=0,RnW=1,地址=0,得到0x00000002。完整请求如下:请求: 05 09 00 00 02 00 00 00 00 00 00 00逐字节拆开:05:DAP_Transfer命令ID09 00:payload长度9,即DAP index占1字节 + Transfer Request占4字节 + Transfer Data占4字节00:DAP index,多核调试器才可能非0,一般固定为002 00 00 00:Transfer Request,读DP地址000 00 00 00:Transfer Data,读操作时这个字段必须占位读操作的Data字段是占位符,但绝不能省略。曾经有人图省事,读操作请求里只发了index和request,长度字段写成5,结果后面所有字节全部错位,固件把响应当垃圾丢掉。这个问题我在好几个自己组包的上位机里都见过。假设目标返回的IDCODE是0x12345678,ACK为OK,响应如下:响应: 05 05 00 01 78 56 34 1205:响应命令ID05 00:payload长度5,即Transfer Response占1字节 + 数据占4字节01:Transfer Response,低三位是ACK状态,0b001表示ACK_OK78 56 34 12:DPIDR的值,小端读到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是0b001,ACK_WAIT是0b010,ACK_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逐字节拆开:06:DAP_TransferBlock命令ID09 00:payload长度9,即index 1字节 + Request 4字节 + Count 4字节00:DAP index0F 00 00 00:Transfer Request,读AP DRW04 00 00 00:Count,连续读4次读操作的Block请求里没有Data区,因为数据是目标返回的。响应如下:响应: 06 11 00 01 AA BB CC DD 11 22 33 44 55 66 77 88 99 00 AA BB06是响应ID,11 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是命令ID,19 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是1+4+4+400=409字节,没超512,可以直接发。如果返回64,那它只能容纳一次写十几个word,超了就必然出错。拆包策略的核心就是“监测目标内存地址的边界 + 按packet size计算每次最多能发多少个word”。我习惯按比上限略小的整数值切块,比如上限是512,我就按每个Block最多64个word切,给固件留足处理富余。另外一个容易被忽略的是:DAP_Info的0xF2和0xF3返回值,在上电后不是一成不变的。某些调试器固件在切换JTAG/SWD模式后会重新上报不同的packet能力,所以最好在每次Connect完成后重新查询一次。5. 控制类命令实测:时钟、引脚、复位、延时5.1 DAP_SWJ_Clock:32位kHz参数DAP_SWJ_Clock用于设置SWD和JTAG时钟频率,请求payload是一个32位小端的kHz值。比如设置1000kHz:请求: 11 04 00 E8 03 00 00 响应: 11 00 0011是命令ID,04 00是长度4,E8 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为0,select的bit5为1:请求: 10 03 00 00 20 6410是命令ID,03 00是长度3,00是output,20是select,64是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是命令ID,05 00是长度5,00是DAP index,1E 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个bit,bit0到bit7依次对应数据字节的第0位到第7位。6.2 输出8bits序列的完整报文用DAP_SWJ_Sequence输出8个bit,序列值为0xA5,也就是1010 0101:请求: 12 02 00 07 A5 响应: 12 00 0012是命令ID,02 00是长度2,07是info字节,低7位为7表示8个bit,A5是要输出的一字节数据。如果序列长度超过8bit,数据字段就要相应增加字节数。例如发送17个bit,info为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协议这条路基本就走顺了。