How esp_flasher Automatically Configures Flash Mode, Frequency, and Size Parameters

esp_flasher queries the ESP chip's flash ID to determine capacity, parses the firmware header to extract mode and frequency, and selects a matching bootloader binary—all without manual intervention unless overridden via CLI flags.

When flashing ESP8266 and ESP32 devices, correctly configuring the flash mode (QIO/DIO), frequency (40MHz/80MHz), and size parameters is critical for successful boot. The jason2866/esp_flasher tool automates this detection through a multi-stage pipeline that inspects hardware IDs, parses binary headers, and conditionally rewrites configuration bytes before transmission.

Detecting Flash Size from Hardware IDs

When you omit the --flash_size argument, esp_flasher interrogates the target chip directly to determine storage capacity. In esp_flasher/own_esptool.py, the detect_flash_size() function calls esp.flash_id() to retrieve the raw flash identification number.

def detect_flash_size(esp, args):
    if args.flash_size == 'detect':
        if esp.secure_download_mode:
            raise FatalError("Detecting flash size …")
        flash_id = esp.flash_id()
        size_id = flash_id >> 16
        args.flash_size = DETECTED_FLASH_SIZES.get(size_id)   # → e.g. '4MB'

The function right-shifts the flash ID by 16 bits to isolate the size nibble, then maps that value to a human-readable string (e.g., 4MB, 8MB) using the DETECTED_FLASH_SIZES dictionary. This detected value propagates through the flashing workflow, ensuring the partition table and bootloader align with physical hardware limits.

Parsing Flash Mode and Frequency from Firmware Headers

Before transmission, esp_flasher inspects the firmware binary to extract the intended flash mode and frequency settings. The read_firmware_info() function in esp_flasher/common.py performs this analysis by examining the 4-byte ESP image header.

Inspecting the Safeboot Offset

The tool first seeks to offset 0x10000 to check for a safeboot (factory) image header:

def read_firmware_info(firmware):
    firmware.seek(0x10000)                     # safeboot image?

    header = firmware.read(4)
    magic, _, flash_mode_raw, flash_size_freq = struct.unpack("BBBB", header)
    if magic == esptool.ESPLoader.ESP_IMAGE_MAGIC:
        flash_freq_raw = flash_size_freq & 0x0F
        flash_mode = {0: "qio", 1: "qout", 2: "dio", 3: "dout"}.get(flash_mode_raw)
        flash_freq = {0: "40m", 1: "26m", 2: "20m", 0xF: "80m"}.get(flash_freq_raw)
        flag_factory = True
        return flash_mode, flash_freq, flag_factory
    # … second try at offset 0 for normal images

The header bytes decode as follows: byte 2 contains the raw flash mode (0-3 mapping to qio/qout/dio/dout), while byte 3 contains the size/frequency composite. The lower nibble (masked with 0x0F) represents frequency, and the upper nibble represents size.

Factory Image Detection

If the magic byte matches ESP_IMAGE_MAGIC at offset 0x10000, the function sets flag_factory = True. This boolean flag signals to downstream logic that the image is factory-calibrated and its flash parameters should remain untouched to preserve manufacturer optimizations.

Rewriting Flash Parameters Before Flashing

For non-factory images—or when CLI overrides are specified—esp_flasher modifies the binary's flash-parameter bytes immediately before transmission. The _update_image_flash_params() function in esp_flasher/own_esptool.py handles this mutation.

def _update_image_flash_params(esp, address, args, image):
    # … sanity checks …

    magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4])
    if args.flash_mode != 'keep':
        flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode]
    flash_freq = flash_size_freq & 0x0F
    if args.flash_freq != 'keep':
        flash_freq = esp.parse_flash_freq_arg(args.flash_freq)
    flash_size = flash_size_freq & 0xF0
    if args.flash_size != 'keep':
        flash_size = esp.parse_flash_size_arg(args.flash_size)

    flash_params = struct.pack(b'BB', flash_mode, flash_size + flash_freq)
    if flash_params != image[2:4]:
        print('Flash params set to 0x%04x' % struct.unpack(">H", flash_params))
        image = image[0:2] + flash_params + image[4:]
    return image

The function uses struct.pack('BB', ...) to reassemble the two-byte parameter field (bytes 2-3 of the header). If the packed value differs from the original header, the tool prints a diagnostic message and returns the modified image buffer. This ensures the ESP chip receives configuration bytes matching either the auto-detected hardware capabilities or the user's explicit CLI instructions.

Selecting the Matching Bootloader Binary

After determining the flash mode and frequency, esp_flasher must load a compatible bootloader stub. In esp_flasher/common.py, the configure_write_flash_args() function constructs the bootloader filename dynamically:

flash_mode, flash_freq, flag_factory = read_firmware_info(firmware)
…
bootloaderstring = "bootloader_" + flash_mode + "_" + flash_freq + ".elf"

This string concatenation produces filenames such as bootloader_dio_40m.elf or bootloader_qio_80m.elf, ensuring the stub loader matches the target flash configuration. The tool ships with pre-compiled bootloader binaries for all valid mode/frequency combinations in the repository's bootloader/ directory tree.

Command-Line Overrides and the 'keep' Option

While esp_flasher automatically configures flash parameters, you retain full manual control via three CLI arguments:

  • --flash_mode: Accepts qio, qout, dio, dout, or keep
  • --flash_freq: Accepts 40m, 26m, 20m, 80m, or keep
  • --flash_size: Accepts specific sizes (e.g., 4MB, 8MB), detect, or keep

When any parameter is set to keep, the tool preserves the value extracted from the firmware header. For example, to force DIO mode while auto-detecting size and preserving the original frequency:

esp_flasher -p /dev/ttyUSB0 -b 921600 --flash_mode dio firmware.bin

To flash a binary verbatim without modifying header bytes:

esp_flasher -p /dev/ttyUSB0 -b 921600 \
  --flash_mode keep --flash_freq keep --flash_size keep firmware.bin

Summary

  • Flash size is auto-detected by querying the chip's flash ID via detect_flash_size() in esp_flasher/own_esptool.py unless overridden with --flash_size.
  • Flash mode and frequency are extracted from the firmware header at offset 0x10000 (safeboot) or offset 0 by read_firmware_info() in esp_flasher/common.py.
  • Factory images (detected by magic byte) bypass parameter rewriting to preserve calibration data via the flag_factory boolean.
  • Parameter rewriting occurs in _update_image_flash_params(), which uses struct.pack('BB', ...) to inject the final 2-byte configuration into the binary header.
  • Bootloader selection is determined by concatenating the detected mode and frequency into a filename pattern (bootloader_<mode>_<freq>.elf) within configure_write_flash_args().

Frequently Asked Questions

How does esp_flasher detect the flash size if I don't specify it?

According to the jason2866/esp_flasher source code, the tool calls esp.flash_id() to retrieve the hardware identification number, then isolates the size nibble by shifting right 16 bits (flash_id >> 16). This value maps to a capacity string (e.g., 4MB, 8MB) in the DETECTED_FLASH_SIZES dictionary defined in esp_flasher/own_esptool.py.

What happens if the firmware is a factory-calibrated image?

When read_firmware_info() in esp_flasher/common.py detects the ESP_IMAGE_MAGIC byte at offset 0x10000, it returns flag_factory = True. This signals the flashing pipeline to skip the _update_image_flash_params() rewriting step, preserving the manufacturer's original flash settings to maintain calibration integrity.

Can I override the flash mode while keeping auto-detected size and frequency?

Yes. Pass --flash_mode dio (or qio, qout, dout) while omitting --flash_size and --flash_freq. The tool will auto-detect the size from hardware, parse the frequency from the firmware header, and only override the mode field in the final 2-byte parameter block.

Where does esp_flasher get the bootloader binary from?

The tool constructs the filename by concatenating the detected flash mode and frequency (e.g., bootloader_dio_40m.elf) in configure_write_flash_args() within esp_flasher/common.py. It then loads this pre-compiled ELF file from the bootloader/ directory tree bundled with the repository, ensuring the stub matches the target flash configuration.

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 →