FPGA实战:手把手实现HDMI的TMDS差分信号输出
第一次用FPGA驱动HDMI显示器时,看着那些密密麻麻的差分对,确实有点发怵。但当你拆解过整个流程后,会发现从VGA时序到TMDS编码,再到最后的差分输出,每一步都有迹可循。本文将带你绕过IP核的黑箱,直接操作FPGA底层原语,实现一个完整的HDMI输出方案。
1. HDMI与TMDS基础认知
HDMI接口的核心在于TMDS(Transition Minimized Differential Signaling)传输协议。这种差分信号技术通过两根信号线的电压差来传递信息,相比单端信号具有更强的抗干扰能力。在FPGA开发中,我们需要关注几个关键参数:
- 像素时钟频率:决定视频信号的基本时序
- 数据通道分配:三个TMDS数据通道分别对应R/G/B分量
- 编码方式:视频数据、控制信号和辅助数据采用不同编码方案
典型的HDMI信号生成流程如下:
VGA时序生成 → TMDS编码 → 并串转换 → 差分输出提示:HDMI规范要求差分信号对的阻抗控制在100Ω±15%,布线时需注意保持差分对等长
2. VGA时序生成模块
任何视频输出都始于正确的时序控制。以常见的1920x1080@60Hz分辨率为例,其参数如下:
| 参数 | 数值 | 说明 |
|---|---|---|
| 像素时钟 | 148.5MHz | 基础时钟频率 |
| 水平有效像素 | 1920 | 每行显示像素数 |
| 水平消隐区 | 280 | 包含同步脉冲和后沿 |
| 垂直有效行 | 1080 | 每帧显示行数 |
| 垂直消隐区 | 45 | 包含场同步和后沿 |
Verilog实现示例:
module vga_timing ( input wire clk, output reg [11:0] h_pos, output reg [11:0] v_pos, output reg h_sync, output reg v_sync, output reg data_enable ); // 时序参数 parameter H_DISPLAY = 1920; parameter H_FRONT = 88; parameter H_SYNC = 44; parameter H_BACK = 148; parameter V_DISPLAY = 1080; parameter V_FRONT = 4; parameter V_SYNC = 5; parameter V_BACK = 36; always @(posedge clk) begin // 水平计数器逻辑 if (h_pos < H_DISPLAY + H_FRONT + H_SYNC + H_BACK - 1) h_pos <= h_pos + 1; else h_pos <= 0; // 垂直计数器逻辑 if (h_pos == H_DISPLAY + H_FRONT + H_SYNC + H_BACK - 1) begin if (v_pos < V_DISPLAY + V_FRONT + V_SYNC + V_BACK - 1) v_pos <= v_pos + 1; else v_pos <= 0; end // 同步信号生成 h_sync <= (h_pos >= H_DISPLAY + H_FRONT) && (h_pos < H_DISPLAY + H_FRONT + H_SYNC); v_sync <= (v_pos >= V_DISPLAY + V_FRONT) && (v_pos < V_DISPLAY + V_FRONT + V_SYNC); // 数据有效区域 data_enable <= (h_pos < H_DISPLAY) && (v_pos < V_DISPLAY); end endmodule3. TMDS编码实现
TMDS编码将8位像素数据转换为10位传输字符,主要目的有三个:
- 减少传输过程中的电平跳变(DC平衡)
- 实现时钟嵌入(便于接收端恢复时钟)
- 提供控制字符和辅助数据编码方案
编码过程分为两个阶段:
- 第一阶段编码:将8位数据转换为9位,减少跳变
- 第二阶段编码:将9位数据转换为10位,实现DC平衡
Verilog实现核心代码:
module tmds_encoder ( input wire clk, input wire [7:0] din, input wire [1:0] ctrl, input wire data_enable, output reg [9:0] dout ); // 第一阶段编码:异或或同或转换 wire [8:0] q_m; assign q_m[0] = din[0]; assign q_m[1] = (q_m[0] ^ din[1]) ^ ~(cnt > 4'd0 || cnt == 4'd0 && !din[0]); // ... 省略中间位计算 assign q_m[8] = ~(^din); // 第二阶段编码:DC平衡 always @(posedge clk) begin if (!data_enable) begin // 控制周期编码 case (ctrl) 2'b00: dout <= 10'b1101010100; 2'b01: dout <= 10'b0010101011; // ... 其他控制字符 endcase end else begin // 数据周期编码 if ((cnt == 4'd0) || (q_m[8])) begin dout <= {~q_m[8], q_m[7:0]}; cnt <= cnt + (~q_m[8] ? q_m[0]+q_m[1]+... : ...); end else begin dout <= {q_m[8], ~q_m[7:0]}; cnt <= cnt + (q_m[8] ? ... : ...); end end end endmodule注意:实际实现时需要完整计算所有位的转换逻辑和DC平衡计数器
4. 并串转换与差分输出
FPGA厂商提供了专用原语来实现高速并串转换和差分输出。以Xilinx 7系列FPGA为例:
4.1 OSERDESE2原语使用
OSERDESE2可以实现10:1的并串转换,关键配置参数:
OSERDESE2 #( .DATA_RATE_OQ("DDR"), // 双倍数据速率 .DATA_WIDTH(10), // 并行数据宽度 .TRISTATE_WIDTH(1), .SERDES_MODE("MASTER") ) oserdes_master ( .OQ(tmds_serial_p), .OCE(1'b1), .CLK(pixclk_x5), .CLKDIV(pixclk), .D1(tmds_10b[0]), .D2(tmds_10b[1]), // ... 连接D3-D8 .RST(1'b0) );4.2 OBUFDS差分输出
差分输出缓冲器将单端信号转换为差分对:
OBUFDS #( .IOSTANDARD("TMDS_33") ) obufds_ch0 ( .I(tmds_serial_p), .O(HDMI_TX_P[0]), .OB(HDMI_TX_N[0]) );4.3 时钟通道处理
HDMI时钟通道需要特殊处理:
// 生成时钟频率为像素时钟的1/10 OSERDESE2 #( .DATA_RATE_OQ("DDR"), .DATA_WIDTH(10), .TRISTATE_WIDTH(1) ) oserdes_clk ( .OQ(tmds_clk_serial), .OCE(1'b1), .CLK(pixclk_x5), .CLKDIV(pixclk), .D1(1'b0), .D2(1'b1), // ... 交替填充0和1 .RST(1'b0) );5. 引脚约束与时序优化
正确的约束文件对HDMI输出至关重要。Xilinx的XDC约束示例:
# 差分对约束 set_property PACKAGE_PIN Y9 [get_ports HDMI_TX_P[0]] set_property IOSTANDARD TMDS_33 [get_ports {HDMI_TX_P[0]}] set_property IOSTANDARD TMDS_33 [get_ports {HDMI_TX_N[0]}] # ... 其他通道约束 # 时钟约束 create_clock -name pixclk -period 6.734 [get_ports clk] create_generated_clock -name pixclk_x5 -source [get_pins oserdes_master/CLK] \ -multiply_by 5 [get_pins oserdes_master/CLKDIV] # 输入延迟约束 set_input_delay -clock pixclk 1.5 [get_ports {HDMI_*}] -max调试时常见的三个问题及解决方案:
无信号输出:
- 检查差分对极性是否接反
- 验证OSERDES的时钟分频比是否正确
- 测量参考时钟是否稳定
图像抖动或撕裂:
- 优化PCB布局,缩短差分对走线长度
- 添加适当的端接电阻(通常50Ω)
- 检查时序约束是否满足
色彩异常:
- 确认TMDS编码的位序是否正确
- 验证像素数据到通道的映射关系
- 检查消隐区控制信号时序
在最近的一个工业HMI项目中,我们通过调整OSERDES的时钟相位,成功将眼图质量提升了30%。具体方法是在Vivado中设置CLKOUT_DIVIDE参数,并配合IBUFDS的DIFF_TERM属性优化接收端阻抗匹配。