#!/usr/bin/env python3 # Copyright 2020 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Flashes firmware using Segger J-Link. This script requires Segger hardware attached via JTAG/SWD. See https://chromium.googlesource.com/chromiumos/platform/ec/+/HEAD/docs/fingerprint/fingerprint-debugging.md#flash for instructions. """ import argparse import logging import os import shutil import socket import subprocess import sys import tempfile import time DEFAULT_SEGGER_REMOTE_PORT = 19020 # Commands are documented here: https://wiki.segger.com/J-Link_Commander JLINK_COMMANDS = ''' exitonerror 1 r loadfile {FIRMWARE} {FLASH_ADDRESS} r go exit ''' class BoardConfig: def __init__(self, interface, device, flash_address): self.interface = interface self.device = device self.flash_address = flash_address SWD_INTERFACE = 'SWD' STM32_DEFAULT_FLASH_ADDRESS = '0x8000000' DRAGONCLAW_CONFIG = BoardConfig(interface=SWD_INTERFACE, device='STM32F412CG', flash_address=STM32_DEFAULT_FLASH_ADDRESS) ICETOWER_CONFIG = BoardConfig(interface=SWD_INTERFACE, device='STM32H743ZI', flash_address=STM32_DEFAULT_FLASH_ADDRESS) BOARD_CONFIGS = { 'dragonclaw': DRAGONCLAW_CONFIG, 'bloonchipper': DRAGONCLAW_CONFIG, 'nucleo-f412zg': DRAGONCLAW_CONFIG, 'dartmonkey': ICETOWER_CONFIG, 'icetower': ICETOWER_CONFIG, 'nucleo-dartmonkey': ICETOWER_CONFIG, 'nucleo-h743zi': ICETOWER_CONFIG, } def is_tcp_port_open(host: str, tcp_port: int) -> bool: """Checks if the TCP host port is open.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) # 2 Second Timeout try: sock.connect((host, tcp_port)) sock.shutdown(socket.SHUT_RDWR) except ConnectionRefusedError: return False except socket.timeout: return False finally: sock.close() # Other errors are propagated as odd exceptions. # We shutdown and closed the connection, but the server may need a second # to start listening again. If the following error is seen, this timeout # should be increased. 300ms seems to be the minimum. # # Connecting to J-Link via IP...FAILED: Can not connect to J-Link via \ # TCP/IP (127.0.0.1, port 19020) time.sleep(0.5) return True def create_jlink_command_file(firmware_file, config): tmp = tempfile.NamedTemporaryFile() tmp.write(JLINK_COMMANDS.format(FIRMWARE=firmware_file, FLASH_ADDRESS=config.flash_address).encode( 'utf-8')) tmp.flush() return tmp def flash(jlink_exe, remote, device, interface, cmd_file): cmd = [ jlink_exe, ] if remote: logging.debug(f'Connecting to J-Link over TCP/IP {remote}.') remote_components = remote.split(':') if len(remote_components) not in [1, 2]: logging.debug(f'Given remote "{remote}" is malformed.') return 1 host = remote_components[0] try: ip = socket.gethostbyname(host) except socket.gaierror as e: logging.error(f'Failed to resolve host "{host}": {e}.') return 1 logging.debug(f'Resolved {host} as {ip}.') port = DEFAULT_SEGGER_REMOTE_PORT if len(remote_components) == 2: try: port = int(remote_components[1]) except ValueError: logging.error( f'Given remote port "{remote_components[1]}" is malformed.') return 1 remote = f'{ip}:{port}' logging.debug(f'Checking connection to {remote}.') if not is_tcp_port_open(ip, port): logging.error( f'JLink server doesn\'t seem to be listening on {remote}.') logging.error('Ensure that JLinkRemoteServerCLExe is running.') return 1 cmd.extend(['-ip', remote]) cmd.extend([ '-device', device, '-if', interface, '-speed', 'auto', '-autoconnect', '1', '-CommandFile', cmd_file, ]) logging.debug('Running command: "%s"', ' '.join(cmd)) completed_process = subprocess.run(cmd) logging.debug('JLink return code: %d', completed_process.returncode) return completed_process.returncode def main(argv: list): parser = argparse.ArgumentParser() default_jlink = './JLink_Linux_V684a_x86_64/JLinkExe' if shutil.which(default_jlink) is None: default_jlink = 'JLinkExe' parser.add_argument( '--jlink', '-j', help='JLinkExe path (default: ' + default_jlink + ')', default=default_jlink) parser.add_argument( '--remote', '-n', help='Use TCP/IP host[:port] to connect to a J-Link or ' 'JLinkRemoteServerCLExe. If unspecified, connect over USB.') default_board = 'bloonchipper' parser.add_argument( '--board', '-b', help='Board (default: ' + default_board + ')', default=default_board) default_firmware = os.path.join('./build', default_board, 'ec.bin') parser.add_argument( '--image', '-i', help='Firmware binary (default: ' + default_firmware + ')', default=default_firmware) log_level_choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] parser.add_argument( '--log_level', '-l', choices=log_level_choices, default='DEBUG' ) args = parser.parse_args(argv) logging.basicConfig(level=args.log_level) if args.board not in BOARD_CONFIGS: logging.error('Unable to find a config for board: "%s"', args.board) sys.exit(1) config = BOARD_CONFIGS[args.board] args.image = os.path.realpath(args.image) args.jlink = args.jlink cmd_file = create_jlink_command_file(args.image, config) ret_code = flash(args.jlink, args.remote, config.device, config.interface, cmd_file.name) cmd_file.close() return ret_code if __name__ == '__main__': sys.exit(main(sys.argv[1:]))