1

FPGA 驱动 HDMI 屏全讲解

 4 months ago
source link: https://www.taterli.com/9667/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

FPGA 驱动 HDMI 屏全讲解

FPGA 驱动 HDMI 屏全讲解

其实一直也没怎么讲这个,网上各种教程都是串口收发等等,要不就是特别高级特别复杂的项目,什么以太网USB统统用上,其实我觉得驱动HDMI是一个比较简单,但是又比较需要整体考量的东西.

HDMI显示,本质上也是编码RGB显示屏数据,最后发送,不管是RGB时序生成,还是编码算法其实都非常简单,大家写个串口收发那种功力,就能把这个全部实现好,无非是东西比较多,由多个东西组合而成罢了.如果你使用像高云这样的平台,他们HDMI是封装成IP的,那就更加没东西可以讨论了,等于根本不用你自己写.

一开始我们要了解RGB驱动时序,毕竟笼统地说RGB驱动时序通过串并转换后就是HDMI时序嘛,我们不要太过复杂去说几种模式,就单纯说一下HDMI编码中用到的DE模式模式.

一帧时序.

image-11.png

一行时序.

image-12.png

我们需要定义计数器的起始位置,当然这都是我们开发时候可以自己定的,上面的VSPW和HSPW我成为SYNC信号,即记作VSYNC,HSYNC时间,这段时间他们是低电平的.之后经过一段时间的Back Porch,然后是有效像素,我称为VACTIVE,HACTIVE,最后再经历一段时间的Front Porch,一个HSYNC+HBP+HACTIVE+HFP周期刷完一行像素,很多个行像素周期构成一页.

第二个关键问题,VSYNC的时钟是基于HSYNC切换时切换的,看VSYNC图的虚线可见,在HSYNC的HFP的最后一刻,VSYNC时钟计数.

所以很自然就写出下面的逻辑.

// Horizontal Counter
always @ (posedge clk or negedge resetn)
begin
    if(!resetn)
        h_cnt <= 0;
    else if(h_cnt == H_TOTAL - 1)
        h_cnt <= 0;
    else
        h_cnt <= h_cnt + 1;
end

// Vertical Counter
always @ (posedge clk or negedge resetn)
begin
    if(!resetn)
        v_cnt <= 0;
    else if(h_cnt == H_FP - 1) // Horizontal Sync Time
        if(v_cnt == V_TOTAL - 1)
            v_cnt <= 0;
        else
            v_cnt <= v_cnt + 1;
    else
        v_cnt <= v_cnt;
end

第二个问题是计算DE时刻,DE都在H_ACTIVE且V_ACTIVE时候出现,根据条件,可以写出DE时机.

// Horizontal Active
assign h_active = ((h_cnt >= H_SYNC + H_BP) && (h_cnt < H_SYNC + H_BP + H_ACTIVE))?1'b1:1'b0;

// Vertical Active
assign v_active = ((v_cnt >= V_SYNC + V_BP) && (v_cnt < V_SYNC + V_BP + V_ACTIVE))?1'b1:1'b0;

assign de = h_active & v_active;

第三个问题就简单很多了,计算HSYNC,VSYNC这个信号什么时候变化,图上可以看到,除了H_SYNC和V_SYNC时间,其他时间都是高,所以也很容易写啦.

// HS Generate
assign hs = (h_cnt > H_SYNC)?1'b0:1'b1;

// VS Generate
assign vs = (v_cnt > V_SYNC)?1'b0:1'b1;

至此全部时序发生就写完了,至于具体屏幕坐标,已经知道H_ACTIVE第一点开始是X=0,V_ACTIVE第一个点开始是Y=0,但是后面再说怎么写,毕竟这里还是有点小技巧的,你现在可以仿真一下,时序目前是配上了.

接下来要看看如何把RGB时序转换成HDMI时序,HDMI时序其实是TMDS信号,三个通道,每个通道传输部分内容,而TMDS信号前面还要经历8b10b转换,所以这里还涉及两个时钟,一个是RGB时候的PCLK,即像素时钟,一个是编码后的时钟,即10倍的PCLK,即使我们串并转换后支持DDR模式,也需要5倍的PCLK.我们先看PCLK是多少,我这里找到一个常规显示信号的规格计算网站.

https://tomverbeure.github.io/video_timings_calculator

首先,明确我们使用的片子性能,比如我用的7Z010-1,PLL最大输出800MHz,选择2560*1440分辨率,总H_TOTAL是2640,总V_TOTAL是1471,刷一页就需要2640*1471=3883440,而800MHz/5=160MHz,160MHz/3883440≈41Hz,满足.如果分辨率再提高,下一个标准分辨率则30Hz都不能满足,因此选择这个就是最大能力了.当然,这时候也不用倍频到800MHz,降低EMI也是好事嘛,那就先做一个PLL出来.(我这里依然偷懒了,直接按最大性能拉满,但是警告也推荐不要这么干,想满足条件稳定的可以做1080p/30设计,而且时钟这么高,也不太好收敛.)

image-17.png
image-13-1024x689.png

接下来进行比较难的HDMI编码部分,但是说比较难也并不算难,自己按着步骤写也很快的,毕竟也就没几步,看着是个流程图,像对FPGA这种并行来说不太友好,实际上只要打拍的时机合适,也是非常简单的,仅需三个时钟就能完成.

image-14.png

明白几个概念,就很简单了,DE就DE,C0,C1是控制信号输入,其中对于蓝色通道刚好对应HS/VS,其他我们暂时没用就暂时不考虑,D当然是RGB数据,分成三个8位,所以构成RGB888,至于Cnt(t-1)就是上一次执行时候Cnt的结果.既然是上一次结果,那没有上一次呢?哎,不可能的.因为你一开始就VS拉低了,之后Cnt就为0,这不就等于给了初值.

第一个判断条件,要看1的个数是否大于4,或者1的个数等于4但是第0位是0,统计个数,那当然也会占用一拍,CLK来了,统计一下,这样n1d就赋值了.这就是第一个时钟完成的事情.

//统计待编码输入数据中1的个数,最多8个1,所以位宽为4.
always@(posedge clk)
begin
    if(!resetn)
    begin
        n1d <= 4'd0;
    end
    else if(de)
    begin
        n1d <= din[0] + din[1] + din[2] + din[3] + din[4] + din[5] + din[6] + din[7];
    end
    else begin // DE为低时候传输的是控制字符,硬编码的,没有统计必要.
        n1d <= 4'd0;
    end
end

随后写上判断条件.

assign condition1 = ((n1d > 4'd4) || ((n1d == 4'd4) && (~din_r[0])));

记住,刚才为了计算n1d是过了1个CLK的.如果我们此时把输入数据也打一拍,也可以锁存起来.

// 前面统计数据时候打了一拍,这里也要打一拍来同步.
always@(posedge clk)begin
    din_r   <= din;
    de_r    <= {de_r[0],de};
    c0_r    <= {c0_r[0],c0};
    c1_r    <= {c1_r[0],c1};
    q_m_r   <= q_m;
end

由于他们都是经过1CLK,所以是同时发生的,至于为什么要锁存DE/C0/C1,以及等下要处理的q_m,则是因为数据还会随着PCLK进来而进来,不存起来不就覆盖了,这里计算又不止一个周期.

根据条件1进行计算,这里是连接到wire,因组合逻辑理论不占时间,所以不用考虑打拍.

//对输入的信号进行XOR/XNOR运算.
assign q_m[0] = din_r[0];
assign q_m[1] = condition1 ? ~((q_m[0] ^ din_r[1])) : (q_m[0] ^ din_r[1]);
assign q_m[2] = condition1 ? ~((q_m[1] ^ din_r[2])) : (q_m[1] ^ din_r[2]);
assign q_m[3] = condition1 ? ~((q_m[2] ^ din_r[3])) : (q_m[2] ^ din_r[3]);
assign q_m[4] = condition1 ? ~((q_m[3] ^ din_r[4])) : (q_m[3] ^ din_r[4]);
assign q_m[5] = condition1 ? ~((q_m[4] ^ din_r[5])) : (q_m[4] ^ din_r[5]);
assign q_m[6] = condition1 ? ~((q_m[5] ^ din_r[6])) : (q_m[5] ^ din_r[6]);
assign q_m[7] = condition1 ? ~((q_m[6] ^ din_r[7])) : (q_m[6] ^ din_r[7]);
assign q_m[8] = ~condition1;

之后看看DE状态,DE无效会直接跳到输出,但是我们为了所有输出都同步起来,现在暂时不输出任何东西,依然需要计算条件2和条件3,这里因为要统计n1q_m和n0q_m,所以这里也要一个时钟.为什么不能wire计算呢?因为这是依赖上一级的,是时序逻辑,前面没计算完这里自然没法继续,继续也没意义.

//判断条件2:一行已编码数据(上一次传输会更新Cnt)中1的个数等于0的个数或者本次编码数据中1的个数等于0的个数.
assign condition2 = ((cnt == 6'd0) || (n1q_m == n0q_m));

//判断条件3:已编码数据中1的多余0并且本次编码中间数据1的个数也多与0的个数或者已编码数据中0的个数较多并且此次编码中0的个数也比较多时拉高,其余时间拉低,为什么判断bit5,因为对于我们看,他就是符号位.
assign condition3 = (((~cnt[5]) && (n1q_m > n0q_m)) || (cnt[5] && (n1q_m < n0q_m)));

下一刻开始,可以输出结果了,因为条件2或者条件3之后也没什么判断了.至于q_m[8]用三元比较就可以,本质上也会综合成组合逻辑,不会打拍.

这下这段就很好理解了吧,最后一个时钟进行输出.

always@(posedge clk)
begin
    if(!resetn)
    begin
        cnt <= 6'd0;
        q_out <= 10'd0;
    end
    else if(de_r[1]) //又打了一拍之后,de_r[1]就是原来的de.
    begin
        q_out[8] <= q_m_r[8]; //第8位为编码方式位,直接输出即可.
        if(condition2)
        begin
            q_out[9] <= ~q_m_r[8];
            q_out[7:0] <= q_m_r[8] ? q_m_r[7:0] : ~q_m_r[7:0];
            // 按照规范更新Cnt.
            cnt <= q_m_r[8] ? (cnt + n1q_m - n0q_m) : (cnt + n0q_m - n1q_m);
        end
        else if(condition3)
        begin
            q_out[9] <= 1'b1;
            q_out[7:0] <= ~q_m_r[7:0];
            // 按照规范更新Cnt.
            cnt <= cnt + {q_m_r[8],1'b0} + n0q_m - n1q_m;
        end
        else 
        begin
            q_out[9] <= 1'b0;
            q_out[7:0] <= q_m_r[7:0];
            // 按照规范更新Cnt.
            cnt <= cnt - {~q_m_r[8],1'b0} + n1q_m - n0q_m;
        end
    end
    else 
    begin
        // DE = 0时,需要设置Cnt = 0.
        cnt <= 6'd0;
        case ({c1_r[1],c0_r[1]})
            2'b00   : q_out <= CTRLTOKEN0;
            2'b01   : q_out <= CTRLTOKEN1;
            2'b10   : q_out <= CTRLTOKEN2;
            2'b11   : q_out <= CTRLTOKEN3;
        endcase
    end
end

这样就把8b编码成10b了,这个编码能提供很好的直流平衡等等优势,反正高速传输很有用就是了.

还要例化三份这玩意,因为三个通道嘛,这里给出蓝色通道,如果C0,C1不使用,则设置为0.

tmds_encoder tmds_encoder_blue(
    .clk(clk_pclk),
    .resetn(reset0_n),
    .din(color[7:0]),
    .c0(hs),
    .c1(vs),
    .de(de),
    .q_out(tmds_blue)
);

接下来要使用并串转换,毕竟q_out是10b,而HDMI数据线上是串行的,所以要用OSERDESE2进行转换,还要用到级联模式.

wire shift1;
wire shift2;

OSERDESE2 #(
   .DATA_RATE_OQ("DDR"), // DDR, SDR
   .DATA_RATE_TQ("SDR"), // DDR, BUF, SDR
   .DATA_WIDTH(10), // Parallel data width (2-8,10,14)
   .SERDES_MODE("MASTER"), // MASTER, SLAVE
   .TBYTE_CTL("FALSE"), // Enable tristate byte operation (FALSE, TRUE)
   .TBYTE_SRC("FALSE"), // Tristate byte source (FALSE, TRUE)
   .TRISTATE_WIDTH(1)
)
OSERDESE2_M (      
   .CLK(clk_5x), // 1-bit input: High speed clock
   .CLKDIV(clk), // 1-bit input: Divided clock
   .OQ(out),

   // D1 - D8: 1-bit (each) input: Parallel data inputs (1-bit each)
   .D1(d[0]),
   .D2(d[1]),
   .D3(d[2]),
   .D4(d[3]),
   .D5(d[4]),
   .D6(d[5]),
   .D7(d[6]),
   .D8(d[7]),

   .RST(reset), // 1-bit input: Reset

   .SHIFTIN1(shift1),
   .SHIFTIN2(shift2),

   // T1 - T4: 1-bit (each) input: Parallel 3-state inputs
   .T1(0),
   .T2(0),
   .T3(0),
   .T4(0),
   .TBYTEIN(0), // 1-bit input: Byte group tristate
   .TCE(0) // 1-bit input: 3-state clock enable
);

OSERDESE2 #(
   .DATA_RATE_OQ("DDR"), // DDR, SDR
   .DATA_RATE_TQ("SDR"), // DDR, BUF, SDR
   .DATA_WIDTH(10), // Parallel data width (2-8,10,14)
   .SERDES_MODE("SLAVE"), // MASTER, SLAVE
   .TBYTE_CTL("FALSE"), // Enable tristate byte operation (FALSE, TRUE)
   .TBYTE_SRC("FALSE"), // Tristate byte source (FALSE, TRUE)
   .TRISTATE_WIDTH(1)
)
OSERDESE2_S (      
   .CLK(clk_5x), // 1-bit input: High speed clock
   .CLKDIV(clk), // 1-bit input: Divided clock

   // D1 - D8: 1-bit (each) input: Parallel data inputs (1-bit each)
   .D1(0),
   .D2(0),
   .D3(d[8]),
   .D4(d[9]),
   .D5(0),
   .D6(0),
   .D7(0),
   .D8(0),

   .RST(reset), // 1-bit input: Reset

   .SHIFTOUT1(shift1),
   .SHIFTOUT2(shift2),

   // T1 - T4: 1-bit (each) input: Parallel 3-state inputs
   .T1(0),
   .T2(0),
   .T3(0),
   .T4(0),
   .TBYTEIN(0), // 1-bit input: Byte group tristate
   .TCE(0) // 1-bit input: 3-state clock enable
);

最后再用OBUFDS单端转差分.,时钟则是硬编码的.

OBUFDS #(
  .IOSTANDARD("TMDS_33")
) tmds_diff_1 (
  .O(tmds_data_p[1]),     // Diff_p output (connect directly to top-level port)
  .OB(tmds_data_n[1]),   // Diff_n output (connect directly to top-level port)
  .I(tmds_data[1])      // Buffer input
);

看起来完成了,是不是还漏了什么,对的,color还没填充.

回到HS/VS生成的模块里,我们知道H_ACTIVE一旦开始,坐标就是0,如果我们同发送数据,则可以直接减去SYNC和Back Porch就是坐标,否则可能还需要FIFO/打拍或者其他手段,毕竟开发本就不可能完全一样.都应该根据实际选择.

always @ (posedge clk)
begin
    if(h_active)
        active_x <= h_cnt - H_SYNC - H_BP;
    else
        active_x <= 0;
end

always @ (posedge clk)
begin
    if(v_active)
        active_y <= v_cnt - V_SYNC - V_BP; 
    else
        active_y <= active_y;
end

至于testbench和下板测试,应该不用写了吧.

2ff829807d38c622ca2b19e07cde791-1024x576.jpg
c178ea0d9d275914a2b70b1f640deac-1024x576.jpg

https://gist.github.com/nickfox-taterli/f8d622b43cc97b7e37ae137f05e9a203

整体RTL.

image-15-1024x351.png
image-16-1024x613.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK