☰
ZYNQMP平台RGB LCD显示驱动移植:从framebuffer到DRM/KMS实战
2026/10/5 9:51:38 网站建设 项目流程

干嵌入式显示这块的,应该都经历过那个阶段:板子启动到一半,屏幕还是黑的,手头一块 800x480 LCD 已经用杜邦线连好,却在“用什么驱动方式点亮它”这件事上来回折腾。我这次就是在 ZYNQMP 平台上从头把一块 7 寸 RGB LCD 屏点亮,从传统 framebuffer 路子一路迁到 DRM/KMS 框架,中间踩了不少坑,也把 Linux 显示子系统的脉络理清了。这篇文章就把整个移植过程、设备树写法、内核配置和排障经验完整记录下来,给正在做类似驱动的朋友一个可以直接参考的路线图。

先说结论:如果你的应用只是开机显示一张静态图,那 framebuffer 方案确实够用;但如果后面要接 Qt、Weston 或者做视频叠加,那最好还是直接上 DRM。这次项目里我最终跑通了 DRM 路径,并且保留了对 /dev/fb0 的兼容,整个过程非常值得复盘一遍。

1. 显示驱动路径怎么选:framebuffer 和 DRM 的定位差异

1.1 framebuffer 时代的经典玩法

Linux 早期的显示方案几乎全靠 fbdev(framebuffer device),它在内核里抽象出一个简单的帧缓冲设备,用户态拿到 /dev/fb0 之后,可以用 mmap 直接映射显存,往里面填充像素数据。配合 fbset 这类工具设置分辨率、像素格式和时序,再写一个几十行的 C 程序,就能把一张 BMP 图片刷到屏幕上。

这种方式为什么能火那么久?原因就是“简单”。它把复杂的显示控制器细节封装成一组固定的 ioctl 和内存映射接口,应用层不关心你是 RGB565 还是 XRGB8888,也不关心后端接的是 TFT 屏还是 HDMI 转接芯片。对很多 MCU 级别或低性能嵌入式平台来说,framebuffer 是最快能出效果的手段。

但它的天花板也很明显。fbdev 驱动的模型基本是“一个屏幕一个 buffer”,缺少对多图层、多输出的支持。你在 fb0 上画一个窗口,系统不会帮你做合成;想要 vsync 同步,很多驱动实现得也相当粗糙;更不用说热插拔、EDID 解析、多重平面 alpha 混合这些现代显示需求。到了 ZYNQMP 这种带硬核 GPU、DisplayPort、多路输出的 SoC 上,再用纯 fbdev 方案会非常吃力。因此内核社区早就把显示子系统重心移到了 DRM 上。

1.2 DRM/KMS 到底带来了什么

DRM 是 Linux 内核里的 Direct Rendering Manager,它分两大部分:一部分是 KMS(Kernel Mode Setting),负责显示模式、连接器、编码器、CRTC 的管理;另一部分是 GEM 和相关框架,负责显存分配和渲染缓冲管理。和 fbdev 最大的区别在于,DRM 把显示链路拆成了四个核心对象:CRTC、Encoder、Connector、Plane。

CRTC 可以简单理解成显示控制器,负责把一块内存buffer扫描输出;Encoder 负责把 CRTC 输出的信号编码成具体接口格式,比如 RGB 并行信号、LVDS、DSI 或者 HDMI;Connector 负责描述物理接口上接了什么屏幕,是否连接、分辨率是多少;Plane 则是图层,DRM 支持多个 Plane 做硬件合成,UI 层和视频层可以各自独立更新。

这套模型的优势在调试时特别明显。用 modetest 工具跑一下,系统里有哪些 CRTC、哪些 Encoder、哪些 Connector,支持哪些分辨率和像素格式,一目了然。用户态程序通过 atomic commit 把 Plane、CRTC、Connector 绑定关系一次性提交给内核,内核负责在下一个 vblank 周期完成切换,几乎没有撕裂问题。

1.3 这次项目为什么值得做两套路径

我这次的目标平台是 ZYNQMP,PS 侧有四个 Cortex-A53 核,运行 Linux 6.x 内核;PL 侧则是 FPGA 逻辑,可以用来搭建 DMA 和视频时序输出通路。为了把 800x480 RGB LCD 屏点亮,我分别在 framebuffer 和 DRM 两条路径上做了移植验证。

一个有意思的插曲是,刚开始很多人听到“linux drm”第一反应是音频设备里的“动态范围激励器”,尤其搜索热度高的时候,“drm数字激励器和dam数字激励器的区别”这类词经常被关联进来。其实在嵌入式显示领域,DRM 和音频激励器完全不是一回事,咱们这里说的 DRM 就是内核显示管理器。另外有人容易把 TM1622 这类段码 LCD 驱动芯片和 TFT RGB 接口屏搞混,TM1622 本身是 MCU 用的段码显示屏专用驱动,跟我们要处理的 800x480 TFT 屏走的是完全不同的数据通路,选型的时候千万别弄错。

2. 硬件通路和 LCD 屏参数的那些事

2.1 ZYNQMP 里显示通路是怎么搭出来的

ZYNQMP 和普通 ARM SoC 不太一样,它的显示链路通常不是“上电就能用”的固定硬件,而是需要在 Vivado 里用 PL 逻辑搭出来的。拿我这次 800x480 RGB 屏来说,典型通路是:DDR 里的帧缓冲数据,通过 AXI VDMA(Video Direct Memory Access)搬运出来,送进 Video Timing Controller(VTC)生成行场同步信号,再经过 AXI4-Stream to Video Out 模块把像素流和时序同步信号打包成 RGB 接口信号,最终引脚接到 LCD 的排线上。

Vivado 工程里主要就是三颗 IP:AXI VDMA、Video Timing Controller、AXI4-Stream to Video Out。VDMA 通过 AXI4-Lite 接口配置寄存器,通过 AXI4 主接口从 DDR 读取帧数据;视频输出侧则是一个 AXI4-Stream 接口,数据宽度配成 24bit 或 16bit 都可以,取决于你的 LCD 屏是 RGB888 还是 RGB565。

硬件搭建完成后,PL 侧会暴露一组 AXI-Lite 寄存器和 DMA 中断,这些要在设备树里描述清楚,Linux 驱动才能找到它们。所以严格来说,ZYNQMP 上做 LCD 驱动移植,一半功夫在硬件逻辑,一半功夫在软件设备树上。

2.2 800x480 屏的关键时序参数怎么确认

拿到一块 LCD 模组,首先别急着连线,先翻 datasheet 看 timing。很多新手在这里栽跟头:线都接对了,屏幕一亮就花屏,十有八九是时序参数写错了。

一块典型的 7 寸 800x480 RGB 屏,关键参数大概是这么一组:水平方向 800 个有效像素,水平前沿 hfront-porch 通常 40 个像素时钟,水平同步脉冲 hsync-len 48 个像素时钟,水平后沿 hback-porch 88 个像素时钟;垂直方向 480 行有效,垂直前沿 vfront-porch 13 行,垂直同步脉冲 vsync-len 3 行,垂直后沿 vback-porch 32 行。像素时钟大概在 33.3MHz 上下。

这些数字怎么理解?简单说,同步信号要在有效数据前后留出一段“空档期”,让 LCD 内部的驱动电路有足够时间准备。前沿、后沿和同步脉冲的宽度,加上有效像素数,才是完整的一整行扫描时间。我这里算过一笔账:每行是 800 + 40 + 48 + 88 = 976 个像素时钟,每帧是 480 + 13 + 3 + 32 = 528 行,60Hz 刷新率下需要的像素时钟就是 976 × 528 × 60 ≈ 30.9MHz,datasheet 给 33.3MHz 是正常的,因为实际同步参数可能略有差别,留有余量更稳妥。

2.3 VDMA 地址对齐和缓存一致性问题

VDMA 是这条显示链路里最容易出诡异 bug 的环节。它本质上就是一个 DMA 控制器,缺点在于 Linux 内核里的 framebuffer 内存地址必须经过正确的页表映射,保证虚拟地址和物理地址都能被 DMA 访问。ZYNQMP 的 Linux 下如果启用 IOMMU 或者使用 CMA 区域,地址转换要考虑一遍;如果直接让 VDMA 访问物理连续内存,还需要把设备树里相关节点配成 dma-coherent,避免 DMA 和 CPU 的缓存不一致导致画面出现随机花块。

我在调 VDMA 时最常用的验证方法是:先在裸机或者 U-Boot 阶段用一个最简单的循环把固定颜色写到测试 buffer,喂给 VDMA,看屏幕上能否出现纯色画面。这一关过了,说明 PL 逻辑和数据通路没问题,再看 Linux 驱动的配置,排查范围就小得多。

3. framebuffer 方案怎么跑通 800x480

3.1 内核配置和设备树要点

在 ZYNQMP 上要跑纯 framebuffer 方案,其实没有想象的那么“纯”。因为 Xilinx 的显示驱动基本都在 DRM 框架下,纯 fbdev 驱动反而很少见。但 Linux 内核提供了 DRM 的 fbdev 兼容层 CONFIG_DRM_FBDEV_EMULATION,只要 DRM 驱动注册了,内核会自动给你生成 /dev/fb0,用户态程序感觉不到 DRM 的存在。

所以我的 framebuffer 验证实际是建立在 DRM 驱动之上的。内核里需要打开 CONFIG_DRM、CONFIG_DRM_XILINX、CONFIG_DRM_KMS_HELPER、CONFIG_DRM_FBDEV_EMULATION,编译一个支持 fbdev 模拟的镜像。设备树里 DRM 节点注册成功后,你会在 /sys/class/graphics/fb0 下看到虚拟分辨率和 stride 信息。

这里有个小坑:有很多教程会让新手在内核配置里选 CONFIG_FB_SIMPLE,ZYNQMP 平台如果已经有 DRM 驱动,再开一个简单 framebuffer 驱动反而可能抢占资源,导致 DRM 的 fbdev 模拟层不生效。最好的做法是让 DRM 驱动独占显示硬件,通过 fbdev emulation 提供兼容接口。

3.2 用 /dev/fb0 快速点亮屏幕

设备树和内核都弄好之后,启动到 Linux,先检查 /dev/fb0 是否存在。然后可以用 fbset 查看当前参数:

fbset -fb /dev/fb0

如果 fbset 不在根文件系统里,也可以直接用 modetest 的 fbdev 兼容测试工具。更直接的办法,写一段十几行的 C 代码,把整屏填充成红色:

#include <stdio.h> #include <fcntl.h> #include <linux/fb.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <string.h> int main(void) { int fd = open("/dev/fb0", O_RDWR); struct fb_var_screeninfo vinfo; struct fb_fix_screeninfo finfo; ioctl(fd, FBIOGET_VSCREENINFO, &vinfo); ioctl(fd, FBIOGET_FSCREENINFO, &finfo); long screensize = vinfo.yres_virtual * finfo.line_length; unsigned char *fbp = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); for (int i = 0; i < screensize; i += 4) { fbp[i] = 0x00; // B fbp[i + 1] = 0x00; // G fbp[i + 2] = 0xFF; // R fbp[i + 3] = 0x00; // A } return 0; }

这段代码跑起来,屏幕如果能出现满屏红色,说明 framebuffer 链路已经通了。注意像素字节序:很多 LCD 模组是 RGB 顺序,但 framebuffer 默认可能是 BGRX,写反的话红蓝会互换,这是最常见的“颜色不对”原因。

3.3 framebuffer 方案的硬伤

fbdev 方案跑通了,但实际用起来体验很一般。最明显的是:我想在屏幕上显示一个 Qt 窗口,同时底下还要跑一个摄像头预览视频层,纯 framebuffer 根本做不了图层叠加,只能自己在应用层把两路数据软合成,CPU 占用率一下子就上去了。

还有一个问题是 vblank 同步。framebuffer 的 pan display 接口虽然存在,但很多驱动实现不够完善,双缓冲切换时经常看到画面撕裂。而且在 Xilinx 内核里,fbdev 模拟层只是 DRM 的一个“影子”,如果你想上 Weston 或者做 DRM plane 测试,还得回到 DRM 接口本身。

4. DRM/KMS 驱动移植全流程实战

4.1 内核配置:把 DRM 相关选项一次性打开

DRM 方案才是这次移植的重点。我用的内核是 Xilinx 维护的 linux-xlnx 分支,版本号 6.1 左右。编译前先加载基础配置:

make ARCH=arm64 xilinx_zynqmp_defconfig make ARCH=arm64 menuconfig

menuconfig 里需要确认以下选项:

Device Drivers → Graphics support <*> Direct Rendering Manager (Xilinx DRM driver) [*] Enable legacy fbdev support [*] DRM driver for Xilinx <*> DRM panel support for simple panels

其中 CONFIG_DRM_XILINX 是 Xilinx 显示驱动的主开关,支持 CRTC、Encoder 和 VDMA 节点的注册。CONFIG_DRM_PANEL_SIMPLE 则提供了一类通用 panel 驱动,如果你的 LCD 屏恰好匹配 simple-panel 列表里的 compatible,设备树就能直接引用;不匹配的话需要自己扩展。

配置完编译,会在 arch/arm64/boot/ 下生成 Image,在 arch/arm64/boot/dts/xilinx/ 下生成 dtb。这一整套流程里最容易忽略的是内核里 FB_EFI、FB_SIMPLE 这类配置和 DRM 的互斥,建议直接用 defconfig 再增量改,别凭空手写配置。

4.2 设备树节点到底怎么写

设备树是 DRM 移植最繁琐也最关键的部分。我这次整理的完整节点结构大致如下。

首先是 PL 侧的 VDMA 节点,它负责把内存中的帧数据搬到视频输出逻辑:

axi_vdma_0: dma@a0010000 { compatible = "xlnx,axi-dma-1.00.a"; reg = <0x0 0xa0010000 0x0 0x10000>; dma-channel@0 { compatible = "xlnx,axi-vdma-mm2s-channel"; interrupts = <0 89 4>; xlnx,datawidth = <0x20>; }; };

然后是 VTC 和 Video Out 相关节点,Xilinx 的 DRM 驱动主要靠这里拿到时序和像素时钟信息。设备树里通常会有一个总的显示控制器节点,platform driver 通过 compatible 匹配进 xlnx_drm 驱动,并引用 VDMA 节点。

面板侧,假设屏挂在 I2C 上(有些屏需要初始化序列)或者直接用 GPIO 控制使能,可以这样描述:

&i2c0 { status = "okay"; lcd_panel: lcd-panel@0 { compatible = "myvendor,800x480-rgb", "simple-panel"; reg = <0x0>; backlight = <&backlight>; enable-gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>; reset-gpios = <&gpio0 1 GPIO_ACTIVE_HIGH>; panel-timing { clock-frequency = <33300000>; hactive = <800>; vactive = <480>; hfront-porch = <40>; hback-porch = <88>; hsync-len = <48>; vfront-porch = <13>; vback-porch = <32>; vsync-len = <3>; hsync-active = <0>; vsync-active = <0>; de-active = <1>; pixelclk-active = <0>; }; }; };

设备树里的要点在于 compatible。有一种最快的调试办法:先不加自定义 compatible,直接在面板节点里写 simple-panel,然后在启动日志里看它有没有被认出来。如果 simple-panel 驱动不认识这种屏,就需要改内核代码了。

4.3 扩展 simple-panel 驱动,加一块新屏的 timing

绝大多数情况下,你的 800x480 屏不会刚好在 panel-simple.c 的兼容列表里。最简单的做法是往这个文件里加一个新的匹配项和 timing 结构体。

以当前 6.1 内核为例,panel-simple.c 里已经有大量面板的 display_timing 结构。你需要添加类似这样的一段:

static const struct display_timing myvendor_800x480_timing = { .pixelclock = { 30000000, 33000000, 35000000 }, .hactive = { 800, 800, 800 }, .hfront_porch = { 40, 40, 40 }, .hback_porch = { 88, 88, 88 }, .hsync_len = { 48, 48, 48 }, .vactive = { 480, 480, 480 }, .vfront_porch = { 13, 13, 13 }, .vback_porch = { 32, 32, 32 }, .vsync_len = { 3, 3, 3 }, .flags = DISPLAY_FLAGS_HSYNC_LOW | DISPLAY_FLAGS_VSYNC_LOW | DISPLAY_FLAGS_DE_HIGH | DISPLAY_FLAGS_PIXDATA_NEGEDGE, }; static const struct panel_desc myvendor_800x480 = { .timings = &myvendor_800x480_timing, .num_timings = 1, .bpc = 8, .size = { .width = 152, .height = 91, }, .bus_format = MEDIA_BUS_FMT_RGB888_1X24, }; static const struct of_device_id platform_of_match[] = { ... { .compatible = "myvendor,800x480-rgb", .data = &myvendor_800x480, }, ... };

加完之后重新编译内核,设备树里 compatible 匹配到这个字符串,simple-panel 就会自动加载,并在 DRM 的 connector 上暴露 800x480 的模式。这里有个细节:display_timing 里三个数值分别是最小值、典型值、最大值,不能像设备树里的 panel-timing 那样只给单一值,否则编译不会报错但行为可能不符合预期。

4.4 启动测试:modetest 是怎么验证点屏成功的

内核和设备树都更新完,启动后先看 dmesg:

dmesg | grep -i drm

正常情况下会看到 xlnx_drm 初始化的日志,以及一个带 panel 的 connector 被发现。接着用 libdrm 提供的 modetest 工具:

modetest -M xlnx -p

输出里可以看到 CRTC、Encoder、Connector 的 id 和当前分辨率。给 connector 设置 800x480 模式:

modetest -M xlnx -s 40:800x480-60

然后再用 Plane 测试图案。先查一下可用的 plane id 和格式:

modetest -M xlnx -p | grep plane

找到 plane id 后,比如是 29,可以刷一个测试画面:

modetest -M xlnx -P 29:800x480@XR24

如果屏幕出现彩条测试图,说明 DRM 链路已经完全打通。这一步成功之后,剩下就是跑用户态图形栈的问题了。

4.5 接上 Weston、Qt 和 LVGL

DRM 链路通了,实际应用就好办了。最简单的验证是跑 Weston:

weston --backend=drm-backend.so --use-pixman

Weston 启动后能看到桌面背景和鼠标指针,说明 DRM master、Plane 分配、模式切换都没问题。如果你的系统有 GPU,还可以换成渲染 backend,体验会更流畅。

嵌入式 GUI 上,Qt 可以直接用 eglfs 平台插件接 DRM,或者用 linuxfb 接 framebuffer 模拟层;但我实测下来,在同一块屏上,Qt 的 eglfs 走 DRM 的效率明显比 linuxfb 高,尤其是动画场景。另一个热门选择是 LVGL,它有专门的 linux framebuffer 驱动示例,对 800x480 这种分辨率非常合适,显示中文字体时注意字体文件大小和缓存策略,别把内存吃满。

5. 常见问题与排查技巧实录

5.1 花屏、颜色混乱、上下颠倒

花屏是 LCD 调试里最容易出现的问题,也是最考验耐心的。我在这次项目里遇到花屏主要有三类原因。

第一类是 VDMA 的 stride 配置错误。比如屏是 800x480@RGB888,一行 800 个像素其实是 2400 字节,但如果你把 stride 和 width 都设成 800,那 DMA 搬运一帧数据时实际只搬运了原来三分之一的字节,屏幕自然花得没法看。解决方法是仔细核对 VDMA 的 frame_store_start_addr 和 line buffer 配置,确保 stride 是像素宽度乘以每像素字节数。

第二类是色彩字节序错误。如果你在 framebuffer 里写的是 RGBA,但 LCD 屏期望的是 BGRA,整个画面会呈现一种“红蓝互换”的怪颜色。解决方法是先用纯红、纯蓝单色图案测试,一键定位是不是字节序问题。

第三类是像素时钟极性反了。设备树里 pixelclk-active 如果设反,屏幕会显示成像是有一层“斜纹”或者轻微毛刺。修一下 panel-timing 里的极性标志就好。

至于上下颠倒,多为 DE 极性或 RGB 信号线序问题,尤其手工飞线时很容易把 24 根 RGB 数据线中的一组高字节和低字节接反,建议先拿示波器或逻辑分析仪确认几个关键信号的字节顺序。

5.2 背光不亮、白屏、黑屏

背光不亮最常见的原因是 backlight 节点没配上,或者 GPIO 控制方向不对。设备树里 enable-gpios 往往要带 GPIO_ACTIVE_HIGH/LOW 标志,如果屏的背光使能脚是低电平有效,你写成了高电平,自然不亮。另外有些屏用的不是 GPIO 而是 PWM 调光,那就要接 PWM 控制器节点,并把 backlight 节点的 pwms 属性指向正确设备。

白屏意味着背光亮了但面板没有收到像素数据,重点查 data enable 信号和像素时钟是否正常。黑屏则可能是 DRM 驱动没有成功 set mode,或者连接器没有 enable。用 modetest -p 看一下 connector 状态是否 connected,如果显示 disconnected,多半是 panel 节点匹配不上或者 GPIO 复位没拉起来。

5.3 模式列表异常:为什么显示 1024x768 而不是 800x480

有一次启动后 modetest 显示最高分辨率是 1024x768,把 connector 强制设成 800x480 又报错。这类问题通常是 DRM 驱动没有从 panel 节点拿到正确的 timings,而是走了默认的 EDID 路径。

对于这种不带 EDID 的 RGB 屏,解决办法有两条:一是确认 panel-timing 节点被内核正确解析,二是直接在 CRTC 或 connector 的初始化路径里覆盖 mode 列表。检查 dmesg 里的 panel-simple 解析日志,如果显示 “ignore 1024x768” 之类的信息,说明数据来源不对。

5.4 VSYNC 超时和 DMA 中断风暴

跑图形栈时偶尔会遇到 dmesg 里刷 vblank wait timed out 或者 DMA 中断风暴。这类问题往往和中断配置有关。VDMA 结束后会产生中断,如果设备树里中断号写错,或者中断触发电平配错,内核会收不到完成信号,DRM 的 vblank 机制就没法工作。

还有一次是内存一致性问题,表现为画面随机闪块。最终解决方案是在 VDMA 对应的设备树节点加上 dma-coherent;再配合保留 CMA 池,把帧缓冲放在连续物理内存里,问题就消失了。这几个坑排查起来非常费时,建议从裸机阶段就开始验证中断和 DMA 数据通路。

6. 实测经验小结:给后来者的一些建议

这次从 framebuffer 到 DRM 的移植过程中,最深的体会是:显示驱动调试一定要自上而下分层验证。先在 Vivado 的硬件工程里确认 PL 时序输出,再在 U-Boot 里用简单的寄存器操作验证 VDMA 搬运,最后才轮到 Linux 内核介入。每一层都通过之后再逐层叠加,能省下大量排查时间。

另外,尽量保留传统 framebuffer 兼容层。很多老应用只认 /dev/fb0,开了 CONFIG_DRM_FBDEV_EMULATION 之后,DRM 和 framebuffer 两套接口可以共存,测试时就用 modetest 验证新栈,应用层暂时还能继续用 fbdev,等 Qt 或者 Weston 调通后再迁移,这样风险最小。

最后分享一个小技巧:800x480 屏的时序参数可以先从 datasheet 表格里抄一个典型值,但别迷信,务必在实际屏幕上验证。用 modetest 刷测试图案时,把 hfront-porch、hback-porch、hsync-len 三个值逐步调整,观察屏幕边缘是否干净、有无闪烁,往往一组看起来“合理”的参数实际效果并不理想。调试显示驱动,耐心比聪明重要,每次只改一个参数,记录现象,再决定下一步,这比盲目猜测要高效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询