diff --git a/doc/conf.py b/doc/conf.py index 62a955c57be..48561a68da9 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -28,6 +28,10 @@ ZEPHYR_BUILD = os.path.abspath(os.environ["ZEPHYR_BUILD"]) # extensions within. sys.path.insert(0, os.path.join(ZEPHYR_BASE, 'doc', 'extensions')) +# Add the directory which contains the runners package as well, +# for autodoc directives on runners.xyz. +sys.path.insert(0, os.path.join(ZEPHYR_BASE, 'scripts', 'west_commands')) + west_found = False try: diff --git a/doc/tools/west/flash-debug.rst b/doc/tools/west/flash-debug.rst index 501a3475570..06f626d39ff 100644 --- a/doc/tools/west/flash-debug.rst +++ b/doc/tools/west/flash-debug.rst @@ -11,6 +11,14 @@ These use information stored in the CMake cache [#cmakecache]_ to flash or attach a debugger to a board supported by Zephyr. The CMake build system commands with the same names directly delegate to West. +.. Add a per-page contents at the top of the page. This page is nested + deeply enough that it doesn't have any subheadings in the main nav. + +.. only:: html + + .. contents:: + :local: + .. _west-flashing: Flashing: ``west flash`` @@ -191,37 +199,57 @@ For example, to print usage information about the ``jlink`` runner:: .. _west-runner: -Library Backend: ``west.runners`` -********************************* +Implementation Details +********************** -In keeping with West's :ref:`west-design-constraints`, the flash and -debug commands are wrappers around a separate library that is part of -West, and can be used by other code. +The flash and debug commands are implemented as west *extension +commands*: that is, they are west commands whose source code lives +outside the west repository. Some reasons this choice was made are: -This library is the ``west.runners`` subpackage of West itself. The -central abstraction within this library is ``ZephyrBinaryRunner``, an -abstract class which represents *runner* objects, which can flash +- Their implementations are tightly coupled to the Zephyr build + system, e.g. due to their reliance on CMake cache variables. + +- Pull requests adding features to them are almost always motivated by + a corresponding change to an upstream board, so it makes sense to + put them in Zephyr to avoid needing pull requests in multiple + repositories. + +- Many users find it natural to search for their implementations in + the Zephyr source tree. + +The extension commands are a thin wrapper around a package called +``runners`` (this package is also in the Zephyr tree, in +:file:`scripts/west_commands/runners`). + +The central abstraction within this library is ``ZephyrBinaryRunner``, +an abstract class which represents *runner* objects, which can flash and/or debug Zephyr programs. The set of available runners is determined by the imported subclasses of ``ZephyrBinaryRunner``. -``ZephyrBinaryRunner`` is available in the ``west.runners.core`` -module; individual runner implementations are in other submodules, -such as ``west.runners.nrfjprog``, ``west.runners.openocd``, etc. +``ZephyrBinaryRunner`` is available in the ``runners.core`` module; +individual runner implementations are in other submodules, such as +``runners.nrfjprog``, ``runners.openocd``, etc. + +Hacking and APIs +**************** Developers can add support for new ways to flash and debug Zephyr programs by implementing additional runners. To get this support into upstream Zephyr, the runner should be added into a new or existing -``west.runners`` module, and imported from -:file:`west/runner/__init__.py`. +``runners`` module, and imported from :file:`runner/__init__.py`. -.. important:: +.. note:: - Submit any changes to West to its own separate Git repository - (https://github.com/zephyrproject-rtos/west), not to the copy of - West currently present in the Zephyr tree. This copy is a temporary - measure; when West learns how to manage multiple repositories, the - copy will be removed. + The test cases in :file:`scripts/west_commands/tests` add unit test + coverage for the runners package and individual runner classes. -API documentation for the core module can be found in :ref:`west-apis`. + Please try to add tests when adding new runners. Note that if your + changes break existing test cases, CI testing on upstream pull + requests will fail. + +API Documentation for the ``runners.core`` module follows. + +.. automodule:: runners.core + :members: Doing it By Hand **************** @@ -236,8 +264,8 @@ e.g. as a source of symbol tables. By default, these West commands rebuild binaries before flashing and debugging. This can of course also be accomplished using the usual -targets provided by Zephyr's build system (in fact, that's how West -does it). +targets provided by Zephyr's build system (in fact, that's how these +commands do it). .. rubric:: Footnotes @@ -249,3 +277,6 @@ does it). .. _cmake(1): https://cmake.org/cmake/help/latest/manual/cmake.1.html + +.. _namespace package: + https://www.python.org/dev/peps/pep-0420/ diff --git a/doc/tools/west/west-apis.rst b/doc/tools/west/west-apis.rst index 7eebad3d577..e4cd6c9d9f1 100644 --- a/doc/tools/west/west-apis.rst +++ b/doc/tools/west/west-apis.rst @@ -5,6 +5,4 @@ West APIs ######### -.. automodule:: west.runners.core - :members: diff --git a/doc/tools/west/west-not-found.rst b/doc/tools/west/west-not-found.rst deleted file mode 100644 index e9ab2fbf76d..00000000000 --- a/doc/tools/west/west-not-found.rst +++ /dev/null @@ -1,10 +0,0 @@ -:orphan: - -.. _west-apis: - -West APIs -######### - -The west APIs are not documented since west was missing during the -documentation build. - diff --git a/scripts/west-commands.yml b/scripts/west-commands.yml new file mode 100644 index 00000000000..90e655c321c --- /dev/null +++ b/scripts/west-commands.yml @@ -0,0 +1,17 @@ +west-commands: + - file: scripts/west_commands/build.py + commands: + - name: build + class: Build + - file: scripts/west_commands/flash.py + commands: + - name: flash + class: Flash + - file: scripts/west_commands/debug.py + commands: + - name: debug + class: Debug + - name: debugserver + class: DebugServer + - name: attach + class: Attach diff --git a/scripts/west_commands/README.txt b/scripts/west_commands/README.txt new file mode 100644 index 00000000000..0c2a5800ace --- /dev/null +++ b/scripts/west_commands/README.txt @@ -0,0 +1,11 @@ +This directory contains implementations for west commands which are +tightly coupled to the zephyr tree. Currently, those are the build, +flash, and debug commands. + +Before adding more here, consider whether you might want to put new +extensions in upstream west. For example, any commands which operate +on the multi-repo need to be in upstream west, not here. Try to limit +what goes in here to just those files that change along with Zephyr +itself. + +Thanks! diff --git a/scripts/west_commands/build.py b/scripts/west_commands/build.py new file mode 100644 index 00000000000..981bd1f33bc --- /dev/null +++ b/scripts/west_commands/build.py @@ -0,0 +1,290 @@ +# Copyright (c) 2018 Foundries.io +# +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import os + +from west import log +from west import cmake +from west.build import DEFAULT_BUILD_DIR, DEFAULT_CMAKE_GENERATOR, \ + is_zephyr_build +from west.commands import WestCommand + +BUILD_DESCRIPTION = '''\ +Convenience wrapper for building Zephyr applications. + +This command attempts to do what you mean when run from a Zephyr +application source or a pre-existing build directory: + +- When "west build" is run from a Zephyr build directory, the source + directory is obtained from the CMake cache, and that build directory + is re-compiled. + +- Otherwise, the source directory defaults to the current working + directory, so running "west build" from a Zephyr application's + source directory compiles it. + +The source and build directories can be explicitly set with the +--source-dir and --build-dir options. The build directory defaults to +'build' if it is not auto-detected. The build directory is always +created if it does not exist. + +This command runs CMake to generate a build system if one is not +present in the build directory, then builds the application. +Subsequent builds try to avoid re-running CMake; you can force it +to run by setting --cmake. + +To pass additional options to CMake, give them as extra arguments +after a '--' For example, "west build -- -DOVERLAY_CONFIG=some.conf" sets +an overlay config file. (Doing this forces a CMake run.)''' + + +class Build(WestCommand): + + def __init__(self): + super(Build, self).__init__( + 'build', + 'compile a Zephyr application', + BUILD_DESCRIPTION, + accepts_unknown_args=False) + + self.source_dir = None + '''Source directory for the build, or None on error.''' + + self.build_dir = None + '''Final build directory used to run the build, or None on error.''' + + self.created_build_dir = False + '''True if the build directory was created; False otherwise.''' + + self.run_cmake = False + '''True if CMake was run; False otherwise. + + Note: this only describes CMake runs done by this command. The + build system generated by CMake may also update itself due to + internal logic.''' + + self.cmake_cache = None + '''Final parsed CMake cache for the build, or None on error.''' + + def do_add_parser(self, parser_adder): + parser = parser_adder.add_parser( + self.name, + help=self.help, + formatter_class=argparse.RawDescriptionHelpFormatter, + description=self.description) + + # Remember to update scripts/west-completion.bash if you add or remove + # flags + + parser.add_argument('-b', '--board', + help='''Board to build for (must be given for the + first build, can be omitted later)''') + parser.add_argument('-s', '--source-dir', + help='''Explicitly set the source directory. + If not given and rebuilding an existing Zephyr + build directory, this is taken from the CMake + cache. Otherwise, the current directory is + assumed.''') + parser.add_argument('-d', '--build-dir', + help='''Explicitly sets the build directory. + If not given and the current directory is a Zephyr + build directory, it will be used; otherwise, "{}" + is assumed. The directory will be created if + it doesn't exist.'''.format(DEFAULT_BUILD_DIR)) + parser.add_argument('-t', '--target', + help='''Override the build system target (e.g. + 'clean', 'pristine', etc.)''') + parser.add_argument('-c', '--cmake', action='store_true', + help='Force CMake to run') + parser.add_argument('-f', '--force', action='store_true', + help='Ignore any errors and try to build anyway') + parser.add_argument('cmake_opts', nargs='*', metavar='cmake_opt', + help='Extra option to pass to CMake; implies -c') + + return parser + + def do_run(self, args, ignored): + self.args = args # Avoid having to pass them around + log.dbg('args:', args, level=log.VERBOSE_EXTREME) + self._sanity_precheck() + self._setup_build_dir() + if is_zephyr_build(self.build_dir): + self._update_cache() + if self.args.cmake or self.args.cmake_opts: + self.run_cmake = True + else: + self.run_cmake = True + self._setup_source_dir() + self._sanity_check() + + log.inf('source directory: {}'.format(self.source_dir), colorize=True) + log.inf('build directory: {}{}'. + format(self.build_dir, + (' (created)' if self.created_build_dir + else '')), + colorize=True) + if self.cmake_cache: + board = self.cmake_cache.get('CACHED_BOARD') + elif self.args.board: + board = self.args.board + else: + board = 'UNKNOWN' # shouldn't happen + log.inf('BOARD:', board, colorize=True) + + self._run_cmake(self.args.cmake_opts) + self._sanity_check() + self._update_cache() + + extra_args = ['--target', args.target] if args.target else [] + cmake.run_build(self.build_dir, extra_args=extra_args) + + def _sanity_precheck(self): + app = self.args.source_dir + if app: + if not os.path.isdir(app): + self._check_force('source directory {} does not exist'. + format(app)) + elif 'CMakeLists.txt' not in os.listdir(app): + self._check_force("{} doesn't contain a CMakeLists.txt". + format(app)) + + def _update_cache(self): + try: + self.cmake_cache = cmake.CMakeCache.from_build_dir(self.build_dir) + except FileNotFoundError: + pass + + def _setup_build_dir(self): + # Initialize build_dir and created_build_dir attributes. + log.dbg('setting up build directory', level=log.VERBOSE_EXTREME) + if self.args.build_dir: + build_dir = self.args.build_dir + else: + cwd = os.getcwd() + if is_zephyr_build(cwd): + build_dir = cwd + else: + build_dir = DEFAULT_BUILD_DIR + build_dir = os.path.abspath(build_dir) + + if os.path.exists(build_dir): + if not os.path.isdir(build_dir): + log.die('build directory {} exists and is not a directory'. + format(build_dir)) + else: + os.makedirs(build_dir, exist_ok=False) + self.created_build_dir = True + self.run_cmake = True + + self.build_dir = build_dir + + def _setup_source_dir(self): + # Initialize source_dir attribute, either from command line argument, + # implicitly from the build directory's CMake cache, or using the + # default (current working directory). + log.dbg('setting up source directory', level=log.VERBOSE_EXTREME) + if self.args.source_dir: + source_dir = self.args.source_dir + elif self.cmake_cache: + source_dir = self.cmake_cache.get('APPLICATION_SOURCE_DIR') + if not source_dir: + # Maybe Zephyr changed the key? Give the user a way + # to retry, at least. + log.die("can't determine application from build directory " + "{}, please specify an application to build". + format(self.build_dir)) + else: + source_dir = os.getcwd() + self.source_dir = os.path.abspath(source_dir) + + def _sanity_check(self): + # Sanity check the build configuration. + # Side effect: may update cmake_cache attribute. + log.dbg('sanity checking the build', level=log.VERBOSE_EXTREME) + if self.source_dir == self.build_dir: + # There's no forcing this. + log.die('source and build directory {} cannot be the same; ' + 'use --build-dir {} to specify a build directory'. + format(self.source_dir, self.build_dir)) + + srcrel = os.path.relpath(self.source_dir) + if is_zephyr_build(self.source_dir): + self._check_force('it looks like {srcrel} is a build directory: ' + 'did you mean -build-dir {srcrel} instead?'. + format(srcrel=srcrel)) + elif 'CMakeLists.txt' not in os.listdir(self.source_dir): + self._check_force('source directory "{srcrel}" does not contain ' + 'a CMakeLists.txt; is that really what you ' + 'want to build? (Use -s SOURCE_DIR to specify ' + 'the application source directory)'. + format(srcrel=srcrel)) + + if not is_zephyr_build(self.build_dir) and not self.args.board: + self._check_force('this looks like a new or clean build, ' + 'please provide --board') + + if not self.cmake_cache: + return # That's all we can check without a cache. + + cached_app = self.cmake_cache.get('APPLICATION_SOURCE_DIR') + log.dbg('APPLICATION_SOURCE_DIR:', cached_app, + level=log.VERBOSE_EXTREME) + source_abs = (os.path.abspath(self.args.source_dir) + if self.args.source_dir else None) + cached_abs = os.path.abspath(cached_app) if cached_app else None + if cached_abs and source_abs and source_abs != cached_abs: + self._check_force('build directory "{}" is for application "{}", ' + 'but source directory "{}" was specified; ' + 'please clean it or use --build-dir to set ' + 'another build directory'. + format(self.build_dir, cached_abs, + source_abs)) + self.run_cmake = True # If they insist, we need to re-run cmake. + + cached_board = self.cmake_cache.get('CACHED_BOARD') + log.dbg('CACHED_BOARD:', cached_board, level=log.VERBOSE_EXTREME) + if not cached_board and not self.args.board: + if self.created_build_dir: + self._check_force( + 'Building for the first time: you must provide --board') + else: + self._check_force( + 'Board is missing or unknown, please provide --board') + if self.args.board and cached_board and \ + self.args.board != cached_board: + self._check_force('Build directory {} targets board {}, ' + 'but board {} was specified. (Clean that ' + 'directory or use --build-dir to specify ' + 'a different one.)'. + format(self.build_dir, cached_board, + self.args.board)) + + def _check_force(self, msg): + if not self.args.force: + log.err(msg) + log.die('refusing to proceed without --force due to above error') + + def _run_cmake(self, cmake_opts): + if not self.run_cmake: + log.dbg('not running cmake; build system is present') + return + + # It's unfortunate to have to use the undocumented -B and -H + # options to set the source and binary directories. + # + # However, it's the only known way to set that directory and + # run CMake from the current working directory. This is + # important because users expect invocations like this to Just + # Work: + # + # west build -- -DOVERLAY_CONFIG=relative-path.conf + final_cmake_args = ['-B{}'.format(self.build_dir), + '-H{}'.format(self.source_dir), + '-G{}'.format(DEFAULT_CMAKE_GENERATOR)] + if self.args.board: + final_cmake_args.append('-DBOARD={}'.format(self.args.board)) + if cmake_opts: + final_cmake_args.extend(cmake_opts) + cmake.run_cmake(final_cmake_args) diff --git a/scripts/west_commands/debug.py b/scripts/west_commands/debug.py new file mode 100644 index 00000000000..53615316856 --- /dev/null +++ b/scripts/west_commands/debug.py @@ -0,0 +1,75 @@ +# Copyright (c) 2018 Open Source Foundries Limited. +# Copyright 2019 Foundries.io +# +# SPDX-License-Identifier: Apache-2.0 + +'''west "debug" and "debugserver" commands.''' + +from textwrap import dedent + +from west.commands import WestCommand + +from run_common import desc_common, add_parser_common, do_run_common + + +class Debug(WestCommand): + + def __init__(self): + super(Debug, self).__init__( + 'debug', + 'flash and interactively debug a Zephyr application', + dedent(''' + Connect to the board, program the flash, and start a + debugging session.\n\n''') + + desc_common('debug'), + accepts_unknown_args=True) + + def do_add_parser(self, parser_adder): + return add_parser_common(parser_adder, self) + + def do_run(self, my_args, runner_args): + do_run_common(self, my_args, runner_args, + 'ZEPHYR_BOARD_DEBUG_RUNNER') + + +class DebugServer(WestCommand): + + def __init__(self): + super(DebugServer, self).__init__( + 'debugserver', + 'connect to board and launch a debug server', + dedent(''' + Connect to the board and launch a debug server which accepts + incoming connections for debugging the connected board. + + The debug server binds to a known port, and allows client software + started elsewhere to connect to it and debug the running + Zephyr image.\n\n''') + + desc_common('debugserver'), + accepts_unknown_args=True) + + def do_add_parser(self, parser_adder): + return add_parser_common(parser_adder, self) + + def do_run(self, my_args, runner_args): + do_run_common(self, my_args, runner_args, + 'ZEPHYR_BOARD_DEBUG_RUNNER') + +class Attach(WestCommand): + + def __init__(self): + super(Attach, self).__init__( + 'attach', + 'interactively debug a board', + dedent(''' + Like 'debug', this connects to the board and starts a debugging + session, but it doesn't reflash the program on the board.\n\n''') + + desc_common('attach'), + accepts_unknown_args=True) + + def do_add_parser(self, parser_adder): + return add_parser_common(parser_adder, self) + + def do_run(self, my_args, runner_args): + do_run_common(self, my_args, runner_args, + 'ZEPHYR_BOARD_DEBUG_RUNNER') diff --git a/scripts/west_commands/flash.py b/scripts/west_commands/flash.py new file mode 100644 index 00000000000..dd03675d27d --- /dev/null +++ b/scripts/west_commands/flash.py @@ -0,0 +1,31 @@ +# Copyright (c) 2018 Open Source Foundries Limited. +# Copyright 2019 Foundries.io +# +# SPDX-License-Identifier: Apache-2.0 + +'''west "flash" command''' + +from textwrap import dedent + +from west.commands import WestCommand + +from run_common import desc_common, add_parser_common, do_run_common + + +class Flash(WestCommand): + + def __init__(self): + super(Flash, self).__init__( + 'flash', + 'flash and run a binary on a board', + dedent(''' + Connects to the board and reprograms it with a new binary\n\n''') + + desc_common('flash'), + accepts_unknown_args=True) + + def do_add_parser(self, parser_adder): + return add_parser_common(parser_adder, self) + + def do_run(self, my_args, runner_args): + do_run_common(self, my_args, runner_args, + 'ZEPHYR_BOARD_FLASH_RUNNER') diff --git a/scripts/west_commands/run_common.py b/scripts/west_commands/run_common.py new file mode 100644 index 00000000000..5167c028ce8 --- /dev/null +++ b/scripts/west_commands/run_common.py @@ -0,0 +1,454 @@ +# Copyright (c) 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Common code used by commands which execute runners. +''' + +import argparse +from os import getcwd, path +from subprocess import CalledProcessError +import textwrap + +from west import cmake +from west import log +from west import util +from west.build import DEFAULT_BUILD_DIR, is_zephyr_build +from west.commands import CommandContextError + +from runners import get_runner_cls, ZephyrBinaryRunner +from runners.core import RunnerConfig + +# Context-sensitive help indentation. +# Don't change this, or output from argparse won't match up. +INDENT = ' ' * 2 + + +def add_parser_common(parser_adder, command): + parser = parser_adder.add_parser( + command.name, + formatter_class=argparse.RawDescriptionHelpFormatter, + help=command.help, + description=command.description) + + # Remember to update scripts/west-completion.bash if you add or remove + # flags + + parser.add_argument('-H', '--context', action='store_true', + help='''Rebuild application and print context-sensitive + help; this may be combined with --runner to restrict + output to a given runner.''') + + group = parser.add_argument_group(title='General Options') + + group.add_argument('-d', '--build-dir', + help='''Build directory to obtain runner information + from. If not given, this command tries to use build/ + and then the current working directory, in that + order.''') + group.add_argument('-c', '--cmake-cache', + help='''Path to CMake cache file containing runner + configuration (this is generated by the Zephyr + build system when compiling binaries); + default: {}. + + If this is a relative path, it is assumed relative to + the build directory. An absolute path can also be + given instead.'''.format(cmake.DEFAULT_CACHE)) + group.add_argument('-r', '--runner', + help='''If given, overrides any cached {} + runner.'''.format(command.name)) + group.add_argument('--skip-rebuild', action='store_true', + help='''If given, do not rebuild the application + before running {} commands.'''.format(command.name)) + + group = parser.add_argument_group( + title='Configuration overrides', + description=textwrap.dedent('''\ + These values usually come from the Zephyr build system itself + as stored in the CMake cache; providing these options + overrides those settings.''')) + + # Important: + # + # 1. The destination variables of these options must match + # the RunnerConfig slots. + # 2. The default values for all of these must be None. + # + # This is how we detect if the user provided them or not when + # overriding values from the cached configuration. + + command_verb = "flash" if command == "flash" else "debug" + + group.add_argument('--board-dir', + help='Zephyr board directory') + group.add_argument('--elf-file', + help='Path to elf file to {0}'.format(command_verb)) + group.add_argument('--hex-file', + help='Path to hex file to {0}'.format(command_verb)) + group.add_argument('--bin-file', + help='Path to binary file to {0}'.format(command_verb)) + group.add_argument('--gdb', + help='Path to GDB, if applicable') + group.add_argument('--openocd', + help='Path to OpenOCD, if applicable') + group.add_argument( + '--openocd-search', + help='Path to add to OpenOCD search path, if applicable') + + return parser + + +def desc_common(command_name): + return textwrap.dedent('''\ + Any options not recognized by this command are passed to the + back-end {command} runner (run "west {command} --context" + for help on available runner-specific options). + + If you need to pass an option to a runner which has the + same name as one recognized by this command, you can + end argument parsing with a '--', like so: + + west {command} --{command}-arg=value -- --runner-arg=value2 + '''.format(**{'command': command_name})) + + +def cached_runner_config(build_dir, cache): + '''Parse the RunnerConfig from a build directory and CMake Cache.''' + board_dir = cache['ZEPHYR_RUNNER_CONFIG_BOARD_DIR'] + elf_file = cache.get('ZEPHYR_RUNNER_CONFIG_ELF_FILE', + cache['ZEPHYR_RUNNER_CONFIG_KERNEL_ELF']) + hex_file = cache.get('ZEPHYR_RUNNER_CONFIG_HEX_FILE', + cache['ZEPHYR_RUNNER_CONFIG_KERNEL_HEX']) + bin_file = cache.get('ZEPHYR_RUNNER_CONFIG_BIN_FILE', + cache['ZEPHYR_RUNNER_CONFIG_KERNEL_BIN']) + gdb = cache.get('ZEPHYR_RUNNER_CONFIG_GDB') + openocd = cache.get('ZEPHYR_RUNNER_CONFIG_OPENOCD') + openocd_search = cache.get('ZEPHYR_RUNNER_CONFIG_OPENOCD_SEARCH') + + return RunnerConfig(build_dir, board_dir, + elf_file, hex_file, bin_file, + gdb=gdb, openocd=openocd, + openocd_search=openocd_search) + + +def _override_config_from_namespace(cfg, namespace): + '''Override a RunnerConfig's contents with command-line values.''' + for var in cfg.__slots__: + if var in namespace: + val = getattr(namespace, var) + if val is not None: + setattr(cfg, var, val) + + +def _build_dir(args, die_if_none=True): + # Get the build directory for the given argument list and environment. + if args.build_dir: + return args.build_dir + + cwd = getcwd() + default = path.join(cwd, DEFAULT_BUILD_DIR) + if is_zephyr_build(default): + return default + elif is_zephyr_build(cwd): + return cwd + elif die_if_none: + log.die('--build-dir was not given, and neither {} ' + 'nor {} are zephyr build directories.'. + format(default, cwd)) + else: + return None + + +def do_run_common(command, args, runner_args, cached_runner_var): + if args.context: + _dump_context(command, args, runner_args, cached_runner_var) + return + + command_name = command.name + build_dir = _build_dir(args) + + if not args.skip_rebuild: + try: + cmake.run_build(build_dir) + except CalledProcessError: + if args.build_dir: + log.die('cannot run {}, build in {} failed'.format( + command_name, args.build_dir)) + else: + log.die('cannot run {}; no --build-dir given and build in ' + 'current directory {} failed'.format(command_name, + build_dir)) + + # Runner creation, phase 1. + # + # Get the default runner name from the cache, allowing a command + # line override. Get the ZephyrBinaryRunner class by name, and + # make sure it supports the command. + + cache_file = path.join(build_dir, args.cmake_cache or cmake.DEFAULT_CACHE) + cache = cmake.CMakeCache(cache_file) + board = cache['CACHED_BOARD'] + available = cache.get_list('ZEPHYR_RUNNERS') + if not available: + log.wrn('No cached runners are available in', cache_file) + runner = args.runner or cache.get(cached_runner_var) + + if runner is None: + raise CommandContextError(textwrap.dedent(""" + No {} runner available for {}. Please either specify one + manually, or check your board's documentation for + alternative instructions.""".format(command_name, board))) + + log.inf('Using runner:', runner) + if runner not in available: + log.wrn('Runner {} is not configured for use with {}, ' + 'this may not work'.format(runner, board)) + runner_cls = get_runner_cls(runner) + if command_name not in runner_cls.capabilities().commands: + log.die('Runner {} does not support command {}'.format( + runner, command_name)) + + # Runner creation, phase 2. + # + # At this point, the common options above are already parsed in + # 'args', and unrecognized arguments are in 'runner_args'. + # + # - Pull the RunnerConfig out of the cache + # - Override cached values with applicable command-line options + + cfg = cached_runner_config(build_dir, cache) + _override_config_from_namespace(cfg, args) + + # Runner creation, phase 3. + # + # - Pull out cached runner arguments, and append command-line + # values (which should override the cache) + # - Construct a runner-specific argument parser to handle cached + # values plus overrides given in runner_args + # - Parse arguments and create runner instance from final + # RunnerConfig and parsed arguments. + + cached_runner_args = cache.get_list( + 'ZEPHYR_RUNNER_ARGS_{}'.format(cmake.make_c_identifier(runner))) + assert isinstance(runner_args, list), runner_args + # If the user passed -- to force the parent argument parser to stop + # parsing, it will show up here, and needs to be filtered out. + runner_args = [arg for arg in runner_args if arg != '--'] + final_runner_args = cached_runner_args + runner_args + parser = argparse.ArgumentParser(prog=runner) + runner_cls.add_parser(parser) + parsed_args, unknown = parser.parse_known_args(args=final_runner_args) + if unknown: + raise CommandContextError('Runner', runner, + 'received unknown arguments', unknown) + runner = runner_cls.create(cfg, parsed_args) + runner.run(command_name) + + +# +# Context-specific help +# + +def _dump_context(command, args, runner_args, cached_runner_var): + build_dir = _build_dir(args, die_if_none=False) + + # Try to figure out the CMake cache file based on the build + # directory or an explicit argument. + if build_dir is not None: + cache_file = path.abspath( + path.join(build_dir, args.cmake_cache or cmake.DEFAULT_CACHE)) + elif args.cmake_cache: + cache_file = path.abspath(args.cmake_cache) + else: + cache_file = None + + # Load the cache itself, if possible. + if cache_file is None: + log.wrn('No build directory (--build-dir) or CMake cache ' + '(--cache-file) given or found; output will be limited') + cache = None + else: + try: + cache = cmake.CMakeCache(cache_file) + except Exception: + log.die('Cannot load cache {}.'.format(cache_file)) + + # If we have a build directory, try to ensure build artifacts are + # up to date. If that doesn't work, still try to print information + # on a best-effort basis. + if build_dir and not args.skip_rebuild: + try: + cmake.run_build(build_dir) + except CalledProcessError: + msg = 'Failed re-building application; cannot load context. ' + if args.build_dir: + msg += 'Is {} the right --build-dir?'.format(args.build_dir) + else: + msg += textwrap.dedent('''\ + Use --build-dir (-d) to specify a build directory; the one + used was {}.'''.format(build_dir)) + log.die('\n'.join(textwrap.wrap(msg, initial_indent='', + subsequent_indent=INDENT, + break_on_hyphens=False))) + + if cache is None: + _dump_no_context_info(command, args) + if not args.runner: + return + + if args.runner: + # Just information on one runner was requested. + _dump_one_runner_info(cache, args, build_dir, INDENT) + return + + board = cache['CACHED_BOARD'] + + all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() if + command.name in cls.capabilities().commands} + available = [r for r in cache.get_list('ZEPHYR_RUNNERS') if r in all_cls] + available_cls = {r: all_cls[r] for r in available if r in all_cls} + + default_runner = cache.get(cached_runner_var) + cfg = cached_runner_config(build_dir, cache) + + log.inf('All Zephyr runners which support {}:'.format(command.name), + colorize=True) + for line in util.wrap(', '.join(all_cls.keys()), INDENT): + log.inf(line) + log.inf('(Not all may work with this build, see available runners below.)', + colorize=True) + + if cache is None: + log.warn('Missing or invalid CMake cache {}; there is no context.', + 'Use --build-dir to specify the build directory.') + return + + log.inf('Build directory:', colorize=True) + log.inf(INDENT + build_dir) + log.inf('Board:', colorize=True) + log.inf(INDENT + board) + log.inf('CMake cache:', colorize=True) + log.inf(INDENT + cache_file) + + if not available: + # Bail with a message if no runners are available. + msg = ('No runners available for {}. ' + 'Consult the documentation for instructions on how to run ' + 'binaries on this target.').format(board) + for line in util.wrap(msg, ''): + log.inf(line, colorize=True) + return + + log.inf('Available {} runners:'.format(command.name), colorize=True) + log.inf(INDENT + ', '.join(available)) + log.inf('Additional options for available', command.name, 'runners:', + colorize=True) + for runner in available: + _dump_runner_opt_help(runner, all_cls[runner]) + log.inf('Default {} runner:'.format(command.name), colorize=True) + log.inf(INDENT + default_runner) + _dump_runner_config(cfg, '', INDENT) + log.inf('Runner-specific information:', colorize=True) + for runner in available: + log.inf('{}{}:'.format(INDENT, runner), colorize=True) + _dump_runner_cached_opts(cache, runner, INDENT * 2, INDENT * 3) + _dump_runner_caps(available_cls[runner], INDENT * 2) + + if len(available) > 1: + log.inf('(Add -r RUNNER to just print information about one runner.)', + colorize=True) + + +def _dump_no_context_info(command, args): + all_cls = {cls.name(): cls for cls in ZephyrBinaryRunner.get_runners() if + command.name in cls.capabilities().commands} + log.inf('All Zephyr runners which support {}:'.format(command.name), + colorize=True) + for line in util.wrap(', '.join(all_cls.keys()), INDENT): + log.inf(line) + if not args.runner: + log.inf('Add -r RUNNER to print more information about any runner.', + colorize=True) + + +def _dump_one_runner_info(cache, args, build_dir, indent): + runner = args.runner + cls = get_runner_cls(runner) + + if cache is None: + _dump_runner_opt_help(runner, cls) + _dump_runner_caps(cls, '') + return + + available = runner in cache.get_list('ZEPHYR_RUNNERS') + cfg = cached_runner_config(build_dir, cache) + + log.inf('Build directory:', colorize=True) + log.inf(INDENT + build_dir) + log.inf('Board:', colorize=True) + log.inf(INDENT + cache['CACHED_BOARD']) + log.inf('CMake cache:', colorize=True) + log.inf(INDENT + cache.cache_file) + log.inf(runner, 'is available:', 'yes' if available else 'no', + colorize=True) + _dump_runner_opt_help(runner, cls) + _dump_runner_config(cfg, '', indent) + if available: + _dump_runner_cached_opts(cache, runner, '', indent) + _dump_runner_caps(cls, '') + if not available: + log.wrn('Runner', runner, 'is not configured in this build.') + + +def _dump_runner_caps(cls, base_indent): + log.inf('{}Capabilities:'.format(base_indent), colorize=True) + log.inf('{}{}'.format(base_indent + INDENT, cls.capabilities())) + + +def _dump_runner_opt_help(runner, cls): + # Construct and print the usage text + dummy_parser = argparse.ArgumentParser(prog='', add_help=False) + cls.add_parser(dummy_parser) + formatter = dummy_parser._get_formatter() + for group in dummy_parser._action_groups: + # Break the abstraction to filter out the 'flash', 'debug', etc. + # TODO: come up with something cleaner (may require changes + # in the runner core). + actions = group._group_actions + if len(actions) == 1 and actions[0].dest == 'command': + # This is the lone positional argument. Skip it. + continue + formatter.start_section('REMOVE ME') + formatter.add_text(group.description) + formatter.add_arguments(actions) + formatter.end_section() + # Get the runner help, with the "REMOVE ME" string gone + runner_help = '\n'.join(formatter.format_help().splitlines()[1:]) + + log.inf('{} options:'.format(runner), colorize=True) + log.inf(runner_help) + + +def _dump_runner_config(cfg, initial_indent, subsequent_indent): + log.inf('{}Cached common runner configuration:'.format(initial_indent), + colorize=True) + for var in cfg.__slots__: + log.inf('{}--{}={}'.format(subsequent_indent, var, getattr(cfg, var))) + + +def _dump_runner_cached_opts(cache, runner, initial_indent, subsequent_indent): + runner_args = _get_runner_args(cache, runner) + if not runner_args: + return + + log.inf('{}Cached runner-specific options:'.format(initial_indent), + colorize=True) + for arg in runner_args: + log.inf('{}{}'.format(subsequent_indent, arg)) + + +def _get_runner_args(cache, runner): + runner_ident = cmake.make_c_identifier(runner) + args_var = 'ZEPHYR_RUNNER_ARGS_{}'.format(runner_ident) + return cache.get_list(args_var) diff --git a/scripts/west_commands/runners/__init__.py b/scripts/west_commands/runners/__init__.py new file mode 100644 index 00000000000..1d0a67019e7 --- /dev/null +++ b/scripts/west_commands/runners/__init__.py @@ -0,0 +1,35 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +from runners.core import ZephyrBinaryRunner + +# We import these here to ensure the ZephyrBinaryRunner subclasses are +# defined; otherwise, ZephyrBinaryRunner.create_for_shell_script() +# won't work. + +# Explicitly silence the unused import warning. +# flake8: noqa: F401 +from runners import arc +from runners import bossac +from runners import dfu +from runners import esp32 +from runners import jlink +from runners import nios2 +from runners import nrfjprog +from runners import nsim +from runners import openocd +from runners import pyocd +from runners import qemu +from runners import xtensa +from runners import intel_s1000 +from runners import blackmagicprobe + +def get_runner_cls(runner): + '''Get a runner's class object, given its name.''' + for cls in ZephyrBinaryRunner.get_runners(): + if cls.name() == runner: + return cls + raise ValueError('unknown runner "{}"'.format(runner)) + +__all__ = ['ZephyrBinaryRunner', 'get_runner_cls'] diff --git a/scripts/west_commands/runners/arc.py b/scripts/west_commands/runners/arc.py new file mode 100644 index 00000000000..5d5c8675878 --- /dev/null +++ b/scripts/west_commands/runners/arc.py @@ -0,0 +1,107 @@ +# Copyright (c) 2017 Linaro Limited. +# Copyright (c) 2017 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''ARC architecture-specific runners.''' + +from os import path + +from runners.core import ZephyrBinaryRunner + +DEFAULT_ARC_TCL_PORT = 6333 +DEFAULT_ARC_TELNET_PORT = 4444 +DEFAULT_ARC_GDB_PORT = 3333 + + +class EmStarterKitBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for the EM Starterkit board, using openocd.''' + + # This unusual 'flash' implementation matches 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. + # + + def __init__(self, cfg, + tui=False, tcl_port=DEFAULT_ARC_TCL_PORT, + telnet_port=DEFAULT_ARC_TELNET_PORT, + gdb_port=DEFAULT_ARC_GDB_PORT): + super(EmStarterKitBinaryRunner, self).__init__(cfg) + self.gdb_cmd = [cfg.gdb] + (['-tui'] if tui else []) + search_args = [] + if cfg.openocd_search is not None: + search_args = ['-s', cfg.openocd_search] + self.openocd_cmd = [cfg.openocd or 'openocd'] + search_args + self.tcl_port = tcl_port + self.telnet_port = telnet_port + self.gdb_port = gdb_port + + @classmethod + def name(cls): + return 'em-starterkit' + + @classmethod + def do_add_parser(cls, parser): + parser.add_argument('--tui', default=False, action='store_true', + help='if given, GDB uses -tui') + parser.add_argument('--tcl-port', default=DEFAULT_ARC_TCL_PORT, + help='openocd TCL port, defaults to 6333') + parser.add_argument('--telnet-port', default=DEFAULT_ARC_TELNET_PORT, + help='openocd telnet port, defaults to 4444') + parser.add_argument('--gdb-port', default=DEFAULT_ARC_GDB_PORT, + help='openocd gdb port, defaults to 3333') + + @classmethod + def create(cls, cfg, args): + if cfg.gdb is None: + raise ValueError('--gdb not provided at command line') + + return EmStarterKitBinaryRunner( + cfg, + tui=args.tui, tcl_port=args.tcl_port, telnet_port=args.telnet_port, + gdb_port=args.gdb_port) + + def do_run(self, command, **kwargs): + kwargs['openocd-cfg'] = path.join(self.cfg.board_dir, 'support', + 'openocd.cfg') + + if command in {'flash', 'debug'}: + self.flash_debug(command, **kwargs) + else: + self.debugserver(**kwargs) + + def flash_debug(self, command, **kwargs): + config = kwargs['openocd-cfg'] + + server_cmd = (self.openocd_cmd + + ['-f', config] + + ['-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']) + + continue_arg = [] + if command == 'flash': + continue_arg = ['-ex', 'set confirm off', '-ex', 'monitor resume', + '-ex', 'quit'] + + gdb_cmd = (self.gdb_cmd + + ['-ex', 'target remote :{}'.format(self.gdb_port), + '-ex', 'load'] + + continue_arg + + [self.cfg.elf_file]) + + self.run_server_and_client(server_cmd, gdb_cmd) + + def debugserver(self, **kwargs): + config = kwargs['openocd-cfg'] + cmd = (self.openocd_cmd + + ['-f', config, + '-c', 'init', + '-c', 'targets', + '-c', 'reset halt']) + self.check_call(cmd) diff --git a/scripts/west_commands/runners/blackmagicprobe.py b/scripts/west_commands/runners/blackmagicprobe.py new file mode 100644 index 00000000000..7b34060a550 --- /dev/null +++ b/scripts/west_commands/runners/blackmagicprobe.py @@ -0,0 +1,96 @@ +# Copyright (c) 2018 Roman Tataurov +# Modified 2018 Tavish Naruka +# +# SPDX-License-Identifier: Apache-2.0 +'''Runner for flashing with Black Magic Probe.''' +# https://github.com/blacksphere/blackmagic/wiki + +from runners.core import ZephyrBinaryRunner, RunnerCaps + + +class BlackMagicProbeRunner(ZephyrBinaryRunner): + '''Runner front-end for Black Magic probe.''' + + def __init__(self, cfg, gdb_serial): + super(BlackMagicProbeRunner, self).__init__(cfg) + self.gdb = [cfg.gdb] if cfg.gdb else None + self.elf_file = cfg.elf_file + self.gdb_serial = gdb_serial + + @classmethod + def name(cls): + return 'blackmagicprobe' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'flash', 'debug', 'attach'}) + + @classmethod + def create(cls, cfg, args): + return BlackMagicProbeRunner(cfg, args.gdb_serial) + + @classmethod + def do_add_parser(cls, parser): + parser.add_argument('--gdb-serial', default='/dev/ttyACM0', + help='GDB serial port') + + def bmp_flash(self, command, **kwargs): + if self.gdb is None: + raise ValueError('Cannot flash; gdb is missing') + if self.elf_file is None: + raise ValueError('Cannot debug; elf file is missing') + command = (self.gdb + + ['-ex', "set confirm off", + '-ex', "target extended-remote {}".format(self.gdb_serial), + '-ex', "monitor swdp_scan", + '-ex', "attach 1", + '-ex', "load {}".format(self.elf_file), + '-ex', "kill", + '-ex', "quit", + '-silent']) + self.check_call(command) + + def bmp_attach(self, command, **kwargs): + if self.gdb is None: + raise ValueError('Cannot attach; gdb is missing') + if self.elf_file is None: + command = (self.gdb + + ['-ex', "set confirm off", + '-ex', "target extended-remote {}".format( + self.gdb_serial), + '-ex', "monitor swdp_scan", + '-ex', "attach 1"]) + else: + command = (self.gdb + + ['-ex', "set confirm off", + '-ex', "target extended-remote {}".format( + self.gdb_serial), + '-ex', "monitor swdp_scan", + '-ex', "attach 1", + '-ex', "file {}".format(self.elf_file)]) + self.check_call(command) + + def bmp_debug(self, command, **kwargs): + if self.gdb is None: + raise ValueError('Cannot debug; gdb is missing') + if self.elf_file is None: + raise ValueError('Cannot debug; elf file is missing') + command = (self.gdb + + ['-ex', "set confirm off", + '-ex', "target extended-remote {}".format(self.gdb_serial), + '-ex', "monitor swdp_scan", + '-ex', "attach 1", + '-ex', "file {}".format(self.elf_file), + '-ex', "load {}".format(self.elf_file)]) + self.check_call(command) + + def do_run(self, command, **kwargs): + + if command == 'flash': + self.bmp_flash(command, **kwargs) + elif command == 'debug': + self.bmp_debug(command, **kwargs) + elif command == 'attach': + self.bmp_attach(command, **kwargs) + else: + self.bmp_flash(command, **kwargs) diff --git a/scripts/west_commands/runners/bossac.py b/scripts/west_commands/runners/bossac.py new file mode 100644 index 00000000000..02d6894abc9 --- /dev/null +++ b/scripts/west_commands/runners/bossac.py @@ -0,0 +1,54 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''bossac-specific runner (flash only) for Atmel SAM microcontrollers.''' + +import platform + +from runners.core import ZephyrBinaryRunner, RunnerCaps + +DEFAULT_BOSSAC_PORT = '/dev/ttyACM0' + + +class BossacBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for bossac.''' + + def __init__(self, cfg, bossac='bossac', port=DEFAULT_BOSSAC_PORT): + super(BossacBinaryRunner, self).__init__(cfg) + self.bossac = bossac + self.port = port + + @classmethod + def name(cls): + return 'bossac' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'flash'}) + + @classmethod + def do_add_parser(cls, parser): + parser.add_argument('--bossac', default='bossac', + help='path to bossac, default is bossac') + parser.add_argument('--bossac-port', default='/dev/ttyACM0', + help='serial port to use, default is /dev/ttyACM0') + + @classmethod + def create(cls, cfg, args): + return BossacBinaryRunner(cfg, bossac=args.bossac, + port=args.bossac_port) + + def do_run(self, command, **kwargs): + if platform.system() != 'Linux': + msg = 'CAUTION: No flash tool for your host system found!' + raise NotImplementedError(msg) + + cmd_stty = ['stty', '-F', self.port, 'raw', 'ispeed', '1200', + 'ospeed', '1200', 'cs8', '-cstopb', 'ignpar', 'eol', '255', + 'eof', '255'] + cmd_flash = [self.bossac, '-p', self.port, '-R', '-e', '-w', '-v', + '-b', self.cfg.bin_file] + + self.check_call(cmd_stty) + self.check_call(cmd_flash) diff --git a/scripts/west_commands/runners/core.py b/scripts/west_commands/runners/core.py new file mode 100644 index 00000000000..60dc6cc5979 --- /dev/null +++ b/scripts/west_commands/runners/core.py @@ -0,0 +1,508 @@ +#! /usr/bin/env python3 + +# Copyright (c) 2017 Linaro Limited. +# Copyright (c) 2017 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Zephyr binary runner core interfaces + +This provides the core ZephyrBinaryRunner class meant for public use, +as well as some other helpers for concrete runner classes. +""" + +import abc +import argparse +import os +import platform +import signal +import subprocess + +from west import log +from west.util import quote_sh_list + +# Turn on to enable just printing the commands that would be run, +# without actually running them. This can break runners that are expecting +# output or if one command depends on another, so it's just for debugging. +JUST_PRINT = False + + +class _DebugDummyPopen: + + def terminate(self): + pass + + def wait(self): + pass + + +MAX_PORT = 49151 + + +class NetworkPortHelper: + '''Helper class for dealing with local IP network ports.''' + + def get_unused_ports(self, starting_from): + '''Find unused network ports, starting at given values. + + starting_from is an iterable of ports the caller would like to use. + + The return value is an iterable of ports, in the same order, using + the given values if they were unused, or the next sequentially + available unused port otherwise. + + Ports may be bound between this call's check and actual usage, so + callers still need to handle errors involving returned ports.''' + start = list(starting_from) + used = self._used_now() + ret = [] + + for desired in start: + port = desired + while port in used: + port += 1 + if port > MAX_PORT: + msg = "ports above {} are in use" + raise ValueError(msg.format(desired)) + used.add(port) + ret.append(port) + + return ret + + def _used_now(self): + handlers = { + 'Windows': self._used_now_windows, + 'Linux': self._used_now_linux, + 'Darwin': self._used_now_darwin, + } + handler = handlers[platform.system()] + return handler() + + def _used_now_windows(self): + cmd = ['netstat', '-a', '-n', '-p', 'tcp'] + return self._parser_windows(cmd) + + def _used_now_linux(self): + cmd = ['ss', '-a', '-n', '-t'] + return self._parser_linux(cmd) + + def _used_now_darwin(self): + cmd = ['netstat', '-a', '-n', '-p', 'tcp'] + return self._parser_darwin(cmd) + + def _parser_windows(self, cmd): + out = subprocess.check_output(cmd).split(b'\r\n') + used_bytes = [x.split()[1].rsplit(b':', 1)[1] for x in out + if x.startswith(b' TCP')] + return {int(b) for b in used_bytes} + + def _parser_linux(self, cmd): + out = subprocess.check_output(cmd).splitlines()[1:] + used_bytes = [s.split()[3].rsplit(b':', 1)[1] for s in out] + return {int(b) for b in used_bytes} + + def _parser_darwin(self, cmd): + out = subprocess.check_output(cmd).split(b'\n') + used_bytes = [x.split()[3].rsplit(b':', 1)[1] for x in out + if x.startswith(b'tcp')] + return {int(b) for b in used_bytes} + + +class BuildConfiguration: + '''This helper class provides access to build-time configuration. + + Configuration options can be read as if the object were a dict, + either object['CONFIG_FOO'] or object.get('CONFIG_FOO'). + + Configuration values in .config and generated_dts_board.conf are + available.''' + + def __init__(self, build_dir): + self.build_dir = build_dir + self.options = {} + self._init() + + def __getitem__(self, item): + return self.options[item] + + def get(self, option, *args): + return self.options.get(option, *args) + + def _init(self): + build_z = os.path.join(self.build_dir, 'zephyr') + generated = os.path.join(build_z, 'include', 'generated') + files = [os.path.join(build_z, '.config'), + os.path.join(generated, 'generated_dts_board.conf')] + for f in files: + self._parse(f) + + def _parse(self, filename): + with open(filename, 'r') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + option, value = line.split('=', 1) + self.options[option] = self._parse_value(value) + + def _parse_value(self, value): + if value.startswith('"') or value.startswith("'"): + return value.split() + try: + return int(value, 0) + except ValueError: + return value + + +class RunnerCaps: + '''This class represents a runner class's capabilities. + + Each capability is represented as an attribute with the same + name. Flag attributes are True or False. + + Available capabilities: + + - commands: set of supported commands; default is {'flash', + 'debug', 'debugserver', 'attach'}. + + - flash_addr: whether the runner supports flashing to an + arbitrary address. Default is False. If true, the runner + must honor the --dt-flash option. + ''' + + def __init__(self, + commands={'flash', 'debug', 'debugserver', 'attach'}, + flash_addr=False): + self.commands = commands + self.flash_addr = bool(flash_addr) + + def __str__(self): + return 'RunnerCaps(commands={}, flash_addr={})'.format( + self.commands, self.flash_addr) + + +class RunnerConfig: + '''Runner execution-time configuration. + + This is a common object shared by all runners. Individual runners + can register specific configuration options using their + do_add_parser() hooks. + + This class's __slots__ contains exactly the configuration variables. + ''' + + __slots__ = ['build_dir', 'board_dir', 'elf_file', 'hex_file', + 'bin_file', 'gdb', 'openocd', 'openocd_search'] + + # TODO: revisit whether we can get rid of some of these. Having + # tool-specific configuration options here is a layering + # violation, but it's very convenient to have a single place to + # store the locations of tools (like gdb and openocd) that are + # needed by multiple ZephyrBinaryRunner subclasses. + def __init__(self, build_dir, board_dir, + elf_file, hex_file, bin_file, + gdb=None, openocd=None, openocd_search=None): + self.build_dir = build_dir + '''Zephyr application build directory''' + + self.board_dir = board_dir + '''Zephyr board directory''' + + self.elf_file = elf_file + '''Path to the elf file that the runner should operate on''' + + self.hex_file = hex_file + '''Path to the hex file that the runner should operate on''' + + self.bin_file = bin_file + '''Path to the bin file that the runner should operate on''' + + self.gdb = gdb + ''''Path to GDB compatible with the target, may be None.''' + + self.openocd = openocd + '''Path to OpenOCD to use for this target, may be None.''' + + self.openocd_search = openocd_search + '''directory to add to OpenOCD search path, may be None.''' + + +_YN_CHOICES = ['Y', 'y', 'N', 'n', 'yes', 'no', 'YES', 'NO'] + + +class _DTFlashAction(argparse.Action): + + def __call__(self, parser, namespace, values, option_string=None): + if values.lower().startswith('y'): + namespace.dt_flash = True + else: + namespace.dt_flash = False + + +class ZephyrBinaryRunner(abc.ABC): + '''Abstract superclass for binary runners (flashers, debuggers). + + **Note**: these APIs are still evolving, and will change! + + With some exceptions, boards supported by Zephyr must provide + generic means to be flashed (have a Zephyr firmware binary + permanently installed on the device for running) and debugged + (have a breakpoint debugger and program loader on a host + workstation attached to a running target). + + This is supported by three top-level commands managed by the + Zephyr build system: + + - 'flash': flash a previously configured binary to the board, + start execution on the target, then return. + + - 'debug': connect to the board via a debugging protocol, program + the flash, then drop the user into a debugger interface with + symbol tables loaded from the current binary, and block until it + exits. + + - 'debugserver': connect via a board-specific debugging protocol, + then reset and halt the target. Ensure the user is now able to + connect to a debug server with symbol tables loaded from the + binary. + + - 'attach': connect to the board via a debugging protocol, then drop + the user into a debugger interface with symbol tables loaded from + the current binary, and block until it exits. Unlike 'debug', this + command does not program the flash. + + This class provides an API for these commands. Every runner has a + name (like 'pyocd'), and declares commands it can handle (like + 'flash'). Zephyr boards (like 'nrf52_pca10040') declare compatible + runner(s) by name to the build system, which makes concrete runner + instances to execute commands via this class. + + If your board can use an existing runner, all you have to do is + give its name to the build system. How to do that is out of the + scope of this documentation, but use the existing boards as a + starting point. + + If you want to define and use your own runner: + + 1. Define a ZephyrBinaryRunner subclass, and implement its + abstract methods. You may need to override capabilities(). + + 2. Make sure the Python module defining your runner class is + imported, e.g. by editing this package's __init__.py (otherwise, + get_runners() won't work). + + 3. Give your runner's name to the Zephyr build system in your + board's build files. + + For command-line invocation from the Zephyr build system, runners + define their own argparse-based interface through the common + add_parser() (and runner-specific do_add_parser() it delegates + to), and provide a way to create instances of themselves from + a RunnerConfig and parsed runner-specific arguments via create(). + + Runners use a variety of target-specific tools and configuration + values, the user interface to which is abstracted by this + class. Each runner subclass should take any values it needs to + execute one of these commands in its constructor. The actual + command execution is handled in the run() method.''' + + def __init__(self, cfg): + '''Initialize core runner state. + + `cfg` is a RunnerConfig instance.''' + self.cfg = cfg + + @staticmethod + def get_runners(): + '''Get a list of all currently defined runner classes.''' + return ZephyrBinaryRunner.__subclasses__() + + @classmethod + @abc.abstractmethod + def name(cls): + '''Return this runner's user-visible name. + + When choosing a name, pick something short and lowercase, + based on the name of the tool (like openocd, jlink, etc.) or + the target architecture/board (like xtensa, em-starterkit, + etc.).''' + + @classmethod + def capabilities(cls): + '''Returns a RunnerCaps representing this runner's capabilities. + + This implementation returns the default capabilities. + + Subclasses should override appropriately if needed.''' + return RunnerCaps() + + @classmethod + def add_parser(cls, parser): + '''Adds a sub-command parser for this runner. + + The given object, parser, is a sub-command parser from the + argparse module. For more details, refer to the documentation + for argparse.ArgumentParser.add_subparsers(). + + The lone common optional argument is: + + * --dt-flash (if the runner capabilities includes flash_addr) + + Runner-specific options are added through the do_add_parser() + hook.''' + # Common options that depend on runner capabilities. + if cls.capabilities().flash_addr: + parser.add_argument('--dt-flash', default='n', choices=_YN_CHOICES, + action=_DTFlashAction, + help='''If 'yes', use configuration generated + by device tree (DT) to compute flash + addresses.''') + + # Runner-specific options. + cls.do_add_parser(parser) + + @classmethod + @abc.abstractmethod + def do_add_parser(cls, parser): + '''Hook for adding runner-specific options.''' + + @classmethod + @abc.abstractmethod + def create(cls, cfg, args): + '''Create an instance from command-line arguments. + + - `cfg`: RunnerConfig instance (pass to superclass __init__) + - `args`: runner-specific argument namespace parsed from + execution environment, as specified by `add_parser()`.''' + + @classmethod + def get_flash_address(cls, args, build_conf, default=0x0): + '''Helper method for extracting a flash address. + + If args.dt_flash is true, get the address from the + BoardConfiguration, build_conf. (If + CONFIG_HAS_FLASH_LOAD_OFFSET is n in that configuration, it + returns CONFIG_FLASH_BASE_ADDRESS. Otherwise, it returns + CONFIG_FLASH_BASE_ADDRESS + CONFIG_FLASH_LOAD_OFFSET.) + + Otherwise (when args.dt_flash is False), the default value is + returned.''' + if args.dt_flash: + if build_conf['CONFIG_HAS_FLASH_LOAD_OFFSET']: + return (build_conf['CONFIG_FLASH_BASE_ADDRESS'] + + build_conf['CONFIG_FLASH_LOAD_OFFSET']) + else: + return build_conf['CONFIG_FLASH_BASE_ADDRESS'] + else: + return default + + def run(self, command, **kwargs): + '''Runs command ('flash', 'debug', 'debugserver', 'attach'). + + This is the main entry point to this runner.''' + caps = self.capabilities() + if command not in caps.commands: + raise ValueError('runner {} does not implement command {}'.format( + self.name(), command)) + self.do_run(command, **kwargs) + + @abc.abstractmethod + def do_run(self, command, **kwargs): + '''Concrete runner; run() delegates to this. Implement in subclasses. + + In case of an unsupported command, raise a ValueError.''' + + def run_server_and_client(self, server, client): + '''Run a server that ignores SIGINT, and a client that handles it. + + This routine portably: + + - creates a Popen object for the ``server`` command which ignores + SIGINT + - runs ``client`` in a subprocess while temporarily ignoring SIGINT + - cleans up the server after the client exits. + + It's useful to e.g. open a GDB server and client.''' + server_proc = self.popen_ignore_int(server) + previous = signal.signal(signal.SIGINT, signal.SIG_IGN) + try: + self.check_call(client) + finally: + signal.signal(signal.SIGINT, previous) + server_proc.terminate() + server_proc.wait() + + def call(self, cmd): + '''Subclass subprocess.call() wrapper. + + Subclasses should use this method to run command in a + subprocess and get its return code, rather than + using subprocess directly, to keep accurate debug logs. + ''' + quoted = quote_sh_list(cmd) + + if JUST_PRINT: + log.inf(quoted) + return 0 + + log.dbg(quoted) + return subprocess.call(cmd) + + def check_call(self, cmd): + '''Subclass subprocess.check_call() wrapper. + + Subclasses should use this method to run command in a + subprocess and check that it executed correctly, rather than + using subprocess directly, to keep accurate debug logs. + ''' + quoted = quote_sh_list(cmd) + + if JUST_PRINT: + log.inf(quoted) + return + + log.dbg(quoted) + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError: + raise + + def check_output(self, cmd): + '''Subclass subprocess.check_output() wrapper. + + Subclasses should use this method to run command in a + subprocess and check that it executed correctly, rather than + using subprocess directly, to keep accurate debug logs. + ''' + quoted = quote_sh_list(cmd) + + if JUST_PRINT: + log.inf(quoted) + return b'' + + log.dbg(quoted) + try: + return subprocess.check_output(cmd) + except subprocess.CalledProcessError: + raise + + def popen_ignore_int(self, cmd): + '''Spawn a child command, ensuring it ignores SIGINT. + + The returned subprocess.Popen object must be manually terminated.''' + cflags = 0 + preexec = None + system = platform.system() + quoted = quote_sh_list(cmd) + + if system == 'Windows': + cflags |= subprocess.CREATE_NEW_PROCESS_GROUP + elif system in {'Linux', 'Darwin'}: + preexec = os.setsid + + if JUST_PRINT: + log.inf(quoted) + return _DebugDummyPopen() + + log.dbg(quoted) + return subprocess.Popen(cmd, creationflags=cflags, preexec_fn=preexec) diff --git a/scripts/west_commands/runners/dfu.py b/scripts/west_commands/runners/dfu.py new file mode 100644 index 00000000000..17969631029 --- /dev/null +++ b/scripts/west_commands/runners/dfu.py @@ -0,0 +1,122 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for flashing with dfu-util.''' + +from collections import namedtuple +import sys +import time + +from west import log + +from runners.core import ZephyrBinaryRunner, RunnerCaps, \ + BuildConfiguration + + +DfuSeConfig = namedtuple('DfuSeConfig', ['address', 'options']) + + +class DfuUtilBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for dfu-util.''' + + def __init__(self, cfg, pid, alt, img, exe='dfu-util', + dfuse_config=None): + super(DfuUtilBinaryRunner, self).__init__(cfg) + self.alt = alt + self.img = img + self.cmd = [exe, '-d,{}'.format(pid)] + try: + self.list_pattern = ', alt={},'.format(int(self.alt)) + except ValueError: + self.list_pattern = ', name="{}",'.format(self.alt) + + if dfuse_config is None: + self.dfuse = False + else: + self.dfuse = True + self.dfuse_config = dfuse_config + + @classmethod + def name(cls): + return 'dfu-util' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'flash'}, flash_addr=True) + + @classmethod + def do_add_parser(cls, parser): + # Required: + parser.add_argument("--pid", required=True, + help="USB VID:PID of the board") + parser.add_argument("--alt", required=True, + help="interface alternate setting number or name") + + # Optional: + parser.add_argument("--img", + help="binary to flash, default is --bin-file") + parser.add_argument("--dfuse", default=False, action='store_true', + help='''set if target is a DfuSe device; + implies --dt-flash.''') + parser.add_argument("--dfuse-modifiers", default='leave', + help='''colon-separated list of DfuSe modifiers + (default is "leave", which starts execution + immediately); --dfuse must also be given for this + option to take effect.''') + parser.add_argument('--dfu-util', default='dfu-util', + help='dfu-util executable; defaults to "dfu-util"') + + @classmethod + def create(cls, cfg, args): + if args.img is None: + args.img = cfg.bin_file + + if args.dfuse: + args.dt_flash = True # --dfuse implies --dt-flash. + build_conf = BuildConfiguration(cfg.build_dir) + dcfg = DfuSeConfig(address=cls.get_flash_address(args, build_conf), + options=args.dfuse_modifiers) + else: + dcfg = None + + return DfuUtilBinaryRunner(cfg, args.pid, args.alt, args.img, + exe=args.dfu_util, dfuse_config=dcfg) + + def find_device(self): + cmd = list(self.cmd) + ['-l'] + output = self.check_output(cmd) + output = output.decode(sys.getdefaultencoding()) + return self.list_pattern in output + + def do_run(self, command, **kwargs): + reset = False + if not self.find_device(): + reset = True + log.dbg('Device not found, waiting for it', + level=log.VERBOSE_EXTREME) + # Use of print() here is advised. We don't want to lose + # this information in a separate log -- this is + # interactive and requires a terminal. + print('Please reset your board to switch to DFU mode...') + while not self.find_device(): + time.sleep(0.1) + + cmd = list(self.cmd) + if self.dfuse: + # http://dfu-util.sourceforge.net/dfuse.html + dcfg = self.dfuse_config + addr_opts = hex(dcfg.address) + ':' + dcfg.options + cmd.extend(['-s', addr_opts]) + cmd.extend(['-a', self.alt, '-D', self.img]) + self.check_call(cmd) + + if self.dfuse and 'leave' in dcfg.options.split(':'): + # Normal DFU devices generally need to be reset to switch + # back to the flashed program. + # + # DfuSe targets do as well, except when 'leave' is given + # as an option. + reset = False + if reset: + print('Now reset your board again to switch back to runtime mode.') diff --git a/scripts/west_commands/runners/esp32.py b/scripts/west_commands/runners/esp32.py new file mode 100644 index 00000000000..fca284c5719 --- /dev/null +++ b/scripts/west_commands/runners/esp32.py @@ -0,0 +1,101 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for flashing ESP32 devices with esptool/espidf.''' + +from os import path + +from west import log + +from runners.core import ZephyrBinaryRunner, RunnerCaps + + +class Esp32BinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for espidf.''' + + def __init__(self, cfg, device, baud=921600, flash_size='detect', + flash_freq='40m', flash_mode='dio', espidf='espidf', + bootloader_bin=None, partition_table_bin=None): + super(Esp32BinaryRunner, self).__init__(cfg) + self.elf = cfg.elf_file + self.device = device + self.baud = baud + self.flash_size = flash_size + self.flash_freq = flash_freq + self.flash_mode = flash_mode + self.espidf = espidf + self.bootloader_bin = bootloader_bin + self.partition_table_bin = partition_table_bin + + @classmethod + def name(cls): + return 'esp32' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'flash'}) + + @classmethod + def do_add_parser(cls, parser): + # Required + parser.add_argument('--esp-idf-path', required=True, + help='path to ESP-IDF') + + # Optional + parser.add_argument('--esp-device', default='/dev/ttyUSB0', + help='serial port to flash, default /dev/ttyUSB0') + parser.add_argument('--esp-baud-rate', default='921600', + help='serial baud rate, default 921600') + parser.add_argument('--esp-flash-size', default='detect', + help='flash size, default "detect"') + parser.add_argument('--esp-flash-freq', default='40m', + help='flash frequency, default "40m"') + parser.add_argument('--esp-flash-mode', default='dio', + help='flash mode, default "dio"') + parser.add_argument( + '--esp-tool', + help='''if given, complete path to espidf. default is to search for + it in [ESP_IDF_PATH]/components/esptool_py/esptool/esptool.py''') + parser.add_argument('--esp-flash-bootloader', + help='Bootloader image to flash') + parser.add_argument('--esp-flash-partition_table', + help='Partition table to flash') + + @classmethod + def create(cls, cfg, args): + if args.esp_tool: + espidf = args.esp_tool + else: + espidf = path.join(args.esp_idf_path, 'components', 'esptool_py', + 'esptool', 'esptool.py') + + return Esp32BinaryRunner( + cfg, args.esp_device, baud=args.esp_baud_rate, + flash_size=args.esp_flash_size, flash_freq=args.esp_flash_freq, + flash_mode=args.esp_flash_mode, espidf=espidf, + bootloader_bin=args.esp_flash_bootloader, + partition_table_bin=args.esp_flash_partition_table) + + def do_run(self, command, **kwargs): + bin_name = path.splitext(self.elf)[0] + path.extsep + 'bin' + cmd_convert = [self.espidf, '--chip', 'esp32', 'elf2image', self.elf] + cmd_flash = [self.espidf, '--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] + + if self.bootloader_bin: + cmd_flash.extend(['0x1000', self.bootloader_bin]) + cmd_flash.extend(['0x8000', self.partition_table_bin]) + cmd_flash.extend(['0x10000', bin_name]) + else: + cmd_flash.extend(['0x1000', bin_name]) + + log.inf("Converting ELF to BIN") + self.check_call(cmd_convert) + + log.inf("Flashing ESP32 on {} ({}bps)".format(self.device, self.baud)) + self.check_call(cmd_flash) diff --git a/scripts/west_commands/runners/intel_s1000.py b/scripts/west_commands/runners/intel_s1000.py new file mode 100644 index 00000000000..938df41963d --- /dev/null +++ b/scripts/west_commands/runners/intel_s1000.py @@ -0,0 +1,167 @@ +# Copyright (c) 2018 Intel Corporation. +# Copyright 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for debugging and flashing Intel S1000 CRB''' +from os import path +import time +import signal +from west import log + +from runners.core import ZephyrBinaryRunner + +DEFAULT_XT_GDB_PORT = 20000 + + +class IntelS1000BinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for Intel S1000.''' + + def __init__(self, cfg, xt_ocd_dir, + ocd_topology, ocd_jtag_instr, gdb_flash_file, + gdb_port=DEFAULT_XT_GDB_PORT): + super(IntelS1000BinaryRunner, self).__init__(cfg) + self.board_dir = cfg.board_dir + self.elf_name = cfg.elf_file + self.gdb_cmd = cfg.gdb + self.xt_ocd_dir = xt_ocd_dir + self.ocd_topology = ocd_topology + self.ocd_jtag_instr = ocd_jtag_instr + self.gdb_flash_file = gdb_flash_file + self.gdb_port = gdb_port + + @classmethod + def name(cls): + return 'intel_s1000' + + @classmethod + def do_add_parser(cls, parser): + # Optional + parser.add_argument( + '--xt-ocd-dir', default='/opt/tensilica/xocd-12.0.4/xt-ocd', + help='ocd-dir, defaults to /opt/tensilica/xocd-12.0.4/xt-ocd') + parser.add_argument( + '--ocd-topology', default='topology_dsp0_flyswatter2.xml', + help='ocd-topology, defaults to topology_dsp0_flyswatter2.xml') + parser.add_argument( + '--ocd-jtag-instr', default='dsp0_gdb.txt', + help='ocd-jtag-instr, defaults to dsp0_gdb.txt') + parser.add_argument( + '--gdb-flash-file', default='load_elf.txt', + help='gdb-flash-file, defaults to load_elf.txt') + parser.add_argument( + '--gdb-port', default=DEFAULT_XT_GDB_PORT, + help='xt-gdb port, defaults to 20000') + + @classmethod + def create(cls, cfg, args): + return IntelS1000BinaryRunner( + cfg, args.xt_ocd_dir, + args.ocd_topology, args.ocd_jtag_instr, args.gdb_flash_file, + gdb_port=args.gdb_port) + + def do_run(self, command, **kwargs): + kwargs['ocd-topology'] = path.join(self.board_dir, 'support', + self.ocd_topology) + kwargs['ocd-jtag-instr'] = path.join(self.board_dir, 'support', + self.ocd_jtag_instr) + kwargs['gdb-flash-file'] = path.join(self.board_dir, 'support', + self.gdb_flash_file) + + if command == 'flash': + self.flash(**kwargs) + elif command == 'debugserver': + self.debugserver(**kwargs) + else: + self.do_debug(**kwargs) + + def flash(self, **kwargs): + topology_file = kwargs['ocd-topology'] + jtag_instr_file = kwargs['ocd-jtag-instr'] + gdb_flash_file = kwargs['gdb-flash-file'] + + self.print_gdbserver_message(self.gdb_port) + server_cmd = [self.xt_ocd_dir, + '-c', topology_file, + '-I', jtag_instr_file] + + # Start the server + # Note that XTOCD takes a few seconds to execute and always fails the + # first time. It has to be relaunched the second time to work. + server_proc = self.popen_ignore_int(server_cmd) + time.sleep(6) + server_proc.terminate() + server_proc = self.popen_ignore_int(server_cmd) + time.sleep(6) + + # Start the client + gdb_cmd = [self.gdb_cmd, '-x', gdb_flash_file] + client_proc = self.popen_ignore_int(gdb_cmd) + + # Wait for 3 seconds (waiting for XTGDB to finish loading the image) + time.sleep(3) + + # At this point, the ELF image is loaded and the program is in + # execution. Now we can quit the client (xt-gdb) and the server + # (xt-ocd) as they are not needed anymore. The loaded program + # (ELF) will continue to run though. + client_proc.terminate() + server_proc.terminate() + + def do_debug(self, **kwargs): + if self.elf_name is None: + raise ValueError('Cannot debug; elf is missing') + if self.gdb_cmd is None: + raise ValueError('Cannot debug; no gdb specified') + + topology_file = kwargs['ocd-topology'] + jtag_instr_file = kwargs['ocd-jtag-instr'] + + self.print_gdbserver_message(self.gdb_port) + server_cmd = [self.xt_ocd_dir, + '-c', topology_file, + '-I', jtag_instr_file] + + # Start the server + # Note that XTOCD takes a few seconds to execute and always fails the + # first time. It has to be relaunched the second time to work. + server_proc = self.popen_ignore_int(server_cmd) + time.sleep(6) + server_proc.terminate() + server_proc = self.popen_ignore_int(server_cmd) + time.sleep(6) + + gdb_cmd = [self.gdb_cmd, + '-ex', 'target remote :{}'.format(self.gdb_port), + self.elf_name] + + # Start the client + # The below statement will consume the "^C" keypress ensuring + # the python main application doesn't exit. This is important + # since ^C in gdb means a "halt" operation. + previous = signal.signal(signal.SIGINT, signal.SIG_IGN) + try: + self.check_call(gdb_cmd) + finally: + signal.signal(signal.SIGINT, previous) + server_proc.terminate() + server_proc.wait() + + def print_gdbserver_message(self, gdb_port): + log.inf('Intel S1000 GDB server running on port {}'.format(gdb_port)) + + def debugserver(self, **kwargs): + topology_file = kwargs['ocd-topology'] + jtag_instr_file = kwargs['ocd-jtag-instr'] + + self.print_gdbserver_message(self.gdb_port) + server_cmd = [self.xt_ocd_dir, + '-c', topology_file, + '-I', jtag_instr_file] + + # Note that XTOCD takes a few seconds to execute and always fails the + # first time. It has to be relaunched the second time to work. + server_proc = self.popen_ignore_int(server_cmd) + time.sleep(6) + server_proc.terminate() + self.check_call(server_cmd) diff --git a/scripts/west_commands/runners/jlink.py b/scripts/west_commands/runners/jlink.py new file mode 100644 index 00000000000..d672ffb7419 --- /dev/null +++ b/scripts/west_commands/runners/jlink.py @@ -0,0 +1,150 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for debugging with J-Link.''' + +import os +import tempfile +import sys + +from west import log +from runners.core import ZephyrBinaryRunner, RunnerCaps, \ + BuildConfiguration + +DEFAULT_JLINK_EXE = 'JLink.exe' if sys.platform == 'win32' else 'JLinkExe' +DEFAULT_JLINK_GDB_PORT = 2331 + + +class JLinkBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for the J-Link GDB server.''' + + def __init__(self, cfg, device, + commander=DEFAULT_JLINK_EXE, + flash_addr=0x0, erase=True, + iface='swd', speed='auto', + gdbserver='JLinkGDBServer', gdb_port=DEFAULT_JLINK_GDB_PORT, + tui=False): + super(JLinkBinaryRunner, self).__init__(cfg) + self.bin_name = cfg.bin_file + self.elf_name = cfg.elf_file + self.gdb_cmd = [cfg.gdb] if cfg.gdb else None + self.device = device + self.commander = commander + self.flash_addr = flash_addr + self.erase = erase + self.gdbserver_cmd = [gdbserver] + self.iface = iface + self.speed = speed + self.gdb_port = gdb_port + self.tui_arg = ['-tui'] if tui else [] + + @classmethod + def name(cls): + return 'jlink' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach'}, + flash_addr=True) + + @classmethod + def do_add_parser(cls, parser): + # Required: + parser.add_argument('--device', required=True, help='device name') + + # Optional: + parser.add_argument('--iface', default='swd', + help='interface to use, default is swd') + parser.add_argument('--speed', default='auto', + help='interface speed, default is autodetect') + parser.add_argument('--tui', default=False, action='store_true', + help='if given, GDB uses -tui') + parser.add_argument('--gdbserver', default='JLinkGDBServer', + help='GDB server, default is JLinkGDBServer') + parser.add_argument('--gdb-port', default=DEFAULT_JLINK_GDB_PORT, + help='pyocd gdb port, defaults to {}'.format( + DEFAULT_JLINK_GDB_PORT)) + parser.add_argument('--commander', default=DEFAULT_JLINK_EXE, + help='J-Link Commander, default is JLinkExe') + parser.add_argument('--erase', default=False, action='store_true', + help='if given, mass erase flash before loading') + + @classmethod + def create(cls, cfg, args): + build_conf = BuildConfiguration(cfg.build_dir) + flash_addr = cls.get_flash_address(args, build_conf) + return JLinkBinaryRunner(cfg, args.device, + commander=args.commander, + flash_addr=flash_addr, erase=args.erase, + iface=args.iface, speed=args.speed, + gdbserver=args.gdbserver, + gdb_port=args.gdb_port, + tui=args.tui) + + def print_gdbserver_message(self): + log.inf('J-Link GDB server running on port {}'.format(self.gdb_port)) + + def do_run(self, command, **kwargs): + server_cmd = (self.gdbserver_cmd + + ['-select', 'usb', # only USB connections supported + '-port', str(self.gdb_port), + '-if', self.iface, + '-speed', self.speed, + '-device', self.device, + '-silent', + '-singlerun']) + + if command == 'flash': + self.flash(**kwargs) + elif command == 'debugserver': + self.print_gdbserver_message() + self.check_call(server_cmd) + else: + if self.gdb_cmd is None: + raise ValueError('Cannot debug; gdb is missing') + if self.elf_name is None: + raise ValueError('Cannot debug; elf is missing') + client_cmd = (self.gdb_cmd + + self.tui_arg + + [self.elf_name] + + ['-ex', 'target remote :{}'.format(self.gdb_port)]) + if command == 'debug': + client_cmd += ['-ex', 'monitor halt', + '-ex', 'monitor reset', + '-ex', 'load'] + self.print_gdbserver_message() + self.run_server_and_client(server_cmd, client_cmd) + + def flash(self, **kwargs): + if self.bin_name is None: + raise ValueError('Cannot flash; bin_name is missing') + + lines = ['r'] # Reset and halt the target + + if self.erase: + lines.append('erase') # Erase all flash sectors + + lines.append('loadfile {} 0x{:x}'.format(self.bin_name, + self.flash_addr)) + lines.append('g') # Start the CPU + lines.append('q') # Close the connection and quit + + log.dbg('JLink commander script:') + log.dbg('\n'.join(lines)) + + # Don't use NamedTemporaryFile: the resulting file can't be + # opened again on Windows. + with tempfile.TemporaryDirectory(suffix='jlink') as d: + fname = os.path.join(d, 'runner.jlink') + with open(fname, 'wb') as f: + f.writelines(bytes(line + '\n', 'utf-8') for line in lines) + + cmd = ([self.commander] + + ['-if', self.iface, + '-speed', self.speed, + '-device', self.device, + '-CommanderScript', fname]) + + log.inf('Flashing Target Device') + self.check_call(cmd) diff --git a/scripts/west_commands/runners/nios2.py b/scripts/west_commands/runners/nios2.py new file mode 100644 index 00000000000..f6242d29d59 --- /dev/null +++ b/scripts/west_commands/runners/nios2.py @@ -0,0 +1,99 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for NIOS II, based on quartus-flash.py and GDB.''' + +from west import log +from runners.core import ZephyrBinaryRunner, NetworkPortHelper + + +class Nios2BinaryRunner(ZephyrBinaryRunner): + '''Runner 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, cfg, quartus_py=None, cpu_sof=None, tui=False): + super(Nios2BinaryRunner, self).__init__(cfg) + self.hex_name = cfg.hex_file + self.elf_name = cfg.elf_file + self.cpu_sof = cpu_sof + self.quartus_py = quartus_py + self.gdb_cmd = [cfg.gdb] if cfg.gdb else None + self.tui_arg = ['-tui'] if tui else [] + + @classmethod + def name(cls): + return 'nios2' + + @classmethod + def do_add_parser(cls, parser): + # TODO merge quartus-flash.py script into this file. + parser.add_argument('--quartus-flash', required=True) + parser.add_argument('--cpu-sof', required=True, + help='path to the the CPU .sof data') + parser.add_argument('--tui', default=False, action='store_true', + help='if given, GDB uses -tui') + + @classmethod + def create(cls, cfg, args): + return Nios2BinaryRunner(cfg, + quartus_py=args.quartus_flash, + cpu_sof=args.cpu_sof, + tui=args.tui) + + def do_run(self, command, **kwargs): + if command == 'flash': + self.flash(**kwargs) + else: + self.debug_debugserver(command, **kwargs) + + def flash(self, **kwargs): + if self.quartus_py is None: + raise ValueError('Cannot flash; --quartus-flash not given.') + if self.cpu_sof is None: + raise ValueError('Cannot flash; --cpu-sof not given.') + + cmd = [self.quartus_py, + '--sof', self.cpu_sof, + '--kernel', self.hex_name] + + self.check_call(cmd) + + def print_gdbserver_message(self, gdb_port): + log.inf('Nios II GDB server running on port {}'.format(gdb_port)) + + def debug_debugserver(self, command, **kwargs): + # Per comments in the shell script, the NIOSII GDB server + # doesn't exit gracefully, so it's better to explicitly search + # for an unused port. The script picks a random value in + # between 1024 and 49151, but we'll start with the + # "traditional" 3333 choice. + gdb_start = 3333 + nh = NetworkPortHelper() + gdb_port = nh.get_unused_ports([gdb_start])[0] + + server_cmd = (['nios2-gdb-server', + '--tcpport', str(gdb_port), + '--stop', '--reset-target']) + + if command == 'debugserver': + self.print_gdbserver_message(gdb_port) + self.check_call(server_cmd) + else: + if self.elf_name is None: + raise ValueError('Cannot debug; elf is missing') + if self.gdb_cmd is None: + raise ValueError('Cannot debug; no gdb specified') + + gdb_cmd = (self.gdb_cmd + + self.tui_arg + + [self.elf_name, + '-ex', 'target remote :{}'.format(gdb_port)]) + + self.print_gdbserver_message(gdb_port) + self.run_server_and_client(server_cmd, gdb_cmd) diff --git a/scripts/west_commands/runners/nrfjprog.py b/scripts/west_commands/runners/nrfjprog.py new file mode 100644 index 00000000000..c991cadb795 --- /dev/null +++ b/scripts/west_commands/runners/nrfjprog.py @@ -0,0 +1,130 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for flashing with nrfjprog.''' + +import sys + +from west import log + +from runners.core import ZephyrBinaryRunner, RunnerCaps + + +class NrfJprogBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for nrfjprog.''' + + def __init__(self, cfg, family, softreset, snr, erase=False): + super(NrfJprogBinaryRunner, self).__init__(cfg) + self.hex_ = cfg.hex_file + self.family = family + self.softreset = softreset + self.snr = snr + self.erase = erase + + @classmethod + def name(cls): + return 'nrfjprog' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'flash'}) + + @classmethod + def do_add_parser(cls, parser): + parser.add_argument('--nrf-family', required=True, + choices=['NRF51', 'NRF52'], + help='family of nRF MCU') + parser.add_argument('--softreset', required=False, + action='store_true', + help='use reset instead of pinreset') + parser.add_argument('--erase', action='store_true', + help='if given, mass erase flash before loading') + parser.add_argument('--snr', required=False, + help='serial number of board to use') + + @classmethod + def create(cls, cfg, args): + return NrfJprogBinaryRunner(cfg, args.nrf_family, args.softreset, + args.snr, erase=args.erase) + + def get_board_snr_from_user(self): + snrs = self.check_output(['nrfjprog', '--ids']) + snrs = snrs.decode(sys.getdefaultencoding()).strip().splitlines() + + if len(snrs) == 0: + raise RuntimeError('"nrfjprog --ids" did not find a board; ' + 'is the board connected?') + elif len(snrs) == 1: + board_snr = snrs[0] + if board_snr == '0': + raise RuntimeError('"nrfjprog --ids" returned 0; ' + 'is a debugger already connected?') + return board_snr + + log.dbg("Refusing the temptation to guess a board", + level=log.VERBOSE_EXTREME) + + # Use of print() here is advised. We don't want to lose + # this information in a separate log -- this is + # interactive and requires a terminal. + 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 do_run(self, command, **kwargs): + commands = [] + if (self.snr is None): + board_snr = self.get_board_snr_from_user() + else: + board_snr = self.snr.lstrip("0") + program_cmd = ['nrfjprog', '--program', self.hex_, '-f', self.family, + '--snr', board_snr] + + print('Flashing file: {}'.format(self.hex_)) + if self.erase: + commands.extend([ + ['nrfjprog', + '--eraseall', + '-f', self.family, + '--snr', board_snr], + program_cmd + ]) + else: + if self.family == 'NRF51': + commands.append(program_cmd + ['--sectorerase']) + else: + commands.append(program_cmd + ['--sectoranduicrerase']) + + if self.family == 'NRF52' and not self.softreset: + commands.extend([ + # Enable pin reset + ['nrfjprog', '--pinresetenable', '-f', self.family, + '--snr', board_snr], + ]) + + if self.softreset: + commands.append(['nrfjprog', '--reset', '-f', self.family, + '--snr', board_snr]) + else: + commands.append(['nrfjprog', '--pinreset', '-f', self.family, + '--snr', board_snr]) + + for cmd in commands: + self.check_call(cmd) + + log.inf('Board with serial number {} flashed successfully.'.format( + board_snr)) diff --git a/scripts/west_commands/runners/nsim.py b/scripts/west_commands/runners/nsim.py new file mode 100644 index 00000000000..00112b3e75f --- /dev/null +++ b/scripts/west_commands/runners/nsim.py @@ -0,0 +1,94 @@ +# Copyright (c) 2018 Synopsys Inc. +# Copyright (c) 2017 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''ARC architecture-specific runners.''' + +from os import path + +from runners.core import ZephyrBinaryRunner + +DEFAULT_ARC_GDB_PORT = 3333 +DEFAULT_PROPS_FILE = 'nsim.props' + + +class NsimBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for the ARC si.''' + + # This unusual 'flash' implementation matches 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. + # + + def __init__(self, cfg, + tui=False, + gdb_port=DEFAULT_ARC_GDB_PORT, + props=DEFAULT_PROPS_FILE): + super(NsimBinaryRunner, self).__init__(cfg) + self.gdb_cmd = [cfg.gdb] + (['-tui'] if tui else []) + self.nsim_cmd = ['nsimdrv'] + self.gdb_port = gdb_port + self.props = props + + @classmethod + def name(cls): + return 'arc-nsim' + + @classmethod + def do_add_parser(cls, parser): + parser.add_argument('--gdb-port', default=DEFAULT_ARC_GDB_PORT, + help='nsim gdb port, defaults to 3333') + parser.add_argument('--props', default=DEFAULT_PROPS_FILE, + help='nsim props file, defaults to nsim.props') + + @classmethod + def create(cls, cfg, args): + if cfg.gdb is None: + raise ValueError('--gdb not provided at command line') + + return NsimBinaryRunner( + cfg, + gdb_port=args.gdb_port, + props=args.props) + + def do_run(self, command, **kwargs): + kwargs['nsim-cfg'] = path.join(self.cfg.board_dir, 'support', + self.props) + + if command == 'flash': + self.do_flash(**kwargs) + elif command == 'debug': + self.do_debug(**kwargs) + else: + self.debugserver(**kwargs) + + def do_flash(self, **kwargs): + config = kwargs['nsim-cfg'] + + cmd = (self.nsim_cmd + ['-propsfile', config, self.cfg.elf_file]) + self.check_call(cmd) + + def do_debug(self, **kwargs): + config = kwargs['nsim-cfg'] + + server_cmd = (self.nsim_cmd + ['-gdb', + '-port={}'.format(self.gdb_port), + '-propsfile', config]) + + gdb_cmd = (self.gdb_cmd + + ['-ex', 'target remote :{}'.format(self.gdb_port), + '-ex', 'load', self.cfg.elf_file]) + + self.run_server_and_client(server_cmd, gdb_cmd) + + def debugserver(self, **kwargs): + config = kwargs['nsim-cfg'] + + cmd = (self.nsim_cmd + + ['-gdb', '-port={}'.format(self.gdb_port), + '-propsfile', config]) + + self.check_call(cmd) diff --git a/scripts/west_commands/runners/openocd.py b/scripts/west_commands/runners/openocd.py new file mode 100644 index 00000000000..abad47874f1 --- /dev/null +++ b/scripts/west_commands/runners/openocd.py @@ -0,0 +1,145 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for openocd.''' + +from os import path + +from runners.core import ZephyrBinaryRunner + +DEFAULT_OPENOCD_TCL_PORT = 6333 +DEFAULT_OPENOCD_TELNET_PORT = 4444 +DEFAULT_OPENOCD_GDB_PORT = 3333 + + +class OpenOcdBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for openocd.''' + + def __init__(self, cfg, + pre_cmd=None, load_cmd=None, verify_cmd=None, post_cmd=None, + tui=None, + tcl_port=DEFAULT_OPENOCD_TCL_PORT, + telnet_port=DEFAULT_OPENOCD_TELNET_PORT, + gdb_port=DEFAULT_OPENOCD_GDB_PORT): + super(OpenOcdBinaryRunner, self).__init__(cfg) + self.openocd_config = path.join(cfg.board_dir, 'support', + 'openocd.cfg') + + search_args = [] + if cfg.openocd_search is not None: + search_args = ['-s', cfg.openocd_search] + self.openocd_cmd = [cfg.openocd] + search_args + self.elf_name = cfg.elf_file + self.load_cmd = load_cmd + self.verify_cmd = verify_cmd + self.pre_cmd = pre_cmd + self.post_cmd = post_cmd + self.tcl_port = tcl_port + self.telnet_port = telnet_port + self.gdb_port = gdb_port + self.gdb_cmd = [cfg.gdb] if cfg.gdb else None + self.tui_arg = ['-tui'] if tui else [] + + @classmethod + def name(cls): + return 'openocd' + + @classmethod + def do_add_parser(cls, parser): + # Options for flashing: + parser.add_argument('--cmd-pre-load', + help='Command to run before flashing') + parser.add_argument('--cmd-load', + help='''Command to load/flash binary + (required when flashing)''') + parser.add_argument('--cmd-verify', + help='''Command to verify flashed binary''') + parser.add_argument('--cmd-post-verify', + help='Command to run after verification') + + # Options for debugging: + parser.add_argument('--tui', default=False, action='store_true', + help='if given, GDB uses -tui') + parser.add_argument('--tcl-port', default=DEFAULT_OPENOCD_TCL_PORT, + help='openocd TCL port, defaults to 6333') + parser.add_argument('--telnet-port', + default=DEFAULT_OPENOCD_TELNET_PORT, + help='openocd telnet port, defaults to 4444') + parser.add_argument('--gdb-port', default=DEFAULT_OPENOCD_GDB_PORT, + help='openocd gdb port, defaults to 3333') + + @classmethod + def create(cls, cfg, args): + return OpenOcdBinaryRunner( + cfg, + pre_cmd=args.cmd_pre_load, load_cmd=args.cmd_load, + verify_cmd=args.cmd_verify, post_cmd=args.cmd_post_verify, + tui=args.tui, + tcl_port=args.tcl_port, telnet_port=args.telnet_port, + gdb_port=args.gdb_port) + + def do_run(self, command, **kwargs): + if command == 'flash': + self.do_flash(**kwargs) + elif command == 'debug': + self.do_debug(**kwargs) + else: + self.do_debugserver(**kwargs) + + def do_flash(self, **kwargs): + if self.load_cmd is None: + raise ValueError('Cannot flash; load command is missing') + if self.verify_cmd is None: + raise ValueError('Cannot flash; verify command is missing') + + 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_cmd + + ['-f', self.openocd_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']) + self.check_call(cmd) + + def do_debug(self, **kwargs): + if self.gdb_cmd is None: + raise ValueError('Cannot debug; no gdb specified') + if self.elf_name is None: + raise ValueError('Cannot debug; no .elf specified') + + server_cmd = (self.openocd_cmd + + ['-f', self.openocd_config, + '-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']) + + gdb_cmd = (self.gdb_cmd + self.tui_arg + + ['-ex', 'target remote :{}'.format(self.gdb_port), + self.elf_name]) + + self.run_server_and_client(server_cmd, gdb_cmd) + + def do_debugserver(self, **kwargs): + cmd = (self.openocd_cmd + + ['-f', self.openocd_config, + '-c', 'init', + '-c', 'targets', + '-c', 'reset halt']) + self.check_call(cmd) diff --git a/scripts/west_commands/runners/pyocd.py b/scripts/west_commands/runners/pyocd.py new file mode 100644 index 00000000000..3072a45b4b5 --- /dev/null +++ b/scripts/west_commands/runners/pyocd.py @@ -0,0 +1,170 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for pyOCD .''' + +import os + +from west import log + +from runners.core import ZephyrBinaryRunner, RunnerCaps, \ + BuildConfiguration + +DEFAULT_PYOCD_GDB_PORT = 3333 + + +class PyOcdBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for pyOCD.''' + + def __init__(self, cfg, target, + flashtool='pyocd-flashtool', flash_addr=0x0, + flashtool_opts=None, + gdbserver='pyocd-gdbserver', + gdb_port=DEFAULT_PYOCD_GDB_PORT, tui=False, + board_id=None, daparg=None, frequency=None): + super(PyOcdBinaryRunner, self).__init__(cfg) + + self.target_args = ['-t', target] + self.flashtool = flashtool + self.flash_addr_args = ['-a', hex(flash_addr)] if flash_addr else [] + self.gdb_cmd = [cfg.gdb] if cfg.gdb is not None else None + self.gdbserver = gdbserver + self.gdb_port = gdb_port + self.tui_args = ['-tui'] if tui else [] + self.hex_name = cfg.hex_file + self.bin_name = cfg.bin_file + self.elf_name = cfg.elf_file + + board_args = [] + if board_id is not None: + board_args = ['-b', board_id] + self.board_args = board_args + + daparg_args = [] + if daparg is not None: + daparg_args = ['-da', daparg] + self.daparg_args = daparg_args + + frequency_args = [] + if frequency is not None: + frequency_args = ['-f', frequency] + self.frequency_args = frequency_args + + self.flashtool_extra = flashtool_opts if flashtool_opts else [] + + @classmethod + def name(cls): + return 'pyocd' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach'}, + flash_addr=True) + + @classmethod + def do_add_parser(cls, parser): + parser.add_argument('--target', required=True, + help='target override') + + parser.add_argument('--daparg', + help='Additional -da arguments to pyocd tool') + parser.add_argument('--flashtool', default='pyocd-flashtool', + help='flash tool path, default is pyocd-flashtool') + parser.add_argument('--flashtool-opt', default=[], action='append', + help='''Additional options for pyocd-flashtool, + e.g. -ce to chip erase''') + parser.add_argument('--frequency', + help='SWD clock frequency in Hz') + parser.add_argument('--gdbserver', default='pyocd-gdbserver', + help='GDB server, default is pyocd-gdbserver') + parser.add_argument('--gdb-port', default=DEFAULT_PYOCD_GDB_PORT, + help='pyocd gdb port, defaults to {}'.format( + DEFAULT_PYOCD_GDB_PORT)) + parser.add_argument('--tui', default=False, action='store_true', + help='if given, GDB uses -tui') + parser.add_argument('--board-id', + help='ID of board to flash, default is to prompt') + + @classmethod + def create(cls, cfg, args): + daparg = os.environ.get('PYOCD_DAPARG') + if daparg: + log.wrn('Setting PYOCD_DAPARG in the environment is', + 'deprecated; use the --daparg option instead.') + if args.daparg is None: + log.dbg('Missing --daparg set to {} from environment'.format( + daparg), level=log.VERBOSE_VERY) + args.daparg = daparg + + build_conf = BuildConfiguration(cfg.build_dir) + flash_addr = cls.get_flash_address(args, build_conf) + + return PyOcdBinaryRunner( + cfg, args.target, flashtool=args.flashtool, + flash_addr=flash_addr, flashtool_opts=args.flashtool_opt, + gdbserver=args.gdbserver, gdb_port=args.gdb_port, tui=args.tui, + board_id=args.board_id, daparg=args.daparg, + frequency=args.frequency) + + def port_args(self): + return ['-p', str(self.gdb_port)] + + def do_run(self, command, **kwargs): + if command == 'flash': + self.flash(**kwargs) + else: + self.debug_debugserver(command, **kwargs) + + def flash(self, **kwargs): + if os.path.isfile(self.hex_name): + fname = self.hex_name + elif os.path.isfile(self.bin_name): + fname = self.bin_name + else: + raise ValueError( + 'Cannot flash; no hex ({}) or bin ({}) files'.format( + self.hex_name, self.bin_name)) + + cmd = ([self.flashtool] + + self.flash_addr_args + + self.daparg_args + + self.target_args + + self.board_args + + self.frequency_args + + self.flashtool_extra + + [fname]) + + log.inf('Flashing Target Device') + self.check_call(cmd) + + def print_gdbserver_message(self): + log.inf('pyOCD GDB server running on port {}'.format(self.gdb_port)) + + def debug_debugserver(self, command, **kwargs): + server_cmd = ([self.gdbserver] + + self.daparg_args + + self.port_args() + + self.target_args + + self.board_args + + self.frequency_args) + + if command == 'debugserver': + self.print_gdbserver_message() + self.check_call(server_cmd) + else: + if self.gdb_cmd is None: + raise ValueError('Cannot debug; gdb is missing') + if self.elf_name is None: + raise ValueError('Cannot debug; elf is missing') + client_cmd = (self.gdb_cmd + + self.tui_args + + [self.elf_name] + + ['-ex', 'target remote :{}'.format(self.gdb_port)]) + if command == 'debug': + client_cmd += ['-ex', 'monitor halt', + '-ex', 'monitor reset', + '-ex', 'load'] + + self.print_gdbserver_message() + self.run_server_and_client(server_cmd, client_cmd) diff --git a/scripts/west_commands/runners/qemu.py b/scripts/west_commands/runners/qemu.py new file mode 100644 index 00000000000..ea39be8ad41 --- /dev/null +++ b/scripts/west_commands/runners/qemu.py @@ -0,0 +1,34 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner stub for QEMU.''' + +from runners.core import ZephyrBinaryRunner, RunnerCaps + + +class QemuBinaryRunner(ZephyrBinaryRunner): + '''Place-holder for QEMU runner customizations.''' + + def __init__(self, cfg): + super(QemuBinaryRunner, self).__init__(cfg) + + @classmethod + def name(cls): + return 'qemu' + + @classmethod + def capabilities(cls): + # This is a stub. + return RunnerCaps(commands=set()) + + @classmethod + def do_add_parser(cls, parser): + pass # Nothing to do. + + @classmethod + def create(cls, cfg, args): + return QemuBinaryRunner(cfg) + + def do_run(self, command, **kwargs): + pass diff --git a/scripts/west_commands/runners/xtensa.py b/scripts/west_commands/runners/xtensa.py new file mode 100644 index 00000000000..244c8c2e181 --- /dev/null +++ b/scripts/west_commands/runners/xtensa.py @@ -0,0 +1,40 @@ +# Copyright (c) 2017 Linaro Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Runner for debugging with xt-gdb.''' + +from os import path + +from runners.core import ZephyrBinaryRunner, RunnerCaps + + +class XtensaBinaryRunner(ZephyrBinaryRunner): + '''Runner front-end for xt-gdb.''' + + def __init__(self, cfg): + super(XtensaBinaryRunner, self).__init__(cfg) + + @classmethod + def name(cls): + return 'xtensa' + + @classmethod + def capabilities(cls): + return RunnerCaps(commands={'debug'}) + + @classmethod + def do_add_parser(cls, parser): + parser.add_argument('--xcc-tools', required=True, + help='path to XTensa tools') + + @classmethod + def create(cls, cfg, args): + # Override any GDB with the one provided by the XTensa tools. + cfg.gdb = path.join(args.xcc_tools, 'bin', 'xt-gdb') + return XtensaBinaryRunner(cfg) + + def do_run(self, command, **kwargs): + gdb_cmd = [self.cfg.gdb, self.cfg.elf_file] + + self.check_call(gdb_cmd)