--- /dev/null
+#!/usr/bin/python3
+
+# svdpoke uses SVD files to display MCU register values and their meanings,
+# including the current value if given. It can also be used in combination
+# with openocd to connect to a MCU and read the register values directly from
+# the connected device.
+
+# SPDX-License-Identifier: GPL-3.0-or-later
+# Copyright (C) 2025 Simon Ruderich
+
+import argparse
+import functools
+import os
+import re
+import socket
+import sys
+
+import cmsis_svd
+
+
+def parse_svd(path):
+ device = cmsis_svd.SVDParser.for_xml_file(path).get_device()
+ peripherals = []
+ register_map = dict()
+ register_peripheral_map = dict()
+ for p in device.peripherals:
+ peripherals.append(p)
+ xs = []
+ for r in p.get_registers():
+ register_map[f'{p.name}_{r.name}'] = r
+ xs.append(r)
+ register_peripheral_map[p.name] = xs
+ return peripherals, register_map, register_peripheral_map
+
+def clean_description(desc):
+ # Remove newlines and multiple spaces in descriptions which somehow are
+ # present in some SVD files
+ return re.sub(r'\s+', ' ', desc)
+
+def print_peripherals(peripherals):
+ width = 0
+ for p in peripherals:
+ if width < len(p.name):
+ width = len(p.name)
+ for p in peripherals:
+ desc = clean_description(p.description)
+ print(f'{p.name:{width}} @ 0x{p.base_address:08X}: {desc}')
+
+def print_register(register, register_value):
+ parent = register.parent
+ base = parent.base_address
+ offset = register.address_offset
+ addr = base + offset
+ print(f'{parent.name}_{register.name} @ 0x{addr:08X} (0x{base:08X} + 0x{offset:X}):')
+
+ data = []
+ for f in register.get_fields():
+ if f.bit_width == 1:
+ bits = f'{f.bit_offset}'
+ name = f'{f.name}'
+ else:
+ bits = f'{f.bit_offset+f.bit_width-1}:{f.bit_offset}'
+ name = f'{f.name}[{f.bit_width-1}:0]'
+ if register_value is None:
+ value_bin = None
+ value_hex = None
+ value_dec = None
+ else:
+ mask = (1 << f.bit_width) - 1
+ x = (register_value >> f.bit_offset) & mask
+ value_bin = f'0b{x:0{f.bit_width}b}'
+ value_hex = f'0x{x:X}'
+ value_dec = f'{x:d}'
+ if f.access is cmsis_svd.model.SVDAccessType.READ_ONLY:
+ access = 'r'
+ elif f.access is cmsis_svd.model.SVDAccessType.WRITE_ONLY:
+ access = 'w'
+ elif f.access is cmsis_svd.model.SVDAccessType.READ_WRITE:
+ access = 'rw'
+ elif f.access is cmsis_svd.model.SVDAccessType.WRITE_ONCE:
+ access = 'wo'
+ elif f.access is cmsis_svd.model.READ_WRITE_ONCE:
+ access = 'rwo'
+ else:
+ access = '?'
+ desc = clean_description(f.description)
+ data.append([bits, name, value_bin, value_hex, value_dec, access, desc])
+
+ # Get maximum width per column
+ widths = [0 for x in data[0]]
+ for xs in data:
+ for i, x in enumerate(xs):
+ if x is None:
+ continue
+ if widths[i] < len(x):
+ widths[i] = len(x)
+ w_bits, w_name, w_value_bin, w_value_hex, w_value_dec, w_access, _ = widths
+
+ for xs in data:
+ bits, name, value_bin, value_hex, value_dec, access, desc = xs
+ if value_bin is None:
+ value = ''
+ else:
+ value = f' = {value_bin:>{w_value_bin}} {value_hex:>{w_value_hex}} {value_dec:>{w_value_dec}}'
+ print(f' {bits:>{w_bits}} {name:{w_name}}{value} {access:{w_access}} {desc}')
+
+def openocd_connect():
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect(('localhost', 4444))
+ s.settimeout(10.0) # in seconds
+ # Wait for first prompt
+ data = b''
+ while True:
+ data += s.recv(4096)
+ if data.endswith(b'\n\r> '):
+ break
+ return s
+
+def openocd_read_memory(socket, addr):
+ socket.send(f'mdw 0x{addr:x}\n'.encode('ascii'))
+
+ data = b''
+ while True:
+ data += socket.recv(4096)
+ if data.endswith(b'\n\r> '): # wait for prompt
+ match = re.search(b'\x000x[0-9a-f]+: ([0-9a-f]+) \r\n', data)
+ if match is None:
+ sys.exit(f'Unexpected output from openocd: {data} ' +
+ '(see openocd log for details)')
+ return int(match.group(1), 16)
+
+def openocd_read_register(socket, register):
+ addr = register.parent.base_address + register.address_offset
+ return openocd_read_memory(socket, addr)
+
+
+def integer(x):
+ return int(x, base=0)
+
+svd_env = 'SVDPOKE_SVD'
+parser = argparse.ArgumentParser()
+parser.add_argument('-s', '--svd', help='path to SVD file ' +
+ f'(defaults to {svd_env} environment variable)')
+parser.add_argument('-v', '--value', help='set register value and show bits',
+ type=integer)
+parser.add_argument('-o', '--openocd', help='read register value via openocd',
+ action='store_true')
+parser.add_argument('register', nargs='*', help='register or peripheral name')
+args = parser.parse_args()
+
+if args.svd is None:
+ if svd_env not in os.environ:
+ sys.exit(f'Either --svd or {svd_env} must be specified, see --help')
+ args.svd = os.environ[svd_env]
+
+if args.value is not None and args.openocd:
+ sys.exit(f'--value conflicts with --openocd')
+
+
+peripherals, register_map, register_peripheral_map = parse_svd(args.svd)
+
+# No arguments, print all peripherals
+if len(args.register) == 0:
+ print_peripherals(peripherals)
+
+else:
+ if args.openocd:
+ s = openocd_connect()
+
+ for name in args.register:
+ # Full register name (i.e. RCC_ICSCR): show this register (with value)
+ if name in register_map:
+ register = register_map[name]
+ if args.openocd:
+ value = openocd_read_register(s, register)
+ else:
+ value = args.value
+ print_register(register, value)
+ print()
+ # Peripheral name only (i.e. RCC): show all registers (ignore value)
+ elif name in register_peripheral_map:
+ for register in register_peripheral_map[name]:
+ if args.openocd:
+ value = openocd_read_register(s, register)
+ else:
+ value = None
+ print_register(register, value)
+ print()
+ else:
+ sys.exit(f'Register {name} not found; '
+ 'must be either a full name (i.e. RCC_ICSCR) '
+ 'or a peripheral (i.e. RCC)')