基于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或者搞个游戏渲染管线,让这自制的视频接口真正骚起来。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐