Skip to content

Commit 68a34ea

Browse files
authored
Merge pull request #1034 from shorepine/hwci-chipid-reset
hwci: boot the AMYboard with an esptool chip-id reset (fixes the boot-wedge)
2 parents 34a81e0 + c0fa561 commit 68a34ea

1 file changed

Lines changed: 55 additions & 36 deletions

File tree

tulip/amyboard/hwci/hwci.py

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,25 @@ def flash(port, bin_path, baud):
7070
subprocess.run(cmd, check=True)
7171

7272

73+
def reset_via_chipid(port):
74+
"""Clean-boot the board with an esptool `chip-id`: it enters download mode,
75+
reads the chip id (returning the MAC), then hard-resets and releases the
76+
port. On this bench the flash's own `--after hard-reset` can leave the board
77+
wedged in a boot-loop, but a chip-id cycle reliably boots it. Returns True
78+
only if the MAC came back -- i.e. esptool actually completed the handshake
79+
and reset the board (a bare `chip-id` defaults to --before default-reset /
80+
--after hard-reset). Give the board a few seconds afterward to boot and
81+
enumerate USB-MIDI before looking for it (see --boot-settle)."""
82+
cmd = [sys.executable, "-m", "esptool", "--chip", "esp32s3", "--port", port, "chip-id"]
83+
print("[reset]", " ".join(cmd))
84+
r = subprocess.run(cmd, capture_output=True, text=True)
85+
sys.stdout.write(r.stdout)
86+
ok = r.returncode == 0 and "MAC:" in r.stdout
87+
if not ok:
88+
print("[reset] chip-id did not return a MAC -- board/dongle not responding")
89+
return ok
90+
91+
7392
# ── MIDI / sysex backends ───────────────────────────────────────────────────
7493
def alsa_find_midi():
7594
out = subprocess.run(["amidi", "-l"], capture_output=True, text=True).stdout
@@ -357,9 +376,12 @@ def main():
357376
ap.add_argument("--port", help="esptool serial port (the USB-serial dongle)")
358377
ap.add_argument("--baud", type=int, default=921600)
359378
ap.add_argument("--boot-attempts", type=int, default=3,
360-
help="flash+boot tries before failing. Retries the bench's "
361-
"intermittent early-boot flake (a real boot failure fails "
379+
help="chip-id reset + boot tries before failing. Retries the "
380+
"bench's intermittent boot-wedge (a real boot failure fails "
362381
"every try).")
382+
ap.add_argument("--boot-settle", type=float, default=5.0,
383+
help="seconds to wait after a chip-id reset for the board to "
384+
"boot and enumerate USB-MIDI before looking for it.")
363385
ap.add_argument("--no-flash", action="store_true")
364386
ap.add_argument("--flash-only", action="store_true")
365387
ap.add_argument("--midi-port", help="ALSA hw:X,Y or mido name (auto if omitted)")
@@ -395,53 +417,50 @@ def main():
395417
sys.exit("--port (the USB-serial dongle) is required to flash.")
396418
bin_path, tmp = resolve_firmware(args) if not args.no_flash else (None, False)
397419

398-
# The debug UART (the dongle on the board's debug header) is a stable hardware
399-
# UART carrying the ESP-IDF console; opening it pulses DTR/RTS through the
400-
# board's auto-reset circuit, so the board reboots as we connect — which is
401-
# exactly how we capture a clean boot log. The native CDC, by contrast,
402-
# re-enumerates during boot (ROM USB-Serial-JTAG -> TinyUSB CDC), so we start
403-
# tailing it only *after* the board is back up, for clean MicroPython stdout.
420+
# We deliberately do NOT hold the debug UART open while the board boots: the
421+
# proven-good reset on this bench is `esptool chip-id` (hard-reset + release
422+
# the port), wait a few seconds, then `amidi -l` -- holding the dongle's
423+
# DTR/RTS lines across boot is what left the board wedged in a boot-loop. The
424+
# consoles are tailed only *after* MIDI is up.
404425
debug_port = args.debug_port or args.port or ("/dev/ttyACM1" if ALSA else None)
405426
loggers = []
406427
try:
407-
# Flash + boot, retrying the bench's intermittent early-boot flake. The
408-
# firmware is fine — its 2nd-stage bootloader is byte-identical to builds
409-
# that boot — but a transient flash-read/power/reset glitch after the long
410-
# USB flash can panic-loop the board before USB-MIDI enumerates. Re-flashing
411-
# forces ROM download mode (a known-good reset) and usually clears it; a
412-
# genuinely broken build instead fails on every attempt.
428+
# Flash once, then bring the board up with a clean esptool chip-id reset
429+
# cycle. A plain flash hard-reset can leave it wedged in a boot-loop on
430+
# this bench; a chip-id cycle reliably boots it. Retries do the (cheap)
431+
# reset again, not a re-flash -- the firmware is already written + verified.
432+
if not args.no_flash:
433+
flash(args.port, bin_path, args.baud)
434+
if args.flash_only:
435+
print("[done] flashed."); return 0
413436
for attempt in range(1, args.boot_attempts + 1):
414-
if not args.no_flash:
415-
flash(args.port, bin_path, args.baud)
416-
if args.flash_only:
417-
print("[done] flashed."); return 0
418-
dbg = None
419-
if not args.no_serial_log:
420-
label = "DEBUG UART (ESP-IDF console / stderr)"
421-
if args.boot_attempts > 1:
422-
label += f" [boot {attempt}/{args.boot_attempts}]"
423-
dbg = start_serial_log(debug_port, args.debug_baud, label)
424-
if dbg:
425-
loggers.append(dbg)
426-
time.sleep(2.0) # let the connect-reset drop the old USB enumeration
437+
if args.port:
438+
reset_via_chipid(args.port)
439+
time.sleep(args.boot_settle) # board boots + USB-MIDI enumerates
427440
args.midi_port = args.midi_port or wait_for_board()
428441
if args.midi_port:
429442
break
430-
# Boot failed: stop this attempt's logger so esptool can drive the
431-
# dongle's reset lines, then retry. The crash log stays in `loggers`.
432-
if dbg:
433-
dbg.stop()
434-
time.sleep(0.5) # let the OS release the port before re-flashing
435443
if attempt < args.boot_attempts:
436-
print(f"[boot] attempt {attempt}/{args.boot_attempts} failed; "
437-
"re-flashing and retrying")
444+
print(f"[boot] attempt {attempt}/{args.boot_attempts}: MIDI not up; "
445+
"re-resetting (chip-id) and retrying")
438446
else:
447+
# Real boot failure -- grab a short debug-UART snapshot so it's still
448+
# debuggable (the only time we open the dongle before MIDI is up).
449+
if not args.no_serial_log:
450+
snap = start_serial_log(debug_port, args.debug_baud,
451+
"DEBUG UART (post-failure snapshot)")
452+
if snap:
453+
loggers.append(snap)
454+
time.sleep(3.0)
439455
sys.exit(f"[boot] board never enumerated MIDI after {args.boot_attempts} "
440-
"attempt(s) — bench boot flake that didn't clear, or a real "
441-
"boot failure (see serial log).")
456+
"chip-id reset attempt(s) — see serial log.")
442457

443458
time.sleep(1.5) # let the MIDI endpoint + synth settle post-boot
444459
if not args.no_serial_log:
460+
# CDC is the board's own USB (no reset lines), so opening it mid-run
461+
# can't perturb the board or the recording. We skip the debug UART
462+
# here for the same reason -- its log only matters on boot failure
463+
# (captured in the snapshot above).
445464
cdc = start_serial_log(args.cdc_port, 115200, "CDC (MicroPython stdout)")
446465
if cdc:
447466
loggers.append(cdc)

0 commit comments

Comments
 (0)