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

简介: 本文深度拆解MIPI屏ESD检测机制:基于Linux DRM框架,详解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**示例

#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上下电时序复位屏幕。

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

相关文章
|
17天前
|
安全 Linux 调度
第2篇:DRM 是什么?为什么显示驱动绕不开它?
DRM是Linux显示体系的核心框架,统一管理CRTC、Encoder、Connector、Plane及内存与同步,实现安全、有序的图层提交与模式设置,是定位黑屏、花屏等显示问题的关键枢纽。
|
19小时前
|
人工智能 并行计算 PyTorch
Stable Diffusion 本地部署、秋叶 aki 整合包、SD WebUI 安装教程、AI 绘画本地搭建、ControlNet 配置
本文详解秋叶Aki SD整合包在Windows下的本地部署:涵盖版本选型(N卡/AMD适配)、ControlNet模型部署、显存优化启动参数(如--xformers --medvram)及常见报错排查,附PowerShell自动化脚本,零基础也能快速搭建稳定AI绘画环境。
|
20小时前
|
人工智能 C++ 开发者
VS Code 正式上线 HydraFusion:以后不是你在选模型,是模型在选工作流
HydraFusion 是 VS Code 推出的自适应AI编排系统(研究预览版),不选模型,而选**解决任务的最佳流程**:单模型直答、级联升级或双模互审。让复杂编程任务(重构、调试、跨文件修改)自动匹配最优工作流,解放开发者模型选择负担。(239字)
|
20小时前
|
JSON Go 开发者
Zed 2026年上手教程一自定义主题: 打造专属深色界面
Zed支持通过`theme_overrides`在`settings.json`中轻量覆写主题(如Aura Dark),无需开发扩展:几行代码即可定制边框、面板背景、光标色、注释斜体等,实时生效,简洁高效。(239字)
|
23小时前
|
弹性计算 人工智能 关系型数据库
阿里云服务器多少钱一年?2026年10月18日最新优惠服务器配置推荐,共18台!
2026年10月阿里云服务器价格全面优化:38元/年起(轻量机秒杀),覆盖个人开发、企业建站、AI算力等四大场景;18款热门配置排行榜含入门到万元级方案,ECS与轻量机选购指南助避坑。
32 0
|
20小时前
|
人工智能 前端开发 JavaScript
Claude Code 全新命令:一键拉起一个团队给你干活
Claude Code的`/batch`命令支持将大任务自动拆解为最多30个子任务,并行调度AI代理协同执行。各代理共享上下文、实时同步状态,显著压缩等待时间——从“监工式”串行等待,升级为“审批式”高效并行。(239字)
|
20小时前
|
人工智能 JSON 前端开发
设计系统负责人:编译工作流中的上下游要求,评审的原材料从哪来,评审的产物给谁用
定义设计系统负责人在编译工作流中的上下游契约:上游以三条准入条款(结构、语义、证据)把关输入,退回不否决;下游以三条消费约定规范产物使用,反馈回流定修订对象。无人岗,只有契约,一人即可跑通最小闭环。
|
20小时前
|
人工智能 数据可视化 IDE
VSCode 藏隐技巧!99%开发者都不知道
VSCode代码补全中,Enter是插入、Shift+Enter才是替换(非Tab),这一设计承袭自IntelliJ IDEA的20年交互传统。VSCode更进一步,通过状态栏可视化提示模式,并支持光标移动动态刷新建议——细节处见真章,正是它成为国民编辑器的关键。(239字)
|
1天前
|
人工智能 缓存 运维
基于 YOLO11 的电力线路绝缘子智能检测系统 | 完整源码分享
本项目基于YOLO11n构建电力绝缘子智能检测系统,专注“单类高召回”定位,支持航拍图/视频/实时摄像头输入;模型仅6MB,适配边缘部署;Streamlit前端实现上传即检、ROI导出,大幅提升巡检效率。(239字)
|
1天前
|
机器学习/深度学习 人工智能 自然语言处理
我为什么做 InBrief:把 AI 一手信源收进一个入口
InBrief 是我参与的 AI 资讯聚合项目,起因不是信息太少,而是入口太多:官方博客、GitHub Trending、Hugging Face 论文榜、X、Hacker News、Reddit 各占一个标签页,刷完却记不住几条,二手转述还常滞后、丢掉适用范围和接口变化等细节。InBrief 从这些源头持续采集,用 LLM 过滤、打分、去重,输出中英双语摘要,目前追踪 370+ 个信息源,每 2 小时更新一次,采集引擎以 MIT 协议开源、可自行部署。它的目标很窄:把「源头发布 → 摘要 → 判断值不值得点开原文」这段路缩短,第一层优先看官方发布与仓库更新。
30 0