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 : TP 3 — Communiquer entre tâches, TP 5 — La liaison série avec le PC et TP 6 — Le niveau de 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 :
Mémoire |
Adresse |
Taille |
Particularité |
|---|---|---|---|
DTCM |
|
128 Kio |
couplée au cœur, la plus rapide ; DMA1 et DMA2 n’y ont pas accès |
AXI SRAM |
|
512 Kio |
la grande mémoire principale, passe par le cache |
SRAM D2 |
|
288 Kio |
à côté de DMA1 et DMA2 ; son horloge doit être activée |
SRAM D3 |
|
64 Kio |
domaine basse consommation, pour le BDMA |

Les mémoires du STM32H750 et leurs liaisons : le cœur atteint tout, le DMA pas la DTCM.
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_dmacomme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 etprintf, LED_1 à LED_4 en sortie). Deux tâches :blinkTaskfait clignoter LED_1 toutes les 100 ms ;sendTaskenvoie 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_Transmiten cycles du processeur, avec le compteurDWT->CYCCNT:/* 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).
Question
Quelle durée mesurez-vous ? Comparez-la au calcul. Pendant ce temps,
blinkTaskclignote-t-elle ? Pourquoi, et à quel prix ?
Solution de l’étape 1
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.
Réponse à la question 1
1. La durée mesurée. Environ 174 ms :
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 :HAL_UART_Transmit_DMA(&huart5, (uint8_t *)texte, len); osSemaphoreAcquire(txDoneSemHandle, osWaitForever);
La fin du transfert est signalée par le rappel :
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart5) { osSemaphoreRelease(txDoneSemHandle); } }
et une erreur par
HAL_UART_ErrorCallback: allumez-y LED_4.
Questions
Le texte arrive-t-il au terminal ? LED_4 s’allume-t-elle ? Cherchez l’adresse de
textedans le fichier.mapdu projet (dossierDebug) : dans quelle mémoire est-il ?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 ?
Solution de l’étape 2
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 :
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).
Réponses aux questions 2 et 3
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
texteest en DTCM (une adresse en0x2000xxxxdans 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
texteest 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.lddu projet, ajoutez la régionRAM_D2si elle n’existe pas, puis une section, juste avant/DISCARD/:.dma_buffer (NOLOAD) : { . = ALIGN(32); *(.dma_buffer) . = ALIGN(32); } >RAM_D2Déclarez le tampon dans cette section :
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 desendTask, avecsnprintfpar exemple.Activez l’horloge de la SRAM D2 au début de
main(USER CODE BEGIN 2) :__HAL_RCC_D2SRAM1_CLK_ENABLE(); __HAL_RCC_D2SRAM2_CLK_ENABLE();
Questions
Que vaut maintenant la durée de l’appel
HAL_UART_Transmit_DMA? Combien de fois plus courte que l’émission bloquante ?Pendant les 170 ms du transfert, que fait
sendTask? et le processeur ?
Solution de l’étape 3
Dans le script d’édition de liens, la région (si elle manque) :
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K
et la section, avant /DISCARD/ :
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} >RAM_D2
Dans le code :
static char texte[2048] __attribute__((section(".dma_buffer"), aligned(32)));
et, dans USER CODE BEGIN 2 :
__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é.
Réponses aux questions 4 et 5
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 :
SCB_CleanDCache_by_Addr((uint32_t *)texte, sizeof texte);
Questions
Avec le cache actif et sans nettoyage, les compteurs affichés suivent-ils ? Expliquez avec le schéma cache / mémoire / DMA.
Pourquoi le tampon est-il aligné sur 32 octets, et sa taille un multiple de 32 ? (Indice : la taille d’une ligne de cache.)
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 ?
Solution de l’étape 4
À chaque envoi, le compteur change le premier caractère de chaque ligne :
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.
Réponses aux questions 6 à 8
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.

Le DMA ne lit que la mémoire : ce qui est resté dans le cache ne part pas.
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.
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.
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_buffernon cacheable avec une région du MPU (CubeMX, CORTEX_M7 → MPU), et vérifiez que le texte reste juste sansSCB_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 Remettre la carte en état RatBot.