1# Copyright (c) 2020 Teslabs Engineering S.L. 2# 3# SPDX-License-Identifier: Apache-2.0 4 5"""Runner for flashing with STM32CubeProgrammer CLI, the official programming 6 utility from ST Microelectronics. 7""" 8 9import argparse 10import functools 11import os 12import platform 13import shlex 14import shutil 15from pathlib import Path 16from typing import ClassVar 17 18from runners.core import RunnerCaps, RunnerConfig, ZephyrBinaryRunner 19 20 21class STM32CubeProgrammerBinaryRunner(ZephyrBinaryRunner): 22 """Runner front-end for STM32CubeProgrammer CLI.""" 23 24 _RESET_MODES: ClassVar[dict[str, str]] = { 25 "sw": "SWrst", 26 "hw": "HWrst", 27 "core": "Crst", 28 } 29 """Reset mode argument mappings.""" 30 31 def __init__( 32 self, 33 cfg: RunnerConfig, 34 port: str, 35 dev_id: str | None, 36 frequency: int | None, 37 reset_mode: str | None, 38 download_address: int | None, 39 download_modifiers: list[str], 40 start_address: int | None, 41 start_modifiers: list[str], 42 conn_modifiers: str | None, 43 cli: Path | None, 44 use_elf: bool, 45 erase: bool, 46 extload: str | None, 47 tool_opt: list[str], 48 ) -> None: 49 super().__init__(cfg) 50 51 self._port = port 52 self._dev_id = dev_id 53 self._frequency = frequency 54 55 self._download_address = download_address 56 self._download_modifiers: list[str] = list() 57 for opts in [shlex.split(opt) for opt in download_modifiers]: 58 self._download_modifiers += opts 59 60 self._start_address = start_address 61 self._start_modifiers: list[str] = list() 62 for opts in [shlex.split(opt) for opt in start_modifiers]: 63 self._start_modifiers += opts 64 65 self._reset_mode = reset_mode 66 self._conn_modifiers = conn_modifiers 67 self._cli = ( 68 cli or STM32CubeProgrammerBinaryRunner._get_stm32cubeprogrammer_path() 69 ) 70 self._use_elf = use_elf 71 self._erase = erase 72 73 if extload: 74 p = ( 75 STM32CubeProgrammerBinaryRunner._get_stm32cubeprogrammer_path().parent.resolve() 76 / 'ExternalLoader' 77 ) 78 self._extload = ['-el', str(p / extload)] 79 else: 80 self._extload = [] 81 82 self._tool_opt: list[str] = list() 83 for opts in [shlex.split(opt) for opt in tool_opt]: 84 self._tool_opt += opts 85 86 # add required library loader path to the environment (Linux only) 87 if platform.system() == "Linux": 88 os.environ["LD_LIBRARY_PATH"] = str(self._cli.parent / ".." / "lib") 89 90 @staticmethod 91 def _get_stm32cubeprogrammer_path() -> Path: 92 """Obtain path of the STM32CubeProgrammer CLI tool.""" 93 94 if platform.system() == "Linux": 95 cmd = shutil.which("STM32_Programmer_CLI") 96 if cmd is not None: 97 return Path(cmd) 98 99 return ( 100 Path.home() 101 / "STMicroelectronics" 102 / "STM32Cube" 103 / "STM32CubeProgrammer" 104 / "bin" 105 / "STM32_Programmer_CLI" 106 ) 107 108 if platform.system() == "Windows": 109 cmd = shutil.which("STM32_Programmer_CLI") 110 if cmd is not None: 111 return Path(cmd) 112 113 cli = ( 114 Path("STMicroelectronics") 115 / "STM32Cube" 116 / "STM32CubeProgrammer" 117 / "bin" 118 / "STM32_Programmer_CLI.exe" 119 ) 120 x86_path = Path(os.environ["PROGRAMFILES(X86)"]) / cli 121 if x86_path.exists(): 122 return x86_path 123 124 return Path(os.environ["PROGRAMW6432"]) / cli 125 126 if platform.system() == "Darwin": 127 cmd = shutil.which("STM32_Programmer_CLI") 128 if cmd is not None: 129 return Path(cmd) 130 131 return ( 132 Path("/Applications") 133 / "STMicroelectronics" 134 / "STM32Cube" 135 / "STM32CubeProgrammer" 136 / "STM32CubeProgrammer.app" 137 / "Contents" 138 / "MacOs" 139 / "bin" 140 / "STM32_Programmer_CLI" 141 ) 142 143 raise NotImplementedError("Could not determine STM32_Programmer_CLI path") 144 145 @classmethod 146 def name(cls): 147 return "stm32cubeprogrammer" 148 149 @classmethod 150 def capabilities(cls): 151 return RunnerCaps(commands={"flash"}, dev_id=True, erase=True, extload=True, tool_opt=True) 152 153 @classmethod 154 def do_add_parser(cls, parser): 155 # To accept arguments in hex format, a wrapper lambda around int() must be used. 156 # Wrapping the lambda with functools.wraps() makes it so that 'invalid int value' 157 # is displayed when an invalid value is provided for these arguments. 158 multi_base=functools.wraps(int)(lambda s: int(s, base=0)) 159 parser.add_argument( 160 "--port", 161 type=str, 162 required=True, 163 help="Interface identifier, e.g. swd, jtag, /dev/ttyS0...", 164 ) 165 parser.add_argument( 166 "--frequency", type=int, required=False, help="Programmer frequency in KHz" 167 ) 168 parser.add_argument( 169 "--reset-mode", 170 type=str, 171 required=False, 172 choices=["sw", "hw", "core"], 173 help="Reset mode", 174 ) 175 parser.add_argument( 176 "--download-address", 177 type=multi_base, 178 required=False, 179 help="Flashing location address. If present, .bin used instead of .hex" 180 ) 181 parser.add_argument( 182 "--download-modifiers", 183 default=[], 184 required=False, 185 action='append', 186 help="Additional options for the --download argument" 187 ) 188 parser.add_argument( 189 "--start-address", 190 type=multi_base, 191 required=False, 192 help="Address where execution should begin after flashing" 193 ) 194 parser.add_argument( 195 "--start-modifiers", 196 default=[], 197 required=False, 198 action='append', 199 help="Additional options for the --start argument" 200 ) 201 parser.add_argument( 202 "--conn-modifiers", 203 type=str, 204 required=False, 205 help="Additional options for the --connect argument", 206 ) 207 parser.add_argument( 208 "--cli", 209 type=Path, 210 required=False, 211 help="STM32CubeProgrammer CLI tool path", 212 ) 213 parser.add_argument( 214 "--use-elf", 215 action="store_true", 216 required=False, 217 help="Use ELF file when flashing instead of HEX file", 218 ) 219 220 @classmethod 221 def extload_help(cls) -> str: 222 return "External Loader for STM32_Programmer_CLI" 223 224 @classmethod 225 def tool_opt_help(cls) -> str: 226 return "Additional options for STM32_Programmer_CLI" 227 228 @classmethod 229 def do_create( 230 cls, cfg: RunnerConfig, args: argparse.Namespace 231 ) -> "STM32CubeProgrammerBinaryRunner": 232 return STM32CubeProgrammerBinaryRunner( 233 cfg, 234 port=args.port, 235 dev_id=args.dev_id, 236 frequency=args.frequency, 237 reset_mode=args.reset_mode, 238 download_address=args.download_address, 239 download_modifiers=args.download_modifiers, 240 start_address=args.start_address, 241 start_modifiers=args.start_modifiers, 242 conn_modifiers=args.conn_modifiers, 243 cli=args.cli, 244 use_elf=args.use_elf, 245 erase=args.erase, 246 extload=args.extload, 247 tool_opt=args.tool_opt, 248 ) 249 250 def do_run(self, command: str, **kwargs): 251 if command == "flash": 252 self.flash(**kwargs) 253 254 def flash(self, **kwargs) -> None: 255 self.require(str(self._cli)) 256 257 # prepare base command 258 cmd = [str(self._cli)] 259 260 connect_opts = f"port={self._port}" 261 if self._frequency: 262 connect_opts += f" freq={self._frequency}" 263 if self._reset_mode: 264 reset_mode = STM32CubeProgrammerBinaryRunner._RESET_MODES[self._reset_mode] 265 connect_opts += f" reset={reset_mode}" 266 if self._conn_modifiers: 267 connect_opts += f" {self._conn_modifiers}" 268 if self._dev_id: 269 connect_opts += f" sn={self._dev_id}" 270 271 cmd += ["--connect", connect_opts] 272 cmd += self._tool_opt 273 if self._extload: 274 # external loader to come after the tool option in STM32CubeProgrammer 275 cmd += self._extload 276 277 # erase first if requested 278 if self._erase: 279 self.check_call(cmd + ["--erase", "all"]) 280 281 # Define binary to be loaded 282 dl_file = None 283 284 if self._use_elf: 285 # Use elf file if instructed to do so. 286 dl_file = self.cfg.elf_file 287 elif (self.cfg.bin_file is not None and 288 (self._download_address is not None or 289 (str(self._port).startswith("usb") and self._download_modifiers is not None))): 290 # Use bin file if a binary is available and 291 # --download-address provided 292 # or flashing by dfu (port=usb and download-modifier used) 293 dl_file = self.cfg.bin_file 294 elif self.cfg.hex_file is not None: 295 # Neither --use-elf nor --download-address are present: 296 # default to flashing using hex file. 297 dl_file = self.cfg.hex_file 298 299 # Verify file configuration 300 if dl_file is None: 301 raise RuntimeError('cannot flash; no download file was specified') 302 elif not os.path.isfile(dl_file): 303 raise RuntimeError(f'download file {dl_file} does not exist') 304 305 flash_and_run_args = ["--download", dl_file] 306 if self._download_address is not None: 307 flash_and_run_args.append(f"0x{self._download_address:X}") 308 flash_and_run_args += self._download_modifiers 309 310 # '--start' is needed to start execution after flash. 311 # The default start address is the beggining of the flash, 312 # but another value can be explicitly specified if desired. 313 flash_and_run_args.append("--start") 314 if self._start_address is not None: 315 flash_and_run_args.append(f"0x{self._start_address:X}") 316 flash_and_run_args += self._start_modifiers 317 318 self.check_call(cmd + flash_and_run_args) 319