跳轉到主要內容

通過部分影像緩衝降低記憶體使用率

本節以時鐘應用為例,解釋了如何配置和使用部分影像緩衝區,並以犧牲一些性能為代價來降低存儲空間的要求。

下面是在STM32L4R9Discovery評估套件上運行的應用的影片

完整影像緩衝記憶體

影像緩衝區通常是一個大型記憶體陣列,其存儲空間足以容納顯示器上的所有可用像素。 如果在解析度為480 x 272的24位元顯示器上運行,則完整大小的影像緩衝區可容納480 x 272 x 3位元組 = 391,680位元組。

一些應用可能有2(“雙重緩衝”)甚至3個影像緩衝區。 在這種情況下,要求的總存儲空間將是783,360和1,175,040位元組。

在繪製UI的任何一個部分時,TouchGFX向影像緩衝區寫入像素值,在所有繪製操作完成後,影像緩衝區被傳輸到顯示器。 通常會將整個影像緩衝區傳輸到顯示器,即使只更新了UI的一部分。 在傳輸前,通常可以將影像緩衝分成許多小塊進行更新。

更新1、更新2、更新3、……、更新N,傳輸至顯示器

在某些情況下,特別是在沒有外部RAM的低成本解決方案中,影像緩衝區必須足夠小,以使應用程式的其餘部分能夠和影像緩衝共用內部RAM。 部分影像緩衝區在這種情況下十分有用。

部分影像緩衝記憶體

部分影像緩衝區使TouchGFX應用程式能夠在幾個小於完整大小的影像緩衝區之上運行。 影像緩衝區的數量和大小是可配置的。 此技術可大幅降低應用程式的存儲空間要求,但也帶來了一些限制:

  • 部分影像緩衝區只能在具有內置記憶體的顯示器上工作。 這些顯示器通常是DSI顯示器或具有平行匯流排連接(DBI A/B型,8080/6800)或SPI匯流排連接的顯示器。
  • 複雜應用可能發生撕裂。

不同於使用影像緩衝區將每一個像素顯示在顯示器上,部分影像緩衝區通常只覆蓋部分區域。 在本文使用的時鐘範例中,使用了三個大小均為11,700位元組的影像緩衝區。 這會導致影像緩衝區佔用記憶體35,100位元組的空間。

當應用程式需要更新UI的某一部分時,TouchGFX將選擇配置的部分影像緩衝區中的其中一個,在該部分影像緩衝中完成其繪圖操作,並將該部分傳輸到顯示器。 將對需要渲染的所有UI區域重複此操作 - 這會將更新和傳輸資料的公式變更為:

更新1、傳輸1、更新2、傳輸2、更新3、傳輸3、……、更新N、傳輸N

在某些情況下,可以在更新下一個緩衝區的同時進行一個部分影像緩衝區的傳輸。

顯示器畫面撕裂

相比於使用完整影像緩衝區,在使用部分影像緩衝區時,TouchGFX傳輸UI上更新的更快。 由於顯示器需要定期刷新,在最多16 ms後(就60 fps顯示器而言),顯示器將在其螢幕上顯示接收到的更新。 因此,在所有更新傳輸完畢之前,對顯示器的最初更新可能會被用戶注意到。

如果繪圖操作和傳輸的完整序列需要花費較長時間才能完成( > 16 ms),則用戶很可能會看到上一幀與下一幀的組合。 這被稱為畫面撕裂。 因此,部分影像緩衝區不適合使用複雜動畫、需要長時間渲染的應用。

顯示器更新範例

在討論如何在應用程式中配置部分影像緩衝區之前,我們先來看一個具體範例,該範例顯示了一個用移動的圓弧代表秒數的數位時鐘。 綠色圓弧每秒移動6度,一分鐘完成一整圈。 用如下圖所示的四個小部件構建UI:

以下是更新數位時鐘和圓弧的程式碼:

MainView.cpp
void MainView::handleTickEvent()
{
ticks++;
if (ticks == 10)
{
ticks = 0;
secs += 1;
if (secs == 60) //increment minutes
{
secs = 0;
min += 1;
if (min == 60) //increment hours
{
min = 0;
hour += 1;
if (hour == 24)
{
hour = 0;
}
}
//Only update digital clock when minutes or hours change
digitalClock.setTime24Hour(hour, min, secs);
}
//Always update seconds
circleSeconds.updateArc(secs*6 - 20, secs*6);
}
}

下圖所示為在圓弧接近頂點和數位時鐘更新時前幾秒更新的區域(灰色矩形)。 在前兩幀中,只有秒數在變化(第58和59秒)。 在第三幀中,秒數達到60,小時和分鐘的文字更新:

上面第三幅圖像中更新的矩形為154 x 60個像素、20 x 12個像素和33 x 8個像素。 在使用標準影像緩衝區時,這些矩形會被存入完整個影像緩衝區中(覆蓋之前的像素),完整影像緩衝區隨後被傳輸到顯示器。 在使用部分影像緩衝區時,這三個矩形會被存入它們自己的小影像緩衝區中,小影像緩衝區隨後立即被傳輸到顯示器並予以顯示。

配置部分影像緩衝區

TouchGFX需通過下面的步驟來使用部分影像緩衝區:

  1. 用記憶體緩衝區創建影像緩衝區分配器物件
  2. 配置TouchGFX HAL類以使用該分配器
  3. 寫入程式碼以將緩衝區傳輸至顯示器

步驟1和2由TouchGFX Generator通過STM32CubeMX自動生成,而步驟3是一個專用驅動程式,用於將像素傳輸至顯示器。

以上配置生成以下程式碼:

TouchGFXGeneratedHAL.cpp
// Block Allocator for Partial Framebuffer strategy
static ManyBlockAllocator<1920, /* block size */
3, /* number of blocks */
2 /* bytes per pixel */
> blockAllocator;

該影像緩衝區分配器分配3個塊,每個1920位元組。 TouchGFX HAL被自動配置為使用部分影像緩衝策略,並使用Block Allocator。

TouchGFXGeneratedHAL.cpp
void TouchGFXGeneratedHAL::initialize()
{
HAL::initialize();

registerEventListener(*(Application::getInstance()));
enableLCDControllerInterrupt();
enableInterrupts();
// Partial framebuffer strategy
setFrameBufferAllocator(&blockAllocator);
setFrameRefreshStrategy(HAL::REFRESH_STRATEGY_PARTIAL_FRAMEBUFFER);
}

使用此配置,TouchGFX將分配小的影像緩衝區,並在其中繪製UI。 現在,只需將小的影像緩衝區傳輸到顯示器。

首先來看分配用於繪製小圓圈更新的兩個影像緩衝區的位置和大小(上面第二幅圖):

矩形xy寬度高度像素
矩形1112562214308個像素 = 924位元組
矩形2153422911319個像素 = 957位元組

這些矩形都很小,可以放入由影像緩衝區分配器分配的塊中。

在上面的第三幅圖中,更新了3個矩形:小矩形更新圓圈,較大的矩形覆蓋文字:

矩形xy寬度高度像素
矩形1126512012240個像素 = 720位元組
矩形216542338264個像素 = 792位元組
矩形3118165154609.240個像素 = 27.720位元組

同樣地,矩形1和2很小,可以放入由影像緩衝區分配器分配的塊中,但影像緩衝區3過大。 此矩形過大,將被分成多個可放入影像緩衝區(11,700位元組)的矩形。

這裡我們更新了3個矩形,但分配器只有2個塊。 在這種情況下,TouchGFX將等待第一個塊傳輸完畢,然後重複使用塊。

將影像緩衝區傳輸到螢幕

當需要重新繪製矩形時,TouchGFX將從FrameBufferAllocator分配影像緩衝區。 在存入緩衝區後,TouchGFX將呼叫此方法:

void HAL::flushFrameBuffer(const Rect& rect);

在HAL子類中,此函數可以被重寫,以便將影像緩衝區傳輸至螢幕。 為了讓部分影像緩衝發揮作用,需要用到此特殊實現。 下面幾節將描述如何對搭載SPI顯示器的STM32G071評估套件和搭載DSI顯示器的STM32L4R9Discovery評估套件進行相關配置。

將影像緩衝區傳輸到X-NUCLEO-GFX01M1 SPI顯示器

本節將討論STM32G071 nucleo板和X-Nucleo-GFX01M1擴展板的TouchGFX設置。 此擴展板(MB1642B)包含一個2.2” 240x320 SPI顯示器和一個64-Mbit SPI NOR Flash。

Nucleo-G071RB和X-Nucleo-GFX01M1擴展板

在TouchGFX開發板設置中,我們使用框架中的一個C++類別來管理部分影像緩衝區塊。 這樣可以稍微縮短TouchGFX開發板設置中的程式碼。

使用TouchGFX Generator構建TouchGFX開發板設置。 Read more about that here

最重要的部分是flushFrameBuffer函數:

TouchGFXGeneratedHAL.cpp
void TouchGFXGeneratedHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
HAL::flushFrameBuffer(rect);
// Try transmitting a block
PartialFrameBufferManager::tryTransmitBlock();
}

這裡我們只需呼叫PartialFrameBufferManager 框架類來獲取發送的塊。

在TouchGFXGeneratedHAL::endFrame函數中,我們也呼叫PartialFrameBufferManager 來獲取發送的任何剩餘影像緩衝塊:

TouchGFXGeneratedHAL.cpp
void TouchGFXGeneratedHAL::endFrame()
{
// We must guard the next frame until we're done transferring all blocks over our display interface
// through either a semaphore if user is running an OS or a simple variable if not
PartialFrameBufferManager::transmitRemainingBlocks();

HAL::endFrame();
touchgfx::OSWrappers::signalRenderingDone();
}

PartialFrameBufferManager 使用三個函數與顯示器驅動程式程式碼進行交互。 這些必須在TouchGFX開發板設置中實現:

TouchGFXGeneratedHAL.cpp
/**
* Check if a Frame Buffer Block is beeing transmitted.
*/
__weak int transmitActive()
{
return touchgfxDisplayDriverTransmitActive();
}

/**
* Check if a Frame Buffer Block ending at bottom may be sent.
*/
__weak int shouldTransferBlock(uint16_t bottom)
{
return touchgfxDisplayDriverShouldTransferBlock(bottom);
}

/**
* Transmit a Frame Buffer Block.
*/
__weak void transmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
touchgfxDisplayDriverTransmitBlock(pixels, x, y, w, h);
}

以上程式碼將轉發到MB1642B驅動程式程式碼中的C函數。

MB1642BDisplayDriver.c
int touchgfxDisplayDriverTransmitActive(void)
{
return IsTransmittingBlock_;
}

void touchgfxDisplayDriverTransmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
Display_Bitmap((uint16_t*)pixels, x, y, w, h);
}

此驅動程式程式碼的實現高度依賴於使用的顯示器。 在MB1642B包含兩個用於控制SPI晶片選擇和資料/指令模式的GPIO。

使用揮發性uint8t變數(volatile uint8_t) *IsTransmittingBlock*實現發送狀態。 在開始發送時,此變數置為1,並在DMA回檔中置為0:

MB1642BDisplayDriver.c
void MB1642BDisplayDriver_DMACallback(void)
{
/* Transfer Complete Interrupt management ***********************************/
if ((0U != (DMA1->ISR & (DMA_FLAG_TC1))) && (0U != (hdma_spi1_tx.Instance->CCR & DMA_IT_TC)))
{
/* Disable the transfer complete and error interrupt */
__HAL_DMA_DISABLE_IT(&hdma_spi1_tx, DMA_IT_TE | DMA_IT_TC);

/* Clear the transfer complete flag */
__HAL_DMA_CLEAR_FLAG(&hdma_spi1_tx, DMA_FLAG_TC1);

IsTransmittingBlock_ = 0;

...

// Signal Transfer Complete to TouchGFX
DisplayDriver_TransferCompleteCallback();

在上文中可以看到,DMA callback 也呼叫transfer complete callback。 在生成的HAL中實現此函數:

TouchGFXGeneratedHAL.cpp
extern "C"
void DisplayDriver_TransferCompleteCallback()
{
// After completed transmission start new transfer if blocks are ready.
PartialFrameBufferManager::tryTransmitBlockFromIRQ();
}

此處對PartialFrameBufferManager 的呼叫能夠開始新的傳輸。

DSI顯示器上的影像緩衝區傳輸

STM32L4R9Discovery評估套件使用DSI顯示器。 普通的HAL類稱為STM32HAL_DSI(位於STM32HAL_DSI.cpp中)。

以下是我們重寫HAL::flushFrameBuffer方法,以便通知FrameBufferAllocator塊已繪製:

TouchGFXHAL.hpp
void TouchGFXHAL::flushFrameBuffer(const Rect& rect)
{
frameBufferAllocator->markBlockReadyForTransfer();
HAL::flushFrameBuffer(rect); //call normal implementation
}

當塊可以進行傳輸時,FrameBufferAllocator子類ManyBlockAllocator將呼叫全域函數FrameBufferAllocatorSignalBlockDrawn()。 此方法必須在BSP層實現:

TouchGFXGeneratedHAL.cpp
void FrameBufferAllocatorSignalBlockDrawn()
{
if (!dsiIsTransferring)
{
sendBlock();
}
}

此函式呼叫sendBlock函數,除非DSI上已經在進行傳輸。 對於TouchGFX繪製的第一個塊,不存在這種情況,因此開始傳輸。 如果在DSI傳輸仍在運行時完成了另一個塊的繪製,塊將保持“準備傳輸狀態”,並將繼續另一個空閒塊(如可用)的繪製。

當DSI傳輸完成時,我們必須首先釋放已傳輸的塊,以便將其重複用於另一個矩形,然後檢查下一個塊是否可以開始傳輸。 這些全部在ER中斷中完成:

TouchGFXHAL.cpp
__irq void DSI_IRQHandler(void) {
if (__HAL_DSI_GET_FLAG(&hdsi, DSI_IT_ER))
{
// End-of-refresh interrupt. Meaning last DSI transfer is complete
__HAL_DSI_CLEAR_FLAG(&hdsi, DSI_IT_ER);
if (dsiIsTransferring)
{
HAL::getInstance()->getFrameBufferAllocator()->freeBlockAfterTransfer();
dsiIsTransferring = 0;
}
sendBlock(); //transfer next block if availble
}

函數sendBlock更為複雜。 下面我們配置LTDC和DSI外設,以便傳輸影像緩衝。 我們還將配置顯示器,以便將傳輸的資料放入顯示器記憶體中的正確位置。 這部分程式碼依賴於特定的顯示器。 請檢查顯示器資料手冊瞭解指令規範。

TouchGFXHAL.cpp
static void sendBlock()
{
FrameBufferAllocator* fbAllocator = HAL::getInstance()->getFrameBufferAllocator();

//Is a block ready for transfer?
if (fbAllocator->hasBlockReadyForTransfer())
{
Rect transfer_rect;
const uint8_t* src = fbAllocator->getBlockForTransfer(transfer_rect);
dsiIsTransferring = 1;

//1. Setup LTDC and layer address and dimension
//2. Configure display active area
//3. Start DSI

__HAL_DSI_WRAPPER_DISABLE(&hdsi);

//1: Setup LTDC
LTDC_Layer1->CFBAR = (uint32_t)src;

const uint32_t width = transfer_rect.width;
const uint32_t height = transfer_rect.height;

LTDC->AWCR = ((width + 1) << 16) | (height + 1);
LTDC->TWCR = ((width + 1 + 1) << 16) | (height + 1 + 1);

const uint16_t layer_x0 = 2 + 0;
const uint16_t layer_x1 = 2 + width - 1;
LTDC_Layer1->WHPCR = (layer_x1 << 16) | layer_x0;

const uint16_t layer_y0 = 2 + 0;
const uint16_t layer_y1 = 2 + height - 1;
LTDC_Layer1->WVPCR = (layer_y1 << 16) | layer_y0;

LTDC_Layer1->CFBLR = ((width * 3) << 16) | (width * 3 + 3);
LTDC_Layer1->CFBLNR = height;

LTDC->SRCR = (uint32_t)LTDC_SRCR_IMR;

//2: Configure display
const int16_t x = transfer_rect.x + 4;
const int16_t x2 = transfer_rect.x + 4 + width - 1;
uint8_t InitParam1[4] = { (uint8_t)(x >> 8), (uint8_t)(x & 0xFF), (uint8_t)(x2 >> 8), (uint8_t)(x2 & 0xFF)};
HAL_DSI_LongWrite(&hdsi, 0, DSI_DCS_LONG_PKT_WRITE, 4, DSI_SET_COLUMN_ADDRESS, InitParam1);

const int16_t y = transfer_rect.y;
const int16_t y2 = transfer_rect.y + height - 1;
uint8_t InitParam2[4] = { (uint8_t)(y >> 8), (uint8_t)(y & 0xFF), (uint8_t)(y2 >> 8), (uint8_t)(y2 & 0xFF) };
HAL_DSI_LongWrite(&hdsi, 0, DSI_DCS_LONG_PKT_WRITE, 4, DSI_SET_PAGE_ADDRESS, InitParam2);

//3: Start DSI transfer
__HAL_DSI_WRAPPER_ENABLE(&hdsi);
HAL_DSI_Refresh(&hdsi);
}
}

SPI顯示器上的影像緩衝區傳輸

將矩形傳輸到顯示器的原理與DSI的相同,但在細節上存在一些差異。

首先,在繪製矩形時,我們在目前沒有進行任何傳輸的情況下開始傳輸:

TouchGFXHAL.cpp
void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
HAL::flushFrameBuffer(rect);
frameBufferAllocator->markBlockReadyForTransfer();
//start transfer if not running already!
if (!LCDManager_IsTransmittingData())
{
touchgfx::Rect r;
const uint8_t* pixels = frameBufferAllocator->getBlockForTransfer(r);
LCDManager_SendFrameBufferBlockWithPosition((uint8_t*)pixels, r.x, r.y, r.width, r.height);
}
}

函數 LCDManager_SendFrameBufferBlockWithPosition 使用DMA,開始對顯示器的SPI傳輸。

當傳輸完成時,SPI傳輸完成處理函數呼叫另一個函數:

TouchGFXHAL.cpp
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
UNUSED(hspi);
LCD_CS_HIGH();
isTransmittingData = 0;

//Change to SPI datasize to 8 bit from 16 bit
heval_Spi.Instance->CR2 &= ~(SPI_DATASIZE_16BIT - SPI_DATASIZE_8BIT);

//signal transfer complete
LCDManager_TransferComplete();
}

LCDManager_TransferComplete函數發起新的傳輸:

TouchGFXHAL.cpp
void LCDManager_TransferComplete()
{
touchgfx::startNewTransfer();
}

void startNewTransfer()
{
FrameBufferAllocator* fba = HAL::getInstance()->getFrameBufferAllocator();
fba->freeBlockAfterTransfer();
blockIsTransferred = true;

if (fba->hasBlockReadyForTransfer())
{
touchgfx::Rect r;
const uint8_t* pixels = fba->getBlockForTransfer(r);
LCDManager_SendFrameBufferBlockWithPosition((uint8_t*)pixels, r.x, r.y, r.width, r.height);
}
}

結論

在本文中,我們討論了對於顯示器具有集成影像緩衝記憶體的平臺而言,部分影像緩衝區策略如何降低其存儲空間要求。

配置和設置部分影像緩衝的方法在所有平臺上都是一樣的,但向顯示器發送塊內容的方法並不相同。 我們看到,對於基於LTDC/DSI的平臺(STM32L4R9-DISCO),我們能夠重新配置LTDC層,以使下一個塊適合通過DSI進行傳輸,而在沒有LCD控制器(STM32G071)的平臺上,我們能夠使用SPI向顯示器發送塊。