3. Linux DSI 驱动框架:DRM/KMS 架构简介、MIPI DSI 主机控制器驱动、MIPI DSI 设备(Panel)驱动、DTS 设备树配置

好,我们进入第三章。这一章是整个DSI驱动开发的基石。说白了,你得先搞清楚Linux显示子系统是怎么组织的,然后才知道你的DSI驱动该往哪儿插。

我刚开始接触这块时,也觉得很乱。DRM、KMS、Framebuffer、V4L2……一堆缩写。但别怕,我们一层层剥开。

3.1 DRM/KMS 架构:显示子系统的“总指挥部”

DRM(Direct Rendering Manager)是Linux内核的显示框架。KMS(Kernel Mode Setting)是它的一部分,负责设置显示模式、分辨率、刷新率这些。

你可以把DRM/KMS想象成一个总指挥部。它不关心具体硬件怎么操作,只定义了一套标准接口。你的DSI驱动,就是这套接口下的一个“执行单元”。

核心概念:DRM/KMS 将显示流程抽象为几个关键对象:CRTC(显示控制器)、Encoder(编码器)、Connector(连接器)、Plane(图层)、Bridge(桥接芯片)。DSI 主机控制器驱动通常扮演 Encoder 或 Bridge 的角色,而 Panel 驱动则对应 Connector。

为什么要有这么多层?你想想看,一个SoC可能同时支持HDMI、DP、DSI、LVDS多种输出。如果没有统一抽象,每个驱动都得自己管所有事,那代码就乱套了。

我个人习惯把DRM/KMS的驱动开发分为两步:

  1. 实现底层硬件操作(寄存器读写、时钟配置、时序控制)
  2. 注册到DRM框架(填充回调函数、绑定组件)

下面这张图展示了DRM/KMS框架与DSI驱动的关系:

DRM/KMS 框架与 DSI 驱动关系图 用户空间 (Weston / SurfaceFlinger) DRM Core / KMS API 内核驱动层 CRTC (显示控制器) 时序生成 / VBLANK Encoder (编码器) DSI 主机控制器 Connector (连接器) Panel 驱动 Bridge 桥接芯片 硬件层 (DSI PHY / DPHY / Panel IC) 说明: DSI 主机控制器驱动通常实现为 Encoder 或 Bridge,Panel 驱动实现为 Connector。 CRTC 负责生成时序,数据流经 Encoder 编码为 DSI 包,最终由 Panel 显示。

3.2 MIPI DSI 主机控制器驱动

主机控制器驱动,就是SoC内部那个DSI IP核的驱动。它负责把DRM/KMS传过来的图像数据,打包成MIPI DSI协议包,通过D-PHY物理层发出去。

这个驱动要干的事不少:

  • 初始化D-PHY:配置时钟、电压、阻抗匹配
  • 配置DSI协议层:设置虚拟通道、数据类型、包格式
  • 实现传输回调:发送长包(图像数据)、短包(命令)
  • 中断处理:处理TE(Tearing Effect)信号、错误报告

我的经验:很多SoC的DSI主机控制器都基于Synopsys DesignWare IP。如果你用的是这个IP,内核里已经有现成的 dw-mipi-dsi 驱动。你只需要写一个平台驱动,把寄存器基址、时钟、中断传进去就行。别自己从头造轮子。

主机控制器驱动的核心数据结构是 struct mipi_dsi_host_ops:

struct mipi_dsi_host_ops {
    int (*attach)(struct mipi_dsi_host *host,
                  struct mipi_dsi_device *dev);
    int (*detach)(struct mipi_dsi_host *host,
                  struct mipi_dsi_device *dev);
    ssize_t (*transfer)(struct mipi_dsi_host *host,
                        const struct mipi_dsi_msg *msg);
};

这三个回调是必须实现的。attach 和 detach 管理Panel设备的绑定,transfer 负责实际的数据传输。我曾经在一个项目里,因为 transfer 函数没处理好超时,导致开机偶尔花屏。后来加了超时重传机制才稳定。

3.3 MIPI DSI 设备(Panel)驱动

Panel驱动,就是屏幕本身的驱动。它不关心数据怎么从SoC传过来,只关心怎么让屏幕亮起来、显示正常。

Panel驱动的核心工作:

  1. 上电时序控制:先供电、再复位、最后发初始化命令
  2. 初始化序列:通过DCS命令配置屏幕参数(伽马、对比度、扫描方向)
  3. 背光控制:PWM调光或通过DCS命令调光
  4. 休眠/唤醒:实现低功耗模式

内核提供了 struct drm_panel 和 struct mipi_dsi_device 两个结构体来抽象Panel驱动。前者是DRM框架的视角,后者是MIPI DSI总线的视角。

static const struct drm_panel_funcs panel_funcs = {
    .unprepare = panel_unprepare,
    .prepare = panel_prepare,
    .enable = panel_enable,
    .disable = panel_disable,
    .get_modes = panel_get_modes,
};

注意:prepare 和 enable 是有区别的。prepare 负责上电和初始化,enable 负责启动显示。反过来,disable 停止显示,unprepare 断电。顺序不能乱,否则屏幕可能点不亮或者烧坏。

我记得有一次调试,屏幕一直闪。查了半天,发现是 prepare 里发了初始化命令后没等足够延时,Panel还没准备好就开始传数据了。加了个msleep(120)就好了。

3.4 DTS 设备树配置

设备树是Linux嵌入式系统的“硬件描述语言”。你的DSI主机控制器和Panel,都得在DTS里声明。

一个典型的DSI设备树配置长这样:

&dsi0 {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;

    panel@0 {
        compatible = "boe,tv101wum-nl6";
        reg = <0>;
        reset-gpios = <&gpio3 6 GPIO_ACTIVE_LOW>;
        backlight = <&backlight>;
        pinctrl-names = "default";
        pinctrl-0 = <&panel_pins>;

        port {
            panel_in: endpoint {
                remote-endpoint = <&dsi0_out>;
            };
        };
    };

    ports {
        port@1 {
            reg = <1>;
            dsi0_out: endpoint {
                remote-endpoint = <&panel_in>;
            };
        };
    };
};

这里有几个关键点:

  • compatible:驱动匹配的关键字符串,必须和驱动里的 of_match_table 一致
  • reg:虚拟通道号,通常为0
  • reset-gpios:复位引脚,注意电平极性
  • remote-endpoint:连接关系,把主机控制器和Panel绑定起来

避坑指南:我曾经在DTS里把 reset-gpios 的极性写反了,结果屏幕一直处于复位状态,点不亮。查了两天才发现。所以,拿到原理图后,第一件事就是确认GPIO的有效电平。

另外,如果你的Panel需要额外的供电轨,也要在DTS里声明:

panel@0 {
    compatible = "boe,tv101wum-nl6";
    reg = <0>;
    vdd-supply = <&vcc3v3_lcd>;
    vddio-supply = <&vcc1v8_lcd>;
    reset-gpios = <&gpio3 6 GPIO_ACTIVE_LOW>;
    backlight = <&backlight>;
};

驱动里通过 devm_regulator_get() 获取这些供电,然后在 prepare 里使能它们。