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的驱动开发分为两步:
- 实现底层硬件操作(寄存器读写、时钟配置、时序控制)
- 注册到DRM框架(填充回调函数、绑定组件)
下面这张图展示了DRM/KMS框架与DSI驱动的关系:
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驱动的核心工作:
- 上电时序控制:先供电、再复位、最后发初始化命令
- 初始化序列:通过DCS命令配置屏幕参数(伽马、对比度、扫描方向)
- 背光控制:PWM调光或通过DCS命令调光
- 休眠/唤醒:实现低功耗模式
内核提供了 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:虚拟通道号,通常为0reset-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 里使能它们。