Proyecto DMA2D en gráficos integrados

¿Qué es DMA2D?

Con el avance de los gráficos integrados, los microcontroladores están asumiendo tareas de cálculo y visualización gráfica cada vez más complejas. Sin embargo, es posible que la potencia de procesamiento de la CPU no sea suficiente para manejar gráficos de alta resolución y colores vivos. Afortunadamente, a partir del STM32F429, ST ha introducido en los microcontroladores STM32 un periférico externo similar a una GPU, conocido como Chrom-ART Accelerator o DMA2D. DMA2D proporciona aceleración en muchos escenarios gráficos 2D e integra eficazmente funciones similares a las de una «GPU» que se encuentra en las tarjetas gráficas modernas.

Aunque DMA2D solo ofrece aceleración 2D y sus capacidades son relativamente básicas en comparación con las GPU de los ordenadores personales, puede satisfacer la mayoría de los requisitos de aceleración de la visualización de gráficos en el desarrollo integrado. Al aprovechar DMA2D de manera eficaz, podemos lograr efectos de interfaz de usuario fluidos e impresionantes en los microcontroladores.

Funciones DMA2D

  1. Relleno de color (áreas rectangulares)
  2. Copiar imagen (memoria)
  3. Conversión de formato de color (por ejemplo, YCbCr a RGB o RGB888 a RGB565)
  4. Mezcla de transparencia (mezcla alfa)

Las dos primeras son operaciones basadas en la memoria, mientras que las dos últimas implican aceleración computacional. La mezcla de transparencia y la conversión de formato de color se pueden combinar con la copia de imágenes, lo que proporciona una gran flexibilidad.

En la práctica, el uso de DMA2D es similar al de los controladores DMA tradicionales. En determinados escenarios no gráficos, DMA2D puede incluso sustituir al DMA convencional para ciertas tareas.

Es importante tener en cuenta que los aceleradores DMA2D de las diferentes líneas de productos de ST pueden presentar ligeras diferencias. Por ejemplo, el DMA2D de la serie MCU STM32F4 carece de la capacidad de convertir entre los formatos de color ARGB y AGBR. Por lo tanto, cuando se necesite una funcionalidad específica, es aconsejable consultar el manual de programación para confirmar su compatibilidad.

Modos de funcionamiento DMA2D

Al igual que el DMA tradicional tiene modos de periférico a periférico, de periférico a memoria y de memoria a periférico, el DMA2D, como componente del DMA, también tiene cuatro modos de funcionamiento:

  1. Registro a memoria
  2. De memoria a memoria
  3. De memoria a memoria con conversión de formato de color de píxeles
  4. De memoria a memoria con conversión de formato de color de píxeles y mezcla de transparencia

Los dos primeros modos implican operaciones de memoria sencillas, mientras que los dos últimos realizan copias de memoria y, al mismo tiempo, gestionan la conversión del formato de color y/o la mezcla de transparencia según sea necesario.

DMA2D y biblioteca HAL

En muchos casos, el uso de la biblioteca HAL simplifica la escritura de código y mejora la portabilidad. Sin embargo, hay una excepción cuando se trata de usar DMA2D. El principal problema con HAL radica en su excesivo anidamiento y comprobaciones de seguridad, que reducen la eficiencia. Si bien la pérdida de eficiencia al tratar con otros periféricos puede no ser sustancial, para DMA2D, un acelerador centrado en el cálculo y la velocidad, el uso de la biblioteca HAL puede disminuir significativamente su eficiencia de aceleración.

Por consiguiente, a menudo evitamos utilizar las funciones HAL relevantes para las operaciones DMA2D. En aras de la eficiencia, se emplea la manipulación directa de registros, lo que garantiza los máximos beneficios de aceleración.

Dado que la mayoría de los casos de uso de DMA2D implican cambios frecuentes en los modos de funcionamiento, la configuración gráfica de DMA2D en CubeMX pierde su practicidad.

Aplicación de DMA2D en el desarrollo de gráficos integrados

Herramientas necesarias

  • Placa de desarrollo STM32 con periférico DMA2D x1
  • Pantalla TFT en color x1

En este ejemplo, utilizamos la placa de desarrollo ART-Pi de RT-Thread, que cuenta con un STM32H750XB con una frecuencia de reloj de hasta 480 MHz y 32 MB de SDRAM. También incluye un depurador (ST-Link V2.1). Y utilizamos una pantalla LCD TFT de 3,5" con una interfaz RGB666 y una resolución de 320×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)

Entorno de desarrollo

El contenido y el código presentados en este artículo se pueden utilizar en diversos entornos de desarrollo, como RT-Thread Studio, MDK, IAR, etc.

Antes de comenzar los experimentos de este artículo, necesita un proyecto básico que controle la pantalla LCD utilizando la tecnología framebuffer. Es necesario habilitar DMA2D antes de ejecutar cualquiera de los códigos proporcionados.

DMA2D se puede habilitar utilizando esta macro:

				
					__HAL_RCC_DMA2D_CLK_ENABLE();
				
			

Proyecto de aplicación: Relleno de rectángulos

Los gráficos integrados abarcan varios tipos de operaciones, incluyendo el relleno de rectángulos, la copia de memoria, la mezcla de transparencias, etc. Usaremos el relleno de rectángulos como ejemplo. El objetivo es crear un gráfico de barras sencillo utilizando DMA2D para el relleno de rectángulos:

A Simple Histogram
A Simple Histogram

En primer lugar, debemos rellenar la pantalla con color blanco, que servirá como fondo para el patrón. Este paso es crucial, ya que el patrón existente en la pantalla podría interferir con el diseño que queremos crear. A continuación, se construye el gráfico de barras utilizando cuatro bloques rectangulares azules y un segmento de línea, que puede considerarse un bloque rectangular especial con una altura de 1. Por lo tanto, dibujar este gráfico implica una serie de operaciones de «relleno de rectángulos»:

  • Rellenar un rectángulo con color blanco, cubriendo toda la pantalla.
  • Rellenar cuatro barras de datos con color azul.
  • Rellenar un segmento de línea con color negro, con una altura de 1.

Básicamente, dibujar un rectángulo de cualquier tamaño en cualquier posición del lienzo implica establecer los datos de píxeles en la ubicación de memoria correspondiente al color deseado. Sin embargo, debido al almacenamiento lineal del búfer de trama en la memoria, a menos que la anchura del rectángulo se alinee exactamente con la anchura de la pantalla, las áreas rectangulares aparentemente continuas tienen direcciones de memoria no contiguas.

El siguiente diagrama muestra una disposición típica de la memoria, en la que los números indican la dirección de memoria de cada píxel en el búfer de trama (desplazamiento relativo a la dirección base, sin tener en cuenta los píxeles multibyte). El área azul representa el rectángulo que se va a rellenar. Es evidente que las direcciones de memoria dentro del rectángulo no son contiguas.

Memory Distribution in Frame Buffer
Memory Distribution in Frame Buffer

Esta propiedad del búfer de trama nos impide utilizar operaciones eficientes como memset para rellenar regiones rectangulares. Normalmente, utilizaríamos un enfoque de bucles anidados como el que se muestra a continuación para rellenar cualquier rectángulo. Aquí, xs e ys son las coordenadas de la esquina superior izquierda del rectángulo en la pantalla, width y height definen las dimensiones del rectángulo y color especifica el color de relleno:

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

				
			

Aunque el código pueda parecer sencillo, durante la ejecución se desperdicia un número considerable de ciclos de CPU en operaciones como comprobaciones de condiciones, cálculos de direcciones e incrementos, y solo una mínima parte se dedica a la escritura real en la memoria. Esta situación provoca una disminución de la eficiencia.

En tales casos, entra en juego el modo de funcionamiento de registro a memoria de DMA2D. DMA2D puede llenar rápidamente una región rectangular de memoria, incluso si el área no es contigua en la memoria.

Utilizando el ejemplo que se muestra en la imagen anterior, veamos cómo se consigue esto:

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

En primer lugar, dado que solo estamos trabajando con el llenado de memoria y no con la copia, necesitamos que DMA2D funcione en modo registro a memoria. Esto se consigue estableciendo los bits [17:16] del registro CR de DMA2D en «11», tal y como se muestra en el fragmento de código:

				
					DMA2D->CR = 0x00030000UL;

				
			

A continuación, informamos a DMA2D sobre los atributos del rectángulo que se va a rellenar, como la dirección inicial de la región, su anchura en píxeles y su altura.

La dirección inicial de la región es la dirección de memoria del píxel superior izquierdo del rectángulo (dirección del píxel rojo en el diagrama), gestionada por el registro OMAR de DMA2D. La anchura y la altura del rectángulo se expresan en píxeles y se gestionan mediante los 16 bits altos (anchura) y los 16 bits bajos (altura) del registro NLR. El código para establecer estos valores es el siguiente:

				
					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

				
			

Posteriormente, dado que las direcciones de memoria del rectángulo no son contiguas, debemos indicar a DMA2D que omita un determinado número de píxeles después de rellenar una fila de datos (es decir, la longitud del área amarilla del diagrama). Este valor se gestiona mediante el registro OOR. El cálculo del número de píxeles que se deben omitir se realiza mediante un método sencillo: restar la anchura del rectángulo de la anchura del área de visualización. El código para implementar esto es:

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

				
			

Por último, debemos informar a DMA2D del color que se utilizará para rellenar y del formato de color. Estos se gestionan mediante los registros OCOLR y OPFCCR, respectivamente. El formato de color se define mediante las macros LTDC_PIXEL_FORMAT_XXX. El código es el siguiente:

				
					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 todos los ajustes configurados, DMA2D ha obtenido toda la información necesaria para rellenar el rectángulo. Para iniciar la transferencia, establecemos el bit 0 del registro CR de DMA2D en «1»:

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

				
			

Una vez que comienza la transferencia DMA2D, simplemente esperamos a que finalice. Cuando DMA2D termina la transferencia, restablece automáticamente el bit 0 del registro CR a «0», lo que nos permite esperar a que finalice utilizando el siguiente código:

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

				
			

Consejo: Si utiliza un sistema operativo, puede habilitar la interrupción de transferencia completa DMA2D. A continuación, puede crear un semáforo, esperar a que se active después de iniciar la transferencia y liberarlo en la rutina de servicio de interrupción de transferencia completa DMA2D.

En aras de la generalidad de la función, la dirección de inicio de la transferencia y el desplazamiento de fila se calculan fuera de la función y se pasan a ella. Este es el código completo de la función:

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

				
			

Por comodidad, envolvamos esto en una función de relleno rectangular basada en el sistema de coordenadas de tu pantalla:

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

				
			

Por último, utilicemos el código para dibujar el gráfico presentado al principio de esta sección:

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

				
			

El efecto de la operación del código es el siguiente:

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

Compartir en:

Scroll al inicio

Cotización