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

我一直觉得fpga的进阶应用领域是在图像处理这一块,所以这一次的扩展板上 特别增加了一路HDMI的输出接口。本文演示如何用PL逻辑资源去驱动转接板上的 HDMI部分的电路来实现点屏的操作

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

一、硬件部分介绍:

HDMI部分的原理图:

扩展板HDMI部分原理图

其中HDMI_TX_TMDS_DATA0-1-2 以及HDMI_CLK_TX_TMDS信号都是接到ZYNQ的PL 差分信号线上的。

如上文所说,EBAZ4205转接板的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工程创建

工程创建的过程可以参考实验一中的内容,这里不详细描述了。基于EBAZ4205 的FPGA实验一 用ZYNQ的PL资源点亮一个LED (完整图文) (芯片型号选XC7Z010CLG400-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生成模块

2导入下下来的DVI2RGB模块

1)点击Setting

2)在弹出的设置窗口,如下图,展开IP选项,选中Packager,在点击里面的加号增加目录,选中RGB2DVI的目录(这里已提前将RGB2DVI的目录复制到工程目录下),点击ok

3)导入RGB2DVI IP,如下图所示,在IP管理器里搜索RGB 双击并打开RGB to DVI Video Encoder 选项

4)设置RGB2DVI模块,因为我们已经在时钟模块中设置了 5倍的编码时钟,所以 模块里不需要再生成模块时钟,如下图去掉复位和内部串行时钟前面的勾,点击OK

完成之后 我们便得到了两个模块

四、增加我们的代码内容

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 引脚,所以请根据实际板子的版本调用对应的约束

set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property PACKAGE_PIN D19 [get_ports {TMDS_DATA_p[0]}]
set_property PACKAGE_PIN C20 [get_ports {TMDS_DATA_p[1]}]
set_property PACKAGE_PIN B19 [get_ports {TMDS_DATA_p[2]}]
set_property PACKAGE_PIN F19 [get_ports TMDS_CLK_p]
set_property PACKAGE_PIN N19 [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_countv_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为例(备注,受接插件以及主板上HDMI部分对应的走线不是差分线布线等原因,这里1080P 只能上到50帧,60帧会不稳定), 将 clk_out1 修改成1080p 50帧的 123.75 ,而clk_out2修改成5倍的clk_out1 即618.75即可。 (如果大家要尝试1080P 60帧 时钟设置成148.5 和742.5 这个会不太稳定 大家自行尝试)

备注:我们的时钟模块尽量只给HDMI提供时钟,因为如果添加多路时钟输出,有可能会造成Actual和我们的设定值偏离过大的情况。(因为FPGA内部有多个时钟模块,所以其他功能可再例化新的时钟模块)

重新再编译综合,并运行,我们就可以看到板子的分辨被修改成1080P的了。

因为1080P情况下mmcm的时钟已经超过mmcm IP设置的最大推荐值了,所以不一定能兼容所有的显示器,大家请自行尝试。

本章节的工程:

  • 本文的完整工程下载: 12_PL_HDMI_TEST
  • VIVADO的版本:2018.3
  • 工程适用主板: EBAZ4205主板+转接板

“基于EBAZ4205的FPGA实验十二  基于ZYNQ PL资源的HDMI功能演示”的2个回复

  1. 这个例程里面的RGB显示中G和B是不是反了啊,我测试只显示蓝色,结果显示的是绿色,显示绿色出来的是蓝色

发表回复

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