基于Lemon ZYNQ 的FPGA实验十二  基于ZYNQ PL资源的HDMI功能演示

本文演示如何用PL逻辑资源去驱动最小系统板上 HDMI部分的电路来实现点屏的操作

  • 此章节内容适用于Lemon ZYNQ主板,如是其他板子请看对应板子目录
  • 本文在 vivado2018.3版本上演示

实验内容:本次实验我们将用Verilog设计一个符合RGB888输出的图像生成模块(分辨率800X600以及1080P)(彩条纹生成模块),并借助板子上的HDMI资源将图像输出并在外部HDMI显示屏上进行显示,以此来熟悉我们板子的HDMI开发流程,为后续FPGA图像处理相关的开发做前期准备工作。

一、硬件部分介绍:

HDMI部分的原理图:

原理图上HDMI_TX_TMDS_DATA0-1-2 以及HDMI_TX_TMDS_CLK信号都是接到ZYNQ的PL 差分信号线上的。板子的HDMI 都是接ESD 芯片的(PUSB3FR4) 这样插拔 HDMI的过程当中不会因为静电而打坏板子

如上文所说,Lemon 主板的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部分)

RGB2DVI 的输入时序满足VGA的标准,以下是VGA分辨率的参数

时序图,以及消隐的概念,还有不同分辨率时,各个区域的参数

一、Vivado工程创建

工程创建的过程可以参考试验一中的内容,这里不再详细描述了。基于Lemon ZYNQ的FPGA实验一 用ZYNQ的PL资源点亮一个LED(完整图文)(芯片型号选XC7Z020CLG400-1)

二、时钟模块设计

我们尝试输出800X600的分辨率的图像,参考上文中VGA的分辨率参数表中标注的,800X600分辨率需要的工作的像素时钟为40MH,而我们板子上焊接的有源晶体是50MHZ,这里就需要用时钟管理模块MMCM来生成我们要的40MHZ频率和 5倍像素时钟的200Mhz频率

VIVADO系统给我们内置了很多功能强大的模块,包括我们要的时钟模块,这里我们只需要调用时钟模块输入我们要的参数就好

1)第一步,点击IP Catalog 打开模块选择器, 在里面的搜索栏输入 CLOCKING ,系统会自动跳出符合的 Clocking Wizard选项,双击它

2)在弹出的窗口中我们将input Frequence 输入频率修改为板子上焊接晶振的时钟的125M, 右边改为单端输入

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)
//White
begin rgb_r<=8'hff; rgb_g<=8'hff; rgb_b<=8'hff; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 1)
//Yellow
begin rgb_r<=8'hff; rgb_g<=8'hff; rgb_b<=8'h00; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 2)
//Cyan
begin rgb_r<=8'h00; rgb_g<=8'hff; rgb_b<=8'hff; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 3)
//Green
begin rgb_r<=8'h00; rgb_g<=8'hff; rgb_b<=8'h00; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 4)
//Magenta
begin rgb_r<=8'hff; rgb_g<=8'h00; rgb_b<=8'hff; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 5)
//Red
begin rgb_r<=8'hff; rgb_g<=8'h00; rgb_b<=8'h00; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 6)
//Blue
begin rgb_r<=8'h00; rgb_g<=8'h00; rgb_b<=8'hff; end
else if(pix_xpos ==( H_ACTIVE / 8 ) * 7)
//Black
begin rgb_r<=8'h00; rgb_g<=8'h00; rgb_b<=8'h00; end
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)

set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property PACKAGE_PIN K17 [get_ports {TMDS_DATA_p[0]}]
set_property PACKAGE_PIN K19 [get_ports {TMDS_DATA_p[1]}]
set_property PACKAGE_PIN J18 [get_ports {TMDS_DATA_p[2]}]
set_property PACKAGE_PIN L16 [get_ports TMDS_CLK_p]
set_property PACKAGE_PIN H16 [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设置的最大推荐值了,所以不一定能兼容所有的显示器,大家请自行尝试。

下列是本次实验的完整工程:

  • 本文的完整工程下载:12_PL_HDMI_TEST
  • VIVADO的版本:2018.3
  • 工程创建目录:E:\Lemon_ZYNQ\FPGA\12_PL_HDMI_TEST

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注