Cos'è DMA2D?
Con l’avanzamento della grafica embedded, i microcontrollori stanno assumendo compiti di calcolo e visualizzazione grafica sempre più complessi. Tuttavia, la potenza di elaborazione della CPU potrebbe non essere sufficiente per gestire grafica ad alta risoluzione e con colori vivaci. Fortunatamente, a partire dalla STM32F429, ST ha introdotto nei microcontrollori STM32 una periferica esterna simile a una GPU, nota come Chrom-ART Accelerator o DMA2D. DMA2D fornisce accelerazione in molti scenari grafici 2D e integra efficacemente funzioni simili a una “GPU” presente nelle moderne schede grafiche.
Sebbene DMA2D offra solo accelerazione 2D e le sue capacità siano relativamente basilari rispetto alle GPU dei PC, può soddisfare la maggior parte dei requisiti di accelerazione della visualizzazione grafica nello sviluppo embedded. Sfruttando efficacemente DMA2D, possiamo ottenere effetti UI fluidi e sorprendenti sui microcontrollori.
Funzioni DMA2D
- Riempimento colore (aree rettangolari)
- Copia di immagini (memoria)
- Conversione del formato colore (ad esempio, YCbCr in RGB o RGB888 in RGB565)
- Blending di trasparenza (Alpha Blend)
I primi due sono operazioni basate sulla memoria, mentre gli ultimi due implicano un’accelerazione computazionale. Il blending di trasparenza e la conversione del formato colore possono essere combinati con la copia di immagini, offrendo una notevole flessibilità.
Nello sviluppo pratico, l’utilizzo di DMA2D è simile a quello dei tradizionali controller DMA. In alcuni scenari non grafici, DMA2D può persino sostituire il DMA convenzionale per determinati compiti.
È importante notare che gli acceleratori DMA2D in diverse linee di prodotti ST potrebbero avere lievi differenze. Ad esempio, il DMA2D nella MCU della serie STM32F4 manca della capacità di convertire tra i formati colore ARGB e AGBR. Pertanto, quando è necessaria una funzionalità specifica, è consigliabile consultare il manuale di programmazione per confermarne il supporto.
Modalità operative DMA2D
Similmente a come il DMA tradizionale ha modalità periferica-periferica, periferica-memoria e memoria-periferica, DMA2D, come componente DMA, è disponibile anche in quattro modalità operative:
- Registro alla memoria
- Memoria alla memoria
- Memoria alla memoria con conversione del formato colore dei pixel
- Memoria alla memoria con conversione del formato colore dei pixel e blending di trasparenza
Le prime due modalità implicano operazioni di memoria semplici, mentre le ultime due modalità eseguono la copia di memoria gestendo contemporaneamente la conversione del formato colore e/o il blending di trasparenza, se necessario.
DMA2D e libreria HAL
In molti casi, l’utilizzo della libreria HAL semplifica la scrittura del codice e ne migliora la portabilità. Tuttavia, c’è un’eccezione quando si tratta di utilizzare DMA2D. Il problema principale con HAL risiede nella sua eccessiva nidificazione e nei controlli di sicurezza, che riducono l’efficienza. Sebbene la perdita di efficienza nella gestione di altre periferiche potrebbe non essere sostanziale, per DMA2D – un acceleratore focalizzato sul calcolo e sulla velocità – l’utilizzo della libreria HAL può diminuire significativamente la sua efficienza di accelerazione.
Di conseguenza, spesso evitiamo di utilizzare le funzioni HAL pertinenti per le operazioni DMA2D. Per motivi di efficienza, viene utilizzato direttamente la manipolazione dei registri, garantendo il massimo beneficio di accelerazione.
Poiché la maggior parte dei casi d’uso di DMA2D comporta frequenti modifiche alle modalità operative, la configurazione grafica di DMA2D in CubeMX perde la sua praticità.
Applicazione di DMA2D nello sviluppo di grafica embedded
Strumenti richiesti
- Scheda di sviluppo STM32 con periferica DMA2D x1
- Schermo TFT a colori x1
In questo esempio, utilizziamo la scheda di sviluppo ART-Pi di RT-Thread, con un STM32H750XB con una frequenza di clock fino a 480 MHz e 32 MB di SDRAM. Include anche un debugger (ST-Link V2.1). E utilizziamo uno schermo LCD TFT da 3,5″ con un’interfaccia RGB666 e una risoluzione di 320×240 (QVGA).

Ambiente di sviluppo
Il contenuto e il codice presentati in questo articolo possono essere utilizzati in vari ambienti di sviluppo come RT-Thread Studio, MDK, IAR, ecc.
Prima di iniziare gli esperimenti in questo articolo, è necessario un progetto di base che gestisca il display LCD utilizzando la tecnologia framebuffer. L’abilitazione di DMA2D è richiesta prima di eseguire uno qualsiasi del codice fornito.
DMA2D può essere abilitato utilizzando questa macro:
__HAL_RCC_DMA2D_CLK_ENABLE();
Progetto applicativo: riempimento rettangoli
La grafica embedded comprende vari tipi di operazioni, tra cui il riempimento di rettangoli, la copia di memoria, la fusione di trasparenza, ecc. Utilizzeremo il riempimento di rettangoli come esempio. L’obiettivo è creare un semplice grafico a barre utilizzando DMA2D per il riempimento di rettangoli:

Innanzitutto, dobbiamo riempire lo schermo con un colore bianco, fungendo da sfondo per il modello. Questo passaggio è fondamentale, poiché il modello esistente sullo schermo potrebbe interferire con il nostro progetto previsto. Quindi, il grafico a barre è costruito utilizzando quattro blocchi rettangolari blu e un segmento di linea, che può essere considerato un blocco rettangolare speciale con un’altezza di 1. Pertanto, il disegno di questa grafica comporta una serie di operazioni di “riempimento rettangoli”:
- Riempire un rettangolo con il colore bianco, coprendo l’intero schermo.
- Riempire quattro barre di dati con il colore blu.
- Riempire un segmento di linea con il colore nero, con un’altezza di 1.
Essenzialmente, disegnare un rettangolo di qualsiasi dimensione in qualsiasi posizione sulla tela comporta l’impostazione dei dati dei pixel nella posizione di memoria corrispondente al colore desiderato. Tuttavia, a causa dell’archiviazione lineare del framebuffer in memoria, a meno che la larghezza del rettangolo non si allinei esattamente con la larghezza dello schermo, le aree rettangolari apparentemente continue hanno indirizzi di memoria non contigui.
Il diagramma seguente illustra un tipico layout di memoria, dove i numeri indicano l’indirizzo di memoria di ciascun pixel nel framebuffer (offset rispetto all’indirizzo di base, senza considerare i pixel multi-byte). L’area blu rappresenta il rettangolo da riempire. È evidente che gli indirizzi di memoria all’interno del rettangolo non sono contigui.

Questa proprietà del framebuffer ci impedisce di utilizzare operazioni efficienti come memset per riempire le regioni rettangolari. In genere, utilizzeremmo un approccio a ciclo nidificato come quello seguente per riempire qualsiasi rettangolo. Qui, xs e ys sono le coordinate dell’angolo in alto a sinistra del rettangolo sullo schermo, larghezza e altezza definiscono le dimensioni del rettangolo e colore specifica il colore di riempimento:
for (int y = ys; y < ys + height; y++) {
for (int x = xs; x < xs + width; x++) {
framebuffer[y][x] = color;
}
}
Sebbene il codice possa sembrare semplice, durante l’esecuzione, un numero sostanziale di cicli della CPU viene sprecato in operazioni come i controlli delle condizioni, i calcoli degli indirizzi e gli incrementi, con una porzione minima dedicata alla scrittura effettiva della memoria. Questa situazione porta a una diminuzione dell’efficienza.
In questi casi, entra in gioco la modalità di lavoro da registro a memoria di DMA2D. DMA2D può riempire rapidamente una regione di memoria rettangolare, anche se l’area non è contigua in memoria.
Utilizzando l’esempio illustrato nell’immagine precedente, vediamo come ciò viene realizzato:

Innanzitutto, poiché abbiamo a che fare esclusivamente con il riempimento della memoria e non con la copia, dobbiamo far funzionare DMA2D in modalità da registro a memoria. Ciò si ottiene impostando i bit [17:16] del registro CR di DMA2D su ’11’, come mostrato nello snippet di codice:
DMA2D->CR = 0x00030000UL;
Successivamente, informiamo DMA2D degli attributi del rettangolo da riempire, come l’indirizzo di partenza della regione, la sua larghezza in pixel e la sua altezza.
L’indirizzo di partenza della regione è l’indirizzo di memoria del pixel in alto a sinistra del rettangolo (indirizzo del pixel rosso nel diagramma), gestito dal registro OMAR di DMA2D. La larghezza e l’altezza del rettangolo sono entrambe in pixel e sono gestite dai 16 bit superiori (larghezza) e dai 16 bit inferiori (altezza) del registro NLR. Il codice per impostare questi valori è il seguente:
DMA2D->OMAR = (uint32_t)(&framebuffer[y][x]); // Set the starting pixel memory address for filling
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height; // Set the width and height of the rectangle
Successivamente, poiché gli indirizzi di memoria del rettangolo non sono contigui, dobbiamo istruire DMA2D a saltare un certo numero di pixel dopo aver riempito una riga di dati (ovvero, la lunghezza dell’area gialla nel diagramma). Questo valore è gestito dal registro OOR. Calcolare il numero di pixel da saltare ha un metodo semplice: sottrarre la larghezza del rettangolo dalla larghezza dell’area di visualizzazione. Il codice per implementare questo è:
DMA2D->OOR = screenWidthPx - width; // Set the row offset, i.e., skip pixels
Infine, dobbiamo informare DMA2D del colore da utilizzare per il riempimento e del formato del colore. Questi sono gestiti dai registri OCOLR e OPFCCR, rispettivamente. Il formato del colore è definito dalle macro LTDC_PIXEL_FORMAT_XXX. Il codice è il seguente:
DMA2D->OCOLR = color; // Set the color for filling
DMA2D->OPFCCR = pixelFormat; // Set the color format, e.g., use the macro LTDC_PIXEL_FORMAT_RGB565 for RGB565
Con tutte le impostazioni in atto, DMA2D ha acquisito tutte le informazioni necessarie per riempire il rettangolo. Per avviare il trasferimento, impostiamo il bit 0 del registro CR di DMA2D su ‘1’:
DMA2D->CR |= DMA2D_CR_START; // Start DMA2D data transfer, where DMA2D_CR_START is a macro with the value 0x01
Una volta che il trasferimento DMA2D inizia, attendiamo semplicemente il suo completamento. Dopo che DMA2D ha terminato il trasferimento, reimposta automaticamente il bit 0 del registro CR su ‘0’, consentendoci di attendere il completamento utilizzando il seguente codice:
while (DMA2D->CR & DMA2D_CR_START) {} // Wait for DMA2D transfer completion
Suggerimento: se stai utilizzando un sistema operativo, puoi abilitare l’interrupt di completamento del trasferimento DMA2D. Quindi, puoi creare un semaforo, attendere dopo aver avviato il trasferimento e rilasciarlo nella routine di servizio dell’interrupt di completamento del trasferimento DMA2D.
Per il bene della generalità della funzione, l’indirizzo di trasferimento iniziale e l’offset di riga vengono calcolati al di fuori della funzione e passati come parametri. Ecco il codice completo della funzione:
static inline void DMA2D_Fill(void * pDst, uint32_t width, uint32_t height, uint32_t lineOff, uint32_t pixelFormat, uint32_t color) {
/* Configure DMA2D */
DMA2D->CR = 0x00030000UL; // Configure for register-to-memory mode
DMA2D->OCOLR = color; // Set the color for filling (format should match the configured color format)
DMA2D->OMAR = (uint32_t)pDst; // Starting memory address of the fill region
DMA2D->OOR = lineOff; // Row offset, i.e., skip pixels (in pixel units)
DMA2D->OPFCCR = pixelFormat; // Set the color format
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height; // Set the width and height of the fill region (in pixel units)
/* Start transfer */
DMA2D->CR |= DMA2D_CR_START;
/* Wait for DMA2D transfer completion */
while (DMA2D->CR & DMA2D_CR_START) {}
}
Per comodità, avvolgiamo questo in una funzione di riempimento rettangolare basata sul sistema di coordinate del tuo schermo:
void FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
void* pDist = &(((uint16_t*)framebuffer)[y*320 + x]);
DMA2D_Fill(pDist, w, h, 320 - w, LTDC_PIXEL_FORMAT_RGB565, color);
}
Infine, usiamo il codice per disegnare il grafico presentato all’inizio di questa sezione:
// Fill background color
FillRect(0, 0, 320, 240, 0xFFFF);
// Draw data bars
FillRect(80, 80, 20, 120, 0x001F);
FillRect(120, 100, 20, 100, 0x001F);
FillRect(160, 40, 20, 160, 0x001F);
FillRect(200, 60, 20, 140, 0x001F);
// Draw X-axis
FillRect(40, 200, 240, 1, 0x0000);
L’effetto operativo del codice è il seguente:





