Programmation embarquée sur Raspberry Pi sans sonde JTAG -Partie 1/2

Le standard JTAG, au succès indéniable, est aujourd’hui ancré dans la majorité des processeurs et proposé comme moyen privilégié de programmation embarquée et de debug. Toutefois, l’utilisation d’une sonde JTAG n’est en rien triviale. L’utilisation du débuggeur GNU s’impose alors, car il est possible de l’employer en bare-metal, c’est-à-dire sans aucun système d’exploitation embarqué pour gérer le processeur et sa carte. À titre d’illustration, nous nous intéressons dans cet article à la programmation bare-metal d’une Raspberry Pi à l’aide de GDB uniquement.

En situation de développement embarqué, la Raspberry Pi (RPi) est appelée « système cible » (target), tandis que le poste de travail est appelé « système hôte » (host). L’hôte doit donc programmer le système cible à distance (remote) via un moyen de communication dédié. À noter également que nous sommes dans une situation de développement croisé : la RPi, bare-metal et architecture ARM, ne correspond pas au système hôte, couramment muni d’une architecture Intel avec un système d’exploitation (OS). Il convient donc d’utiliser une chaîne d’outils croisés pour produire et manipuler un programme cible depuis un système hôte.

Pour ce faire, nous déploierons d’abord sur le poste de travail la chaîne d’outils GNU croisés, arm-none-eabi, incluant le compilateur GCC et le débuggeur GDB, puis embarquerons le serveur GDB freemium Alpha directement sur la RPi, et l’utiliserons comme moyen de développement afin de développer et d’embarquer, avec la seule aide de GDB, une série d’exemples de programmes C standards allant du simple Hello World sur la sortie standard, au plus avancé raytracer utilisant le processeur graphique (GPU) pour produire des images sur la sortie HDMI de la RPi. Nous finirons enfin par embarquer le raytracer directement sur la RPi, pour qu’il soit démarré par son bootloader, comme si nous passions en production une fois le développement terminé.

Figure 1 : Vue générale du développement embarqué à l’aide de GDB.

1. Installation

Figure 2 : Matériel complet nécessaire : une RPi, un poste de travail, une carte microSD pour la RPi, un lecteur de carte microSD pour le poste de travail, un câble HDMI pour la RPi, un câble micro-USB pour alimenter la RPi, un convertisseur USB/UART-TTL3.3V, trois câbles jumper femelle pour le branchement convertisseur/RPi, un câble mini-USB pour le branchement convertisseur/poste de travail.

1.1 Poste de travail

Un poste de travail POSIX (GNU/Linux, Cygwin, OS X…) est nécessaire. Il exécutera la compilation croisée ARM pour RPi, ainsi que le client GDB. Les droits de lecture et écriture sont également nécessaires sur l’interface de communication employée et décrite plus bas.

Un dépôt git est mis à disposition : il contient les éléments nécessaires pour la suite (et pour échanger sur https://github.com/farjump/raspberry-pi en cas de problème). Les commandes ci-dessous permettent de le cloner :

Optionnellement, les plus modernes d’entre nous apprécieront certainement la commande make shell qui produit directement le poste de travail attendu dans un container docker basé sur Debian 9, puis y lance un terminal dans lequel peut s’exécuter de manière garantie la suite de l’article.

1.1.1 Chaîne de compilation croisée ARM

ARM distribue la chaîne de compilation croisée pour les principaux systèmes d’exploitation au format 64 bits uniquement [1]. Le script scripts/install-toolchain.sh, fourni dans le dépôt, télécharge et décompresse la version Linux 64 bits de la chaîne :

Compte tenu du succès de l’architecture ARM, les distributions les plus courantes (Debian, Archlinux, Fedora, etc.) la mettent aussi directement à disposition dans leurs gestionnaires de paquets, toujours en partie nommée avec le triplet arm-none-eabi. Pour OS X et Windows, il convient d’adapter les étapes précédentes à la distribution officielle ARM [1] en la déployant dans votre environnement.

1.2 Raspberry Pi

1.2.1 Carte microSD

La RPi démarre le programme qu’elle trouve sur sa carte microSD suivant les directives contenues dans le fichier de configuration config.txt. Le serveur GDB Alpha doit donc être copié sur la carte microSD pour être démarré à l’allumage de la RPi.

L’étape décrite ici simplifie au maximum la préparation de la carte microSD pour la RPi et son bootloader. Elle suppose une carte microSD vierge compatible avec la RPi. Un script d’aide à son installation est fourni, mais l’étape de formatage en FAT32, attendu par le firmware et le bootloader de la RPi, est laissée explicite afin d’insister sur le caractère irréversible de cette opération destructrice pour la carte microSD. Enfin, le script scripts/install-rpi-boot.sh installe le firmware, le bootloader et Alpha sur la carte microSD :

La carte microSD est alors prête à l’emploi et la RPi y trouvera tout le nécessaire pour démarrer le serveur GDB. À son prochain démarrage, le firmware, ayant pour tâche d’initialiser le processeur et sa carte, lira les directives contenues dans le fichier config.txt indiquant que le programme Alpha.bin doit être copié à l’adresse d’exécution 0x7F0_8000, point d’entrée du serveur.

1.2.2 Interface de communication GDB

Une fois le serveur GDB en cours d’exécution, celui-ci attend la connexion d’un client GDB et ses commandes, en écoutant une interface de communication. Le serveur GDB Alpha utilise l’interface série Mini-UART, car commune à toutes les versions de la RPi. Le port de cette interface n’étant pas disponible tel quel sur un poste de travail standard, il est nécessaire d’interposer un convertisseur entre les deux.

Du coté poste de travail, les ports séries sont les ports USB (et RS232 pour les plus anciens). Du côté RPi, l’interface Mini-UART, présente sur le connecteur GPIO (figure 3), se décompose en trois broches TTL 3.3 Volts. Il est donc nécessaire d’utiliser un convertisseur UART TTL 3.3V vers USB. Attention à bien utiliser un modèle 3.3V.

Figure 3 : Les connecteurs TTL 3.3V de l’interface Mini-UART sont toujours les mêmes pour toutes les RPi.

Figure 4 : Schéma de câblage entre la RPi 3 et un convertisseur : connexion de la masse et croisement entre les fils d’émission et réception.

Figure 5 : Exemple de câblage d’une RPi 3 avec un convertisseur série UART TTL-3.3V vers USB.

 

Figure 6 : Exemple de câblage d’une RPi 1 A+ avec un convertisseur série UART TTL-3.3V vers USB.


Une fois relié au poste de travail par un câble USB, le convertisseur est géré par l’OS et son driver de périphériques série, dont les points de montage possibles sont /dev/ttyUSBx ou bien /dev/ttyACMx pour Linux, COMx pour Windows, ou encore /dev/cu.usbserial-xxxxxx pour OS X. Pour la suite de cet article, /dev/ttyUSB0 désignera notre port série de communication GDB.

Le coût total du montage avoisine les 9€ et nécessite l’achat d’un convertisseur [2], de trois câbles jumper femelle [3] ainsi que d’un câble USB [4]. Des montages complets, couramment nommés « câbles TTL », plus compacts et incluant les câbles, sont aussi disponibles, mais à des prix beaucoup plus aléatoires [5].

2. Éditer, compiler & débugger

Nous sommes désormais prêts à utiliser GDB comme moyen de développement embarqué. Mais contrairement à une utilisation classique de GDB, dite « native », où le programme à exécuter et débugger est au préalable préparé par le système d’exploitation, il est nécessaire ici d’effectuer explicitement ces mêmes étapes préliminaires : téléchargement du programme sur la cible une fois la connexion distante établie. Le mode client/serveur de GDB offre en effet au serveur la possibilité de supporter la commande load qui prend alors directement en charge le format d’exécutable ELF et en télécharge les sections de code et de données sur la cible, puis place finalement le pointeur sur instruction sur son point d’entrée. Les sections de debug sont quant à elles lues et chargées par le client GDB et lui permettent d’apporter les fonctionnalités de debug de code et de données depuis le code source (source-level debugging).

Une session de debug d’un logiciel embarqué commence donc souvent par :

Il est ensuite possible de profiter des fonctionnalités élémentaires de debug de code et de données. Les fonctionnalités plus avancées dépendent quant à elles du serveur. Par exemple, la version freemium d’Alpha, le serveur GDB utilisé, n’implémente pas les tracepoints [6], mais implémente l’extension File I/O [7] (dont l’utilité sera révélée par la suite). GDB signalera au final son incapacité à exécuter une commande si le serveur n’en est pas capable.

GDB est une solution d’instrumentation dynamique, par opposition à l’instrumentation statique qui se fait à la compilation du code source en le modifiant. GDB n’altère donc pas le programme et utilise les ressources de debug du processeur afin d’arrêter et d’observer l’exécution du programme.

De plus, Alpha met en place un environnement d’exécution permettant un développement bare-metal plus simple et dont le programme embarqué via GDB peut alors profiter. Alpha initialise en effet le processeur plus vastement que le bootloader de la RPi en activant, par exemple, l’unité flottante ou encore en programmant un espace mémoire (figure 7). Ceci doit donc être pris en considération lorsque GDB n’est plus utilisé pour charger le programme, qui doit alors lui-même effectuer les initialisations dont il a besoin. Il en est de même pour toutes les fonctionnalités nécessaires et en dehors du périmètre de cet environnement d’exécution.

Figure 7 : Mapping mémoire initialisé par Alpha dont le programme hérite.


Enfin, il ne sera pas nécessaire de redémarrer manuellement la RPi entre chaque session de debug puisqu’Alpha effectue un reset du processeur lorsque GDB lui transmet la commande kill. Cette commande est aussi induite par toutes celles impliquant, en debug natif, l’arrêt du processus, et notamment celle de connexion distante target remote <interface>, ainsi que la commande quit lorsque GDB est quitté. Relancer l’un des scripts GDB qui suit ou bien quitter GDB (correctement) implique donc un reset de la RPi, permettant ainsi de repartir depuis l’état zéro du processeur et d’éviter les effets de bord indésirables d’une session à une autre. À noter que le reset est observable grâce aux LED situées sur la RPi.

Toutes les sessions de debug présentées par la suite utilisent exclusivement l’interface ligne de commandes de GDB (CLI). Il est toutefois conseillé de profiter de son interface texte, TUI [8], et ce à tout moment, grâce à la commande tui enable (figure 8).

Figure 8 : Interface utilisateur texte de GDB.

2.1 Hello World

2.1.1 Compilation

Soit le programme :

Malgré les apparences, ce programme n’est en rien anodin. Il fait en effet appel à des fonctions de la bibliothèque C standard alors qu’elles nécessitent normalement un système d’exploitation, absent dans le cas présent. Il y a d’une part l’initialisation de l’environnement d’exécution (runtime) C préalable à l’appel de la fonction main() (initialisation de la pile et des données), et d’autre part l’écriture sur la sortie standard par printf().

Pour ce faire, les fonctions et appels systèmes nécessaires sont implémentés dans la mesure du possible dans le contexte bare-metal. GDB bénéficie d’une fonctionnalité peu connue que le serveur GDB peut optionnellement implémenter pour transmettre et déléguer au client les appels système, alors exécutés sur le poste de travail sur lequel le client GDB s’exécute. Ainsi, printf()utilise l’appel système write() qui est communiqué et délégué au client tel quel, pour écrire donc finalement sur la sortie standard du client GDB, sur le poste de travail.

Figure 9 : Un appel système effectué depuis la bibliothèque C sur la RPi est délégué au client GDB sur le système hôte.


Cette astuce n’est possible qu’avec un nombre restreint d’appels systèmes [9] dont les capacités sont parfois limitées par GDB (ex. : ouvrir un fichier spécial), tandis que d’autres pourront être implémentés très simplement sans aucune aide d’un système d’exploitation ou de GDB (ex. : brk() pour malloc()).

Cette fonctionnalité de GDB s’appelle File I/O [7] et est implémentée par Alpha. Nous l’interfaçons avec la bibliothèque C newlib en remplaçant ses appels systèmes par des appels à Alpha (figure 9). Cette bibliothèque C est la seule, avec la bibliothèque C GNU glibc, à s’intégrer officiellement dans la chaîne de compilation GCC, ce qui en rend l’utilisation aussi aisée qu’en compilation native : compiler un programme est aussi simple et direct que arm-eabi-none-gcc <cppflags> <cflags> <ldflags> -o main.elf main.c <libs>. Le résultat final est la possibilité d’embarquer n’importe quel programme C standard.

Pour compiler le programme :

Les options de compilation gcc présentes dans le Makefile sont précisément adaptées au processeur ARM11 de la première RPi : -mfloat-abi=hard -mfpu=vfp -march=armv6zk -mtune=arm1176jzf-s.

L’option –gN permet d’activer la génération des informations de debug du programme, où N est le niveau d’informations souhaité allant de 0 (désactivation) à 3 (maximum). L’option -ggdb permet quant à elle d’activer les extensions spécifiques à GDB.

Christophe Plé
[CEO de Farjump et Expert en développement logiciel embarqué]

Julio Guerra
[CTO de Farjump]

La seconde partie de cet article sera publiée prochainement sur le blog, restez connectés 😉

Retrouvez cet article (et bien d’autres) dans GNU/Linux Magazine n°203, disponible sur la boutique et sur la plateforme de lecture en ligne Connect !