第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.5V7V像素阳极正高压)等等电压,每一路的电压值、上电/掉电顺序(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配置里面,而不是复杂的内核逻辑。

posted @ 2026-10-03 12:13  素师良码  阅读(1)  评论(0)    收藏  举报