Test de la NUCLEO-F412ZG avec Hubris et Humility
Porter Hubris, l'OS embarqué d'Oxide Computer, sur une NUCLEO-F412ZG à 100 MHz : ce qui a été touché, du kernel intact aux deux correctifs de Humility, expliqués.
Le sujet : une NUCLEO-F412ZG, une carte de développement ST à Cortex-M4, et Hubris, l’OS embarqué d’Oxide Computer, qui ne la supporte pas de base. Le kernel n’a pas bougé d’une ligne. Il a fallu ajouter la carte, adapter trois drivers, mettre à jour le firmware de la sonde et corriger deux fois Humility, le debugger.
# Hubris, Humility, c’est quoi ? C’est qui ?
Hubris est un petit OS en Rust qu’Oxide utilise dans ses serveurs pour le contrôleur de service. Chaque tâche vit dans ses propres régions mémoire, protégées par la MPU. Les tâches, leurs priorités et leurs droits sur les périphériques sont tous déclarés à la compilation, dans un app.toml. Humility lit la mémoire de la carte à travers la sonde et la traduit grâce aux infos de debug : état des tâches, piles, registres, dumps.
L’auteur principal, c’est Cliff Biffle. Sa présentation à l’Open Source Firmware Conference 2021 explique pourquoi Oxide a écrit son propre OS. Ce talk est vraiment incroyable, quelle belle pièce d’ingénierie !
On peut aussi citer Bryan Cantrill, cofondateur et CTO d’Oxide, et Adam Leventhal, parmi les grands manitous derrière Hubris. Dans sa présentation sur le debug de Docker en production, à la DockerCon 2015, Cantrill défend une idée simple : un système planté ne se redémarre pas avant d’avoir été débogué, en direct ou d’après une image de sa mémoire, le core dump. Humility fait les deux : humility tasks lit la carte en marche, humility dump sauve son état complet dans un fichier qu’on examine ensuite carte débranchée.
# Ce qu’il a fallu patcher
Je colle un prompt de mission à Claude : faire tourner Hubris sur cette carte, branchée en USB sur le Mac. Sa cible la plus proche dans le dépôt est une carte Discovery à STM32F407. Le soir même, Hubris tourne à 100 MHz sur la NUCLEO.
Ce qui a été patché, couche par couche :
- Le kernel : rien, pas une ligne.
- La carte et l’app : des ajouts purs. Un fichier de carte de 2 lignes (le nom de la puce pour probe-rs), une description de la puce (1 Mio de flash, 256 Kio de RAM, adresses des périphériques), et l’app elle-même, avec la configuration d’horloge à 100 MHz. La branche ajoute 621 lignes, documentation comprise.
- Trois drivers partagés, environ 150 lignes :
stm32fx-rccchoisit la variante F412 de la bibliothèque de registres,user-ledspilote les LEDs de la Nucleo sur PB0, PB7 et PB14, etstm32fx-usartpasse sur USART3 (PD8/PD9), le port série virtuel du ST-LINK, cadencé à 50 MHz. Le point délicat : deux variantes de puce du cratestm32f4ne peuvent pas cohabiter dans une même tâche, et deux de ces drivers forçaient la F407 dans leurCargo.toml. Ce choix est passé dans une feature. La Discovery compile à l’identique, revérifié à chaque étape. - La sonde : le firmware du ST-LINK, trop ancien, a dû être mis à jour.
- Humility, le debugger : deux correctifs, détaillés plus bas. Le premier répare Humility face au Hubris le plus récent, quelle que soit la carte. Le second corrige une mesure fausse qu’on voit sur cette carte.
# La mission, en tranches
Le north star du prompt tient en une phrase : « humility tasks montre toutes les tâches Healthy sur la NUCLEO-F412ZG, les LEDs utilisateur clignotent sous le contrôle de pong, et ping écrit sur le port COM virtuel du ST-LINK. » Cinq tranches, chacune doit finir sur du matériel qui marche : préflight, build de référence, premier allumage sur l’oscillateur interne à 16 MHz, UART, puis 100 MHz.
Premier arrêt dès la tranche 0. Le firmware du ST-LINK de la carte est en V2J25 ; probe-rs, sur lequel s’appuie Humility, exige J26. Le prompt disait de s’arrêter et de demander. Claude propose de patcher automatiquement, je lui donne le zip téléchargé du site de ST et c’est fait.
Toutes les tâches sont Healthy. Claude ne voit pas les LEDs : il lit le registre de sortie de GPIOB plusieurs fois de suite et le voit alterner entre PB14, PB0 et rien, au rythme de pong. La carte tourne à 100 MHz. Sur 10 secondes, l’horloge de Hubris compte 10 235 ms pour 10,32 s au mur, l’écart étant la latence de connexion de Humility.
# Le moment où ça a cassé : SavedState
Juste avant le premier allumage, humility tasks échoue : « expected structure SavedState not found ».
Pour comprendre, il faut savoir comment Humility lit une tâche. Quand le kernel interrompt une tâche, il range ses registres (r4 à r11, le pointeur de pile, etc.) dans une structure, le champ save de l’entrée Task de cette tâche. Humility lit la table des tâches directement dans la RAM de la carte. Pour savoir à quel octet se trouve chaque registre, il a besoin du plan de cette structure, qu’il trouve dans les infos de debug (DWARF) du binaire du kernel. Et il la cherchait par son nom : SavedState.
Le 9 octobre, quatre jours plus tôt, la PR #2704 de Hubris a introduit un trait Arch pour que le kernel puisse viser plusieurs architectures. La structure de l’ARMv7-M y a pris le nom ArmMSavedState. Humility ne la trouve plus, et ni humility tasks ni la lecture des registres d’une tâche ne marchent, quelle que soit la carte.
Deux façons de s’en sortir. Claude fige d’abord la branche juste avant ce commit et continue, ce qui suffit pour cette carte. C’est dans DECISIONS.md, et ça débloque. Ensuite, le correctif, sur la branche savedstate-by-member de mon fork de Humility : au lieu de chercher le type par son nom, Humility prend le type du champ save de Task, que le DWARF donne directement. Le nom peut changer, le champ reste. Onze lignes ajoutées, quatre retirées, aux deux endroits qui lisaient cette structure : humility tasks et la lecture des registres. Le correctif marche avec l’ancien kernel et avec le nouveau, testé sur la carte dans les deux cas.
# Le second correctif : stackmargin affichait des zéros
humility stackmargin dit, pour chaque tâche, combien de pile elle n’a jamais utilisée. Le principe : au démarrage d’une tâche, le kernel remplit toute sa pile avec un motif, 0xbaddcafe. Humility lit la pile depuis le bas et compte les mots encore intacts : c’est la marge.
Sur la carte, il affiche une marge de 0 pour jefe, le superviseur, avec une pile de 152 octets alors que son app.toml en déclare 2200.
La MPU d’un Cortex-M4 ne connaît que des régions dont la taille est une puissance de 2. Depuis avril (PR #2458), Hubris découpe la RAM d’une tâche en plusieurs régions contiguës pour moins gaspiller : les 2560 octets de jefe font 1 Kio + 1 Kio + 512 octets au lieu d’une région de 4 Kio. Une pile peut donc s’étaler sur plusieurs régions. Le kernel a été corrigé en mai (PR #2492) pour la peindre en entier. Humility, lui, ne lisait que la région qui contient le sommet de pile : ici la dernière, où ne tiennent que 152 octets de pile, tous utilisés. D’où la marge de 0.
STACKBASE STACKSIZE MAXDEPTH MARGIN
avant jefe 0x20001c00 152 152 0
après jefe 0x20001400 2200 272 1928Le correctif, sur la branche stackmargin-multi-region, descend les régions contiguës qui appartiennent à la même tâche, comme le fait le kernel, avant de mesurer. Le test de non-régression de Humility l’a aussi validé : sur un dump de Sidecar, un vrai switch d’Oxide, l’instantané attendu contenait trois faux « MARGIN 0 ». Après correction, chaque taille de pile égale à l’octet près celle déclarée dans l’app.toml de ce Sidecar.
# Où j’en suis
La branche nucleo-f412zg compte cinq commits, avec un DECISIONS.md et un README. Elle est figée avant la PR #2704 ; avec le premier correctif, Humility suit aussi le kernel récent. Les deux correctifs sont sur mon fork de Humility. La suite de tests du kernel sur la F412, la tranche 5 optionnelle, n’est pas faite. Et « tout Healthy » n’est vrai qu’à moitié : ping fait exprès une faute toutes les 100 réponses de pong, comme dans les démos d’Oxide.
Une piste pour la suite, pas encore faite : un capteur I2C ou SPI derrière une tâche driver. Dans Hubris, un driver est une tâche comme une autre : elle seule a accès aux registres du périphérique, déclarés dans l’app.toml, et les autres tâches lui parlent par messages. Le driver I2C du dépôt vise les STM32H7 et G0 (plus un driver à part pour le LPC55), le SPI seulement les STM32H7 et le LPC55 : rien pour la famille F4, il faudrait l’écrire pour cette carte.
À suivre…