TP 6 — Le DMA ============= Objectifs --------- - transférer des données **sans occuper le processeur**, avec un contrôleur DMA ; - prévenir une tâche de la fin d'un transfert, par une interruption et un sémaphore ; - comprendre les deux pièges du DMA sur le STM32H7 : les **mémoires qu'il ne peut pas atteindre**, et le **cache de données**. Durée indicative : deux séances de 3 h. Prérequis : :doc:`tp03_communiquer`, :doc:`../sans_os/tp05_uart` et :doc:`../sans_os/tp06_batterie`. Ce qu'il faut savoir -------------------- Le DMA ~~~~~~ Envoyer 2 000 caractères à 115 200 bit/s prend environ 170 ms. Avec ``HAL_UART_Transmit``, le processeur attend chaque octet : 170 ms perdues. Un contrôleur **DMA** (*Direct Memory Access*) recopie lui-même les octets de la mémoire vers l'UART, au rythme où l'UART les demande. Le processeur lance le transfert, puis fait autre chose ; une interruption le prévient à la fin. Le STM32H750 a plusieurs contrôleurs : **DMA1** et **DMA2** (ceux qu'on utilise ici), le **BDMA** et le **MDMA**. Un **DMAMUX** relie chaque requête de périphérique (« UART5 a besoin d'un octet ») à un canal DMA. Les mémoires du STM32H750 ~~~~~~~~~~~~~~~~~~~~~~~~~ Le microcontrôleur a plusieurs RAM, reliées différemment au cœur et aux contrôleurs DMA : .. list-table:: :header-rows: 1 :widths: 18 18 12 52 * - Mémoire - Adresse - Taille - Particularité * - DTCM - ``0x20000000`` - 128 Kio - couplée au cœur, la plus rapide ; **DMA1 et DMA2 n'y ont pas accès** * - AXI SRAM - ``0x24000000`` - 512 Kio - la grande mémoire principale, passe par le **cache** * - SRAM D2 - ``0x30000000`` - 288 Kio - à côté de DMA1 et DMA2 ; son horloge doit être activée * - SRAM D3 - ``0x38000000`` - 64 Kio - domaine basse consommation, pour le BDMA .. 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); Selon le script d'édition de liens généré, les variables globales peuvent se retrouver en DTCM. Un transfert DMA depuis ou vers la DTCM **échoue** : le contrôleur signale une erreur de bus, et rien ne part. Le cache de données ~~~~~~~~~~~~~~~~~~~ Le Cortex-M7 a un **cache de données** de 16 Kio : il garde une copie des données récemment lues ou écrites, pour ne pas retourner chaque fois en mémoire. Le DMA, lui, ne voit que la mémoire. Si le cache est actif : - après une **écriture** du processeur, la donnée peut rester dans le cache : le DMA envoie l'**ancienne** valeur ; - après une **écriture** du DMA, le processeur peut relire la copie **périmée** de son cache. Trois solutions : nettoyer (*clean*) ou invalider (*invalidate*) le cache avant ou après chaque transfert, placer les tampons DMA dans une zone **non cacheable** grâce au MPU, ou laisser le cache de données désactivé. Le firmware du robot place tous ses tampons DMA en SRAM D2, dans une zone déclarée non cacheable : sans cela, il émettait de temps en temps des trames périmées, une panne intermittente et silencieuse. Étape 1 — Une émission bloquante, mesurée ----------------------------------------- #. Créez un projet ``rtos_tp06_dma`` comme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 et ``printf``, LED_1 à LED_4 en sortie). Deux tâches : ``blinkTask`` fait clignoter LED_1 toutes les 100 ms ; ``sendTask`` envoie un texte toutes les 2 s. #. Préparez un texte de 2 000 caractères dans un tableau **global** (40 lignes de 50 caractères, par exemple). #. Mesurez la durée de ``HAL_UART_Transmit`` en **cycles** du processeur, avec le compteur ``DWT->CYCCNT`` : .. code-block:: c /* une fois, au demarrage */ CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0u; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; uint32_t t0 = DWT->CYCCNT; HAL_UART_Transmit(&huart5, (uint8_t *)texte, len, 1000u); uint32_t cycles = DWT->CYCCNT - t0; Affichez la durée en microsecondes (64 cycles par µs). .. admonition:: Question 1. Quelle durée mesurez-vous ? Comparez-la au calcul. Pendant ce temps, ``blinkTask`` clignote-t-elle ? Pourquoi, et à quel prix ? .. dropdown:: Solution de l'étape 1 :icon: code :color: secondary .. code-block:: c static char texte[2048]; /* 40 lignes de 50 caractères */ void StartSend(void *argument) { uint32_t len = 40u * 50u; (void)argument; for (uint32_t l = 0u; l < 40u; l++) /* 48 caractères + \r\n */ { snprintf(&texte[l * 50u], 51u, "0 ligne %02lu .....................................\r\n", (unsigned long)l); } for (;;) { uint32_t t0 = DWT->CYCCNT; HAL_UART_Transmit(&huart5, (uint8_t *)texte, (uint16_t)len, 1000u); uint32_t cycles = DWT->CYCCNT - t0; printf("%lu us dans l'appel\r\n", (unsigned long)(cycles / 64u)); osDelay(2000); } } **Le compteur de cycles.** Le Cortex-M7 a un bloc de mise au point, le **DWT** (*Data Watchpoint and Trace*), qui contient un compteur de cycles d'horloge de 32 bits, ``CYCCNT``. Il faut d'abord autoriser les blocs de trace (bit ``TRCENA`` du registre ``DEMCR``), puis lancer le compteur (``CYCCNTENA``). À 64 MHz, un cycle dure 15,6 ns : c'est l'outil le plus précis pour mesurer une durée de code, sans aucun matériel. Il reboucle toutes les 67 s : une mesure plus courte reste juste grâce à la soustraction non signée. **snprintf** et sa taille : chaque ligne fait 50 caractères, et ``snprintf`` écrit aussi le ``'\0'`` final : on lui donne 51 octets. Le ``'\0'`` de chaque ligne est écrasé par le premier caractère de la ligne suivante. .. dropdown:: Réponse à la question 1 :icon: light-bulb :color: info **1. La durée mesurée.** Environ **174 ms** : .. code-block:: text 2 000 octets × 10 bits / 115 200 bit/s = 173,6 ms ``blinkTask`` continue de clignoter : à priorité égale, l'ordonnanceur lui donne son tour à chaque tic où elle est prête (TP 1). Mais pendant ces 174 ms, ``sendTask`` occupe **tout le reste** du processeur à attendre l'UART : la HAL boucle sur le drapeau ``TXE``, octet après octet. Le processeur est occupé à 100 % pour envoyer 11 520 octets par seconde, alors qu'il pourrait en copier des centaines de millions. Étape 2 — La même émission par DMA ---------------------------------- #. Dans CubeMX, onglet **DMA Settings** de UART5 : **Add**, ``UART5_TX``, **DMA1 Stream 0**, direction *Memory To Peripheral*, mode **Normal**, incrément de l'adresse mémoire coché, données en octets. Dans **NVIC Settings**, activez l'interruption **UART5 global interrupt**. Vérifiez dans **System Core → NVIC** que ces deux interruptions ont une priorité de **5 ou plus**. #. Créez un sémaphore binaire ``txDoneSem``, initialement **indisponible**. #. Remplacez l'appel par ``HAL_UART_Transmit_DMA``, mesurez sa durée, puis attendez la fin du transfert : .. Capture d'écran à insérer : CubeMX, UART5, onglet DMA Settings : UART5_TX sur DMA1 Stream 0, Memory To Peripheral, Normal. Déposez _static/captures/rtos/tp06-dma-uart5.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp06-dma-uart5.png .. :align: center .. :width: 90% .. code-block:: c HAL_UART_Transmit_DMA(&huart5, (uint8_t *)texte, len); osSemaphoreAcquire(txDoneSemHandle, osWaitForever); #. La fin du transfert est signalée par le rappel : .. code-block:: c void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart5) { osSemaphoreRelease(txDoneSemHandle); } } et une erreur par ``HAL_UART_ErrorCallback`` : allumez-y LED_4. .. admonition:: Questions 2. Le texte arrive-t-il au terminal ? LED_4 s'allume-t-elle ? Cherchez l'adresse de ``texte`` dans le fichier ``.map`` du projet (dossier ``Debug``) : dans quelle mémoire est-il ? 3. Si le texte n'arrive pas : pourquoi ? Si le texte arrive : dans quelle mémoire est-il, et pourquoi le DMA peut-il l'atteindre ? .. Capture d'écran à insérer : le fichier .map ouvert dans CubeIDE, la ligne qui donne l'adresse de texte. Déposez _static/captures/rtos/tp06-fichier-map.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/rtos/tp06-fichier-map.png .. :align: center .. :width: 90% .. dropdown:: Solution de l'étape 2 :icon: code :color: secondary .. code-block:: c void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart5) { osSemaphoreRelease(txDoneSemHandle); } } void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart == &huart5) { HAL_GPIO_WritePin(LED_4_GPIO_Port, LED_4_Pin, GPIO_PIN_SET); osSemaphoreRelease(txDoneSemHandle); /* ne pas bloquer sendTask */ } } et dans la boucle de ``sendTask`` : .. code-block:: c t0 = DWT->CYCCNT; HAL_UART_Transmit_DMA(&huart5, (uint8_t *)texte, (uint16_t)len); cycles = DWT->CYCCNT - t0; osSemaphoreAcquire(txDoneSemHandle, osWaitForever); **Ce que fait HAL_UART_Transmit_DMA.** Elle programme le flux DMA : adresse source (le tampon), adresse destination (le registre ``TDR`` de l'UART), nombre d'octets, incrément de l'adresse source seulement. Elle autorise l'UART à **demander** des octets au DMA (bit ``DMAT``), et rend la main. Ensuite, chaque fois que l'UART peut accepter un octet, il envoie une **requête** ; le DMAMUX l'aiguille vers le flux 0 de DMA1, qui copie l'octet suivant. À la fin, le DMA lève une interruption, et la HAL appelle ``HAL_UART_TxCpltCallback``. **Pourquoi un rappel d'erreur qui libère aussi le sémaphore.** Sans lui, en cas d'erreur, la tâche attendrait un sémaphore qui ne viendrait jamais : bloquée pour toujours. Toute attente d'un événement matériel doit prévoir le cas où il n'arrive pas (rappel d'erreur, ou délai d'attente). .. dropdown:: Réponses aux questions 2 et 3 :icon: light-bulb :color: info **2. et 3.** Le résultat dépend du **script d'édition de liens** généré par votre version de CubeIDE : c'est lui qui décide où vont les variables globales. - Si ``texte`` est en **DTCM** (une adresse en ``0x2000xxxx`` dans le ``.map``) : **rien ne part**, et LED_4 s'allume. La DTCM est reliée **directement au cœur**, par un bus privé ; DMA1 et DMA2 sont sur la matrice de bus AXI/AHB, qui n'a pas de chemin vers elle. Le DMA tente de lire à cette adresse, reçoit une **erreur de bus**, et s'arrête en levant son drapeau d'erreur de transfert ; la HAL appelle le rappel d'erreur. - Si ``texte`` est en **AXI SRAM** (``0x2400xxxx``) : le texte part. DMA1 atteint l'AXI SRAM par la matrice d'interconnexion, en traversant un pont entre les domaines D2 et D1. Le fichier ``.map``, produit par l'éditeur de liens dans le dossier ``Debug``, donne l'adresse et la taille de **chaque** symbole : c'est le premier réflexe quand on se demande où vit une variable. Étape 3 — Placer le tampon où le DMA peut l'atteindre ----------------------------------------------------- Quel qu'ait été le résultat de l'étape 2, placez le texte explicitement en **SRAM D2** : #. Dans le fichier ``STM32H750VBTX_FLASH.ld`` du projet, ajoutez la région ``RAM_D2`` si elle n'existe pas, puis une section, juste avant ``/DISCARD/`` : .. code-block:: text .dma_buffer (NOLOAD) : { . = ALIGN(32); *(.dma_buffer) . = ALIGN(32); } >RAM_D2 #. Déclarez le tampon dans cette section : .. code-block:: c static char texte[2048] __attribute__((section(".dma_buffer"))); ``NOLOAD`` : la section n'est pas initialisée au démarrage. Remplissez le texte au début de ``sendTask``, avec ``snprintf`` par exemple. #. Activez l'horloge de la SRAM D2 au début de ``main`` (``USER CODE BEGIN 2``) : .. code-block:: c __HAL_RCC_D2SRAM1_CLK_ENABLE(); __HAL_RCC_D2SRAM2_CLK_ENABLE(); .. admonition:: Questions 4. Que vaut maintenant la durée de l'appel ``HAL_UART_Transmit_DMA`` ? Combien de fois plus courte que l'émission bloquante ? 5. Pendant les 170 ms du transfert, que fait ``sendTask`` ? et le processeur ? .. dropdown:: Solution de l'étape 3 :icon: code :color: secondary Dans le script d'édition de liens, la région (si elle manque) : .. code-block:: text RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K et la section, avant ``/DISCARD/`` : .. code-block:: text .dma_buffer (NOLOAD) : { . = ALIGN(32); *(.dma_buffer) . = ALIGN(32); } >RAM_D2 Dans le code : .. code-block:: c static char texte[2048] __attribute__((section(".dma_buffer"), aligned(32))); et, dans ``USER CODE BEGIN 2`` : .. code-block:: c __HAL_RCC_D2SRAM1_CLK_ENABLE(); __HAL_RCC_D2SRAM2_CLK_ENABLE(); **L'attribut section** demande au compilateur de ranger la variable dans une section nommée, au lieu de ``.bss`` ; le script d'édition de liens place cette section en SRAM D2. **NOLOAD** : rien n'est recopié depuis la flash au démarrage (le tampon n'a pas de valeur initiale), ce qui évite de gaspiller la flash. **L'horloge des SRAM D2.** Sur le H7, chaque mémoire a son horloge, et celles de la SRAM D2 sont **éteintes** au démarrage. Sans les activer, les écritures partent dans le vide **sans aucune erreur** : le tampon se relit plein de zéros. C'est un piège classique de ce microcontrôleur, que le firmware du robot a rencontré. .. dropdown:: Réponses aux questions 4 et 5 :icon: light-bulb :color: info **4. La durée de l'appel.** Quelques **microsecondes** : le temps de programmer les registres du flux DMA. Contre 174 ms en mode bloquant : plusieurs dizaines de milliers de fois plus court. **5. Pendant le transfert.** ``sendTask`` est **bloquée** sur le sémaphore : elle ne consomme rien. Le processeur exécute les autres tâches, ou la tâche d'inactivité. Le DMA, lui, partage le bus avec le processeur : il prend un cycle de bus par octet, toutes les 87 µs — une charge négligeable. Les 174 ms de processeur sont **rendues** au système : c'est tout l'intérêt du DMA. Étape 4 — Le cache de données ----------------------------- #. Dans **System Core → CORTEX_M7**, activez **CPU ICache** et **CPU DCache**. Régénérez. #. Dans ``sendTask``, à chaque envoi, changez le texte : remplacez le premier caractère de chaque ligne par un compteur qui augmente (``'0'`` à ``'9'``). #. Observez le terminal sur plusieurs envois. #. Corrigez avec un **nettoyage** du cache juste avant de lancer le DMA : .. code-block:: c SCB_CleanDCache_by_Addr((uint32_t *)texte, sizeof texte); .. admonition:: Questions 6. Avec le cache actif et sans nettoyage, les compteurs affichés suivent-ils ? Expliquez avec le schéma cache / mémoire / DMA. 7. Pourquoi le tampon est-il aligné sur 32 octets, et sa taille un multiple de 32 ? (Indice : la taille d'une ligne de cache.) 8. Pour une **réception** par DMA (le DMA écrit, le processeur lit), faut-il nettoyer ou invalider le cache ? Avant ou après le transfert ? .. dropdown:: Solution de l'étape 4 :icon: code :color: secondary À chaque envoi, le compteur change le premier caractère de chaque ligne : .. code-block:: c for (uint32_t l = 0u; l < 40u; l++) { texte[l * 50u] = (char)('0' + (n % 10u)); } SCB_CleanDCache_by_Addr((uint32_t *)texte, (int32_t)sizeof texte); /* la correction */ HAL_UART_Transmit_DMA(&huart5, (uint8_t *)texte, (uint16_t)len); **Ce que fait SCB_CleanDCache_by_Addr.** Le cache de données fonctionne en **write-back** : quand le processeur écrit, la donnée est modifiée **dans le cache**, et la ligne est marquée « sale » ; elle n'est recopiée en mémoire que plus tard, quand la ligne est évincée pour faire de la place. **Nettoyer** (*clean*) une zone, c'est forcer la recopie en mémoire des lignes sales de cette zone. Après le nettoyage, mémoire et cache sont identiques, et le DMA lit les bonnes valeurs. .. dropdown:: Réponses aux questions 6 à 8 :icon: light-bulb :color: info **6. Sans nettoyage.** Les compteurs affichés **ne suivent pas** : ils restent figés, ou changent en retard, parfois d'une ligne à l'autre de façon incohérente. .. tikz:: Le DMA ne lit que la mémoire : ce qui est resté dans le cache ne part pas. \tikzset{box/.style={draw, rounded corners, align=center, font=\small, minimum height=10mm, inner sep=4pt}, ar/.style={-{Stealth}, thick}, lb/.style={font=\scriptsize, align=center}} \node[box, fill=orange!12, minimum width=26mm] (cpu) at (0,0) {processeur}; \node[box, fill=yellow!20, minimum width=40mm] (cache) at (4.6,0) {cache de données\\[-2pt]\scriptsize nouvelles valeurs, lignes « sales »}; \node[box, fill=blue!8, minimum width=40mm] (mem) at (4.6,-2.4) {mémoire (SRAM)\\[-2pt]\scriptsize anciennes valeurs}; \node[box, fill=green!8, minimum width=22mm] (dma) at (10,-2.4) {DMA → UART}; \draw[ar] (cpu) -- node[above, lb]{écrit} (cache); \draw[ar, dashed] (cache) -- node[right, lb]{recopie… plus tard\\(ou par un \emph{clean})} (mem); \draw[ar] (mem) -- node[above, lb]{lit} (dma); \draw[red!70!black, thick] (8.2,-0.6) -- (8.9,-1.3) (8.2,-1.3) -- (8.9,-0.6); \node[lb, red!70!black, anchor=west] at (9.0,-0.95) {le DMA ne voit\\pas le cache}; Le processeur écrit les nouveaux caractères **dans le cache**. Le DMA ne voit **que la mémoire**, où restent les anciens. Il envoie donc l'ancien texte, sauf pour les lignes de cache qui ont été évincées entre-temps : d'où un résultat à moitié juste, qui change selon ce que fait le reste du programme. C'est le défaut le plus sournois du DMA sur Cortex-M7 : il ne se voit pas toujours, ni de la même façon d'un essai à l'autre. **7. L'alignement sur 32 octets.** Le cache travaille par **lignes de 32 octets** : il charge, nettoie et invalide toujours des lignes entières. Si le tampon ne commence pas sur une frontière de ligne, sa première ligne de cache contient aussi la fin d'une **autre variable**. Nettoyer est sans danger ; mais **invalider** cette ligne (question 8) jetterait aussi les modifications récentes de l'autre variable, qui seraient perdues. Un tampon DMA aligné, de taille multiple de 32, n'a aucune ligne commune avec ses voisins. **8. Pour une réception par DMA.** Il faut **invalider** le cache (``SCB_InvalidateDCache_by_Addr``), **après** le transfert, avant que le processeur lise le tampon : on jette les copies du cache, qui datent d'avant le transfert, pour forcer une relecture en mémoire, où le DMA a écrit. On invalide aussi **avant** de lancer la réception : sinon, une ligne sale du cache pourrait être recopiée en mémoire **pendant** le transfert, et écraser ce que le DMA vient d'écrire. La solution du firmware du robot évite tout cela : ses tampons DMA sont dans une région déclarée **non cacheable** par le MPU (exercice supplémentaire). Plus de nettoyage ni d'invalidation à oublier. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/rtos/tp06/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. - **La batterie en continu** : la tension de la batterie (TP 6 de la partie 1) est convertie en permanence par l'ADC, en mode **circulaire** par DMA, dans un tableau de 16 mesures ; une tâche calcule la moyenne une fois par seconde sans jamais démarrer de conversion. - **Zone non cacheable** : au lieu de nettoyer le cache, déclarez la section ``.dma_buffer`` non cacheable avec une région du **MPU** (CubeMX, **CORTEX_M7 → MPU**), et vérifiez que le texte reste juste sans ``SCB_CleanDCache_by_Addr``. - **Double tampon** : préparez le texte suivant dans un second tampon pendant que le DMA envoie le premier, et alternez. Après le TP ----------- Pour rendre la carte au robot, voir :ref:`etat-ratbot`.