HDMI显示器驱动设计与验证
HDMI简介
HDMI,英文全称“High Definition Multimedia Interface”,高清多媒体接口
VGA接口成本低,结构简单,应用灵活,但是传输的是模拟信号,就容易受到外界干扰源的影响,受到影响会导致信号畸变,传输的图像就会出现问题,VGA接口体积大,不利于便携设备的使用。后来出现了DVI接口,DVIA(只传输模拟信号)、DVID(只传输数字信号)、DVII(兼容数字信号和模拟信号),数字信号比较不容易受到干扰,体积比VGA还大,只能传输图像,不能传输音频信号。后来出现了HDMI接口,体积小,抗干扰能力更强,可以实现音频和数字信号的同步传输,兼容性好
引脚 | 定义 | 引脚 | 定义 |
---|---|---|---|
1 | 数据2+ | 11 | 时钟屏蔽 |
2 | 数据2屏蔽 | 12 | 时钟- |
3 | 数据2- | 13 | CEC |
4 | 数据1+ | 14 | 保留 |
5 | 数据1屏蔽 | 15 | DDC时钟线(SCL) |
6 | 数据1- | 16 | DDC数据线(SDA) |
7 | 数据0+ | 17 | DDC / CEC地 |
8 | 数据0屏蔽 | 18 | +5V电源 |
9 | 数据0- | 19 | 热插拔检测 |
10 | 时钟+ |
按功能可以分为四大类:
- TMDS,是一种传输技术,引脚1~引脚12,用于发送音频、视频和各种辅助数据,遵循DVI编码方式,引脚1和引脚3、引脚4和引脚6、引脚7和引脚9、引脚10和引脚12是四对差分信号
- DDC通道,显示数据通道,引脚15~引脚17,只需要单向获取接收端的信息,也就是显示器,其实就是I2C接口
- CEC通道,引脚13和引脚17
- 其他,保留、电源、热插拔检测
HDMI显示原理
发送端将图像数据、音频数据和控制信号转换成4对差分信号,传给接收方,供接收方解码使用
TMDS传输原理
TMDS,英文全称“Transition Minimized Differential Signaling”,最小化传输差分信号
发送端收到表示RGB信号的24bit并行数据,这些数据在行场同步信号和使能信号下,与时钟信号一起进行编码和并串转换,之后将3个表示RGB的数据和1个时钟信号分别分配到4个独立的通道发送出去
接收端接收到发送端发过来的串行信号,进行解码和串并转换,之后发送到显示器的控制端,在时钟信号的同步下进行显示
信号传输步骤
- 8bit视频、音频数据转换为10bit的最小传输、直流均衡数据,编码
- 并行10bit编码后的数据转换为串行差分信号,并串转换
模块设计
可以使用前面的vga_coloarbar工程,得到rgb、hsync、vsync信号,clk由PPL锁相环得到,DE使能信号可以用从vga_ctrl模块引出的rgb_valid信号代替;经过了vga_pic和vga_ctrl,就可以得到基于TMDS传输原理的hdmi_ctrl模块输入端口信号,R、G、B信号,hsync、vsync行场同步和DE使能信号
hdmi_ctrl模块要完成TMDS传输方式,需要有两组逻辑实现,encode对接收过来的数据进行编码、par_to_ser对编码后的10bit数据进行串行发送,可以给里面安排独立的两个模块逻辑
HDMI的15、16引脚分别是DDC时钟线(SCL)、DDC数据线(SDA),这两路信号连接的是显示器的ROM,采用的是I2C协议,用于读取显示器ROM中的数据,比如生产厂商、型号、支持的分辨率等。这两个接口暂时不使用,强制置位高电平
hdmi_ctrl模块
hdmi_ctrl是采用TDMS原理实现的,需要将输入数据进行扩展编码、双沿发送差分信号,差分信号也就是一组数据和该组数据的反,比如有一组数据3’b010,那么它本身和3’b101就是一组差分信号,分成两个字模块实现,hdmi_ctrl内部就没有逻辑代码,全都是连线
encode模块代码
8bit并行数据转10bit数据转换
module encode (
input wire vga_clk ,
input wire sys_rst_n ,
input wire hsync ,
input wire vsync ,
input wire rgb_valid ,
input wire [ 7: 0] data_in ,
output reg [ 9: 0] data_out
);
wire ctrl_1 ; // 组合逻辑计算D数据1的个数
wire ctrl_2 ;
wire ctrl_3 ;
wire [ 8: 0] q_m ; // 8bit到9bit扩展
reg [ 3: 0] data_in_n1 ;
reg [ 7: 0] data_in_reg ; // 输入数据打拍
reg [ 4: 0] cnt ; // 后面要计算(cnt(t-1) + (N0{q_m[0:7]} - N1{q_m[0:7]})),8+8就需要4bit,再加上一个符号位
reg [ 3: 0] q_m_n1 ;
reg [ 3: 0] q_m_n0 ;
reg [ 8: 0] q_m_reg ;
reg de_reg1 ;
reg de_reg2 ;
reg c0_reg1 ;
reg c0_reg2 ;
reg c1_reg1 ;
reg c1_reg2 ;
always @ (posedge vga_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
data_in_n1 <= 4'd0;
else
data_in_n1 <= data_in[0] + data_in[1] + data_in[2] + data_in[3] +
data_in[4] + data_in[5] + data_in[6] + data_in[7];
always @ (posedge vga_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
data_in_reg <= 8'b0;
else
data_in_reg <= data_in;
assign ctrl_1 = ((data_in_n1 > 4'd4) || ((data_in_n1 == 4'd4) && (data_in_reg[0] == 1'b0)))
? 1'b1 : 1'b0 ;
assign q_m[0] = data_in_reg[0];
assign q_m[1] = (ctrl_1 == 1'b1) ? (q_m[0] ^~ data_in_reg[1]) : (q_m[0] ^ data_in_reg[1]) ;
assign q_m[2] = (ctrl_1 == 1'b1) ? (q_m[1] ^~ data_in_reg[2]) : (q_m[1] ^ data_in_reg[2]) ;
assign q_m[3] = (ctrl_1 == 1'b1) ? (q_m[2] ^~ data_in_reg[3]) : (q_m[2] ^ data_in_reg[3]) ;
assign q_m[4] = (ctrl_1 == 1'b1) ? (q_m[3] ^~ data_in_reg[4]) : (q_m[3] ^ data_in_reg[4]) ;
assign q_m[5] = (ctrl_1 == 1'b1) ? (q_m[4] ^~ data_in_reg[5]) : (q_m[4] ^ data_in_reg[5]) ;
assign q_m[6] = (ctrl_1 == 1'b1) ? (q_m[5] ^~ data_in_reg[6]) : (q_m[5] ^ data_in_reg[6]) ;
assign q_m[7] = (ctrl_1 == 1'b1) ? (q_m[6] ^~ data_in_reg[7]) : (q_m[6] ^ data_in_reg[7]) ;
assign q_m[8] = (ctrl_1 == 1'b1) ? 1'b0 : 1'b1;
always @ (posedge vga_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
begin
q_m_n1 <= 4'd0;
q_m_n0 <= 4'd0;
end
else
begin
q_m_n1 <= q_m[0] + q_m[1] + q_m[2] + q_m[3] +
q_m[4] + q_m[5] + q_m[6] + q_m[7];
q_m_n0 <= 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
// assign ctrl_2 = ((cnt == 5'd0) || (q_m_n1 == q_m_n0)) ? 1'b1 : 1'b0;
// assign ctrl_3 = (((cnt > 5'd0) && (q_m_n1 > q_m_n0))
// || ((cnt < 5'd0) && (q_m_n0 > q_m_n1)))
// ? 1'b1 : 1'b0;
/* cnt是由符号位的,所以不能简单的进行大于小于判断,而是应该判断符号位*/
assign ctrl_2 = ((cnt == 5'd0) || (q_m_n1 == q_m_n0)) ? 1'b1 : 1'b0;
assign ctrl_3 = (((cnt[4] == 1'b0) && (q_m_n1 > q_m_n0))
|| ((cnt[4] == 1'b1) && (q_m_n0 > q_m_n1)))
? 1'b1 : 1'b0;
// 打拍信号统一赋值
always @ (posedge vga_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
begin
q_m_reg <= 9'b0;
de_reg1 <= 1'b0;
de_reg2 <= 1'b0;
c0_reg1 <= 1'b0;
c0_reg2 <= 1'b0;
c1_reg1 <= 1'b0;
c1_reg2 <= 1'b0;
end
else
begin
q_m_reg <= q_m;
de_reg1 <= rgb_valid;
de_reg2 <= de_reg1;
c0_reg1 <= hsync;
c0_reg2 <= c0_reg1;
c1_reg1 <= vsync;
c1_reg2 <= c1_reg1;
end
always @ (posedge vga_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
begin
data_out <= 10'b0;
cnt <= 5'd0;
end
else
begin
if (de_reg2 == 1'b1)
begin
if (ctrl_2 == 1'b1)
begin
data_out[9] <= ~q_m_reg[8];
data_out[8] <= q_m_reg[8];
data_out[ 7: 0] <= (q_m_reg[8] ? q_m_reg[ 7: 0] : ~q_m_reg[ 7: 0]);
cnt <= (q_m_reg[8] == 1'b0) ? (cnt + (q_m_n0 - q_m_n1)) : (cnt + (q_m_n1 - q_m_n0));
end
else
begin
if (ctrl_3 == 1'b1)
begin
data_out[9] <= 1'b1;
data_out[8] <= q_m_reg[8];
data_out[ 7: 0] <= ~q_m_reg[ 7: 0];
cnt <= cnt + {q_m_reg[8], 1'b0} + (q_m_n0 - q_m_n1);
end
else
begin
data_out[9] <= 1'b0;
data_out[8] <= q_m_reg[8];
data_out[ 7: 0] <= q_m_reg[ 7: 0];
cnt <= cnt - {~q_m_reg[8], 1'b0} + (q_m_n1 - q_m_n0);
end
end
end
else
begin
case ({c1_reg2, c0_reg2})
2'b00: data_out <= 10'b00_1010_1011;
2'b01: data_out <= 10'b11_0101_0100;
2'b10: data_out <= 10'b00_1010_1010;
2'b11: data_out <= 10'b11_0101_0101;
default: ;
endcase
cnt <= 5'd0;
end
end
endmodule
par_to_ser模块代码
- 并行转串行
- 单端信号到差分信号的转换
- 单沿到双沿的转换
这个模块分为两部分,
-
第一部分,将并行的10bit数据在25MHz下经过重组、移位,变成串行的10bit数据,再将这10bit数据分成两组5bit信号data_rise、data_fall,方便区分双沿发送,在125MHz下,以移位寄存的方式从低位逐比特发送
-
第二部分,调用ddio模块,在125MHz下,上升沿发送送入的data_rise[0]、下降沿发送送入的data_fall[0]
Quartus有IP核可以调,我用vivado没找着对应的,简单写一下
module ALTERA_ddio_out (
input wire datain_h ,
input wire datain_l ,
input wire outclock ,
output wire data_out
);
// always @ (outclock)
// if (outclock == 1'b1)
// data_out <= datain_h;
// else
// data_out <= datain_l;
assign data_out = (outclock == 1'b1) ? datain_h : datain_l;
endmodule
module par_to_ser (
input wire clk_5x ,
input wire [ 9: 0] data_in ,
output wire ser_p ,
output wire ser_n
);
wire [ 4: 0] data_rise ; // 偶数位上升沿发送
wire [ 4: 0] data_fall ; // 奇数位下降沿发送
reg [ 4: 0] data_rise_s = 0;
reg [ 4: 0] data_fall_s = 0;
reg [ 2: 0] cnt = 0;
assign data_rise = {data_in[8], data_in[6], data_in[4], data_in[2], data_in[0]};
assign data_fall = {data_in[9], data_in[7], data_in[5], data_in[3], data_in[1]};
always @ (posedge clk_5x)
begin
cnt <= (cnt[2] == 1'b1) ? 3'd0 : (cnt + 1'b1);
data_rise_s <= (cnt[2] == 1'b1) ? data_rise : {1'b0, data_rise_s[4:1]};
data_fall_s <= (cnt[2] == 1'b1) ? data_fall : {1'b0, data_fall_s[4:1]};
end
// 时钟取反的好处:源clk_5x上升沿数据传送过来,取反后ddio相当于在源clk_5x的下降沿位置进行数据采样,更稳定
ALTERA_ddio_out ddio_inst_p (
.datain_h (data_rise_s[0] ),
.datain_l (data_fall_s[0] ),
.outclock (~clk_5x ),
.data_out (ser_p )
);
// 对data_in进行取反,两根线传输
ALTERA_ddio_out ddio_inst_n (
.datain_h (~data_rise_s[0] ),
.datain_l (~data_fall_s[0] ),
.outclock (~clk_5x ),
.data_out (ser_n )
);
endmodule
总结
HDMI的实现一般都是复用了VGA的相关模块,主要是编写了hdmi_ctrl