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