14.1.1.4. Debugging der Firmware

On-Hardware-Debugging bedeutet, den Prozessor anzuhalten, Breakpoints im C-Quellcode zu setzen, einzeln zu steppen und Variablen, Speicher, Register und Peripheriegeräte zu inspizieren – direkt aus VS Code heraus. Dafür sind drei Dinge erforderlich: ein Debug-Build, eine SWD-Debug-Probe (ein Segger J-Link) und die Cortex-Debug-Erweiterung, die arm-none-eabi-gdb gegen einen J-Link-GDB-Server ansteuert.

14.1.1.4.1. Build für das Debugging

Bauen Sie das Target immer mit DEBUG=1 neu:

make -j$(nproc) TARGET=<TARGET> DEBUG=1

Ein Release-Image (DEBUG=0) wird mit -O2 kompiliert; im Debugger sehen Sie für viele Variablen <optimized out>, inlinete Funktionen verschmelzen mit ihren Aufrufern und das Steppen springt unvorhersehbar umher. DEBUG=1 baut mit -Og -ggdb3, was debugbar ist und gleichzeitig noch auf der Kamera bootet. Die ELF-Datei, auf die Sie den Debugger richten, ist:

build/<TARGET>/bin/firmware.elf

(Für die Alif AE3 debuggen Sie build/OPENMV_AE3/bin/firmware_M55_HP.elf – den Hochleistungskern.)

14.1.1.4.3. VS-Code-Cortex-Debug-Einrichtung

Erstellen Sie .vscode/launch.json im Repository. Der einfachste Fall – VS Code, der J-Link und der Build befinden sich alle auf demselben Linux-/macOS-Rechner – verwendet servertype: "jlink", wodurch Cortex-Debug selbst einen J-Link-GDB-Server startet:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "OpenMV J-Link",
      "type": "cortex-debug",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "executable": "${workspaceFolder}/build/OPENMV4/bin/firmware.elf",
      "servertype": "jlink",
      "device": "STM32H743VI",
      "interface": "swd",
      "runToEntryPoint": "main",
      "armToolchainPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin",
      "gdbPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin/arm-none-eabi-gdb"
    }
  ]
}

Ändern Sie executable und device für Ihr Board (siehe die Tabelle oben). Drücken Sie F5, um zu bauen, zu flashen, bis main auszuführen und dort anzuhalten.

Tipp

Um bei jedem Start des Debuggings automatisch neu zu bauen, fügen Sie eine Build-Task zu .vscode/tasks.json hinzu und referenzieren Sie sie aus der Launch-Konfiguration mit "preLaunchTask". Zum Beispiel eine Task, die make -j$(nproc) TARGET=OPENMV4 DEBUG=1 ausführt und "build-firmware" heißt, plus "preLaunchTask": "build-firmware" in der obigen Konfiguration, sodass F5 in einem einzigen Schritt neu baut, flasht und den Debugger startet.

Warnung

Cortex-Debug benötigt arm-none-eabi-gdb. Es wird im SDK unter ~/openmv-sdk-<version>/gcc/bin mitgeliefert, befindet sich aber standardmäßig nicht im PATH, sodass das Debugging mit „GDB executable ‚arm-none-eabi-gdb‘ was not found“ fehlschlägt. Beheben Sie dies entweder, indem Sie armToolchainPath / gdbPath wie oben gezeigt setzen, oder indem Sie ~/openmv-sdk-<version>/gcc/bin zu Ihrem PATH hinzufügen (printenv PATH sollte es dann auflisten).

14.1.1.4.4. Peripherieregister-Ansicht (SVD)

Richten Sie Cortex-Debug auf eine CMSIS-SVD-Datei, um eine dekodierte Peripherieregister-Ansicht (Timer, DMA, die Kameraschnittstelle usw.) nach Name und Bitfeld zu erhalten:

"svdFile": "/path/to/STM32H743.svd"

Für STM32 und MIMXRT beziehen Sie die SVD aus den ST-/NXP-CMSIS-Packs oder der Cortex-Debug-SVD-Registry. Die Alif-SVDs sind im Firmware-Repository unter lib/micropython/lib/alif_ensemble-cmsis-dfp/Debug/SVD/ vendoriert (verwenden Sie die ..._CM55_HP_View.svd für den AE3-HP-Kern).

14.1.1.4.6. Kommandozeilen-Debugging mit gdbrunner

Eine GDB-Sitzung gegen ein Embedded-Target von Hand einzurichten ist ein fünfstufiger Tanz: starten Sie den J-Link-/ST-Link-GDB-Server in einem Fenster mit den richtigen Geräte-, Port- und Interface-Flags; warten Sie, bis er Waiting for GDB connection ausgibt; führen Sie arm-none-eabi-gdb in einem zweiten Fenster aus; tippen Sie target remote localhost:<port>; richten Sie gdb auf die ELF. Wenn die gdb-Sitzung endet, denken Sie daran, das Serverfenster zu beenden. gdbrunner ist ein kleines CLI, das all dies zu einem einzigen Vordergrundbefehl zusammenfasst. Es wird in der Python-Umgebung des OpenMV-SDK mitgeliefert, sodass es nichts zu installieren gibt; der übliche Einstiegspunkt ist das make debug-Target des Firmware-Repositorys:

make -j$(nproc) TARGET=<TARGET> DEBUG=1 debug

Dies führt gdbrunner mit den Debugger-Argumenten aus der Board-Konfiguration aus – dem J-Link-Gerätenamen und, wo nötig, dem ST-Link-Loader für externen Flash – wobei sich das arm-none-eabi-gdb des SDK bereits im PATH befindet. Das Standard-Backend ist J-Link; make DEBUGGER=STLINK debug funktioniert stattdessen mit einer ST-Link-Probe.

gdbrunner kann auch direkt aufgerufen werden (außerhalb des SDK, pip install gdbrunner):

gdbrunner jlink --device STM32H743VI build/OPENMV4/bin/firmware.elf
gdbrunner stlink build/OPENMV4/bin/firmware.elf
gdbrunner qemu --machine mps2-an500 build/MPS2_AN500/bin/firmware.elf

Das erste Positionsargument wählt das Server-Backend (jlink, stlink, qemu); der Rest wird an dieses Backend weitergereicht, mit Standardwerten, die für die OpenMV-Cams funktionieren. gdbrunner --help listet die vollständige Flag-Liste pro Backend auf; die Argumenttabelle jedes Backends ist JSON-gesteuert (src/gdbrunner/backends.json), sodass das Hinzufügen eines neuen Servers eher eine Konfigurationsänderung als eine Codeänderung ist.

Was gdbrunner für die Kommandozeilenarbeit erledigt:

  • Ein Prozess, sauberer Lebenszyklus. Der Server startet, gdb hängt sich an, sobald der Port offen ist, der Server wird sauber beendet, wenn gdb beendet wird. Kein verwaister JLinkGDBServer, der die Sitzung überlebt, keine zwei Terminals zu verwalten.

  • STM32CubeProgrammer-Autoerkennung. Das stlink-Backend durchsucht die üblichen Installationsorte (~/STM32CubeProgrammer/, /opt/st/, den STM32CubeIDE-Plugin-Baum) nach den STM32CubeProgrammer-Tools, sodass der lange --cube-prog-Pfad nicht jedes Mal eingetippt werden muss. Das SDK bringt seine eigene Kopie unter ~/openmv-sdk-<version>/stcubeprog/bin mit – richten Sie --cube-prog dorthin, wenn keine Systeminstallation existiert.

  • Pro-Projekt-gdbinit wird berücksichtigt. Eine .gdbinit im aktuellen Verzeichnis wird mit -ix geladen – wodurch die benutzerweite ~/.gdbinit überschrieben wird – sodass projektspezifisches gdb-Scripting (Pretty-Printer, board-spezifische Makros, Breakpoint-Sets) durch bloßes Vorhandensein im Arbeitsverzeichnis greift. make debug läuft vom Repository-Stammverzeichnis aus, sodass eine dort liegende .gdbinit angewendet wird.

  • Trockenlauf. --dryrun gibt den Serverbefehl aus, ohne ihn auszuführen, nützlich, um den Aufruf an ein Wrapper-Skript anzupassen, ihn in eine IDE-Launcher-Konfiguration zu kopieren oder einfach zu prüfen, welche Argumente gdbrunner zusammenstellt.

  • Serverausgabe sichtbar. --show-output hält die stdout-/stderr-Ausgabe des Servers sichtbar. Standardmäßig wird sie unterdrückt (damit die Benutzeroberfläche von gdb sauber bleibt); kippen Sie das Flag, wenn der Server selbst dasjenige ist, das sich falsch verhält.

  • QEMU-Backend. qemu-system-arm debuggt einen Firmware-Build ohne angeschlossenes Board. Das MPS2_AN500-Target wählt dieses Backend in seiner Board-Konfiguration, sodass make TARGET=MPS2_AN500 DEBUG=1 debug für QEMUs mps2-an500-Maschine baut und den plattformunabhängigen Code – alles, was keine cam-spezifischen Peripheriegeräte berührt – im Flug durchsteppt. (qemu-system-arm ist eine Host-Installation, nicht Teil des SDK.)

Für das Stepping auf Quellcodeebene mit Breakpoint-Rinnen und einer Peripherieregister-Ansicht ist die obige VS-Code-Cortex-Debug-Einrichtung das bessere Werkzeug; gdbrunner ist das richtige für alles, was auf der Kommandozeile lebt.

14.1.1.4.7. Den Debugger verwenden

Sobald eine Sitzung läuft (der Prozessor bei main angehalten):

  • Breakpoints – klicken Sie in die Rinne neben einer C-Zeile oder in der Debug-Konsole break <file>:<line> / break <function>. Cortex-M-Kerne haben eine kleine Anzahl von Hardware-Breakpoint-Komparatoren (typischerweise 6–8 bei M7 / H7, 8 bei M55). Wird diese bei Code im Flash überschritten, schlägt es stillschweigend fehl – halten Sie die Anzahl aktiver Breakpoints bescheiden.

  • SteppingF10 Step Over (next), F11 Step Into (step), Shift+F11 Step Out (finish), F5 Continue. Stepping auf Instruktionsebene ist stepi / nexti in der Debug-Konsole.

  • Variablen / Watch / Aufrufstapel – die Bereiche Variables und Call Stack zeigen lokale Variablen und den Backtrace; fügen Sie Ausdrücke zu Watch hinzu. Fahren Sie im Quellcode über eine Variable, um ihren Wert zu sehen. Alles, was <optimized out> anzeigt, bedeutet, dass Sie nicht auf einem DEBUG=1-Build sind.

  • Watchpoints (Daten-Breakpoints)watch <expr> hält an, wenn eine Variable geschrieben wird, rwatch beim Lesen, awatch bei beidem. Die Cortex-M-DWT-Einheit unterstützt ~4 Hardware-Watchpoints – unverzichtbar, um herauszufinden, wer eine Variable beschädigt hat.

  • Register und Peripheriegeräte – die Ansicht Cortex Registers zeigt die Kernregister; mit gesetztem svdFile dekodiert die Ansicht Peripherals jedes Peripherieregister und Bitfeld (DMA, Timer, die Kamera-/CSI-Schnittstelle, XSPI usw.) – der schnellste Weg zu sehen, warum sich ein Treiber falsch verhält.

  • Speicher – verwenden Sie den Speicher-Viewer von Cortex-Debug oder gdb x/, um Framebuffer, DMA-Puffer und Strukturen direkt zu inspizieren.

  • printf ohne Anhalten (SWO/RTT) – für timing-sensitive Probleme bietet Seggers RTT oder SWO ein printf mit nahezu null Overhead, während das Target läuft. Bauen Sie mit DEBUG_PRINTF=1 und fügen Sie Cortex-Debugs rttConfig (RTT) oder swoConfig (SWO, benötigt den Kerntakt) hinzu. Dies ist das richtige Werkzeug, wenn ein Breakpoint das Timing verändern würde, das Sie beobachten wollen.

  • TrennenStop bei einer launch-Sitzung hält das Target an; Disconnect bei einer attach-Sitzung lässt die Kamera weiterlaufen. Führen Sie danach einen Power-Cycle der Kamera durch, um sie in den Normalbetrieb zurückzuversetzen.

14.1.1.4.8. Debugging-Fallstricke

  • Wegoptimierte Variablen. Alles zeigt <optimized out> – Sie haben mit DEBUG=0 gebaut. Bauen Sie mit DEBUG=1 neu.

  • „GDB executable not found“ – das gcc/bin des SDK befindet sich nicht im PATH; setzen Sie armToolchainPath / gdbPath.

  • „Cannot connect“ / falsche Speicherkarte – falscher oder fehlender device-Name; verwenden Sie die exakte Zeichenkette aus der Tabelle.

  • Breakpoints werden stillschweigend nicht getroffen – zu viele Hardware-Breakpoints auf flash-residentem Code; reduzieren Sie sie.

  • Quellpfade stimmen nicht überein (Docker-gebaute ELF) – bauen Sie mit dem Docker-build-firmware-dev-Target (gleicher absoluter Pfad innerhalb und außerhalb des Containers) oder setzen Sie gdb set substitute-path.