
1. 为什么搞懂 SWD 协议比背命令更重要调试 ARM Cortex-M 时10 个人里有 9 个人只是点一下 Keil 的 Download 按钮或者 OpenOCD 里敲一句reset halt。真正问起调试器和芯片之间到底发生了什么很多人会卡壳。直到你在现场遇到could not stop cortex-m device! please check the jtag cable.这种报错才发现自己对 SWD 的理解一直停留在“能用就行”的黑盒阶段。我自己第一次被这个报错折磨是在一块自己画的板子上。原理图查了三遍焊接补了两次最后用逻辑分析仪抓 SWDIO 波形才发现问题出在目标板供电时序上——调试器比芯片先上电导致 SWD 端口初始化失败。从那一刻起我意识到搞懂 SWD 协议层的细节不是学院派的知识炫技而是关键时刻能救命的实战技能。这篇文章我来手把手带你过一遍 SWD 协议读写寄存器的完整链路协议格式、包结构、实际抓波形、对照时序图逐位拆解再把常见连不上的问题串一遍。适合正在用 Cortex-M 做开发、被调试器折磨过、或者想从“照着点按钮”进阶到“看得懂时序图”的工程师。2. SWD 协议核心约定与硬件基础2.1 SWD 相比 JTAG 解决了什么SWDSerial Wire Debug是 ARM 定义的调试接口目的很直接用更少的引脚实现和 JTAG 几乎等价的调试能力。标准的 JTAG 需要 TCK、TMS、TDI、TDO 四根信号线加一根 GNDCortex-M 内核本身也支持这个接口。但在小封装芯片上引脚资源非常宝贵。SWD 把信号线压缩到两根双向的 SWDIO数据线和单向的 SWCLK时钟线配合 GND 就可以完成调试。也就是说一颗芯片只需要少到 3 个引脚SWDIO、SWCLK、GND就能完成烧录、调试、寄存器读写全部操作。除了省引脚SWD 在实际布线中的优势也很明显。两根线不像 JTAG 那样对信号完整性和布线长度那么敏感在多层板或批量产线上更稳定。而且 SWD 支持热插拔的容错能力也更强一些。ST-Link、J-Link、CMSIS-DAP 这些调试器之所以默认跑 SWD 模式看的正是这套方案的综合性价比。但省引脚是有代价的协议更复杂数据是半双工分时复用的必须严格按时序收发不能像 JTAG 那样全双工并行。这就导致很多引脚的“操作习惯”完全不能直接用我们必须重新过一遍数据包结构。2.2 必须认识的几个 CoreSight 组件在正式开始读寄存器之前还要建立一张地址地图。Cortex-M 内核内部的调试架构属于 ARM CoreSight 体系包含两个关键的调试组件DPDebug Port调试端口和 APAccess Port访问端口。DP 是调试器在物理接口上直接操作的那一层。SWD 协议本身就是面向 DP 的读写协议所有通过 SWD 发出来的包第一个目标都是 DP 中的寄存器。AP 是访问芯片内部系统总线的窗口。常用的 AP 类型叫 MEM-AP通过它发起 AHB/APB 总线访问才能最终读写 Flash、SRAM、外设寄存器。从 SWD 协议的角度看读写流程分为“两步跳转”第一步先通过 DP 寄存器选中 AP 和 AP 内部的 bank第二步再通过 DP 的 AP 数据寄存器拿具体地址去访问目标存储器或寄存器。稍后的波形分析里你会看到实际抓到的每一个读操作都伴随着两段不同的时序就是因为这个过程的存在。2.3 典型的硬件连接参考动手抓波形前先确认硬件接线。以常用的 CMSIS-DAP 调试器为例标准的 2×5 10pin 排针接口上SWD 模式基本只需要连接引脚信号说明1VTref目标板参考电压检测只做电平判断不要让它供电2SWDIO双向数据线必须连接4SWCLK时钟线必须连接7NC未连接9GND共地必须连接细节提醒有些调试器排针上的 VTref 脚如果悬空调试器无法识别目标板电压会直接报 “No target connected” 之类的错误。这个脚只要接到目标板的 3.3V 电源点上即可不要额外灌电流。如果你用逻辑分析仪抓波形建议把 SWDIO 接到分析仪的通道 0SWCLK 接到通道 1采样率设置成调试器 SWCLK 频率的 10 倍以上。比如调试器跑 4MHz分析仪至少 40MS/s否则时序边缘会失真。另外还要保证共地否则抓出来的波形全是毛刺。3. SWD 包结构与读写时序拆解3.1 包结构逐字段解析SWD 协议里每一次传输都是一个固定格式的包理解这些字段是看懂波形的基础。完整的数据包包含以下内容Start1 bit固定为 0表示包起始APnDP1 bit访问目标选择0 表示 DP1 表示 APRnW1 bit读/写方向0 表示写1 表示读A[2:3]2 bit寄存器地址选择位注意这里只有两位配合 APnDP 和 bank 来选择具体寄存器Parity1 bit以上几个 bit 的偶校验位Stop1 bit固定为 1Park1 bit固定为 0ACK[0:2]3 bit目标返回的应答信号复位值为 0b001OKData[0:31]32 bit 数据读操作由目标驱动写操作由主机驱动Parity1 bit数据线的偶校验位如果计算一下一个完整的写操作包大约是 8 位请求 3 位 ACK 32 位数据 1 位校验总共 50 个时钟周期左右。读操作因为有 turnaround 周期会略长一些。偶校验的算法很简单统计所有需要校验的 bit 中 1 的个数若为偶数则校验位填 0奇数则填 1。这个机制不复杂但它能有效防止线缆接触不良时的单 bit 跳变漏过实际调试中确实能帮我们快速定位物理层问题。3.2 为什么要区分 DP 和 AP 两层访问很多初学者刚接触 SWD 协议时最困惑的问题就是“我明明只想读某个外设寄存器为什么还要先操作 DP”原因是包里的地址位只有两位A[2:3]加上 APnDP 也就只能表示 4 个 DP 寄存器或 4 个 AP 寄存器。但芯片内部寄存器空间远大于此所以必须加一层选择机制。DP 中有个关键寄存器叫 SELECT0x08其中包含 APBANKSEL[3:0] 和 APSEL[7:0] 等字段。每次访问 AP 寄存器之前调试器软件都必须先写 SELECT把要访问的 AP 编号和 AP 内部寄存器 bank 号配置好然后再发起目标 AP 的读写。整个过程就像先翻目录再翻页前面看着繁琐但换来的是协议简单、硬件开销小。这也是为什么纯手动操作 SWD 协议时“先配 SELECT 再访问 AP”是最容易出错的一步。后面实战抓波形时你会清楚地看到这个两次请求的序列。3.3 读操作与写操作的时序差异读操作和写操作在时序上最明显的差异在 turnaround 周期缩写为 Trn。写操作主机在发完请求包后不需要切换方向直接继续驱动数据线发送 32 bit 数据。读操作主机发完请求包必须释放 SWDIO 的控制权等待目标器件的 ACK 和数据输出。这个释放控制权的时间窗口就是 turnaround 周期SWD 协议允许配置为 1 个或 4 个周期默认通常是 1 个周期。数据方向切换带来的坑在裸机环境下非常常见如果调试器在 Trn 周期没有把 IO 模式从输出切到输入就会丢失输入数据表现就是读回来的数据全是 0xFF 或者 ACK 超时。从波形分析的角度读操作数据段的第一个 bit 前会有一个明显的“空闲窗口”这就是 Trn 存在的证据。4. 实战基于 CMSIS-DAP 抓取并分析 SWD 波形4.1 如何给 SWD 发第一条命令这里以 STM32F103 芯片为例通过 PyOCD 来发起访问因为你可以在命令行中直观看到它调用了哪些操作。相比 OpenOCDPyOCD 的 Python 接口更容易让我们逐步控制过程适合演示协议流程。先安装并连接好调试器后在命令行中运行pyocd list看到你的 CMSIS-DAP 设备之后用交互式 Python 客户端连接from pyocd.core.helpers import ConnectHelper session ConnectHelper.session_with_chosen_probe(target_overridestm32f103c8) session.open() target session.target这里target_override指定目标芯片型号PyOCD 会自动加载相应的 Flash 算法和 CoreSight 配置。连接成功后用下面这段代码读取 PC程序计数器寄存器pc target.read_core_register(pc) print(hex(pc))不出意外你会得到一个类似0x0800012c这样的地址。但问题来了这一句代码背后SWD 线上到底发生了多少次事务答案是很多次。PyOCD 会先执行 halt 操作再通过 CoreSight 寄存器获取 PC这中间可能包含十几次甚至几十次 SWD 包交换。为了看清细节我们需要抓波形。4.2 用逻辑分析仪抓取 SWDIO/SWCLK启动逻辑分析仪连接好 SWDIO 和 SWCLK把触发条件设为 SWDIO 下降沿然后复位目标板或者重新执行一次连接脚本。如果你用的是 Saleae 逻辑分析仪还可以直接选 SWD 协议解析器它能帮你自动标记 Start、ACK、Data 等字段非常方便。如果没有协议解析功能也完全可以手动分析。关键在于定位数据包边界。SWD 包有一个典型特征每次传输都以一个低电平 Start bit 开始而且 SWDIO 线在空闲状态时保持高电平由主机上拉驱动。找到高电平到低电平的跳变往往就是一个新包的起点。下面这段是从实际抓到的读写序列中取出的部分关键信号状态。假设 SWCLK 是 4MHz一个 bit 占 250ns逻辑分析仪设置 50MS/s 采样时每个 bit 有 200 个采样点足够精确判断电平变化。4.3 一步步解析写请求波形先看一个“写 SELECT 寄存器”的完整包。写操作目标DP 的 SELECT 寄存器地址为 0x08。请求阶段的 8 个 bit 会是这样的Start 0 APnDP 0 (访问 DP) RnW 0 (写) A[2:3] 00 (SELECT 寄存器地址 bit0-1) Parity 1 (前面 4 位中 0000偶数校验位取反逻辑按协议实现实际根据原始位计算) Stop 1 Park 0严格按协议校验位算法计算Start 不参与校验APnDP(0)、RnW(0)、A(0)、A(0)四个 bit 中 1 的个数是 0偶校验应为 0但 SWD 协议要求请求字段的奇偶校验位计算时包含 APnDP、RnW、A[2:3]不含 Start同时 Stop 和 Park 不参与。如果四个 bit 中 1 的个数为 0偶校验结果应为 0。这里特别容易记混我把常见的 DP 寄存器请求地址整理成了表格方便实际对照。寄存器地址APnDPA[2:3]典型用途DPIDR0x00000读 ID 号验证连接CTRL/STAT0x04001控制调试电源域、请求复位SELECT0x08010选择 AP 和 bankRDBUFF0x0C011缓冲上一次 AP 读结果写完 SELECT 之后紧接着发起 AP 写操作APnDP变为 1。从波形上你会看到两个特征明显的包第一个包 APnDP0第二个包 APnDP1。第二个包的数据段就是你要真正写入内存映射地址的内容。两者缺一个或者顺序颠倒调试器都会报错。4.4 一步步解析读请求波形与数据采样读操作要稍微复杂一些因为涉及总线方向切换。以“读 DP 的 IDCODE 寄存器”为例主机发出请求包Start0、APnDP0、RnW1、A[2:3]00、Parity0、Stop1、Park0主机释放 SWDIO 控制权进入 turnaround默认 1 个时钟目标器件驱动 ACK[0:2]001表示 OK目标器件继续驱动 32 bit 数据目标器件驱动 1 bit 校验位然后释放总线逻辑分析仪上你会看到这样的特征请求包结束后SWDIO 上有 1 个时钟的空闲周期高阻或保持电平不确定然后是 3 个周期的 ACK 信号。数据段以 IDCODE 值开头比如 STM32F103 的 IDCODE 是 0x1BA01477波形上表现为连续 32 个 bit 的组合。如果你使用 Saleae 的 SWD 解析器它会直接标注出[Request] APnDP0 RnW1 A0x0、[ACK] OK、[Data] 0x1BA01477这样的字段工作效率会高很多。但建议你至少手动拆解一次整个包才能真正理解解析器输出的是什么。4.5 结合波形明确“读取外设寄存器”的完整过程前面两个小节讲的是 DP 和 AP 的基础操作实际项目里我们最常做的是读取外设寄存器比如读某个 UART 的状态寄存器或者 GPIO 的输出数据寄存器。完整流程应该是写 DP SELECT配置 APSEL0选中 MEM-AP、APBANKSEL 为相应 bank写 AP CSW 寄存器配置传输大小如 32-bit、地址自增模式写 AP TAR 寄存器设置目标地址如外设地址 0x40021000发起 AP 读请求数据会暂存在 AP 的 DRW 寄存器中再发起一次 DP RDBUFF 读请求拿到上一步读取的数据注意第 4 步和第 5 步的配合第一次读 AP 的 DRW 时实际返回的数据是上一次 TAR 地址访问的结果。因此连续读数据时要先发起一次“预读”来填充流水线再通过 RDBUFF 取回数据。如果漏掉这个细节你读到的数据永远是上一地址的值整整偏移一个周期。从波形上看你会注意到每次 AP 读和 DP RDBUFF 读之间通常没有任何空闲这是调试器在刻意保持流水线忙碌提升连续读取速度。5. 实战中遇到的高频问题与排查思路5.1 “could not stop cortex-m device”到底在说什么这个报错翻译过来是“无法停止 Cortex-M 内核”。调试图上最核心的前提就是先把内核 Halt 住。如果做不到一切寄存器读写都无从谈起。J-Link 或者 ST-Link 在报这个错的同时通常还会提示please check the jtag cable。但根据我的经验线缆松动只是原因之一实际情况中它经常掩盖了更深层的问题。常见的诱因包括SWDIO/SWCLK 被目标板上的其他外设占用目标芯片的调试功能被读保护RDP锁住SWD 引脚复用被重映射成 GPIO而代码里没恢复调试功能电源不稳定导致复位向量执行到一半芯片又复位了时钟配置错误导致内核时钟完全停止排查时建议按这个顺序先测 SWDIO/SWCLK 静态电平排除物理层再用调试器读 DPIDR 确认连接最后看电源和复位时序。不要一上来就怀疑线缆90% 的情况问题在别处。5.2 读 IDCODE 失败波形显示 ACK 超时如果你抓波形时看到请求包发出来了但 ACK 段一直是高电平0b111说明目标芯片没有响应。可能原因之一SWD 接口被代码禁用。很多 Cortex-M 芯片在上电后默认能访问调试接口但如果程序里对 SWD 引脚做了 GPIO 重映射或者调用了类似的禁用操作调试器就再也连不上了。解决方案是使用“连接前复位”功能。在 PyOCD 中可以通过session ConnectHelper.session_with_chosen_probe( target_overridestm32f103c8, options{reset_on_connect: hw} )让调试器在连接前先拉低复位引脚使芯片停在复位状态此时 SWD 端口被强制释放调试器就能重新建立连接。这种方法在实际现场中成功率极高建议作为首选方案。5.3 波形有数据但读回全 0xFF这种情况通常是电平不匹配或者线缆过长导致信号质量差。SWDIO 是双向线读操作时芯片驱动低电平表示 0释放总线外部上拉表示 1。如果上拉电阻阻值太大比如大于 100K加上线缆电容高电平恢复时间会变长导致位周期内采样点还没到达 Vih。解决方法是降低 SWCLK 频率。很多调试器默认跑 4MHz你可以强行压到 1MHz 或者 100KHz 测试。在 PyOCD 中session.probe.set_clock(100000) # 100 kHz把时钟降下来之后如果问题消失就基本确诊为信号完整性问题而不是协议问题。5.4 寄存器读出的值是上一次的值这个问题在前面 4.5 节已经提过是 AP 读流水线机制导致的。简单说AP 的读操作并不是实时返回当前地址的数据而是返回上一次地址访问的结果。很多驱动代码初次写 AP 读功能时都会遇到这种“数据慢一个周期”的诡异现象。解决办法是在读序列的最后额外加一个 RDBUFF 读请求。调试器本身已经这样处理了但如果你自己写裸机 SWD 代码要特别注意这个流水线延迟。5.5 SWD 波形正常但 Keil 仍报错如果你用逻辑分析仪看到包收发正常ACK 也正常但 Keil 仍然报错。这时候把关注点从 SWD 协议转移出来检查一下芯片的 supply voltage。很多板子调试器和目标板供电不是同一路如果 VTref 检测脚电压异常调试器会在连接阶段直接拒绝工作。另外也要确认复位电路没有异常。某些复位芯片在上电后会拉低复位脚几百毫秒如果调试器在这个窗口内发起连接可能被芯片的复位信号打断。通过调试器配置“连接时忽略复位”或者人为延长上电到连接之间的延时往往能绕过这类问题。6. 进一步提升自己动手实现一个最简单的 SWD 主机6.1 软件实现 SWD 主机的核心代码框架很多人看完前面的波形分析后会有种“好像自己也能写一个 SWD 驱动”的感觉。确实可以而且用普通 GPIO 模拟 SWD 并不复杂——只要严格按时序翻转电平即可。下面是基于 STM32 HAL 库实现的 SWD 写函数主体逻辑核心思路就是先把 SWDIO 配成输出按 bit 序发送请求包然后等待 ACK再发送 32bit 数据。void swd_write(uint8_t apndp, uint8_t addr, uint32_t data) { uint8_t request 0x81; // Start0, APnDP, RnW0, A[2:3], Parity, Stop1, Park0 // 实际应根据 apndp 和 addr 动态计算 request 字节 // 发送请求包 for (int i 0; i 8; i) { swdio_write((request i) 1); swclk_toggle(); } // 读取 ACK需要切换 SWDIO 方向为输入 swdio_set_input(); uint8_t ack 0; for (int i 0; i 3; i) { ack | (swdio_read() i); swclk_toggle(); } // 如果是 OK继续发送数据 if (ack 1) { swdio_set_output(); for (int i 0; i 32; i) { swdio_write((data i) 1); swclk_toggle(); } // 发送校验位这里省略计算 } }需要注意这里为了可读性简化了request字节的计算。实际算的时候要用 2.1 节讲的字段规则把 APnDP、RnW、A[2:3] 按位组装并计算偶校验位不能直接填充 0x81。6.2 调试这种软件 SWD 的注意事项用模拟 GPIO 实现 SWD 时最常踩的坑是 GPIO 方向切换的延迟。如果切换后立刻读数据可能因为引脚电容没有充放电完成而读到错误电平。稳妥做法是在方向切换后插入至少 1 个时钟周期的软件延时。另外很多 MCU 的 GPIO 输出模式需要配置成开漏加上拉而不是推挽输出。原因是 SWD 总线在空闲时必须保持高电平并且读操作时目标芯片要能主动拉低总线。如果主机用推挽输出且一直驱动高电平目标芯片根本拉不低整个协议直接卡死。还有一个经验之谈开始时把 SWCLK 频率降得非常低比如 10KHz先用逻辑分析仪抓下来确认每个 bit 都正确了再逐步提高频率。这是我调试自定义 SWD 主机时最有效的排错手段——频率一高示波器上看不清逻辑分析仪也容易误触发问题定位难度会指数级上升。6.3 从协议走到实际应用还需要什么会了 SWD 读写寄存器也就掌握了调试器最核心的能力。但从协议层走到一个完整的调试工具还需要补上内存映射表、Flash 编程算法、异常向量处理、断点指令覆盖这几块内容。比如读写内存虽然可以直接通过 AP 的 TAR/DRW 完成但要写 Flash就必须在 RAM 中执行一段 Flash 编程算法通过目标芯片自己的 Flash 控制器来擦写不能直接往 Flash 地址写数据了事。这些内容再展开又是一篇长文但核心的 SWD 协议部分了解了前面的原理和时序后面的内容基本就是在这个基础上的扩展。7. 写在最后的一点个人复盘SWD 协议刚开始接触时确实比较劝退字段多、半双工、还要区分 DP 和 AP稍不注意就绕晕了。但等你真正拿起逻辑分析仪亲自把一条写请求、一条读请求从波形上完整解读出来以后后面再遇到任何调试器连接问题内心都会踏实很多——因为你不是在黑盒上猜而是清楚地知道每一步应该在波形上看到什么。我个人建议所有做嵌入式开发的朋友哪怕日常工作只需要点几个按钮也值得花半天时间抓一次 SWD 波形手动解一个完整的数据包。这是低成本高回报的投资。下次你再遇到could not stop cortex-m device时就不会只是拔插线缆碰运气而是会用协议分析的思路一步步定位问题。根据自己的经验真正把 SWD 协议读懂之后解决调试连接类问题的时间平均能缩短一半以上这在项目交付的压力下价值是实打实的。