RK3588上LVGL显示优化:从FrameBuffer到DRM的完整实践
1. 为什么非要在RK3588上折腾LVGL选型背后的现实压力LVGL本身定位是嵌入式轻量级图形库主战场本来是MCU和低端MPU。然而在RK3588这种四核Cortex-A76加四核Cortex-A55、还带GPU和NPU的旗舰级芯片上我依然选择了LVGL而不是Qt或者Flutter很多人第一反应是“大材小用”。但这个选择恰恰是从实际项目需求倒推出来的。当时要做一个带复杂交互的工业设备显示终端界面设计稿大概有十几个页面需要动画、图表、动态控件但核心诉求是启动速度要快、内存占用要小、UI更新要可控、长时间运行要稳定。Qt复杂度和内存开销都不小光QML引擎起来就得多花几百毫秒而LVGL纯C实现、依赖少、资源占用低在RK3588上跑起来几乎是毫无压力而且裁减和定制非常灵活。更重要的是LVGL允许你直接操控底层显示后端这反而成了我在RK3588上做显示优化的一个突破口。既然选择了LVGL接下来就要面临一个绕不开的问题显示输出到底走哪条路。RK3588的显示链路比我之前玩过的STM32、全志V3s要复杂得多内核态有完整的DRM子系统用户态有libdrm、weston、X11而传统做法里最常见的FrameBuffer路径在这里已经变得非常“边缘”。大多数教程只会告诉你“编译LVGL然后打开fb0”但实际在RK3588上这条路往往走不通或者跑不满。于是我把整套移植和优化过程完整记录下来重点讲清楚从FrameBuffer到DRM这条演进阶路的每一步以及其中值得注意的性能瓶颈和踩坑点。这篇文章适合这些人刚拿到RK3588开发板、想把LVGL跑起来但不知道显示后端怎么选的初学者已经在用FrameBuffer刷屏、但觉得CPU占用过高、界面撕裂严重的开发者以及想深入了解DRM显示链路、想在ARM Linux上做GUI优化的嵌入式老兵。2. 移植前的平台摸底RK3588显示链路的家底在动手改代码之前先把平台吃透是节约时间的最有效方式。RK3588的显示链路和传统的“LCD控制器PANEL”不是一个量级的东西它实际上是一套完整的VOP显示流水线对外通过DRM子系统暴露能力。如果一上来就直接写屏后面一定会踩坑。2.1 设备节点与显示子系统的文件线索在终端下执行ls /dev大概率你会看到/dev/dri/card0和/dev/dri/card1但不见得有/dev/fb0。这是第一个关键信号RK3588的默认内核配置通常以DRM为主FrameBuffer设备要么未注册要么仅作为DRM的兼容模拟层存在。查看内核日志能拿到更多信息dmesg | grep -i -E drm|vop|panel|hdmi|dsiRK3588 DRM驱动一般会枚举出多个Video Port也就是显示控制器内部不同的显示通道。不同的板卡、不同的屏幕接口对应关系不一样。例如HDMI输出通常在card0-encoder上eDP或MIPI DSI又可能是另一个端口。你需要在用户态把这些资源枚举出来而不是像以前单片机开发那样直接按寄存器地址操作。进一步通过modetest工具可以列出所有DRM资源modetest -M rockchip -p输出里会看到Connector、CRTC、Plane三类角色的明细。拿到的信息越清楚后面写DRM代码就越省事。2.2 硬件图层、CRTC与连接器三者的关系为了便于理解可以做一个类比CRTC就是“显示器肚子里负责逐行扫描的引擎”它有固定的时序和分辨率Plane是一层层“透明胶片”能被独立地映射到CRTC上Connector则是“物理接口”本身负责给屏幕供电、发送数据、协商EDID。RK3588的VOP对于Plane数量的支持非常大方一个CRTC上常常有多个硬件层可以用它们分别显示视频帧、UI、光标等不同内容。这意味着在RK3588上LVGL完全可以跑在一个独立Plane上面甚至可以把不同内容的刷新率分开控制——这一点在后文的性能优化里会详细讲。如果只是把LVGL当成一个普通窗口系统来画那就浪费了这套硬件能力。正确的思路是把LVGL的最终渲染结果放到一个独立Plane上交给VOP硬件去合成。这样即使UI频繁刷新也不至于干扰背景视频层。2.3 确认当前内核态的显示后端在命令行下执行cat /sys/class/graphics/fb0/name如果返回drmfb之类的内容说明fb0是DRM模拟出来的如果这个节点根本不存在说明当前内核没开FrameBuffer模拟。这时老办法就会直接失效。检查内核配置可以确认zcat /proc/config.gz | grep -i -E DRM_FBDEV_EMULATION|FB_ROCKCHIP|DRM_ROCKCHIP如果DRM_FBDEV_EMULATIONy那么一般可以在/dev/fb0上看到设备如果完全没有可以有两条路一是改内核配置重新编译二是干脆走上层DRM路线。既然标题就是“从FrameBuffer到DRM驱动优化”那我的结论很明确不要浪费时间去容器里各种加载fbdev模块直接切DRM才是正途。提示RK3588上如果还跑着X11或者Wayland合成器它们会抢占DRM master权限。后面DRM程序启动时如果不能拿到master很多操作会被拒绝这和普通Linux程序的权限模型完全不一样。3. FrameBuffer后端移植先把界面跑起来DRM虽然强大但上手门槛确实比一份代码写死显存地址要高不少。所以我的建议是不要把路线一次性跨越得太大先保留一个完全能跑通的FrameBuffer后端把LVGL调通、确认GUI逻辑没问题再逐步替换显示后端。这样每一步都处于可验证的状态排查问题会轻松很多。3.1 LVGL源码组织与工程接入LVGL 8.2源码结构其实很简单核心代码在src目录用户配置在lv_conf.h。特别要注意的是LVGL 8.2开始对lv_conf.h的处理方式是要么定义LV_CONF_SKIP要么保证头文件能被找得到。我的做法是在lv_conf.h里把LV_COLOR_DEPTH设为16或32这需要和实际屏幕像素格式严格对应。工程接入步骤可以总结如下把lvgl源码目录加入CMake编译目标。定义宏LV_LVGL_H_INCLUDE_SIMPLE让#include lvgl.h可以被正常解析。自己写一个平台层源码文件里面实现LVGL要求的显示驱动、输入驱动和时间基准。具体到CMake一个最小化但可用的做法是set(LVGL_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/lvgl) file(GLOB LVGL_SRCS ${LVGL_ROOT}/src/*.c ${LVGL_ROOT}/src/**/*.c) add_executable(lvgl_app main.c platform/display.c platform/input.c ${LVGL_SRCS}) target_include_directories(lvgl_app PRIVATE ${LVGL_ROOT} ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(lvgl_app PRIVATE LV_LVGL_H_INCLUDE_SIMPLE)真正的业务逻辑都在main循环里while(1) { lv_timer_handler(); usleep(5 * 1000); }3.2 monitor函数LVGL刷新机制的核心LVGL不直接操作屏幕像素它有一套自己的内部缓冲机制。简单来说LVGL把所有控件渲染到一块由用户提供的draw buffer上渲染填充完这块缓冲后回调flush_cb把整块或者局部脏区推送到显示器。static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[1920 * 1080 / 10]; static lv_color_t buf2[1920 * 1080 / 10]; void my_display_init(void) { lv_disp_draw_buf_init(draw_buf, buf1, buf2, 1920 * 1080 / 10); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res 1920; disp_drv.ver_res 1080; disp_drv.flush_cb my_flush_cb; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); }在flush_cb里传统FrameBuffer做法就是直接把draw buffer用memcpy拷到fb0的映射地址。void my_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t w area-x2 - area-x1 1; uint32_t h area-y2 - area-y1 1; uint32_t dst_offset area-y1 * fb_fix.line_length area-x1 * bytes_per_pixel; uint8_t *dst fb_mem dst_offset; for (uint32_t y 0; y h; y) { memcpy(dst, color_p, w * bytes_per_pixel); dst fb_fix.line_length; color_p w; } lv_disp_flush_ready(drv); }注意不要把area忽略了很多初学移植的人直接拿整屏buffer去拷贝虽然能显示但效率极差违背了LVGL局部刷新的设计初衷。3.3 输入设备接入输入部分我推荐用event设备接口RK3588开发板上的触摸屏通常以/dev/input/eventX存在。LVGL 8.2里注册输入设备是这样的static lv_indev_drv_t indev_drv; lv_indev_drv_init(indev_drv); indev_drv.type LV_INDEV_TYPE_POINTER; indev_drv.read_cb my_touchpad_read; lv_indev_drv_register(indev_drv);read_cb里逻辑非常简单读一个input_event结构体判断事件类型更新坐标点和按下状态。注意有些屏幕的触摸坐标方向和屏幕本身的旋转方向不一致需要在软件里做坐标映射这一点后面单独说。3.4 跑通后的效果与FrameBuffer的三个硬伤用FrameBuffer方式跑通LVGL并不难最初界面也确实能正常显示、能触摸、能切换页面。但一旦画面动起来比如做一个平滑的进度动画或者图表绘制帧率就很难看。这个阶段我做了profile发现主要瓶颈有三个整屏刷新时memcpy带宽消耗大。1080p、32位色深一次整屏拷贝接近8MB数据CPU L2 cache被折腾得够呛。没有垂直同步。fbdev无法精确等待VBlank画面撕裂是家常便饭尤其上下滚动时特别明显。无法利用硬件合成。所有的内容都挤在一层framebuffer里背景层、UI层、硬件光标都要自己通过软件去合并白白浪费了RK3588的VOP能力。这三个问题在“验证功能”阶段可以忍受一旦进入真实产品阶段绝对会被客户直接打回。所以下一步就得考虑绕开fbdev直接使用DRM接管显示路径。4. 切换到DRM直接跟显示控制器对话DRM比FrameBuffer多出来的是“资源协商”的概念。你不能像写全局变量一样直接写显存地址而是先通过libdrm申请buffer、配置CRTC、绑定Plane最后才显示。这个过程初看繁琐但每一步都让你对显示链路有更精确的控制。4.1 程序身份DRM master与leaseDRM设备节点是一个有状态的对象。如果你只是open(/dev/dri/card0)默认拿到的身份是render-only或者普通client很多配置类操作会被EACCES或EPERM拒绝。在命令行环境里如果当前终端是本地VT且没有其他进程占用master可以调用int fd open(/dev/dri/card0, O_RDWR); ioctl(fd, DRM_IOCTL_SET_MASTER, 0);如果板子上跑着weston或Xorg你需要先停掉它们否则拿不到master。这也是很多初学者遇到DRM方式黑屏时最常见的原因。4.2 分配显存dumb buffer、mmap与framebufferDRM编程的经典流程是先创建一个dumb bufferstruct drm_mode_create_dumb create {0}; create.width 1920; create.height 1080; create.bpp 32; ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, create);拿到create.handle之后还要向内核注册为framebufferuint32_t fb_id; int ret drmModeAddFB(fd, 1920, 1080, 24, 32, create.pitch, create.handle, fb_id);注册完成之后这个buffer才能绑定到Plane上用于扫描输出。但应用要往里面画东西还需要mmap到用户空间struct drm_mode_map_dumb map {0}; map.handle create.handle; ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, map); uint8_t *map_base mmap(NULL, create.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map.offset);画完之后还有一个容易被忽视的操作显存同步。尤其当buffer被GPU写或者被显示器异步读取时需要用DMA_BUF_IOCTL_SYNC告诉内核显存访问结束了否则能看到“画了一半”的怪画面。4.3 设置CRTC和Plane比想象中简单配置显示输出之前先获取connector和crtc资源drmModeRes *res drmModeGetResources(fd); drmModeConnector *conn drmModeGetConnector(fd, res-connectors[0]); drmModeEncoder *enc drmModeGetEncoder(fd, conn-encoder_id); int crtc_id enc-crtc_id;然后从connector上挑一个合适的mode分辨率要和LVGL的hor_res/ver_res保持一致。drmModeModeInfo *mode conn-modes[0]; drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, conn-connector_id, 1, mode);设置完CRTC底层VOP就开始持续扫描这块buffer并输出到屏幕。如果想走更精细的Plane路线则用drmModeSetPlanedrmModeSetPlane(fd, plane_id, crtc_id, fb_id, 0, 0, 0, 1920, 1080, 0, 0, 1920 16, 1080 16);这里的0, 0, 1920, 1080是目标显示区域在原buffer上的裁剪框。许多优化思路比如只让屏幕的某一个矩形区域显示LVGL就是从这四个参数下手的。4.4 把LVGL的flush_cb换掉从fb0到drmModeSetPlane到了这一节我们终于触及标题的核心。在DRM路径下LVGL的flush_cb不再做memcpy而是完成一次buffer轮转假设有两个dumb bufferbuf A和buf B。LVGL在draw buffer里绘制某一帧。flush时把draw buffer内容复制或混合到当前空闲的dumb buffer上。然后通过drmModePageFlip或drmModeSetPlane把显示源切到这个buffer。等待VBlank信号到来确认显示器已完成切换再释放buffer给下一帧使用。一个核心实现片段void drm_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { struct dumb_fb *cur get_free_fb(); memcpy_draw_buf_to_dumb(cur, area, color_p); int ret drmModePageFlip(drm_fd, crtc_id, cur-fb_id, DRM_MODE_PAGE_FLIP_EVENT, cur); if (ret) { lv_disp_flush_ready(drv); return; } // 等待flip事件回调 drmHandleEvent(drm_fd, drm_evctx); lv_disp_flush_ready(drv); }注意PageFlip是异步的需要drmHandleEvent来处理完成事件。开发者最常犯的错就是“PageFlip还没完成就开始写下一帧”导致画面闪烁和撕裂。处理方式也很简单维护一个flip_pending标志位只有等到完成事件才允许下一轮flush。这一步完成之后LVGL的显示后端已经从“写文件”变成“操作显示引擎”。画面整体会有一个质的提升尤其是滚动和动画场景不再出现奇怪的水平撕裂线。5. 性能优化双缓冲、VBlank同步与RGA硬件加速DRM只是把“通路”打通了真正让LVGL在RK3588上流畅如飞还需要在性能优化上下功夫。我这里给出几个实际项目中用过且有效的方向按收益从高到低排列。5.1 双缓冲与Page Flip为什么单缓冲会卡如果只有一个buffer那么你必须先画完再显示显示器在等待期间只能看到旧内容或者更糟——“画到一半的内容”。LVGL自身的draw buffer通常小于整屏分辨率如果直接让它一帧帧覆盖到显存帧率很容易掉到20fps以下。最好的方案是申请两块尺寸相同、格式相同的dumb buffer。当前显示的称为front buffer正在绘制的称为back buffer。PageFlip的本质就是“让显示引擎在下一个VBlank时刻把读Buffer的指针从front切换到back”。这个切换由硬件完成CPU不需要介入极大减少卡顿。我的实测数据很直观单缓冲memcpy方案在1080p下跑一个3D旋转立方体demo平均帧率18fpsCPU占用65%切到PageFlip双缓冲后帧率稳定在60fpsCPU占用降到22%。5.2 用VBlank同步避免撕裂PageFlip本身自带“等待VBlank”的能力。只要在drmModePageFlip时传DRM_MODE_PAGE_FLIP_EVENT并且正确读取drm事件硬件就会在安全时刻切换buffer。这里有一个常见误区很多人以为drmModePageFlip是同步的调用完马上就可以写buffer。实际上PageFlip只是提交了一个请求驱动会在下一个VBlank时真正执行。我的做法是给每个buffer加一个状态机enum fb_state { FB_FREE, FB_QUEUED, FB_DISPLAYING };只有FB_FREE状态的buffer才能被LVGL写入。当收到PageFlip完成事件时把上一块的FB_DISPLAYING改回FB_FREE。这样既不会踩到正在显示的显存也不会因为等待而浪费CPU空转。5.3 RGA 2D加速把CPU拷贝替换为硬件scaleRK3588内部有一个RGARockchip Graphic Acceleration模块专门做2D图形操作比如格式转换、缩放、旋转、blend。在LVGL场景里RGA最大的价值不是做UI渲染而是在从LVGL draw buffer到dumb buffer的数据搬运阶段把CPU释放出来。举个例子如果LVGL的draw buffer配置为RGB565而你的屏幕原生输入格式是ARGB8888那么CPU端就需要做一次像素格式转换240fps的转换能吃掉不少CPU。用RGA的话只需要设置src format为RGB565、dst format为ARGB8888然后提交一个blit任务rga_info_t src_info {0}; rga_info_t dst_info {0}; src_info.fd lvgl_buffer_fd; src_info.mmuFlag 1; src_info.rect {0, 0, width, height}; dst_info.fd dumb_buffer_fd; dst_info.mmuFlag 1; dst_info.rect {0, 0, width, height}; rga_set_fmt(src_info, RK_FORMAT_RGB_565, width, height); rga_set_fmt(dst_info, RK_FORMAT_ARGB_8888, width, height); ioctl(rga_fd, RGA_BLIT_SYNC, src_info, dst_info);虽然代码比memcpy复杂但收益非常高。原本拷贝加格式转换需要约8ms用RGA后基本能做到3ms以内而且CPU占用接近于0。这也是为什么RK3588这种带RGA的芯片比纯CPU平台更适合跑LVGL的原因。5.4 还能再挖的性能空间局部刷新、图层拆分与AFBC如果做完双缓冲和RGA帧率还是不够可以考虑LVGL和DRM的联调优化。局部刷新在LVGL中天然支持只要flush_cb区分传入area并只更新对应区域。DRM的dirty area机制可以把这个区域直接映射到drmModeSetPlane的裁剪窗口上让显示器只更新那块矩形区域。不过不是所有显示控制器都支持任意位置的局部刷新RK3588的VOP对Plane整体有效局部区域需要通过buffer内容变更完成所以这个优化要与自己的产品形态结合考虑。图层拆分是充分利用RK3588多Plane能力的思路。比如把背景图和UI内容分别放到两个Plane上背景基本不变只有UI层需要更新。由于硬件合成器会自动把它们叠加UI层刷新时功耗和带宽远低于单层全屏刷新。这个玩法的代价是LVGL代码里需要维护“背景绘制”和“前景绘制”两条路径两者互不影响。**AFBCArm Frame Buffer Compression**是一种帧缓冲压缩技术能有效降低外部内存带宽尤其在高分辨率高帧率场景下收益明显。RK3588对AFBC的支持比较完善但要求buffer分配时使用特殊modifierLVGL侧不能直接通过普通dumb buffer申请而要借助DRM的drmModeAddFB2WithModifiers或者在DMA-BUF分配阶段明确modifier信息。这套方案能进一步降低刷新成本但代码复杂度又上一个台阶适合对功耗和性能都有硬指标的产品。6. 踩坑实录从黑屏到花屏的完整排查链路这部分是我最想分享的内容也是很多东西没法直接从文档里学到的。我把整个移植过程中遇到过的典型问题按“现象-排查-解决方法”梳理一遍希望对读者有实际帮助。6.1 黑屏一类dumb buffer不能直接写DRM初始化完成后第一次执行drmModeSetCrtc时屏幕无输出这看起来像是配置不对但随后我发现问题不在crtc配置而是在于dumb buffer创建后没有正确做mmap。值得一提的有两个细节。第一个是drm_mode_create_dumb结构里的bpp字段传32并不代表像素格式就是ARGB8888它只是一个“bits per pixel”的请求值真正的格式由后续drmModeAddFB的depth和bpp参数决定。第二个是drm_mode_map_dumb返回的offset需要配合设备的DRM_IOCTL_MODE_MAP_DUMB拿到用户空间可用地址。如果在mmap时offset传错后续写入的全部是无效地址屏幕自然没有输出。排查方法也很朴素往mmap后用memset(map_base, 0xFF, size)刷一遍全白如果屏幕仍然是黑的问题基本就锁定在buffer验证环节。6.2 花屏一类格式、pitch对不上画面能显示了但颜色完全不对或者图像出现斜条纹大概率是像素格式和pitch每行字节数出了问题。RGB565的数据按ARGB8888解释会有色偏而pitch因为有内存对齐要求常常不等于width * bpp / 8。RK3588的VOP对pitch有严格的对齐要求常见的是64字节或256字节对齐。比如1920宽度、32位色深常规计算是7680字节但实际pitch可能被align到7808之类的值。如果你在拷贝时直接用width * bpp / 8算行跨度那么每行末尾都会偏几个字节画面就会出现“阶梯状”花屏。正确做法是始终读取dumb buffer创建后返回的create.pitch或者使用drmModeGetFB获取系统实际确定的pitch值。LVGL侧传入flush_cb的area要严格按这个pitch计算偏移不要自己重新推算。6.3 撕裂一类PageFlip事件没处理双缓冲后依然有撕裂我把问题定位到没有正确等待PageFlip完成事件。DRM的PageFlip支持事件通知如果设置了DRM_MODE_PAGE_FLIP_EVENT需要调用drmHandleEvent来处理异步事件。很多示例代码把drmHandleEvent放在一个单独的线程里阻塞等待但LVGL的主循环本身就有自己的帧率控制所以最简单可靠的方式是在flush_cb里同步调用drmEventContext evctx {0}; evctx.version DRM_EVENT_CONTEXT_VERSION; evctx.page_flip_handler flip_handler; drmHandleEvent(fd, evctx);这样做的好处是“写buffer→等待切换完成→继续下一帧”的顺序非常直观不会出现两帧同时修改同一块buffer的情况。缺点是会阻塞LVGL主循环但实测在60fps内完全没有问题。6.4 输入坐标错乱触摸坐标需要旋转映射RGB屏幕方向和触摸面板方向不一致时最典型的现象是手指往上滑界面却往下走或者点击左上角焦点出现在右下角。LVGL本身不负责触摸坐标变换这个工作必须在read_cb里完成。我的做法是先查看触摸设备的event上报分辨率范围getevent -l /dev/input/event2或者读取/sys/class/input/event2/device/device_properties。拿到ABS_X和ABS_Y的最大值后根据LVGL的屏幕尺寸做一个4象限映射int32_t x (int32_t)(value_x * LVGL_HOR_RES / touch_max_x); int32_t y (int32_t)(value_y * LVGL_VER_RES / touch_max_y); if (need_swap_xy) { ... } if (need_reverse_x) { ... }关键点是这个映射必须从“触摸面板实际安装方向”出发而不是从RGB屏幕的默认方向出发。市面上很多开发板的屏幕和触摸排线方向不一致每块板子都要实测一遍再确定映射方案。建议把触摸方向做成一个配置项后期换屏不用改代码。6.5 一个容易忽略的点DRM master与调试工具如果你在SSH终端里运行DRM程序而不是在本地终端很可能遇到DRM_IOCTL_SET_MASTER被拒绝的问题。这是因为DRM master只在当前会话的VT上有意义SSH会话不属于同一个显示会话。调试阶段建议直接在板子本地终端运行或者用openvt把程序挂到本地VT上。同时准备好modetest、drm_info这些工具排查资源冲突比写代码还重要。再补充一个实战经验调试DRM程序时建议把weston或X11在启动时禁用否则等你在终端里启动LVGL程序会发现CRTC资源已经被占用而报错信息往往让人摸不着头脑容易误判成“LVGL移植失败”。7. 最后的优化心得与扩展空间在RK3588上完整走完FrameBuffer到DRM这条路线后我对LVGL的取舍有了更深的体会。LVGL确实不是一个追求极致渲染效果的GUI框架但它和DRM这套Linux标准显示机制结合后完全能够支撑起一个高质量、低延迟、强交互的工业或商业显示产品。只要释放掉“只能memcpy”的惯性思维它还有很大的性能潜力可以挖掘。如果你的产品对启动速度也有要求其实还有一个小技巧值得尝试U-Boot阶段就可以初始化显示并且把LVGL的启动画面先放进一个保留的dumb buffer里Kernel起来后再无缝接管DRM资源。这样能做到“按电源键到看到界面只需要几百毫秒”的效果这在很多工控和车载场景就是核心竞争力。我自己的经验是DRM这条新路径带来的不只是性能提升更是对整个显示链路控制力的提升。遇到问题的时候你能直接通过modetest、drm_info去核验每一个Plane、每一个Buffer的状态而不像以前在fbdev里“只闻其声不见其人”。这种可控性才是嵌入式GUI开发的长期价值所在。