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 : TP 7 — Un programme en 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 :

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).

Figure made with TikZ

Les mémoires du STM32H750 et leurs liaisons : le cœur atteint tout, le DMA pas la DTCM.

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.

  1. Dans STM32H750VBTX_FLASH.ld, vérifiez que la région ITCMRAM existe, puis ajoutez cette section juste après la section .data :

    .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.

  2. Dans main, avant tout appel à une fonction placée en ITCM (USER CODE BEGIN 1, après __enable_irq()), recopiez la section :

    extern uint32_t _siitcm, _sitcm, _eitcm;
    uint32_t *src = &_siitcm;
    uint32_t *dst = &_sitcm;
    
    while (dst < &_eitcm) { *dst++ = *src++; }
    __DSB();
    __ISB();
    

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 ?

Réponses aux questions 1 et 2

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

  1. Placez les vingt copies du TP 7 en ITCM :

    __attribute__((section(".itcm"), noinline))
    static uint32_t crc_a(const uint8_t *p, uint32_t n) { ... }
    
  2. Compilez. Dans le fichier .map, vérifiez l’adresse de crc_a et la taille totale de la section .itcm.

  3. Programmez et mesurez, comme au TP 7.

Questions

  1. Quel est le coût par octet des vingt copies, maintenant ? Comparez avec le TP 7 : flash interne, flash externe sans cache, avec cache.

  2. Placez aussi crc32 seule en ITCM. Gagne-t-elle quelque chose ? Pourquoi ?

  3. 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.)

Solution des étapes 1 et 2

Le script d’édition de liens (après .data) et la recopie sont donnés dans le sujet. Puis on marque les fonctions :

__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.

Réponses aux questions 3 à 5

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.

  1. 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) :

    .dtcm_bss (NOLOAD) : { . = ALIGN(4); *(.dtcm_bss) } >DTCMRAM
    .axi_bss  (NOLOAD) : { . = ALIGN(4); *(.axi_bss)  } >RAM_D1
    
  2. 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é.

Questions

  1. Quelle différence entre DTCM et AXI SRAM, sans D-cache ? Et avec ?

  2. Pour un tampon de réception DMA, laquelle des deux choisir ? Et pour la pile d’une tâche ?

Solution de l’étape 3
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.

Réponses aux questions 6 et 7

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.

Tout le code de la correction

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 Remettre la carte en état RatBot.