嵌入式GUI开发实战:emWin多缓冲与虚拟屏幕配置详解

嵌入式GUI开发实战:emWin多缓冲与虚拟屏幕配置详解

1. 项目概述:为什么嵌入式GUI需要多缓冲与虚拟屏幕?

在嵌入式系统里做图形界面开发,尤其是用emWin这类库,最头疼的莫过于画面闪烁、撕裂,或者滑动列表、切换窗口时能看到“刷屏”的痕迹。这些问题在资源受限的单片机平台上尤其突出,因为CPU和内存带宽都有限,图形绘制和屏幕刷新常常“打架”。我接手过不少从简单菜单升级到复杂动态界面的项目,初期没处理好缓冲,用户体验简直是一场灾难——用户会觉得设备“卡顿”、“不跟手”,甚至怀疑硬件有问题。

多缓冲(Multiple Buffering)和虚拟屏幕(Virtual Screens)就是解决这些痛点的两大利器。简单来说,多缓冲的核心思想是“备菜”和“上菜”分开。想象一下餐厅后厨:厨师(CPU)在后台(后缓冲区)把下一道菜准备好,服务员(显示控制器)把前一道菜(前缓冲区)端给客人(屏幕)后,立刻就能无缝切换上新的。这样客人永远看不到厨房里手忙脚乱的准备过程。虚拟屏幕则像是给厨师一个超大的备餐台,他可以提前把好几桌的菜都摆好,需要哪桌就立刻把对应的区域推出去,实现瞬间的场景切换。

emWin官方手册里把这两项技术讲得很透,但真正要落地到你的STM32、NXP或者国产MCU项目里,光看手册是不够的。你得搞清楚你的LCD控制器支不支持多帧缓冲地址寄存器、VSYNC中断能不能稳定触发、内存到底够不够开三个缓冲区。这篇文章,我就结合手册里的原理和这些年踩过的坑,把emWin多缓冲和虚拟屏幕的配置、调试和实战心得掰开揉碎了讲清楚。无论你是刚接触emWin的新手,还是想优化现有界面流畅度的老鸟,都能找到可以直接“抄作业”的代码和避坑指南。

2. 核心原理深度拆解:双缓冲、三缓冲与虚拟屏幕的工作机制

2.1 多缓冲技术:从“闪烁”到“丝滑”的本质

要理解多缓冲,得先明白没有它的时候问题出在哪。在单缓冲模式下,显示控制器读取帧缓冲(Frame Buffer)来刷新屏幕,同时GUI绘制操作也直接写入同一个缓冲。这就好比同一块黑板,一边在擦写,一边有人在抄录。如果抄录(屏幕刷新)的速度和擦写(GUI绘制)的速度不同步,就会产生两种典型问题:

  1. 撕裂(Tearing):当屏幕刷新到一半时,GUI绘制更新了后半部分缓冲区的数据。导致屏幕上半部分显示旧画面,下半部分显示新画面,中间出现一条错位的“撕裂线”。
  2. 闪烁(Flickering):在绘制复杂界面(如清除背景再逐个绘制控件)时,屏幕在刷新周期内可能看到中间状态。比如先看到白色背景,再看到按钮画上去,视觉上就是闪烁。

多缓冲通过引入一个或多个“后台”缓冲区来解决这个问题。所有绘制操作只在后台缓冲区进行,完成后,通过一个原子操作(通常是修改显示控制器的帧缓冲起始地址寄存器)将后台缓冲区“提升”为前台显示缓冲区。这个切换动作理想情况下应该在屏幕两次刷新之间的空白期(垂直消隐期,Vertical Blanking Period)完成,此时没有像素正在被传输,切换对用户完全无感。

2.2 双缓冲 vs. 三缓冲:性能与资源的权衡

emWin支持双缓冲(Double Buffering)和三缓冲(Triple Buffering),选择哪种不是拍脑袋决定的。

双缓冲(一前一后): 这是最基础的配置。只有一个前缓冲(Front Buffer)用于显示,一个后缓冲(Back Buffer)用于绘制。流程是:

  1. GUI_MULTIBUF_Begin(): 将前缓冲内容复制到后缓冲(如果内容需要保留)。
  2. 在后缓冲执行所有GUI绘制命令。
  3. GUI_MULTIBUF_End(): 通知驱动,准备交换缓冲区。
  4. 驱动在LCD_X_DisplayDriver()中处理LCD_X_SHOWBUFFER命令,将后缓冲设为新前缓冲。

它的致命弱点在于“等待”GUI_MULTIBUF_End()调用后,如果立即切换缓冲区,而此时屏幕刷新还没到垂直消隐期,就可能引发撕裂。如果为了等VSYNC信号再切换,那么在这段等待时间里,GUI线程会被阻塞,无法开始下一帧的绘制,降低了最大帧率。在动画场景中,这可能表现为帧率不稳定或响应延迟。

三缓冲(一前两后): 三缓冲引入了第二个后缓冲,形成了一个缓冲区队列。它的工作流更复杂,但解决了双缓冲的阻塞问题:

  1. 缓冲区A正在显示(前缓冲)。
  2. GUI开始在缓冲区B绘制(后缓冲1)。
  3. 绘制完成,缓冲区B标记为“待显示”(Pending Buffer),但不立即切换,而是等待下一个VSYNC中断。
  4. 在等待VSYNC期间,GUI可以立即开始在缓冲区C(后缓冲2)绘制下一帧。
  5. VSYNC中断到来,在中断服务程序(ISR)中将缓冲区B设为前缓冲,缓冲区A被释放。
  6. 此时,如果缓冲区C已经绘制完成,则它成为新的“待显示”缓冲区;如果没完成,则继续绘制。

三缓冲的优势在于它将帧率的理论上限从“受限于VSYNC频率”提升到了“受限于GPU/CPU的绘制速度”,只要绘制速度比屏幕刷新快,就能持续输出帧。这对于需要60fps甚至更高流畅度的界面至关重要。代价就是需要多占用一个完整屏幕大小的帧缓冲内存。

选择建议:如果你的应用主要是静态界面,偶尔有简单动画,且内存紧张,双缓冲配合好VSYNC同步(下文会讲)基本够用。如果你的界面有连续动画(如滑动、渐变)、视频播放或复杂的仪表盘刷新,并且内存有盈余,强烈建议上三缓冲。实测下来,在STM32F429+RGB屏的平台上,三缓冲能让复杂列表滑动的流畅度有肉眼可见的提升。

2.3 虚拟屏幕:内存换时间的空间魔法

虚拟屏幕和多重缓冲解决的是不同维度的问题。多重缓冲关注的是时间轴上的绘制与显示分离,而虚拟屏幕关注的是空间轴上的扩展。

它允许你定义一个比物理显示屏尺寸更大的逻辑绘图区域。这个“虚拟画布”存储在显存中,你可以通过GUI_SetOrg()函数改变显示控制器从这块画布的哪个位置开始读取数据并显示。

主要应用场景有两个

  1. 平移(Panning):比如地图应用,虚拟画布是整个地图,物理屏幕是观察窗口,通过改变原点可以平滑地浏览地图的不同部分,无需重新绘制屏幕外区域。
  2. 多页面/场景快速切换(Virtual Pages):这是更常用的场景。比如一个设备有“主菜单”、“设置”、“数据监控”三个完全不同的界面。你可以将虚拟屏幕的高度(Y方向)设置为物理屏幕高度的3倍。这样在显存中,你就拥有了连续的三个“页面”。初始化时,可以提前把三个界面分别绘制在页面的不同区域(0-63行,64-127行,128-191行)。当需要切换界面时,只需调用GUI_SetOrg(0, 64)GUI_SetOrg(0, 128),修改显示起始地址,界面切换是“瞬间”完成的,没有重绘开销,这对于低速MCU实现快速UI切换极具价值。

虚拟屏幕的局限性:手册明确提到,虚拟屏幕与多缓冲功能互斥。因为虚拟屏幕本身已经通过改变显存起始地址来实现“切换”,这与多缓冲通过交换缓冲区地址来实现“切换”的机制在底层管理上存在冲突。所以你需要根据项目需求做取舍:要极致的动画流畅度(多缓冲),还是要瞬间的场景切换能力(虚拟屏幕)。

3. 硬件与驱动层配置实战

理论懂了,关键是怎么让emWin和你的硬件跑起来。这部分的配置集中在LCDConf.c中的两个函数:LCD_X_Config()LCD_X_DisplayDriver()

3.1 基础环境搭建与内存规划

在开始之前,你必须确认三件事:

  1. LCD控制器支持:你的LCD控制器(无论是MCU内置的LTDC、DPI,还是外挂的RA8875、SSD1963等)必须支持可编程的帧缓冲起始地址(Frame Buffer Start Address Register)。通常数据手册里会有一个叫LCD_*_FBADDR或类似的寄存器。
  2. 足够的内存:计算所需显存。公式为:X_SIZE * Y_SIZE * (BIT_PER_PIXEL/8) * N
    • X_SIZE, Y_SIZE: 屏幕分辨率(如320x240)。
    • BIT_PER_PIXEL: 色彩深度(如RGB565为16,RGB888为24)。
    • N: 缓冲区数量。双缓冲N=2,三缓冲N=3,虚拟屏幕若分3页则N=3(但用法不同)。 例如,320x240 RGB565双缓冲需要:320*240*2*2 = 307200字节。你必须确保这块内存在你的RAM(可能是SDRAM)中是连续且对齐的(通常需要32位或64位对齐以提高DMA效率)。
  3. VSYNC信号:如果追求无撕裂,最好能获取到LCD控制器的垂直同步中断(VSYNC IRQ)。很多驱动IC(如ILI9341)的TE(Tearing Effect)信号线就可以提供这个功能。

3.2 多缓冲配置详解(LCD_X_Config)

LCD_X_Config()函数在emWin初始化时被调用,这里是我们启用和配置多缓冲的地方。

// LCDConf.c // 假设我们使用三缓冲 #define NUM_BUFFERS 3 // 假设帧缓冲存储在SDRAM中,起始地址为0xC0000000 static U32 _aBuffer[NUM_BUFFERS][XSIZE_PHYS * YSIZE_PHYS] __attribute__((section(".SDRAM"))); void LCD_X_Config(void) { // !!!必须第一步:配置多缓冲,必须在创建显示设备之前调用 !!! GUI_MULTIBUF_Config(NUM_BUFFERS); // 使用默认的第0层 // 然后创建并链接显示驱动和颜色转换 GUI_DEVICE_CreateAndLink(&GUIDRV_Template_API, GUICC_M565, 0, 0); // ... 其他配置,如设置显示尺寸等 LCD_SetSizeEx(0, XSIZE_PHYS, YSIZE_PHYS); // 如果你有多个不连续的显存块,或者想用DMA/BLT引擎加速缓冲复制,可以设置自定义回调 // 否则,emWin默认使用memcpy // LCD_SetDevFunc(0, LCD_DEVFUNC_COPYBUFFER, (void(*))_MyCopyBuffer); }

关键点与避坑

  • 调用顺序GUI_MULTIBUF_Config()必须在GUI_DEVICE_CreateAndLink()之前调用,否则多缓冲无法生效。
  • 内存管理_aBuffer的声明方式很重要。对于大型缓冲,务必将其放到外部SDRAM区域(通过链接脚本或attribute指定),并确保地址对齐。不对齐的内存访问在开启Cache的Cortex-M7等内核上会导致严重性能问题甚至数据错误。
  • 自定义复制回调:大多数情况下用默认的memcpy即可。只有当你使用的硬件有2D加速(BitBLT)引擎,并且用DMA复制比CPU用memcpy更快时,才需要实现_MyCopyBuffer。实现时要特别注意Cache一致性,在DMA传输前后可能需要SCB_CleanInvalidateDCache操作。

3.3 驱动回调实现与VSYNC同步(LCD_X_DisplayDriver)

这是多缓冲能否流畅工作的核心。LCD_X_DisplayDriver()是emWin驱动与底层硬件的桥梁,多缓冲的关键命令是LCD_X_SHOWBUFFER

方案一:无VSYNC中断,简单直接(可能有撕裂)如果你的硬件没有VSYNC中断,或者对轻微撕裂不敏感,可以采用最简单的方式。

// LCDConf.c int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void * pData) { switch (Cmd) { // ... 其他命令处理,如初始化、设置坐标等 case LCD_X_SHOWBUFFER: { LCD_X_SHOWBUFFER_INFO * pInfo = (LCD_X_SHOWBUFFER_INFO *)pData; U32 BufferSize = XSIZE_PHYS * YSIZE_PHYS * BYTES_PER_PIXEL; U32 NewFBAddr = (U32)_aBuffer[pInfo->Index]; // 获取新缓冲区的物理地址 // 直接更新LCD控制器的帧缓冲起始地址寄存器 // 这里以STM32的LTDC为例: LTDC_Layer1->CFBAR = NewFBAddr; LTDC_ReloadConfig(LTDC_RELOAD_IMMEDIATE); // 立即重载配置 // !!!必须调用此函数通知emWin缓冲区已切换 !!! GUI_MULTIBUF_Confirm(pInfo->Index); break; } default: rv = -1; break; } return rv; }

风险:在非VSYNC期间更新CFBAR,很可能导致当前帧显示的数据来自新旧两个缓冲区,产生撕裂。对于动画,这种撕裂会非常明显。

方案二:基于VSYNC中断的无撕裂方案(推荐)这是实现流畅体验的标准做法。我们需要一个全局变量来记录“待显示”的缓冲区索引,并在VSYNC中断中执行实际切换。

// LCDConf.c static int _PendingBufferIndex = -1; // -1表示没有待显示的缓冲区 // VSYNC中断服务函数(通常由LCD控制器或EXTI触发) void LCD_VSYNC_IRQHandler(void) { if (_PendingBufferIndex >= 0) { U32 NewFBAddr = (U32)_aBuffer[_PendingBufferIndex]; // 在中断中安全地更新地址寄存器 LTDC_Layer1->CFBAR = NewFBAddr; // 注意:LTDC重载操作可能需要在非中断上下文中进行,具体看硬件 // 有些控制器需要在中断中设置标志,在主循环中重载 LTDC_ReloadConfig(LTDC_RELOAD_IMMEDIATE); // 确认切换完成 GUI_MULTIBUF_Confirm(_PendingBufferIndex); _PendingBufferIndex = -1; // 重置状态 } // ... 清除中断标志位 } int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void * pData) { switch (Cmd) { case LCD_X_SHOWBUFFER: { LCD_X_SHOWBUFFER_INFO * pInfo = (LCD_X_SHOWBUFFER_INFO *)pData; // 仅仅记录哪个缓冲区需要被显示,不立即切换 _PendingBufferIndex = pInfo->Index; // !!!注意:这里不调用 GUI_MULTIBUF_Confirm !!! break; } // ... 其他命令 } return rv; }

关键点与避坑

  • 中断优先级:VSYNC中断的优先级应设置为较低,避免阻塞其他关键中断(如触摸、通信)。其唯一任务就是安全地切换缓冲区地址。
  • 线程安全_PendingBufferIndex这个变量可能被主线程(LCD_X_DisplayDriver)和中断同时访问。在Cortex-M内核上,对32位整数的读写通常是原子的,但为了绝对安全,可以在主线程赋值前关闭中断,或者使用原子操作函数(如__atomic_store_n)。
  • 硬件差异:有些LCD控制器(如一些SPI接口的屏)的“设置起始地址”命令可能不是立即生效,而是等到下一帧开始。对于这类控制器,即使不用中断,在LCD_X_SHOWBUFFER中直接发命令也可能不会撕裂,但为了代码通用性,建议按有VSYNC中断的方式设计框架。

3.4 虚拟屏幕配置实践

虚拟屏幕的配置相对独立,因为它不与多缓冲共用。

// 在LCD_X_Config中或初始化阶段的某个地方 void APP_Init(void) { // 首先设置物理显示尺寸 LCD_SetSizeEx(0, 320, 240); // 物理屏是320x240 // 然后设置虚拟屏幕尺寸,例如我们想要2个页面(高度翻倍) LCD_SetVSizeEx(0, 320, 480); // 虚拟区域为320x480 // 初始化驱动,设置显存足以容纳320x480x2字节 // ... } // 在应用代码中 void SwitchToPage1(void) { // 将显示原点移动到虚拟区域的第二页起始处 GUI_SetOrg(0, 240); // Y偏移240个像素,即切换到下半部分 // 注意:GUI_SetOrg之后,所有的GUI坐标依然是相对于整个虚拟画布的原点(0,0)! // 如果你要在新页面画图,坐标需要加上偏移量,或者先调用GUI_SetOrg再画。 } void DrawOnVirtualPages(void) { // 在第一页(0-239行)画红色背景 GUI_SetColor(GUI_RED); GUI_FillRect(0, 0, 319, 239); GUI_DispStringAt("Page 0", 10, 10); // 在第二页(240-479行)画蓝色背景 GUI_SetColor(GUI_BLUE); GUI_FillRect(0, 240, 319, 479); // Y坐标从240开始 GUI_DispStringAt("Page 1", 10, 250); // Y坐标也需要加240 // 初始显示第一页 GUI_SetOrg(0, 0); }

重要提醒:当你调用GUI_SetOrg(x, y)后,它改变的是显示控制器读取数据的起始点,并没有改变emWin的绘图坐标系。绘图坐标依然以虚拟画布的左上角(0,0)为原点。这是一个常见的混淆点。

4. 应用层API使用与窗口管理器集成

配置好底层驱动后,在应用层使用多缓冲就非常简单了。

4.1 手动控制多缓冲流程

对于需要最高控制权的场景,你可以手动调用API来控制缓冲区的开始和结束。

void UpdateComplexAnimationFrame(void) { // 1. 开始一帧的绘制 GUI_MULTIBUF_Begin(); // 内部会执行缓冲区复制(如果需要) // 2. 执行所有的绘制操作 GUI_Clear(); GUI_DrawBitmap(&_bmBackground, 0, 0); // ... 绘制其他动态元素 WM_InvalidateWindow(hItem); // 使能窗口管理器时,触发重绘 WM_Exec(); // 执行窗口管理器重绘 // 3. 结束绘制,触发缓冲区交换(最终在VSYNC中断中完成) GUI_MULTIBUF_End(); }

这种模式适合游戏或自定义动画循环。你需要自己管理帧率,比如在GUI_MULTIBUF_End()后根据时间决定是否延时。

4.2 与窗口管理器(WM)协同实现自动多缓冲

对于大多数基于窗口、控件的标准GUI应用,让窗口管理器自动管理多缓冲是更省心的方式。emWin的窗口管理器可以自动在重绘窗口前切换到后缓冲区,在所有无效窗口重绘完毕后交换缓冲区。

#include "WM.h" void MainTask(void) { // ... 初始化GUI、创建窗口和控件 WM_SetCreateFlags(WM_CF_MEMDEV); // 使用存储设备通常也是个好主意,可与多缓冲叠加 WM_MULTIBUF_Enable(1); // 启用WM的多缓冲支持!参数1表示启用 while(1) { GUI_Delay(10); // GUI_Delay内部会调用WM_Exec() // 当有任何控件无效(如被触摸、数据更新)时, // WM会自动在重绘前调用GUI_MULTIBUF_Begin(), // 重绘后调用GUI_MULTIBUF_End()。 } }

启用WM_MULTIBUF_Enable(1)后,你几乎不需要再手动调用多缓冲相关的API。窗口管理器会智能地处理一切,将整个界面的更新打包到一次缓冲区交换中,最大化地减少视觉瑕疵。这是开发标准应用程序的首选方式。

4.3 虚拟屏幕API的使用技巧

虚拟屏幕的API更简单,核心就是GUI_SetOrg()。但有些技巧能让你用得更好:

  • 平滑滚动:不要一次跳跃很多像素。可以实现一个函数,每帧将原点移动1-2个像素,实现平滑的滚动效果。
    void SmoothScrollTo(int targetY) { int currentY = GUI_GetOrgY(); // 注意:可能需要自己维护一个变量,因为GUI_GetOrg()可能不是实时获取硬件值 while(currentY != targetY) { int step = (targetY > currentY) ? 2 : -2; currentY += step; GUI_SetOrg(0, currentY); GUI_Delay(5); // 控制滚动速度 } }
  • 预绘制与懒加载:利用虚拟屏幕可以提前绘制好不急于显示的页面。但要注意内存占用。对于非常复杂的页面,也可以只提前绘制静态部分,动态部分等到页面即将显示时再更新。
  • 与存储设备结合:虚拟屏幕切换快,但占用连续大内存。存储设备(Memory Device)可以为单个复杂窗口提供离屏渲染,占用内存更灵活。两者可以结合使用,比如用虚拟屏幕管理几个主要的全屏页面,用存储设备来渲染页面内复杂的子窗口。

5. 调试技巧、性能优化与常见问题排查

即使配置正确,在实际项目中也可能遇到各种奇怪的问题。这里分享一些实战中积累的排查方法和优化经验。

5.1 调试与验证

  1. 验证缓冲区是否真的在切换

    • 软件方法:在每个缓冲区的固定位置(比如角落)用不同的颜色画一个小方块。然后让界面持续刷新。如果方块颜色在闪烁,说明缓冲区在切换。如果永远只显示一个颜色,说明切换没生效。
    • 硬件方法:使用逻辑分析仪或示波器,在LCD_X_SHOWBUFFER命令处理函数中翻转一个GPIO引脚。观察这个引脚的电平变化是否与屏幕刷新率(如60Hz)同步。如果根本没有脉冲,说明GUI_MULTIBUF_End()没被调用或驱动没正确处理命令。
  2. 检查撕裂

    • 绘制一个从屏幕顶部移动到底部的水平亮线。如果线条在移动过程中出现断裂、弯曲或变成两条,基本可以断定是撕裂。这说明缓冲区切换没有在VSYNC期间发生。
  3. 测量帧率与性能

    • GUI_MULTIBUF_BeginGUI_MULTIBUF_End之间用定时器测量耗时。这个时间应小于一帧的时间(如60Hz下小于16.67ms),否则就会掉帧。如果绘制时间过长,需要考虑优化绘图指令(如减少透明混合、使用位图代替矢量绘制)、启用硬件加速或降低色彩深度。

5.2 性能优化建议

  • 显存带宽是瓶颈:无论是缓冲区的复制(memcpy)还是LCD控制器的持续读取,都消耗大量内存带宽。确保:
    • 使用32位或更宽的总线访问显存(SDRAM/DDR)。
    • 显存地址按Cache行对齐,并合理使用Cache策略(对于被CPU和DMA/LCD控制器共同访问的显存,通常配置为Write-through或Non-cacheable)。
    • 如果芯片有图形加速器(DMA2D, PXP等),务必用其加速GUI_MULTIBUF_Begin中的缓冲区复制操作和常见的填充、混合操作。
  • 减少不必要的全屏更新:即使有多缓冲,全屏清屏和重绘也是昂贵的。充分利用窗口管理器的无效区域(Invalidation)机制,只重绘界面中真正变化的部分。
  • 三缓冲的“第三帧”效应:三缓冲会引入额外的延迟(Latency)。从用户输入到画面响应,最多可能延迟2帧(约33ms)。对于极度要求实时性的操作(如触控笔书写),可能需要权衡。双缓冲的延迟更低(最多1帧)。

5.3 常见问题速查表

问题现象可能原因排查步骤与解决方案
屏幕全黑或花屏1. 帧缓冲地址设置错误。
2. 缓冲区内存未初始化或内容被破坏。
3. 多缓冲未正确启用,驱动使用了错误的内存区域。
1. 检查LCD_X_DisplayDriver中计算的地址是否正确,特别是pInfo->Index
2. 在初始化时用memset将整个显存区域填充为某个测试色。
3. 确认GUI_MULTIBUF_Config()在创建设备前调用。
画面静止,不更新1.GUI_MULTIBUF_End()未被调用。
2.LCD_X_SHOWBUFFER命令未处理或GUI_MULTIBUF_Confirm()未调用。
3. VSYNC中断未触发,且_PendingBufferIndex机制卡住。
1. 确保你的绘制流程调用了GUI_MULTIBUF_End(),或WM已启用多缓冲。
2. 在LCD_X_DisplayDriverLCD_X_SHOWBUFFERcase里打日志或点灯。
3. 检查VSYNC中断是否使能,或尝试改用无中断的直接切换模式测试。
严重撕裂缓冲区切换未在VSYNC期间进行。1. 确认是否使用了VSYNC中断方案。
2. 检查VSYNC中断优先级是否被更高优先级中断长时间阻塞。
3. 在LCD_X_SHOWBUFFER中直接切换,并增加一个小的延时(如等待LCD->SR中的VSYNC标志),模拟VSYNC等待。
轻微闪烁或抖动1. 双缓冲模式下,绘制时间超过一帧,且未等VSYNC。
2. 三缓冲模式下,绘制速度不稳定,时而快于刷新率,时而慢于刷新率。
1. 优化绘制代码,确保每帧绘制在16.67ms内完成。
2. 尝试启用三缓冲,它更能容忍帧时间波动。
3. 使用性能分析工具定位耗时最长的绘图函数。
启用多缓冲后,触摸响应变慢三缓冲引入了额外的输入延迟。这是正常权衡。如果无法接受,可考虑换回双缓冲,并尽力优化绘制至一帧时间内完成。或者,将触摸响应处理与图形渲染放在不同优先级的任务中。
虚拟屏幕切换后,绘图位置错乱混淆了显示原点与绘图原点。牢记:GUI_SetOrg()改变的是显示窗口在虚拟画布上的位置,所有GUI绘图函数的坐标依然是相对于虚拟画布(0,0)的。在切换原点后绘图,需要手动计算偏移量。

最后,再分享一个很隐蔽的坑:Cache一致性问题。如果你的显存在CPU的Cacheable区域,而LCD控制器通过DMA直接读取内存(不经过Cache),那么CPU绘制到缓冲区的数据可能还留在Cache里,没有写回内存。导致LCD控制器读到的还是旧数据。解决方法是在GUI_MULTIBUF_End()之后、或DMA启动之前,执行数据缓存清理(Clean)操作。对于Cortex-M7,可以使用SCB_CleanDCache_by_Addr()函数清理特定缓冲区地址范围。这个问题在启用硬件加速(如DMA2D)时尤其常见,花了我整整两天时间才定位到。