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 37267268e4
commit 08c170b90e
6 changed files with 133 additions and 34 deletions

View File

@@ -283,6 +283,13 @@ 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. * **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. * **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.
This splitting process allows each section to be analyzed independently, producing output similar to running ``image-info`` on the original files before merging (with the only difference being the splitting based on gaps).
In contrast, analyzing a merged raw binary file only processes the header of the first file, providing less detailed information.
The splitting behavior of Intel Hex files offers an additional 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 .. 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 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

@@ -553,10 +553,15 @@ def prepare_esp_object(ctx):
@cli.command("load-ram") @cli.command("load-ram")
@click.argument("filename", type=AutoHex2BinType()) @click.argument("filename", type=AutoHex2BinType())
@click.pass_context @click.pass_context
def load_ram_cli(ctx, filename): def load_ram_cli(ctx, filename: list[tuple[int | None, t.IO[bytes]]]):
"""Download an image to RAM and execute.""" """Download an image to RAM and execute."""
if len(filename) > 1:
raise FatalError(
"Merged binary image detected. "
"Only one file can be specified for the load-ram command."
)
prepare_esp_object(ctx) prepare_esp_object(ctx)
load_ram(ctx.obj["esp"], filename) load_ram(ctx.obj["esp"], filename[0][1].name)
@cli.command("dump-mem") @cli.command("dump-mem")
@@ -670,9 +675,13 @@ def run_cli(ctx):
@cli.command("image-info") @cli.command("image-info")
@click.argument("filename", type=AutoHex2BinType()) @click.argument("filename", type=AutoHex2BinType())
@click.pass_context @click.pass_context
def image_info_cli(ctx, filename): def image_info_cli(ctx, filename: list[tuple[int | None, t.IO[bytes]]]):
"""Print information about a firmware image (bootloader or application).""" """Print information about a firmware image (bootloader or application)."""
image_info(filename, chip=None if ctx.obj["chip"] == "auto" else ctx.obj["chip"]) chip = None if ctx.obj["chip"] == "auto" else ctx.obj["chip"]
if len(filename) == 1:
image_info(filename[0][1].name, chip=chip)
else:
image_info(filename, chip=chip) # type: ignore
@cli.command("elf2image") @cli.command("elf2image")

View File

@@ -41,9 +41,46 @@ def align_file_position(f, size):
f.seek(align, 1) f.seek(align, 1)
def intel_hex_to_bin(file: IO[bytes], start_addr: int | None = None) -> IO[bytes]: def _find_subsequences(addresses: list[int]) -> list[tuple[int, int]]:
"""Convert IntelHex file to temp binary file with padding from start_addr """Find continuous subsequences in a list of addresses"""
If hex file was detected return temp bin file object; input file otherwise""" 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: int | None = None
) -> list[tuple[int | None, IO[bytes]]]:
"""Convert IntelHex file to list of temp binary files
If not hex file return input file otherwise"""
INTEL_HEX_MAGIC = b":" INTEL_HEX_MAGIC = b":"
magic = file.read(1) magic = file.read(1)
file.seek(0) file.seek(0)
@@ -52,14 +89,12 @@ def intel_hex_to_bin(file: IO[bytes], start_addr: int | None = None) -> IO[bytes
ih = IntelHex() ih = IntelHex()
ih.loadhex(file.name) ih.loadhex(file.name)
file.close() file.close()
bin = tempfile.NamedTemporaryFile(suffix=".bin", delete=False) return _split_intel_hex_file(ih) # type: ignore
ih.tobinfile(bin, start=start_addr)
return bin
else: else:
return file return [(start_addr, file)]
except (HexRecordError, UnicodeDecodeError): except (HexRecordError, UnicodeDecodeError):
# file started with HEX magic but the rest was not according to the standard # file started with HEX magic but the rest was not according to the standard
return file return [(start_addr, file)]
def LoadFirmwareImage(chip: str, image_data: ImageSource): def LoadFirmwareImage(chip: str, image_data: ImageSource):

View File

@@ -9,7 +9,7 @@ from esptool.bin_image import ESPLoader, intel_hex_to_bin
from esptool.cmds import detect_flash_size from esptool.cmds import detect_flash_size
from esptool.util import FatalError, flash_size_bytes, strip_chip_name from esptool.util import FatalError, flash_size_bytes, strip_chip_name
from esptool.logger import log from esptool.logger import log
from typing import Any from typing import IO, Any
################################ Custom types ################################# ################################ Custom types #################################
@@ -140,12 +140,12 @@ class AutoHex2BinType(click.Path):
def convert( def convert(
self, value: str, param: click.Parameter | None, ctx: click.Context self, value: str, param: click.Parameter | None, ctx: click.Context
) -> str: ) -> list[tuple[int | None, IO[bytes]]]:
try: try:
with open(value, "rb") as f: with open(value, "rb") as f:
# if hex file was detected replace hex file with converted temp bin # if hex file was detected replace hex file with converted temp bin
# otherwise keep the original file # otherwise keep the original file
return intel_hex_to_bin(f).name return intel_hex_to_bin(f)
except IOError as e: except IOError as e:
raise click.BadParameter(str(e)) raise click.BadParameter(str(e))
@@ -171,7 +171,7 @@ class AddrFilenamePairType(click.Path):
if len(value) == 0: if len(value) == 0:
return value return value
pairs = [] pairs: list[tuple[int, IO[bytes]]] = []
for i in range(0, len(value), 2): for i in range(0, len(value), 2):
try: try:
address = arg_auto_int(value[i]) address = arg_auto_int(value[i])
@@ -186,8 +186,8 @@ class AddrFilenamePairType(click.Path):
except IOError as e: except IOError as e:
raise click.BadParameter(str(e)) raise click.BadParameter(str(e))
# check for intel hex files and convert them to bin # check for intel hex files and convert them to bin
argfile = intel_hex_to_bin(argfile_f, address) argfile_list = intel_hex_to_bin(argfile_f, address)
pairs.append((address, argfile)) pairs.extend(argfile_list) # type: ignore
# Sort the addresses and check for overlapping # Sort the addresses and check for overlapping
end = 0 end = 0

View File

@@ -1731,19 +1731,62 @@ def _parse_bootloader_info(bootloader_info_segment):
} }
def image_info(input: ImageSource, chip: str | None = None) -> None: def image_info(
input: ImageSource | list[tuple[int, ImageSource]], chip: str | None = None
) -> None:
""" """
Display detailed information about an ESP firmware image. Display detailed information about an ESP firmware image.
Args: Args:
input: Path to the firmware image file, opened file-like object, input: Path to the firmware image file, opened file-like object,
or the image data as bytes. or the image data as bytes. If a list of tuples is provided,
each tuple contains an offset and an image data as bytes. Used for
merged binary images.
chip: Target ESP device type (e.g., ``"esp32"``). If None, the chip chip: Target ESP device type (e.g., ``"esp32"``). If None, the chip
type will be automatically detected from the image header. type will be automatically detected from the image header.
""" """
if isinstance(input, list):
log.print("Merged binary image detected. Processing each file individually.")
for i, file in enumerate(input):
data, _ = get_bytes(file[1])
data, _ = get_bytes(input) offset_str = hex(file[0]) if file[0] is not None else "unknown"
log.print(f"Image size: {len(data)} bytes") line = (
f"Processing file {i + 1}/{len(input)}, "
f"offset: {offset_str}, size: {len(data)} bytes"
)
log.print()
log.print("=" * len(line))
log.print(line)
log.print("=" * len(line))
try:
detected_chip = _parse_image_info_header(data, chip)
except Exception as e:
log.error(f"Error processing file {i + 1}/{len(input)}: {e}")
log.error("Probably not a valid firmware image (e.g. partition table).")
continue
if (
i == 0 and chip is None
): # We don't need to print the image type for each file
log.print(f"Detected image type: {detected_chip.upper()}")
chip = detected_chip
_print_image_info(detected_chip, data)
else:
data, _ = get_bytes(input)
detected_chip = _parse_image_info_header(data, chip)
log.print(f"Image size: {len(data)} bytes")
if chip is None:
log.print(f"Detected image type: {detected_chip.upper()}")
_print_image_info(detected_chip, data)
def _parse_image_info_header(data: bytes, chip: str | None = None) -> str:
"""Parse the image info header and return the chip type."""
stream = io.BytesIO(data) stream = io.BytesIO(data)
common_header = stream.read(8) common_header = stream.read(8)
if chip is None: if chip is None:
@@ -1779,8 +1822,10 @@ def image_info(input: ImageSource, chip: str | None = None) -> None:
except FatalError: except FatalError:
chip = "esp8266" chip = "esp8266"
log.print(f"Detected image type: {chip.upper()}") return chip
def _print_image_info(chip: str, data: bytes) -> None:
image = LoadFirmwareImage(chip, data) image = LoadFirmwareImage(chip, data)
log.print() log.print()

View File

@@ -167,9 +167,9 @@ class TestImageInfo:
assert "Compile time: Apr 25 2023 00:13:32" in out assert "Compile time: Apr 25 2023 00:13:32" in out
def test_intel_hex(self): def test_intel_hex(self):
# This bootloader binary is built from "hello_world" project # Convert and merge two files to Intel Hex using merge-bin
# with default settings, IDF version is v5.2. # Run image-info on the resulting Intel Hex file
# File is converted to Intel Hex using merge-bin # Verify that image info is shown for both files
def convert_bin2hex(file): def convert_bin2hex(file):
subprocess.check_output( subprocess.check_output(
@@ -178,12 +178,14 @@ class TestImageInfo:
"-m", "-m",
"esptool", "esptool",
"--chip", "--chip",
"esp32", "esp32c3",
"merge-bin", "merge-bin",
"--format", "--format",
"hex", "hex",
"0x0", "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", "-o",
file, file,
] ]
@@ -192,12 +194,13 @@ class TestImageInfo:
fd, file = tempfile.mkstemp(suffix=".hex") fd, file = tempfile.mkstemp(suffix=".hex")
try: try:
convert_bin2hex(file) convert_bin2hex(file)
out = self.run_image_info("esp32", file) out = self.run_image_info("esp32c3", file)
assert "Image size: 26768 bytes" in out assert (
assert "Bootloader Information" in out "Merged binary image detected. Processing each file individually."
assert "Bootloader version: 1" in out in out
assert "ESP-IDF: v5.2-dev-254-g1950b15" in out )
assert "Compile time: Apr 25 2023 00:13:32" in out assert "Processing file 1/2, offset: 0x0, size: 17744 bytes" in out
assert "Processing file 2/2, offset: 0x8000, size: 48 bytes" in out
finally: finally:
try: try:
# make sure that file was closed before removing it # make sure that file was closed before removing it