1# Copyright (c) 2017 Linaro Limited. 2# Copyright (c) 2024 Tenstorrent AI ULC 3# 4# SPDX-License-Identifier: Apache-2.0 5# 6# pylint: disable=duplicate-code 7 8'''Runner for openocd.''' 9 10import re 11import subprocess 12from os import name as os_name 13from os import path 14from pathlib import Path 15 16from zephyr_ext_common import ZEPHYR_BASE 17 18if os_name != "nt": 19 import sys 20 import termios 21 22try: # noqa SIM105 23 from elftools.elf.elffile import ELFFile 24except ImportError: 25 pass 26 27from runners.core import RunnerCaps, ZephyrBinaryRunner 28 29DEFAULT_OPENOCD_TCL_PORT = 6333 30DEFAULT_OPENOCD_TELNET_PORT = 4444 31DEFAULT_OPENOCD_GDB_PORT = 3333 32DEFAULT_OPENOCD_RTT_PORT = 5555 33DEFAULT_OPENOCD_RESET_HALT_CMD = 'reset init' 34DEFAULT_OPENOCD_TARGET_HANDLE = "_TARGETNAME" 35 36def to_num(number): 37 dev_match = re.search(r"^\d*\+dev", number) 38 dev_version = dev_match is not None 39 40 num_match = re.search(r"^\d*", number) 41 num = int(num_match.group(0)) 42 43 if dev_version: 44 num += 1 45 46 return num 47 48class OpenOcdBinaryRunner(ZephyrBinaryRunner): 49 '''Runner front-end for openocd.''' 50 51 def __init__(self, cfg, pre_init=None, reset_halt_cmd=DEFAULT_OPENOCD_RESET_HALT_CMD, 52 pre_load=None, erase_cmd=None, load_cmd=None, verify_cmd=None, 53 post_verify=None, do_verify=False, do_verify_only=False, do_erase=False, 54 tui=None, config=None, serial=None, use_elf=None, 55 no_halt=False, no_init=False, no_targets=False, 56 tcl_port=DEFAULT_OPENOCD_TCL_PORT, 57 telnet_port=DEFAULT_OPENOCD_TELNET_PORT, 58 gdb_port=DEFAULT_OPENOCD_GDB_PORT, 59 gdb_client_port=DEFAULT_OPENOCD_GDB_PORT, 60 gdb_init=None, no_load=False, 61 target_handle=DEFAULT_OPENOCD_TARGET_HANDLE, 62 rtt_port=DEFAULT_OPENOCD_RTT_PORT, rtt_server=False): 63 super().__init__(cfg) 64 65 if not path.exists(cfg.board_dir): 66 # try to find the board support in-tree 67 cfg_board_path = path.normpath(cfg.board_dir) 68 _temp_path = cfg_board_path.split("boards/")[1] 69 support = path.join(ZEPHYR_BASE, "boards", _temp_path, 'support') 70 else: 71 support = path.join(cfg.board_dir, 'support') 72 73 74 if not config: 75 default = path.join(support, 'openocd.cfg') 76 if path.exists(default): 77 config = [default] 78 self.openocd_config = config 79 80 search_args = [] 81 if path.exists(support): 82 search_args.append('-s') 83 search_args.append(support) 84 85 if self.openocd_config is not None: 86 for i in self.openocd_config: 87 if path.exists(i) and not path.samefile(path.dirname(i), support): 88 search_args.append('-s') 89 search_args.append(path.dirname(i)) 90 91 if cfg.openocd_search is not None: 92 for p in cfg.openocd_search: 93 search_args.extend(['-s', p]) 94 self.openocd_cmd = [cfg.openocd or 'openocd'] + search_args 95 # openocd doesn't cope with Windows path names, so convert 96 # them to POSIX style just to be sure. 97 self.elf_name = Path(cfg.elf_file).as_posix() if cfg.elf_file else None 98 self.pre_init = pre_init or [] 99 self.reset_halt_cmd = reset_halt_cmd 100 self.pre_load = pre_load or [] 101 self.erase_cmd = erase_cmd 102 self.load_cmd = load_cmd 103 self.verify_cmd = verify_cmd 104 self.post_verify = post_verify or [] 105 self.do_verify = do_verify or False 106 self.do_verify_only = do_verify_only or False 107 self.do_erase = do_erase or False 108 self.tcl_port = tcl_port 109 self.telnet_port = telnet_port 110 self.gdb_port = gdb_port 111 self.gdb_client_port = gdb_client_port 112 self.gdb_cmd = [cfg.gdb] if cfg.gdb else None 113 self.tui_arg = ['-tui'] if tui else [] 114 self.halt_arg = [] if no_halt else ['-c halt'] 115 self.init_arg = [] if no_init else ['-c init'] 116 self.targets_arg = [] if no_targets else ['-c targets'] 117 self.serial = ['-c set _ZEPHYR_BOARD_SERIAL ' + serial] if serial else [] 118 self.use_elf = use_elf 119 self.gdb_init = gdb_init 120 self.load_arg = [] if no_load else ['-ex', 'load'] 121 self.target_handle = target_handle 122 self.rtt_port = rtt_port 123 self.rtt_server = rtt_server 124 125 @classmethod 126 def name(cls): 127 return 'openocd' 128 129 @classmethod 130 def capabilities(cls): 131 return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach', 'rtt'}, 132 rtt=True, erase=True) 133 134 @classmethod 135 def do_add_parser(cls, parser): 136 parser.add_argument('--config', action='append', 137 help='''if given, override default config file; 138 may be given multiple times''') 139 parser.add_argument('--serial', default="", 140 help='''if given, selects FTDI instance by its serial number, 141 defaults to empty''') 142 parser.add_argument('--use-elf', default=False, action='store_true', 143 help='if given, Elf file will be used for loading instead of HEX image') 144 # Options for flashing: 145 parser.add_argument('--cmd-pre-init', action='append', 146 help='''Command to run before calling init; 147 may be given multiple times''') 148 parser.add_argument('--cmd-reset-halt', default=DEFAULT_OPENOCD_RESET_HALT_CMD, 149 help=f'''Command to run for resetting and halting the target, 150 defaults to "{DEFAULT_OPENOCD_RESET_HALT_CMD}"''') 151 parser.add_argument('--cmd-pre-load', action='append', 152 help='''Command to run before flashing; 153 may be given multiple times''') 154 parser.add_argument('--cmd-erase', action='append', 155 help='''Command to erase device; may be given multiple times''') 156 parser.add_argument('--cmd-load', 157 help='''Command to load/flash binary 158 (required when flashing)''') 159 parser.add_argument('--cmd-verify', 160 help='''Command to verify flashed binary''') 161 parser.add_argument('--cmd-post-verify', action='append', 162 help='''Command to run after verification; 163 may be given multiple times''') 164 parser.add_argument('--verify', action='store_true', 165 help='if given, verify after flash') 166 parser.add_argument('--verify-only', action='store_true', 167 help='if given, do verify and verify only. No flashing') 168 169 # Options for debugging: 170 parser.add_argument('--tui', default=False, action='store_true', 171 help='if given, GDB uses -tui') 172 parser.add_argument('--tcl-port', default=DEFAULT_OPENOCD_TCL_PORT, 173 help='openocd TCL port, defaults to 6333') 174 parser.add_argument('--telnet-port', 175 default=DEFAULT_OPENOCD_TELNET_PORT, 176 help='openocd telnet port, defaults to 4444') 177 parser.add_argument('--gdb-port', default=DEFAULT_OPENOCD_GDB_PORT, 178 help='openocd gdb port, defaults to 3333') 179 parser.add_argument('--gdb-client-port', default=DEFAULT_OPENOCD_GDB_PORT, 180 help='''openocd gdb client port if multiple ports come 181 up, defaults to 3333''') 182 parser.add_argument('--gdb-init', action='append', 183 help='if given, add GDB init commands') 184 parser.add_argument('--no-halt', action='store_true', 185 help='if given, no halt issued in gdb server cmd') 186 parser.add_argument('--no-init', action='store_true', 187 help='if given, no init issued in gdb server cmd') 188 parser.add_argument('--no-targets', action='store_true', 189 help='if given, no target issued in gdb server cmd') 190 parser.add_argument('--no-load', action='store_true', 191 help='if given, no load issued in gdb server cmd') 192 parser.add_argument('--target-handle', default=DEFAULT_OPENOCD_TARGET_HANDLE, 193 help=f'''Internal handle used in openocd targets cfg 194 files, defaults to "{DEFAULT_OPENOCD_TARGET_HANDLE}". 195 ''') 196 parser.add_argument('--rtt-port', default=DEFAULT_OPENOCD_RTT_PORT, 197 help='openocd rtt port, defaults to 5555') 198 parser.add_argument('--rtt-server', default=False, action='store_true', 199 help='''start the RTT server while debugging. 200 To view the RTT log, connect to the rtt port using 201 a command like telnet.''') 202 203 204 @classmethod 205 def do_create(cls, cfg, args): 206 return OpenOcdBinaryRunner( 207 cfg, 208 pre_init=args.cmd_pre_init, reset_halt_cmd=args.cmd_reset_halt, 209 pre_load=args.cmd_pre_load, erase_cmd=args.cmd_erase, load_cmd=args.cmd_load, 210 verify_cmd=args.cmd_verify, post_verify=args.cmd_post_verify, 211 do_verify=args.verify, do_verify_only=args.verify_only, do_erase=args.erase, 212 tui=args.tui, config=args.config, serial=args.serial, 213 use_elf=args.use_elf, no_halt=args.no_halt, no_init=args.no_init, 214 no_targets=args.no_targets, tcl_port=args.tcl_port, 215 telnet_port=args.telnet_port, gdb_port=args.gdb_port, 216 gdb_client_port=args.gdb_client_port, gdb_init=args.gdb_init, 217 no_load=args.no_load, target_handle=args.target_handle, 218 rtt_port=args.rtt_port, rtt_server=args.rtt_server) 219 220 def print_gdbserver_message(self): 221 if not self.thread_info_enabled: 222 thread_msg = '; no thread info available' 223 elif self.supports_thread_info(): 224 thread_msg = '; thread info enabled' 225 else: 226 thread_msg = '; update OpenOCD software for thread info' 227 self.logger.info('OpenOCD GDB server running on port ' 228 f'{self.gdb_port}{thread_msg}') 229 230 def print_rttserver_message(self): 231 self.logger.info(f'OpenOCD RTT server running on port {self.rtt_port}') 232 233 def read_version(self): 234 self.require(self.openocd_cmd[0]) 235 236 # OpenOCD prints in stderr, need redirect to get output 237 out = self.check_output([self.openocd_cmd[0], '--version'], 238 stderr=subprocess.STDOUT).decode() 239 240 # Account for version info format of ADI fork of OpenOCD as well 241 version_match = re.search(r"Open On-Chip Debugger.* v?(\d+.\d+.\d+)", out) 242 version = version_match.group(1).split('.') 243 244 return [to_num(i) for i in version] 245 246 def supports_thread_info(self): 247 # Zephyr rtos was introduced after 0.11.0 248 (major, minor, rev) = self.read_version() 249 return (major, minor, rev) > (0, 11, 0) 250 251 def do_run(self, command, **kwargs): 252 self.require(self.openocd_cmd[0]) 253 if globals().get('ELFFile') is None: 254 raise RuntimeError( 255 'elftools missing; please "pip3 install elftools"') 256 257 self.cfg_cmd = [] 258 if self.openocd_config is not None: 259 for i in self.openocd_config: 260 self.cfg_cmd.append('-f') 261 self.cfg_cmd.append(i) 262 263 if command == 'flash' and self.use_elf: 264 self.do_flash_elf(**kwargs) 265 elif command == 'flash': 266 self.do_flash(**kwargs) 267 elif command in ('attach', 'debug', 'rtt'): 268 self.do_attach_debug_rtt(command, **kwargs) 269 elif command == 'load': 270 self.do_load(**kwargs) 271 else: 272 self.do_debugserver(**kwargs) 273 274 def do_flash(self, **kwargs): 275 self.ensure_output('hex') 276 if self.load_cmd is None: 277 raise ValueError('Cannot flash; load command is missing') 278 if self.verify_cmd is None: 279 raise ValueError('Cannot flash; verify command is missing') 280 281 # openocd doesn't cope with Windows path names, so convert 282 # them to POSIX style just to be sure. 283 hex_name = Path(self.cfg.hex_file).as_posix() 284 285 self.logger.info(f'Flashing file: {hex_name}') 286 287 pre_init_cmd = [] 288 pre_load_cmd = [] 289 post_verify_cmd = [] 290 for i in self.pre_init: 291 pre_init_cmd.append("-c") 292 pre_init_cmd.append(i) 293 294 for i in self.pre_load: 295 pre_load_cmd.append("-c") 296 pre_load_cmd.append(i) 297 298 for i in self.post_verify: 299 post_verify_cmd.append("-c") 300 post_verify_cmd.append(i) 301 302 load_image = [] 303 if not self.do_verify_only: 304 # Halt target 305 load_image = ['-c', self.reset_halt_cmd] 306 # Perform any erase operations 307 if self.do_erase: 308 if self.erase_cmd is None: 309 self.logger.error('--erase not supported for target without --cmd-erase') 310 return 311 for erase_cmd in self.erase_cmd: 312 load_image += ["-c", erase_cmd] 313 # Trim the "erase" from "flash write_image erase" since a mass erase is already done 314 if self.load_cmd.endswith(' erase'): 315 self.load_cmd = self.load_cmd[:-6] 316 # Load image 317 load_image +=['-c', self.load_cmd + ' ' + hex_name] 318 319 verify_image = [] 320 if self.do_verify or self.do_verify_only: 321 verify_image = ['-c', self.reset_halt_cmd, 322 '-c', self.verify_cmd + ' ' + hex_name] 323 324 cmd = (self.openocd_cmd + self.serial + self.cfg_cmd + 325 pre_init_cmd + self.init_arg + self.targets_arg + 326 pre_load_cmd + load_image + 327 verify_image + 328 post_verify_cmd + 329 ['-c', 'reset run', 330 '-c', 'shutdown']) 331 self.check_call(cmd) 332 333 def do_flash_elf(self, **kwargs): 334 if self.elf_name is None: 335 raise ValueError('Cannot debug; no .elf specified') 336 337 # Extract entry point address from Elf to use it later with 338 # "resume" command of OpenOCD. 339 with open(self.elf_name, 'rb') as f: 340 ep_addr = f"0x{ELFFile(f).header['e_entry']:016x}" 341 342 pre_init_cmd = [] 343 for i in self.pre_init: 344 pre_init_cmd.append("-c") 345 pre_init_cmd.append(i) 346 347 pre_load_cmd = [] 348 load_image = [] 349 if not self.do_verify_only: 350 for i in self.pre_load: 351 pre_load_cmd.append("-c") 352 pre_load_cmd.append(i) 353 load_image = ['-c', 'load_image ' + self.elf_name] 354 355 verify_image = [] 356 post_verify_cmd = [] 357 if self.do_verify or self.do_verify_only: 358 verify_image = ['-c', 'verify_image ' + self.elf_name] 359 for i in self.post_verify: 360 post_verify_cmd.append("-c") 361 post_verify_cmd.append(i) 362 363 prologue = ['-c', 'resume ' + ep_addr, 364 '-c', 'shutdown'] 365 366 cmd = (self.openocd_cmd + self.serial + self.cfg_cmd + 367 pre_init_cmd + self.init_arg + self.targets_arg + 368 pre_load_cmd + ['-c', self.reset_halt_cmd] + 369 load_image + 370 verify_image + post_verify_cmd + 371 prologue) 372 373 self.check_call(cmd) 374 375 def do_attach_debug_rtt(self, command, **kwargs): 376 if self.gdb_cmd is None: 377 raise ValueError('Cannot debug; no gdb specified') 378 if self.elf_name is None: 379 raise ValueError('Cannot debug; no .elf specified') 380 381 pre_init_cmd = [] 382 for i in self.pre_init: 383 pre_init_cmd.append("-c") 384 pre_init_cmd.append(i) 385 386 if self.thread_info_enabled and self.supports_thread_info(): 387 pre_init_cmd.append("-c") 388 rtos_command = f'${self.target_handle} configure -rtos Zephyr' 389 pre_init_cmd.append(rtos_command) 390 391 server_cmd = (self.openocd_cmd + self.serial + self.cfg_cmd + 392 ['-c', f'tcl_port {self.tcl_port}', 393 '-c', f'telnet_port {self.telnet_port}', 394 '-c', f'gdb_port {self.gdb_port}'] + 395 pre_init_cmd + self.init_arg + self.targets_arg + 396 self.halt_arg) 397 398 if self.rtt_server and command != 'rtt': 399 rtt_address = self.get_rtt_address() 400 if rtt_address is None: 401 raise ValueError("RTT Control block not found") 402 403 server_cmd = ( 404 server_cmd 405 + ['-c', f'rtt setup 0x{rtt_address:x} 0x10 "SEGGER RTT"'] 406 + ['-c', 'rtt start'] 407 + ['-c', f'rtt server start {self.rtt_port} 0'] 408 ) 409 410 gdb_cmd = (self.gdb_cmd + self.tui_arg + 411 ['-ex', f'target extended-remote :{self.gdb_client_port}', 412 self.elf_name]) 413 if command == 'debug': 414 gdb_cmd.extend(self.load_arg) 415 if self.gdb_init is not None: 416 for i in self.gdb_init: 417 gdb_cmd.append("-ex") 418 gdb_cmd.append(i) 419 if command == 'rtt': 420 rtt_address = self.get_rtt_address() 421 if rtt_address is None: 422 raise ValueError("RTT Control block not found") 423 424 # cannot prompt the user to press return for automation purposes 425 gdb_cmd.extend(['-ex', 'set pagination off']) 426 # start the internal openocd rtt service via gdb monitor commands 427 gdb_cmd.extend( 428 ['-ex', f'monitor rtt setup 0x{rtt_address:x} 0x10 "SEGGER RTT"']) 429 gdb_cmd.extend(['-ex', 'monitor reset run']) 430 gdb_cmd.extend(['-ex', 'monitor rtt start']) 431 gdb_cmd.extend( 432 ['-ex', f'monitor rtt server start {self.rtt_port} 0']) 433 # detach from the target and quit the gdb client session 434 gdb_cmd.extend(['-ex', 'detach', '-ex', 'quit']) 435 436 self.require(gdb_cmd[0]) 437 self.print_gdbserver_message() 438 439 if command in ('attach', 'debug'): 440 server_proc = self.popen_ignore_int(server_cmd, stderr=subprocess.DEVNULL) 441 try: 442 self.run_client(gdb_cmd) 443 finally: 444 server_proc.terminate() 445 server_proc.wait() 446 elif command == 'rtt': 447 self.print_rttserver_message() 448 server_proc = self.popen_ignore_int(server_cmd) 449 450 if os_name != 'nt': 451 # Save the terminal settings 452 fd = sys.stdin.fileno() 453 new_term = termios.tcgetattr(fd) 454 old_term = termios.tcgetattr(fd) 455 456 # New terminal setting unbuffered 457 new_term[3] = new_term[3] & ~termios.ICANON & ~termios.ECHO 458 termios.tcsetattr(fd, termios.TCSAFLUSH, new_term) 459 else: 460 fd = None 461 old_term = None 462 463 try: 464 # run the binary with gdb, set up the rtt server (runs to completion) 465 subprocess.run(gdb_cmd) 466 # run the rtt client in the foreground 467 self.run_telnet_client('localhost', self.rtt_port) 468 finally: 469 if old_term is not None and fd is not None: 470 termios.tcsetattr(fd, termios.TCSAFLUSH, old_term) 471 472 server_proc.terminate() 473 server_proc.wait() 474 475 def do_debugserver(self, **kwargs): 476 pre_init_cmd = [] 477 for i in self.pre_init: 478 pre_init_cmd.append("-c") 479 pre_init_cmd.append(i) 480 481 if self.thread_info_enabled and self.supports_thread_info(): 482 pre_init_cmd.append("-c") 483 rtos_command = f'${self.target_handle} configure -rtos Zephyr' 484 pre_init_cmd.append(rtos_command) 485 486 cmd = (self.openocd_cmd + self.cfg_cmd + 487 ['-c', f'tcl_port {self.tcl_port}', 488 '-c', f'telnet_port {self.telnet_port}', 489 '-c', f'gdb_port {self.gdb_port}'] + 490 pre_init_cmd + self.init_arg + self.targets_arg + 491 ['-c', self.reset_halt_cmd]) 492 493 if self.rtt_server: 494 rtt_address = self.get_rtt_address() 495 if rtt_address is None: 496 raise ValueError("RTT Control block not found") 497 498 cmd = ( 499 cmd 500 + ['-c', f'rtt setup 0x{rtt_address:x} 0x10 "SEGGER RTT"'] 501 + ['-c', 'rtt start'] 502 + ['-c', f'rtt server start {self.rtt_port} 0'] 503 ) 504 505 self.print_gdbserver_message() 506 self.check_call(cmd) 507