第4篇:移植 Panel 驱动-点亮一块屏幕流程浅浅浅析

摘要

做Android显示驱动,最经典入门任务就是点亮一块新Panel。本篇完整梳理从屏幕手册阅读、上电时序、DTS编写、驱动适配,到内核调试、屏幕点亮的全流程,附带主线内核标准drm_panel驱动示例代码,同时标注高频踩坑点。

前言

前面几篇,我们初步搞懂了Android显示完整链路、DRM核心框架、MIPI-DSI协议。理论看懂之后,第一个工程实战任务就是移植Panel驱动,点亮屏幕 。

很多新手同学上手时,直接上手改驱动代码,最后卡在黑屏、上电时序错乱、DTS配置错误,反复排查几天,但在实际工程中,根本不会给你很久的时间bringup,几乎屏幕和主板到手当天就要亮起来,因此需要后合理的策略进行bringup(不仅技术角度,还有其他主观客观因素,后续篇章展开聊聊编者的理解。)屏幕决定了后续其他模块同事的开发进度,虽然可以通过投屏来操作屏幕,但是会极大的降低开发进度。

屏幕点亮本质上就两件核心事:硬件上电时序 + DTS设备树描述 ,再搭配Panel DDIC(Display Driver Integrated Circuit)驱动的初始化序列。

核心一句话:DTS告诉内核这块屏幕硬件资源是什么;上电时序保证DDIC在正确的电源、复位顺序下启动;Panel驱动下发初始化命令,最终屏幕正常上电、接收MIPI图像。

一、前期准备:拿到屏幕规格书

移植第一步不是写代码,而是读屏的datasheet,提取关键信息:

  1. 电源:VDD(3.0V DDIC模拟电)、VCI(1.8V逻辑数字电)、ELVSS(-5.5V~-7V像素阴极负高压)、ELVDD(+5.5V~7V像素阳极正高压)等等电压,每一路的电压值、上电/掉电顺序(OLED发光靠的就是ELVSS和ELVDD的大压差)
  2. 复位引脚:复位信号(RST)的高低电平、复位保持时间
  3. MIPI-DSI参数:lane数量、传输速率、Video/Command模式、像素时序(HSA/HBP/ACTIVE/HFP,VSA/VBP/ACTIVE/VFP)
  4. 屏幕初始化命令序列(DSI写寄存器指令)
  5. 屏幕最大分辨率、色深

踩坑点:

不要随便抄别的屏DTS ,不同屏幕电源域、复位时序、时序参数完全不一样,直接复制大概率黑屏。(当然确定是同DDIC的阔以抄,提高开发速度)

二、重中之重:Panel上电/下电时序(80%黑屏问题根源)

绝大多数点亮失败,不是DRM或者MIPI代码问题,而是上电顺序错误 (因为DRM框架层的code,几乎不需要重新开发,MTK或者Qcom已经提供较为完整可靠的source code)。

编者理解的一般 标准MIPI屏上电流程:

  1. 供给数字电源 VCI,数字先稳
  2. delay_ms(10)
  3. 供给屏幕模拟电源 VDD
  4. delay_ms(10)
  5. 等待电源稳定延时(datasheet指定,一般ms级)
  6. 高低高RST复位引脚,保持复位一段时间
  7. 释放复位(拉高RST),等待屏IC内部稳定
  8. ELVDD上电(正高压先上,再上负高压)
  9. delay_ms(10)
  10. ELVSS上电
  11. delay_ms(10)
  12. DSI Host开启,发送Panel初始化命令序列
  13. 初始化完成,开启MIPI视频流,屏幕输出图像
  14. ...

下电时序(休眠/关机同样重要,顺序反过来)

  1. 关显示(发0x28 0x10命令)
  2. 停止MIPI图像数据流
  3. ELVSS下电
  4. delay_ms(10)
  5. ELVDD下电
  6. delay_ms(10)
  7. 拉低RST复位
  8. 关闭VCI
  9. 关闭VDDI

常见错误:

复位释放太早、电源上电顺序颠倒、延时时间不足,现象:黑屏、偶尔能点亮、冷启动失败等等。

三、DTS设备树编写:描述屏幕硬件资源

DTS的作用:向内核描述硬件资源,不需要修改内核C代码,描述电源、复位引脚、DSI通道、屏幕时序。

基础示例 DTS里核心配置项

复制代码
/* 示例DTS片段,主线开源内核风格,仅用于演示 */`
`panel: panel@0 {`
`    compatible = "xxx,mipi-panel";`
`    reg = <0>;`
`    vsp-supply = <&pmic_vsp>;`
`    vsn-supply = <&pmic_vsn>;`
`    vddio-supply = <&pmic_vddio>;`
`    reset-gpios = <&gpio 10 GPIO_ACTIVE_LOW>;`
`    #address-cells = <1>;`
`    #size-cells = <0>;`

`    panel-timing {`
`        hactive = <720>;`
`        vactive = <1280>;`
`        hfront-porch = <20>;`
`        hback-porch = <40>;`
`        hsync-len = <10>;`
`        vfront-porch = <10>;`
`        vback-porch = <10>;`
`        vsync-len = <2>;`
`    };`

`    panel-init-sequence = [`
`        /* DSI初始化指令,根据屏规格书填充 */`
`        05 00 00 00 00 11,`
`        05 00 00 00 00 29,`
`    ];`
`};`
`
  1. 电源供应:vsp-supply、vsn-supply、vddio-supply,关联PMIC电源域
  2. 复位引脚:reset-gpios,指定GPIO,定义复位极性
  3. MIPI DSI配置:data lanes数量、dsi时钟速率
  4. 显示时序参数:hactive、vactive,以及前后消隐HBP/HSA/HFP,VBP/VSA/VFP
  5. 初始化命令序列:panel-init-sequence,DSI发送的一系列寄存器指令
  6. 电源、复位的延时参数,对应datasheet时序

简单理解:

内核MIPI DSI host驱动读到DTS配置,就知道:用哪个GPIO复位、打开哪几路PMIC电源、屏幕时序是多少、开机要发哪些初始化命令。

调试小技巧:

DTS修改后,重新单编译dtbo,替换设备树(如果Android全编译的话,老慢了1h起步;另外,刷机后反编译看下dtbo里的参数是否是自己配置的,否则容易导致修改不带入的风险);优先核对DTS参数和屏幕手册,不要先去修改panel驱动c文件。

对于Qcom平台来说,bringup阶段的参数配置大部分在dtsi文件;对于MTK平台来说,bringup阶段的参数配置大部分是driver的.c驱动文件(当然也可重构到dtsi,但目前主流是这么分的)。

四、Panel驱动代码适配(drm_panel框架,主线Linux开源代码片段)

说明:下面代码取自Linux主线内核drm_panel示例模板,开源通用框架,不属于任何公司项目代码 。 内核标准drm_panel驱动,核心回调:prepare(上电复位+初始化)、enable(开启MIPI数据流)、disable、unprepare(下电)

复制代码
#include <linux/module.h>`
`#include <linux/of.h>`
`#include <linux/gpio/consumer.h>`
`#include <linux/regulator/consumer.h>`
`#include <drm/drm_panel.h>`
`#include <drm/drm_mipi_dsi.h>`

`struct my_panel_data {`
`    struct drm_panel panel;`
`    struct mipi_dsi_device *dsi;`
`    struct regulator *vsp;`
`    struct regulator *vsn;`
`    struct regulator *vddio;`
`    struct gpio_desc *reset_gpio;`
`    bool prepared;`
`};`

`// 容器宏,通过drm_panel结构体反向拿到自定义panel私有数据`
`#define to_my_panel(p) container_of(p, struct my_panel_data, panel)`

`// 【1】prepare:上电、复位、发送初始化序列,硬件准备阶段`
`static int my_panel_prepare(struct drm_panel *panel)`
`{`
`    struct my_panel_data *pdata = to_my_panel(panel);`
`    struct mipi_dsi_device *dsi = pdata->dsi;`
`    int ret;`

`    if (pdata->prepared)`
`        return 0;`

`    // 1.上电,按照datasheet时序打开三路电源`
`    ret = regulator_enable(pdata->vsp);`
`    if (ret) goto err_vsp;`
`    ret = regulator_enable(pdata->vsn);`
`    if (ret) goto err_vsn;`
`    ret = regulator_enable(pdata->vddio);`
`    if (ret) goto err_vddio;`

`    msleep(20); // 等待电源稳定,参考屏规格书`

`    // 硬件复位:拉低复位,保持延时,再释放复位`
`    gpiod_set_value(pdata->reset_gpio, 0);`
`    msleep(10);`
`    gpiod_set_value(pdata->reset_gpio, 1);`
`    msleep(20);`

`    // 发送DTS中定义的panel初始化序列`
`    ret = mipi_dsi_panel_init(dsi);`
`    if (ret) {`
`        dev_err(&dsi->dev, "panel init cmd failed\n");`
`        goto err_init;`
`    }`

`    pdata->prepared = true;`
`    return 0;`

`err_init:`
`    gpiod_set_value(pdata->reset_gpio, 0);`
`    regulator_disable(pdata->vddio);`
`err_vddio:`
`    regulator_disable(pdata->vsn);`
`err_vsn:`
`    regulator_disable(pdata->vsp);`
`err_vsp:`
`    return ret;`
`}`

`// 【2】enable:开启MIPI视频流,输出像素画面`
`static int my_panel_enable(struct drm_panel *panel)`
`{`
`    struct my_panel_data *pdata = to_my_panel(panel);`
`    // 发送DSI指令,开启显示输出`
`    return mipi_dsi_dcs_write(pdata->dsi, MIPI_DCS_SET_DISPLAY_ON, NULL, 0);`
`}`

`// 【3】disable:关闭画面输出`
`static int my_panel_disable(struct drm_panel *panel)`
`{`
`    struct my_panel_data *pdata = to_my_panel(panel);`
`    return mipi_dsi_dcs_write(pdata->dsi, MIPI_DCS_SET_DISPLAY_OFF, NULL, 0);`
`}`

`// 【4】unprepare:下电,关闭电源,复位`
`static int my_panel_unprepare(struct drm_panel *panel)`
`{`
`    struct my_panel_data *pdata = to_my_panel(panel);`
`    if (!pdata->prepared)`
`        return 0;`

`    gpiod_set_value(pdata->reset_gpio, 0);`
`    regulator_disable(pdata->vddio);`
`    regulator_disable(pdata->vsn);`
`    regulator_disable(pdata->vsp);`
`    pdata->prepared = false;`
`    return 0;`
`}`

`// 注册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,`
`};`

`// DSI设备probe:解析DTS资源,获取regulator、gpio`
`static int my_panel_probe(struct mipi_dsi_device *dsi)`
`{`
`    struct my_panel_data *pdata;`
`    int ret;`

`    pdata = devm_kzalloc(&dsi->dev, sizeof(*pdata), GFP_KERNEL);`
`    if (!pdata)`
`        return -ENOMEM;`

`    pdata->dsi = dsi;`
`    drm_panel_init(&pdata->panel, &dsi->dev, &my_panel_funcs, DRM_PANEL_CONNECTOR);`

`    // 从DTS解析电源、复位GPIO`
`    pdata->vsp = devm_regulator_get(&dsi->dev, "vsp");`
`    pdata->vsn = devm_regulator_get(&dsi->dev, "vsn");`
`    pdata->vddio = devm_regulator_get(&dsi->dev, "vddio");`
`    pdata->reset_gpio = devm_gpiod_get(&dsi->dev, "reset", GPIOD_OUT_LOW);`

`    // 绑定DSI设备,注册panel`
`    mipi_dsi_set_drvdata(dsi, pdata);`
`    drm_panel_add(&pdata->panel);`
`    return 0;`
`}`

`static const struct of_device_id my_panel_of_match[] = {`
`    { .compatible = "xxx,mipi-panel" },`
`    { /* Sentinel */ }`
`};`
`MODULE_DEVICE_TABLE(of, my_panel_of_match);`

`static struct mipi_dsi_driver my_panel_driver = {`
`    .probe = my_panel_probe,`
`    .driver = {`
`        .name = "xxx_mipi_panel",`
`        .of_match_table = my_panel_of_match,`
`    },`
`};`
`module_mipi_dsi_driver(my_panel_driver);`

`MODULE_LICENSE("GPL");`
`MODULE_DESCRIPTION("sslm` `Demo MIPI panel driver based on drm_panel");`
`

重点区分prepare与enable: prepare:上电、复位、发初始化命令(硬件准备阶段) enable:打开MIPI视频流,输出像素画面 很多新手混淆这两个回调,导致时序错乱,开机先走probe函数(原理后续拆细论述,属于设备驱动开发基础知识)。

五、完整调试排错流程(实战顺序)

  1. 先确认硬件:供电、排线、硬件焊接,用万用表测量各路电源电压、复位引脚电平,确认硬件本身没问题
  2. 检查DTS:核对时序、电源、gpio极性,dtbo是否成功替换
  3. 打开内核log,查看drm、mipi dsi驱动打印:adb shell "dmesg -w| grep -E 'dsi|panel|drm|lcm|disp|panel'"
  4. 看probe是否成功,有没有资源获取失败(电源、gpio获取失败)
  5. 看prepare阶段:上电、复位、初始化命令是否发送成功
  6. 看时序参数是否被DRM校验通过,atomic模式有没有报错
  7. 最后看MIPI链路是否成功建立,有没有MIPI报CRC错误

高频故障现象与根因

  • 黑屏,无任何屏幕初始化log:DTS没加载成功 / 电源没打开
  • 背光有,但无图像:初始化命令发送失败 / MIPI时序参数错误
  • 冷启动黑屏,热重启正常:上电延时不足,复位时序问题
  • 花屏:MIPI速率、lane配置错误,或者像素时序参数不对

常见问题和排除思路后续新开篇章论述。

六、新手学习建议

  1. 先拿一块成熟可点亮的Panel驱动+DTS作为参考,对照屏幕手册逐行看懂每一个配置
  2. 用已经bringup项目,打开log开关(不开可能抓不全乎)抓重启开机log,逐步梳理kernel log流程,一步一步在code里和log打印对应,明确代码流向。
  3. 优先调试DTS与时序,尽量不要一开始就大量修改C驱动代码
  4. 每改一项,单独测试,不要一次性修改大量参数,否则很难定位问题
  5. 保存调试日志,后续遇到同类黑屏问题可以快速复盘
  6. 有空的话详细解构每行code逻辑

结尾

点亮Panel,是显示驱动工程师的第一个实战关卡。 它不是单纯写代码,而是硬件时序 + 设备树 + DRM panel driver 三者结合。 80%的黑屏问题,根源都藏在上电时序和DTS配置里面,而不是复杂的内核逻辑。

更多详细笔记,WeChat搜索:素师良码