From 7b474698a8bfc9acc3edccfbebb4098d46b91968 Mon Sep 17 00:00:00 2001 From: Marian Buschsieweke Date: Sun, 9 Nov 2025 21:20:08 +0100 Subject: [PATCH] dist/tools/PyCortexMDebug: Integrate GDB extension into RIOT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running make RIOT_USE_PYCORTEXMDEBUG=1 debug RIOT will now fetch PyCortexMDebug and instruct GDB to load that extension on start. If additionally RIOT provides `SVD_VENDOR` and `SVD_MODEL` to identify the SVD file to load, an `svd_load $(SVD_VENDOR) $(SVD_CLIENT)` is also passed to GDB. Co-authored-by: crasbe Co-authored-by: Ann🐸 --- dist/tools/PyCortexMDebug/.gitignore | 1 + dist/tools/PyCortexMDebug/Makefile | 16 +++ dist/tools/PyCortexMDebug/README.md | 25 ++++ ...ug-LoadSVD-Fix-exception-in-complete.patch | 41 +++++++ ...-from-ZIP-file-instead-of-pkg_resour.patch | 115 ++++++++++++++++++ makefiles/tools/dbg_common.inc.mk | 11 ++ makefiles/tools/jlink.inc.mk | 10 +- makefiles/tools/openocd.inc.mk | 7 ++ makefiles/tools/targets.inc.mk | 5 + 9 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 dist/tools/PyCortexMDebug/.gitignore create mode 100644 dist/tools/PyCortexMDebug/Makefile create mode 100644 dist/tools/PyCortexMDebug/README.md create mode 100644 dist/tools/PyCortexMDebug/patches/0001-cmdebug-LoadSVD-Fix-exception-in-complete.patch create mode 100644 dist/tools/PyCortexMDebug/patches/0002-LoadSVD-Load-SVD-from-ZIP-file-instead-of-pkg_resour.patch create mode 100644 makefiles/tools/dbg_common.inc.mk diff --git a/dist/tools/PyCortexMDebug/.gitignore b/dist/tools/PyCortexMDebug/.gitignore new file mode 100644 index 0000000000..5e660dc18e --- /dev/null +++ b/dist/tools/PyCortexMDebug/.gitignore @@ -0,0 +1 @@ +/checkout diff --git a/dist/tools/PyCortexMDebug/Makefile b/dist/tools/PyCortexMDebug/Makefile new file mode 100644 index 0000000000..b78c6a092d --- /dev/null +++ b/dist/tools/PyCortexMDebug/Makefile @@ -0,0 +1,16 @@ +PKG_NAME = PyCortexMDebug +PKG_URL = https://github.com/bnahill/PyCortexMDebug +PKG_VERSION = ce371500500a9168729f04b9dcad5f84440deadd +PKG_LICENSE = GPL-3.0-only + +# manually set some RIOT env vars, so this Makefile can be called stand-alone +RIOTBASE ?= $(CURDIR)/../../.. +RIOTTOOLS ?= $(CURDIR)/.. + +PKG_SOURCE_DIR = $(CURDIR)/checkout +PKG_BUILD_OUT_OF_SOURCE := 0 + +include $(RIOTBASE)/pkg/pkg.mk + +.PHONY: all +all: prepare diff --git a/dist/tools/PyCortexMDebug/README.md b/dist/tools/PyCortexMDebug/README.md new file mode 100644 index 0000000000..9a063b88f8 --- /dev/null +++ b/dist/tools/PyCortexMDebug/README.md @@ -0,0 +1,25 @@ +PyCortexMDebug +============== + +This is the RIOT integration of [PyCortexMDebug][], a GDB extension that allows +inspecting and interpreting memory of MCUs based on the info provided in SVD +files. + +This integration adds PRs 58 and 59 on top of the upstream code. To get the +CMSIS database, run: + +```sh +wget -O ~/.cache/cmdebug/cmsis-svd-data.zip https://github.com/cmsis-svd/cmsis-svd-data/archive/refs/heads/main.zip +``` + +Automating Loading of Correct SVD File +--------------------------------------- + +If `SVD_MODEL` and `SVD_VENDOR` are declared, the build system will inject +an `svd_load $(SVD_MODEL) $(SVD_VENDOR)` command into GDB, so that users don't +need to call that themselves. + +Ideally, those variables are provided in `cpu/$(CPU)/Makefile.include` +by inferring it from the `$(CPU_MODEL)` variable each board already provides. + +[PyCortexMDebug]: https://github.com/bnahill/PyCortexMDebug diff --git a/dist/tools/PyCortexMDebug/patches/0001-cmdebug-LoadSVD-Fix-exception-in-complete.patch b/dist/tools/PyCortexMDebug/patches/0001-cmdebug-LoadSVD-Fix-exception-in-complete.patch new file mode 100644 index 0000000000..5cfe38905f --- /dev/null +++ b/dist/tools/PyCortexMDebug/patches/0001-cmdebug-LoadSVD-Fix-exception-in-complete.patch @@ -0,0 +1,41 @@ +From 597434eab992cd3f9495434ed6b990c8c491d66b Mon Sep 17 00:00:00 2001 +From: Marian Buschsieweke +Date: Mon, 3 Nov 2025 23:11:09 +0100 +Subject: [PATCH] cmdebug: LoadSVD(): Fix exception in complete() + +The parameter `word` may actually be `None`, as shown by this +exception: + + (gdb) svd_load STraceback (most recent call last): + File "/usr/lib/python3.12/site-packages/cmdebug/svd_gdb.py", line 65, in complete + prefix = word.lower() + ^^^^^^^^^^ + AttributeError: 'NoneType' object has no attribute 'lower' + +This lets prefix fall back to `""` in case word is `None` to fix the +issue. +--- + cmdebug/svd_gdb.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/cmdebug/svd_gdb.py b/cmdebug/svd_gdb.py +index 4fb993b..c6e1e9c 100644 +--- a/cmdebug/svd_gdb.py ++++ b/cmdebug/svd_gdb.py +@@ -62,11 +62,11 @@ class LoadSVD(gdb.Command): + + # "svd_load " or "svd_load ST" + if num_args == 1: +- prefix = word.lower() ++ prefix = "" if word is None else word.lower() + return [vendor for vendor in self.vendors if vendor.lower().startswith(prefix)] + # "svd_load STMicro" or "svd_load STMicro STM32F1" + elif num_args == 2 and args[0] in self.vendors: +- prefix = word.lower() ++ prefix = "" if word is None else word.lower() + filenames = self.vendors[args[0]] + return [fname for fname in filenames if fname.lower().startswith(prefix)] + return gdb.COMPLETE_NONE +-- +2.51.2 + diff --git a/dist/tools/PyCortexMDebug/patches/0002-LoadSVD-Load-SVD-from-ZIP-file-instead-of-pkg_resour.patch b/dist/tools/PyCortexMDebug/patches/0002-LoadSVD-Load-SVD-from-ZIP-file-instead-of-pkg_resour.patch new file mode 100644 index 0000000000..51afe21140 --- /dev/null +++ b/dist/tools/PyCortexMDebug/patches/0002-LoadSVD-Load-SVD-from-ZIP-file-instead-of-pkg_resour.patch @@ -0,0 +1,115 @@ +From 568ef712d68eb70574fbce5808fea63963aa908f Mon Sep 17 00:00:00 2001 +From: Marian Buschsieweke +Date: Sun, 9 Nov 2025 12:25:11 +0100 +Subject: [PATCH] LoadSVD: Load SVD from ZIP file instead of pkg_resources + +This changes the logic with `svd_load ` to not search in +the pkg_resources of Python module `cmsis_svd.data` but instead search +in a ZIP file at `$CMSIS_SVD_ZIPFILE` if that environment variable is +defined, otherwise it looks `$XDG_CACHE_HOME/cmdebug/cmsis-svd-data.zip` +with `$XDG_CACHE_HOME` falling back to `~/.cache` if not found in the +environment. + +As of 2025-11-09, the `HEAD` of https://github.com/cmsis-svd/cmsis-svd-data +is 5.8 GiB in size uncompressed, or 254 MiB when zipped. Personally, I +do not perceive any additional latency by having to unzip the chosen +SVD file first. +--- + cmdebug/svd_gdb.py | 52 +++++++++++++++++++++++++++++++++++++--------- + 1 file changed, 42 insertions(+), 10 deletions(-) + +diff --git a/cmdebug/svd_gdb.py b/cmdebug/svd_gdb.py +index 4fb993b..33c7bec 100644 +--- a/cmdebug/svd_gdb.py ++++ b/cmdebug/svd_gdb.py +@@ -16,11 +16,15 @@ along with PyCortexMDebug. If not, see . + """ + + import gdb +-import re + import math +-import sys ++import pathlib ++import re + import struct +-import pkg_resources ++import sys ++import tempfile ++import zipfile ++ ++from os import environ + + sys.path.append('.') + from cmdebug.svd import SVDFile +@@ -31,6 +35,16 @@ BITS_TO_UNPACK_FORMAT = { + 32: "I", + } + ++if "XDG_CACHE_HOME" in environ and environ["XDG_CACHE_HOME"] != "": ++ CACHE_DIR = pathlib.Path.joinpath(environ["XDG_CACHE_HOME"], "cmdebug") ++else: ++ CACHE_DIR = pathlib.Path.joinpath(pathlib.Path.home(), ".cache", "cmdebug") ++ ++if "CMSIS_SVD_ZIPFILE" in environ and environ["CMSIS_SVD_ZIPFILE"] != "": ++ CMSIS_SVD_ZIPFILE = pathlib.Path(environ("CMSIS_SVD_ZIPFILE")) ++else: ++ CMSIS_SVD_ZIPFILE = pathlib.Path.joinpath(CACHE_DIR, "cmsis-svd-data.zip") ++ + + class LoadSVD(gdb.Command): + """ A command to load an SVD file and to create the command for inspecting +@@ -40,11 +54,16 @@ class LoadSVD(gdb.Command): + def __init__(self): + self.vendors = {} + try: +- vendor_names = pkg_resources.resource_listdir("cmsis_svd", "data") +- for vendor in vendor_names: +- fnames = pkg_resources.resource_listdir("cmsis_svd", "data/{}".format(vendor)) +- self.vendors[vendor] = [fname for fname in fnames if fname.lower().endswith(".svd")] +- except: ++ with zipfile.ZipFile(CMSIS_SVD_ZIPFILE, mode='r') as svdzip: ++ for entry in svdzip.namelist(): ++ if entry.lower().endswith(".svd"): ++ p = pathlib.Path(entry) ++ vendor = p.parent.name ++ model = p.name[:-4] ++ if vendor not in self.vendors: ++ self.vendors[vendor] = list() ++ self.vendors[vendor].append(model) ++ except FileNotFoundError: + pass + + if len(self.vendors) > 0: +@@ -75,14 +94,27 @@ class LoadSVD(gdb.Command): + def invoke(args, from_tty): + args = gdb.string_to_argv(args) + argc = len(args) ++ f = None + if argc == 1: + gdb.write("Loading SVD file {}...\n".format(args[0])) + f = args[0] + elif argc == 2: + gdb.write("Loading SVD file {}/{}...\n".format(args[0], args[1])) +- f = pkg_resources.resource_filename("cmsis_svd", "data/{}/{}".format(args[0], args[1])) ++ name = args[0].lower() + "/" + args[1].lower() + ".svd" ++ try: ++ with zipfile.ZipFile(CMSIS_SVD_ZIPFILE, mode='r') as svdzip: ++ for entry in svdzip.namelist(): ++ lower = entry.lower() ++ if lower == name or lower.endswith("/" + name): ++ f = svdzip.extract(entry, path=tempfile.TemporaryDirectory().name) ++ break ++ except FileNotFoundError: ++ raise gdb.GdbError(f"Could not open \"{CMSIS_SVD_ZIPFILE}\"") ++ ++ if f is None: ++ raise gdb.GdbError(f"Could not find SVD for \"{args[0]}/{args[1]}\" in \"{CMSIS_SVD_ZIPFILE}\": No case-insensitive match for \"**/{name}\"") + else: +- raise gdb.GdbError("Usage: svd_load or svd_load \n") ++ raise gdb.GdbError("Usage: svd_load or svd_load \n") + try: + SVD(SVDFile(f)) + except Exception as e: +-- +2.51.2 + diff --git a/makefiles/tools/dbg_common.inc.mk b/makefiles/tools/dbg_common.inc.mk new file mode 100644 index 0000000000..06ba82ff08 --- /dev/null +++ b/makefiles/tools/dbg_common.inc.mk @@ -0,0 +1,11 @@ +# Control whether to use PyCortexMDebug +USE_PYCORTEXMDEBUG ?= 0 + +ifeq (1,$(USE_PYCORTEXMDEBUG)) + DEBUGDEPS += $(RIOTTOOLS)/PyCortexMDebug/checkout + DBG_EXTRA_FLAGS += --init-command='$(RIOTTOOLS)/PyCortexMDebug/checkout/scripts/gdb.py' + + ifneq (,$(SVD_MODEL)) + DBG_EXTRA_FLAGS += --eval-command='svd_load $(SVD_VENDOR) $(SVD_MODEL)' + endif +endif diff --git a/makefiles/tools/jlink.inc.mk b/makefiles/tools/jlink.inc.mk index 645e272578..ce5d12f576 100644 --- a/makefiles/tools/jlink.inc.mk +++ b/makefiles/tools/jlink.inc.mk @@ -1,3 +1,5 @@ +include $(RIOTMAKE)/tools/dbg_common.inc.mk + FLASHER ?= $(RIOTTOOLS)/jlink/jlink.sh DEBUGGER ?= $(RIOTTOOLS)/jlink/jlink.sh DEBUGSERVER ?= $(RIOTTOOLS)/jlink/jlink.sh @@ -18,7 +20,8 @@ JLINK_PRE_FLASH ?= JLINK_POST_FLASH ?= JLINK_FLASH_TARGETS = flash flash% -JLINK_TARGETS = debug% $(JLINK_FLASH_TARGETS) reset term-rtt +JLINK_DEBUG_TARGETS = debug debug-server +JLINK_TARGETS = $(JLINK_DEBUG_TARGETS) $(JLINK_FLASH_TARGETS) reset term-rtt # Export JLINK_SERIAL to required targets $(call target-export-variables,$(JLINK_TARGETS),JLINK_SERIAL) @@ -50,3 +53,8 @@ endif ifneq (,$(JLINK_POST_FLASH)) $(call target-export-variables,JLINK_FLASH_TARGETS,JLINK_POST_FLASH) endif + +ifneq (,$(DBG_EXTRA_FLAGS)) + # Export OPENOCD_DBG_EXTRA_CMD only to the flash/flash-only target + $(call target-export-variables,$(JLINK_DEBUG_TARGETS),DBG_EXTRA_FLAGS) +endif diff --git a/makefiles/tools/openocd.inc.mk b/makefiles/tools/openocd.inc.mk index e8ace950a9..dda963b801 100644 --- a/makefiles/tools/openocd.inc.mk +++ b/makefiles/tools/openocd.inc.mk @@ -1,3 +1,5 @@ +include $(RIOTMAKE)/tools/dbg_common.inc.mk + FLASHER ?= $(RIOTTOOLS)/openocd/openocd.sh DEBUGGER ?= $(RIOTTOOLS)/openocd/openocd.sh DEBUGSERVER ?= $(RIOTTOOLS)/openocd/openocd.sh @@ -78,6 +80,11 @@ ifneq (,$(OPENOCD_DBG_EXTRA_CMD)) $(call target-export-variables,$(OPENOCD_DEBUG_TARGETS),OPENOCD_DBG_EXTRA_CMD) endif +ifneq (,$(DBG_EXTRA_FLAGS)) + # Export OPENOCD_DBG_EXTRA_CMD only to the flash/flash-only target + $(call target-export-variables,$(OPENOCD_DEBUG_TARGETS),DBG_EXTRA_FLAGS) +endif + OPENOCD_FLASH_TARGETS = flash flash-only flashr ifneq (,$(IMAGE_OFFSET)) diff --git a/makefiles/tools/targets.inc.mk b/makefiles/tools/targets.inc.mk index 36d642f603..345b4496da 100644 --- a/makefiles/tools/targets.inc.mk +++ b/makefiles/tools/targets.inc.mk @@ -75,3 +75,8 @@ $(RIOTTOOLS)/pioasm/pioasm: $(RIOTTOOLS)/pioasm/Makefile @echo "[INFO] pioasm not found - fetching it from GitHub now" CC= CFLAGS= $(MAKE) -C $(RIOTTOOLS)/pioasm @echo "[INFO] pioasm successfully fetched!" + +$(RIOTTOOLS)/PyCortexMDebug/checkout: $(RIOTTOOLS)/PyCortexMDebug/Makefile + @echo "[INFO] PyCortexMDebug not found - fetching it from GitHub now" + CC= CFLAGS= $(MAKE) -C $(RIOTTOOLS)/PyCortexMDebug + @echo "[INFO] PyCortexMDebug successfully fetched!"