├── src └── openocd │ ├── __init__.py │ ├── target.py │ ├── nm.py │ └── ocd.py ├── TODO.md ├── setup.py ├── README.md ├── .gitignore ├── tests └── test_case.py └── notes.md /src/openocd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | # TODO 3 | 4 | - Some way to make sure the image flashed on the target matches the loaded 5 | ELF file 6 | - Disable and enable interrupts. An interrupt could occur during a test. 7 | - How can we work around waiting on locks? We want to be able to test periphs 8 | over SPI, for example, but as it stands function calls act like ISRs. 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import setuptools 5 | 6 | if __name__ == "__main__": 7 | setuptools.setup( 8 | name="python-openocd", 9 | author="Zack Marvel", 10 | author_email="zpmarvel@gmail.com", 11 | description="Communication with OpenOCD server", 12 | url="https://github.com/zmarvel/python-openocd.git", 13 | packages=setuptools.find_packages(where="src"), 14 | package_dir={"": "src"} 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ocd-monitor 3 | 4 | Part of this project is a thin Python wrapper around some of OpenOCD's 5 | functionality, like reading/writing memory and registers. Then, given an ELF 6 | file running on a target, we can find the addresses of symbols, which lets us 7 | call functions and observe their effects. 8 | 9 | # Test cases 10 | 11 | A use case for this project is automated testing on a target. `test_case.py` 12 | provides an example using Python's unittest mostly as a test-runner. If OpenOCD 13 | is listening, you can run the example from the project root with 14 | ``` 15 | $ python3 -m unittest test_case 16 | ``` 17 | Just make sure to modify the path to match the location of the elf file which 18 | is loaded on the target. 19 | 20 | # ChibiOS notes 21 | 22 | This project is being developed with a target running ChibiOS 17.6.x. Since 23 | function calls are performed by looking up a symbol in the output of `nm` 24 | with the provided ELF file, functions that do not appear in the symbol table, 25 | such as macros, inline functions, and even because of optimizations cannot be 26 | called. I had to build ChibiOS with `make USE_LINK_GC=no USE_LTO=no`. 27 | 28 | Currently if a function on the target is called with this API, it should avoid 29 | waiting on resources that might be locked, like an ISR would do. The API also 30 | makes no attempt to disable interrupts, but it will in the future. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # IPython Notebook 67 | .ipynb_checkpoints 68 | 69 | # pyenv 70 | .python-version 71 | 72 | # celery beat schedule file 73 | celerybeat-schedule 74 | 75 | # dotenv 76 | .env 77 | 78 | # virtualenv 79 | .venv/ 80 | venv/ 81 | ENV/ 82 | 83 | # Spyder project settings 84 | .spyderproject 85 | 86 | # Rope project settings 87 | .ropeproject 88 | -------------------------------------------------------------------------------- /tests/test_case.py: -------------------------------------------------------------------------------- 1 | # To run test case, use this command: 2 | # PYTHONPATH='src' python3 -m pytest tests/test_case.py 3 | 4 | from time import sleep 5 | import unittest 6 | from openocd import nm, ocd, target 7 | 8 | class OCDTestCase(unittest.TestCase): 9 | """Example test case. Note that OpenOCD must be attached to the target; if 10 | the connection fails, an exception will be raised.""" 11 | def setUp(self): 12 | self.elf_file = "/home/zack/src/acceltag_chibios/firmware/build/ch.elf" 13 | self.symbol_table = nm.SymbolTable(self.elf_file) 14 | self.openocd = ocd.OpenOCD().__enter__() 15 | self.target = target.Target(self.openocd, self.elf_file) 16 | 17 | def tearDown(self): 18 | if self.openocd.curstate() != "running": 19 | self.openocd.resume() 20 | self.openocd.__exit__(None, None, None) 21 | 22 | def test_led_functions(self): 23 | self.openocd.reset(halt=False) 24 | sleep(1) 25 | self.openocd.halt() 26 | 27 | initial_state = self.target.get_led() 28 | self.target.toggle_led() 29 | new_state = self.target.get_led() 30 | self.assertGreaterEqual(initial_state, 0) 31 | self.assertLessEqual(initial_state, 1) 32 | self.assertGreaterEqual(new_state, 0) 33 | self.assertLessEqual(new_state, 1) 34 | if initial_state == 0: 35 | self.assertEqual(new_state, 1) 36 | elif initial_state == 1: 37 | self.assertEqual(new_state, 0) 38 | 39 | self.openocd.resume() 40 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | 2 | # Notes 3 | 4 | ## Testing a SPI peripheral 5 | 6 | Using SPI is more complicated than toggling an LED. The debugger could attach to 7 | ChibiOS in a number of states, and we could create a deadlock if we aren't 8 | careful. Also, function calls we perform with the debugger could be interrupted. 9 | We can disable interrupts, but we might not want to if we want to use DMA-driven 10 | SPI calls, for example, which rely on interrupts. 11 | 12 | A safe strategy is to reset and halt the system at the beginning of the test, 13 | then set a breakpoint somewhere the system is in a known state before resuming 14 | execution---for example, the main thread. 15 | 16 | ### Testing from the shell 17 | 18 | Another strategy is communicating with the ChibiOS shell with a UART adapter. 19 | There are some evident disadvantages: 20 | - The target must have a shell command to support a test. 21 | - The target must have a shell running. Shell support requires a few ChibiOS 22 | features which would not normally be built, such as heap-allocated threads 23 | and the mempools API, as well as the shell code itself and any commands. 24 | 25 | The main advantage is simplicity. Calling functions across a debug interface 26 | might feel clunky and requires special care. Writing tests in C to run directly 27 | on the target is quicker. Although they replicate the production environment 28 | less closely, they give us a higher chance of catching regressions than no 29 | tests at all. 30 | 31 | 32 | ## Build notes 33 | 34 | If running under ChibiOS, make sure `USE_LINK_GC` and `USE_LTO` are set to no in 35 | the Makefile. 36 | -------------------------------------------------------------------------------- /src/openocd/target.py: -------------------------------------------------------------------------------- 1 | from openocd import ocd, nm 2 | 3 | BUFSIZE = 2048 4 | 5 | 6 | class Target(): 7 | """Represents a target which OpenOCD is connected to. 8 | """ 9 | def __init__(self, openocd, elf_file): 10 | self.ocd = openocd 11 | self.symbol_table = nm.SymbolTable(elf_file) 12 | self.buffer = bytearray(BUFSIZE) 13 | 14 | def call(self, function, halt=False, resume=False): 15 | """Lookup the functions address in the provided ELF file using 16 | `self.symbol_table` and call it using OpenOCD 17 | """ 18 | addr = self.symbol_table.lookup_address("toggle_led") 19 | 20 | if halt: 21 | self.ocd.halt() 22 | val = self.ocd.call(addr) 23 | if resume: 24 | self.ocd.resume() 25 | return val 26 | 27 | def toggle_led(self): 28 | self.call("toggle_led") 29 | 30 | def get_led(self): 31 | val = self.call("get_led") 32 | return val 33 | 34 | def set_led(self, val): 35 | if val != 0 and val != 1: 36 | raise ValueError("expecting 0 or 1") 37 | self.call("set_led") 38 | 39 | 40 | if __name__ == "__main__": 41 | from time import sleep 42 | 43 | def print_led(state): 44 | if state == 0: 45 | print("LED is off") 46 | elif state == 1: 47 | print("LED is on") 48 | else: 49 | print("Saw unexpected LED state") 50 | 51 | with ocd.OpenOCD() as openocd: 52 | mon = Target(openocd, 53 | "/home/zack/src/acceltag_chibios/firmware/build/ch.elf") 54 | state = mon.get_led() 55 | print_led(state) 56 | for _ in range(10): 57 | mon.toggle_led() 58 | sleep(0.1) 59 | 60 | state = mon.get_led() 61 | print_led(state) 62 | sleep(2) 63 | mon.set_led(0) 64 | sleep(2) 65 | mon.set_led(1) 66 | sleep(2) 67 | mon.set_led(0) 68 | sleep(2) 69 | -------------------------------------------------------------------------------- /src/openocd/nm.py: -------------------------------------------------------------------------------- 1 | """Look up symbols in an ELF file. 2 | """ 3 | from collections import namedtuple 4 | import subprocess 5 | import re 6 | 7 | Symbol = namedtuple('Symbol', ['name', 'type', 'address']) 8 | 9 | 10 | symbol_regex = re.compile('^([0-9a-f]+) ([A-z]) ([A-z0-9_.\$]+)$', 11 | flags=re.MULTILINE) 12 | 13 | 14 | def list_symbols(elf_file): 15 | proc = subprocess.run(['nm', elf_file], stdout=subprocess.PIPE) 16 | text = proc.stdout.decode() 17 | return symbol_regex.findall(text) 18 | 19 | 20 | def find_symbol(nm_output, symbol_name): 21 | result = re.search('^([0-9a-f]+) ([A-z]) {}$'.format(symbol_name), 22 | nm_output, flags=re.MULTILINE) 23 | if result is None: 24 | return None 25 | else: 26 | return Symbol(name=symbol_name, type=result.group(2), 27 | address=int(result.group(1), 16)) 28 | 29 | 30 | class SymbolTable(): 31 | """On initialization, loads an ELF file to make looking up symbol addresses 32 | and types by name easy. 33 | """ 34 | def __init__(self, elf_file): 35 | self.elf_file = elf_file 36 | self._table = {} 37 | """An internal dict mapping symbol names to their address and type as 38 | a tuple (int, str). 39 | """ 40 | self._load_table() 41 | 42 | def _load_table(self): 43 | """Assumes `self.elf_file` has already been loaded and `self._table` 44 | has been initialized as an empty dict. 45 | """ 46 | for tup in list_symbols(self.elf_file): 47 | self._table[tup[2]] = (int(tup[0], 16), tup[1]) 48 | 49 | def lookup(self, name): 50 | return self._table[name] 51 | 52 | def lookup_address(self, name): 53 | addr, type = self._table[name] 54 | return addr 55 | 56 | def lookup_type(self, name): 57 | addr, type = self._table[name] 58 | return type 59 | 60 | 61 | if __name__ == '__main__': 62 | import argparse 63 | 64 | parser = argparse.ArgumentParser(description=( 65 | 'Find a symbol\'s address in an ELF file' 66 | )) 67 | parser.add_argument('elf_file', type=str) 68 | parser.add_argument('symbol_name', type=str) 69 | args = parser.parse_args() 70 | 71 | symbol_table = SymbolTable(args.elf_file) 72 | symbol = symbol_table.lookup(args.symbol_name) 73 | print(symbol) 74 | -------------------------------------------------------------------------------- /src/openocd/ocd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Thin wrapper around part of OpenOCD's Tcl interface. Supports reading and 4 | writing memory and registers. 5 | 6 | Based on OpenOCD RPC example by Andreas Ortmann (ortmann@finf.uni-hannover.de) 7 | """ 8 | 9 | import socket 10 | import itertools 11 | from time import sleep 12 | 13 | 14 | def strtohex(data): 15 | return map(strtohex, data) if isinstance(data, list) else int(data, 16) 16 | 17 | 18 | def hexify(data): 19 | return "" if data is None else ("0x%08x" % data) 20 | 21 | 22 | def compare_data(a, b): 23 | for i, j, num in zip(a, b, itertools.count(0)): 24 | if i != j: 25 | print("difference at %d: %s != %s" % (num, hexify(i), hexify(j))) 26 | 27 | 28 | class OCDError(Exception): 29 | pass 30 | 31 | 32 | class OpenOCD(): 33 | COMMAND_TOKEN = '\x1a' 34 | 35 | def __init__(self, verbose=False, tcl_ip="127.0.0.1", tcl_port=6666): 36 | self.verbose = verbose 37 | self.tcl_ip = tcl_ip 38 | self.tcl_port = tcl_port 39 | self.buffer_size = 4096 40 | 41 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 42 | 43 | def __enter__(self): 44 | self.sock.connect((self.tcl_ip, self.tcl_port)) 45 | return self 46 | 47 | def __exit__(self, type, value, traceback): 48 | try: 49 | self.send("exit") 50 | finally: 51 | self.sock.close() 52 | 53 | def send(self, cmd): 54 | """Send a command string to TCL RPC. Return the result that was read. 55 | """ 56 | if self.verbose: 57 | print(cmd) 58 | data = (cmd + OpenOCD.COMMAND_TOKEN).encode("utf-8") 59 | if self.verbose: 60 | print("<- ", data) 61 | 62 | self.sock.send(data) 63 | return self._recv() 64 | 65 | def _recv(self): 66 | """Read from the stream until the token (\x1a) was received.""" 67 | data = bytes() 68 | while True: 69 | chunk = self.sock.recv(self.buffer_size) 70 | data += chunk 71 | if bytes(OpenOCD.COMMAND_TOKEN, encoding="utf-8") in chunk: 72 | break 73 | 74 | if self.verbose: 75 | print("-> ", data) 76 | 77 | data = data.decode("utf-8").strip() 78 | data = data[:-1] # strip trailing \x1a 79 | 80 | return data 81 | 82 | def read_variable(self, address): 83 | raw = self.send("ocd_mdw 0x%x" % address).split(": ") 84 | return None if (len(raw) < 2) else strtohex(raw[1]) 85 | 86 | def read_memory(self, wordLen, address, n): 87 | self.send("array unset output") # better to clear the array before 88 | self.send("mem2array output %d 0x%x %d" % (wordLen, address, n)) 89 | 90 | output = self.send("ocd_echo $output").split(" ") 91 | 92 | return [int(output[2*i+1]) for i in range(len(output)//2)] 93 | 94 | def write_variable(self, address, value): 95 | assert value is not None 96 | self.send("mww 0x%x 0x%x" % (address, value)) 97 | 98 | def write_memory(self, wordLen, address, n, data): 99 | array = " ".join(["%d 0x%x" % (a, b) for a, b in enumerate(data)]) 100 | 101 | self.send("array unset buffer") # better to clear the array before 102 | self.send("array set buffer { %s }" % array) 103 | self.send("array2mem buffer 0x%x %s %d" % (wordLen, address, n)) 104 | 105 | def dump_image(self, filename, address, size): 106 | self.send("dump_image {} {:#x} {}".format(filename, address, size)) 107 | 108 | def verify_image(self, filename, address, fmt='bin'): 109 | ret = self.send("ocd_verify_image {} {:#x} {}".format(filename, address, fmt)) 110 | status = ret.split('\n')[-2] 111 | return status.startswith('verified') 112 | 113 | def read_register(self, reg): 114 | raw = self.send("ocd_reg {} force".format(reg)) 115 | value = raw.split(":")[1].strip() 116 | 117 | return int(value, 16) 118 | 119 | def write_register(self, reg, value): 120 | self.send("ocd_reg {} {:#x}".format(reg, value)) 121 | 122 | def push(self, value): 123 | sp = self.read_register("sp") 124 | self.write_register("sp", sp - 4) 125 | self.write_variable(sp - 4, value) 126 | 127 | def pop(self): 128 | sp = self.read_register("sp") 129 | value = self.read_variable(sp) 130 | self.write_register("sp", sp + 4) 131 | return value 132 | 133 | def set_breakpoint(self, addr): 134 | self.send("ocd_bp {:#x} 2".format(addr)) 135 | 136 | def delete_breakpoint(self, addr): 137 | self.send("ocd_rbp {:#x}".format(addr)) 138 | 139 | def reset(self, halt=True): 140 | """Reset the target. The default sends `reset halt` to OpenOCD, but 141 | specifying `halt=False` will send `reset run`. 142 | """ 143 | if halt: 144 | self.send("reset halt") 145 | else: 146 | self.send("reset run") 147 | 148 | def curstate(self): 149 | return self.send("$_TARGETNAME curstate") 150 | 151 | def halt(self): 152 | self.send("halt") 153 | 154 | def wait_halt(self): 155 | res = self.send("ocd_wait_halt") 156 | if not res.startswith('target halted'): 157 | self._recv() 158 | 159 | def resume(self): 160 | self.send("ocd_resume") 161 | 162 | def set_tcl_variable(self, name, value): 163 | self.send("set {} {}".format(name, value)) 164 | 165 | def get_tcl_variable(self, name): 166 | self.send("set {}".format(name)) 167 | 168 | def call(self, addr, *args): 169 | """Call the function at `addr`. The target must be halted before calling 170 | this method, and it must be resumed afterwards. 171 | """ 172 | if len(args) > 4: 173 | raise OCDError("A maximum of 4 1-word arguments are accepted") 174 | 175 | addr = addr & 0xfffffffe 176 | 177 | if self.curstate() != "halted": 178 | raise OCDError("Target must be halted to call function") 179 | # save caller-save registers 180 | regnames = ["pc", "lr", "sp", "r0", "r1", "r2", "r3"] 181 | regs = {reg: self.read_register(reg) for reg in regnames} 182 | for i, arg in enumerate(args): 183 | self.write_register("r{}".format(i), arg) 184 | 185 | self.send("set call_done 0") 186 | self.send(("$_TARGETNAME configure -event halted " 187 | "{ set call_done 1 ; echo done }")) 188 | self.send(("$_TARGETNAME configure -event debug-halted " 189 | "{ set call_done 1 ; echo done }")) 190 | self.write_register("lr", regs["pc"] | 1) 191 | self.write_register("pc", addr) 192 | self.set_breakpoint(regs["pc"]) 193 | self.resume() 194 | while self.send("set call_done") != '1': 195 | sleep(0.01) 196 | 197 | ret = self.read_register("r0") 198 | self.send("$_TARGETNAME configure -event halted { }") 199 | self.send("$_TARGETNAME configure -event debug-halted { }") 200 | self.delete_breakpoint(regs["pc"]) 201 | for name, val in regs.items(): 202 | self.write_register(name, val) 203 | return ret 204 | --------------------------------------------------------------------------------