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