基于FPGA实现HDMI视频输出的实践
以常见的640x480@60Hz为例,像素时钟得跑到25.175MHz,不过实际操作咱们直接用25MHz也能凑合。实际调试时最容易翻车的是差分对方向——HDMI插座的正负极性得和FPGA管脚定义一致。二十年前显示器屁股后头还拖着VGA线的时候,估计没人想到现在满大街的HDMI接口能这么普及。今天咱们就来整点硬核的——用FPGA直接怼出HDMI信号,手搓数字视频接口这事可比玩单片机刺激多了。这个编码
基于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或者搞个游戏渲染管线,让这自制的视频接口真正骚起来。

更多推荐
所有评论(0)