1. Multiple Buffering的工作原理
多缓冲是一种使用多个帧缓冲器的方法。其基本原理如下:在启用多个缓冲器的情况下,由显示控制器所使用的前置缓冲器(front buffer)会在屏幕上产生图像,同时,一个或多个后置缓冲器(back buffers)则用于绘图操作。绘图操作完成后,后置缓冲器成为可见的前置缓冲器。
如果使用两个缓冲器 (即一个前置缓冲器和一个后置缓冲器),通常称之为 “双缓冲”;如果使用两个后置缓冲器和一个前置缓冲器,则称之为 “三缓冲”。
由于多缓冲方法使用多个帧缓冲器,因此,即便绘图操作仍在进行中,屏幕画面也是完全渲染的结果。启动绘图过程时,前置缓冲器的当前内容会被复制到一个后置缓冲器中。在该操作完成后,所有绘图操作只对该后置缓冲器起作用。绘图操作完成后,后置缓冲器成为前置缓冲器。如果要使后置缓冲器成为可见的前置缓冲器,通常只需修改显示控制器的帧缓冲器起始地址寄存器即可。
可以认为,显示器的持续刷新是通过显示控制器的应用程序得以实现的。每秒60次。每个周期完成之后有一个垂直同步信号,通常称之为VSYNC信号。使后置缓冲器成为前置缓冲器的最佳时机是该信号出现之时。如果不考虑VSYNC信号,则可能产生撕裂效果(如图1.1所示):
图1.1 撕裂效果
2. 开发环境
硬件:STM32F429Discovery开发板,主板芯片是STM32F429ZI,外部64Mb的SDRAM,LCD是LG的LD050WV1。
软件:基于ST官方提供的STemWin_Library_V1.0.0固件库和例程进行移植,操作系统是FreeRTOS,emWin版本为5.22。
3. 如何启用Multiple Buffering
3.1Multiple Buffering功能的工作流程
图3.1 Multiple Buffering的工作流程
如图3.1所示,Multiple Buffering机制的工作顺序为:在void LCD_X_Config(void)时进行配置;调用GUI_MULTIBUF_Begin()或GUI_MULTIBUF_BeginEx()对绘图进行缓冲,此时emWin会将front buffer的内容拷贝到back buffer中;进行绘图工作,此时所有的绘图工作都将在back buffer中进行;调用GUI_MULTIBUF_End()或GUI_MULTIBUF_EndEX()结束缓冲,此时emWin会将back buffer切换到front。
3.2 官方代码修改、实现以及实现原理解说
主要修改STemWinLibrary555_4x9i目录下的GUIDRV_stm32f429i_discovery.c文件。由于该文件已经对Multiple Buffering功能进行了支持,所以重点修改此处即可:
//
// Buffers / VScreens
//
#define NUM_BUFFERS 2 // Number of multiple buffers to be used
即使用两个buffer,也就是双缓冲。后续的内容官方代码已经写好了,这里指进行介绍说明,同时捋清emWin进行双缓冲功能的工作流程。
开启双缓冲后,在void LCD_X_Config(void)函数中,预编译条件被打开:
#if (NUM_BUFFERS > 1)
for (i = 0; i < GUI_NUM_LAYERS; i++) {
GUI_MULTIBUF_ConfigEx(i, NUM_BUFFERS);
}
#endif
这是对多缓冲功能进行配置,由于NUM_BUFFERS的值是2,所以多缓冲功能被打开。
前面提及开启双缓冲后,所有的绘图操作将在back buffer里进行,为了确保绘图的正确性,emWin需要一个copy函数将front buffer中的数据首先拷贝到back buffer中,然后再进行绘图。ST官方代码使用了用户自定义的buffer copy程序,这样可使使用DMA进行内存拷贝,以提高工作效率。使用自定义buffer copy程序,需要在void LCD_X_Config(void)函数中添加下面内容:
LCD_SetDevFunc(i, LCD_DEVFUNC_COPYBUFFER, (void(*)(void))_LCD_CopyBuffer);
下面是官方给出的“_LCD_CopyBuffer”函数代码:
/*********************************************************************
*
* _LCD_CopyBuffer
*/
static void _LCD_CopyBuffer(int LayerIndex, int IndexSrc, int IndexDst) {
U32 BufferSize, AddrSrc, AddrDst;
BufferSize = _GetBufferSize(LayerIndex);
AddrSrc = _aAddr[LayerIndex] + BufferSize * IndexSrc;
AddrDst = _aAddr[LayerIndex] + BufferSize * IndexDst;
_DMA_Copy(LayerIndex, (void *)AddrSrc, (void *)AddrDst, _axSize[LayerIndex], _aySize[LayerIndex], 0);
}
在绘制完图形后,用户调用GUI_MULTIBUF_End()函数,此时emWin将back buffer切换到前台显示出来。这个切换动作最好在LCD进行第一行的行扫描时进行,否则容易出现撕裂效果。因此这段代码被写在LTDC的中断处理程序中。
/*********************************************************************
*
* LTDC_ISR_Handler
*
* Purpose:
* End-Of-Frame-Interrupt for managing multiple buffering
*/
void LTDC_ISR_Handler(void) {
U32 Addr;
int i;
LTDC->ICR = (U32)LTDC_IER_LIE;
for (i = 0; i < GUI_NUM_LAYERS; i++) {
if (_aPendingBuffer[i] >= 0) {
//
// Calculate address of buffer to be used as visible frame buffer
//
Addr = _aAddr[i] + _axSize[i] * _aySize[i] * _aPendingBuffer[i] * _aBytesPerPixels[i];
//
// Store address into SFR
//
_apLayer[i]->CFBAR &= ~(LTDC_LxCFBAR_CFBADD);
_apLayer[i]->CFBAR = Addr;
//
// Reload configuration
//
LTDC_ReloadConfig(LTDC_SRCR_IMR);
//
// Tell emWin that buffer is used
//
GUI_MULTIBUF_ConfirmEx(i, _aPendingBuffer[i]);
//
// Clear pending buffer flag of layer
//
_aBufferIndex[i] = _aPendingBuffer[i];
_aPendingBuffer[i] = -1;
}
}
}
从以上程序可以看出,back buffer到front buffer的切换,只是将back buffer的首地址赋值给了LTDC的CFBAR寄存器。这样的切换时很迅速的,因此用户看不到图像的绘制过程,避免了由绘图工作带来的屏幕闪烁问题。
那么,emWin如何知道何时需要进行buffer的切换工作呢?实际上,当用户调用GUI_MULTIBUF_End()等函数的时候,会发出LCD_X_SHOWBUFFER信号给“LCD_X_DisplayDriver”函数。
int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void * pData)
{
switch (Cmd) {
case LCD_X_SHOWBUFFER: {
//
// Required if multiple buffers are used. The 'Index' element of p contains the buffer index.
//
LCD_X_SHOWBUFFER_INFO * p;
p = (LCD_X_SHOWBUFFER_INFO *)pData;
_aPendingBuffer[LayerIndex] = p->Index;
break;
}
在需要进行buffer切换时,传入的p->Index为1,这样,在“LTDC_ISR_Handler”函数里就满足了“_aPendingBuffer[i] >= 0”的条件。
3.3 官方代码中存在的bug
如果按上述方法进行修改和配置,并启用Multiple Buffering功能,那你可能发现,屏幕上能够出现文字,但是却没有方块、水平直线和竖直直线。如果仿真调试,会发现,其实矩形、水平直线和竖直直线被画在了front buffer里。
这是由于官方示例代码中存在一个bug,导致用户自定义的“LCD_DEVFUNC_FILLRECT”功能函数使用了错误地址。该示例代码中配置使用了自定义“LCD_DEVFUNC_FILLRECT”函数:
if (_GetPixelformat(i) <= LTDC_Pixelformat_ARGB4444) {
LCD_SetDevFunc(i, LCD_DEVFUNC_FILLRECT, (void(*)(void))_LCD_FillRect);
}
而“ _LCD_FillRect"函数的实现如下:
/*********************************************************************
*
* _LCD_FillRect
*/
static void _LCD_FillRect(int LayerIndex, int x0, int y0, int x1, int y1, U32 PixelIndex) {
U32 BufferSize, AddrDst;
int xSize, ySize;
if (GUI_GetDrawMode() == GUI_DM_XOR) {
LCD_SetDevFunc(LayerIndex, LCD_DEVFUNC_FILLRECT, NULL);
LCD_FillRect(x0, y0, x1, y1);
LCD_SetDevFunc(LayerIndex, LCD_DEVFUNC_FILLRECT, (void(*) (void))_LCD_FillRect);
} else {
xSize = x1 - x0 + 1;
ySize = y1 - y0 + 1;
BufferSize = _GetBufferSize(LayerIndex);
AddrDst = _aAddr[LayerIndex] + BufferSize * _aBufferIndex[LayerIndex] + (y0 * _axSize[LayerIndex] + x0) * _aBytesPerPixels[LayerIndex];
_DMA_Fill(LayerIndex, (void *)AddrDst, xSize, ySize, _axSize[LayerIndex] - xSize, PixelIndex);
}
}
该代码将在绘制矩形、水平直线和竖直直线时进行调用,将绘制的图形拷贝到相应的buffer中。如果对该处代码进行调试,会发现“AddrDst”的值始终都是front buffer的值。而在启用双缓冲时,“AddrDst”应该指向back buffer。进一步会发现“_aBufferIndex[LayerIndex]”的值是错误的。这是因为不应该在LTDC的中断服务程序中对"_aBufferIndex[LayerIndex]"进行赋值。因为中断服务程序中的赋值只有在用户调用GUI_MULTIBUF_End()时才执行,这个时候绘图工作都已经完成了。正确的方式应该在GUI_MULTIBUF_Begin()后立即对"_aBufferIndex[LayerIndex]"进行赋值。
那么,应该在代码的何处进行赋值呢?答案是“_LCD_CopyBuffer”函数,因为调用GUI_MULTIBUF_Begin()后,emWin会调用该函数将front buffer中的内容拷贝的back buffer,然后才绘图。
因此,修改后的代码为:
/*********************************************************************
*
* _LCD_CopyBuffer
*/
static void _LCD_CopyBuffer(int LayerIndex, int IndexSrc, int IndexDst) {
U32 BufferSize, AddrSrc, AddrDst;
BufferSize = _GetBufferSize(LayerIndex);
AddrSrc = _aAddr[LayerIndex] + BufferSize * IndexSrc;
AddrDst = _aAddr[LayerIndex] + BufferSize * IndexDst;
_aBufferIndex[LayerIndex] = IndexDst;
_DMA_Copy(LayerIndex, (void *)AddrSrc, (void *)AddrDst, _axSize[LayerIndex], _aySize[LayerIndex], 0);
}
/*********************************************************************
*
* LTDC_ISR_Handler
*
* Purpose:
* End-Of-Frame-Interrupt for managing multiple buffering
*/
void LTDC_ISR_Handler(void) {
U32 Addr;
int i;
LTDC->ICR = (U32)LTDC_IER_LIE;
for (i = 0; i < GUI_NUM_LAYERS; i++) {
if (_aPendingBuffer[i] >= 0) {
//
// Calculate address of buffer to be used as visible frame buffer
//
Addr = _aAddr[i] + _axSize[i] * _aySize[i] * _aPendingBuffer[i] * _aBytesPerPixels[i];
//
// Store address into SFR
//
// _apLayer[i]->CFBAR &= ~(LTDC_LxCFBAR_CFBADD); // 没发现这句有什么用,一起注掉了吧!
_apLayer[i]->CFBAR = Addr;
//
// Reload configuration
//
LTDC_ReloadConfig(LTDC_SRCR_IMR);
//
// Tell emWin that buffer is used
//
GUI_MULTIBUF_ConfirmEx(i, _aPendingBuffer[i]);
//
// Clear pending buffer flag of layer
//
_aPendingBuffer[i] = -1;
}
}
} 图3.2 官方例程明显开启了Multiple Buffering功能 看起来似乎ST方面发现了Multiple Buffering功能不好用,但是没有从根本上解决问题,所以官方例程就不使用该功能了。这种治标不治本的处理方式,让我对st的工作态度表示怀疑。 |
|
来自: yxz1212_bao > 《emwin》