T113-i MIPI屏Panel驱动移植:从uboot到内核DRM调试实战

1. 从一块黑屏说起:Panel 驱动移植到底在搞什么

搞嵌入式显示这块的朋友,大概率都经历过这样的场景:板子上电,背光亮了,但屏幕一片漆黑,或者花屏闪屏,串口 log 里 DRM 报一堆 timeout。这时候你心里清楚,Panel 驱动没配对。我这次搞的是一块 T113-i 平台上的 MIPI-DSI 屏,从 uboot 2018 的 DTS 配置一路调到内核 DRM 框架下的 Panel 驱动,中间踩的坑够写一篇长文了。

先把话说清楚:这篇讲的“点亮一块屏幕”,指的是从硬件上电到屏幕正常显示图像这条完整链路上,Panel 驱动相关的移植和调试工作。它涉及的核心技术点包括DTS 设备树配置、DRM 显示框架、MIPI-DSI 协议时序、uboot 阶段的显示初始化,以及内核阶段 Panel 驱动的注册与匹配。适合谁看?如果你正在做全志、瑞芯微这类国产 SoC 的显示驱动移植,或者你刚接触 DRM 框架想搞清楚 Panel 驱动在整个显示管线里的位置,这篇内容应该能帮你省下不少查资料的时间。

为什么 Panel 驱动这么容易出问题?因为一块屏幕要亮起来,涉及的东西太多了:供电时序、复位时序、MIPI 时钟频率、lane 数量、时序参数(porch、sync、active)、像素格式、背光控制、初始化命令序列。任何一个环节对不上,屏幕就是黑的或者花的。而且不同厂家的屏,这些参数都不一样,甚至同一款屏不同批次都可能需要微调。所以 Panel 驱动的移植,本质上是一个“参数对齐 + 时序匹配 + 框架适配”的活。

我这次用的平台是 T113-i,双核 A7 加 RISC-V 协处理器,显示子系统支持 MIPI-DSI 输出。屏幕是一块 1024x600 的 MIPI 屏,2 lane,RGB888 格式。整个调试过程从 uboot 到 kernel 走了一遍,下面我把每个环节拆开来讲。

2. 显示链路全景:从 SoC 到屏幕之间到底经过了什么

2.1 硬件链路拆解

先搞清楚数据从 SoC 出来到屏幕亮起来,中间经过了哪些环节。T113-i 的显示子系统大致是这样的:Display Engine(DE)负责图层合成和缩放,然后输出到 TCON(Timing Controller),TCON 负责生成时序信号,再通过 MIPI-DSI 控制器把并行数据转成 MIPI 差分信号,经过 D-PHY 发送到屏幕的 MIPI 接收端。屏幕内部有自己的驱动 IC,收到 MIPI 信号后解码,驱动液晶面板显示图像。

这条链路上,Panel 驱动管的是哪一段?严格来说,Panel 驱动管的是“屏幕侧”的配置:屏幕支持什么时序、需要什么初始化命令、供电和复位怎么控制、背光怎么调。而 SoC 侧的 MIPI-DSI 控制器驱动管的是“主机侧”的配置:时钟怎么设、lane 怎么映射、数据格式怎么打包。两边必须对齐才能亮屏。

用个生活类比:Panel 驱动就像是跟屏幕“对话”的翻译官,你得告诉屏幕“我要给你发 1024x600 的图像,刷新率 60Hz,时序参数是这样这样的”,屏幕才会正常工作。如果你说的参数它听不懂或者对不上,它就不理你,屏幕就黑着。

2.2 DRM 框架下 Panel 驱动的角色

Linux 内核的显示框架经历了从 FBDEV 到 DRM 的演进。现在主流 SoC 厂商基本都往 DRM 上靠了。DRM 框架下,Panel 驱动通常实现为一个drm_panel设备,通过drm_panel_funcs提供prepare、enable、disable、unprepare等回调。MIPI-DSI 控制器驱动在初始化时会通过drm_panel_chain或者of_drm_find_panel找到对应的 Panel,然后在合适的时机调用这些回调。

具体到代码层面,Panel 驱动一般是一个 platform driver,在 probe 阶段解析 DTS 里的时序参数和初始化序列,注册mipi_dsi_device,然后通过mipi_dsi_attach把自己挂到 DSI 总线上。DSI 控制器驱动在 host attach 的时候会去匹配 panel,匹配成功后就建立了连接。

这里有个关键点:Panel 驱动的匹配方式。老一点的做法是用panel-simple这种通用驱动,在 DTS 里写 compatible 和 display-timings。新一点的做法是写一个专用的 Panel 驱动,把初始化序列硬编码在驱动里。两种方式各有优劣,后面会详细说。

2.3 uboot 阶段和内核阶段的分工

很多人容易忽略的一点是:uboot 阶段也可能需要初始化屏幕。为什么?因为如果 uboot 不初始化显示,内核启动过程中屏幕一直是黑的,直到内核 DRM 驱动加载完成才亮。这中间可能有几秒钟的黑屏时间,用户体验不好。而且有些产品要求在 uboot 阶段就显示 logo 或者开机画面。

T113-i 的 uboot 2018 里,显示驱动用的是全志自己的 disp 框架,不是标准 DRM。所以 uboot 阶段的 DTS 配置和内核阶段的 DTS 配置是两套东西,虽然都写在同一个.dts文件里,但被不同的驱动解析。uboot 阶段主要配置 DE、TCON、DSI 控制器的参数,以及 Panel 的基本时序。内核阶段则走标准 DRM 框架,Panel 驱动作为独立设备注册。

这就带来一个问题:两边的参数必须一致,否则会出现 uboot 显示正常但内核起来后花屏,或者反过来。我这次调试就遇到了 uboot 能亮但内核黑屏的情况,后面会详细讲排查过程。

3. DTS 配置实战:uboot 2018 下的 Panel 节点怎么写

3.1 uboot DTS 里显示相关的节点结构

T113-i 的 uboot 2018 里,显示相关的 DTS 节点主要在disp、tcon、dsi、panel这几个节点下。我先把关键结构列出来,然后逐个解释。

&disp { disp_init_enable = <1>; disp_mode = <0>; screen0_output_type = <3>; screen0_output_mode = <4>; fb0_format = <0>; fb0_width = <1024>; fb0_height = <600>; ... }; &tcon0 { tcon0_frm_mode = <0>; tcon0_out_type = <1>; ... }; &dsi0 { status = "okay"; dsi0_pixel_format = <0>; dsi0_lane_num = <2>; dsi0_hsync_active = <0>; dsi0_vsync_active = <0>; ... }; &panel0 { status = "okay"; panel0_power_en = <&pio PB 5 1 1 1 1>; panel0_reset_en = <&pio PB 6 1 1 1 1>; panel0_backlight_en = <&pio PB 7 1 1 1 1>; panel0_init_cmds = [...]; ... };

这些节点的具体含义:disp节点配置显示引擎的输出参数,screen0_output_type = <3>表示输出到 DSI,screen0_output_mode = <4>表示 1024x600 分辨率。tcon0配置时序控制器,tcon0_out_type = <1>表示输出到 DSI。dsi0配置 MIPI-DSI 控制器的参数,包括 lane 数量、像素格式、同步极性等。panel0配置屏幕侧的参数,包括供电 GPIO、复位 GPIO、背光 GPIO 和初始化命令序列。

3.2 时序参数的计算与填写

时序参数是 Panel 驱动里最容易出错的地方。MIPI 屏的时序参数包括:hactive、vactive、hfront porch、hback porch、hsync、vfront porch、vback porch、vsync、pixel clock。这些参数必须和屏幕规格书完全一致。

以我用的这块 1024x600 屏为例,规格书给的时序是:

参数值单位
hactive1024pixel
vactive600line
hfront porch160pixel
hback porch160pixel
hsync10pixel
vfront porch12line
vback porch23line
vsync1line
pixel clock51.2MHz

在 uboot DTS 里,这些参数通常写成panel0_hactive、panel0_vactive这样的形式。但不同厂商的 uboot 驱动解析方式不一样,有些是直接写数值,有些是通过display-timings子节点写。T113-i 的 uboot 2018 里,我用的方式是直接在 panel 节点下写这些参数。

这里有个计算要注意:pixel clock 和 MIPI 时钟的关系。MIPI-DSI 的时钟频率不是直接等于 pixel clock,而是要根据 lane 数量和像素格式来算。对于 RGB888 格式,每个像素 24 bit,2 lane 的话,MIPI 时钟频率 = pixel clock * 24 / 2 = 51.2 * 12 = 614.4 MHz。这个值要填到 DSI 控制器的配置里,如果填错了,屏幕可能显示异常或者直接不亮。

3.3 初始化命令序列的编写

MIPI 屏通常需要在上电后发送一串初始化命令,配置屏幕内部驱动 IC 的寄存器。这些命令写在 DTS 的panel0_init_cmds属性里,格式一般是[数据类型 延时 长度 数据...]。

比如一条典型的命令:

[0x05 0x00 0x01 0x11] // 发送 0x11 命令,延时 0ms,长度 1 字节 [0x05 0x78 0x01 0x29] // 发送 0x29 命令,延时 120ms,长度 1 字节

这里的0x05是数据类型,表示 DCS 短写命令。0x78是延时,单位是毫秒,0x78 = 120。0x01是数据长度。后面的0x11和0x29是实际的命令码。0x11 是 Sleep Out,0x29 是 Display On。

初始化序列一般从屏幕规格书里抄,但要注意几点:第一,延时时间要够,特别是 Sleep Out 之后通常要延时 120ms 以上;第二,命令顺序不能乱,有些命令有依赖关系;第三,如果屏幕有多个初始化序列(比如不同刷新率),要选对。

我这次踩的一个坑是:规格书里的初始化序列是用十六进制写的,但 uboot DTS 里需要的是十进制或者带 0x 前缀的十六进制。我一开始直接抄了规格书的格式,结果编译报错。后来改成[0x05 0x00 0x01 0x11]这种格式就好了。

4. 内核 DRM 框架下的 Panel 驱动移植

4.1 Panel 驱动的注册与匹配流程

内核阶段的 Panel 驱动走的是标准 DRM 框架。一个典型的 MIPI Panel 驱动结构是这样的:

static int panel_probe(struct mipi_dsi_device *dsi) { struct panel_priv *priv; int ret; priv = devm_kzalloc(&dsi->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->dsi = dsi; dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST; dsi->format = MIPI_DSI_FMT_RGB888; dsi->lanes = 2; ret = mipi_dsi_attach(dsi); if (ret < 0) return ret; drm_panel_init(&priv->panel, &dsi->dev, &panel_funcs, DRM_MODE_CONNECTOR_DSI); drm_panel_add(&priv->panel); return 0; }

这段代码的关键点:dsi->mode_flags决定 DSI 控制器的工作模式,MIPI_DSI_MODE_VIDEO表示视频模式,MIPI_DSI_MODE_VIDEO_BURST表示 burst 模式。dsi->format是像素格式,dsi->lanes是 lane 数量。这些参数必须和屏幕规格书一致。

drm_panel_init初始化 panel 结构,注册panel_funcs回调。drm_panel_add把 panel 加到 DRM 框架里。之后 DSI 控制器驱动在 attach 的时候会通过of_drm_find_panel找到这个 panel,建立连接。

匹配方式是通过 DTS 里的compatible属性。比如:

&dsi0 { status = "okay"; panel@0 { compatible = "vendor,panel-1024x600"; reg = <0>; reset-gpios = <&pio PB 6 GPIO_ACTIVE_LOW>; backlight = <&backlight>; ... }; };

驱动里定义of_device_id匹配表:

static const struct of_device_id panel_of_match[] = { { .compatible = "vendor,panel-1024x600" }, { } };

这样内核启动时就会匹配到对应的 panel 驱动。

4.2 prepare/enable 回调里的时序控制

Panel 驱动的核心逻辑在prepare和enable回调里。prepare负责上电和发送初始化命令,enable负责开背光。disable和unprepare反过来。

一个典型的prepare实现:

static int panel_prepare(struct drm_panel *panel) { struct panel_priv *priv = to_panel_priv(panel); int ret; ret = regulator_enable(priv->supply); if (ret < 0) return ret; gpiod_set_value(priv->reset_gpio, 0); msleep(10); gpiod_set_value(priv->reset_gpio, 1); msleep(120); ret = mipi_dsi_dcs_exit_sleep_mode(priv->dsi); if (ret < 0) return ret; msleep(120); ret = mipi_dsi_dcs_set_display_on(priv->dsi); if (ret < 0) return ret; msleep(20); return 0; }

这里的时序很关键:上电后先拉低复位,延时 10ms,再拉高复位,延时 120ms,然后发 Sleep Out 命令,再延时 120ms,最后发 Display On 命令。这些延时不能省,否则屏幕可能初始化失败。

enable回调就简单了,开背光:

static int panel_enable(struct drm_panel *panel) { struct panel_priv *priv = to_panel_priv(panel); backlight_enable(priv->backlight); return 0; }

背光控制一般通过backlight子系统,在 DTS 里配置 PWM 或者 GPIO 背光。

4.3 跨 DRM 录制场景下的注意事项

热词里提到了“跨 DRM 录制”,这个场景其实是指在一个系统里同时使用多个 DRM 设备,比如一个用于显示,一个用于视频采集或者编码。这种情况下,Panel 驱动的初始化时机和资源分配需要特别注意。

跨 DRM 录制时,显示管线和采集管线可能共享一些硬件资源,比如时钟、电源域、内存带宽。如果 Panel 驱动在初始化时占用了这些资源,采集管线可能就拿不到了。解决办法是在 DTS 里合理分配资源,或者用drm_bridge框架把 Panel 驱动和采集驱动解耦。

另外,跨 DRM 场景下,Panel 驱动的prepare和enable回调可能被不同的 DRM 设备调用,要注意加锁保护,避免竞态。我一般会在驱动里加一个mutex,在prepare、enable、disable、unprepare里都加锁。

5. 点亮调试实录:从黑屏到正常显示的排查过程

5.1 第一轮:uboot 能亮,内核黑屏

第一次调试时,uboot 阶段屏幕能正常显示 logo,但内核启动后屏幕就黑了。串口 log 里看到 DRM 报dsi host attach failed。这说明内核阶段的 DSI 控制器没有成功 attach 到 Panel。

排查思路:先确认 Panel 驱动有没有加载。ls /sys/bus/mipi-dsi/devices/看到没有设备。再查 DTS,发现内核 DTS 里panel@0节点的status没写okay,默认是disabled。加上status = "okay"后重新编译,Panel 驱动加载了,但屏幕还是不亮。

继续查 log,发现mipi_dsi_attach返回-ENODEV。这说明 DSI 控制器没有找到 Panel。检查 DTS 里的compatible属性,发现和驱动里的of_device_id表对不上。驱动里写的是vendor,panel-1024x600,DTS 里写的是vendor,panel1024x600,少了一个横杠。改过来后,attach 成功了。

5.2 第二轮:屏幕亮了但花屏

attach 成功后,屏幕亮了,但显示花屏,颜色也不对。这种情况一般是像素格式或者 lane 数量配错了。检查 DTS 里的dsi0_pixel_format和dsi0_lane_num,发现 pixel format 写的是0,对应 RGB888,lane num 写的是2,和屏幕规格书一致。

再查时序参数,发现panel0_hfront_porch写的是160,但规格书上是160,没问题。继续查 pixel clock,发现 uboot DTS 里写的是51200(单位 KHz),但内核 DTS 里写的是51200000(单位 Hz)。单位不一致导致内核算出来的时钟频率错了。统一改成 Hz 后,花屏问题解决。

5.3 第三轮:显示正常但背光闪烁

花屏解决后,显示正常了,但背光有轻微闪烁。这种情况一般是 PWM 频率或者占空比设置不对。检查背光 DTS 配置,发现 PWM 频率设的是1000Hz,占空比50%。把频率提高到20000Hz 后,闪烁消失。

背光闪烁的另一个可能原因是电源纹波。如果 PWM 频率太低,电源纹波会比较大,导致背光闪烁。一般建议 PWM 频率在 20KHz 以上,超出人耳听觉范围,也不会引起可见闪烁。

5.4 常见问题速查表

现象可能原因排查方法
屏幕完全不亮供电/复位时序不对用万用表量 GPIO 电平,查 DTS 里的 GPIO 配置
屏幕亮但黑屏初始化命令没发或发错抓 MIPI 波形,确认命令序列
花屏像素格式/lane 数量/时钟频率不对对照规格书逐项检查
颜色不对像素格式配错确认 RGB 顺序和 bit 深度
背光闪烁PWM 频率太低提高 PWM 频率到 20KHz 以上
内核黑屏但 uboot 正常内核 DTS 配置缺失或错误对比 uboot 和内核 DTS 的显示节点
attach 失败compatible 不匹配检查 DTS 和驱动的 of_device_id 表

6. 实操心得与避坑指南

6.1 参数对齐是核心

Panel 驱动移植 80% 的问题都是参数不对齐。我的经验是:拿到一块屏,先把规格书里的时序参数、初始化序列、供电要求、接口类型全部整理成一张表,然后逐项和 DTS 里的配置对照。不要凭感觉填,也不要抄别的项目的配置,哪怕屏的型号看起来一样。

另外,规格书里的参数有时候会有多个版本,比如不同刷新率对应不同的 porch 值。要确认你用的是哪个版本,然后在 DTS 里填对应的值。

6.2 善用示波器和逻辑分析仪

调试 MIPI 屏,光看 log 是不够的。有条件的话,用示波器量一下 MIPI 时钟和数据线的波形,确认时钟频率和数据眼图。用逻辑分析仪抓一下初始化命令,确认命令序列有没有发出去。我这次调试就是靠逻辑分析仪发现初始化命令的延时不够,导致屏幕没响应。

6.3 uboot 和内核 DTS 要同步维护

uboot 和内核的 DTS 虽然被不同的驱动解析,但显示的硬件是同一套。所以两边的参数必须一致。我的做法是:在 uboot DTS 里改完参数后,同步更新内核 DTS,反之亦然。最好用一个脚本或者 Makefile 来管理,避免手动同步出错。

6.4 注意 DRM 框架的初始化顺序

DRM 框架下,Panel 驱动的初始化顺序很重要。Panel 必须在 DSI 控制器之前或者同时初始化,否则 DSI 控制器 attach 的时候找不到 Panel。如果 Panel 驱动是作为模块加载的,要确保它在 DSI 控制器驱动之前加载。可以在 DTS 里用dependencies属性或者调整驱动加载顺序。

6.5 跨 DRM 场景下的资源隔离

如果系统里有多个 DRM 设备,比如显示和采集,要注意资源隔离。Panel 驱动占用的 GPIO、时钟、电源域,不要让采集驱动也用。可以在 DTS 里用reserved或者exclusive属性标记。另外,跨 DRM 场景下,Panel 驱动的prepare和enable可能被多个 DRM 设备调用,要加锁保护。

6.6 调试 log 的打开方式

内核 DRM 子系统的 log 默认比较少,调试时可以打开 debug 开关:

echo 0x1f > /sys/module/drm/parameters/debug

这样可以看到 DRM 框架的详细 log,包括 Panel 匹配、DSI attach、时序设置等信息。uboot 阶段的 log 可以通过修改CONFIG_DISP_DEBUG或者类似的宏来打开。

7. 后续可以扩展的方向

Panel 驱动移植只是显示调试的第一步。屏幕亮起来之后,还有很多可以优化的地方。比如:多屏异显场景下,怎么管理多个 Panel 驱动;低功耗场景下,怎么在 suspend/resume 时正确关闭和恢复 Panel;高刷新率场景下,怎么调整时序参数和 MIPI 时钟来支持 90Hz 或者 120Hz。

另外,现在越来越多的项目开始用drm_bridge框架来替代传统的drm_panel,因为 bridge 框架更灵活,支持链式连接。如果你的项目后续要支持 Type-C 转接或者多路显示,可以考虑往 bridge 框架迁移。

我个人在实际操作中的体会是:Panel 驱动移植这件事,看起来简单,但细节特别多。每块屏都有自己的脾气,规格书上的参数只是参考,实际调试时可能还需要微调。多准备几块不同型号的屏,多积累一些调试经验,慢慢就能形成自己的排查套路了。