Skip to content

build: flash the board without manual BOOT/RESET presses - #20

Merged
DriftKingTW merged 6 commits into
masterfrom
feat/hands-free-flashing
Aug 30, 2026
Merged

build: flash the board without manual BOOT/RESET presses#20
DriftKingTW merged 6 commits into
masterfrom
feat/hands-free-flashing

Conversation

@DriftKingTW

@DriftKingTW DriftKingTW commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Flashing this board took two button presses: hold BOOT and tap RESET to enter
download mode, then tap RESET again afterwards to leave it. Both are fixable in
software, and both fixes are the ones the ecosystem prescribes.

Why the board needs this at all

Espressif's own boards carry a two-transistor auto-reset circuit that ties the
USB-UART bridge's DTR and RTS to GPIO0 and CHIP_PU (plus a 1–10 µF capacitor
on EN), and esptool's --before default_reset drives exactly that circuit.

This board has no bridge — it runs the S3's native USB (ARDUINO_USB_MODE=0) —
so there is nothing for DTR/RTS to reach, and esptool cannot reboot it in either
direction. That is a property of the hardware design, not a misconfiguration.

What changed

Entering download modescripts/enter_bootloader.py, a pre-upload action
performing the 1200bps touch: opening the CDC port at 1200 baud and dropping DTR
makes the Arduino core restart the chip into the ROM downloader. This is the
same mechanism the Arduino ecosystem uses for every native-USB board, and it is
implemented in the core we already build against. The script waits for the
device to re-enumerate and hands the new port to esptool as UPLOAD_PORT. It is
best effort — a failure is reported and the upload proceeds — so a board already
in download mode still flashes.

Leaving download mode — two parts:

  • esptool 4.5.1, which espressif32@6.3.2 pins, does not drive the
    USB-Serial-JTAG reset. 4.11.0 does. PlatformIO was already passing
    --before usb_reset --after hard_reset; only the implementation was at fault.
  • board_upload.after_reset = watchdog_reset, which is what Espressif documents
    for this peripheral. The USB-Serial-JTAG can only raise a core reset, and a
    core reset does not re-sample the boot strapping pin — so a board put into
    download mode with the BOOT button stays there even after the pin is released.
    A watchdog reset is a full system reset and re-samples it. Upstream does not
    enable this automatically, so it is set per-board here.

A RESET serial command, for restarting the board while the firmware is
running rather than as part of flashing.

A note on esptool's dependencies

esptool 4.11.0 imports intelhex. PlatformIO's tool manager normally installs
that along with the package — the CI run here does exactly that — but it skipped
them on an existing ~/.platformio during development, and the build then fails
with ModuleNotFoundError: No module named 'intelhex'. If you hit that:

~/.platformio/penv/bin/pip install intelhex

The workflows install it explicitly for the same reason. Both are fallbacks, not
a standing requirement.

Testing

  • Repeated pio run -t upload cycles complete with no button presses: the port
    goes firmware CDC → download mode → firmware CDC, and the uptime counter
    restarts from zero each time. Verified with both hard_reset and
    watchdog_reset.
  • RESET verified against the uptime counter: the USB port drops and
    re-enumerates, and Time since boot restarts. Note that the Restarting...
    acknowledgement is not observable in practice — USB drops faster than the host
    reads it — so watch for the port disappearing instead.
  • CI green on this branch, building with the new esptool.

Not covered: entering download mode by hand with BOOT+RESET and then flashing.
watchdog_reset is precisely the documented fix for that case, but it was not
exercised here.

🤖 Generated with Claude Code

https://claude.ai/code/session_014qWMQSRRkLjQs3fudZAWXh

DriftKingTW and others added 6 commits August 30, 2026 15:29
This board runs the ESP32-S3's native USB (ARDUINO_USB_MODE=0), which has no
auto-reset circuit: DTR and RTS are plain CDC line-state bits here, not lines
wired to EN/IO0 through a USB-UART bridge. Nothing on the host can restart the
board, so every reboot means reaching for the physical button.

Handle "RESET" alongside the existing serial commands and call ESP.restart(),
flushing first so the acknowledgement is not lost to the reboot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qWMQSRRkLjQs3fudZAWXh
Flashing this board meant holding BOOT and tapping RESET by hand, because the
S3's native USB has no auto-reset circuit for esptool's --before default_reset
to drive.

The Arduino core implements the "1200bps touch" in software instead: opening
the CDC port at 1200 baud and dropping DTR restarts the chip into the ROM
downloader. Do that in a pre-upload action, then wait for the device to
re-enumerate and hand the new port name to esptool as UPLOAD_PORT.

The touch is best effort -- a failure is reported and the upload proceeds
unchanged -- so a board already sitting in download mode still flashes. Leaving
the downloader afterwards still needs a RESET press; that side has no software
path on this hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qWMQSRRkLjQs3fudZAWXh
espressif32@6.3.2 pins esptool 4.5.1, whose hard reset drives RTS. This board
runs the S3's native USB, which has no auto-reset circuit for RTS to reach, so
every flash ended with the board parked in the ROM downloader waiting for a
RESET press -- the other half of the problem the pre-upload touch solved.

esptool 4.11.0 drives the USB-Serial-JTAG reset correctly and the app starts on
its own, verified over repeated flash cycles. Together with the 1200bps touch
the whole cycle now runs without touching the board.

PlatformIO does not install esptool's dependencies, so the new version needs
`intelhex` in its Python environment; the requirement is noted next to the pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qWMQSRRkLjQs3fudZAWXh
The esptool version pinned in platformio.ini imports intelhex, and PlatformIO
does not install esptool's dependencies. esptool runs during a plain `pio run`
(elf2image), and release additionally calls it through merge_firmware.py, so
both workflows fail with ModuleNotFoundError without this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qWMQSRRkLjQs3fudZAWXh
The CI run showed PlatformIO's tool manager installing intelhex, reedsolo and
the rest of esptool's dependencies by itself, so the earlier claim that it never
does was wrong and would send people chasing a manual install they do not need.

It did skip them on an existing ~/.platformio during development, which is what
prompted the note, so keep the workaround and the CI install as a fallback --
just describe them as such.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qWMQSRRkLjQs3fudZAWXh
Espressif documents this case for the USB-Serial-JTAG: the peripheral can only
raise a core reset, and a core reset does not re-sample the boot strapping pin.
A board put into download mode with the BOOT button therefore stays there after
flashing, because the pin still reads low even once released.

A watchdog reset is a full system reset, so the strapping pin is re-sampled and
the app boots. The software path into download mode never latches that pin and
already worked with a plain hard reset; this covers the manual path too.

Not enabled by default upstream, so it is set per-board here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qWMQSRRkLjQs3fudZAWXh
@DriftKingTW DriftKingTW changed the title Flash the board without touching it build: flash the board without manual BOOT/RESET presses Aug 30, 2026
@DriftKingTW
DriftKingTW merged commit abec185 into master Aug 30, 2026
1 check passed
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.

1 participant