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控制器,发现它们都绕不开这几件事:
- 初始化PHY:设置HS时钟频率、LP模式下的电压摆幅
- 配置DPI接口:告诉控制器从CRTC拿数据时用什么格式
- 设置视频模式或命令模式:这个选择直接影响帧率和功耗
- 处理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():断电
嗯,这里要注意,prepare和enable是分开的。为什么这么设计?因为有些场景需要先准备好电源,但延迟一会儿再发命令。比如有些屏要求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显示来说,设备树配置直接决定了驱动能不能正确初始化。
我总结了几条必须配对的设备树节点:
- DSI控制器节点:描述SoC内部的DSI主机
- 面板节点:描述外接的LCD屏
- 端口连接:把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显示驱动的完整链路。下一章我会讲具体的调试方法和工具使用,到时候咱们拿示波器量信号,拿逻辑分析仪抓数据,把那些玄学问题一个一个揪出来。