Journal de bord et boîte noire
Trois mémoires, trois portées
survit à |
contenu |
où le lire |
|
|---|---|---|---|
|
rien |
l’état maintenant, une fois par seconde |
|
boîte noire |
reset, coupure de courant (sur pile) |
la dernière cause de plantage |
bips au démarrage, bloc |
journal de bord |
tout, tant que la flash tient |
les 256 derniers événements datés |
|
Il manquait celle du milieu et celle du bas : le robot perdait une roue à 3 h du matin, le watchdog le relançait, et au réveil tout paraissait normal.
La boîte noire
Trois registres de sauvegarde de la RTC (RTC->BKP29R, 30R, 31R),
alimentés par la pile : cause, détail, et un compteur cumulé de plantages.
Écrire trois registres est la seule chose encore possible depuis un gestionnaire de faute — plus d’ordonnanceur, pas de pile utilisable, aucun bus à solliciter. Écrire en flash y demanderait d’effacer un secteur, soit des centaines de millisecondes qui peuvent échouer à moitié.
cause |
origine |
détail enregistré |
|---|---|---|
1 |
débordement de pile ( |
les 4 premières lettres du nom de la tâche |
2 |
tas FreeRTOS plein ( |
— |
3 |
faute matérielle (HardFault, MemManage, BusFault, UsageFault) |
|
4 |
|
— |
Le numéro de cause est le nombre de bips courts du code à 5 longs : voir la page « Signaux ».
Le SOS
blackbox_panic() enregistre, puis fait sonner ···---··· sur le buzzer
actif, et ne revient jamais. Le buzzer actif porte son propre oscillateur :
une écriture GPIO suffit, sans timer, sans interruption, sans RTOS. C’est le
seul signal qui marche encore quand plus rien ne marche.
Les temporisations viennent du compteur de cycles du cœur (DWT->CYCCNT) et
non de HAL_Delay, détournée vers l’ordonnanceur — rendre la main à un
ordonnanceur mort figerait la carte en silence. Point 150 ms, trait 450 ms,
2 s entre deux SOS.
Deux gardes valent d’être connues : le bus APB de la RTC est rallumé avant l’écriture (une faute très précoce arrive avant l’init des horloges), et un drapeau anti-récursion saute directement au SOS si l’enregistrement lui-même a fauté — une faute dans un gestionnaire de faute verrouille le cœur.
Comme plus personne ne caresse le watchdog, la carte redémarre 8 s plus tard et annonce le plantage en bips. Le SOS a le temps de sonner une fois.
Le journal de bord
Où il vit
Flash de données U7 (W25Q64, 8 Mio, SPI1) — pas la QSPI d’où le code s’exécute. Elle est sur SPI, donc non projetée en mémoire : ni le ST-LINK ni la Pi ne peuvent la lire directement, le seul accès passe par le firmware.
secteur 0 0x000000 configuration (config.c)
secteur 1 0x001000 journal, 128 lignes de 32 octets
secteur 2 0x002000 journal, 128 lignes } anneau de 256 lignes
reste 0x003000 libre (7,99 Mio)
Deux secteurs et non un : quand l’anneau retombe sur le plus vieux, on l’efface
en entier — il faut donc que l’autre garde l’histoire pendant ce temps. Deux
_Static_assert interdisent au journal d’empiéter sur la configuration.
Ce qu’on écrit, et ce qu’on n’écrit pas
Pas de trace périodique, pas de mesure, pas de « tout va bien ». Une flash NOR s’use à l’effacement (100 000 cycles par secteur), et un journal noyé dans la routine ne se lit plus. Uniquement des changements d’état qu’on voudra pouvoir expliquer plus tard.
n° |
événement |
paramètres |
|---|---|---|
1 |
démarrage |
|
2 |
relance par le watchdog |
— |
3 |
plantage au tour précédent |
|
4 |
agent micro-ROS connecté |
— |
5 |
agent micro-ROS perdu |
— |
6 |
arrêt d’urgence |
|
7 |
réarmement |
|
8 |
batterie basse |
|
9 |
batterie critique |
|
10 |
roue perdue |
|
11 |
roue revenue |
|
12 |
capteur perdu |
|
13 |
paramètres sauvegardés |
|
14 |
heure de la RTC mise à jour |
|
15 |
journal effacé |
— |
16 |
capteur revenu |
|
17 |
erreur de réception sur la liaison Pi |
|
18 |
liaison Pi perdue, état au moment de la perte |
|
19 |
sécurité inertielle : robot coupé |
|
20 |
mode banc |
|
Important
Les numéros sont dans les lignes déjà écrites. On en ajoute à la fin, on
n’en renumérote jamais : un vieux journal se relirait faux. La table de
libellés de journal.c et cette page doivent suivre.
Les roues et les capteurs sont surveillés depuis la tâche IHM, qui regarde déjà l’état du robot 50 fois par seconde pour choisir la couleur du voyant : rien n’est ajouté dans les boucles chaudes de la traction ou des capteurs. Seules les transitions partent au journal.
L’horodatage
Chaque ligne porte deux dates : l’heure UTC de la RTC et le temps depuis le
démarrage. La seconde date les événements arrivés avant que la Pi n’ait donné
l’heure, quand la première vaut zéro et que l’affichage montre boot+12.3s.
L’heure est lue une fois par rafale, au moment de l’écriture, et l’attente passée dans l’anneau est retirée : sans cela, cinq événements groupés porteraient cinq fois la même date. L’écart est calculé en signé — une ligne déposée après la prise de l’heure donnait, en non signé sur 32 bits, une date de 49,7 jours plus tôt. Vu en essai, corrigé.
Format d’une ligne
32 octets, structure packed : numéro d’ordre, numéro de démarrage,
événement, gravité, temps depuis le démarrage, heure UTC en millisecondes, deux
paramètres, CRC-8. Le CRC ne protège de rien de malveillant : il distingue une
ligne écrite d’une place vierge ou d’une écriture coupée par une panne de
courant au milieu des 32 octets.
Le curseur d’écriture n’est stocké nulle part : au démarrage, la carte relit les 256 places et prend celle qui porte le plus grand numéro d’ordre. La comparaison est tolérante au bouclage du compteur 16 bits.
Le lire
./scripts/ratbot log # les lignes, de la plus ancienne à la plus récente
./scripts/ratbot log --clear # repartir de zéro
Sous le capot, deux services et un topic :
ros2 topic echo /ratbot_1/status/log & # s'abonner AVANT
ros2 service call /ratbot_1/log std_srvs/srv/Trigger
ros2 service call /ratbot_1/log_clear std_srvs/srv/Trigger
La carte déverse 4 lignes toutes les 50 ms, soit 80 par seconde : les 256 lignes passent en trois secondes sans remplir les tampons de l’agent et sans voler de temps à l’odométrie. Le topic est fiable — un journal troué ne vaut rien — et il ne sort qu’à la demande.
L’effacement est demandé par le service mais exécuté par la tâche journal : deux tâches qui déplacent le curseur en même temps le casseraient.
Vérifier que tout va bien
ros2 topic echo /diagnostics | grep -A12 "journal"
perdues non nul signifie que l’anneau RAM a débordé — quelque chose écrit
bien trop souvent. relectures_crc non nul signifie que la flash rend des
octets incohérents ; relectures_2e_essai dit combien de lignes ont été
sauvées par une seconde lecture.
Comment il écrit
journal_add()ne touche pas la flash : elle dépose la ligne dans un anneau de 32 entrées en RAM, en quelques microsecondes, section critique de quelques instructions. Elle est donc appelable depuis n’importe quelle tâche, y compris l’asservissement à 100 Hz, et même depuis une interruption. Anneau plein : la ligne est perdue et comptée comme telle — on ne retarde jamais l’asservissement pour un journal.La tâche journal vide cet anneau toutes les 200 ms. Les lignes s’ajoutent à la suite, 32 octets chacune : écrire ne demande pas d’effacer. On n’efface un secteur — 57 ms — qu’une fois toutes les 128 lignes.
Note
La tâche journal est en priorité normale, pas « basse » comme on l’écrirait spontanément pour une tâche d’arrière-plan : la tâche micro-ROS ne dort jamais, donc toute tâche moins prioritaire qu’elle ne serait jamais élue.
Le bus SPI de la flash est maintenant partagé : la tâche micro-ROS y sauve les paramètres, la tâche journal y écrit ses lignes. Le verrou est dans le pilote (
w25q64.c), pas chez les appelants — c’est le seul endroit où on ne peut pas l’oublier. Il hérite de la priorité, sans quoi la tâche journal attendrait derrière micro-ROS, moins prioritaire.Warning
Le FIFO de réception du SPI doit être vidé avant chaque échange. Sur STM32H7 il se remplit aussi pendant une émission seule : l’en-tête d’une écriture de page fait entrer des octets qui décalent la lecture suivante.
Ça ne se voyait pas tant que la flash n’était lue qu’au démarrage, avant toute écriture. Avec le journal, qui écrit puis relit en permanence, une ligne sur trois revenait fausse — et le même emplacement se relisait bon la fois d’après.
spi_flush_rx()règle la question ;journal_read()réessaie tout de même trois fois et compte les échecs.