Why AI Writes Broken ESP32 Code (and How to Fix It for Arduino-ESP32 3.x)
4 November 2026 · 6 min read
You ask an AI assistant for a simple ESP32 sketch, it hands you clean, confident code, and the compiler replies with 'ledcSetup' was not declared in this scope. Or worse, it compiles, you flash it, and the board boot-loops.
This isn't bad luck. It's the most predictable failure in AI-assisted firmware, and once you know its shape it's easy to prevent.
Firmware is not a forgiving sandbox
A hallucinated function in a web app gives you a red squiggle. A hallucinated pin number on a microcontroller can drive current into the wrong pin and release the "magic smoke". Firmware talks to real hardware in real time: a blocking call trips a watchdog and reboots the board, and a wrong register writes garbage to real silicon. The AI can't see your wiring, feel heat or measure current. It only sees text.
Two failure modes dominate:
- Hallucination. The model invents a function, register, pin or whole library. A USENIX Security 2025 study of 576,000 generated code samples found hallucinated packages in at least 5.2% of cases for commercial models and 21.7% for open-source ones.
- Deprecated APIs. The model writes real code that used to work. An ICSE 2025 study measured deprecated API usage of 25% to 38% across eight Python libraries. On the ESP32 this shows up constantly.
A 2026 study that tested 27 LLMs on ESP32 hardware across eight embedded scenarios found that 19 to 23 models could produce compilable code for simple apps, but only 3 to 5 produced working solutions for complex ones. The most common failure was hallucinated libraries or incorrect API usage.
The version trap: Arduino-ESP32 2.x vs 3.x
The ESP32 world had two big breaking changes recently: Arduino-ESP32 went from 2.x to 3.x, and ESP-IDF went from 4.x to 5.x. Models were trained on years of 2.x examples, so they confidently generate the old API.
PWM (LEDC) is the classic example. In core 2.x you set up a channel, attached a pin to it, and wrote to the channel:
// Arduino-ESP32 2.x: does NOT compile on core 3.x
ledcSetup(0, 5000, 8); // channel, frequency, resolution
ledcAttachPin(5, 0); // pin, channel
ledcWrite(0, 128); // channel, duty
In 3.x, channels are assigned automatically and you work with the pin directly:
// Arduino-ESP32 3.x
const int ledPin = 5; // a safe output pin
void setup() {
ledcAttach(ledPin, 5000, 8); // pin, 5 kHz, 8-bit resolution
}
void loop() {
for (int duty = 0; duty <= 255; duty++) {
ledcWrite(ledPin, duty);
delay(5);
}
}
Hardware timers changed too. The old four-argument style, timerBegin(0, 80, true) with a timer number, prescaler and count direction, is gone. In 3.x, timerBegin takes the timer frequency in hertz, and alarms are set with timerAlarm.
I2C in ESP-IDF 5.x replaced the legacy driver (i2c_config_t, i2c_driver_install, i2c_master_write_to_device) with a bus-and-device model (i2c_new_master_bus, i2c_master_bus_add_device, i2c_master_transmit).
The fix is to make the model confront the version before it writes anything:
I'm on Arduino-ESP32 core 3.3.x (ESP-IDF 5.5). Before writing code,
list any APIs you plan to use that CHANGED between core 2.x and 3.x
(e.g., LEDC, timer, I2C). For each, show the OLD call and the NEW 3.x call.
Then write the sketch using ONLY the 3.x API.
The pin rules your AI doesn't reliably know
ESP32 pins are not interchangeable, and this is where AI most often bricks projects. For the classic ESP32-WROOM-32:
| Pins | Rule |
|---|---|
| GPIO 6–11 | Wired to the internal SPI flash. Using them crashes the chip. Never touch them. |
| GPIO 34, 35, 36, 39 | Input only: no output drivers and no internal pull-ups. They can't drive an LED or relay. |
| GPIO 0, 2, 12, 15 | Strapping pins, sampled at boot. The wrong level or an attached load can stop the board booting. |
| ADC2 pins (0, 2, 4, 12–15, 25–27) | Unusable for analogRead() while Wi-Fi is on. Use ADC1 (GPIO 32–39) in any networked project. |
I2C also needs pull-up resistors (4.7 kΩ is typical). That is a hardware fact no amount of code can fix.
Put these rules in the prompt, and ask the model to justify every pin it picks:
Write an Arduino-ESP32 3.x sketch for an ESP32-WROOM-32 that:
- reads a potentiometer on an ADC1 pin (Wi-Fi is ON, so NOT ADC2),
- drives an LED with PWM using ledcAttach()/ledcWrite() (NOT ledcSetup),
- avoids GPIO 6–11 and never uses 34–39 for the LED.
State which pins you chose and why each is safe.
Make the rules permanent with an instruction file
Rather than repeating yourself, put the rules in the project instruction file your agent reads: CLAUDE.md for Claude Code, AGENTS.md for Codex and many other tools, or .cursor/rules for Cursor.
# Firmware project rules
- Board: ESP32-S3-DevKitC-1. Framework: Arduino-ESP32 core 3.3.x (ESP-IDF 5.5).
- Use ONLY the 3.x API. Never use ledcSetup/ledcAttachPin or the 4-arg timerBegin.
- Never assign GPIO 6–11 (SPI flash).
- Analog sensors must use ADC1 pins because Wi-Fi is enabled.
- After any change, run `pio run`. Paste the full error output before editing.
- Never use delay() inside a FreeRTOS task longer than 50 ms; use vTaskDelay.
- Cite the doc page for any peripheral API you use.
Pin the board and framework in platformio.ini too, so builds are reproducible and the agent can run pio run itself. Then it can write, compile, read the errors and fix them in a loop.
A five-step verification ritual
The model's confidence has nothing to do with its correctness, so check every time:
- Compile. If it doesn't build, feed back the full error verbatim.
- Static analysis.
cppcheck --enable=all --inconclusive src/catches null dereferences, buffer overflows and uninitialised reads that both the compiler and the AI miss. - Cross-check the docs. Verify every pin, register and function against the current reference. This is where deprecated APIs get caught.
- Demand citations. Ask the model to name the documentation page for each peripheral call. Unsourced claims are where hallucinations hide.
- Simulate before you flash. Wokwi simulates the ESP32 family in the browser or VS Code, so the AI can test logic without risking your board.
AI is a strong assistant and a weak authority. Used with a tight loop and these guardrails, it will write a lot of your firmware, and your board will survive the experience.
This article is adapted from our ebook Vibe-Coding Your ESP32, which also covers ESPHome, hardware-in-the-loop debugging, memory, timing and power, safety rails for mains and motors, a complete 3D-printer enclosure monitor build, and a 12-point pre-flash checklist.