
驱动之路系列写到这里已经是第四篇了。前几篇分别聊了内核启动、串口调试和GPIO中断这次轮到显示链路里最常打交道的一块LCD 驱动。先说平台。RK3576 是瑞芯微新一代中高端 AIoT 芯片8nm 制程四核 A72 加四核 A53 的大小核架构带 6 TOPS NPU显示方面继续沿用 Rockchip 自研的 VOP2 控制器。接触过 RK3568、RK3588 的朋友对这套体系应该不陌生整体思路一脉相承但在接口规格、时钟树和具体设备树节点上又各有差异。我这次调试的内容一块是 MIPI DSI 接口的 1080P 屏另一块是 eDP 接口的模组中间还牵扯到 PWM 背光、电源时序和初始化序列的上送。整个过程走下来最大的感受是LCD 驱动本身并不难难的是把“屏的物理时序”和“SoC 的控制器配置”对齐再把“驱动模型的调用时机”搞清楚。这篇文章就把这些点按顺序拆开讲。1. 拿到一块 LCD 屏驱动要做什么1.1 RK3576 显示链路的基本组成先说清楚一块屏从点亮到显示画面数据到底走了一条什么路。在 RK3576 上应用处理器把图像数据交给 VOP2Video Output Processor显示输出处理器VOP2 负责把内存里的 Framebuffer 数据按一定格式和时序取出来然后根据目标接口协议把数据分发到对应的显示控制器上。常见的通路包括 HDMI、MIPI DSI、eDP/DP以及部分场景下会用到的 LVDS 或 RGB 桥接。对于自带液晶面板的 LCD 模组最典型的链路是VOP2 → MIPI DSI Controller → Panel液晶模组这条链路中VOP2 负责产生视频时序Pixel Clock、Hsync、Vsync、DE 等MIPI DSI Controller 负责把并行视频数据打包成 MIPI DSI 协议包通过差分信号线发往屏端。如果屏本身不带 DSI 接口而是 LVDS 或并行 RGB 接口通常要在这条链路上插入一颗桥接芯片比如市面上常见的 DSI 转 LVDS、DSI 转 RGB 的 bridge。RK3576 这类应用处理器的显示接口里没有传统意义上直接拉出来的 RGB 大排线接口早年 RAspberry Pi 那种直接 RGB565 并口屏的做法在 RK3576 上已经行不通了必须要经过桥接。驱动开发要打交道的对象就是这条链路里的每一个环节。很多人以为 LCD 驱动就是“上电、拉复位、发初始化命令、亮屏”其实这只是最浅的一层。真正工程的难点在于设备树怎么描述这条硬件链路、内核驱动模型怎么把各个节点串起来、以及出现问题时如何快速判断是硬件信号的问题还是软件时序配置的问题。1.2 驱动要交出的四份“作业”用一句话概括一个完整的 LCD 驱动工程要交四样东西设备树节点告诉内核“这块板子上接了哪块屏、走的是哪个控制器、复位脚是哪个、背光用什么控制”。Panel 驱动负责屏的上电时序、复位时序、初始化命令序列的发送以及亮度、休眠唤醒等运行时操作。背光驱动配置通常是 PWM 背光需要配置 PWM 通道、极性、周期和亮度等级映射。Bridge 驱动可选如果链路里插了桥接芯片就需要对应的 bridge 驱动或者直接走 Rockchip 通用 bridge 框架。四份作业按顺序做完再通过内核的 DRM/KMS 框架发布出来系统才能看到一块可用的显示设备。用户空间的 Android 或 Linux 桌面通过/dev/dri/card0操作这块屏我们驱动开发者的任务到此才算完成。我见过有同事在调试过程中只盯着初始化命令序列发没发对却忽略了 VOP2 里 pixel clock 没有配好结果屏虽然“醒了”但图像一直不对。所以这篇文章我会反复强调一个观点接好一个屏要看整条链路而不是某一个环节。2. 设备树配置全解析2.1 DSI 接口屏的 DTS 配置设备树是整个 LCD 驱动的地基。RK3576 的 BSP 内核采用的是 6.1 版本内核节点风格和 RK3588 比较接近。以一块 MIPI DSI 接口的 1080P 屏为例核心设备树配置如下。首先是 DSI 控制器节点的启用和状态mipi_dsi1 { status okay; panel0 { compatible example,tv101wum-nl6; reg 0; backlight backlight; reset-gpios gpio3 RK_PB6 GPIO_ACTIVE_LOW; pinctrl-names default; pinctrl-0 lcd_reset_gpio; port { panel_in_dsi1: endpoint { remote-endpoint dsi1_out_panel; }; }; }; }; dsi1_out_panel { remote-endpoint panel_in_dsi1; };这段配置有几个关键点要解释。reg 0表示的是 DSI 设备地址。MIPI DSI 规范中允许同一总线上挂多个设备地址 0 一般用于唯一的显示屏设备。如果你的项目里同一条 DSI 总线上挂了触摸屏或者其他 DSI 外设这里需要显式分配地址不能乱填。reset-gpios是屏的复位引脚GPIO_ACTIVE_LOW表示低电平有效。这一点非常容易被忽略因为不同屏厂的模组复位极性设计并不统一有的要求高电平复位有的要求低电平复位。拿到屏的手册后一定要先确认清这个极性否则会出现“代码看着都对屏就是起不来”的尴尬局面。接下来还要在设备树中配置路由关系。Rockchip 平台在设备树中用一个 route 节点描述“VP 时钟通路绑到哪个显示接口”route_dsi1 { status okay; connect vp2_out_dsi1; };这个vp2_out_dsi1引用的语义是VOP2 的 VP2 输出绑定到 DSI1 控制器。具体编号要和硬件原理图对应上不是随便写的。如果接反了可能出现一个奇怪的现象两个控制器都配置正确、各自看起来也没问题但实际只有一路能点亮因为时钟树的源分配出现了冲突。2.2 eDP/DP 接口屏的 DTS 配置eDP 接口屏的处理方式与 DSI 有较大差异原因在于 eDP 协议本身是 DisplayPort 的嵌入式版本屏端带有一颗 eDP 接收器通常是显卡笔记本面板中的时序控制器它从 AUX 通道读取面板信息自动协商链路速率和通道数。在设备树层面eDP 屏的配置更简洁edp { status okay; force_hpd 1; panel0 { compatible edp-panel; reg 0; backlight backlight; power-supply vcc_lcd_3v3; pinctrl-names default; pinctrl-0 backlight_en_gpio; }; };注意 eDP 屏一般不需要像 MIPI DSI 屏那样去配置大段的初始化命令序列它更像一块“小显示器”通过 AUX 通道和 source 端协商好参数就可以直接工作。force_hpd是强制热插拔检测这对 eDP 屏来说通常是需要的因为内嵌的 eDP 面板并没有物理上的热插拔动作不做强制处理的话链路可能会因为 HPD 引脚没有拉高而一直无法建立连接。2.3 背光与电源的 DTS 配置背光这块我要单独拿出来说因为它在设备树配置里最容易出错出了问题外在表现又很隐蔽。最常见的方案是 PWM 背光SoC 输出一路 PWM 波给背光驱动芯片通过占空比控制 LED 平均电流从而调节亮度。对应的设备树节点如下backlight: backlight { compatible pwm-backlight; pwms pwm2 0 1000000 0; brightness-levels 0 20 40 80 120 160 200 255; default-brightness-level 4; enable-gpios gpio1 RK_PC2 GPIO_ACTIVE_HIGH; power-supply vcc_bl_3v3; status okay; };这个节点里信息量不小。pwms pwm2 0 1000000 0这一行表示使用 pwm2 通道 0周期为 1000000 纳秒也就是 1kHz 的 PWM 频率。第四个数 0 是极性标志表示正常极性。如果你在示波器上看到背光波形是反的把最后一个参数改成 PWM_POLARITY_INVERTED数值为 1即可。brightness-levels定义的是亮度等级映射表。这是一个很实用的特性很多屏在低亮度区间人眼对亮度变化特别敏感直接做线性映射会导致低亮度时调整一点点就感觉天翻地覆而高亮度区间又察觉不到变化。通过映射表可以人为把低亮度区间的分辨率提高视觉体验会好很多。enable-gpios是背光电路的使能引脚一般接到背光驱动芯片的 EN 脚。只有当这个引脚拉高、PWM 波形正常、背光电源正常三个条件同时满足时背光才会亮。排查背光问题时按这个顺序查效率会高不少。3. DRM 框架下的驱动实现3.1 Panel 驱动的核心接口设备树描述完静态信息内核需要按 DRM/KMS 框架把设备动态组织起来。在 Rockchip 平台上面板驱动主要围绕drm_panel结构体展开。我们开发时一般做两件事一是注册一个drm_panel二是实现它的回调函数。常用的回调有static const struct drm_panel_funcs my_panel_funcs { .prepare my_panel_prepare, .enable my_panel_enable, .disable my_panel_disable, .unprepare my_panel_unprepare, .get_modes my_panel_get_modes, };prepare和enable的区别初学者总容易搞混。简单记prepare做的事是“把屏叫醒”包括上电、拉复位、发初始化命令enable做的事是“叫醒之后正式开始刷新”通常在函数里会开启 DSI 视频流。对应的disable和unprepare是关闭流程的两阶段操作disable负责停止视频流unprepare负责进一步下电和进入休眠。一个简化但不失典型的prepare实现如下static int my_panel_prepare(struct drm_panel *panel) { struct my_panel *p to_my_panel(panel); struct mipi_dsi_device *dsi p-dsi; int ret; /* 1. 打开屏幕供电 */ ret regulator_enable(p-supply); if (ret) return ret; /* 2. 拉高复位引脚等一段稳定时间 */ gpiod_set_value_cansleep(p-reset_gpio, 1); msleep(20); /* 3. 拉低复位引脚进入正常模式 */ gpiod_set_value_cansleep(p-reset_gpio, 0); msleep(120); /* 4. 发送屏厂提供的初始化序列 */ ret mipi_dsi_dcs_write_buffer(dsi, init_cmd, ARRAY_SIZE(init_cmd)); if (ret) return ret; return 0; }上电时序这点我要多说一句屏厂手册上给的时序图一定不要凭感觉“优化”。比如有些屏要求 VCI 和 VDDIO 要按顺序先后上电复位引脚在电源稳定之后还要再保持几十毫秒高电平才允许拉低。如果为了省事把延时缩短轻则初始化失败重则在多块板子里出现“偶尔能亮、偶尔不亮”这种最难排查的随机性问题。3.2 初始化序列的发送策略初始化命令序列也就是屏厂通常说的 init sequence是驱动里最关键的非逻辑代码。它是十几条甚至几十条通过 MIPI DSI 写寄存器命令的集合用于配置屏内部 TCON 的伽马、电压、扫描方向等参数。发送策略上要区分两种屏的差异一是**视频模式Video Mode**的 DSI 屏。这种屏内部没有 FrameBuffer数据要在每个刷新周期持续不断从 host 端传过来。初始化序列发送时间很短之后就需要 DSI Controller 立即开始推流否则屏上什么都看不到。二是**命令模式Command Mode**的 DSI 屏。这种屏内部自带 RAMhost 端把整帧数据写入屏内 GRAM屏自己负责刷新。初始化命令相对复杂还需要处理 TETearing Effect信号来做帧同步否则快速滑动画面时会出现撕裂。RK3576 上如果调试的是常见 1080P 屏一般走的是视频模式。这种情况下初始化序列通常在prepare阶段发送。注意发送时序里要遵从一个原则复位释放之后不能马上发命令要等屏端电源和内部时钟稳定。我一般复位释放之后至少延时 120ms 再发第一条命令虽然屏厂手册里可能只标 20ms但留出余量能避开很多批次差异问题。对于上送命令Linux 内核提供了一组mipi_dsi_dcs_*接口mipi_dsi_dcs_write_buffer(dsi, data, len); mipi_dsi_dcs_set_display_on(dsi); mipi_dsi_dcs_exit_sleep_mode(dsi);这里还有一个实战经验初始化命令延时是分命令级的有些命令后面必须跟一段 delay有些则要求连续发。正确做法是把命令和延时做成一张表逐条顺序执行struct panel_init_cmd { u8 type; u8 len; u8 data[128]; u32 delay_ms; };然后用一个循环统一执行避免在驱动代码里到处散落msleep()后期维护起来痛苦。3.3 Bridge 芯片驱动的落地如果硬件上没有直接使用 MIPI DSI 接口的屏而是用了一颗桥接芯片把 DSI 转成 LVDS 或者 RGB 信号那驱动开发就还要多处理一个 bridge。Linux DRM 框架下bridge 用drm_bridge结构体描述。Rockchip 的 DSI Controller 驱动中会调用drm_bridge_attach把挂在它下游的 bridge 连接起来。一个典型的 DSI 转 LVDS 桥接驱动需要实现drm_bridge_funcs的attach、mode_valid、enable等回调。实际操作中选用桥接芯片时除了分辨率支持、接口类型、供电和时钟要求还要特别注意它的 MIPI DSI 输入格式。很多桥接芯片对“RGB888 还是 RGB666、单通道还是双通道”有明确限制配置不对时常见现象是画面偏色或者直接无显示。如果不是自己从头写 bridge 驱动一般优先确认内核里有没有现成的panel-simple、ti-sn65dsi86、lt8912这类现成驱动能用主线驱动解决的问题尽量不要自己造轮子。自己造轮子带来的增量维护成本在项目后期是很麻烦的。4. 屏参确认与调试路径4.1 屏参的来源与确认方法“屏参”是嵌入式显示圈的黑话指的是屏的时序参数水平有效像素、水平前肩/后肩/同步脉宽HFP/HBP/Hsync垂直方向对应的 VFP/VBP/Vsync以及像素时钟频率。这些数据组成drm_display_mode决定 VOP2 按什么时序把数据推给屏。屏参一般有三个来源屏厂提供的规格书里面通常有一张 Timing 参数表。内核里现成 panel 驱动如果屏型号和某个知名模组兼容可以直接复用。通过示波器实测屏的 Demo 板驱动信号获得。拿到屏参后关键是会换算drm_display_mode里的字段。举个例子一块 1080P 屏规格书里标的是Horizontal: 1920 64 (HFP) 44 (Hsync) 88 (HBP) Vertical: 1080 40 (VFP) 8 (Vsync) 12 (VBP) Pixel Clock: 148.5MHz那么在my_panel_get_modes里面可以这样填static int my_panel_get_modes(struct drm_panel *panel, struct drm_connector *connector) { struct drm_display_mode *mode; mode drm_mode_duplicate(connector-dev, my_panel_default_mode); if (!mode) return 0; drm_mode_set_name(mode); mode-type DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED; drm_mode_probed_add(connector, mode); return 1; }而my_panel_default_mode的定义是static const struct drm_display_mode my_panel_default_mode { .clock 148500, .hdisplay 1920, .hsync_start 1920 64, .hsync_end 1920 64 44, .htotal 1920 64 44 88, .vdisplay 1080, .vsync_start 1080 40, .vsync_end 1080 40 8, .vtotal 1080 40 8 12, .flags DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC, };这里有个很常见的错觉像素时钟越大越好。实际上 VOP2 的dclk要按这个clock值来配如果传的时钟比屏实际支持的偏高屏上可能出现杂点或者闪动。偏低了又会出现显示区域偏移或刷新率不达标。计算时建议把时钟保留到小数点后一位我见过一些驱动写法直接四舍五入成整数导致 60Hz 刷新率差那么一点屏幕整体发虚。4.2 用 sysfs 和 debugfs 进行状态排查模块加载之后系统里会挂出一套 DRM 相关的调试接口这是排查问题最快的手段。先看内核日志dmesg | grep -i -E dsi|panel|backlight|vop这个命令可以快速确认驱动有没有 probe 成功、有没有在设备树里面找到对应节点。如果驱动自己报了 error比如 “failed to attach dsi device”那就回头查 DSI 控制器的设备树状态以及 controller 和 panel 的 port endpoint 是否接上。再看 DRM 的整体状态cat /sys/kernel/debug/dri/0/state这个节点会把当前所有 connector、crtc、plane 的绑定情况打出来是亮屏前必看的。如果能看到 connector 状态为 connectedmode 列表正确基本就说明 panel 驱动那边没问题了。亮度和背光的运行时操作则看cat /sys/class/backlight/backlight/brightness cat /sys/class/backlight/backlight/actual_brightness echo 128 /sys/class/backlight/backlight/brightness如果 echo 之后actual_brightness不变化问题通常在 PWM 配置或者背光驱动芯片的使能脚如果能变化但屏的亮度不随之变化那就要看 PWM 波形是够真的到了背光芯片的引脚上以及亮度等级映射表是否映射合理。4.3 示波器和逻辑分析仪的波形验证软件层面检查完问题还定位不了那就动用硬件工具。示波器和逻辑分析仪在显示驱动调试中是不可替代的。最优先测的是三个信号第一测 MIPI DSI 的差分时钟通道。MIPI DSI 的 Lane 是用差分对传输的时钟频率和像素时钟、lane 数有固定换算关系。如果屏是 4-lane DSI1080P60Hz 的速率大致在 600 Mbps 量级测到明显的连续翻转的差分时钟说明 SoC 侧数据通道已经拉起来了。第二测背光 PWM 波形。用示波器看 PWM 的频率和占空比是否正常。这里能查出不少奇怪问题比如 PWM 的极性反了占空比拉满时反而等于熄灭。第三测复位引脚的时序。用示波器抓上电瞬间复位引脚的拉高拉低过程对照屏厂要求的时序图检查上升沿的位置和持续时间。我遇到过屏偶尔不亮的 case最后定位就是复位时间和电源稳定时间有几百微妙级的差异导致批间波动。如果手头有逻辑分析仪可以抓 DSI 总线上的命令包确认 host 到底有没有把正确的 init sequence 发出去。5. 常见问题与排查技巧5.1 屏幕点不亮供电与时序点不亮是 LCD 驱动开发里遇到最多的现象没有之一。我的排查顺序是固定的先看背光、再看复位、再看初始化命令、最后看数据通路。背光不亮的典型症状是屏幕“黑得干净”用手电筒照屏幕可以隐约看到图像轮廓。这种情况说明面板已经被点亮只是背光源没有工作。查背光时优先查 enable 引脚、PWM 波形和背光电源三件事按这个顺序排查效率最高。如果是完全没有图像用手电筒照也什么都看不到那重点查屏的供电和复位。用万用表量模组的各路电压确认 VDD、VCI、VDDIO 等电源脚都正常再看复位送进去没有。这里我经常踩的一个坑是 GPIO 申请失败后直接无视返回值继续往下走结果整个 prepare 流程全乱。代码里 GPIO 操作一定要查返回值至少打一条错误日志。还有一种隐蔽情况是“开机第一次能亮重启后不亮”。这个多半是休眠唤醒流程没有把屏的状态恢复干净。在unprepare里做了什么prepare里就要对称地恢复尤其是复位引脚和电源的控制顺序不能依赖侥幸状态。5.2 亮屏但花屏时序、带宽与 lane 配置花屏问题比点不亮更让人头疼因为软件配置看起来都对电源时序也对屏就是显示乱码。排除物理连接问题后第一个要确认的就是 MIPI DSI 的 lane 数量和时钟参数。在设备树里 DSI 节点一般有>mipi_dsi1 { ... panel0 { ... ports { port0 { reg 0; ... mipi_dsi1_in_panel: endpoint { remote-endpoint dsi1_out_panel; >