#! /usr/bin/env python3 # Copyright (c) 2017 Linaro Limited. # # SPDX-License-Identifier: Apache-2.0 """Zephyr board flashing script This script is a transparent replacement for an existing Zephyr flash script. If it can invoke the flashing tools natively, it will do so; otherwise, it delegates to the shell script passed as second argument.""" import abc from os import path import os import pprint import platform import signal import sys import subprocess import time def get_env_or_bail(env_var): try: return os.environ[env_var] except KeyError: print('Variable {} not in environment:'.format( env_var), file=sys.stderr) pprint.pprint(dict(os.environ), stream=sys.stderr) raise def get_env_bool_or(env_var, default_value): try: return bool(int(os.environ[env_var])) except KeyError: return default_value def check_call(cmd, debug): if debug: print(' '.join(cmd)) subprocess.check_call(cmd) def check_output(cmd, debug): if debug: print(' '.join(cmd)) return subprocess.check_output(cmd) class ZephyrBinaryFlasher(abc.ABC): '''Abstract superclass for flasher objects.''' def __init__(self, debug=False): self.debug = debug @staticmethod def create_for_shell_script(shell_script, debug): '''Factory for using as a drop-in replacement to a shell script. Get flasher instance to use in place of shell_script, deriving flasher configuration from the environment.''' for sub_cls in ZephyrBinaryFlasher.__subclasses__(): if sub_cls.replaces_shell_script(shell_script): return sub_cls.create_from_env(debug) raise ValueError('no flasher replaces script {}'.format(shell_script)) @staticmethod @abc.abstractmethod def replaces_shell_script(shell_script): '''Check if this flasher class replaces FLASH_SCRIPT=shell_script.''' @staticmethod @abc.abstractmethod def create_from_env(debug): '''Create new flasher instance from environment variables. This class must be able to replace the current FLASH_SCRIPT. The environment variables expected by that script are used to build the flasher in a backwards-compatible manner.''' @abc.abstractmethod def flash(self, **kwargs): '''Flash the board.''' DEFAULT_ARC_TCL_PORT = 6333 DEFAULT_ARC_TELNET_PORT = 4444 DEFAULT_ARC_GDB_PORT = 3333 class ArcBinaryFlasher(ZephyrBinaryFlasher): '''Flasher front-end for the ARC architecture, using openocd.''' # This unusual flasher matches behavior in the original shell script. # # It works by starting a GDB server in a separate session, connecting a # client to it to load the program, and running 'continue' within the # client to execute the application. Ignoring SIGINT allows GDB's interrupt # functionality to work normally; since the server is in another session, # it doesn't receive the signal, either. # # Rather than exiting immediately when flashing is done, the client keeps # running, and the user must exit GDB manually to leave the flash # invocation. The flasher then cleans up the server. # # TODO: # # - make consistent with the other flashers, exiting immediately when # flashing is done and the program runs. # - make portable, rather than assuming POSIX session behavior etc. def __init__(self, elf, zephyr_base, arch, board_name, python, gdb, openocd='openocd', extra_init=None, default_path=None, tui=None, tcl_port=DEFAULT_ARC_TCL_PORT, telnet_port=DEFAULT_ARC_TELNET_PORT, gdb_port=DEFAULT_ARC_GDB_PORT, debug=False): super(ArcBinaryFlasher, self).__init__(debug=debug) self.elf = elf self.zephyr_base = zephyr_base self.arch = arch self.board_name = board_name self.python = python self.gdb = gdb self.openocd = openocd self.extra_init = extra_init self.default_path = default_path self.tui = tui self.tcl_port = tcl_port self.telnet_port = telnet_port self.gdb_port = gdb_port def replaces_shell_script(shell_script): return shell_script == 'arc_debugger.sh' def create_from_env(debug): '''Create flasher from environment. Required: - O: build output directory - KERNEL_ELF_NAME: zephyr kernel binary in ELF format - ZEPHYR_BASE: zephyr Git repository base directory - ARCH: board architecture - BOARD_NAME: zephyr name of board - PYTHON: python executable - GDB: gdb executable Optional: - OPENOCD: path to openocd, defaults to openocd - OPENOCD_EXTRA_INIT: initialization command for GDB server - OPENOCD_DEFAULT_PATH: openocd search path to use - TUI: if present, passed to gdb server used to flash - TCL_PORT: openocd TCL port, defaults to 6333 - TELNET_PORT: openocd telnet port, defaults to 4444 - GDB_PORT: openocd gdb port, defaults to 3333 ''' elf = path.join(get_env_or_bail('O'), get_env_or_bail('KERNEL_ELF_NAME')) zephyr_base = get_env_or_bail('ZEPHYR_BASE') arch = get_env_or_bail('ARCH') board_name = get_env_or_bail('BOARD_NAME') python = get_env_or_bail('PYTHON') gdb = get_env_or_bail('GDB') openocd = os.environ.get('OPENOCD', 'openocd') extra_init = os.environ.get('OPENOCD_EXTRA_INIT', None) default_path = os.environ.get('OPENOCD_DEFAULT_PATH', None) tui = os.environ.get('TUI', None) tcl_port = int(os.environ.get('TCL_PORT', str(DEFAULT_ARC_TCL_PORT))) telnet_port = int(os.environ.get('TELNET_PORT', str(DEFAULT_ARC_TELNET_PORT))) gdb_port = int(os.environ.get('GDB_PORT', str(DEFAULT_ARC_GDB_PORT))) return ArcBinaryFlasher(elf, zephyr_base, arch, board_name, python, gdb, openocd=openocd, extra_init=extra_init, default_path=default_path, tui=tui, tcl_port=tcl_port, telnet_port=telnet_port, gdb_port=gdb_port, debug=debug) def flash(self, **kwargs): if platform.system() != 'Linux': raise NotImplementedError('Linux is currently required.') search_args = [] if self.default_path is not None: search_args = ['-s', self.default_path] extra_init_args = [] if self.extra_init is not None: extra_init_args = self.extra_init.split() config = path.join(self.zephyr_base, 'boards', self.arch, self.board_name, 'support', 'openocd.cfg') helper_cmd = ([self.openocd] + search_args + ['-f', config] + extra_init_args + ['-c', 'tcl_port {}'.format(self.tcl_port), '-c', 'telnet_port {}'.format(self.telnet_port), '-c', 'gdb_port {}'.format(self.gdb_port), '-c', 'init', '-c', 'targets', '-c', 'halt']) helper = subprocess.Popen([self.python, 'arc-helper.py'] + helper_cmd) tui_arg = [] if self.tui is not None: tui_arg = [self.tui] gdb_cmd = ([self.gdb] + tui_arg + ['-ex', 'target remote :{}'.format(self.gdb_port), '-ex', 'load', '-ex', 'c', self.elf]) previous = signal.signal(signal.SIGINT, signal.SIG_IGN) try: check_call(gdb_cmd, self.debug) finally: signal.signal(signal.SIGINT, previous) helper.terminate() helper.wait() class BossacBinaryFlasher(ZephyrBinaryFlasher): '''Flasher front-end for bossac.''' def __init__(self, bin_name, bossac='bossac', debug=False): super(BossacBinaryFlasher, self).__init__(debug=debug) self.bin_name = bin_name self.bossac = bossac def replaces_shell_script(shell_script): return shell_script == 'bossa-flash.sh' def create_from_env(debug): '''Create flasher from environment. Required: - O: build output directory - KERNEL_BIN_NAME: name of kernel binary Optional: - BOSSAC: path to bossac, default is bossac ''' bin_name = path.join(get_env_or_bail('O'), get_env_or_bail('KERNEL_BIN_NAME')) bossac = os.environ.get('BOSSAC', 'bossac') return BossacBinaryFlasher(bin_name, bossac=bossac, debug=debug) def flash(self, **kwargs): if platform.system() != 'Linux': msg = 'CAUTION: No flash tool for your host system found!' raise NotImplementedError(msg) cmd_stty = ['stty', '-F', '/dev/ttyACM0', 'raw', 'ispeed', '1200', 'ospeed', '1200', 'cs8', '-cstopb', 'ignpar', 'eol', '255', 'eof', '255'] cmd_flash = [self.bossac, '-R', '-e', '-w', '-v', '-b', self.bin_name] check_call(cmd_stty, self.debug) check_call(cmd_flash, self.debug) class DfuUtilBinaryFlasher(ZephyrBinaryFlasher): '''Flasher front-end for dfu-util.''' def __init__(self, pid, alt, img, dfuse=None, exe='dfu-util', debug=False): super(DfuUtilBinaryFlasher, self).__init__(debug=debug) self.pid = pid self.alt = alt self.img = img self.dfuse = dfuse self.exe = exe try: self.list_pattern = ', alt={}'.format(int(self.alt)) except ValueError: self.list_pattern = ', name={}'.format(self.alt) def replaces_shell_script(shell_script): return shell_script == 'dfuutil.sh' def create_from_env(debug): '''Create flasher from environment. Required: - DFUUTIL_PID: USB VID:PID of the board - DFUUTIL_ALT: interface alternate setting number or name - DFUUTIL_IMG: binary to flash Optional: - DFUUTIL_DFUSE_ADDR: target address if the board is a DfuSe device. Ignored if not present. - DFUUTIL: dfu-util executable, defaults to dfu-util. ''' pid = get_env_or_bail('DFUUTIL_PID') alt = get_env_or_bail('DFUUTIL_ALT') img = get_env_or_bail('DFUUTIL_IMG') dfuse = os.environ.get('DFUUTIL_DFUSE_ADDR', None) exe = os.environ.get('DFUUTIL', 'dfu-util') return DfuUtilBinaryFlasher(pid, alt, img, dfuse=dfuse, exe=exe, debug=debug) def find_device(self): output = check_output([self.exe, '-l'], self.debug) output = output.decode(sys.getdefaultencoding()) return self.list_pattern in output def flash(self, **kwargs): reset = 0 if not self.find_device(): reset = 1 print('Please reset your board to switch to DFU mode...') while not self.find_device(): time.sleep(0.1) cmd = [self.exe, '-d,{}'.format(self.pid)] if self.dfuse is not None: cmd.extend(['-s', '{}:leave'.format(self.dfuse)]) cmd.extend(['-a', self.alt, '-D', self.img]) check_call(cmd, self.debug) if reset: print('Now reset your board again to switch back to runtime mode.') class Esp32BinaryFlasher(ZephyrBinaryFlasher): '''Flasher front-end for espidf.''' def __init__(self, elf, device, baud=921600, flash_size='detect', flash_freq='40m', flash_mode='dio', espdif='espidf', debug=False): super(Esp32BinaryFlasher, self).__init__(debug=debug) self.elf = elf self.device = device self.baud = baud self.flash_size = flash_size self.flash_freq = flash_freq self.flash_mode = flash_mode self.espdif = espdif def replaces_shell_script(shell_script): return shell_script == 'esp32.sh' def create_from_env(debug): '''Create flasher from environment. Required: - O: build output directory - KERNEL_ELF_NAME: name of kernel binary in ELF format Optional: - ESP_DEVICE: serial port to flash, default /dev/ttyUSB0 - ESP_BAUD_RATE: serial baud rate, default 921600 - ESP_FLASH_SIZE: flash size, default 'detect' - ESP_FLASH_FREQ: flash frequency, default '40m' - ESP_FLASH_MODE: flash mode, default 'dio' - ESP_TOOL: complete path to espdif, or set to 'espidf' to look for it in $ESP_IDF_PATH/components/esptool_py/esptool/esptool.py ''' elf = path.join(get_env_or_bail('O'), get_env_or_bail('KERNEL_ELF_NAME')) # TODO add sane device defaults on other platforms than Linux. device = os.environ.get('ESP_DEVICE', '/dev/ttyUSB0') baud = os.environ.get('ESP_BAUD_RATE', '921600') flash_size = os.environ.get('ESP_FLASH_SIZE', 'detect') flash_freq = os.environ.get('ESP_FLASH_FREQ', '40m') flash_mode = os.environ.get('ESP_FLASH_MODE', 'dio') espdif = os.environ.get('ESP_TOOL', 'espidf') if espdif == 'espdif': idf_path = get_env_or_bail('ESP_IDF_PATH') espdif = path.join(idf_path, 'components', 'esptool_py', 'esptool', 'esptool.py') return Esp32BinaryFlasher(elf, device, baud=baud, flash_size=flash_size, flash_freq=flash_freq, flash_mode=flash_mode, espdif=espdif, debug=debug) def flash(self, **kwargs): bin_name = path.splitext(self.elf)[0] + path.extsep + 'bin' cmd_convert = [self.espdif, '--chip', 'esp32', 'elf2image', self.elf] cmd_flash = [self.espdif, '--chip', 'esp32', '--port', self.device, '--baud', self.baud, '--before', 'default_reset', '--after', 'hard_reset', 'write_flash', '-u', '--flash_mode', self.flash_mode, '--flash_freq', self.flash_freq, '--flash_size', self.flash_size, '0x1000', bin_name] print("Converting ELF to BIN") check_call(cmd_convert, self.debug) print("Flashing ESP32 on {} ({}bps)".format(self.device, self.baud)) check_call(cmd_flash, self.debug) class Nios2Flasher(ZephyrBinaryFlasher): '''Flasher front-end for NIOS II.''' # From the original shell script: # # "XXX [flash] only support[s] cases where the .elf is sent # over the JTAG and the CPU directly boots from __start. CONFIG_XIP # and CONFIG_INCLUDE_RESET_VECTOR must be disabled." def __init__(self, hex_, cpu_sof, zephyr_base, debug=False): super(Nios2Flasher, self).__init__(debug=debug) self.hex_ = hex_ self.cpu_sof = cpu_sof self.zephyr_base = zephyr_base def replaces_shell_script(shell_script): return shell_script == 'nios2.sh' def create_from_env(debug): '''Create flasher from environment. Required: - O: build output directory - KERNEL_HEX_NAME: name of kernel binary in ELF format - NIOS2_CPU_SOF: location of the CPU .sof data - ZEPHYR_BASE: zephyr Git repository base directory ''' hex_ = path.join(get_env_or_bail('O'), get_env_or_bail('KERNEL_HEX_NAME')) cpu_sof = get_env_or_bail('NIOS2_CPU_SOF') zephyr_base = get_env_or_bail('ZEPHYR_BASE') return Nios2Flasher(hex_, cpu_sof, zephyr_base, debug=debug) def flash(self, **kwargs): cmd = [path.join(self.zephyr_base, 'scripts', 'support', 'quartus-flash.py'), '--sof', self.cpu_sof, '--kernel', self.hex_] check_call(cmd, self.debug) class NrfJprogFlasher(ZephyrBinaryFlasher): '''Flasher front-end for nrfjprog.''' def __init__(self, hex_, family, board, debug=False): super(NrfJprogFlasher, self).__init__(debug=debug) self.hex_ = hex_ self.family = family self.board = board def replaces_shell_script(shell_script): return shell_script == 'nrf_flash.sh' def create_from_env(debug): '''Create flasher from environment. Required: - O: build output directory - KERNEL_HEX_NAME: name of kernel binary in ELF format - NRF_FAMILY: e.g. NRF51 or NRF52 - BOARD: Zephyr board name ''' hex_ = path.join(get_env_or_bail('O'), get_env_or_bail('KERNEL_HEX_NAME')) family = get_env_or_bail('NRF_FAMILY') board = get_env_or_bail('BOARD') return NrfJprogFlasher(hex_, family, board, debug=debug) def get_board_snr_from_user(self): snrs = check_output(['nrfjprog', '--ids'], self.debug) snrs = snrs.decode(sys.getdefaultencoding()).strip().splitlines() if len(snrs) == 1: return snrs[0] print('There are multiple boards connected.') for i, snr in enumerate(snrs, 1): print('{}. {}'.format(i, snr)) p = 'Please select one with desired serial number (1-{}): '.format( len(snrs)) while True: value = input(p) try: value = int(value) except ValueError: continue if 1 <= value <= len(snrs): break return snrs[value - 1] def flash(self, **kwargs): board_snr = self.get_board_snr_from_user() print('Flashing file: {}'.format(self.hex_)) commands = [ ['nrfjprog', '--eraseall', '-f', self.family, '--snr', board_snr], ['nrfjprog', '--program', self.hex_, '-f', self.family, '--snr', board_snr], ] if self.family == 'NRF52': commands.extend([ # Set reset pin ['nrfjprog', '--memwr', '0x10001200', '--val', '0x00000015', '-f', self.family, '--snr', board_snr], ['nrfjprog', '--memwr', '0x10001204', '--val', '0x00000015', '-f', self.family, '--snr', board_snr], ['nrfjprog', '--reset', '-f', self.family, '--snr', board_snr], ]) for cmd in commands: check_call(cmd, self.debug) print('{} Serial Number {} flashed with success.'.format( self.board, board_snr)) class OpenOcdBinaryFlasher(ZephyrBinaryFlasher): '''Flasher front-end for openocd.''' def __init__(self, bin_name, zephyr_base, arch, board_name, load_cmd, verify_cmd, openocd='openocd', default_path=None, pre_cmd=None, post_cmd=None, debug=False): super(OpenOcdBinaryFlasher, self).__init__(debug=debug) self.bin_name = bin_name self.zephyr_base = zephyr_base self.arch = arch self.board_name = board_name self.load_cmd = load_cmd self.verify_cmd = verify_cmd self.openocd = openocd self.default_path = default_path self.pre_cmd = pre_cmd self.post_cmd = post_cmd def replaces_shell_script(shell_script): return shell_script == 'openocd.sh' def create_from_env(debug): '''Create flasher from environment. Required: - O: build output directory - KERNEL_BIN_NAME: zephyr kernel binary - ZEPHYR_BASE: zephyr Git repository base directory - ARCH: board architecture - BOARD_NAME: zephyr name of board - OPENOCD_LOAD_CMD: command to load binary into flash - OPENOCD_VERIFY_CMD: command to verify flash executed correctly Optional: - OPENOCD: path to openocd, defaults to openocd - OPENOCD_DEFAULT_PATH: openocd search path to use - OPENOCD_PRE_CMD: command to run before any others - OPENOCD_POST_CMD: command to run after verifying flash write ''' bin_name = path.join(get_env_or_bail('O'), get_env_or_bail('KERNEL_BIN_NAME')) zephyr_base = get_env_or_bail('ZEPHYR_BASE') arch = get_env_or_bail('ARCH') board_name = get_env_or_bail('BOARD_NAME') load_cmd = get_env_or_bail('OPENOCD_LOAD_CMD').strip('"') verify_cmd = get_env_or_bail('OPENOCD_VERIFY_CMD').strip('"') openocd = os.environ.get('OPENOCD', 'openocd') default_path = os.environ.get('OPENOCD_DEFAULT_PATH', None) pre_cmd = os.environ.get('OPENOCD_PRE_CMD', None) post_cmd = os.environ.get('OPENOCD_POST_CMD', None) return OpenOcdBinaryFlasher(bin_name, zephyr_base, arch, board_name, load_cmd, verify_cmd, openocd=openocd, default_path=default_path, pre_cmd=pre_cmd, post_cmd=post_cmd, debug=debug) def flash(self, **kwargs): search_args = [] if self.default_path is not None: search_args = ['-s', self.default_path] config = path.join(self.zephyr_base, 'boards', self.arch, self.board_name, 'support', 'openocd.cfg') pre_cmd = [] if self.pre_cmd is not None: pre_cmd = ['-c', self.pre_cmd] post_cmd = [] if self.post_cmd is not None: post_cmd = ['-c', self.post_cmd] cmd = ([self.openocd] + search_args + ['-f', config, '-c', 'init', '-c', 'targets'] + pre_cmd + ['-c', 'reset halt', '-c', self.load_cmd, '-c', 'reset halt', '-c', self.verify_cmd] + post_cmd + ['-c', 'reset run', '-c', 'shutdown']) check_call(cmd, self.debug) class PyOcdBinaryFlasher(ZephyrBinaryFlasher): '''Flasher front-end for pyocd-flashtool.''' def __init__(self, bin_name, target, flashtool='pyocd-flashtool', board_id=None, daparg=None, debug=False): super(PyOcdBinaryFlasher, self).__init__(debug=debug) self.bin_name = bin_name self.target = target self.flashtool = flashtool self.board_id = board_id self.daparg = daparg def replaces_shell_script(shell_script): return shell_script == 'pyocd.sh' def create_from_env(debug): '''Create flasher from environment. Required: - O: build output directory - KERNEL_BIN_NAME: name of kernel binary - PYOCD_TARGET: target override Optional: - PYOCD_FLASHTOOL: flash tool path, defaults to pyocd-flashtool - PYOCD_BOARD_ID: ID of board to flash, default is to guess - PYOCD_DAPARG_ARG: arguments to pass to flashtool, default is none ''' bin_name = path.join(get_env_or_bail('O'), get_env_or_bail('KERNEL_BIN_NAME')) target = get_env_or_bail('PYOCD_TARGET') flashtool = os.environ.get('PYOCD_FLASHTOOL', 'pyocd-flashtool') board_id = os.environ.get('PYOCD_BOARD_ID', None) daparg = os.environ.get('PYOCD_DAPARG_ARG', None) return PyOcdBinaryFlasher(bin_name, target, flashtool=flashtool, board_id=board_id, daparg=daparg, debug=debug) def flash(self, **kwargs): daparg_args = [] if self.daparg is not None: daparg_args = ['-da', self.daparg] board_args = [] if self.board_id is not None: board_args = ['-b', self.board_id] cmd = ([self.flashtool] + daparg_args + ['-t', self.target] + board_args + [self.bin_name]) print('Flashing Target Device') check_call(cmd, self.debug) # TODO: Stop using environment variables. # # Migrate the build system so we can use an argparse.ArgumentParser and # per-flasher subparsers, so invoking the script becomes something like: # # python zephyr_flash_debug.py openocd --openocd-bin=/openocd/path ... # # For now, maintain compatibility. def flash(shell_script_full, debug): shell_script = path.basename(shell_script_full) try: flasher = ZephyrBinaryFlasher.create_for_shell_script(shell_script, debug) except ValueError: # Can't create a flasher; fall back on shell script. check_call([shell_script_full, 'flash'], debug) return flasher.flash() if __name__ == '__main__': debug = True try: debug = get_env_bool_or('KBUILD_VERBOSE', False) if len(sys.argv) != 3 or sys.argv[1] != 'flash': raise ValueError('usage: {} flash path-to-script'.format( sys.argv[0])) flash(sys.argv[2], debug) except Exception as e: if debug: raise else: print('Error: {}'.format(e), file=sys.stderr) print('Re-run with KBUILD_VERBOSE=1 for a stack trace.', file=sys.stderr) sys.exit(1)