ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ZYNQMP RGB LCD驱动移植:从framebuffer到DRM/KMS框架实战

ZYNQMP RGB LCD驱动移植:从framebuffer到DRM/KMS框架实战 最近在 ZYNQMP 平台上做了一块 800x480 的七寸 RGB LCD 点屏调试项目本身说大不大但涉及的内容跨度很典型老方案用的是 kernel 里的 framebuffer 驱动改到新内核和新 BSP 后原来的 /dev/fb0 方式越来越难维持最终整体迁到了 DRM/KMS 框架。整个过程踩了不少坑也把 framebuffer 和 DRM 这两套显示架构的差异彻底摸了一遍。这篇文章就是这次移植的完整复盘。我会从硬件平台、设备树、fbdev 传统实现再到 DRM 驱动重构按实际操作顺序一步一步写出来包括 800x480 屏的时序参数、驱动代码核心片段、modetest 验证方法、以及调试时遇到的各类奇葩问题。适合手里正好有 ZYNQMP RGB 接口 LCD 的人参考也适合想搞懂 fbdev 和 DRM 差异的嵌入式 Linux 驱动开发者阅读。文章里所有方案和代码都来自我实际跑过的环境不是纯理论推导直接复制基本能落地。1. 项目概述与移植路线拆解1.1 这次要解决的到底是什么问题先说硬件。主控用的是 ZYNQMP 系列的 XCZU7EV屏幕是普通的 800x480 TFT LCDRGB 接口24bit 颜色带行场同步和时钟信号背光通过一个使能脚控制。这种屏在工控和车载项目里非常常见参数不复杂但正因为常见很多人一开始会掉以轻心。ZYNQMP 这颗片子有个比较特殊的地方它不像很多 MCU 或者 SoC 那样原生带一个专门接 RGB LCD 的 LCD 控制器。PS 端虽然集成了 DisplayPort 控制器但 DP 口和 RGB 屏之间一般要加转换芯片或者干脆用 PL可编程逻辑自己生成一套 RGB 接口时序。实际项目里大哥有做法是 PL 里用 VDMA Video Timing Controller 搭一个视频输出链路输出 RGB 信号给屏幕PS 端 Linux 通过驱动去配置和搬运显示数据。我这次的情况是硬件链路是 PL 里已经有一套成熟的 RGB 输出逻辑并且在上一个版本的 Linux 内核5.4 内核 厂商 BSP上软件走的是传统的 framebuffer 驱动/dev/fb0 也能正常出来屏幕能显示。问题出在 BSP 升级之后内核版本到了 5.10/5.15新的显示子系统开始默认用 DRM/KMS 管理 GPU 和显示控制器旧的那套 fbdev 驱动和平台数据在设备树里越来越不受待见甚至直接编译不过。所以整个项目的本质不是“点不亮屏”而是“如何在新内核的 DRM 框架下把 800x480 这块屏重新驱动起来同时保留上层应用的兼容性”。这也就是文章标题说的“从 framebuffer 到 DRM 框架的完整驱动移植”。1.2 为什么必须从 framebuffer 迁到 DRM很多人一听到迁移就头疼生怕把原来能用的东西弄坏。但如果你真正理解了 DRM 为什么出现就会明白这不是为了折腾人而是框架本身确实更合理。framebufferfbdev的好处是极其简单内核里分配一块连续内存作为显示缓冲区应用通过 mmap 直接往里面填像素驱动做的事情就是按固定时序把这块内存里的数据刷到屏幕上。没有合成器、没有图层、也没有复杂的对象模型非常直观。坏处也同样明显。第一它没有统一的显示资源管理机制多个应用往同一个 /dev/fb0 上写数据只能靠用户自己协调没有任何内核层面的互斥和仲裁。第二不支持垂直同步VSYNC和页翻转Page Flip这种基础的显示同步操作上层做 GUI 容易出现撕裂感尤其在你的系统要跑 Qt 或者 Wayland 的时候这种缺陷会被放大得十分明显。第三fbdev 对多显示器、多图层、缩放、旋转这些新需求几乎没有支持内核社区早就把 fbdev 标记为 legacy 状态。DRMDirect Rendering Manager就是为替代 fbdev 设计的新一代显示/图形管理框架。它把显示链路抽象成 DRM Device、CRTC、Encoder、Connector、Plane 这些标准对象统一管理 GPU/显示控制器的资源。就算你不需要 3D 加速只要你的 display controller 需要被 Linux 正常管理用 DRM/KMS 也是最顺理成章的路径。更关键的是现代嵌入式的 GUI 生态几乎都往 DRM 上靠Qt 的 linuxfb 插件已经没人维护Weston、wlroots 这些 Wayland compositor 直接对接 KMS你不要 KMS 就根本跑不了。以后你再遇到类似的老项目升级一定记住fbdev 不是不能工作但它是越来越边缘化的一条腿DRM 才是当前和未来 Linux 显示子系统的绝对主线。1.3 移植任务的整体拆解这次的移植任务被我拆成了四步后面每一节都会对应展开硬件准备与环境确认确认 800x480 LCD 的接口、引脚、背光控制模式确认 ZYNQMP 侧 PL 里的视频输出链路和寄存器接口。传统 framebuffer 驱动复现先用最简单的方式把老驱动跑起来搞清楚时序参数和 DMA 内存路径做到“心里有底”。DRM/KMS 驱动重构基于 DRM 框架新写或者改造设备驱动把同一块屏接入到 KMS 体系里生成 /dev/dri/card0 和 connector。验证与踩坑用 modetest、weston、Qt 等逐级验证收集常见故障并沉淀排查方法。这四步看起来是线性的实际上我在项目里反复回退了两次因为中途发现设备树里 DMA 中断号和时序参数写错了。所以后面你跟着做的时候也建议每步都独立验证不要指望最后一次性通过。2. 硬件平台与底层准备2.1 先分清这块屏的“型号”再动手嵌入式项目里叫“LCD 屏”的设备实在太多驱动思路完全不一样。有一种是单片机上的段码 LCD比如常见的 TM1622 这一类驱动芯片它们通过若干段引脚控制特定符号显示内容一般不是点阵而是固定的数字和字符。还有一种是字符型液晶比如 1602/12864自带控制器通过并行或 I2C 接口发送命令和字符数据。以及我们这次要驱动的 RGB 点阵 LCD这种屏幕本身不含控制器必须外部提供像素时钟、行场同步和 RGB 数据每一帧画面都需要主控实时刷过去。这三种设备的驱动方式天差地别。很多初学者拿到一块 TM1622 的段码屏发现它也是 LCD就以为和 800x480 点阵屏是一回事结果被接口文档彻底搞懵。实际上 TM1622 这类芯片驱动起来根本不涉及 framebuffer 或者 DRM它就是一个普通的 SPI/IO 设备甚至用 GPIO 模拟时序都能点亮。所以拿到项目的时候第一件事不是打开内核而是把这块屏的 datasheet 找出来确认三件事接口类型RGB 并行、LVDS、MIPI DSI分辨率刷新率还有行场同步信号的极性。眼前这块 800x480 屏是标准 RGB24 并行接口信号包括 LCD_CLK、LCD_HSYNC、LCD_VSYNC、LCD_DE、LCD_D[23:0]外加一个背光使能背光脚。这个接口类型决定了后面所有折腾的路线。2.2 ZYNQMP 里的视频链路到底怎么走ZYNQMP 从 PS 到屏幕路径大概是这样的APUARM Cortex-A53跑 Linux应用程序把要显示的画面通过 DRM 提交给显示驱动显示驱动配置 PL 端的视频 DMA 和时序控制器把内存中的帧数据通过 AXI 总线送进 PL 里的 FIFO再按照 800x480 的时序输出给屏。说得更具体一点PL 里通常有三个 IP 在协同工作VDMAVideo Direct Memory Access负责从 DDR 里按帧读取图像数据支持行同步、帧同步、以及半帧中断。Video Timing ControllerVTC负责生成像素时钟和行场同步信号时序参数一般通过 AXI-Lite 寄存器配置。AXI-Stream to Video Out 或者自定义 RTL把 AXI-Stream 数据流拼接成平行的 RGB 信号输出。这种路径在 Xilinx 的官方 example design 里非常常见但具体到不同项目寄存器地址和中断号可能不同。所以你在写驱动前一定要跟硬件工程师确认清楚VDMA 的基地址是多少VTC 的基地址是多少输出的中断号是哪个DMA 能否连续读取。我在这个项目里最开始犯的错就是默认了跟上一个项目相同的 PL 寄存器布局结果屏幕死活不出图最后抓 AXI 波形才发现 VDMA Base Address 在 bit 级差了很大一段。2.3 内核配置与设备树准备因为平台是 ZYNQMP内核优先使用 Xilinx 维护的 Linux-XLNX 分支或者上游主线内核加对应补丁。无论哪种内核配置里有一组选项必须打开不然后面所有努力都是白费CONFIG_DRMy CONFIG_DRM_KMS_HELPERy CONFIG_DRM_FBDEV_EMULATIONy CONFIG_DRM_XLNXy # Xilinx DRM 驱动 CONFIG_DRM_XLNX_DPSUBy # 如果走 DP 子系统 CONFIG_FBy CONFIG_FB_CMDLINEy CONFIG_FB_MODE_HELPERSy如果你暂时不想切到 DRM还想用老式的 framebufferCONFIG_FB 肯定要保留并且要打开 CONFIG_FB_VIRTUAL 或者相关平台 framebuffer 驱动。实际上在新内核里即使你注册的是一个纯 DRM 驱动也可以打开 CONFIG_DRM_FBDEV_EMULATION让系统自动在 /dev/dri/card0 下生成一个模拟的 /dev/fb0这样老的上层应用不至于全部瘫痪这个兼容层后面会专门讲。设备树方面至少要描述这样几个节点DMA 控制器节点、时序控制器节点、LCD panel 节点。以我们项目为例PL 端 VDMA 和 VTC 都挂在 AXI 总线上设备树里长这样v_dma: vdmaa0000000 { compatible xlnx,axi-vdma-1.00.a; reg 0x0 0xa0000000 0x0 0x1000; interrupts 0 32 4; interrupt-parent gic; clocks zynqmp_clk 71; #dma-cells 1; xlnx,num-fstores 0x3; }; v_tc: v-tca0010000 { compatible xlnx,v-tc-6.2; reg 0x0 0xa0010000 0x0 0x1000; clocks zynqmp_clk 71, zynqmp_clk 71; clock-names clk, s_axi_aclk; }; lcd_panel: lcd-panel { compatible myproject,spot480-lcd; panel-supply reg_lcd_3v3; backlight lcd_backlight; enable-gpios gpio 34 GPIO_ACTIVE_HIGH; };这里注意 panel 节点我用的是自定义 compatible后面 DRM 驱动会匹配这个节点。如果屏幕支持通过 I2C/DDC 读取 EDID那可以省掉固化的时序参数直接靠 EDID 解析但 800x480 这种屏绝大多数不支持 EDID必须把时序写死进驱动或者按 panel-simple 的 compatible 直接填参数。3. framebuffer 驱动先把屏幕点亮3.1 从最原始的内存写入开始我一直觉得不管最终方案用什么框架先把屏幕用最原始的方式点亮一次是建立信心和排查硬件问题最快的路径。所以这里我先把原来的 framebuffer 驱动思路完整讲一遍。framebuffer 驱动的核心是一个struct fb_info结构体驱动要做的事情就是分配这块内存、填好固定信息和可变信息、注册进内核。屏幕上每个像素在内存里按 RGB888 三字节排列800x480 屏幕上整整一行有 800 个像素一帧就是800 * 480 * 3 1152000字节约 1.1MB。这个内存不能是一般的 kmalloc 内存因为显示控制器需要持续访问它必须用 DMA 一致性内存接口分配保证物理地址连续并且和缓存之间的关系是正确的。老驱动里常见的初始化流程是这样的static int spot480fb_probe(struct platform_device *pdev) { struct fb_info *info; struct spot480fb_par *par; void *mem; dma_addr_t dma_handle; mem dma_alloc_coherent(pdev-dev, SZ_2M, dma_handle, GFP_KERNEL); if (!mem) return -ENOMEM; info framebuffer_alloc(sizeof(*par), pdev-dev); if (!info) { dma_free_coherent(pdev-dev, SZ_2M, mem, dma_handle); return -ENOMEM; } par info-par; par-mem mem; par-dma_handle dma_handle; snprintf(info-fix.id, 16, spot480fb); info-fix.smem_start dma_handle; info-fix.smem_len SZ_2M; info-fix.type FB_TYPE_PACKED_PIXELS; info-fix.visual FB_VISUAL_TRUECOLOR; info-fix.line_length 800 * 3; info-fix.accel FB_ACCEL_NONE; info-var.xres 800; info-var.yres 480; info-var.xres_virtual 800; info-var.yres_virtual 480; info-var.bits_per_pixel 32; info-var.red.offset 16; info-var.red.length 8; info-var.green.offset 8; info-var.green.length 8; info-var.blue.offset 0; info-var.blue.length 8; info-var.activate FB_ACTIVATE_NOW; info-var.nonstd 0; info-fbops spot480fb_ops; info-screen_base mem; info-pseudo_palette par-pseudo_palette; if (register_framebuffer(info)) return -EINVAL; platform_set_drvdata(pdev, info); return 0; }这里有个细节我故意把 var.bits_per_pixel 设置成 32而不是硬件实际输出的 24bit。原因是通过 fbdev 层和用户空间打交道的像素格式不一定需要和硬件完全一致VTC 输出端会从内存里按配置取对应格式如果你在 framebuffer 层用 32bit用户空间填像素更方便驱动做的事就是把数据搬到硬件寄存器里。当然如果硬件链路只支持 24bit你也可以用 24bit/32bit 的转换逻辑。驱动程序本身不负责直接产生时序时序产生在 PL 的 VTC 里。fbdev 驱动的核心职责就是为 VDMA 提供可用的内存地址并且告知 VTC 开始搬运。所以 probe 最后需要把 dma_handle 写进 VDMA 的寄存器让硬件知道从哪里读取像素数据。这一步具体写寄存器的方式因硬件不同而有差异但思路一定是关闭 DMA 通道 - 设置帧地址 - 设置行步长 - 配置帧尺寸 - 启动通道。3.2 800x480 屏的时序参数到底怎么填这里是整个项目最关键、也最容易出问题的地方。VTC 的时序参数决定了屏幕能否稳定地锁住画面差一点都不行。800x480 的屏在 60Hz 刷新条件下的典型参数如下记得一定要以你的屏体规格书为准参数值说明HActive800有效行像素HFrontPorch40行前沿HSyncWidth48行同步脉宽HBackPorch40行后沿VActive480有效列像素VFrontPorch13帧前沿VSyncWidth3帧同步脉宽VBackPorch29帧后沿PixelClock33.3 MHz像素时钟你可以根据自己的屏幕手册微调。像素时钟 33.3MHz 是怎么来的呢简单算一下行总长度 800404840 928帧总高 48013329 525一帧的总像素数 928 * 525 487200乘以刷新率 60Hz得到 29.233MHz。但通常 800x480 屏需要的时钟会比理论值略高很多手册直接标注 30MHz 或者 33.3MHz只要 DCLK 在屏允许范围内VTC 就能正常工作。配置 VTC 时HSYNC 和 VSYNC 极性也要关注多数屏是低有效。如果你配置反了画面会偏一边或者完全不同步表现很像是信号没通实际上就是极性问题。另外提醒一点VTC 里有两种同步模式一种是自动生成Free Run一种是外部同步。对于直接接 RGB 屏的场景用 Free Run 就好让 VTC 自己产生时序如果你接的是一个需要从外部源接收时序的桥接芯片就得把 VTC 配置成从模式否则画面即使生成了桥接芯片也锁不住。3.3 验证 framebuffer 是否正常工作屏幕时序配好驱动注册成功后系统里就会出现 /dev/fb0。验证方法分成三个层次第一层是用fbset查看当前分辨率信息确认驱动注册的时序参数和 VTC 实际配置一致fbset -i第二层是直接用 dd 命令写一堆原始数据到 fb0比如让屏幕变成全白dd if/dev/zero of/dev/fb0 bs1024 count100这会把显存区域清零如果颜色是 RGB888屏幕会显示黑色。想显示全白就用dd if/dev/urandom或者干脆写0xFF。如果能看到颜色变化说明你的 DMA 链路基本是通的像素数据已经从内存走到了屏幕。第三层是画一个渐变色或者简单的图形。这一段我当时是写了一个非常简单的 C 程序用 mmap 映射 /dev/fb0然后逐像素填充 RGB屏幕上能看到红绿蓝三色条基本确定 framebuffer 这条路彻底没毛病。#include stdio.h #include stdint.h #include fcntl.h #include sys/mman.h #include sys/ioctl.h #include linux/fb.h int main() { int fd open(/dev/fb0, O_RDWR); struct fb_var_screeninfo vinfo; unsigned char *fbp; ioctl(fd, FBIOGET_VSCREENINFO, vinfo); int screensize vinfo.xres * vinfo.yres * vinfo.bits_per_pixel / 8; fbp mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); for (int y 0; y vinfo.yres; y) { for (int x 0; x vinfo.xres; x) { long offset (x y * vinfo.xres) * (vinfo.bits_per_pixel / 8); fbp[offset 0] x 0xff; // Blue fbp[offset 1] y 0xff; // Green fbp[offset 2] (x y) 0xff; // Red } } munmap(fbp, screensize); close(fd); return 0; }3.4 传统 framebuffer 路线的三个致命痛点屏幕点亮之后我先让旧应用跑了一遍确认业务功能没大问题然后才开始真正思考迁移到 DRM 的必要性。如果只是跑一个简单的 GUIframebuffer 确实撑得住但只要想稍微深入一点它就露馅了。第一个痛点是 memset 或者写屏有时候会跟 DMA 搬运打架导致屏幕出现乱七八糟的横纹尤其当 GUI 更新频繁的时候。为什么因为 fbdev 并没有提供“等 vsync 再把数据交出去”的机制应用可能正在写显存的时候DMA 已经把半帧数据读出去送到屏幕上了结果是上半屏是新的下半屏是旧的撕裂感非常严重。老一点的 GUI 程序会自己通过 poll 一个专门的中断去凑合但这种做法很不通用。第二个痛点是多进程无法共享和协调。Qt 跑在前台后面一个业务进程也想往屏上写个状态条两者同时写 /dev/fb0没有协调机制最后显示的心跳慢了半拍排查程序本身却查不出任何逻辑错误。这种问题非常耗时因为它是内核层缺少资源管理造成的。第三个痛点是 fbdev 无法扩展到多图层。800x480 这块屏虽然不大但 UI 想要做一个背景层、一个视频层、一个光标层用 fbdev 就得自己去做 alpha 混合或者反复整帧刷新浪费 CPU 不说效果还很差。DRM 的 plane 天生就是为了解决这个问题的。所以不管你当前的项目是不是立刻要上 Westeon只要是 linux 内核版本已经比较新、还想长期维护把显示驱动迁到 DRM 是迟早要做的事。与其到时候再来痛苦重构不如现在就在 framebuffer 验证完成的基础上一步步把驱动翻成 DRM 版本。4. 从 framebuffer 迁到 DRM/KMS驱动架构与实现4.1 DRM 框架里的几个对象和屏幕如何对上号从 fbdev 迁移到 DRM最重要的不是写代码而是把概念映射清楚。DRM 不是一上来就让你写一大堆程序它是一套对象模型。你只需要理解每个对象代表什么再考虑如何把它们按你的硬件链路串起来。一个完整的 DRM 显示链路大概是这样DRM Device整个显示系统的顶层出口用户看到的 /dev/dri/card0。CRTC显示控制器负责从内存读取帧数据产生时序并输出显示内容。对应到我们的硬件就是 PL 里的 VTC VDMA 组合抽象出来的“显示引擎”。Encoder编码器把 CRTC 输出的信号编码成特定的物理接口比如 DP、HDMI、LVDS、RGB。如果 CRTC 直接输出 RGB 信号那 Encoder 可以是一个非常简单的 pass-through encoder。Connector物理连接器描述“屏幕接到了哪个口上”也负责读 EDID 或获取固定 mode。Plane图层同一时刻可以有多层叠加。最底层的 plane 是主平面处理方式类似 framebuffer。Panel代表物理 LCD 面板panel-simple 驱动里可以直接定义时序参数和使能接口。Bridge桥接芯片如果 CRTC 输出 DP 但面板吃 LVDS/RGB中间接的转换芯片就是 bridge。这些对象之间的关系可以类比成一条生产线CRTC 是“生成画面的机器”Encoder 是“把产品包装成某种规格的包装机”Connector 是“发货口”Panel 就是“最终收到货的客户”。设备树里的节点通过 compatible 被对应驱动匹配然后在 bind 阶段一层层连接起来。我这次用到的并不是特别复杂的图形 GPU所以选择用drm_simple_display_pipe这个辅助 API。它把一个 CRTC Encoder Connector 打包成一个“简单的显示管道”对单图层 RGB 屏来说完全够用代码量比完整 DRM 驱动少一半以上。4.2 注册第一个 DRM 面板驱动面板驱动是整个迁移里最先要写的部分因为它定义了 800x480 屏的物理属性和时序。DRM 框架里已经有一个panel-simple驱动把常见屏幕的参数都收录进去了。如果你的屏幕恰好是某款知名面板设备树 panel 节点 compatible 写成对应的boe,7inch-800x480之类的就能直接用。但现实中很多屏是白牌屏没有标准 compatible所以还是自己写一个最小的 panel 驱动比较稳妥。panel 驱动要实现的核心接口是drm_panel_funcsstatic int spot480_panel_get_modes(struct drm_panel *panel, struct drm_connector *connector) { struct drm_display_mode *mode; mode drm_mode_duplicate(connector-dev, spot480_default_mode); if (!mode) return 0; mode-type DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED; drm_mode_set_name(mode); drm_mode_probed_add(connector, mode); return 1; } static int spot480_panel_prepare(struct drm_panel *panel) { /* 打开背光、使能电源 */ panel-backlight-props.power FB_BLANK_POWERDOWN; backlight_update_status(panel-backlight); return 0; } static int spot480_panel_enable(struct drm_panel *panel) { /* 等待时序稳定使能 LCD */ gpiod_set_value_cansleep(panel-enable_gpio, 1); return 0; }get_modes的工作相当于向整个 DRM 系统宣告我这块屏支持以下这些显示模式。如果你的面板支持多个分辨率这里可以循环添加多个drm_display_mode。不过 800x480 的屏一般就一个原生分辨率你加一个模式就好并且一定设置DRM_MODE_TYPE_PREFERRED这样上层应用默认就会选择这个分辨率。drm_display_mode里的时序字段和前面 framebuffer 里设置的那一堆参数是一一对应的static const struct drm_display_mode spot480_default_mode { .clock 33300, // kHz单位是 kHz不是 Hz .hdisplay 800, .hsync_start 800 40, .hsync_end 800 40 48, .htotal 800 40 48 40, .vdisplay 480, .vsync_start 480 13, .vsync_end 480 13 3, .vtotal 480 13 3 29, .flags DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC, }; static const struct drm_panel_funcs spot480_panel_funcs { .get_modes spot480_panel_get_modes, .prepare spot480_panel_prepare, .enable spot480_panel_enable, .disable spot480_panel_disable, .unprepare spot480_panel_unprepare, };很多初次接触 DRM 的同学容易把 clock 字段的单位写错写成 Hz结果屏幕黑屏或者提示 mode 不在范围内。记住这个字段单位是 kHz33300 代表 33.3MHz不是 33.3Hz。这个小坑我在不止一个项目里见过。panel 驱动的 probe 也很朴素主要是把设备树里的 GPIO 和 backlight 拿下来存到结构体里static int spot480_panel_probe(struct device *dev) { struct spot480_panel *panel; panel devm_kzalloc(dev, sizeof(*panel), GFP_KERNEL); panel-enable_gpio devm_gpiod_get_optional(dev, enable, GPIOD_OUT_LOW); panel-backlight devm_of_find_backlight(dev); drm_panel_init(panel-base, dev, spot480_panel_funcs, DRM_MODE_CONNECTOR_DPI); drm_panel_add(panel-base); return 0; }connector 类型选DRM_MODE_CONNECTOR_DPI因为它是并行 RGB 接口不是 HDMI 或者 eDP。如果你接的是 LVDS可以选DRM_MODE_CONNECTOR_LVDS如果经由 bridge 转换最终 connector 类型以 bridge 输出为准。4.3 主显示驱动用 drm_simple_display_pipe 接入电路面板驱动写好后真正的主角是主显示驱动。它负责创建 DRM Device注册传入的 panel并且实现一个“从内存到屏幕”的显示管线。用drm_simple_display_pipe的好处是我们不需要去单独实现 CRTC/Encoder/Connector只要实现四个回调用。static const struct drm_simple_display_pipe_funcs spot480_pipe_funcs { .atomic_check spot480_pipe_check, .update spot480_pipe_update, .enable spot480_pipe_enable, .disable spot480_pipe_disable, }; static const struct drm_display_mode spot480_pipe_mode { .clock 33300, .hdisplay 800, .hsync_start 800 40, .hsync_end 800 40 48, .htotal 800 40 48 40, .vdisplay 480, .vsync_start 480 13, .vsync_end 480 13 3, .vtotal 480 13 3 29, .flags DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC, .width_mm 154, .height_mm 86, }; static const struct drm_simple_display_pipe spot480_pipe { .mode spot480_pipe_mode, .connector_type DRM_MODE_CONNECTOR_DPI, };这里和 panel 驱动里重复定义了同一组时序。如果 panel 已经提供 mode那么 pipe 里可以不再指定 mode由drm_panel_get_modes提供。但如果你的 pipe 驱动要和 panel 配合那么当 panel 驱动也调get_modes时你可能需要在get_modes回调里调用 panel 的get_modes并把它返回的模式加上。probe 里的操作顺序很关键我建议按下面的顺序做获取 DT 里 VDMA/VTC 的寄存器地址ioremap。获取 DMA channel。分配显存仍然用 dma_alloc_coherent。注册 DRM device 和 simple display pipe。在 DRM 子系统中添加 panel。注册 fbdev emulation。代码骨架大概是这样的static int spot480_drm_probe(struct platform_device *pdev) { struct spot480_drm *priv; struct device *dev pdev-dev; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); priv-regs devm_platform_ioremap_resource(pdev, 0); priv-dma_chan dma_request_chan(dev, video); priv-drm drm_dev_alloc(spot480_drm_driver, dev); drm_simple_display_pipe_init(priv-drm, priv-pipe, spot480_pipe_funcs, DRM_PLANE_TYPE_PRIMARY); drm_panel_attach(panel, priv-pipe.connector); drm_fbdev_generic_setup(priv-drm, 32); drm_dev_register(priv-drm, 0); return 0; }关键就是drm_fbdev_generic_setup这一步。它会把 DRM 主平面导出成一个模拟的 /dev/fb0老应用如果不想立刻迁移到 DRM 的 dumb buffer 接口也能用旧方式继续写屏。这在项目过渡阶段能帮上大忙。4.4 CTM / DAM / DRM 的关系以及“数字激励器”误区在查资料的时候很多人会被一些词干扰比如“DRM 数字激励器”“DAM 数字激励器”。这两类东西其实是音频/信号测量领域的术语DRM 是一种动态范围测量DAM 是另一种失真分析仪和 Linux 的 DRM 完全不是一个概念。但正因为同名缩写网上经常混在一起导致新手搜“DRM LCD”的时候看到一堆测量仪器文档一脸问号。我这里专门提一句Linux 的 DRM 全称是 Direct Rendering Manager它管的是显示内存、显示控制器、GPU 这些资源音频领域的“数字激励器”跟内核图形框架没有任何关系。遇到这种缩写冲突记住上下文是 Linux 驱动开发还是电子测量看代码和手册千万别被名词带偏。4.5 用 modetest 验证 DRM 驱动是否成功DRM 驱动写完、编译进内核或者做成模块加载之后验证方式跟 framebuffer 时代不太一样。你需要用modetest这个工具来查看 DRM 设备的所有对象和状态。它来自 libdrm 测试集PetaLinux 和 Yocto 里通常有现成的包没有的话交叉编译 libdrm 里面的 tests 目录就能得到。先看整体状态modetest -M xlnx这里-M指定 DRM driver name如果你不知名字直接modetest -M help看有哪些可用 driver。执行后输出会很多核心关注两个部分connector 状态要有 connectedmode 列表要有 800x480crtc 状态要有 mode。如果看到 connector 状态是 disconnected大概率是 panel 节点和 DRM 驱动之间的 probe/attach 出了问。排查思路是先看 dmesg 里面 panel probe 是否成功再确认 i2c 是不是通了如果你用的是带 I2C 的屏。然后跑一个全屏测试modetest -M xlnx -s 32:800x480这里的 32 是 connector id必须替换成实际数值。执行后屏幕应该会有彩条或者某种测试画面说明 DRM 链路已经真的打通。如果这里没画面回到 framebuffer 阶段的排查逻辑大概率还是时序参数或者 DMA 地址问题。4.6 framebuffer 仿真层老应用的兼容策略迁移之后系统里同时会有 /dev/dri/card0 和 /dev/fb0。后者是由 DRM 的 fbdev emulation 提供的。ls -l /dev/fb* /dev/dri/*你往 /dev/fb0 写入内容最终会通过 DRM framebuffer 打到屏幕上。实现原理是 fbdev emulation 在底层创建了一个 DRM framebuffer并且通过主 plane 显示。这样老的应用不需要改任何代码就能跑视觉效果和以前没有区别。但注意fbdev emulation 的内部实现并不是“所有 fbdev 的像素数据和 DRM 共享同一个物理内存”很多实现里 framebuffer 和 DRM plane 是同步关系。所以如果你在 fb0 上做了 mmap觉得自己可以直接操作物理显存然后又用 DRM 接口开了另一个 framebuffer那就可能踩到内存不一致的坑。项目里如果新旧代码要共存务必让新代码走 DRM dumb buffer不要让两边同时画同一个区域。5. 移植中的调试方法和常见故障记录5.1 屏幕全白 / 黑屏先恢复原始 framebuffer 测试调试时第一步永远不是分析代码而是先确认硬件本身能出图。移植过程中最容易出现的情况是改了设备树或驱动后屏幕要么全白要么全黑。这时候我会直接把之前的 framebuffer 驱动重新加载用第 3 节的方法先点屏。如果 framebuffer 能正常显示说明硬件链路没问题故障一定在 DRM 驱动和设备树。如果 framebuffer 也不亮那就是 PL 时序或硬件连接问题。有一次我花了大半天排查 DRM 注册流程后来发现是之前测试的时候把 VTC 的一个寄存器写死了重新上电后 PL 逻辑没重新加载整个图像输出通道是空的。这说明在 ZYNQMP 项目里PL 的 bitstream 是否加载成功、加载版本是否正确是比内核驱动更优先要确认的底层问题。建议每次调试前都查一下/sys/class/fpga_manager/或者cat /proc/device-tree/fpga-*确认硬件逻辑已经就位。5.2 fb0 出现但画面扭曲 / 花屏多半是时序和格式问题屏幕能亮但画面整个偏了、有斜纹或者像隔行扫描一样跳这种情形一般不是 DRM 对象注册的问题而是时序参数或者 DMA 行步长把屏幕搞晕了。我遇到过最典型的一次是看规格书是 800x480但我把hsync_start写成了hdisplay hsync_width少加了 front porch结果屏幕显示像是往左压缩了一截。这类问题用眼睛看很难判断我的处理方法是写一个测试工具把当前实际配置的时序参数原样打印出来跟规格书逐项比对。DMA 行步长也特别容易出错。如果你的 framebuffer 每行像素是xres * bytes_per_pixel但 VDMA 配置的行步长比这个值大屏幕右下角就会出垃圾数据。这种情况下看锯齿状的空白间隔就能定位。还有一种情况是内存里实际用的是 32bit 像素格式但你给 DMA 配置成 24bit颜色会混乱画面像被“颜色替换”了一样。直接用上面写的小程序画红绿蓝三色渐变就能很快分辨格式错误。5.3 屏幕反映慢 / 刷新率不对屏幕能显示但用眼睛看或者用手机慢动作拍总觉得画面刷新特别慢。先别怀疑屏坏了大概率是 pixel clock 与实际期望不符。800x480 屏如果 VTC 里配的时钟是 10MHz刷新率会掉到 20Hz 左右肉眼看着就会卡顿。用示波器或者逻辑分析仪测量 LCD_CLK 的信号频率应接近 33.3MHz。如果没有仪器可以用 modetest 打印出来的 mode 和 VTC 寄存器值互相印证。这里强调一点DRM mode 里的 clock 字段是驱动上报给用户态的值如果它和实际 VTC 硬件产生的时钟不一致上层会很困惑但你测量硬件信号才是最终依据。5.4 屏幕显示中文乱码这块 800x480 屏如果跑 framebuffer console很常见的一个现象是开机时可以显示启动信息但中文全部变成方块或者乱码。这跟 LCD 驱动本身关系不大而是内核终端控制台的内建字体不支持中文字符集。framebuffer console 显示中文前提是内核配置了相应的字体和字符编码支持。CONFIG_FONT_8x16、CONFIG_FONT_SUN12x22这些只处理 ASCII 和部分扩展字符要支持完整中文要么靠用户空间程序画要么加载一个带中文点阵的字体文件借助setfont之类的工具。实际上嵌入式产品几乎没有直接在控制台显示中文的需求中文界面都由 Qt/GTK 在用户空间画好然后显存只是像素数据根本不存在“中文字库”的问题。所以如果你看到屏幕上中文乱码别去折腾内核字体正确的做法是检查你的 GUI 应用是否带了对的字体文件以及 libfreetype 是否编译进系统。5.5 DRM 驱动加载后直接 OopsDRM 驱动里最容易导致 Oops 的操作是空指针访问原因通常集中在设备树节点和驱动结构体之间的绑定关系上。比如 panel 在 attach 时还没 probe 完成或者 dma_request_chan 返回了空代码却没有判空就继续往下走注册 DRM device 时直接崩。我的建议是在 probe 里把每个需要的外部资源都单独检查并且打印一条明确的报错日志。开发阶段日志宁可多也不要少我甚至会在调试期临时在每个关键步骤后加dev_info。上线前再把冗余日志删掉。这样看起来土实际上比任何 trace 工具都直观。另外drm_simple_display_pipe_init的 plane type 不要随便传DRM_PLANE_TYPE_CURSOR否则 fbdev emulation 可能找不到可以用的主平面加载时报错。除非你有明确的可复用光标层诉求否则主显示通路用DRM_PLANE_TYPE_PRIMARY就对了。5.6 常见问题速查表为了方便后续对照我把这个项目里踩过的问题整理成了表格现象可能原因排查方向屏幕完全不亮PL bitstream 没加载 / 电源和背光未使能检查 PL、背光 GPIO、电源树有背光但无图像时序无输出 / DMA 通道未启动查 VTC 寄存器、VDMA 帧地址图像偏移或扭曲h/v porch 错误 / 极性反了比对屏规格书时序逐一核对颜色完全不对像素格式不匹配 / 行步长错误画 RGB 三色渐变计算 line_length启动时中文乱码内核字体无中文点阵确认应用字体和 freetypemodetest 显示 disconnectedpanel 节点 attach 失败dmesg 查 panel probe加载驱动 Oops资源获取失败后未判空每个 devm 接口后都查返回值5.7 一点调试心得整个项目真正花费时间最多的地方其实不是写 DRM 驱动而是“让设备树里每个资源名称和驱动代码里每个名字完全对应”。这个问题在 ZYNQMP 上尤其突出因为 DMA channel 的名字、中断号、时钟名称都可能来自 PL IP 的配置不同工程里可能不同。我的做法是建议建立一个硬件资源清单表把 PL 地址、中断号、DMA channel 名称、GPIO bank 号全部列出来放在工程仓库里。调试一旦出问题先查表再查代码能省掉一大半的无效分析时间。另外内核日志的打印等级记得调到调试模式echo file drivers/gpu/drm/* p /sys/kernel/debug/dynamic_debug/control或者简单粗暴一点在启动参数里加上drm.debug0x1f loglevel8drivers/gpu/drm 下会输出非常详细的日志。生产环境再把这些关掉。6. 写在最后几个让我少走弯路的经验这篇文章写到这里其实已经把我这次移植的核心过程都讲完了但我还想分享三个从实战中沉淀下来的经验。第一个经验永远是先点亮再讲架构。不要一上来就照着 DRM 的文档写一堆 object先把 framebuffer 驱动跑起来确认硬件链路通再谈迁移。framebuffer 虽然老但在调试阶段是最直接的“硬件体检工具”把 /dev/fb0 点亮的那一刻很多问题已经解决了一半。第二个经验设备树里的时序参数必须跟硬件手册一一对应宁可多花一小时人工核对也不要靠猜测和试错。DRM/KMS 虽然是一个更科学的框架但它不会替你变出一个正确的时序。800x480 屏的 HBP、HFP、VSYNC 这些值差一个数字都可能让你多调一整天。第三个经验也是我反复吃亏后总结出来的给项目留一个“快速降级”的后备方案。我在新内核里已经把 DRM 驱动跑起来了但我仍然保留着老 framebuffer 驱动的设备树 overlay。一旦新驱动出了诡异问题我可以五秒钟切回 fbdev 验证是软件问题还是硬件问题。这个习惯救了我很多次。最后再补充一句关于中文显示的点。如果你做的是面向国内用户的嵌入式设备中文字体问题一定要在选型阶段就考虑进去。说到底LCD 驱动只负责像素中文字形是用户态程序的责任。驱动移植做得再好如果字体文件没配套用户看到的依然是满屏的方块那时候再回头查驱动就是浪费时间了。希望这篇实战记录能帮你顺利点亮自己的那块屏。嵌入式驱动开发的调试过程确实磨人但只要把 fbdev 和 DRM 这两条路线都摸透今后再遇到其他显示接口、其他分辨率的屏你都会发现万变不离其宗。
返回列表