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);
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;
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
4.4 数据通道对齐——三路TMDS通道的同步问题
数据通道对齐,是HDMI发送控制器里最容易被忽视的问题。三个TMDS通道各自独立编码,但接收端需要把它们对齐才能正确解码。如果通道间有延迟差异,就会出现颜色错位或图像撕裂。
我记得在做一个4K@30Hz的项目时,三个通道的布线长度不一样,导致通道2比通道0慢了大约2个像素时钟。结果就是图像上出现了彩虹条纹,怎么调参数都解决不了。后来加了FIFO做延迟补偿才搞定。
通道对齐的常用方法有两种:
- FIFO对齐法:每个通道后面加一个FIFO,用同一个读指针控制输出,确保三路数据同时送出
- 延迟链对齐法:在较快的通道上插入延迟单元,使三路数据到达时间一致
我个人更推荐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
好了,关于HDMI发送控制器的设计,我们就聊到这里。这部分内容比较多,但核心就是时序发生器、数据打包、控制字符插入、通道对齐和仿真验证这五个环节。每个环节都有它的坑,但只要理解了原理,设计起来并不难。
下面我用一张图来总结本章的知识体系:
最后提醒一句:HDMI设计最忌讳的就是“差不多就行”。时序参数差一个时钟周期,显示器可能就不认了。所以,严格按照规范来,仿真验证做到位,上板才能一次点亮。