build: flash the board without manual BOOT/RESET presses - #20
Merged
Conversation
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
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.
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
GPIO0andCHIP_PU(plus a 1–10 µF capacitoron EN), and esptool's
--before default_resetdrives 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 mode —
scripts/enter_bootloader.py, a pre-upload actionperforming 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 isbest effort — a failure is reported and the upload proceeds — so a board already
in download mode still flashes.
Leaving download mode — two parts:
espressif32@6.3.2pins, does not drive theUSB-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 documentsfor 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
RESETserial command, for restarting the board while the firmware isrunning rather than as part of flashing.
A note on esptool's dependencies
esptool 4.11.0 imports
intelhex. PlatformIO's tool manager normally installsthat along with the package — the CI run here does exactly that — but it skipped
them on an existing
~/.platformioduring development, and the build then failswith
ModuleNotFoundError: No module named 'intelhex'. If you hit that:The workflows install it explicitly for the same reason. Both are fallbacks, not
a standing requirement.
Testing
pio run -t uploadcycles complete with no button presses: the portgoes firmware CDC → download mode → firmware CDC, and the uptime counter
restarts from zero each time. Verified with both
hard_resetandwatchdog_reset.RESETverified against the uptime counter: the USB port drops andre-enumerates, and
Time since bootrestarts. Note that theRestarting...acknowledgement is not observable in practice — USB drops faster than the host
reads it — so watch for the port disappearing instead.
Not covered: entering download mode by hand with BOOT+RESET and then flashing.
watchdog_resetis precisely the documented fix for that case, but it was notexercised here.
🤖 Generated with Claude Code
https://claude.ai/code/session_014qWMQSRRkLjQs3fudZAWXh