4、HDMI发送控制器设计:视频时序发生器、像素数据打包、控制字符插入、数据通道对齐与仿真验证

各位同学,今天我们来聊聊HDMI发送控制器里最核心的几个模块。说实话,这部分内容在项目中踩坑最多,也最考验对时序的理解。我个人习惯把HDMI发送控制器拆成五个部分来看:时序发生器、数据打包、控制字符插入、通道对齐,最后是仿真验证。咱们一个一个说。

4.1 视频时序发生器——H/V Sync是怎么来的?

视频时序发生器,说白了就是产生行同步(HSync)和场同步(VSync)信号的计数器。你想想看,显示器要逐行扫描,每扫完一行就得告诉它“该换行了”,这就是HSync。扫完一帧就得说“该换帧了”,这就是VSync。

我刚开始做HDMI项目时,以为时序发生器就是几个计数器的事。后来发现,不同分辨率对应的时序参数完全不同。比如1080p@60Hz,一行总共有2200个像素时钟,其中有效像素是1920,剩下的280个是消隐区。消隐区里又分前肩、同步脉冲和后肩。

这里我贴一个常用的时序参数表,大家做设计时可以直接参考:

分辨率 刷新率 H总像素 H有效 H前肩 H同步 H后肩 V总行数 V有效 V前肩 V同步 V后肩
1920x1080 60Hz 2200 1920 88 44 148 1125 1080 4 5 36
1280x720 60Hz 1650 1280 110 40 220 750 720 5 5 20
640x480 60Hz 800 640 16 96 48 525 480 10 2 33

代码实现上,我习惯用两个计数器:一个行计数器(h_cnt),一个场计数器(v_cnt)。行计数器在每个像素时钟下加1,计到H总像素-1时归零,同时场计数器加1。场计数器计到V总行数-1时归零,表示一帧结束。

// 视频时序发生器核心代码
always @(posedge clk or posedge rst) begin
    if (rst) begin
        h_cnt <= 0;
        v_cnt <= 0;
    end else begin
        if (h_cnt == H_TOTAL - 1) begin
            h_cnt <= 0;
            if (v_cnt == V_TOTAL - 1)
                v_cnt <= 0;
            else
                v_cnt <= v_cnt + 1;
        end else begin
            h_cnt <= h_cnt + 1;
        end
    end
end

// 生成HSync和VSync
assign hsync = (h_cnt >= H_SYNC_START) && (h_cnt < H_SYNC_END);
assign vsync = (v_cnt >= V_SYNC_START) && (v_cnt < V_SYNC_END);
// 生成数据使能DE
assign de = (h_cnt >= H_ACTIVE_START) && (h_cnt < H_ACTIVE_END) &&
            (v_cnt >= V_ACTIVE_START) && (v_cnt < V_ACTIVE_END);
小提示: 我建议把时序参数定义成参数(parameter),而不是直接写死数字。这样换分辨率时只需要改参数,不用动逻辑代码。我在项目中吃过这个亏,当时为了调试一个1440p的分辨率,硬是改了一堆魔数,后来重构代码时才统一成参数化设计。

4.2 像素数据打包——把RGB变成TMDS能吃的格式

像素数据打包,就是把RGB三个颜色分量打包成HDMI能传输的数据格式。HDMI用的是TMDS编码,每个颜色通道8位数据会被编码成10位。但在这之前,我们需要先把像素数据组织好。

我一般会在数据打包模块里做三件事:

  • 像素缓冲:从帧缓存或视频源读取像素数据,通常是24位RGB(R:G:B = 8:8:8)
  • 通道分配:把RGB分别送到三个TMDS通道(Channel 0、1、2)
  • 控制信号对齐:把DE、HSync、VSync信号与像素数据对齐

这里要注意一个细节:HDMI规范要求数据通道0传输B分量和HSync/VSync,通道1传输G分量,通道2传输R分量。嗯,这个分配是固定的,别搞混了。

// 像素数据打包
assign ch0_data = de ? pixel_b : {4'b0, vsync, hsync, 2'b0};
assign ch1_data = de ? pixel_g : 8'b0;
assign ch2_data = de ? pixel_r : 8'b0;
注意: 在消隐区(DE=0)时,数据通道0要插入控制信号。我曾经在项目中忽略了这一点,结果显示器一直显示花屏,排查了两天才发现是消隐区的数据没处理好。

4.3 控制字符插入——DE/HS/VS的编码艺术

控制字符插入,是HDMI发送控制器里最容易出错的地方。为什么?因为HDMI在消隐区传输的不是像素数据,而是控制字符。这些控制字符有固定的编码规则。

HDMI定义了四种控制字符:

  • CTL0:对应HSync=0, VSync=0
  • CTL1:对应HSync=1, VSync=0
  • CTL2:对应HSync=0, VSync=1
  • CTL3:对应HSync=1, VSync=1

在数据使能(DE)为高电平时,我们传输的是像素数据。DE为低电平时,传输的是控制字符。控制字符在TMDS编码前会被转换成特定的10位码字。

我个人习惯用一个状态机来控制数据通道的切换:

// 控制字符插入状态机
always @(posedge clk) begin
    if (de) begin
        // 传输像素数据
        tmds_data <= pixel_data;
    end else begin
        // 传输控制字符
        case ({vsync, hsync})
            2'b00: tmds_data <= CTL0_CODE;
            2'b10: tmds_data <= CTL1_CODE;
            2'b01: tmds_data <= CTL2_CODE;
            2'b11: tmds_data <= CTL3_CODE;
        endcase
    end
end
核心要点: 控制字符的插入时机必须与DE信号严格对齐。DE下降沿之后的第一拍就要插入控制字符,DE上升沿之前最后一拍要结束控制字符。这个时序窗口非常窄,稍有不慎就会导致显示器无法锁定信号。

4.4 数据通道对齐——三路TMDS通道的同步问题

数据通道对齐,是HDMI发送控制器里最容易被忽视的问题。三个TMDS通道各自独立编码,但接收端需要把它们对齐才能正确解码。如果通道间有延迟差异,就会出现颜色错位或图像撕裂。

我记得在做一个4K@30Hz的项目时,三个通道的布线长度不一样,导致通道2比通道0慢了大约2个像素时钟。结果就是图像上出现了彩虹条纹,怎么调参数都解决不了。后来加了FIFO做延迟补偿才搞定。

通道对齐的常用方法有两种:

  1. FIFO对齐法:每个通道后面加一个FIFO,用同一个读指针控制输出,确保三路数据同时送出
  2. 延迟链对齐法:在较快的通道上插入延迟单元,使三路数据到达时间一致

我个人更推荐FIFO对齐法,因为它更灵活,也更容易调试。延迟链法受工艺和温度影响大,不太稳定。

// 通道对齐FIFO示例
// 每个通道使用独立的写指针,共享读指针
always @(posedge clk) begin
    if (wr_en) begin
        fifo_ch0[wr_ptr_ch0] <= data_ch0;
        fifo_ch1[wr_ptr_ch1] <= data_ch1;
        fifo_ch2[wr_ptr_ch2] <= data_ch2;
    end
    // 所有通道使用同一个读指针
    if (rd_en) begin
        out_ch0 <= fifo_ch0[rd_ptr];
        out_ch1 <= fifo_ch1[rd_ptr];
        out_ch2 <= fifo_ch2[rd_ptr];
    end
end

4.5 仿真验证方法——别等上板才发现问题

仿真验证,这部分我想多说几句。很多同学喜欢直接上板调试,觉得仿真浪费时间。但我告诉你,HDMI这种高速接口,上板调试一次可能要花十几分钟甚至半小时,而仿真跑一次只需要几秒钟。

我曾经在一个项目里,因为时序发生器的边界条件没处理好,导致显示器间歇性黑屏。上板调试了整整三天,最后用仿真一跑,发现是场同步的结束条件写错了。从那以后,我再也不敢跳过仿真了。

仿真验证我一般分三步走:

  • 第一步:功能仿真——验证时序发生器产生的HSync、VSync、DE是否符合预期参数
  • 第二步:数据通路仿真——验证像素数据能否正确打包、控制字符能否正确插入
  • 第三步:通道对齐仿真——验证三路TMDS数据是否对齐,有没有延迟差异

下面是一个简单的仿真测试代码框架:

// 仿真测试代码
initial begin
    // 初始化
    clk = 0;
    rst = 1;
    #100 rst = 0;
    
    // 等待一帧结束
    repeat (V_TOTAL * H_TOTAL) @(posedge clk);
    
    // 检查HSync脉冲宽度
    if (hsync_pulse_width != H_SYNC_WIDTH)
        $error("HSync pulse width mismatch!");
    
    // 检查VSync脉冲宽度
    if (vsync_pulse_width != V_SYNC_WIDTH)
        $error("VSync pulse width mismatch!");
    
    // 检查DE有效区域
    if (de_active_pixels != H_ACTIVE * V_ACTIVE)
        $error("DE active area mismatch!");
    
    $display("All tests passed!");
    $finish;
end
仿真技巧: 我习惯在仿真脚本里加入波形dump功能,把关键信号(h_cnt、v_cnt、hsync、vsync、de、ch0_data等)都dump出来。这样一旦仿真报错,可以直接看波形定位问题。另外,建议用断言(assertion)来检查时序约束,比如“DE高电平时HSync必须为低”这种规则。

好了,关于HDMI发送控制器的设计,我们就聊到这里。这部分内容比较多,但核心就是时序发生器、数据打包、控制字符插入、通道对齐和仿真验证这五个环节。每个环节都有它的坑,但只要理解了原理,设计起来并不难。

下面我用一张图来总结本章的知识体系:

HDMI发送控制器设计知识体系 HDMI发送控制器 视频时序发生器 H/V Sync计数器 像素数据打包 RGB通道分配 控制字符插入 DE/HS/VS编码 数据通道对齐 FIFO/延迟链 仿真验证方法 功能/通路/对齐 五个模块协同工作,构成完整的HDMI发送链路 关键设计参数 • 像素时钟:148.5MHz (1080p@60Hz) • TMDS编码:8b/10b,每通道10位数据 • 数据通道:3路TMDS通道 + 1路时钟通道

最后提醒一句:HDMI设计最忌讳的就是“差不多就行”。时序参数差一个时钟周期,显示器可能就不认了。所以,严格按照规范来,仿真验证做到位,上板才能一次点亮。