O que é DMA2D?
Com o avanço dos gráficos incorporados, os microcontroladores estão assumindo tarefas cada vez mais complexas de computação gráfica e exibição. No entanto, o poder de processamento da CPU pode não ser suficiente para lidar com gráficos de alta resolução e cores vibrantes. Felizmente, a partir do STM32F429, um periférico externo semelhante a uma GPU foi introduzido nos microcontroladores STM32 pela ST, conhecido como Chrom-ART Accelerator ou DMA2D. O DMA2D fornece aceleração em muitos cenários gráficos 2D e integra efetivamente funções semelhantes a uma "GPU" encontrada nas placas gráficas modernas.
Embora o DMA2D ofereça apenas aceleração 2D e suas capacidades sejam relativamente básicas em comparação com as GPUs em PCs, ele pode atender à maioria dos requisitos de aceleração de exibição gráfica no desenvolvimento embarcado. Ao aproveitar o DMA2D de forma eficaz, podemos obter efeitos de interface do usuário suaves e impressionantes em microcontroladores.
Funções DMA2D
- Preenchimento de cor (áreas retangulares)
- Cópia de imagem (memória)
- Conversão de formato de cor (por exemplo, YCbCr para RGB ou RGB888 para RGB565)
- Mistura de transparência (mistura alfa)
As duas primeiras são operações baseadas em memória, enquanto as duas últimas envolvem aceleração computacional. A mistura de transparência e a conversão de formato de cor podem ser combinadas com a cópia de imagens, proporcionando uma flexibilidade significativa.
No desenvolvimento prático, o uso do DMA2D é semelhante ao dos controladores DMA tradicionais. Em certos cenários não gráficos, o DMA2D pode até substituir o DMA convencional para determinadas tarefas.
É importante observar que os aceleradores DMA2D em diferentes linhas de produtos ST podem apresentar pequenas diferenças. Por exemplo, o DMA2D na série MCU STM32F4 não possui a capacidade de converter entre os formatos de cor ARGB e AGBR. Portanto, quando for necessária uma funcionalidade específica, é aconselhável consultar o manual de programação para confirmar seu suporte.
Modos de operação DMA2D
Semelhante à forma como o DMA tradicional tem os modos periférico-para-periférico, periférico-para-memória e memória-para-periférico, o DMA2D, como componente DMA, também vem em quatro modos de operação:
- Registro para memória
- Memória para memória
- Memória para memória com conversão de formato de cor de pixel
- Memória para memória com conversão de formato de cor de pixel e mistura de transparência
Os dois primeiros modos envolvem operações de memória diretas, enquanto os dois últimos modos realizam a cópia da memória e, ao mesmo tempo, lidam com a conversão do formato de cor e/ou mistura de transparência, conforme necessário.
Biblioteca DMA2D e HAL
Em muitos casos, usar a biblioteca HAL simplifica a escrita do código e melhora a portabilidade. Mas, tem uma exceção quando se trata de usar o DMA2D. O principal problema com o HAL é o excesso de aninhamento e verificações de segurança, que diminuem a eficiência. Embora a perda de eficiência ao lidar com outros periféricos possa não ser grande, para o DMA2D — um acelerador focado em computação e velocidade — usar a biblioteca HAL pode diminuir bastante a eficiência da aceleração.
Consequentemente, muitas vezes evitamos usar funções HAL relevantes para operações DMA2D. Por uma questão de eficiência, é empregada a manipulação direta do registro, garantindo o máximo de benefícios de aceleração.
Como a maioria dos casos de uso do DMA2D envolve mudanças frequentes nos modos de operação, a configuração gráfica do DMA2D no CubeMX perde sua praticidade.
Aplicação do DMA2D no desenvolvimento de gráficos incorporados
Ferramentas necessárias
- Placa de desenvolvimento STM32 com periférico DMA2D x1
- Ecrã TFT a cores x1
Neste exemplo, usamos a placa de desenvolvimento ART-Pi da RT-Thread, com um STM32H750XB com frequência de clock de até 480 MHz e 32 MB de SDRAM. Ela também inclui um depurador (ST-Link V2.1). E usamos uma tela LCD TFT de 3,5" com interface RGB666 e resolução de 320×240 (QVGA).

Ambiente de desenvolvimento
O conteúdo e o código apresentados neste artigo podem ser usados em vários ambientes de desenvolvimento, como RT-Thread Studio, MDK, IAR, etc.
Antes de iniciar as experiências descritas neste artigo, você precisa de um projeto básico que acione o display LCD usando a tecnologia framebuffer. É necessário habilitar o DMA2D antes de executar qualquer um dos códigos fornecidos.
O DMA2D pode ser habilitado usando esta macro:
__HAL_RCC_DMA2D_CLK_ENABLE();
Projeto de aplicação: Preenchimento de retângulos
Os gráficos incorporados abrangem vários tipos de operações, incluindo preenchimento de retângulos, cópia de memória, mistura de transparência, etc. Usaremos o preenchimento de retângulos como exemplo. O objetivo é criar um gráfico de barras simples usando DMA2D para preenchimento de retângulos:

Primeiro, precisamos preencher a tela com uma cor branca, que servirá como fundo para o padrão. Essa etapa é crucial, pois o padrão existente na tela pode interferir no design pretendido. Em seguida, o gráfico de barras é construído usando quatro blocos retangulares azuis e um segmento de linha, que pode ser considerado um bloco retangular especial com altura 1. Portanto, desenhar esse gráfico envolve uma série de operações de "preenchimento de retângulos":
- Preencha um retângulo com a cor branca, cobrindo toda a tela.
- Preencher quatro barras de dados com a cor azul.
- Preencha um segmento de linha com a cor preta, com altura 1.
Essencialmente, desenhar um retângulo de qualquer tamanho em qualquer posição na tela envolve definir os dados de pixel no local de memória correspondente para a cor desejada. No entanto, devido ao armazenamento linear do buffer de quadros na memória, a menos que a largura do retângulo se alinhe exatamente com a largura da tela, as áreas retangulares aparentemente contínuas têm endereços de memória não contíguos.
O diagrama abaixo mostra um layout de memória típico, onde os números indicam o endereço de memória de cada pixel no buffer de quadros (deslocamento em relação ao endereço base, sem considerar pixels multibyte). A área azul representa o retângulo a ser preenchido. É evidente que os endereços de memória dentro do retângulo não são contíguos.

Essa propriedade do framebuffer nos impede de usar operações eficientes como memset para preencher regiões retangulares. Normalmente, usaríamos uma abordagem de loop aninhado como a abaixo para preencher qualquer retângulo. Aqui, xs e ys são as coordenadas do canto superior esquerdo do retângulo na tela, width e height definem as dimensões do retângulo e color especifica a cor de preenchimento:
for (int y = ys; y < ys + height; y++) {
for (int x = xs; x < xs + width; x++) {
framebuffer[y][x] = color;
}
}
Embora o código possa parecer simples, durante a execução, um número substancial de ciclos da CPU é desperdiçado em operações como verificações de condições, cálculos de endereços e incrementos, com uma parte mínima dedicada à gravação real na memória. Essa situação leva à diminuição da eficiência.
Nesses casos, o modo de trabalho do registro para a memória do DMA2D entra em ação. O DMA2D pode preencher rapidamente uma região retangular da memória, mesmo que a área não seja contígua na memória.
Usando o exemplo ilustrado na imagem acima, vamos nos aprofundar em como isso é feito:

Em primeiro lugar, como estamos lidando exclusivamente com preenchimento de memória e não com cópia, precisamos que o DMA2D opere no modo registro-para-memória. Isso é obtido definindo os bits [17:16] do registro CR do DMA2D como '11', conforme mostrado no trecho de código:
DMA2D->CR = 0x00030000UL;
Em seguida, informamos ao DMA2D os atributos do retângulo a ser preenchido, como o endereço inicial da região, sua largura em pixels e sua altura.
O endereço inicial da região é o endereço de memória do pixel superior esquerdo do retângulo (endereço do pixel vermelho no diagrama), gerenciado pelo registro OMAR do DMA2D. A largura e a altura do retângulo são ambas em pixels e são gerenciadas pelos 16 bits altos (largura) e 16 bits baixos (altura) do registro NLR. O código para definir esses valores é o seguinte:
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, como os endereços de memória do retângulo não são contíguos, precisamos instruir o DMA2D a pular um certo número de pixels após preencher uma linha de dados (ou seja, o comprimento da área amarela no diagrama). Esse valor é gerenciado pelo registro OOR. O cálculo do número de pixels a serem pulados tem um método simples: subtraia a largura do retângulo da largura da área de exibição. O código para implementar isso é:
DMA2D->OOR = screenWidthPx - width; // Set the row offset, i.e., skip pixels
Por fim, precisamos informar ao DMA2D a cor a ser usada para preenchimento e o formato da cor. Eles são gerenciados pelos registros OCOLR e OPFCCR, respectivamente. O formato da cor é definido pelas macros LTDC_PIXEL_FORMAT_XXX. O código é o seguinte:
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
Com todas as configurações definidas, o DMA2D adquiriu todas as informações necessárias para preencher o retângulo. Para iniciar a transferência, definimos o bit 0 do registro CR do DMA2D como '1':
DMA2D->CR |= DMA2D_CR_START; // Start DMA2D data transfer, where DMA2D_CR_START is a macro with the value 0x01
Assim que a transferência DMA2D começa, basta aguardar a sua conclusão. Depois de a DMA2D concluir a transferência, redefine automaticamente o bit 0 do registo CR para «0», permitindo-nos aguardar a conclusão utilizando o seguinte código:
while (DMA2D->CR & DMA2D_CR_START) {} // Wait for DMA2D transfer completion
Dica: Se você estiver usando um sistema operacional, pode habilitar a interrupção de conclusão da transferência DMA2D. Em seguida, você pode criar um semáforo, aguardá-lo após iniciar a transferência e liberá-lo na rotina de serviço de interrupção de conclusão da transferência DMA2D.
Por uma questão de generalidade da função, o endereço de início da transferência e o deslocamento da linha são calculados fora da função e passados para ela. Aqui está o código completo da função:
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 conveniência, vamos envolver isso em uma função de preenchimento retangular com base no sistema de coordenadas da sua tela:
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 fim, vamos usar o código para desenhar o gráfico apresentado no início desta seção:
// 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);
O efeito da operação do código é o seguinte:





