第4章 I2C/DDC通信:HDMI DDC通道协议、EDID数据结构解析、通过DDC读取音频能力

好,咱们进入第4章。这一章讲的是HDMI通信里一个特别基础、但又特别容易踩坑的部分——DDC通道。说白了,DDC就是HDMI用来“握手”和“自我介绍”的通道。你的显卡或者播放器插上电视,第一件事就是通过DDC去读电视的EDID,看看这电视到底支持什么分辨率、什么音频格式。

我刚开始做HDMI驱动的时候,以为DDC就是个简单的I2C读写,结果被EDID里的各种字段坑得够呛。嗯,咱们今天就把这块彻底讲透。

4.1 DDC通道协议:I2C的“变种”

DDC(Display Data Channel)本质上就是I2C总线,只不过它跑在HDMI的特定引脚上。HDMI接口里有专门的DDC引脚(通常是第15脚和第16脚),分别对应SCL和SDA。

这里有个关键点:DDC的I2C地址是固定的。EDID的读取地址是0x50(7位地址),写地址是0x51。你想想看,整个HDMI生态里,所有显示设备都得用这个地址,不然源端怎么找到它?

DDC的通信速率呢?标准是100kHz,但很多现代设备支持400kHz。我个人习惯在驱动里先尝试400kHz,如果通信失败再降回100kHz。为什么?因为有些老电视的DDC实现很烂,跑快了就丢数据。

核心要点:DDC就是I2C,地址0x50读EDID,速率100kHz起步。

4.2 EDID数据结构解析:128字节的“身份证”

EDID(Extended Display Identification Data)是显示设备给源端看的“身份证”。标准EDID是128字节,扩展EDID可以到256字节甚至更多。音频能力信息主要藏在128字节的基块里,以及CEA扩展块里。

咱们先看基块的结构。我直接给你画个表:

字节偏移 长度 内容 说明
0x00 8 Header 固定为00 FF FF FF FF FF FF 00
0x08 10 厂商信息 包括厂商ID、产品代码、序列号
0x12 2 EDID版本 通常是01 03(版本1,修订3)
0x14 1 基本参数 包括视频输入类型、信号类型等
0x15 1 屏幕尺寸 水平尺寸(cm)和垂直尺寸(cm)
0x18 1 特性支持 包括DPMS、色域等
0x23 1 音频输入类型 这个字段很关键,后面细讲
0x36 18 标准时序 每个时序3字节,共6个
0x48 72 详细时序描述 4个18字节的详细描述块
0x7E 1 扩展块数量 0表示没有扩展块
0x7F 1 校验和 所有字节相加应为0

嗯,这里要注意:EDID的校验和算法很简单,就是把前127个字节加起来,取低8位,然后用256减去这个值。如果算出来不对,那EDID就是坏的。我在项目中遇到过一台电视,EDID校验和总是错,后来发现是HDMI线太长,信号衰减导致读取错误。

4.3 音频能力藏在哪?

音频能力信息主要藏在两个地方:

  1. 基块的0x23字节:这个字节的低4位表示音频输入类型。0x00表示没有音频,0x01表示模拟音频,0x02表示I2S,0x03表示SPDIF。但说实话,这个字段太粗糙了,很多设备直接填0x00,然后靠扩展块来提供详细信息。
  2. CEA扩展块:这才是重点。CEA-861标准定义了扩展块,里面包含详细的音频能力描述。扩展块的标签是0x02,在EDID的0x7E字节指示扩展块数量后,紧接着就是扩展块数据。

CEA扩展块的结构是这样的:

字节偏移 长度 内容 说明
0x00 1 扩展块标签 0x02表示CEA扩展
0x01 1 版本号 通常是3
0x02 1 DTD数量 详细时序描述的数量
0x03 1 音频能力标志 位0:基本音频,位1:DTS,位2:Dolby Digital等
0x04 可变 音频数据块 每个音频格式一个数据块

音频数据块(Audio Data Block)的格式是:

  • 头字节:高4位是标签(0x01表示音频),低4位是长度(后续字节数)
  • 后续字节:每3个字节描述一种音频格式。第一个字节的位7-3是音频格式代码,位2-0是声道数减1。第二个字节是采样率支持位图。第三个字节是位宽支持位图。

举个例子:如果读到音频数据块是 02 0A 07 01 00 00,那意味着:

  • 头字节0x02:标签0x01(音频),长度2(后续2字节)
  • 0x0A:音频格式代码00010(LPCM),声道数011(3+1=4声道)
  • 0x07:采样率支持位图,位0=32kHz,位1=44.1kHz,位2=48kHz
  • 0x01:位宽支持位图,位0=16位

个人经验:解析音频数据块时,一定要先检查头字节的标签。我曾经遇到过一台设备,它的音频数据块标签写成了0x02(视频数据块),结果我按音频格式去解析,读出来的全是乱码。后来加了标签校验才搞定。

4.4 通过DDC读取音频能力:实战代码

好了,理论讲完了,咱们直接上代码。下面是一个通过DDC读取EDID并解析音频能力的示例:

#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>

#define EDID_ADDR 0x50
#define EDID_SIZE 128
#define CEA_TAG 0x02
#define AUDIO_TAG 0x01

int read_edid(int fd, uint8_t *buffer) {
    uint8_t offset = 0;
    struct i2c_msg msgs[2];
    struct i2c_rdwr_ioctl_data msgset;
    
    // 设置读取偏移为0
    msgs[0].addr = EDID_ADDR;
    msgs[0].flags = 0;  // 写
    msgs[0].len = 1;
    msgs[0].buf = &offset;
    
    // 读取128字节
    msgs[1].addr = EDID_ADDR;
    msgs[1].flags = I2C_M_RD;  // 读
    msgs[1].len = EDID_SIZE;
    msgs[1].buf = buffer;
    
    msgset.msgs = msgs;
    msgset.nmsgs = 2;
    
    if (ioctl(fd, I2C_RDWR, &msgset) < 0) {
        perror("I2C read failed");
        return -1;
    }
    return 0;
}

void parse_audio_capabilities(uint8_t *edid) {
    // 检查校验和
    uint8_t checksum = 0;
    for (int i = 0; i < 128; i++) {
        checksum += edid[i];
    }
    if (checksum != 0) {
        printf("EDID checksum error!\n");
        return;
    }
    
    // 检查扩展块
    uint8_t ext_count = edid[0x7E];
    if (ext_count == 0) {
        printf("No CEA extension block found\n");
        return;
    }
    
    // 读取第一个扩展块(假设在128字节后)
    // 实际项目中需要读取第二个128字节块
    uint8_t *ext_block = edid + 128;
    
    if (ext_block[0] != CEA_TAG) {
        printf("Not a CEA extension block\n");
        return;
    }
    
    // 解析音频数据块
    int offset = 4;  // 跳过CEA头
    while (offset < 128) {
        uint8_t tag = ext_block[offset] >> 4;
        uint8_t len = ext_block[offset] & 0x0F;
        
        if (tag == AUDIO_TAG) {
            printf("Audio Data Block found:\n");
            for (int i = 1; i <= len; i += 3) {
                uint8_t format = (ext_block[offset + i] >> 3) & 0x1F;
                uint8_t channels = (ext_block[offset + i] & 0x07) + 1;
                uint8_t rates = ext_block[offset + i + 1];
                uint8_t bit_depths = ext_block[offset + i + 2];
                
                printf("  Format: %d, Channels: %d\n", format, channels);
                printf("  Rates: %s%s%s\n",
                       (rates & 0x01) ? "32kHz " : "",
                       (rates & 0x02) ? "44.1kHz " : "",
                       (rates & 0x04) ? "48kHz " : "");
                printf("  Bit depths: %s%s%s\n",
                       (bit_depths & 0x01) ? "16bit " : "",
                       (bit_depths & 0x02) ? "20bit " : "",
                       (bit_depths & 0x04) ? "24bit " : "");
            }
            break;
        }
        
        offset += 1 + len;
    }
}

int main() {
    int fd = open("/dev/i2c-1", O_RDWR);
    if (fd < 0) {
        perror("Failed to open I2C device");
        return -1;
    }
    
    uint8_t edid[256] = {0};
    if (read_edid(fd, edid) == 0) {
        parse_audio_capabilities(edid);
    }
    
    close(fd);
    return 0;
}

避坑指南:我曾经在读取EDID时,发现有些设备需要先写一个偏移地址才能读取。但有些设备不需要,直接读就能拿到数据。我建议你在驱动里先尝试不带偏移的读取,如果失败再尝试带偏移的。另外,DDC通信时一定要加超时处理,我遇到过一台电视,读EDID时卡住了整整5秒才返回数据。

4.5 常见问题与调试技巧

做DDC通信时,有几个坑是绕不开的:

  • I2C总线冲突:如果HDMI线太长或者接触不良,DDC信号会受干扰。我建议你在驱动里加一个重试机制,比如连续读3次,取多数一致的结果。
  • EDID校验和错误:这通常意味着读取不完整或者数据损坏。可以尝试降低I2C速率,或者增加读取延时。
  • 扩展块读取:有些设备只提供128字节的基块,没有扩展块。这时候音频能力信息可能不全,你需要根据基块的0x23字节做推断。
  • 热插拔检测:HDMI的热插拔检测(HPD)信号和DDC是联动的。如果HPD没拉高,DDC通信可能会失败。我建议你在读取EDID前先检查HPD状态。

嗯,这一章的内容就到这里。DDC通信和EDID解析是HDMI音频驱动开发的基础,你想想看,如果连电视支持什么音频格式都不知道,你怎么配置音频输出?下一章咱们会讲音频时钟恢复和同步,那才是真正考验功底的地方。