Debug embarqué : carte électronique et sonde JTAG pour le debug de firmware

Debug embarqué professionnel : JTAG, OpenOCD et GDB — le workflow complet

Debug embarqué professionnel : JTAG, OpenOCD et GDB — le workflow complet

Le debug des systèmes embarqués est resté longtemps le parent pauvre des pratiques d’ingénierie logicielle. Dans le développement web ou applicatif, un débogueur graphique, des logs structurés et des tests automatisés font partie du quotidien. Sur microcontrôleur, beaucoup d’équipes se contentent encore de la méthode du printf — et passent des journées entières à deviner pourquoi un firmware se comporte mal en conditions réelles. Ce n’est pas une fatalité : une chaîne d’outils complète, composée d’une sonde JTAG/SWD, d’OpenOCD et de GDB, permet d’atteindre sur un microcontrôleur un niveau de visibilité équivalent à celui d’un environnement de bureau. Cet article détaille le workflow complet, de la sélection du matériel à l’analyse d’un hard fault, en passant par l’intégration dans une chaîne d’intégration continue.

Pourquoi le debug par printf atteint ses limites

Le réflexe du printf — ou de son équivalent UART_LOG, ESP_LOGx ou Serial.print() — fonctionne tant que le bug se trouve dans la logique applicative pure. Mais il échoue dans quatre situations typiques de l’embarqué industriel :

  • Les problèmes de timing. Ajouter un printf modifie les temps d’exécution, décale les interruptions et peut faire disparaître — ou apparaître — le bug. Le debugger modifie le comportement observé, un classique du « ça marche en debug, ça plante en release ».
  • Les hard faults et autres exceptions. Quand le processeur saute dans le vecteur d’exception, le printf ne s’exécute plus. Il faut inspecter les registres, la pile et le PC pour comprendre ce qui s’est passé.
  • Les bugs liés à l’optimisation. Un compilateur avec -O2 réordonne, supprime et transforme le code. Les variables inspectées via printf peuvent être optimisées, et le comportement différer de la version non optimisée.
  • La consommation de ressources. Une sortie texte consomme de la flash (le formatage printf peut représenter plusieurs ko), du CPU, des cycles UART et de l’énergie — rédhibitoire sur un nœud IoT alimenté sur batterie.

Le debug par sonde matérielle répond à toutes ces limites : il n’ajoute aucun code au firmware, n’altère pas les timings (sauf aux points d’arrêt), et donne accès à l’état complet du processeur, y compris lors d’une exception.

JTAG et SWD : comprendre les deux interfaces de debug

Avant de choisir une sonde, il faut comprendre les deux protocoles qu’elle parle. Ce ne sont pas des alternatives équivalentes : ils répondent à des contraintes différentes.

JTAG — IEEE 1149.1, le standard historique

JTAG (Joint Test Action Group) est normalisé sous IEEE 1149.1. Conçu à l’origine pour le test de cartes via le boundary scan — la capacité à piloter chaque broche d’un circuit depuis la chaîne de test — il est devenu le standard de fait pour l’accès debug des processeurs. Côté broches, il en faut quatre signaux obligatoires : TCK (horloge), TMS (sélecteur d’état), TDI (entrée de données), TDO (sortie de données), auxquels s’ajoute souvent TRST (reset du TAP controller). La communication repose sur une machine à états (le TAP, Test Access Port) pilotée par TMS et TCK, qui permet d’accéder à une chaîne de registres internes.

L’intérêt majeur de JTAG est sa généricité : il fonctionne sur ARM, RISC-V, x86 embarqué, FPGA et la plupart des architectures. Il permet aussi de chaîner plusieurs circuits sur la même interface (daisy-chain), utile pour programmer plusieurs composants d’une même carte. Son inconvénient est le nombre de broches et la vitesse maximale, souvent bridée par la topologie de la carte.

SWD — Serial Wire Debug, l’alternative ARM

Développé par ARM pour ses cœurs Cortex, SWD (Serial Wire Debug) réduit l’interface à deux signaux : SWDIO (données bidirectionnelles) et SWCLK (horloge). À performances comparables, il libère de la place sur des cartes où chaque broche compte, et il est devenu l’interface par défaut de la quasi-totalité des microcontrôleurs ARM Cortex-M. Tous les outils modernes — ST-Link, J-Link, CMSIS-DAP, DAPLink — le supportent nativement.

Le choix pratique est simple : sur un cœur ARM Cortex-M, on utilise SWD. JTAG reste pertinent pour les architectures non-ARM (RISC-V, FPGA), pour le boundary scan en production, ou quand l’équipement de test de la ligne de fabrication impose JTAG.

Choisir sa sonde de debug

La sonde fait le pont entre l’hôte (PC, serveur de CI) et la cible. Son choix dépend de la famille de microcontrôleurs, du budget et des besoins en vitesse.

  • ST-Link/V2 et V3 : la sonde fournie avec les boards STM32. Excellente pour l’écosystème ST, support SWD et JTAG (selon version), débit suffisant pour 99 % des usages. Le V3 monte à quelques dizaines de MHz et ajoute des canaux UART/VCP.
  • SEGGER J-Link : la référence en vitesse et en fonctionnalités (RTT, flash download accéléré, support multi-architectures ARM, RISC-V sur les modèles récents). Le modèle EDU (~20 €) suffit pour le prototypage ; les versions commerciales se justifient par la vitesse de programmation en production.
  • CMSIS-DAP / DAPLink : l’interface ouverte définie par ARM, implémentée sur des cartes à quelques euros (Nucleo, Raspberry Pi Pico en mode debug, KL27Z, etc.). Parfait pour du debug bas coût et du déploiement CI à grande échelle.
  • ESP-Prog et sondes ESP32 : pour l’écosystème Espressif, la sonde officielle ESP-Prog expose JTAG, UART et la programmation ; les modules ESP32-S3 et C3 embarquent un contrôleur USB-Serial-JTAG directement sur la puce — un simple câble USB suffit pour debugger.
  • Sondes FTDI/FT2232H : les adaptateurs génériques à base de FT2232H (y compris les fameuses boards « JTAG » à 15 €) sont compatibles OpenOCD et permettent de couvrir des architectures exotiques.

Pour une équipe qui conçoit des produits IoT industriels, la règle pragmatique : une sonde par ingénieur pour le développement (J-Link ou ST-Link selon l’architecture), et des sondes CMSIS-DAP à 10 € pour les bancs de test automatisés, où le débit importe peu et le coût unitaire beaucoup.

OpenOCD : le pont logiciel universel

OpenOCD (Open On-Chip Debugger) est le logiciel open source qui fait dialoguer la sonde et la cible. Il supporte des dizaines de sondes et des centaines de cibles, expose un serveur GDB (port 3333 par défaut), un serveur Telnet (4444) et un serveur Tcl (6666). C’est l’outil central de toute chaîne de debug non propriétaire.

Un lancement typique pour une cible STM32 avec une sonde ST-Link :

openocd -f interface/stlink.cfg -f target/stm32f4x.cfg

Pour un ESP32-S3 avec la sonde intégrée USB-Serial-JTAG :

openocd -f interface/esp_usb_jtag.cfg -f target/esp32s3.cfg

Les fichiers de configuration sont l’élément clé : interface/*.cfg décrit la sonde (broches, vitesse, protocole), target/*.cfg décrit le processeur (architecture, registres, mémoire, algorithmes de programmation flash). OpenOCD supporte nativement la plupart des microcontrôleurs du marché ; pour les puces récentes, il faut parfois écrire ou adapter un fichier cible, une opération bien documentée dans le wiki du projet.

Les commandes essentielles à connaître :

  • reset halt — remet la cible à zéro et la gèle immédiatement, l’état de base pour tout debug.
  • flash write_image erase — programme la flash avec effacement préalable.
  • program fichier.elf — raccourci qui programme et vérifie, très pratique en CI.
  • mdw / mww — lecture/écriture mémoire mot par mot.
  • reg — affiche les registres du cœur courant.
  • halt / resume — gèle et relance l’exécution.

GDB en mode remote : le debug complet

Une fois OpenOCD lancé, GDB s’y connecte comme à une cible distante :

arm-none-eabi-gdb firmware.elf
(gdb) target remote :3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue

Le couple OpenOCD + GDB donne accès à l’ensemble des fonctionnalités attendues d’un débogueur : points d’arrêt, exécution pas à pas, inspection des variables et de la mémoire, backtrace. Deux points méritent une attention particulière en embarqué.

Points d’arrêt matériels et logiciels

Sur un Cortex-M, GDB utilise par défaut des points d’arrêt matériels (breakpoints) quand le code est en flash, car il ne peut pas y insérer d’instruction de break (BKPT). Le nombre de breakpoints matériels est limité — typiquement 4 à 8 selon le cœur (FPB, Flash Patch and Breakpoint unit) — et il faut les gérer : au-delà, GDB bascule sur des points d’arrêt logiciels si la zone est en RAM, ou échoue. Les points d’arrêt conditionnels, très utiles, consomment eux aussi des ressources : la condition est évaluée par GDB, ce qui ralentit fortement l’exécution. Une astuce efficace : mettre un breakpoint sur une ligne très fréquentée, puis affiner avec une condition matérielle (hwbreak) ou en inspectant les variables à chaque hit.

Watchpoints : surveiller une variable

Le watchpoint interrompt le processeur quand une adresse mémoire est lue ou écrite. C’est l’outil roi pour traquer une corruption mémoire : une variable qui change de valeur sans explication, un buffer overflow qui écrase une structure voisine. Sur ARM, les watchpoints sont matériels (DWT) et limités à 2 à 4 selon les cœurs — il faut donc les utiliser avec parcimonie. La commande est simple :

(gdb) watch variable_suspecte
(gdb) watch *(uint32_t*)0x20000100

Quand le watchpoint se déclenche, la backtrace montre immédiatement quel code a écrit à cette adresse. C’est le debug le plus rapide qui existe pour les corruptions mémoire.

Inspecter la mémoire et les périphériques

Au-delà des variables C, GDB permet d’inspecter directement la mémoire et les registres des périphériques :

(gdb) x/16wx 0x20000000      # 16 mots en hexadécimal
(gdb) x/8bx 0x40021000       # registres d'un périphérique
(gdb) p/x *(uint32_t*)0x40021000

Cette capacité transforme GDB en véritable oscilloscope logiciel : on peut vérifier l’état d’un registre périphérique, l’alignement d’un buffer DMA, ou le contenu exact d’une file avant qu’elle ne soit consommée.

Debugger FreeRTOS : voir les tâches, pas juste le CPU

Un firmware FreeRTOS ne se debugge pas comme un programme linéaire : le code qui s’exécute au moment du breakpoint n’est qu’une tâche parmi d’autres, et l’état global du système se trouve dans la liste des tâches, les files et les sémaphores. GDB nu montre le thread courant ; il faut de l’aide pour voir le reste.

OpenOCD embarque un support FreeRTOS natif : dès qu’il détecte le symbole pxCurrentTCB (ou l’équivalent selon la version), il expose chaque tâche comme un thread GDB. La commande info threads liste alors toutes les tâches avec leur état, et thread N permet de basculer sur une tâche spécifique pour inspecter sa pile. C’est la différence entre « le système est bloqué » et « la tâche du capteur attend un sémaphore que la tâche réseau ne libère jamais ».

Deux vérifications systématiques en cas de comportement erratique :

  • Le high-water mark de chaque pile. La fonction uxTaskGetStackHighWaterMark() retourne l’espace libre minimum observé sur la pile d’une tâche. Un débordement de pile de tâche est une des causes les plus fréquentes de hard faults aléatoires en production — et elle est invisible sans cette mesure.
  • Le hook de détection de débordement. configCHECK_FOR_STACK_OVERFLOW activé (valeur 1 ou 2) déclenche vApplicationStackOverflowHook() en cas de débordement. En debug, ce hook peut simplement geler le système (__asm volatile("bkpt 0")) pour analyser la pile avec GDB.

Analyser un hard fault : la méthode qui fait gagner des jours

Le hard fault est l’événement le plus redouté : le processeur saute dans le handler, et le programme semble « mort ». Sans sonde, il ne reste que le décodage manuel des registres de faute — un travail fastidieux. Avec GDB, la procédure est quasi immédiate :

  1. monitor reset halt — remettre la cible dans un état connu.
  2. Placer un breakpoint sur le handler HardFault_Handler (ou activer set $pc = HardFault_Handler et continue).
  3. Quand il se déclenche, examiner les registres de faute : sur Cortex-M, SCB->CFSR (0xE000ED28) indique la cause précise — division par zéro, accès mémoire non aligné, exécution d’une instruction illégale, défaut de bus…
  4. Lire SCB->BFAR (0xE000ED38) pour l’adresse fautive en cas de défaut de bus ou de mémoire, et SCB->MMFAR (0xE000ED34) pour les défauts de protection mémoire.
  5. Dépiler les registres sauvegardés (R0-R3, R12, LR, PC, xPSR sont poussés sur la pile par le matériel) pour retrouver le PC de l’instruction fautive et le LR de retour — puis faire un bt pour la backtrace complète.

En pratique, 80 % des hard faults se résolvent en regardant CFSR + le PC fautif : c’est soit un accès à un pointeur NULL ou invalide, soit un débordement de pile, soit un accès à un périphérique non horlogé. Les trois se voient en une minute avec la sonde, contre des heures de suppositions avec des printf.

Semihosting, ITM et RTT : les sorties debug sans UART

Quand on a besoin de logs mais qu’on ne veut ni consommer un UART ni modifier le firmware, trois mécanismes matériels viennent à la rescousse.

Le semihosting : le code cible appelle une fonction hôte (printf, fopen…) via une instruction spéciale (BKPT ou SVC). La sonde intercepte l’appel et le traite sur l’hôte. Zéro broche consommée, mais l’exécution est stoppée à chaque appel — inutilisable en production, parfait en développement.

ITM/SWO (Instrumentation Trace Macrocell) : sur Cortex-M, le port ITM permet d’envoyer des données de trace sur la broche SWO (une seule broche), à des débits élevés, sans arrêter le processeur. En redirigeant printf (ou mieux, ITM_SendChar) vers le port ITM, on obtient des logs temps réel avec un overhead minimal. C’est la solution élégante pour logger en conditions réelles de timing.

SEGGER RTT (Real-Time Transfer) : une petite zone mémoire partagée entre cible et hôte, lue par la sonde sans arrêt du processeur. Très rapide (le MCU écrit dans la RAM, la sonde lit), aucun pin dédié, supporté par OpenOCD et J-Link. C’est devenu la méthode préférée des équipes qui veulent des logs haute fréquence — jusqu’à plusieurs Mo/s en pratique sur J-Link.

Dans un projet IoT industriel, le pattern recommandé : RTT ou ITM pour les logs de développement, un UART de service pour les logs de production (diagnostic sur site), et jamais de printf bloquant dans les ISR.

Intégrer le debug dans la chaîne CI

La sonde ne sert pas qu’à debugger interactivement : elle transforme la validation firmware en un processus automatisable et reproductible. OpenOCD en mode batch, piloté depuis un script, permet de :

  • Programmer la cible après chaque build : openocd -f interface/... -f target/... -c "program build/firmware.elf verify reset exit".
  • Exécuter des tests matériels : lancer le firmware, attendre un motif RTT ou une valeur mémoire, vérifier qu’une GPIO passe à l’état attendu, puis générer un rapport de test.
  • Capturer des coredumps : quand un test échoue, geler la cible et extraire l’état complet (registres, pile, mémoire) pour l’analyser hors-ligne — les mêmes informations qu’un crash dump applicatif.
  • Mesurer des métriques : temps de boot, consommation CPU par tâche (via le compteur de cycles DWT), latence entre une interruption et le traitement — des données objectives pour valider des exigences temps réel.

GDB en mode batch (gdb -batch -ex "target remote :3333" -ex "..." firmware.elf) permet d’automatiser les vérifications : lire une variable, comparer à une valeur attendue, sortir avec un code d’erreur exploitable par le serveur CI. Couplé à des sondes CMSIS-DAP à quelques euros branchées sur des bancs de test, cela donne une infrastructure de test matériel distribuée pour une fraction du coût d’un équipement de test dédié.

Bonnes pratiques issues du terrain

Quelques réflexes qui changent réellement la productivité d’une équipe embarquée :

  • Compiler avec les infos de debug systématiquement (-g), même en release : le symbole et les infos de ligne ne coûtent rien en flash si on strip l’ELF final (arm-none-eabi-strip) après programmation.
  • Désactiver l’optimisation sur les zones suspectes : compiler un fichier précis en -O0 avec __attribute__((optimize("O0"))) permet de debugger un module sans perdre les bénéfices de l’optimisation globale.
  • Garder un ELF non strippé par build, archivé avec le numéro de version : c’est la seule façon d’analyser un crash d’un firmware déployé en production — retrouver le build exact et charger son ELF dans GDB donne la backtrace de la panne.
  • Utiliser les assertions et les hooks de faute : configASSERT de FreeRTOS, les hooks de débordement de pile, et un handler de hard fault qui fige le système et écrit l’état dans une zone mémoire persistante. En production, cette zone peut être remontée par le canal de télémétrie — c’est un mini-coredump à distance.
  • Documenter les commandes OpenOCD/GDB du projet dans un Makefile : make debug, make flash, make test — pour que chaque membre de l’équipe utilise exactement la même configuration, et que la CI fasse pareil.
  • Vérifier les niveaux de tension : une sonde qui tolère mal le 3,3 V ou un niveau logique atypique (1,8 V) produit des erreurs intermittentes qui ressemblent à des bugs firmware. Le choix du voltage de la sonde (adapter speed, VREF) doit être aligné sur la cible dès le premier jour.

Conclusion

Le debug embarqué professionnel n’est pas un luxe réservé aux grands bureaux d’études : une sonde à quelques dizaines d’euros, OpenOCD et GDB forment une chaîne complète qui transforme le développement firmware. Les journées perdues à deviner l’origine d’un hard fault deviennent des analyses de quelques minutes ; les logs de production deviennent des coredumps exploitables ; et la validation matérielle entre dans la CI au même titre que les tests unitaires. Pour une PME industrielle, c’est souvent la différence entre un projet qui dérape de plusieurs semaines et une mise sur le marché maîtrisée.

Chez IOTINNOV, nous appliquons cette méthodologie sur l’ensemble de nos développements embarqués — des nœuds de télémétrie aux systèmes de supervision multi-équipements : sonde systématique au banc, analyse de hard fault outillée, et validation automatisée avant chaque livraison. Si votre équipe passe trop de temps à traquer des bugs intermittents ou souhaite industrialiser ses tests matériels, contactez-nous pour en discuter.