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 ZephyrBinaryRunner, RunnerCaps
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', "target extended-remote {}".format(
174                        self.gdb_serial)] +
175                    self.connect_rst_enable_arg +
176                   ['-ex', "monitor swdp_scan",
177                    '-ex', "attach 1",
178                    '-ex', "load {}".format(flash_file),
179                    '-ex', "kill",
180                    '-ex', "quit",
181                    '-silent'])
182        self.check_call(command)
183
184    def check_call_ignore_sigint(self, command):
185        previous = signal.signal(signal.SIGINT, signal.SIG_IGN)
186        try:
187            self.check_call(command)
188        finally:
189            signal.signal(signal.SIGINT, previous)
190
191    def bmp_attach(self, command, **kwargs):
192        if self.elf_file is None:
193            command = (self.gdb +
194                       ['-ex', "set confirm off",
195                        '-ex', "target extended-remote {}".format(
196                            self.gdb_serial)] +
197                        self.connect_rst_disable_arg +
198                       ['-ex', "monitor swdp_scan",
199                        '-ex', "attach 1"])
200        else:
201            command = (self.gdb +
202                       ['-ex', "set confirm off",
203                        '-ex', "target extended-remote {}".format(
204                            self.gdb_serial)] +
205                        self.connect_rst_disable_arg +
206                       ['-ex', "monitor swdp_scan",
207                        '-ex', "attach 1",
208                        '-ex', "file {}".format(self.elf_file)])
209        self.check_call_ignore_sigint(command)
210
211    def bmp_debug(self, command, **kwargs):
212        if self.elf_file is None:
213            raise ValueError('Cannot debug; elf file is missing')
214        command = (self.gdb +
215                   ['-ex', "set confirm off",
216                    '-ex', "target extended-remote {}".format(
217                        self.gdb_serial)] +
218                    self.connect_rst_enable_arg +
219                   ['-ex', "monitor swdp_scan",
220                    '-ex', "attach 1",
221                    '-ex', "file {}".format(self.elf_file),
222                    '-ex', "load {}".format(self.elf_file)])
223        self.check_call_ignore_sigint(command)
224
225    def do_run(self, command, **kwargs):
226        if self.gdb is None:
227            raise ValueError('Cannot execute; gdb not specified')
228        self.require(self.gdb[0])
229
230        if command == 'flash':
231            self.bmp_flash(command, **kwargs)
232        elif command == 'debug':
233            self.bmp_debug(command, **kwargs)
234        elif command == 'attach':
235            self.bmp_attach(command, **kwargs)
236        else:
237            self.bmp_flash(command, **kwargs)
238