环境:
1、Quartus18.0
2、vscode
3、板子型号:原子哥开拓者2(EP4CE10F17C8)
4、显示屏
要求:
是驱动开拓者开发板上的 HDMI 接口,在显示器上显示彩条图案。HDMI 接口输出的分辨率为 1280*720,刷新速率为 60hz。
1、在开拓者2的HDMI中,我们只是支持输出,把输入限制死了。
2、HDMI_HPD 指的是热拔插检测(Hot Plug Detect): 当视频设备与接收设备通过 HDMI 连接时,接收设备将 HPD 置为高电平,通知发送设备。当发送设备检测到 HPD 为低电平时,表明断开连接。加了一个抑制二极管做电压敏感保护。
3、SCL、SDA使用CMOS管用于电平的转换作用: 用于 HDMI 发送端和接收端之间交换一些配置信息,通过 I2C 协议通信。
1、4、7和3、6、9组成了三对差分信号,用于发送颜色数据。
这里实现了RGB转DVI模块,主要想提一下这里的ddio实现的并转串。这里我们需要一下子将10bit数字转成串行的,这就需要10倍的像素时钟。这里我们只用了一个 5 倍的时钟频率,这是因为 ALTDDIO_OUT IP核可以实现 DDR 的功能,即它在五倍时钟频率的基础上又实现了双倍数据速率。
这里我们选择的数据为为1比特,但是,其有上升沿以及下降沿都进行相应的发送处理。
datain_h: 在 outclock 时钟上升沿输入的数据,输入数据的位宽为WIDTH;
datain_l: 在 outclock 时钟下降沿输入的数据,输入数据的位宽为 WIDTH;
outclock: 用于寄存数据输出的时钟信号,在 outclock 时钟信号的每个电平输出 DDR 数据到 dataout 端口;
dataout: DDR 输出数据端口,位宽为 WIDTH。dataout 端口应该直接提供顶层设计的输出引脚。
观察发现,在一个时钟·周期内,其输出了两个比特位,那么我们只需要5倍的时钟速率即可。
在行&场同步信号有效信号过来时我们的DE(像素有效信号)还不能立刻拉高,我们需要等到显示后沿结束过后才拉高,开始输出有效数据。然后DE拉低进入显示前沿,待显示前沿过后我们的行场同步信号又开始下一行或帧的传输。
这里没有完整的,我们实验用的是1024x720。其中的e等于a、b、c、d的总和。同样s等于前四个相加。这个对我们后面写代码是必要的,完整文件在结尾。
这里文件比较多,我就放三个主要的就行,结尾我会放完整工程。
这里主要参照上面的行场时序图进行编程理解。
//在video_driver 模块的驱动下按照工业标准的VGA 显示时序
//输出视频信号、行场同步信号以及视频有效信号。这些信号作为RGB2DVI模块的输入
module video_driver (
input pixel_clk ,
input sys_rst_n ,
input [23:0] pixel_data ,//像素点数据
//RGB接口
output video_hs ,//行同步信号
output video_vs ,//场同步信号
output video_de ,//数据使能
output [23:0] video_rgb ,//RGB888颜色数据
output [10:0] pixel_xpos ,//像素点横坐标
output [10:0] pixel_ypos //像素点纵坐标
);
//1280*720 分辨率时序参数
parameter H_SYNC = 11'd40, //行同步
H_BACK = 11'd220, //行显示后沿
H_DISP = 11'd1280, //行有效数据
H_FRONT = 11'd110, //行显示前沿
H_TOTAL = 11'd1650; //行扫描周期
parameter V_SYNC = 11'd5, //场同步
V_BACK = 11'd20, //场显示后沿
V_DISP = 11'd720, //场有效数据
V_FRONT = 11'd5, //场显示前沿
V_TOTAL = 11'd750; //场扫描周期
reg [10:0] cnt_h;
reg [10:0] cnt_v;
wire video_en;
wire data_req;
assign video_de = video_en;
assign video_hs = (cnt_h < H_SYNC ) ? 1'b0 : 1'b1;
assign video_vs = (cnt_v < V_SYNC ) ? 1'b0 : 1'b1;
//有效信号输出时序
assign video_en = (((cnt_h >= H_SYNC + H_BACK) && (cnt_h < H_SYNC + H_BACK + H_DISP))
&& ((cnt_v >= V_SYNC + V_BACK) && (cnt_v < V_SYNC + V_BACK + V_DISP)))
? 1'b1 : 1'b0;
//RGB888数据输出
assign video_rgb = video_en ? pixel_data : 24'd0;
//请求像素点颜色数据输入
assign data_req = (((cnt_h >= H_SYNC + H_BACK - 1'b1) && (cnt_h < H_SYNC + H_BACK + H_DISP - 1'b1))
&& ((cnt_v >= V_SYNC + V_BACK) && (cnt_v < V_SYNC + V_BACK + V_DISP)))
? 1'b1 : 1'b0;
//像素点坐标
assign pixel_xpos = data_req ? (cnt_h - (H_SYNC + H_BACK - 1'b1)) : 11'd0;
assign pixel_ypos = data_req ? (cnt_v - (V_SYNC + V_BACK - 1'b1)) : 11'd0;
//行计数器对像素时钟计数
//使用同步复位信号:抗干扰能力强、消除毛刺,同步消耗资源较多、
//需要保持一个时钟周期以上才可以复位、可能带来时钟偏移等问题
//异步复位信号则相反消耗资源少、抗干扰弱、有亚稳态问题
//行计数器对像素时钟进行计数
always @(posedge pixel_clk) begin
if(!sys_rst_n)
cnt_h <= 11'd0;
else if(cnt_h == H_TOTAL - 1'b1)
cnt_h <= 11'd0;
else
cnt_h <= cnt_h + 1'b1;
end
//场计数器对行计数,一个场内的行计完切换下一帧
always @(posedge pixel_clk) begin
if(!sys_rst_n)
cnt_v <= 11'd0;
else if(cnt_h == H_TOTAL - 1'b1) begin
if(cnt_v < V_TOTAL - 1'b1)
cnt_v <= cnt_v + 1'b1;
else
cnt_v <= 11'd0;
end
end
endmodule
这里通过例化一个PLL实现一个五倍的像素时钟和一个ALTDDIO_OUT IP核,用于并转串的操作。
module serializer_10_to_1(
input serial_clk_5x,//输入串行数据时钟
input [9:0] paralell_data,//输入并行数据
output serial_data_p,//输出串行差分数据p
output serial_data_n //输出串行差分数据N
);
reg [2:0] bit_cnt = 0;
reg [4:0] datain_rise_shift = 0;
reg [4:0] datain_fall_shift = 0;
wire [4:0] datain_rise;
wire [4:0] datain_fall;
//拆分数据
assign datain_rise = {paralell_data[8],paralell_data[6],paralell_data[4],
paralell_data[2],paralell_data[0]};
assign datain_fall = {paralell_data[9],paralell_data[7],paralell_data[5],
paralell_data[3],paralell_data[1]};
//位计数器
always @(posedge serial_clk_5x) begin
if(bit_cnt == 3'd4)
bit_cnt <= 1'b0;
else
bit_cnt <= bit_cnt + 1'b1;
end
//移位寄存器,用于并行数据的每一位
always @(posedge serial_clk_5x) begin
if(bit_cnt == 3'd4)begin
datain_rise_shift <= datain_rise;
datain_fall_shift <= datain_fall;
end
else begin
datain_rise_shift <= datain_rise_shift[4:1];
datain_fall_shift <= datain_fall_shift[4:1];
end
end
//例化DDIO_OUT IP核,因为我们我们是差分传输,实际上是两条线,需要例化两个IP
ddio_out ddio_out_inst_1(
.datain_h (datain_rise_shift[0]),
.datain_l (datain_fall_shift[0]),
.outclock (serial_clk_5x),
.dataout (serial_data_p)
);
ddio_out ddio_out_inst_2(
.datain_h (~datain_rise_shift[0]),
.datain_l (~datain_fall_shift[0]),
.outclock (serial_clk_5x),
.dataout (serial_data_n)
);
endmodule
这里参照那张编码流程图更容易理解。
/***********
下面会用到一个#1,在1ns过后才开始进行赋值
作用:该延迟有效地实现了 1ns clk-to-q 或 rst_n-to-q 延迟,使用波形查看器查看时可以轻松解释。
比如:波形查看器中的小延迟还可以很容易地看到时序逻辑输出的值在时钟边沿之前是什么,
通过将波形查看器光标放在时钟边沿本身,大多数波形查看工具将显示相应的靠近波形显示左侧的信号名称旁边的二进制、十进制或十六进制值。
***********/
`timescale 1ps/1ps
module dvi_encoder(
input clkin ,//像素时钟
input rst_n ,//异步复位同步释放的复位信号、高有效
input [7:0] din ,//8位数据
input c0 ,//控制信号1
input c1 ,//控制信号2
input de ,//发送时序
output reg [9:0] dout //编码后的数据
);
reg [3:0] n1d;//1的个数
reg [7:0] din_q;//
//计算1的个数
always @(posedge clkin) begin
n1d <=#1 din[0] + din[1] + din[2] + din[3] + din[4] + din[5] + din[6] + din[7];
din_q <=#1 din;
end
wire decision1;
assign decision1 = (n1d > 4'h4) || ((n1d == 4'h4) && (din_q[0] == 1'b0));
wire [8:0] q_m;
assign q_m[0] = din_q[0];
assign q_m[1] = (decision1) ? (q_m[0] ^~ din_q[1]) : (q_m[0] ^ din_q[1]);
assign q_m[2] = (decision1) ? (q_m[1] ^~ din_q[2]) : (q_m[1] ^ din_q[2]);
assign q_m[3] = (decision1) ? (q_m[2] ^~ din_q[3]) : (q_m[2] ^ din_q[3]);
assign q_m[4] = (decision1) ? (q_m[3] ^~ din_q[4]) : (q_m[3] ^ din_q[4]);
assign q_m[5] = (decision1) ? (q_m[4] ^~ din_q[5]) : (q_m[4] ^ din_q[5]);
assign q_m[6] = (decision1) ? (q_m[5] ^~ din_q[6]) : (q_m[5] ^ din_q[6]);
assign q_m[7] = (decision1) ? (q_m[6] ^~ din_q[7]) : (q_m[6] ^ din_q[7]);
assign q_m[8] = (decision1) ? 1'b0 : 1'b1;
//计算q_m的1的个数用于下一个decision条件
reg [3:0] n1q_m, n0q_m;
always @(posedge clkin) begin
n1q_m <=#1 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 <=#1 4'h8 - (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
//两个控制信号的四种编码情况
parameter CTRLTOKEN0 = 10'b1101010100,
CTRLTOKEN1 = 10'b0010101011,
CTRLTOKEN2 = 10'b0101010100,
CTRLTOKEN3 = 10'b1010101011;
reg [4:0] cnt ;
wire decision2,decision3;
assign decision2 = (cnt == 5'h0) || (n0q_m == n1q_m);
assign decision3 = ((~cnt[4]) && (n1q_m > n0q_m)) || ((cnt[4]) && (n1q_m < n0q_m));
reg de_q, de_reg;
reg c0_q,c1_q;
reg c0_reg,c1_reg;
reg [8:0] q_m_reg;
//应该是延后一个时钟周期用于与前面寄存din_q同步
always @(posedge clkin)begin
de_q <=#1 de;
de_reg <=#1 de_q;
c0_q <=#1 c0;
c0_reg <=#1 c0_q;
c1_q <=#1 c1;
c0_reg <=#1 c1_q;
q_m_reg <=#1 q_m;
end
always @(posedge clkin or posedge rst_n) begin
if(rst_n)begin
dout <= 10'h0;
cnt <= 5'h0;
end
else begin
if(de_reg) begin//有效数据传输过程
if(decision2) begin
dout[9] <=#1 ~q_m_reg[8];
dout[8] <=#1 q_m_reg[8];
dout[7:0] <=#1 (q_m_reg[8]) ? q_m_reg[7:0] : ~q_m_reg[7:0];
cnt <=#1 (~q_m_reg[8]) ? (cnt + n0q_m - n1q_m) : (cnt + n1q_m - n0q_m);
end
else begin
if(decision3)begin
dout[9] <=#1 1'b1;
dout[8] <=#1 q_m_reg[8];
dout[7:0] <=#1 ~q_m_reg[7:0];
cnt <=#1 cnt + {q_m_reg[8],1'b0} + (n0q_m - n1q_m);
end
else begin
dout[9] <=#1 1'b0;
dout[8] <=#1 q_m_reg[8];
dout[7:0] <=#1 q_m_reg[7:0];
cnt <=#1 cnt - {~q_m_reg[8],1'b0} + (n1q_m - n0q_m);
end
end
end
else begin
case ({c1_reg , c0_reg})
2'b00: dout <=#1 CTRLTOKEN0;
2'b01: dout <=#1 CTRLTOKEN1;
2'b10: dout <=#1 CTRLTOKEN2;
default: dout <=#1 CTRLTOKEN3;
endcase
cnt <=#1 5'h0;
end
end
end
endmodule
//这个模块是用于生成显示图案的
//这里我们只需要实现彩条的显示,
//其实就是通过x、y的坐标对不同范围的频幕给不同的颜色即可
module video_display(
input pixel_clk ,//75MHZ
input sys_rst_n ,
input [10:0] pixel_xpos ,//像素点横坐标
input [10:0] pixel_ypos ,//像素点纵坐标
output reg [23:0] pixel_data//像素点数据,传回驱动模块用于在有效数据发送期间进行发送
);
/**************
这里的方块动态显示模块实际上就是依据左上角的一个坐标确定整个方块的位置
并给小方块颜色,同样根据左上角坐标点的位置判定是否到达边界。
**************/
parameter H_DISP = 11'd1280;//分辨率--行
parameter V_DISP = 11'd720;//分辨率--列
localparam SIDE_W = 11'd40,//屏幕边框
BLOCK_W = 11'd40,//方块宽度
BLUE = 24'b00000000_00000000_11111111, //屏幕颜色,蓝色
WHITE = 24'b11111111_11111111_11111111, //背景颜色,白色
BLACK = 24'b00000000_00000000_00000000; //方块颜色, 黑色
//左上角方块初始位置
reg [10:0] block_x = SIDE_W;
reg [10:0] block_y = SIDE_W;
reg [21:0] div_cnt;//分频计数器
reg h_direct;//指示水平移动方向 1:右移,0:左移
reg v_direct;//指示垂直移动方向 1:向下,0:向上
wire move_en;//方块移动使能,100HZ
assign move_en = (div_cnt == 22'd742500) ? 1'b1 : 1'b0;
//计数实现时钟分频
always @(posedge pixel_clk) begin
if(!sys_rst_n)begin
div_cnt <= 22'd0;
end
else begin
if(div_cnt < 22'd742500)
div_cnt <= div_cnt + 1'b1;
else
div_cnt <= 22'd0;
end
end
//判断方块移动到边界,改变移动方向
always @(posedge pixel_clk) begin
if(!sys_rst_n) begin
h_direct <= 1'b1; //初始向右移动
v_direct <= 1'b1; //初始竖直向下
end
else begin
//判断水平位置
if(block_x == SIDE_W - 1'b1) //到达左边界,水平向右
h_direct <= 1'b1;
else if(block_x == H_DISP - SIDE_W - BLOCK_W)
h_direct <= 1'b0;
else
h_direct <= h_direct;
//判断垂直位置
if(block_y == SIDE_W - 1'b1)
v_direct <= 1'b1;
else if(block_y == V_DISP - SIDE_W - BLOCK_W)
v_direct <= 1'b0;
else
v_direct <= v_direct;
end
end
//根据方块移动方向,改变坐标
always @(posedge pixel_clk) begin
if(!sys_rst_n)begin
block_x <= SIDE_W; //初始化方块坐标
block_y <= SIDE_W;
end
else if(move_en) begin//10ms改变坐标,实现动态效果
if(h_direct)
block_x <= block_x + 1'b1;//向右
else
block_x <= block_x - 1'b1;
if(v_direct)
block_y <= block_y + 1'b1;//向下
else
block_y <= block_y - 1'b1;
end
else begin
block_x <= block_x;
block_y <= block_y;
end
end
//不同区域绘制不同颜色
always @(posedge pixel_clk) begin
if(!sys_rst_n)
pixel_data <= BLACK;
else begin
if( (pixel_xpos < SIDE_W) || (pixel_xpos >= H_DISP - SIDE_W)
|| (pixel_ypos < SIDE_W) || (pixel_ypos >= V_DISP - SIDE_W))
pixel_data <= BLUE; //绘制屏幕边框为蓝色
else
if( (pixel_xpos >= block_x) && (pixel_xpos < block_x + BLOCK_W)
&& (pixel_ypos >= block_y) && (pixel_ypos < block_y + BLOCK_W))
pixel_data <= BLACK; //绘制方块为黑色
else
pixel_data <= WHITE; //绘制背景为白色
end
end
endmodule
由于博主离校在外,没有支持HDMI的显示屏,无法对效果进行演示,在经过多方寻找有心无力,还冲动买了一块,后面考虑到用不了多少次就退款了。等我返校有时间了再给大伙试试效果,上面的代码就当理解学习先吧,有条件的可以自己试试效果。
说实话这个相对前面的学习好像又上了一个坡,还是有一定的理解难度的。这个项目我拢共看了四便,手敲了一遍代码过后理解更加的深刻,之前只用过VGA做过屏幕的显示,这次又是不一样的体验。这个项目里涉及到有PLL、DDIO、HDMI协议等,我觉得里面我觉得比较巧妙的就是并转串那里,真的太棒了。可惜的就是没有能够进一步去了解到DDIO这个IP,这是我第一次使用,仅仅是配置了下。下一步准备看看如何显示动态的画面。
1、以上资料均来自正点原子的教学视频或开拓者2开发教程:原子官方
2、完整的显示器时序参数文件:显示器时序参数文件 提取码:hzjj
2、源码:https://github.com/no1jiangjiang/HDMI