Skip to content

Commit 0dec6fe

Browse files
committed
feat(tools): Add idf.py merge-bin command and cmake target
1 parent e96da70 commit 0dec6fe

File tree

4 files changed

+146
-9
lines changed

4 files changed

+146
-9
lines changed

components/esptool_py/project_include.cmake

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,24 @@ add_custom_target(uf2-app
240240
VERBATIM
241241
)
242242

243+
set(MERGE_BIN_ARGS merge_bin)
244+
if(DEFINED ENV{ESP_MERGE_BIN_OUTPUT})
245+
list(APPEND MERGE_BIN_ARGS "-o" "$ENV{ESP_MERGE_BIN_OUTPUT}")
246+
else()
247+
if(DEFINED ENV{ESP_MERGE_BIN_FORMAT} AND "$ENV{ESP_MERGE_BIN_FORMAT}" STREQUAL "hex")
248+
list(APPEND MERGE_BIN_ARGS "-o" "${CMAKE_CURRENT_BINARY_DIR}/merged-binary.hex")
249+
else()
250+
list(APPEND MERGE_BIN_ARGS "-o" "${CMAKE_CURRENT_BINARY_DIR}/merged-binary.bin")
251+
endif()
252+
endif()
253+
254+
if(DEFINED ENV{ESP_MERGE_BIN_FORMAT})
255+
list(APPEND MERGE_BIN_ARGS "-f" "$ENV{ESP_MERGE_BIN_FORMAT}")
256+
endif()
243257

244-
set(MERGE_BIN_ARGS merge_bin -o "${CMAKE_CURRENT_BINARY_DIR}/merge.bin" "@${CMAKE_CURRENT_BINARY_DIR}/flash_args")
258+
list(APPEND MERGE_BIN_ARGS "@${CMAKE_CURRENT_BINARY_DIR}/flash_args")
245259

246-
add_custom_target(merge_bin
260+
add_custom_target(merge-bin
247261
COMMAND ${CMAKE_COMMAND}
248262
-D "IDF_PATH=${idf_path}"
249263
-D "SERIAL_TOOL=${ESPTOOLPY}"

docs/en/api-guides/tools/idf-py.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,37 @@ This command automatically builds the project if necessary, and then flash it to
122122

123123
Similarly to the ``build`` command, the command can be run with ``app``, ``bootloader`` and ``partition-table`` arguments to flash only the app, bootloader or partition table as applicable.
124124

125+
.. _merging-binaries:
126+
127+
Merge binaries: ``merge-bin``
128+
-----------------------------
129+
130+
.. code-block:: bash
131+
132+
idf.py merge-bin [-o output-file] [-f format] [<format-specific-options>]
133+
134+
There are some situations, e.g. transferring the file to another machine and flashing it without ESP-IDF, where it is convenient to have only one file for flashing instead the several file output of ``idf.py build``.
135+
136+
The command ``idf.py merge-bin`` will merge the bootloader, partition table, the application itself, and other partitions (if there are any) according to the project configuration and create a single binary file ``merged-binary.[bin|hex]`` in the build folder, which can then be flashed later.
137+
138+
It is possible to output merged file in binary (raw), IntelHex (hex) and UF2 (uf2) formats.
139+
140+
The uf2 binary can also be generated by :ref:`idf.py uf2 <generate-uf2-binary>`. The ``idf.py uf2`` is functionally equivalent to ``idf.py merge-bin -f uf2``. However, the ``idf.py merge-bin`` command provides more flexibility and options for merging binaries into various formats described above.
141+
142+
Example usage:
143+
144+
.. code-block:: bash
145+
146+
idf.py merge-bin -o my-merged-binary.bin -f raw
147+
148+
There are also some format specific options, which are listed below:
149+
150+
- Only for raw format:
151+
- ``--flash-offset``: This option will create a merged binary that should be flashed at the specified offset, instead of at the standard offset of 0x0.
152+
- ``--fill-flash-size``: If set, the final binary file will be padded with FF bytes up to this flash size in order to fill the full flash content with the image and re-write the whole flash chip upon flashing.
153+
- Only for uf2 format:
154+
- ``--md5-disable``: This option will disable MD5 checksums at the end of each block. This can be useful for integration with e.g. `tinyuf2 <https://github.com/adafruit/tinyuf2>`__.
155+
125156
Hints on How to Resolve Errors
126157
==============================
127158

@@ -201,6 +232,8 @@ Clean the Python Byte Code: ``python-clean``
201232
202233
This command deletes generated python byte code from the ESP-IDF directory. The byte code may cause issues when switching between ESP-IDF and Python versions. It is advised to run this target after switching versions of Python.
203234

235+
.. _generate-uf2-binary:
236+
204237
Generate a UF2 Binary: ``uf2``
205238
------------------------------
206239

@@ -214,6 +247,8 @@ This UF2 file can be copied to a USB mass storage device exposed by another ESP
214247

215248
To generate a UF2 binary for the application only (not including the bootloader and partition table), use the ``uf2-app`` command.
216249

250+
The ``idf.py uf2`` command is functionally equivalent to ``idf.py merge-bin -f uf2`` described :ref:`above <merging-binaries>`. However, the ``idf.py merge-bin`` command provides more flexibility and options for merging binaries into various formats, not only uf2.
251+
217252
.. code-block:: bash
218253
219254
idf.py uf2-app

tools/idf_py_actions/serial_ext.py

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
# SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
1+
# SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Apache-2.0
3-
43
import json
54
import os
65
import shlex
76
import signal
87
import sys
9-
from typing import Any, Dict, List, Optional
8+
from typing import Any
9+
from typing import Dict
10+
from typing import List
11+
from typing import Optional
1012

1113
import click
1214
from idf_py_actions.global_options import global_options
13-
from idf_py_actions.tools import (PropertyDict, RunTool, ensure_build_directory, get_default_serial_port,
14-
get_sdkconfig_value, run_target)
15+
from idf_py_actions.tools import ensure_build_directory
16+
from idf_py_actions.tools import get_default_serial_port
17+
from idf_py_actions.tools import get_sdkconfig_value
18+
from idf_py_actions.tools import PropertyDict
19+
from idf_py_actions.tools import run_target
20+
from idf_py_actions.tools import RunTool
1521

1622
PYTHON = sys.executable
1723

@@ -34,7 +40,7 @@
3440
}
3541

3642

37-
def yellow_print(message, newline='\n'): # type: (str, Optional[str]) -> None
43+
def yellow_print(message: str, newline: Optional[str]='\n') -> None:
3844
"""Print a message to stderr with yellow highlighting """
3945
sys.stderr.write('%s%s%s%s' % ('\033[0;33m', message, '\033[0m', newline))
4046
sys.stderr.flush()
@@ -212,6 +218,47 @@ def ota_targets(target_name: str, ctx: click.core.Context, args: PropertyDict) -
212218
ensure_build_directory(args, ctx.info_name)
213219
run_target(target_name, args, {'ESPBAUD': str(args.baud), 'ESPPORT': args.port})
214220

221+
def merge_bin(action: str,
222+
ctx: click.core.Context,
223+
args: PropertyDict,
224+
output: str,
225+
format: str,
226+
md5_disable: str,
227+
flash_offset: str,
228+
fill_flash_size: str) -> None:
229+
ensure_build_directory(args, ctx.info_name)
230+
project_desc = _get_project_desc(ctx, args)
231+
merge_bin_args = [PYTHON, '-m', 'esptool']
232+
target = project_desc['target']
233+
merge_bin_args += ['--chip', target]
234+
merge_bin_args += ['merge_bin'] # needs to be after the --chip option
235+
if not output:
236+
if format in ('raw', 'uf2'):
237+
output = 'merged-binary.bin'
238+
elif format == 'hex':
239+
output = 'merged-binary.hex'
240+
merge_bin_args += ['-o', output]
241+
if format:
242+
merge_bin_args += ['-f', format]
243+
if md5_disable:
244+
if format != 'uf2':
245+
yellow_print('idf.py merge-bin: --md5-disable is only valid for UF2 format. Option will be ignored.')
246+
else:
247+
merge_bin_args += ['--md5-disable']
248+
if flash_offset:
249+
if format != 'raw':
250+
yellow_print('idf.py merge-bin: --flash-offset is only valid for RAW format. Option will be ignored.')
251+
else:
252+
merge_bin_args += ['-t', flash_offset]
253+
if fill_flash_size:
254+
if format != 'raw':
255+
yellow_print('idf.py merge-bin: --fill-flash-size is only valid for RAW format, option will be ignored.')
256+
else:
257+
merge_bin_args += ['--fill-flash-size', fill_flash_size]
258+
merge_bin_args += ['@flash_args']
259+
print(f'Merged binary {output} will be created in the build directory...')
260+
RunTool('merge_bin', merge_bin_args, args.build_dir, build_dir=args.build_dir, hints=not args.no_hints)()
261+
215262
BAUD_AND_PORT = [BAUD_RATE, PORT]
216263
flash_options = BAUD_AND_PORT + [
217264
{
@@ -252,6 +299,37 @@ def ota_targets(target_name: str, ctx: click.core.Context, args: PropertyDict) -
252299
'help': 'Erase entire flash chip.',
253300
'options': BAUD_AND_PORT,
254301
},
302+
'merge-bin': {
303+
'callback': merge_bin,
304+
'options': [
305+
{
306+
'names': ['--output', '-o'],
307+
'help': ('Output filename'),
308+
'type': click.Path(),
309+
},
310+
{
311+
'names': ['--format', '-f'],
312+
'help': ('Format of the output file'),
313+
'type': click.Choice(['hex', 'uf2', 'raw']),
314+
'default': 'raw',
315+
},
316+
{
317+
'names': ['--md5-disable'],
318+
'is_flag': True,
319+
'help': ('[ONLY UF2] Disable MD5 checksum in UF2 output.'),
320+
},
321+
{
322+
'names': ['--flash-offset', '-t'],
323+
'help': ('[ONLY RAW] Flash offset where the output file will be flashed.'),
324+
},
325+
{
326+
'names': ['--fill-flash-size'],
327+
'help': ('[ONLY RAW] If set, the final binary file will be padded with FF bytes up to this flash size.'),
328+
'type': click.Choice(['256KB', '512KB', '1MB', '2MB', '4MB', '8MB', '16MB', '32MB', '64MB', '128MB']),
329+
},
330+
],
331+
'dependencies': ['all'], # all = build
332+
},
255333
'monitor': {
256334
'callback':
257335
monitor,

tools/test_build_system/test_common.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def change_to_readonly(src: Path) -> None:
252252
for name in files:
253253
path = os.path.join(root, name)
254254
if '/bin/' in path:
255-
continue # skip excutables
255+
continue # skip executables
256256
os.chmod(os.path.join(root, name), 0o444) # readonly
257257
logging.info('Check that command for creating new project will success if the IDF itself is readonly.')
258258
change_to_readonly(idf_copy)
@@ -308,3 +308,13 @@ def test_save_defconfig_check(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
308308
'Missing CONFIG_IDF_TARGET="esp32c3" in sdkconfig.defaults'
309309
assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_PARTITION_TABLE_OFFSET=0x8001'), \
310310
'Missing CONFIG_PARTITION_TABLE_OFFSET=0x8001 in sdkconfig.defaults'
311+
312+
313+
def test_merge_bin_cmd(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
314+
logging.info('Test if merge-bin command works correctly')
315+
idf_py('merge-bin')
316+
assert (test_app_copy / 'build' / 'merged-binary.bin').is_file()
317+
idf_py('merge-bin', '--output', 'merged-binary-2.bin')
318+
assert (test_app_copy / 'build' / 'merged-binary-2.bin').is_file()
319+
idf_py('merge-bin', '--format', 'hex')
320+
assert (test_app_copy / 'build' / 'merged-binary.hex').is_file()

0 commit comments

Comments
 (0)