3、显示驱动框架搭建:基于Linux DRM/KMS框架

好,咱们进入正题。屏幕点亮这件事,说简单也简单,说复杂也复杂。我刚开始做NXP平台时,以为只要把背光供电打开,屏幕就能显示东西。结果呢?黑屏。折腾了两天才发现,是DRM框架没初始化对。

这一章,我们就来聊聊怎么在NXP平台上,用Linux标准的DRM/KMS框架把屏幕点亮。说白了,就是让内核知道「我有块屏幕,它长这样,你要往这里画图」。

3.1 DRM/KMS是什么?

DRM全称是Direct Rendering Manager,KMS是Kernel Mode Setting。这两个东西加在一起,就是Linux下显示系统的标准方案。

你想想看,以前嵌入式Linux做显示,各家有各家的驱动,互不兼容。今天换块屏,驱动得重写。后来大家觉得这样不行,就搞了DRM/KMS这个统一框架。

我个人习惯把DRM/KMS理解成三层:

  • 底层:硬件驱动层,负责和具体的显示控制器打交道
  • 中间层:DRM核心层,提供统一的API接口
  • 上层:用户空间,应用程序通过ioctl调用显示功能

说白了,你写应用的时候,根本不用关心底层是NXP的i.MX系列还是瑞萨的R-Car。只要调DRM的API,就能干活。

核心概念:DRM/KMS把显示流程抽象成了几个对象——CRTC(显示控制器)、Encoder(编码器)、Connector(连接器)、Plane(图层)、Framebuffer(帧缓冲)。

3.2 NXP平台的显示硬件特点

NXP的i.MX系列,显示控制器叫LCDIF(LCD Interface)或者eLCDIF。不同型号略有差异,但核心逻辑差不多。

我记得第一次看i.MX8M的参考手册时,被那些寄存器搞得头大。但其实你不用管那么多,因为DRM驱动已经帮你封装好了。

这里有个关键点:NXP的显示控制器支持多种接口类型:

接口类型 说明 常见场景
RGB并行 传统的RGB888/565接口 小尺寸LCD屏
LVDS 低压差分信号 工业屏、大尺寸屏
MIPI DSI 移动设备标准接口 平板、车载
HDMI 高清多媒体接口 外接显示器

嗯,这里要注意:不同的接口,设备树里的配置方式不一样。我踩过最大的坑就是LVDS屏的时序配置,差一个像素的HFP值,屏幕就是花的。

3.3 设备树配置

在NXP平台上,显示驱动的初始化从设备树开始。设备树就是告诉内核:「我这块板子上接了什么样的屏幕,用哪个接口,时序参数是多少」。

来看一个典型的i.MX8M Mini设备树配置:

&lcdif: lcdif@32e00000 {
    compatible = "fsl,imx8mm-lcdif";
    reg = <0x32e00000 0x10000>;
    interrupts = <GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk IMX8MM_CLK_LCDIF_PIXEL>,
             <&clk IMX8MM_CLK_DISP_AXI>,
             <&clk IMX8MM_CLK_DISP_APB>;
    clock-names = "pix", "axi", "apb";
    assigned-clocks = <&clk IMX8MM_CLK_LCDIF_PIXEL>;
    assigned-clock-parents = <&clk IMX8MM_CLK_SYS_PLL2_100M>;
    assigned-clock-rates = <59400000>;
    status = "disabled";
};

这段代码里,我特别想强调assigned-clock-rates这个参数。它决定了像素时钟的频率,直接影响到屏幕刷新率。

小技巧:像素时钟的计算公式是:pix_clk = (H_total × V_total × refresh_rate)。比如1024×600@60Hz的屏,H_total=1344,V_total=635,算出来就是1344×635×60≈51.2MHz。我一般会留点余量,设成52MHz左右。

然后是显示输出节点的配置。以MIPI DSI为例:

&mipi_dsi: mipi_dsi@32e10000 {
    compatible = "fsl,imx8mm-mipi-dsi";
    reg = <0x32e10000 0x1000>;
    clocks = <&clk IMX8MM_CLK_DSI_PHY_REF>,
             <&clk IMX8MM_CLK_DSI_AHB>;
    clock-names = "phy_ref", "ahb";
    status = "disabled";

    panel@0 {
        compatible = "ilitek,ili9881c";
        reg = <0>;
        pinctrl-0 = <&pinctrl_mipi_dsi_en>;
        enable-gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>;
        reset-gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>;
        backlight = &&backlight>;
        status = "okay";
    };
};

这里有个细节:enable-gpiosreset-gpios的顺序不能搞反。我曾经在项目中把这两个引脚接反了,结果屏幕死活不亮,查了两天才发现是GPIO顺序问题。

3.4 驱动初始化流程

设备树配好之后,内核启动时就会加载DRM驱动。整个初始化流程大概是这样的:

  1. probe阶段:驱动检测到设备树中的LCDIF节点,开始初始化硬件
  2. 组件绑定:把CRTC、Encoder、Connector这些组件注册到DRM核心
  3. 模式设置:读取屏幕的EDID(如果有)或者设备树中的时序参数
  4. Framebuffer创建:分配显存,创建默认的帧缓冲
  5. 输出使能:打开背光,开始刷新画面

我个人习惯在调试时,把drm.debug=0x1e加到内核启动参数里。这样内核会打印出详细的DRM日志,方便定位问题。

注意:如果屏幕没有显示,先检查dmesg里有没有「drm: failed to initialize」之类的错误。我遇到过最坑的一次,是内核编译时没把DRM驱动编进去,结果折腾了半天设备树。

3.5 用户空间的测试

驱动初始化成功后,怎么验证屏幕亮了?最简单的方法是用modetest工具。

# 查看可用的显示设备
modetest -M imx-drm

# 强制输出测试画面
modetest -M imx-drm -s 32:1024x600-60

modetest会输出彩条或者棋盘格,如果屏幕显示了,说明驱动框架搭建成功。

如果没显示,别慌。按这个顺序排查:

  • 先量背光供电,看有没有电压
  • 再量像素时钟,看频率对不对
  • 然后用示波器看数据线上的信号
  • 最后检查设备树里的GPIO配置

我曾经在一个项目里,屏幕怎么都不亮,最后发现是背光使能引脚的上拉电阻没焊。硬件问题有时候比软件问题更隐蔽。

3.6 小结

这一章我们走了一遍DRM/KMS框架的搭建流程。从设备树配置到驱动初始化,再到用户空间验证,每一步都有坑,但每一步也都有规律可循。

下一章,我们会深入Framebuffer的管理,看看怎么在屏幕上画出真正的图形。到时候我会分享一个我踩过的性能优化的大坑,保证让你少走弯路。