基于fpga实现hdmi视频输出的实现
二十年前显示器屁股后头还拖着VGA线的时候,估计没人想到现在满大街的HDMI接口能这么普及。今天咱们就来整点硬核的——用FPGA直接怼出HDMI信号,手搓数字视频接口这事可比玩单片机刺激多了。
先搞明白HDMI底层怎么传数据的。这货用了TMDS编码方案,简单说就是把8位像素数据通过算法转成10位传输码。三个数据通道分别传RGB,时钟通道保持同步。以常见的640x480@60Hz为例,像素时钟得跑到25.175MHz,不过实际操作咱们直接用25MHz也能凑合。
上代码!先整时序生成模块:
module video_timing( input clk25, output reg [11:0] pixel_x, output reg [11:0] pixel_y, output reg hsync, output reg vsync, output reg active ); // 时序参数 parameter H_ACTIVE = 640; parameter H_FP = 16; parameter H_SYNC = 96; parameter H_BP = 48; parameter V_ACTIVE = 480; parameter V_FP = 10; parameter V_SYNC = 2; parameter V_BP = 33; always @(posedge clk25) begin if(pixel_x < H_ACTIVE + H_FP + H_SYNC + H_BP -1) pixel_x <= pixel_x + 1; else begin pixel_x <= 0; if(pixel_y < V_ACTIVE + V_FP + V_SYNC + V_BP -1) pixel_y <= pixel_y + 1; else pixel_y <= 0; end hsync <= (pixel_x >= H_ACTIVE + H_FP) && (pixel_x < H_ACTIVE + H_FP + H_SYNC); vsync <= (pixel_y >= V_ACTIVE + V_FP) && (pixel_y < V_ACTIVE + V_FP + V_SYNC); active <= (pixel_x < H_ACTIVE) && (pixel_y < V_ACTIVE); end endmodule这个模块负责生成扫描时序,pixelx/pixely记录当前扫描位置,active信号控制何时输出有效像素。注意hsync和vsync是低电平有效,有些显示器对同步脉冲宽度比较敏感,参数别乱改。
基于fpga实现hdmi视频输出的实现
接下来是TMDS编码的重头戏,直接上查表法实现:
module tmds_encoder( input [7:0] data, input c0, input c1, input de, output reg [9:0] tmds ); // 计算异或/同或差异 function [3:0] xdcnt; input [7:0] d; integer i; begin xdcnt = 0; for(i=0; i<8; i=i+1) xdcnt = xdcnt + d[i]; end endfunction // 编码状态机 always @(*) begin if(!de) begin // 控制周期 case({c1,c0}) 2'b00: tmds = 10'b1101010100; 2'b01: tmds = 10'b0010101011; 2'b10: tmds = 10'b0101010100; 2'b11: tmds = 10'b1010101011; endcase end else begin // 数据周期 wire [7:0] din = data; wire [3:0] cnt = xdcnt(din); wire [8:0] q_m; // 选择XOR/XNOR编码 if(cnt > 4'd4 || (cnt == 4'd4 && !din[0])) begin q_m[0] = din[0]; for(int i=1; i<8; i++) q_m[i] = q_m[i-1] ^ ~din[i]; q_m[8] = 0; end else begin q_m[0] = din[0]; for(int i=1; i<8; i++) q_m[i] = q_m[i-1] ^ din[i]; q_m[8] = 1; end // 添加直流平衡位 wire [4:0] cnt_qm = xdcnt(q_m[7:0]) + q_m[8]; if(cnt_qm > 5 || (cnt_qm == 5 && !q_m[8])) tmds = {~{q_m[8], q_m[7:0]}, 1'b1}; else tmds = {q_m[8], q_m[7:0], 1'b0}; end end endmodule这个编码器实现里有个骚操作——根据数据中1的个数动态选择异或或同或编码,最后还要做直流平衡。注意控制周期的编码对应四种状态:VSYNC和HSYNC的组合。
顶层模块要把这三个通道的编码输出串行化:
module hdmi_top( input clk, output [3:0] tmds ); wire clk25, clk250; wire [9:0] tmds_r, tmds_g, tmds_b; // 时钟生成 pll_hdmi pll_inst(.clk_in(clk), .clk25(clk25), .clk250(clk250)); // 生成测试图案 wire [7:0] red = {pixel_x[7:0] ^ pixel_y[7:0]}; wire [7:0] green = pixel_x[7:0]; wire [7:0] blue = pixel_y[7:0]; // 实例化三个编码通道 tmds_encoder red_enc(red, vsync, hsync, active, tmds_r); tmds_encoder green_enc(green, 1'b0, 1'b0, active, tmds_g); tmds_encoder blue_enc(blue, 1'b0, 1'b0, active, tmds_b); // 串行化输出 genvar i; generate for(i=0; i<3; i=i+1) begin : ser OSERDESE2 #( .DATA_RATE_OQ("DDR"), .DATA_WIDTH(10) ) ser_inst ( .CLK(clk250), .CLKDIV(clk25), .D1(tmds_r[i]), .D2(tmds_r[i+5]), ... ); end endgenerate endmodule这里用OSERDESE2原语实现10:1的并串转换,250MHz时钟驱动。测试图案直接拿坐标的低8位生成彩虹条纹,烧进板子接显示器能看到斜向渐变效果。
实际调试时最容易翻车的是差分对方向——HDMI插座的正负极性得和FPGA管脚定义一致。遇到过最玄学的问题是显示器死活不认信号,最后发现是VSYNC脉冲宽度比标准少了一个时钟周期。建议用Signaltap抓取编码后的波形,对照VIC时序规范检查参数。
搞定这些,你的FPGA就能像正规显卡一样输出了。下次可以试试上1080P或者搞个游戏渲染管线,让这自制的视频接口真正骚起来。