debug.py
1 #!/usr/bin/env python3 2 3 from dotenv import load_dotenv, find_dotenv 4 import argparse 5 import git 6 import platform 7 import os 8 import signal 9 import socket 10 import subprocess 11 import sys 12 import tempfile 13 import time 14 15 16 def get_project_root(): 17 git_repo = git.Repo(search_parent_directories=True) 18 return os.path.realpath(git_repo.git.rev_parse("--show-toplevel")) 19 20 21 # Don't close python script with Ctrl+C since GDB uses it 22 def sig_handler(signum, frame): 23 pass 24 25 # Try ports in range until one is open 26 # Used in case we launch multiple openocd/gdb instances at once 27 def get_free_port(port=3333, max_port=4000): 28 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 29 while port <= max_port: 30 try: 31 sock.bind(("", port)) 32 sock.close() 33 return port 34 except OSError: 35 port += 1 36 raise IOError("no free ports") 37 38 39 # Register alternate SIGINT handler above to ignore Ctrl+C 40 signal.signal(signal.SIGINT, sig_handler) 41 42 debug_scripts_path = "{}/tools/scripts/debug".format(get_project_root()) 43 44 45 # 46 # Create gdb script with memfault coredump information (gathered from .env) 47 # Since it needs to have user info to run 48 # 49 def create_coredump_gdb_script(): 50 memfault_gdb_path = os.path.join( 51 get_project_root(), 52 "src/third_party/memfault-firmware-sdk/scripts/memfault_gdb.py", 53 ) 54 55 coredump_script = """ 56 define coredump 57 source {script_path} 58 memfault login {username} {password} -o sofar-ocean -p {project} 59 memfault coredump --region (uint32_t)&_ram_start (uint32_t)&_ram_size 60 end 61 """.format( 62 script_path=memfault_gdb_path, 63 username=os.getenv("MEMFAULT_USERNAME"), 64 password=os.getenv("MEMFAULT_PASSWORD"), 65 project=os.getenv("MEMFAULT_PROJECT"), 66 ) 67 68 fd, path = tempfile.mkstemp(suffix=".gdb_cmds") 69 os.write(fd, coredump_script.encode()) 70 os.close(fd) 71 72 return path 73 74 75 def run_openocd(args, port=3333): 76 # OpenOCD config overrides 77 part_cfg = "st_nucleo_u5.cfg" 78 if args.rtos: 79 part_cfg = "st_nucleo_u5_rtos.cfg" 80 81 openocd_cmd = [ 82 "openocd", 83 "-f", 84 f"{debug_scripts_path}/stlink-mod.cfg", 85 "-f", 86 f"{debug_scripts_path}/{part_cfg}", 87 "-f", 88 f"{debug_scripts_path}/utils.cfg", 89 "-c", 90 "tcl_port disabled", 91 "-c", 92 "telnet_port disabled", 93 "-c", 94 f"gdb_port {port}", 95 "-c", 96 "init", 97 ] 98 99 if platform.system() == "Windows" and platform.architecture()[0] == "64bit": 100 # Create a STARTUPINFO structure to specify that the subprocess should be run in a hidden window 101 startupinfo = subprocess.STARTUPINFO() 102 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 103 startupinfo.wShowWindow = subprocess.SW_HIDE 104 105 return subprocess.Popen( 106 openocd_cmd, 107 # Comment out the following two lines to print openocd output to console 108 # stdout=subprocess.DEVNULL, 109 # stderr=subprocess.STDOUT, 110 startupinfo=startupinfo 111 ) 112 elif os.name == "posix": 113 # https://stackoverflow.com/questions/5045771/python-how-to-prevent-subprocesses-from-receiving-ctrl-c-control-c-sigint 114 return subprocess.Popen( 115 openocd_cmd, 116 # Comment out the following two lines to print openocd output to console 117 # stdout=subprocess.DEVNULL, 118 # stderr=subprocess.STDOUT, 119 preexec_fn=os.setpgrp, 120 ) 121 else: 122 print("Unsupported platform") 123 124 125 # 126 # Run GDB with all the required scripts 127 # 128 def run_gdb(args, port=3333): 129 if platform.system() == "Windows" and platform.architecture()[0] == "64bit": 130 gdb_cmd = ["gdb-multiarch"] 131 else: 132 gdb_cmd = ["gdb"] 133 134 for source_dir in args.dirs: 135 gdb_cmd.append(f"--directory={source_dir}") 136 137 # Create `coredump` command script 138 coredump_script_path = create_coredump_gdb_script() 139 140 # Include debug gdb commands (reset, rh, etc.) 141 gdb_cmd += [ 142 "-ex", 143 f"target remote localhost:{port}", 144 "-x", 145 f"{debug_scripts_path}/debug.gdb_cmds", 146 "-x", 147 coredump_script_path, 148 ] 149 150 # Add executable 151 gdb_cmd.append(f"{args.elf}") 152 153 if args.tui: 154 gdb_cmd.append("-tui") 155 156 subprocess.run(gdb_cmd) 157 158 # Remove temporary coredump script file 159 if coredump_script_path: 160 os.remove(coredump_script_path) 161 162 163 parser = argparse.ArgumentParser() 164 parser.add_argument("elf", help="Firmware elf file") 165 parser.add_argument("dirs", help="Source search directories", nargs="+") 166 167 parser.add_argument("--tui", action="store_true", help="Use gdb-tui") 168 parser.add_argument( 169 "--rtos", action="store_true", help="Enable RTOS support in openocd" 170 ) 171 172 args, unknownargs = parser.parse_known_args() 173 174 load_dotenv(find_dotenv()) 175 176 # Get port to use for openocd/gdb 177 port = get_free_port() 178 179 # Run openocd in the background 180 openocd = run_openocd(args, port) 181 182 # Wait for openocd to start up before trying to connect with gdb 183 time.sleep(1) 184 185 # Make sure openocd actually ran 186 openocd_rval = openocd.poll() 187 if openocd_rval is not None: 188 print(f"Error starting openocd! ({openocd_rval})") 189 sys.exit(openocd_rval) 190 191 # Run GDB to do all the things! 192 run_gdb(args, port) 193 194 # Make sure we don't leave openocd running 195 openocd.terminate()