SWD GPIO Implementation
This document describes LibXR::Debug::SwdGeneralGPIO<SwclkGpioType, SwdioGpioType>: a GPIO polling (bit-bang) SWD probe implementation. It inherits from LibXR::Debug::Swd and provides SWD link-layer capability, typically used by upper layers (e.g., a CMSIS-DAP processor/debugger).
This document focuses on how to use it and how to choose/calibrate the delay parameter (loops_per_us), without diving into implementation details.
1. Overview
SwdGeneralGPIO implements SWD using two GPIOs:
- SWCLK: driven by
SwclkGpioTypeto output the clock. - SWDIO: provided by
SwdioGpioTypefor bidirectional data, switching at runtime between “output drive” and “input sampling”.
Timing is generated in software: it computes the number of busy-loop iterations for each half-period from the target clock hz and the delay calibration factor loops_per_us, approximating the requested SWCLK.
Recommended external circuitry (strongly recommended):
- 33Ω series resistors on SWCLK/SWDIO (current limiting / ringing suppression)
- 10k pull-up on SWDIO
2. Template Parameters and Required GPIO Capabilities
Class definition:
template <typename SwclkGpioType, typename SwdioGpioType>
class SwdGeneralGPIO final : public Swd;
Minimum expected capabilities from the GPIO types (abstractly):
SetConfig({Direction, Pull}) -> ErrorCodeWrite(bool)Read() -> bool
SWDIO must support two configurations:
- Output drive:
OUTPUT_PUSH_PULL - Input sampling:
INPUT + PULL_UP
3. Quick Start
Constructor:
explicit SwdGeneralGPIO(SwclkGpioType& swclk,
SwdioGpioType& swdio,
uint32_t loops_per_us,
uint32_t default_hz = DEFAULT_CLOCK_HZ);
Typical usage flow:
- Create GPIO objects and the probe instance (start with a “conservative” frequency and a calibrated
loops_per_us). - Call
EnterSwd()on the target (recommended if you are unsure which debug mode the target is in). - Use the
Swdbase class “retry-enabled” APIs in upper layers (to handle WAIT, insert idle clocks, etc.).
Example:
using Probe = LibXR::Debug::SwdGeneralGPIO<MyGpio, MyGpio>;
MyGpio swclk, swdio;
// Calibrate loops_per_us first; start default_hz conservatively (e.g., 100k~500k)
Probe probe(swclk, swdio, /*loops_per_us=*/calibrated, /*default_hz=*/500000);
probe.EnterSwd();
// Upper layers should prefer a retry-enabled path (illustrative; depends on your Swd wrappers)
uint32_t idcode = 0;
LibXR::Debug::SwdProtocol::Ack ack;
probe.ReadIdCode(idcode, ack);
4. Clock and Delay Parameter (Key)
4.1 Meaning and Behavior of hz
SetClockHz(hz) sets the target SWCLK frequency.
hzis clamped to the range supported by this implementation (values below the minimum are raised; values above the maximum are lowered).- Whether the actual frequency matches the configured value depends on
loops_per_us, GPIO toggle speed, CPU load, etc. hz == 0forces the internal “no-delay path” (no BusyLoop delay is inserted). In this mode, the SWCLK frequency is determined by CPU and GPIO toggle speed and is typically “as fast as possible”.
4.2 What loops_per_us Means
loops_per_us is a delay calibration factor: the approximate number of BusyLoop iterations required per 1 microsecond.
It is not a universal constant and typically varies significantly with:
- CPU clock frequency
- Compiler optimization level (
-O0/-O2/-Os, etc.) - LTO and compiler version
- Instruction/bus wait states (on some MCUs, power/clock domains may also affect it)
Whenever any of the above changes, you should re-calibrate loops_per_us.
4.3 When the “No-Delay Path” Is Used
In practice, the implementation enters the “no-delay path” in two cases:
- You explicitly set
loops_per_us = 0; or - At a high enough
hz, the computed half-period delay is < 1 loop, so the internal half-period loop count becomes 0 and the implementation automatically switches to the no-delay path.
Implications of the no-delay path:
- Pros: lower overhead and higher throughput.
- Cons: SWCLK is no longer precisely controlled by
hz; it becomes “as fast (and as stable) as the platform allows”.
Engineering guidance:
- If you need a controllable and reproducible SWCLK, ensure the computed
half_period_loops_is clearly > 0 (i.e., calibrateloops_per_usproperly and avoid overly highhz). - If you only need it to work and be as fast as possible, you may set
loops_per_us = 0, or sethzhigh enough that the implementation naturally falls into the no-delay path.
5. Choosing and Calibrating loops_per_us
Two common calibration methods are provided below. The key principle: calibrate under the same compiler options and the same CPU frequency as your final firmware.
Method A: Calibrate with a Hardware Timer / Cycle Counter
Idea: run a BusyLoop with a known iteration count and measure elapsed time, then infer loops_per_us.
Steps:
- Choose a sufficiently large iteration count N (e.g., around 1e6) so measurement time is well above timer resolution.
- Record start time t0, run BusyLoop(N), record end time t1.
- Compute elapsed time
dt_usin microseconds. loops_per_us ≈ N / dt_us(integer rounding is fine).
Pros: independent of SWD/target state; after calibration, frequency control is predictable.
Method B: Fit by Measuring SWCLK with a Logic Analyzer / Oscilloscope
Idea: start with a rough loops_per_us, set a low-to-mid hz (e.g., 100 kHz or 500 kHz), measure SWCLK frequency, and adjust loops_per_us until it matches.
Steps:
- Start with a configuration that ensures the delayed path is used (i.e., choose a lower
hz). - Measure SWCLK frequency
f_meas. - Adjust proportionally:
loops_per_us_new ≈ loops_per_us_old * (f_meas / f_target), iterate 1–2 times to converge.
Pros: no need to use timer resources; GPIO toggle overhead is included in the fit.
Note: If you are already in the no-delay path (very high frequency), this method may stop working, because adjusting loops_per_us may not bring you back into the delayed path.
6. Practical Frequency Selection
Increase frequency gradually from conservative to aggressive, rather than starting at the limit:
- Initial bring-up: 100 kHz ~ 500 kHz (easier to debug wiring and signal integrity)
- After stable: try 1 MHz, 2 MHz, 4 MHz in steps
- For higher speeds: first confirm trace length, series damping, ground reference, and target pin drive capability; then increase frequency
If you see the following symptoms, it usually indicates frequency is too high or signal integrity is insufficient:
- ACK frequently becomes WAIT/FAULT/NO_ACK
- Parity failures on read data
- Instability only on certain cable lengths / certain boards
Recommended mitigation order:
- Lower frequency first;
- Verify 33Ω series resistors and SWDIO pull-up;
- Shorten the cable / improve grounding;
- Re-calibrate
loops_per_us(especially after changing compiler optimization options).
7. Close and Safe State
Calling Close() returns the probe pins to a safer state (useful when exiting debug or switching pin muxing):
- SWCLK driven high
- SWDIO switched to input with pull-up