Création d'un exécutable

Avant de commencer à débugger, on va déjà créer un exécutable minimal qui nous permettra de vérifier qu'on peut :

  • télécharger un programme sur la carte dans une zone adaptée (en RAM dans un premier temps),
  • lancer ce programme pas à pas (instruction assembleur par instruction assembleur),
  • bref, qu'on sait générer un exécutable correct et le débugger.

Pour cela nous allons procéder en plusieurs temps :

  1. D'abord la création d'un script de link minimal qui assurera que l'exécutable est logé aux bonnes adresses.
  2. Écriture d'un programme minimal (un main qui fait une boucle infinie) et on le testera.
  3. Écriture d'un programme un peu plus complexe, faisant appel à la pile, qu'on testera.
  4. Une fois arrivés là, on aura de quoi commencer à programmer les périphériques !

Mapping mémoire

On rappelle que le mapping mémoire du processeur est disponible en page 75 du reference manual du processeur. L'emplacement des zones qui nous intéressent est rappelé ci-dessous :

  • Flash : adresse de début = 0x08000000, taille = 1MB
  • RAM : elle est séparée en deux blocs non contigus :
    • SRAM1 : début = 0x20000000, taille = 96kB
    • SRAM2 : début = 0x10000000, taille = 32kB

Dans un premier temps, le programme qu'on écrira sera logé en RAM. Plus précisément, le programme (code + données) sera dans SRAM1 et la pile dans SRAM2.

L'avantage de cette façon de faire est qu'un débordement de pile se traduira par un accès dans une zone réservée qui passera le processeur en mode erreur. On pourra donc tout de suite identifier le problème. A contrario, si on avait mis la pile tout en haut de la RAM (à la fin de SRAM1), un débordement de pile se serait traduit par un écrasement de la zone de données (bss ou data), ce qui ne produit un comportement erratique (crash) que longtemps après. À débugger, c'est un enfer !..

Linker script

Layout mémoire

Créez un fichier appelé ld_ram.lds, dans lequel à l'aide de la directive MEMORY vous définirez les différentes régions de mémoire disponibles dans le processeur.

Création des sections

Dans votre script de link, à l'aide de la directive SECTIONS, créez les différentes sections dont vous aurez besoin. Pour l'instant on partira du principe que l'exécutable réside entièrement en RAM, il n'y a donc pas de recopies à faire. Autrement dit, pas besoin de spécifier les LMA des sections, la flash n'étant pas utilisée.

On mettra en premier la section .text, puis la section .rodata, puis .data, puis le bss / COMMON.

Point d'entrée

Le programme est destiné à être exécuté directement par le processeur, sans passer par un loader ELF. Il n'y aurait donc pas besoin de spécifier un point d'entrée.

Mais : le processeur est câblé pour booter à l'adresse 0, qui se situe en flash. Or on voudrait qu'il boote directement sur notre programme en RAM. Pour cela, on pourrait flasher à l'adresse 0 un petit bout de code faisant juste un saut au début de la RAM. Mais on va plutôt exploiter une caractéristique bien pratique de gdb : lorsqu'on lui demande de transférer un exécutable ELF sur une carte, si celui-ci comporte un point d'entrée, alors gdb positionne automatiquement le PC sur ce point d'entrée. On n'a plus après qu'à faire "continue", et tout se passe comme si on avait booté directement depuis la RAM (ou presque : on verra par la suite qu'en fait pas tout à fait. Patience !)

À l'aide de la directive ENTRY, définissez donc un point d'entrée, par exemple sur la fonction main.

Programme de base

Écriture du programme de test

Écrivez en C un programme test le plus simple possible : une fonction main qui fait une boucle infinie.

Compilation du programme

Pour compiler votre programme, il faut indiquer au compilateur que le processeur cible est un cortex-M4 en mode thumb. Ce qui donne une commande du style : arm-none-eabi-gcc -c -g -O1 -mcpu=cortex-m4 -mthumb main.c -o main.o

Si on souhaire en plus tirer partie du FPU pour les opérations flottantes, il faut rajouter les options :  -mfloat-abi=hard -mfpu=fpv4-sp-d16

Pour linker votre programme, la commande est du style : arm-none-eabi-gcc -T ld_ram.lds main.o -o main

Bien entendu, il est hors de question de taper ces lignes à la main. Créez donc un Makefile, en utilisant les variables et règles implicites adéquates !

Vérification du link

Avant de charger votre programme sur la carte, il est impératif de vérifier qu'il a bien été généré correctement.

Pour cela utilisez objdump pour vérifier que :

  • le point d'entrée est bien en 0x20000000,
  • tout l'exécutable est bien logé dans SRAM1 (à partir de 0x20000000).

Tant que ce n'est pas le cas, ne passez surtout pas à la suite, vous risqueriez de programmer une zone contenant des fusibles et de passer la carte dans un état irrécupérable !

Test du programme de base

  1. Lancez le driver de sonde : make startgdbserver (cf. page précédente).
  2. Dans un autre terminal, lancez le débuggeur en lui passant en argument le fichier ELF généré : arm-none-eabi-gdb main
  3. Chargez le programme : flash
  4. Mettez-vous en affichage "registres + code assembleur + fenêtre de commande" : split
  5. Vérifiez que le PC est positionné à la bonne valeur.
  6. Exécutez votre programme pas à pas (instruction assembleur par instruction assembleur) : si

Si tout se passe bien, tant mieux ! Sinon, recherchez la cause de l'erreur en examinant à chaque fois les registres du processeur et en vérifiant que ce qui est exécuté l'est correctement.

Pour sortir de gdb, tapez quit ou control-d.

Attention : si vous terminal se retrouve dans un mode bizarre après la sortie de gdb, ne paniquez pas ! Tapez reset (à l'aveugle si nécessaire) et tout devrait rentrer dans l'ordre :)

Programme plus évolué

Fibonacci

Écrivez une fonction récursive int fibo(int n), qui calcule le n-ième nombre de la suite de Fibonacci.

Modifiez main pour qu'il ne fasse que renvoyer fibo(8).

Compilez, et vérifiez à coup d'objdump que le programme est bien logé aux bonnes adresses.

Testez-le en vrai. Que se passe-t-il, et pourquoi ?

Indice crucial : exécutez le programme instruction assembleur par instruction assembleur et vérifiez à chaque étape que tout s'est bien passé :

  • pour toute opération arithmétique / logique, vérifiez le résultat en examinant les registres ;
  • pour tout accès à la mémoire, regardez le contenu de la mémoire avant l'instruction puis après l'instruction et vérifiez que c'est cohérent.

Correction des choses

Vous venez de constater qu'il manque quelque chose de crucial avant main pour que les choses s'exécutent correctement.

Créez donc le fichier qui va bien, qui se chargera de mettre en place un environnement d'exécution correct pour le code C.

Compilez, et vérifiez à coup d'objdump que le programme est bien logé aux bonnes adresses.

Testez votre programme, qui doit à présent s'exécuter correctement.

Attention :

  • L'assembleur a besoin de connaître le processeur cible : -mcpu=cortex-m4
  • Contrairement au C, en assembleur les symboles sont privés par défaut. Pour les exporter (de ce façon à ce qu'ils soient visibles depuis le C ou le linker script), il faut les déclarer .global
  • On utilisera la syntaxe unifiée, donc la directive .syntax unified 
  • Enfin il faut dire à l'assembleur qu'on compile en mode thumb : .thumb

Exemple :

    .syntax unified
    .global _start
    .thumb

_start:
    blablabla

Initialisation du BSS

Dans le fichier que vous venez de créer, appelez avant main une procédure void init_bss() (écrite en C dans un fichier appelé init.c), qui se chargera d'initialiser le BSS à zéro, en s'aidant de symboles exportés depuis le script de link.

Testez cette procédure en déclarant des variables qui seront stockées dans le BSS et en vérifiant au débugger qu'une fois arrivé à main, elles valent bien 0.

Après le main

Dans un système embarqué, main ne devrait jamais terminer. Au cas où ça arriverait, pour éviter que le processeur aille exécuter ce qui se trouve dans la zone de code après l'appel à main, insérez après l'appel à main une boucle infinie sur un symbole appelé _exit.

Conclusion

Vous avez maintenant un environnement d'exécution correct pour exécuter du C. Nous pouvons passer à la mise en marche du premier périphérique, le plus basique : l'allumage d'une LED !

Au fait, vous avez pensé à committer / pusher combien de fois ? :)

Mettez le tag LDSCRIPT sur le commit prêt à être corrigé et pushez le.