第10篇:屏ESD Check内核逻辑驱动代码完整拆解

前言

做Android/Linux显示驱动,MIPI屏ESD问题是高频现场bug。静电干扰会导致MIPI链路异常、面板寄存器错乱、黑屏花屏闪屏。很多Panel驱动里都会实现ESD Check(静电检测)机制,定时去读取Panel寄存器、DDIC关于ESD相关的中断或者某个GPIO脚的电平变化,判断屏幕是否异常,触发软复位/硬复位恢复屏幕。

很多同学只知道驱动里有个ESD检测线程,但不清楚:

  1. ESD Check的底层原理是什么?
  2. 在内核DRM panel驱动中放在什么位置?
  3. 检测线程如何创建、启停、休眠?
  4. 读寄存器失败为什么就能判定ESD异常?
  5. 完整的代码逻辑怎么写,有哪些坑?

本文编者结合Linux DRM Panel开源驱动框架,完整拆解ESD Check。

一、ESD Check 原理

1. 静电对MIPI屏的影响

静电(ESD)冲击面板或者MIPI走线后,会出现几种现象:

  • MIPI DSI链路不稳定,Host和Panel通信断连
  • Panel内部寄存器值被篡改
  • 面板逻辑卡死,不再刷新画面,表现黑屏/花屏/闪屏硬件本身无法自动恢复,需要软件检测到异常,执行复位流程。

2. ESD Check核心思路

定期轮询读取Panel的状态寄存器(一般是Panel的ID寄存器、状态寄存器,读这个寄存器不需要改变屏幕显示):

  1. 定时发起DSI读指令,读取Panel指定寄存器
  2. 判断读取返回值: 读取成功,数据符合预期 > 屏幕正常,继续等待下一轮检测 ;读取失败 / 返回数据异常 > 判断发生ESD干扰
  3. 触发ESD恢复流程:Panel复位(断电时序+上电时序+重新发送初始化序列),恢复屏幕

为什么读寄存器可以判断ESD? 正常工作的Panel,ID寄存器是固定值。静电干扰后,DSI传输出错,要么读超时,要么读到乱码。

另外,ESD Check不能100%捕获所有静电问题,只能检测Panel通信链路异常,属于软件兜底方案,不能替代硬件ESD防护。

3. 触发时机

  • 屏幕已经点亮之后才启动ESD检测线程;
  • 屏幕suspend休眠时,停止ESD线程;resume唤醒后,重新开启线程。不能在屏没点亮时跑ESD检测,会造成无效DSI读写、内核报错。

二、内核架构:ESD Check放在哪里?

当前主流Linux DRM框架下,ESD检测逻辑一般封装在drm_panel驱动内(panel-xxx.c)。

  1. kthread 内核线程:ESD轮询任务主体,循环休眠 → 读寄存器 → 判断异常
  2. dsi host 接口:dsi_host_transfer,发送MIPI读寄存器指令
  3. work/workqueue(其他方案):部分老方案用延时工作队列,但是workqueue容易被其他任务抢占;长时间轮询ESD场景优先用kthread
  4. drm_panel ops:panel prepare/unprepare,屏上电下电,ESD异常复位时调用
  5. flag控制变量:esd_enable / esd_running,用来控制线程启停,防止竞态

两种实现方案对比

  • kthread:可控性强,可以长时间sleep,适合周期性轮询ESD;异常复位过程可以在线程内串行执行
  • delayed_work:简单,但复位流程较长时容易出现work重入,需要加锁保护

存在的竞态场景

  1. ESD 线程正在执行 Panel 复位(unprepare/prepare),同时系统触发 suspend 休眠,两套流程同时操作 Panel 电源 / MIPI;
  2. ESD 连续多次触发复位,多次上下电并发;
  3. 线程退出和 ESD 复位同时执行,访问野指针。

解决方案:

  • mutex互斥锁:保护 Panel 上电 / 下电 / ESD 复位临界区;
  • 状态标记esd_thread_run:安全控制线程循环退出;
  • 增加panel_in_recovery标志,防止 ESD 连续重复触发复位;
  • 日志分级:区分 dbg 调试日志、info 正常打印、warn 告警、error 错误,量产的user版本可关闭 debug 日志,减少 log 刷屏,userdebug版本可默认打开。

三、code示例

bash 复制代码
#include <drm/drm_panel.h>
#include <linux/kthread.h>
#include <linux/mutex.h>
#include <linux/dev_printk.h>
#include <video/mipi_display.h>
// Panel私有结构体,ESD相关成员增加竞态保护标记
struct my_panel_data {
    struct drm_panel base;
    struct mipi_dsi_device *dsi;
    /* ESD Check */
    struct task_struct *esd_thread;
    struct mutex esd_lock;          // ESD临界区互斥锁
    bool esd_enable;
    bool esd_thread_run;
    bool panel_in_recovery;         // 标记:正在执行ESD复位,防止重入
    unsigned int esd_poll_interval; // 轮询周期 ms
    unsigned int esd_max_retry;     // 最大连续重试次数,防止死循环复位
    u8 check_reg;
    u8 expect_val;
};
static inline struct my_panel_data *to_my_panel(struct drm_panel *panel)
{
    return container_of(panel, struct my_panel_data, base);
}
/**
 * my_panel_esd_read_reg - DSI DCS读Panel寄存器
 * @ctx: panel私有数据
 * @reg: 待读取寄存器地址
 * @val: 读出的值输出
 * Return: 0成功,负数失败
 */
static int my_panel_esd_read_reg(struct my_panel_data *ctx, u8 reg, u8 *val)
{
    struct mipi_dsi_msg msg;
    ssize_t ret;
    u8 buf[1];
    memset(&msg, 0, sizeof(msg));
    msg.channel = ctx->dsi->channel;
    msg.type = MIPI_DSI_DCS_READ;
    msg.tx_buf = &reg;
    msg.tx_len = 1;
    msg.rx_buf = buf;
    msg.rx_len = 1;
    ret = mipi_dsi_transfer(ctx->dsi, &msg);
    if (ret < 0) {
        dev_dbg(&ctx->dsi->dev, "ESD read reg 0x%02x failed, ret:%zd\n", reg, ret);
        return ret;
    }
    *val = buf[0];
    dev_dbg(&ctx->dsi->dev, "ESD read reg 0x%02x val:0x%02x\n", reg, *val);
    return 0;
}
/**
 * my_panel_esd_recover - ESD异常复位恢复流程
 * 加锁保护,防止suspend与ESD复位并发操作panel
 */
static void my_panel_esd_recover(struct my_panel_data *ctx)
{
    mutex_lock(&ctx->esd_lock);
    if (ctx->panel_in_recovery) {
        dev_warn(&ctx->dsi->dev, "ESD recover already in progress, skip\n");
        mutex_unlock(&ctx->esd_lock);
        return;
    }
    ctx->panel_in_recovery = true;
    dev_info(&ctx->dsi->dev, "ESD detected, start panel recovery\n");
    // 执行屏下电
    drm_panel_unprepare(&ctx->base);
    msleep(100);
    // 重新上电+发送panel初始化序列
    drm_panel_prepare(&ctx->base);
    ctx->panel_in_recovery = false;
    mutex_unlock(&ctx->esd_lock);
    dev_info(&ctx->dsi->dev, "ESD recovery complete\n");
}
/**
 * ESD内核线程主循环
 */
static int my_panel_esd_thread(void *data)
{
    struct my_panel_data *ctx = data;
    u8 read_val;
    int ret;
    unsigned int fail_cnt = 0;
    dev_info(&ctx->dsi->dev, "ESD thread start\n");
    while (ctx->esd_thread_run) {
        msleep(ctx->esd_poll_interval);
        if (!ctx->esd_thread_run)
            break;
        if (!ctx->esd_enable)
            continue;
        ret = my_panel_esd_read_reg(ctx, ctx->check_reg, &read_val);
        if (ret != 0 || read_val != ctx->expect_val) {
            fail_cnt++;
            dev_warn(&ctx->dsi->dev,
                "ESD abnormal! reg:0x%02x read:0x%02x expect:0x%02x, fail_cnt:%u\n",
                ctx->check_reg, read_val, ctx->expect_val, fail_cnt);
            // 超过最大重试次数不再反复复位,防止死循环
            if (fail_cnt >= ctx->esd_max_retry) {
                dev_err(&ctx->dsi->dev, "ESD exceed max retry, stop auto recovery\n");
                fail_cnt = 0;
                continue;
            }
            my_panel_esd_recover(ctx);
        } else {
            // 读取正常,清空失败计数
            if (fail_cnt > 0) {
                dev_dbg(&ctx->dsi->dev, "ESD back to normal, clear fail counter\n");
                fail_cnt = 0;
            }
        }
    }
    dev_info(&ctx->dsi->dev, "ESD thread exit\n");
    return 0;
}
/**
 * my_panel_esd_start - 点亮屏后,启动ESD检测线程
 */
static int my_panel_esd_start(struct my_panel_data *ctx)
{
    if (ctx->esd_thread) {
        dev_warn(&ctx->dsi->dev, "ESD thread already running\n");
        return 0;
    }
    ctx->esd_thread_run = true;
    ctx->esd_thread = kthread_run(my_panel_esd_thread, ctx, "my_panel_esd");
    if (IS_ERR(ctx->esd_thread)) {
        dev_err(&ctx->dsi->dev, "Create esd kthread failed\n");
        ctx->esd_thread = NULL;
        return PTR_ERR(ctx->esd_thread);
    }
    dev_info(&ctx->dsi->dev, "ESD thread created, interval:%d ms\n", ctx->esd_poll_interval);
    return 0;
}
/**
 * my_panel_esd_stop - suspend休眠,停止ESD线程
 */
static void my_panel_esd_stop(struct my_panel_data *ctx)
{
    if (!ctx->esd_thread)
        return;
    ctx->esd_thread_run = false;
    kthread_stop(ctx->esd_thread);
    ctx->esd_thread = NULL;
    dev_info(&ctx->dsi->dev, "ESD thread stopped\n");
}
// panel prepare:屏幕上电点亮,启动ESD
static int my_panel_prepare(struct drm_panel *panel)
{
    struct my_panel_data *ctx = to_my_panel(panel);
    int ret;
    // 电源、复位、MIPI初始化时序
    ret = xxx_panel_power_on(ctx);
    if (ret) {
        dev_err(&ctx->dsi->dev, "panel power on fail\n");
        return ret;
    }
    if (ctx->esd_enable) {
        my_panel_esd_start(ctx);
    }
    return 0;
}
// panel unprepare:屏幕休眠下电,停止ESD线程
static int my_panel_unprepare(struct drm_panel *panel)
{
    struct my_panel_data *ctx = to_my_panel(panel);
    if (ctx->esd_enable) {
        my_panel_esd_stop(ctx);
    }
    // 下电时序
    xxx_panel_power_off(ctx);
    return 0;
}
// probe初始化ESD参数、互斥锁
static int my_panel_probe(struct mipi_dsi_device *dsi)
{
    struct my_panel_data *ctx;
    ctx = devm_kzalloc(&dsi->dev, sizeof(*ctx), GFP_KERNEL);
    // drm_panel 基础初始化代码省略
    mutex_init(&ctx->esd_lock);
    // ESD参数,可放到DTS属性配置
    ctx->esd_enable = true;
    ctx->esd_poll_interval = 3000;    // 3s轮询一次
    ctx->esd_max_retry = 3;           // 最多连续3次自动复位
    ctx->check_reg = 0x04;
    ctx->expect_val = 0x12;
    ctx->panel_in_recovery = false;
    // 后续panel注册逻辑省略
    return 0;
}

code说明:锁、竞态、日志分级

1. 互斥锁与防重入保护

  • mutex esd_lock:保护drm_panel_prepare/unprepare整个上下电临界区;
  • panel_in_recovery标记:避免 ESD 线程短时间多次触发复位,防止重复进入恢复流程;
  • suspend/resume 调用 unprepare 时,停止 ESD 线程,休眠阶段不会触发 DSI 读写。

2. 失败计数器 & 最大重试限制

增加fail_cnt和esd_max_retry:如果静电持续存在,复位后读寄存器依旧异常,ESD 线程无限循环复位,系统反复闪屏。超过最大重试次数,停止自动恢复,避免死循环导致系统crash。

3. dmesg 日志分级

  • dev_dbg:调试信息,打开 DEBUG 宏才打印,量产默认关闭,防止 log 刷屏;
  • dev_info:正常事件,线程启停、复位完成;
  • dev_warn:异常告警,ESD 检测到异常;
  • dev_err:严重错误,创建线程失败、超过最大重试次数。

内核编译开启CONFIG_DYNAMIC_DEBUG可以动态开关 dev_dbg 日志,调试很方便。当然很多平台又自己客制化的debug log,可通过sys节点开关。

四、代码关键点拆解

1. kthread线程启停规则

  • kthread_run:创建并启动内核线程;
  • kthread_stop:发信号等待线程退出,必须等待线程退出,不能直接置空指针;
  • 增加esd_thread_run标志做循环条件,保证线程可以安全退出;
  • suspend休眠时一定要停止ESD线程。休眠状态MIPI Host会关闭,继续读寄存器会大量打印错误log,甚至系统panic。比如在AOD状态会close esd。

2. 锁

ESD复位会调用drm_panel_prepare/unprepare,和panel本身suspend/resume流程存在并发竞争。 使用mutex互斥锁,保证同一时间,屏幕上电/下电/ESD复位只能有一个流程执行。

3. 寄存器选择坑

不要选择会改变屏幕状态的寄存器;优先选择Panel ID寄存器、只读状态寄存器。

4. 轮询间隔怎么设置

常规产品:3~5秒轮询一次。 太短:频繁DSI读操作,增加MIPI带宽开销、功耗; 太长:ESD发生后很久才能检测到,用户长时间看到花屏黑屏。

5. 常见bug:ESD反复复位

现象:ESD触发复位,复位完成读寄存器依然异常,线程无限循环复位。

根因:硬件静电持续存在,或者上电时序本身有问题,复位后屏幕依然无法正常工作。 对策:增加最大重试次数,连续多次失败后停止自动复位,避免死循环。

如果还解决不了就需要找FAE和器件同事协助了。

五、调试手段

  1. dmesg看ESD打印:dmesg | grep esd
  2. 临时关闭ESD:DTS或者驱动中关闭esd_enable,对比复现静电现象
  3. 抓MIPI总线波形:DSI读写指令,确认读寄存器有没有真实发出去
  4. 模拟ESD:静电枪测试,验证是否可以自动恢复

六、总结

ESD Check不是硬件消除静电,是软件故障兜底恢复机制。

整个链路:内核kthread周期运行 > MIPI DCS读Panel寄存器 > 校验返回值 > 判断ESD异常 > 调用panel上下电时序复位屏幕。

项目开发时重点需要把控:线程启停时机、休眠关闭检测、并发竞态保护。

静电导致花屏?Panel如何自动修复花屏黑屏

相关推荐
问心无愧05135 小时前
[GXYCTF2019]Ping Ping Ping
android·笔记
事圆则缓5 小时前
Kotlin 异步与并发:从一次搜索请求走到状态流
android·开发语言·kotlin
帅得不敢出门6 小时前
Android Framework定制卸载确认窗口
android
Android-Flutter7 小时前
Compose 什么叫重组?怎么样触发重组?
android·compose
硫酸锌018 小时前
安卓手机连接Windows电脑后通过DHCP服务为手机分配IP地址
android·windows·计算机网络
骑着蜗牛撵大象3278 小时前
设备监控数据存与查:Spring Boot 接入 TimescaleDB 超表的完整落地
android·spring boot·后端·springboot·时序数据库·es·设备监控
云贝贝贝8 小时前
MySQL 线上报 Lock wait timeout 别慌:6 招定位锁等待与死锁
android
mmsx8 小时前
Android 签名校验为什么能拦住二次打包:指纹与完整性校验
android·签名
jason.zeng@15022079 小时前
使用vscode+flutter开发android程序(入门)
android·vscode·flutter