Skip to content

Stm32u5 driver - #3

Open
mikh3x4 wants to merge 18 commits into
masterfrom
stm32u5-driver
Open

Stm32u5 driver#3
mikh3x4 wants to merge 18 commits into
masterfrom
stm32u5-driver

Conversation

@mikh3x4

@mikh3x4 mikh3x4 commented Aug 7, 2026

Copy link
Copy Markdown

No description provided.

lzhang219 and others added 18 commits July 10, 2026 04:53
…ealing)

Root cause (bench-characterized) of unreliable STM32U5 flashing over a long
USB-C cable: the SWD signals ride AC-coupled USB-C pins (SuperSpeed pairs carry
series coupling caps), making the link a BANDPASS channel, not the low-pass a
long wire is assumed to be:
  - high-pass (AC coupling): reads FAIL <= ~2 MHz, so lowering the clock -- the
    intuitive "long cable" fix -- makes it WORSE;
  - low-pass (cable RC): high-toggle data corrupts at 16 MHz (0xAA/random 100%);
  - a reflection NOTCH at ~9 MHz.
Usable passband ~3-8 MHz; 8 MHz measured lowest glitch rate. Rare in-band line-
noise glitches (bursty, sub-ms to seconds) desync the DP into an ack=7 latch.

Fix, layered so each handles a different failure scale:
- dap.c: latch-free transfers. A glitched transfer no longer aborts -- both the
  single-transfer (dap_transfer) and bulk (dap_block_xfer) paths re-sync the
  link and retry, draining in-flight responses to stay framed and re-writing
  CSW+TAR after. Recovery re-syncs at 2 MHz then restores the flash clock (the
  host-driven reset sequence survives slow where an 8 MHz re-sync can't clear a
  wedge on an AC-coupled cable) with exponential backoff to ride out bursts.
  Makes edbg self-recovering -- no external unwedge helper needed.
- target_st_stm32u5.c: self-healing verify. On a CRC mismatch (a silent in-band
  bit-flip that ack'd OK; compression amplifies one bad byte to a whole page)
  the differential program re-runs -- reprogramming only the still-wrong pages
  -- and re-verifies, before falling to the readback diagnostic.
- edbg.c: top-level reconnect + differential re-run. A failure during
  program/verify unwinds (setjmp) to a fresh reconnect and re-run rather than
  dying -- the robust way to outlast a multi-second burst, converging because
  the differential scan skips already-correct pages.

Env knobs: EDBG_BLOCK_RETRIES (8), EDBG_FLASH_RETRIES (8). Clean-cable behavior
is unchanged. Bench (F119 -> b4 over the AC-coupled cable): ~20% -> ~70-90%
one-shot at 8 MHz under heavy noise, ~2.3 s when clean; residual failures are
sustained EMI bursts (a hardware limit). Optimal invocation: -c 8000.
SWD over AC-coupled USB-C SuperSpeed pins is a bandpass channel: reads fail LOW
and pass HIGH (so lowering the clock is backwards), fast data corrupts at 16MHz,
a reflection notch mid-band; passband ~3-8MHz, 8MHz optimal. Diagnosis by the
read clock-response, and the driver's self-recovery approach.
…retry

Two gaps found soaking over the AC-coupled cable:

- After a failed flash the DP can be hard-wedged, and a single link reset at the
  flash clock does NOT clear it (the reset is a sustained-high level the AC
  coupling attenuates). target_select() now repeats the link reset slow (2 MHz)
  a few times before use -- the same thing the standalone unwedge helper does,
  and why it recovered where edbg's connect used to give up. Verified: identify
  self-recovers a wedged DP with no external helper (0/3 -> 4/4). Harmless on a
  clean cable (one extra slow reset).

- The top-level flash retry only wrapped program/verify, so a transient
  connect/select failure was still fatal. It now wraps select() too: a failure
  anywhere in the flashing sequence reconnects and re-runs.

Note: the reset-catch (SYSRESETREQ + vector-catch halt) is timing-critical and
remains the most noise-sensitive step -- under a sustained EMI burst it can fail
where steady streaming succeeds. That is a hardware limit of the cable.
… (opt-in)

dap_probe_clock() tests candidate clocks (fastest-first: 16/12/10/8/6 MHz) with
a small RANDOM-data write+readback to a scratch SRAM word and returns the
fastest that transfers cleanly. Random data is deliberately broadband so it
exercises the AC-coupling high-pass and cable-RC low-pass together -- a clock
that would pass a constant-register read yet corrupt real flash data (16 MHz on
an AC-coupled cable) is correctly rejected. Non-fatal (uses dap_block_attempt,
which returns a bool); the DP is re-synced at 2 MHz between candidates in case a
bad clock wedged it. "Highest clean clock" selection gives 16 MHz on a clean
cable, the passband on an AC-coupled one, low on a resistive one, and can never
land on the ~9 MHz reflection notch (not a candidate).

target_select() calls it when EDBG_U5_AUTO_CLOCK is set, so the default 16 MHz
(which corrupts an AC-coupled long cable) is replaced automatically with no user
tuning. Opt-in for now; make it default / have oc set it once validated.

CAVEAT: compiles clean but UNTESTED on hardware -- the bench went offline before
I could run it. Needs a bench check that it prints "clock probe 16 MHz: dirty"
down to a clean one and then flashes, on both the AC-coupled cable and a clean
cable. Opt-in, so default flashing behavior is unaffected meanwhile.
…hatches

- Move the clock probe to AFTER the reset-catch halts the core. The earlier
  core-running probe raced the app (which overwrites the scratch SRAM word), so
  every candidate looked "dirty"; halting first makes the scratch stable.
- Bring the link up at 8 MHz under auto-clock so the connect doesn't fail at the
  harmful 16 MHz default before the probe can lower it.
- EDBG_U5_GENTLE_HALT: attach-and-halt instead of the reset-catch (nRESET +
  SYSRESETREQ + vector-catch) -- the reset-catch is the timing-critical step
  that a marginal cable breaks while steady traffic works.
- EDBG_NO_DORMANT: skip the ADIv5.2 dormant alert in the link reset (a corrupted
  alert on a marginal cable can knock a healthy DP into a bad state).
- Recovery re-syncs at 4 MHz (2 MHz sits at/below the AC-coupling corner).

CAVEAT: probe LOGIC verified running on hardware (it probed all candidates), but
end-to-end validation was blocked -- the bench board+cable degraded mid-session
into a state where edbg's connect fails consistently (the committed known-good
build fails identically) though a raw non-reset attach still streams cleanly. So
the reset-catch/connect on a marginal cable is the real bottleneck; the gentle-
halt and dormant escapes target it but need a stable bench to confirm. All new
paths are opt-in/gated, so default behavior is unchanged.
… clear WDATAERR

Root cause (found by bisecting edbg's connect against a raw-HID script that
worked on the same board+cable): a write's data phase over a long/AC-coupled
USB-C cable occasionally picks up a parity glitch, latching CTRL/STAT.WDATAERR.
While WDATAERR is set the DP FAULTs *every* write until cleared via
ABORT.WDERRCLR. dap_reset_link's ABORT mask was the classic
STKCMPCLR|STKERRCLR|ORUNERRCLR (0x16) — missing WDERRCLR — so recovery re-synced
the link but never cleared WDATAERR, and one glitched write wedged the DP
forever ("cable may be unusable"). Cable-length dependent only because a short
cable rarely sets WDATAERR. Bench: CTRL/STAT=0xf00000c0 after a glitch;
ABORT=0x16 -> FAULT (WDATAERR stays); ABORT=0x1e/0x1f -> OK (cleared).

Fixes:
- dap_reset_link ABORT now clears ALL sticky flags incl. WDERRCLR, plus DAPABORT
  to drop any stuck AP transaction. This un-wedges the DP so retry/recovery
  actually works.
- stm32u5 target_select defaults to 8 MHz when -c is not given (g_clock_explicit
  tracks the flag). edbg's 16 MHz global default is in this cable's stop-band:
  bulk writes measured 0/8 full flashes at 16 MHz vs 10/10 at 8 MHz. Safe on a
  short cable too (~1.6 s for a full ~0.5 MB flash+verify).
- Remove the earlier band-aids that were chasing the wrong theory: the 6x 4 MHz
  self-recovery loop, the env-gated auto-clock probe (dap_probe_clock), and the
  EDBG_U5_AUTO_CLOCK plumbing. The WDERRCLR fix + 8 MHz default supersede them.
- EDBG_DEBUG_XFER traces every transfer and dumps the failing batch on give-up.

Verified on the bench (F119 mux2, long cable): 10/10 full-write flash+verify via
the exact command oc runs (edbg -bpvi, no -c), ~1.5 s each, self-healing on the
occasional glitch. Previously the connect alone failed 0/many.
…an at 16 MHz)

My previous commit set the 8 MHz default inside target_select(), but on the flash
path (-bpvi) identify() runs BEFORE select(), so the identify connected at the
16 MHz global default and failed on a long cable while the later select() never
got the chance to drop the clock. Caught on DEV1, the actual long-cable board:
no -c was 0/N (identify failed at 16 MHz) while explicit -c 8000 was N/N.

Fix: give target_ops_t a `default_clock` field; stm32u5 sets 8 MHz; main() applies
it to g_clock right after the target is resolved, before reconnect_debugger() and
the first connect. Now identify + select + program all run at 8 MHz when the user
gives no -c. Other targets leave default_clock 0 (unchanged). Removes the
target_select()-local clock override and the g_clock_explicit extern there.

Verified on DEV1 (F119 mux1, long cable, prints "Clock frequency: 8.0 MHz"):
16 MHz full-write 0/4 (storms — confirms long cable), no-c 8 MHz default 8/8 +
4/4, board boots. This is the path oc uses (edbg -bpvi, no -c).
…rges

8 MHz clears this long cable first-try, but a longer/noisier one could storm
there too. Before, that failed loudly (no wedge, thanks to WDERRCLR, but the
flash gave up and you had to hand-pick a lower -c). Now the top-level flash
retry steps the clock down when it keeps failing:

- Ride the current clock for the first retry or two -- a transient noise burst
  recovers at the same speed via the existing wait + differential rerun.
- If it KEEPS failing (a genuinely worse cable, not a burst), drop the clock
  x3/4 every other retry, floored at 3 MHz (below ~3 MHz an AC-coupled USB-C
  cable's high-pass starts killing reads, so lower would hurt). The differential
  flash keeps pages already written, so each lower-clock rerun only reprograms
  what's still wrong -- convergent, not a full redo.

Also move identify() into the retry region for the flash path, so the same
reconnect + step-down that protects programming covers the Core ID read too
(it previously ran once, outside the retry region, and a bad starting clock
aborted the whole run before any retry).

Verified on DEV1 (long cable): normal 8 MHz default succeeds first-try 6/6 (no
step-down); forced -c 16000 rides 16, steps to 12 MHz, and completes CRC-verified
(EXIT 0) via the differential rerun -- proving the mechanism recovers a too-high
start clock end to end.
…eck, min-clock floor; add swd_diagnose

Prepared theory-fixes for the remaining long-wire failures on frogs still
running the 2021 fp2-dap firmware (fleet-wide except F119/F124). UNTESTED on
hardware -- bench validation matrix in scripts/swd_diagnose.py; do not vendor
until validated.

- dap_recover(): re-issue DAP_Connect before the line reset. On 2021 probe
  firmware a garbage ACK leaves SWDIO as INPUT (protocol-error branch never
  restores drive; its SWJ_Sequence only sets the latch), so our recovery's
  line reset was clocked into a floating wire and could never work.
  ID_DAP_CONNECT is the one command that re-runs DAP_CONFIG_CONNECT_SWD and
  restores drive. Harmless on fixed firmware; host<->probe only.

- dap_reset_link(): poll CTRL/STAT until CDBGPWRUPACK|CSYSPWRUPACK before
  returning (ADIv5 requirement we always skipped). The very next op is an AP
  write batch; an AP access to an unpowered debug domain is UNPREDICTABLE and
  on a marginal wire yields the garbage ACK that wedges 2021 firmware.
  Affects all targets (spec-correct for any Cortex-M DP); skipped in recovery
  mode where responses are discarded.

- target_select(stm32u5): after asserting nRESET, read the pin back via new
  dap_read_pins(); warn if it stays high (reset line not wired on the port ->
  reset-catch silently degrades to poking the running app, which fails in
  app-specific ways -- WFI sleep gating AP access -- that look like a bad cable).

- step-down floor: EDBG_MIN_CLOCK (kHz) overrides the 3 MHz default; purely
  resistive long wires keep improving below 3 MHz (bench: clean reads at
  500 kHz) while AC-coupled ones do not -- the floor is now per-rig tunable.

- scripts/swd_diagnose.py: raw-hidraw one-shot discriminator for the open
  theory set (T-RESET / T-SLEEP / T-BATCH / T-CFG / T-PWRUP / T-WEDGE) so the
  next bench session validates each theory in minutes.
…reached

The 3 MHz floor was set for AC-coupled cables (high-pass kills reads below
~3 MHz) but the long cables that actually fail in the field are resistive --
reads stay clean well below 1 MHz (bench: 500 kHz). Since the step-down always
tries the higher clocks first and only continues after a failure, going lower
can only help; a pure AC-coupled cable just fails the sub-3 MHz tries
harmlessly after the good clocks were already tried.

- Floor default 3 MHz -> 1 MHz (EDBG_MIN_CLOCK kHz still overrides).
- Step *0.75 EVERY retry after the first (was every other): the old cadence
  only reached ~2.5 MHz within the retry budget and never actually hit the
  floor. First retry still re-tries the start clock to ride a transient burst.
- Default flash retries 8 -> 10 so the 8 MHz start walks all the way down:
  8 -> 6 -> 4.5 -> 3.4 -> 2.5 -> 1.9 -> 1.4 -> 1.1 -> 1.0 MHz.

Bench-verified (no-target descent through F119): reaches exactly 1000 kHz.
The frog's DAP_Vendor0 handler clears GPNVM1 and resets into the SAM-BA ROM
bootloader, so bossac can reflash it over its own USB -- no SWD jig, no helper
frog. Fire-and-forget submit (the frog resets before it can reply); works on
Linux (hidraw) and Mac (hidapi) via the shared dbg_dap_cmd_submit path.

Factor debugger selection into select_debugger() so the DFU path (which needs
no target/clock/SWD connect) can reuse it.
New frog firmware requires the DFU vendor command to carry the frog's own
serial, so a stray/fuzzed 1-byte 0x80 can't drop a probe into SAM-BA and a
broadcast 0x80 can't take out a whole bench. Old firmware ignores the payload,
so this is backward-compatible with the current fleet.
…alt)

Host-side RTT reader for tacoma-over-SWD. Connects (reconnect_debugger, DP power-up)
but never select()/halts, so it reads/writes the target's ring buffers via the
existing MEM-AP primitives (dap_read/write_word/block) while the core runs. Pumps
the up-ring to stdout and stdin to the down-ring; word-aligned reads + byte extract
for the byte-granular ring, non-blocking stdin, id-validated so a wrong --rtt-addr
fails loud. Fault recovery is by supervision: a SWD fault error_exits, the
supervisor respawns us -> reconnect (re-attach after a target OTA reset / marginal
cable). Wrap in a PTY and tacoma opens it as a serial device, unchanged.

Usage: edbg -t <target> -s <frog> --rtt --rtt-addr <_SEGGER_RTT from the ELF>.
Reuses all of edbg's long-cable recovery. Compile-clean; needs a target to exercise.
…LF needed)

Previously `--rtt` required `--rtt-addr <_SEGGER_RTT>` (from the board ELF/.map).
Add a scan path so `--rtt` works with neither an ELF nor an address: when
`--rtt-addr` is absent, scan the target's SRAM for the 16-byte "SEGGER RTT\0..."
id string and validate the candidate before bridging.

How it works (matches probe-rs / OpenOCD rtt.c):
  - rtt_scan() reads the range in 8 KB word-aligned chunks (via the fast
    dap_block_read TransferBlock path) with a 16-byte overlap so an id that
    straddles a chunk boundary is still seen, and byte-scans each chunk for the
    full 16-byte acID (literal "SEGGER RTT" + 6 NULs -- stricter than the
    fast-path's 10-byte compare, fewer false positives).
  - rtt_validate_cb() rejects false hits (the id bytes appearing in ring data):
    MaxNumUp in 1..16 / MaxNumDown in 0..16, up-ring SizeOfBuffer non-zero and
    <=1 MB, WrOff/RdOff < SizeOfBuffer, and pBuffer/size inside the target RAM
    window. Only the first validated hit is used.

Scan range comes from the selected target driver's new ram_start/ram_size fields
(target_ops_t), populated for the parts we drive over SWD:
  - stm32u5  : 0x20000000, 768 KB (U575/U585 contiguous SRAM1+2+3; b4's part)
  - atmel_cm4v2 (SAM D5x/E5x): 0x20000000, 256 KB
  - atmel_cm7 (SAM E7x/S7x/V7x): 0x20400000, 384 KB
New flags --rtt-scan <addr> / --rtt-scan-len <bytes> override the range (needed
for U5 variants with different SRAM, or a target with no ram map). --rtt-addr
stays the fast path (one read, no scan).

The literal-match scan is correct for our target because the ring uses a
statically-initialized `.acID = "SEGGER RTT"` that the C runtime copies into
.bss/.data before main(), so the full id is always present in RAM -- SEGGER's
"write the 'S' last" half-init guard does not apply here.

Build-verified only: compiles warnings-clean natively on macOS (clang -Wall
-Wextra), edbg.c and all modified target*.c objects clean. NOT yet run against a
target -- the scan/validation/bridge paths need a board with the RTT ring on the
bench (unavailable ~2 days) to confirm end-to-end.
Host-side memory snapshot through the CMSIS-DAP probe, reusing the
existing fast block-read path (dap_block_read: 1KB TAR-boundary
segmentation + retry + dap_recover link re-sync, long-cable hardened).
No frog firmware change.

  --memdump   read [<-o offset>, +<-z size>) of target memory and write
              it raw to -f <file>. Attaches over the live link WITHOUT
              resetting the target (mirrors the gentle MEM-AP attach the
              stm32u5 driver uses; connect + reset_link, no reset-catch,
              no select()). Returns directly instead of the normal exit
              path so disconnect_debugger()'s nRESET pulse never fires.

  --freeze    halt the core across the read and resume ASAP, for a
              coherent snapshot (no torn multi-word reads):
                1. read DHCSR; if S_HALT is already set (a gdb session
                   stopped it) dump as-is and never resume -- resuming a
                   core someone else deliberately stopped would be wrong.
                2. else write DHCSR = 0xa05f0003 (DBGKEY|C_DEBUGEN|C_HALT)
                3. dap_block_read the region
                4. write DHCSR = 0xa05f0000 (DBGKEY only; no C_MASKINTS)
                5. write DP ABORT = 0x1e to clear any sticky
              The halt->resume window is measured and printed to stderr.

Live mode (no --freeze) does steps 1/3/5 only -- non-intrusive MEM-AP
reads while the core runs -- and warns that the result is not a coherent
snapshot. The read window is word-aligned outward and sliced back to
exactly `size` on output. Honors EDBG_BLOCK_RETRIES via dap_block_read.

Adds dap_write_abort() (raw DP ABORT write, mirrors the ABORT in
dap_reset_link) so memdump can clear stickies through the public API.

Freeze-window ballparks (USB-round-trip-bound, NOT SWD-bound; MUST be
validated on hardware): 1KB ~1-3ms, 16KB ~15-40ms, 256KB SRAM ~0.3-0.7s.
Do NOT --freeze large regions -- the target IWDG is not fed while halted.

Builds clean native (make); the sole warning (dap.c:524) is pre-existing.
Opt-in (env var EDBG_CUR): hold nRESET low while attaching to the DP -- which
lives in the always-on debug power domain and answers with the core in reset --
arm the reset vector-catch, then release nRESET so the core halts at the reset
vector before running an instruction. The way in when resident firmware wedges
the SWD debug port or won't let a live attach halt it. Added during an F127
frog rescue; wasn't the fix that time (the real issue was a stale mux) but it's
the right tool to keep for a genuinely wedged target.
Adds a --v2 option that talks to the frog vendor bulk interface (CMSIS-DAP
v2) over usbfs instead of the HID interrupt endpoints. Raw DAP packets, no
HID report byte, higher throughput; needs no libusb (usbfs BULK/CLAIM
ioctls, matching the existing hidraw ioctl style). Enumeration is shared --
every frog advertises CMSIS-DAP on its HID interface -- so only open,
dap_cmd and close switch on dbg_use_v2. Frog v2 layout: interface 3,
EP6 OUT / EP7 IN, 512-byte bulk.

- dbg_dap_cmd_v2: bulk OUT (+ terminating ZLP when the request is an exact
  multiple of 512), bulk IN, strip the command echo like the HID backend.
- submit/reap: the frog is one-packet-in-flight, so queue submitted requests
  FIFO and do the bulk round-trip at reap.
- Linux-only for now (usbfs); dbg_use_v2 is defined in edbg.c so the mac/win
  backends still link.

Bench-validated (F127 v2 frog -> Dev0 -> b4 STM32U5): --v2 --identify byte-
identical to HID (same OPTR, Core ID); 16 KB flash read over v2 md5-identical
to the HID read; full DAP handshake + multi-packet SWD transfers over bulk.
Only 3 of 11 target drivers implement identify(); the other 8 (incl.
atmel_cm7 / SAM E7x) leave it NULL. edbg called it unconditionally for
--identify, so -i segfaulted on those targets right after select() had
already printed the device name. Guard the call.
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants