fix: Do not use padding for merged IntelHex files

Split merged hex files into multiple temporary binary files for
commands like `write_flash` and `image_info` and others that support
IntelHex files.

Splitting is done based on the gaps in the addresses.
This should prevent overriding the flash region between the files.

Closes https://github.com/espressif/esptool/issues/1075
This commit is contained in:
Peter Dragun
2025-04-07 13:56:14 +02:00
committed by Radim Karniš
parent 98688abde1
commit 739669ffca
4 changed files with 90 additions and 24 deletions

View File

@@ -281,6 +281,14 @@ Intel Hex format offers distinct advantages when compared to the binary format,
* **Size**: Data is carefully allocated to specific memory addresses eliminating the need for unnecessary padding. Binary images often lack detailed addressing information, leading to the inclusion of data for all memory locations from the file's initial address to its end.
* **Validity Checks**: Each line in an Intel Hex file has a checksum to help find errors and make sure data stays unchanged.
When using a merged Intel Hex file with the ``write_flash`` or ``image_info`` commands, the file is automatically split into temporary raw binary files at the gaps between input files.
.. note::
The ``image_info`` command will show the information for the first file in a merged Intel Hex file.
The splitting behavior of Intel Hex files offers an advantage during flashing: since no padding is used between sections, flash sectors between input files remain unerased. This can significantly improve flashing speed compared to using a merged raw binary file.
.. code:: sh
esptool.py --chip {IDF_TARGET_NAME} merge_bin --format hex -o merged-flash.hex --flash_mode dio --flash_size 4MB 0x1000 bootloader.bin 0x8000 partition-table.bin 0x10000 app.bin

View File

@@ -1291,7 +1291,12 @@ class AutoHex2BinAction(argparse.Action):
with open(value, "rb") as f:
# if hex file was detected replace hex file with converted temp bin
# otherwise keep the original file
value = intel_hex_to_bin(f).name
converted = intel_hex_to_bin(f)
if len(converted) != 1:
print(
"Note: Detected merged IntelHex file, processing only first file"
)
value = converted[0][1].name
except IOError as e:
raise argparse.ArgumentError(self, e)
setattr(namespace, self.dest, value)
@@ -1326,8 +1331,7 @@ class AddrFilenamePairAction(argparse.Action):
"and the binary filename to write there",
)
# check for intel hex files and convert them to bin
argfile = intel_hex_to_bin(argfile, address)
pairs.append((address, argfile))
pairs.extend(intel_hex_to_bin(argfile, address))
# Sort the addresses and check for overlapping
end = 0

View File

@@ -11,7 +11,7 @@ import os
import re
import struct
import tempfile
from typing import IO, Optional, Tuple
from typing import IO, List, Optional, Tuple
from intelhex import HexRecordError, IntelHex
@@ -45,9 +45,46 @@ def align_file_position(f, size):
f.seek(align, 1)
def intel_hex_to_bin(file: IO[bytes], start_addr: Optional[int] = None) -> IO[bytes]:
"""Convert IntelHex file to temp binary file with padding from start_addr
If hex file was detected return temp bin file object; input file otherwise"""
def _find_subsequences(addresses: List[int]) -> List[Tuple[int, int]]:
"""Find continuous subsequences in a list of addresses"""
if not addresses:
return []
sorted_seq = sorted(addresses)
subsequences = []
start = sorted_seq[0]
for prev, num in zip(sorted_seq, sorted_seq[1:]):
if num != prev + 1:
# Found a gap, save the current subsequence
subsequences.append((start, prev))
start = num
# Add the last subsequence
subsequences.append((start, sorted_seq[-1]))
return subsequences
def _split_intel_hex_file(ih: IntelHex) -> List[Tuple[int, IO[bytes]]]:
"""Split an IntelHex file into multiple temporary binary files based on the gaps
in the addresses"""
subsequences = _find_subsequences(ih.addresses())
bins: List[Tuple[int, IO[bytes]]] = []
for start, end in subsequences:
bin = tempfile.NamedTemporaryFile(suffix=".bin", delete=False)
ih.tobinfile(bin, start=start, end=end)
bin.seek(0) # make sure the file is at the beginning
bins.append((start, bin))
return bins
def intel_hex_to_bin(
file: IO[bytes], start_addr: Optional[int] = None
) -> List[Tuple[Optional[int], IO[bytes]]]:
"""Convert IntelHex file to list of temp binary files
If not hex file return input file otherwise"""
INTEL_HEX_MAGIC = b":"
magic = file.read(1)
file.seek(0)
@@ -56,14 +93,12 @@ def intel_hex_to_bin(file: IO[bytes], start_addr: Optional[int] = None) -> IO[by
ih = IntelHex()
ih.loadhex(file.name)
file.close()
bin = tempfile.NamedTemporaryFile(suffix=".bin", delete=False)
ih.tobinfile(bin, start=start_addr)
return bin
return _split_intel_hex_file(ih) # type: ignore
else:
return file
return [(start_addr, file)]
except (HexRecordError, UnicodeDecodeError):
# file started with HEX magic but the rest was not according to the standard
return file
return [(start_addr, file)]
def LoadFirmwareImage(chip, image_file):

View File

@@ -97,7 +97,7 @@ class TestImageInfo:
), "Wrong flash pins drive settings"
assert "Minimal chip revision: v0.0" in out, "Wrong min revision"
assert "Maximal chip revision: v0.0" in out, "Wrong min revision"
assert "Maximal chip revision: v0.0" in out, "Wrong max revision"
# Segments
assert (
@@ -194,9 +194,9 @@ class TestImageInfo:
assert "Compile time: Apr 25 2023 00:13:32" in out
def test_intel_hex(self):
# This bootloader binary is built from "hello_world" project
# with default settings, IDF version is v5.2.
# File is converted to Intel Hex using merge_bin
# Convert and merge two files to Intel Hex using merge_bin
# Run image_info on the resulting Intel Hex file
# Verify that image info is shown for the first file
def convert_bin2hex(file):
subprocess.check_output(
@@ -205,12 +205,14 @@ class TestImageInfo:
"-m",
"esptool",
"--chip",
"esp32",
"esp32c3",
"merge_bin",
"--format",
"hex",
"0x0",
"".join([IMAGES_DIR, os.sep, "bootloader_esp32_v5_2.bin"]),
os.path.join(IMAGES_DIR, "bootloader_esp32c3.bin"),
"0x8000",
os.path.join(IMAGES_DIR, "esp32c3_header_min_rev.bin"),
"-o",
file,
]
@@ -219,12 +221,29 @@ class TestImageInfo:
fd, file = tempfile.mkstemp(suffix=".hex")
try:
convert_bin2hex(file)
out = self.run_image_info("esp32", file, "2")
assert "File size: 26768 (bytes)" in out
assert "Bootloader information" in out
assert "Bootloader version: 1" in out
assert "ESP-IDF: v5.2-dev-254-g1950b15" in out
assert "Compile time: Apr 25 2023 00:13:32" in out
out = self.run_image_info("esp32c3", file, "2")
# Only the first files should be shown with Note
assert (
"Note: Detected merged IntelHex file, processing only first file" in out
)
# Header
assert "Entry point: 0x403c0000" in out, "Wrong entry point"
assert "Segments: 4" in out, "Wrong num of segments"
assert "Flash size: 2MB" in out, "Wrong flash size"
assert "Flash freq: 40m" in out, "Wrong flash frequency"
assert "Flash mode: DIO" in out, "Wrong flash mode"
# Extended header
assert "WP pin: 0xee (disabled)" in out, "Wrong WP pin"
assert "Chip ID: 5 (ESP32-C3)" in out, "Wrong chip ID"
assert (
"clk_drv: 0x0, q_drv: 0x0, d_drv: 0x0, "
"cs0_drv: 0x0, hd_drv: 0x0, wp_drv: 0x0" in out
), "Wrong flash pins drive settings"
assert "Minimal chip revision: v0.0" in out, "Wrong min revision"
assert "Maximal chip revision: v0.0" in out, "Wrong max revision"
finally:
try:
# make sure that file was closed before removing it