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...
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.
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.).
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 :
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 :
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 :
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 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)).
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 :
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 :
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 :
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...
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 :
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) :
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
[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