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核。我画了张图,帮你理清它的内部结构。
你看,数据从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驱动调试,说白了就是跟波形打交道。我总结了几条经验:
- 先看时钟:用示波器量CLKP/N,频率对不对?幅值够不够?
- 再看数据:用逻辑分析仪抓D0上的包,看初始化序列有没有发出去
- 最后看画面:如果画面有噪点,多半是时序不对;如果画面偏移,多半是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驱动就稳了。