Skip to content

Commit 08c170b

Browse files
peterdragunradimkarnis
authored andcommitted
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 espressif#1075
1 parent 3726726 commit 08c170b

File tree

6 files changed

+133
-34
lines changed

6 files changed

+133
-34
lines changed

docs/en/esptool/basic-commands.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,13 @@ Intel Hex format offers distinct advantages when compared to the binary format,
283283
* **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.
284284
* **Validity Checks**: Each line in an Intel Hex file has a checksum to help find errors and make sure data stays unchanged.
285285

286+
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.
287+
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).
288+
289+
In contrast, analyzing a merged raw binary file only processes the header of the first file, providing less detailed information.
290+
291+
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.
292+
286293
.. code:: sh
287294
288295
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/__init__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -553,10 +553,15 @@ def teardown():
553553
@cli.command("load-ram")
554554
@click.argument("filename", type=AutoHex2BinType())
555555
@click.pass_context
556-
def load_ram_cli(ctx, filename):
556+
def load_ram_cli(ctx, filename: list[tuple[int | None, t.IO[bytes]]]):
557557
"""Download an image to RAM and execute."""
558+
if len(filename) > 1:
559+
raise FatalError(
560+
"Merged binary image detected. "
561+
"Only one file can be specified for the load-ram command."
562+
)
558563
prepare_esp_object(ctx)
559-
load_ram(ctx.obj["esp"], filename)
564+
load_ram(ctx.obj["esp"], filename[0][1].name)
560565

561566

562567
@cli.command("dump-mem")
@@ -670,9 +675,13 @@ def run_cli(ctx):
670675
@cli.command("image-info")
671676
@click.argument("filename", type=AutoHex2BinType())
672677
@click.pass_context
673-
def image_info_cli(ctx, filename):
678+
def image_info_cli(ctx, filename: list[tuple[int | None, t.IO[bytes]]]):
674679
"""Print information about a firmware image (bootloader or application)."""
675-
image_info(filename, chip=None if ctx.obj["chip"] == "auto" else ctx.obj["chip"])
680+
chip = None if ctx.obj["chip"] == "auto" else ctx.obj["chip"]
681+
if len(filename) == 1:
682+
image_info(filename[0][1].name, chip=chip)
683+
else:
684+
image_info(filename, chip=chip) # type: ignore
676685

677686

678687
@cli.command("elf2image")

esptool/bin_image.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,46 @@ def align_file_position(f, size):
4141
f.seek(align, 1)
4242

4343

44-
def intel_hex_to_bin(file: IO[bytes], start_addr: int | None = None) -> IO[bytes]:
45-
"""Convert IntelHex file to temp binary file with padding from start_addr
46-
If hex file was detected return temp bin file object; input file otherwise"""
44+
def _find_subsequences(addresses: list[int]) -> list[tuple[int, int]]:
45+
"""Find continuous subsequences in a list of addresses"""
46+
if not addresses:
47+
return []
48+
49+
sorted_seq = sorted(addresses)
50+
51+
subsequences = []
52+
start = sorted_seq[0]
53+
54+
for prev, num in zip(sorted_seq, sorted_seq[1:]):
55+
if num != prev + 1:
56+
# Found a gap, save the current subsequence
57+
subsequences.append((start, prev))
58+
start = num
59+
60+
# Add the last subsequence
61+
subsequences.append((start, sorted_seq[-1]))
62+
63+
return subsequences
64+
65+
66+
def _split_intel_hex_file(ih: IntelHex) -> list[tuple[int, IO[bytes]]]:
67+
"""Split an IntelHex file into multiple temporary binary files based on the gaps
68+
in the addresses"""
69+
subsequences = _find_subsequences(ih.addresses())
70+
bins: list[tuple[int, IO[bytes]]] = []
71+
for start, end in subsequences:
72+
bin = tempfile.NamedTemporaryFile(suffix=".bin", delete=False)
73+
ih.tobinfile(bin, start=start, end=end)
74+
bin.seek(0) # make sure the file is at the beginning
75+
bins.append((start, bin))
76+
return bins
77+
78+
79+
def intel_hex_to_bin(
80+
file: IO[bytes], start_addr: int | None = None
81+
) -> list[tuple[int | None, IO[bytes]]]:
82+
"""Convert IntelHex file to list of temp binary files
83+
If not hex file return input file otherwise"""
4784
INTEL_HEX_MAGIC = b":"
4885
magic = file.read(1)
4986
file.seek(0)
@@ -52,14 +89,12 @@ def intel_hex_to_bin(file: IO[bytes], start_addr: int | None = None) -> IO[bytes
5289
ih = IntelHex()
5390
ih.loadhex(file.name)
5491
file.close()
55-
bin = tempfile.NamedTemporaryFile(suffix=".bin", delete=False)
56-
ih.tobinfile(bin, start=start_addr)
57-
return bin
92+
return _split_intel_hex_file(ih) # type: ignore
5893
else:
59-
return file
94+
return [(start_addr, file)]
6095
except (HexRecordError, UnicodeDecodeError):
6196
# file started with HEX magic but the rest was not according to the standard
62-
return file
97+
return [(start_addr, file)]
6398

6499

65100
def LoadFirmwareImage(chip: str, image_data: ImageSource):

esptool/cli_util.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from esptool.cmds import detect_flash_size
1010
from esptool.util import FatalError, flash_size_bytes, strip_chip_name
1111
from esptool.logger import log
12-
from typing import Any
12+
from typing import IO, Any
1313

1414
################################ Custom types #################################
1515

@@ -140,12 +140,12 @@ def __init__(self, exists=True):
140140

141141
def convert(
142142
self, value: str, param: click.Parameter | None, ctx: click.Context
143-
) -> str:
143+
) -> list[tuple[int | None, IO[bytes]]]:
144144
try:
145145
with open(value, "rb") as f:
146146
# if hex file was detected replace hex file with converted temp bin
147147
# otherwise keep the original file
148-
return intel_hex_to_bin(f).name
148+
return intel_hex_to_bin(f)
149149
except IOError as e:
150150
raise click.BadParameter(str(e))
151151

@@ -171,7 +171,7 @@ def convert(
171171
if len(value) == 0:
172172
return value
173173

174-
pairs = []
174+
pairs: list[tuple[int, IO[bytes]]] = []
175175
for i in range(0, len(value), 2):
176176
try:
177177
address = arg_auto_int(value[i])
@@ -186,8 +186,8 @@ def convert(
186186
except IOError as e:
187187
raise click.BadParameter(str(e))
188188
# check for intel hex files and convert them to bin
189-
argfile = intel_hex_to_bin(argfile_f, address)
190-
pairs.append((address, argfile))
189+
argfile_list = intel_hex_to_bin(argfile_f, address)
190+
pairs.extend(argfile_list) # type: ignore
191191

192192
# Sort the addresses and check for overlapping
193193
end = 0

esptool/cmds.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,19 +1731,62 @@ def _parse_bootloader_info(bootloader_info_segment):
17311731
}
17321732

17331733

1734-
def image_info(input: ImageSource, chip: str | None = None) -> None:
1734+
def image_info(
1735+
input: ImageSource | list[tuple[int, ImageSource]], chip: str | None = None
1736+
) -> None:
17351737
"""
17361738
Display detailed information about an ESP firmware image.
17371739
17381740
Args:
17391741
input: Path to the firmware image file, opened file-like object,
1740-
or the image data as bytes.
1742+
or the image data as bytes. If a list of tuples is provided,
1743+
each tuple contains an offset and an image data as bytes. Used for
1744+
merged binary images.
17411745
chip: Target ESP device type (e.g., ``"esp32"``). If None, the chip
17421746
type will be automatically detected from the image header.
17431747
"""
1748+
if isinstance(input, list):
1749+
log.print("Merged binary image detected. Processing each file individually.")
1750+
for i, file in enumerate(input):
1751+
data, _ = get_bytes(file[1])
1752+
1753+
offset_str = hex(file[0]) if file[0] is not None else "unknown"
1754+
line = (
1755+
f"Processing file {i + 1}/{len(input)}, "
1756+
f"offset: {offset_str}, size: {len(data)} bytes"
1757+
)
1758+
log.print()
1759+
log.print("=" * len(line))
1760+
log.print(line)
1761+
log.print("=" * len(line))
1762+
1763+
try:
1764+
detected_chip = _parse_image_info_header(data, chip)
1765+
except Exception as e:
1766+
log.error(f"Error processing file {i + 1}/{len(input)}: {e}")
1767+
log.error("Probably not a valid firmware image (e.g. partition table).")
1768+
continue
17441769

1745-
data, _ = get_bytes(input)
1746-
log.print(f"Image size: {len(data)} bytes")
1770+
if (
1771+
i == 0 and chip is None
1772+
): # We don't need to print the image type for each file
1773+
log.print(f"Detected image type: {detected_chip.upper()}")
1774+
chip = detected_chip
1775+
_print_image_info(detected_chip, data)
1776+
1777+
else:
1778+
data, _ = get_bytes(input)
1779+
detected_chip = _parse_image_info_header(data, chip)
1780+
1781+
log.print(f"Image size: {len(data)} bytes")
1782+
if chip is None:
1783+
log.print(f"Detected image type: {detected_chip.upper()}")
1784+
1785+
_print_image_info(detected_chip, data)
1786+
1787+
1788+
def _parse_image_info_header(data: bytes, chip: str | None = None) -> str:
1789+
"""Parse the image info header and return the chip type."""
17471790
stream = io.BytesIO(data)
17481791
common_header = stream.read(8)
17491792
if chip is None:
@@ -1779,8 +1822,10 @@ def image_info(input: ImageSource, chip: str | None = None) -> None:
17791822
except FatalError:
17801823
chip = "esp8266"
17811824

1782-
log.print(f"Detected image type: {chip.upper()}")
1825+
return chip
1826+
17831827

1828+
def _print_image_info(chip: str, data: bytes) -> None:
17841829
image = LoadFirmwareImage(chip, data)
17851830

17861831
log.print()

test/test_image_info.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,9 @@ def test_bootloader_info(self):
167167
assert "Compile time: Apr 25 2023 00:13:32" in out
168168

169169
def test_intel_hex(self):
170-
# This bootloader binary is built from "hello_world" project
171-
# with default settings, IDF version is v5.2.
172-
# File is converted to Intel Hex using merge-bin
170+
# Convert and merge two files to Intel Hex using merge-bin
171+
# Run image-info on the resulting Intel Hex file
172+
# Verify that image info is shown for both files
173173

174174
def convert_bin2hex(file):
175175
subprocess.check_output(
@@ -178,12 +178,14 @@ def convert_bin2hex(file):
178178
"-m",
179179
"esptool",
180180
"--chip",
181-
"esp32",
181+
"esp32c3",
182182
"merge-bin",
183183
"--format",
184184
"hex",
185185
"0x0",
186-
"".join([IMAGES_DIR, os.sep, "bootloader_esp32_v5_2.bin"]),
186+
os.path.join(IMAGES_DIR, "bootloader_esp32c3.bin"),
187+
"0x8000",
188+
os.path.join(IMAGES_DIR, "esp32c3_header_min_rev.bin"),
187189
"-o",
188190
file,
189191
]
@@ -192,12 +194,13 @@ def convert_bin2hex(file):
192194
fd, file = tempfile.mkstemp(suffix=".hex")
193195
try:
194196
convert_bin2hex(file)
195-
out = self.run_image_info("esp32", file)
196-
assert "Image size: 26768 bytes" in out
197-
assert "Bootloader Information" in out
198-
assert "Bootloader version: 1" in out
199-
assert "ESP-IDF: v5.2-dev-254-g1950b15" in out
200-
assert "Compile time: Apr 25 2023 00:13:32" in out
197+
out = self.run_image_info("esp32c3", file)
198+
assert (
199+
"Merged binary image detected. Processing each file individually."
200+
in out
201+
)
202+
assert "Processing file 1/2, offset: 0x0, size: 17744 bytes" in out
203+
assert "Processing file 2/2, offset: 0x8000, size: 48 bytes" in out
201204
finally:
202205
try:
203206
# make sure that file was closed before removing it

0 commit comments

Comments
 (0)