Complete ESP Flashing Flow: From Chip Detection to Hard Reset in esp_flasher

The esp_flasher tool executes a 14-step sequence that auto-detects the ESP chip, uploads a flashing stub to RAM, writes the firmware image, and concludes with a hard reset via RTS/EN pin toggling to boot the new firmware.

The jason2866/esp_flasher repository provides a streamlined Python utility for programming ESP32 and ESP8266 microcontrollers. Understanding the complete flashing flow from chip detection to hard reset helps developers diagnose connection failures and optimize upload speeds. This guide traces the exact execution path through the source code, from CLI parsing to the final hardware reset.

CLI Argument Parsing and Port Selection

The process begins in esp_flasher/__main__.py where the parse_args() function processes command-line flags including --port, chip-family selectors (--esp32*), and baud-rate options.

If the --port argument is omitted, the select_port() function auto-detects a single available USB/COM device. This ensures the tool can run without manual port specification when only one ESP device is connected.

Chip Detection and Information Retrieval

The detect_chip() function in esp_flasher/common.py establishes the initial connection. It either forces a specific ROM class via the --esp32* flags or queries the ESP's ROM directly through ESPLoader.detect_chip to identify the chip family.

After detection, the tool opens a no-reset connection to keep the device in bootloader mode. The read_chip_info() function then retrieves the MAC address, model name, core count, and flash capabilities from the chip.

Stub Upload and Baud Rate Configuration

With the chip identified, chip_run_stub() in common.py calls chip.run_stub() to upload the flashing stub into RAM. This stub provides higher-level flash commands and faster data transfer than the ROM bootloader.

If the user specified a custom upload speed via --upload-baud-rate, the change_baud() function in esp_flasher/own_esptool.py attempts to raise the serial rate. If the chip rejects the requested rate, the connection automatically rebuilds at the default 115200 baud.

Flash Size Detection and Argument Preparation

The detect_flash_size() function reads the flash ID via the stub and maps it to a human-readable capacity. Meanwhile, configure_write_flash_args() in common.py builds a MockEsptoolArgs object containing the binary data and flash-layout parameters.

For ESP32 targets, if the input is an ELF file rather than a binary, esptool.elf2image() converts it to flashable bin format before proceeding, as implemented in the run_esp_flasher() function in __main__.py.

Flash Parameter Setup and Erasure

Before writing, flash_set_parameters() in own_esptool.py configures the stub with the correct flash size, mode, and frequency settings. These parameters ensure subsequent write commands use the proper timing and layout.

Unless the --no-erase flag is present, erase_flash() issues the ESP_ERASE_FLASH command through the stub to clear the chip's memory.

Firmware Write and Verification

The write_flash() function in own_esptool.py streams the prepared binary blocks to the device. This function includes automatic retry logic for failed blocks, ensuring reliable transmission even over unstable serial connections.

Hard Reset and Post-Flash Cleanup

After the write completes, hard_reset() in own_esptool.py toggles the RTS pin to pulse the EN line low then high. This sequence resets the ESP into normal execution mode, launching the newly flashed firmware.

If a high baud rate was used for the upload, the port returns to 115200 baud, flushes remaining data, and optionally streams device logs via show_logs() in the final block of run_esp_flasher().

Practical Usage Examples

Flash a binary to an ESP32 with auto-detection and default erasing:

esp_flasher my_firmware.bin

Flash with a custom upload speed and automatic fallback:

esp_flasher --upload-baud-rate 3000000 my_firmware.bin

Flash an ELF bootloader for ESP32-S3 with partition tables:

esp_flasher \
    --esp32s3 \
    --bootloader bootloader.bin \
    --partitions partitions.csv \
    --otadata otadata.bin \
    --input bootloader.elf \
    factory_image.bin

Summary

  • The flashing flow starts with CLI parsing in __main__.py and auto-detects the serial port via select_port() if not specified.
  • detect_chip() in common.py identifies the ESP family and opens a no-reset connection to the bootloader.
  • chip_run_stub() uploads a RAM stub that enables high-speed flashing commands beyond the ROM's capabilities.
  • The tool automatically detects flash size, converts ELF files to binary when necessary, and configures flash parameters via flash_set_parameters().
  • erase_flash() clears memory (unless skipped with --no-erase), followed by write_flash() streaming data with automatic retry logic.
  • hard_reset() toggles the EN pin to boot the new firmware, after which the port returns to 115200 baud for log streaming.

Frequently Asked Questions

How does esp_flasher detect which ESP chip model is connected?

The detect_chip() function in esp_flasher/common.py either uses the forced ROM class from --esp32* flags or queries the chip's ROM directly via ESPLoader.detect_chip. It maintains a no-reset connection to keep the device in bootloader mode during this process.

What happens if the requested upload baud rate is not supported?

If change_baud() in esp_flasher/own_esptool.py fails to negotiate the higher speed, the connection automatically rebuilds at the default 115200 baud rate. The flashing continues at this safe speed rather than failing entirely.

Why does esp_flasher upload a stub to RAM before flashing?

The chip_run_stub() function uploads a small program into the ESP's RAM because the ROM bootloader has limited functionality and slower transfer rates. The stub provides optimized flash commands and handles high-speed serial communication more efficiently than the built-in ROM routines.

What triggers the hard reset at the end of the flashing process?

The hard_reset() function in esp_flasher/own_esptool.py toggles the RTS control line, which pulses the EN (enable) pin low then high. This hardware reset sequence exits the bootloader and starts execution of the newly written firmware from address 0x0.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →