Utilisation du débogueur pas-à-pas avec STM32duino

Un débogueur est un logiciel qui aide un développeur à analyser les erreurs de son programme. Pour cela, il permet notamment d’exécuter le programme pas-à-pas (c’est-à-dire, le plus souvent, ligne par ligne), d’afficher la valeur des variables à tout moment et de mettre en place des points d’arrêt. Un point d’arrêt est un emplacement d’un programme (ligne, instruction) où son exécution est mise en pause (par le débogueur). Un point d’arrêt peut aussi être conditionnel et suspendre l’exécution lorsque certaines conditions se réalisent (par exemple, si une variable donnée du programme prend la valeur zéro).

Le débogueur de l’IDE Arduino 2 étant désormais compatible avec (presque toutes) les cartes de STMicroelectronics supportées par STM32duino, cette section a pour objectif de vous en donner un aperçu, sans autre prétention car c’est un outil qui ne révèle son plein potentiel qu’après une longue pratique de la programmation embarquée.

Si vous êtes anglophone, vous pouvez en complément consulter la documentation minimum pour démarrer avec cet outil sur le site officiel d’Arduino ici ou encore sa documentation complète sur le site de VS Code.

Etapes préalables

Les deux étapes préalables à suivre avant la configuration proprement dite du débogueur sont les suivantes :

  1. Télécharger et installer le logiciel STM32CubeProgrammer, comme expliqué par ce tutoriel.

  2. Installer les définitions des cartes de STMicroelectronics dans l’IDE Arduino 2 et la configurer correctement comme expliqué par ce tutoriel.

Configuration et lancement du débogueur pour les cartes de STMicroelectronics

Nous pouvons à présent configurer l’IDE Arduino 2 pour pour utiliser le débogueur.

Pour cela, créez un sketch que vous souhaitez déboguer. Dans cette section, nous supposerons que le sketch est Blink.ino (accessible via le menu Fichier, puis Exemples, puis 01.Basics, enfin Blink) et que vous souhaitez l’exécuter et le déboguer sur une carte NUCLEO-L476RG.

Commencez par connecter votre carte NUCLEO-L476RG à l’IDE Arduino via la liaison USB PC / ST-Link.

Dans le menu Outils procédez aux configurations suivantes …

  • Entrée Carte : Choisir Nucleo 64
  • Entrée Port : Sélectionnez le port COM virtuel associé par Windows à votre carte (dans notre cas il s’agit de COM4).
  • Entrée Board part number : Choisir Nucleo L476RG
  • Entrée Debug symbols and core logs : Choisir Core Logs And Symbols Enabled (-g)
  • Entrée Optimize : Choisir Debug (-Og)
  • Entrée Upload method : Choisir STM32CubeProgrammer (SWD)

Dans le menu Croquis, cochez l’entrée Optimisé pour le débogage.

Vous pouvez alors lancer le débogage en cliquant sur le bouton Démarrer le débogage, après avoir vérifié que votre carte NUCLEO-L476RG est correctement identifiée comme une NUCLEO 64, ainsi que son port COM Virtuel, dans le menu déroulant juste à côté, conformément à la figure ci-dessous :


Lancer le debogueur, étape 1


Si tout est correctement configuré, l’IDE Arduino devrait vous poser la question suivante (en bas à droite) :


Lancer le debogueur, étape 2


Cliquez sur OUI et laissez l’IDE recompiler et téléverser votre sketch dans la mémoire flash du MCU la carte NUCLEO.

Une fois cette étape terminée, cliquez de nouveau sur le bouton Démarrer le débogage. L’IDE Arduino devrait ouvrir deux nouvelles consoles intitulées gdb-server et Console de débogage. Vous devriez lire dans ces consoles des retours de l’outil de débogage (gdb pour ARM) ressemblant à ceci :

... (lignes supprimées par soucis de concision)

Unable to match requested speed 500 kHz, using 480 kHz
[stm32l4x.cpu] halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08004d44 msp: 0x20018000

Vous pourrez alors passer à l’étape suivante : Utilisation du débogueur pour les cartes de STMicroelectronics.

Si vous avez “raté” quelque chose lors des configurations, il est fort probable que le lancement de gdb échoue avec les logs suivants :


... (lignes supprimées par soucis de concision)

Error: libusb_open() failed with LIBUSB_ERROR_NOT_FOUND
Error: open failed

[2024-07-31T12:41:59.337Z] SERVER CONSOLE DEBUG: onBackendConnect: gdb-server session closed
GDB server session ended. This terminal will be reused, waiting for next session to start...

Le cas échant, reprenez le présent tutoriel depuis le début, ça finira par fonctionner !

L’interface graphique du débogueur

A l’issue de l’étape précédente, le menu latéral de débogage se déploie dans l’IDE Arduino, qui devrait ressembler à ceci :


Fenêtre du debogueur


Examinons ces éléments d’interface un par un …

1 - Démarrer le débogage

Appuyez sur ce bouton pour lancer la session de débogage.

2 - Commandes de débogage pas-à-pas

Cette barre d’outils permet de réaliser le débogage pas-à-pas. La traduction française de ses fonctions étant une véritable catastrophe, nous reprenons ici leurs noms anglais, qui ne portent pas à confusion :


Commandes de débogage pas à pas


  • L’outil Reset device force un reset logiciel du MCU en cours de débogage.

  • L’outil Pause / Continue met en pause l’exécution du script, ou la relance. Lorsqu’un point d’arrêt est positionné dans le script, l’exécution est mise en pause sur sa ligne.

  • L’outil Step over exécute d’une traite l’ensemble des instructions d’une ligne du sketch et remet en pause son exécution avant le début de la ligne suivante.

  • L’outil Step into “rentre” dans le listing d’une fonction et met en pause l’exécution sur sa première ligne.

  • L’outil Step out interromps l’exécution pas-à-pas d’une fonction dans laquelle on est entré via Step into, termine son exécution d’une traite, et reste en pause sur la ligne du sketch appelant celle-ci.

  • L’outil Restart interromps la session de débogage et la relance. Le programme va recommencer depuis le début et le débogueur fera à nouveau une pose au premier point d’arrêt rencontré.

  • L’outil Stop termine l’exécution du programme et sort du mode débogage.

IMPORTANT
En version 2.3.2 de l’IDE Arduino, il est malheureusement fréquent qu’une session de débogage soit complètement plantée par suite d’un appel à l’un des outils Reset Device, Restart ou Stop à un moment qui ne “convient pas” au débogueur, sans que l’on comprenne pourquoi. Ne prêtez surtout pas foi aux messages de la console de débogage qui prétendent qu’il faut attendre que la session gdb redémarre, dans ce genre là :

[2024-08-03T19:53:34.566Z] SERVER CONSOLE DEBUG: onBackendConnect: gdb-server session closed
GDB server session ended. This terminal will be reused, waiting for next session to start...

Le cas échéant, il est nécessaire de quitter l’IDE Arduino puis de la relancer pour que le débogueur fonctionne à nouveau.

3 - Fils d’exécution (threads) en cours de débogage

Pertinent si le firmware est “multitâches”. En général, ce n’est pas le cas et il n’y a qu’un seul thread.

4 - Historique de la pile des appels de fonctions

Affiche la “pile” des fonctions appelées et non encore terminées (donc, en cours d’exécution) ainsi que leurs adresses de début en mémoire principale.

La fonction main, entrée du programme c++ contenant notre sketch Arduino, est naturellement toujours présente dans cette pile.
La fonction setup apparaît au sommet de la pile, puis en est retirée puisqu’elle n’est exécutée qu’une seule fois.
Ensuite la fonction loop, “boucle principale et infinie” du sketch Arduino, vient se placer au-dessus de main et y restera également jusqu’à la fin de l’exécution du sketch.

Si vous cliquez avec le bouton droit de la souris sur l’une des fonctions dans la pile (dans notre cas loop), un menu contextuel apparait avec trois options :


Menu contextuel pile des appels


Si vous choisissez Ouvrir la vue désassemblage, un nouvel onglet s’ouvrira dans la fenêtre principale, intitulé Code Machine, qui affichera le listing en langage d’assemblage de la fonction loop :


Vue code assembleur


Vous constaterez que ce listing contient beaucoup plus d’instructions que celui en C++ du sketch Arduino. Rien d’étonnant à cela puisque chaque instruction C++ est remplacée au moment de la compilation par plusieurs instructions d’assemblage. Qui plus est, selon les options d’optimisation que vous aurez configurées via l’IDE Arduino, la liste et l’ordre des instructions d’assemblage sera différente (tandis que le code C++ de “haut niveau” restera bien sûr inchangé). C’est la raison pour laquelle il est fortement déconseillé d’activer les options d’optimisation d’un compilateur pendant la phase de débogage d’un programme : cela rend impossible toute correspondance entre le code d’assemblage effectivement exécuté et le langage de haut niveau compilé.

5 - Liste des variables et visualisation de leurs valeurs

Noms, attributs et valeurs des différentes variables du sketch. Elles ne sont mises à jour et accessibles que lorsque l’exécution du programme est en pause, en général sur un point d’arrêt.

On constate que la variable counter figure bien dans la liste des variables locales mais aussi dans la liste des variables statiques (ce qui est cohérent avec sa déclaration dans la fenêtre principale, en ligne 14). Le débogueur nous donne même sa valeur (“17” en l’occurrence), mise à jour à l’occasion de chaque mise en pause de l’exécution sur le point d’arrêt de la ligne 19.
On trouve aussi dans cette section la liste de toutes les variables globales et de tous les registres du cœur Cortex du MCU STM32 et leurs contenus.

Si vous cliquez avec le bouton droit de la souris sur l’une des entrées de cette section, un menu contextuel apparaitra, vous proposant de basculer l’affichage du contenu des variables entre une représentation en base hexadécimale (Enable Hex mode) ou en base décimale (Disable Hex mode).
D’autres commandes sont proposées, pour copier la valeur des variables dans le presse papier, ou, nettement plus intéressant, pour modifier la valeur de n’importe quelle variable (Définir la valeur de la variable). Ainsi, dans notre exemple, vous pourriez changer la valeur de counter à “1” et relancer l’exécution pour observer les conséquences.

6 - Surveiller une variable ou évaluer une expression entre deux pauses

Dans cette section, vous pouvez définir des expressions qui seront évaluées chaque fois que le programme sera mis en pause. L’ajout ou la modification d’une expression se fait en cliquant sur l’icône “+” ou bien via le menu contextuel accessible par un clic droit de la souris :


Edition d'espions


L’expression saisie peut être simplement un nom de variable (ce que nous avons fait dans notre exemple avec counter) ou quelque chose de plus compliqué (par exemple un test écrit en syntaxe c tel que counter == 7, un calcul plus compliqué entre plusieurs variables, etc).

7 - Inventaire et gestion des points d’arrêt et de journalisation

Cette section donne la liste des points d’arrêt placés dans le listing et permet de les manipuler (voir plus bas). Dans notre exemple, quatre points ont été placés :

  • Un point d’arrêt (Breakpoint en anglais, pastille rouge) en ligne 17. De ce fait, l’exécution du sketch sera mise en pause sur la ligne 17, juste avant digitalWrite(LED_BUILTIN, HIGH);. Dans cet état, vous pouvez examiner le contenu des variables locales ou globales, changer leurs valeurs, naviguer dans le code de la fonction digitalWrite grâce à l’outil Step into (voir 2 - Commandes de débogage pas-à-pas ci-avant), etc.

  • Un point d’arrêt conditionnel (Conditional breakpoints en anglais, point rouge avec deux traits horizontaux) en ligne 15, avec pour condition counter == 5. De ce fait, l’exécution du sketch sera mise en pause sur la ligne 15, juste avant counter++;, si la valeur de counter vaut “5” (donc au cinquième “tour” de loop()).
    Plutôt qu’une condition, on peut aussi paramétrer un nombre d’accès. Le point mettra alors en pause le programme après qu’il soit passé sur sa ligne le nombre spécifié de fois.

  • Deux points de journalisation (Logpoints en anglais, losanges rouges) en lignes 18 et 20. Chaque fois que les instructions de ces lignes sont exécutées, ces points affichent le message “Attente 1 seconde” dans la console de débogage. Ils ne mettent pas en pause l’exécution du sketch et sont donc comparables à des instructions Serial.print() que vous auriez rajoutées au code, mais avec justement l’avantage de ne pas modifier celui-ci et de ne pas ralentir son exécution. Ces points peuvent aussi exécuter des expressions (voir ce tutoriel).

Créer et paramétrer les points d’arrêt ou de journalisation se fait avec la souris, par des double-clics en face des lignes concernées, puis en modifiant éventuellement ensuite leurs propriétés via un clic droit dessus (qui appelle un menu contextuel). On peut également faire un clic doit de la souris sur la référence du point dans la fenêtre POINTS D’ARRET, comme ceci :


Edition de points d'arrêt


Si on choisit, par exemple, l’entrée Modifier Point d’arrêt, une zone d’édition s’affiche alors, dans laquelle vous pouvez préciser votre modification :


Edition de points d'arrêt


ATTENTION
Nous avons constaté un bug particulièrement pénible dans la version 2.3.2 de l’IDE Arduino : parfois, les zones d’édition pour paramétrer les points d’arrêt ou de journalisation n’apparaissent pas. Vous aurez beau cliquer dans les menus contextuels, rien ne se passera ! Toutefois, en relançant l’IDE Arduino, nous sommes toujours parvenu à “réactiver” ces fonctionnalités dans les sketchs.

8 - Valeurs des registres des périphériques intégrés au MCU

Comme son nom l’indique, cette section permet de parcourir (la longue) liste des périphériques intégrés au MCU STM32 et d’obtenir les adresses en mémoire principale de leurs registres d’entrées / sorties ainsi que de lire le contenu de ces derniers.

En “promenant” la souris sur les registres listés, vous provoquerez l’affichage de fenêtres “popup” détaillant leur structure et leur contenu. Le menu contextuel, accessible via un clic droit sur une des entrées avec la souris, propose trois bases pour l’affichage : binaire, hexadécimal ou décimal.

Vous avez aussi la possibilité de changer le contenu des registres pendant que l’exécution est en pause exactement comme avec les variables (voir point 5), en cliquant sur leur ligne puis sur le bouton d’édition associé (Update Value, via une icône en forme de crayon).

La copie d’écran ci-dessous illustre le menu “popup” qui détaille le registre BTR1 du périphérique intégré FMC (pour “Flexible Memory Controller”) :


Visualisation registre BTR1 du FMC


9 - Valeurs des registres du cœur Cortex

Cette section est semblable à celle qui précède, vous pourrez explorer et manipuler grâce à elle les registres du cœur Cortex intégré au MCU STM32.

Débogage pas-à-pas d’un sketch

Pour réaliser une courte démonstration de l’usage du débogueur, nous considérons le sketch suivant, fortement inspiré de celui proposé par Dominique CLAUSE sur son blog :

01 /*
02   Blink adapté pour les besoins de l'étude du déboggueur.
03   Exemple fortement inspiré de celui fourni par Dominique CLAUSE :
04   https://freelance-drupal.com/blog/debug-arduino-code.
05 */
06 
07 // Compteur déclaré comme variable globale
08 uint8_t counter = 0;
09 
10 // Fonction exécutée une seule fois (initialisations)
11 void setup() {
12   Serial.begin(9600);
13   pinMode(LED_BUILTIN, OUTPUT);
14   __NOP(); // Ne réalise aucune opération
15 }
16  
17 // Fonction "clignote"
18 void blink(uint16_t tempo) {
19   digitalWrite(LED_BUILTIN, HIGH);
20   delay(tempo);
21   digitalWrite(LED_BUILTIN, LOW);
22   delay(tempo);
23 }
24 
25 // Boucle sans clause de fin
26 void loop() {
27   
28   const uint16_t TEMPO_MS = 100;
29 
30   if (counter < 300) {
31     Serial.print("Valeur du compteur : ");
32     Serial.println(counter);
33     blink(TEMPO_MS);
34   }
35   counter++;
36 }

Commençons par ajouter un “espion” sur la variable globale counter

Dans la ligne de titre de la zone ESPION cliquez sur + (Ajouter une expression). Une boite de dialogue intitulée Modifier l’expression s’affiche alors. Dans son champ Expression à espionner tapez 300 - counter :


Visualisation registre BTR1 du FMC


Cliquez sur le bouton OK pour confirmer et fermer la boite de dialogue (ou bien sur x pour quitter sans valider).

Plaçons ensuite un point d’arrêt sur la ligne 33

Cliquez une seule fois juste à gauche du numéro de ligne 33 blink(TEMPO_MS); dans la fenêtre principale. Une pastille rouge apparaît, qui confirme que le point d’arrêt est actif.

Démarrons le débogage pas-à-pas

Cliquez ensuite sur le bouton démarrer (Démarrer). Votre affichage devrait ressembler à ceci :


Débogage en pause sur ligne 33


La ligne 33 est en surbrillance, ce qui signifie que l’exécution du sketch est en pause juste avant, le “pointeur de débogage pas-à-pas” est positionné dessus. La fenêtre ESPION affiche 300 - counter = 300.

Cliquez sur continuer (Continuer). Le pointeur “passe” le point d’arrêt, counter est incrémenté, et le sketch se remet en pause en ligne 33. La fenêtre ESPION affiche 300 - counter = 299.

Cliquez sur step-into (Step into). Le “pointeur “saute” alors en ligne 18 void blink(...), le sketch s’y remet en pause.

Cliquez sur step-over (Step over). Le pointeur vient se positionner sur la ligne 19 void digitalWrite(...).
Vous souhaitez explorer le code source de cette fonction ? Cliquez à nouveau sur Step into ! L’IDE ouvre alors le fichier wiring_digital.c dans la fenêtre principale et l’exécution se met en pause sur la ligne 80 :


Code source de digitalWrite


Vous pouvez continuer votre exploration de “l’arbre du code source” avec Step over et faire un Step into sur la ligne 81 void digitalWriteFast(...), etc.
Pour revenir à votre sketch, cliquez sur step-out (Step out).

Cliquez ensuite sur Continuer continuer pour ramener le pointeur de débogage pas-à-pas devant le point d’arrêt de la ligne 33.

Imaginons à présent que vous souhaitiez laisser le code s’exécuter sans pause jusqu’à ce que counter soit égal à 299.
Commencez par supprimer le point d’arrêt en ligne 33, en faisant un double-clic dessus.
Ajoutez (double-clic) un point d’arrêt sur la ligne 35 counter++.
Modifiez ensuite sont comportement afin qu’il devienne un point d’arrêt conditionnel :

  • Clic droit dessus, puis Modifier Point d’arrêt ….
  • Sélectionnez Expression, tapez counter == 299, puis Entrée :


Point d'arrêt conditionnel


  • Nous aurions aussi pu choisir Nombre d’accès et préciser 200 :


Point d'arrêt hits count


Afin que notre point d’arrêt conditionnel soit pris en compte vous devrez cliquez sur Arrêter arrêter puis Démarrer démarrer pour relancer le débogage.

Le point prend alors l’aspect attendu (pastille rouge avec deux traits) et l’exécution du sketch se poursuit jusqu’à ce que counter prenne la valeur 299.
Pendant cette phase vous pouvez contrôler la valeur de counter via l’expression dans la fenêtre ESPION en faisant des Pauses pause et des Continuer continuer.

Mais, contrairement à ce que vous imaginez, le sketch ne se met jamais en pause sur la ligne 35. Notre programme comporte donc un “bug”, que nous vous laissons le soin de traquer !
Indice : changez la condition du point d’arrêt conditionnel en counter == 200


Notre aperçu du débogueur pas-à-pas de l’IDE Arduino 2.3.2 est à présent terminé.
C’est un outil très pratique, voire indispensable, pour tout développeur engagé dans un projet complexe.
En revanche, pour des projets “légers”, la question de son utilité se pose compte tenu du nombre de “bugs” qui, ironiquement, rendent sa manipulation franchement pénible par comparaison avec des tests if, des Serial.print() et des while(1) judicieusement placés au fil de votre code (voir cette fiche).
Ces problèmes seront évidemment corrigés au fil des révisons de l’IDE Arduino.

Liens et ressources