Dans le précédent article, nous avons vu comment prendre en main le STM32MP157F-DK2, mettre à jour le système, utiliser le SDK et reconstruire l'ensemble avec OpenEmbedded sur la base d'OpenSTLinux, mais aussi de Poky. Le système de build du projet Yocto n'est cependant pas le seul utilisable et, grâce au travail de Bootlin en partenariat avec ST, il est également possible d'utiliser Buildroot. Voyons cela ensemble...
La philosophie de Buildroot est sensiblement différente de celle d'OpenEmbedded. Il n'y a pas ici de layers (couches) s'ajoutant les uns aux autres sur une base minimale pour composer une distribution qu'on enrichira, ensuite, avec un ou des layers personnalisés. Buildroot est un ensemble complet destiné à produire un système en fonction des éléments que le développeur activera ou non, via une interface similaire à celle utilisée pour configurer le noyau Linux (outil kconfig). L'objectif reste identique, que ce soit avec OpenEmbedded ou Buildroot, nous voulons un noyau, une chaîne de compilation, un bootloader et un système de fichiers racine, mais la façon de construire cet ensemble sera radicalement différente. Autre différence importante, il n'y a pas de notion de paquet avec Buildroot, l'objectif étant de produire un système complet en une fois.
Mais ce qui distingue réellement les deux systèmes de build sera avant tout leur complexité relative en termes de configuration. OpenEmbedded est très modulaire, ce qui implique que la configuration se trouve dispersée dans une collection de fichiers, dont les interactions et les dépendances peuvent être difficiles à appréhender. Buildroot, en revanche, centralise la configuration en un seul fichier et un seul projet, rendant l'ensemble plus simple à embrasser globalement. Comme nous le verrons plus loin dans l'article, ceci n'empêche pas l'intégration d'éléments externes maintenus en parallèle (BR2_EXTERNAL), mais ce n'est pas la base de l'architecture, contrairement à ce que propose le projet Yocto.
La préférence envers l'un ou l'autre système est une affaire de goût personnel, mais aussi, et surtout, de disponibilité de prise en charge de la cible (SoC ou devkit) choisie. Fort heureusement, dans le cas du STM32MP157, les deux environnements de construction sont parfaitement supportés, ce qui en fait une plateforme idéale d'un point de vue pédagogique. En effet, quel que soit votre sentiment sur ces systèmes de build, il est important de ne pas privilégier l'un au détriment de l'autre, car les deux sont activement maintenus et utilisés. Il faut donc savoir utiliser à la fois OpenEmbedded et Buildroot.
Dans le précédent article [1], nous avons fait connaissance avec la distribution OpenSTLinux et ce choix n'était pas totalement innocent. Non seulement il s'agit de la distribution officielle initialement supportée, mais sa relation avec OpenEmbedded implique davantage d'efforts de compréhension. Face à cela, un des objectifs de Buildroot est de rester concis et simple, le rendant facile à comprendre, en particulier pour un utilisateur GNU/Linux connaissant son sujet. Par rapport à Yocto, c'en est presque un soulagement...
1. Construire un système avec Buildroot
Contrairement à OpenEmbedded, nous n'avons ici qu'une seule source pour récupérer Buildroot, ou presque. En effet, à l'heure où est composé cet article, les deux devkits STM32MP157 ne sont encore que partiellement supportés par le projet officiel (mais l'intégration est en cours). Il est donc nécessaire de compléter l'environnement avec une branche externe (BR2_EXTERNAL) ajoutant des éléments de configuration qui ne font pas partie de l'arborescence officielle. Notez que même si ce mécanisme ressemble aux layers d'OpenEmbedded, avec Buildroot il n'est possible de l'utiliser qu'une fois et ceci uniquement pour ajouter des éléments et non écraser ceux qui sont existants. C'est une solution pour ajouter le support d'une plateforme, mais de préférence comme phase préliminaire d'intégration au projet officiel.
Étant donné la nature « temporaire » du support des cartes DK, il sera nécessaire d'utiliser un fork Bootlin de Buildroot [2] en version LTS (2021.02.*) en compagnie de l'arborescence externe, mais tout ceci est susceptible d'évoluer dans le futur. Quoi qu'il en soit, Buildroot étant très facile à « comprendre », adapter ces explications par la suite ne devrait pas être un problème.
Nous commençons donc par récupérer l'arborescence « officielle » depuis GitHub, ainsi que l'arborescence externe :
Notez l'utilisation de branches Git identiques pour les deux dépôts afin de disposer de configurations parfaitement synchronisées. Nous avons maintenant deux répertoires, buildroot pour l'environnement de construction standard et buildroot-external-st constituant l'arborescence externe. La prochaine étape consiste donc à lier les deux :
Nous utilisons ici l'une des configurations par défaut (defconfigs) fournies par l'arborescence externe, destinée au SoC STM32MP157CACx et incluant un certain nombre de codes de démonstration. La variable d'environnement BR2_EXTERNAL nous permet de spécifier l'emplacement de l'arborescence externe durant cette phase de configuration, mais il ne sera pas nécessaire de l'utiliser par la suite, ceci est inclus dans la configuration locale (BR2_EXTERNAL_ST_PATH dans le .config). Vous pouvez lister toutes les configurations par défaut, de la branche principale et de l'arborescence externe avec un simple make list-defconfigs. Remarquez que nous utilisons une carte STM32MP157F-DK2, équipée d'un SoC STM32MP157FACx, sensiblement différent du STM32MP157CACx équipant le STM32MP157C-DK2 mais majoritairement compatible (ce qui expliquera deux messages d'erreur concernant l'OPP, pour Operating Performance Points, au moment du boot, le devicetree pour le STM32MP157CACxdu ne spécifiant pas de table OPP).
Nous avons ici un mécanisme assez similaire à une configuration de noyau Linux, puisque nous appliquons une configuration par défaut qui sera copiée dans un .config local. Nous pourrons ensuite modifier ce fichier à l'aide d'un make menuconfig pour éventuellement ajuster les options. Ceci est généralement une bonne idée, en particulier sur une machine où vous travaillez en parallèle. Dans le menu présenté avec un make menuconfig, visitez « Build options » et vous trouverez l'entrée « Number of jobs to run simultaneously » ajustant la valeur de BR2_JLEVEL, correspondant à l'option -j de make. La configuration par défaut règle cette valeur automatique sur le nombre de CPU (ou cœurs/threads) plus 1. En réduisant celle-ci, à la moitié par exemple, vous permettez à votre système de rester réactif pendant la construction. Vous pouvez également activer « Enable compiler cache » (BR2_CCACHE) permettant d'utiliser ccache afin d'accélérer grandement les compilations consécutives (le cache sera créé dans ~/.buildroot-ccache/ par défaut, mais peut être ajusté également). Enfin, l'entrée « gcc optimization level » correspond à l'option -O de GCC et donc au niveau d'optimisation du compilateur. Le réglage par défaut sur -Os (BR2_OPTIMIZE_S) optimise pour la taille, mais vous pouvez vouloir optimiser pour les performances (-O3 / BR2_OPTIMIZE_3) ou ne pas optimiser du tout (-O0 / BR2_OPTIMIZE_0).
Dans la suite de cet article, je préciserai systématiquement, entre parenthèses, le nom de l'élément de configuration tel qu'il apparaît dans le fichier de configuration (.config). L'interface accessible via make menuconfig, comme celle du noyau Linux permet de procéder à une recherche sur ces éléments via le raccourci /, vous présentant alors le ou les résultats, accompagnés d'un descriptif, des dépendances liées et aussi de l'emplacement de l'option dans l'arborescence de menus pour y accéder facilement.
Une fois satisfait de vos réglages, quittez l'interface de configuration et lancez la construction d'un simple make :
Comme avec d'autres systèmes de construction, Buildroot va tout d'abord produire une chaîne de compilation adaptée à la cible (deux dans le cas présent, une pour le Cortex-A7 et la seconde pour le M4 afin de compiler les codes de démonstration), puis télécharger les sources des composants du système pour les extraire et les compiler. Enfin, une fois cet ensemble d'étapes accomplies, le ou les systèmes de fichiers seront assemblés pour produire une image qu'il vous sera possible de flasher sur la plateforme. L'ensemble de la procédure, sur mon bi-Xeon E5520, avec la configuration par défaut (avec les codes de démonstration et donc Qt) et 10 jobs prend initialement une bonne heure. Moins que le build OpenEmbedded, mais ceci n'est pas réellement comparable étant donné que le système produit n'est que vaguement similaire.
Au final, vous obtenez dans output/images/ le résultat de la construction sous la forme de plusieurs fichiers :
- fip.bin : le FIP ou Firmware Image Package regroupant de façon structurée et binaire les bootloaders, un devicetree et un certificat pour le boot TF-A (Trusted Firmware-A [3]).
- rootfs.ext2 : une image du système de fichiers racine au format EXT4 (l'extension ext2 étant présente pour des raisons de compatibilité).
- rootfs.ext4 : un lien symbolique vers rootfs.ext2 avec une extension plus adaptée.
- sdcard.img : une image du support amovible contenant plusieurs partitions GPT (FSBL1, FSBL2, FIP et système de fichiers racine).
- stm32mp157c-dk2.dtb : un devicetree binaire utilisé par le noyau Linux pour la prise en charge des périphériques non détectables.
- tf-a-stm32mp157c-dk2-mx.stm32 : un bootloader de premier niveau (ou FSBL pour First Stage BootLoader) utilisé comme FSBL1 et FSBL2.
- tee.bin, tee-header_v2.bin, tee-pageable_v2.bin et tee-pager_v2.bin : les éléments composant l’environnement d'exécution OP-TEE, la partie sécurisée du système Trust Zone.
- u-boot.dtb : le devicetree binaire utilisé par le bootloader U-Boot.
- u-boot-nodtb.bin : le binaire U-Boot lui-même.
- zImage : l'image du noyau Linux.
Comme avec OpenSTLinux, ces éléments peuvent être utilisés pour écrire la microSD ou être utilisés avec STM32CubeProgammer. Pour ce faire, la branche externe développée par Bootlin intègre un fichier TSV que vous trouverez dans buildroot-external-st/board/stmicroelectronics/stm32mp157 sous le nom flash.tsv. Pour mettre à jour votre devkit, vous pouvez placer les micro-interrupteurs BOOT0 et BOOT2 sur OFF, connecter la carte en USB-C, l'alimenter puis utiliser :
Une fois l'opération terminée, repositionnez les micro-interrupteurs sur ON et procédez à un reset via le bouton dédié. En utilisant la console série mise à votre disposition via le connecteur ST-LINK/V2-1 (ttyACM et 115200 8N1), vous devrez alors constater le démarrage du système et arriver à l'invite de connexion. Vous pouvez alors vous identifier comme root et obtenir une invite de shell sans saisir de mot de passe. Bravo, vous avez construit, flashé et démarré votre premier système GNU/Linux sur STM32MP157F-DK2 avec Buildroot.
2. Procédons à quelques ajustements
Si vous faites le tour du propriétaire, vous vous rendrez rapidement compte que tout ceci est assez spartiate. Vous n'avez, par exemple, ni console sur l'écran LCD ni services intéressants (SSH, Avahi, etc.) ou même de connectivité réseau. Vous trouverez cependant quelques éléments intéressants découlant de la configuration utilisée par défaut, comme le contenu de /usr/lib/qt/examples réunissant quelques exemples amusants de ce qu'il est possible de faire avec Qt, ou encore dans /usr/lib/Cube-M4-examples, un jeu de firmwares à destination du coprocesseur Cortex-M4 intégré au STM32MP157.
Même si la plateforme n'est en rien destinée à être une sorte de mini-ordinateur générique, un minimum de confort est nécessaire, ne serait-ce que pour sereinement développer sa ou ses applications pour ensuite les faire s'exécuter sur la cible. Notre objectif est de changer la configuration de Buildroot pour disposer d'un système toujours aussi light, mais capable de supporter un minimum de manipulations (oui, c'est une excuse pour découvrir les spécificités de Buildroot).
Une partie des modifications que nous souhaitons apporter impliquent l'utilisation de fichiers qui vont venir s'ajouter en complément de ceux déjà utilisés ou remplacer ceux existants. Il nous faut donc un emplacement pour stocker ces fichiers. Un parfait exemple concerne l'ajout d'un utilisateur standard, qui passe généralement par la création d'un fichier (la users table) stockant les informations utilisées par Buildroot. L'emplacement recommandé pour un tel fichier, selon la documentation officielle, est board/<company>/<boardname>/ et donc, dans le board/stmicroelectronics/stm32mp157 de buildroot-external-st. Nous ne voulons cependant pas modifier notre copie du dépôt de Bootlin.
Nous pourrions forker ce dépôt sur GitHub, mais dans ce cas, il ne nous sera pas possible d'en réduire la visibilité pour le rendre privé. La solution consiste donc à créer un dépôt vide quelque part (GitHub ou ailleurs) avec git init --bare et de tout simplement ajouter le dépôt distant avec git remote add, puis de faire un git push -u suivi du nouveau nom. Nous pouvons même basculer sur la nouvelle branche avec git checkout -b et donc suivre nos modifications avec Git, sans pour autant risquer de nous mélanger les pinceaux.
2.1 Réseau, SSH, etc.
La première chose qu'on peut souhaiter activer est le support réseau ou plus exactement sa configuration. En effet, les interfaces sont bien présentes, comme en témoigne la sortie d'un simple ifconfig -a ou ip addr. Nous retrouvons là l'interface loopback (lo), Ethernet (eth0), Wi-Fi (wlan0) et l'interface virtuelle IPv6/IPv4 (sit0). Ce qui nous manque en revanche, c'est la configuration de l'interface eth0, comme en témoigne le contenu du fichier /etc/network/interfaces :
Le client DHCP est également déjà présent puisqu'il s'agit de celui fourni par BusyBox [4], /sbin/udhcpc. Nous pouvons d'ailleurs nous assurer du bon fonctionnement de l'ensemble en l'invoquant manuellement :
Pour modifier la configuration en place, nous pouvons utiliser la notion d'overlay de Buildroot. Cette approche nous permet de modifier le système de fichiers racine avant la création de l'image et un certain nombre de choses sont d'ores et déjà ajustées de cette façon. Un coup d’œil au répertoire buildroot-external-st/board/stmicroelectronics/stm32mp157/dk2-overlay nous montre, entre autres choses, une configuration ALSA via etc/asound.conf. Pour ajouter le support DHCP, il nous suffit donc de créer un fichier interfaces dans un sous-répertoire network/ de etc/ contenant :
Nous pouvons également choisir de ne pas changer l'arborescence existante et dupliquer dk2-overlay sous un autre nom avant d'y faire notre ajout. Il suffira alors de faire la modification, via make menuconfig, « System configuration » puis « Root filesystem overlay directories » (BR2_ROOTFS_OVERLAY) pour adapter le chemin correspondant (relatif à BR2_EXTERNAL_ST_PATH). Ceci d'autant que, puisque nous sommes dans l'interface, nous pouvons également en profiter pour :
- changer le nom d'hôte de « buildroot » en quelque chose de plus reconnaissable (« stm32mp1br » par exemple) : « System configuration », « System hostname » (BR2_TARGET_GENERIC_HOSTNAME) ;
- activer le serveur SSH : « Target packages », « Networking applications », « dropbear » (BR2_PACKAGE_DROPBEAR) ;
- activer Avahi pour la résolution mDNS : « Target packages », « Networking applications », « avahi » (BR2_PACKAGE_AVAHI et BR2_PACKAGE_AVAHI_DAEMON).
En quittant l'interface, sauvegardez les changements (dans .config), puis relancer la construction avec un simple make. De nouvelles archives sources seront automatiquement téléchargées, désarchivées, compilées et intégrées au système de fichiers racine pour produire une nouvelle image que vous pourrez immédiatement flasher, comme précédemment. Toutes les dépendances liées aux deux ajouts auront bien entendu été traitées dans le même temps.
En surveillant sur la console série ce premier redémarrage, on pourra constater le lancement du serveur SSH, ainsi que la résolution DHCP et l'exécution du démon avahi-daemon. La résolution fonctionnera d'ailleurs sans problème en utilisant le nom d'hôte défini depuis une autre machine du réseau :
OpenSSH nous pause par contre un problème sensiblement différent :
En effet, non seulement notre root n'a pas de mot de passe, mais en plus, il n'est pas question de procéder à une connexion SSH sous cette identité. La configuration par défaut du serveur SSH (que ce soit Dropbear ou OpenSSH) ne le permet pas et c'est très bien ainsi. Pour nous connecter depuis une autre machine, nous devons disposer d'un autre utilisateur.
2.2 Le root de tous les maux
N'avoir que l'utilisateur root et/ou l'utiliser pour des tâches courantes, même de développement et de mise au point, est une mauvaise idée. Mieux vaut disposer d'un utilisateur standard qui, le moment venu, obtiendra temporairement les privilèges adéquats pour des actions spécifiques. Nous devons donc intégrer la création de nouveaux utilisateurs dans notre configuration Buildroot. Pour cela, nous avons à notre disposition un mécanisme adapté passant par la création d'un fichier faisant office de users table.
Ce fichier sera référencé via l'interface accessible obtenue avec make menuconfig, sous « System configuration » puis « Path to the users tables » (BR2_ROOTFS_USERS_TABLES). Nous le placerons dans l'arborescence externe, en compagnie des overlays et le nommerons mkusers.table. Il sera donc désigné par $(BR2_EXTERNAL_ST_PATH)/board/stmicroelectronics/stm32mp157/mkusers.table dans la configuration.
Ce fichier respecte une syntaxe relativement simple, voici le nôtre (sur une ligne !) :
Nous avons une liste de champs séparés par des espaces avec, de gauche à droite :
- le nom d'utilisateur (denis) ;
- son ID, avec -1 pour laisser Buildroot le déterminer à la construction ;
- le nom du groupe de l'utilisateur (denis) ;
- l'ID du groupe (GID) également calculé par Buildroot (-1) ;
- le mot de passe en clair, précédé de =, ceci sera chiffré (crypt) avant d'être intégré au système. Notez qu'en faisant précéder le tout d'un !, nous pouvons interdire le login (pour un service fonctionnant sous une identité particulière, par exemple) ;
- le répertoire personnel de l'utilisateur ($HOME) ;
- le shell par défaut, ici /bin/sh (fourni par BusyBox) ;
- une liste de groupes supplémentaires séparés par des virgules ;
- et enfin, un commentaire qui sera intégré dans (/etc/passwd), ce champ étant le dernier, l'utilisation d'espaces est possible.
Nous avons ajouté ce nouvel utilisateur dans le groupe sudo, car la configuration par défaut de cette commande dans Buildroot ajoute automatiquement un /etc/sudoers incluant une ligne %sudo ALL=(ALL) ALL, signifiant que tous les utilisateurs du groupe sudo peuvent passer super-utilisateur pour n'importe quelle commande, en s'authentifiant. Ce réglage est fait par le script package/sudo/sudo.mk, et donc par Buildroot.
Tout ce que nous avons à faire est de nous plier d'un make menuconfig et d’activer « Target packages », « Shell and utilities » et « sudo » (BR2_PACKAGE_SUDO). Nous en profitons au passage pour faire de même, quelques lignes plus haut, avec le multiplexeur de terminal GNU Screen (BR2_PACKAGE_SCREEN) (ou Tmux (BR2_PACKAGE_TMUX) si vous préférez), qui peut également s'avérer très utile, que ce soit en SSH ou via la console série. Il peut être également intéressant d'activer « linux-pam » (BR2_PACKAGE_LINUX_PAM) pour Pluggable Authentication Modules permettant d'ajuster finement les paramètres d'authentification, de validation de compte et de session des utilisateurs.
À ce stade (peut-être après avoir préalablement testé les changements), nous pouvons terminer en faisant un tour dans « System configuration » puis désactiver « Enable root login with password » (BR2_TARGET_ENABLE_ROOT_LOGIN). Nous interdisons alors l'utilisation du compte root, ne laissant plus que sudo être là pour obtenir les privilèges en question :
Ajout d'une clé SSH statique.
En phase de développement et de composition du système, il peut rapidement devenir pénible de devoir sans cesse supprimer et valider à nouveau la signature du serveur SSH du système embarqué. En effet, à chaque premier démarrage du système, Dropbear génère une nouvelle clé d'hôte qui sera forcément différente de la précédente.
Pour nous simplifier la vie, nous pouvons parfaitement récupérer cet élément (via scp) depuis /etc/dropbear/dropbear_ecdsa_host_key et l'intégrer dans notre overlay. Ainsi, la clé d'hôte sera déjà présente dans le système et l'empreinte correspondante sera donc toujours la même.
Ce faisant, vous remarquerez peut-être que les permissions sur le fichier seront sensiblement différentes, passant de 600 à 644, rendant le contenu du fichier lisible par tout le monde. Cela ne pose pas un problème particulier à Dropbear, mais n'est cependant pas correct, et nous donne l'occasion de voir comment ajuster ce genre de choses.
De la même manière que nous avons utilisé une table pour créer l'utilisateur supplémentaire, il en existe une pour fixer les permissions, c'est system/device_table.txt depuis la racine de notre Buildroot. Nous pouvons donc copier ce fichier au côté de mkusers.table (en le renommant éventuellement) et y ajouter une entrée pour notre fichier de clé. Là, nous ajoutons : /etc/dropbear/dropbear_ecdsa_host_key f 600 0 0 - - - - -, puis ajustons le chemin depuis l'interface menuconfig, « System configuration », « Path to the permission tables » (BR2_ROOTFS_DEVICE_TABLE), exactement comme nous l'avons fait pour mkusers.table.
2.3 Quelque chose à l'écran
Notez tout d'abord que ce qui va suivre n'est ni nécessaire, ni forcément souhaitable. L'objectif ici sera simplement de prendre en main le système de build en obtenant un résultat visuel amusant, tout en apprenant quelque chose. Raisonnablement, la configuration par défaut, n'affichant strictement rien à l'écran, si ce n'est en appelant les codes de démonstration Qt, est l'approche à adopter dans un projet destiné à présenter une IHM à l'utilisateur.
Cependant, comme nous aimons fouiller dans les coins et faire pleinement le tour du propriétaire, nous nous fixons comme objectif de présenter sur l'écran LCD une console telle que celle dont nous disposons sur la liaison série. Ceci va nécessiter une modification de la configuration du noyau Linux, sa recompilation et naturellement, la génération d'une nouvelle image.
L'ensemble nous est grandement simplifié par Buildroot, puisque les composants majeurs d'un système embarqué utilisent généralement le même système de configuration que Buildroot. Ainsi, nous connaissons déjà make menuconfig, mais avons également à notre disposition :
- make busybox-menuconfig pour BusyBox ;
- make uclibc-menuconfig pour la bibliothèque C standard uClibc ;
- make uboot-menuconfig pour le bootloader U-Boot ;
- make barebox-menuconfig pour Barebox, un bootloader alternatif à U-Boot ;
- et make linux-menuconfig pour le noyau Linux.
Ces différentes interfaces de configuration peuvent être utilisées, sous réserve que l'élément concerné soit activé dans la configuration principale, pour procéder à une configuration de ces composants. Il ne s'agit cependant pas d'une intégration complète à Buildroot, mais d'interfaces distinctes. Ainsi, make linux-menuconfig nous renvoie sur l'interface classique de configuration du noyau Linux, où nous pouvons ajuster des éléments sur la base d'un fichier de configuration préalablement chargé et ceci fait, nous enregistrons ces changements dans le même fichier ou sous un autre nom. Le fichier de configuration en question est ensuite spécifié dans la configuration Buildroot, exactement comme nous l'avons fait pour les tables des utilisateurs ou des permissions.
L'arborescence externe proposée par Bootlin intègre une configuration Linux stockée dans board/stmicroelectronics/stm32mp157/linux.config. Nous devons donc charger ce fichier via « < Load > » (au bas de l'écran) pour ensuite modifier cette configuration. Mais avant de faire cela, nous avons besoin de parler de la gestion de l'affichage avec Linux.
Les plus anciens utilisateurs du système se souviennent très certainement du périphérique framebuffer, alias fbdev, généralement accessible via un pseudofichier /dev/fb0. La notion de framebuffer est relativement simple, il s'agit d'une zone mémoire (buffer) représentant littéralement la valeur de chaque pixel à l'écran. Ceci explique pourquoi un cat /dev/urandom > /dev/fb0 permet de remplir l'écran de pixels aux couleurs aléatoires, et il était donc très facile de dessiner à l'écran de cette manière en considérant le buffer comme un canevas. Facile, certes, mais totalement inefficace, car fbdev était très limité (buffer de taille statique préallouée, pas de pipeline, problème de synchronisation, etc.) et obligeait donc des serveurs d'affichage comme Xorg à accéder directement aux registres matériels pour reconfigurer l'affichage. Un autre système a donc été créé pour corriger ces imperfections et permettre au noyau de prendre en charge totalement l'infrastructure d'affichage incluant la gestion du framebuffer, mais également la configuration du matériel et de l'affichage.
Pour comprendre ce système, il est tout d'abord important de faire la distinction entre un contrôleur d'affichage (display controller) et un GPU (Graphics Processing Unit). Le travail du contrôleur d'affichage se limite à une tâche et une seule : copier le contenu d'un ou plusieurs framebuffers à l'écran, qu'il soit interfacé en HDMI, DisplayPort ou MIPI DSI. Il peut utiliser et combiner plusieurs framebuffers pour composer cet affichage, mais ne fait absolument aucun travail de rendering 3D ou autre opération impliquant des choses comme OpenGL.
Le GPU, en revanche, est chargé du rendering. C'est un processeur graphique programmable, généralement avec OpenGL, dont le travail est de décharger le processeur généraliste (CPU) des opérations graphiques. Mais le GPU, par lui-même, n'affiche rien et, au contraire, s'en remet au contrôleur d'affichage pour cela. Voilà pourquoi, il est parfaitement possible d'utiliser OpenGL et des shaders (microprogramme fonctionnant purement sur le GPU) sans afficher quoi que ce soit à l'écran (voir [5] pour un exemple). Dans le monde de l'embarqué, certains SoC disposent d'un GPU et d'autres non, mais si un écran ou une sortie vidéo est disponible, il y a toujours un contrôleur d'affichage. Le STM32MP157 dispose des deux.
Avec le noyau Linux moderne, ces deux éléments sont à présent contrôlés par un sous-système appelé DRM pour Direct Rendering Manager, parfois également appelé DRM KMS (pour Kernel Mode-Setting), avec KMS étant un sous-élément de l'API DRM, chargé de la configuration du mode (résolution, nombre de couleurs, vitesse de rafraîchissement) d'affichage. KMS règle précisément le problème évoqué plus haut, évitant un accès privilégié au matériel depuis l'espace utilisateur (pilotes X). D'autre part, l'architecture DRM permet un niveau abstraction supplémentaire puisque n'importe quel programme de l'espace utilisateur faisant usage de l'API DRM fonctionnera forcément avec n'importe quel pilote DRM, quel que soit le matériel contrôlé.
Aujourd'hui, l'ancien fbdev est considéré comme totalement obsolète et n'est presque plus utilisé, au bénéfice de DRM KMS. Le seul élément du noyau en faisant encore usage est... fbcon, la console permettant un affichage des messages de démarrage et la gestion d'un terminal associé (TTY). Fort heureusement, DRM met à disposition une émulation fbdev et donc une solution pour disposer d'un /dev/fb0, mais aussi, et surtout, d’une console. Précisément ce que nous désirons obtenir.
Nous disposons déjà d'un support DRM pour le contrôleur d'affichage et le GPU (Vivante) intégré au SoC dans la configuration par défaut du noyau, comme le prouve la parfaite exécution des démonstrations QT (alors même que rien ne s'affiche au démarrage du devkit). Vous pouvez d'ailleurs très simplement tester cette fonctionnalité (via la console série ou l'accès SSH) en utilisant la commande modetest. sudo modetest -M stm permettra d'afficher des informations (connecteurs, encodeurs, modes disponibles, etc.) et de procéder à un test avec sudo modetest -M stm -s suivi d'un 33:1280×720 pour la sortie DVI, ou d'un 35:480×800 pour l'écran LCD (à ajuster en fonction de la sortie de sudo modetest -M stm -c).
Ce qui nous manque, en premier lieu, c'est une console pour afficher les messages du noyau et éventuellement nous fournir une interface textuelle pour le login. En d'autres termes, il nous manque fbcon. Tout ce que nous avons donc à faire est d'activer quelques fonctionnalités.
Commençons par l'émulation fbdev en passant par :
- « Device Drivers » ;
- « Graphics support » ;
- « Direct Rendering Manager » ;
- « Enable legacy fbdev support for your modesetting driver » (CONFIG_DRM_FBDEV_EMULATION).
Comme l'émulation est active, nous pouvons avoir accès à l'activation de fbcon :
- « Device Drivers » ;
- « Graphics support » ;
- « Console display driver support » ;
- « Framebuffer Console support » (CONFIG_FRAMEBUFFER_CONSOLE).
Et puisqu'un simple défilement de texte sur un écran serait bien triste sans pingouins (et pas assez nostalgique), nous pouvons également activer l'affichage du logo :
- « Device Drivers » ;
- « Graphics support » ;
- « Bootup logo » (CONFIG_LOGO_LINUX_MONO, CONFIG_LOGO_LINUX_VGA16 et CONFIG_LOGO_LINUX_CLUT224).
C'est tout. Nous enregistrons les modifications avant de quitter l'interface de configuration et nous stockons cette configuration, au même emplacement que linux.config, mais en nommant le fichier linux-fbcon.config. Il nous suffit ensuite d'un petit make menuconfig pour ajuster cet élément via « Kernel » et « Configuration file path » (BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE).
Il faudra ensuite nettoyer ce composant du système et reconstruire le noyau avec :
Mais avant de reconstruire l'image et de la flasher sur la plateforme, nous avons un dernier point à configurer. Dans l'état, le noyau configurera effectivement fbcon et nous verrons les messages de boot s'afficher en compagnie des deux petits pingouins, mais nous n'avons pas pour autant de terminal sur cette sortie. Pour cela, nous devons nous pencher sur la configuration de getty, fourni par BusyBox, ainsi que sur le système d'init, également pris en charge par cet outil.
Fort heureusement, la configuration par défaut proposée par Bootlin n'utilise pas systemd (contrairement au build Yocto/OpenSTLinux) et tout cela reste donc extrêmement simple, efficace et cohérent. Comme l'option « Run a getty (login prompt) after boot » (BR2_TARGET_GENERIC_GETTY) est activée, ceci signifie qu'un squelette de base (package/busybox/inittab) est utilisé, puis modifié par le système de build pour décommenter la ligne concernant la console série. Celle-ci est ajustée en fonction des paramètres choisis dans l'interface (port, vitesse, type de terminal).
Nous ne pouvons donc pas simplement et brutalement remplacer inittab avec l'overlay, mais Buildroot propose d'autres solutions. Dans board/stmicroelectronics/stm32mp157/ vous trouverez, par exemple, le fichier post-image.sh, un script permettant de générer le fichier genimage.cfg utilisé pour produire l'image finale. Ce script est exécuté à un moment précis du build, juste après que Buildroot ait composé le système de fichiers racine. Vous retrouvez cette configuration dans « System configuration » et « Custom scripts to run after creating filesystem images » (BR2_ROOTFS_POST_IMAGE_SCRIPT).
Ce hook n'est pas le seul utilisable, il en existe un autre, désigné par « Custom scripts to run before creating filesystem images » (BR2_ROOTFS_POST_BUILD_SCRIPT) exécutable juste après le build et avant la composition de l'image du système de fichiers. C'est précisément ce qu'il nous faut pour exécuter un script, que nous appelons post-build.sh, pour modifier à la voler le contenu du fichier inittab :
Ce script n'est pas de moi, il est utilisé dans plusieurs entrées dans buildroot/board/, raspberrypi/ ou n'importe quelle autre carte proposant une sortie HDMI. Il se contente de trouver la ligne concernant la console série (via la chaîne GENERIC_SERIAL en commentaire) et d'en ajouter une, juste après, pour lancer un nouveau getty.
Nous plaçons ce script à côté de post-image.sh et pouvons alors lancer un nouveau build, puis flasher la carte. Celle-ci va alors présenter les messages de démarrage et se terminer sur :
Si un clavier USB est branché, nous pouvons alors entrer notre login et son mot de passe pour obtenir un shell. À ce stade, si vous êtes satisfait du résultat, vous pouvez choisir d'enregistrer votre configuration Buildroot courante (présentement dans .config), sous un nom plus convenable et à un emplacement adapté, comme buildroot-external-st/configs/. Vous pouvez également utiliser make savedefconfig qui produira un fichier plus concis en supprimant les redondances et les lignes indiquant les éléments non activés, mais mettra à jour le fichier initialement utilisé (st_stm32mp157c_dk2_demo_defconfig). Vous pouvez cependant forcer l'emplacement et le nom du fichier en ajoutant BR2_DEFCONFIG= à la commande, suivie du chemin complet. Une commande équivalente existe pour la configuration du noyau Linux (make linux-savedefconfig ou make linux-update-defconfig BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE= suivi du chemin vers le fichier), pour U-Boot (make uboot-savedefconfig ou uboot-update-defconfig) et Barebox (make barebox-savedefconfig).
Attention cependant, si vous comptez partager cette configuration, à ne pas oublier d'ajuster l'ensemble en conséquence en supprimant les éléments qui ne doivent pas être statiques, je pense en particulier à la clé d'hôte de Dropbear.
Enfin, je le précise une nouvelle fois, ajouter la compatibilité fbdev et donc fbcon est amusant d'un point de vue pédagogique, puisque cela nous a permis de voir comment modifier la configuration du noyau et d'autres éléments de Buildroot, mais ce n'est pas une bonne idée pour un projet. Si l'on veut proposer une IHM, mieux vaut ne pas s'en occuper et se concentrer sur la pile graphique standard en utilisant la LibDRM par exemple ou utiliser un toolkit comme GTK+, QT ou les EFL, éventuellement en passant par Weston (l'implémentation de référence de Wayland).
Conclusion
Je ne vous cacherai pas une certaine préférence personnelle pour Buildroot, que je trouve bien plus « cohérent » qu'OpenEmbbeded d'un point de vue de l'architecture. Certes, cette approche monolithique est plus rigide que ce qu'il est possible d'obtenir avec un mécanisme de layers, mais en contre-partie, la composition et la configuration « locale » d'un système s'en trouvent grandement simplifiée. Bien entendu, la comparaison est totalement dépendante du support effectivement offert par les constructeurs, car même si l'approche consistant à simplement produire un layer fournissant un BSP pour une carte est fort séduisante, en pratique, la quantité de layers non maintenus est relativement importante [6]. Inversement, Buildroot incite les constructeurs à proposer un support s'intégrant pleinement au système de build, les options comme BR2_EXTERNAL n'étant souvent que les prémices à une intégration upstream. Au final, Buildroot est plus « maîtrisable » de la part de l'utilisateur final et l'adaptation à un projet spécifique s'en trouve d'autant simplifiée. Bien entendu, ceci est également une question d'expérience, mais la courbe d'apprentissage est clairement plus clémente dans le cas de Buildroot.
En ce qui concerne les manipulations que nous venons de voir, un certain nombre d'améliorations restent à faire. Le fait que ce soit les anciennes versions du devkit (STM32MP157A-DK1 et STM32MP157C-DK2), maintenant considérées comme obsolètes, car remplacées par les STM32MP157D-DK1 et STM32MP157F-DK2, ne pose pas de grands problèmes, même si cela mériterait d'être complété. Vous avez sans doute remarqué un message du noyau au démarrage vous faisant remarquer que « OPP table can't be empty ». Ceci provient d'éléments manquants dans le devicetree, en particulier concernant les horloges du SoC et le support OPP gérant des paires fréquence/tension supportées par le SoC. Il est possible de régler ce problème en générant un nouveau source du devicetree avec STM32CubeMX et en l'adaptant (voir le fichier doc/stm32cubemx.md de buildroot-external-st pour plus d'informations).
Un autre élément à régler est le fait que le devkit, démarré avec le système de démonstration, perturbe l'hôte auquel il est connecté en USB, remplissant par la même occasion les logs de messages comme :
Le port USB-C par lequel nous flashons la microSD est, en effet, USB OTG, et donc en mesure de se comporter comme un périphérique. Mais rien n'est configuré par défaut et l'hôte ne cesse de tenter d'énumérer ce qu'il pense être un périphérique. L'une des conséquences est une perturbation complète du sous-système USB de l'hôte qui devient incapable, par exemple, d'utiliser correctement une YubiKey 5 (ce qui est relativement pénible lorsqu'on tente de s'authentifier sur GitHub pour proposer un patch justement). Pour régler ce problème, la solution simple est de débrancher le câble USB, mais il serait bien plus intéressant de configurer l'USB OTG via l'API USB Gadget au démarrage (pour fournir un port série ou une interface Ethernet).
Il est fort probable que nous parlions encore à l'avenir de cette sympathique plateforme, peut-être pour traiter de ces points, peut-être pour nous pencher sur le Cortex-M4 intégré au SoC, ou tout simplement pour faire plus ample connaissance avec Buildroot...
Références
[2] https://github.com/bootlin/buildroot-external-st/blob/st/2021.02/docs/internals.md
[3] https://wiki.st.com/stm32mpu/wiki/TF-A_overview#FIP
[5] https://github.com/matusnovak/rpi-opengl-without-x
[6] https://layers.openembedded.org/layerindex/branch/master/layers/