4、MIPI显示驱动框架:Linux DRM框架简介、MIPI DSI主机控制器驱动、面板驱动结构、设备树配置要点

好,咱们进入第四章。这一章我打算聊聊MIPI显示驱动的软件框架。说实话,很多工程师在调MIPI屏的时候,硬件上信号都量对了,但屏幕就是点不亮。问题出在哪?多半是驱动框架没搞明白。

Linux下的显示驱动,现在基本都围绕DRM框架来展开。你想想看,如果没有一个统一的框架,每个芯片厂都自己搞一套,那驱动工程师得疯掉。DRM就是来解决这个问题的。

4.1 Linux DRM框架简介

DRM全称是Direct Rendering Manager,直接渲染管理器。它最早是为图形加速设计的,后来慢慢把显示输出也管起来了。

我个人习惯把DRM框架分成三层来看:

  • 最底层:跟硬件打交道的部分,比如GPU、Display Controller的寄存器操作
  • 中间层:DRM核心层,提供统一的接口抽象
  • 最上层:用户空间接口,通过/dev/dri/card0这样的设备节点暴露给应用

对于MIPI显示来说,我们最关心的是DRM框架中的这几个核心对象:

对象 作用 我踩过的坑
CRTC 显示控制器,负责扫描输出 曾经搞混了CRTC和Encoder的绑定关系,画面一直偏移
Encoder 编码器,将像素数据转换成接口信号 DSI Encoder的初始化顺序搞反,导致时钟不稳定
Connector 连接器,代表物理接口 Connector的检测状态没处理好,热插拔失效
Panel 面板抽象,管理上下电时序 这个坑最多,后面细说

核心要点:DRM框架的本质是「把显示链路拆成一个个组件,每个组件只管自己的事」。CRTC只管产生时序,Encoder只管协议转换,Panel只管面板本身。这样设计的好处是——换一块屏,你只需要改Panel驱动,其他都不用动。

4.2 MIPI DSI主机控制器驱动

DSI主机控制器,说白了就是SoC内部那个负责发送MIPI数据的硬件模块。不同的芯片厂实现不一样,但核心逻辑是相通的。

我调过好几款不同厂家的DSI控制器,发现它们都绕不开这几件事:

  1. 初始化PHY:设置HS时钟频率、LP模式下的电压摆幅
  2. 配置DPI接口:告诉控制器从CRTC拿数据时用什么格式
  3. 设置视频模式或命令模式:这个选择直接影响帧率和功耗
  4. 处理LP和HS模式切换:每次发送数据都要考虑要不要切模式

来看一段典型的DSI主机控制器初始化代码,我简化了一下:

static int dsi_host_init(struct mipi_dsi_host *host)
{
    struct dsi_private *priv = host_to_priv(host);
    
    // 1. 先复位控制器
    writel(DSI_SOFT_RESET, priv->base + DSI_CTRL);
    udelay(10);
    
    // 2. 配置时钟
    // 这里要根据面板的规格计算分频系数
    u32 clk_div = dsi_calc_clk_div(priv->panel->mode.clock);
    writel(clk_div, priv->base + DSI_CLK_CTRL);
    
    // 3. 设置DPI输入格式
    // 我习惯用RGB888,兼容性最好
    writel(DSI_DPI_24BPP, priv->base + DPI_CFG);
    
    // 4. 配置视频模式参数
    // HSA, HBP, HFP这些时序参数要跟面板匹配
    dsi_set_video_timing(priv, &priv->panel->mode);
    
    return 0;
}

个人经验:初始化DSI控制器时,最容易出问题的是时钟配置。我曾经遇到一个案子,屏的规格书要求500MHz的HS时钟,但我们的SoC最高只能跑到450MHz。后来发现其实可以用4条lane来降低单lane速率。所以遇到时钟不够的情况,先别急着换芯片,看看能不能多开几条lane。

4.3 面板驱动(Panel Driver)结构

面板驱动,这是MIPI调试中最容易出幺蛾子的地方。为什么?因为每块屏的初始化序列都不一样,而且这些序列往往藏在屏厂给的Excel表格里,你得一个一个手动翻译成代码。

Linux内核里,面板驱动通常基于drm_panel结构体来实现。核心接口就这几个:

  • prepare():上电,拉高复位引脚
  • enable():发送初始化命令,开启显示
  • disable():关闭显示,进入休眠
  • unprepare():断电

嗯,这里要注意,prepareenable是分开的。为什么这么设计?因为有些场景需要先准备好电源,但延迟一会儿再发命令。比如有些屏要求VCC稳定后等120ms才能发初始化序列。

来看一个实际的面板驱动片段:

static int panel_simple_prepare(struct drm_panel *panel)
{
    struct panel_simple *p = to_panel_simple(panel);
    int err;
    
    // 1. 先供VCC
    err = regulator_enable(p->supply);
    if (err)
        return err;
    
    // 2. 等100ms,让电源稳定
    msleep(100);
    
    // 3. 拉高复位引脚
    gpiod_set_value_cansleep(p->reset_gpio, 1);
    msleep(20);
    
    return 0;
}

static int panel_simple_enable(struct drm_panel *panel)
{
    struct panel_simple *p = to_panel_simple(panel);
    
    // 发送初始化命令序列
    // 这些命令来自屏厂的规格书
    mipi_dsi_dcs_write_seq(p->dsi, 0x11, 0x00);  // 退出休眠
    msleep(120);
    mipi_dsi_dcs_write_seq(p->dsi, 0x29, 0x00);  // 开启显示
    
    return 0;
}

避坑指南:我曾经遇到一块屏,按照规格书发了初始化序列,但屏幕就是花屏。折腾了两天,最后发现是复位引脚的时序不对。规格书上写的是「RESET low for at least 10us」,但我只拉了5us。有些屏厂给的时序参数是「建议值」,不是「最小值」,你得自己试出最稳妥的值。

4.4 设备树配置要点

设备树,说白了就是告诉内核「你的硬件长什么样」。对于MIPI显示来说,设备树配置直接决定了驱动能不能正确初始化。

我总结了几条必须配对的设备树节点:

  1. DSI控制器节点:描述SoC内部的DSI主机
  2. 面板节点:描述外接的LCD屏
  3. 端口连接:把DSI控制器和面板连起来

来看一个完整的设备树配置示例:

&dsi0: dsi@ff450000 {
    compatible = "rockchip,rk3399-mipi-dsi";
    reg = <0x0 0xff450000 0x0 0x10000>;
    clocks = <&cru SCLK_MIPIDPHY_CFG>,
             <&cru PCLK_MIPI_DSI0>;
    clock-names = "ref", "pclk";
    
    // 关键:端口连接
    ports {
        #address-cells = <1>;
        #size-cells = <0>;
        
        port@0 {
            reg = <0>;
            dsi0_in: endpoint {
                remote-endpoint = <&vopb_out_dsi0>;
            };
        };
        
        port@1 {
            reg = <1>;
            dsi0_out: endpoint {
                remote-endpoint = <&panel_in>;
            };
        };
    };
    
    // 面板节点挂在DSI控制器下面
    panel@0 {
        compatible = "boe,tv080wum-nl0";
        reg = <0>;
        enable-gpios = <&gpio1 13 GPIO_ACTIVE_HIGH>;
        reset-gpios = <&gpio4 29 GPIO_ACTIVE_LOW>;
        
        port {
            panel_in: endpoint {
                remote-endpoint = <&dsi0_out>;
            };
        };
    };
};

配置要点

  • remote-endpoint必须成对出现,一个输出对应一个输入
  • GPIO的极性要跟硬件设计一致,我见过太多因为ACTIVE_HIGH/ACTIVE_LOW搞反而点不亮的
  • 时钟频率最好在设备树里显式指定,不要依赖驱动自动计算

最后说一句,设备树配好后,可以用dtc工具反编译检查。我每次都会跑一遍dtc -I dtb -O dts,看看有没有语法错误。这个习惯帮我避免了好几次低级错误。

调试小技巧:如果屏幕点不亮,先别急着改代码。在设备树里把status = "okay";改成status = "disabled";,然后看内核日志。如果日志里连DSI控制器的probe都没调用,说明设备树根本没匹配上。这时候去查compatible字符串,八成是拼写错了。

好了,这一章的内容就这些。DRM框架、DSI控制器、面板驱动、设备树,这四个东西串起来就是MIPI显示驱动的完整链路。下一章我会讲具体的调试方法和工具使用,到时候咱们拿示波器量信号,拿逻辑分析仪抓数据,把那些玄学问题一个一个揪出来。