4. I2C与DDC通信:DDC协议详解、EDID读取流程、I2C控制器驱动适配

HDMI 显示驱动里,最绕不开的一个环节就是 I2C 和 DDC。说白了,显卡得知道显示器是什么来头——支持什么分辨率、什么刷新率、色彩空间对不对。这些信息,全靠 DDC 通道去读 EDID 数据。我刚开始做 HDMI 驱动时,觉得 EDID 不就是读个 128 字节吗?结果被各种奇奇怪怪的兼容性问题折腾得够呛。

4.1 DDC 协议到底是什么?

DDC(Display Data Channel)是显示器和显卡之间的一条“信息高速公路”。它基于 I2C 总线,物理上就是 HDMI 接口里的第 15 脚(SCL)和第 16 脚(SDA)。

DDC 的核心任务只有一个:让显卡读取显示器的 EDID 数据。EDID 就像显示器的身份证,里面写满了它的能力参数。

DDC 的关键特性:

  • 物理层:标准 I2C 总线,速率通常为 100kHz
  • 从设备地址:0x50(7 位地址)或 0xA0(8 位地址)
  • 数据格式:I2C 块读取(Block Read)
  • 最大数据量:标准 EDID 为 128 字节,扩展 EDID 可达 32KB

你想想看,为什么 DDC 要用 I2C?因为简单。两根线就能搞定,而且几乎所有嵌入式芯片都自带 I2C 控制器。但简单归简单,坑也不少。

4.2 EDID 读取流程:一步一步来

EDID 读取看起来就是发个 I2C 读请求,但实际流程里有很多细节。我习惯把读取流程分成三个阶段:

4.2.1 阶段一:探测与握手

在发起读取之前,必须先确认显示器是否在线。这一步很多人会忽略,结果读回来的全是 0xFF。

/* 伪代码:DDC 探测流程 */
int ddc_probe(struct i2c_adapter *adap)
{
    struct i2c_msg msg;
    unsigned char addr = 0x50;

    msg.addr = addr;
    msg.flags = 0;          /* 写操作 */
    msg.len = 0;            /* 空写,只发地址 */
    msg.buf = NULL;

    /* 发送从设备地址,看是否有 ACK */
    if (i2c_transfer(adap, &msg, 1) != 1) {
        pr_err("DDC: 显示器未响应,地址 0x%02x\n", addr);
        return -ENODEV;
    }

    return 0;
}

我记得有一次调试,显示器明明亮着,但 DDC 就是读不到。后来发现是 HDMI 线缆的第 15 脚虚焊了。嗯,硬件问题往往比软件更隐蔽。

4.2.2 阶段二:读取 EDID 块

标准 EDID 是 128 字节,但 I2C 一次传输不能发太多数据。所以需要分多次读取,每次读 8 字节或 16 字节。

/* 伪代码:读取 EDID 块 */
int ddc_read_edid_block(struct i2c_adapter *adap, unsigned char *buf, int offset, int len)
{
    struct i2c_msg msgs[2];
    unsigned char addr_byte;

    /* 第一步:写偏移地址 */
    addr_byte = offset & 0xFF;
    msgs[0].addr = 0x50;
    msgs[0].flags = 0;          /* 写 */
    msgs[0].len = 1;
    msgs[0].buf = &addr_byte;

    /* 第二步:读数据 */
    msgs[1].addr = 0x50;
    msgs[1].flags = I2C_M_RD;   /* 读 */
    msgs[1].len = len;
    msgs[1].buf = buf;

    if (i2c_transfer(adap, msgs, 2) != 2) {
        pr_err("DDC: 读取 EDID 失败,offset=%d\n", offset);
        return -EIO;
    }

    return 0;
}

个人经验:读取 EDID 时,建议每次读 16 字节。读太少会降低效率,读太多容易触发某些显示器的 I2C 超时。16 字节是个比较稳妥的折中值。

4.2.3 阶段三:校验与解析

EDID 数据读回来后,不能直接用。先要校验,再解析。校验靠的是最后一个字节的校验和。

/* 伪代码:EDID 校验和检查 */
int edid_checksum(unsigned char *edid)
{
    unsigned char sum = 0;
    int i;

    for (i = 0; i < 128; i++) {
        sum += edid[i];
    }

    if (sum != 0) {
        pr_err("EDID: 校验和错误,sum=0x%02x\n", sum);
        return -EINVAL;
    }

    return 0;
}

校验通过后,就可以解析 EDID 里的关键字段了:

偏移地址 字段名称 说明
0x00 - 0x07 Header 固定为 00 FF FF FF FF FF FF 00
0x08 - 0x0F 厂商 ID 3 个字符的 PNP ID
0x14 - 0x18 EDID 版本 通常为 1.3 或 1.4
0x36 - 0x47 标准时序 8 个标准分辨率条目
0x48 - 0x6F 详细时序描述符 4 个 18 字节的时序块

4.3 I2C 控制器驱动适配

EDID 读取最终要落到 I2C 控制器驱动上。不同芯片的 I2C 控制器差异很大,但核心逻辑是一样的。

4.3.1 控制器初始化

我建议在 HDMI 驱动初始化时,先获取 I2C 适配器。如果适配器没准备好,后面所有 DDC 操作都会失败。

/* 伪代码:获取 I2C 适配器 */
struct i2c_adapter *hdmi_get_i2c_adapter(struct platform_device *pdev)
{
    struct i2c_adapter *adap;

    /* 从设备树或平台数据中获取 I2C 总线编号 */
    int bus_num = of_get_named_gpio(pdev->dev.of_node, "ddc-i2c-bus", 0);

    adap = i2c_get_adapter(bus_num);
    if (!adap) {
        pr_err("HDMI: 无法获取 I2C 适配器,bus=%d\n", bus_num);
        return ERR_PTR(-EPROBE_DEFER);
    }

    return adap;
}

注意:EPROBE_DEFER 是 Linux 驱动里常用的延迟探测机制。如果 I2C 控制器还没注册,HDMI 驱动就返回这个错误,内核会在后续重新尝试探测。千万不要直接返回 -ENODEV,否则驱动就彻底起不来了。

4.3.2 时序适配

I2C 时序直接影响 DDC 通信的稳定性。标准 DDC 要求 100kHz,但有些显示器对时序很敏感。

我曾经遇到过一个案例:某款 4K 显示器,在 100kHz 下 EDID 读取总是超时。后来把 I2C 时钟降到 50kHz,问题就解决了。所以,我习惯在驱动里留一个可配置的时钟参数。

/* 伪代码:配置 I2C 时钟 */
static int hdmi_i2c_set_speed(struct i2c_adapter *adap, unsigned int speed_hz)
{
    struct i2c_gpio_platform_data *pdata;
    struct i2c_gpio *priv;

    /* 这里假设使用 GPIO 模拟 I2C */
    priv = container_of(adap, struct i2c_gpio, adap);
    pdata = priv->pdata;

    /* 设置时钟延迟,控制 SCL 频率 */
    pdata->udelay = (1000000 / speed_hz) / 2;

    pr_info("HDMI: I2C 时钟设置为 %u Hz\n", speed_hz);
    return 0;
}

4.3.3 错误处理与重试

DDC 通信很容易受干扰。热插拔瞬间、静电放电、线缆质量差,都可能导致 I2C 传输失败。我建议在驱动里加入重试机制。

/* 伪代码:带重试的 EDID 读取 */
int hdmi_ddc_read_edid_retry(struct i2c_adapter *adap, unsigned char *edid)
{
    int ret, retry;

    for (retry = 0; retry < 3; retry++) {
        ret = ddc_read_edid_block(adap, edid, 0, 128);
        if (ret == 0) {
            /* 校验和检查 */
            if (edid_checksum(edid) == 0)
                return 0;
        }

        pr_warn("DDC: 第 %d 次读取失败,重试中...\n", retry + 1);
        msleep(100);  /* 等待 100ms 后重试 */
    }

    pr_err("DDC: 连续 3 次读取 EDID 失败\n");
    return -EIO;
}

4.4 知识体系总览

下面这张图把 DDC 通信的整个链路串起来了。从物理层到应用层,每一步都有对应的驱动模块。

DDC 通信知识体系结构图 物理层 HDMI Pin 15(SCL) / 16(SDA) 链路层 I2C 协议 (100kHz) 传输层 DDC 协议 (从地址 0x50) 应用层 EDID 读取 / 解析 / 校验 驱动适配层 I2C 控制器初始化 / 时序配置 / 错误重试 / 设备树绑定 关键要点: • DDC 通信的稳定性取决于物理层、链路层、驱动适配层的协同工作 • EDID 读取失败时,优先检查 I2C 时序和线缆连接 • 驱动适配层要加入重试机制,应对热插拔和干扰场景

4.5 避坑指南

做 DDC 通信这几年,我踩过的坑不少。挑几个典型的说说:

  • 热插拔瞬间读取失败:显示器刚插入时,I2C 总线可能还没稳定。我建议在检测到热插拔事件后,延迟 200ms 再发起 EDID 读取。
  • EDID 校验和错误:有时候读回来的 EDID 校验和不对,但显示器确实能正常工作。这种情况多半是 I2C 传输过程中有数据错位。加个重试就能解决。
  • I2C 地址冲突:有些显示器会把 DDC 地址映射到 0x50 以外的位置。遇到这种情况,可以尝试扫描 0x50 到 0x57 之间的所有地址。
  • 时钟延展问题:部分显示器在读取 EDID 时会拉低 SCL 做时钟延展。如果 I2C 控制器不支持时钟延展,通信就会卡死。这时候只能换用支持时钟延展的控制器,或者用 GPIO 模拟 I2C。

我的调试习惯:遇到 DDC 通信问题,先用逻辑分析仪抓 SCL 和 SDA 的波形。看一眼波形,基本就能判断是硬件问题还是软件问题。比瞎猜代码快多了。

4.6 小结

DDC 通信看起来简单,但实际做起来细节很多。从物理层的线缆连接,到链路层的 I2C 时序,再到应用层的 EDID 解析,每一层都可能出问题。我个人的经验是:先把基础流程跑通,再逐步优化稳定性和兼容性。别一上来就想把所有显示器都支持,那不现实。