4. SWD通信协议基础:总线状态机、请求包与响应包格式、SWD AP/DP寄存器访问
SWD协议,说白了就是一条两线制的串行调试总线。一条线是SWDIO(数据线),另一条是SWCLK(时钟线)。别看它只有两根线,里面藏着不少门道。我刚开始接触SWD时,总觉得它比JTAG简单,结果第一次调试就栽在了状态机转换上——嗯,今天咱们就把这些坑填平。
4.1 SWD总线状态机:线上的“暗号”
SWD总线没有单独的片选信号,所有命令和数据的边界全靠线序状态来区分。调试器通过SWDIO和SWCLK的时序组合,告诉目标芯片“我要开始发请求了”或者“轮到你来回答了”。
我个人习惯把SWD总线状态机理解为三个核心阶段:
- 空闲态(Idle):SWDIO保持高电平,SWCLK连续翻转。此时总线不传输任何有效数据。
- 请求态(Request):调试器拉低SWDIO,输出8个bit的请求包。目标芯片在这8个时钟周期内采样。
- 响应态(Response):请求包发送完毕后,总线控制权交给目标芯片。目标芯片在3个时钟周期内拉高或拉低SWDIO,输出响应包。
关键点:SWDIO在请求包发送期间由调试器驱动,在响应包期间由目标芯片驱动。两者之间有一个“转向周期”(Turnaround Cycle),用来避免总线冲突。我曾经见过一个项目,就是因为转向周期没留够,调试器和目标芯片同时驱动SWDIO,直接把IO口烧了。
下面这张图展示了SWD总线一次完整的事务流程:
4.2 SWD请求包格式:8个bit说了算
SWD请求包只有8个bit,但每个bit都有明确分工。你想想看,8个bit要完成“读/写”、“访问哪个寄存器”、“哪个AP”这些信息,设计得相当紧凑。
| Bit位置 | 名称 | 说明 |
|---|---|---|
| Bit 0 | Start | 起始位,固定为0。调试器拉低SWDIO表示开始传输。 |
| Bit 1 | APnDP | 0=访问DP寄存器,1=访问AP寄存器。 |
| Bit 2 | RnW | 0=写操作,1=读操作。 |
| Bit 3:4 | A[3:2] | 寄存器地址的高两位。DP和AP内部寄存器通过A[3:2]寻址。 |
| Bit 5 | Parity | 奇偶校验位,覆盖Bit 0~4。采用偶校验。 |
| Bit 6 | Stop | 停止位,固定为1。 |
| Bit 7 | Park | 驱动位,调试器保持SWDIO为高电平。 |
实战技巧:我在调试一个国产MCU时,发现它的SWD实现有个小bug——奇偶校验位计算方式跟ARM标准不一致。标准要求偶校验,它却用了奇校验。排查了整整一天,最后用逻辑分析仪抓波形才找到原因。所以,遇到SWD通信失败,先拿示波器看看Parity bit对不对。
4.3 SWD响应包格式:目标芯片的“回话”
请求包发完后,目标芯片会在3个时钟周期内给出响应。响应包只有3个bit,但包含了ACK确认和协议错误信息。
| 响应值 | 含义 | 说明 |
|---|---|---|
| 001 | OK | 请求成功,后续可以传输数据。 |
| 010 | WAIT | 目标芯片忙,调试器需要重试当前请求。 |
| 100 | FAULT | 协议错误或访问失败,调试器应终止当前事务。 |
响应包之后,如果是读操作,目标芯片会继续输出32位数据 + 1位奇偶校验位。如果是写操作,调试器会在响应包之后输出32位数据 + 1位奇偶校验位。
避坑指南:我曾经遇到过WAIT响应无限循环的情况。目标芯片的DP一直返回WAIT,调试器不断重试,结果死锁了。后来发现是目标芯片的时钟频率太低,而调试器发送请求的间隔太短,芯片根本来不及处理。解决办法是在WAIT响应后插入至少50个空闲时钟周期再重试。
4.4 SWD AP/DP寄存器访问:调试的“钥匙”
SWD协议把寄存器分为两大类:DP(Debug Port)寄存器和AP(Access Port)寄存器。DP是调试端口,负责协议层控制;AP是访问端口,负责访问芯片内部资源(如内存、寄存器)。
我个人习惯把DP比作“门卫”,AP比作“仓库管理员”。你要进仓库拿东西,先得跟门卫打招呼,门卫确认身份后,再让仓库管理员帮你取货。
4.4.1 DP寄存器
DP寄存器通过A[3:2]地址线寻址,共有4个寄存器:
| A[3:2] | 寄存器名称 | 功能 |
|---|---|---|
| 00 | DPIDR | DP标识寄存器,只读。用于识别调试接口版本和厂商。 |
| 01 | CTRL/STAT | 控制/状态寄存器。控制SWD协议行为,读取当前状态。 |
| 10 | RESEND | 重发寄存器,只读。用于读取上一次失败的数据。 |
| 11 | SELECT | 选择寄存器。用于选择当前要访问的AP和AP内部的寄存器组。 |
重点:SELECT寄存器是AP访问的“路由表”。它包含APSEL(AP选择)和APBANKSEL(AP内部寄存器组选择)两个字段。你访问AP寄存器之前,必须先通过SELECT寄存器指定要访问哪个AP以及AP内部的哪个寄存器组。我见过不少新手直接往AP寄存器写数据,结果SELECT没配,数据全写到了默认AP上。
4.4.2 AP寄存器
AP寄存器通过SELECT寄存器的APBANKSEL字段选择寄存器组,每组包含4个寄存器。最常见的AP是MEM-AP(内存访问端口),它的寄存器组如下:
| APBANKSEL | A[3:2]=00 | A[3:2]=01 | A[3:2]=10 | A[3:2]=11 |
|---|---|---|---|---|
| 0 | CSW | TAR | — | DRW |
| 1 | BD0 | BD1 | BD2 | BD3 |
| F | IDR | — | — | — |
其中,CSW(Control/Status Word)控制传输模式,TAR(Transfer Address Register)设置目标地址,DRW(Data Read/Write)是数据寄存器。读写内存的流程就是:先配CSW,再写TAR,最后读写DRW。
4.4.3 一次完整的AP寄存器访问示例
假设我们要读取地址0x20000000处的32位数据。完整的SWD事务序列如下:
// 步骤1:选择AP 0,APBANKSEL=0(因为CSW在Bank0)
写DP SELECT寄存器:APSEL=0, APBANKSEL=0
// 步骤2:配置CSW,设置32位访问、增量地址模式
写AP CSW寄存器:0x23000002
// 步骤3:选择APBANKSEL=0(TAR也在Bank0)
写DP SELECT寄存器:APSEL=0, APBANKSEL=0
// 步骤4:设置目标地址
写AP TAR寄存器:0x20000000
// 步骤5:选择APBANKSEL=0(DRW也在Bank0)
写DP SELECT寄存器:APSEL=0, APBANKSEL=0
// 步骤6:读取数据
读AP DRW寄存器:返回0x20000000处的32位数据
优化建议:上面的流程是标准做法,但效率不高。实际项目中,如果连续读取多个地址,可以只写一次CSW和SELECT,然后循环写TAR、读DRW。我做过一个优化,把连续读操作的吞吐量从200KB/s提升到了1.2MB/s。
4.5 本章小结
SWD协议虽然只有两根线,但状态机、请求包、响应包、寄存器访问这些环节环环相扣。你只要记住:空闲态开始,请求态发命令,转向周期换手,响应态等回复。DP是门卫,AP是仓库管理员,SELECT是路由表。把这些基础打牢,后面分析SWD安全漏洞时才能游刃有余。