Programmation embarquée sur Raspberry Pi sans sonde JTAG

GNU/Linux Magazine n° 203 | avril 2017 | Julio Guerra - Christophe Plé
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
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 :

$ git clone https://github.com/farjump/raspberry-pi.git glmf-rpi

$ cd glmf-rpi

glmf-rpi/ $ git checkout v1.0.0

glmf-rpi/ $ ls

LICENSE  Makefile  boot/  run.gdb  scripts/  sdk/  src/

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 :

glmf-rpi/ $ sudo ./scripts/install-toolchain.sh --prefix /opt/glmf-arm-none-eabi

[+] Installing ARM cross-compiler into `/opt/glmf-arm-none-eabi`

[+] The toolchain has been successfully installed

glmf-rpi/ $ PATH=/opt/glmf-arm-none-eabi/bin:$PATH

glmf-rpi/ $ export PATH

glmf-rpi/ $ arm-none-eabi-gcc -v && arm-none-eabi-gdb -v && echo mini test ok

...

mini test ok

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 :

glmf-rpi/ $ # 1. Insérer la carte microSD dans le lecteur de carte du poste de travail

glmf-rpi/ $ # 2. Trouver son point de montage

glmf-rpi/ $ dmesg -H | tail

[Feb14 13:37] sd 3:0:0:0: [sdc] 30318592 512-byte logical blocks: (15.5 GB/14.5 GiB)

[  +0.026020]  sdc:

glmf-rpi/ $ # La carte microSD est ici apparue sur le nœud /dev/sdc

glmf-rpi/ $ # 3. Formater la carte microSD en FAT32

glmf-rpi/ $ # Attention, cette étape supprime entièrement le contenu de la carte microSD

glmf-rpi/ $ sudo mkfs.vfat -F 32 -n RPI -I /dev/sdc

glmf-rpi/ $ # 4. Utiliser le script fourni, installant les fichiers sur la carte formatée

glmf-rpi/ $ ./scripts/install-rpi-boot.sh /dev/sdc

[+] Downloading the Raspberry Pi's firmware version 1.20161215

######################################################################## 100.0%

######################################################################## 100.0%

[+] Temporarily mounting `/dev/sdc` into `/tmp/rpi-sdcard-mountpoint`

[+] Installing the RPi firmware and the Alpha debugger

'boot/bootcode.bin' -> '/tmp/rpi-sdcard-mountpoint/bootcode.bin'

'boot/start.elf' -> '/tmp/rpi-sdcard-mountpoint/start.elf'

'boot/Alpha.bin' -> '/tmp/rpi-sdcard-mountpoint/Alpha.bin'

'boot/config.txt' -> '/tmp/rpi-sdcard-mountpoint/config.txt'

[+] Checking the integrity

/tmp/rpi-sdcard-mountpoint/bootcode.bin: OK

/tmp/rpi-sdcard-mountpoint/start.elf: OK

/tmp/rpi-sdcard-mountpoint/Alpha.bin: OK

[+] Un-mounting `/tmp/rpi-sdcard-mountpoint`

[+] Your SD card is ready!

[+] You can now insert it into the RPi and use Alpha through the RPI's Mini-UART

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 :

$ arm-none-eabi-gdb <fichier ELF avec informations de debug>

(gdb) # 1. Configuration de l’interface de communication et connexion à la cible

(gdb) target remote <interface>

(gdb) # 2. Téléchargement du programme sur la cible

(gdb) load

(gdb) # Prêt!

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.

Note

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 :

#include <stdio.h>

void main(void)

{

  printf("Hello RPi!\n");

}

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 :

glmf-rpi/ $ make hello.elf

arm-none-eabi-gcc -specs=sdk/Alpha.specs -mfloat-abi=hard -mfpu=vfp -march=armv6zk -mtune=arm1176jzf-s -g3 -ggdb -Wl,-Tsdk/link.ld -Lsdk -Wl,-Map,hello.map -o hello.elf  src/hello-world/HelloWorld.c

Note

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   (désactivation) à 3 (maximum). L’option -ggdb permet quant à elle d’activer les extensions spécifiques à GDB.

2.1.2 GDB

glmf-rpi/ $ arm-none-eabi-gdb hello.elf

GNU gdb (GNU Tools for ARM Embedded Processors) 7.12.0.20161204-git

For help, type "help".

Type "apropos word" to search for commands related to "word".

(gdb) # 1. Configuration GDB dépendante de Alpha

(gdb) source sdk/alpha.gdb

(gdb)

(gdb) # 2. Connexion a la RPi

(gdb) # Nécessite les droits de lecture/écriture sur le TTY du lien série,

(gdb) # ici le nœud `/dev/ttyUSB0`

(gdb) set serial baud 115200

(gdb) target remote /dev/ttyUSB0

(gdb)

(gdb) # 3. Téléchargement sur la RPi du programme passé en argument à GDB

(gdb) load

(gdb) # `load` place aussi le pointeur sur instruction sur son point

(gdb) # d’entrée `_start`

(gdb)

(gdb) # 4. Lancer alors l’exécution jusqu’à atteindre la fonction main

(gdb) tbreak main

(gdb) continue

Il est conseillé d’écrire ces commandes, préalables à toute les prochaines sessions de debug, dans un fichier que GDB pourra alors lire et exécuter via l’option de lancement -x <fichier>, ou la commande source <fichier>. Le fichier run.gdb, dont le TTY doit être ajusté selon le cas, est à ce titre fourni à la racine du dépôt.

glmf-rpi/ $ arm-none-eabi-gdb -x run.gdb hello.elf

Temporary breakpoint 1, main () at src/hello-world/HelloWorld.c:5

5         printf("Hello %s!\n", "RPi");

(gdb) # Nous avons atteint la fonction main()

(gdb) list

1       #include <stdio.h>

2

3       int main(void)

4       {

5         printf("Hello RPi!\n");

6       }

(gdb) # Prêt pour une nouvelle session de debug

Ou encore :

glmf-rpi/ $ arm-none-eabi-gdb hello.elf

(gdb) source -v run.gdb

Temporary breakpoint 1, main () at src/hello-world/HelloWorld.c:5

5         printf("Hello %s!\n", "RPi");

(gdb) # Nous avons atteint la fonction main()

(gdb) list

1       #include <stdio.h>

2

3       int main(void)

4       {

5         printf("Hello RPi!\n");

6       }

(gdb) # Prêt pour une nouvelle session de debug

Nous pouvons alors le laisser s’exécuter et observer la sortie standard sur la console GDB :

(gdb) continue

Continuing.

Hello RPi!

Program received signal SIGTRAP, Trace/breakpoint trap.

_exit (rc=0) at SYSFILEIO/MAKEFILE/../SOURCE/SYSFILEIO_EXIT.c:11

11      SYSFILEIO/MAKEFILE/../SOURCE/SYSFILEIO_EXIT.c: No such file or directory.

(gdb)

Ou encore l’exécuter au pas-à-pas :

(gdb) source run.gdb

Temporary breakpoint 1, main () at src/hello-world/HelloWorld.c:5

5         printf("Hello %s!\n", "RPi");

(gdb) break printf

Breakpoint 2 at 0x854c

(gdb) continue

Continuing.

Breakpoint 2, 0x0000854c in printf ()

(gdb) bt

#0  0x0000854c in printf ()

#1  0x00008334 in main () at src/hello-world/HelloWorld.c:5

(gdb) finish

Run till exit from #0  0x0000854c in printf ()

Hello RPi!

0x00008334 in main () at src/hello-world/HelloWorld.c:5

5         printf("Hello %s!\n", "RPi");

(gdb) next

6       }

(gdb) delete 2

(gdb) call printf("appel dynamique!\n")

appel dynamique!

$1 = 17

Les fonctionnalités de GDB se classent en deux grandes catégories : le debug de code et le debug de données. Entre autres, il est possible de visualiser la pile d’appel, de lire/écrire les données, les registres et la mémoire. Mais aussi d’insérer des breakpoints, d’intercepter des appels de fonctions à l’aide de breakpoints conditionnels…

Il est également possible d’envoyer des commandes spécifiques au serveur GDB via la commande monitor <commande>, lui permettant d’implémenter ses propres commandes. Par exemple, ici, la capture d’exceptions :

(gdb) monitor help

(gdb) monitor gdb/catch

   RST : no  : Reset Exception

   UND : no  : Undefined Instruction Exception

   SWI : no  : Software Interrupt Exception

 PABRT : no  : Prefetch Abort Exception

 DABRT : no  : Data Abort Exception

   IRQ : no  : IRQ (interrupt) Exception

  FIQR : no  : FIQ (fast interrupt) Exception

Cet exemple de programme constitue donc un point de départ idéal pour une exploration plus approfondie des possibilités de GDB et/ou du développement embarqué sur RPi. Le lecteur peut laisser libre cours à son imagination pour exploiter d’autres fonctions de la bibliothèque C (ex. : scanf(), fopen()...) ou en utilisant d’autres exemples de programmes C standards à embarquer sur la RPi en se reposant sur la bibliothèque C fournie (ex. : la runtime C++).

Note

Un programme se reposant sur l’extension File I/O ne peut pas s’exécuter sans client ou serveur GDB puisque ce sont eux qui échangent et exécutent les appels systèmes (figure 9). Cette fonctionnalité doit donc être considérée comme un moyen de développement supplémentaire dont les utilisateurs peuvent bénéficier ou non selon leur bon vouloir. À bon entendeur.

2.2 Raytracer

Cet exemple est une nouvelle illustration des concepts vus jusqu’à présent et appliqués cette fois-ci à un programme exploitant de manière plus avancée la RPi. Il utilise en effet son unité flottante ainsi que son GPU. Nous finirons avec sa « mise en production » sur la carte microSD de la RPi pour qu’il démarre de manière autonome, sans aucune intervention de GDB.

Figure 10 : Image générée par le raytracer en bare-metal sur la sortie HDMI de la RPi.

2.2.1 Compilation

Ce raytracer affiche sur la sortie HDMI les images calculées par l’algorithme de raytracing (figure 10), contrôlé par le GPU, lui-même commandé par le processeur ARM à travers des requêtes échangées par mailbox [10]. Le calcul se fait pixel par pixel et les résultats sont écrits dans le framebuffer alloué par le GPU. Le fichier VC.c contient le code source des fonctions d’affichage graphiques : une fonction pour demander au GPU de préparer un framebuffer, et une seconde fonction pour récupérer son adresse. Le framebuffer consiste alors en une matrice de pixels à écrire au format 32 bits ARGB.

Pour le compiler :

glmf-rpi/ $ make raytracer.elf

arm-none-eabi-gcc -specs=sdk/Alpha.specs -mfloat-abi=hard -mfpu=vfp -march=armv6zk -mtune=arm1176jzf-s -g3 -ggdb -Wl,-Tsdk/link.ld -Lsdk -Wl,-umalloc -Wl,-Map,raytracer.map -o raytracer.elf -Og src/raytracer/main.c src/raytracer/Raytracing.c src/raytracer/VC.c src/raytracer/VC_aligned_buffer.S -lm

Cet exemple introduit aussi le niveau d’optimisation g avec l’option -Og qui permet d’optimiser le programme tout en conservant une qualité de debug du programme acceptable. Elle évite donc les optimisations les plus agressives qui entraînent trop de perte de traçabilité entre le code objet et son code source. L’expérience de debug est quoi qu’il en soit dégradée (par exemple, impossibilité de lire certaines variables, exécution au pas-à-pas du code réordonnancé, etc.) par rapport à un programme compilé sans aucune optimisation.

2.2.2 GDB

Nous allons utiliser GDB afin d’observer le fonctionnement de l’algorithme de raytracing en employant les moyens de modification dynamique des données du programme.

Démarrer d’abord la session de debug :

glmf-rpi/ $ arm-none-eabi-gdb -x run.gdb raytracer.elf

Temporary breakpoint 1, main () at src/raytracer/main.c:221

221     {

(gdb)

Les temps de traitement longs du raytracer et sa boucle infinie de rendu vidéo sont de parfaits candidats à la fonctionnalité d’interruption asynchrone de l’exécution de la cible, c’est-à-dire tandis qu’elle exécute le raytracer, lorsque le client reçoit le signal <Ctrl> + <c> (SIGINT). Cette fonctionnalité repose sur une interruption matérielle qui nécessite donc de démasquer les interruptions externes du processeur. Pour ce faire, nous utilisons les fonctionnalités de modification des registres ainsi que de scripting GDB, dont la syntaxe des expressions est identique à celle du langage C. Le registre en question est en plus l’un des registres superviseurs, absents du mode GDB « natif », mais ici communiqué par Alpha :

(gdb) # Démasquer les interruptions externes pour rendre possible  

(gdb) # l’interruption du programme via ctrl-c.

(gdb) print /x $cpsr &= ~(1 << 7)

$1 = 0x6000015f

Nous sommes désormais en mesure d’interrompre à tout moment l’exécution du raytracer sur la RPi :

(gdb) continue

Continuing.

^C

Program received signal SIGSTOP, Stopped (signal).

0x0000921c in __ieee754_sqrt ()

Note

Les interruptions externes sont masquées par le point d’entrée _start tel qu’implémenté par la bibliothèque C newlib. Le démasquage doit donc intervenir une fois cette fonction terminée, lorsque nous atteignons la fonction main().

Nous reprenons alors la main avec GDB où le programme a été interrompu. Voici donc un exemple modifiant successivement la couleur d’une sphère :

(gdb) set A_SPHERE[0].S_PROPERTY.S_A.F_GREEN = 0.9

(gdb) continue

Continuing.

^C

Program received signal SIGSTOP, Stopped (signal).

0x00008aa0 in RAYT_TRACE (…) at src/raytracer/Raytracing.c:306

306         S_RGB.F_BLUE = (P_PROPERTY->S_A.F_BLUE * P_RAYT_WORLD->S_AMBIANT_LIGHT.F_BLUE);

(gdb) bt

#0  0x00008aa0 in RAYT_TRACE (…) at src/raytracer/Raytracing.c:306

#1  0x00008e24 in RAYT_RENDER (…) at src/raytracer/Raytracing.c:421

#2  0x000083d0 in main () at src/raytracer/main.c:239

(gdb) set A_SPHERE[0].S_PROPERTY.S_A.F_GREEN = 0.3

(gdb) continue

Continuing.

^C

Program received signal SIGSTOP, Stopped (signal).

0x00009080 in sqrt ()

(gdb) set A_SPHERE[0].S_PROPERTY.S_A.F_GREEN = 0.6

(gdb) # Execution background

(gdb) # Permet de garder la main sur le client tandis que le serveur la rend au programme.

(gdb) continue &

Continuing.

(gdb) # Commande GDB équivalente au signal ctrl-c

(gdb) interrupt

Program received signal SIGSTOP, Stopped (signal).

0x00009240 in __ieee754_sqrt ()

(gdb) # Les variables telles que I_RESOLUTION_FACTOR sont lues une seule fois

(gdb) # lors de l’initialisation du programme. Il est donc nécessaire de reprendre  

(gdb) # depuis le début l’exécution du programme pour pouvoir la modifier.

(gdb) print I_RESOLUTION_FACTOR

$2 = 2

(gdb) # Alpha reset le processeur lorsqu’il reçoit la commande kill, aussi induite

(gdb) # par d’autres commandes GDB. C’est le cas notamment de la commande de

(gdb) # connexion contenue dans le script run.gdb. Il suffit donc de le relancer

(gdb) # pour recommencer une session.

(gdb) source run.gdb

Temporary breakpoint 1, main () at src/raytracer/main.c:221

221     {

(gdb) # Nous pouvons ici modifier la résolution de l’image avant sa lecture

(gdb) # par la fonction d’initialisation du GPU.

(gdb) set I_RESOLUTION_FACTOR = 4

(gdb) continue

(gdb) # Observer la nouvelle résolution.

Note

Le raytracer est statistiquement le plus souvent interrompu durant l’exécution de la fonction sqrt() (racine carré), car longue à exécuter et appelée très fréquemment.

2.2.3 Embarquement

Nous souhaitons finalement embarquer le programme et qu’il s’exécute automatiquement au démarrage de la RPi sans avoir besoin d’utiliser GDB pour le charger. Nous allons donc l’interfacer avec le bootloader de la RPi qui charge puis exécute le contenu de la carte microSD, selon les directives contenues dans le fichier config.txt. En l’absence de ce fichier, le comportement par défaut copie le fichier kernel.img dans la RAM à l’adresse 0x8000 et lance l’exécution à cette adresse.

Il est avant tout nécessaire de prendre en compte les dépendances avec l’utilisation de GDB (ex. : File I/O) ou du serveur Alpha (ex. : mapping mémoire). Si nécessaire, il convient alors de les implémenter. Ainsi, nous avons fait le choix arbitraire de profiter du comportement par défaut du bootloader pour placer à l’adresse 0x8000 un second point d’entrée contenant le code d’initialisation, puis invoquant le point d’entrée de la runtime C _start. D’autres solutions sont parfaitement envisageables et le code source de cette implémentation est fourni à titre d’exemple dans sdk/CPU_start.S.

Pour générer kernel.img et le copier sur la carte microSD :

glmf-rpi/ $ make kernel.img

arm-none-eabi-objcopy -O binary raytracer.elf kernel.img

arm-none-eabi-objcopy --only-keep-debug raytracer.elf kernel.dbg

glmf-rpi/ $ # Insérer la carte microSD dans le poste de travail

glmf-rpi/ $ sudo mount -t vfat -o rw,umask=0000 /dev/sdc /mnt

glmf-rpi/ $ mv /mnt/config.txt /mnt/config.txt.gdb

glmf-rpi/ $ cp -v kernel.img /mnt

'kernel.img' -> '/mnt/hdd/kernel.img'

glmf-rpi/ $ sudo umount /mnt

Insérez à nouveau la carte microSD dans la RPi. Celle-ci démarre directement avec le raytracer sans plus aucune intervention de GDB.

Enfin, le fichier kernel.img, forme purement binaire de l’exécutable raytracer.elf, ne contient plus aucune information de debug. Il n’est donc plus possible de le débugger depuis le code source en tant que tel, mais uniquement sous forme binaire désassemblée par GDB (layout asm). Il est toutefois possible d’obtenir les informations de debug dans un fichier séparé. Le fichier kernel.dbg généré à ses côtés par la commande de compilation précédente contient toutes les informations de debug de kernel.img. Pour finir, voici comment les utiliser :

glmf-rpi/ $ # Une fois le fichier config.txt restauré…

glmf-rpi/ $ arm-none-eabi-gdb

(gdb) source sdk/alpha.gdb

(gdb) set serial baud 115200

(gdb) target remote /dev/ttyUSB0

Remote debugging using /dev/ttyUSB0

warning: No executable has been specified and target does not support

determining executable automatically.  Try using the "file" command.

0x07f10570 in ?? ()

(gdb) # 1. Lecture des informations de debug

(gdb) file kernel.dbg

 program is being debugged already.

Are you sure you want to change the file? (y or n) y

Reading symbols from kernel.dbg...done.

(gdb) # 2. Téléchargement binaire de kernel.img à l’adresse 0x8000

(gdb) restore kernel.img binary 0x8000

Restoring binary file kernel.img into memory (0x8000 to 0x1b130)

(gdb) # 3. Placer le registre pointeur sur instruction à l’adresse

(gdb) # de démarrage avec Alpha

(gdb) set $pc = &_start

(gdb) break main

Breakpoint 1 at 0x8320: file src/raytracer/main.c, line 221.

(gdb) continue

Continuing.

Breakpoint 1, main () at src/raytracer/main.c:221

221     {

(gdb) # Prêt pour débugger kernel.img

Dans cet exemple, nous faisons manuellement ce que la commande load effectuait pour nous depuis le fichier ELF raytracer.elf. Les deux méthodes sont d’ailleurs strictement équivalentes dans le cas présent qui ne comporte aucune autre distinction que les formats des fichiers raytracer.elf et kernel.img: tous deux contiennent le même programme binaire.

Ce dernier exemple est un moyen de distribuer le programme tout en conservant les informations de debug. Il constitue le point de départ d’une possible stratégie de support et maintenance d’une distribution binaire d’un programme embarqué.

Conclusion

Nous nous sommes donc totalement affranchis du coût et de la complexité que peut représenter une sonde JTAG. Maîtriser un tel équipement est une tâche complexe, souvent attribuée aux professionnels les plus aguerris. L’utilisation de GDB, couplée à une implémentation du serveur entièrement logicielle, facilite la démocratisation et la simplification du développement embarqué au quotidien. L’effort nécessaire pour mettre en œuvre le serveur relève strictement des mêmes compétences que celles du développement embarqué. GDB est en plus le débugger bénéficiant de la plus large communauté en ligne, et tout étudiant en informatique sait par exemple l’utiliser après avoir débuggé son premier programme C ou C++. Depuis les choix d’architecture en phases amont de R&D jusqu’à la vérification et la validation d’un logiciel embarqué complet en phase de production, chacun peut trouver son compte parmi les fonctionnalités de GDB. Les sondes restent néanmoins nécessaires dans certains cas bien précis, comme le debug des firmwares, le debug spécialisé de certaines unités processeur (ex. : lire/écrire des caches internes) ou encore le debug temps réel de bus rapides (ex. : debug PCIe).

Les opportunités sont désormais nombreuses sur ce terrain de jeu RPi. Comprendre le mode superviseur ARM, développer du logiciel bare-metal haute performance, ou encore découvrir les interfaces SPI ou USB, sont tout autant de sujets bas niveaux et universels à explorer.

Références

[1] Distribution ARM officielle de la toolchain GNU sur https://developer.arm.com/open-source/gnu-toolchain/gnu-rm

[2] Exemple de convertisseur seul sur http://amzn.eu/3H5FIx9

[3] Exemple de câbles jumper sur http://amzn.eu/jiuRBbi

[4] Exemple de câble USB pour le convertisseur [1] sur http://amzn.eu/ihFNmFf

[5] Exemple de convertisseur format « câble » sur http://amzn.eu/flbbyHF

[6] Documentation des tracepoints sur https://sourceware.org/gdb/onlinedocs/gdb/Tracepoints.html

[7] Documentation de l’extension File I/O de GDB sur https://sourceware.org/gdb/onlinedocs/gdb/File_002dI_002fO-Remote-Protocol-Extension.html

[8] Documentation de l’interface texte sur https://sourceware.org/gdb/onlinedocs/gdb/TUI.html

[9] Liste des appels système supportés sur https://sourceware.org/gdb/onlinedocs/gdb/List-of-Supported-Calls.html

[10] Documentation du framebuffer du SoC BCM2835 sur http://elinux.org/RPi_Framebuffer

Tags : C, JTAG, Raspberry Pi