1# Copyright (c) 2017 Linaro Limited. 2# 3# SPDX-License-Identifier: Apache-2.0 4 5'''Runner for debugging with J-Link.''' 6 7import argparse 8import ipaddress 9import logging 10import os 11import shlex 12import socket 13import subprocess 14import sys 15import tempfile 16import time 17from pathlib import Path 18 19from runners.core import FileType, RunnerCaps, ZephyrBinaryRunner 20 21try: 22 import pylink 23 from pylink.library import Library 24 MISSING_REQUIREMENTS = False 25except ImportError: 26 MISSING_REQUIREMENTS = True 27 28DEFAULT_JLINK_EXE = 'JLink.exe' if sys.platform == 'win32' else 'JLinkExe' 29DEFAULT_JLINK_GDB_PORT = 2331 30DEFAULT_JLINK_RTT_PORT = 19021 31 32def is_ip(ip): 33 if not ip: 34 return False 35 try: 36 ipaddress.ip_address(ip.split(':')[0]) 37 except ValueError: 38 return False 39 return True 40 41def is_tunnel(tunnel): 42 return tunnel.startswith("tunnel:") if tunnel else False 43 44class ToggleAction(argparse.Action): 45 46 def __call__(self, parser, args, ignored, option): 47 setattr(args, self.dest, not option.startswith('--no-')) 48 49class JLinkBinaryRunner(ZephyrBinaryRunner): 50 '''Runner front-end for the J-Link GDB server.''' 51 52 def __init__(self, cfg, device, dev_id=None, 53 commander=DEFAULT_JLINK_EXE, 54 dt_flash=True, erase=True, reset=False, 55 iface='swd', speed='auto', flash_script = None, 56 loader=None, 57 gdbserver='JLinkGDBServer', 58 gdb_host='', 59 gdb_port=DEFAULT_JLINK_GDB_PORT, 60 rtt_port=DEFAULT_JLINK_RTT_PORT, 61 tui=False, tool_opt=None): 62 super().__init__(cfg) 63 self.file = cfg.file 64 self.file_type = cfg.file_type 65 self.hex_name = cfg.hex_file 66 self.bin_name = cfg.bin_file 67 self.elf_name = cfg.elf_file 68 self.gdb_cmd = [cfg.gdb] if cfg.gdb else None 69 self.device = device 70 self.dev_id = dev_id 71 self.commander = commander 72 self.flash_script = flash_script 73 self.dt_flash = dt_flash 74 self.erase = erase 75 self.reset = reset 76 self.gdbserver = gdbserver 77 self.iface = iface 78 self.speed = speed 79 self.gdb_host = gdb_host 80 self.gdb_port = gdb_port 81 self.tui_arg = ['-tui'] if tui else [] 82 self.loader = loader 83 self.rtt_port = rtt_port 84 85 self.tool_opt = [] 86 if tool_opt is not None: 87 for opts in [shlex.split(opt) for opt in tool_opt]: 88 self.tool_opt += opts 89 90 @classmethod 91 def name(cls): 92 return 'jlink' 93 94 @classmethod 95 def capabilities(cls): 96 return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach', 'rtt'}, 97 dev_id=True, flash_addr=True, erase=True, reset=True, 98 tool_opt=True, file=True, rtt=True) 99 100 @classmethod 101 def dev_id_help(cls) -> str: 102 return '''Device identifier. Use it to select the J-Link Serial Number 103 of the device connected over USB. If the J-Link is connected over ip, 104 the Device identifier is the ip.''' 105 106 @classmethod 107 def tool_opt_help(cls) -> str: 108 return "Additional options for JLink Commander, e.g. '-autoconnect 1'" 109 110 @classmethod 111 def do_add_parser(cls, parser): 112 # Required: 113 parser.add_argument('--device', required=True, help='device name') 114 115 # Optional: 116 parser.add_argument('--loader', required=False, dest='loader', 117 help='specifies a loader type') 118 parser.add_argument('--id', required=False, dest='dev_id', 119 help='obsolete synonym for -i/--dev-id') 120 parser.add_argument('--iface', default='swd', 121 help='interface to use, default is swd') 122 parser.add_argument('--speed', default='auto', 123 help='interface speed, default is autodetect') 124 parser.add_argument('--flash-script', default=None, 125 help='Custom flashing script, default is None') 126 parser.add_argument('--tui', default=False, action='store_true', 127 help='if given, GDB uses -tui') 128 parser.add_argument('--gdbserver', default='JLinkGDBServer', 129 help='GDB server, default is JLinkGDBServer') 130 parser.add_argument('--gdb-host', default='', 131 help='custom gdb host, defaults to the empty string ' 132 'and runs a gdb server') 133 parser.add_argument('--gdb-port', default=DEFAULT_JLINK_GDB_PORT, 134 help=f'pyocd gdb port, defaults to {DEFAULT_JLINK_GDB_PORT}') 135 parser.add_argument('--commander', default=DEFAULT_JLINK_EXE, 136 help=f'''J-Link Commander, default is 137 {DEFAULT_JLINK_EXE}''') 138 parser.add_argument('--reset-after-load', '--no-reset-after-load', 139 dest='reset', nargs=0, 140 action=ToggleAction, 141 help='obsolete synonym for --reset/--no-reset') 142 parser.add_argument('--rtt-client', default='JLinkRTTClient', 143 help='RTT client, default is JLinkRTTClient') 144 parser.add_argument('--rtt-port', default=DEFAULT_JLINK_RTT_PORT, 145 help=f'jlink rtt port, defaults to {DEFAULT_JLINK_RTT_PORT}') 146 147 parser.set_defaults(reset=False) 148 149 @classmethod 150 def do_create(cls, cfg, args): 151 return JLinkBinaryRunner(cfg, args.device, 152 dev_id=args.dev_id, 153 commander=args.commander, 154 dt_flash=args.dt_flash, 155 erase=args.erase, 156 reset=args.reset, 157 iface=args.iface, speed=args.speed, 158 flash_script=args.flash_script, 159 gdbserver=args.gdbserver, 160 loader=args.loader, 161 gdb_host=args.gdb_host, 162 gdb_port=args.gdb_port, 163 rtt_port=args.rtt_port, 164 tui=args.tui, tool_opt=args.tool_opt) 165 166 def print_gdbserver_message(self): 167 if not self.thread_info_enabled: 168 thread_msg = '; no thread info available' 169 elif self.supports_thread_info: 170 thread_msg = '; thread info enabled' 171 else: 172 thread_msg = '; update J-Link software for thread info' 173 self.logger.info('J-Link GDB server running on port ' 174 f'{self.gdb_port}{thread_msg}') 175 176 def print_rttserver_message(self): 177 self.logger.info(f'J-Link RTT server running on port {self.rtt_port}') 178 179 @property 180 def jlink_version(self): 181 # Get the J-Link version as a (major, minor, rev) tuple of integers. 182 # 183 # J-Link's command line tools provide neither a standalone 184 # "--version" nor help output that contains the version. Hack 185 # around this deficiency by using the third-party pylink library 186 # to load the shared library distributed with the tools, which 187 # provides an API call for getting the version. 188 if not hasattr(self, '_jlink_version'): 189 # pylink 0.14.0/0.14.1 exposes JLink SDK DLL (libjlinkarm) in 190 # JLINK_SDK_STARTS_WITH, while other versions use JLINK_SDK_NAME 191 if pylink.__version__ in ('0.14.0', '0.14.1'): 192 sdk = Library.JLINK_SDK_STARTS_WITH 193 else: 194 sdk = Library.JLINK_SDK_NAME 195 196 plat = sys.platform 197 if plat.startswith('win32'): 198 libname = Library.get_appropriate_windows_sdk_name() + '.dll' 199 elif plat.startswith('linux'): 200 libname = sdk + '.so' 201 elif plat.startswith('darwin'): 202 libname = sdk + '.dylib' 203 else: 204 self.logger.warning(f'unknown platform {plat}; assuming UNIX') 205 libname = sdk + '.so' 206 207 lib = Library(dllpath=os.fspath(Path(self.commander).parent / 208 libname)) 209 version = int(lib.dll().JLINKARM_GetDLLVersion()) 210 self.logger.debug('JLINKARM_GetDLLVersion()=%s', version) 211 # The return value is an int with 2 decimal digits per 212 # version subfield. 213 self._jlink_version = (version // 10000, 214 (version // 100) % 100, 215 version % 100) 216 217 return self._jlink_version 218 219 @property 220 def jlink_version_str(self): 221 # Converts the numeric revision tuple to something human-readable. 222 if not hasattr(self, '_jlink_version_str'): 223 major, minor, rev = self.jlink_version 224 rev_str = chr(ord('a') + rev - 1) if rev else '' 225 self._jlink_version_str = f'{major}.{minor:02}{rev_str}' 226 return self._jlink_version_str 227 228 @property 229 def supports_nogui(self): 230 # -nogui was introduced in J-Link Commander v6.80 231 return self.jlink_version >= (6, 80, 0) 232 233 @property 234 def supports_thread_info(self): 235 # RTOSPlugin_Zephyr was introduced in 7.11b 236 return self.jlink_version >= (7, 11, 2) 237 238 @property 239 def supports_loader(self): 240 return self.jlink_version >= (7, 70, 4) 241 242 def do_run(self, command, **kwargs): 243 244 if MISSING_REQUIREMENTS: 245 raise RuntimeError('one or more Python dependencies were missing; ' 246 "see the getting started guide for details on " 247 "how to fix") 248 # Convert commander to a real absolute path. We need this to 249 # be able to find the shared library that tells us what 250 # version of the tools we're using. 251 self.commander = os.fspath( 252 Path(self.require(self.commander)).resolve()) 253 self.logger.info(f'JLink version: {self.jlink_version_str}') 254 255 rtos = self.thread_info_enabled and self.supports_thread_info 256 plugin_dir = os.fspath(Path(self.commander).parent / 'GDBServer' / 257 'RTOSPlugin_Zephyr') 258 big_endian = self.build_conf.getboolean('CONFIG_BIG_ENDIAN') 259 260 server_cmd = ( 261 [self.gdbserver] 262 + [ 263 '-select', 264 ('ip' if (is_ip(self.dev_id) or is_tunnel(self.dev_id)) else 'usb') 265 + (f'={self.dev_id}' if self.dev_id else ''), 266 ] 267 + ['-port', str(self.gdb_port)] 268 + ['-if', self.iface] 269 + ['-speed', self.speed] 270 + ['-device', self.device] 271 + ['-silent'] 272 + ['-endian' 'big' if big_endian else 'little'] 273 + ['-singlerun'] 274 + (['-nogui'] if self.supports_nogui else []) 275 + (['-rtos', plugin_dir] if rtos else []) 276 + ['-rtttelnetport', str(self.rtt_port)] 277 + self.tool_opt 278 ) 279 280 if command == 'flash': 281 self.flash(**kwargs) 282 elif command == 'debugserver': 283 if self.gdb_host: 284 raise ValueError('Cannot run debugserver with --gdb-host') 285 self.require(self.gdbserver) 286 self.print_gdbserver_message() 287 self.check_call(server_cmd) 288 elif command == 'rtt': 289 self.print_gdbserver_message() 290 self.print_rttserver_message() 291 server_cmd += ['-nohalt'] 292 server_proc = self.popen_ignore_int(server_cmd) 293 try: 294 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 295 # wait for the port to be open 296 while server_proc.poll() is None: 297 try: 298 sock.connect(('localhost', self.rtt_port)) 299 break 300 except ConnectionRefusedError: 301 time.sleep(0.1) 302 sock.shutdown(socket.SHUT_RDWR) 303 time.sleep(0.1) 304 self.run_telnet_client('localhost', self.rtt_port) 305 except Exception as e: 306 self.logger.error(e) 307 finally: 308 server_proc.terminate() 309 server_proc.wait() 310 else: 311 if self.gdb_cmd is None: 312 raise ValueError('Cannot debug; gdb is missing') 313 if self.file is not None: 314 if self.file_type != FileType.ELF: 315 raise ValueError('Cannot debug; elf file required') 316 elf_name = self.file 317 elif self.elf_name is None: 318 raise ValueError('Cannot debug; elf is missing') 319 else: 320 elf_name = self.elf_name 321 client_cmd = (self.gdb_cmd + 322 self.tui_arg + 323 [elf_name] + 324 ['-ex', f'target remote {self.gdb_host}:{self.gdb_port}']) 325 if command == 'debug': 326 client_cmd += ['-ex', 'monitor halt', 327 '-ex', 'monitor reset', 328 '-ex', 'load'] 329 if self.reset: 330 client_cmd += ['-ex', 'monitor reset'] 331 if not self.gdb_host: 332 self.require(self.gdbserver) 333 self.print_gdbserver_message() 334 self.run_server_and_client(server_cmd, client_cmd) 335 else: 336 self.run_client(client_cmd) 337 338 def get_default_flash_commands(self): 339 lines = [ 340 'ExitOnError 1', # Treat any command-error as fatal 341 'r', # Reset and halt the target 342 'BE' if self.build_conf.getboolean('CONFIG_BIG_ENDIAN') else 'LE' 343 ] 344 345 if self.erase: 346 lines.append('erase') # Erase all flash sectors 347 348 # Get the build artifact to flash 349 if self.file is not None: 350 # use file provided by the user 351 if not os.path.isfile(self.file): 352 err = 'Cannot flash; file ({}) not found' 353 raise ValueError(err.format(self.file)) 354 355 flash_file = self.file 356 357 if self.file_type == FileType.HEX: 358 flash_cmd = f'loadfile "{self.file}"' 359 elif self.file_type == FileType.BIN: 360 if self.dt_flash: 361 flash_addr = self.flash_address_from_build_conf(self.build_conf) 362 else: 363 flash_addr = 0 364 flash_cmd = f'loadfile "{self.file}" 0x{flash_addr:x}' 365 else: 366 err = 'Cannot flash; jlink runner only supports hex and bin files' 367 raise ValueError(err) 368 369 else: 370 # Use hex, bin or elf file provided by the buildsystem. 371 # Preferring .hex over .bin and .elf 372 if self.hex_name is not None and os.path.isfile(self.hex_name): 373 flash_file = self.hex_name 374 flash_cmd = f'loadfile "{self.hex_name}"' 375 # Preferring .bin over .elf 376 elif self.bin_name is not None and os.path.isfile(self.bin_name): 377 if self.dt_flash: 378 flash_addr = self.flash_address_from_build_conf(self.build_conf) 379 else: 380 flash_addr = 0 381 flash_file = self.bin_name 382 flash_cmd = f'loadfile "{self.bin_name}" 0x{flash_addr:x}' 383 elif self.elf_name is not None and os.path.isfile(self.elf_name): 384 flash_file = self.elf_name 385 flash_cmd = f'loadfile "{self.elf_name}"' 386 else: 387 err = 'Cannot flash; no hex ({}), bin ({}) or elf ({}) files found.' 388 raise ValueError(err.format(self.hex_name, self.bin_name, self.elf_name)) 389 390 # Flash the selected build artifact 391 lines.append(flash_cmd) 392 393 if self.reset: 394 lines.append('r') # Reset and halt the target 395 396 lines.append('g') # Start the CPU 397 398 # Reset the Debug Port CTRL/STAT register 399 # Under normal operation this is done automatically, but if other 400 # JLink tools are running, it is not performed. 401 # The J-Link scripting layer chains commands, meaning that writes are 402 # not actually performed until after the next operation. After writing 403 # the register, read it back to perform this flushing. 404 lines.append('writeDP 1 0') 405 lines.append('readDP 1') 406 407 lines.append('q') # Close the connection and quit 408 409 self.logger.debug('JLink commander script:\n' + 410 '\n'.join(lines)) 411 return flash_file, lines 412 413 def run_flash_cmd(self, fname, flash_file, **kwargs): 414 loader_details = "" 415 if self.supports_loader and self.loader: 416 loader_details = "?" + self.loader 417 418 cmd = ( 419 [self.commander] 420 + ( 421 ['-IP', f'{self.dev_id}'] 422 if (is_ip(self.dev_id) or is_tunnel(self.dev_id)) 423 else (['-USB', f'{self.dev_id}'] if self.dev_id else []) 424 ) 425 + (['-nogui', '1'] if self.supports_nogui else []) 426 + ['-if', self.iface] 427 + ['-speed', self.speed] 428 + ['-device', self.device + loader_details] 429 + ['-CommanderScript', fname] 430 + (['-nogui', '1'] if self.supports_nogui else []) 431 + self.tool_opt 432 ) 433 434 if flash_file: 435 self.logger.info(f'Flashing file: {flash_file}') 436 kwargs = {} 437 if not self.logger.isEnabledFor(logging.DEBUG): 438 kwargs['stdout'] = subprocess.DEVNULL 439 self.check_call(cmd, **kwargs) 440 441 def flash(self, **kwargs): 442 fname = self.flash_script 443 if fname is None: 444 # Don't use NamedTemporaryFile: the resulting file can't be 445 # opened again on Windows. 446 with tempfile.TemporaryDirectory(suffix='jlink') as d: 447 flash_file, lines = self.get_default_flash_commands() 448 fname = os.path.join(d, 'runner.jlink') 449 with open(fname, 'wb') as f: 450 f.writelines(bytes(line + '\n', 'utf-8') for line in lines) 451 452 self.run_flash_cmd(fname, flash_file, **kwargs) 453 else: 454 self.run_flash_cmd(fname, None, **kwargs) 455