Développement baremetal sur Raspberry Pi 3

Magazine
Marque
Hackable
Numéro
40
Mois de parution
janvier 2022
Spécialité(s)


Résumé

Le terme « baremetal », également orthographié « bare metal » ou « bare-metal » signifie « métal nu » et dans le contexte de développement sur plateforme embarquée désigne un développement reposant directement sur le matériel, sans la moindre couche d’abstraction. Ce type de programmation courant avec les microcontrôleurs est plus rare avec des plateformes utilisant des SoC puissants ou disposant de beaucoup de ressources. Pour autant, il est parfaitement possible d'utiliser l'ARM Cortex-A53 d'une Raspberry Pi de cette manière. Voyons cela...


Body

Pourquoi donc vouloir utiliser une Raspberry Pi sans GNU/Linux ou un autre système d'exploitation généraliste, et donc ne plus avoir à disposition toutes les fonctionnalités précisément mises à votre disposition pour vous faciliter la vie ? La réponse tient un mot : « performances ». Lorsque vous développez un programme destiné à fonctionner dans un environnement GNU/Linux, par exemple, vous reposez sur un certain nombre de facilités mises à disposition par le système d'exploitation. Ouvrir un fichier et en lire le contenu, par exemple, est un couple d'opérations très courant qui semble simple, et pour cause, le noyau ainsi qu'un certain nombre de bibliothèques vous facilitent grandement la tâche. Vous n'avez pas à vous soucier du type de système de fichiers utilisé, des mécanismes de permissions, de la gestion des accès concurrents, de l'emplacement réel des données sur le support de stockage, de la non-continuité des données sur ce support, ou encore des éventuelles erreurs liées au matériel. Entre vous (votre code) et les données se trouve une importante quantité de couches d'abstraction.

Ces couches d'abstraction ont bien entendu leurs avantages, mais constituent des intermédiaires consommant des ressources. Avec un système comme GNU/Linux, votre programme est loin d'être le seul à utiliser le matériel. Même en l'absence complète d'autres programmes s'exécutant dans l'espace utilisateur (par exemple en développant votre remplaçant d'init ou en faisant exécuter votre programme par le noyau), le noyau s'exécute et utilise mémoire et processeur, qui sont alors factuellement non disponibles pour votre usage personnel.

Avec un microcontrôleur comme un ESP8266, un Atmel AVR ou un STM32, en revanche, votre code est seul à s'exécuter et toutes les ressources sont à votre seule disposition. Vous utilisez directement le matériel sans, souvent, aucune couche d'abstraction et y accédez de façon « brute » (ou « nue »). Ce type de développement est appelé baremetal, même si dans ce cas précis il est inutile de le préciser, puisque c'est la norme. Sur microcontrôleur, au contraire, c'est lorsqu'on ne développe pas de cette façon qu'on précisera l'usage d'un RTOS fournissant un certain nombre de couches d'abstraction.

Développer en baremetal sur une plateforme aussi puissante qu'une Raspberry Pi peut paraître de peu d'intérêt, mais de la même manière qu'on se passe aisément d'interface graphique pour un serveur, il est intéressant de se passer de tout l'environnement utilisateur, voir du noyau lui-même. Bien entendu, cela implique de prendre directement en charge le matériel et les fonctionnalités dont on a besoin, mais le bénéfice est évident. À titre d'exemple, considérons le projet BMC64 [1], un émulateur Commodore 64/128 basé sur l'applicatif VICE, mais fonctionnant sur la Raspberry Pi en baremetal. Le résultat :

  • un démarrage en 4 secondes ;
  • un scrolling parfaitement fluide en 50/60 Hz ;
  • une synchronisation parfaite audio/vidéo ;
  • un arrêt par simple coupure d'alimentation ;
  • une très faible latence sur les entrées.

Tout ceci pour un niveau d'émulation que seul un équipement spécialisé, comme le MISTer [2] reposant sur un FPGA, peut concurrencer. Et bien entendu, les contraintes qu'impose un tel émulateur sont similaires à celles d'applications industrielles (la faible latence en particulier). D'autres exemples existent, à commencer bien entendu par des systèmes spécialisés, comme SO3 [3], qui par définition sont des développements baremetal.

pi3bm pi3bcm2837-s

Le Soc Broadcom BCM2837 est utilisé sur la Raspberry Pi 3 et avec les derniers modèles de Raspberry Pi 2. Il est majoritairement identique au BCM2836 de la Raspberry Pi 2 modèle B et diffère en particulier par son quadruple cœur ARM Cortex A53, remplaçant l'ARMv7 précédent (32 bits).

1. Le cas Raspberry Pi

Le choix d'une plateforme dépend, bien entendu, du projet à implémenter, mais nous souhaitons ici avoir une approche pédagogique. Le plus populaire des SBC sera donc notre victime, du fait de sa disponibilité et de son prix. Il est important cependant de prendre en compte le fait que le SoC de cette famille de cartes est assez spécifique, en particulier au niveau de son démarrage. Pour comprendre cela, commençons par faire le tour de ce qui se trouve sur la première partition de la carte SD d'une Raspberry Pi (les fichiers *.dtb sont délibérément ignorés ici puisqu'ils ne servent qu'à Linux) :

  • bootcode.bin : le bootloader chargé après l'exécution du code en ROM. Il a pour tâche de charger le blob dans le VideoCore (GPU) du SoC Broadcom. Notez que ceci n'est pas utilisé par les Raspberry Pi 4 où une EEPROM contient ce code de boot.
  • start*.elf : le blob (Binary Large OBject), ou « firmware », contenant le code destiné au GPU VideoCore et chargé de configurer le système, démarrer le(s) CPU ARM, puis passer le relais au binaire du noyau Linux. Il existe plusieurs versions de ce blob :
    • start.elf : firmware de base.
    • start_x.elf : firmware incluant l'activation de la caméra et les codecs. Ce fichier est utilisé si start_x=1 est spécifié dans config.txt.
    • start_cd.elf : firmware en version « Cut-Down » (« réduite ») sans support 3D et sans codecs. Celui-ci est utilisé quand gpu_mem=16 est précisé dans config.txt afin de réduire la quantité de mémoire dédiée au GPU et maximiser celle à la disposition du ou des CPU ARM. gpu_mem vaut par défaut 64 sur une Pi avec moins de 1 Go de RAM et 76 pour les Pi avec 1 Go ou plus (le GPU des Pi4 ayant sa propre MMU, la valeur de gpu_mem n'est pas utilisée et la mémoire allouée dynamiquement).
    • start_db.elf : version de debug du blob, chargé si start_debug=1 est présent dans config.txt.
    • start4cd.elf, start4db.elf, start4.elf, start4x.elf : les déclinaisons équivalentes pour Raspberry Pi 4 utilisant un SoC BCM2711 incluant un ARM Cortex-A72 non compatible à ce niveau avec les SoC utilisés avec les précédentes générations de Pi (BCM2835, BCM2836, BCM2837 et BCM2837B0).
  • fixup*.dat : fichier intimement lié à start*.elf et chargé de corriger (fix) le mapping mémoire en fonction de la valeur de gpu_mem. Sur Raspberry Pi, la mémoire est partagée entre le GPU et le CPU, et cette division est configurable par l'utilisateur. De ce fait, l'espace d'adressage doit être ajusté et chaque start*.elf utilise un fixup*.dat correspondant :
    • fixup_cd.dat, fixup.dat, fixup_db.dat et fixup_x.dat pour Pi, Pi2, Pi3.
    • fixup4cd.dat, fixup4.dat, fixup4db.dat et fixup4x.dat pour Pi4.
  • kernel*.img : Il s'agit d'une image du noyau Linux devant être chargé en mémoire par le firmware. Notez que le code de start*.elf déclenche l'exécution du noyau, mais continue de fonctionner sur le GPU. Les deux éléments fonctionnent de concert sur le SoC, exactement comme le firmware chargé sur un adaptateur Wi-Fi fonctionne de pair avec le pilote inclus dans le noyau Linux. Notez que le noyau va utiliser le DTB chargé en mémoire par le firmware. Il existe plusieurs images du noyau, utilisées en fonction du modèle de Raspberry Pi et des préférences de l'utilisateur :
    • kernel.img : Le noyau utilisé par le BCM2835 des Pi et Pi0.
    • kernel7.img : Celui pour les BCM2836 et BCM2837 des Pi2 et Pi3.
    • kernel7l.img : Un noyau 32 bits (architecture ARMv7l) pour le BCM2711 des Pi4. Notez que le « l » dans le nom du fichier signifie LPAE pour Large Physical Address Extension, une fonctionnalité permettant d'utiliser les 8 Go de RAM d'une Pi4 en 32 bits sur architecture ARMv7l (ce « l » là en revanche signifie little-endian et non LPAE).
    • kernel8.img : Le noyau 64 bits (AArch64) pour BCM2837 et BCM2711 des Pi2, Pi3 et Pi4. On parle ici d'une Pi2 avec SoC BCM2837 (RPi 2 modèle B v1.2) avec 4 cœurs Cortex-A53 et non avec un BCM2836 à cœurs Cortex-A7 (RPi 2 modèle B).

Ces fichiers correspondent au minimum nécessaire pour démarrer une Raspberry Pi suivant le processus suivant (pour une Pi 3) :

  • Après mise sous tension, le code en ROM du SoC est exécuté. C'est la boot ROM ou « first stage bootloader ». Ce code cherche le fichier bootcode.bin sur la SD, le charge et l'exécute.
  • Le code dans bootcode.bin prend le relais et configure la DRAM. C'est ce « second stage bootloader » qui cherche le fichier config.txt, le charge et en interprète le contenu pour déterminer quel blob start*.elf doit être chargé. Si config.txt n'est pas trouvé ou lisible, start.elf est utilisé par défaut.
  • Une fois le blob chargé et exécuté, celui-ci charge et consulte config.txt à plusieurs reprises pour configurer et initialiser le système selon les préférences de l'utilisateur et la plateforme. Il charge également le DTB (Device Tree Blob) décrivant la configuration matérielle devant être utilisée par le noyau et en particulier les périphériques non détectables automatiquement, ainsi que le noyau Linux qui prend alors le relais et charge le reste du système. Notez que le nom de l'image du noyau dépend du matériel détecté et du fait que arm_64bit=1 (noyau 64 bits) soit précisé ou non dans config.txt. Vous pouvez également explicitement spécifier le nom du fichier avec kernel=. C'est ce que nous ferons.

Le code de bootcode.bin et de start.elf est destiné au GPU Broadcom VideoCore et non au processeur ARM qui, au moment de la mise sous tension, n'est pas actif. C'est le bootloader qui active la partie ARM du SoC et finit par lui passer le contrôle en parallèle de l'exécution du firmware. Ce comportement, sans être unique aux Raspberry Pi, est relativement peu courant et impose l'utilisation d'un support de stockage SD sur la quasi-totalité des modèles de Raspberry Pi, même pour un système GNU/Linux sur support USB ou en TFTP/NFS (l'exception étant la Pi4 et son EEPROM). Pour développer en baremetal, vous devrez donc disposer d'une SD incluant au minimum les fichiers nécessaires au GPU Broadcom, en plus de votre propre code.

Pour la suite de cet article, nous utiliserons une Raspberry Pi 3 modèle B v1.2 équipée d'un SoC BCM2837 et de 1 Go de RAM. Bien qu'il soit tentant d'opter pour un modèle plus ancien en 32 bits, le choix du BCM2837 nous permet de directement programmer un code 64 bits. Il existe en effet des plateformes 32 bits se prêtant bien mieux à ce genre d'exercice (STM32 par exemple), et le BCM2711 équipant une Pi4 ne présente pas d'avantage notable dans ce contexte. Avec une Pi3 (ou une Pi2 v1.2), nous disposons d'un SoC 4 cœurs 64 bits (AArch64) à un prix raisonnable.

2. De quoi avons-nous besoin ?

Notre objectif ici sera de faire connaissance avec le monde de la programmation baremetal, mais nous souhaitons obtenir un résultat satisfaisant et démonstratif. Nous ferons donc l'impasse sur la classique LED clignotante pour directement afficher un message. Comme l'utilisation de la sortie HDMI nécessite un certain nombre de prérequis, en particulier concernant l'architecture de la plateforme, nous utiliserons une console série. Nous aurons donc besoin d'un convertisseur USB/série 3,3 v afin de connecter la Pi (via les broches 8 et 10, alias GPIO 14 et 15) à la machine de développement faisant fonctionner un émulateur de terminal série comme GNU Screen.

À propos de système de développement, n'importe quel environnement capable de procéder à une compilation croisée vers une cible AArch64 fera l'affaire, mais un système GNU/Linux facilitera grandement les choses (même dans une VM ou via WSL). Vous aurez besoin d'une chaîne de compilation AArch64 qui pourra être installée via le gestionnaire de paquets de votre distribution, sous la forme d'un compilateur natif si vous êtes déjà sur une architecture AArch64 ou idéalement téléchargée depuis developer.arm.com [4] pour avoir une version la plus récente possible. Ce dont vous avez besoin est un compilateur pour votre plateforme à destination de « aarch64-none-elf » (ne pas confondre avec « arm-none-eabi » pour 32 bits ou « aarch64-none-linux-gnu » pour systèmes GNU/Linux 64 bits). La dernière version en date à ce jour est la 10.3-2021.07.

La chaîne de compilation fournit tous les outils nécessaires (compilateur, assembleur, débogueur, éditeur de liens), mais nous aurons également besoin de GNU Make pour faciliter les opérations et bien entendu, d'un bon éditeur de code (Vim, VSCode, etc.).

pi3bm stm32disco-s

Nous avons choisi le BCM2837 des Pi récentes, car il permet de débuter en développement baremetal sur une plateforme 64 bits et non 32. D'autres solutions, bien plus accessibles, existent pour ce type de développement 32 bits, avec davantage de support et de documentation officielle. Comme les nombreux devkits STM32.

3. Développons !

3.1 Plantons le décor

Notre petit projet consistera à simplement envoyer une chaîne de caractères sur l'interface série de la Pi. En espace utilisateur d'un système GNU/Linux, ceci se résumerait à quelques lignes de C, mais il s'agit ici de baremetal. En plus de l'accès direct au matériel, nous devons faire tout un travail préparatoire pour correctement provoquer l'exécution de notre code. La carte SD sur laquelle résidera notre binaire sera préparée exactement comme pour l'exécution d'un noyau Linux (les éléments peuvent être récupérés dans le répertoire boot/ sur https://github.com/raspberrypi/firmware). Les fichiers cités précédemment sont nécessaires au démarrage, mais la configuration, le contenu du fichier config.txt, sera personnalisée ainsi :

# bootloader
uart_2ndstage=1
 
# 64 bits
arm_64bit=1
 
# Mon "kernel"
kernel=hello.bin

Nous avons déjà parlé de arm_64bit et de kernel, mais uart_2ndstage nous permettra, de plus, d'afficher les messages du bootloader du firmware. Attention, ceci n'activera pas pour autant le port série pour notre code.

Pour créer hello.bin, nous allons devoir développer en C et en assembleur. La construction de ce binaire passera par la création d'un Makefile relativement simple :

CROSS=aarch64-none-elf-
CC=$(CROSS)gcc
CFLAGS=-Wall -O0 -g -ffreestanding
LDSCRIPT=rpi3.ld
 
OBJS=crt0.o uart.o hello.o
 
all: hello.bin
 
hello.elf: $(OBJS) $(LDSCRIPT)
        $(CROSS)ld -o $@ $(OBJS) -T$(LDSCRIPT) -Map hello.map
 
hello.bin: hello.elf
        $(CROSS)objcopy -O binary $< $@
 
clean:
        rm -f $(OBJS) *.bin *.elf *.map

Comme vous allez le voir dans le reste de cet article, le développeur dispose d'une grande liberté quant aux choix techniques avec ce type de développement. De ce fait, les documentations en rapport avec la programmation baremetal sur Pi sont diverses et variées, concrétisant souvent les préférences personnelles de l'auteur. Il arrive également qu'un certain nombre de confusions soit fait. Ainsi, on retrouve souvent l'utilisation d'options comme -nostartfiles (pas de fichier start par défaut, cf. plus loin dans l'article) et/ou -nostdlib (pas de libc standard) avec GCC, alors qu'elles ne sont pas toujours nécessaires. Ces arguments sont utilisés avec un compilateur produisant des binaires à destination d'un système GNU/Linux AArch64 et non d'une cible « aarch64-none-elf ». En revanche, -ffreestanding, qu'on peut sauvagement traduire en « je me débrouille tout seul », doit être utilisé et indique au compilateur de ne pas partir du principe que les fonctions standard (comme putc() et puts() que nous utiliserons) sont implémentées avec leurs définitions habituelles.

Notre Makefile est relativement commun, en particulier pour un développement de ce type. Vous remarquerez que l'éditeur de liens prend en compte un script spécifique et produira un fichier .map qui peut être très intéressant pour analyser la source d'un éventuel problème. Avant de nous pencher sur ce point, parlons du premier code exécuté.

3.2 crt0.S

Contrairement à une simple application utilisateur, un programme baremetal doit lui-même préparer le terrain pour l'exécution d'un code écrit en C. Nous en avons brièvement parlé dans un article précédent sur la console Game & Watch [5], c'est un petit programme en assembleur qui a pour tâche de configurer le pointeur pile et de mettre à zéro les variables non initialisées, avant de passer la main au programme principal écrit en C. Dans le cas d'une Pi3, il fait un peu plus que cela :

    .section .text.boot
    .global __start
    .type   __start, %function
 
__start:
    // n'utiliser que le core 0
    mrs     x7, mpidr_el1
    and     x7, x7, #3
    cbz     x7, __start_master
0: wfe
    b       0b
 
__start_master:
    // pointeur de pile
    ldr     x2, =__stack_start
    mov     sp, x2
 
    // Efface BSS
    ldr     w0, =__bss_start
    ldr     w1, =__bss_size
1: cbz     x1, 2f
    str     xzr, [x0], #8
    sub     x1, x1, #1
    cbnz    x1, 1b
2:
    bl      kernelmain
    b       0b

Nous ne nous attarderons ici que sur les points les plus notables de ce code et non sur le sens de chaque instruction. Ce code source est divisé en trois parties : le choix du processeur, l'initialisation du pointeur de pile (stack pointer ou « sp ») et l'exécution de la fonction principale de notre code en C.

Le BCM2837 dispose de 4 cœurs ARM Cortex-A53, mais une fois le relais passé par le firmware, nous nous retrouvons dans une situation où chaque cœur exécutera les instructions en mémoire. Il nous faut donc, avant toute chose, déterminer qui exécute le présent code et, le cas échéant, partir sur une boucle infinie. Les premières lignes du code assembleur font précisément cela, en récupérant le numéro ou ID du cœur et en ne sautant à l'étiquette __start_master que s'il s'agit du cœur 0.

Notez les lignes :

0: wfe
    b       0b

0 est ici une étiquette (ou label) et b est l'instruction procédant au saut. Il s'agit d'une étiquette locale numérique susceptible d'être utilisée plusieurs fois dans le code (ce n'est pas le cas ici) et l'assembleur a besoin de savoir dans quelle direction le saut se fait. 0b désigne donc l'étiquette 0 se trouvant avant l'instruction courante, avec « b » comme backward. On retrouve le même type de fonctionnement avec « f » pour forward. Bien sûr, nous aurions parfaitement pu utiliser des étiquettes standard, mais vous risquez très probablement de tomber sur ce type de syntaxe en cherchant d'autres codes similaires.

Dans la seconde partie du code, nous initialisons le registre SP avec la valeur symbolisée par __stack_start que nous définirons par ailleurs. Rappelez-vous que la pile croît dans le sens opposé du tas, d'une adresse haute vers le bas. SP doit donc être initialisé loin de notre code en mémoire de façon à ce que la pile n'écrase ni le code ni les données. BSS est initialisé peu après et, là encore, nous utilisons deux symboles, __bss_start et __bss_size, représentant respectivement le début de la section et sa taille.

Enfin, la dernière partie du code consiste à appeler kernelmain qui sera défini dans notre code en C. Nous aurions simplement pu appeler cette fonction main, mais ceci illustre, je trouve, parfaitement la liberté dont nous jouissons dans ce genre de développement (merci -ffreestanding, sans qui le compilateur se plaint de l'absence de main (entre autres choses)).

pi3bm adaptserie-s

Les adaptateurs USB/série existent sous bien des formes, prix, disponibilités et niveaux de qualité. C'est généralement une bonne chose que d'en disposer en de nombreuses déclinaisons, ce qui a tendance à arriver naturellement au fil du temps...

3.3 Le script pour l'éditeur de liens

À ce stade, plutôt que de nous pencher sur le code en C, il est important de clarifier certaines choses. Dans le code assembleur, nous utilisons différents symboles qui semblent sortir de nulle part. Ce n'est bien entendu pas le cas. Ceux-ci désignent des emplacements en mémoire qui ne sont pas définis pour l'instant et ne peuvent l'être qu'au moment de l'édition de liens, lorsque la taille du code binaire est connue. Nous arrivons là sur un terrain où les choix techniques sont en grande partie ceux du développeur, puisqu'il y a plus d'une façon d'organiser la mémoire de son projet.

Le plan choisi ici consiste à conserver l'adresse mémoire classique de chargement d'un noyau (0x80000 en 64 bits) et de spécifier à l'éditeur de liens qu'il s'agit de l'adresse d'origine de la mémoire. Ceci nous permet d'avoir une sortie « cohérente » des commandes comme objdump. Notre code binaire sera donc placé par le firmware à cette adresse, sera suivi de la section BSS, d'un trou et de l'adresse de départ de la pile :

MEMORY
{
  RAM (rwx) : ORIGIN = 0x80000, LENGTH = 256M
}
 
SECTIONS
{
  .text : { *(.text .text.*) }
  .rodata : { *(.rodata .rodata.*) }
  .data : { *(.data .data.*) }
 
  .bss (NOLOAD):
  {
   __bss_start = ALIGN(0x10);
   *(.bss .bss.*)
   __bss_end = ALIGN(0x10);
  }
  __end = .;
 
  . = ALIGN(0x10);
  . += 0x1000;
  __stack_start = .;
 
  /DISCARD/ :
  {
   *(.comment) *(.gnu*)
   *(.note*) *(.eh_frame*)
  }
}
 
__bss_size = (__bss_end - __bss_start) >> 3;

On retrouve dans ce script notre __stack_start initialisant SP, ainsi que les symboles permettant de mettre à zéro les variables en BSS.

D'autres variations sont possibles. Nous pouvons par exemple démarrer à l'adresse 0x0, à condition de spécifier kernel_old=1 et disable_commandline_tags=1 dans le config.txt. Nous pouvons également oublier la partie MEMORY et ajouter un . = 0x80000; avant la section .text. L'emplacement de la pile peut aussi être sujet à changement, puisque rien ne nous empêche d'initialiser SP avec l'adresse de __start et donc avoir une pile placée avant notre code. Nous faisons littéralement ce que nous voulons ou pouvons même, éventuellement, changer d'avis en cours de développement. Avec ce script, la pile a une taille maximum de 4 Ko, ce qui est bien suffisant pour un exemple aussi simple, mais c'est à vous d'adapter cela en fonction de vos projets et préférences.

3.4 uart.h et uart.c

Nous avons maintenant de quoi démarrer sereinement la partie en C. Nous pourrions à ce stade nous plier d'un hello.c simpliste se limitant à une fonction void kernelmain() incluant une boucle while, mais autant nous attaquer sans attendre à ce qui est l'une des grandes difficultés de la programmation baremetal : l'accès aux périphériques.

Comme avec n'importe quel microcontrôleur, les différents registres utilisés pour configurer et contrôler les périphériques sont mappés en mémoire. Dans le cas d'une Raspberry Pi 3 et son BCM2837, il n'y a pas de datasheet officielle disponible et nous devons nous référer à celle du BCM2835 [6], tout en prenant en compte un certain nombre de différences. Il est également possible de se référer aux sources des fichiers DTB [7]. Le BCM2837 est d'ailleurs identique au BCM2836, si ce n'est concernant les cœurs ARM (ARMv7 vs ARMV8), le BCM2836 lui-même étant similaire au BCM2835, si ce n'est là encore par une différence en termes de processeur (ARM1176JZF-S vs cluster Cortex-A7 (ARMv7). Ceci explique, partiellement, l'absence de datasheets pour chaque SoC (ou pas).

Dans cette documentation du BCM2835, on apprend que les périphériques sont mappés à l'adresse physique 0x20000000 pour le BCM2835 (point 1.2.3, page 6) alors qu'il s'agit de 0x3f000000 pour le BCM2837. En dehors de cela, le reste des informations peut s'utiliser à l'identique... À une particularité près : toute la documentation est écrite du point de vue du GPU qui voit des « bus addresses » alors que l'ARM voit des « ARM physical addresses ». Le schéma en page 5 du document résume l'architecture très spécifique aux SoC Broadcom des Pi.

Ainsi, en page 8, on apprend que le début des registres contrôlant les périphériques auxiliaires consistant en un port « mini UART » et deux bus SPI se trouve à 0x7e215000. Encore une fois, ceux-ci sont des bus addresses avec un mappage des périphériques à partir de 0x7e000000 et non quelque chose de directement manipulable depuis le « côté ARM ». Pour traduire cette adresse en quelque chose d'utilisable, il suffit de décaler les adresses spécifiées de 0x7e000000 à 0x3f000000. Ainsi, pour nous, le registre AUX_ENABLES se trouve à l'adresse 0x3f215004 et, plus précisément, via quelques déclarations de macros nous permet de créer un fichier uart.h :

#pragma once
 
// mapping périphériques
#define IO_BASE 0x3f000000
// mapping mini Uart
#define MU_BASE (IO_BASE + 0x215000)
// mapping GPIO
#define GP_BASE (IO_BASE + 0x200000)
 
// Auxiliary enables
#define AUX_ENB (*(volatile unsigned *)(MU_BASE + 0x04))
// Mini Uart I/O Data
#define MU_IO   (*(volatile unsigned *)(MU_BASE + 0x40))
// Mini Uart Line Control
#define MU_LCR (*(volatile unsigned *)(MU_BASE + 0x4c))
// Mini Uart Line Status
#define MU_LSR (*(volatile unsigned *)(MU_BASE + 0x54))
// Mini Uart Extra Control
#define MU_CNTL (*(volatile unsigned *)(MU_BASE + 0x60))
// Mini Uart Baudrate
#define MU_BAUD (*(volatile unsigned *)(MU_BASE + 0x68))
 
// GPIO Function Select 1
#define GPFSEL1 (*(volatile unsigned *)(GP_BASE + 0x04))
// GPIO Pin Pull-up/down Enable
#define GPPUD   (*(volatile unsigned *)(GP_BASE + 0x94))
// GPIO Pin Pull-up/down Enable Clock 0
#define GPPUDCLK0   (*(volatile unsigned *)(GP_BASE + 0x98))
 
void init_uart(void);
void puts(const char *s);

Ces macros désignent non seulement les registres liés au mini-UART, mais également ceux (page 90 de la datasheet) en rapport avec les GPIO, puisque c'est par GPFSEL1, par exemple, que nous pouvons spécifier les modes de fonctionnement alternatifs des broches à notre disposition. Nous profitons de l'écriture de ce fichier pour inclure les prototypes des fonctions que nous utiliserons ensuite dans hello.c. Grâce à ces informations, nous pouvons créer uart.c :

#include <stdint.h>
#include "uart.h"
 
void init_uart(void)
{
  // Active mini-uart
  AUX_ENB |= 1;
  // 8 bits de data
  MU_LCR = 3;
  // 115200 bps
  MU_BAUD = 270;
  // GPIO 14 & 15 en alt5
  GPFSEL1 &= ~((7 << 12) | (7 << 15));
  GPFSEL1 |=   (2 << 12) | (2 << 15);
 
  // Pas de pull-up/down.
  GPPUD = 0;
  // pause (cf. synopsis p.101)
  for(uint8_t i = 0; i < 150; i++)
    asm volatile ("nop");
  GPPUDCLK0 = (2 << 14) | (2 << 15);
  // pause
  for(uint8_t i = 0; i < 150; i++)
    asm volatile ("nop");
  GPPUD = 0;
  GPPUDCLK0 = 0;
 
  MU_CNTL = 3; // active Tx & Rx
}
 
 
void putc(char c)
{
  while(!(MU_LSR & 0x20))
   ;
  MU_IO = c;
}
 
void puts(const char *s)
{
  while(*s)
    putc(*s++);
}

Le code est assez basique, puisque nous nous contentons, dans init_uart(), de configurer le périphérique et de changer la fonctionnalité liée aux GPIO 14 et 15. Pour implémenter puts(), dont nous nous servirons pour envoyer une chaîne de caractères via l'adaptateur USB/série, il nous suffira d'envoyer les caractères un par un via putc(). Cette fonction se contente de vérifier le bit 5 du registre AUX_MU_LSR_REG pour nous assurer que le FIFO est vide et nous plaçons ensuite le caractère dans AUX_MU_IO_REG.

Une petite remarque à propos de l'initialisation. La configuration des GPIO et en particulier des résistances de rappel haut/bas suit une procédure précise décrite dans la documentation (page 101). Il s'agit de spécifier la configuration, puis de « clocker » celle-ci en manipulant GPPUDCLK0, avec une attente impérative de 150 cycles. Comme c'est généralement le cas pour la plupart des microcontrôleurs, il est très important de lire la datasheet très méticuleusement, de préférence avant de rencontrer des problèmes qui semblent aléatoires...

pi3bm 3v3-s

Un point très important concernant la liaison série porte sur les tensions utilisées. Les Pi utilisant 0/3,3 v et n'étant (en principe) pas tolérantes au 5 v, il est fortement conseillé d'opter pour un adaptateur permettant de sélectionner la tension via un cavalier ou un interrupteur. Il n'est pas rare, en effet, que les adaptateurs peu chers soient 0/5 v malgré le fait que leur descriptif mentionne 3,3 v.

3.5 hello.c

Le plus gros du travail est fait. Il ne nous reste plus qu'à nous occuper de hello.c, qui est d'une simplicité exemplaire :

#include <stdint.h>
#include "uart.h"
 
int kernelmain(void)
{
    init_uart();
 
    puts("\r\n\r\n\r\nBonjour Hackable !\r\n");
 
    while(1) {
        ;
    }
 
    return 0;
}

Un simple petit make nous permettra de compiler tout cela et de produire un fichier hello.bin d'une taille ridicule (547 octets, à pleine plus que config.txt), que nous placerons sur la SD en compagnie des autres fichiers cités en début d'article. Dès la mise sous tension de la Pi3, nous verrons ceci apparaître sur la liaison série (115200 8N1) :

Raspberry Pi Bootcode
Read File: config.txt, 447
Read File: start.elf, 2965632 (bytes)
Read File: fixup.dat, 7279 (bytes)
MESS:00:00:01.150803:0: brfs: File read: /mfs/sd/config.txt
MESS:00:00:01.155198:0: brfs: File read: 447 bytes
[..]
MESS:00:00:01.281683:0: brfs: File read: /mfs/sd/config.txt
MESS:00:00:01.286403:0: gpioman: gpioman_get_pin_num: pin LEDS_PWR_OK not defined
MESS:00:00:02.109839:0: gpioman: gpioman_get_pin_num: pin DISPLAY_DSI_PORT not defined
MESS:00:00:02.117377:0: gpioman: gpioman_get_pin_num: pin LEDS_PWR_OK not defined
MESS:00:00:02.123305:0: *** Restart logging
MESS:00:00:02.127181:0: brfs: File read: 447 bytes
[..]
MESS:00:00:02.201275:0: hdmi: HDMI0:EDID giving up on reading EDID block 0
MESS:00:00:02.290809:0: HDMI0: hdmi_pixel_encoding: 162000000
MESS:00:00:02.296506:0: vec: vec_middleware_power_on: vec_base: 0x7e806000
   rev-id 0x00002708 @ vec: 0x7e806100 @ 0x00000420 enc:
   0x7e806060 @ 0x00000220 cgmsae: 0x7e80605c @ 0x00000000
MESS:00:00:02.317910:0: dtb_file 'bcm2710-rpi-3-b.dtb'
MESS:00:00:02.321780:0: Failed to load Device Tree file '?'
MESS:00:00:02.327073:0: Failed to open command line file 'cmdline.txt'
MESS:00:00:02.335924:0: brfs: File read: /mfs/sd/hello.bin
MESS:00:00:02.339712:0: Loading 'hello.bin' to 0x80000 size 0x223
MESS:00:00:02.345602:0: gpioman: gpioman_get_pin_num: pin EMMC_ENABLE not defined
MESS:00:00:02.354390:0: uart: Set PL011 baud rate to 103448.300000 Hz
MESS:00:00:02.360684:0: uart: Baud rate change done...
MESS:00:00:02.364114:0: uart: Baud rate
 
 
Bonjour Hackable !

Le fait d'activer les messages du bootloader nous permet de suivre la progression du démarrage et de connaître les fichiers utilisés. Remarquez également que les lignes étant horodatées, nous apprenons que le processus complet prendra moins de trois secondes. Ce code est relativement simple, mais constitue une base pratique pour entamer un développement. N'hésitez pas à explorer et à tester des variations, en particulier sur l'aspect « organisation mémoire ». Les sources de BMC64 méritent également un coup d’œil, ainsi que Circle [8], un environnement de programmation baremetal C++ utilisable sur tous les modèles de Pi (si vous aimez le C++).

4. Pour finir

Si vous vous lancez plus avant dans ce genre de développement, votre meilleur ami sera bcm2835-peripherals.pdf. C'est non seulement une excellente source d'informations concernant les différents périphériques à votre disposition, mais aussi, et surtout, c'est presque la seule. En effet, Broadcom est relativement avare en termes de documentation et un certain nombre de fonctionnalités sont peu ou pas documentées. Fort heureusement, vous pourrez également vous référer aux sources du noyau Linux version Raspberry Pi [9], contenant littéralement tout le code nécessaire pour supporter ce que vous utilisez habituellement avec ce matériel. Nous reviendrons sur le sujet dans un instant avec l'article suivant, pour parler du mécanisme de communication entre l'ARM et le GPU (système de mailbox) et de l'utilisation de la sortie HDMI.

Références

[1] https://accentual.com/bmc64/

[2] https://connect.ed-diamond.com/Hackable/hk-035/mister-la-solution-retro-ultime

[3] https://smartobject.gitlab.io/so3/index.html

[4] https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads

[5] https://connect.ed-diamond.com/contenu-premium/game-watch-utilisons-judicieusement-la-memoire ainsi que dans celui sur le développement en assembleur sur ARM :
https://connect.ed-diamond.com/hackable/hk-039/assembleur-sur-arm-cortex-m-technique-mais-pas-si-difficile...

[6] https://datasheets.raspberrypi.com/bcm2835/bcm2835-peripherals.pdf

[7] https://github.com/raspberrypi/linux/tree/rpi-5.10.y/arch/arm/boot/dts

[8] https://github.com/rsta2/circle

[9] https://github.com/raspberrypi/linux



Article rédigé par

Par le(s) même(s) auteur(s)

Édito : Heu... Arduino ? Vous faites quoi, là ?

Magazine
Marque
Hackable
Numéro
50
Mois de parution
septembre 2023
Résumé

Récemment, les nouvelles cartes Arduino UNO R4 Minima et UNO R4 WiFi ont fait leur apparition et sont très souvent présentées (ne serait-ce qu'en raison de leur nom) comme les successeurs de la bonne vieille UNO Rev3 (ou R3). La belle affaire, me direz-vous, la UNO avait bien besoin d'une mise à jour et, en effet, le changement est violent. Au revoir l'AVR 8 bits obsolète et bonjour le microcontrôleur Renesas RA4M1 (ARM Cortex-M4) !

Jouons aux LEGO... avec des tags NFC

Magazine
Marque
Hackable
Numéro
50
Mois de parution
septembre 2023
Spécialité(s)
Résumé

LEGO Dimensions est un jeu vidéo sorti en 2015 et reposant sur un concept original, également utilisé par d'autres éditeurs à la même époque, comme Nintendo avec Amiibo ou Disney Interactive Studios avec Disney Infinity. L'idée de base consiste à utiliser des objets ou figurines équipés d'un tag NFC et permettant des interactions entre le jeu vidéo et le monde physique/réel. Ce type de jeux semble aujourd'hui passé de mode, mais le matériel reste très intéressant et surtout, réutilisable dans un autre contexte.

Carte Z180 : le mystère de la RAM

Magazine
Marque
Hackable
Numéro
50
Mois de parution
septembre 2023
Spécialité(s)
Résumé

Dans l'épisode précédent, nous avons fait connaissance avec une carte industrielle à base de Z180 et nous sommes fixé comme objectif d'en faire notre « ordinateur 8 bits ». Nous avons chargé et exécuté notre programme en EEPROM et l'avons fait communiquer via une liaison série. Pour aller plus loin, nous devons cependant régler un problème qui consiste à comprendre comment est gérée la RAM et arriver à l'utiliser pour, au minimum, disposer d'une pile (stack) nous permettant d'appeler des sous-routines. Attaquons-nous au problème sans attendre...

Les derniers articles Premiums

Les derniers articles Premium

Stubby : protection de votre vie privée via le chiffrement des requêtes DNS

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Depuis les révélations d’Edward Snowden sur l’espionnage de masse des communications sur Internet par la NSA, un effort massif a été fait pour protéger la vie en ligne des internautes. Cet effort s’est principalement concentré sur les outils de communication avec la généralisation de l’usage du chiffrement sur le web (désormais, plus de 90 % des échanges se font en HTTPS) et l’adoption en masse des messageries utilisant des protocoles de chiffrement de bout en bout. Cependant, toutes ces communications, bien que chiffrées, utilisent un protocole qui, lui, n’est pas chiffré par défaut, loin de là : le DNS. Voyons ensemble quels sont les risques que cela induit pour les internautes et comment nous pouvons améliorer la situation.

Surveillez la consommation énergétique de votre code

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Être en mesure de surveiller la consommation énergétique de nos applications est une idée attrayante, qui n'est que trop souvent mise à la marge aujourd'hui. C'est d'ailleurs paradoxal, quand on pense que de plus en plus de voitures permettent de connaître la consommation instantanée et la consommation moyenne du véhicule, mais que nos chers ordinateurs, fleurons de la technologie, ne le permettent pas pour nos applications... Mais c'est aussi une tendance qui s'affirme petit à petit et à laquelle à terme, il devrait être difficile d'échapper. Car même si ce n'est qu'un effet de bord, elle nous amène à créer des programmes plus efficaces, qui sont également moins chers à exécuter.

Donnez une autre dimension à vos logs avec Vector

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Avoir des informations précises et détaillées sur ce qu’il se passe dans une infrastructure, et sur les applications qu'elle héberge est un enjeu critique pour votre business. Cependant, ça demande du temps, temps qu'on préfère parfois se réserver pour d'autres tâches jugées plus prioritaires. Mais qu'un système plante, qu'une application perde les pédales ou qu'une faille de sécurité soit découverte et c'est la panique à bord ! Alors je vous le demande, qui voudrait rester aveugle quand l'observabilité a tout à vous offrir ?

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 90 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous