Projet DMA2D dans le domaine des graphiques embarqués

Qu'est-ce que DMA2D ?

Avec les progrès réalisés dans le domaine des graphiques intégrés, les microcontrôleurs prennent en charge des tâches de calcul et d'affichage graphiques de plus en plus complexes. Cependant, la puissance de traitement du CPU peut s'avérer insuffisante pour gérer des graphiques haute résolution et aux couleurs vives. Heureusement, à partir du STM32F429, un périphérique externe similaire à un GPU a été introduit dans les microcontrôleurs STM32 par ST, connu sous le nom de Chrom-ART Accelerator ou DMA2D. Le DMA2D fournit une accélération dans de nombreux scénarios graphiques 2D et intègre efficacement des fonctions similaires à celles d'un « GPU » que l'on trouve dans les cartes graphiques modernes.

Bien que le DMA2D n'offre qu'une accélération 2D et que ses capacités soient relativement basiques par rapport aux GPU des PC, il peut répondre à la plupart des exigences d'accélération de l'affichage graphique dans le développement embarqué. En exploitant efficacement le DMA2D, nous pouvons obtenir des effets d'interface utilisateur fluides et époustouflants sur les microcontrôleurs.

Fonctions DMA2D

  1. Remplissage de couleur (zones rectangulaires)
  2. Copie d'image (mémoire)
  3. Conversion du format couleur (par exemple, YCbCr vers RGB ou RGB888 vers RGB565)
  4. Mélange de transparence (mélange alpha)

Les deux premières opérations sont basées sur la mémoire, tandis que les deux dernières impliquent une accélération informatique. Le mélange de transparence et la conversion de format de couleur peuvent être combinés avec la copie d'image, offrant ainsi une grande flexibilité.

Dans la pratique, l'utilisation du DMA2D est similaire à celle des contrôleurs DMA traditionnels. Dans certains scénarios non graphiques, le DMA2D peut même remplacer le DMA conventionnel pour certaines tâches.

Il est important de noter que les accélérateurs DMA2D des différentes gammes de produits ST peuvent présenter de légères différences. Par exemple, le DMA2D de la série de microcontrôleurs STM32F4 ne permet pas de convertir les formats de couleur ARGB et AGBR. Par conséquent, lorsque vous avez besoin d'une fonctionnalité spécifique, il est conseillé de consulter le manuel de programmation pour vérifier qu'elle est prise en charge.

Modes de fonctionnement DMA2D

Tout comme le DMA traditionnel dispose des modes périphérique-à-périphérique, périphérique-à-mémoire et mémoire-à-périphérique, le DMA2D, en tant que composant DMA, propose également quatre modes de fonctionnement :

  1. Registre vers mémoire
  2. De la mémoire à la mémoire
  3. De la mémoire à la mémoire avec conversion du format de couleur des pixels
  4. Mémoire vers mémoire avec conversion du format couleur des pixels et mélange de transparence

Les deux premiers modes impliquent des opérations mémoire simples, tandis que les deux derniers modes effectuent une copie de la mémoire tout en gérant simultanément la conversion du format couleur et/ou le mélange de transparence selon les besoins.

Bibliothèque DMA2D et HAL

Dans de nombreux cas, l'utilisation de la bibliothèque HAL simplifie l'écriture du code et améliore la portabilité. Cependant, il existe une exception lorsqu'il s'agit d'utiliser DMA2D. Le principal problème avec HAL réside dans ses imbrications excessives et ses contrôles de sécurité, qui réduisent l'efficacité. Si la perte d'efficacité lors du traitement d'autres périphériques n'est peut-être pas considérable, pour DMA2D, un accélérateur axé sur le calcul et la vitesse, l'utilisation de la bibliothèque HAL peut réduire considérablement son efficacité d'accélération.

Par conséquent, nous évitons souvent d'utiliser les fonctions HAL pertinentes pour les opérations DMA2D. Pour des raisons d'efficacité, nous utilisons la manipulation directe des registres, ce qui garantit des avantages d'accélération maximaux.

Étant donné que la majorité des cas d'utilisation de DMA2D impliquent des changements fréquents de modes de fonctionnement, la configuration graphique de DMA2D dans CubeMX perd de son utilité pratique.

Application de DMA2D dans le développement graphique embarqué

Outils nécessaires

  • Carte de développement STM32 avec périphérique DMA2D x1
  • Écran couleur TFT x1

Dans cet exemple, nous utilisons la carte de développement ART-Pi de RT-Thread, équipée d'un STM32H750XB avec une fréquence d'horloge pouvant atteindre 480 MHz et 32 Mo de SDRAM. Elle comprend également un débogueur (ST-Link V2.1). Nous utilisons un écran LCD TFT de 3,5 pouces avec une interface RGB666 et une résolution de 320 x 240 (QVGA).

ART-Pi Development Board-Produced by RT-Thread (with TFT LCD Display)
ART-Pi Development Board-Produced by RT-Thread (with TFT LCD Display)

Environnement de développement

Le contenu et le code présentés dans cet article peuvent être utilisés dans divers environnements de développement tels que RT-Thread Studio, MDK, IAR, etc.

Avant de commencer les expériences décrites dans cet article, vous devez disposer d'un projet de base qui pilote l'écran LCD à l'aide de la technologie framebuffer. Il est nécessaire d'activer DMA2D avant d'exécuter le code fourni.

Le DMA2D peut être activé à l'aide de cette macro :

				
					__HAL_RCC_DMA2D_CLK_ENABLE();
				
			

Projet d'application : remplissage rectangulaire

Les graphiques intégrés englobent différents types d'opérations, notamment le remplissage de rectangles, la copie de mémoire, le mélange de transparence, etc. Nous utiliserons le remplissage de rectangles comme exemple. L'objectif est de créer un graphique à barres simple à l'aide de DMA2D pour le remplissage de rectangles :

A Simple Histogram
A Simple Histogram

Tout d'abord, nous devons remplir l'écran avec une couleur blanche, qui servira d'arrière-plan pour le motif. Cette étape est cruciale, car le motif existant à l'écran pourrait interférer avec le dessin que nous souhaitons réaliser. Ensuite, le graphique à barres est construit à l'aide de quatre blocs rectangulaires bleus et d'un segment de ligne, qui peut être considéré comme un bloc rectangulaire spécial d'une hauteur de 1. Par conséquent, le dessin de ce graphique implique une série d'opérations de « remplissage de rectangle » :

  • Remplir un rectangle de couleur blanche, couvrant tout l'écran.
  • Remplir quatre barres de données avec la couleur bleue.
  • Remplir un segment de ligne avec la couleur noire, d'une hauteur de 1.

Essentiellement, dessiner un rectangle de n'importe quelle taille à n'importe quelle position sur la toile implique de définir les données de pixels à l'emplacement mémoire correspondant à la couleur souhaitée. Cependant, en raison du stockage linéaire du tampon de trame dans la mémoire, à moins que la largeur du rectangle ne corresponde exactement à la largeur de l'écran, les zones rectangulaires apparemment continues ont des adresses mémoire non contiguës.

Le diagramme ci-dessous illustre une disposition mémoire typique, où les chiffres indiquent l'adresse mémoire de chaque pixel dans le tampon de trame (décalage par rapport à l'adresse de base, sans tenir compte des pixels multioctets). La zone bleue représente le rectangle à remplir. Il est évident que les adresses mémoire à l'intérieur du rectangle ne sont pas contiguës.

Memory Distribution in Frame Buffer
Memory Distribution in Frame Buffer

Cette propriété du tampon graphique nous empêche d'utiliser des opérations efficaces telles que memset pour remplir des régions rectangulaires. En général, nous utilisons une approche à boucles imbriquées comme celle ci-dessous pour remplir n'importe quel rectangle. Ici, xs et ys sont les coordonnées du coin supérieur gauche du rectangle à l'écran, width et height définissent les dimensions du rectangle, et color spécifie la couleur de remplissage :

				
					for (int y = ys; y < ys + height; y++) {
    for (int x = xs; x < xs + width; x++) {
        framebuffer[y][x] = color;
    }
}

				
			

Bien que le code puisse sembler simple, lors de son exécution, un nombre important de cycles CPU est gaspillé dans des opérations telles que les vérifications de conditions, les calculs d'adresses et les incréments, seule une partie minime étant consacrée à l'écriture effective en mémoire. Cette situation entraîne une baisse d'efficacité.

Dans de tels cas, le mode de fonctionnement registre-mémoire du DMA2D entre en jeu. Le DMA2D peut rapidement remplir une région de mémoire rectangulaire, même si celle-ci n'est pas contiguë dans la mémoire.

À l'aide de l'exemple illustré dans l'image ci-dessus, voyons comment cela est possible :

Illustration of DMA2D Filling Memory Area
Illustration of DMA2D Filling Memory Area

Tout d'abord, comme nous traitons uniquement le remplissage de la mémoire et non la copie, nous avons besoin que DMA2D fonctionne en mode registre-mémoire. Pour cela, il suffit de régler les bits [17:16] du registre CR de DMA2D sur « 11 », comme indiqué dans l'extrait de code :

				
					DMA2D->CR = 0x00030000UL;

				
			

Ensuite, nous informons DMA2D des attributs du rectangle à remplir, tels que l'adresse de départ de la région, sa largeur en pixels et sa hauteur.

L'adresse de départ de la région est l'adresse mémoire du pixel supérieur gauche du rectangle (adresse du pixel rouge dans le diagramme), gérée par le registre OMAR de DMA2D. La largeur et la hauteur du rectangle sont toutes deux exprimées en pixels et sont gérées par les 16 bits supérieurs (largeur) et les 16 bits inférieurs (hauteur) du registre NLR. Le code permettant de définir ces valeurs est le suivant :

				
					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

				
			

Par la suite, comme les adresses mémoire du rectangle ne sont pas contiguës, nous devons demander à DMA2D de sauter un certain nombre de pixels après avoir rempli une ligne de données (c'est-à-dire la longueur de la zone jaune dans le diagramme). Cette valeur est gérée par le registre OOR. Le calcul du nombre de pixels à sauter est simple : il suffit de soustraire la largeur du rectangle de la largeur de la zone d'affichage. Le code pour implémenter cela est le suivant :

				
					DMA2D->OOR = screenWidthPx - width; // Set the row offset, i.e., skip pixels

				
			

Enfin, nous devons informer DMA2D de la couleur à utiliser pour le remplissage et du format de couleur. Ceux-ci sont gérés respectivement par les registres OCOLR et OPFCCR. Le format de couleur est défini par les macros LTDC_PIXEL_FORMAT_XXX. Le code est le suivant :

				
					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

				
			

Une fois tous les paramètres définis, DMA2D dispose de toutes les informations nécessaires pour remplir le rectangle. Pour lancer le transfert, nous définissons le bit 0 du registre CR de DMA2D sur « 1 » :

				
					DMA2D->CR |= DMA2D_CR_START; // Start DMA2D data transfer, where DMA2D_CR_START is a macro with the value 0x01

				
			

Une fois que le transfert DMA2D commence, nous attendons simplement qu'il soit terminé. Une fois le transfert DMA2D terminé, le bit 0 du registre CR est automatiquement réinitialisé à « 0 », ce qui nous permet d'attendre la fin du transfert à l'aide du code suivant :

				
					while (DMA2D->CR & DMA2D_CR_START) {} // Wait for DMA2D transfer completion

				
			

Conseil : si vous utilisez un système d'exploitation, vous pouvez activer l'interruption de fin de transfert DMA2D. Vous pouvez ensuite créer un sémaphore, l'attendre après avoir lancé le transfert, puis le libérer dans la routine de service d'interruption de fin de transfert DMA2D.

Pour des raisons de généralité de la fonction, l'adresse de début du transfert et le décalage de ligne sont calculés en dehors de la fonction et transmis. Voici le code complet de la fonction :

				
					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) {}
}

				
			

Pour plus de commodité, intégrons cela dans une fonction de remplissage rectangulaire basée sur le système de coordonnées de votre écran :

				
					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);
}

				
			

Enfin, utilisons le code pour dessiner le graphique présenté au début de cette section :

				
					// 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'effet du fonctionnement du code est le suivant :

The Final Effect of DMA2D Rectangle Filling
The Final Effect of DMA2D Rectangle Filling

Part à:

Retour en haut

Instant Quote