ARTICLE DETAIL

资讯详情

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

Linux DRM modes 深度解析:从模式切换到黑屏排查实战

Linux DRM modes 深度解析:从模式切换到黑屏排查实战 写这篇东西的起因是最近又有同事抱着屏幕来找我说设备一启动就黑屏日志里只留下一句编译期自带的提示往上翻才看到一个很老的长尾问题starcraft was unable to switch video modes。很多人第一反应是显卡坏了其实这类问题背后几乎都指向同一个内核框架——Linux DRM以及它每天都在处理的“modes”这件事。如果你搜索“DRM”会发现网上同时存在两条完全不同的线索做嵌入式显示、搞内核驱动的人搜到的是 Direct Rendering Manager也就是 Linux 图形显示子系统做音响、调音台的人搜到的可能是某个数字激励器型号。本文主要讲前者把显示子系统里 modes 的来龙去脉、切换方式、常见踩坑场景一次说清。音频圈那个“DRM”我会在最后一节单独做下区分免得大家翻资料时被绕晕。1. 先把概念捋顺DRM 里的 modes到底指什么1.1 一句话说清 DRM 和 modes 的关系DRMDirect Rendering Manager是 Linux 内核里的显示子系统它向下对接 GPU 和显示控制器向上给用户空间提供一套标准的接口。你可以把它理解成显示设备的“总调度室”应用程序说我要一块画布、我要切换分辨率、我要开启垂直同步DRM 负责把这些请求翻译成硬件寄存器操作并按流程验证这一套动作是否合法。而 modes 就是“显示模式”它描述的是显示设备在某一时刻的输出形态分辨率、刷新率、像素时钟、行场同步参数、扫描方式等等。你平时说的 1080p60、720p30、4K120在 DRM 里都被组织成一个结构体叫drm_mode_modeinfo。显示器最终显示什么内容本质上就是由一个或多个 mode 决定的。这两者的关系可以打个比方DRM 是整个“展览馆管理系统”modes 是展馆内每一种“灯光/幕布/进场通道”的组合方案。展览馆要接待不同规模的参观者就得切换不同方案显示系统要适配不同屏幕也得切换不同 mode。1.2 显示模式长什么样分辨率、刷新率、时序三位一体很多人以为模式就是“宽 x 高”比如 1920x1080。其实远没这么简单。决定一个画面能不能正确显示至少涉及三个层面有效像素区域水平显示多少像素、垂直显示多少行也就是 hdisplay 和 vdisplay。消隐区域每行扫描的空白间隔、每帧的空白行这些被称作 hsync、vsync 的起止位置以及 htotal、vtotal。时钟频率像素时钟pixel clock单位通常是 kHz代表每秒要送多少个像素点。时钟太高面板接收端跟不上时钟太低画面会闪烁或者压缩。在 DRM 源码里一个 mode 的结构体大概长这样struct drm_mode_modeinfo { __u32 clock; /* 像素时钟单位 kHz */ __u16 hdisplay; /* 水平有效像素 */ __u16 hsync_start; __u16 hsync_end; __u16 htotal; /* 水平总像素含消隐 */ __u16 hskew; __u16 vdisplay; /* 垂直有效行 */ __u16 vsync_start; __u16 vsync_end; __u16 vtotal; /* 垂直总行 */ __u32 vrefresh; /* 刷新率整数值根因是各模式实际刷新率可能不为整数 */ __u32 flags; ... char name[32]; };你看到这个名字里只带“1920x1080”但内核计算刷新率用的是clock / (htotal * vtotal)。以前有人手工调设备树时序只改了 hdisplay 和 vdisplay没动 htotal、vtotal结果刷新率算出来是 59.94Hz 还是 60Hz 完全看命严重的直接导致画面抖动。1.3 模式的来源EDID、panel-simple 和自定义 modelinemode 不是内核凭空捏造的主要有三个来源显示器的 EDIDExtended Display Identification DataHDMI、DP、VGA 接口的显示器会把自己支持的模式列表和物理尺寸等信息通过 I2C 发出来。DRM 收到 EDID 后调用drm_add_edid_modes()解析成标准 mode 列表。板级面板信息嵌入式场景下MIPI DSI、LVDS 这类接口的屏通常没有 EDIDkernel 只能靠设备树里panel-timing节点或panel-simple驱动内置的display_timing结构来构造 mode。用户自定义 modelineX11 时代可以用cvt或gtf生成自定义时序然后通过 xrandr 加进去DRM 侧也有DRM_IOCTL_MODE_ADD_FB等扩展调用不过实际生产环境里的嵌入式设备多数改的是设备树。搞懂 mode 的来源之后你才能判断“为什么我的屏幕只能输出某个分辨率”这类问题自带 EDID 的显示器模式基本是显示器说了算自带设备树的屏模式是写死在配置里的。接下来我们要深入到内核侧看 DRM 是怎么处理这些 mode 对象的。2. 内核侧的核心对象Connector、Encoder、CRTC 与 Display Mode2.1 四个角色各干各的活DRM 在内部把显示链路抽象成四个角色它们协作起来才能完成模式设置Connector物理接口HDMI、DP、DSI、LVDS 都属于 Connector。它维护着当前接入的设备状态以及设备支持的模式列表。Encoder信号编码器负责把 CRTC 输出的信号转换成面板需要的格式。比如 DSI 的 D-PHY 信号、LVDS 的差分信号通常就在这里处理。CRTCCathode Ray Tube Controller历史叫法现在是显示控制器。它负责根据 mode 里的时序去扫描显存生成行场同步信号和像素时钟。Plane显示图层。framebuffer 要先绑定到 Plane再由 CRTC 扫描输出。当你请求一个 mode 时DRM 要沿着Connector - Encoder - CRTC找到一条链路拿到当前 Connector 上所有可用的 mode再从里面挑一个匹配的设置到 CRTC 上。这个过程也被称为 modeset。2.2 mode 在驱动里是怎么流转的以常见的内部流程来说驱动程序会实现一组 helper 回调比如static int my_connector_get_modes(struct drm_connector *connector) { struct drm_display_mode *mode; // 从 panel 对象读取 display_timing drm_panel_get_modes(panel, connector); // 或解析 EDID drm_add_edid_modes(connector, edid); return 1; } static enum drm_mode_status my_connector_mode_valid(struct drm_connector *connector, struct drm_display_mode *mode) { // 检查像素时钟是否超出范围 if (mode-clock MAX_PIXEL_CLOCK_KHZ) return MODE_CLOCK_HIGH; return MODE_OK; }get_modes负责把可用的 mode 注册到 connector 的probed_modes列表里mode_valid负责过滤掉当前硬件跑不了的 mode。用户空间发起设置请求后内核会做完整的状态校验时钟范围、带宽、链接速率、显存尺寸是否匹配。任何一个环节说不整个 modeset 都会失败。这里我强烈建议嵌入式开发者多留意mode_valid。许多“分辨率设不上去”“切完黑屏”的问题根源就是mode_valid没有做足够严格的过滤或者设备树里的时序本身就超过面板规格驱动又没拦住。2.3 从设备树到用户态一个 LVDS/DSI 屏的 mode 从哪来嵌入式设备上典型的流程是设备树里定义一个 panel 节点里面有compatible、backlight、enable-gpios最关键的是显示时序。dsi0 { status okay; panel0 { compatible startek,kd070fht2; reg 0; backlight backlight; reset-gpios gpio1 10 GPIO_ACTIVE_LOW; panel-timing { clock-frequency 33000000; hactive 800; vactive 480; hback-porch 40; hfront-porch 40; hsync-len 48; vback-porch 29; vfront-porch 13; vsync-len 3; }; }; };内核的 panel driver 把它解析成drm_display_mode注册进 connector。用户在用户态用modetest -M imx-drm就能看到这个 800x480 模式。到这一步显示链路从设备树到用户态的 mode 通路就算打通了。3. 用户态实操查询、切换 modes 的标准姿势3.1 用 modetest 把当前模式扒干净libdrm 自带的 modetest 是排查 modes 问题的第一把工具。我以前调试 MIPI DSI 屏时第一步永远是modetest -M imx-drm -c这条命令会列出所有 connector 及其 mode 列表。重点关注带*号的当前模式以及该 connector 支持的preferred模式。如果发现模式列表为空问题基本出在get_modes回调常见原因分别是panel 的display_timing没解析对DSI 链路还没完成初始化panel 未注册成功EDID 读取失败I2C 上根本没有数据。3.2 xrandr 切换模式与背后 ioctl在 X11 环境下大家最常碰到的模式切换命令是xrandr --output HDMI-1 --mode 1920x1080 --rate 60表面看是 X server 在处理实际上 X server 最终要调用 DRM 的 ioctl 完成模式设置。xrandr 之前还有个老工具叫xfree86的modeline现在基本都用 xrandr。xrandr 的好处是会做一次用户态校验比如列出当前屏幕可用的 mode 列表防止你设一个硬件根本不支持的参数。但它也有局限X11 本身有自己的一套显示栈某些嵌入式环境用 Wayland 或纯 KMS 时xrandr 并不适用。3.3 一个完整的 DRM_IOCTL_MODE_SETCRTC 调用示例直接走 DRM ioctl 能让你完全绕开 X server这对测试裸环境或者做合成器时很有用。一个最小流程如下int fd open(/dev/dri/card0, O_RDWR); drmModeRes *resources drmModeGetResources(fd); drmModeConnector *connector drmModeGetConnector(fd, resources-connectors[0]); drmModeEncoder *encoder drmModeGetEncoder(fd, connector-encoder_id); drmModeCrtc *crtc drmModeGetCrtc(fd, encoder-crtc_id); drmModeModeInfo mode connector-modes[0]; int fb_id create_framebuffer(fd, mode.hdisplay, mode.vdisplay); drmModeSetCrtc(fd, crtc-crtc_id, fb_id, 0, 0, connector-connector_id, 1, mode);这里有几个容易出错的点帧缓冲的宽高必须和 mode 一致否则 SetCrtc 直接返回EINVAL。用 legacy ioctl 切换模式时planes上的缓冲可能没正确更新需要手动drmModePageFlip。多屏环境要注意 connector 的encoder_id可能为 0需要遍历 encoder 才能找到可用 CRTC。这部分是搞嵌入式和显示服务器的人绕不开的硬知识。熟悉这套调用链之后理解 X11/Wayland 的显示初始化逻辑就会轻松很多。4. 项目实战MIPI DSI 竖屏改横屏显示怎么改才不出黑屏4.1 竖屏 panel 与方向问题的根源嵌入式产品里经常遇到这样的事液晶屏的物理形态是竖屏比如 720x1280但产品外壳和 UI 设计需要横屏使用比如要显示成 1280x720 的桌面效果。很多人第一反应是“把设备树里的 hactive、vactive 交换一下”结果一开机就花屏或黑屏。原因在于物理屏的扫描方向是固定的面板的 gate 和 source 驱动决定了像素是从左到右、从上到下还是别的顺序。DPU显示处理单元输出的时序如果直接交换宽高只会让画面按错误方向扫描并不会自动完成“旋转”。正确的做法是在显示链路的某个环节做“旋转”而不是改 mode 宽高。4.2 修改设备树 panel-rotation 还是走 DRM 属性在 DRM 体系里旋转可以发生在多个层级panel 驱动级别在struct drm_panel或 connector 初始化时设置display_info.panel_orientation让上层知道物理屏方向。用户空间能看到panel orientation信息然后决定是否旋转 UI。CRTC/Plane 级别通过 rotation 属性比如DRM_MODE_ROTATE_90、DRM_MODE_ROTATE_270。这种方式是硬件或驱动把源帧缓冲的内容做旋转后再送给面板。应用/合成器级别Wayland 合成器、X11 compositor 可以在构图时直接旋转图层然后输出到竖屏模式上。大多数国产 MIPI DSI 方案最终落地是在设备树里开rotation属性panel0 { compatible example,720p-panel; rotation 90; /* 硬件扫描方向旋转 90 度 */ };然后驱动把 rotation 解析成 plane 的 rotation property。某些老驱动不支持硬件旋转只能靠 GPU 或软件合成这种场景下如果直接改设备树 rotation反而会出现性能骤降。务必先确认 SoC 的显示控制器是否支持显存地址偏移旋转。4.3 验证旋转后的模式配置旋转修改完成后我用modetest检查当前 CRTC 的模式和 plane rotation 属性modetest -M imx-drm -p重点关注 plane 的rotation属性值是否真的生效。有些驱动里旋转属性默认不暴露需要在drm_plane_create_rotation_property()里手动注册。另外要提醒一件事有些面板的初始化序列里包含“画面翻转/镜像”寄存器这部分通常控制在 panel driver 的 init code 里而不是 DRM 层。当你发现旋转后画面上下颠倒、左右镜像先别急着改 DRM rotation去查 panel 的初始化命令是不是带了0x36set address mode这类寄存器的值。竖屏改横屏的经验总结下来就一句mode 解决“分辨率对不对”rotation 解决“方向正不正”两者分开看待混一起改必踩坑。5. 模式切换报错排查从“Unable to switch video modes”聊起5.1 这个报错背后其实是一类模式切换问题老玩家玩星际争霸时大概率见过starcraft was unable to switch video modes这个弹窗。该报错发生在 DirectDraw 切换全屏模式时系统无法把显卡设置成游戏期望的分辨率、色深或刷新率。放到 Linux DRM 环境下类似的问题每天都在发生用户空间请求切换模式内核拒绝返回EINVAL然后应用崩溃或者黑屏。从根上看模式切换失败就四类原因请求的 mode 根本不存在或被拒绝mode_valid返回MODE_BAD。帧缓冲尺寸/格式与 mode 不匹配。CRTC 或 connector 处于忙状态资源被占用。驱动在切模式过程中初始化命令没跑完比如 DSI 面板还没unblank。5.2 排查手段dmesg、modetest、EDID遇到模式切换失败我一般按以下顺序排查先看内核日志dmesg | grep drm重点找[drm] failed to set mode或mode is not valid。再跑modetest -c看当前 connector 是否挂载了预期模式列表。如果列表为空说明get_modes有问题。如果是 HDMI/DP 屏抓取 EDID 原始数据判断是否解析异常edid-decode /sys/class/drm/card0-HDMI-A-1/edid如果是 DSI/LVDS检查设备树的panel-timing与屏规格书逐项对照hsync-len、hback-porch、vfront-porch 这些值差一个都可能算错刷新率。怀疑 CRTC 被占用时modetest -p看当前 crtc 和 plane 状态确认有没有其他进程正在使用显示资源。5.3 避坑清单别让用户一开全屏就黑屏从用户视角“屏幕黑一下再恢复”很正常但“黑屏后永远恢复不了”就是严重 bug。以下是我在实际项目里总结出的避坑清单现象可能原因排查/解法切分辨率后黑屏背光亮新 mode 与面板时序不匹配或平面没重新绑定检查 mode_valid确认时序参数回滚到上一个 mode切完分辨率花屏帧缓冲宽度未按 stride 对齐检查 framebuffer pitch使用drmModeAddFB2手动指定 stride切换全屏失败应用直接退出用户空间请求无效 mode在进程内先枚举 mode 列表选择 closest_matchDSI 屏偶尔切换失败Link training 时序不够或 panel 未 enable确认 DSI 时钟裕量加延时等待drm_panel_enable多屏时某一路切不动CRTC 已被占用或 Encoder 绑定冲突用modetest -p查看资源分配必要时解绑再绑定排查模式切换问题最重要的心态是“分而治之”先判定问题在用户态还是内核态再判定是时钟问题、时序问题还是面板状态问题。日志说话不要靠猜。6. 顺带澄清音频圈里也有个“DRM”——数字激励器和 DAM 的区别6.1 DRM 数字激励器和 DAM 数字激励器名字有点像别混前面说过搜索“DRM”时音频玩家会得到完全不同的结果。在音响、调音、吉他效果器领域存在一类设备叫“数字激励器”厂商缩写里可能出现 DRMDigital Rewiring Module或 DAMDigital Audio Exciter 等。严格来说这类设备的作用是对音频信号进行动态谐波处理让声音听起来更亮、更有“冲劲”常见于车载音响、KTV 前级、吉他效果链中。DRM 和 DAM 都属于“动态激励器/谐波增强器”这一大类但算法取向不同DRM 通常偏温和用于修复声音发闷的问题DAM 更强调动态增益控制瞬态处理更激进。具体到每个品牌参数差异就更大了。6.2 这两者在音频链路里的角色音频链路里数字激励器一般串在均衡器和功放之间作为“润色”环节。它不像均衡器那样直接改频响曲线而是通过增加谐波失真或动态压缩让最终听感变亮、变“贴脸”。如果你搜“DRM 数字激励器和 DAM 数字激励器的区别”大概率是想选一台前级效果器。这时别太纠结型号名称关键是听感取向和接口电平匹配。但如果你在搜 Linux 显示问题那就要回到本文的主线——Direct Rendering Manager 的 modes。同一个缩写两个圈子别用错搜索引擎。7. 个人经验补充关于 modes这些年我踩过的真正硬坑最后分享几个我在实际项目中反复踩过的“硬坑”能帮你省下大量排查时间。第一个是帧缓冲 stride 问题。之前在一台老 SoC 上切 1280x720 模式用户态用drmModeAddFB创建 framebuffer默认把 stride 算成 1280*45120但硬件要求 64 字节对齐那自然没问题。后来换到另一颗 SoC它要求 256 字节对齐如果还用 5120 就不满足STRIDE校验模式设置直接失败。这种问题看日志往往只有一行Invalid argument必须自己用drmModeAddFB2WithModifiers手动指定正确的 stride 和 modifier。第二个是 DSI 面板的初始化时序。切模式时必须保证 panel 已经完成prepare和enable不然你这边把 CRTC 切换到新模式那边屏其实还停在初始化前的状态画面直接灰屏。正确的做法是在 modeset 之前先调用drm_panel_prepare()切完再调用drm_panel_enable()。第三个是我个人最推荐的调试习惯写一个简单的 C 程序只调用drmModeGetResources - drmModeGetConnector - drmModeSetCrtc专门用来测试模式切换。它比 X11 和 Wayland 调试栈要干净得多遇到问题能直接定位到内核与面板驱动而不是被合成器干扰。我的工作站上至今保留着这个小工具后来帮同事排查过不下五次“切模式黑屏”问题。显示模式的本质是硬件时序与软件资源之间的一场精确博弈。表面上你只需要一个分辨率和刷新率实际上像素时钟、消隐、对齐、面板初始化、DRM 状态对象全都在参与决策。希望这篇把 DRM modes 从概念讲到实操的文章能让你下一次遇到黑屏、花屏、切模式失败时不再从零开始试错。
返回列表