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 :

  1. configurer les six broches QSPI en fonction alternative ;

  2. mapper la flash externe en mémoire (lecture 0x03, un fil) ;

  3. vérifier que le pointeur de pile à 0x90000000 est 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

main.c

drive LSE MEDIUMHIGH dans SystemClock_Config

CubeMX y écrit LOW en dur et écrase le réglage posé plus haut en USER CODE : le quartz ne démarre pas à froid, timeout 5 s, Error_Handler

main.c

HAL_RCCEx_EnableLSECSS() déplacé en USER CODE BEGIN 2

armé dans SystemClock_Config, donc avant que MX_RTC_Init() ait programmé RTCSEL, il latche une fausse panne LSE (RM0433 § 8.5.13)

Makefile

LDSCRIPT = STM32H750XX_FLASH.ld

CubeMX pointe sur STM32H750xx_FLASH.ld (flash interne). Les deux noms ne diffèrent que par la casse

stm32h750xx.h

FLASH_BANK1_BASE = QSPI_BASE

la table des vecteurs reste en flash interne

main.c

MPU_Config() désactivé

sa région de 4 Go en NO_ACCESS couvre 0x90000000 et interdit l’exécution

main.c

MX_QUADSPI_Init() désactivé

réinitialise la flash pendant que le CPU exécute depuis elle : blocage immédiat

main.c

SCB_EnableICache()

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 ms désigne celui du LSE.

  • Espacer les lectures. Chaque appel -r32 fige 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

0x20000000

128 Ko

la pile principale (_estack) et le tas newlib. La plus rapide, mais invisible du DMA

AXI SRAM

0x24000000

512 Ko

.data et .bss : toutes les variables globales, le tas FreeRTOS et les piles de tâches

RAM_D2

0x30000000

288 Ko

section .dma_buffer : tout ce que le DMA touche

RAM_D3

0x38000000

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.