4. 显示子系统架构

做显示驱动开发,你迟早要跟EDID打交道。说白了,EDID就是显示器的身份证。它告诉显卡:我是什么牌子、支持多大分辨率、刷新率能跑多高、色彩深度是多少。没有它,你的屏幕可能只能亮个640×480的默认画面,甚至直接黑屏。

我刚开始做HDMI驱动时,就遇到过一块屏死活点不亮。查了三天,最后发现是EDID校验和不对,系统直接拒读了。嗯,从那以后我再也不敢小看这128字节的数据块。

4.1 EDID是什么?为什么需要它?

EDID的全称是Extended Display Identification Data(扩展显示标识数据)。它存储在显示器的EEPROM里,通过I2C/DDC通道与主机通信。主机上电后,通过DDC读取EDID,才知道怎么正确驱动这块屏。

你想想看,如果没有EDID,显卡怎么知道显示器支持4K还是1080p?强行输出不匹配的信号,轻则画面撕裂,重则烧坏面板。EDID就是双方握手的第一步。

核心要点:EDID是显示器的“自我介绍”,包含分辨率、刷新率、色彩格式、物理尺寸、制造商信息等。主机必须正确解析EDID,才能输出兼容的视频信号。

4.2 I2C/DDC通道:物理层怎么走?

DDC(Display Data Channel)是I2C总线的一个变种,专门用于主机和显示器之间的控制通信。标准DDC使用I2C的0x50地址(7位地址,写操作是0xA0,读操作是0xA1)。

我记得第一次用逻辑分析仪抓DDC波形时,发现很多开发者会忽略I2C的时序要求。DDC的时钟频率通常限制在100kHz以内,但有些廉价显示器对时序特别敏感,稍微快一点就丢ACK。

参数 标准值 说明
I2C地址 0x50 (7-bit) EDID读取专用地址
时钟频率 ≤100 kHz DDC标准限制,部分显示器支持400kHz
数据格式 I2C标准协议 起始位、地址、ACK、数据、停止位
最大读取长度 128字节(Block 0) 扩展EDID可达256字节以上

避坑指南:我曾经在某个项目里,因为I2C时钟线没有加合适的上拉电阻,导致DDC通信时好时坏。显示器偶尔能识别,偶尔黑屏。最后用示波器一看,时钟信号上升沿太慢,超过了I2C规范。建议DDC的SCL和SDA上拉电阻选4.7kΩ~10kΩ,具体看总线电容。

4.3 EDID数据结构:128字节里藏了什么?

标准的EDID 1.3/1.4版本,固定128字节。结构非常清晰,分为几个主要区域:

  • Header(0x00-0x07):固定为00 FF FF FF FF FF FF 00,用于识别EDID起始。
  • 制造商信息(0x08-0x11):3字节的PNP ID,2字节的产品代码,4字节的序列号。
  • EDID版本(0x12-0x13):版本号和修订号,常见的是1.3或1.4。
  • 基本显示参数(0x14-0x18):包括模拟/数字接口、最大分辨率、色彩深度等。
  • 色度信息(0x19-0x25):显示器的色坐标和伽马值。
  • 标准时序(0x26-0x35):8个标准分辨率/刷新率组合。
  • 详细时序描述符(0x36-0x7D):4个18字节的时序块,包含最精确的显示参数。
  • 扩展标志(0x7E):指示是否有后续扩展块。
  • 校验和(0x7F):前127字节的和取低8位,加上此字节应为0。

我个人习惯在解析EDID时,先校验Header和Checksum。如果这两步没过,后面的数据基本不用看了,直接返回错误。

4.4 详细时序描述符(DTD):最核心的部分

详细时序描述符(Detailed Timing Descriptor)是EDID里最有价值的数据。每个DTD占18字节,定义了显示器支持的一种精确时序。包括:

  • 水平/垂直有效像素
  • 水平/垂直消隐区
  • 水平/垂直同步脉冲宽度
  • 像素时钟频率
  • 同步极性(正/负)
  • 隔行/逐行扫描标志

举个例子,一个1080p@60Hz的DTD,像素时钟大约是148.5MHz。水平有效像素1920,水平消隐280,水平总像素2200。垂直有效像素1080,垂直消隐45,垂直总像素1125。

小技巧:解析DTD时,像素时钟的单位是10kHz。也就是说,如果你读到的值是14850,实际像素时钟是148.5MHz。我见过不少新手直接拿这个值去配置PLL,结果画面完全不对。

4.5 代码实战:读取并解析EDID

下面是一个简化的EDID读取和解析示例,基于Linux I2C设备驱动框架:

#include <linux/i2c.h>
#include <linux/delay.h>

#define DDC_ADDR        0x50
#define EDID_BLOCK_SIZE 128

struct edid_info {
    u8 header[8];
    u16 manufacturer_id;
    u16 product_code;
    u32 serial_number;
    u8 version;
    u8 revision;
    u8 video_input_interface;
    u8 max_horizontal_size;  // cm
    u8 max_vertical_size;    // cm
    u8 gamma;
    u8 features;
    u8 chromaticity[10];
    u8 established_timings[3];
    u8 standard_timings[16];
    u8 detailed_timings[72]; // 4 * 18 bytes
    u8 extension_flag;
    u8 checksum;
};

static int edid_read_block(struct i2c_adapter *adap, u8 *buf)
{
    struct i2c_msg msg[2];
    u8 offset = 0;
    int ret;

    // 写偏移地址
    msg[0].addr = DDC_ADDR;
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = &offset;

    // 读EDID数据
    msg[1].addr = DDC_ADDR;
    msg[1].flags = I2C_M_RD;
    msg[1].len = EDID_BLOCK_SIZE;
    msg[1].buf = buf;

    ret = i2c_transfer(adap, msg, 2);
    if (ret != 2) {
        pr_err("EDID read failed: %d\n", ret);
        return -EIO;
    }

    return 0;
}

static int edid_validate(u8 *buf)
{
    const u8 header[] = {0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
    u8 sum = 0;
    int i;

    // 检查Header
    if (memcmp(buf, header, 8) != 0) {
        pr_err("Invalid EDID header\n");
        return -EINVAL;
    }

    // 计算校验和
    for (i = 0; i < EDID_BLOCK_SIZE; i++)
        sum += buf[i];

    if (sum != 0) {
        pr_err("EDID checksum error: 0x%02x\n", sum);
        return -EINVAL;
    }

    return 0;
}

static void edid_parse_timing(u8 *dtd, struct display_timing *timing)
{
    u32 pixel_clock;
    u16 h_active, h_blank, h_sync_offset, h_sync_width;
    u16 v_active, v_blank, v_sync_offset, v_sync_width;
    u8 flags;

    // 像素时钟(单位10kHz)
    pixel_clock = dtd[0] | (dtd[1] << 8);
    timing->pixel_clock_hz = pixel_clock * 10000;

    // 水平有效像素
    h_active = dtd[2] | ((dtd[4] & 0xF0) << 4);
    timing->h_active = h_active;

    // 水平消隐
    h_blank = dtd[3] | ((dtd[4] & 0x0F) << 8);
    timing->h_total = h_active + h_blank;

    // 垂直有效像素
    v_active = dtd[5] | ((dtd[7] & 0xF0) << 4);
    timing->v_active = v_active;

    // 垂直消隐
    v_blank = dtd[6] | ((dtd[7] & 0x0F) << 8);
    timing->v_total = v_active + v_blank;

    // 同步极性
    flags = dtd[17];
    timing->h_sync_polarity = (flags & 0x02) ? 1 : 0;
    timing->v_sync_polarity = (flags & 0x04) ? 1 : 0;

    pr_info("EDID timing: %dx%d @ %d Hz\n",
            h_active, v_active,
            pixel_clock * 10000 / (h_active + h_blank) / (v_active + v_blank));
}

这段代码做了三件事:通过I2C读取128字节、校验Header和Checksum、解析详细时序。实际项目中,你还需要处理扩展块(EDID 1.4支持最多128个扩展块),以及处理显示器热插拔时的重新读取。

4.6 EDID读取的常见问题与调试技巧

做EDID开发,你大概率会遇到下面这些问题:

问题现象 可能原因 排查方法
读取全为0xFF I2C地址错误、显示器未上电、DDC线路断开 用万用表测SCL/SDA电压,确认上拉正常
读取数据随机变化 I2C时钟太快、干扰、上拉电阻太小 降低时钟到50kHz,加长延时
校验和错误 读取不完整、显示器EDID损坏 尝试重新读取3次,取多数一致的结果
解析出的分辨率不对 字节序搞反、位域解析错误 对照EDID规范逐字节核对

我曾经踩过的坑:有一次读取EDID总是成功,但画面就是不对。后来发现是显示器支持4K@60Hz,但EDID里只声明了4K@30Hz的时序。原因是显示器固件版本太老,EDID没更新。最后通过显示器菜单升级固件才解决。所以,EDID读出来了不代表它是对的,最好跟显示器规格书交叉验证。

4.7 EDID读取流程总览

下面这张图概括了从I2C读取到时序配置的完整流程:

EDID读取与解析流程 上电 / 热插拔检测 I2C/DDC初始化 读取128字节EDID 校验Header + Checksum 失败 重试3次 / 返回错误 成功 解析详细时序(DTD) 配置显示控制器 提示:如果显示器支持4K@60Hz,但EDID只读到4K@30Hz,请检查显示器固件版本

这个流程看起来简单,但实际调试时每一步都可能出问题。我个人建议在驱动里加一个debugfs接口,可以手动触发EDID重新读取,并把原始数据dump出来。这样调试时不用反复插拔显示器,效率高很多。

4.8 扩展EDID与CEA-861块

标准EDID只有128字节,对于现代显示器远远不够。所以有了扩展块机制。第0x7E字节表示扩展块的数量,每个扩展块也是128字节。最常见的扩展块是CEA-861块,它包含了:

  • 支持的视频格式(如4K@60Hz、HDR等)
  • 音频格式(LPCM、AC3、DTS等)
  • 扬声器配置
  • VIC(Video Identification Code)列表

解析CEA扩展块时,要注意它的数据块结构。每个数据块有标签(Tag)和长度(Length),然后才是具体数据。我见过有人直接把整个扩展块当DTD解析,结果当然是错的。

经验之谈:如果你在做HDMI 2.0或DP 1.4驱动,一定要检查CEA扩展块里的VIC列表。有些显示器虽然支持4K@60Hz,但EDID里只通过VIC声明,没有在标准DTD里列出。忽略扩展块,你就永远拿不到那个分辨率。

好了,EDID这块内容就讲到这里。记住一句话:读EDID容易,读对EDID难。多准备几台不同品牌的显示器做测试,你会发现很多意想不到的兼容性问题。驱动开发就是这样,细节决定成败。