其实一直也没怎么讲这个,网上各种教程都是串口收发等等,要不就是特别高级特别复杂的项目,什么以太网USB统统用上,其实我觉得驱动HDMI是一个比较简单,但是又比较需要整体考量的东西.
HDMI显示,本质上也是编码RGB显示屏数据,最后发送,不管是RGB时序生成,还是编码算法其实都非常简单,大家写个串口收发那种功力,就能把这个全部实现好,无非是东西比较多,由多个东西组合而成罢了.如果你使用像高云这样的平台,他们HDMI是封装成IP的,那就更加没东西可以讨论了,等于根本不用你自己写.
一开始我们要了解RGB驱动时序,毕竟笼统地说RGB驱动时序通过串并转换后就是HDMI时序嘛,我们不要太过复杂去说几种模式,就单纯说一下HDMI编码中用到的DE模式模式.
一帧时序.
一行时序.
我们需要定义计数器的起始位置,当然这都是我们开发时候可以自己定的,上面的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设计,而且时钟这么高,也不太好收敛.)
接下来进行比较难的HDMI编码部分,但是说比较难也并不算难,自己按着步骤写也很快的,毕竟也就没几步,看着是个流程图,像对FPGA这种并行来说不太友好,实际上只要打拍的时机合适,也是非常简单的,仅需三个时钟就能完成.
明白几个概念,就很简单了,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和下板测试,应该不用写了吧.
源码
https://gist.github.com/nickfox-taterli/f8d622b43cc97b7e37ae137f05e9a203
整体RTL.