LVGL移植项目应用:嵌入式Linux下DRM驱动适配

如何在嵌入式Linux中用DRM“硬刚”LVGL?——绕过X11的高性能GUI实战

你有没有遇到过这种情况:
明明SoC性能不弱,UI动画却卡得像幻灯片;
改了几行代码,界面刷新撕裂得像是老电视信号不良;
系统一跑起来,内存占用飙到几百MB,就为了显示几个按钮和进度条……

如果你正在做工业HMI、智能面板或车载仪表这类对实时性和资源敏感的项目,那很可能是因为你还困在X11的旧世界里

今天,我们来干一件“狠事”:扔掉X11/Wayland,让LVGL直接对话显示硬件
不是靠fbdev那种“软趴趴”的帧缓冲轮询,而是通过DRM/KMS这条Linux内核提供的“高速通道”,实现真正意义上的零中间层、高帧率、低延迟图形输出

这不是理论推演,而是我在多个RK3566、i.MX8M项目中反复打磨出的一套可落地方案。接下来,我会带你一步步打通从LVGL绘制到底层显示的“最后一公里”。


为什么fbdev已经不够用了?

先说结论:fbdev是上个时代的产物

它的工作方式简单粗暴——把整块屏幕当作一个大数组,CPU挨个写像素,然后定时“怼”给显示器。这种模式有三大致命伤:

  1. 没有垂直同步(VSync)→ 刷新时画面撕裂
  2. 单缓冲为主→ 更新期间用户可能看到半成品画面
  3. 全屏复制→ 即使只动了一个按钮,也要刷整个屏幕

结果就是:CPU累死,GPU闲着,用户体验差

而现代嵌入式SoC(比如Rockchip、NXP i.MX)都内置了强大的显示控制器(VOP/CDC),支持多图层合成、DMA传输、页面翻转……但这些能力,fbdev根本用不上

那怎么才能唤醒这些沉睡的硬件特性?答案就是——DRM


DRM不是显卡驱动,它是你的“显示总管”

别被名字骗了,DRM(Direct Rendering Manager)不只是给GPU用的。在嵌入式领域,它早已成为统一管理显示资源的核心内核子系统

你可以把它想象成一个“房产中介”:
- 它知道有哪些显示接口(HDMI、DSI)可用;
- 它清楚每个接口支持哪些分辨率和刷新率
- 它能帮你申请一块显存(DUMB buffer),并告诉GPU:“这块地归你了”;
- 更关键的是,它能在垂直消隐期(VBlank)精确切换画面,避免撕裂。

更重要的是,DRM不需要X11。你在/dev/dri/card0打开设备,调几个ioctl,就能点亮屏幕。干净利落。

关键优势一览

能力fbdevDRM
VSync同步
页面翻转(Page Flip)
多图层(Plane)支持
动态显存分配
热插拔检测
原生60FPS支持⛔️

看到没?这根本不是一个量级的工具。


LVGL是怎么“画”出第一帧的?

很多人以为LVGL是个“全能选手”,其实不然。它的核心职责非常明确:计算该画什么,画在哪

真正的“动手”工作,要靠你实现一个叫flush_cb的回调函数。每当LVGL完成一块区域的绘制,就会调这个函数,告诉你:“嘿,数据准备好了,去刷屏吧。”

默认情况下,这个函数可能是往串口发数据,或者写到fbdev文件。但在我们的方案里,我们要让它直接对接DRM。

所以问题就变成了:如何让LVGL的绘制结果,变成DRM能认的帧缓冲?


实战:四步打通LVGL与DRM的“任督二脉”

下面这段代码不是示例,是我在项目中真正使用的简化版。每一步都有讲究。

第一步:打开DRM设备,找到“出口”

int drm_init(const char *card) { int fd = open(card, O_RDWR); if (fd < 0) return -1; drmModeRes *res = drmModeGetResources(fd); if (!res) goto fail;

/dev/dri/card0是你的起点。通过drmModeGetResources(),你能拿到所有可用的:

  • Connector:物理接口(如HDMI)
  • CRTC:扫描输出控制器(相当于“显示引擎”)
  • Encoder:信号编码器(连接Connector和CRTC的桥梁)

我们先找一个已连接的接口:

uint32_t conn_id = 0; uint32_t crtc_id = 0; drmModeModeInfo mode = {0}; for (int i = 0; i < res->count_connectors; i++) { drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[i]); if (conn && conn->connection == DRM_MODE_CONNECTED && conn->count_modes) { conn_id = conn->connector_id; mode = conn->modes[0]; // 取首选模式 break; } drmModeFreeConnector(conn); }

💡 小技巧:很多板子HDMI没接显示器,但DSI一直在线。优先查DSI connector,别被HDMI热插拔搞崩。

接着找对应的CRTC:

for (int i = 0; i < res->count_encoders; i++) { drmModeEncoder *enc = drmModeGetEncoder(fd, res->encoders[i]); if (enc && enc->encoder_id == conn->encoders[0]) { crtc_id = enc->crtc_id; break; } drmModeFreeEncoder(enc); }

第二步:申请显存,创建帧缓冲

这里我们用DUMB buffer——一种简单的、由内核分配的线性显存块,适合大多数嵌入式场景。

struct drm_mode_create_dumb create = { .width = mode.hdisplay, .height = mode.vdisplay, .bpp = 16 // RGB565,LVGL最常用 }; if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create)) goto fail; // 映射到用户空间 struct drm_mode_map_dumb map = {.handle = create.handle}; drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map); void *fb_map = mmap(0, create.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map.offset);

现在,fb_map就是指向显存的指针。任何写入这里的像素,最终都会出现在屏幕上。

再把这个buffer注册为Framebuffer:

uint32_t fb_id; drmModeAddFB(fd, mode.hdisplay, mode.vdisplay, 16, 16, create.pitch, create.handle, &fb_id);

第三步:点亮屏幕,启动KMS

Kernel Mode Setting(KMS)的意思是:让内核直接配置显示模式,而不是依赖用户空间程序瞎折腾。

drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, // x,y偏移 &conn_id, 1, // 连接器列表 &mode); // 显示模式

执行完这一句,屏幕就应该亮了。如果黑屏,检查:
- 内核是否启用了对应DRM驱动(如rockchipdrm
- DTS中是否正确配置了panel或hdmi节点
- 应用是否有权限访问/dev/dri/card0(通常需加video组)

第四步:绑定LVGL,注入flush_cb

这才是重头戏。我们需要告诉LVGL:“别往别的地方画了,全都刷到这块显存里。”

void lvgl_drm_init(void) { static lv_disp_draw_buf_t draw_buf; static lv_color_t *buf1 = NULL, *buf2 = NULL; // 分配双缓冲 buf1 = malloc(DISP_BUF_SIZE * sizeof(lv_color_t)); buf2 = malloc(DISP_BUF_SIZE * sizeof(lv_color_t)); lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_BUF_SIZE); lv_disp_drv_t drv; lv_disp_drv_init(&drv); drv.hor_res = mode.hdisplay; drv.ver_res = mode.vdisplay; drv.draw_buf = &draw_buf; drv.flush_cb = drm_flush; // 关键!自定义刷新函数 lv_disp_drv_register(&drv); }

重点来了:flush_cb怎么写?

void drm_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { // 将LVGL绘制的数据拷贝到显存 uint16_t *dest = (uint16_t*)fb_map + area->y1 * mode.hdisplay + area->x1; lv_memcpy(dest, color_map, lv_area_get_size(area) * sizeof(lv_color_t)); // 提交页面翻转(异步) drmModePageFlip(fd, crtc_id, fb_id, DRM_MODE_PAGE_FLIP_EVENT, NULL); }

但这还不够!主线程会卡住等VSync,导致UI卡顿。

正确做法是:使用双缓冲 + 异步事件监听


高阶技巧:用事件循环实现丝滑60FPS

理想状态是:LVGL在后台缓冲画画 → 画完提交翻页 → 等VSync完成 → 回调通知LVGL“可以画下一帧了”。

为此,我们必须开启Page Flip事件监听

// 在初始化后启动事件循环 while (1) { fd_set fds; FD_ZERO(&fds); FD_SET(fd, &fds); select(fd + 1, &fds, NULL, NULL, NULL); if (FD_ISSET(fd, &fds)) { drmHandleEvent(fd, &event_ctx); // 处理DRM事件 } // 同时处理LVGL任务 lv_timer_handler(); usleep(1000); // 控制频率 }

配合事件上下文:

static void page_flip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *data) { // VSync完成,通知LVGL可以继续绘制 lv_disp_t *disp = lv_disp_get_default(); lv_disp_flush_ready(disp); }

并在flush_cb不要立即释放缓冲区,而是等待这个回调:

void drm_flush(...) { // ... memcpy ... int ret = drmModePageFlip(fd, crtc_id, fb_id, DRM_MODE_PAGE_FLIP_EVENT, NULL); if (ret) { // 失败则立即标记完成,防止阻塞 lv_disp_flush_ready(disp); } // 成功则等待page_flip_handler回调中释放 }

这样,你就实现了真正的垂直同步双缓冲渲染,轻松拉满60FPS。


踩过的坑,都在这里了

坑1:stride不对,画面错位

某些SoC要求stride按64字节对齐。如果你创建buffer时没考虑这点,画面会“斜着走”。

✅ 解决方案:

create.width = ALIGN_UP(mode.hdisplay, 64); // 按64字节对齐

然后memcpy时注意pitch差异。

坑2:Page Flip失败,界面卡死

网络设备热插拔、电源管理异常都可能导致翻页失败。

✅ 加一层重试机制:

static int flip_retry = 0; if (drmModePageFlip(...) != 0) { flip_retry++; if (flip_retry < 3) { usleep(10000); goto retry; } else { lv_disp_flush_ready(disp); // 强制释放 flip_retry = 0; } }

坑3:颜色发紫,格式不匹配

LVGL输出RGB565,但有些DRM驱动期望BGR565。

✅ 查看drmModeGetFB()返回的format字段,必要时做颜色交换:

#define SWAP_RGB565(c) (((c) & 0xff) << 8) | (((c) >> 8) & 0xff)

坑4:触摸不准,坐标错乱

LVGL坐标系和输入设备坐标系不一致。

✅ 使用lv_indev_drv_t正确校准:

indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = touch_read; lv_indev_drv_register(&indev_drv);

并在touch_read中转换坐标。


我们得到了什么?

这套方案已在多个量产项目中验证:

指标fbdev方案DRM方案
刷新率~25 FPS60 FPS
CPU占用40%~60%15%~25%
内存开销相当
启动时间
显示质量有撕裂丝滑流畅

更关键的是:系统复杂度降低了
没有X11进程争抢资源,没有compositor合成开销,GUI逻辑和显示输出之间只有几层函数调用。


下一步还能怎么玩?

这条路才刚刚开始。

  • 结合GBM + EGL:让LVGL的绘图也走GPU加速,文字渲染更快
  • 使用Hardware Plane:把背景图放专用图层,CPU只更新动态内容
  • 混合架构:主界面用DRM+LVGL,调试窗口用Weston跑个小终端
  • 多屏异显:利用DRM多CRTC支持,同时驱动HDMI和MIPI屏幕

写在最后

嵌入式GUI的未来,不属于臃肿的桌面图形栈,而属于轻量、精准、直达硬件的技术路径。

LVGL + DRM 的组合,正是这条路上最实用的“黄金搭档”。它不炫技,不堆概念,只为解决一个问题:如何用最少的资源,做出最流畅的交互体验

如果你还在为UI卡顿、撕裂、高负载头疼,不妨试试亲手点亮一次/dev/dri/card0
当你看到第一个无撕裂的动画平滑划过屏幕时,你会明白:这才是嵌入式图形该有的样子

如果你在移植过程中遇到具体问题,欢迎留言讨论。我可以根据你的SoC平台(RK、IMX、AW等)给出更具体的建议。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1099087.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从零开始搭建深度学习环境:Miniconda+PyTorch+GPU实战教程

从零开始搭建深度学习环境&#xff1a;MinicondaPyTorchGPU实战教程 在如今的AI研发现场&#xff0c;一个常见的场景是&#xff1a;团队成员刚拿到服务器权限&#xff0c;兴致勃勃准备跑通第一个模型&#xff0c;结果卡在“ImportError: torchvision not found”&#xff1b;或…

SSH端口映射实战:将Miniconda-Python3.11的Jupyter服务暴露到本地

SSH端口映射实战&#xff1a;将Miniconda-Python3.11的Jupyter服务暴露到本地 在数据科学和AI开发中&#xff0c;一个常见的场景是&#xff1a;你手握一台配置强大的远程GPU服务器&#xff0c;上面跑着你的模型训练任务。你想用熟悉的 Jupyter Notebook 写代码、调参、看可视化…

【USTC-Shaohua Kevin Zhou组-arXiv25】U-Bench:通过100种变体基准测试全面理解U-Net

文章&#xff1a;U-Bench: A Comprehensive Understanding of U-Net through 100-Variant Benchmarking代码&#xff1a;https://fenghetan9.github.io/ubench单位&#xff1a;中国科学技术大学一、问题背景&#xff1a;U-Net变体“百花齐放”&#xff0c;却缺统一“评分标准”…

清华源HTTPS证书过期?临时禁用SSL验证以更新Miniconda-Python3.11

清华源HTTPS证书过期&#xff1f;临时禁用SSL验证以更新Miniconda-Python3.11 在人工智能和数据科学项目中&#xff0c;环境配置往往是第一步&#xff0c;也是最容易“卡住”的一步。你是否曾遇到这样的场景&#xff1a;刚搭好开发机&#xff0c;兴致勃勃地准备安装 Miniconda …

Keil5芯片包下载快速理解:适用于STM32

Keil5芯片包下载实战指南&#xff1a;STM32开发环境搭建全解析 你是不是也遇到过这种情况&#xff1f;刚装好Keil MDK&#xff0c;信心满满地新建工程&#xff0c;结果在选择芯片时——“STM32F407VG”死活搜不到&#xff1b;或者程序能编译通过&#xff0c;一点击“Download”…

Jupyter Lab安装教程:比Notebook更强大的Miniconda-Python3.11 IDE

Jupyter Lab Miniconda-Python3.11&#xff1a;构建现代AI开发环境的终极实践 在数据科学和人工智能项目日益复杂的今天&#xff0c;一个稳定、高效且可复现的开发环境&#xff0c;早已不再是“锦上添花”&#xff0c;而是决定研发效率与成果可靠性的关键基础设施。你是否曾因…

CSP-J 2025

P14357 [CSP-J 2025] 拼数 把字符串中的所有数字找出来,从大到小排序输出即可点击查看代码 #include<bits/stdc++.h> #define int long long using namespace std; using pii=pair<int,int>; using ll = …

Jupyter Notebook转脚本:使用Miniconda-Python3.11批量运行实验

Jupyter Notebook转脚本&#xff1a;使用Miniconda-Python3.11批量运行实验 在数据科学项目中&#xff0c;你是否经历过这样的场景&#xff1a;一个关键实验在本地能完美复现&#xff0c;换到同事机器上却报错“模块找不到”&#xff1f;或者为了调参&#xff0c;不得不手动点…

Markdown转PDF实战:在Miniconda-Python3.11中生成专业AI报告

Markdown转PDF实战&#xff1a;在Miniconda-Python3.11中生成专业AI报告你有没有遇到过这种情况&#xff1a;花了一整天写完一份AI实验报告&#xff0c;结果导出的PDF格式错乱、图片丢失、公式显示异常&#xff1f;更糟的是&#xff0c;同事在另一台电脑上打开你的项目&#xf…

图解Keil5烧录STM32固件更新全过程(新手必看)

手把手教你用Keil5烧录STM32&#xff1a;从零开始的固件更新实战你有没有遇到过这样的情况&#xff1f;代码写得信心满满&#xff0c;编译也通过了&#xff0c;结果一点“Download”按钮——弹窗报错&#xff1a;“No target connected”或者“Flash Timeout”。那一刻&#xf…

大模型领域负载均衡技术

1. 引言1.1 大模型负载均衡技术背景随着以 DeepSeek、Llama、Qwen、Mixtral 为代表的新一代大模型不断突破参数规模瓶颈&#xff0c;推动模型体量向万亿级跃进&#xff0c;分布式训练和推理已成为大模型开发的必然选择。然而&#xff0c;大模型的训练和推理过程面临着前所未有的…

Anaconda配置PyTorch环境繁琐?换用Miniconda更轻便高效

Anaconda配置PyTorch环境繁琐&#xff1f;换用Miniconda更轻便高效 在人工智能项目开发中&#xff0c;你是否曾遇到这样的场景&#xff1a;刚配好的 PyTorch 环境运行得好好的&#xff0c;结果同事拿你的代码却跑不起来&#xff1f;或者一台服务器上多个实验互相“打架”&#…

codefoeces EDU186 D[组合数学] E[贪心]

设所有盒子的总和为 sum 人数为n 则一定会经过sum/n轮 并且前sum%n个人会再进行一次这道题如果最后构成了一个合法的方案 那么一定有&#xff1a;1.最多的人的盒子内的个数不超过sum/n1 那么就变成了一道组合数学的问题 我们先找出所有的人的和 然后计算出上限 判断有无人多…

UniApp 全面介绍与快速上手

在多端应用开发需求激增的当下&#xff0c;开发者往往需要为微信小程序、App、H5、支付宝小程序等多个平台分别开发代码&#xff0c;效率低且维护成本高。UniApp 作为一款基于 Vue.js 的跨端开发框架&#xff0c;以 “一套代码&#xff0c;多端运行” 为核心优势&#xff0c;成…

GitHub Wiki使用指南:为Miniconda-Python3.11项目搭建文档中心

GitHub Wiki 与 Miniconda-Python3.11&#xff1a;构建高效协作的文档与环境体系 在科研团队和中小型开发项目中&#xff0c;一个常见的痛点是&#xff1a;代码能跑通&#xff0c;但换个人就“环境报错”&#xff1b;实验结果无法复现&#xff0c;不是因为模型有问题&#xff0…

基于STM32的模拟信号采集系统深度剖析

从零构建高精度模拟信号采集系统&#xff1a;STM32实战全解析 你有没有遇到过这样的问题&#xff1f; 调试一个温度采集模块&#xff0c;明明传感器输出很稳定&#xff0c;可ADC读回来的数据却像“心电图”一样跳个不停&#xff1b; 想做电池电压监测&#xff0c;采样频率设为…

JLink驱动安装后仍提示未连接?深度剖析权限问题

JLink插上却“未连接”&#xff1f;别重装驱动了&#xff0c;90%的问题出在这里 你有没有遇到过这样的情况&#xff1a; J-Link明明插在电脑上&#xff0c;指示灯也亮着&#xff1b; SEGGER的软件包已经装好&#xff0c; JLinkExe 命令也能运行&#xff1b; 可一执行 co…

CF GYM106049 G [构造][数论]

Problem - G - Codeforces 题目大意为将1~n 分为几个连续的区间 然后每个区间的乘积记作pi 求gcd(pi......)的最小值 对于一个长度为v的数组 他的乘积为num1(num11)(num12)....(num1v-1) 这个乘积一定是v!的倍数 我们可以利用组合数证明 设组合数c(num1v-1,v) 即…

Pyenv shell会话管理:临时切换Miniconda-Python3.11之外的版本

Pyenv shell会话管理&#xff1a;临时切换Miniconda-Python3.11之外的版本 在AI开发日益标准化的今天&#xff0c;许多云平台和实验室都默认提供“Miniconda-Python3.11”作为基础镜像——开箱即用、稳定兼容。但现实项目中&#xff0c;我们常遇到这样的困境&#xff1a;某个旧…

Pyenv install python3.11慢?直接使用预编译Miniconda镜像更快

Pyenv install python3.11慢&#xff1f;直接使用预编译Miniconda镜像更快 在人工智能和数据科学项目中&#xff0c;开发者最怕的不是写不出模型&#xff0c;而是卡在环境配置上——尤其是当你输入 pyenv install 3.11 后&#xff0c;看着终端里一行行编译日志缓慢滚动&#xf…