TP 8 — Les mémoires ITCM et DTCM ================================ Objectifs --------- - connaître les mémoires **couplées au cœur** (*Tightly Coupled Memories*) du Cortex-M7 ; - placer une fonction en **ITCM** : déclarer une section, l'ajouter au script d'édition de liens, et la **recopier** au démarrage ; - placer des données en **DTCM**, et mesurer le gain ; - comprendre comment le firmware du robot a retrouvé sa cadence. Durée indicative : une séance de 3 h. Prérequis : :doc:`tp07_flash_externe`, dont on reprend le programme. Ce qu'il faut savoir -------------------- Les TCM ~~~~~~~ Le cœur Cortex-M7 a deux mémoires reliées **directement** à lui, sans passer par la matrice de bus ni par le cache : .. list-table:: :header-rows: 1 :widths: 14 18 12 56 * - Mémoire - Adresse - Taille - Usage * - **ITCM** - ``0x00000000`` - 64 Kio - **instructions** : lues en un cycle, toujours, sans défaut de cache * - **DTCM** - ``0x20000000`` - 128 Kio - **données** : lues et écrites en un cycle, sans cache Ce sont des RAM : leur contenu est perdu à chaque coupure. Le code destiné à l'ITCM est donc stocké en flash, puis **recopié** au démarrage, exactement comme les valeurs initiales des variables (section ``.data``). Leur limite : la taille. 64 Kio d'ITCM ne suffisent pas pour tout le programme ; on y met ce dont le **temps d'exécution** compte. Et DMA1 et DMA2 n'ont pas accès à la DTCM (TP 6). .. tikz:: Les mémoires du STM32H750 et leurs liaisons : le cœur atteint tout, le DMA pas la DTCM. \tikzset{b/.style={draw, rounded corners, align=center, font=\small, inner sep=4pt, minimum height=10mm}, m/.style={b, fill=blue!7}, lb/.style={font=\scriptsize, align=center}, ar/.style={thick}} \node[b, fill=orange!14, minimum width=30mm, minimum height=24mm] (cpu) at (0,0) {\textbf{cœur Cortex-M7}\\[-1pt]\scriptsize caches I et D : 16 Kio chacun}; \node[m] (itcm) at (-5,0.6) {ITCM\\[-2pt]\scriptsize 64 Kio, 0x0000\,0000}; \node[m] (dtcm) at (-5,-0.6) {DTCM\\[-2pt]\scriptsize 128 Kio, 0x2000\,0000}; \draw[ar, very thick] (itcm.east) -- (cpu.west |- itcm.east); \draw[ar, very thick] (dtcm.east) -- (cpu.west |- dtcm.east); \node[lb] at (-2.6,1.5) {liaisons directes,\\1 cycle par accès}; \node[b, fill=black!6, minimum width=70mm] (axi) at (2.8,-2.6) {matrice de bus AXI (domaine D1)}; \draw[ar] (cpu.south) -- (cpu.south |- axi.north); \node[m] (sram) at (0.2,-4.6) {AXI SRAM\\[-2pt]\scriptsize 512 Kio, 0x2400\,0000}; \node[m] (qspi) at (3.3,-4.6) {flash QSPI\\[-2pt]\scriptsize 8 Mio, 0x9000\,0000}; \node[m] (flash) at (6.3,-4.6) {flash interne\\[-2pt]\scriptsize 128 Kio, 0x0800\,0000}; \foreach \n in {sram, qspi, flash} \draw[ar] (\n.north) -- (\n.north |- axi.south); \node[b, fill=black!6, minimum width=36mm] (ahb) at (8.4,-0.8) {matrice AHB (domaine D2)}; \node[b, fill=green!10] (dma) at (8.4,1.2) {DMA1, DMA2}; \node[m] (d2) at (11.3,-2.6) {SRAM D2\\[-2pt]\scriptsize 288 Kio, 0x3000\,0000}; \draw[ar] (dma.south) -- (ahb.north); \draw[ar] (ahb.south) |- (d2.west); \draw[ar] (ahb.south west) ++(0.4,0) |- (axi.east); \draw[red!70!black, thick, dashed, -{Stealth}] (dma.north) -- ++(0,0.9) -| node[lb, above, pos=0.25]{DMA1 et DMA2 n'ont aucun chemin vers la DTCM} ($(dtcm.west)+(-0.6,0)$) -- (dtcm.west); \draw[red!70!black, very thick] ($(dtcm.west)+(-0.45,0.15)$) -- ++(0.3,-0.3) ($(dtcm.west)+(-0.45,-0.15)$) -- ++(0.3,0.3); Ce que le robot y met ~~~~~~~~~~~~~~~~~~~~~ Comme on l'a vu au TP 7, la boucle de publication micro-ROS du robot était tombée de 105 à 27 Hz : son code, exécuté depuis la flash externe, était sans cesse chassé du cache d'instructions. Le firmware place désormais en ITCM la sérialisation des messages, le client micro-ROS, le transport série et le noyau FreeRTOS, soit environ 59 Kio sur 64. Mesure sur le robot : **175 Hz**. Étape 1 — Déclarer la section ``.itcm`` --------------------------------------- Reprenez le projet du TP 7, en flash externe, avec I-cache actif. #. Dans ``STM32H750VBTX_FLASH.ld``, vérifiez que la région ``ITCMRAM`` existe, puis ajoutez cette section juste **après** la section ``.data`` : .. code-block:: text .itcm : { . = ALIGN(4); _sitcm = .; *(.itcm) *(.itcm*) . = ALIGN(4); _eitcm = .; } >ITCMRAM AT> FLASH _siitcm = LOADADDR(.itcm); ``>ITCMRAM`` : les adresses d'exécution sont en ITCM. ``AT> FLASH`` : le contenu est stocké en flash. ``_siitcm`` est l'adresse de stockage, ``_sitcm`` et ``_eitcm`` les bornes en ITCM. #. Dans ``main``, avant tout appel à une fonction placée en ITCM (``USER CODE BEGIN 1``, après ``__enable_irq()``), recopiez la section : .. code-block:: c extern uint32_t _siitcm, _sitcm, _eitcm; uint32_t *src = &_siitcm; uint32_t *dst = &_sitcm; while (dst < &_eitcm) { *dst++ = *src++; } __DSB(); __ISB(); .. admonition:: Questions 1. Pourquoi la recopie n'est-elle pas faite par le code de démarrage (``startup_stm32h750vbtx.s``), comme celle de ``.data`` ? 2. À quoi servent ``__DSB()`` et ``__ISB()`` après une recopie de **code** ? .. dropdown:: Réponses aux questions 1 et 2 :icon: light-bulb :color: info **1. Pourquoi pas dans le code de démarrage.** Le fichier ``startup_stm32h750vbtx.s`` est **généré** : il ne connaît que les sections standard, ``.data`` (à recopier) et ``.bss`` (à mettre à zéro), à travers les symboles ``_sidata``, ``_sdata``, ``_edata``, ``_sbss``, ``_ebss`` du script d'édition de liens. La section ``.itcm`` est une invention à nous : c'est à nous de la charger. On pourrait modifier le fichier de démarrage, mais il risquerait d'être réécrit ; le faire dans ``main``, dans un bloc ``USER CODE``, est plus sûr. Le firmware du robot fait de même. **2. DSB et ISB.** Le Cortex-M7 est un processeur **à pipeline** et à **tampon d'écriture** : il peut avoir déjà lu, décodé, voire commencé à exécuter les instructions suivantes, pendant que les écritures précédentes ne sont pas encore arrivées en mémoire. - ``DSB`` (*Data Synchronization Barrier*) attend que **toutes les écritures** en cours soient terminées : le code est vraiment en ITCM. - ``ISB`` (*Instruction Synchronization Barrier*) **vide le pipeline** : les instructions suivantes seront relues, depuis la mémoire à jour. Sans eux, le premier appel d'une fonction en ITCM pourrait exécuter l'ancien contenu de la mémoire. C'est la recommandation d'ARM après toute écriture de code ; le défaut serait rarissime, donc d'autant plus difficile à trouver. Étape 2 — Le code critique en ITCM ---------------------------------- #. Placez les vingt copies du TP 7 en ITCM : .. code-block:: c __attribute__((section(".itcm"), noinline)) static uint32_t crc_a(const uint8_t *p, uint32_t n) { ... } #. Compilez. Dans le fichier ``.map``, vérifiez l'adresse de ``crc_a`` et la taille totale de la section ``.itcm``. #. Programmez et mesurez, comme au TP 7. .. Capture d'écran à insérer : le fichier .map : la section .itcm à l'adresse 0, et crc_a en 0x0000xxxx. Déposez _static/captures/rtos/tp08-map-itcm.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp08-map-itcm.png .. :align: center .. :width: 90% .. admonition:: Questions 3. Quel est le coût par octet des vingt copies, maintenant ? Comparez avec le TP 7 : flash interne, flash externe sans cache, avec cache. 4. Placez **aussi** ``crc32`` seule en ITCM. Gagne-t-elle quelque chose ? Pourquoi ? 5. Que se passe-t-il si l'on oublie la recopie ? (Essayez : le résultat est instructif, et la carte se relance par un simple reset.) .. dropdown:: Solution des étapes 1 et 2 :icon: code :color: secondary Le script d'édition de liens (après ``.data``) et la recopie sont donnés dans le sujet. Puis on marque les fonctions : .. code-block:: c __attribute__((section(".itcm"), noinline)) static uint32_t crc_a(const uint8_t *p, uint32_t n) { /* … */ } (dans la macro ``CRC_COPY`` du TP 7, l'attribut se met une fois pour les vingt copies). **Ce que fait l'éditeur de liens.** ``>ITCMRAM AT> FLASH`` donne à la section **deux adresses** : une adresse d'**exécution** (VMA) en ITCM, en ``0x0000xxxx``, à laquelle tout le code qui appelle ``crc_a`` fait référence ; et une adresse de **chargement** (LMA) en flash, où le contenu est rangé dans le fichier programmé. La recopie au démarrage fait passer le code de l'une à l'autre. C'est exactement le mécanisme de ``.data``. **Un appel lointain.** Le code qui appelle ``crc_a`` est en flash externe (``0x9000xxxx``), la fonction en ITCM (``0x0000xxxx``) : 2,4 Gio d'écart, bien plus que la portée d'une instruction d'appel directe (±16 Mio). L'éditeur de liens le détecte et insère seul un petit relais (*veneer*) qui charge l'adresse complète. Rien à faire, mais c'est pour cela qu'un appel vers l'ITCM coûte quelques cycles de plus. **Vérifier au .map** : la section ``.itcm`` doit apparaître à l'adresse 0 avec une taille d'une trentaine de Kio, et ``crc_a`` à une adresse en ``0x0000xxxx``. .. dropdown:: Réponses aux questions 3 à 5 :icon: light-bulb :color: info **3. Les vingt copies en ITCM.** Leur coût par octet **redevient celui de crc32 seule** (et celui de la flash interne avec cache). L'ITCM livre les instructions **en un cycle, à chaque fois**, sans passer par le cache : la taille du code n'a plus d'importance, tant qu'il tient dans les 64 Kio. Récapitulatif attendu, du plus lent au plus rapide (coût par octet des vingt copies) : flash externe sans cache ≫ flash externe avec cache (le code dépasse le cache) > flash externe avec ITCM ≈ flash interne avec cache. **4. crc32 seule en ITCM.** Elle ne gagne **presque rien** : sa boucle tenait déjà dans le cache, qui la servait aussi vite. L'ITCM est utile pour le code qui **ne tient pas** dans le cache, ou dont le temps d'exécution doit être **garanti** (une interruption fréquente, une boucle de commande) : en ITCM, pas de premier passage plus lent, pas de variation selon ce qu'ont fait les autres tâches. Les 64 Kio sont précieux : on y place le code mesuré comme critique, pas tout le programme. **5. Sans la recopie.** L'ITCM contient n'importe quoi (souvent des zéros après une mise sous tension, ou les restes du programme précédent). Le premier appel de ``crc_a`` exécute ces octets comme des instructions : **HardFault** (instruction invalide), ou pire, un code qui fait n'importe quoi. Le ``.map``, lui, montre des adresses parfaitement justes : **rien** ne prévient à la compilation. C'est le genre d'erreur qu'on ne trouve qu'en sachant comment le programme est chargé. Étape 3 — Les données en DTCM ----------------------------- Les données lues par le calcul (``g_data``) sont aussi en mémoire. Selon le script d'édition de liens, elles sont en DTCM ou en AXI SRAM : vérifiez au ``.map``. #. Ajoutez au script deux sections non initialisées, une par mémoire (le nom de la région AXI SRAM peut être ``RAM_D1`` ou ``RAM`` selon le script) : .. code-block:: text .dtcm_bss (NOLOAD) : { . = ALIGN(4); *(.dtcm_bss) } >DTCMRAM .axi_bss (NOLOAD) : { . = ALIGN(4); *(.axi_bss) } >RAM_D1 #. Déclarez deux tableaux de 4 Kio, un dans chaque section, remplissez-les de la même façon, et mesurez ``crc32`` (placée en ITCM) sur chacun, **D-cache désactivé**, puis **D-cache activé**. .. admonition:: Questions 6. Quelle différence entre DTCM et AXI SRAM, sans D-cache ? Et avec ? 7. Pour un tampon de réception DMA, laquelle des deux choisir ? Et pour la pile d'une tâche ? .. dropdown:: Solution de l'étape 3 :icon: code :color: secondary .. code-block:: c static uint8_t g_dtcm[4096] __attribute__((section(".dtcm_bss"))); static uint8_t g_axi[4096] __attribute__((section(".axi_bss"))); /* remplissage identique, puis : */ t0 = DWT->CYCCNT; crc = crc32(g_dtcm, 4096u); cy = DWT->CYCCNT - t0; printf("donnees en DTCM (%08lx) : %lu cycles/octet\r\n", (unsigned long)crc, (unsigned long)(cy / 4096u)); /* idem avec g_axi */ Le D-cache s'active dans CubeMX (**CPU DCache : Enabled**). ``crc32`` est en ITCM : seule la mémoire des **données** change entre les deux mesures. .. dropdown:: Réponses aux questions 6 et 7 :icon: light-bulb :color: info **6. DTCM et AXI SRAM.** - **Sans D-cache** : la DTCM est nettement plus rapide. Elle répond en un cycle, sur un bus privé du cœur. L'AXI SRAM est derrière la matrice de bus AXI, avec plusieurs cycles de latence par accès, et partagée avec le DMA et les autres maîtres. - **Avec D-cache** : les deux deviennent **comparables**. Le tableau de 4 Kio tient dans le cache de 16 Kio : après le premier passage, toutes les lectures dans l'AXI SRAM sont servies par le cache. La DTCM, elle, ne passe pas par le cache (elle n'en a pas besoin). **7. Quelle mémoire pour quoi.** - **Un tampon de réception DMA** : jamais la DTCM, que DMA1 et DMA2 n'atteignent pas (TP 6). L'AXI SRAM, ou mieux la SRAM D2, à côté des contrôleurs DMA, de préférence dans une zone non cacheable. - **La pile d'une tâche** : la DTCM convient très bien. Le DMA n'y touche jamais, et la pile est la mémoire la plus sollicitée (variables locales, sauvegarde des registres à chaque appel et à chaque changement de tâche) : autant qu'elle soit la plus rapide. Le firmware du robot a fait d'autres choix, guidés par la **place** : sa section ``.bss`` est devenue trop grosse pour la DTCM (109 Kio sur 128), et a été déplacée en AXI SRAM, qui en offre 512 ; la vitesse de la DTCM n'était plus le facteur limitant, le code s'exécutant depuis la flash externe. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/rtos/tp08/taches.c>` — compilé sans avertissement contre la HAL et le FreeRTOS 10.6.2 du firmware RatBot. Le fichier contient les **fonctions des tâches**, qui remplacent les squelettes générés par CubeMX (dans ``main.c`` ou ``freertos.c`` selon la version), et les extraits à placer dans les blocs ``USER CODE`` signalés en commentaire. La configuration CubeMX attendue est rappelée en tête du fichier, et les réponses aux questions à la fin. Exercices supplémentaires ------------------------- Ces exercices n'ont pas de correction. - **Les interruptions en ITCM** : placez la table des vecteurs et le gestionnaire d'une interruption fréquente en ITCM, et mesurez le temps de réponse (une broche basculée dans l'interruption, observée à l'oscilloscope). - **Le noyau en ITCM** : comme le firmware du robot, placez les fichiers ``tasks.o``, ``queue.o``, ``list.o`` et ``port.o`` de FreeRTOS en ITCM avec des motifs de fichier dans le script (``*/tasks.o(.text .text*)``), et excluez-les de la section ``.text``. Mesurez le temps d'un changement de tâche. - **Le budget** : écrivez une règle qui fait échouer l'édition de liens si ``.itcm`` dépasse 60 Kio (``ASSERT`` dans le script). Après le TP ----------- La flash externe contient votre programme : réinstallez celui du robot, voir :ref:`etat-ratbot`.