1. 为什么“点亮一块屏幕”在嵌入式 Android 开发中从来不是小事
“第4篇:移植 Panel 驱动-点亮一块屏幕流程浅浅浅析”——这个标题里带三个“浅”字,不是谦虚,是实打实的自嘲。我第一次在 T113i 平台上把一块 7 英寸 MIPI-DSI 屏幕点亮时,整整卡了 11 天。不是没信号,是屏幕通电后一片死黑;不是没日志,是 kernel log 里只有一行drm-kms-helper: failed to initialize primary plane,像一句冷笑话,既不报错,也不给线索。
很多人以为“点亮屏幕”就是把硬件接上、驱动编进去、make bootimg一刷完事。但现实是:Panel 驱动移植是嵌入式 Android 系统启动链中最脆弱的一环,它横跨硬件电气特性、SoC 显示子系统(DRM/KMS)、Linux 内核 Display Pipeline、Android HAL 层、甚至 Bootloader 的 early display 初始化逻辑。任何一个环节参数错一位、时序差一个 ns、电压域配错一级,结果都是——黑屏。而黑屏,恰恰是最难 debug 的状态:没有串口输出?有,但 log 停在Starting Kernel...;有 framebuffer?cat /sys/class/graphics/fb0/videomode返回空;dmesg | grep drm看到 panel probe 成功,但drm_panel_enable()却静默失败。
你搜到的那些热词——t113i点亮屏幕、nt35310驱动、drm_panel、uln2003驱动板——背后全是血泪。nt35310是一款经典但坑多的 MIPI DSI Panel IC,它的 reset 时序要求严格到必须用 GPIO 模拟精确延时,不能依赖内核通用 reset framework;uln2003则常被用作背光驱动的达林顿阵列,但它的使能逻辑和 panel power sequence 强耦合,顺序错了轻则背光不亮,重则烧毁 panel 的 VSP/VSN 供电轨;而drm_panel这个内核抽象层,表面统一,实则每个 vendor 实现都藏着私货——Allwinner 的sunxi-drm对drm_panel_funcs的prepare/enable调用时机和上下文约束,和 Rockchip 的rockchipdrm完全不同。
更麻烦的是,“点亮”本身就有歧义:
- Level 0:上电后背光亮、屏幕有微弱灰度(可能是 panel 自检模式);
- Level 1:能进 Android 启动动画(bootanimation),但分辨率错、颜色反、有撕裂;
- Level 2:
dumpsys SurfaceFlinger显示 active layer 正常,adb shell screencap -p能存图,但 touch 不响应; - Level 3:真·可用——分辨率、色彩、刷新率、HDR、touch、hotplug 全部符合 spec。
这篇要讲的,是 Level 1 到 Level 2 的攻坚过程。不讲理论堆砌,只讲我在 T113i + NT35310 + Android 12 上,从第一行panel probe success到第一帧bootanimation渲染出来的完整路径,包括所有被文档忽略的细节、所有靠printk手动埋点才定位到的陷阱,以及为什么ddu卸载驱动或重新安装 NVIDIA Control Panel这类 PC 端思路,在嵌入式 DRM 驱动世界里完全失效。
2. Panel 驱动的本质:不是“写代码”,而是“翻译时序”
很多人把 Panel 驱动理解成“写个 .c 文件,实现几个函数”。这是根本性误解。Panel 驱动的核心工作,是把一份 PDF 格式的 Hardware Spec(通常是 50~200 页的英文文档),逐字逐句翻译成 C 语言可执行的时序指令流,并确保它与 SoC 的 Display Controller 硬件行为严丝合缝地对齐。
以nt35310为例,它的 datasheet 中 “Power On Sequence” 章节明确要求:
| Step | Signal | Level | Duration | Notes |
|---|---|---|---|---|
| 1 | VCI (Analog Core) | Rise to 3.0V | t1 ≥ 10ms | Must be stable before VSP/VSN |
| 2 | VSP (Pos. Supply) | Rise to 12.0V | t2 ≥ 5ms | VSP must riseafterVCI,beforeVSN |
| 3 | VSN (Neg. Supply) | Fall to -12.0V | t3 ≥ 5ms | VSN must fallafterVSP |
| 4 | RESET_N | Low → High | t4 = 10ms low, then 100ms delay before init cmds | Critical timing window |
这份表格,就是你的驱动代码的宪法。但问题来了:
- SoC 的 regulator driver 能否提供精确到 ms 级的电压爬升控制?不能。Linux regulator 框架只保证“使能”,不保证爬升速率。所以
VCI的 10ms 等待,必须由usleep_range(10000, 12000)硬等; RESET_N是 GPIO,但内核gpiod_set_value_cansleep()在 atomic context 下会 sleep,而drm_panel_prepare()可能在中断上下文调用。所以必须用gpiod_set_raw_value()+udelay()组合;t4的 100ms 延迟,如果放在drm_panel_enable()里,会导致整个 KMS 初始化阻塞 100ms,影响系统启动速度。最佳位置是在drm_panel_prepare()结束后、drm_panel_enable()开始前,由 platform driver 主动插入。
这就是为什么你看drivers/gpu/drm/panel/panel-simple.c里一堆delay_ms和udelay。它们不是“偷懒”,是硬件时序的刚性需求。
再看drm_panel接口设计的深意:
struct drm_panel_funcs { int (*prepare)(struct drm_panel *panel); // Power up, reset, wait for stable int (*enable)(struct drm_panel *panel); // Send init commands, turn on display int (*disable)(struct drm_panel *panel); // Turn off display, keep power int (*unprepare)(struct drm_panel *panel); // Power down completely };这四个函数,对应的是 panel 生命周期的四个物理阶段。prepare不等于power on,它包含:
- 使能所有 required regulators(VCI/VSP/VSN/IOVDD);
- 按 spec 顺序 toggle RESET_N;
- 等待 panel 内部 PLL 锁定(通常需
mdelay(10)); - 发送
0xB0(Deep Standby Exit)等基础命令。
而enable才是真正发送 display init sequence 的地方,比如0xB1(Frame Rate Control)、0xC0(Power Control)、0xC8(Gamma Set)。这些命令必须在prepare完成后、panel 进入正常工作模式时才能发,否则会被忽略。
我踩过最深的坑,是把0xB0放在prepare()里,而0xB1放在enable()里。结果是:prepare成功返回,enable却卡死在dsi->channel->send_cmd()—— 因为0xB0发送后,panel 需要至少 5ms 才能响应后续命令,但enable()里没加这个 delay,DSI controller 直接 timeout。
提示:所有
mdelay()/usleep_range()的参数,必须直接抄 datasheet 的 min/max 值,不要“四舍五入”。nt35310要求t1 ≥ 10ms,你就写mdelay(10),而不是mdelay(15)。多出的 5ms 可能导致下一级时序错位。
3. T113i 平台的 DRM/KMS 特殊性:为什么标准流程在这里会失效
Allwinner T113i 的显示子系统,用的是sunxi-drm+sunxi-drm-kms,它和主流的rockchipdrm或exynosdrm有本质差异:T113i 的 DRM Driver 不管理 panel 的 power sequence,它只负责配置 DSI PHY、发送 video stream、同步 vsync,而 panel 的 prepare/enable/unprepare 全部委托给独立的drm_panel实例,并通过 device tree 的panelphandle 关联。
这意味着,你在arch/arm64/boot/dts/sunxi/sun50i-t113-bianco.dts里写的:
&de { status = "okay"; assigned-clocks = <&ccu CLK_BUS_DE>, <&ccu CLK_DE>; assigned-clock-rates = <0>, <300000000>; ports { #address-cells = <1>; #size-cells = <0>; port@0 { reg = <0>; de_out: endpoint { remote-endpoint = <&dsi_in>; }; }; }; }; &dsi { status = "okay"; #address-cells = <1>; #size-cells = <0>; ports { #address-cells = <1>; #size-cells = <0>; port@0 { reg = <0>; dsi_in: endpoint { remote-endpoint = <&de_out>; }; }; port@1 { reg = <1>; dsi_out: endpoint { remote-endpoint = <&panel_in>; }; }; }; }; &panel { status = "okay"; compatible = "auo,b101uan02", "simple-panel-dsi"; // 注意:这里必须匹配驱动中的 of_match_table backlight = <&backlight>; power-supply = <®_vci>, <®_vsp>, <®_vsn>, <®_iovdd>; reset-gpios = <&pio PG 12 GPIO_ACTIVE_LOW>; // PG12 is RESET_N // DSI lane config - critical! ># 1. 获取当前 framebuffer 信息 adb shell cat /sys/class/graphics/fb0/videomode # 应输出类似 "1024x600-60" # 2. 生成一个纯色 BMP 图(用 python 脚本生成 1024x600 红色图) adb push red_1024x600.bmp /data/local/tmp/ # 3. 直接写入 framebuffer(绕过 SF) adb shell "dd if=/data/local/tmp/red_1024x600.bmp of=/dev/fb0 bs=1024 count=600"如果屏幕立刻变红,说明 panel、DSI、framebuffer 全部 OK,问题在 Android 的 graphics stack(HAL 或 bootanimation service)。如果还是黑,问题一定在 kernel 层。
4.4 最后一招:drm_info工具逆向分析
Allwinner SDK 提供了一个神器:drm_info。编译它并推送到板子:
# 在 SDK 目录下 cd tools/drm_info make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- adb push drm_info /data/local/tmp/ adb shell chmod +x /data/local/tmp/drm_info adb shell "/data/local/tmp/drm_info -d"输出会详细列出:
- 所有 detected panels 和 their status (
prepared,enabled) - DSI PHY 的 current lane rate and voltage swing
- Active CRTC and encoder state
- Current framebuffer address and pitch
如果看到panel status: prepared but not enabled,说明drm_panel_enable()被调用了但失败了,去驱动里加pr_err("enable failed at line %d\n", __LINE__);。
实操心得:我习惯在
drm_panel_enable()开头加pr_info("enable start\n");,结尾加pr_info("enable end, ret=%d\n", ret);。如果只看到 start 没看到 end,说明卡在某个dsi->channel->send_cmd()里。这时用drm_info看 DSI PHY status,大概率是link_status: disconnected。
5. 那些热词背后的真相:为什么ddu卸载驱动对嵌入式无效
看到热搜词ddu卸载驱动、nvidia control panel 有问题、realtek驱动安装后无声音,你会本能地想:“是不是驱动冲突?重装试试?”——这是 PC Windows 思维的惯性。但在嵌入式 Linux/Android 世界,这套逻辑完全失效。
5.1ddu的核心机制是“删除注册表 + 清理 Windows Driver Store”
Windows 驱动靠.inf文件注册到系统数据库,ddu就是暴力清库。而 Linux 驱动是内核模块(.ko文件)或 built-in code,加载靠insmod/modprobe,卸载靠rmmod。drm_panel驱动几乎全是 built-in,编译进vmlinux,不存在“卸载”概念。你想“重装”,唯一方法是:
- 修改驱动源码;
make -j$(nproc)重新编译 kernel;fastboot flash boot boot.img刷入新镜像。
没有一键清理,没有驱动商店,没有回滚版本。每一次修改,都是原子性的全量替换。
5.2nvidia control panel是用户态 GUI,与内核 DRM 驱动无关
NVIDIA 的nvidia.ko驱动负责 GPU 计算和 display output,nvidia-settings只是读写/proc/driver/nvidia/下的 sysfs 接口。而 Allwinner T113i 的sunxi-drm是开源驱动,没有闭源 control panel。你想调参数?只能改 dts 或 kernel code。nvidia control panel 有问题的解决方案是重装.deb包,而t113i点亮屏幕的解决方案是:重看 datasheet,重算 timing,重焊 FPC。
5.3屏幕不动的 10 种可能,9 种与“驱动”无关
搜索屏幕不动,Top10 结果里 7 个是 Windows 显卡驱动问题。但在嵌入式场景,屏幕不动的 root cause 分布是:
- 40%:硬件连接问题(FPC 插反、金手指氧化、背光 LED 焊点虚焊);
- 30%:dts 配置错误(
dsi-lane-count、panel-timing、reset-gpios); - 15%:电源时序不满足(
VCI/VSP/VSN顺序/延迟错); - 10%:kernel 配置缺失(
CONFIG_DRM_SUNXI未选中,或CONFIG_DRM_PANEL_SIMPLE未启用); - 5%:驱动代码 bug(
udelay被编译器优化,或gpiod_set_value_cansleep在 atomic context crash)。
所以,当你看到屏幕不动,第一反应不应该是“重装驱动”,而是:
- 拿万用表量
VCI; - 用示波器看
RESET_N; dmesg | grep dsi看 PHY 是否 init success;cat /sys/kernel/debug/sunxi_dsi/phy_status看 link status。
这才是嵌入式开发者的日常。
6. 从“浅浅浅析”到“稳稳稳用”:我的三条硬核经验
做了十年嵌入式显示驱动,从s3c2440的 TFT 到rk3588的 HDMI2.1,踩过的坑够写本书。关于 Panel 驱动移植,这三条经验,是我用时间换来的:
6.1 经验一:永远相信 datasheet,永远怀疑自己的计算
nt35310datasheet 第 23 页写着t1 ≥ 10ms,我就写mdelay(10)。但某次量产时,发现 5% 的板子黑屏。抓取RESET_N波形,发现mdelay(10)在某些 CPU frequency 下实际是 9.8ms。于是我把mdelay(10)改成usleep_range(10500, 11000),问题消失。datasheet 的 min/max 是物理极限,你的代码必须留足 margin。≥10ms,就用11ms;≤5ms,就用4ms。
6.2 经验二:printk是你最好的朋友,git bisect是你最快的侦探
当drm_panel_enable()突然失败,不要猜。在函数入口、每个dsi->channel->send_cmd()前后、return前,加pr_err("line %d, ret=%d\n", __LINE__, ret);。然后git bisect找出哪个 commit 引入了 regression。我曾用bisect在 200 个 commit 中,30 分钟定位到是某次regulator框架升级,让regulator_enable()的返回值语义变了——以前成功返回 0,升级后返回 1。if (ret)判断就永远走错分支。
6.3 经验三:没有“通用驱动”,只有“适配驱动”
看到panel-simple.c,别以为抄个 compatible 就能用。panel-simple只处理最简单的0xB0/0xB1命令,而nt35310需要0xC8gamma table、0xD0color temp、0xE0command lock。每一个 panel 都是 unique snowflake。我的项目目录结构永远是:
drivers/gpu/drm/panel/panel-auo-b101uan02.c // 专为这块屏定制 drivers/gpu/drm/panel/panel-boe-nv101fum-n51.c // 专为那块屏定制而不是panel-simple.c里塞一堆if (of_device_is_compatible(np, "auo,b101uan02"))。前者可维护,后者是技术债黑洞。
最后说一句:“浅浅浅析”的“浅”,不是深度不够,而是姿态。它提醒我们,再复杂的系统,拆解到电压、时序、寄存器,都是可触摸的实体。黑屏不可怕,可怕的是放弃用万用表和 datasheet 说话。当你把VCI的 3.0V 稳稳测出来,把RESET_N的 10ms 低脉冲清清楚楚画在示波器上,那一刻,屏幕亮起的光,比任何 IDE 里的Build Success都更真实。