Bootloader et exécution depuis la flash externe
Le STM32H750VBT6 n’a que 128 Ko de flash interne. L’application du RatBot
vit donc dans la W25Q64 externe (8 Mio) et s’y exécute en place (XIP), à
l’adresse 0x90000000. Un petit programme en flash interne — le
bootloader — mappe la flash externe en mémoire puis saute dedans.
Cette page décrit le bootloader du projet, et surtout les réglages que CubeMX efface à chaque régénération.
Important
Le montage ne tient qu’à quatorze réglages dispersés dans du code généré. Après chaque « GENERATE CODE », lancer :
./scripts/firmware-fixes # etat des lieux
./scripts/firmware-fixes --apply # repose ce qui peut l'etre
Le script les vérifie tous en deux secondes. Il a été écrit après deux pannes causées, le même jour, par des réglages silencieusement perdus.
Pourquoi un bootloader maison
Les bootloaders génériques qu’on trouve pour le STM32H750 initialisent le RTC,
donc le quartz LSE 32,768 kHz. Or notre quartz (CL = 7 pF, boîtier HC-49S
miniature) ne démarre qu’en drive MEDIUMHIGH, réglage propre à cette carte.
Un binaire précompilé pour une autre carte reste donc bloqué dans
MX_RTC_Init() au premier démarrage à froid, et n’atteint jamais la
flash externe.
Constaté le 2026-09-06, mesures à l’appui :
RCC->BDCR = 0x8171 LSEON=1, LSERDY=0, LSECSSD=1 (quartz arrete)
GPIOD->MODER = 0xFFFFFFFF broches QSPI jamais configurees
Note
Le défaut ne se voit qu’au démarrage à froid. Le domaine backup reste alimenté par VDD à travers les resets : tant que la carte n’est pas débranchée, le LSE continue d’osciller et tout semble normal. C’est exactement ce qui se passera la première fois qu’un robot sera allumé sur le terrain.
Et comme seul le binaire était disponible, il était impossible de le corriger. D’où la règle retenue : un bootloader ne touche à rien dont il n’a pas besoin.
Le bootloader du projet
firmware/ratbot_bootloader/ — 364 octets, un seul .c, un linker, un
Makefile. Ni HAL, ni CMSIS, ni CubeMX : rien qu’une régénération puisse casser.
Il fait trois choses :
configurer les six broches QSPI en fonction alternative ;
mapper la flash externe en mémoire (lecture
0x03, un fil) ;vérifier que le pointeur de pile à
0x90000000est plausible, puis sauter.
Pas de RTC, pas de LSE, pas d’USB, pas de bouton, pas de LED. Toute la classe de panne décrite ci-dessus disparaît par construction.
Il ne configure même pas l’horloge : il reste sur le HSI actif au reset. Le
prescaler QUADSPI (3) est choisi pour rester valide dans les deux régimes —
16 MHz au boot sur HSI, 30 MHz une fois que l’application a monté HCLK à
120 MHz — et les deux restent sous les 50 MHz maximum de la commande 0x03.
Un étage de moins, un mode de panne de moins.
cd firmware/ratbot_bootloader
make # construit build/ratbot_bootloader.elf
make flash # l'ecrit dans la flash INTERNE via ST-LINK
Le linker refuse de produire un binaire si quelqu’un ajoute une variable
globale : il n’y a pas de code d’initialisation de .data / .bss, et
l’oubli serait silencieux.
Warning
Piège de validation à connaître. La pile Cortex-M est descendante :
_estack vaut exactement fin de RAM + 1, soit 0x20020000 pour le
DTCMRAM. Un contrôle par masque du type
(sp & 0xFFFE0000) == 0x20000000 rejette cette valeur pourtant
légitime. Le bootloader compare donc des plages, borne haute incluse.
Programmer la carte
Deux mémoires, deux opérations distinctes. L’external loader se passe en argument, sans avoir à l’installer dans STM32CubeProgrammer :
# 1. l'application -> flash QSPI externe
STM32_Programmer_CLI -c port=SWD mode=UR \
-el <chemin>/STM32H7xx_W25Q128_WeActStudio.stldr \
-w build/uROSProjet.elf -v
# 2. le bootloader -> flash interne
cd firmware/ratbot_bootloader && make flash
Le .stldr est un greffon pour STM32CubeProgrammer, qui lui apprend à écrire
dans une W25Q128. Il vit dans l’outil de programmation, le temps d’un flash, et
ne tourne jamais sur le robot. Le nôtre vient de
WeActStudio/MiniSTM32H7xx,
dossier SDK/QSPI_Flasher/.
Danger
Toujours finir par un reset LOGICIEL (-rst), jamais par -hardRst.
STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -rst
Après que l’external loader a piloté la QSPI, NRST réinitialise le
microcontrôleur mais pas la puce W25Q, qui reste dans un mode que le
bootloader de 364 octets ne remet pas à plat. La carte part alors en
HardFault dès la première instruction lue :
CFSR = 0x00000100 IBUSERR — erreur de bus sur la lecture d'instruction
HFSR = 0xC0000000
Et la SRAM AXI se relit en charabia, donc uwTick et g_uros semblent
incohérents sous ST-LINK. Tout désigne un firmware cassé alors que la flash
est parfaitement écrite.
Un reset logiciel démarre correctement. Coût de la leçon : vingt minutes le 2026-09-09, diagnostiquées comme un bug firmware.
Vérifier que ça part bien de la flash externe : lire le registre VTOR.
STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -r32 0xE000ED08 0x4
# 0x90000000 -> l'application s'execute depuis la QSPI
# 0x08000000 -> le bootloader n'a pas saute
Les réglages effacés par CubeMX
Aucun de ces réglages n’est exprimable dans le .ioc : soit la case n’existe
pas dans le GUI, soit le réglage contredit ce que CubeMX croit savoir. Ils
vivent donc dans du code généré et sautent à chaque régénération.
Fichier |
Réglage |
Sans lui |
|---|---|---|
|
drive LSE |
CubeMX y écrit |
|
|
armé dans |
|
|
CubeMX pointe sur |
|
|
la table des vecteurs reste en flash interne |
|
|
sa région de 4 Go en |
|
|
réinitialise la flash pendant que le CPU exécute depuis elle : blocage immédiat |
|
|
chaque instruction traverse la liaison série de la flash : trop lent pour tenir les interruptions UART |
Le cache de données reste volontairement désactivé : les structures de diagnostic sont relues à chaud au ST-LINK, et un D-cache actif rendrait ces lectures incohérentes.
Diagnostiquer sans UART de debug
La carte n’a pas d’UART de debug câblé, et PB3 (SWO) est utilisée par la
flash SPI : pas de printf, ni par ITM. L’état est donc exposé dans des
structures en RAM, relues au ST-LINK.
arm-none-eabi-nm build/uROSProjet.elf | grep g_uros
STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -r32 <adresse> 0x14
Deux réflexes qui font gagner des heures :
``uwTick`` figé entre deux lectures = ``Error_Handler()``. Celui-ci fait
__disable_irq(); while(1);, ce qui gèle aussi le tick. La valeur du tick indique quel timeout a expiré —~5000 msdésigne celui du LSE.Espacer les lectures. Chaque appel
-r32fige le cœur : mesuré ~57 ms de temps CPU seulement entre deux lectures rapprochées. Un compteur qui semble bloqué ne l’est pas forcément.
Organisation de la mémoire
Le H750 a quatre RAM aux propriétés très différentes, et le choix n’est pas libre : le DMA n’en atteint pas certaines, et une pile ne se place pas n’importe où.
Région |
Adresse |
Taille |
Ce qu’on y met |
|---|---|---|---|
DTCMRAM |
|
128 Ko |
la pile principale ( |
AXI SRAM |
|
512 Ko |
|
RAM_D2 |
|
288 Ko |
section |
RAM_D3 |
|
64 Ko |
libre |
.bss a été déplacé du DTCMRAM vers l’AXI SRAM le 2026-09-06 : l’extension
des entités micro-ROS l’avait fait passer à 109 Ko, ne laissant que 21 Ko de
marge. On dispose désormais de ~400 Ko pour les variables globales. Le coût
est nul ici : le code s’exécute depuis la flash QSPI, la vitesse du DTCM n’est
plus le facteur limitant.
Règles d’écriture du firmware
Trois règles qui découlent directement de ce qui précède. Elles évitent les deux modes de panne silencieux de cette carte.
1. Un gros tampon se déclare static, jamais en local.
void ma_tache(void *arg) {
uint8_t image[1024]; /* NON : sur la pile de la tache */
static uint8_t image[1024]; /* OUI : en .bss, donc en AXI SRAM */
}
Une globale trop grosse fait échouer l’édition de liens, avec le message
exact : region RAM overflowed by N bytes. Un tableau local trop gros
déborde la pile à l’exécution, sans un mot.
2. Tout tampon touché par le DMA va dans .dma_buffer.
static uint8_t buf[512] __attribute__((section(".dma_buffer")));
Sinon il atterrit en .bss — aujourd’hui en AXI SRAM, que le DMA peut
atteindre, mais au prix d’une traversée de pont inter-domaines. La section
.dma_buffer le place en RAM_D2, le domaine des contrôleurs DMA eux-mêmes.
Warning
Attention au cas où le tampon n’est pas le vôtre. dma_transport.c
reçoit de micro-ROS un pointeur sur son tas : il faut alors recopier
dans un tampon de transit avant d’armer le DMA. C’est ce que fait
cubemx_transport_write.
3. Dimensionner explicitement la pile de chaque tâche.
configCHECK_FOR_STACK_OVERFLOW est à 2 : FreeRTOS pose un motif témoin
dans chaque pile et le vérifie à chaque changement de contexte. En cas de
débordement, la carte s’arrête et laisse le nom de la tâche fautive dans
g_fault :
arm-none-eabi-nm build/uROSProjet.elf | grep g_fault
STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -r32 <adresse> 0x18
L’arrêt est délibéré : à ce stade la mémoire voisine est déjà corrompue, et laisser tourner produirait des symptômes incompréhensibles bien plus loin. Mieux vaut un robot figé et diagnosticable qu’un robot qui dérive.