Phase 2: Setup & Development 15 min read read

Uploading Code to ESP32: Methods, Troubleshooting, and OTA Updates

Master all methods for uploading code to ESP32 — USB serial, manual boot mode, OTA over Wi-Fi, esptool.py, and ESP Flash Download Tool — with complete troubleshooting for every upload failure.

Updated June 18, 2026

The Upload Pipeline Explained

Every time you click Upload in the Arduino IDE, a multi-step pipeline runs invisibly behind the progress bar. Understanding this pipeline transforms upload failures from mysterious black-box errors into diagnosable problems with specific fixes. The pipeline has four stages: compilation, binary packaging, bootloader negotiation, and flash writing.

Stage 1 — Compilation: The Arduino IDE invokes the Xtensa GCC toolchain to compile your .ino sketch (converted to .cpp) plus all linked library source files into object files, then links them into an ELF (Executable and Linkable Format) binary. This is where syntax errors and missing library failures appear.

Stage 2 — Binary packaging: The IDE runs esptool.py’s merge_bin command to extract the application firmware from the ELF, create the bootloader and partition table binaries, and determine the correct flash addresses for each component.

Stage 3 — Bootloader negotiation: esptool.py drives the RTS line on the USB-to-serial chip low (which connects to the EN/RST pin through a 0.1 µF capacitor), then drives DTR low (connected to GPIO 0). This sequence resets the ESP32 with GPIO 0 held low, putting it into UART download mode. The ESP32 ROM bootloader then starts responding at 115200 baud before synchronising to the upload speed.

Stage 4 — Flash writing: esptool.py detects the connected chip, erases the relevant flash sectors, and writes the firmware at up to 921600 baud. A CRC check verifies each written block. When complete, RTS pulses again to reset the ESP32 into normal boot mode, starting your new sketch.

Method 1: USB Serial Upload (Standard)

This is the default and most used upload method. Requirements: a USB cable with data lines (charge-only cables have only two wires — power and ground — and cannot carry serial data), the correct USB-to-serial driver installed, and the correct COM port selected in the IDE.

Typical upload session in Arduino IDE 2.x:

  1. Select Tools → Board → your ESP32 board variant
  2. Select Tools → Port → the COM port showing your board
  3. Click the Upload button (→ arrow) or press Ctrl+U
  4. Watch the output console — compilation messages appear first, then “Connecting…”, then flash write progress
  5. Upload complete when you see “Hard resetting via RTS pin…”

The total time from clicking Upload to the sketch running is typically 10–30 seconds for small sketches and up to 60 seconds for large ones with many libraries. The compilation step is the slowest, but Arduino IDE caches compiled library objects — only changed code recompiles, making subsequent uploads faster.

Method 2: Manual Boot Mode for Boards Without Auto-Reset

Some ESP32 boards (particularly the ESP32-CAM and some custom hardware) lack the capacitor-resistor circuit that allows the IDE to automatically trigger download mode. For these boards:

  1. Press and hold the BOOT button (GPIO 0 to GND)
  2. Press and release the EN (reset) button while holding BOOT
  3. Release the BOOT button — the chip is now in download mode
  4. Click Upload in the IDE — it should connect immediately

You can confirm the board is in download mode by opening a serial terminal at 115200 baud — the ROM bootloader prints a brief message then waits silently. No LED activity occurs in download mode.

Method 3: OTA — Over-the-Air Updates

OTA allows uploading new firmware to an ESP32 over Wi-Fi — no USB cable required. You must first upload a sketch that includes OTA support. The ArduinoOTA library is the simplest approach:

#include <WiFi.h>
#include <ArduinoOTA.h>

const char* ssid     = "YourNetwork";
const char* password = "YourPassword";

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.print("IP: "); Serial.println(WiFi.localIP());

  ArduinoOTA.setPassword("admin");  // optional password protection
  ArduinoOTA.onStart([]()  { Serial.println("OTA start");   });
  ArduinoOTA.onEnd([]()    { Serial.println("OTA done");    });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%n", progress * 100 / total);
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("OTA Error[%u]n", error);
  });
  ArduinoOTA.begin();
}

void loop() {
  ArduinoOTA.handle();  // must call this in every loop iteration
  // ... your application code ...
}

After uploading this sketch via USB, the ESP32 appears as a network port in the Arduino IDE. Under Tools → Port, you will see an entry like “ESP32-XXXXXX at 192.168.1.x”. Select it and upload subsequent sketches wirelessly. Each OTA sketch must also include ArduinoOTA.handle() in loop() to remain updateable over the air — forgetting this locks you out and requires the next update to be via USB again.

Method 4: esptool.py Direct Flashing

For advanced use cases — flashing pre-built binaries, batch programming, or automated CI/CD deployments — use esptool.py directly from the command line. Install it:

pip install esptool

Erase all flash:

esptool.py --port COM3 erase_flash

Flash a pre-built firmware (three files at their specific addresses):

esptool.py --port COM3 --baud 921600 write_flash 
  0x1000  bootloader.bin 
  0x8000  partitions.bin 
  0x10000 firmware.bin

The three addresses are standard for the ESP32: 0x1000 for the bootloader, 0x8000 for the partition table, and 0x10000 for the application. You can find the exact addresses by enabling verbose upload output in the Arduino IDE (File → Preferences → Show verbose output during upload) and reading the esptool.py command it runs.

Method 5: Web Browser-Based OTA with ElegantOTA

ElegantOTA is a library that adds a web-based update page to your ESP32. Navigate to the ESP32’s IP address in any browser, select a .bin file, and upload — no IDE required, works from any device including smartphones. Install from the Arduino library manager. This approach is excellent for field firmware updates on deployed devices.

Troubleshooting Upload Failures

“Connecting…” hangs indefinitely

The IDE cannot reach the ESP32 bootloader. Check: USB cable (try a different one); driver installation (Device Manager on Windows); correct COM port selected; board not held in a crash loop by a malfunctioning sketch (hold BOOT before clicking Upload to override). If the board previously uploaded fine and now does not, the sketch may have disabled UART or used GPIO 0 as an output that holds it LOW.

“A fatal error occurred: Invalid head of packet”

Communication noise or baud rate mismatch during connection. The ESP32 started negotiating at one speed but the PC expects another. Fix: reduce upload speed to 115200 in Tools → Upload Speed. Also check for power supply instability causing glitches during the negotiation phase.

“MD5 of file does not match data in flash”

Data written to flash did not match the source binary — the flash write was corrupted. This suggests either a hardware problem (power instability during write, damaged flash chip) or USB communication errors (long cable, noisy USB hub). Try: shorter cable, direct laptop USB port (not hub), and lower upload speed.

Upload succeeds but sketch does not run correctly

If the IDE reports “Hash of data verified” but the sketch misbehaves, the firmware was written correctly — the problem is in the sketch logic itself. Open the Serial Monitor immediately after upload to catch any crash messages or panic outputs before they scroll away.

OTA Best Practices for Deployed Devices

For production devices that will receive OTA updates in the field, always use a dual-partition OTA layout (the default “OTA” partition scheme in the IDE). This allocates two equal app partitions: the currently running firmware and the update slot. The OTA process writes the new firmware to the unused slot, verifies it with a CRC check, then switches the boot partition pointer. If the new firmware fails to boot within a configured timeout, the device automatically rolls back to the previous working firmware. This rollback mechanism prevents a bad OTA update from permanently bricking a deployed device.

Frequently Asked Questions

Projects to Build

Put this knowledge to work — try one of these hands-on projects.