第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 音频能力藏在哪?
音频能力信息主要藏在两个地方:
- 基块的0x23字节:这个字节的低4位表示音频输入类型。0x00表示没有音频,0x01表示模拟音频,0x02表示I2S,0x03表示SPDIF。但说实话,这个字段太粗糙了,很多设备直接填0x00,然后靠扩展块来提供详细信息。
- 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音频驱动开发的基础,你想想看,如果连电视支持什么音频格式都不知道,你怎么配置音频输出?下一章咱们会讲音频时钟恢复和同步,那才是真正考验功底的地方。