跳转到主要内容

通过部分帧缓冲降低内存使用率

本节以时钟应用为例,解释了如何配置和使用部分帧缓冲区,以牺牲一些性能为代价来降低对内存的需求。

下面是在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个矩形太大,无法直接存入最后第3个缓冲区块。 在这种情况下,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 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模块,这包含两个GPIO,用于控制SPI芯片选择和数据/指令模式。

使用易失性uint8t变量*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回调也调用传输完成回调。 此函数在生成的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. 表示最后一次DSI传输已完成
__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函数使用SPI DMA方式进行传输,将数据发给显示屏。

当传输完成时,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向显示屏发送帧缓冲块。