本文演示如何用PL逻辑资源去驱动最小系统板上 HDMI部分的电路来实现点屏的操作
- 此章节内容适用于Smart ZYNQ SP SP2和 SL 版的板子 ( 不包含Smart ZYNQ 标准版 ),如是标准版或本站其他板子请看对应板子目录
- 本文在 vivado2018.3版本上演示
实验内容:本次实验我们将用Verilog设计一个符合RGB888输出的图像生成模块(分辨率800X600以及1080P)(彩条纹生成模块),并借助板子上的HDMI资源将图像输出并在外部HDMI显示屏上进行显示,以此来熟悉我们板子的HDMI开发流程,为后续FPGA图像处理相关的开发做前期准备工作。
一、硬件部分介绍:
HDMI部分的原理图:
原理图上HDMI_TX_TMDS_DATA0-1-2 以及HDMI_CLK_TX_TMDS信号都是接到ZYNQ的PL 差分信号线上的。板子的HDMI 都是接ESD 芯片的(PUSB3FR4) 这样插拔 HDMI的过程当中不会因为静电而打坏板子
如上文所说,Smart ZYNQ 主板的HDMI 是没有接外部HDMI芯片的,而是通过差分线的方式直接连到FPGA的IO上,相当于用FPGA的逻辑来实现外部HDMI芯片的功能,该方法可以满足大部分HDMI输出显示的使用场景,并最高支持到1080P60帧的画面输出。
(经反馈,有部分非标准HDMI 显示器会出现兼容性上的问题,集中在极客DIY的HDMI小屏上。除此之外绝大多数品牌HDMI在现实上都没有问题)
想要驱动HDMI部分并输出图像,我们还需要用到 digilent官方设计的一个IP核 RGB2DVI,这个IP核的官方下载地址如下:https://github.com/Digilent/vivado-library,这里为了方便使用,我也把这个IP核下下来放在本站供大家参考(IP版权归digilent所有)http://www.hellofpga.com/wp-content/uploads/2021/07/rgb2dvi.zip
RGB2DVI 的输入时序满足VGA的标准,以下是VGA分辨率的参数
时序图,以及消隐的概念,还有不同分辨率时,各个区域的参数
一、Vivado工程创建
工程创建的过程可以参考实验一中的内容,这里不详细描述了。基于Smart ZYNQ (SP/SP2/SL 版) 的FPGA实验一 用ZYNQ的PL资源点亮一个LED(完整图文) (芯片型号选XC7Z020CLG484-1)
二、时钟模块设计
我们尝试输出800X600的分辨率的图像,参考上文中VGA的分辨率参数表中标注的,800X600分辨率需要的工作的像素时钟为40MH,而我们板子上焊接的有源晶体是50MHZ,这里就需要用时钟管理模块MMCM来生成我们要的40MHZ频率和 5倍像素时钟的200Mhz频率。
VIVADO系统给我们内置了很多功能强大的模块,包括我们要的时钟模块,这里我们只需要调用时钟模块输入我们要的参数就好
1)第一步,点击IP Catalog 打开模块选择器, 在里面的搜索栏输入 CLOCKING ,系统会自动跳出符合的 Clocking Wizard选项,双击它
2)在弹出的窗口中我们将input Frequence 输入频率修改为板子上焊接的50M时钟, 右边改为单端输入
3)在output Clocks选项中 将clk_out1改成40m,将clk_out2改成200m
4)将界面托到最下面,因为我们这里的要求并不高,所以把时钟的复位reset,和locked选项去除,最后点击ok生成模块
三、导入下下来的RGB2DVI模块
1)点击Setting
2)在弹出的设置窗口,如下图,展开IP选项,选中Packager,在点击里面的加号增加目录,选中RGB2DVI的目录(这里已提前将RGB2DVI的目录复制到工程目录下),点击ok
3)导入RGB2DVI IP,如下图所示,在IP管理器里搜索RGB 双击并打开RGB to DVI Video Encoder 选项
4)设置RGB2DVI模块,因为我们已经在时钟模块中设置了 5倍的编码时钟,所以 模块里不需要再生成模块时钟,如下图去掉复位和内部串行时钟前面的勾,点击OK
完成之后 我们便得到了两个模块
这里需要注意一个事情 RGB2DVI 官方手册上有明确说, 对应的24bit 线序是 RBG的 ,而不是 RGB使用时候需要格外注意。
四、增加我们的代码内容
1)这里创建一个显示模块显示模块负责输出标准的RGB时序,这里调用了一个网上的参考代码,具体如下( color_bar.v)
//www.hellofpga.com//
`timescale 1ns / 1ps
module color_bar (
input wire clk,
input wire rst_n,
output reg hsync,
output reg vsync,
output reg de,
output reg [7:0] rgb_r,
output reg [7:0] rgb_g,
output reg [7:0] rgb_b
);
//`define RES_1080P
`define RES_800x600
`ifdef RES_800x600
parameter H_ACTIVE = 800;
parameter H_FRONT = 40;
parameter H_SYNC = 128;
parameter H_BACK = 88;
parameter V_ACTIVE = 600;
parameter V_FRONT = 1;
parameter V_SYNC = 4;
parameter V_BACK = 23;
`endif
`ifdef RES_1080P
parameter H_ACTIVE = 1920;
parameter H_FRONT = 88;
parameter H_SYNC = 44;
parameter H_BACK = 148;
parameter V_ACTIVE = 1080;
parameter V_FRONT = 4;
parameter V_SYNC = 5;
parameter V_BACK = 36;
`endif
reg [11:0] h_count = 0;
reg [11:0] v_count = 0;
reg pix_data_req;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
h_count<=12'd0;
v_count<=12'd0;
pix_data_req<=1'b0;
de<=1'b0;
end
else begin
if (h_count < H_ACTIVE + H_FRONT + H_SYNC + H_BACK - 1'b1)
h_count <= h_count + 1;
else begin
h_count <= 0;
if (v_count < V_ACTIVE + V_FRONT + V_SYNC + V_BACK - 1'b1)
v_count <= v_count + 1;
else
v_count <= 0;
end
hsync <= (h_count < H_SYNC) ? 0 : 1;
vsync <= (v_count < V_SYNC) ? 0 : 1;
de <= ((h_count >= H_SYNC + H_BACK ) && (h_count < H_SYNC + H_BACK + H_ACTIVE)
&&(v_count >= V_SYNC + V_BACK ) && (v_count < V_SYNC + V_BACK + V_ACTIVE)) ? 1'b1:1'b0;
pix_data_req <= ((h_count >= H_SYNC + H_BACK -1'b1 ) && (h_count < H_SYNC + H_BACK + H_ACTIVE -1'b1)
&&(v_count >= V_SYNC + V_BACK ) && (v_count < V_SYNC + V_BACK + V_ACTIVE)) ? 1'b1:1'b0;
end
end
wire [11:0] pix_xpos = pix_data_req ? (h_count - (H_SYNC + H_BACK) ):12'd0;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
rgb_r<=8'h00; rgb_g<=8'h00; rgb_b<=8'h00;
end
else if(pix_data_req)begin
if(pix_xpos==12'd0)
begin rgb_r<=8'hff; rgb_g<=8'hff; rgb_b<=8'hff; end //White
else if(pix_xpos ==( H_ACTIVE / 8 ) * 1)
begin rgb_r<=8'hff; rgb_g<=8'hff; rgb_b<=8'h00; end //Yellow
else if(pix_xpos ==( H_ACTIVE / 8 ) * 2)
begin rgb_r<=8'h00; rgb_g<=8'hff; rgb_b<=8'hff; end //Cyan
else if(pix_xpos ==( H_ACTIVE / 8 ) * 3)
begin rgb_r<=8'h00; rgb_g<=8'hff; rgb_b<=8'h00; end //Green
else if(pix_xpos ==( H_ACTIVE / 8 ) * 4)
begin rgb_r<=8'hff; rgb_g<=8'h00; rgb_b<=8'hff; end //Magenta
else if(pix_xpos ==( H_ACTIVE / 8 ) * 5)
begin rgb_r<=8'hff; rgb_g<=8'h00; rgb_b<=8'h00; end //Red
else if(pix_xpos ==( H_ACTIVE / 8 ) * 6)
begin rgb_r<=8'h00; rgb_g<=8'h00; rgb_b<=8'hff; end //Blue
else if(pix_xpos ==( H_ACTIVE / 8 ) * 7)
begin rgb_r<=8'h00; rgb_g<=8'h00; rgb_b<=8'h00; end //Black
end
else begin
rgb_r<=8'h00; rgb_g<=8'h00; rgb_b<=8'h00;
end
end
endmodule
2)创建一个顶层模块,分别调用时钟、RGB2DVI、以及彩条纹显示模块(top.v),这里注意vid_pData({R,B,G}),而不是R G B(原因是 RGB2DVI线序接口是 RBG顺序,而不是RGB)
//www.hellofpga.com//
`timescale 1ns / 1ps
module top(
input clk,
output[2:0] TMDS_DATA_p,
output[2:0] TMDS_DATA_n,
output TMDS_CLK_p,
output TMDS_CLK_n
);
wire clk_40m;
wire clk_200m;
clk_wiz_0 u2(
.clk_in1(clk),
.clk_out1(clk_40m),
.clk_out2(clk_200m)
);
wire VGA_HS,VGA_VS,VGA_DE;
wire[7:0] R,G,B;
color_bar u4 (
.clk(clk_40m),
.rst_n(1'b1),
.hsync(VGA_HS),
.vsync(VGA_VS),
.de(VGA_DE),
.rgb_r(R),
.rgb_g(G),
.rgb_b(B)
);
rgb2dvi_0 u1(
.aRst_n(1'b1),
.SerialClk(clk_200m),
.PixelClk(clk_40m),
.TMDS_Clk_p(TMDS_CLK_p),
.TMDS_Clk_n(TMDS_CLK_n),
.TMDS_Data_p(TMDS_DATA_p),
.TMDS_Data_n(TMDS_DATA_n),
.vid_pData({R,B,G}),
.vid_pHSync(VGA_HS),
.vid_pVSync(VGA_VS),
.vid_pVDE(VGA_DE)
);
endmodule
最后再增加约束文件 (HDMI_TEST.XDC) 因V1.3版本硬件调整了HDMI CLK 引脚,所以请根据实际板子的版本调用对应的约束
#V1.2 or Earlier
set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property PACKAGE_PIN M21 [get_ports {TMDS_DATA_p[0]}]
set_property PACKAGE_PIN L21 [get_ports {TMDS_DATA_p[1]}]
set_property PACKAGE_PIN J21 [get_ports {TMDS_DATA_p[2]}]
set_property PACKAGE_PIN N22 [get_ports TMDS_CLK_p]
set_property PACKAGE_PIN M19 [get_ports clk]
#V1.3
set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property PACKAGE_PIN M21 [get_ports {TMDS_DATA_p[0]}]
set_property PACKAGE_PIN L21 [get_ports {TMDS_DATA_p[1]}]
set_property PACKAGE_PIN J21 [get_ports {TMDS_DATA_p[2]}]
set_property PACKAGE_PIN N19 [get_ports TMDS_CLK_p]
set_property PACKAGE_PIN M19 [get_ports clk]
五、编译综合,并运行代码
最终显示效果如图,以上代码实现 800X600的 60hz HDMI 稳定输出(完整代码在本文最后面)
六、代码解读:
1) 不同分辨率的不同参数
RGB2DVI 的输入时序满足VGA的标准,以下是VGA分辨率的参数
时序图,以及消隐的概念,还有不同分辨率时,各个区域的参数
还是回到这两张图, 不同的分辨率有不同的参数,下面是800×600 和1080P对应的参数,通过 `define RES_800x600 或者`define RES_1080P 来进行选择,其中的参数均和上述图表中的内容对应。
//`define RES_1080P
`define RES_800x600
`ifdef RES_800x600
parameter H_ACTIVE = 800;
parameter H_FRONT = 40;
parameter H_SYNC = 128;
parameter H_BACK = 88;
parameter V_ACTIVE = 600;
parameter V_FRONT = 1;
parameter V_SYNC = 4;
parameter V_BACK = 23;
`endif
`ifdef RES_1080P
parameter H_ACTIVE = 1920;
parameter H_FRONT = 88;
parameter H_SYNC = 44;
parameter H_BACK = 148;
parameter V_ACTIVE = 1080;
parameter V_FRONT = 4;
parameter V_SYNC = 5;
parameter V_BACK = 36;
`endif
2)hsync 和vsync 以及de的逻辑如下:
if (h_count < H_ACTIVE + H_FRONT + H_SYNC + H_BACK - 1'b1)
h_count <= h_count + 1;
else begin
h_count <= 0;
if (v_count < V_ACTIVE + V_FRONT + V_SYNC + V_BACK - 1'b1)
v_count <= v_count + 1;
else
v_count <= 0;
end
hsync <= (h_count < H_SYNC) ? 0 : 1;
vsync <= (v_count < V_SYNC) ? 0 : 1;
de <= ((h_count >= H_SYNC + H_BACK ) && (h_count < H_SYNC + H_BACK + H_ACTIVE) &&(v_count >= V_SYNC + V_BACK ) && (v_count < V_SYNC + V_BACK + V_ACTIVE)) ? 1'b1:1'b0;
其中 h_count
和 v_count
分别是水平和垂直方向的计数器。
- h_count周期是从 H_SYNC 到 H_BACK(H_BP)到 H_ACTIVE 到 H_FRONT(H_FP)
- v_count周期是从V_SYNC 到 V_BACK(V_BP)到 V_ACTIVE 到 V_FRONT(H_FP)
- de 就是 H_ACTIVE 和V_ACTIVE 都有效的区域,即图像显示区域
下图是ST的一个LCD控制时序,我们也可以通过该图来对流程进行了解。
3) 彩条纹的显示部分:
a) 增加一个信号pix_data_req,这个信号和de相类似,都代表有效显示区域,但是pix_data_req在h计数上会早de一个像素点,用于在de信号之前就将待显示的颜色数据准备好
pix_data_req <= ((h_count >= H_SYNC + H_BACK -1'b1 ) && (h_count < H_SYNC + H_BACK + H_ACTIVE -1'b1)&&(v_count >= V_SYNC + V_BACK ) && (v_count < V_SYNC + V_BACK + V_ACTIVE)) ? 1'b1:1'b0;
b) 增加pix_xpos信号,该信号对应有效显示区域中x的坐标(从0开始计数)
wire [11:0] pix_xpos = pix_data_req ? (h_count - (H_SYNC + H_BACK) ):12'd0;
c) 彩条纹的逻辑也比较容易理解, 我们将H_ACTIVE 分割成8等份,每等份都显示不同颜色作演示,8等分通过下列方式来划分
(H_ACTIVE / 8 ) * n 这部分是8等分颜色的切分点,当我们当前的pix_xpos==(H_ACTIVE / 8 ) * n时,就将RGB输出对应的颜色。 (因为我们的H_ACTIVE这个值是确认的,所以这里的乘法和除法是在vivado 编译过程中就换算成对应的结果了,给进FPGA的值会是一个常数,而不是由FPGA进行运算的)
if(pix_data_req)begin
if(pix_xpos==12'd0)
begin rgb_r<=8'hff; rgb_g<=8'hff; rgb_b<=8'hff; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 1)
begin rgb_r<=8'hff; rgb_g<=8'hff; rgb_b<=8'h00; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 2)
begin rgb_r<=8'h00; rgb_g<=8'hff; rgb_b<=8'hff; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 3)
begin rgb_r<=8'h00; rgb_g<=8'hff; rgb_b<=8'h00; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 4)
begin rgb_r<=8'hff; rgb_g<=8'h00; rgb_b<=8'hff; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 5)
begin rgb_r<=8'hff; rgb_g<=8'h00; rgb_b<=8'h00; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 6)
begin rgb_r<=8'h00; rgb_g<=8'h00; rgb_b<=8'hff; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 7)
begin rgb_r<=8'h00; rgb_g<=8'h00; rgb_b<=8'h00; end
end
七、修改分辨率(1080P)
1) 修改时序参数
如果我们要将分辨率修改成1080P或者其他的分辨率,只需要对我们color_bar中的参数进行调整,因为我们的演示代码中已经增加了1080p的参数, 我们只需要将将程序中的宏定义由`define RES_800x600 修改成 `define RES_1080P即可。其他的分辨率按照之前VGA时序的表格修改对应的参数即可。
`ifdef RES_1080P
parameter H_ACTIVE = 1920;
parameter H_FRONT = 88;
parameter H_SYNC = 44;
parameter H_BACK = 148;
parameter V_ACTIVE = 1080;
parameter V_FRONT = 4;
parameter V_SYNC = 5;
parameter V_BACK = 36;
`endif
2) 修改对应时钟
以1080P为例, 将 clk_out1 修改成1080p的 148.5 ,而clk_out2修改成5倍的clk_out1 即742.5即可。
实际我们输出的Actual 的时钟值742.188M和理论设置值742.5可能会有点区别,但是因为底层都是用的一套倍频分频逻辑,所以clk_out1和clk_out2仍然会保证 5倍分频关系。如下图所示:
备注:我们的时钟模块尽量只给HDMI提供时钟,因为如果添加多路时钟输出,有可能会造成Actual和我们的设定值偏离过大的情况。(因为FPGA内部有多个时钟模块,所以其他功能可再例化新的时钟模块)
重新再编译综合,并运行,我们就可以看到板子的分辨被修改成1080P的了。
因为1080P情况下mmcm的时钟已经超过mmcm IP设置的最大推荐值了,所以不一定能兼容所有的显示器,大家请自行尝试。
下列是本次实验的完整工程:
备注:因为Smart ZYNQ SP/ SP2/ SL 已经更新了三个版本,在最新的V1.3 版本中 HDMI的CLK引脚从 N22修改成N19了, 为了兼容各个版本,所以DEMO中对RGB2DVI 模块进行了改造,可以同时输出两个CLK引脚,这样一套程序就可以兼容所有版本的主板了。
- 本文的完整工程下载:12_PL_HDMI_TEST
- VIVADO的版本:2018.3
- 工程创建目录:E:\Smart_ZYNQ_SP_SL\FPGA\12_PL_HDMI_TEST
- 工程适用主板: Smart ZYNQ (SP / SP2 / SL) (不适用于Smart ZYNQ 标准版 )