- New esp32-s3-supermini board profile: ESP32-S3FH4R2, 4MB flash, 2MB in-package PSRAM, WS2812 on GPIO48, native USB-Serial/JTAG. Covers 35 exposed GPIOs; USB, flash/PSRAM bus and BOOT stay untouched. - Bundle rebuilt S3 images plus an Xtensa sdkconfig defaults file, and teach the build/idf helpers about the esp32s3 target. - BOOT button is now opt-in via --confirm-button and reports SKIP otherwise, so a missed press can no longer fail a healthy board. The wait is split into short windows to survive a dropped serial request. - ADC check biases each pad with the internal pull-up/pull-down and requires the reading to move, replacing the flaky 0/4095 heuristic. - Fix BOOT pin configuration (pull-up only) so presses are detected. - Add `j probe` bring-up helper for mapping header pads. - Refresh firmware images for C6 and P4; extend tests and README.
espcheck — end-to-end ESP32 board validator
One command finds a supported board, confirms the chip and factory fuse state,
flashes a board-specific self-test firmware, runs every on-chip test, and
optionally asks you to press the BOOT button (--confirm-button). You get a
single pass/fail table — no serial spew. Only a USB cable is needed; no
headers, jumpers or external peripherals.
The project was previously
c6check; the CLI is nowespcheckand picks the board profile from the chip it detects.
Install
Requires uv and Python 3.10 or newer. Run it
straight from the repository, without cloning:
uvx --from git+https://git.arc-systems.org/ARCS/esp32-diagnostics espcheck
Or clone it for development:
git clone https://git.arc-systems.org/ARCS/esp32-diagnostics
cd esp32-diagnostics
uv run espcheck
uv run creates the virtualenv and installs esptool, pyserial and rich
automatically.
The board is detected from the chip identity, so the same command works for every board in the registry:
| Profile | Board | Chip | Flash | PSRAM | Radios | Programming port |
|---|---|---|---|---|---|---|
esp32-c6-supermini |
ESP32-C6 Super Mini | ESP32-C6FH4 (QFN32) | 4 MB | — | Wi-Fi 6, BLE, 802.15.4 | native USB-Serial/JTAG |
esp32-p4-pico |
Waveshare ESP32-P4-Pico | ESP32-P4 | 32 MB | 32 MB | none | CH343P USB-UART |
esp32-s3-supermini |
ESP32-S3 Super Mini | ESP32-S3 (QFN56) | 4 MB | 2 MB | Wi-Fi, BLE | native USB-Serial/JTAG |
uv run espcheck --list-boards # show the registry and exit
uv run espcheck --list # show serial ports and which board they look like
uv run espcheck -b p4 # force a profile; the chip must still match
-b accepts the full slug or an alias such as c6, p4, s3, p4-pico or
esp32s3. Without -b, the profile is chosen from the detected chip and flash
size. If no profile matches, nothing is flashed and the tool explains what it
found.
Example: ESP32-P4-Pico
╭───────────────────────────────────────────────╮
│ ESP32-P4-Pico │
│ ESP32-P4 v3.2 · /dev/cu.usbmodem5C931270381 │
╰───────────────────────────────────────────────╯
Check Result Details
HARDWARE
USB port ✓ PASS /dev/cu.usbmodem5C931270381 USB Single Serial
Chip ✓ PASS ESP32-P4 v3.2
Flash ✓ PASS 32MB
Base MAC ✓ PASS 80:f1:b2:d7:f8:00
Security fuses ✓ PASS secure boot off, flash encryption off, no locks
Firmware image ✓ PASS 3 image(s) written
Test firmware ✓ PASS esp32-p4-pico / v6.1
MAC cross-check ✓ PASS firmware and bootloader agree
Radios ✓ PASS none - no radio hardware
PSRAM ✓ PASS 32 MB (expected 32 MB)
ON-CHIP TESTS
GPIO pads ✓ PASS 27/27 pads respond
ADC1 (GPIO16-23) ✓ PASS 8/8 channels, calibrated
Temperature sensor ✓ PASS 31.7 °C
I2C bus (SDA7/SCL8) ✓ PASS ES8311 codec (0x18)
MANUAL CHECKS
BOOT button (GPIO35) ✓ PASS press detected
26.6s
15 passed
Board passed validation.
Example: ESP32-C6 Super Mini
╭──────────────────────────────────────────────────╮
│ ESP32-C6 Super Mini │
│ ESP32-C6FH4 (QFN32) v0.2 · /dev/cu.usbmodem2101 │
╰──────────────────────────────────────────────────╯
Check Result Details
HARDWARE
USB port ✓ PASS /dev/cu.usbmodem2101 USB-Serial/JTAG
Chip ✓ PASS ESP32-C6FH4 (QFN32) v0.2
Flash ✓ PASS 4MB
Base MAC ✓ PASS 8c:fd:49:10:9a:fc
Security fuses ✓ PASS secure boot off, flash encryption off, no locks
Firmware image ✓ PASS 3 image(s) written
Test firmware ✓ PASS esp32-c6-supermini / v6.1
MAC cross-check ✓ PASS firmware and bootloader agree
Radios ✓ PASS WiFi, BLE, 802.15.4
ON-CHIP TESTS
GPIO pads ✓ PASS 21/21 pads respond
ADC1 (GPIO0-6) ✓ PASS 7/7 channels, calibrated
Temperature sensor ✓ PASS 31.9 °C
Wi-Fi scan ✓ PASS 5 AP(s), best -72 dBm
RGB LED signal ✓ PASS 24-bit frame decoded intact (0x123456)
MANUAL CHECKS
BOOT button (GPIO9)✓ PASS press detected
18.9s
15 passed
Board passed validation.
Both examples were run with --confirm-button. Without it, the BOOT row is
reported as SKIP --confirm-button not set and does not affect the result.
Exit code is 0 when the board passes, 1 when a check fails, and 2 when
the board or the toolchain cannot be used.
What it checks
| Check | How |
|---|---|
| Board present | USB VID/PID of the programming port (Espressif native USB or WCH CH343) |
| Exact model | esptool chip name, package where reported, and flash size; anything else is refused before flashing |
| Flash | reported by the bootloader and cross-checked by the firmware |
| MAC | base MAC read by the bootloader, cross-checked against the firmware |
| Security fuses | secure boot, flash encryption, WR_DIS, RD_DIS |
| PSRAM | boards that have it report the detected size (P4-Pico: 32 MB; S3 Super Mini: 2 MB) |
| Radios | Wi-Fi / BLE / 802.15.4 feature bits, checked against what the board actually has |
| GPIO pads | every exposed GPIO, pulled up, pulled down, driven low and driven high |
| ADC | every ADC1 channel is biased with the internal pull-up and pull-down; the reading must move, which catches a shorted or stuck pad |
| Temperature | internal sensor returns a plausible value |
| I2C | scan the onboard bus and confirm the expected device answers (P4-Pico: ES8311 codec at 0x18) |
| Wi-Fi | active scan validates the RF path and PHY calibration (radio boards only) |
| RGB LED | RMT loopback: transmit a known 24-bit WS2812 frame and decode it back on the same pin (boards with a user LED only) |
| BOOT button | optional, with --confirm-button: you press it; the firmware reports the edge |
Deliberately never driven:
- ESP32-C6 Super Mini: GPIO11 (
VDD_SPI), GPIO12/13 (USB D−/D+), GPIO24–30 (in-package flash bus). - ESP32-S3 Super Mini: GPIO0 (BOOT button, not on a header), GPIO19/20 (USB D−/D+), GPIO26–32 (in-package flash/PSRAM bus). Strapping pins that are broken out (3, 45, 46, 47) and the UART0/JTAG pins (39–44) are swept as ordinary pads only after the application has booted.
- ESP32-P4-Pico: the in-package flash/PSRAM bus and the dedicated UART0/BOOT/PA-control pins are not in the header list. GPIO24/25 (USB OTG D−/D+) are tested as ordinary pads because only the Type-C UART cable is assumed to be connected.
Options
| Flag | Meaning |
|---|---|
--board, -b |
force a board profile instead of auto-detecting |
--list-boards |
list supported profiles and exit |
--port, -p |
pick a serial port instead of auto-detecting |
--list |
list detected serial ports and exit |
--no-flash |
skip flashing; assume the self-test firmware is installed |
--skip-manual |
skip the optional manual checks (BOOT button / LED confirmation) |
--confirm-led |
also ask a human to confirm the LED colours (signal already checked automatically; boards with a user LED only) |
--confirm-button |
also ask a human to press the BOOT button (opt-in; skipped by default and not part of the pass/fail gate) |
--json |
machine-readable report (implies --skip-manual) |
--btn-timeout N |
seconds to wait for a button press (default 30) |
--timeout N |
per-test timeout in seconds (default 40) |
--verbose, -v |
echo protocol traffic |
--version |
print the tool version and exit |
Use --json in CI:
uv run espcheck --json | jq -e '.ok'
Requirements
- macOS or Linux with a data-capable USB-C cable.
- Python 3.10 or newer and
uv. - No ESP-IDF is needed to run the validator: the prebuilt self-test images
are bundled under
src/espcheck/data/<profile>/. - Rebuilding the firmware needs ESP-IDF v6.1; see Rebuilding the firmware.
The tool flashes the board, overwriting whatever firmware is on it. Don't point it at a board you care about. The self-test firmware is harmless and leaves the eFuses untouched.
How it works
esptool flash-ididentifies the chip, package, MAC and flash size;espefuse summary --format jsonreads the fuse state. Both leave the chip in the ROM bootloader.- The identity is matched to a board profile. If the chip, package, flash size or minimum chip revision does not match, the board is reset back to its application and the tool exits without flashing.
- The profile's bundled manifest is written with
esptool write-flashat the offsets the chip needs (the P4 bootloader lives at0x2000, the C6 bootloader at0x0), then the board is reset. - The CLI opens the console and drives the firmware over a small line
protocol: every reply is a single JSON object prefixed with
@, terminated by{"t":"end","cmd":"…"}. Human-readable output is ignored, so the CLI never depends on log formatting. - Each board's
board_config.hdecides which GPIOs are swept, which ADC channels exist, whether there is an LED or radio, and which pin is BOOT. - The only manual step is the optional BOOT button check (
--confirm-button): the firmware waits for a press and exits as soon as it sees one.
Rebuilding the firmware
Both images are built from the shared project in firmware/:
./tools/build-firmware.sh # every board
./tools/build-firmware.sh esp32p4 # just one target
To use ESP-IDF interactively (build, flash, monitor, menuconfig):
./tools/idf.sh esp32p4 build
./tools/idf.sh esp32p4 -p /dev/cu.usbmodem5C931270381 flash monitor
./tools/idf.sh esp32s3 -p /dev/cu.usbmodem2101 flash monitor
./tools/idf.sh esp32c6 menuconfig
Building the ESP32-S3 image needs the Xtensa toolchain, which an ESP-IDF
installation set up only for RISC-V targets may not have. If idf.py reports
that xtensa-esp32s3-elf-gcc is missing, install it with:
python "$IDF_PATH/tools/idf_tools.py" install --targets=esp32s3
The firmware keeps an interactive console on the same port. idf.py monitor
sends CR on Enter, which the console expects.
| Command | Action |
|---|---|
info |
chip / flash / PSRAM / MAC summary |
pins |
re-run the GPIO pad test |
adc |
ADC channel readings |
temp |
internal temperature sensor |
i2c |
scan the onboard I2C bus |
led [pin] |
cycle the WS2812 (boards with a user LED) |
btn [sec] |
watch the BOOT button, default 10 s |
wifi |
scan for access points (radio boards only) |
sleep [sec] |
deep sleep, CPU/radios off (default 30 s) |
all |
everything except btn and loop |
loop A B |
jumper-wire continuity test between two header pins |
loop A B drives a pair of header pins against each other. Jumper them
together and it reports CONNECTED or no connection, which is useful for
checking a specific header solder joint. It is optional; the automated
validator never needs it.
The j … commands (j info, j pads, j adc, j temp, j i2c, j wifi,
j led start|stop, j btn [sec], j probe [sec]) emit the JSON protocol the
CLI consumes. j probe is a bring-up helper: it configures every exposed GPIO
as an input with a pull-up and reports any level change, which is useful for
confirming which pin a button or header pad is actually wired to.
Tests for the report and board-matching logic:
uv run python -m unittest discover -s tests
Adding a board
- Add a
BoardProfileinsrc/espcheck/boards.py(chip, flash, header pins, ADC channels, BOOT pin, USB IDs, radios/LED/PSRAM/onboard I2C). - Add a target block to
firmware/main/board_config.h. - Add
firmware/sdkconfig.defaults.<target>and one entry toALL_BOARDSintools/build-firmware.sh. - Run
./tools/build-firmware.sh <target>to producesrc/espcheck/data/<slug>/.
If the board will not enter download mode
esptool sometimes cannot reset a board that is running an application and
reports Serial data stream stopped or Failed to connect. Unplug the USB
cable, plug it back in, and run the command again. If that still fails, hold
the BOOT button while plugging the cable in, then release it.
Board notes
ESP32-S3 Super Mini
| Item | Value |
|---|---|
| Chip | ESP32-S3FH4R2 (QFN56), revision v0.2 |
| Cores | 2 Xtensa LX7 + 1 LP core, 240 MHz |
| Radios | Wi-Fi 4 (2.4 GHz), BLE 5 |
| Flash | 4 MB in-package (XMC), quad |
| PSRAM | 2 MB quad in-package, 80 MHz |
| Crystal | 40 MHz |
| USB | native USB-Serial/JTAG |
| GPIOs | 35 exposed across the side headers and rear pads |
| LED | onboard WS2812 on GPIO48 |
| BOOT | GPIO0 (button; not broken out) |
The 35 exposed GPIOs are 1–18, 21, 33–48. The profile deliberately leaves
out GPIO0 (BOOT), GPIO19/20 (USB D−/D+) and GPIO26–32 (the in-package
flash/PSRAM bus). ADC1 is on GPIO1–10, which are all on the side headers.
GPIO33–37 are broken out on the rear pads and are usable here because the
FH4R2 uses quad, not octal, PSRAM.
The ESP32-S3FH4R2 ships with 2 MB of PSRAM that most board definitions leave disabled. The bundled image enables quad PSRAM at 80 MHz, so the self-test reports and validates it (expected 2 MB). If a clone carries a different in-package variant with no PSRAM, the PSRAM check fails rather than silently passing — use a board profile that matches that variant.
GPIO48 drives the onboard WS2812, and the RGB LED signal check works the same way as on the C6: the RMT transmitter and receiver are both routed to the pin, and the 24-bit GRB frame is decoded back.
ESP32-C6 Super Mini
| Item | Value |
|---|---|
| Chip | ESP32-C6FH4 (QFN32), revision v0.2 |
| Cores | 1 HP RISC-V + 1 LP core, 160 MHz |
| Radios | Wi-Fi 6 (2.4 GHz), BLE 5, IEEE 802.15.4 |
| Flash | 4 MB in-package, DIO @ 80 MHz |
| Crystal | 40 MHz |
| USB | native USB-Serial/JTAG |
| LED | onboard WS2812 on GPIO8 |
| BOOT | GPIO9 |
Factory state (recorded before flashing). The flash was blank: the full
4 MB dump was 0xFF except a 24-byte 5A 00 FF A5 test pattern at 0x50000.
No bootloader, partition table or app. The dump is saved as
factory-dump/factory-4MB.bin. The eFuses were factory-pristine: secure boot
and flash encryption off, WR_DIS = 0, USB-Serial/JTAG and download mode
enabled, ADC1 and temperature calibration present, and a unique 128-bit ID.
Nothing was burned or locked. To return the board to that state:
uv run esptool --port /dev/cu.usbmodem2101 erase-flash
The green BAT LED flickers with no battery. It is driven by the onboard charger IC, not by a GPIO. With no cell attached the charger cannot terminate a charge cycle, so it hunts and the LED flickers at tens of Hz. This is not a fault. A deep-sleep test confirmed it: with the ESP32 in deep sleep (CPU, radios and USB off, and the USB port dropped), the LED flickered exactly as before. Asked for a 20 s sleep, the port was gone for 19.8 s and came back on its own.
Over USB-Serial/JTAG a deep-sleep wake is indistinguishable from a fresh boot: reattaching the USB host issues a
USB_UART_HPSYSreset (reset reason 21), which overwrites the stored wake cause. A battery-only setup, or UART0 on the header, reports the timer wake correctly.
Waveshare ESP32-P4-Pico
| Item | Value |
|---|---|
| Chip | ESP32-P4NRW32X, revision v3.2 |
| Cores | 2 HP RISC-V + 1 LP core, 360/400 MHz |
| Radios | none (this board carries no ESP32-C6 companion) |
| Flash | 32 MB NOR (GD25Q256) |
| PSRAM | 32 MB stacked, 200 MHz hex mode |
| USB | Type-C CH343P USB-UART on UART0 (GPIO37 TX / GPIO38 RX) |
| Header | 40-pin Pico-style, 27 GPIOs |
| BOOT | GPIO35 |
| LED | power indicator only, not GPIO-controlled |
The P4-Pico's Type-C port is a CH343P USB-to-UART bridge, not native USB, so the self-test firmware uses UART0 for its console. The separate 4-pin USB OTG header is not used.
The 27 header GPIOs are 2, 3, 4, 5, 6, 7, 8, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 32, 33, 46, 47, 48, 54. GPIO7/8 are the onboard I2C
bus (with external pull-ups) and GPIO9–13 feed the audio codec; the pad test
duly reports them as externally pulled/driven rather than free. ADC1 is on
GPIO16–23. The I2C check probes that bus and expects the ES8311 codec at
0x18, which confirms the codec and its bus without a speaker or microphone.
The P4 has no Wi-Fi, BLE or 802.15.4 hardware, so the radio check and Wi-Fi scan do not apply. It also has no user-controllable LED, so the LED signal check is skipped — only the hard-wired power indicator is present.
Observed state. The MAC is 80:f1:b2:d7:f8:00 and the flash is a 32 MB
GigaDevice GD25Q256. The eFuses were factory-pristine: secure boot and flash
encryption off, WR_DIS/RD_DIS 0. The original factory firmware was replaced
by the self-test during bring-up; Waveshare's ESP32-P4-Platform demo can be
rebuilt and flashed to restore it.
Chip revision caveat. ESP-IDF cannot build one image for both pre-v3.0 and
v3.0+ P4 revisions. The bundled image is built for the v3.0+ family with a
minimum of v3.1 (which covers the current P4NRW32X boards). espcheck detects
an older revision and refuses to flash it; that board needs its own target
build.
License
espcheck is released under the MIT License. The bundled firmware images contain code from ESP-IDF and other third-party components; see THIRD_PARTY_NOTICES.md.