前言
做Android/Linux显示驱动,MIPI屏ESD问题是高频现场bug。静电干扰会导致MIPI链路异常、面板寄存器错乱、黑屏花屏闪屏。很多Panel驱动里都会实现ESD Check(静电检测)机制,定时去读取Panel寄存器、DDIC关于ESD相关的中断或者某个GPIO脚的电平变化,判断屏幕是否异常,触发软复位/硬复位恢复屏幕。
很多同学只知道驱动里有个ESD检测线程,但不清楚:
- ESD Check的底层原理是什么?
- 在内核DRM panel驱动中放在什么位置?
- 检测线程如何创建、启停、休眠?
- 读寄存器失败为什么就能判定ESD异常?
- 完整的代码逻辑怎么写,有哪些坑?
本文编者结合Linux DRM Panel开源驱动框架,完整拆解ESD Check。
一、ESD Check 原理
1. 静电对MIPI屏的影响
静电(ESD)冲击面板或者MIPI走线后,会出现几种现象:
- MIPI DSI链路不稳定,Host和Panel通信断连
- Panel内部寄存器值被篡改
- 面板逻辑卡死,不再刷新画面,表现黑屏/花屏/闪屏硬件本身无法自动恢复,需要软件检测到异常,执行复位流程。
2. ESD Check核心思路
定期轮询读取Panel的状态寄存器(一般是Panel的ID寄存器、状态寄存器,读这个寄存器不需要改变屏幕显示):
- 定时发起DSI读指令,读取Panel指定寄存器
- 判断读取返回值: 读取成功,数据符合预期 > 屏幕正常,继续等待下一轮检测 ;读取失败 / 返回数据异常 > 判断发生ESD干扰
- 触发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)。
- kthread 内核线程:ESD轮询任务主体,循环休眠 → 读寄存器 → 判断异常
- dsi host 接口:dsi_host_transfer,发送MIPI读寄存器指令
- work/workqueue(**其他**方案):部分老方案用延时工作队列,但是workqueue容易被其他任务抢占;长时间轮询ESD场景优先用kthread
- drm_panel ops:panel prepare/unprepare,屏上电下电,ESD异常复位时调用
- flag控制变量:esd_enable / esd_running,用来控制线程启停,防止竞态
两种实现方案对比
- kthread:可控性强,可以长时间sleep,适合周期性轮询ESD;异常复位过程可以在线程内串行执行
- delayed_work:简单,但复位流程较长时容易出现work重入,需要加锁保护
存在的竞态场景
- ESD 线程正在执行 Panel 复位(unprepare/prepare),同时系统触发 suspend 休眠,两套流程同时操作 Panel 电源 / MIPI;
- ESD 连续多次触发复位,多次上下电并发;
- 线程退出和 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 = ®
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和器件同事协助了。
五、调试手段
- dmesg看ESD打印:dmesg | grep esd
- 临时关闭ESD:DTS或者驱动中关闭esd_enable,对比复现静电现象
- 抓MIPI总线波形:DSI读写指令,确认读寄存器有没有真实发出去
- 模拟ESD:静电枪测试,验证是否可以自动恢复
六、总结
ESD Check不是硬件消除静电,是软件故障兜底恢复机制。
整个链路:内核kthread周期运行 > MIPI DCS读Panel寄存器 > 校验返回值 > 判断ESD异常 > 调用panel上下电时序复位屏幕。
项目开发时重点需要把控:线程启停时机、休眠关闭检测、并发竞态保护。