前言
GPU即圖形處理器,是現代顯卡的核心。在沒有GPU的時代,所有圖形的繪制都是由CPU來完成的,CPU需要計算圖形的邊界、顏色等數據,并且負責將數據寫入顯存。
簡單的圖形還沒有什么問題,但隨著計算機的發展(尤其是游戲的發展),需要顯示的圖形圖像越來越復雜,CPU也就越來越力不從心。所以后來GPU應運而生,將CPU從繁重的圖形計算任務中拯救了出來,大大加速了圖形的顯示速度。
而單片機這邊也有類似的發展歷程。在早期的單片機使用場景中,極少有圖形顯示的需求。即使有,也只是簡單的12864之類的顯示設備,運算量不大,單片機的CPU可以很好的處理。
但是隨著嵌入式圖形的發展,單片機需要承擔的圖形計算和顯示任務越來越多,嵌入式系統的顯示分辨率和色彩也一路飆升。慢慢地,單片機的CPU對這些計算就開始力不從心了。
所以,自STM32F429開始,一個類似GPU的外設開始加入到STM32的單片機中,ST稱之為Chrom-ART Accelerator,也叫DMA2D(本文將使用此名稱)。DMA2D可以在很多2D繪圖的場合提供加速,完美嵌合了現代顯卡中“GPU”的功能。
雖然這個“GPU”只能提供2D加速,而且功能非常簡單,與PC中的GPU不可同日而語。但是它已經可以滿足大多數嵌入式開發中的圖形顯示加速需求,只要用好了DMA2D,我們在單片機上也可以做出流暢、華麗的UI效果。
本文將從實例出發,介紹DMA2D在嵌入式圖形開發中的可以發揮的作用。目的是使讀者能簡單、快速地對DAM2D的建立最基本的概念并且學會最基本的用法。
為了防止內容過于晦澀和難懂,本文不會對DMA2D的高級功能和特性進行深入地刨析(如詳細介紹DMA2D的架構、全部的寄存器等等)。如果需要更加詳細、專業地學習DAM2D,可以在閱讀完本文后參考《STM32H743中文編程手冊》。
閱讀本文之前需要對STM32中的TFT液晶控制器(LTDC)和基本的圖形知識(如幀緩沖framebuffer、像素、顏色格式等概念)有一定的了解。
另,除了ST之外,其他不少廠商生產的MCU中也存在類似功能的外設(如NXP在RT系列中設計的的PxP),不過這些不在本文的討論范圍內,有興趣的朋友可以自行了解。
準備工作
硬件準備
可以使用任何的,帶有DMA2D外設的STM32開發板來驗證本文中的例子,如STM32F429,STM32F746,STM32H750等MCU的開發板。本文中使用的開發板是ART-Pi 。
ART-Pi是由RT-Thread官方出品的開發板,采用了主頻高達480MHz的STM32H750XB+32MB SDRAM的強悍配置。而且板載了調試器(ST-Link V2.1),使用起來非常方便,特別適合各種技術方案的驗證,用來作為本文的硬件演示平臺再合適不過了。
顯示屏可以是任意的彩色TFT顯示屏,推薦使用16位或24位顏色的RGB接口顯示屏。本文中使用的是一塊 3.5‘’ 的TFT液晶顯示屏,接口為RGB666,分辨率為320x240(QVGA)。在LTDC中,配置使用的顏色格式為RGB565
開發環境準備
本文中介紹的內容和出現的代碼可以在任何你喜歡的開發環境中使用,如RT-Thread Studio,MDK,IAR等。
開始本文的實驗前你需要一個以framebuffer技術驅動LCD顯示屏的基本工程。運行本文中所有的代碼前都需要預先使能DMA2D。
使能DMA2D可以通過這個宏來實現(硬件初始化時使能一次即可):
1// 使用DMA2D之前一定要先使能DMA2D外設2__HAL_RCC_DMA2D_CLK_ENABLE();
DMA2D的簡介
我們先來看看ST是怎么描述DMA2D的
乍一看有點晦澀,但其實說白了就以下幾個功能:
顏色填充(矩形區域)
圖像(內存)復制
顏色格式轉換(如YCbCr轉RGB或RGB888轉RGB565)
透明度混合(Alpha Blend)
前兩種都是針對內存的操作,后兩個則是運算加速操作。其中,透明度混合、顏色格式轉換可以和圖像復制一起進行,這樣就帶來了較大的靈活性。
可以看到,ST對DMA2D的定位就像它的名字一樣,是一個針對圖像處理功能強化過的DMA。而在實際開發的過程中,我們會發現DMA2D的使用方式也非常類似傳統的DMA控制器。在某些非圖形處理場合,DMA2D甚至也可以代替傳統的DMA來發揮作用。
需要注意的是,ST的不同產品線的DMA2D加速器是有微小區別的,比如STM32F4系列MCU的DMA2D就沒有ARGB和AGBR顏色格式互轉的功能,所以具體需要用到某個功能的時候,最好先查看編程手冊看所需的功能是否被支持。
本文只介紹所有平臺的DMA2D共有的功能。
DMA2D的工作模式
就像傳統DMA有外設到外設,外設到存儲器,存儲器到外設三種工作模式一樣,DMA2D作為一個DMA,也分為以下四種工作模式:
寄存器到存儲器
存儲器到存儲器
存儲器到存儲器并執行像素顏色格式轉換
存儲器到存儲器且支持像素顏色格式轉換和透明度混合
可以看出,前兩種模式起始就是簡單的內存操作,而后面兩種模式,則是在進行內存復制時,根據需要同時進行顏色格式轉換或/和透明度混合。
DMA2D和HAL庫
大多數情況下,使用HAL庫可以簡化代碼編寫,提高可移植性。但是在DMA2D的使用時則是個例外。因為HAL庫存在的最大問題就是嵌套層數再加上各種安全檢測過多效率不夠高。
在操作別的外設時,使用HAL庫損失的效率并不會有多大的影響。但是對于DMA2D這種以計算和加速為目的的外設,考慮到相關的操作會在一個屏幕的繪制周期內被多次調用,此時再使用HAL庫就會導致DAM2D的加速效率嚴重下降。
所以,我們大多時候都不會用HAL庫中的相關函數來對DMA2D進行操作。為了效率,我們會直接操作寄存器,這樣才能起到最大化的加速效果。
因為我們使用DMA2D的大多數場合都會頻繁變更工作模式,所以CubeMX中對DMA2D的圖形化配置也失去了意義。
DMA2D場景實例
1. 顏色填充
我們來思考一下如何把它繪制出來。
首先,我們需要使用白色來填充屏幕,作為圖案的背景。這個過程是不能忽略的,否則屏幕上原來顯示的圖案會對我們的主體產生干擾。然后,柱狀圖其實是由4個藍色的矩形方塊和一條線段構成的,而線段也可以視作一個特殊的,高度為1的矩形。所以,這個圖形的繪制可以分解為一系列“矩形填充”操作:
使用白色填充一個大小等于屏幕大小的的矩形
使用藍色填充四個數據條
使用黑色填充一根高度為1的線段
在畫布中實現任意位置繪制任意大小的矩形的本質就是將內存區域中對應像素位置的數據設定為指定的顏色。但是因為framebuffer在內存中的存儲是線性的,所以除非矩形的寬度正好和顯示區域的寬度重合,否看似連續的矩形的區域在內存中的地址是不連續的。
下圖展示了典型的內存分布情況,其中的數字表示了frame buffer中每個像素的內存地址(相對首地址的偏移,這里忽略掉了一個像素占多個字節的情況),藍色區域是我們要填充的矩形。可以看出矩形區域的內存地址是不連續的。
framebuffer的這種特性使得我們不能簡單使用memset這類高效的操作來實現矩形區域的填充。通常情況下,我們會使用以下方式的雙重循環來填充任意矩形,其中xs和ys是矩形左上角在屏幕上的坐標,width和height表示矩形的寬和高,color表示需要填充的顏色:
1for(int y = ys; y 《 ys + height; y++){
2 for(int x = xs; x 《 xs + width; x++){
3 framebuffer[y][x] = color;
4 }
5}
代碼雖然簡單,但實際執行時,大量的CPU周期浪費在了判斷、尋址、自增等的操作,實際寫內存的時間占比很少。這樣一來,效率就會下降。
這時候DMA2D的寄存器到存儲器工作模式就可以發揮用場了,DAM2D可以以極高的速度填充矩形的內存區域,即使這些區域在內存中實際是不連續的。
首先,因為我們只是進行內存填充,而不需要進行內存拷貝,所以我們要讓DAM2D工作在寄存器到存儲器模式。這通過設置DMA2D的CR寄存器的[17:16]位為11來實現,代碼如下:
1DMA2D-》CR = 0x00030000UL;
然后,我們要告訴DAM2D要填充的矩形的屬性,比如區域的起始地址在哪里,矩形的寬度有多少像素,矩形的高度有多少。
區域起始地址是矩形區域左上角第一個像素的內存地址(圖中紅色像素的地址),這個地址由DAM2D的OMAR寄存器管理。而矩形的寬度和高度都是以像素為單位的,分別由NLR寄存器的高16位(寬度)和低16位(高度)來進行管理,具體的代碼如下:
1DMA2D-》OMAR = (uint32_t)(&framebuffer[y][x]); // 設置填充區域的起始像素內存地址2DMA2D-》NLR = (uint32_t)(width 《《 16) | (uint16_t)height; // 設置矩形區域的寬高
接著,因為矩形在內存中的地址不連續,所以我們要告訴DMA2D在填充完一行的數據后,需要跳過多少個像素(即圖中黃色區域的長度)。這個值由OOR寄存器管理。計算跳過的像素數量有一個簡單的方法,即顯示區域的寬度減去矩形的寬度即可。具體實現代碼如下:
1DMA2D-》OOR = screenWidthPx - width; // 設置行偏移,即跳過的像素
最后,我們需要告知DAM2D,你將使用什么顏色來進行填充,顏色的格式是什么。這分別由OCOLR和OPFCCR寄存器來管理,其中顏色格式由LTDC_PIXEL_FORMAT_XXX宏來定義,具體代碼如下:
1DMA2D-》OCOLR = color; // 設置填充使用的顏色2DMA2D-》OPFCCR = pixelFormat; // 設置顏色格式,比如想設置成RGB565,就可以使用宏LTDC_PIXEL_FORMAT_RGB565
一切都設置完畢,DMA2D已經獲取到了填充這個矩形所需要的全部信息,接下來,我們要開啟DMA2D的傳輸,這通過將DMA2D的CR寄存器的第0位設置為1來實現:
1DMA2D-》CR |= DMA2D_CR_START; // 開啟DMA2D的數據傳輸,DMA2D_CR_START是一個宏,其值為0x01
等DMA2D傳輸開始后,我們只需要等待它傳輸完畢即可。DAM2D傳輸完成后,會自動把CR寄存器的第0位設置為0,所以我們可以通過以下代碼來等待DAM2D傳輸完成:
1while (DMA2D-》CR & DMA2D_CR_START) {} // 等待DMA2D傳輸完成
tips0:如果你使用了OS,則可以使能DMA2D的傳輸完畢中斷。然后我們可以創建一個信號量并且在開啟傳輸后等待它,隨后在DMA2D的傳輸完畢中斷服務函數中釋放該信號量。這樣的話CPU就可以在DMA2D工作的時候去干點別的事兒而不是在此處傻等。
tips1:當然,由于實際執行時,DMA2D進行內存填充的速度實在是太快了,以至于OS切換任務的開銷都比這個時間要長,所以即便使用了OS,我們還是會選擇死等 :)。
為了函數的通用性考慮,起始傳輸地址和行偏移都在函數外計算完畢后傳入,我們抽出的完整的函數代碼如下:
1static inline void DMA2D_Fill( void * pDst, uint32_t width, uint32_t height, uint32_t lineOff, uint32_t pixelFormat, uint32_t color) {
2 3 /* DMA2D配置 */
4 DMA2D-》CR = 0x00030000UL; // 配置為寄存器到儲存器模式 5 DMA2D-》OCOLR = color; // 設置填充使用的顏色,格式應該與設置的顏色格式相同 6 DMA2D-》OMAR = (uint32_t)pDst; // 填充區域的起始內存地址 7 DMA2D-》OOR = lineOff; // 行偏移,即跳過的像素,注意是以像素為單位 8 DMA2D-》OPFCCR = pixelFormat; // 設置顏色格式 9 DMA2D-》NLR = (uint32_t)(width 《《 16) | (uint16_t)height; // 設置填充區域的寬和高,單位是像素1011 /* 啟動傳輸 */12 DMA2D-》CR |= DMA2D_CR_START;
1314 /* 等待DMA2D傳輸完成 */15 while (DMA2D-》CR & DMA2D_CR_START) {}
16}
為了方便編寫代碼,我們再包裝一個針對所使用屏幕坐標系的矩形填充函數:
1void FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color){
2 void* pDist = &(((uint16_t*)framebuffer)[y*320 + x]);
3 DMA2D_Fill(pDist, w, h, 320 - w, LTDC_PIXEL_FORMAT_RGB565, color);
4}
最后我們嘗試用代碼把本小節剛開始的示例圖表畫出來:
1 // 填充背景色2 FillRect(0, 0, 320, 240, 0xFFFF);
3 // 繪制數據條4 FillRect(80, 80, 20, 120, 0x001f);
5 FillRect(120, 100, 20, 100, 0x001f);
6 FillRect(160, 40, 20, 160, 0x001f);
7 FillRect(200, 60, 20, 140, 0x001f);
8 // 繪制X軸9 FillRect(40, 200, 240, 1, 0x0000);
2.圖片顯示(內存復制)
假設我們現在要開發一個游戲,然后想在屏幕上顯示一團跳動的火焰。一般是由美工先把火焰的每一幀都畫出來,然后放到同一張圖片素材里面。
然后我們以一定的間隔輪流顯示每一幀圖像,就可以在屏幕上實現“跳動的火焰”這個效果了。
我們現在略過素材文件加載到內存的過程,假設這張素材圖片已經在內存中了。然后我們來考慮如何將其中的一幀圖片顯示到屏幕上。通常情況下,我們會這樣實現:先計算得出每一幀的數據在內存中的地址,然后將這一幀圖片的數據復制到framebuffer中相應的位置即可。代碼類似于這樣:
1/**
2 * 將素材中的一幀畫面復制到framebuffer中的對應位置
3 * index為畫面在幀序列中的索引
4 */ 5static void General_DisplayFrameAt(uint16_t index) {
6 // 宏說明 7 // #define FRAME_COUNTS 25 // 幀數量 8 // #define TILE_WIDTH_PIXEL 96 // 每一幀畫面的寬度(等于高度) 9 // #define TILE_COUNT_ROW 5 // 素材中每一行有多少幀1011 // 計算幀起始地址12 uint16_t *pStart = (uint16_t *) img_fireSequenceFrame;
13 pStart += (index / TILE_COUNT_ROW) * (TILE_WIDTH_PIXEL * TILE_WIDTH_PIXEL * TILE_COUNT_ROW);
14 pStart += (index % TILE_COUNT_ROW) * TILE_WIDTH_PIXEL;
1516 // 計算素材地址偏移17 uint32_t offlineSrc = (TILE_COUNT_ROW - 1) * TILE_WIDTH_PIXEL;
18 // 計算framebuffer地址偏移(320是屏幕寬度)19 uint32_t offlineDist = 320 - TILE_WIDTH_PIXEL;
2021 // 將數據復制到framebuffer22 uint16_t* pFb = (uint16_t*) framebuffer;
23 for (int y = 0; y 《 TILE_WIDTH_PIXEL; y++) {
24 memcpy(pFb, pStart, TILE_WIDTH_PIXEL * sizeof(uint16_t));
25 pStart += offlineSrc + TILE_WIDTH_PIXEL;
26 pFb += offlineDist + TILE_WIDTH_PIXEL;
27 }
28}
可見要實現這個效果需要大量的內存復制操作。在嵌入式系統中,需要大量數據復制的時候,硬件DMA的效率是最高的。但是硬件DMA只能搬運地址連續的數據,而這里,需要復制的數據在源圖片和frambuffer中的地址都是不連續的,這引來了額外的開銷(與第一小節中出現的問題相同),也導致我們無法使用硬件DMA來進行高效的數據復制。
所以,雖然我們實現了目標,但是效率不高(或者說沒有達到最高)。
為了以最快的速度把素材圖片中的某一塊數據搬運到幀緩沖中,我們來看如何使用DMA2D來實現。
首先,因為這次是要在存儲器中進行數據復制,所以我們要把DMA2D的工作模式設定為“存儲器到存儲器模式”,這通過設置DMA2D的CR寄存器的[17:16]位為00來實現,代碼如下:
1DMA2D-》CR = 0x00000000UL;
然后我們要分別設置源和目標的內存地址,與第一節中不同,因為數據源也存在內存偏移,所以我們要同時設定源和目標位置的數據偏移
1DMA2D-》FGMAR = (uint32_t)pSrc; // 源地址2DMA2D-》OMAR = (uint32_t)pDst; // 目標地址3DMA2D-》FGOR = OffLineSrc; // 源數據偏移(像素)4DMA2D-》OOR = OffLineDst; // 目標地址偏移(像素)
然后依然是設置要復制的圖像的寬和高,以及顏色格式,這點與第一小節中的相同
1DMA2D-》FGPFCCR = pixelFormat;
2DMA2D-》NLR = (uint32_t)(xSize 《《 16) | (uint16_t)ySize;
同樣的方式,我們開啟DMA2D的傳輸,并等待傳輸完成:
1/* 啟動傳輸 */2DMA2D-》CR |= DMA2D_CR_START;
34/* 等待DMA2D傳輸完成 */5while (DMA2D-》CR & DMA2D_CR_START) {}
最終我們抽出的函數如下:
1static void DMA2D_MemCopy(uint32_t pixelFormat, void * pSrc, void * pDst, int xSize, int ySize, int OffLineSrc, int OffLineDst)
2{
3 /* DMA2D配置 */ 4 DMA2D-》CR = 0x00000000UL;
5 DMA2D-》FGMAR = (uint32_t)pSrc;
6 DMA2D-》OMAR = (uint32_t)pDst;
7 DMA2D-》FGOR = OffLineSrc;
8 DMA2D-》OOR = OffLineDst;
9 DMA2D-》FGPFCCR = pixelFormat;
10 DMA2D-》NLR = (uint32_t)(xSize 《《 16) | (uint16_t)ySize;
1112 /* 啟動傳輸 */13 DMA2D-》CR |= DMA2D_CR_START;
1415 /* 等待DMA2D傳輸完成 */16 while (DMA2D-》CR & DMA2D_CR_START) {}
17}
為了方便,我們包裝一個調用它的函數:
1static void DMA2D_DisplayFrameAt(uint16_t index){
2 3 uint16_t *pStart = (uint16_t *)img_fireSequenceFrame;
4 pStart += (index / TILE_COUNT_ROW) * (TILE_WIDTH_PIXEL * TILE_WIDTH_PIXEL * TILE_COUNT_ROW);
5 pStart += (index % TILE_COUNT_ROW) * TILE_WIDTH_PIXEL;
6 uint32_t offlineSrc = (TILE_COUNT_ROW - 1) * TILE_WIDTH_PIXEL;
7 8 9 DMA2D_MemCopy(LTDC_PIXEL_FORMAT_RGB565, (void*) pStart, pDist, TILE_WIDTH_PIXEL, TILE_WIDTH_PIXEL, offlineSrc, offlineDist);
10}
然后輪流播放每一幀圖片,這里設置的幀間隔是50毫秒,并且將目標地址定義到了frambuffer的中央:
1while(1){
2 for(int i = 0; i 《 FRAME_COUNTS; i++){
3 DMA2D_DisplayFrameAt(i);
4 HAL_Delay(FRAME_TIME_INTERVAL);
5 }
6}
3.圖片漸變切換
假設我們要開發一個看圖應用,在兩張圖片進行切換時,直接進行切換會顯得比較生硬,所以我們要加入切換時的動態效果,而漸變切換(淡入淡出)是一個很經常使用的,而且看起來還不錯的效果。
這里我們需要先了解一下透明度混合(Alpha Blend)的基本概念。首先透明度混合需要有一個前景,一個背景。而混合的結果就相當于透過前景看背景時的效果。如果前景完全不透明,那么就完全看不到背景,反之如果前景完全透明,那么就只能看到背景。而如果前景是半透明的,則結果就是兩者根據前景色的透明度按照一定的規則進行混合。
如果1表示完全透明,0表示不透明,則透明度的混合公式如下,其中A是背景色,B是前景色:
1X(C)=(1-alpha)*X(B) + alpha*X(A)
因為顏色有RGB三個通道,所以我們需要對三通道都進行計算,計算完成后在進行組合:
1R(C)=(1-alpha)*R(B) + alpha*R(A)
2G(C)=(1-alpha)*G(B) + alpha*G(A)
3B(C)=(1-alpha)*B(B) + alpha*B(A)
而在程序中為了效率起見(CPU對于浮點的運算速度很慢),我們并不用0~1這個范圍的值。通常情況下我們一般會使用一個8bit的數值來表示透明度,范圍從0~255。需要注意的是,這個數值越大表示越不透明,也就是說255是完全不透明,而0表示完全透明(所以也叫不透明度),然后我們可以得到最終的公式:
1outColor = ((int) (fgColor * alpha) + (int) (bgColor) * (256 - alpha)) 》》 8;
實現RGB565顏色格式像素的透明度混合代碼:
1typedef struct{ 2 uint16_t r:5;
3 uint16_t g:6;
4 uint16_t b:5;
5}RGB565Struct;
6 7static inline uint16_t AlphaBlend_RGB565_8BPP(uint16_t fg, uint16_t bg, uint8_t alpha) {
8 RGB565Struct *fgColor = (RGB565Struct*) (&fg);
9 RGB565Struct *bgColor = (RGB565Struct*) (&bg);
10 RGB565Struct outColor;
1112 outColor.r = ((int) (fgColor-》r * alpha) + (int) (bgColor-》r) * (256 - alpha)) 》》 8;
13 outColor.g = ((int) (fgColor-》g * alpha) + (int) (bgColor-》g) * (256 - alpha)) 》》 8;
14 outColor.b = ((int) (fgColor-》b * alpha) + (int) (bgColor-》b) * (256 - alpha)) 》》 8;
151617 return *((uint16_t*)&outColor);
18}
了解了透明度混合的概念,也實現了單個像素的透明度混合后,我們來看如何實現圖片的漸變切換。
假設整個漸變在30幀內完成,我們需要在內存中開辟一塊兒大小等于圖片的緩沖區。然后我們以第一張圖片(當前顯示的圖片)為背景,第二張圖片(接下來顯示的圖片)為前景,然后為前景設置一個透明度,對每個像素進行透明度混合,并且將混合結果暫存至緩沖區中。待混合結束后,將緩沖區中的數據復制到framebuffer中即完成了一幀的顯示。接下來繼續進行第二幀、第三幀……逐漸增大前景的不透明度,直到前景色的變為不透明,即完成了圖片的漸變切換。
因為每一幀都需要對兩張圖片中的每一個像素都進行混合運算,這帶了來巨大的運算量。交給CPU實現是很不明智的行為,所以我們還是把這些工作交給DMA2D來實現吧。
這次用到了DMA2D的混合功能,所以我們要使能DAM2D的帶顏色混合的存儲器到存儲器模式,對應CR寄存器[17:16]位的值為10,即:
1DMA2D-》CR = 0x00020000UL; // 設置工作模式為存儲器到存儲器并帶顏色混合
然后分別設置前景、背景和輸出數據的內存地址和數據傳輸偏移、傳輸圖像的寬和高:
1DMA2D-》FGMAR = (uint32_t)pFg; // 設置前景數據內存地址2DMA2D-》BGMAR = (uint32_t)pBg; // 設置背景數據內存地址3DMA2D-》OMAR = (uint32_t)pDst; // 設置數據輸出內存地址45DMA2D-》FGOR = offlineFg; // 設置前景數據傳輸偏移6DMA2D-》BGOR = offlineBg; // 設置背景數據傳輸偏移7DMA2D-》OOR = offlineDist; // 設置數據輸出傳輸偏移89DMA2D-》NLR = (uint32_t)(xSize 《《 16) | (uint16_t)ySize; // 設置圖像數據寬高(像素)
設置顏色格式。這里設置前景色的顏色格式時需要注意,因為如果使用的是ARGB這樣的顏色格式,那么我們進行透明度混合時,顏色數據中本身的alpha通道就會對混合結果產生影響,所以我們這里要設定在進行混合操作時,忽略前景色自身的alpha通道。并強制設定混合時的透明度。
輸出顏色格式和背景顏色格式
1DMA2D-》FGPFCCR = pixelFormat // 設置前景色顏色格式2 | (1UL 《《 16) // 忽略前景顏色數據中的Alpha通道3 | ((uint32_t)opa 《《 24); // 設置前景色不透明度45DMA2D-》BGPFCCR = pixelFormat; // 設置背景顏色格式6DMA2D-》OPFCCR = pixelFormat; // 設置輸出顏色格式
tips0:有時我們會遇到一張帶有透明通道的圖片與背景疊加顯示的情況,此時就不應該禁用顏色本身的alpha通道
tips1:這個模式下,我們不僅可以進行顏色混合,還可以同時轉換顏色格式,可以根據需要設置前景和背景以及輸出的顏色格式
最后,啟動傳輸即可:
1/* 啟動傳輸 */2DMA2D-》CR |= DMA2D_CR_START;
34/* 等待DMA2D傳輸完成 */5while (DMA2D-》CR & DMA2D_CR_START) {}
完整代碼如下:
1void _DMA2D_MixColors(void* pFg, void* pBg, void* pDst,
2 uint32_t offlineFg, uint32_t offlineBg, uint32_t offlineDist,
3 uint16_t xSize, uint16_t ySize,
4 uint32_t pixelFormat, uint8_t opa) {
5 6 DMA2D-》CR = 0x00020000UL; // 設置工作模式為存儲器到存儲器并帶顏色混合 7 8 DMA2D-》FGMAR = (uint32_t)pFg; // 設置前景數據內存地址 9 DMA2D-》BGMAR = (uint32_t)pBg; // 設置背景數據內存地址10 DMA2D-》OMAR = (uint32_t)pDst; // 設置數據輸出內存地址1112 DMA2D-》FGOR = offlineFg; // 設置前景數據傳輸偏移13 DMA2D-》BGOR = offlineBg; // 設置背景數據傳輸偏移14 DMA2D-》OOR = offlineDist; // 設置數據輸出傳輸偏移1516 DMA2D-》NLR = (uint32_t)(xSize 《《 16) | (uint16_t)ySize; // 設置圖像數據寬高(像素)1718 DMA2D-》FGPFCCR = pixelFormat // 設置前景色顏色格式19 | (1UL 《《 16) // 忽略前景顏色數據中的Alpha通道20 | ((uint32_t)opa 《《 24); // 設置前景色不透明度2122 DMA2D-》BGPFCCR = pixelFormat; // 設置背景顏色格式23 DMA2D-》OPFCCR = pixelFormat; // 設置輸出顏色格式2425 /* 啟動傳輸 */26 DMA2D-》CR |= DMA2D_CR_START;
2728 /* 等待DMA2D傳輸完成 */29 while (DMA2D-》CR & DMA2D_CR_START) {}
30}
編寫測試代碼,這次不需要二次包裝函數了:
1void DMA2D_AlphaBlendDemo(){
2 3 const uint16_t lcdXSize = 320, lcdYSize = 240;
4 const uint8_t cnvFrames = 60; // 60幀完成切換 5 const uint32_t interval = 33; // 每秒30幀 6 uint32_t time = 0;
7 8 // 計算輸出位置的內存地址 9 uint16_t distX = (lcdXSize - DEMO_IMG_WIDTH) / 2;
10 uint16_t distY = (lcdYSize - DEMO_IMG_HEIGHT) / 2;
11 uint16_t* pFb = (uint16_t*) framebuffer;
12 uint16_t* pDist = pFb + distX + distY * lcdYSize;
13 uint16_t offlineDist = lcdXSize - DEMO_IMG_WIDTH;
1415 uint8_t nextImg = 1;
16 uint16_t opa = 0;
17 void* pFg = 0;
18 void* pBg = 0;
19 while(1){
20 // 切換前景/背景圖片21 if(nextImg){
22 pFg = (void*)img_cat;
23 pBg = (void*)img_fox;
24 }
25 else{
26 pFg = (void*)img_fox;
27 pBg = (void*)img_cat;
28 }
2930 // 完成切換31 for(int i = 0; i 《 cnvFrames; i++){
32 time = HAL_GetTick();
33 opa = 255 * i / (cnvFrames-1);
34 _DMA2D_MixColors(pFg, pBg, pDist,
35 0,0,offlineDist,
36 DEMO_IMG_WIDTH, DEMO_IMG_HEIGHT,
37 LTDC_PIXEL_FORMAT_RGB565, opa);
38 time = HAL_GetTick() - time;
39 if(time 《 interval){
40 HAL_Delay(interval - time);
41 }
42 }
43 nextImg = !nextImg;
44 HAL_Delay(5000);
45 }
46}
性能對比
前面介紹了三種嵌入式圖形開發種的實例,并對分別介紹了通過傳統和DMA2D實現的方法。這時候肯定有朋友會問,DMA2D實現,比起傳統方法實現,到底能快多少呢?我們來實際測試一下。
共同的測試條件如下:
framebuffer放置在SDRAM中,320x240,RGB565
SDRAM工作頻率100MHz,CL2,16位帶寬。
MCU為STM32H750XB,主頻400MHz,開啟I-Cache和D-Cache
代碼和資源在內部Flash上,64位AXI總線,速度為200MHz。
GCC編譯器(版本:arm-atollic-eabi-gcc-6.3.1)
測試項目:矩形填充
繪制上一章第1節中的圖表,繪制10000次,統計結果
測試項目:內存復制
繪制上一章第2節中的序列幀10000幀,統計結果
測試項目:透明度混合
漸變切換上一章第3小節中的兩張圖片100次,每次30幀完成,共計3000幀
混合結果直接輸出到framebuffer,不再通過緩沖區緩沖
性能測試總結
由上面的測試結果可以看出,DAM2D至少有2個優勢:
一是速度更快:在部分項目中,DMA2D實現的速度相比純軟件實現最高可以達到30倍的差距!這還是在主頻高達400MHz還帶L1-Cache的STM32H750平臺上測試的結果,如果是在無cache且主頻較低的STM32F4平臺上進行測試,差距會進一步拉大。
二是性能更加穩定:由測試結果可以看出,DMA2D實現的方式受編譯器優化等級的影響非常小,幾乎可以忽略不計,這意味著,無論你使用IAR,GCC或是MDK,使用DMA2D都可以達到相同的性能表現。不會出現同一段代碼移植后性能相差很大的情況。
除這兩個直觀的結果外,其實還有第三點優勢,那就是代碼編寫更加簡單。DMA2D的寄存器不多,而且比較直觀。在某些場合,使用起來要比軟件實現方便的多。
結語
本文中的三個實例,都是我本人在嵌入式圖形開發中經常遇到的情況。實際上,DMA2D的用法還有很多,有興趣的話可以參考《STM32H743中文編程手冊》中的相關內容,相信有了本文的基礎,在閱讀里面的內容時一定會事半功倍。
受限于作者的技術,文章中的內容無法做到100%的正確,如果存在錯誤,請大家指出,謝謝。
編輯:jq
-
mcu
+關注
關注
146文章
17129瀏覽量
351007 -
cpu
+關注
關注
68文章
10855瀏覽量
211595 -
gpu
+關注
關注
28文章
4729瀏覽量
128897 -
液晶控制器
+關注
關注
0文章
12瀏覽量
7552 -
DMA2D
+關注
關注
0文章
5瀏覽量
2111
原文標題:STM32的“GPU”——DMA2D實例詳解
文章出處:【微信號:RTThread,微信公眾號:RTThread物聯網操作系統】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論