分享

STemWin如何启用Multiple Buffering功能

 yxz1212_bao 2018-07-19

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;
        }
    }

}

这个bug在STemWin_Library_V1.0.1中仍然存在。但是1.0.1的官方例程中没有使用Multiple Buffering功能,而1.0.0中的demo明显使用了该功能(如图3.2):


图3.2 官方例程明显开启了Multiple Buffering功能

看起来似乎ST方面发现了Multiple Buffering功能不好用,但是没有从根本上解决问题,所以官方例程就不使用该功能了。这种治标不治本的处理方式,让我对st的工作态度表示怀疑。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约