diff --git a/dist/tools/bmp/README.md b/dist/tools/bmp/README.md new file mode 100644 index 000000000000..54b7e20553df --- /dev/null +++ b/dist/tools/bmp/README.md @@ -0,0 +1,131 @@ +Black Magic Probe Helper script +================================ + +This script can detect [Black Magic Probes](https://github.com/blacksphere/blackmagic/wiki) and act as a flashloader (and more). +It is compatible with Linux and macOS. + +All important options that can be set via the monitor command are available as arguments. + +For example, SWD is the default protocol, but JTAG can be used by specifying `--jtag`. + +``` +usage: bmp.py [-h] [--jtag] [--swd] [--connect-srst] [--tpwr] + [--serial SERIAL] [--port PORT] [--attach ATTACH] + [--gdb-path GDB_PATH] [--term-cmd TERM_CMD] + [{list,flash,erase,debug,term,reset}] [file] + +Black Magic Tool helper script. + +positional arguments: + {list,flash,erase,debug,term,reset} + choose a task to perform + file file to load to target (hex or elf) + +optional arguments: + -h, --help show this help message and exit + --jtag use JTAG transport + --swd use SWD transport (default) + --connect-srst reset target while connecting + --tpwr enable target power + --serial SERIAL choose specific probe by serial number + --port PORT choose specific probe by port + --attach ATTACH choose specific target by number + --gdb-path GDB_PATH path to GDB + --term-cmd TERM_CMD serial terminal command +``` + +## Available Actions +* `list` lists connected targets (default action) +* `flash` load file to target +* `erase` erase target flash +* `debug` start GDB shell that is attached to target +* `term` start TTY emulator program to look into connected UART +* `reset` reset target (using RST line) + +## Examples (tested with BluePill STM32F103F8C6) +* test connection: +``` +user@pc:~$ ./bmp.py --connect-srst +found following Black Magic GDB servers: + [/dev/ttyACM0] Serial: BDD391D6 <- default +connecting to [/dev/ttyACM0]... +connecting successful. +scanning using SWD... +found following targets: + STM32F1 medium density M3/M4 +``` +* flashing: +``` +user@pc:~$ ./bmp.py --connect-srst flash example.elf +found following Black Magic GDB servers: + [/dev/ttyACM0] Serial: BDD391D6 <- default +connecting to [/dev/ttyACM0]... +connecting successful. +scanning using SWD... +found following targets: + STM32F1 medium density M3/M4 + +attaching to target successful. +downloading... total size: 742.5K +downloading section [.text] (12.8K) | +100%|##########################################################################| +downloading section [.relocate] (120B) | +100%|##########################################################################| +downloading finished +checking flash successful. +killing successful. +``` +* erasing: +``` +user@pc:~$ ./bmp.py --connect-srst erase +found following Black Magic GDB servers: + [/dev/ttyACM0] Serial: BDD391D6 <- default +connecting to [/dev/ttyACM0]... +connecting successful. +scanning using SWD... +found following targets: + STM32F1 medium density M3/M4 + +attaching to target successful. +erasing... +erasing target successful. +``` +* open GDB shell: +``` +user@pc:~$ ./bmp.py --connect-srst debug example.elf +found following Black Magic GDB servers: + [/dev/ttyACM0] Serial: BDD391D6 <- default +connecting to [/dev/ttyACM0]... +GNU gdb (Debian 8.2.1-2+b1) 8.2.1 +Copyright (C) 2018 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +Type "show copying" and "show warranty" for details. +This GDB was configured as "x86_64-linux-gnu". +Type "show configuration" for configuration details. +For bug reporting instructions, please see: +. +Find the GDB manual and other documentation resources online at: + . + +For help, type "help". +Type "apropos word" to search for commands related to "word"... +Reading symbols from /home/user/dev/helloriot/bin/bluepill/example.elf...done. +Remote debugging using /dev/ttyACM0 +Assert SRST during connect: enabled +Target voltage: unknown +Available Targets: +No. Att Driver + 1 STM32F1 medium density M3/M4 +Attaching to program: /home/user/dev/helloriot/bin/bluepill/example.elf, Remote target +reset_handler_default () + at /home/max/dev/RIOT/cpu/cortexm_common/vectors_cortexm.c:79 +79 { +(gdb) +``` +* open UART: +``` +user@pc:~$ ./bmp.py --connect-srst term +main(): This is RIOT! (Version: 2019.10-devel-800-g4f5d6) +``` diff --git a/dist/tools/bmp/bmp.py b/dist/tools/bmp/bmp.py new file mode 100755 index 000000000000..10d19618846c --- /dev/null +++ b/dist/tools/bmp/bmp.py @@ -0,0 +1,287 @@ +#!/usr/bin/python3 + +# Copyright (C) 2019 Otto-von-Guericke-Universität Magdeburg +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. +# +# @author Maximilian Deubel + +# Black Magic Probe helper script +# This script can detect connected Black Magic Probes and can be used as a flashloader and much more + +import argparse +import os +import re +import sys + +import humanize +import serial.tools.list_ports +from progressbar import Bar, Percentage, ProgressBar +from pygdbmi.gdbcontroller import GdbController +import distutils.spawn + +parser = argparse.ArgumentParser(description='Black Magic Tool helper script.') +parser.add_argument('--jtag', action='store_true', help='use JTAG transport') +parser.add_argument('--swd', action='store_true', help='use SWD transport (default)') +parser.add_argument('--connect-srst', action='store_true', help='reset target while connecting') +parser.add_argument('--tpwr', action='store_true', help='enable target power') +parser.add_argument('--serial', help='choose specific probe by serial number') +parser.add_argument('--port', help='choose specific probe by port') +parser.add_argument('--attach', help='choose specific target by number', default='1') +parser.add_argument('--gdb-path', help='path to GDB', default='gdb-multiarch') +parser.add_argument('--term-cmd', help='serial terminal command', + default='picocom --nolock --imap lfcrlf --baud 115200 %s') +parser.add_argument('action', help='choose a task to perform', nargs='?', + choices=['list', 'flash', 'erase', 'debug', 'term', 'reset'], + default='list') +parser.add_argument('file', help='file to load to target (hex or elf)', nargs='?') + +TIMEOUT = 100 # seconds + + +# find a suitable gdb executable, falling back to defaults if needed +def find_suitable_gdb(gdb_path): + if distutils.spawn.find_executable(gdb_path): + return gdb_path + else: + for p in ['arm-none-eabi-gdb', 'gdb-multiarch']: + p = distutils.spawn.find_executable(p) + if p: + print("GDB EXECUTABLE NOT FOUND! FALLING BACK TO %s" % p, file=sys.stderr) + return p + print("CANNOT LOCATE SUITABLE GDB EXECUTABLE!", file=sys.stderr) + sys.exit(-1) + + +# find all connected BMPs and store both GDB and UART interfaces +def detect_probes(): + gdb_ports = [] + uart_ports = [] + for p in serial.tools.list_ports.comports(): + if p.vid == 0x1D50 and p.pid in {0x6018, 0x6017}: + if re.fullmatch(r'COM\d\d', p.device): + p.device = '//./' + p.device + if 'GDB' in str(p.interface) \ + or re.fullmatch(r'/dev/cu\.usbmodem([A-F0-9]*)1', p.device) \ + or p.location[-1] == '0' and os.name == 'nt': + gdb_ports.append(p) + else: + uart_ports.append(p) + return gdb_ports, uart_ports + + +# search device with specific serial number in list +def search_serial(snr, l): + for p in l: + if snr in p.serial_number: + return p.device + + +# parse GDB output for targets +def detect_targets(gdbmi, res): + targets = [] + while True: + for msg in res: + if msg['type'] == 'target': + m = re.fullmatch(pattern=r"\s*(\d)+\s*(.*)\\n", string=msg['payload']) + if m: + targets.append(m.group(2)) + elif msg['type'] == 'result': + assert msg['message'] == 'done', str(msg) + return targets + + res = gdbmi.get_gdb_response(timeout_sec=TIMEOUT) + + +def gdb_write_and_wait_for_result(gdbmi, cmd, description, expected_result='done'): + res = gdbmi.write(cmd, timeout_sec=TIMEOUT) + while True: + for msg in res: + if msg['type'] == 'result': + if msg['message'] == expected_result: + print(description, "successful.") + return True + else: + print(description, "failed.", file=sys.stderr) + return False + res = gdbmi.get_gdb_response(timeout_sec=TIMEOUT) + + +def parse_download_msg(msg): + m = re.fullmatch( + pattern=r"\+download," + r"\{(?:section=\"(.*?)\")?,?(?:section-sent=\"(.*?)\")?,?" + r"(?:section-size=\"(.*?)\")?,?(?:total-sent=\"(.*?)\")?,?" + r"(?:total-size=\"(.*?)\")?,?\}", + string=msg['payload']) + if m: + section_name = m.group(1) + section_sent = int(m.group(2)) if m.group(2) else None + section_size = int(m.group(3)) if m.group(3) else None + total_sent = int(m.group(4)) if m.group(4) else None + total_size = int(m.group(5)) if m.group(5) else None + return section_name, section_sent, section_size, total_sent, total_size + + +def download_to_flash(gdbmi): + res = gdbmi.write('-target-download', timeout_sec=TIMEOUT) + first = True # whether this is the first status message + current_sec = None # name of current section + pbar = ProgressBar() + while True: + for msg in res: + if msg['type'] == 'result': + assert msg['message'] == 'done', "download failed: %s" % str(msg) + if pbar.start_time: + pbar.finish() + print("downloading finished") + return + elif msg['type'] == 'output': + section_name, section_sent, section_size, total_sent, total_size = parse_download_msg(msg) + if section_name: + if first: + first = False + print("downloading... total size: %s" + % humanize.naturalsize(total_size, gnu=True)) + if section_name != current_sec: + if pbar.start_time: + pbar.finish() + current_sec = section_name + print("downloading section [%s] (%s)" % ( + section_name, humanize.naturalsize(section_size, gnu=True))) + pbar = ProgressBar(widgets=[Percentage(), Bar()], maxval=section_size).start() + if section_sent: + pbar.update(section_sent) + res = gdbmi.get_gdb_response(timeout_sec=TIMEOUT) + + +def check_flash(gdbmi): + res = gdbmi.write('compare-sections', timeout_sec=TIMEOUT) + while True: + for msg in res: + if msg['type'] == 'result': + assert msg['message'] == 'done', "checking failed: %s" % str(msg) + print("checking successful") + return + elif msg['type'] == 'console': + assert 'matched' in msg['payload'] and 'MIS-MATCHED' not in msg['payload'], \ + "checking failed: %s" % str(msg) + res = gdbmi.get_gdb_response(timeout_sec=TIMEOUT) + + +def choose_bmp_port(gdb_ports): + print("found following Black Magic GDB servers:") + for i, s in enumerate(gdb_ports): + print("\t[%s]" % s.device, end=' ') + if len(s.serial_number) > 1: + print("Serial:", s.serial_number, end=' ') + if i == 0: + print("<- default", end=' ') + print('') + port = gdb_ports[0].device + if args.port: + port = args.port + elif args.serial: + port = search_serial(args.serial, gdb_ports) + assert port, "no BMP with this serial found" + print('connecting to [%s]...' % port) + return port + + +# terminal mode, opens TTY program +def term_mode(uart_ports): + port = uart_ports[0].device + if args.port: + port = args.port + elif args.serial: + port = search_serial(args.serial, uart_ports) + assert port, "no BMP with this serial found" + os.system(args.term_cmd % port) + sys.exit(0) + + +# debug mode, opens GDB shell with options +def debug_mode(port): + gdb_args = ['-ex \'target extended-remote %s\'' % port] + if args.tpwr: + gdb_args.append('-ex \'monitor tpwr enable\'') + if args.connect_srst: + gdb_args.append('-ex \'monitor connect_srst enable\'') + if args.jtag: + gdb_args.append('-ex \'monitor jtag_scan\'') + else: + gdb_args.append('-ex \'monitor swdp_scan\'') + gdb_args.append('-ex \'attach %s\'' % args.attach) + os.system(" ".join(['\"' + args.gdb_path + '\"'] + gdb_args + [args.file])) + + +def connect_to_target(port): + # open GDB in machine interface mode + gdbmi = GdbController(gdb_path=args.gdb_path, gdb_args=["--nx", "--quiet", "--interpreter=mi2", args.file]) + assert gdb_write_and_wait_for_result(gdbmi, '-target-select extended-remote %s' % port, 'connecting', + expected_result='connected') + # set options + if args.connect_srst: + gdbmi.write('monitor connect_srst enable', timeout_sec=TIMEOUT) + if args.tpwr: + gdbmi.write('monitor tpwr enable', timeout_sec=TIMEOUT) + # scan for targets + if not args.jtag: + print("scanning using SWD...") + res = gdbmi.write('monitor swdp_scan', timeout_sec=TIMEOUT) + else: + print("scanning using JTAG...") + res = gdbmi.write('monitor jtag_scan', timeout_sec=TIMEOUT) + targets = detect_targets(gdbmi, res) + assert len(targets) > 0, "no targets found" + print("found following targets:") + for t in targets: + print("\t%s" % t) + print("") + return gdbmi + + +if __name__ == '__main__': + args = parser.parse_args() + assert not (args.swd and args.jtag), "you may only choose one protocol" + assert not (args.serial and args.port), "you may only specify the probe by port or by serial" + g, u = detect_probes() + assert len(g) > 0, "no Black Magic Probes found 😔" + + if args.action == 'term': + term_mode(u) + else: + port = choose_bmp_port(g) + + args.file = args.file if args.file else '' + args.gdb_path = find_suitable_gdb(args.gdb_path) + + if args.action == 'debug': + debug_mode(port) + sys.exit(0) + + gdbmi = connect_to_target(port) + + if args.action == 'list': + sys.exit(0) + + assert gdb_write_and_wait_for_result(gdbmi, '-target-attach %s' % args.attach, 'attaching to target') + + # reset mode: reset device using reset pin + if args.action == 'reset': + assert gdb_write_and_wait_for_result(gdbmi, 'monitor hard_srst', 'resetting target') + sys.exit(0) + # erase mode + elif args.action == 'erase': + print('erasing...') + assert gdb_write_and_wait_for_result(gdbmi, '-target-flash-erase', 'erasing target') + sys.exit(0) + # flashloader mode: flash, check and restart + elif args.action == 'flash': + download_to_flash(gdbmi) + check_flash(gdbmi) + + # kill and reset + assert gdb_write_and_wait_for_result(gdbmi, 'kill', 'killing') diff --git a/dist/tools/bmp/requirements.txt b/dist/tools/bmp/requirements.txt new file mode 100644 index 000000000000..4e814345da41 --- /dev/null +++ b/dist/tools/bmp/requirements.txt @@ -0,0 +1,4 @@ +humanize +pygdbmi +pyserial +progressbar \ No newline at end of file diff --git a/makefiles/tools/bmp-serial.inc.mk b/makefiles/tools/bmp-serial.inc.mk new file mode 100644 index 000000000000..adc826b0abca --- /dev/null +++ b/makefiles/tools/bmp-serial.inc.mk @@ -0,0 +1,2 @@ +TERMPROG ?= $(RIOTTOOLS)/bmp/bmp.py +TERMFLAGS ?= $(BMP_OPTIONS) term diff --git a/makefiles/tools/bmp.inc.mk b/makefiles/tools/bmp.inc.mk new file mode 100644 index 000000000000..208793e211f3 --- /dev/null +++ b/makefiles/tools/bmp.inc.mk @@ -0,0 +1,9 @@ +FLASHER ?= $(RIOTTOOLS)/bmp/bmp.py +DEBUGGER = $(RIOTTOOLS)/bmp/bmp.py +RESET ?= $(RIOTTOOLS)/bmp/bmp.py + +FLASHFILE ?= $(ELFFILE) + +FFLAGS ?= $(BMP_OPTIONS) flash $(FLASHFILE) +DEBUGGER_FLAGS ?= $(BMP_OPTIONS) debug $(ELFFILE) +RESET_FLAGS ?= $(BMP_OPTIONS) reset