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 re 12import shlex 13import socket 14import subprocess 15import sys 16import tempfile 17import time 18from pathlib import Path 19 20from runners.core import FileType, RunnerCaps, ZephyrBinaryRunner 21 22try: 23 import pylink 24 from pylink.library import Library 25 MISSING_REQUIREMENTS = False 26except ImportError: 27 MISSING_REQUIREMENTS = True 28 29# Populated in do_add_parser() 30DEFAULT_JLINK_EXE = None 31DEFAULT_JLINK_GDB_PORT = 2331 32DEFAULT_JLINK_RTT_PORT = 19021 33 34def is_ip(ip): 35 if not ip: 36 return False 37 try: 38 ipaddress.ip_address(ip.split(':')[0]) 39 except ValueError: 40 return False 41 return True 42 43def is_tunnel(tunnel): 44 return tunnel.startswith("tunnel:") if tunnel else False 45 46class ToggleAction(argparse.Action): 47 48 def __call__(self, parser, args, ignored, option): 49 setattr(args, self.dest, not option.startswith('--no-')) 50 51class JLinkBinaryRunner(ZephyrBinaryRunner): 52 '''Runner front-end for the J-Link GDB server.''' 53 54 def __init__(self, cfg, device, dev_id=None, 55 commander=DEFAULT_JLINK_EXE, 56 dt_flash=True, erase=True, reset=False, 57 iface='swd', speed='auto', flash_script = None, 58 loader=None, flash_sram=False, 59 gdbserver='JLinkGDBServer', 60 gdb_host='', 61 gdb_port=DEFAULT_JLINK_GDB_PORT, 62 rtt_port=DEFAULT_JLINK_RTT_PORT, 63 tui=False, tool_opt=None, dev_id_type=None): 64 super().__init__(cfg) 65 self.file = cfg.file 66 self.file_type = cfg.file_type 67 self.hex_name = cfg.hex_file 68 self.bin_name = cfg.bin_file 69 self.elf_name = cfg.elf_file 70 self.mot_name = cfg.mot_file 71 self.gdb_cmd = [cfg.gdb] if cfg.gdb else None 72 self.device = device 73 self.dev_id = dev_id 74 self.commander = commander 75 self.flash_script = flash_script 76 self.dt_flash = dt_flash 77 self.flash_sram = flash_sram 78 self.erase = erase 79 self.reset = reset 80 self.gdbserver = gdbserver 81 self.iface = iface 82 self.speed = speed 83 self.gdb_host = gdb_host 84 self.gdb_port = gdb_port 85 self.tui_arg = ['-tui'] if tui else [] 86 self.loader = loader 87 self.rtt_port = rtt_port 88 self.dev_id_type = dev_id_type 89 90 self.tool_opt = [] 91 if tool_opt is not None: 92 for opts in [shlex.split(opt) for opt in tool_opt]: 93 self.tool_opt += opts 94 95 @staticmethod 96 def _detect_dev_id_type(dev_id): 97 """Detect device type based on dev_id pattern.""" 98 if not dev_id: 99 return None 100 101 # Check if dev_id is numeric (serialno) 102 if re.match(r'^[0-9]+$', dev_id): 103 return 'serialno' 104 105 # Check if dev_id is an existing file (tty) 106 if os.path.exists(dev_id): 107 return 'tty' 108 109 # Check if dev_id is a valid IPv4 110 if is_ip(dev_id): 111 return 'ip' 112 113 # Check if dev_id is a tunnel 114 if is_tunnel(dev_id): 115 return 'tunnel' 116 117 # No match found 118 return None 119 120 @classmethod 121 def name(cls): 122 return 'jlink' 123 124 @classmethod 125 def capabilities(cls): 126 return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach', 'rtt'}, 127 dev_id=True, flash_addr=True, erase=True, reset=True, 128 tool_opt=True, file=True, rtt=True) 129 130 @classmethod 131 def dev_id_help(cls) -> str: 132 return '''Device identifier. Can be either a serial number (for a USB connection), a path 133 to a tty device, an IP address or a tunnel address. User can enforce the type of 134 the identifier with --dev-id-type. 135 If not specified, the first USB device detected is used. ''' 136 137 @classmethod 138 def tool_opt_help(cls) -> str: 139 return "Additional options for JLink Commander, e.g. '-autoconnect 1'" 140 141 @staticmethod 142 def default_jlink(): 143 global DEFAULT_JLINK_EXE 144 145 if sys.platform == 'win32': 146 # JLink.exe can collide with the JDK executable of the same name 147 # Locate the executable using the registry 148 try: 149 import winreg 150 151 # Note that when multiple JLink versions are installed on the 152 # machine this points to the one that was installed 153 # last, and not to the latest version. 154 key = winreg.OpenKeyEx( 155 winreg.HKEY_CURRENT_USER, r"Software\SEGGER\J-Link") 156 DEFAULT_JLINK_EXE = ( 157 Path(winreg.QueryValueEx(key, "InstallPath")[0]) 158 / "JLink.exe") 159 except Exception: 160 # Not found via the registry, hope that $PATH is correct 161 DEFAULT_JLINK_EXE = "JLink.exe" 162 else: 163 DEFAULT_JLINK_EXE = "JLinkExe" 164 165 @classmethod 166 def do_add_parser(cls, parser): 167 168 # Find the default JLink executable 169 cls.default_jlink() 170 171 # Required: 172 parser.add_argument('--device', required=True, help='device name') 173 174 # Optional: 175 parser.add_argument('--loader', required=False, dest='loader', 176 help='specifies a loader type') 177 parser.add_argument('--id', required=False, dest='dev_id', 178 help='obsolete synonym for -i/--dev-id') 179 parser.add_argument('--iface', default='swd', 180 help='interface to use, default is swd') 181 parser.add_argument('--speed', default='auto', 182 help='interface speed, default is autodetect') 183 parser.add_argument('--flash-script', default=None, 184 help='Custom flashing script, default is None') 185 parser.add_argument('--tui', default=False, action='store_true', 186 help='if given, GDB uses -tui') 187 parser.add_argument('--gdbserver', default='JLinkGDBServer', 188 help='GDB server, default is JLinkGDBServer') 189 parser.add_argument('--gdb-host', default='', 190 help='custom gdb host, defaults to the empty string ' 191 'and runs a gdb server') 192 parser.add_argument('--gdb-port', default=DEFAULT_JLINK_GDB_PORT, 193 help=f'pyocd gdb port, defaults to {DEFAULT_JLINK_GDB_PORT}') 194 parser.add_argument('--commander', default=DEFAULT_JLINK_EXE, 195 help=f'''J-Link Commander, default is 196 {DEFAULT_JLINK_EXE}''') 197 parser.add_argument('--reset-after-load', '--no-reset-after-load', 198 dest='reset', nargs=0, 199 action=ToggleAction, 200 help='obsolete synonym for --reset/--no-reset') 201 parser.add_argument('--rtt-client', default='JLinkRTTClient', 202 help='RTT client, default is JLinkRTTClient') 203 parser.add_argument('--rtt-port', default=DEFAULT_JLINK_RTT_PORT, 204 help=f'jlink rtt port, defaults to {DEFAULT_JLINK_RTT_PORT}') 205 parser.add_argument('--flash-sram', default=False, action='store_true', 206 help='if given, flashing the image to SRAM and ' 207 'modify PC register to be SRAM base address') 208 parser.add_argument('--dev-id-type', choices=['auto', 'serialno', 'tty', 'ip', 'tunnel'], 209 default='auto', help='Device type. "auto" (default) auto-detects ' 210 'the type, or specify explicitly') 211 212 parser.set_defaults(reset=False) 213 214 @classmethod 215 def do_create(cls, cfg, args): 216 return JLinkBinaryRunner(cfg, args.device, 217 dev_id=args.dev_id, 218 commander=args.commander, 219 dt_flash=args.dt_flash, 220 flash_sram=args.flash_sram, 221 erase=args.erase, 222 reset=args.reset, 223 iface=args.iface, speed=args.speed, 224 flash_script=args.flash_script, 225 gdbserver=args.gdbserver, 226 loader=args.loader, 227 gdb_host=args.gdb_host, 228 gdb_port=args.gdb_port, 229 rtt_port=args.rtt_port, 230 tui=args.tui, tool_opt=args.tool_opt, 231 dev_id_type=args.dev_id_type) 232 233 def print_gdbserver_message(self): 234 if not self.thread_info_enabled: 235 thread_msg = '; no thread info available' 236 elif self.supports_thread_info: 237 thread_msg = '; thread info enabled' 238 else: 239 thread_msg = '; update J-Link software for thread info' 240 self.logger.info('J-Link GDB server running on port ' 241 f'{self.gdb_port}{thread_msg}') 242 243 def print_rttserver_message(self): 244 self.logger.info(f'J-Link RTT server running on port {self.rtt_port}') 245 246 @property 247 def jlink_version(self): 248 # Get the J-Link version as a (major, minor, rev) tuple of integers. 249 # 250 # J-Link's command line tools provide neither a standalone 251 # "--version" nor help output that contains the version. Hack 252 # around this deficiency by using the third-party pylink library 253 # to load the shared library distributed with the tools, which 254 # provides an API call for getting the version. 255 if not hasattr(self, '_jlink_version'): 256 # pylink 0.14.0/0.14.1 exposes JLink SDK DLL (libjlinkarm) in 257 # JLINK_SDK_STARTS_WITH, while other versions use JLINK_SDK_NAME 258 if pylink.__version__ in ('0.14.0', '0.14.1'): 259 sdk = Library.JLINK_SDK_STARTS_WITH 260 else: 261 sdk = Library.JLINK_SDK_NAME 262 263 plat = sys.platform 264 if plat.startswith('win32'): 265 libname = Library.get_appropriate_windows_sdk_name() + '.dll' 266 elif plat.startswith('linux'): 267 libname = sdk + '.so' 268 elif plat.startswith('darwin'): 269 libname = sdk + '.dylib' 270 else: 271 self.logger.warning(f'unknown platform {plat}; assuming UNIX') 272 libname = sdk + '.so' 273 274 lib = Library(dllpath=os.fspath(Path(self.commander).parent / 275 libname)) 276 version = int(lib.dll().JLINKARM_GetDLLVersion()) 277 self.logger.debug('JLINKARM_GetDLLVersion()=%s', version) 278 # The return value is an int with 2 decimal digits per 279 # version subfield. 280 self._jlink_version = (version // 10000, 281 (version // 100) % 100, 282 version % 100) 283 284 return self._jlink_version 285 286 @property 287 def jlink_version_str(self): 288 # Converts the numeric revision tuple to something human-readable. 289 if not hasattr(self, '_jlink_version_str'): 290 major, minor, rev = self.jlink_version 291 rev_str = chr(ord('a') + rev - 1) if rev else '' 292 self._jlink_version_str = f'{major}.{minor:02}{rev_str}' 293 return self._jlink_version_str 294 295 @property 296 def supports_nogui(self): 297 # -nogui was introduced in J-Link Commander v6.80 298 return self.jlink_version >= (6, 80, 0) 299 300 @property 301 def supports_thread_info(self): 302 # RTOSPlugin_Zephyr was introduced in 7.11b 303 return self.jlink_version >= (7, 11, 2) 304 305 @property 306 def supports_loader(self): 307 return self.jlink_version >= (7, 70, 4) 308 309 def do_run(self, command, **kwargs): 310 311 if MISSING_REQUIREMENTS: 312 raise RuntimeError('one or more Python dependencies were missing; ' 313 "see the getting started guide for details on " 314 "how to fix") 315 # Convert commander to a real absolute path. We need this to 316 # be able to find the shared library that tells us what 317 # version of the tools we're using. 318 self.commander = os.fspath( 319 Path(self.require(self.commander)).resolve()) 320 self.logger.debug(f'JLink executable: {self.commander}') 321 self.logger.info(f'JLink version: {self.jlink_version_str}') 322 323 rtos = self.thread_info_enabled and self.supports_thread_info 324 plugin_dir = os.fspath(Path(self.commander).parent / 'GDBServer' / 325 'RTOSPlugin_Zephyr') 326 big_endian = self.build_conf.getboolean('CONFIG_BIG_ENDIAN') 327 328 # Determine device type for GDB server 329 if self.dev_id: 330 if self.dev_id_type == 'auto': 331 # Auto-detect device type 332 detected_type = self._detect_dev_id_type(self.dev_id) 333 else: 334 # Use manually specified device type 335 detected_type = self.dev_id_type 336 337 # Build device selection for GDB server 338 if detected_type == 'serialno': 339 device_select = f'usb={self.dev_id}' 340 elif detected_type == 'tty': 341 device_select = f'tty={self.dev_id}' 342 elif detected_type == 'ip': 343 device_select = f'ip={self.dev_id}' 344 elif detected_type == 'tunnel': 345 device_select = f'tunnel={self.dev_id}' 346 else: 347 # Fallback to legacy behavior (usb) 348 device_select = f'usb={self.dev_id}' 349 self.logger.warning(f'"{self.dev_id}" does not match any known pattern') 350 else: 351 device_select = 'usb' 352 353 server_cmd = ( 354 [self.gdbserver] 355 + ['-select', device_select] 356 + ['-port', str(self.gdb_port)] 357 + ['-if', self.iface] 358 + ['-speed', self.speed] 359 + ['-device', self.device] 360 + ['-silent'] 361 + ['-endian', 'big' if big_endian else 'little'] 362 + ['-singlerun'] 363 + (['-nogui'] if self.supports_nogui else []) 364 + (['-rtos', plugin_dir] if rtos else []) 365 + ['-rtttelnetport', str(self.rtt_port)] 366 + self.tool_opt 367 ) 368 369 if command == 'flash': 370 self.flash(**kwargs) 371 elif command == 'debugserver': 372 if self.gdb_host: 373 raise ValueError('Cannot run debugserver with --gdb-host') 374 self.require(self.gdbserver) 375 self.print_gdbserver_message() 376 self.check_call(server_cmd) 377 elif command == 'rtt': 378 self.print_gdbserver_message() 379 self.print_rttserver_message() 380 server_cmd += ['-nohalt'] 381 server_proc = self.popen_ignore_int(server_cmd) 382 try: 383 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 384 # wait for the port to be open 385 while server_proc.poll() is None: 386 try: 387 sock.connect(('localhost', self.rtt_port)) 388 break 389 except ConnectionRefusedError: 390 time.sleep(0.1) 391 self.run_telnet_client('localhost', self.rtt_port, sock) 392 except Exception as e: 393 self.logger.error(e) 394 finally: 395 server_proc.terminate() 396 server_proc.wait() 397 else: 398 if self.gdb_cmd is None: 399 raise ValueError('Cannot debug; gdb is missing') 400 if self.file is not None: 401 if self.file_type != FileType.ELF: 402 raise ValueError('Cannot debug; elf file required') 403 elf_name = self.file 404 elif self.elf_name is None: 405 raise ValueError('Cannot debug; elf is missing') 406 else: 407 elf_name = self.elf_name 408 client_cmd = (self.gdb_cmd + 409 self.tui_arg + 410 [elf_name] + 411 ['-ex', f'target remote {self.gdb_host}:{self.gdb_port}']) 412 if command == 'debug': 413 client_cmd += ['-ex', 'monitor halt', 414 '-ex', 'monitor reset', 415 '-ex', 'load'] 416 if self.reset: 417 client_cmd += ['-ex', 'monitor reset'] 418 if not self.gdb_host: 419 self.require(self.gdbserver) 420 self.print_gdbserver_message() 421 self.run_server_and_client(server_cmd, client_cmd) 422 else: 423 self.run_client(client_cmd) 424 425 def get_default_flash_commands(self): 426 lines = [ 427 'ExitOnError 1', # Treat any command-error as fatal 428 'r', # Reset and halt the target 429 'BE' if self.build_conf.getboolean('CONFIG_BIG_ENDIAN') else 'LE' 430 ] 431 432 if self.erase: 433 lines.append('erase') # Erase all flash sectors 434 435 # Get the build artifact to flash 436 if self.file is not None: 437 # use file provided by the user 438 if not os.path.isfile(self.file): 439 err = 'Cannot flash; file ({}) not found' 440 raise ValueError(err.format(self.file)) 441 442 flash_file = self.file 443 444 if self.file_type == FileType.HEX: 445 flash_cmd = f'loadfile "{self.file}"' 446 elif self.file_type == (FileType.BIN or FileType.MOT): 447 if self.flash_sram: 448 flash_addr = self.sram_address_from_build_conf(self.build_conf) 449 elif self.dt_flash: 450 flash_addr = self.flash_address_from_build_conf(self.build_conf) 451 else: 452 flash_addr = 0 453 flash_cmd = f'loadfile "{self.file}" 0x{flash_addr:x}' 454 else: 455 err = 'Cannot flash; jlink runner only supports hex and bin files' 456 raise ValueError(err) 457 458 else: 459 # Use hex, bin or elf file provided by the buildsystem. 460 # Preferring .hex over .mot, .bin and .elf 461 if self.hex_name is not None and os.path.isfile(self.hex_name): 462 flash_file = self.hex_name 463 flash_cmd = f'loadfile "{self.hex_name}"' 464 # Preferring .mot over .bin and .elf 465 elif self.mot_name is not None and os.path.isfile(self.mot_name): 466 flash_file = self.mot_name 467 flash_cmd = f'loadfile {self.mot_name}' 468 # Preferring .bin over .elf 469 elif self.bin_name is not None and os.path.isfile(self.bin_name): 470 if self.flash_sram: 471 flash_addr = self.sram_address_from_build_conf(self.build_conf) 472 elif self.dt_flash: 473 flash_addr = self.flash_address_from_build_conf(self.build_conf) 474 else: 475 flash_addr = 0 476 flash_file = self.bin_name 477 flash_cmd = f'loadfile "{self.bin_name}" 0x{flash_addr:x}' 478 elif self.elf_name is not None and os.path.isfile(self.elf_name): 479 flash_file = self.elf_name 480 flash_cmd = f'loadfile "{self.elf_name}"' 481 elif self.mot_name is not None and os.path.isfile(self.mot_name): 482 flash_file = self.mot_name 483 flash_cmd = f'loadfile {self.mot_name}' 484 else: 485 err = 'Cannot flash; no hex ({}), bin ({}) or mot ({}) files found.' 486 raise ValueError(err.format(self.hex_name, self.bin_name)) 487 488 # Flash the selected build artifact 489 lines.append(flash_cmd) 490 491 if self.reset: 492 lines.append('r') # Reset and halt the target 493 494 if self.flash_sram: 495 sram_addr = self.sram_address_from_build_conf(self.build_conf) 496 lines.append(f'WReg PC 0x{sram_addr:x}') # Change PC to start of SRAM 497 498 lines.append('g') # Start the CPU 499 500 # Reset the Debug Port CTRL/STAT register 501 # Under normal operation this is done automatically, but if other 502 # JLink tools are running, it is not performed. 503 # The J-Link scripting layer chains commands, meaning that writes are 504 # not actually performed until after the next operation. After writing 505 # the register, read it back to perform this flushing. 506 lines.append('writeDP 1 0') 507 lines.append('readDP 1') 508 509 lines.append('q') # Close the connection and quit 510 511 self.logger.debug('JLink commander script:\n' + 512 '\n'.join(lines)) 513 return flash_file, lines 514 515 def run_flash_cmd(self, fname, flash_file, **kwargs): 516 loader_details = "" 517 if self.supports_loader and self.loader: 518 loader_details = "?" + self.loader 519 520 # Determine device type for Commander 521 if self.dev_id: 522 if self.dev_id_type == 'auto': 523 # Auto-detect device type 524 detected_type = self._detect_dev_id_type(self.dev_id) 525 else: 526 # Use manually specified device type 527 detected_type = self.dev_id_type 528 529 # Build device selection for Commander 530 if detected_type == 'serialno': 531 device_args = ['-USB', f'{self.dev_id}'] 532 elif detected_type == 'tty': 533 device_args = ['-USB', f'{self.dev_id}'] # J-Link Commander uses -USB for tty 534 elif detected_type == 'ip': 535 device_args = ['-IP', f'{self.dev_id}'] 536 elif detected_type == 'tunnel': 537 device_args = ['-IP', f'{self.dev_id}'] # Treat tunnel as IP 538 else: 539 # Fallback to legacy behavior (USB) 540 device_args = ['-USB', f'{self.dev_id}'] 541 self.logger.warning(f'"{self.dev_id}" does not match any known pattern') 542 else: 543 device_args = [] 544 545 cmd = ( 546 [self.commander] 547 + device_args 548 + (['-nogui', '1'] if self.supports_nogui else []) 549 + ['-if', self.iface] 550 + ['-speed', self.speed] 551 + ['-device', self.device + loader_details] 552 + ['-CommanderScript', fname] 553 + (['-nogui', '1'] if self.supports_nogui else []) 554 + self.tool_opt 555 ) 556 557 if flash_file: 558 self.logger.info(f'Flashing file: {flash_file}') 559 kwargs = {} 560 if not self.logger.isEnabledFor(logging.DEBUG): 561 kwargs['stdout'] = subprocess.DEVNULL 562 self.check_call(cmd, **kwargs) 563 564 def flash(self, **kwargs): 565 fname = self.flash_script 566 if fname is None: 567 # Don't use NamedTemporaryFile: the resulting file can't be 568 # opened again on Windows. 569 with tempfile.TemporaryDirectory(suffix='jlink') as d: 570 flash_file, lines = self.get_default_flash_commands() 571 fname = os.path.join(d, 'runner.jlink') 572 with open(fname, 'wb') as f: 573 f.writelines(bytes(line + '\n', 'utf-8') for line in lines) 574 575 self.run_flash_cmd(fname, flash_file, **kwargs) 576 else: 577 self.run_flash_cmd(fname, None, **kwargs) 578