mirror of
https://github.com/espressif/esptool.git
synced 2025-10-18 18:01:15 +08:00
feat(merge_bin): add support for uf2 format
This commit is contained in:

committed by
Radim Karniš

parent
4c75bb6e59
commit
3d899b2f47
@@ -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:
|
||||
|
@@ -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:
|
||||
|
||||
@@ -247,16 +246,12 @@ 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
|
||||
@@ -264,6 +259,41 @@ Will create a file ``merged-flash.bin`` with the contents of the other 3 files.
|
||||
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
|
||||
-----------------
|
||||
|
||||
|
@@ -587,18 +587,36 @@ def main(argv=None, esp=None):
|
||||
"--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.",
|
||||
@@ -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(
|
||||
|
@@ -25,6 +25,7 @@ from .loader import (
|
||||
timeout_per_mb,
|
||||
)
|
||||
from .targets import CHIP_DEFS, CHIP_LIST, ROM_LIST
|
||||
from .uf2_writer import UF2Writer
|
||||
from .util import (
|
||||
FatalError,
|
||||
NotImplementedInROMError,
|
||||
@@ -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.
|
||||
@@ -1291,33 +1292,46 @@ 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"
|
||||
)
|
||||
|
||||
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()))
|
||||
|
||||
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))
|
||||
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(
|
||||
"Wrote 0x%x bytes to file %s, ready to flash to offset 0x%x"
|
||||
% (of.tell(), args.output, args.target_offset)
|
||||
f"Wrote {os.path.getsize(args.output):#x} bytes to file {args.output}, "
|
||||
f"ready to be flashed with any ESP USB Bridge"
|
||||
)
|
||||
|
||||
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()))
|
||||
|
||||
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):
|
||||
from . import __version__
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -79,6 +79,8 @@ class ESP32P4ROM(ESP32ROM):
|
||||
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
|
||||
]
|
||||
|
||||
UF2_FAMILY_ID = 0x3D308E94
|
||||
|
||||
def get_pkg_version(self):
|
||||
# ESP32P4 TODO
|
||||
return 0
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
96
esptool/uf2_writer.py
Normal file
96
esptool/uf2_writer.py
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2020-2023 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
# Code was originally licensed under Apache 2.0 before the release of ESP-IDF v5.2
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
from typing import List
|
||||
|
||||
from esptool.util import div_roundup
|
||||
|
||||
|
||||
class UF2Writer(object):
|
||||
# The UF2 format is described here: https://github.com/microsoft/uf2
|
||||
UF2_BLOCK_SIZE = 512
|
||||
# max value of CHUNK_SIZE reduced by optional parts. Currently, MD5_PART only.
|
||||
UF2_DATA_SIZE = 476
|
||||
UF2_MD5_PART_SIZE = 24
|
||||
UF2_FIRST_MAGIC = 0x0A324655
|
||||
UF2_SECOND_MAGIC = 0x9E5D5157
|
||||
UF2_FINAL_MAGIC = 0x0AB16F30
|
||||
UF2_FLAG_FAMILYID_PRESENT = 0x00002000
|
||||
UF2_FLAG_MD5_PRESENT = 0x00004000
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chip_id: int,
|
||||
output_file: os.PathLike,
|
||||
chunk_size: int,
|
||||
md5_enabled: bool = True,
|
||||
) -> None:
|
||||
if not md5_enabled:
|
||||
self.UF2_MD5_PART_SIZE = 0
|
||||
self.UF2_FLAG_MD5_PRESENT = 0x00000000
|
||||
self.md5_enabled = md5_enabled
|
||||
self.chip_id = chip_id
|
||||
self.CHUNK_SIZE = (
|
||||
self.UF2_DATA_SIZE - self.UF2_MD5_PART_SIZE
|
||||
if chunk_size is None
|
||||
else chunk_size
|
||||
)
|
||||
self.f = open(output_file, "wb")
|
||||
|
||||
def __enter__(self) -> "UF2Writer":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: str, exc_val: int, exc_tb: List) -> None:
|
||||
if self.f:
|
||||
self.f.close()
|
||||
|
||||
@staticmethod
|
||||
def _to_uint32(num: int) -> bytes:
|
||||
return struct.pack("<I", num)
|
||||
|
||||
def _write_block(
|
||||
self, addr: int, chunk: bytes, len_chunk: int, block_no: int, blocks: int
|
||||
) -> None:
|
||||
assert len_chunk > 0
|
||||
assert len_chunk <= self.CHUNK_SIZE
|
||||
assert block_no < blocks
|
||||
block = struct.pack(
|
||||
"<IIIIIIII",
|
||||
self.UF2_FIRST_MAGIC,
|
||||
self.UF2_SECOND_MAGIC,
|
||||
self.UF2_FLAG_FAMILYID_PRESENT | self.UF2_FLAG_MD5_PRESENT,
|
||||
addr,
|
||||
len_chunk,
|
||||
block_no,
|
||||
blocks,
|
||||
self.chip_id,
|
||||
)
|
||||
block += chunk
|
||||
|
||||
if self.md5_enabled:
|
||||
md5_part = struct.pack("<II", addr, len_chunk)
|
||||
md5_part += hashlib.md5(chunk).digest()
|
||||
assert len(md5_part) == self.UF2_MD5_PART_SIZE
|
||||
|
||||
block += md5_part
|
||||
block += b"\x00" * (self.UF2_DATA_SIZE - self.UF2_MD5_PART_SIZE - len_chunk)
|
||||
block += self._to_uint32(self.UF2_FINAL_MAGIC)
|
||||
assert len(block) == self.UF2_BLOCK_SIZE
|
||||
self.f.write(block)
|
||||
|
||||
def add_file(self, addr: int, image: bytes) -> None:
|
||||
blocks = div_roundup(len(image), self.CHUNK_SIZE)
|
||||
chunks = [
|
||||
image[i : i + self.CHUNK_SIZE]
|
||||
for i in range(0, len(image), self.CHUNK_SIZE)
|
||||
]
|
||||
for i, chunk in enumerate(chunks):
|
||||
len_chunk = len(chunk)
|
||||
self._write_block(addr, chunk, len_chunk, i, blocks)
|
||||
addr += len_chunk
|
2
setup.py
2
setup.py
@@ -101,6 +101,7 @@ setup(
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
],
|
||||
python_requires=">=3.7",
|
||||
setup_requires=(["wheel"] if "bdist_wheel" in sys.argv else []),
|
||||
@@ -115,6 +116,7 @@ setup(
|
||||
"pre-commit",
|
||||
"pytest",
|
||||
"pytest-rerunfailures",
|
||||
"requests",
|
||||
],
|
||||
"hsm": [
|
||||
"python-pkcs11",
|
||||
|
@@ -1,9 +1,14 @@
|
||||
import filecmp
|
||||
import hashlib
|
||||
import itertools
|
||||
import os
|
||||
import os.path
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from functools import partial
|
||||
|
||||
IMAGES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "images")
|
||||
|
||||
@@ -13,6 +18,8 @@ import pytest
|
||||
|
||||
try:
|
||||
from esptool.util import byte
|
||||
from esptool.uf2_writer import UF2Writer
|
||||
from esptool.targets import CHIP_DEFS
|
||||
except ImportError:
|
||||
need_to_install_package_err()
|
||||
|
||||
@@ -189,3 +196,239 @@ class TestMergeBin:
|
||||
assert bootloader == merged[: len(bootloader)]
|
||||
assert helloworld == merged[0xF000 : 0xF000 + len(helloworld)]
|
||||
self.assertAllFF(merged[0xF000 + len(helloworld) :])
|
||||
|
||||
|
||||
class UF2Block(object):
|
||||
def __init__(self, bs):
|
||||
self.length = len(bs)
|
||||
|
||||
# See https://github.com/microsoft/uf2 for the format
|
||||
first_part = "<" + "I" * 8
|
||||
# payload is between
|
||||
last_part = "<I"
|
||||
|
||||
first_part_len = struct.calcsize(first_part)
|
||||
last_part_len = struct.calcsize(last_part)
|
||||
|
||||
(
|
||||
self.magicStart0,
|
||||
self.magicStart1,
|
||||
self.flags,
|
||||
self.targetAddr,
|
||||
self.payloadSize,
|
||||
self.blockNo,
|
||||
self.numBlocks,
|
||||
self.familyID,
|
||||
) = struct.unpack(first_part, bs[:first_part_len])
|
||||
|
||||
self.data = bs[first_part_len:-last_part_len]
|
||||
|
||||
(self.magicEnd,) = struct.unpack(last_part, bs[-last_part_len:])
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
|
||||
class UF2BlockReader(object):
|
||||
def __init__(self, f_name):
|
||||
self.f_name = f_name
|
||||
|
||||
def get(self):
|
||||
with open(self.f_name, "rb") as f:
|
||||
for chunk in iter(partial(f.read, UF2Writer.UF2_BLOCK_SIZE), b""):
|
||||
yield UF2Block(chunk)
|
||||
|
||||
|
||||
class BinaryWriter(object):
|
||||
def __init__(self, f_name):
|
||||
self.f_name = f_name
|
||||
|
||||
def append(self, data):
|
||||
# File is reopened several times in order to make sure that won't left open
|
||||
with open(self.f_name, "ab") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
class TestUF2:
|
||||
def generate_binary(self, size):
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
for _ in range(size):
|
||||
f.write(struct.pack("B", random.randrange(0, 1 << 8)))
|
||||
return f.name
|
||||
|
||||
@staticmethod
|
||||
def generate_chipID():
|
||||
chip, rom = random.choice(list(CHIP_DEFS.items()))
|
||||
family_id = rom.UF2_FAMILY_ID
|
||||
return chip, family_id
|
||||
|
||||
def generate_uf2(
|
||||
self,
|
||||
of_name,
|
||||
chip_id,
|
||||
iter_addr_offset_tuples,
|
||||
chunk_size=None,
|
||||
md5_enable=True,
|
||||
):
|
||||
com_args = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"esptool",
|
||||
"--chip",
|
||||
chip_id,
|
||||
"merge_bin",
|
||||
"--format",
|
||||
"uf2",
|
||||
"-o",
|
||||
of_name,
|
||||
]
|
||||
if not md5_enable:
|
||||
com_args.append("--md5-disable")
|
||||
com_args += [] if chunk_size is None else ["--chunk-size", str(chunk_size)]
|
||||
file_args = list(
|
||||
itertools.chain(*[(hex(addr), f) for addr, f in iter_addr_offset_tuples])
|
||||
)
|
||||
|
||||
output = subprocess.check_output(com_args + file_args, stderr=subprocess.STDOUT)
|
||||
output = output.decode("utf-8")
|
||||
print(output)
|
||||
assert "warning" not in output.lower(), "merge_bin should not output warnings"
|
||||
|
||||
exp_list = [f"Adding {f} at {hex(addr)}" for addr, f in iter_addr_offset_tuples]
|
||||
exp_list += [
|
||||
f"bytes to file {of_name}, ready to be flashed with any ESP USB Bridge"
|
||||
]
|
||||
for e in exp_list:
|
||||
assert e in output
|
||||
|
||||
return of_name
|
||||
|
||||
def process_blocks(self, uf2block, expected_chip_id, md5_enable=True):
|
||||
flags = UF2Writer.UF2_FLAG_FAMILYID_PRESENT
|
||||
if md5_enable:
|
||||
flags |= UF2Writer.UF2_FLAG_MD5_PRESENT
|
||||
|
||||
parsed_binaries = []
|
||||
|
||||
block_list = [] # collect block numbers here
|
||||
total_blocks = set() # collect total block numbers here
|
||||
for block in UF2BlockReader(uf2block).get():
|
||||
if block.blockNo == 0:
|
||||
# new file has been detected
|
||||
base_addr = block.targetAddr
|
||||
current_addr = base_addr
|
||||
binary_writer = BinaryWriter(self.generate_binary(0))
|
||||
|
||||
assert len(block) == UF2Writer.UF2_BLOCK_SIZE
|
||||
assert block.magicStart0 == UF2Writer.UF2_FIRST_MAGIC
|
||||
assert block.magicStart1 == UF2Writer.UF2_SECOND_MAGIC
|
||||
assert block.flags & flags == flags
|
||||
|
||||
assert len(block.data) == UF2Writer.UF2_DATA_SIZE
|
||||
payload = block.data[: block.payloadSize]
|
||||
if md5_enable:
|
||||
md5_obj = hashlib.md5(payload)
|
||||
md5_part = block.data[
|
||||
block.payloadSize : block.payloadSize + UF2Writer.UF2_MD5_PART_SIZE
|
||||
]
|
||||
address, length = struct.unpack("<II", md5_part[: -md5_obj.digest_size])
|
||||
md5sum = md5_part[-md5_obj.digest_size :]
|
||||
assert address == block.targetAddr
|
||||
assert length == block.payloadSize
|
||||
assert md5sum == md5_obj.digest()
|
||||
|
||||
assert block.familyID == expected_chip_id
|
||||
assert block.magicEnd == UF2Writer.UF2_FINAL_MAGIC
|
||||
|
||||
assert current_addr == block.targetAddr
|
||||
binary_writer.append(payload)
|
||||
|
||||
block_list.append(block.blockNo)
|
||||
total_blocks.add(block.numBlocks)
|
||||
if block.blockNo == block.numBlocks - 1:
|
||||
assert block_list == list(range(block.numBlocks))
|
||||
# we have found all blocks and in the right order
|
||||
assert total_blocks == {
|
||||
block.numBlocks
|
||||
} # numBlocks are the same in all the blocks
|
||||
del block_list[:]
|
||||
total_blocks.clear()
|
||||
|
||||
parsed_binaries += [(base_addr, binary_writer.f_name)]
|
||||
|
||||
current_addr += block.payloadSize
|
||||
return parsed_binaries
|
||||
|
||||
def common(self, t, chunk_size=None, md5_enable=True):
|
||||
of_name = self.generate_binary(0)
|
||||
try:
|
||||
chip_name, chip_id = self.generate_chipID()
|
||||
self.generate_uf2(of_name, chip_name, t, chunk_size, md5_enable)
|
||||
parsed_t = self.process_blocks(of_name, chip_id, md5_enable)
|
||||
|
||||
assert len(t) == len(parsed_t)
|
||||
for (orig_addr, orig_fname), (addr, fname) in zip(t, parsed_t):
|
||||
assert orig_addr == addr
|
||||
assert filecmp.cmp(orig_fname, fname)
|
||||
finally:
|
||||
os.unlink(of_name)
|
||||
for _, file_name in t:
|
||||
os.unlink(file_name)
|
||||
|
||||
def test_simple(self):
|
||||
self.common([(0, self.generate_binary(1))])
|
||||
|
||||
def test_more_files(self):
|
||||
self.common(
|
||||
[(0x100, self.generate_binary(1)), (0x1000, self.generate_binary(1))]
|
||||
)
|
||||
|
||||
def test_larger_files(self):
|
||||
self.common(
|
||||
[(0x100, self.generate_binary(6)), (0x1000, self.generate_binary(8))]
|
||||
)
|
||||
|
||||
def test_boundaries(self):
|
||||
self.common(
|
||||
[
|
||||
(0x100, self.generate_binary(UF2Writer.UF2_DATA_SIZE)),
|
||||
(0x2000, self.generate_binary(UF2Writer.UF2_DATA_SIZE + 1)),
|
||||
(0x3000, self.generate_binary(UF2Writer.UF2_DATA_SIZE - 1)),
|
||||
]
|
||||
)
|
||||
|
||||
def test_files_with_more_blocks(self):
|
||||
self.common(
|
||||
[
|
||||
(0x100, self.generate_binary(3 * UF2Writer.UF2_DATA_SIZE)),
|
||||
(0x2000, self.generate_binary(2 * UF2Writer.UF2_DATA_SIZE + 1)),
|
||||
(0x3000, self.generate_binary(2 * UF2Writer.UF2_DATA_SIZE - 1)),
|
||||
]
|
||||
)
|
||||
|
||||
def test_very_large_files(self):
|
||||
self.common(
|
||||
[
|
||||
(0x100, self.generate_binary(20 * UF2Writer.UF2_DATA_SIZE + 5)),
|
||||
(0x10000, self.generate_binary(50 * UF2Writer.UF2_DATA_SIZE + 100)),
|
||||
(0x100000, self.generate_binary(100 * UF2Writer.UF2_DATA_SIZE)),
|
||||
]
|
||||
)
|
||||
|
||||
def test_chunk_size(self):
|
||||
chunk_size = 256
|
||||
self.common(
|
||||
[
|
||||
(0x1000, self.generate_binary(chunk_size)),
|
||||
(0x2000, self.generate_binary(chunk_size + 1)),
|
||||
(0x3000, self.generate_binary(chunk_size - 1)),
|
||||
],
|
||||
chunk_size,
|
||||
)
|
||||
|
||||
def test_md5_disable(self):
|
||||
self.common(
|
||||
[(0x100, self.generate_binary(1)), (0x2000, self.generate_binary(1))],
|
||||
md5_enable=False,
|
||||
)
|
||||
|
66
test/test_uf2_ids.py
Normal file
66
test/test_uf2_ids.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import json
|
||||
|
||||
from conftest import need_to_install_package_err
|
||||
|
||||
import pytest
|
||||
|
||||
import requests
|
||||
|
||||
try:
|
||||
from esptool.targets import CHIP_DEFS
|
||||
except ImportError:
|
||||
need_to_install_package_err()
|
||||
|
||||
|
||||
FAMILIES_URL = (
|
||||
"https://raw.githubusercontent.com/microsoft/uf2/master/utils/uf2families.json"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def uf2_json():
|
||||
"""Download UF2 family IDs from Microsoft UF2 repo and filter out ESP chips"""
|
||||
res = requests.get(FAMILIES_URL)
|
||||
assert res.status_code == 200
|
||||
uf2_families_json = json.loads(res.content)
|
||||
# filter out just ESP chips
|
||||
chips = [
|
||||
chip
|
||||
for chip in uf2_families_json
|
||||
if chip["short_name"].upper().startswith("ESP")
|
||||
]
|
||||
return chips
|
||||
|
||||
|
||||
def test_check_uf2family_ids(uf2_json):
|
||||
"""Compare UF2 family IDs from Microsoft UF2 repo and with stored values"""
|
||||
# check if all UF2 family ids match
|
||||
for chip in uf2_json:
|
||||
assert int(chip["id"], 0) == CHIP_DEFS[chip["short_name"].lower()].UF2_FAMILY_ID
|
||||
|
||||
|
||||
def test_check_uf2(uf2_json):
|
||||
"""Check if all non-beta chip definition has UF2 family id in esptool
|
||||
and also in Miscrosoft repo
|
||||
"""
|
||||
# remove beta chip definitions
|
||||
esptool_chips = set(
|
||||
[chip.upper() for chip in CHIP_DEFS.keys() if "beta" not in chip]
|
||||
)
|
||||
microsoft_repo_chips = set([chip["short_name"] for chip in uf2_json])
|
||||
diff = esptool_chips.symmetric_difference(microsoft_repo_chips)
|
||||
if diff:
|
||||
out = []
|
||||
# there was a difference between the chip support
|
||||
for chip in diff:
|
||||
if chip in esptool_chips:
|
||||
out.append(
|
||||
f"Missing chip definition for '{chip}' in esptool "
|
||||
"which was defined in Microsoft UF2 Github repo."
|
||||
)
|
||||
else:
|
||||
out.append(
|
||||
f"Please consider adding support for chip '{chip}' "
|
||||
f"to the UF2 repository: {FAMILIES_URL}"
|
||||
)
|
||||
pytest.fail("\n".join(out))
|
Reference in New Issue
Block a user