SWD GPIO实现
本文档描述 LibXR::Debug::SwdGeneralGPIO<SwclkGpioType, SwdioGpioType>:一个基于 GPIO 轮询(bit-bang)的 SWD 探针实现。它继承自 LibXR::Debug::Swd,提供 SWD 链路层能力,通常由上层(如 CMSIS-DAP 处理器/调试器)调用。
本文重点放在“如何使用”和“延时参数(loops_per_us)的选择/标定”,不展开实现细节。
1. 总览
SwdGeneralGPIO 使用两根 GPIO 完成 SWD:
- SWCLK:由
SwclkGpioType驱动输出时钟。 - SWDIO:由
SwdioGpioType承担双向数据,运行时在“输出驱动”和“输入采样”之间切换。
时序由软件生成:通过目标时钟 hz 和延时标定系数 loops_per_us 计算“半周期应空转的循环次数”,从 而逼近指定 SWCLK。
推荐外围电路(强烈建议按此做):
- SWCLK/SWDIO 串联 33Ω(限流/抑制振铃)
- SWDIO 端接 10k 上拉
2. 模板参数与 GPIO 依赖能力
类定义:
template <typename SwclkGpioType, typename SwdioGpioType>
class SwdGeneralGPIO final : public Swd;
期望 GPIO 类型提供的最小能力(抽象描述):
SetConfig({Direction, Pull}) -> ErrorCodeWrite(bool)Read() -> bool
其中 SWDIO 需要支持两种配置:
- 输出驱动:
OUTPUT_PUSH_PULL - 输入采样:
INPUT + PULL_UP
3. 快速开始
构造函数:
explicit SwdGeneralGPIO(SwclkGpioType& swclk,
SwdioGpioType& swdio,
uint32_t loops_per_us,
uint32_t default_hz = DEFAULT_CLOCK_HZ);
典型使用流程:
- 初始化 GPIO 对象与探针实例(先给一个“保守”的频率与已标定的
loops_per_us)。 - 对目标执行
EnterSwd()(不确定目标当前处于何种调试模式时建议调用)。 - 上层用
Swd基类的“带重试”接口进行传输(处理 WAIT、插入 idle clocks 等)。
示例:
using Probe = LibXR::Debug::SwdGeneralGPIO<MyGpio, MyGpio>;
MyGpio swclk, swdio;
// loops_per_us 建议先标定;default_hz 从保守值起步(如 100k~500k)
Probe probe(swclk, swdio, /*loops_per_us=*/calibrated, /*default_hz=*/500000);
probe.EnterSwd();
// 上层建议统一走带重试的路径(示意,具体看你的 Swd 基类封装)
uint32_t idcode = 0;
LibXR::Debug::SwdProtocol::Ack ack;
probe.ReadIdCode(idcode, ack);
4. 时钟与延时参数(重点)
4.1 hz 的含义与行为
SetClockHz(hz) 用于设置“期望的 SWCLK 频率”。
hz会在实现支持的范围内被钳制(低于最小值会被抬高,高于最大值会被压低)。- 频率是否“真的等于设定值”,取决于
loops_per_us是否合理、GPIO 翻转速度、CPU 负载等。 hz == 0会使内部进入“无延时路径”(不主动插入 BusyLoop 延时),此时 SWCLK 实际频率由 CPU 与 GPIO 翻转速度决定,通常是“尽可能快”。
4.2 loops_per_us 是什么
loops_per_us 是一个“延时标定系数”:表示 BusyLoop 大约每 1 微秒需要的循环迭代次数。
它不是通用常量,通常会随以下因素显著变化:
- CPU 主频
- 编译优化等级(
-O0/-O2/-Os等) - 是否开启 LTO、不同编译器版本
- 指令/总线等待状态(某些 MCU 在不同电源/时钟域下也可能变化)
只要上述条件变化,就应重新标定 loops_per_us。
4.3 何时会进入“无延时路径”
实际工作时有两种情况会进入“无延时路径”:
- 你显式把
loops_per_us设为 0;或 - 在某个较高的
hz下,计算出来“半周期需要的循环次数 < 1”,内部会把半周期循环数置 0,从而自动走无延时路径。
无延时路径的意义:
- 优点:开销更低,吞吐更高。
- 代价:SWCLK 频率不再受
hz精准约束,而是“尽可能快/尽可能稳定”,以平台极限为准。
工程建议:
- 需要“可控且可复现”的 SWCLK:让
half_period_loops_明确大于 0(即loops_per_us标定合理,且hz不要设得过高)。 - 只追求“能跑、尽量快”:可以把
loops_per_us=0,或把hz调到足够高让系统自然进入无延时。
5. loops_per_us 的选择与标定方法
下面给出两种常用标定方法。原则是:标定应在与你最终固件“相同编译选项 + 相同主频”条件下进行。
方法 A:用硬件计时器/周期计数器标定
思路:执行一次已知循环次数的 BusyLoop,测出耗时,再反推 loops_per_us。
操作要点:
- 选择足够大的循环次数 N(例如 1e6 级别),保证测量时间明显大于计时器分辨率。
- 记录开始时间 t0,执行 BusyLoop(N),记录结束时间 t1。
- 计算耗时
dt_us(微秒)。 loops_per_us ≈ N / dt_us(取整即可)。
优点:不依赖 SWD 外设、也不依赖目标板状态;标定完成后频率可预期。
方法 B:用逻辑分析仪/示波器对 SWCLK 实测回归
思路:先给一个粗略的 loops_per_us,设置一个低到中等的 hz(例如 100 kHz 或 500 kHz),测 SWCLK 实际频率,再调整 loops_per_us 使之贴合。
操作要点:
- 先用“可确保进入有延时路径”的配置(例如较低
hz)。 - 测量 SWCLK 频率
f_meas。 - 按比例调整:
loops_per_us_new ≈ loops_per_us_old * (f_meas / f_target),反复迭代 1~2 次即可收敛。
优点:不需要访问计时器资源;同时把 GPIO 翻转开销一并纳入拟合。
注意:当你已经进入无延时路径时(频率很高),这种方法会失效,因为再调 loops_per_us 也未必能“拉回”到有延时路径。
6. 频率选择建议(实践导向)
建议从保守到激进逐步上调,避免一上来就把链路推到边界:
- 初始 bring-up:100 kHz ~ 500 kHz(更容易排查连线/电气问题)
- 稳定后提速:1 MHz、2 MHz、4 MHz 分档尝试
- 如果需要更高:优先确认走线长度、阻尼电阻、地参考与目标板 SWD 引脚驱动能力;然后再上调
当出现以下症状,通常意味着频率过高或信号完整性不足:
- ACK 频繁出现 WAIT/FAULT/NO_ACK
- 读回数据 parity 失败
- 只在某些线长/某些板子上不稳定
处理顺序建议:
- 先降频;
- 确认 33Ω 串阻与 SWDIO 上拉;
- 缩短线缆/改善接地;
- 再考虑重新标定
loops_per_us(尤其在改了优化选项之后)。
7. 关闭与安全态
调用 Close() 会把探针引脚恢复到更安全的状态(用于退出调试或切换复用功能时):
- SWCLK 置高
- SWDIO 切到输入上拉