Probando la NUCLEO-F412ZG con Hubris y Humility
Portar Hubris, el sistema operativo embebido de Oxide Computer, a una NUCLEO-F412ZG a 100 MHz: lo que hubo que tocar, del kernel intacto a los dos parches de Humility, explicado.
El tema: una NUCLEO-F412ZG, una placa de desarrollo de ST con un Cortex-M4, y Hubris, el sistema operativo embebido de Oxide Computer, que no la soporta de serie. El kernel no cambió ni una línea. Hubo que añadir la placa, adaptar tres drivers, actualizar el firmware de la sonda y corregir dos veces Humility, el depurador.
# Hubris, Humility: ¿qué son? ¿Quién está detrás?
Hubris es un pequeño sistema operativo en Rust que Oxide usa en sus servidores para el procesador de servicio. Cada tarea vive en sus propias regiones de memoria, protegidas por la MPU. Las tareas, sus prioridades y sus permisos sobre los periféricos se declaran todos en tiempo de compilación, en un app.toml. Humility lee la memoria de la placa a través de la sonda y la traduce gracias a la información de depuración: estado de las tareas, pilas, registros, volcados.
El autor principal es Cliff Biffle. Su charla en la Open Source Firmware Conference 2021 explica por qué Oxide escribió su propio sistema operativo. Esta charla es realmente increíble, ¡qué gran obra de ingeniería!
También hay que citar a Bryan Cantrill, cofundador y CTO de Oxide, y a Adam Leventhal entre los grandes nombres detrás de Hubris. En su charla sobre la depuración de Docker en producción, en la DockerCon 2015, Cantrill defiende una idea sencilla: un sistema colgado no se reinicia antes de haberlo depurado, en vivo o a partir de una imagen de su memoria, el core dump. Humility hace las dos cosas: humility tasks lee la placa en marcha y humility dump guarda su estado completo en un archivo que se examina después, con la placa desconectada.
# Lo que hubo que parchear
Le paso a Claude un prompt de misión: hacer funcionar Hubris en esta placa, conectada por USB al Mac. Su objetivo más cercano en el repositorio es una placa Discovery con un STM32F407. Esa misma noche, Hubris corre a 100 MHz en la NUCLEO.
Lo que se parcheó, capa por capa:
- El kernel: nada, ni una línea.
- La placa y la app: solo añadidos. Un archivo de placa de 2 líneas (el nombre del chip para probe-rs), una descripción del chip (1 MiB de flash, 256 KiB de RAM, direcciones de los periféricos) y la app en sí, con la configuración de reloj a 100 MHz. La rama añade 621 líneas, documentación incluida.
- Tres drivers compartidos, unas 150 líneas:
stm32fx-rccelige la variante F412 de la biblioteca de registros,user-ledscontrola los LED de la Nucleo en PB0, PB7 y PB14, ystm32fx-usartpasa a USART3 (PD8/PD9), el puerto serie virtual del ST-LINK, a 50 MHz. El punto delicado: dos variantes de chip del cratestm32f4no pueden convivir en una misma tarea, y dos de estos drivers forzaban la F407 en suCargo.toml. Esa elección pasó a una feature. La Discovery compila exactamente igual, comprobado de nuevo en cada paso. - La sonda: el firmware del ST-LINK, demasiado antiguo, tuvo que actualizarse.
- Humility, el depurador: dos parches, detallados más abajo. El primero arregla Humility frente al Hubris más reciente, sea cual sea la placa. El segundo corrige una medida errónea que aparece en esta placa.
# La misión, por tramos
La north star del prompt cabe en una frase: «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.» [humility tasks muestra todas las tareas Healthy en la NUCLEO-F412ZG, los LED de usuario parpadean bajo el control de pong, y ping escribe en el puerto COM virtual del ST-LINK.] Cinco tramos, cada uno debe terminar en hardware que funciona: preflight, build de referencia, primer arranque con el oscilador interno a 16 MHz, UART y luego 100 MHz.
Primera parada ya en el tramo 0. El firmware del ST-LINK de la placa está en V2J25; probe-rs, en el que se apoya Humility, exige J26. El prompt decía que había que pararse y preguntar. Claude propone parchearlo automáticamente, le doy el zip descargado de la web de ST y listo.
Todas las tareas están Healthy. Claude no ve los LED: lee el registro de salida de GPIOB varias veces seguidas y lo ve alternar entre PB14, PB0 y nada, al ritmo de pong. La placa corre a 100 MHz. En 10 segundos, el reloj de Hubris cuenta 10 235 ms frente a 10,32 s de reloj de pared; la diferencia es la latencia de conexión de Humility.
# Cuando se rompió: SavedState
Justo antes del primer arranque, humility tasks falla: «expected structure SavedState not found».
Para entenderlo, hay que saber cómo lee Humility una tarea. Cuando el kernel interrumpe una tarea, guarda sus registros (r4 a r11, el puntero de pila, etc.) en una estructura, el campo save de la entrada Task de esa tarea. Humility lee la tabla de tareas directamente en la RAM de la placa. Para saber en qué byte está cada registro, necesita el plano de esa estructura, que encuentra en la información de depuración (DWARF) del binario del kernel. Y la buscaba por su nombre: SavedState.
El 9 de octubre, cuatro días antes, la PR #2704 de Hubris introdujo un trait Arch para que el kernel pueda apuntar a varias arquitecturas. La estructura del ARMv7-M pasó a llamarse ArmMSavedState. Humility ya no la encuentra, y ni humility tasks ni la lectura de los registros de una tarea funcionan, sea cual sea la placa.
Dos maneras de salir del paso. Claude primero fija la rama justo antes de ese commit y sigue adelante, lo que basta para esta placa. Está en DECISIONS.md, y desbloquea. Después, el parche, en la rama savedstate-by-member de mi fork de Humility: en lugar de buscar el tipo por su nombre, Humility toma el tipo del campo save de Task, que DWARF da directamente. El nombre puede cambiar; el campo se queda. Once líneas añadidas, cuatro eliminadas, en los dos sitios que leían esa estructura: humility tasks y la lectura de los registros. El parche funciona con el kernel antiguo y con el nuevo, probado en la placa en ambos casos.
# El segundo parche: stackmargin mostraba ceros
humility stackmargin indica, para cada tarea, cuánta pila no ha usado nunca. El principio: al arrancar una tarea, el kernel llena toda su pila con un patrón, 0xbaddcafe. Humility lee la pila desde abajo y cuenta las palabras que siguen intactas: ese es el margen.
En la placa, muestra un margen de 0 para jefe, el supervisor, con una pila de 152 bytes, cuando su app.toml declara 2200.
La MPU de un Cortex-M4 solo admite regiones cuyo tamaño es una potencia de 2. Desde abril (PR #2458), Hubris divide la RAM de una tarea en varias regiones contiguas para desperdiciar menos: los 2560 bytes de jefe son 1 KiB + 1 KiB + 512 bytes en lugar de una única región de 4 KiB. Así que una pila puede extenderse por varias regiones. El kernel se corrigió en mayo (PR #2492) para pintarla entera. Humility, en cambio, solo leía la región que contiene la cima de la pila: aquí la última, donde caben solo 152 bytes de pila, todos usados. De ahí el margen de 0.
STACKBASE STACKSIZE MAXDEPTH MARGIN
antes jefe 0x20001c00 152 152 0
después jefe 0x20001400 2200 272 1928El parche, en la rama stackmargin-multi-region, recorre hacia abajo las regiones contiguas que pertenecen a la misma tarea, como hace el kernel, antes de medir. La prueba de no regresión de Humility también lo validó: en un volcado de un Sidecar, un switch real de Oxide, la instantánea esperada contenía tres falsos «MARGIN 0». Tras la corrección, cada tamaño de pila coincide al byte con el declarado en el app.toml de ese Sidecar.
# Dónde estoy
La rama nucleo-f412zg tiene cinco commits, con un DECISIONS.md y un README. Está fijada antes de la PR #2704; con el primer parche, Humility también sigue al kernel reciente. Los dos parches están en mi fork de Humility. La batería de pruebas del kernel en la F412, el tramo 5 opcional, no está hecha. Y «todo Healthy» solo es verdad a medias: ping provoca a propósito un fallo cada 100 respuestas de pong, como en las demos de Oxide.
Una pista para más adelante, aún sin hacer: un sensor I2C o SPI detrás de una tarea driver. En Hubris, un driver es una tarea como cualquier otra: solo ella tiene acceso a los registros del periférico, declarados en el app.toml, y las demás tareas le hablan mediante mensajes. El driver I2C del repositorio apunta a los STM32H7 y G0 (más un driver aparte para el LPC55), el SPI solo a los STM32H7 y al LPC55: nada para la familia F4, habría que escribirlo para esta placa.
Continuará…