TMDS是最小化差分传输的简称,实际上就是一种编码规则,主要是适用于HDMI接口、DVI接口的视频图像编码。TMDS编码规则是将8比特的像素数据转换成10比特数据,这10比特数据的前8比特是由原始8位像素数据通过异或运算或者同或运算得到,如果前8比特采用同或运算得到,那么第9比特为0,如果前8比特数据是由原始8比特像素数据通过异或运算得到,那么第9比特为1。
第10比特是直流平衡位,当转换后的10比特数据中0比较多,那么比特10位为1。如果转化后的数据中1比较多,那么比特10位为0,达到直流平衡的作用。
为什么要做直流平衡?因为数字信号在传输过程中,尤其是像HDMI、 DVI 这些传输速度比较快的高速信号,电路会采用交流耦合方式进行连接,在交流耦合电路中的信号线会接电容,电容具有隔直通交的作用,如果传输的数据在一段时间内全是1或者全是0,那么这段时间传输的信号可以等效成直流信号,会产生直流偏移,在通过电容时,有可能解码错误。为避免这种情况,一般会在第10比特去做直流平衡,如果前面传输的数据全是1,比特10位补个0,相当于电平翻转一次。如果后续0比较多,比特10位补个1,又让电平翻转一次,不至于直流偏移量比较大。
TMDS编码除了把8比特像素数据转换成10比特数据有一个直流平衡外,还有另外一个作用。在TMDS编码处理后,每10比特数据的上冲(上升沿)和下冲(下降沿)一般是限制在5次以内。
具体如何编码?TMDS的编码框图,框图左边有B、G、R三个通道,这三个通道对应24比特的像素数据,分别为8比特蓝色像素、8比特绿色像素、8比特红色像素。
本文只讲解视频图像的TMDS编码,在视频图像编码中,除了要对B、G、R这三个通道的8比特像素数据进行编码,还需要对行、场同步信号进行编码,行、场同步是接到蓝色数据通道的D[1:0]的,行、场同步信号的编码不会经过一系列的异或、同或等逻辑运算,而是直接采用这个查找表的形式去实现。
每个数据传输通道还接了一个DE信号,DE信号就是像素数据使能信号。当DE信号拉高时,对每个通道的8比特像素数据进行编码,当DE信号拉低时,在蓝色通道中去对行、场同步信号进行编码,而绿色通道和红色通道一般接控制信号,但在本文没有使用到控制信号,所以绿色通道和红色通道的D[1:0]都给0。
由于使用FPGA进行HDMI传输时,大多时候不会传输音频信号,那么此时相当于使用的是DVI接口的协议,DVI和HDMI对视频信号都是采用TMDS编码,区别在与DVI不能传输音频信号,所以本文其实是使用DVI的编码,然后使用HDMI的硬件接口实现HDMI视频信号传输。
DVI编码器在视频有效数据段输出像素数据,在消隐期输出控制数据,如图2所示。其中 VDE(Video Data Enable)为高电平时表示视频数据有效,为低电平代表当前处于视频消隐期。
图3给出了三个通道的DVI编码器示意图,与图1左边编码器部分大致相同,只是没有音频传输部分,以及把VDE信号展示出来了。原理与前面的HDMI编码一致,就不再赘述,后续代码将使用DVI的编码器实现TMDS算法。
编码规则原理讲解会涉及到一些信号、参数,如下表1所示。首先D是需要进行编码的8比特原始像素数据,蓝色通道对应的是蓝色通道的8比特数据,在另外两个颜色通道中,对应的就是另外两个颜色通道的8比特数据。而C0、C1是两个控制信号,在蓝色通道中C0、C1是行、场同步信号。DE是像素有效使能信号。
信号名 | 含义 |
---|---|
D | D:输入视频像素信号 |
C1 C0 | C1,C0:控制信号 |
DE | DE:使能信号 |
Cnt | Cnt寄存上次编码过程中0的个数减1的个数 |
N1{X} | 输入视频像素信号中1的个数 |
N0{X} | 输入视频像素信号中0的个数 |
q_out | 编码输出信号 |
cnt用来存储上一次编码过程中1的个数比0的个数多多少,在编码时会对每个8比特像素数据都进行编码,但在这8比特数据当中,可能第一个8比特数据全是1,第二个8比特数据当中4个1,4个0等等。为了让整个数据流能够达到直流平衡,即0和1的个数差不多,并且减少上冲和下冲。cnt就是用来寄存上一次编码当中1的个数比0的个数多多少。如果在上一次编码的10比特数据当中,如果0的个数太多了,那么在当前编码中将0的个数给它取反,适当增加1的个数。
N1{X}、N0{X}是指待编码的视频像素数据中1的个数、0的个数,q_out就是最终TMDS编码后的10比特数据,给其他模块进行使用,下面看一下整个 TMDS 的编码过程。
TMDS编码过程如图4所示,以红色通道为例,DE是红色像素有效数据信号,D[0:7]是红色通道像素数据信号,C0、C1是两个控制信号。cnt(t-1)表示上一次编码结果中1的个数减去0的个数,是有符号数。
首先要判断输入的像素数据1的个数是否大于4或者输入像素等于4且最低位为0,即(N1{D}>4)|| (N1{D}==4 && D[0]==0)为真,则执行右边的运算,如果上述条件为假,则执行左边的运算。q_m只是一个临时寄存器,用来寄存中间数据。右边的运算是对输入的像素数据进行同或运算(将输入数据最低位寄存在q_m最低位,然后q_m的低位与输入数据的下一位数据同或得到q_m当前位数据),并且第9比特q_m[8]赋值为0,左边的运算是对输入的像素数据进行异或运算,并且第9比特q_m[8]赋值为1。第9位就是用来表示TMDS对输入数据采用异或还是同或运算的,由此也应证了前文的讲解。
求取q_m的9比特数据出来之后,如图5所示,接下来判断DE信号,如果DE为低电平,就是对C0、C1控制信号进行编码,这个编码很简单,只需要通过查找表去实现就行了。C1、C0为2’b11时,编码结果q_out[9:0]=10’b1010101011,要注意下图中输出数据的写法是q_out[0:9],与FPGA常用写法是相反的,C0、C1其余取值类似,以下图为准。
并且是需要把cnt(t)赋值为0的,因为从图5中C0、C1编码结果可知q_out输出数据中1的个数和0的个数是相等的,控制信号一般会存在一行或者一帧数据的结尾,在对这种信号进行编码时,也表示一行或一帧数据传输结束,此时需要把cnt(t)清零,方便用于下一行数据的计数。
如果DE为高电平,则对像素数据继续编码,如图6所示,首先判断上一次编码输出的10比特数据中1的个数与0的个数是否相等(通过后面可知cnt(t-1)是一行已经编码数据中1的总数比0的总数多多少,此处也就是1行已经编码数据中1的个数与0的个数是否相等,为啥是一行?因为对控制信号进行编码时,会将cnt(t)清零,熟悉图像数据传输的同学都知道控制信号只出现在一行数据的开头与结尾。)或者本次编码数据中0的个数是否等于1的个数,即(cnt{t-1}==0) || (N1{q_m[7:0]}== N0{q_m[7:0]}) 为真。q_m[8]取反得到编码输出的第10比特q_out[9],编码输出的第8位根据编码的方式赋值,如果是异或运算(q_m[8]为1),则将q_m[7:0]作为编码结果的低8位数据,如果是同或运算,则将q_m[7:0]取反作为编码结果的低8位数据,第9比特是编码方式,直接把q_m[8]输出即可,从而得到编码的10位数据。
然后还要计算本次编码输出的10位数据中1的个数比0的个数多多少,并与cnt(t-1)相加。编码结果的第9位与第10位是相反数据,则0和1的个数取决于低8位数据。如果采用同或编码方式(q_m[8]==0),则q_m前8比特数据的0多余1,而编码结果q_out[7:0]=~ q_m[7:0],那么q_m中0的个数减去1的个数就表示q_out中1的个数减去0的个数,即N0{q_m[0:7]}- N1{q_m[0:7]}表示本次编码输出数据q_out中1的个数比0的个数多多少。同理如果采用异或编码方式(q_m[8]==1),N1{q_m[0:7]}- N0{q_m[0:7]}也表示本次编码输出数据q_out中1的个数比0的个数多多少。
由此也可得到cnt(t-1)其实就是表示一行已经编码数据中1的个数比0的个数多多少,而不是最开始定义的上一次编码数据中1的个数比0的个数多多少。
如果(cnt{t-1}==0) || (N1{q_m[7:0]}== N0{q_m[7:0]}) 为假,代表之前传输的数据1的个数与0的个数不等并且本次编码数据1的个数与0的个数也不等。如图6执行左边fase部分,当一行已编码数据中1的个数多于0的个数并且本次编码q_m中1的个数较多 或者 一行以编码数据中0的个数多余1的个数并且本次编码q_m中0的个数较多时,即(cnt(t-1)>0 && N1{q_m[0:7]}>N0{q_m [0:7]}) || (cnt(t-1)<0 && N0{q_m[0:7]}>N1{q_m[0:7]})。如果此条件为真,表示1比较多,因为编码结果q_out会将q_m[7:0]取反输出,故编码输出数据的0会比较多,此时将输出数据的第10位置为1。
为什么此处要加2*q_m[8]?
本质是没有理解cnt的含义,cnt是指一行已编码像素数据q_out中1的个数比0的个数多多少。N1{q_m[0:7]}和N0{q_m[0:7]}都只对编码输出10位数据中的低8位数据q_out[7:0]中的1和0个数做了统计,那q_out[8]和q_out[9]的0和1呢?
q_out[9]为1,如果q_out[8]为0时,q_out[9:8]=2’b10,此时cnt就不需要额外计算,因为0和1均为1个。如果q_out[8]也为1,即q_out[9:8]=2’b11,那cnt计算时就应该加2,即cnt应该加2q_out[8]。因为q_out[8]= q_m[8],所以cnt计算时会加2q_m[8]。后续出现2*q_m[8]相关的理解思路一致。
如果上述判断条件为假,则执行左边,基本与右边相反,不再过多赘述。
上述就是TMDS编码的流程,实现的话还是比较简单的,思维复杂一点的在于第10位直流均衡的数据位判断以及一行数据已编码数据中1的个数比0的个数多多少的统计上。
前文讲解了TMDS编码的过程,下文就用Verilog HDL实现TMDS算法并进行仿真。
首先时模块的端口信号,以及控制信号的查找表编码常量,如图7所示,由于FPGA打算使用xilinx的芯片,故使用同步高电平复位,din是待编码信号,c0和c1是控制信号,de是din的有效指示信号,高电平有效,q_out是编码的输出信号。
如图8所示,当使能信号de有效的时候,去统计输入信号的1的个数,使能信号无效时,此时编码的是控制信号,不对输入数据进行统计,故清零。采用移位寄存器暂存一下输入信号,让输入信号与后续的信号数据对齐。
然后判断输入数据中1的个数是否多余4或者等于4且最低位为0,然后决定后续通过异或还是同或都是逻辑运算生成q_m信号,如图9所示。q_m的最低位与输入信号最低位相同,而第8位的值与判断的条件对应的数值刚好相反,其余7位需要根据同或、异或运算得到。
得到q_m信号,需要注意q_m信号与din_r信号对齐,而din_r是延迟de信号一个时钟与de_r[0]对齐的,所以q_m与de_r[0]对齐。然后就需要统计q_m中1的个数和0的个数,结果与de_r[1]对齐,代码如图10所示。之后需要生成图6中两个判断条件。
最后就是通过判断条件去生产编码输出信号q_out和统计已经编码的数据中1的个数比0的个数多多少个,即cnt计数器的值。注意cnt是当成有符号数进行处理的,在Verilog HDL信号定义时写不写signed没啥差别,FPGA能够得知的就是几根信号线而已,至于有符号还是无符号都只是跟用户的使用有关。比如3’b111这个数据,用户认为他是无符号数,那他的值就是7,如果用户认为这是有符号数,那他的值就是-1,仅此而已。
其中{q_m_r[8],1’b0}与2*q_m_r[8]是相同的,左移一位相等于乘以2。主要以此处的信号对齐,n1q_m和n0q_m与de_r[1]对齐,所以其余的信号都必须与de_r[1]进行对齐,这就是为什么使用的是q_m_r而不是q_m信号,c1_r[1]和c0_r[1]而不是c0和c1信号的原因。
完整代码如下所示:
module dvi_tmds(
input clk ,//系统时钟信号;
input rst ,//系统复位信号,高电平有效;
input [7 : 0] din ,//输入待编码数据
input c0 ,//控制信号C0
input c1 ,//控制信号c1
input de ,//输入数据有效指示信号;
output reg [9 : 0] q_out //编码输出数据
);
localparam CTRLTOKEN0 = 10'b1101010100 ;
localparam CTRLTOKEN1 = 10'b0010101011 ;
localparam CTRLTOKEN2 = 10'b0101010100 ;
localparam CTRLTOKEN3 = 10'b1010101011 ;
reg [7 : 0] din_r ;//
reg [1 : 0] de_r,c0_r,c1_r ;
reg [3 : 0] n1d,n1q_m,n0q_m ;
reg [5 : 0] cnt ;
reg [8 : 0] q_m_r ;
wire [8 : 0] q_m ;//
wire condition1 ;
wire condition2 ;
wire condition3 ;
//统计待编码输入数据中1的个数,最多8个1,所以位宽为4。
always@(posedge clk)begin
if(rst)begin//初始值为0;
n1d <= 4'd0;
end
else if(de)begin//当DE为高电平,统计输入数据中1的个数。
n1d <= din[0] + din[1] + din[2] + din[3] + din[4] + din[5] + din[6] + din[7];
end
else begin//当DE为低电平时,对控制信号编码,此时不需要统计输入信号中1的个数,故清零。
n1d <= 4'd0;
end
end
//移位寄存器将输入数据暂存,与后续信号对齐。
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
//判断条件1,输入数据1的个数多余4或者1的个数等于4并且最低位为0时拉高,其余时间拉低。
assign condition1 = ((n1d > 4'd4) || ((n1d == 4'd4) && (~din_r[0])));
//对输入的信号进行异或运算。
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;
always@(posedge clk)begin
if(rst)begin//初始值为0;
n1q_m <= 4'd0;
n0q_m <= 4'd0;
end
else if(de_r[0])begin//对输入有效数据时,q_m中1和0的个数进行统计;
n1q_m <= q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7];
n0q_m <= 4'd8 - (q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7]);
end
else begin//输入数据无效时清零。
n1q_m <= 4'd0;
n0q_m <= 4'd0;
end
end
//判断条件2,一行已编码数据中1的个数等于0的个数或者本次编码数据中1的个数等于0的个数。
assign condition2 = ((cnt == 6'd0) || (n1q_m == n0q_m));
//判断条件3,已编码数据中1的多余0并且本次编码中间数据1的个数也多与0的个数或者已编码数据中0的个数较多并且此次编码中0的个数也比较多时拉高,其余时间拉低。
assign condition3 = (((~cnt[5]) && (n1q_m > n0q_m)) || (cnt[5] && (n1q_m < n0q_m)));
always@(posedge clk)begin
if(rst)begin//初始值为0;
cnt <= 6'd0;
q_out <= 10'd0;
end
else if(de_r[1])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
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
endmodule
为了便于对该模块仿真,此处提供仿真激励参考代码,由于本人仿真时并没有创建工程,直接使用Vscode调用modelsim进行的仿真,所以不存在工程文件,这里也提供不了工程文件,将这两个文件加入modelsim直接仿真即可。
`timescale 1 ns/1 ns
module test();
parameter CYCLE = 10 ;//系统时钟周期,单位ns,默认20ns;
parameter RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;
reg clk ;//系统时钟,默认50MHz;
reg rst ;//系统复位,默认高电平有效;
reg [7 : 0] din ;
reg c0 ;
reg c1 ;
reg de ;
wire [9 : 0] q_out;
dvi_tmds u_dvi_tmds (
.clk ( clk ),
.rst ( rst ),
.din ( din ),
.c0 ( c0 ),
.c1 ( c1 ),
.de ( de ),
.q_out ( q_out )
);
//生成周期为CYCLE数值的系统时钟;
initial begin
clk = 0;
forever #(CYCLE/2) clk = ~clk;
end
//生成复位信号;
initial begin
rst = 0;din = 0;
c0 = 0; c1 = 0;de = 0;
#2;
rst = 1;//开始时复位10个时钟;
#(RST_TIME*CYCLE);
rst = 0;
repeat(10) @(posedge clk);//延迟10个时钟;
repeat(15)begin//发送数据之前,生产15个随机的控制信号,看是否正确。
{c1,c0} <= {$random} % 4;
@(posedge clk);
end
de = 1'b1;//de信号拉高;
repeat(256)begin//发送256个随机8位数据进行编码,模拟一行像素数据;
din = {$random} % 256;
@(posedge clk);
end
de = 1'b0;
repeat(20)begin//数据发送完成之后,生产20个随机的控制信号,看是否正确。
{c1,c0} <= {$random} % 4;
@(posedge clk);
end
repeat(4) @(posedge clk);
$stop;//停止仿真;
end
endmodule
运行仿真程序的结果如下所示:
该仿真时利用的一些随机数据进行仿真,de信号拉高之前的一段时间内会发送一些随机控制信号,检测控制信号编码是否存在问题,如图13所示,在de信号为低电平时,与数据编码相关的信号都没有变化,都是保持最初的值,而编码结果直接根据控制信号c0、c1生成,对比控制信号的值,可知对控制信号的编码正确,注意是对c1_r[1]、c0_r[1]的值编码。
当de信号拉高时,对齐的数据是8’hc6,其中1的个数是4,所以n1d赋值为4,与de_r[0]对齐,此时第一个条件为真,对输入的数据进行同或运算,然后得到q_m的值为9’h0e8。统计出q_m中1的个数(只统计第8位数据中1的个数)为4,0的个数也为4,然后图6中条件1满足,即condition2为高电平,编码结果的第9位等于q_m_r的第8位取反,低8位数据取反,得到10’b1000010111=10‘h217,与图14结果一致。由于q_out中1与0数目相等,所以cnt也为0,第一个数据编码正确,后续编码也可以逐个推演,都能得到正确答案。
至此,TMDS算法的原理及Verilog HDL实现都已经讲解完毕,相对比较简单,至于为什么没有上板验证,TMDS算法只是存在HDMI接口信号传输的第一部分,即将8位像素数据编码为10位数据,后续还要将10位并行数据转换为串行数据,然后通过在FPGA内部进行单端转差分信号,最后通过差分引脚才能将信号输出到HDMI接口,后续部分分别讲解并串转换和单端转差分,最后将几部分合起来进行上板验证。
HDMI接口协议和DVI协议以及上一篇文章中用到的手册均放在百度网盘,通过在公众号后台回复”HDMI协议”(不包含引号),即可获取。