LCD 驱动在 Linux 驱动开发里一直是个"看着不难,做起来全是事"的方向。尤其当你拿到 RK3576 这种带完整 VOP、支持 MIPI DSI / eDP / HDMI 多种显示接口的 SoC,第一次要把一块陌生 LCD 屏点亮,你会发现"亮"和"显示正确"之间隔着十来个坑。
这篇文章是驱动之路系列的第四篇,聊基于 RK3576 的 LCD 驱动。适合正在做 Rockchip 平台 bringup、或者第一次碰 DRM/KMS 显示驱动的朋友。我会从硬件链路讲起,把设备树里的屏参、panel 驱动的注册流程、点亮后的常见排错,以及一些 RK3576 平台特有的经验串起来。先给个结论:LCD 驱动的核心工作不是"让屏幕亮",而是"让屏幕在正确的时机、以正确的参数、稳定地亮"。这句话理解到位,后面能少走一半弯路。
1. 先别急着写代码,把 RK3576 的显示通路在脑子里画出来
很多新手拿到一块新屏,第一反应是打开 panel 驱动源码开始改。这个方向不能说错,但顺序反了。驱动工程师的活,本质上是在给一条硬件链路"配参数、理时序",而不是在写功能逻辑。如果对硬件链路没有整体认知,改出来的参数大概率是瞎猜。
1.1 VOP 是画师,DSI 是邮差,Panel 是挂画的那面墙
RK3576 上的一帧画面,从内存到屏幕,要经过三段完全不同的硬件。
VOP(Video Output Processor)负责把 framebuffer 里的像素数据取出来,做图层合成、缩放、色彩空间转换,再按屏端需要的时序把数据送出去。分辨率和刷新率由它掌控,这就像画师决定了画什么、画多大、什么时候交稿。
MIPI DSI 控制器负责把 VOP 送出来的并行像素数据,按照 DSI 协议打包成串行差分信号,通过 D-PHY 的 lane 发出去。它不关心画面内容,只关心怎么把数据正确送到对端,是个纯粹的邮差。
Panel 就是屏幕本身。它在收到数据后完成显示,但 panel 驱动真正关心的是另外一套东西:什么时候上电、电源电压多少、复位信号怎么拉、初始化序列在哪个时刻下发、背光什么时候亮。
理解了这个分工,你就知道为什么排查显示问题时得分层看了。VOP 出问题,画面可能整体没了;DSI 链路出问题,可能完全不出图或花屏;panel 时序出问题,画面可能偏移、闪烁、或者压根不工作。
1.2 DRM 框架里,你写的代码挂在哪几个钩子上
在 Linux 主线和 Rockchip BSP 内核里,显示驱动走的是 DRM/KMS 框架。RK3576 相关的驱动代码主要分布在drivers/gpu/drm/rockchip/下,VOP2 驱动、DSI/eDP 控制器驱动都在那里,标准的 panel 驱动则在drivers/gpu/drm/panel/。
硬件链路和 DRM 对象的对应关系很直接:
| 硬件 | DRM 对象 | 常用驱动文件 |
|---|---|---|
| VOP | CRTC | rockchip_drm_vop2.c |
| DSI 控制器 | encoder + connector | dw-mipi-dsi2.c |
| 屏幕本体 | panel driver | panel-simple.c 或自定义 panel 文件 |
设备树里的各个显示节点,就是这条链路的编排表。vop节点声明了 CRTC 能力,dsi节点声明了 encoder 和 connector,panel子节点声明了屏幕的时序和电源要求。你在 dts 里改的每个属性,最终都会被对应驱动解析,变成硬件行为。
我建议所有做 LCD bringup 的人,动手前先把这条链路画一遍。哪怕只是拿个文本文件写清楚"谁产生数据、谁传输数据、谁显示数据",后续排查问题时,你至少知道该去哪一层看日志,而不是像个无头苍蝇一样到处打 log。
2. 屏参就是产品说明书,逐字段对一遍再上手
修改设备树是 LCD 驱动里最日常的操作,但也是最容易出错的一步。所谓"屏参",就是 panel 节点里那个display-timings块,它描述了屏幕显示一帧画面需要的完整时序。这些参数不是随便填的,每一行都来自屏幕的数据手册(datasheet),填错一个,画面就会出现各种诡异现象。
2.1 timing 字段与 datasheet 的对照关系
一个典型的 display-timings 节点长这样:
display-timings { native-mode = <&timing0>; timing0: timing0 { clock-frequency = <148500000>; hactive = <1920>; vactive = <1080>; hback-porch = <148>; hfront-porch = <88>; hsync-len = <44>; vback-porch = <36>; vfront-porch = <4>; vsync-len = <5>; hsync-active = <0>; vsync-active = <0>; de-active = <0>; pixelclk-active = <0>; }; };逐个对照 datasheet,含义如下:
hactive/vactive:有效显示区域的宽和高,就是屏幕的分辨率。hsync-len:行同步脉冲宽度,也就是每行结束时同步信号拉低的时长。hback-porch/hfront-porch:行同步脉冲前/后的消隐区,相当于电子枪"换行"需要的准备时间。vsync-len/vback-porch/vfront-porch:对应的场同步参数。clock-frequency:像素时钟,这是所有参数里唯一需要算的东西。
datasheet 上通常给的是这些参数的最小值、典型值和最大值,一般取典型值就能点亮。display-timings 的语法在 Linux 内核文档Documentation/devicetree/bindings/display/panel/里有详细说明,我不再逐项展开,但有一点要强调:ysync-active等极性标志如果设反,轻则画面微微异常,重则直接黑屏。这类极性问题在调试中很难一眼看出来,后面排查章节会细说。
2.2 像素时钟不是随便填的
clock-frequency的计算公式是:
像素时钟 = (hactive + hfront-porch + hback-porch + hsync-len) × (vactive + vfront-porch + vback-porch + vsync-len) × 刷新率以上面那组参数为例:
横向总周期 = 1920 + 88 + 148 + 44 = 2200 纵向总周期 = 1080 + 4 + 36 + 5 = 1125 148500000 = 2200 × 1125 × 60这是很经典的 1080p60 参数。当你拿到一块新屏,手头没有官方 dts 参考时,按这个公式把 datasheet 的典型值代进去算一次像素时钟,基本不会出错。
这里有个工程上的建议:如果板端对像素时钟要求不高,或者屏的 datasheet 给了频率范围,可以稍微留一点余量,比如目标刷新率按 59.9Hz 算。实际调屏时,clk 计算值偶尔会有频率超限的问题,留 0.1Hz~1Hz 的余量能规避不少 VOP 时序报错。
2.3 那类"datasheet 不写但你得知道"的参数
比如de-active是数据使能信号的极性,只有 RGB 接口的老屏才经常用到;MIPI DSI 屏的 DSI 协议里数据的传输由 DSI 控制器管理,de-active对实际出图影响不大,但若你用的是 RGB666/8080 这类并口屏,这个极性和pixelclk-active就得和硬件原理图严格对应。
再比如屏幕的bpc属性,它表示每通道位数。MIPI DSI 上常见的是 8bit 或 6bit 屏,RK3576 的 DSI 控制器在配置时会根据 bpc 决定每个像素打包的 bit 数。如果屏是 6bit,你却写了 8bit,显示颜色会发白或偏色,而且这种问题用示波器量视频信号都量不出明显异常,只能靠对比 datasheet 反复核对。
还有一个容易忽略的是video-mode属性。MIPI DSI 有 burst mode、sync pulse mode、sync event mode,不同屏对 video mode 的支持不同。rockchip,lane-rate的设置也和它有关,这个在第五节细讲。
3. Panel 驱动:能省事就省事,该自己写的也别慌
设备树配好屏参后,接下来是 panel 驱动。这一步有个原则:能用现成的就别重复造轮子。
3.1 panel-simple 能覆盖的场景,别自己写
panel-simple.c是内核里专门处理"不需要复杂初始化序列"的 panel 驱动。很多 MIPI 屏,尤其是不带内部 TCON 的屏,只要给对电源和时序就能出图,完全不需要在驱动里下发初始化命令。
使用方式是设备树里加一个 compatible 字符串,比如:
panel: panel@0 { compatible = "boe,tv101wum-n53"; reg = <0>; enable-gpios = <&gpio3 17 GPIO_ACTIVE_HIGH>; pinctrl-names = "default"; pinctrl-0 = <&lcd_enable_h>; backlight = <&backlight>; ... port { panel_in_dsi: endpoint { remote-endpoint = <&dsi_out_panel>; }; }; };只要在内核源码里搜一下有没有对应兼容的 panel 记录,优先把相关厂商、型号加到panel_simple结构体里。不要一上来就自建一个mydriver_panel.c,除非你确认屏确实需要初始化序列,或者对上下电时序有严格但 panel-simple 不支持的顺序要求。
3.2 初始化序列的本质:让屏进入正常工作的"状态机"
一些屏,特别是带驱动 IC 的 LCD 或 AMOLED 屏,上电后需要主机通过 DSI 命令接口下发一串初始化指令,屏幕才会进入正常显示状态。这就是我们常说的"初始化序列"或"init code"。
它的本质,是在驱动屏幕内部的寄存器。常见操作包括:设置显示方向、调整伽马曲线、打开 BIST(内建测试图形)、配置内部时序生成器等。厂商提供的初始化序列一般是几百行的数组,格式为{command, payload1, payload2, ...},放在驱动里按序发送。
我在 RK3576 上做过一次的实践是:把厂商给的初始化码整理成static const struct rk_dsi_cmd display_init_cmds[],然后挂到 panel 驱动的 enable 回调里,通过mipi_dsi_dcs_write_buffer逐条下发。关键点是每条命令之间的mdelay不能省略,比如 PLL 锁定、寄存器烧写这类操作需要等待稳定时间,缩短 delay 容易导致偶发初始化失败。
这里必须提醒一句:厂商给的初始化码不是永远可信。同一颗屏的 IC 版本不同,初始化码可能有差异。最典型的例子是屏幕在某个批次后换了内部驱动 IC,旧码仍然能点亮但颜色错乱。遇到这种情况,顺着厂商的 FLASH 版本号核对初始化码,比对两个版本的差异,是我遇到过的调屏案例里最长的一个调查过程(前后耗了将近一周)。
3.3 上下电时序:看起来是玄学,其实可以科学对待
所谓上下电时序,就是 panel 的 reset 引脚和电源 VCC/VCI 的使能顺序,以及各阶段之间的延时。很多文章把这个说得特别玄,其实 datasheet 上通常有一张表,写明t1到tn每个环节的最小时间:
- VCC 上电后等待 >= 10ms,再释放 reset - reset 释放前拉低 >= 12ms - reset 释放后等待 >= 20ms,再下发初始化序列你在 panel 驱动的prepare回调里,按这个顺序写就行了:
static int my_panel_prepare(struct drm_panel *panel) { /* 1. 使能电源 */ regulator_enable(ctx->vcc); msleep(10); /* 2. 拉低复位,保持 12ms */ gpiod_set_value_cansleep(ctx->reset, 1); msleep(12); /* 3. 释放复位 */ gpiod_set_value_cansleep(ctx->reset, 0); msleep(20); return 0; }有个排错经验特别值得分享:如果屏表现为"偶尔点不亮、多试几次又好了",大概率不是初始化码问题,而是某一步的mdelay时间不够。这类偶发问题最难排查,建议优先查时序而不是查命令。
3.4 背光不是附属品,它参与整个显示链路
很多人把背光和显示割裂看,这是不对的。至少在驱动里,panel 驱动和 backlight 驱动是协同工作的。
设备树里backlight = <&backlight>这个属性,就是在告诉 DRM 子系统:这个 panel 对应哪块背光。panel 的enable回调执行后,backlight 驱动的bl_update_status会被调用,此时 PWM 才开始输出,屏幕才真正亮起来。
背光调试中最经典的一个坑是:屏能出图,但背光亮一下又灭了。这时先别怀疑 PWM,去查 backlight 节点里brightness-levels和default-brightness-level。如果 default 设的档次对应亮度为 0,背光自然不起来。另一个坑是 PWM 极性,pwm-inverse-polarity属性如果和硬件反了,背光会以"高电平灭、低电平亮"的相位工作,效果就是亮度调节反向或者干脆不亮。
4. 点亮之后才见真章:四个高频故障的完整排查链路
前面把配置和驱动说完了,现在进入整个 LCD bringup 里最有价值的部分——排错。这里不直接给结论,我把四类高频问题的排查链路都走一遍,你照着走,基本都能定位到根因。
4.1 故障一:背光亮,但屏幕完全没有画面
这是最让人崩溃的情况,因为"背光亮"说明电源正常,但画面没有过来。我的推荐排查顺序是:
第一步,看内核日志。打开 DRM 的调试开关,把drm.debug=0x1f加进 bootargs,然后看dmesg里是否有rockchip-drm、dw-mipi-dsi2、panel-simple相关的报错。重点关注以下现象:
[ 3.360076] rockchip-drm display-subsystem: bound [drm:vop2] [ 3.360086] rockchip-drm display-subsystem: bound [drm:dw-mipi-dsi2] [ 3.360097] rockchip-drm display-subsystem: bound [drm:panel-boe-tv101wum-n53]如果 panel 没有 bound,说明面板驱动没 probe 成功。常见原因是 compatible 不匹配,或者remote-endpoint的 port 连接没对上。
第二步,看 DSI 是否进入 video mode。在 dmesg 里搜dsi相关 log,确认 DSI 控制器完成了lane_ready和video_mode的状态切换。如果卡在command mode出不来,多半是并发配置不匹配或rockchip,lane-rate设得不对。
第三步,确认 VOP 有数据输出。查看/sys/kernel/debug/dri/0/state(需要debugfs挂载),检查 CRTC state 里active是否为 1,plane state 里fb是否有绑定。如果 CRTC active 为 0,说明用户空间没有触发 modeset,问题就不在驱动而在上层的显示服务了。
4.2 故障二:画面上移、下移、或者只显示一部分
画面偏移,尤其是"文字显示只有一半",在屏参正确的情况下几乎都是 porch 参数错了。hback-porch和hfront-porch的语义是同步信号到有效数据之间的消隐时间。如果这两个值写反了,画面会往一侧平移几十个像素。
我的做法是准备一张测试图,图上带边缘色块和网格,看偏移方向就能反推是哪个 porch 问题。比如画面整体往右移了 40 个像素,那很可能是hfront-porch过大或者hback-porch不足,把两者的值按偏移量调整即可。
这里要注意一个细节:不同屏的 datasheet 对 porch 的定义有时不太一样。有的写的是"blanking",有的直接给"porch"。翻译过来的口径差异,会导致你按表填却反而错。遇到这种情况,用公式算一遍总周期,和 datasheet 给定的 total blanking 做个交叉验证,基本能发现是不是单位或口径问题。
4.3 故障三:花屏、闪烁、或者斜纹
花屏和闪烁的成因比偏移复杂,可能是以下三类的叠加:
- 像素时钟不稳定:VOP 输出时钟抖动过大,或者 DSI lane-rate 设得偏高/偏低。排查办法是把
rockchip,lane-rate按计算要求重设,并确保 VOP 的 dclk 约等于clock-frequency。 - 电源纹波或 EMI:尤其在打螺丝、盖后盖后出现闪屏,十有八九是模组编带、FPC 走线或外壳接地问题。驱动层面能做的有限,但可以给 panel 的供电加大电容,或者降低 DSI 的摆率档位。
- 软件层抢锁/刷新不同步:刷屏接口和 GPU 渲染不同步导致的撕裂。这个在 Android 上常见,和你 LCD 驱动本身关系不大,要用 DRM 的
VBLANK机制和 GPU 的 present fence 配合解决。
排查花屏,我强烈建议先在 DSI 命令模式下让屏显示 BIST(内建测试图案)。如果 BIST 图案正常,说明屏端接收没问题,问题在 VOP 到 DSI 之间的数据面;如果 BIST 也花,那问题出在 DSI 链路或屏端,驱动层面的空间就不大了。
4.4 故障四:休眠唤醒后屏幕不恢复
这个在 RK3576 上遇到的概率不低,根因往往是上下电时序在 suspend/resume 路径上只做了一半。
排查第一步,看唤醒日志里panel_prepare和panel_enable有没有被正确调用。如果没调用,检查 DRM 驱动的 suspend/resume 回调注册。Rockchip 的 dts 里,rockchip,skip-suspend或者notify-sync这类属性会影响 PM 流程,配置错了可能导致 panel 驱动跳过 resume 流程。
第二步,如果 prepare 和 enable 都调了但还是黑屏,大概率是唤醒后的初始化序列没重发。很多屏在睡眠后必须重新发一遍完整的 init code 才能恢复,如果你的驱动只在 probe 时发过一次初始化码,那唤醒后不重新下发,屏就会一直黑着。解决办法是把初始化序列的下发包成一个函数,在enable回调里也调用一遍,同时注意时序延时。
5. RK3576 平台那些文档里不会明说的平台化经验
最后聊一些只在 RK3576 这类 Rockchip 平台上才需要注意的细节。这些不是课本内容,是我在具体项目上折腾出来的经验。
5.1 DSI 的 lane-rate 算好,别偷懒
RK3576 的 DSI 控制器针对不同面板,需要设置rockchip,lane-rate。它的含义是每条 lane 的传输速率,单位是 Mbps。计算公式大致是:
lane-rate = pixel_clock × bpp / lane_count注意分子上的bpp是每像素总位数,包含 RGB 三通道和可能的附加 bit。以 1080p60、8bit、4 lane 为例:
lane-rate = 148.5MHz × 24 / 4 ≈ 891Mbps实际工程里建议取整到 900 或 1000 并留出余量。lane-rate如果明显偏低,DSI 端会出花屏;偏高则功耗增加、EMI 变差。不同屏幕在这个属性上的容错程度差异不小,个别屏你把 rate 设高 20% 也能正常出图,但长期稳定性不好,所以还是建议按公式来。
5.2 电源域与时钟依赖顺序
RK3576 的显示相关电源域通常有vdpu、vop等,DSI 的 PHY 供电来自avcc_1v8之类。设备树里电源域的顺序直接影响 probe 时机。如果 panel 的 regulator 依赖的供电域还没上电,panel 的 probe 就会失败。
我踩过最典型的坑是:vcc-lcd这个 regulator 节点在 dts 里定义晚于dsi节点,导致regulator_get时返回EPROBE_DEFER,panel 一直 probe 不进去,日志里反复出现panel-simple: probe deferred。解决办法是调整 dts 节点的定义顺序,或者给 panel 的enable-gpios所在的 pinctrl 也加上pinctrl-0,确保 GPIO 引脚的 iomux 在上电前就配置好。
5.3 多屏场景下的 panel 驱动别写单例
RK3576 支持双屏甚至三屏输出,比如一个 MIPI DSI 屏加一个 eDP 屏。你写 panel 驱动时如果图省事用了全局变量存struct drm_panel *panel,那么第二块屏 probe 时会把第一块覆盖掉,结果就是一块屏能亮、另一块屏永远黑。
正确写法是每块 panel 的实例数据都用devm_kzalloc单独分配,所有的状态都挂在struct drm_panel私有数据里,不要用任何全局变量。这个坑我见过不只一次,改起来不难,但查起来很费劲,因为现象是"偶尔两块屏都亮,偶尔只有一块",很容易被误判成屏参错误。
5.4 调试工具与日志开关清单
做 RK3576 LCD 调试,我常用的工具和开关就这几样,整理成表对你可能有参考价值:
| 工具/开关 | 用途 |
|---|---|
drm.debug=0x1f | 打开 DRM core 的 debug 日志 |
drm.debug=0x3f | 打开全部 drm debug(含 vblank) |
/sys/kernel/debug/dri/0/state | 查看各对象状态 |
cat /sys/kernel/debug/dri/0/summary | 查看 VOP 全局配置概要 |
v4l2-ctl或media-ctl | 查看视频链路(若有 ISP/编解码联动) |
i2cdetect | 检查屏端 I2C 触控或 EEPROM 是否存在 |
另外,Rockchip 内核里CONFIG_DRM_ROCKCHIP_DEBUG开了之后,/sys/kernel/debug/dri/0/下会出现更多有用节点,比如vop和dsi的寄存器 dump。拿到 dump 后对照 TRM(Technical Reference Manual)的寄存器表,能确认 VOP 是否按预期输出时序。这一步对排查非常规问题几乎不可或缺。
写在最后:调屏这事,耐心比天赋值钱
回头看看这几个章节,其实 LCD 驱动没有一个环节是"纯靠猜"的。硬件链路决定了软件结构,datasheet 决定了时序参数,DRM 框架决定了回调顺序,而 RK3576 的平台特性决定了你必须在哪些地方多加小心。只要按照"链路 - 屏参 - 驱动 - 排错 - 平台经验"这条线走下来,每块新屏的 bringup 都只是一遍一遍核对参数的过程,而非痛苦的试错。
我个人的体会是,调屏时最重要的一件小事是把每次修改记录下来。哪一版 dts 改了哪个 porch、哪一版驱动动了哪条 delay,都用 git 提交信息或一个简单的文本文件记下来。这块屏调好了,三个月后另一块新屏来了,翻记录能省掉大量重复劳动。你踩过的每个坑,都应该留下痕迹。