15.4 实验原理
15.4.1 HDMI接口介绍
HDMI(High-Definition Multimedia Interface)是一种高清晰度多媒体接口,用于在各种电子设备之间传输高质量的音频和视频信号。HDMI接口常用于连接电视、显示器、投影仪、音频设备、电脑等各种消费电子设备。在HDMI接口出现之前广泛应用的视频接口有VGA接口和DVI接口。其中VGA是一种模拟信号传输接口,早期的模拟CRT显示器大多使用VGA接口,如今LCD液晶显示器也有大量使用。

而DVI接口是一种数字视频传输接口,和HDMI接口不同的是DVI接口不支持音频信号,所以HDMI接口是向下兼容DVI接口,体 积更小,速度更快。

HDMI接口和DVI接口在数据发送层是可以兼容的,在我们实验中使用了HDMI接口,实际上也可以算是DVI,实验中没有加入音频信号,速度也没有达到HDMI规范的最高速度。
15.4.2.HDMI接口原理
HDMI接口规范中规定数据传输链路中的数据编码方式采用TMDS编码,其包括了输入接口、TMDS发送、TMDS接收和输出接口。

- 输入接口层:典型的RGB行场同步数字信号,包括使能信号(DE)、24位像素数据、6位控制数据信号(包括行同步、场同步及空信号)以及时钟信号;
- TMDS发送器:完成数据TMDS编码并且高速串行发出,包括4个TMDS链路层;
- TMDS接收器:接收高速串行数据并且完成解码;
- 输出接口层:输出到最终显示屏;
FPGA要驱动HDMI接口显示其实就是要产生TMDS的高速串行信号。TMDS编码是对数据和控制信号进行编解码,链路传输采用差分传动方式。

1)每一个标准的HDMI连接,都包含了3个用于传输数据的TMDS传输通道,还有1个独立的TMDS时钟通道,以保证传输 时所需的统一时序。
2)在一个时钟周期内,每个TMDS通道都能传送10bit的数流。而这10bit数据,可以由若干种不同的编码格式构成。一个HDMI包括3个TMDS数据通道和1个TMDS时钟通道。
3)每个数据通道都通过编码算法(异或、异或非等),将8位数据转换成10位数据,前8位数据由原始信号经运算后获得,第9位指示运算的方式,第10位用来对应直流平衡。通过这种算法,会使得数据的传输和恢复更加可靠。
15.4.3 TMDS编码实现
实现TMDS编码传输我们遵循两个原则,
1)直流平衡编码:根据前一个编码过程中统计的1和0的个数差值来指导本次编码过程的1和0的差值,以保证在整个传输过程编码之后的数据流要保持1和0的数量总体上相差不大。
2)最小化传输:数据从1到0或者从0到1称为一次跳变,最小化传输就是对数据以某种方式进行重新编码以确保编码后的数据跳变次数尽量小。信号传输过程中跳变次数越少,产生越小的电磁干扰。

TMDS编码流程图
1)D、C0、C1、DE:编码器输入信号,D是8位输入数据;C0、C1为控制信号;DE是数据有效信号;
2)cnt:寄存器用来记录数据流极性。一个大于0的正值表示数据流中有多少个1,一个小于0的负值表示数据流中有多少个0;cnt(t-1)表示上一次编码中的极性,cnt(t)表示当前编码的极性;
3)q_m:对输入数据D进行最小化传输的编码结果,9位;
4)q_out:对q_m进行直流平衡编码的结果,10位;
5)N1(X) ,N0(X):可以看作一个函数,对数据X统计1或0的个数;
整个编码过程首先统计8位输入数据中有多少个1,个数大于4超过一半,采用同或(XNOR)编码,否则采用异或(XOR)编码;同时判断数据中1个数大于4的逻辑结果作为编码后数据的第九位的值,也就是完成了最小传输编码。
接着根据上一次编码数据极性cnt(t-1)和本次传输数据中极性统计来进行条件判断,确定第10位数据的编码值,完成直流平衡编码。同时输出本次传输编码的极性cnt(t)。
15.4.4 HDMI驱动设计
在本实验中我们传输图像为640x480@60Hz,那么一帧共307200个像素点,每个像素由24位(分别8位RGB)组成,刷新频率为60Hz,计算下来相当于视频传输速度需要为每秒0.44Gbit。

实际传输过程中,图像包括一些消隐区域,例如640x480分辨率图像实际传输一帧是包括800x525个像素点。要使得刷新频率为60Hz,那么HDMI传输的像素频率约为25MHz(Pixel clock)。


上图是常见的视频时序参数,根据这些参数我们可以产生图像的 行同步和场同步信号:
reg [9:0] CounterX, CounterY;
reg hSync, vSync, DrawArea;
always @(posedge pixclk) DrawArea <= (CounterX<X) && (CounterY<Y);
always @(posedge pixclk) CounterX <= (CounterX == OFFX-1) ? 0 : CounterX+1'b1;
always @(posedge pixclk) if(CounterX==OFFX-1) CounterY <= (CounterY==OFFY-1) ? 0 : CounterY+1'b1;
always @(posedge pixclk) hSync <= (CounterX>=656) && (CounterX<752);
always @(posedge pixclk) vSync <= (CounterY>=490) && (CounterY<492);
确定图标的显示区域,在此区域内则读取rom中对应位置的图像内容
reg pic_y_flag,pic_x_flag;
//
always@(posedge clk or negedge rst_n)begin
if (!rst_n) begin
pic_x_flag <= 1'b1;
end
else if (CounterX == x_set) begin
pic_x_flag <= 1'b1;
end
else if (CounterX == x_set + P_WIDTH) begin
pic_x_flag <= 1'b0;
end
else begin
pic_x_flag <= pic_x_flag;
end
end
always@(posedge clk or negedge rst_n)begin
if (!rst_n) begin
pic_y_flag <= 1'b1;
end
else if (CounterY == y_set) begin
pic_y_flag <= 1'b1;
end
else if (CounterY == y_set + P_HIGH)begin
pic_y_flag <= 1'b0;
end
else begin
pic_y_flag <= pic_y_flag;
end
end
确定图标在显示器上的起始坐标
reg [9:0] x_set;
reg [9:0] y_set;
always@(posedge clk or negedge rst_n)begin
if (!rst_n) begin
x_set <= 'd0;
end
else if ((shift_flag)&&(CounterX == HSYNC_B + 1)&&(CounterY == VSYNC_P)) begin
x_set <= (x_shift_flag)?(x_set - 1):(x_set + 1);
end
else begin
x_set <= x_set;
end
end
always@(posedge clk or negedge rst_n)
begin
if(!rst_n) begin
y_set <= 'd0;
end
else if((shift_flag)&&(CounterX == HSYNC_B + 1)&&(CounterY == VSYNC_P)) begin
y_set <= (y_shift_flag)?(y_set -1):(y_set + 1);
end
else begin
y_set <= y_set;
end
end
在每一帧图像之间改变图标的起始位置,实现视频效果。
reg x_shift_flag;
reg y_shift_flag;
reg cnt;
reg shift_flag;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt <= 'd0;
shift_flag <= 1'b0;
end
else if((CounterY == VSYNC_P)&&(CounterX ==HSYNC_B)) begin
cnt <= cnt + 1;
shift_flag <= 1'b1;
end
else begin
cnt <= cnt;
shift_flag <= 1'b0;
end
end
always@(posedge clk or negedge rst_n)
begin
if (!rst_n) begin
x_shift_flag <= 1'b0;
end
else if (x_set == 'b0) begin
x_shift_flag <= 1'b0;
end
else if (x_set == HSYNC_A - P_WIDTH + 7) begin
x_shift_flag <= 1'b1;
end
else begin
x_shift_flag <= x_shift_flag;
end
end
always@(posedge clk or negedge rst_n)
begin
if(!rst_n) begin
y_shift_flag <= 1'b0;
end
else if(y_set == 'd0) begin
y_shift_flag <= 1'b0;
end
else if (y_set == VSYNC_O - P_HIGH + 7) begin
y_shift_flag <= 1'b1;
end
end
输出图像的RGB信号
wire [6:0] the_pixel;
assign the_pixel = ((pic_x_flag)&&(pic_y_flag))?(CounterX-x_set+3):10'd0;
assign rom_addr = ((pic_x_flag)&&(pic_y_flag))?(CounterY - y_set):7'b0;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
TMDS_red <= 8'h00;
TMDS_green <= 8'h00;
TMDS_blue <= 8'h00;
end
if ((pic_x_flag)&&(pic_y_flag)) begin
TMDS_red <= (rom_data[the_pixel])?8'hff:8'h00;
TMDS_green <= (rom_data[the_pixel])?8'h99:8'h00;
TMDS_blue <= (rom_data[the_pixel])?8'h10:8'h00;
end
else begin
TMDS_red <= 8'h00;
TMDS_green <= 8'h00;
TMDS_blue <= 8'h00;
end
end
使用到的参数
parameter HSYNC_A = 'd640;
parameter HSYNC_B = 'd656;
parameter HSYNC_C = 'd752;
parameter HSYNC_D = 'd800;
parameter VSYNC_O = 'd480;
parameter VSYNC_P = 'd490;
parameter VSYNC_Q = 'd492;
parameter VSYNC_R = 'd525;
parameter P_WIDTH = 'd128;
parameter P_HIGH = 'd128;
根据TMDS的编码流程图实现了8位数据的TMDS编码,输出10位编码数据:
module TMDS_encoder(
input clk,
input [7:0] VD, // video data (red, green or blue)
input [1:0] CD, // control data
input VDE, // video data enable, to choose between CD (when VDE=0) and VD (when VDE=1)
output reg [9:0] TMDS = 0
);
wire [3:0] Nb1s = VD[0] + VD[1] + VD[2] + VD[3] + VD[4] + VD[5] + VD[6] + VD[7];
wire XNOR = (Nb1s>4'd4) || (Nb1s==4'd4 && VD[0]==1'b0);
wire [8:0] q_m = {~XNOR, q_m[6:0] ^ VD[7:1] ^ {7{XNOR}}, VD[0]};
reg [3:0] balance_acc = 0;
wire [3:0] balance = q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7] - 4'd4;
wire balance_sign_eq = (balance[3] == balance_acc[3]);
wire invert_q_m = (balance==0 || balance_acc==0) ? ~q_m[8] : balance_sign_eq;
wire [3:0] balance_acc_inc = balance - ({q_m[8] ^ ~balance_sign_eq} & ~(balance==0 || balance_acc==0));
wire [3:0] balance_acc_new = invert_q_m ? balance_acc-balance_acc_inc : balance_acc+balance_acc_inc;
wire [9:0] TMDS_data = {invert_q_m, q_m[8], q_m[7:0] ^ {8{invert_q_m}}};
wire [9:0] TMDS_code = CD[1] ? (CD[0] ? 10'b1010101011 : 10'b0101010100) : (CD[0] ? 10'b0010101011 : 10'b1101010100);
always @(posedge clk) TMDS <= VDE ? TMDS_data : TMDS_code;
always @(posedge clk) balance_acc <= VDE ? balance_acc_new : 4'h0;
endmodule
这里pixel clock是25MHz,每次传输位数是10位,所以TMDS编码的每一位传输时钟我们倍频到250MHz,然后通过并转串把TMDS编码传输出去:
reg [3:0] TMDS_mod10=0; // modulus 10 counter
reg [9:0] TMDS_shift_red=0, TMDS_shift_green=0, TMDS_shift_blue=0;
reg TMDS_shift_load=0;
always @(posedge clk_TMDS) TMDS_shift_load <= (TMDS_mod10==4'd9);
always @(posedge clk_TMDS)
begin
TMDS_shift_red <= TMDS_shift_load ? TMDS_red : TMDS_shift_red [9:1];
TMDS_shift_green <= TMDS_shift_load ? TMDS_green : TMDS_shift_green[9:1];
TMDS_shift_blue <= TMDS_shift_load ? TMDS_blue : TMDS_shift_blue [9:1];
TMDS_mod10 <= (TMDS_mod10==4'd9) ? 4'd0 : TMDS_mod10+4'd1;
end
assign TMDSp[0] = TMDS_shift_red[0];
assign TMDSn[0] = ~TMDS_shift_red[0];
assign TMDSp[1] = TMDS_shift_green[0];
assign TMDSn[1] = ~TMDS_shift_green[0];
assign TMDSp[2] = TMDS_shift_blue[0];
assign TMDSn[2] = ~TMDS_shift_blue[0];
assign TMDSp_clock = pixclk;
assign TMDSn_clock = ~pixclk;
总结,例化pll IP核得到25MHz的pixel clock和250MHz的数据位传输时钟。RGB信号通过一些简单计数器实现,通过TMDS编码得到10位的编码数据,然后转换成串行数据发送出去。