嵌入式GUI性能优化:emWin多缓冲与虚拟屏幕实战解析 1. 项目概述嵌入式GUI性能优化的核心武器在嵌入式系统开发中图形用户界面GUI的流畅度直接决定了产品的用户体验。你是否遇到过界面刷新时出现的画面撕裂、闪烁或者复杂界面切换时的卡顿这些问题在资源受限的MCU上尤为突出。今天我想结合我过去在多个工业HMI和智能穿戴设备项目中的实战经验深入聊聊emWin图形库中两个至关重要的性能优化技术多缓冲Multiple Buffering和虚拟屏幕Virtual Screens。这不仅仅是API调用更是一套从硬件显存管理到软件渲染策略的系统性工程思维。简单来说多缓冲技术是为了解决“画一半就被看见”的尴尬而虚拟屏幕则是为了突破物理屏幕尺寸的限制实现“大画布小窗口”的灵活显示。很多人看过官方手册里的代码片段但真正在项目里用起来总会遇到显存不够、切换时机不对、撕裂依旧存在等各种坑。这篇文章我将带你从原理到配置从代码到调试完整走一遍这两个技术的实战应用并分享那些手册里不会写的“踩坑”心得。无论你是正在评估emWin的新手还是希望优化现有项目性能的老手相信都能从中找到可直接复用的思路和代码。2. 核心原理深度解析为什么需要它们在深入代码之前我们必须先搞清楚这两个技术到底解决了什么问题。很多开发者直接套用API却不明白背后的“为什么”一旦出现问题就无从下手。2.1 多缓冲技术的本质以空间换时间消除视觉撕裂想象一下画家在作画而观众就站在画布旁边看。画家每画一笔观众都能立刻看到未完成的画面这就是单缓冲模式——渲染CPU/GPU绘图和显示LCD控制器读取共享同一块帧缓冲区Frame Buffer。当渲染速度帧率与显示刷新率VSYNC不同步时屏幕上半部分显示的是新帧下半部分还是旧帧这就产生了撕裂Tearing。多缓冲的解决思路非常直观准备两块双缓冲或三块三缓冲画布。画家在后台的“画布B”上专心作画观众始终看着前台已经完成的“画布A”。当画家画完一整幅画后迅速将“画布B”换到前台给观众看然后自己到后台的“画布A”上开始画下一幅。这样观众永远看到的是完整的画面。在emWin中这个过程涉及三个关键角色应用层调用GUI_MULTIBUF_Begin()和GUI_MULTIBUF_End()来声明一个渲染周期。emWin库管理缓冲区的状态在GUI_MULTIBUF_End()被调用时通过驱动层回调函数通知硬件切换缓冲区。驱动层实现LCD_X_DisplayDriver()函数响应LCD_X_SHOWBUFFER命令将指定缓冲区的地址写入LCD控制器的显存起始地址寄存器或者通过中断服务程序ISR在垂直消隐期进行切换。关键计算帧缓冲区大小这是硬件设计的基础。公式很简单但计算错误会导致内存溢出或显示异常。BufferSize (XSIZE * YSIZE * BITSPERPIXEL) / 8例如一个320x240分辨率、16位色深RGB565的屏幕BufferSize 320 * 240 * 16 / 8 153,600 字节双缓冲就需要307,200字节三缓冲则需要460,800字节。在资源紧张的嵌入式系统中这必须作为选型MCU和外部RAM的重要依据。2.2 虚拟屏幕技术的本质预渲染与瞬时切换虚拟屏幕的概念更偏向于软件设计策略。它允许你创建一个比物理屏幕更大的逻辑绘图区域。比如你的LCD只有320x240但你可以定义一个640x480的虚拟画布。这带来了两大核心优势平滑平移Panning适用于地图、长图表浏览。你可以在大画布上一次性渲染好全部内容然后通过GUI_SetOrg(x, y)仅改变显示起点就能实现极其平滑的滚动效果无需重复渲染可见区域外的内容性能开销极低。多页面/场景切换Virtual Pages将虚拟画布在垂直或水平方向上划分为多个与物理屏幕同大的“页”。每一页可以预先渲染好一个完整的UI界面如主菜单页、设置页、关于页。切换界面时只需调用GUI_SetOrg()跳转到对应页的起始坐标显示内容瞬间改变没有任何绘制延迟用户体验是“秒切”。它的硬件基础是LCD控制器支持可编程的显示起始地址通常是一个寄存器。当你改变这个地址时控制器下一帧就会从新的内存地址开始读取数据送显。GUI_SetOrg()函数本质上就是在设置这个地址的偏移量。虚拟显存计算VirtualBufferSize (LCD_VXSIZE * LCD_VYSIZE * BITSPERPIXEL) / 8如果你的物理屏幕是320x240但你想支持3个页面垂直排列那么虚拟Y尺寸LCD_VYSIZE可以设为720240*3。总显存需求就是单屏的3倍。3. 多缓冲技术实战配置与核心代码剖析理解了原理我们来看怎么在emWin中把它用起来。官方手册给的代码是骨架我们需要把它填充上血肉。3.1 驱动层适配实现缓冲区切换多缓冲的核心在驱动层。你需要实现两个关键部分配置回调函数和显示驱动回调。第一步配置多缓冲并设置自定义拷贝函数可选在LCD_X_Config()函数中你需要做三件事调用GUI_MULTIBUF_Config(NUM_BUFFERS)来声明使用的缓冲区数量2或3。创建并链接显示驱动设备。可选设置自定义的缓冲区拷贝函数。默认情况下emWin使用memcpy进行前后台缓冲区同步。但如果你有DMA2D图形加速器或其它硬件加速方式就在这里替换掉默认的memcpy。// 假设我们使用双缓冲 #define NUM_BUFFERS 2 // 假设显存起始地址为0xC0000000SDRAM地址 static U32 _VRamBaseAddr 0xC0000000; // 自定义缓冲区拷贝函数示例使用DMA加速 static void _CopyBuffer(int LayerIndex, int IndexSrc, int IndexDst) { U32 BufferSize, AddrSrc, AddrDst; // 计算一个缓冲区的大小 BufferSize (XSIZE_PHYS * YSIZE_PHYS * LCD_BITSPERPIXEL) / 8; // 计算源地址和目的地址 AddrSrc _VRamBaseAddr BufferSize * IndexSrc; AddrDst _VRamBaseAddr BufferSize * IndexDst; // 关键这里替换为你的硬件加速拷贝如DMA2D传输 // memcpy((void *)AddrDst, (void *)AddrSrc, BufferSize); // 默认软件拷贝 MY_DMA2D_CopyBuffer(AddrDst, AddrSrc, BufferSize); // 使用硬件加速拷贝 } void LCD_X_Config(void) { // 1. 初始化多缓冲告知emWin我们使用2个缓冲区 GUI_MULTIBUF_Config(NUM_BUFFERS); // 2. 创建并链接显示驱动例如使用16位色线性驱动 GUI_DEVICE_CreateAndLink(GUIDRV_Lin_16, // 显示驱动 GUICC_565, // 颜色转换RGB565 0, 0); // 层索引和驱动索引 // 3. 可选设置自定义拷贝回调函数 // 如果你有比memcpy更快的方式如DMA、硬件加速就在这里注册 // 如果只是用memcpy这行可以省略因为这是默认行为 LCD_SetDevFunc(0, LCD_DEVFUNC_COPYBUFFER, (void(*)(void))_CopyBuffer); }注意手册中特别提到如果你的拷贝操作就是简单的memcpy那么注册这个自定义函数是没意义的因为驱动默认就是这么做的。这个回调的价值在于集成硬件加速。第二步在显示驱动回调中实现缓冲区切换这是多缓冲的“开关”。当emWin在后台缓冲区完成一帧的绘制后会调用GUI_MULTIBUF_End()进而触发驱动层的LCD_X_DisplayDriver()函数并传入LCD_X_SHOWBUFFER命令。你有两种方式来实现切换方案A使用VSYNC中断推荐无撕裂这种方式在LCD的垂直消隐期VSYNC进行切换完全避免了撕裂。static int _PendingBuffer -1; // 等待显示的缓冲区索引 // VSYNC中断服务函数 void LCD_VSYNC_IRQHandler(void) { if (_PendingBuffer 0) { U32 Addr, BufferSize; // 计算要显示的缓冲区地址 BufferSize (XSIZE_PHYS * YSIZE_PHYS * LCD_BITSPERPIXEL) / 8; Addr _VRamBaseAddr BufferSize * _PendingBuffer; // 将新缓冲区地址写入LCD控制器的显存起始地址寄存器 // 这是硬件相关操作以下以假设的寄存器为例 LCD_FRAME_BUFFER_REG Addr; // 关键步骤通知emWin缓冲区已切换完成 GUI_MULTIBUF_Confirm(_PendingBuffer); _PendingBuffer -1; // 重置等待状态 } } // emWin显示驱动回调函数 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; // 收到显示新缓冲区的命令记录索引等待VSYNC中断 _PendingBuffer pInfo-Index; break; } // ... 处理其他命令如初始化、设置像素等 default: return -1; // 不支持的命令 } return 0; // 成功处理 }方案B直接切换简单可能有撕裂如果没有可用的VSYNC中断或者对轻微撕裂不敏感可以直接在回调函数中切换。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 Addr, BufferSize; BufferSize (XSIZE_PHYS * YSIZE_PHYS * LCD_BITSPERPIXEL) / 8; Addr _VRamBaseAddr BufferSize * pInfo-Index; // 直接写入LCD控制器寄存器可能在任意时刻可能引发撕裂 LCD_FRAME_BUFFER_REG Addr; // 立即确认 GUI_MULTIBUF_Confirm(pInfo-Index); break; } // ... 其他命令 } return 0; }3.2 应用层使用开启与结束渲染周期驱动层配置好后应用层的使用就非常简单了。你需要把你的绘制代码包裹在GUI_MULTIBUF_Begin()和GUI_MULTIBUF_End()之间。void UpdateMainScreen(void) { // 开始一个多缓冲绘制周期 GUI_MULTIBUF_Begin(); // 所有的GUI绘制操作放在这里 GUI_Clear(); GUI_SetFont(GUI_Font24_ASCII); GUI_DispStringAt(Hello, Double Buffer!, 50, 100); GUI_FillCircle(160, 120, 50); // ... 更多绘制 // 结束绘制周期触发缓冲区切换 GUI_MULTIBUF_End(); }重要细节GUI_MULTIBUF_Begin()会确保你接下来绘制操作的目标是“后台缓冲区”。在双缓冲中如果当前显示缓冲区0那么Begin后会让你在缓冲区1上绘制。GUI_MULTIBUF_End()会通知驱动层“我画完了可以切换显示了”。然后驱动层会执行我们上面实现的切换逻辑。对于窗口管理器WM你可以调用WM_MULTIBUF_Enable(1)来让其自动管理多缓冲。启用后WM在重绘无效窗口前会自动切换到后台缓冲区绘制完成后再切换显示这对基于窗口的应用程序非常方便。3.3 双缓冲 vs 三缓冲如何选择双缓冲最常用。一个前台显示一个后台渲染。逻辑简单内存占用较少。但存在一个问题如果渲染一帧的时间T_render刚好超过屏幕刷新周期T_vsync那么当后台缓冲区渲染完成时可能错过下一个VSYNC必须再等待一个周期才能切换导致帧率下降甚至卡顿。三缓冲增加了一个“等待缓冲区”。当后台缓冲区B正在渲染时如果前台缓冲区A正在显示还有一个缓冲区C已经渲染完毕在等待。这样一旦VSYNC到来可以立即将C切换到前台B变成新的等待缓冲区并立即开始下一帧的渲染到A。这能更好地应对渲染时间波动提供更稳定的帧率但代价是多消耗50%的显存。选择建议对于MCU性能较强、渲染时间稳定且短于刷新周期的应用如简单仪表盘双缓冲足够。对于渲染负载变化大、或追求极限流畅度的复杂动画界面如滑动列表、过渡特效三缓冲是更好的选择可以有效减少因偶尔渲染超时导致的卡顿感。4. 虚拟屏幕技术实战配置与应用模式虚拟屏幕的配置相对独立它主要关注显存布局和坐标管理。4.1 基础配置与驱动适配首先你需要在初始化时告诉emWin物理屏幕和虚拟屏幕的尺寸。void LCD_X_Config(void) { // ... 创建显示驱动设备等 // 设置物理显示尺寸你的LCD实际分辨率 LCD_SetSizeEx(0, 320, 240); // 第0层320x240 // 关键设置虚拟显示尺寸 // 例如我们想要垂直方向上有3个页面虚拟高度为720 LCD_SetVSizeEx(0, 320, 720); // 虚拟区域为320x720 // 设置显存基地址必须足够容纳整个虚拟区域 // 计算总大小320 * 720 * 2 (16bpp) / 8 115,200 字节 LCD_SetVRAMAddrEx(0, (void*)0xC0000000); }其次你的LCD驱动必须支持设置显示起始地址即响应LCD_X_SETORG命令。int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void * pData) { switch (Cmd) { // ... 其他命令处理 case LCD_X_SETORG: { LCD_X_SETORG_INFO * pOrgInfo (LCD_X_SETORG_INFO *)pData; // pOrgInfo-xPos, pOrgInfo-yPos 是新的原点坐标 U32 NewBufferAddr; // 计算新的显存起始地址 // 公式基地址 (yPos * 行字节数) (xPos * 像素字节数) // 对于线性存储且xPos通常为0的平移简化为 U32 BytesPerLine XSIZE_PHYS * LCD_BITSPERPIXEL / 8; NewBufferAddr _VRamBaseAddr (pOrgInfo-yPos * BytesPerLine); // 将新地址写入LCD控制器寄存器 LCD_FRAME_BUFFER_REG NewBufferAddr; break; } default: return -1; } return 0; }4.2 应用模式一平滑平移Panning这种模式适用于内容大于屏幕的场景如地图、波形图、长文档。// 假设虚拟画布是640x480物理屏幕是320x240 LCD_SetSizeEx(0, 320, 240); LCD_SetVSizeEx(0, 640, 480); // 1. 在虚拟画布上绘制完整的大图 GUI_SetOrg(0, 0); // 确保原点在(0,0) GUI_Clear(); // ... 绘制你的大地图或长波形 // 2. 通过定时器或触摸事件改变显示原点实现滚动 static int scrollY 0; void TimerCallback(void) { scrollY 2; // 每次向下滚动2像素 if (scrollY (480 - 240)) scrollY 0; // 循环 // 核心改变显示起点画面立即“滑动” GUI_SetOrg(0, scrollY); }优势滚动极其平滑无渲染延迟因为所有像素都已预先绘制在显存中切换只是改变一个寄存器值。4.3 应用模式二多页面切换Virtual Pages这种模式将虚拟区域划分为多个独立的“屏幕”用于快速切换不同UI场景。#define PAGE_HEIGHT 240 #define PAGE0_Y_OFFSET 0 #define PAGE1_Y_OFFSET 240 #define PAGE2_Y_OFFSET 480 // 配置虚拟高度为3个页面 LCD_SetSizeEx(0, 320, 240); LCD_SetVSizeEx(0, 320, PAGE_HEIGHT * 3); // 初始化时预先渲染所有页面到显存的对应区域 void RenderAllPages(void) { // 渲染页面0 (主菜单) GUI_SetOrg(0, PAGE0_Y_OFFSET); GUI_Clear(); GUI_DispStringAt(Main Menu, 100, 100); // ... 绘制主菜单所有元素 // 渲染页面1 (设置) GUI_SetOrg(0, PAGE1_Y_OFFSET); GUI_Clear(); GUI_DispStringAt(Settings, 100, 100); // ... 绘制设置页面所有元素 // 渲染页面2 (关于) GUI_SetOrg(0, PAGE2_Y_OFFSET); GUI_Clear(); GUI_DispStringAt(About, 100, 100); // ... 绘制关于页面所有元素 // 最后将显示切回页面0 GUI_SetOrg(0, PAGE0_Y_OFFSET); } // 当用户点击“设置”按钮时瞬间切换到页面1 void OnSettingsButtonClicked(void) { // 没有任何重绘直接切换显示原点 GUI_SetOrg(0, PAGE1_Y_OFFSET); } // 返回主菜单 void OnBackButtonClicked(void) { GUI_SetOrg(0, PAGE0_Y_OFFSET); }优势界面切换是“瞬间”的用户体验无缝衔接。特别适合层级固定、内容相对静态的菜单系统。4.4 结合窗口管理器WM的虚拟屏幕emWin的窗口管理器可以很好地与虚拟屏幕协作。你可以为每个“页面”创建一个全屏窗口作为容器所有该页面的子窗口都创建在这个容器内。切换页面时只需移动这个容器窗口的父窗口或改变其位置配合GUI_SetOrg()可以构建出非常复杂的多级界面系统。5. 多缓冲与虚拟屏幕的联合应用与高级技巧单独使用它们已经能解决很多问题但强强联合才能发挥最大威力。5.1 为每个虚拟页面启用多缓冲想象一个场景你有三个虚拟页面主菜单、设置、关于。每个页面内部都有复杂的动画或控件刷新。你可以为整个虚拟区域启用多缓冲。// 配置 LCD_SetVSizeEx(0, 320, 720); // 3个页面 GUI_MULTIBUF_Config(2); // 双缓冲 // 此时emWin会管理两个巨大的缓冲区每个320x720x2字节 // 当你在页面0上调用GUI_MULTIBUF_Begin()/End()时它操作的是页面0所在的那部分显存。 // 切换到页面1后再调用Begin/End操作的就是页面1对应的部分。好处每个独立的页面都享受到了无撕裂的流畅刷新。代价显存需求翻倍双缓冲或翻三倍三缓冲。需要仔细评估硬件资源。5.2 动态分配与内存优化策略在资源极其紧张的项目中可以采用混合策略策略A只为最需要流畅动画的页面如主界面所在虚拟区域启用多缓冲。其他静态页面使用单缓冲。策略B使用三缓冲但虚拟屏幕只开双页。这样总显存占用是2页 * 3缓冲 6倍单屏显存。通过精心设计UI流让第三页作为“预加载页”在用户可能进入的下一个界面提前渲染好。这需要你在LCD_X_Config和LCD_X_DisplayDriver中动态计算缓冲区地址逻辑会复杂很多但能最大限度榨干硬件性能。5.3 实操心得与避坑指南显存对齐很多LCD控制器和DMA对显存地址有对齐要求如4字节、8字节对齐。在计算_VRamBaseAddr和每个缓冲区的起始地址时务必确保地址满足硬件要求否则可能导致显示错乱或DMA传输失败。缓存一致性如果MCU有数据缓存D-Cache而显存区域被CPU和LCD控制器通过DMA共享你必须处理好缓存一致性问题。在CPU写入绘制数据后、LCD控制器读取前需要执行缓存写回Write-Back和无效化Invalidate操作。否则LCD控制器读到的可能是缓存里的旧数据。这是很多开发者容易忽略的致命问题。切换时机使用VSYNC中断切换缓冲区是最佳实践。务必确认你的LCD控制器能产生VSYNC中断并且中断优先级设置合理中断服务程序执行时间尽可能短。性能 profiling使用MCU的定时器或系统滴答计数器测量GUI_MULTIBUF_Begin()到GUI_MULTIBUF_End()之间的时间即一帧渲染时间。确保它小于你的屏幕刷新周期例如60Hz对应16.67ms。如果渲染时间过长需要考虑优化绘制指令、启用图形加速、或降低界面复杂度。虚拟屏幕的绘制污染在虚拟屏幕模式下GUI_SetOrg()改变的是“观察窗口”的位置但所有GUI绘制函数的坐标仍然是基于**虚拟坐标原点(0,0)**的。如果你在设置新原点后不小心在错误的位置绘制可能会污染其他页面。好的实践是在绘制特定页面内容前总是先GUI_SetOrg()到该页面的起始坐标然后以(0,0)作为该页面的左上角进行绘制。调试工具emWin模拟器Simulation是调试多缓冲和虚拟屏幕的利器。你可以清晰地看到前后台缓冲区的切换过程以及虚拟画布上的完整内容。在硬件调试阶段如果没有高级调试器可以尝试在显存特定位置写入特殊颜色标记如切换缓冲区时在角落画一个色块通过肉眼观察屏幕来辅助判断程序执行流程。6. 常见问题排查与解决方案实录在实际项目中我遇到过不少问题这里总结几个典型的问题1启用多缓冲后屏幕闪烁反而更严重了。可能原因缓冲区切换时机错误在LCD控制器正在扫描显示的过程中切换了显存地址导致半帧旧数据、半帧新数据。排查确认是否使用了VSYNC中断进行切换。检查GUI_MULTIBUF_Confirm()是否在真正切换完成之后才被调用。如果是在LCD_X_DisplayDriver里直接切换并确认尝试改为中断方式。解决方案务必在VSYNC中断服务程序ISR中执行最终的缓冲区地址切换和Confirm调用。问题2使用虚拟屏幕调用GUI_SetOrg()后屏幕显示花屏或错位。可能原因1LCD_X_SETORG命令处理函数中地址计算错误。特别是当虚拟宽度不等于物理宽度或者颜色深度不是字节对齐时计算公式会复杂化。排查仔细核对地址计算公式。对于线性存储最常见新地址 基地址 yPos * 行字节数。行字节数 虚拟宽度 * 每像素字节数。确保计算中使用的XSIZE是虚拟宽度LCD_GetVXSize()而不是物理宽度。可能原因2显存分配不足。虚拟区域的总大小超过了LCD_SetVRAMAddrEx分配的显存空间。排查计算虚拟宽度 * 虚拟高度 * 色深 / 8确保其小于你为当前层分配的显存大小。解决方案修正地址计算逻辑并确保分配足够的显存。问题3多缓冲和窗口管理器WM一起使用时部分窗口刷新不正常。可能原因没有正确启用WM的多缓冲自动管理或者手动调用的GUI_MULTIBUF_Begin/End与WM的自动管理冲突。排查检查是否在初始化WM后调用了WM_MULTIBUF_Enable(1)。如果启用了那么WM在重绘无效区域时会自动处理缓冲区的开始和结束。此时你在WM之外手动调用Begin/End可能会破坏这个状态机。解决方案如果使用了WM并且界面更新主要由WM触发如按钮点击、窗口移动建议调用WM_MULTIBUF_Enable(1)并避免在应用层手动调用GUI_MULTIBUF_Begin/End。如果某些全局性、非窗口的绘制如背景动画需要多缓冲则需要仔细设计绘制顺序或考虑将这些绘制也整合到WM的窗口回调中。问题4系统运行一段时间后死机怀疑是内存越界。可能原因多缓冲或虚拟屏幕导致显存访问越界踩踏了其他数据区如堆、栈。排查使用调试器查看显存分配区域的边界。在LCD_X_Config中设置的_VRamBaseAddr和通过LCD_SetVSizeEx计算出的结束地址是否与其他内存区域有重叠确保你的链接脚本Linker Script为显存保留了独立且充足的空间。解决方案精确计算并隔离显存区域。在MPU内存保护单元支持的MCU上可以为显存区域配置为“设备内存”属性并禁止CPU缓存这既能解决缓存一致性问题也能防止误操作。问题5在低端MCU上即使使用双缓冲复杂界面仍有明显卡顿。可能原因渲染一帧的时间超过了屏幕刷新周期。双缓冲下如果渲染一帧耗时20ms而屏幕刷新周期是16.67ms60Hz那么你实际只能达到50Hz的显示帧率并且会因为等待VSYNC而引入延迟。排查进行性能分析定位最耗时的绘制操作如绘制大图片、复杂矢量图形、TrueType字体渲染。解决方案优化绘制使用位图代替矢量图形使用预渲染的字体减少绘制调用次数。启用局部刷新只重绘界面中真正变化的部分而不是整个屏幕。WM的无效区域机制就是干这个的确保它被正确使用。考虑三缓冲三缓冲可以在一定程度上“吸收”偶尔的渲染超时提供更平均的帧率但无法解决持续性的渲染性能瓶颈。降低刷新率如果硬件确实无法满足可以考虑将LCD刷新率从60Hz降低到30Hz这样每帧渲染时间预算就变成了33.3ms。通过深入理解多缓冲与虚拟屏幕的原理结合项目实际需求进行选型和配置再辅以细致的调试和性能优化这两项技术能极大地提升嵌入式GUI应用的视觉流畅度和用户体验。它们不仅仅是emWin库提供的功能更是嵌入式图形系统设计中关于时间与空间权衡的经典思想体现。