The arduino .ino.merged.bin file is the final, unified firmware image containing the ESP32/ESP8266 bootloader, partition table, and compiled application sketch. While the Arduino IDE typically flashes these segments individually to their respective memory offsets during development, generating a single merged binary is mandatory for mass production flashing, Over-The-Air (OTA) payload delivery, and debugging complex boot failures. If you are dealing with a bricked board or setting up a manufacturing jig, understanding this file is non-negotiable.

Difficulty Rating: Intermediate | Target Board: ESP32 DevKit V1 (ESP32-WROOM-32, 38-pin) | Time to Complete: 45 Minutes

What is the .ino.merged.bin File in Arduino?

When you click "Upload" in the Arduino IDE for an ESP32, the underlying build system (arduino-cli) compiles your C++ code into an ELF file, converts it to a raw binary, and uses esptool.py to write three distinct files to the flash memory:

  1. Bootloader: Usually flashed at offset 0x1000. It initializes the hardware and finds the active app partition.
  2. Partition Table: Flashed at 0x8000. It maps out where the app, OTA slots, and SPIFFS/LittleFS data live.
  3. Application Binary: Your actual .ino compiled code, typically starting at 0x10000.

The .ino.merged.bin file stitches these three binaries into a single contiguous image, padding the empty space between offsets with 0xFF (erased flash state). This allows you to flash the entire chip in one pass using a single offset of 0x0.

Parts List & Pin Mapping for ESP32 Flash Test

To reliably test merged binaries outside of the Arduino IDE's automated environment, we use a dedicated USB-to-TTL adapter. This bypasses the onboard USB-to-UART bridge, which can sometimes interfere with manual strapping pin control.

Required Hardware

  • MCU: ESP32 DevKit V1 (38-pin variant, ESP32-WROOM-32 module)
  • Programmer: CP2102 USB-to-TTL Serial Converter Module (with DTR/RTS pins broken out)
  • Passives: 10kΩ resistor (x2), 100nF ceramic capacitor (x2)
  • Wiring: 22 AWG solid core jumper wires, half-size breadboard

Auto-Reset & Flash Mode Pin Mapping

Manual flashing requires precise control of the EN (Enable) and GPIO0 pins to force the ESP32 into the serial bootloader. Wire the CP2102 to the ESP32 as follows:

CP2102 Pin ESP32 Pin Function / Notes
TXD RX0 (GPIO3) Serial data to ESP32
RXD TX0 (GPIO1) Serial data from ESP32
DTR EN (via 100nF cap) Auto-reset circuit (pulse EN low)
RTS GPIO0 (via 100nF cap) Auto-flash circuit (pulse GPIO0 low)
3.3V 3V3 Power (Do NOT use 5V for logic)
GND GND Common ground reference
Callout Tip: If your CP2102 lacks DTR/RTS pins, you must manually hold the BOOT button (GPIO0 to GND) while pressing the EN/RST button to enter flash mode before running the esptool command.

Compiling and Extracting the Merged Binary

The Arduino IDE does not output a .merged.bin by default in the standard sketch folder. You must extract the intermediate build files and merge them manually, or use the Arduino CLI with custom build properties.

Step-by-Step Manual Merge via esptool

  1. Enable Verbose Output: In Arduino IDE 2.x, go to File > Preferences and check "Show verbose output during: compilation".
  2. Compile (Do Not Upload): Click the Verify/Compile button. Watch the black console window at the bottom.
  3. Locate the Temp Directory: Look for a line ending in esp32.map. The path preceding it is your temporary build folder (e.g., /tmp/arduino_build_837492/).
  4. Identify the Three Files: Inside that folder, locate bootloader.bin, partitions.bin, and [sketch_name].ino.bin.
  5. Run the Merge Command: Open your terminal and execute the esptool.py merge command:
esptool.py --chip esp32 merge_bin -o firmware_merged.bin \
  --flash_mode dio --flash_freq 80m --flash_size 4MB \
  0x1000 bootloader.bin \
  0x8000 partitions.bin \
  0x10000 sketch_name.ino.bin

Flashing and Debugging Boot Failures

Below is a robust, compilable test sketch designed for the ESP32 DevKit V1. It includes I2C bus error handling and Serial timeouts to verify that the merged binary booted correctly and the hardware interfaces are responsive.

/*
 * Target Board: ESP32 DevKit V1 (ESP32-WROOM-32)
 * Purpose: I2C Bus & Serial Verification for Merged Bin Testing
 */

#include 

// Pin Definitions for ESP32 DevKit V1
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define STATUS_LED_PIN 2
#define SERIAL_BAUD 115200

void setup() {
  // Initialize Status LED
  pinMode(STATUS_LED_PIN, OUTPUT);
  digitalWrite(STATUS_LED_PIN, LOW);

  // Initialize Serial with timeout error handling
  Serial.begin(SERIAL_BAUD);
  unsigned long serialTimeout = millis() + 2000;
  while (!Serial && millis() < serialTimeout) {
    delay(10); // Wait for Serial monitor or timeout
  }
  
  Serial.println("\n--- ESP32 Merged Bin Boot Verification ---");
  Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());

  // Initialize I2C with explicit pin mapping and error check
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  Wire.setTimeout(100); // 100ms I2C timeout to prevent hard locks
  
  if (checkI2CBus()) {
    Serial.println("[OK] I2C Bus responsive.");
    digitalWrite(STATUS_LED_PIN, HIGH); // Solid LED = Success
  } else {
    Serial.println("[ERR] I2C Bus locked or unresponsive. Check pull-ups.");
    blinkErrorPattern(); // Blink LED = Hardware Fault
  }
}

void loop() {
  // Main loop idle for testing
  delay(1000);
}

bool checkI2CBus() {
  byte error, address;
  int deviceCount = 0;
  for (address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) deviceCount++;
    if (error == 4) { // Error 4 = SDA/SCL shorted or missing pull-ups
      return false; 
    }
  }
  return true; // Bus is clear, even if no devices are found
}

void blinkErrorPattern() {
  for (int i = 0; i < 5; i++) {
    digitalWrite(STATUS_LED_PIN, HIGH);
    delay(100);
    digitalWrite(STATUS_LED_PIN, LOW);
    delay(100);
  }
}

Debugging Exact Error Strings

When flashing or booting the .ino.merged.bin file, you will inevitably encounter esptool or ROM bootloader errors. Here is how to diagnose the most common failures.

Error 1: A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header

This means the host PC cannot establish a UART handshake with the ESP32 ROM bootloader. The first three things to check when it fails:

  1. Strapping Pin State: Verify GPIO0 is physically pulled LOW during the EN reset pulse. If using manual buttons, ensure you release EN before releasing BOOT.
  2. USB Cable Integrity: Swap the cable. Many micro-USB cables are charge-only and lack the D+/D- data lines required for UART communication.
  3. COM Port Selection: Ensure your OS hasn't assigned the CP2102 to a port currently locked by another serial monitor instance.

Error 2: rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) followed by flash read err, 1000 and ets_main.c bootloop

This is the classic "corrupt merged binary" bootloop. The bootloader found the partition table, but the application header is invalid. Ranked causes:

  1. Incorrect Flash Offsets: You merged the bins but flashed the resulting firmware_merged.bin to 0x1000 instead of 0x0. A merged bin must always be flashed to the absolute zero offset.
  2. Flash Size Mismatch: The --flash_size 4MB flag in the merge command did not match the physical SPI flash chip on your specific DevKit board (some clones use 8MB or 16MB).
  3. Partition Table Corruption: The partitions.bin used in the merge was generated for a different board definition (e.g., "Default 4MB with spiffs" vs "Huge APP").

Extending and Simplifying Your Embedded Build

Once you have a stable .ino.merged.bin workflow, you can manipulate the build to suit different production needs.

How to extend the build: To add file system support, modify the Arduino IDE partition scheme to "Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)". You will then need to generate a spiffs.bin using the ESP-IDF build tools and append it to your esptool merge_bin command at offset 0x290000. This is highly useful for shipping devices with pre-loaded web server assets or configuration JSON files.

How to simplify the build: If your sketch is hitting the 1.2MB app limit and throwing "Sketch too big" errors, change the partition scheme to Huge APP (3MB No OTA/1MB SPIFFS). This eliminates the secondary OTA partition, giving your .ino.bin a massive 3MB of contiguous space. The trade-off is that you lose the ability to perform safe, dual-bank Over-The-Air updates in the field.

Frequently Asked Questions (FAQ)

Where does Arduino IDE save the .ino.merged.bin file?

By default, the Arduino IDE 2.x does not generate or save a .ino.merged.bin file. It only saves the individual .ino.bin, bootloader.bin, and partitions.bin in a hidden temporary directory (e.g., /tmp/arduino_build_xxxxx on Linux/Mac or %TEMP%\arduino_build_xxxxx on Windows). You must manually merge them using esptool.py as shown above, or use a third-party plugin like "ESP32 Sketch Data Upload" which sometimes outputs a merged image for SPIFFS workflows.

Can I flash the .ino.merged.bin file via Arduino OTA?

No. The standard Arduino OTA library (and the underlying ESP32 Update library) expects only the application binary (.ino.bin). The OTA process writes strictly to the active OTA application partition (e.g., ota_0 or ota_1). If you attempt to push a .merged.bin via OTA, you will overwrite the partition table and bootloader boundaries, instantly bricking the device and forcing a physical USB recovery.

Why is my .ino.merged.bin file larger than the standard .bin?

The standard .ino.bin is only your compiled application code, typically ranging from 300KB to 900KB. The .ino.merged.bin includes the bootloader (~25KB), the partition table (~3KB), and crucially, the 0xFF padding bytes required to bridge the gaps between the memory offsets (e.g., the empty space between 0x1000 and 0x8000, and 0x8000 to 0x10000). Therefore, a merged binary will always be slightly larger than 0x10000 (64KB) plus the size of your application code.

How to fix esptool merge_bin offset errors?

If esptool.py throws an error stating Address 0x8000 overlaps with previous address, it means your input offsets are out of order or overlapping. esptool requires the offset arguments to be listed in strictly ascending numerical order. Always list the bootloader (0x1000) first, followed by the partition table (0x8000), and finally the application (0x10000). Additionally, ensure you are using the correct --flash_size parameter; specifying a 4MB flash size while providing offsets meant for a 16MB layout will cause immediate overlap rejections.