A basic example for the YD-ESP32-23 board that flashes the onboard RGB LED in red and blue like a police car light bar.
The firmware alternates three quick red flashes and three quick blue flashes in a continuous loop, using the built-in NeoPixel on GPIO 48 and the neopixelWrite() helper provided by the Arduino-ESP32 framework.
- Board: YD-ESP32-23 (ESP32-S3 DevKitC-1 clone)
- Flash: 16 MB
- PSRAM: 8 MB (OPI)
- Framework: Arduino via PlatformIO
New to PlatformIO? Start here. This guide walks you through setup, the physical board, and how to build and run your first test.
- Prerequisites
- Install PlatformIO
- Physical setup — which USB port to use
- Find your serial port and configure platformio.ini
- Build and upload
- Running tests
- Project structure
- AI coding agent setup
- A computer running macOS, Linux, or Windows
- Python 3.8 or newer (required by PlatformIO)
- A USB-C cable (data-capable, not charge-only)
- The YD-ESP32-23 board
PlatformIO can be used as a VS Code extension or as a standalone CLI tool. Both are supported.
VS Code extension (recommended for beginners):
- Install Visual Studio Code
- Open the Extensions panel and search for PlatformIO IDE
- Install it — the
pioCLI becomes available automatically in the integrated terminal
CLI only:
pip install platformioVerify the installation:
pio --versionThe YD-ESP32-23 has two USB-C connectors on the board. Using the wrong one is the most common source of confusion.
┌─────────────────────────────────────┐
│ YD-ESP32-23 │
│ │
│ [UART] ← use this one │
│ [USB] ← do NOT use for flashing │
│ │
└─────────────────────────────────────┘
| Connector label | Chip behind it | When to use |
|---|---|---|
UART |
CH343P (dedicated USB-to-UART bridge) | Always — for upload, tests, and serial monitor |
USB |
ESP32-S3 built-in USB peripheral | Not needed for this workflow |
Always plug into the UART connector.
The UART connector uses a CH343P chip — a separate, dedicated bridge that stays alive regardless of what the ESP32-S3 is doing (including resets and flashing). This is critical for test runs: after the board is flashed it resets, and if the serial port disappears during that reset pio test will hang waiting for output that never arrives. The CH343P port does not disappear.
The USB connector exposes the ESP32-S3's built-in USB peripheral directly. It drops off the bus during resets, which breaks the test runner.
PlatformIO needs to know which serial port your board is connected to. You must set this once for your machine.
List ports before and after plugging in the board to identify the new one:
# Before plugging in
ls /dev/cu.*
# Plug in the UART connector, then run again
ls /dev/cu.*The new entry is your board's port. It will look something like:
/dev/cu.usbmodem5ABA0887541 # macOS (CH343P)
/dev/ttyUSB0 # Linux (CH343P)
Open Device Manager, expand Ports (COM & LPT), and look for the new COM port that appears when you plug in the board (e.g. COM3).
Open platformio.ini and replace the port values in the [env:yd_esp32_23] section:
upload_port = /dev/cu.usbmodem5ABA0887541 ; <- replace with your port
test_port = /dev/cu.usbmodem5ABA0887541 ; <- same port, replace here tooOn Windows it would be:
upload_port = COM3
test_port = COM3Both
upload_portandtest_portshould point to the same port — the CH343PUARTconnector.
Build the project (compiles without uploading):
pio runBuild and flash to the board:
pio run --target uploadOpen the serial monitor to see output:
pio device monitorPress Ctrl+C to exit the monitor.
Tests live in test/. PlatformIO supports two environments:
| Environment | Runs on | Board required |
|---|---|---|
native |
Your computer | No |
yd_esp32_23 |
The physical board | Yes, connected via UART |
# Run logic tests on your computer (no board needed)
pio test -e native
# Run hardware tests on the board (board must be connected)
pio test -e yd_esp32_23See how_to_unit_test.md for a full guide on writing and running tests.
.
├── platformio.ini # Board, framework, and environment config
├── AGENTS.md # AI coding agent guidelines (see below)
├── src/
│ └── main.cpp # Application entry point
├── test/
│ ├── test_basic/ # Logic tests — run native and on device
│ └── test_rgb_led/ # RGB LED smoke tests — device only
└── partitions/
└── partitions_16MB_psram.csv # Custom partition table for 16 MB flash
This project is developed with OpenCode, an AI coding assistant that automatically loads AGENTS.md as a system-level instruction file for the agent working in this repo.
AGENTS.md contains board-specific rules, build constraints, partition table requirements, USB serial flags, and testing conventions that the agent must follow. It is the single source of truth for how code should be written and validated in this project.
Using Claude Code instead? Rename AGENTS.md to CLAUDE.md. Claude Code reads CLAUDE.md from the project root and loads it as agent context automatically. The content is identical — only the filename needs to change.
| Tool | Instruction file |
|---|---|
| OpenCode | AGENTS.md |
| Claude Code | CLAUDE.md |
Custom partition table (partitions/partitions_16MB_psram.csv): required because the default partition table does not include an otadata partition at 0xe000. Without it, esptool corrupts the boot process and the board panics before setup() ever runs. It also includes a coredump partition required by the ESP32-S3 crash handler.
USB serial build flags (in platformio.ini):
board_build.extra_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1These flags redirect Arduino's Serial object to the USB CDC interface so that serial output is visible from boot. Without them, Serial.print() goes to UART0 — a hardware UART pin that nothing is connected to — and pio test hangs forever waiting for output.
Side effect:
Serial.flush()becomes blocking when no serial monitor is open. Do not leave firmware runningSerial.flush()without a connected monitor.