Journal de bord et boîte noire

Trois mémoires, trois portées

survit à

contenu

où le lire

/diagnostics

rien

l’état maintenant, une fois par seconde

ros2 topic echo /diagnostics

boîte noire

reset, coupure de courant (sur pile)

la dernière cause de plantage

bips au démarrage, bloc boîte noire de /diagnostics

journal de bord

tout, tant que la flash tient

les 256 derniers événements datés

./scripts/ratbot log

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 (vApplicationStackOverflowHook)

les 4 premières lettres du nom de la tâche

2

tas FreeRTOS plein (vApplicationMallocFailedHook)

—

3

faute matérielle (HardFault, MemManage, BusFault, UsageFault)

SCB->CFSR

4

Error_Handler : un périphérique a refusé de s’initialiser

—

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.

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.

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

p1 = RCC_RSR >> 16 (cause du reset)

2

relance par le watchdog

—

3

plantage au tour précédent

p1 = cause boîte noire, p2 = détail

4

agent micro-ROS connecté

—

5

agent micro-ROS perdu

—

6

arrêt d’urgence

p1 = nombre de déclenchements

7

réarmement

p1 = nombre de réarmements ; p2 = origine, 0 service ROS, 1 geste SW1 devant le robot

8

batterie basse

p1 = millivolts

9

batterie critique

p1 = millivolts

10

roue perdue

p1 = roues en ligne après, p2 = avant

11

roue revenue

p1 = roues en ligne, p2 = avant

12

capteur perdu

p1 = santé avant, p2 = après

13

paramètres sauvegardés

p1 = nombre de sauvegardes

14

heure de la RTC mise à jour

p1 = écart en secondes

15

journal effacé

—

16

capteur revenu

p1 = santé avant, p2 = après

17

erreur de réception sur la liaison Pi

p1 = types d’erreur (1 trame, 2 bruit, 4 débordement, 8 parité), p2 = cumul depuis le démarrage

18

liaison Pi perdue, état au moment de la perte

p1 = erreurs série cumulées, p2 = synchronisations ratées cumulées

19

sécurité inertielle : robot coupé

p1 = 1 renversé, 2 roue bloquée, 3 patinage ; p2 = inclinaison en degrés, roue bloquée (1 gauche, 2 droite, 3 les deux), écart roues / gyroscope en centièmes de rad/s, ou pic de choc en centièmes de m/s² (p1 = 4)

20

mode banc

p1 = 1 activé, 0 désactivé ; p2 = origine, 0 service ROS, 1 SW2 tenu 3 s

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.