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 : .. code-block:: bash ./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 : .. code-block:: text 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. .. code-block:: bash 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 : .. code-block:: bash # 1. l'application -> flash QSPI externe STM32_Programmer_CLI -c port=SWD mode=UR \ -el /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``. .. code-block:: bash 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 : .. code-block:: text 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. .. code-block:: bash 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. .. list-table:: :header-rows: 1 :widths: 26 30 44 * - 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. .. code-block:: bash arm-none-eabi-nm build/uROSProjet.elf | grep g_uros STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -r32 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ù. .. list-table:: :header-rows: 1 :widths: 22 14 12 52 * - 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.** .. code-block:: c 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``. .. code-block:: c 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`` : .. code-block:: bash arm-none-eabi-nm build/uROSProjet.elf | grep g_fault STM32_Programmer_CLI -c port=SWD mode=HOTPLUG -r32 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.