
之前调一块 i.MX8M Mini 主板的显示驱动时U-Boot 阶段 logo 死活不出来kernel 起来之后 framebuffer 又正常折腾了两天最后发现是把 U-Boot 阶段的显示初始化和 kernel 阶段的 DRM 流程搅在了一起。从那以后我在看任何显示驱动问题之前都会先问一句这个问题发生在哪一层是 U-Boot 的简单 framebuffer 初始化还是 kernel 里完整的 DRM/KMS 驱动还是用户态的 weston/wayland 合成三层代码各有各的任务也各有各的坑混在一起查基本是在浪费时间。这篇先聊清楚偏底层的东西DRM 框架到底解决了什么问题、它在内核里的对象之间是什么关系、U-Boot 阶段虽然没有完整的 DRM但设备树解析和驱动 probe 又是怎么配合的。适合刚接触显示驱动、或者做过 framebuffer 开发但没系统捋过 DRM 的人。内容偏基础但也夹了一些我实际踩过的坑尤其是 MIPI DSI 竖屏改横屏这类时序问题在 U-Boot 阶段的表现和 kernel 阶段完全不一样值得单独拿出来说。1. 为什么内核显示驱动框架最后收敛到 DRM从 FBDEV 的局限性说起1.1 FBDEV 时代一个 fb_info 扛下所有浆糊一样的架构早年间写 Linux 显示驱动绕不开 framebuffer。核心数据结构就是struct fb_info里面塞了var、fix、fbops、screen_base一整套都在描述一块线性的显存和一个显示屏的时序。早期单屏、单图层、单分辨率场景下这套东西确实够用代码路径短应用层直接 mmap 后写像素就行。但显示系统一复杂就崩了。多屏输出的时候你要给每个屏幕注册一个 fb_info应用层还得自己搞清楚/dev/fb0、/dev/fb1分别对应哪个物理显示接口。更麻烦的是fbdev 没有统一的机制去管理显示控制器的硬件流水线像 layer blending、alpha、缩放、旋转这些能力在 fbdev 框架里基本靠私有 ioctl 去扩展。每个驱动各自为政接口风格五花八门上层框架根本没法做统一封装。我记得当时做多屏拼接光是对齐不同驱动的私有 ioctl 就花了两周。另一个核心痛点是 fbdev 不支持原子更新。修改分辨率或者切换显示模式时要先把fb_info-var里的参数改掉再通过fb_set_par触发底层重新计算寄存器。这个过程不可回滚一旦时序参数填得有问题屏幕直接花掉或者黑掉没有事务的概念。对于现代显示场景这个缺陷几乎是致命的。1.2 DRM 的组件化思路把显示流水线拆成可以拼接的积木DRMDirect Rendering Manager最早的起源是为了 GPU 渲染服务后来 KMSKernel Mode Setting被并入慢慢就变成了 Linux 上显示控制的通用框架。它最大的特点是把显示链路抽象成几个明确角色PLANE图层、CRTC显示控制器、ENCODER信号编码器、CONNECTOR物理接口。我常用生活里的例子解释这套模型PLANE 相当于一张张透明胶片CRTC 相当于投影仪ENCODER 相当于把投影仪信号转换成 HDMI/VGA 这类具体格式的转换头CONNECTOR 就是投影仪屁股上那个物理接口。你要让屏幕亮起来必须把这几个角色从软件层面依次串联最终形成一个完整的显示链路。这种组件化设计让驱动开发者的工作变得清晰每个硬件模块只关心自己在链路上的那一段。组件化的价值在 SoC 平台体现得最明显。同一个显示控制器可以接 HDMI、MIPI DSI、LVDS 多种接口如果用 fbdev 的做法每种组合都要写一套独立的驱动逻辑。而在 DRM 下CRTC 驱动只需要实现atomic_check和atomic_flushENCODER 驱动只需要关心时序信号转换CONNECTOR 驱动只需要管热插拔检测和 EDID 读取。各管一段然后通过drm_connector_attach_encoder这类接口把链路搭起来。1.3 DRM 与 FBDEV 最关键的差异原子提交与显存共享DRM 还有一个 FBDEV 完全不具备的机制Atomic Mode Setting。这是我在实际项目中感受最深的一点。atomic 的含义是把所有显示状态修改打包成一个事务对象通过drm_atomic_commit一次性提交硬件在 VBLANK 中断点切换要么全部生效要么全部不动不存在中间状态。这个机制解决了两类实际问题。第一是残影和撕裂用户态在 vsync 切配置不会出现上下两半屏幕分辨率不一致的情况第二是状态一致性多屏配置切换时如果只需要改某一个屏幕的参数其他屏幕的状态不会被波及。我在客户现场遇到过一个问题在 1080p 和 4K 两个显示器同时输出的环境下单独切 4K 分辨率会导致 1080p 那边也闪一下用 fbdev 查了三天无果迁移到 DRM atomic 后这个现象天然消失。显存管理方面DRM 通过 GEMGraphics Execution Manager提供缓冲区管理能力配合 DMA-BUF 可以实现跨设备共享内存。视频解码器输出的画面可以直接导入给 DRM 做显示合成不需要 CPU 拷贝一份。这在 fbdev 时代几乎不敢想——那时候要么走copy_to_user再write回去要么用VIDIOC_QBUF和FBIOPUT_VSCREENINFO各种土办法绕。DRM 加 DMA-BUF 之后零拷贝显示成了标准能力。2. DRM 的框架骨骼设备模型、对象拓扑与用户态接口2.1 drm_device 与 drm_driver 的注册流程打开任意一个 DRM 驱动的源码比如imx-drm、rockchip、msm初始化流程其实大同小异。驱动 probe 之后核心动作是分配struct drm_device填充struct drm_driver然后调drm_dev_register。从 4.7 内核开始推荐用drm_dev_allocdrm_dev_register两步走而不是老的drm_get_pci_dev那套。一个标准驱动的 probe 大致长这样static int my_drm_platform_probe(struct platform_device *pdev) { struct drm_device *drm; struct my_drm_private *priv; priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); drm drm_dev_alloc(my_drm_driver, pdev-dev); if (IS_ERR(drm)) return PTR_ERR(drm); platform_set_drvdata(pdev, drm); priv-drm drm; // 在这里做硬件资源映射、中断请求、对象注册 my_drm_setup_pipeline(drm, priv); my_drm_register_dsi(drm, priv); ret drm_dev_register(drm, 0); if (ret) goto err_unload; }这里面有个值得注意的演化方向新内核5.5越来越推荐用 devm_drm_dev_alloc 配合 DRM managed APIdrmm_*来管理生命周期作用是把 devm 的设备资源管理和 DRM 对象的释放统一到一起驱动卸载路径简单很多。老驱动里的drm_unload、drm_lastclose那些回调基本都废弃了新写的驱动没必要再学那套直接看drm/i915或者drm/vc4这类作为模板比看老书靠谱。2.2 KMS 对象拓扑CRTC、ENCODER、CONNECTOR 之间到底怎么连线对象注册本身不难难的是理解它们之间的拓扑。一个完整的 KMS 链路是用户态拿到一个 DRM framebuffer → 绑定到一个 PLANE → PLANE 绑定到一个 CRTC → CRTC 通过下游的 ENCODER → ENCODER 输出到 CONNECTOR。CRTC 对应的硬件是显示控制器Display Controller它负责从内存中取像素、做图层合成、产生时序信号。一个 SoC 通常有多个 CRTC比如 i.MX8M Mini 有两个显示控制器rockchip RK3568 有三个。ENCODER 负责把 CRTC 送出来的并行 RGB 信号转换成特定标准信号比如 HDMI 的 TMDS、MIPI DSI 的差分串行信号、LVDS 的信号。CONNECTOR 代表物理插口管理热插拔、EDID 和面板电源控制。我把这几者的关系整理成一张表便于对照对象硬件对应主要职责核心 APIPLANE图层/硬件 overlay关联 Framebuffer、做缩放旋转drm_plane_initCRTC显示控制器时钟、时序生成、图层合成drm_crtc_init_with_planesENCODER信号转换芯片格式转换、链路训练drm_encoder_initCONNECTOR物理接口热插拔、EDID、面板电源drm_connector_init调试的时候有一个很实用的思路用户态通过drmModeGetResources拿到的所有 IDcrtc_id、plane_id、connector_id、encoder_id本质上就是这棵树的可视化映射你可以把它理解成内核树形对象模型的一个用户态快照。很多驱动 bug 最终会发现是mode_valid回调里没有拒绝某个 encoder/connector 组合导致链路拓扑非法用户态一配置就报错。2.3 GEM 与 libdrm 的角色划分用户态为什么不能直接写 ioctlDRM 内核侧再完整用户态调用还是得有个库来兜底这个库就是 libdrm。它的主要作用是封装 ioctl 细节把内核态的struct drm_mode_get_connector这类结构体转换成应用友好的drmModeConnectorPtr让上层不用关心字节序对齐之类的问题。我第一次看 libdrm 代码的时候比较困惑既然最终都是 ioctl为啥不直接 open/dev/dri/card0然后自己ioctl(fd, DRM_IOCTL_MODE_GETCONNECTOR, arg)后来发现 libdrm 的价值不止是封装它把内核对象模型映射成带引用的 C 对象处理了资源类型枚举、连接器信息缓存刷新、CRTC 属性快照等公共逻辑。更别说drmModePageFlip这类需要关注 DRM event 的接口底层还有对 DRM fd 事件循环的处理自己做这些纯属重复造轮子。对于写内核驱动的开发者来说对 libdrm 的掌握程度不用太深但建议把drmModeGetResources的调用流程走一遍因为它对应的正是内核端drm_mode_getresources的 ioctl 实现。你在内核里修一个链路拓扑 bug用户态怎么查就靠这套接口理解对齐很重要。另外drmDevice和drmVersion相关的接口在调试多 GPU 设备时也很有用可以区分节点编号确认走的是哪个 DRM 设备。3. U-Boot 阶段的显示驱动解析流程、数据结构与设备树约定3.1 U-Boot 没有 DRM那显示初始化到底是怎么发生的U-Boot 本身并没有完整的 DRM 框架没有drm_device也没有 KMS 对象。但现代 U-Boot 的显示驱动设计是跟着设备树走的而设备树中 display 相关节点的 bindings 恰恰就是 kernel DRM 的 bindings 格式。所以 U-Boot 里的显示初始化本质上是一个简化版的 DRM 驱动解析过程通过UCLASS_VIDEO这个 uclass 来管理所有视频输出设备。U-Boot 的 video 框架核心是struct video_priv和struct video_ops对应的 uclass driver 注册在drivers/video/video-uclass.c。它做的事情很直接为显存分配一片内存从设备树里读出面板的时序参数配置显示控制器寄存器把 logo 或 bmp 写到显存然后就没有然后了——没有合成器、没有图层混合、没有原子提交就是个能点亮屏幕的 framebuffer。这里有一个容易误解的点U-Boot 的 video 驱动其实也会去看设备树里的compatible比如simple-panel、panel-dsi这类节点也会执行 panel 的 probe初始化 backlight 和 reset gpio。所以它虽然在概念上没有 DRM/KMS但从设备树解析的角度看它跟 kernel 侧是「同一套语言」。你在 kernel 里通过修改设备树来适配面板U-Boot 阶段大概率也要同步改否则会出现「U-Boot logo 不亮kernel 正常」或者反过来「U-Boot 正常kernel 黑屏」的割裂问题。3.2 设备树解析链路从 display 节点到 panel probe 的具体流程U-Boot 显示设备树解析从video_bind开始遍历compatible匹配到的 video 设备然后调video_probe。这个 probe 里会做几件事第一通过dev_read_prop读取display-timings节点如果读到native-mode对应的 timing 子节点就解析出clock-frequency、hactive、vactive、hfront-porch、hback-porch等参数第二找panel或panel-simple这类节点触发 panel 驱动的 probe第三初始化背光。一段典型的 U-Boot 设备树显示节点如下lcdif { display-timings { native-mode timing0; timing0: timing0 { clock-frequency 67500000; hactive 1280; vactive 800; hfront-porch 48; hback-porch 80; hsync-len 32; vfront-porch 3; vback-porch 14; vsync-len 4; hsync-active 0; vsync-active 0; de-active 0; pixelclk-active 0; }; }; port { display_out: endpoint { remote-endpoint panel_ep; }; }; }; panel { compatible panel-simple; backlight backlight; enable-gpios gpio1 13 GPIO_ACTIVE_HIGH; port { panel_ep: endpoint { remote-endpoint display_out; }; }; };U-Boot 里驱动解析这些属性用的接口跟 kernel 高度相似比如dev_read_u32(pdev-dev, clock-frequency, freq)或者读取 gpio 描述符用gpio_request_by_name。我在排查问题时常用的一招是在 U-Boot 命令行里用dm tree看 video 节点和 panel 节点是否成功 probe再用dm uclass看UCLASS_VIDEO设备列表。如果 panel 节点没被 probe第一反应不是去查时序代码而是查该节点的compatible是否在 U-Boot 的drivers/video/panel-simple.c里有匹配项。3.3 MIPI DSI 竖屏改横屏的时序问题解析思路与调整点这个点我专门拿出来讲是因为它在实际项目中极其常见也是「DRM 显示」和「U-Boot 显示」行为差异最明显的地方。搜索热词里就有mipi dsi drm竖屏改横屏显示说明很多人卡在这。竖屏改横屏本质上是把分辨率从竖的比如 720x1280变成横的1280x720。在内核 DRM 侧可以通过配置 CONNECTOR 的rotation属性由硬件层的drm_plane_state做旋转或者修改 panel 的timing参数并通知 DSI 控制器重新计算行场参数。但在 U-Boot 阶段没有rotation属性这套机制U-Boot 就是傻子一样按 timing 里面的 hactive 和 vactive 去配置寄存器。所以如果你只是改了 U-Boot 设备树里 timing 的hactive和vactive很多 MIPI DSI 屏幕并不会正确旋转显示。因为 DSI 面板本身有自己的行列扫描方向控制器输出的像素流是按行列逐点发送的简单地交换 h/v 可能导致图像发生镜像、错位甚至完全不显示。我处理的 RK 平台方案里竖屏改横屏需要在 DSI 控制器的寄存器层面对水平扫描顺序和垂直扫描顺序做额外处理通常是修改 HSA、HBP、VSA、VBP 的计算方式让 DSI 打包的 packet 符合面板横屏时序要求。一个值得推荐的排查链条是先在 U-Boot shell 下用printenv检查 video 相关的环境变量然后加载一个测试用的 bmp 图片观察显示出来的图案是镜像、错位、还是撕裂。如果是镜像说明 DSI 的hori_orientation或line_stride方向反了如果是错位多半是因为时序参数与实际 DSI 面板要求的hactive不一致如果是撕裂可能只是 pclk 频率没配好导致像素按错误的节奏被推出。搞清楚这三种表现对应哪种原因能省下一大半 debug 时间。4. 一次 U-Boot 阶段黑屏问题的排查复盘定位与修复4.1 现象描述与初步判断还是回到开头那个 i.MX8M Mini 的项目。客户反馈的现象是量产机器上偶尔有 5% 左右的板子在 U-Boot 阶段 logo 不显示但 kernel 起来之后显示完全正常。这批板子拿到手上第一反应是怀疑面板个体差异于是换了新的屏模组结果故障依旧。然后把 U-Boot 版本回退到老版本问题消失——这就说明问题不在硬件而在 U-Boot 的显示初始化逻辑或设备树解析。我的排查习惯是先收集信息看 U-Boot 启动日志里有没有报错。在那块板子上串口输出里根本没有 video 相关的 probe 失败信息只有一段video: failed to get timing node之类的提示被淹没在启动 log 里不仔细看真发现不了。这个提示说明 U-Boot 在解析设备树时没能正确找到display-timings节点。4.2 逐层确认设备树、时序参数、背光控制与上下电时序第一件事是确认 U-Boot 用的设备树和 kernel 用的设备树是不是同一份。很多 BSP 会把 U-Boot 的设备树单独存放经过fdtgrep裁剪后只保留启动阶段需要的节点。结果发现客户在裁剪时display-timings节点因为引用的 phandle 关系不完整被裁剪掉了U-Boot 拿到的是一个残缺的显示节点解析不到 timing 就直接跳过了 video probe。这就是我在 3.2 节反复强调的点U-Boot 对设备树节点的解析是按名匹配和按 phandle 索引的任何一个中间节点的 phandle 丢失都可能导致整条解析链路断裂。kernel 侧因为使用完整的设备树所以没受影响。这个问题的修复手段也很直接把display-timings和panel相关节点手动加进 U-Boot 设备树的裁剪白名单重新编译后故障消失。第二件事是检查背光控制。即便 timing 解析正常如果enable-gpios配置成GPIO_ACTIVE_LOW但 U-Boot 驱动里按GPIO_ACTIVE_HIGH去拉高背光永远不会亮。这块板子之前遇到过因为 gpio descriptor flag 解析不对导致背光时序和 panel 使能时序冲突现象就是「有图像但看不见」。排查时用了逻辑分析仪抓 panel 的电源和复位信号发现复位信号拉低时间比规格书要求的 10ms 短导致面板内部电路没完成初始化背光虽亮但无图像。这种情况在 kernel 阶段可以通过panel-prepare和panel-enable两步逻辑拉开时序U-Boot 里很多驱动则把这两步合并了踩坑概率就更高。4.3 隐患U-Boot 通了kernel 还是会踩坑的常见情况U-Boot 阶段把显示调亮之后不等于 kernel 侧就万事大吉有几个隐藏的交接问题会在 kernel 启动时爆发。第一个常见情况是U-Boot 把显示控制器的时钟频率配置成跟面板不一致kernel 在 probe 时读取硬件寄存器状态做判断如果 kernel 驱动的初始化依赖 U-Boot 留下的状态就可能出现时钟没有按新参数重置的问题。这类问题表现往往是 kernel 起来后花屏重启几次又正常排查起来特别恼火。解决思路是在 kernel 的 DC 驱动resume或atomic_flush里做寄存器级的状态重建而不是依赖 U-Boot 留下的状态。第二个常见情况是U-Boot 阶段通过fdtdec_setup_memory或panel_simple修改了设备树中的某些属性比如填充了display-timings里的native-mode的clock-frequency但 kernel 的设备树还是原始值。两边信息不同步表现为同一块屏在 U-Boot 下用某个频率能亮进 kernel 后按另一个频率刷新导致闪烁或花屏。这种问题一定要把 U-Boot 的 fixup 逻辑查一遍重点看有没有往设备树里写入运行时参数如果有kernel 侧解析时要特别注意。5. 从 U-Boot 到 Kernel 的显示交接哪些状态要留、哪些必须重来5.1 内核会重新初始化显示U-Boot 留下的是「硬件现场」不是「配置现场」很多刚接触显示驱动的同事会有个错觉U-Boot 把屏幕点亮了kernel 起来就会接着用。实际上kernel 显示驱动的 probe 几乎总会把显示控制器和 DSI 链路重新初始化一遍。U-Boot 阶段设置的寄存器、配置的时序、打开的时钟到了 kernel 手里基本都会先关掉再重新配。U-Boot 阶段真正对 kernel 产生持久影响的只有少数几个东西内存中残存的显存内容、某些 SoC 特有的boot_fb机制保留的 framebuffer、以及可能通过 bootargs 传递的video参数。比如videoDSI-1:1280x720M60这种启动参数kernel 的 DRM 子系统会解析并尝试按这个模式配置。如果你在 U-Boot 改了 bootargs 里的显示参数而 kernel 驱动没有对应的 mode 校验逻辑就会直接报unknown mode错误。最稳妥的做法是U-Boot 阶段只保证最基本的光标和 logo 显示能力不做特殊策略kernel 侧的 DRM/KMS 驱动完全按设备树和 EDID 重新干活。这样两者解耦每个阶段的可维护性都高。5.2 经验建议U-Boot 阶段不要做过多的显示初始化特殊逻辑我见过最折腾的做法是在 U-Boot 里为了显示一个开机动画写了很长的自定义 panel 和 DSI 初始化序列比如读 EEPROM、做色彩校准、来回切换分辨率。结果 kernel 起来后 DSI 链路两边状态冲突屏幕要么不稳定闪烁要么直接黑屏。调试两周后发现几乎所有问题都出在 U-Boot 那套特殊初始化上。根据我做过的平台经验建议把 U-Boot 阶段的显示初始化限制在这么几件事上解析设备树时序参数配置显示控制器基本时钟初始化 DSI/LVDS/HDMI 物理链路的 PHY拉起背光然后显示 logo。所有跟面板色彩、Gamma、DSC 压缩、局部调光相关的功能全部留到 kernel 的 DRM 驱动里做。这不仅是架构上的洁癖更是维护性的需要——U-Boot 的显示代码本来就缺乏复杂的错误处理机制越简单越不容易在生产环境中翻车。另外有一个小技巧在调试 U-Boot 显示问题的时候尽量让 U-Boot 和 kernel 共用一份基准设备树只是在编译时用不同的裁剪配置。这样你在 U-Boot 阶段改动一个时序参数kernel 侧也能通过同一份源文件同步更新避免两边设备树最终内容分叉。我自己现在每改一个 panel 配置都是先跑 U-Boot 确认能亮再进 kernel 验证效果两边各花十分钟比在 kernel 侧反复 reboot 快得多。