1# Copyright (c) 2017 Linaro Limited. 2# Copyright (c) 2020 Gerson Fernando Budke <nandojve@gmail.com> 3# 4# SPDX-License-Identifier: Apache-2.0 5 6'''bossac-specific runner (flash only) for Atmel SAM microcontrollers.''' 7 8import os 9import pathlib 10import pickle 11import platform 12import subprocess 13import sys 14import time 15 16from runners.core import RunnerCaps, ZephyrBinaryRunner 17 18if platform.system() == 'Darwin': 19 DEFAULT_BOSSAC_PORT = None 20else: 21 DEFAULT_BOSSAC_PORT = '/dev/ttyACM0' 22DEFAULT_BOSSAC_SPEED = '115200' 23 24class BossacBinaryRunner(ZephyrBinaryRunner): 25 '''Runner front-end for bossac.''' 26 27 def __init__(self, cfg, bossac='bossac', port=DEFAULT_BOSSAC_PORT, 28 speed=DEFAULT_BOSSAC_SPEED, boot_delay=0, erase=False): 29 super().__init__(cfg) 30 self.bossac = bossac 31 self.port = port 32 self.speed = speed 33 self.boot_delay = boot_delay 34 self.erase = erase 35 36 @classmethod 37 def name(cls): 38 return 'bossac' 39 40 @classmethod 41 def capabilities(cls): 42 return RunnerCaps(commands={'flash'}, erase=True) 43 44 @classmethod 45 def do_add_parser(cls, parser): 46 parser.add_argument('--bossac', default='bossac', 47 help='path to bossac, default is bossac') 48 parser.add_argument('--bossac-port', default=DEFAULT_BOSSAC_PORT, 49 help='serial port to use, default is ' + 50 str(DEFAULT_BOSSAC_PORT)) 51 parser.add_argument('--speed', default=DEFAULT_BOSSAC_SPEED, 52 help='serial port speed to use, default is ' + 53 DEFAULT_BOSSAC_SPEED) 54 parser.add_argument('--delay', default=0, type=float, 55 help='''delay in seconds (may be a floating 56 point number) to wait between putting the board 57 into bootloader mode and running bossac; 58 default is no delay''') 59 60 @classmethod 61 def do_create(cls, cfg, args): 62 return BossacBinaryRunner(cfg, bossac=args.bossac, 63 port=args.bossac_port, speed=args.speed, 64 boot_delay=args.delay, erase=args.erase) 65 66 def read_help(self): 67 """Run bossac --help and return the output as a list of lines""" 68 self.require(self.bossac) 69 try: 70 # BOSSA > 1.9.1 returns OK 71 out = self.check_output([self.bossac, '--help']).decode() 72 except subprocess.CalledProcessError as ex: 73 # BOSSA <= 1.9.1 returns an error 74 out = ex.output.decode() 75 76 return out.split('\n') 77 78 def supports(self, flag): 79 """Check if bossac supports a flag by searching the help""" 80 return any(flag in line for line in self.read_help()) 81 82 def is_extended_samba_protocol(self): 83 ext_samba_versions = ['CONFIG_BOOTLOADER_BOSSA_ARDUINO', 84 'CONFIG_BOOTLOADER_BOSSA_ADAFRUIT_UF2'] 85 86 return any(self.build_conf.getboolean(x) for x in ext_samba_versions) 87 88 def is_partition_enabled(self): 89 return self.build_conf.getboolean('CONFIG_USE_DT_CODE_PARTITION') 90 91 def get_chosen_code_partition_node(self): 92 # Get the EDT Node corresponding to the zephyr,code-partition 93 # chosen DT node 94 95 # Ensure the build directory has a compiled DTS file 96 # where we expect it to be. 97 b = pathlib.Path(self.cfg.build_dir) 98 edt_pickle = b / 'zephyr' / 'edt.pickle' 99 if not edt_pickle.is_file(): 100 error_msg = "can't load devicetree; expected to find:" + str(edt_pickle) 101 102 raise RuntimeError(error_msg) 103 104 # Load the devicetree. 105 try: 106 with open(edt_pickle, 'rb') as f: 107 edt = pickle.load(f) 108 except ModuleNotFoundError as err: 109 error_msg = "could not load devicetree, something may be wrong " \ 110 + "with the python environment" 111 raise RuntimeError(error_msg) from err 112 113 return edt.chosen_node('zephyr,code-partition') 114 115 def get_board_name(self): 116 if 'CONFIG_BOARD' not in self.build_conf: 117 return '<board>' 118 119 return self.build_conf['CONFIG_BOARD'] 120 121 def get_dts_img_offset(self): 122 if self.build_conf.getboolean('CONFIG_BOOTLOADER_BOSSA_LEGACY'): 123 return 0 124 125 if self.build_conf.getboolean('CONFIG_HAS_FLASH_LOAD_OFFSET'): 126 return self.build_conf['CONFIG_FLASH_LOAD_OFFSET'] 127 128 return 0 129 130 def get_image_offset(self, supports_offset): 131 """Validates and returns the flash offset""" 132 133 dts_img_offset = self.get_dts_img_offset() 134 135 if int(str(dts_img_offset), 16) > 0: 136 if not supports_offset: 137 old_sdk = 'This version of BOSSA does not support the' \ 138 ' --offset flag. Please upgrade to a newer Zephyr' \ 139 ' SDK version >= 0.12.0.' 140 raise RuntimeError(old_sdk) 141 142 return dts_img_offset 143 144 return None 145 146 def is_gnu_coreutils_stty(self): 147 try: 148 result = subprocess.run( 149 ['stty', '--version'], capture_output=True, text=True, check=True 150 ) 151 return 'coreutils' in result.stdout 152 except subprocess.CalledProcessError: 153 return False 154 155 def set_serial_config(self): 156 if platform.system() == 'Linux' or platform.system() == 'Darwin': 157 self.require('stty') 158 159 # GNU coreutils uses a capital F flag for 'file' 160 flag = '-F' if self.is_gnu_coreutils_stty() else '-f' 161 162 if self.is_extended_samba_protocol(): 163 self.speed = '1200' 164 165 cmd_stty = ['stty', flag, self.port, 'raw', 'ispeed', self.speed, 166 'ospeed', self.speed, 'cs8', '-cstopb', 'ignpar', 167 'eol', '255', 'eof', '255'] 168 self.check_call(cmd_stty) 169 self.magic_delay() 170 171 def magic_delay(self): 172 '''There can be a time lag between the board resetting into 173 bootloader mode (done via stty above) and the OS enumerating 174 the USB device again. This function lets users tune a magic 175 delay for their system to handle this case. By default, 176 we don't wait. 177 ''' 178 179 if self.boot_delay > 0: 180 time.sleep(self.boot_delay) 181 182 def make_bossac_cmd(self): 183 self.ensure_output('bin') 184 cmd_flash = [self.bossac, '-p', self.port, '-R', '-w', '-v', 185 '-b', self.cfg.bin_file] 186 187 if self.erase: 188 cmd_flash += ['-e'] 189 190 dt_chosen_code_partition_nd = self.get_chosen_code_partition_node() 191 192 if self.is_partition_enabled(): 193 if dt_chosen_code_partition_nd is None: 194 error_msg = 'The device tree zephyr,code-partition chosen' \ 195 ' node must be defined.' 196 197 raise RuntimeError(error_msg) 198 199 offset = self.get_image_offset(self.supports('--offset')) 200 201 if offset is not None and int(str(offset), 16) > 0: 202 cmd_flash += ['-o', str(offset)] 203 204 elif dt_chosen_code_partition_nd is not None: 205 error_msg = 'There is no CONFIG_USE_DT_CODE_PARTITION Kconfig' \ 206 ' defined at ' + self.get_board_name() + \ 207 '_defconfig file.\n This means that' \ 208 ' zephyr,code-partition device tree node should not' \ 209 ' be defined. Check Zephyr SAM-BA documentation.' 210 211 raise RuntimeError(error_msg) 212 213 return cmd_flash 214 215 def get_darwin_serial_device_list(self): 216 """ 217 Get a list of candidate serial ports on Darwin by querying the IOKit 218 registry. 219 """ 220 import plistlib 221 222 ioreg_out = self.check_output(['ioreg', '-r', '-c', 'IOSerialBSDClient', 223 '-k', 'IOCalloutDevice', '-a']) 224 serial_ports = plistlib.loads(ioreg_out, fmt=plistlib.FMT_XML) 225 226 return [port["IOCalloutDevice"] for port in serial_ports] 227 228 def get_darwin_user_port_choice(self): 229 """ 230 Ask the user to select the serial port from a set of candidate ports 231 retrieved from IOKit on Darwin. 232 233 Modelled on get_board_snr() in the nrfjprog runner. 234 """ 235 devices = self.get_darwin_serial_device_list() 236 237 if len(devices) == 0: 238 raise RuntimeError('No candidate serial ports were found!') 239 elif len(devices) == 1: 240 print('Using only serial device on the system: ' + devices[0]) 241 return devices[0] 242 elif not sys.stdin.isatty(): 243 raise RuntimeError('Refusing to guess which serial port to use: ' 244 f'there are {len(devices)} available. ' 245 '(Interactive prompts disabled since standard ' 246 'input is not a terminal - please specify a ' 247 'port using --bossac-port instead)') 248 249 print('There are multiple serial ports available on this system:') 250 251 for i, device in enumerate(devices, 1): 252 print(f' {i}. {device}') 253 254 p = f'Please select one (1-{len(devices)}, or EOF to exit): ' 255 256 while True: 257 try: 258 value = input(p) 259 except EOFError: 260 sys.exit(0) 261 try: 262 value = int(value) 263 except ValueError: 264 continue 265 if 1 <= value <= len(devices): 266 break 267 268 return devices[value - 1] 269 270 def do_run(self, command, **kwargs): 271 if platform.system() == 'Linux': 272 if 'microsoft' in platform.uname().release.lower() or \ 273 os.getenv('WSL_DISTRO_NAME') is not None or \ 274 os.getenv('WSL_INTEROP') is not None: 275 msg = 'CAUTION: BOSSAC runner not supported on WSL!' 276 raise RuntimeError(msg) 277 elif platform.system() == 'Darwin' and self.port is None: 278 self.port = self.get_darwin_user_port_choice() 279 280 self.require(self.bossac) 281 self.set_serial_config() 282 self.check_call(self.make_bossac_cmd()) 283