1# Copyright (c) 2018 Roman Tataurov <diytronic@yandex.ru>
2# Modified 2018 Tavish Naruka <tavishnaruka@gmail.com>
3#
4# SPDX-License-Identifier: Apache-2.0
5'''Runner for flashing with Black Magic Probe.'''
6# https://github.com/blacksphere/blackmagic/wiki
7
8import glob
9import os
10import signal
11import sys
12from pathlib import Path
13
14from runners.core import RunnerCaps, ZephyrBinaryRunner
15
16try:
17    import serial.tools.list_ports
18    MISSING_REQUIREMENTS = False
19except ImportError:
20    MISSING_REQUIREMENTS = True
21
22# Default path for linux, based on the project udev.rules file.
23DEFAULT_LINUX_BMP_PATH = '/dev/ttyBmpGdb'
24
25# Interface descriptor for the GDB port as defined in the BMP firmware.
26BMP_GDB_INTERFACE = 'Black Magic GDB Server'
27
28# Product string as defined in the BMP firmware.
29BMP_GDB_PRODUCT = "Black Magic Probe"
30
31# BMP vendor and product ID.
32BMP_GDB_VID = 0x1d50
33BMP_GDB_PID = 0x6018
34
35LINUX_SERIAL_GLOB = '/dev/ttyACM*'
36DARWIN_SERIAL_GLOB = '/dev/cu.usbmodem*'
37
38def blackmagicprobe_gdb_serial_linux():
39    '''Guess the GDB port on Linux platforms.'''
40    if os.path.exists(DEFAULT_LINUX_BMP_PATH):
41        return DEFAULT_LINUX_BMP_PATH
42
43    if not MISSING_REQUIREMENTS:
44        for port in serial.tools.list_ports.comports():
45            if port.interface == BMP_GDB_INTERFACE:
46                return port.device
47
48    ports = glob.glob(LINUX_SERIAL_GLOB)
49    if not ports:
50        raise RuntimeError(
51                f'cannot find any valid port matching {LINUX_SERIAL_GLOB}')
52    return sorted(ports)[0]
53
54def blackmagicprobe_gdb_serial_darwin():
55    '''Guess the GDB port on Darwin platforms.'''
56    if not MISSING_REQUIREMENTS:
57        bmp_ports = []
58        for port in serial.tools.list_ports.comports():
59            if port.description and port.description.startswith(
60                    BMP_GDB_PRODUCT):
61                bmp_ports.append(port.device)
62        if bmp_ports:
63            return sorted(bmp_ports)[0]
64
65    ports = glob.glob(DARWIN_SERIAL_GLOB)
66    if not ports:
67        raise RuntimeError(
68                f'cannot find any valid port matching {DARWIN_SERIAL_GLOB}')
69    return sorted(ports)[0]
70
71def blackmagicprobe_gdb_serial_win32():
72    '''Guess the GDB port on Windows platforms.'''
73    if not MISSING_REQUIREMENTS:
74        bmp_ports = []
75        for port in serial.tools.list_ports.comports():
76            if port.vid == BMP_GDB_VID and port.pid == BMP_GDB_PID:
77                bmp_ports.append(port.device)
78        if bmp_ports:
79            return sorted(bmp_ports)[0]
80
81    return 'COM1'
82
83def blackmagicprobe_gdb_serial(port):
84    '''Guess the GDB port for the probe.
85
86    Return the port to use, in order of priority:
87        - the port specified manually
88        - the port in the BMP_GDB_SERIAL environment variable
89        - a guessed one depending on the host
90    '''
91    if port:
92        return port
93
94    if 'BMP_GDB_SERIAL' in os.environ:
95        return os.environ['BMP_GDB_SERIAL']
96
97    platform = sys.platform
98    if platform.startswith('linux'):
99        return blackmagicprobe_gdb_serial_linux()
100    elif platform.startswith('darwin'):
101        return blackmagicprobe_gdb_serial_darwin()
102    elif platform.startswith('win32'):
103        return blackmagicprobe_gdb_serial_win32()
104    else:
105        raise RuntimeError(f'unsupported platform: {platform}')
106
107
108class BlackMagicProbeRunner(ZephyrBinaryRunner):
109    '''Runner front-end for Black Magic probe.'''
110
111    def __init__(self, cfg, gdb_serial, connect_rst=False):
112        super().__init__(cfg)
113        self.gdb = [cfg.gdb] if cfg.gdb else None
114        # as_posix() because gdb doesn't recognize backslashes as path
115        # separators for the 'load' command we execute in bmp_flash().
116        #
117        # https://github.com/zephyrproject-rtos/zephyr/issues/50789
118        self.elf_file = Path(cfg.elf_file).as_posix()
119        if cfg.hex_file is not None:
120            self.hex_file = Path(cfg.hex_file).as_posix()
121        else:
122            self.hex_file = None
123        self.gdb_serial = blackmagicprobe_gdb_serial(gdb_serial)
124        self.logger.info(f'using GDB serial: {self.gdb_serial}')
125        if connect_rst:
126            self.connect_rst_enable_arg = [
127                    '-ex', "monitor connect_rst enable",
128                    '-ex', "monitor connect_srst enable",
129                    ]
130            self.connect_rst_disable_arg = [
131                    '-ex', "monitor connect_rst disable",
132                    '-ex', "monitor connect_srst disable",
133                    ]
134        else:
135            self.connect_rst_enable_arg = []
136            self.connect_rst_disable_arg = []
137
138    @classmethod
139    def name(cls):
140        return 'blackmagicprobe'
141
142    @classmethod
143    def capabilities(cls):
144        return RunnerCaps(commands={'flash', 'debug', 'attach'})
145
146    @classmethod
147    def do_create(cls, cfg, args):
148        return BlackMagicProbeRunner(cfg, args.gdb_serial, args.connect_rst)
149
150    @classmethod
151    def do_add_parser(cls, parser):
152        parser.add_argument('--gdb-serial', help='GDB serial port')
153        parser.add_argument('--connect-rst', '--connect-srst', action='store_true',
154                            help='Assert SRST during connect? (default: no)')
155
156    def bmp_flash(self, command, **kwargs):
157        # if hex file is present and signed, use it else use elf file
158        if self.hex_file:
159            split = self.hex_file.split('.')
160            # eg zephyr.signed.hex
161            if len(split) >= 3 and split[-2] == 'signed':
162                flash_file = self.hex_file
163            else:
164                flash_file = self.elf_file
165        else:
166            flash_file = self.elf_file
167
168        if flash_file is None:
169            raise ValueError('Cannot flash; elf file is missing')
170
171        command = (self.gdb +
172                   ['-ex', "set confirm off",
173                    '-ex', f"target extended-remote {self.gdb_serial}"] +
174                    self.connect_rst_enable_arg +
175                   ['-ex', "monitor swdp_scan",
176                    '-ex', "attach 1",
177                    '-ex', f"load {flash_file}",
178                    '-ex', "kill",
179                    '-ex', "quit",
180                    '-silent'])
181        self.check_call(command)
182
183    def check_call_ignore_sigint(self, command):
184        previous = signal.signal(signal.SIGINT, signal.SIG_IGN)
185        try:
186            self.check_call(command)
187        finally:
188            signal.signal(signal.SIGINT, previous)
189
190    def bmp_attach(self, command, **kwargs):
191        if self.elf_file is None:
192            command = (self.gdb +
193                       ['-ex', "set confirm off",
194                        '-ex', f"target extended-remote {self.gdb_serial}"] +
195                        self.connect_rst_disable_arg +
196                       ['-ex', "monitor swdp_scan",
197                        '-ex', "attach 1"])
198        else:
199            command = (self.gdb +
200                       ['-ex', "set confirm off",
201                        '-ex', f"target extended-remote {self.gdb_serial}"] +
202                        self.connect_rst_disable_arg +
203                       ['-ex', "monitor swdp_scan",
204                        '-ex', "attach 1",
205                        '-ex', f"file {self.elf_file}"])
206        self.check_call_ignore_sigint(command)
207
208    def bmp_debug(self, command, **kwargs):
209        if self.elf_file is None:
210            raise ValueError('Cannot debug; elf file is missing')
211        command = (self.gdb +
212                   ['-ex', "set confirm off",
213                    '-ex', f"target extended-remote {self.gdb_serial}"] +
214                    self.connect_rst_enable_arg +
215                   ['-ex', "monitor swdp_scan",
216                    '-ex', "attach 1",
217                    '-ex', f"file {self.elf_file}",
218                    '-ex', f"load {self.elf_file}"])
219        self.check_call_ignore_sigint(command)
220
221    def do_run(self, command, **kwargs):
222        if self.gdb is None:
223            raise ValueError('Cannot execute; gdb not specified')
224        self.require(self.gdb[0])
225
226        if command == 'flash':
227            self.bmp_flash(command, **kwargs)
228        elif command == 'debug':
229            self.bmp_debug(command, **kwargs)
230        elif command == 'attach':
231            self.bmp_attach(command, **kwargs)
232        else:
233            self.bmp_flash(command, **kwargs)
234