Skip to content

Commit

Permalink
feat(merge_bin): add support for uf2 format
Browse files Browse the repository at this point in the history
  • Loading branch information
peterdragun authored and radimkarnis committed Sep 25, 2023
1 parent 4c75bb6 commit 3d899b2
Show file tree
Hide file tree
Showing 17 changed files with 537 additions and 34 deletions.
9 changes: 9 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ version_check:
reports:
junit: test/report.xml

check_uf2_ids:
<<: *host_tests_template
allow_failure: true
variables:
COVERAGE_PROCESS_START: "${CI_PROJECT_DIR}/test/.covconf"
PYTEST_ADDOPTS: "-sv --junitxml=test/report.xml --color=yes"
script:
- coverage run -m pytest ${CI_PROJECT_DIR}/test/test_uf2_ids.py

host_tests:
<<: *host_tests_template
variables:
Expand Down
46 changes: 38 additions & 8 deletions docs/en/esptool/basic-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ This information corresponds to the headers described in :ref:`image-format`.

Merge Binaries for Flashing: merge_bin
--------------------------------------

The ``merge_bin`` command will merge multiple binary files (of any kind) into a single file that can be flashed to a device later. Any gaps between the input files are padded with 0xFF bytes (same as unwritten flash contents).
The ``merge_bin`` command will merge multiple binary files (of any kind) into a single file that can be flashed to a device later. Any gaps between the input files are padded based on selected output format.

For example:

Expand All @@ -247,23 +246,54 @@ For example:

Will create a file ``merged-flash.bin`` with the contents of the other 3 files. This file can be later be written to flash with ``esptool.py write_flash 0x0 merged-flash.bin``.

.. note:
Because gaps between the input files are padded with 0xFF bytes, when the merged binary is written then any flash sectors between the individual files will be erased. To avoid this, write the files individually.

**Options:**
**Common options:**

* The ``merge_bin`` command supports the same ``--flash_mode``, ``--flash_size`` and ``--flash_freq`` options as the ``write_flash`` command to override the bootloader flash header (see above for details).
These options are applied to the output file contents in the same way as when writing to flash. Make sure to pass the ``--chip`` parameter if using these options, as the supported values and the bootloader offset both depend on the chip.
* The ``--target-offset 0xNNN`` option will create a merged binary that should be flashed at the specified offset, instead of at offset 0x0.
* The ``--fill-flash-size SIZE`` option will pad the merged binary with 0xFF bytes to the full flash specified size, for example ``--fill-flash-size 4MB`` will create a 4MB binary file.
* The ``--format`` option will change the format of the output file. For more information about formats see formats description below.
* It is possible to append options from a text file with ``@filename``. As an example, this can be conveniently used with the ESP-IDF build system, which produces a ``flash_args`` file in the build directory of a project:

.. code:: sh
cd build # The build directory of an ESP-IDF project
esptool.py --chip {IDF_TARGET_NAME} merge_bin -o merged-flash.bin @flash_args
RAW Output Format
^^^^^^^^^^^^^^^^^

The output of the command will be in ``raw`` format and gaps between individual files will be filled with `0xFF` bytes (same as unwritten flash contents).

.. note::

Because gaps between the input files are padded with `0xFF` bytes, when the merged binary is written then any flash sectors between the individual files will be erased. To avoid this, write the files individually.


**RAW options:**

* The ``--fill-flash-size SIZE`` option will pad the merged binary with `0xFF` bytes to the full flash specified size, for example ``--fill-flash-size 4MB`` will create a 4MB binary file.
* The ``--target-offset 0xNNN`` option will create a merged binary that should be flashed at the specified offset, instead of at offset 0x0.


UF2 Output Format
^^^^^^^^^^^^^^^^^

This command will generate a UF2 (`USB Flashing Format <https://github.com/microsoft/uf2>`_) binary.
This UF2 file can be copied to a USB mass storage device exposed by another ESP running the `ESP USB Bridge <https://github.com/espressif/esp-usb-bridge>`_ project. The bridge MCU will use it to flash the target MCU. This is as simple copying (or "drag-and-dropping") the file to the exposed disk accessed by a file explorer in your machine.

Gaps between the files will be filled with `0x00` bytes.

**UF2 options:**

* The ``--chunk-size`` option will set what portion of 512 byte block will be used for data. Common value is 256 bytes. By default the largest possible value will be used.
* The ``--md5-disable`` 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>`__.

.. code:: sh
esptool.py --chip {IDF_TARGET_NAME} merge_bin --format uf2 -o merged-flash.uf2 --flash_mode dio --flash_size 4MB 0x1000 bootloader.bin 0x8000 partition-table.bin 0x10000 app.bin
Advanced Commands
-----------------

Expand Down
33 changes: 29 additions & 4 deletions esptool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,18 +587,36 @@ def add_spi_flash_subparsers(parent, allow_keep, auto_detect):
"--output", "-o", help="Output filename", type=str, required=True
)
parser_merge_bin.add_argument(
"--format", "-f", help="Format of the output file", choices="raw", default="raw"
) # for future expansion
"--format",
"-f",
help="Format of the output file",
choices=["raw", "uf2"],
default="raw",
)
uf2_group = parser_merge_bin.add_argument_group("UF2 format")
uf2_group.add_argument(
"--chunk-size",
help="Specify the used data part of the 512 byte UF2 block. "
"A common value is 256. By default the largest possible value will be used.",
default=None,
type=arg_auto_chunk_size,
)
uf2_group.add_argument(
"--md5-disable",
help="Disable MD5 checksum in UF2 output",
action="store_true",
)
add_spi_flash_subparsers(parser_merge_bin, allow_keep=True, auto_detect=False)

parser_merge_bin.add_argument(
raw_group = parser_merge_bin.add_argument_group("RAW format")
raw_group.add_argument(
"--target-offset",
"-t",
help="Target offset where the output file will be flashed",
type=arg_auto_int,
default=0,
)
parser_merge_bin.add_argument(
raw_group.add_argument(
"--fill-flash-size",
help="If set, the final binary file will be padded with FF "
"bytes up to this flash size.",
Expand Down Expand Up @@ -910,6 +928,13 @@ def arg_auto_size(x):
return x if x == "all" else arg_auto_int(x)


def arg_auto_chunk_size(string: str) -> int:
num = int(string, 0)
if num & 3 != 0:
raise argparse.ArgumentTypeError("Chunk size should be a 4-byte aligned number")
return num


def get_port_list():
if list_ports is None:
raise FatalError(
Expand Down
58 changes: 36 additions & 22 deletions esptool/cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
timeout_per_mb,
)
from .targets import CHIP_DEFS, CHIP_LIST, ROM_LIST
from .uf2_writer import UF2Writer
from .util import (
FatalError,
NotImplementedInROMError,
Expand Down Expand Up @@ -1278,9 +1279,9 @@ def merge_bin(args):
msg = (
"Please specify the chip argument"
if args.chip == "auto"
else "Invalid chip choice: '{}'".format(args.chip)
else f"Invalid chip choice: '{args.chip}'"
)
msg = msg + " (choose from {})".format(", ".join(CHIP_LIST))
msg = f"{msg} (choose from {', '.join(CHIP_LIST)})"
raise FatalError(msg)

# sort the files by offset.
Expand All @@ -1291,32 +1292,45 @@ def merge_bin(args):
first_addr = input_files[0][0]
if first_addr < args.target_offset:
raise FatalError(
"Output file target offset is 0x%x. Input file offset 0x%x is before this."
% (args.target_offset, first_addr)
f"Output file target offset is {args.target_offset:#x}. "
f"Input file offset {first_addr:#x} is before this."
)

if args.format != "raw":
raise FatalError(
"This version of esptool only supports the 'raw' output format"
if args.format == "uf2":
with UF2Writer(
chip_class.UF2_FAMILY_ID,
args.output,
args.chunk_size,
md5_enabled=not args.md5_disable,
) as writer:
for addr, argfile in input_files:
print(f"Adding {argfile.name} at {addr:#x}")
image = argfile.read()
image = _update_image_flash_params(chip_class, addr, args, image)
writer.add_file(addr, image)
print(
f"Wrote {os.path.getsize(args.output):#x} bytes to file {args.output}, "
f"ready to be flashed with any ESP USB Bridge"
)

with open(args.output, "wb") as of:
elif args.format == "raw":
with open(args.output, "wb") as of:

def pad_to(flash_offs):
# account for output file offset if there is any
of.write(b"\xFF" * (flash_offs - args.target_offset - of.tell()))
def pad_to(flash_offs):
# account for output file offset if there is any
of.write(b"\xFF" * (flash_offs - args.target_offset - of.tell()))

for addr, argfile in input_files:
pad_to(addr)
image = argfile.read()
image = _update_image_flash_params(chip_class, addr, args, image)
of.write(image)
if args.fill_flash_size:
pad_to(flash_size_bytes(args.fill_flash_size))
print(
"Wrote 0x%x bytes to file %s, ready to flash to offset 0x%x"
% (of.tell(), args.output, args.target_offset)
)
for addr, argfile in input_files:
pad_to(addr)
image = argfile.read()
image = _update_image_flash_params(chip_class, addr, args, image)
of.write(image)
if args.fill_flash_size:
pad_to(flash_size_bytes(args.fill_flash_size))
print(
f"Wrote {of.tell():#x} bytes to file {args.output}, "
f"ready to flash to offset {args.target_offset:#x}"
)


def version(args):
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp32.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class ESP32ROM(ESPLoader):

FLASH_ENCRYPTED_WRITE_ALIGN = 32

UF2_FAMILY_ID = 0x1C5F21B0

""" Try to read the BLOCK1 (encryption key) and check if it is valid """

def is_flash_encryption_key_valid(self):
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp32c2.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class ESP32C2ROM(ESP32C3ROM):
[0x4037C000, 0x403C0000, "IRAM"],
]

UF2_FAMILY_ID = 0x2B88D29C

def get_pkg_version(self):
num_word = 1
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 22) & 0x07
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp32c3.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class ESP32C3ROM(ESP32ROM):
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
]

UF2_FAMILY_ID = 0xD42BA06C

def get_pkg_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp32c6.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ class ESP32C6ROM(ESP32C3ROM):
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
]

UF2_FAMILY_ID = 0x540DDF62

def get_pkg_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp32h2.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class ESP32H2ROM(ESP32C6ROM):
"12m": 0x2,
}

UF2_FAMILY_ID = 0x332726F6

def get_pkg_version(self):
num_word = 4
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp32p4.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class ESP32P4ROM(ESP32ROM):
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
]

UF2_FAMILY_ID = 0x3D308E94

def get_pkg_version(self):
# ESP32P4 TODO
return 0
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp32s2.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ class ESP32S2ROM(ESP32ROM):
[0x50000000, 0x50002000, "RTC_DATA"],
]

UF2_FAMILY_ID = 0xBFDD4EEE

def get_pkg_version(self):
num_word = 4
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp32s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ class ESP32S3ROM(ESP32ROM):
[0x50000000, 0x50002000, "RTC_DATA"],
]

UF2_FAMILY_ID = 0xC47E5767

def get_pkg_version(self):
num_word = 3
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
Expand Down
2 changes: 2 additions & 0 deletions esptool/targets/esp8266.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class ESP8266ROM(ESPLoader):
[0x40201010, 0x402E1010, "IROM"],
]

UF2_FAMILY_ID = 0x7EAB61ED

def get_efuses(self):
# Return the 128 bits of ESP8266 efuse as a single Python integer
result = self.read_reg(0x3FF0005C) << 96
Expand Down
Loading

0 comments on commit 3d899b2

Please sign in to comment.