8.1 mtk_connector.c 核心结构分析
先看文件结构。mtk_connector.c 是MTK自研的Connector抽象层。它不像DRM框架里那种标准Connector,而是做了很多定制化处理。说白了,MTK把物理接口的共性操作抽出来了。
核心数据结构就一个:
struct mtk_connector { struct drm_connector base; struct mtk_ddp_comp *ddp_comp; struct mtk_edid *edid; struct mutex lock; bool hpd_state; struct work_struct hpd_work; void (*hpd_cb)(void *priv); void *hpd_priv; };这里我重点说几个字段:
- ddp_comp:指向对应的显示通路组件。比如HDMI接口,它就指向HDMI的DDP组件。
- edid:缓存解析后的EDID数据。我习惯在热插拔时重新读取,避免用旧数据。
- hpd_work:热插拔的工作队列。注意,这里用的是workqueue,不是tasklet。为什么?因为EDID读取需要休眠等待I2C传输,workqueue最合适。
关键点:mtk_connector 不是简单的DRM Connector封装。它内部维护了HPD状态机、EDID缓存、以及回调机制。你想想看,如果每次查询模式都要重新读EDID,那性能得多差?
8.2 EDID读取:从I2C到解析
EDID读取,说白了就是通过I2C从显示器里把128字节(或扩展块)的数据读出来。MTK的做法是:
static int mtk_connector_read_edid(struct mtk_connector *connector) { struct i2c_adapter *adap; u8 buf[EDID_LENGTH]; int ret; adap = mtk_ddp_comp_get_i2c_adap(connector->ddp_comp); if (!adap) { dev_err(dev, "no i2c adapter\n"); return -ENODEV; } // 标准EDID读取流程 ret = i2c_transfer(adap, &msg, 1); if (ret != 1) { dev_warn(dev, "EDID read failed, ret=%d\n", ret); return -EIO; } // 校验EDID头 if (buf[0] != 0x00 || buf[1] != 0xFF) { dev_err(dev, "invalid EDID header\n"); return -EINVAL; } // 解析并缓存 connector->edid = mtk_edid_parse(buf); return 0; }这里有个坑,我踩过。有些显示器EDID读取需要重试。为什么?因为显示器刚上电时,I2C总线还没准备好。我建议至少重试3次,每次间隔50ms。
我的经验:EDID读取失败时,别急着报错。先检查I2C地址是否正确(标准是0x50)。有些山寨显示器会把地址偏移一位,我遇到过两次这种情况。
8.3 热插拔检测:中断与轮询的博弈
热插拔检测,MTK支持两种模式:
- 中断模式:硬件HPD引脚触发中断,实时性最好。
- 轮询模式:定时检查HPD状态,适合没有中断引脚的场景。
代码里是这样处理的:
static irqreturn_t mtk_connector_hpd_irq_handler(int irq, void *dev_id) { struct mtk_connector *connector = dev_id; // 注意:中断上下文不能做耗时操作 schedule_work(&connector->hpd_work); return IRQ_HANDLED; } static void mtk_connector_hpd_work_handler(struct work_struct *work) { struct mtk_connector *connector; bool new_state; connector = container_of(work, struct mtk_connector, hpd_work); // 读取硬件HPD状态 new_state = mtk_ddp_comp_get_hpd_state(connector->ddp_comp); if (new_state != connector->hpd_state) { connector->hpd_state = new_state; if (new_state) { // 插入:重新读取EDID mtk_connector_read_edid(connector); } else { // 拔出:释放EDID缓存 mtk_edid_free(connector->edid); connector->edid = NULL; } // 通知上层 if (connector->hpd_cb) connector->hpd_cb(connector->hpd_priv); } }嗯,这里要注意。HPD状态变化后,不能立即上报给上层。我建议加一个50ms的去抖延时。为什么?因为物理接触会有抖动,特别是HDMI接口,插拔瞬间会反复触发中断。
避坑指南:我曾经遇到一个案子,用户插拔HDMI时系统频繁重启。查了两天才发现是HPD中断处理里调用了drm_helper_hpd_irq_event,而这个函数内部会加锁,导致死锁。解决方案是用workqueue延迟处理。
8.4 显示模式获取:从EDID到DRM Mode
显示模式获取,说白了就是把EDID里的时序信息转换成DRM能识别的mode结构。MTK的做法是:
static int mtk_connector_get_modes(struct drm_connector *connector) { struct mtk_connector *mtk_conn = to_mtk_connector(connector); struct edid *edid; int count = 0; // 优先使用缓存的EDID if (mtk_conn->edid) { edid = (struct edid *)mtk_conn->edid->raw; } else { // 没有缓存则重新读取 if (mtk_connector_read_edid(mtk_conn)) return 0; edid = (struct edid *)mtk_conn->edid->raw; } // 调用DRM标准解析函数 drm_connector_update_edid_property(connector, edid); count = drm_add_edid_modes(connector, edid); // 添加默认模式(防止EDID为空) if (count == 0) { drm_mode_create(connector->dev); // 设置一个1080p60的默认模式 ... count = 1; } return count; }这里有个细节。drm_add_edid_modes 返回的是有效模式数量。但有些显示器会报告一堆奇怪的分辨率,比如640x480@60Hz。我建议在驱动层做一个白名单过滤,只保留常见的分辨率。
核心思路:显示模式获取的流程是:EDID读取 → 解析 → 生成DRM Mode → 上报给用户空间。每一步都可能失败,所以要做好降级处理。比如EDID读不到时,至少给一个1080p的保底模式。
8.5 实战中的几个坑
最后,我总结几个实战中容易踩的坑:
- EDID缓存失效:有些显示器在切换输入源后会更新EDID。我建议每次HOTPLUG事件都重新读取EDID,不要迷信缓存。
- HPD去抖时间:不同接口的去抖时间不一样。HDMI建议50ms,DP建议100ms。我习惯在设备树里配一个可调的参数。
- 模式优先级:多个显示器同时接入时,要保证主屏优先获取最佳模式。我一般按物理端口顺序分配优先级。
- I2C总线竞争:EDID读取时,如果其他设备也在用同一根I2C总线,会冲突。建议加mutex保护。
好了,Connector驱动的核心内容就这些。说白了,它就是一个翻译官——把物理层的信号翻译成DRM能理解的mode。下一章咱们聊聊如何把这些Connector挂到Display Engine上,实现真正的多屏输出。