The Arduino compiled binary format for classic AVR boards (like the Uno and Mega) is the Intel HEX format (.hex), an ASCII text-based representation of binary machine code. For ARM and ESP32/ESP8266 boards, the format shifts to a raw binary format (.bin). Understanding the exact structure of these files is critical when you move beyond the Arduino IDE's "Upload" button and start dealing with custom bootloaders, over-the-air (OTA) updates, or external flashing tools like avrdude and esptool.

When a flash fails or a checksum mismatches, knowing how to parse the HEX record structure byte-by-byte is the difference between a quick fix and a bricked board. Below, we break down the format specification, build a hardware HEX validator, and troubleshoot the most common flash errors.

Decoding the Format: Intel HEX vs. Raw BIN

The Arduino IDE uses the GCC toolchain to compile your C++ sketch into an ELF file, which is then converted into the final flashable format. The format depends entirely on the target microcontroller architecture.

Feature Intel HEX (.hex) Raw Binary (.bin)
Target MCUs AVR (ATmega328P, ATmega2560) ESP32, ESP8266, ARM Cortex-M (Zero, Due)
Data Encoding ASCII Hexadecimal (Human-readable text) Raw 8-bit binary bytes
Addressing Explicit address per record line Implicit (offset from file start)
Overhead High (~2x file size due to ASCII + metadata) None (1:1 with flash memory size)
Flashing Tool avrdude esptool, bossac, openocd

An Intel HEX file is composed of lines called "records." Every record follows a strict syntax. Let's dissect a standard data record:

:10010000214601360121470136007EFE09D2190140
  • : Start code (always a colon).
  • 10 Byte count (16 bytes of data in this record).
  • 0100 Address (0x0100 in flash memory).
  • 00 Record type (00 = Data, 01 = End of File, 02 = Extended Segment).
  • 2146...01 The actual 16 bytes of compiled machine code.
  • 40 Checksum (Two's complement of the sum of all preceding bytes).
Bench Tip: If you open a .hex file in a text editor and the line endings are corrupted (e.g., missing Carriage Returns when moving between Windows and Linux), avrdude will often throw a parsing error before it even attempts to flash. Always transfer HEX files in binary/zip mode, not ASCII FTP mode.

Project Build: SD Card Intel HEX Validator

To prove we understand the format, we will build a hardware validator. This device reads an arduino compiled binary format HEX file from an SD card, parses the records, verifies the checksums, and outputs a structural report to the Serial Monitor.

Target Board: Arduino Uno R3 (ATmega328P) or compatible clone.
Difficulty: Intermediate | Time: 45 Minutes

Parts List

  • 1x Arduino Uno R3 (Rev3) with ATmega328P
  • 1x MicroSD Card Breakout Board (Adafruit 254 or generic with 3.3V/5V level shifters)
  • 1x 8GB MicroSDHC Card (SanDisk or Kingston, formatted FAT32)
  • Jumper wires (Dupont male-to-female)

Pin Mapping Table

SD Breakout Pin Arduino Uno R3 Pin Function
VCC5VPower (if module has onboard regulator/level shifter)
GNDGNDCommon Ground
MISO12SPI Master In, Slave Out
MOSI11SPI Master Out, Slave In
SCK13SPI Clock
CS10Chip Select (Configurable)

Complete Validator Code with Error Handling

This sketch uses the standard SD and SPI libraries. It includes strict error handling for malformed lines, invalid start codes, and checksum failures. Note: Ensure your SD card contains a file named firmware.hex in the root directory before running.

#include <SPI.h>
#include <SD.h>

// --- Pin Definitions ---
const int CS_PIN = 10;
const int LED_PIN = 13; // Onboard LED for status

// --- Global Stats ---
unsigned long totalBytes = 0;
int recordCount = 0;
int errorCount = 0;

// Helper: Convert ASCII hex char to integer
uint8_t hexToByte(char c) {
  if (c >= '0' && c <= '9') return c - '0';
  if (c >= 'A' && c <= 'F') return c - 'A' + 10;
  if (c >= 'a' && c <= 'f') return c - 'a' + 10;
  return 0xFF; // Error
}

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; } // Wait for serial port
  pinMode(LED_PIN, OUTPUT);

  Serial.println(F("--- Arduino HEX Format Validator ---"));

  if (!SD.begin(CS_PIN)) {
    Serial.println(F("[FATAL] SD Card initialization failed!"));
    Serial.println(F("Check wiring, CS pin, and ensure FAT32 format."));
    blinkError();
  }

  if (!SD.exists("firmware.hex")) {
    Serial.println(F("[FATAL] firmware.hex not found in root directory."));
    blinkError();
  }

  parseHexFile("firmware.hex");
}

void loop() {
  // Validation is a one-shot process in setup
}

void parseHexFile(const char* filename) {
  File hexFile = SD.open(filename, FILE_READ);
  if (!hexFile) {
    Serial.println(F("[ERROR] Could not open file."));
    return;
  }

  char lineBuffer[128];
  int bufferIdx = 0;

  Serial.println(F("Parsing records..."));

  while (hexFile.available()) {
    char c = hexFile.read();
    
    // Handle line endings (CRLF or LF)
    if (c == '\n' || c == '\r') {
      if (bufferIdx > 0) {
        lineBuffer[bufferIdx] = '\0';
        processRecord(lineBuffer);
        bufferIdx = 0;
      }
      if (c == '\r') {
        // Peek next char to consume \n if present
        if (hexFile.peek() == '\n') hexFile.read();
      }
    } else {
      if (bufferIdx < 127) {
        lineBuffer[bufferIdx++] = c;
      }
    }
  }
  
  // Catch last line if no trailing newline
  if (bufferIdx > 0) {
    lineBuffer[bufferIdx] = '\0';
    processRecord(lineBuffer);
  }

  hexFile.close();
  printSummary();
}

void processRecord(char* line) {
  if (line[0] != ':') {
    Serial.print(F("[WARN] Missing start code on line: "));
    Serial.println(line);
    errorCount++;
    return;
  }

  int len = strlen(line);
  if (len < 11) { // Minimum: : + 2(bytecount) + 4(addr) + 2(type) + 2(checksum)
    Serial.println(F("[ERROR] Record too short."));
    errorCount++;
    return;
  }

  uint8_t byteCount = (hexToByte(line[1]) << 4) | hexToByte(line[2]);
  uint8_t recordType = (hexToByte(line[7]) << 4) | hexToByte(line[8]);
  
  // Calculate Checksum
  uint8_t sum = byteCount;
  sum += (hexToByte(line[3]) << 4) | hexToByte(line[4]); // Addr High
  sum += (hexToByte(line[5]) << 4) | hexToByte(line[6]); // Addr Low
  sum += recordType;

  for (int i = 0; i < byteCount; i++) {
    int idx = 9 + (i * 2);
    sum += (hexToByte(line[idx]) << 4) | hexToByte(line[idx+1]);
  }

  uint8_t expectedChecksum = (hexToByte(line[9 + byteCount * 2]) << 4) | hexToByte(line[10 + byteCount * 2]);
  uint8_t calcChecksum = (~sum + 1) & 0xFF; // Two's complement

  if (calcChecksum != expectedChecksum) {
    Serial.print(F("[FAIL] Checksum mismatch! Calc: 0x"));
    Serial.print(calcChecksum, HEX);
    Serial.print(F(" Expected: 0x"));
    Serial.println(expectedChecksum, HEX);
    errorCount++;
  } else {
    totalBytes += byteCount;
    recordCount++;
    if (recordType == 0x01) {
      Serial.println(F("[OK] End of File (EOF) record found and verified."));
    }
  }
}

void printSummary() {
  Serial.println(F("\n--- Validation Summary ---"));
  Serial.print(F("Total Records: ")); Serial.println(recordCount);
  Serial.print(F("Total Payload Bytes: ")); Serial.println(totalBytes);
  Serial.print(F("Parsing Errors: ")); Serial.println(errorCount);
  
  if (errorCount == 0) {
    Serial.println(F("STATUS: PASS - File is structurally valid."));
    digitalWrite(LED_PIN, HIGH);
  } else {
    Serial.println(F("STATUS: FAIL - Do not flash this file!"));
    blinkError();
  }
}

void blinkError() {
  while(1) {
    digitalWrite(LED_PIN, HIGH); delay(150);
    digitalWrite(LED_PIN, LOW); delay(150);
  }
}

Troubleshooting: Flash Failures and Exact Error Strings

When you attempt to flash an arduino compiled binary format file via the command line using avrdude, things can go wrong. Here are the exact error strings you will encounter, ranked by probability, and how to fix them.

Error 1: avrdude: verification error, first mismatch at byte 0x0000

What it means: Avrdude wrote the data to the flash, but when it read it back to verify, the first byte didn't match the HEX file.

  1. Corrupted HEX File: The file was altered during transfer (e.g., line endings changed from CRLF to LF, breaking the byte count). Fix: Re-export the HEX file directly from the Arduino IDE output folder.
  2. Wrong Target Signature: You are flashing an ATmega328P HEX file onto an ATmega168 or ATmega328 (non-P). Fix: Add -F to force, but ideally correct the -p part flag in your avrdude command.
  3. Failing Flash Memory: The physical AVR chip has degraded flash cells (common on clones subjected to high heat). Fix: Replace the microcontroller.

Error 2: avrdude: stk500_recv(): programmer is not responding

What it means: The PC cannot establish the STK500 serial protocol handshake with the bootloader.

  1. Charge-Only USB Cable: The most common bench mistake. Your cable has power lines but no D+/D- data lines. Fix: Swap to a verified data cable.
  2. Wrong COM Port / Baud Rate: The bootloader expects 115200 baud (Optiboot) or 57600 (older Duemilanove). Fix: Check Device Manager and specify -b 115200 in avrdude.
  3. Stuck in Auto-Reset: A capacitor on the DTR line is failing, holding the MCU in reset. Fix: Manually press the reset button exactly when the "Uploading..." text appears.
The First 3 Things to Check When a Flash Fails: 1. Verify the USB cable supports data transfer (test by reading serial output). 2. Confirm the IDE/CLI is targeting the exact board variant and correct COM port. 3. Open the .hex file in a hex editor (like HxD) to ensure it starts with a colon (:) and hasn't been zeroed out.

Extending and Simplifying the Build

The SD card validator is a great diagnostic tool, but you can adapt it to fit different project constraints.

How to Extend the Build

  • Add OTA Capability: Integrate an ESP8266 WiFi module via UART. Have the Arduino download the HEX file from an MQTT broker or HTTP server, write it to the SD card, validate it with this sketch, and then trigger a secondary bootloader to flash the main MCU.
  • LCD Output: Wire an I2C 16x2 LCD (address 0x27) to display "PASS" or "FAIL" and the total byte count, turning this into a standalone manufacturing-line QA tool.

How to Simplify the Build

  • Skip Parsing: If you only need to verify that a file downloaded completely, drop the record parsing. Just use hexFile.size() and compare it against the expected file size from the server's Content-Length header.
  • Use ESP32 Instead: If you move to an ESP32, you bypass the Intel HEX format entirely. The ESP32 uses raw .bin files, and the Arduino build process handles the partition mapping automatically via esptool.

Frequently Asked Questions

What is the exact structure of an Arduino compiled binary format HEX file?

It is an ASCII text file using the Intel HEX specification. Each line is a "record" starting with a colon (:), followed by a byte count, a 16-bit memory address, a record type (00 for data, 01 for EOF), the actual payload data in hex pairs, and a single-byte two's complement checksum. It is not a raw binary dump; it is a structured text protocol designed for reliable serial transmission.

How do I convert an Arduino compiled binary format from HEX to BIN?

You can use the open-source tool objcopy (included with the Arduino AVR toolchain) or a utility like srec_cat. The command line for objcopy is: avr-objcopy -I ihex -O binary firmware.hex firmware.bin. This strips the addressing and checksum metadata, leaving only the raw payload bytes. Note that the resulting .bin file will start at address 0x0000, which may include the bootloader section depending on your linker script.

Why does my ESP32 use a .bin instead of the Arduino compiled binary format .hex?

The Intel HEX format was designed for older, smaller memory spaces where explicit addressing was necessary to skip unprogrammed regions (like fuse bytes or bootloader gaps). The ESP32 uses a complex flash partition table (bootloader, OTA partitions, SPIFFS/LittleFS). Raw .bin files are mapped to specific flash offsets (e.g., 0x10000 for the app partition) by the esptool flashing utility, making the overhead of ASCII HEX encoding unnecessary and inefficient for multi-megabyte firmware images.

Can I decompile an Arduino compiled binary format back to C++ code?

No, you cannot perfectly decompile it back to original C++ source code. The compilation process destroys variable names, comments, and high-level logic structures, reducing everything to AVR machine instructions. You can use tools like avr-objdump -d firmware.elf to generate Assembly language listings, or use Ghidra to reverse-engineer the logic, but recovering the exact original .ino file is impossible.