3. 显示接口概览

MIPI DSI,说白了就是手机和平板上最主流的显示接口标准。iMX8平台集成了三个DSI控制器,每个都能驱动4 lane的屏幕。我最早接触这个接口时,总觉得它跟LVDS差不多,结果调试时踩了不少坑。

今天咱们就把它彻底讲透。从硬件信号到驱动代码,一步步来。

3.1 MIPI DSI协议基础

DSI有两种工作模式:命令模式和视频模式。命令模式适合智能屏(带GRAM的),视频模式适合普通屏(不带GRAM的)。

我个人习惯把DSI协议拆成三层理解:

  • 物理层:差分信号对,D0P/N、D1P/N、D2P/N、D3P/N,外加时钟对CLKP/N
  • 通道管理层:负责把数据分发到各个lane上
  • 协议层:定义包格式,包括短包(4字节)和长包(可变长度)

关键点:DSI的时钟频率决定了你能跑多高的分辨率。计算公式很简单:

Pixel Clock = H_total × V_total × Refresh_Rate

DSI Clock = Pixel Clock × Bits_per_Pixel / (Number_of_Lanes × 2)

举个例子:1080p@60fps,24bpp,4 lane,DSI时钟大约需要 223MHz。

3.2 iMX8 DSI控制器架构

iMX8的DSI控制器,内部其实是个复杂的IP核。我画了张图,帮你理清它的内部结构。

iMX8 MIPI DSI 控制器内部架构 APB总线接口 AXI总线接口 寄存器控制模块 FIFO缓冲模块 包解析/组装器 CRC校验模块 DSI PHY (D-PHY) 4 Lane输出 时序生成器 说明 APB用于寄存器配置,AXI用于高速图像数据传输。FIFO做跨时钟域缓冲,PHY负责串行化。

你看,数据从AXI总线进来,先经过FIFO缓冲,然后被包解析器拆成DSI包,加上CRC校验,最后通过PHY串行化发出去。寄存器控制模块负责配置所有参数。

3.3 驱动开发实战

好了,理论说完了,咱们直接上手写驱动。iMX8的DSI驱动,核心就三步:初始化PHY、配置时序、发送初始化序列。

3.3.1 设备树配置

先看设备树。我习惯把时序参数和屏参分开定义,这样换屏时只需要改一个文件。

/* imx8mq-evk.dts */
&mipi_dsi {
    status = "okay";
    clock-frequency = <223000000>;

    panel@0 {
        compatible = "boe,tv080wum-nl0";
        reg = <0>;
        pinctrl-0 = <&pinctrl_mipi_dsi_en>;
        enable-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>;

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

&mipi_dsi_out {
    remote-endpoint = <&panel_in>;
};

小技巧:clock-frequency这个参数,我建议你根据实际屏的规格书算一遍,别直接抄参考设计。我在一个项目里就吃过这个亏——参考设计写的是200MHz,但实际屏需要223MHz,结果画面一直闪。

3.3.2 PHY初始化

PHY初始化是DSI驱动里最麻烦的部分。iMX8的D-PHY需要配置PLL、偏置电流、阻抗校准等参数。

static int imx8_mipi_dsi_phy_init(struct mipi_dsi_device *dsi)
{
    struct imx8_mipi_dsi *priv = dev_get_drvdata(dsi->dev);
    u32 val;

    /* 1. 复位PHY */
    regmap_write(priv->regmap, DSI_PHY_RSTZ, 0x0);
    usleep_range(10, 20);

    /* 2. 配置PLL分频 */
    val = (priv->hs_clk_rate / 2) / 1000000;
    regmap_write(priv->regmap, DSI_PHY_PLL_CTRL, val);

    /* 3. 配置偏置电流 */
    regmap_write(priv->regmap, DSI_PHY_BIAS_CTRL, 0x1F);

    /* 4. 阻抗校准 */
    regmap_write(priv->regmap, DSI_PHY_IMPEDANCE_CTRL, 0x55);

    /* 5. 释放复位 */
    regmap_write(priv->regmap, DSI_PHY_RSTZ, 0x1);
    usleep_range(100, 200);

    /* 6. 等待PLL锁定 */
    regmap_read(priv->regmap, DSI_PHY_STATUS, &val);
    if (!(val & DSI_PHY_PLL_LOCK)) {
        dev_err(priv->dev, "PLL lock failed\n");
        return -EIO;
    }

    return 0;
}

嗯,这里要注意:PLL锁定时间跟温度有关。我在冬天调试时,PLL锁定需要150us,但夏天只需要80us。所以延时别写死,用轮询方式更靠谱。

3.3.3 时序配置

时序配置是DSI驱动里最容易出错的地方。每个参数都有严格的约束关系。

参数 描述 典型值(1080p@60) 约束
HACT 水平有效像素 1920 必须为4的倍数
HBP 水平后肩 148 ≥ 4
HFP 水平前肩 88 ≥ 4
HSA 水平同步宽度 44 ≥ 4
VACT 垂直有效行 1080 —
VBP 垂直后肩 36 ≥ 1
VFP 垂直前肩 4 ≥ 1
VSA 垂直同步宽度 5 ≥ 1
static void imx8_mipi_dsi_set_timing(struct imx8_mipi_dsi *priv,
                                     struct drm_display_mode *mode)
{
    u32 hsa, hbp, hfp, hact;
    u32 vsa, vbp, vfp, vact;

    hact = mode->hdisplay;
    hsa  = mode->hsync_end - mode->hsync_start;
    hbp  = mode->htotal - mode->hsync_end;
    hfp  = mode->hsync_start - mode->hdisplay;

    vact = mode->vdisplay;
    vsa  = mode->vsync_end - mode->vsync_start;
    vbp  = mode->vtotal - mode->vsync_end;
    vfp  = mode->vsync_start - mode->vdisplay;

    /* 写入DSI控制器寄存器 */
    regmap_write(priv->regmap, DSI_VID_HACT, hact);
    regmap_write(priv->regmap, DSI_VID_HSA,  hsa);
    regmap_write(priv->regmap, DSI_VID_HBP,  hbp);
    regmap_write(priv->regmap, DSI_VID_HFP,  hfp);

    regmap_write(priv->regmap, DSI_VID_VACT, vact);
    regmap_write(priv->regmap, DSI_VID_VSA,  vsa);
    regmap_write(priv->regmap, DSI_VID_VBP,  vbp);
    regmap_write(priv->regmap, DSI_VID_VFP,  vfp);
}

避坑指南:我曾经在一个项目里,HACT设成了1922(不是4的倍数),结果画面出现了奇怪的竖条纹。查了两天才发现是DSI控制器要求HACT必须4字节对齐。你想想看,1920是4的倍数,但1922不是,就这么一点偏差,画面就花了。

3.3.4 初始化序列发送

屏的初始化序列,通常由屏厂提供。我建议你把这些序列放到一个数组中,方便维护。

static const u8 panel_init_seq[] = {
    /* DCS命令:进入睡眠模式 */
    0x05, 0x11, 0x00, 0x00, 0x00,
    /* 延时120ms */
    0xFF, 0x78,
    /* DCS命令:设置像素格式为24bpp */
    0x15, 0x3A, 0x77, 0x00, 0x00,
    /* DCS命令:退出睡眠模式 */
    0x05, 0x11, 0x00, 0x00, 0x00,
    /* 延时120ms */
    0xFF, 0x78,
    /* DCS命令:打开显示 */
    0x05, 0x29, 0x00, 0x00, 0x00,
};

static int panel_send_init_seq(struct mipi_dsi_device *dsi)
{
    int ret, i = 0;

    while (i < ARRAY_SIZE(panel_init_seq)) {
        u8 cmd_type = panel_init_seq[i];

        if (cmd_type == 0xFF) {
            /* 延时命令 */
            u32 delay_ms = panel_init_seq[i + 1];
            msleep(delay_ms);
            i += 2;
            continue;
        }

        if (cmd_type == 0x05) {
            /* DCS短包 */
            ret = mipi_dsi_dcs_write(dsi, panel_init_seq[i + 1],
                                     NULL, 0);
            i += 5;
        } else if (cmd_type == 0x15) {
            /* DCS短包带参数 */
            ret = mipi_dsi_dcs_write(dsi, panel_init_seq[i + 1],
                                     &panel_init_seq[i + 2], 1);
            i += 5;
        }

        if (ret < 0)
            return ret;
    }

    return 0;
}

3.4 调试技巧

DSI驱动调试,说白了就是跟波形打交道。我总结了几条经验:

  1. 先看时钟:用示波器量CLKP/N,频率对不对?幅值够不够?
  2. 再看数据:用逻辑分析仪抓D0上的包,看初始化序列有没有发出去
  3. 最后看画面:如果画面有噪点,多半是时序不对;如果画面偏移,多半是HBP/HFP算错了

调试利器:iMX8的DSI控制器有个debug模式,可以把内部状态通过GPIO输出。我在调试时,会把PLL锁定信号、FIFO空满信号都引出来,配合示波器看,问题定位快很多。

3.5 性能优化

驱动写好了,但性能够不够?我建议你关注两个指标:

  • FIFO水位:如果FIFO经常空,说明AXI带宽不够;如果经常满,说明DSI时钟太高
  • 中断延迟:DSI的中断处理要快,否则会导致画面撕裂

我个人习惯在驱动里加一些统计计数器,比如FIFO underrun次数、CRC错误次数。这些数据能帮你快速定位瓶颈。

static void imx8_mipi_dsi_irq_handler(struct imx8_mipi_dsi *priv)
{
    u32 status;

    regmap_read(priv->regmap, DSI_INT_STATUS, &status);

    if (status & DSI_INT_FIFO_UNDERRUN) {
        priv->stats.fifo_underrun++;
        dev_warn(priv->dev, "FIFO underrun!\n");
    }

    if (status & DSI_INT_CRC_ERROR) {
        priv->stats.crc_errors++;
        dev_err(priv->dev, "CRC error detected!\n");
    }

    /* 清除中断 */
    regmap_write(priv->regmap, DSI_INT_CLEAR, status);
}

好了,DSI驱动开发的核心内容就这些。从协议基础到硬件架构,从设备树配置到代码实现,每一步都有坑,但每一步也都有规律可循。记住:时钟要对、时序要准、初始化要全,这三点做到了,DSI驱动就稳了。