Stm32u5 driver - #3
Open
mikh3x4 wants to merge 18 commits into
Open
Conversation
…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.
|
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.