diff --git a/boards/common/uf2.board.cmake b/boards/common/uf2.board.cmake new file mode 100644 index 00000000000..ff222729ade --- /dev/null +++ b/boards/common/uf2.board.cmake @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 + +board_set_flasher_ifnset(uf2) +board_finalize_runner_args(uf2) # No default arguments to provide. diff --git a/cmake/flash/CMakeLists.txt b/cmake/flash/CMakeLists.txt index 24465c2c167..892cfa081e8 100644 --- a/cmake/flash/CMakeLists.txt +++ b/cmake/flash/CMakeLists.txt @@ -48,6 +48,10 @@ function(runners_yaml_append_config) get_runners_prop(bin_file bin "${KERNEL_BIN_NAME}") runners_yaml_append(" bin_file: ${bin}") endif() + if(CONFIG_BUILD_OUTPUT_UF2) + get_runners_prop(uf2_file uf2 "${KERNEL_UF2_NAME}") + runners_yaml_append(" uf2_file: ${uf2}") + endif() if(CMAKE_GDB OR OPENOCD OR OPENOCD_DEFAULT_PATH) runners_yaml_append(" # Host tools:") diff --git a/scripts/west_commands/run_common.py b/scripts/west_commands/run_common.py index 34f2eb2356e..ba62c5c46f7 100644 --- a/scripts/west_commands/run_common.py +++ b/scripts/west_commands/run_common.py @@ -413,6 +413,7 @@ def get_runner_config(build_dir, yaml_path, runners_yaml, args=None): output_file('elf'), output_file('hex'), output_file('bin'), + output_file('uf2'), config('file'), filetype('file_type'), config('gdb'), diff --git a/scripts/west_commands/runners/__init__.py b/scripts/west_commands/runners/__init__.py index 90eefcadffe..23b1cca650c 100644 --- a/scripts/west_commands/runners/__init__.py +++ b/scripts/west_commands/runners/__init__.py @@ -49,6 +49,7 @@ _names = [ 'stm32cubeprogrammer', 'stm32flash', 'trace32', + 'uf2', 'xtensa', # Keep this list sorted by runner name; don't add to the end. ] diff --git a/scripts/west_commands/runners/core.py b/scripts/west_commands/runners/core.py index 7ed95d5bd1d..5a5b1bdaf6e 100644 --- a/scripts/west_commands/runners/core.py +++ b/scripts/west_commands/runners/core.py @@ -285,6 +285,7 @@ class RunnerConfig(NamedTuple): elf_file: Optional[str] # zephyr.elf path, or None hex_file: Optional[str] # zephyr.hex path, or None bin_file: Optional[str] # zephyr.bin path, or None + uf2_file: Optional[str] # zephyr.uf2 path, or None file: Optional[str] # binary file path (provided by the user), or None file_type: Optional[FileType] = FileType.OTHER # binary file type gdb: Optional[str] = None # path to a usable gdb @@ -758,7 +759,7 @@ class ZephyrBinaryRunner(abc.ABC): else: return - if output_type in ('elf', 'hex', 'bin'): + if output_type in ('elf', 'hex', 'bin', 'uf2'): err += f' Try enabling CONFIG_BUILD_OUTPUT_{output_type.upper()}.' # RuntimeError avoids a stack trace saved in run_common. diff --git a/scripts/west_commands/runners/uf2.py b/scripts/west_commands/runners/uf2.py new file mode 100644 index 00000000000..7cb5eced390 --- /dev/null +++ b/scripts/west_commands/runners/uf2.py @@ -0,0 +1,105 @@ +# Copyright (c) 2023 Peter Johanson +# +# SPDX-License-Identifier: Apache-2.0 + +'''UF2 runner (flash only) for UF2 compatible bootloaders.''' + +from pathlib import Path +from shutil import copy + +from runners.core import ZephyrBinaryRunner, RunnerCaps + +try: + import psutil # pylint: disable=unused-import + MISSING_PSUTIL = False +except ImportError: + # This can happen when building the documentation for the + # runners package if psutil is not on sys.path. This is fine + # to ignore in that case. + MISSING_PSUTIL = True + +class UF2BinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for copying to UF2 USB-MSC mounts.''' + + def __init__(self, cfg, board_id=None): + super().__init__(cfg) + self.board_id = board_id + + @classmethod + def name(cls): + return 'uf2' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'flash'}) + + @classmethod + def do_add_parser(cls, parser): + parser.add_argument('--board-id', dest='board_id', + help='Board-ID value to match from INFO_UF2.TXT') + + @classmethod + def do_create(cls, cfg, args): + return UF2BinaryRunner(cfg, board_id=args.board_id) + + @staticmethod + def get_uf2_info_path(part) -> Path: + return Path(part.mountpoint) / "INFO_UF2.TXT" + + @staticmethod + def is_uf2_partition(part): + try: + return ((part.fstype in ['vfat', 'FAT']) and + UF2BinaryRunner.get_uf2_info_path(part).is_file()) + except PermissionError: + return False + + @staticmethod + def get_uf2_info(part): + lines = UF2BinaryRunner.get_uf2_info_path(part).read_text().splitlines() + + lines = lines[1:] # Skip the first summary line + + def split_uf2_info(line: str): + k, _, val = line.partition(':') + return k.strip(), val.strip() + + return {k: v for k, v in (split_uf2_info(line) for line in lines) if k and v} + + def match_board_id(self, part): + info = self.get_uf2_info(part) + + return info.get('Board-ID') == self.board_id + + def get_uf2_partitions(self): + parts = [part for part in psutil.disk_partitions() if self.is_uf2_partition(part)] + + if (self.board_id is not None) and parts: + parts = [part for part in parts if self.match_board_id(part)] + if not parts: + self.logger.warning("Discovered UF2 partitions don't match Board-ID '%s'", + self.board_id) + + return parts + + def copy_uf2_to_partition(self, part): + self.ensure_output('uf2') + + copy(self.cfg.uf2_file, part.mountpoint) + + def do_run(self, command, **kwargs): + if MISSING_PSUTIL: + raise RuntimeError( + 'could not import psutil; something may be wrong with the ' + 'python environment') + + partitions = self.get_uf2_partitions() + if not partitions: + raise RuntimeError('No matching UF2 partitions found') + + if len(partitions) > 1: + raise RuntimeError('More than one matching UF2 partitions found') + + part = partitions[0] + self.logger.info("Copying UF2 file to '%s'", part.mountpoint) + self.copy_uf2_to_partition(part) diff --git a/scripts/west_commands/tests/test_imports.py b/scripts/west_commands/tests/test_imports.py index d0804be8c8c..33a51049c09 100644 --- a/scripts/west_commands/tests/test_imports.py +++ b/scripts/west_commands/tests/test_imports.py @@ -39,5 +39,6 @@ def test_runner_imports(): 'stm32cubeprogrammer', 'stm32flash', 'trace32', + 'uf2', 'xtensa')) assert runner_names == expected