Aller au contenu
storagebits

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.

8 min de lecture#devlog#agents#rust#hubris

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.

La carte NUCLEO-F412ZG posée sur un tapis noir, branchée en USB, avec ses LEDs utilisateur verte, bleue et rouge allumées.
La NUCLEO-F412ZG, branchée par l'USB du ST-LINK, la sonde intégrée en bas de la carte. Au-dessus, les trois LEDs utilisateur que pilote la tâche pong.

# 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 Hubris and Humility: developing an OS for robustness in Rust », Cliff L. Biffle, Open Source Firmware Conference 2021, 43 min.

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.

« Running Aground: Debugging Docker in Production », Bryan Cantrill, DockerCon 2015. Docker a retiré la vidéo d'origine en 2018 ; c'est la copie republiée par Cantrill.

# 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-rcc choisit la variante F412 de la bibliothèque de registres, user-leds pilote les LEDs de la Nucleo sur PB0, PB7 et PB14, et stm32fx-usart passe sur USART3 (PD8/PD9), le port série virtuel du ST-LINK, cadencé à 50 MHz. Le point délicat : deux variantes de puce du crate stm32f4 ne peuvent pas cohabiter dans une même tâche, et deux de ces drivers forçaient la F407 dans leur Cargo.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.

Arbre d'horloge de la NUCLEO-F412ZG à 100 MHzLe MCO du ST-LINK fournit 8 MHz en entrée HSE bypass. PLLM 4 donne 2 MHz, PLLN 100 donne un VCO à 200 MHz, PLLP 2 donne SYSCLK à 100 MHz. AHB à 100 MHz, APB1 à 50 MHz pour l'USART3, APB2 à 100 MHz.ST-LINK MCO8 MHzcarré, pas un quartzHSE bypass8 MHzHSEBYP avant HSEONPLLM = 42 MHzentrée VCO 1–2 MHzPLLN = 100200 MHzVCO 100–432 MHzPLLP = 2100 MHzSYSCLK, max F412AHB ÷1100 MHzcœur, SysTick : 100 000 cycles/msAPB1 ÷250 MHzUSART3 : BRR 434 → 115 207 baudsAPB2 ÷1100 MHzmax 100 MHzFlash : 3 wait states (90 < HCLK ≤ 100 MHz à 2,7–3,6 V) · régulateur en scale 1Relu sur la carte : RCC_PLLCFGR = 0x28401904 · RCC_CFGR = 0x100a · FLASH_ACR = 0x703
La tranche 4 : de 16 MHz sur la HSI à 100 MHz. Chaque constante est justifiée par une section du RM0402 dans le code, et relue dans les registres une fois flashée.

# 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.

La pile de jefe vue par humility stackmargin, avant et après correctionLa RAM de jefe fait 2560 octets répartis en trois régions MPU contiguës. Sa pile de 2200 octets commence à 0x20001400 et son sommet initial est 0x20001C98, dans la troisième région. Avant correction, Humility ne lisait que cette troisième région : 152 octets, tous utilisés, donc une marge de 0. Après correction, il lit les 2200 octets, trouve 1928 octets encore à 0xbaddcafe et une marge de 1928.0xbaddcafe : 1928 o jamais touchésdonnéesrégion 1 · 1 Kiorégion 2 · 1 Kiorégion 3 · 512 osommet initial 0x20001C98272 o utilisés0x200014000x200018000x20001C000x20001E00avantlit 152 o, bute au 1er mot → MARGIN 0aprèslit 2200 o depuis la base, compte 1928 o de 0xbaddcafe → MARGIN 1928
Relevé sur la carte (humility map, readmem). Humility ne regardait que la région qui contient le sommet de pile. Le kernel, lui, peint toute la pile en 0xbaddcafe sur les trois régions.
           STACKBASE  STACKSIZE  MAXDEPTH  MARGIN
avant  jefe 0x20001c00       152       152       0
après  jefe 0x20001400      2200       272    1928

Le 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…