esp32-wroom-38pin — Claude Code skill for the 38-pin ESP32 board
A skill for firmware work on 38-pin ESP32-WROOM-32 boards: header map, safe GPIOs, flash-pin and IO0 traps, working code recipes.
npx skills add alexex1993/mcu-skills --skill esp32-wroom-38pin -a claude-codeWhat esp32-wroom-38pin does
The 38-pin ESP32-WROOM-32 devkit breaks out every pad on the module. That includes the six flash-bus pins and GPIO0, and a mistake on any of them shows up as something else entirely: a hang past the bootloader banner, a reboot loop, or flash sectors going bad under load. The skill exists so you pick pins from a table instead of from memory.
Four reference files ship with it, plus a project template that compiles. board-hardware.md holds the J2/J3 header table with alternate functions, the IO0 hazard, how WROVER differs on GPIO16/17, and a symptom-to-cause-to-fix table. esp32-soc.md covers the silicon: strapping pins, ADC1 versus ADC2 once Wi-Fi is on, IO_MUX and the SPI speed rule, power modes. esp32-family.md helps if the real question is whether a different chip fits. recipes.md has code you can drop in.
Know this before installing: the chip itself has no USB, so the console runs on UART0 at 115200 through a CP2102N or CH340G bridge. Config copied from an ESP32-C3/C6/S3 project that selects the USB serial JTAG console gives you firmware that runs and prints nothing. GPIO12 has to sit low at reset, or VDD_SDIO goes to 1.8 V while the flash part wants 3.3 V. Count the pins on one side first: 15 or 18 means a different board and a different skill.
What it helps with
- count header pins to confirm the board is the 38-pin variant
- pick a GPIO without touching the flash bus or strapping pins
- explain why uploads broke after something got wired up
- get a working
platformio.iniandsdkconfig.defaultsfor ESP-IDF - trace a hang or reboot loop through the symptom table
Example prompts for the agent
- Check the header table whether an LED can go on GPIO2 on my 38-pin board
- Write
platformio.iniandsdkconfig.defaultsfor an ESP-IDF project onboard = esp32dev - The sketch uploads but the console stays silent — why is there no USB-JTAG on this board?
How to install esp32-wroom-38pin
1Skills CLI (Vercel)
npx skills add alexex1993/mcu-skills --skill esp32-wroom-38pin -a claude-code2Manually: put the files in place
~/.claude/skills/esp32-wroom-38pin/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/esp32-wroom-38pin/
1Skills CLI (Vercel)
npx skills add alexex1993/mcu-skills --skill esp32-wroom-38pin -a codex2Manually: put the files in place
.agents/skills/esp32-wroom-38pin/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add alexex1993/mcu-skills --skill esp32-wroom-38pin -a cursor2Manually: put the files in place
.cursor/skills/esp32-wroom-38pin/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install alexex1993/mcu-skills esp32-wroom-38pin2Manually: put the files in place
.github/skills/esp32-wroom-38pin/SKILL.md1Skills CLI (Vercel)
npx skills add alexex1993/mcu-skills --skill esp32-wroom-38pin -a gemini-cli2Platform CLI
gemini skills install https://github.com/alexex1993/mcu-skills --path skills/esp32/esp32-wroom-38pinManually: put the files in place
.devin/skills/esp32-wroom-38pin/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add alexex1993/mcu-skills --skill esp32-wroom-38pin -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about esp32-wroom-38pin
How do I install esp32-wroom-38pin in Claude Code?
Run npx skills add alexex1993/mcu-skills --skill esp32-wroom-38pin -a claude-code. The skill lands at ~/.claude/skills/esp32-wroom-38pin/SKILL.md.
What does esp32-wroom-38pin do?
It gives the agent hardware and silicon references for this board, ready-made code recipes and a project template, so pin choice and build setup rest on documented facts.
How is the 38-pin board different from the 30- and 36-pin ones?
Pin count per side, the flash pins being on the header, and GPIO0 being broken out. The 30-pin board has neither, the 36-pin board has the flash pins but not GPIO0.
Does the skill cover ESP32-WROVER?
Yes. The reference accounts for WROVER's PSRAM and the GPIO16/17 it occupies, which are free on a plain WROOM.
Source text
SKILL.md · MIT
---
name: esp32-wroom-38pin
description: Firmware development for the 38-pin ESP32-WROOM-32 development board — Espressif ESP32-DevKitC V4, Ai-Thinker NodeMCU-32S, DOIT ESP32 DevKit V1 38-pin and their CH340/CP2102 clones, 19 pins per side, with GPIO0 and the six in-package flash pins (GPIO6-11, labelled D0 D1 D2 D3 CMD CLK or SD0-SD3) broken out. Use when working on a 38-pin ESP32 devkit (ESP32-D0WDQ6 / ESP32-D0WD-V3, ESP-WROOM-32 or ESP32-WROVER module): project setup, platformio.ini and sdkconfig for ESP-IDF or Arduino, pin mapping and which GPIOs are safe, the flash-pin and IO0 traps, WROVER GPIO16/17 PSRAM conflict, strapping pins, ADC1 vs ADC2 with Wi-Fi, SPI speed and IO_MUX pins, deep sleep and RTC wake pins, flashing, BOOT/EN recovery, brownouts, or debugging why something on the board does not work.
---
# ESP32-WROOM-32 devkit — 38-pin
The reference form factor: Espressif's **ESP32-DevKitC V4** and the clones that copy its
J2/J3 header pin for pin. Nineteen pins a side, and every pad of the module is broken out —
including six that will hang the chip and one that will stop it booting.
That completeness is the whole story of this board. The 30-pin version protects you by
omission; this one hands you the flash bus and GPIO0 and expects you to know better.
Read `reference/board-hardware.md` before choosing pins.
- `reference/board-hardware.md` — the board: the Espressif J2/J3 header table with every
pin's alternate functions, the six flash pins, the IO0 hazard, the `D2`-means-GPIO9
silkscreen trap, the WROVER GPIO16/17 difference, the USB bridge and auto-reset circuit,
power tree, flash partitions — **plus** a development guide (§10 toolchain, §11
sdkconfig, §12 flashing and recovery, §13 peripheral cookbook, §14 symptom → cause → fix
table).
- `reference/esp32-soc.md` — the silicon: pins that do not exist, strapping semantics and
the `GPIO_STRAP_REG` bit order, ADC/DAC/touch maps and accuracy, IO_MUX vs GPIO Matrix
and the SPI speed rule, power modes, memory map, deep-sleep wake pins.
- `reference/esp32-family.md` — the rest of the family, for "should this be a different
ESP32?" questions: what does and does not port between chips, radio and USB capability
per chip, the RMT generation table (WS2812 under Wi-Fi load), deep-sleep memory and
ULP/LP-core availability, and a chip-selection table.
- `reference/recipes.md` — code that compiles: `platformio.ini`, `sdkconfig.defaults`,
ADC1 with calibration, LEDC LED and servo, debounced input, the new I2C master driver,
80 MHz SPI, UART2, Wi-Fi scan and station connect, NVS, deep sleep, RMT WS2812.
- `template/` — a **project that builds clean**, in two variants, plus a scaffold script.
See `template/README.md`.
## Confirm the board first
The 30-, 36- and 38-pin ESP32 devkits share silicon but not a header, and the pin map is
what this skill is for. Count the pins on one side:
| Pins per side | Flash pins on the header | `IO0`/`D0` pin | GND pins | Skill |
|---|---|---|---|---|
| 15 | no | no | 2 | `esp32-wroom-30pin` |
| 18 | yes (`SD2 SD3 CMD` / `SD1 SD0 CLK`) | no | 2 | `esp32-wroom-36pin` |
| **19** | **yes** (`D2 D3 CMD` / `D1 D0 CLK`) | **yes** | **3** | **this one** |
If the count does not match, switch skills. `reference/board-hardware.md` §1 has the detail,
including the silkscreen trap where `D2` means GPIO9.
## Orientation
| | |
|---|---|
| Module | ESP32-WROOM-32/-32D/-32E/-32U, ESP-WROOM-32S, or **ESP32-WROVER** — check the can. ESP32-D0WDQ6 or D0WD-V3, **Xtensa LX6 dual-core** @ 240 MHz |
| Memory | 4 MB in-package flash, 520 KB SRAM (**~320 KB linkable**), 8 KB RTC FAST + 8 KB RTC SLOW. **PSRAM only on a WROVER** |
| Header | 38 pins, 19 per side (Espressif J2 / J3). **32 GPIOs — all module pads**, EN, 3V3, 5V, 3× GND |
| USB | **No USB peripheral in the chip.** A CP2102N (DevKitC) or CH340G (clones) bridge on UART0 (GPIO1/GPIO3), with DTR/RTS auto-reset |
| Console | UART0 @ 115200 only. There is no USB-Serial-JTAG and no debug header |
| LED | **none on the Espressif DevKitC V4.** GPIO2, active high, on NodeMCU-32S and clones |
| Buttons | **BOOT** = GPIO0, pressed low, and **GPIO0 is also a header pin** · **EN/RST** acts on CHIP_PU, not readable |
| Free pins | 4, 13, 14, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33 |
| Input-only | 34, 35, 36 (VP), 39 (VN) — no output driver, **no internal pulls** |
| Strapping | 0, 2, 5, 12 (MTDI), 15 (MTDO) |
| **Never use** | **6, 7, 8, 9, 10, 11** — the in-package flash bus, six pins by the USB connector |
| Analog | ADC1 on 32–36, 39 · ADC2 on 0, 2, 4, 12–15, 25–27 · DAC on **25 and 26** |
| Radio | Wi-Fi b/g/n + Bluetooth 4.2 BR/EDR + BLE, PCB antenna (or u.FL on `-U`/`-IE` modules) |
| Power | 3V3 out (~600 mA budget) · 5V in/out. **Three mutually exclusive supply routes** |
| Toolchain | PlatformIO 6.1.19 + `platform-espressif32` 7.0.1 + ESP-IDF 6.0.1, `board = esp32dev` |
## Rules that prevent the expensive mistakes
Each of these produces a failure that looks like something else.
1. **GPIO6–11 are the flash bus. Wire nothing to them, ever.** The six pins nearest the
USB connector — `D2 D3 CMD` on one side, `D1 D0 CLK` on the other — go to the module's
internal 4 MB flash die, which the CPU is executing out of. A pull-up, an LED, or even
a scope probe with too much capacitance gives you one of: a hang just after the
bootloader banner, a boot loop with `invalid header`, or — worst — a board that runs
and corrupts flash sectors under load. Espressif's own footnote says "avoid"; treat
that end of the header as mechanical support.
2. **`D2` on an Espressif silkscreen means GPIO9, not GPIO2.** DOIT-style boards use `D2`
for GPIO2 and `D4` for GPIO4; Espressif uses `D0`–`D3` for the flash pins GPIO7–GPIO10.
The same two characters mean different pins on different boards, and following a
tutorial written for the other one puts your signal on the flash bus (rule 1). Resolve
every silkscreen label to a GPIO number via `board-hardware.md` §2 before writing code.
3. **GPIO0 is on this header, and three things already own it**: the boot strap (low at
reset ⇒ serial download mode), the BOOT button, and the bridge chip's auto-reset
circuit. An LED to ground on IO0 is enough to drop the board into download mode on
every reset — which presents as "my firmware stopped running", not as a pin problem —
and a pull-up breaks `pio run -t upload`. If uploads started failing after you wired
something, unplug IO0 first.
4. **`CONFIG_ESP_CONSOLE_UART_DEFAULT=y`, never `..._USB_SERIAL_JTAG`.** The original
ESP32 has **no USB peripheral at all** — every byte comes out of UART0 through a
separate bridge chip. Configuration copied from an ESP32-C3/C6/S3 project produces a
firmware that runs perfectly and says nothing.
5. **GPIO12 must be LOW at reset.** It is MTDI, and high at reset straps VDD_SDIO to
1.8 V while the flash is a 3.3 V part. The board boots intermittently and the symptom
looks like *flash corruption*. A pull-up, an LED to 3V3, or a peripheral idling a line
high all do it. Fine after boot; add a 10 kΩ pull-down if you need the pin.
6. **Check whether the module is a WROVER before touching GPIO16/17.** On WROVER modules
those two are the 8 MB PSRAM's CS and CLK and are unusable — which also removes UART2's
default pins. Espressif's footnote: available "only on the boards with the modules
ESP32-WROOM and ESP32-SOLO-1". Read the can: `WROOM` ⇒ free, `WROVER` ⇒ taken. The same
distinction decides whether `CONFIG_SPIRAM=y` is correct or fatal.
7. **ADC2 is dead while Wi-Fi is running.** The Wi-Fi PHY arbitrates for the same
hardware and wins: `adc_oneshot_read()` on any ADC2 channel returns `ESP_ERR_TIMEOUT`
until `esp_wifi_stop()`. That leaves **ADC1 — GPIO32, 33, 34, 35, 36, 39** — as the
entire analog budget of a connected application. Plan the pinout around it.
8. **`SPI3_HOST` on GPIO18/19/23/5, or you get 26.67 MHz.** Those four are SPI3's IO_MUX
pads and bypass the GPIO Matrix. Any other pin set — or the same pins on `SPI2_HOST` —
is matrix-routed and the driver silently clamps full-duplex transfers to 80 MHz / 3.
No error is reported; `spi_device_get_actual_freq()` is how you find out.
9. **GPIO34/35/36/39 are input-only and have no internal pull-up or pull-down.**
`gpio_config()` accepts `GPIO_PULLUP_ENABLE` on them and returns `ESP_OK`. Nothing
happens. A button there needs a physical 10 kΩ resistor.
10. **A reset the moment Wi-Fi starts is the power supply, not your code.** The TX burst
pulls ~300 mA against a datasheet requirement of a 500 mA source.
`esp_reset_reason()` returns `ESP_RST_BROWNOUT`; say so rather than reading the stack
trace.
11. **Exactly one power route at a time.** USB, or the 5V pin, or the 3V3 pin — never two.
Espressif warns that more than one "can damage the board and/or the power supply", and
no ORing diode is fitted on the clones either.
12. **UART1's default pins are GPIO9 and GPIO10** — flash pins again (rule 1). Use
**UART2 on GPIO17/GPIO16** (WROOM only, rule 6), and call `uart_set_pin()` *before*
`uart_driver_install()`; the reverse order compiles, runs, and leaves the peripheral on
its defaults.
13. **Only RTC GPIOs wake the chip from deep sleep**: 0, 2, 4, 12–15, 25–27, 32–36, 39.
GPIO5, 16, 17, 18, 19, 21, 22, 23 cannot, however you configure the interrupt. The wake
pin's pull must be set with `rtc_gpio_pullup_en()`, not `gpio_set_pull_mode()`.
14. **Deep sleep on this board is 8–20 mA, not 10 µA.** LDO quiescent + USB bridge + power
LED. The datasheet's 10 µA is the bare chip; never quote it for a devkit.
15. **`CONFIG_ESP32_XTAL_FREQ_40=y`.** The module's crystal is 40 MHz; the 26 MHz setting
garbles the console at *every* baud rate, which reads as a broken cable.
16. **Do not assume there is a user LED.** The genuine Espressif DevKitC V4 has none — only
a power LED. `BOARD_HAS_USER_LED` in `include/board.h` defaults to 1 for the clones;
if the minimal variant prints ticks and nothing lights up, set it to 0 rather than
debugging the GPIO.
## When the task is choosing pins
The 38-pin board has 32 pads on the header but the same **25 usable GPIOs** as the 30-pin
board, because six are flash and GPIO0 is spoken for. Work outward:
1. **Analog first.** With Wi-Fi in the design, every analog input must land on GPIO32, 33,
34, 35, 36 or 39 (rule 7).
2. **Then the fast bus.** SPI above 26 MHz claims GPIO18/19/23/5 (rule 8).
3. **Then outputs that must be quiet at boot.** GPIO2 is driven briefly by the bootloader;
GPIO1 emits the ROM log; GPIO0 is driven by the auto-reset circuit on every upload.
4. **Everything else** goes on 4, 13, 14, 16, 17, 19, 21, 22, 23, 25, 26, 27.
If you run out, the extra pins on this board over the 30-pin one are **not** the answer —
five of the seven are unusable and the sixth is GPIO0. Reach for an I2C GPIO expander.
## When the task is "the board does not work"
Diagnose in this order — each stage only depends on the ones before it:
1. **Is anything wired to the six pins by the USB connector, or to IO0?** Unplug them
first. Rules 1 and 3 account for most mysteries on this board specifically.
2. **Does the port enumerate?** No port ⇒ cable (try a known data cable) or bridge driver.
3. **Does anything print at 115200?** The ROM bootloader's `rst:0x1 (POWERON_RESET)`
banner comes out before any of your configuration applies.
4. **Does it print but say nothing after?** Rule 4 — the console is misconfigured, or
MTDO/GPIO15 is strapped low.
5. **Does it boot-loop?** Read `rst:`. `ESP_RST_BROWNOUT` ⇒ rule 10. `RTCWDT_RTC_RESET` or
`invalid header` ⇒ `esptool.py erase_flash` and reflash the minimal variant.
6. **Does it land in download mode by itself?** Something is holding GPIO0 low (rule 3).
On a genuine early DevKitC V4, it may instead be the **C15** capacitor Espressif
documents — see `board-hardware.md` §3.
7. **Does it reach `app_main()` and then misbehave?** Flash `--minimal` for a baseline,
then add one subsystem at a time.
`template/src/board_report.c` prints the strapping latches (including the GPIO0 one),
the reset reason, the flash size and the partition table, which answers most of steps 4–7.
## Starting a new project
Do not hand-assemble one. `template/` builds clean; scaffold from it:
```sh
~/.claude/skills/esp32-wroom-38pin/template/variants/new-project.sh <target-dir> [--full|--minimal]
cd <target-dir> && pio run
```
- `--minimal` — LED blink on GPIO2 + console heartbeat. **144,493 B flash, 13,380 B RAM.**
Flash this first on a board you have not used before: it touches only the toolchain, the
flashing route, UART0 and one GPIO, and its two outputs fail independently — LED but no
console means the serial side, console but no LED means your board is a DevKitC V4 with
no user LED (rule 16).
- `--full` (default) — board self-test: chip/reset/strapping report, partition dump, ADC1
with calibration, Wi-Fi scan, LEDC heartbeat, BOOT button to re-run. **784,417 B flash,
37,036 B RAM.** The jump is the Wi-Fi stack.
Both build as-is with ESP-IDF 6.0.1 (verified, zero warnings). Nothing is generated and no
paths are embedded, so copying `template/` by hand works identically.
`template/README.md` maps files to subsystems so a `--full` scaffold can be stripped back.
When the user already has a project, prefer bringing it in line with the template's
`platformio.ini`, `sdkconfig.defaults` and `include/board.h` over rewriting their code.
Arduino (`framework = arduino`) is a reasonable choice for this board and every pin rule
above applies unchanged — but the Arduino core's `analogRead()` hides the ADC2/Wi-Fi
conflict of rule 7 instead of reporting it.
## Flashing
```sh
pio run -t upload -t monitor
```
The bridge chip's DTR/RTS drive EN and GPIO0 through the two-transistor auto-reset circuit,
so esptool handles the whole cycle with no buttons. The board appears as
`/dev/cu.usbserial-*` or `/dev/cu.SLAB_USBtoUART` (macOS), `/dev/ttyUSB0` (Linux, needs
`dialout` membership), or a `COM` port (Windows, WCH or Silicon Labs driver).
When it fails with `Wrong boot mode detected` or `No serial data received`, **check IO0
before the cable** (rule 3). Then the manual sequence: **hold BOOT → tap EN → keep BOOT a
second longer → release BOOT → upload.** Some clones need BOOT held for the whole upload.
If 921600 baud is unreliable, drop `upload_speed` to 460800.
**Bad firmware cannot brick this board.** The ROM bootloader is in mask ROM and always
answers that sequence:
```sh
esptool.py --chip esp32 -p <port> erase_flash # ~10 s for 4 MB
```
There is no debug header and no USB-JTAG. External JTAG means wiring a probe to
GPIO12/13/14/15 — and a probe that idles MTDI (GPIO12) high stops the board booting
(rule 5). In practice you debug this board over UART0.
## Reporting
Say what is verified and what is derived.
**Primary-source, high confidence:** the header pin map in `reference/board-hardware.md`
§2 is transcribed from Espressif's ESP32-DevKitC V4 J2/J3 tables and cross-checked against
Ai-Thinker's NodeMCU-32S datasheet, which agree pin for pin. This is the best-documented
of the three board sizes. The flash-pin warning, the GPIO16/17 WROVER note and the C15
issue are Espressif's own text.
**Verified on this machine:** both template variants build clean with PlatformIO 6.1.19 /
platform-espressif32 7.0.1 / ESP-IDF 6.0.1, zero warnings, at the flash and RAM figures
quoted above. Every recipe in `reference/recipes.md` was compiled in the same project. The
26.67 MHz SPI figure is computed from `_GPIO_MATRIX_DELAY_NS` in the IDF HAL.
**Not verified on hardware:** none of this was run on a physical board in this session.
Details that vary by vendor — whether a user LED is fitted, which bridge chip, whether the
auto-reset transistors are present — should be confirmed with the `--minimal` variant on
the user's actual board. Say so rather than presenting them as certain.
© alexex1993 · MIT · alexex1993/mcu-skills