14.1.1.4. Debug del firmware

Il debug su hardware significa fermare il processore, impostare breakpoint nel codice sorgente C, eseguire l’avanzamento passo-passo e ispezionare variabili, memoria, registri e periferiche – dall’interno di VS Code. Questo richiede tre cose: una build di debug, una sonda di debug SWD (un Segger J-Link) e l’estensione Cortex-Debug che pilota arm-none-eabi-gdb contro un server GDB J-Link.

14.1.1.4.1. Compilare per il debug

Ricompila sempre il target con DEBUG=1

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

Un’immagine di release (DEBUG=0) viene compilata con -O2; nel debugger vedrai <optimized out> per molte variabili, le funzioni inline collassano nei loro chiamanti e l’avanzamento passo-passo salta in modo imprevedibile. DEBUG=1 compila con -Og -ggdb3, che è debuggabile pur continuando ad avviarsi sulla camera. L’ELF a cui puntare il debugger è:

build/<TARGET>/bin/firmware.elf

(Per l’Alif AE3, esegui il debug di build/OPENMV_AE3/bin/firmware_M55_HP.elf – il core ad alte prestazioni.)

14.1.1.4.3. Configurazione di Cortex-Debug in VS Code

Crea .vscode/launch.json nel repository. Il caso più semplice – VS Code, il J-Link e la build si trovano tutti sulla stessa macchina Linux / macOS – usa servertype: "jlink", che fa sì che Cortex-Debug avvii da solo un server GDB J-Link:

{
  "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"
    }
  ]
}

Modifica executable e device per la tua scheda (vedi la tabella sopra). Premi F5 per compilare, flashare ed eseguire fino a main e fermarti lì.

Suggerimento

Per ricompilare automaticamente ogni volta che avvii il debug, aggiungi un task di build a .vscode/tasks.json e fai riferimento ad esso dalla configurazione di avvio con "preLaunchTask". Per esempio un task che esegue make -j$(nproc) TARGET=OPENMV4 DEBUG=1, denominato "build-firmware", più "preLaunchTask": "build-firmware" nella configurazione precedente, in modo che F5 ricompili, flashi e avvii il debugger in un unico passaggio.

Avvertimento

Cortex-Debug ha bisogno di arm-none-eabi-gdb. Viene fornito nell’SDK in ~/openmv-sdk-<version>/gcc/bin ma non è presente nel PATH per impostazione predefinita, quindi il debug fallisce con «GDB executable “arm-none-eabi-gdb” was not found». Risolvi il problema impostando armToolchainPath / gdbPath come mostrato sopra, oppure aggiungendo ~/openmv-sdk-<version>/gcc/bin al tuo PATH (printenv PATH dovrebbe quindi elencarlo).

14.1.1.4.4. Vista dei registri delle periferiche (SVD)

Punta Cortex-Debug a un file SVD CMSIS per ottenere una vista decodificata dei registri delle periferiche (timer, DMA, l’interfaccia della camera, ecc.) per nome e campo di bit:

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

Per STM32 e MIMXRT, ottieni l’SVD dai pacchetti CMSIS ST / NXP o dal registro SVD di Cortex-Debug. Gli SVD Alif sono inclusi nel repository del firmware in lib/micropython/lib/alif_ensemble-cmsis-dfp/Debug/SVD/ (usa ..._CM55_HP_View.svd per il core HP dell’AE3).

14.1.1.4.6. Debug da riga di comando con gdbrunner

Impostare a mano una sessione GDB contro un target embedded è una danza in cinque passaggi: avvia il server GDB J-Link / ST-Link in una finestra con il dispositivo, la porta e i flag dell’interfaccia corretti; attendi che stampi Waiting for GDB connection; esegui arm-none-eabi-gdb in una seconda finestra; digita target remote localhost:<port>; punta gdb all’ELF. Quando la sessione gdb termina, ricordati di chiudere la finestra del server. gdbrunner è una piccola CLI che condensa tutto ciò in un singolo comando in foreground. Viene fornita nell’ambiente Python dell’SDK di OpenMV, quindi non c’è nulla da installare; il punto di ingresso abituale è il target make debug del repository del firmware:

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

Questo esegue gdbrunner con gli argomenti del debugger ricavati dalla configurazione della scheda – il nome dispositivo J-Link e, dove necessario, il flash loader esterno ST-Link – con arm-none-eabi-gdb dell’SDK già presente nel PATH. Il backend predefinito è J-Link; make DEBUGGER=STLINK debug funziona invece con una sonda ST-Link.

gdbrunner può anche essere invocato direttamente (al di fuori dell’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

Il primo argomento posizionale seleziona il backend del server (jlink, stlink, qemu); il resto viene inoltrato a quel backend, con valori predefiniti che funzionano per le camere OpenMV. gdbrunner --help elenca la lista completa dei flag per ogni backend; la tabella degli argomenti di ciascun backend è guidata da JSON (src/gdbrunner/backends.json), quindi aggiungere un nuovo server è una modifica alla configurazione anziché al codice.

Cosa fa gdbrunner per il lavoro da riga di comando:

  • Un solo processo, ciclo di vita pulito. Il server si avvia, gdb si collega quando la porta è aperta, il server viene terminato in modo pulito quando gdb esce. Nessun JLinkGDBServer orfano che sopravvive alla sessione, nessun doppio terminale da gestire.

  • Rilevamento automatico di STM32CubeProgrammer. Il backend stlink cerca nelle consuete posizioni di installazione (~/STM32CubeProgrammer/, /opt/st/, l’albero dei plugin di STM32CubeIDE) gli strumenti di STM32CubeProgrammer, in modo da non dover digitare ogni volta il lungo percorso --cube-prog. L’SDK include una propria copia in ~/openmv-sdk-<version>/stcubeprog/bin – punta --cube-prog lì se non esiste un’installazione di sistema.

  • Rispetto del gdbinit per progetto. Un .gdbinit nella directory corrente viene caricato con -ix – prevalendo sul ~/.gdbinit valido per l’intero utente – in modo che lo scripting gdb specifico del progetto (pretty-printer, macro specifiche della scheda, set di breakpoint) si inserisca semplicemente essendo presente nella directory di lavoro. make debug viene eseguito dalla radice del repository, quindi un .gdbinit lì presente viene applicato.

  • Dry run. --dryrun stampa il comando del server senza eseguirlo, utile per adattare l’invocazione a uno script wrapper, per copiarla in una configurazione di avvio di un IDE, o semplicemente per verificare quali argomenti gdbrunner sta componendo.

  • Output del server visibile. --show-output mantiene visibili lo stdout / stderr del server. L’impostazione predefinita li sopprime (così l’interfaccia di gdb resta pulita); attiva il flag quando è il server stesso a comportarsi male.

  • Backend QEMU. qemu-system-arm esegue il debug di una build del firmware senza alcuna scheda collegata. Il target MPS2_AN500 seleziona questo backend nella sua configurazione di scheda, quindi make TARGET=MPS2_AN500 DEBUG=1 debug compila per la macchina mps2-an500 di QEMU ed esegue passo-passo il codice indipendente dalla piattaforma – tutto ciò che non tocca le periferiche specifiche della camera – anche in volo. (qemu-system-arm è un’installazione sull’host, non fa parte dell’SDK.)

Per l’avanzamento passo-passo a livello di sorgente con i margini per i breakpoint e una vista dei registri delle periferiche, la configurazione di Cortex-Debug in VS Code descritta sopra è lo strumento migliore; gdbrunner è quello giusto per tutto ciò che vive sulla riga di comando.

14.1.1.4.7. Usare il debugger

Una volta che una sessione è in esecuzione (il processore fermo a main):

  • Breakpoint – clicca sul margine accanto a una riga C, oppure nella Debug Console break <file>:<line> / break <function>. I core Cortex-M hanno un piccolo numero di comparatori di breakpoint hardware (tipicamente 6–8 su M7 / H7, 8 su M55). Superare quel limite su codice in flash fallisce silenziosamente – mantieni modesto il numero di breakpoint attivi.

  • Avanzamento passo-passoF10 step over (next), F11 step into (step), Shift+F11 step out (finish), F5 continua. L’avanzamento a livello di istruzione è stepi / nexti nella Debug Console.

  • Variabili / watch / stack delle chiamate – i pannelli Variables e Call Stack mostrano le variabili locali e il backtrace; aggiungi espressioni a Watch. Passa il mouse su una variabile nel sorgente per vederne il valore. Qualsiasi cosa che mostra <optimized out> significa che non stai usando una build DEBUG=1.

  • Watchpoint (breakpoint sui dati)watch <expr> si ferma quando una variabile viene scritta, rwatch in lettura, awatch in entrambi i casi. L’unità DWT del Cortex-M supporta circa 4 watchpoint hardware – preziosissimi per intercettare chi ha corrotto una variabile.

  • Registri e periferiche – la vista Cortex Registers mostra i registri del core; con svdFile impostato, la vista Peripherals decodifica ogni registro di periferica e campo di bit (DMA, timer, l’interfaccia camera / CSI, XSPI, ecc.) – il modo più veloce per capire perché un driver si comporta male.

  • Memoria – usa il visualizzatore di memoria di Cortex-Debug o x/ di gdb per ispezionare direttamente frame buffer, buffer DMA e strutture.

  • printf senza fermarsi (SWO/RTT) – per problemi sensibili alla temporizzazione, RTT o SWO di Segger forniscono un printf a overhead pressoché nullo mentre il target è in esecuzione. Compila con DEBUG_PRINTF=1 e aggiungi rttConfig (RTT) o swoConfig (SWO, richiede il clock del core) di Cortex-Debug. Questo è lo strumento giusto quando un breakpoint cambierebbe la temporizzazione che stai cercando di osservare.

  • DisconnessioneStop su una sessione launch ferma il target; Disconnect su una sessione attach lascia la camera in esecuzione. Spegni e riaccendi la camera per riportarla al normale funzionamento successivamente.

14.1.1.4.8. Insidie del debug

  • Variabili ottimizzate via. Tutto mostra <optimized out> – hai compilato con DEBUG=0. Ricompila con DEBUG=1.

  • «GDB executable not found» – la gcc/bin dell’SDK non è nel PATH; imposta armToolchainPath / gdbPath.

  • «Cannot connect» / mappa di memoria errata – nome device errato o mancante; usa la stringa esatta dalla tabella.

  • Breakpoint silenziosamente non raggiunti – troppi breakpoint hardware su codice residente in flash; riducili.

  • I percorsi dei sorgenti non corrispondono (ELF compilato con Docker) – compila con il target Docker build-firmware-dev (stesso percorso assoluto dentro e fuori dal container) oppure imposta set substitute-path in gdb.