zynq 7000 的HDMI 显示实验

用了很多年的zynq 7000,一直就没做hdmi 显示实验。前几天终于做了这个实验,也就做一个总结。

我的实验是在微相的z7-lite下根据他们的教程完成的。平台是windows 10 , Vivado 2018.3。如果硬件设计不一样,主要是替换rgb2dvi 模块和gpio 中断部分。

工程资料下载,

链接:https://pan.baidu.com/s/11-RLOYtl1AyxcQ_XGbw2YQ 
提取码:zvnc 

这个下载里里三个文件,一个hdmi_out 全工程67M,下了这个就包含其他2个了,如果只是ip ,可以下hmdiip(375k),如果只是源代码 hdmisrc( 15M),主要是图形数据占空间。
如果你有微相的资料,那就是 z7_Lite\03_SDK_Demo\17_hdmi_out

IP准备

这个实验用了2个IP,可以从 https://github.com/Digilent/vivado-library 下载。但我实验中用的是微相提供的是rgb2dvi_v1_2,这个ip 在工程的ip_repo目录里。

该链接内有 Digilent 提供的很多个自定义 IP,但这个实验只用2个。其中 axi_dynclk  是时钟发生模块,会根据不同的屏幕分辨率,自动生成相应的像素时钟和串行时钟,而rgb2dvi  模块会将图像的红绿蓝信号转换成MDS 信号, 送往 HDMI 端口发出。网上版本比较新,我本想用新版本做测试的,结果sdk的时候报错,只好用微相提供的版本。IP文件目录如下图所示:


在这里特别注意: if 文件目录也要复制,就是if 目录,其中包含了tmds 目录。ip目录下的 axi_dynclk, rgb2dvi 。图中内容复制到你的你的ip_repo,就是集中放IP的目录,我这目录下就放了其他IP。

开始的时候,我习惯只把ip 下的目录复制过来,结果通过不了。

硬件设计

在 Vivado 下新建一个工程,名字为 hdmi_out。整个硬件设计是比较复杂的,我把它分为几个部分:

添加ip目录,添加并配置zynq,添加 VDMA IP,AXI-Stream Subset Converter 模块,AXI4-Stream to Video Out 模块,Video Timing Controller 模块,rgb2dvi 模块,中断合并。

图像数据的流向是:zynq的DDR中, VDMA 读取,AXI-Stream Subset Converter转换,AXI4-Stream to Video Out 模块,rgb2dvi 模块输出。

Run Automation Connect的时候,一般只勾选当时介绍的IP, 如果多勾了,与后面指定操作冲突时,可以选择脚,右键,Disconnect Pin,使它脱离连接。

添加ip 目录 

在主窗口左侧边栏 Project Manager 下点击 Project Settings 选项, 向工程中添加这两个自定义 IP,其实是把 IP 存放目录加上去如下图所示:


可以Project Manager -> IP Catalog 看到我们添加的IP。 

原理图添加并配置zynq

新建一个原理图, 加入zynq ,原理图是这样的:

 双击zynq进行设置:

点击 PS-PL Configuration 选项, 在 HP Slave AXI interface 下勾选 S AXI HP0  interface, 本节实验要用这个端口获取 DDR 中存储的图像数据。


使能 UART0,这与开发板有关,z7_lite对应的是14,15。

点击 Clock Configuration 选项,在 PL Fabric Clocks 窗口,勾选 FCLK_CLK0和 FCLK_CLK1,并将其分别设为 100, 140MHz。 其中 FCLK_CLK0 将作为 Zynq配置各个模块的时钟, 而 FCLK_CLK1 将作为图像数据流的时钟,


再点击 DDR Configuration, 选择跟开发板一致的 DDR 型号.z7_lite 是 MT41K256M16 RE-125,16 Bit

再点击 Interrupt 选项,激活 IRQ_F2P[15:0]选项,点击 OK,配置完成。


手动连接 FCLK_CLK0 到 MAXI_GPO_ACLK, 连接 FCLK_CLK1 到 S_AXI_HP0_ACLK,

检查并对比,原理图是否一致。

添加 VDMA IP 

如下图所示:

VDMA 添加完成如下图所示:
 


 

双击上图 axi_vdma_0, 进行参数配置。 首先 Basic 选项, Address Width设为 32 bits, 寻址空间可以达到 4GB。 Frame Buffers 设为 1,取消 Enable WriteChannel 选项,因为本实验 VDMA 只从 DDR 中读取图像数据。 在 Enable Read Channel 下, 将 Line Buffer Depth 改为 4096, 其它默认, 如下图所示:

再点击 Advanced 选项卡, 将 GenLock Mode 改为 Master,点击 OK 完成配置。

现在原理图是如下样子,点击 Run Connection Automation,自动生成 AXI 互联总线及系统复位电路,在弹出的对话框中勾选 All Automation, 点击 OK。

然后手动将 m_axis_mm2s_aclk 和 m_axi_mm2s_aclk 两个时钟连在一起。

AXI-Stream Subset Converter 模块

添加 AXI-Stream Subset Converter 模块
 

添加完成后如下图所示:

双击上图的 axis_subset_converter_0,如下配置。

TDATA 输入为 32-bit,即 4-Byte,输出为 24-Bit, 即 3-Byte。

将 TKEEP、 TLAST 修改为 yes,并将 USERWidth 置为 1,

这里要重点说一下 TDATA Remap String 项,因为要显示的图像在 DDR 中的存储方式为 24-bit 默认方式 B, G, R,分别以[7:0], [15:8], [23:16]组成 24-bit,而本例中使用的产生 TMDS 信号模块 rgb2dvi 的像素排序为 R, B, G,所以需要在 TDATA Remap String 这一项做顺序调整。调整字符串为:tdata[23:16],tdata[7:0],tdata[15:8]

配置如下图所示:

配置好了,点OK。

手 动 连 接 axis_subset_converter_0 的 S_AXIS 端 口 到 axi_vdma_0 的M_AXIS_MM2S 端口。

并将其 aclk 连接到 FCLK_CLK1 时钟网络。 如下图所示:

增加一个常量输出模块 Constant, 将 axis_subset_convert_0 的复位端口 aresetn 拉高,使其始终处于工作状态,

双击刚刚添加的 xlconstant_0,保持默认的 1bit 位宽输出高电平 1 的配置,点击 OK,

将 xlconstant_0 的 dout 端口连到 axis_subset_converter_0 的 aresetn端口,
 

AXI4-Stream to Video Out 模块

添加 AXI4-Stream to Video Out 模块。本例用这个模块将 VDMA 从 DDR 读出的 AXI4-Stream 转换成 RGB 图像数据。

双击 v_axi4s_vid_out_0 进行配置。

将 FIFO Depth 改为 4096,

Clock Mode 设为 Independent,

Timing Mode 设为 Master,点击 OK。

手动将 v_axi4s_vid_out_0 的 video_in 端口连接到 axis_subset_converter_0的 M_AXIS 端口,

再将其 aclk 连接到 FCLK_CLK1 时钟网络,如下图,
 

Video Timing Controller 模块

添加 Video Timing Controller 模块。本例使用这个模块,来产生不同的分辨率下时序控制信号。

双击 v_tc_0 进行配置。取消掉 Enable Detection 选项的勾选,其它保持默认,点击 OK,

点击 Diagram 窗口的 Run Connection Automation,自动生成 v_tc_0 模块的相关连接。

勾选 All Automation, 点击“ OK”。
 

连接 v_tc_0 的 vtiming_out 端口到 v_axi4s_vid_out_0 的 vtiming_in 端口,

连接 v_tc_0 的 gen_clken 端口到 v_axi4s_vid_out_0 的 vtg_ce 端口,

rgb2dvi 模块

添加 rgb2dvi 模块。此模块将 RGB 信号转换为 DVI 信号,即 TMDS 格式。

双击 rgb2dvi_0 进行配置。 这里取消勾选 Reset active high

和 Generate SerialClk internally from pixel clock,

信号频率设置选择<120MHz(720p)这一项。

连接 v_axi4s_vid_out_0 的 vid_io_out 端口到 rgb2dvi_0 端口,

右键单击 rgb2dvi_0 的 TMDS 端口, 选择 Make External 生成外部引脚,
 

添加时钟生成模块,点击上方工具栏“ +”,在搜索框输入 dynclk, 回车添加模块。


此模块使用默认参数, 点击 Run Connection Automation

将 axi_dynclk_0的像素时钟 PXL_CLK_O 端口连接到 rgb2dvi_0 的 PixelClk 端口,

并将其串行时钟PXL_CLK_5X_O 端口连接到 rgb2dvi_0 的SerialCLk 端口,

再将 axi_dynclk_0 的 LOCKED_O 端口连接到 rgb2dvi_0 的 aRst_n 端口,

这样当锁相环锁定,即时钟信号稳定输出时,rgb2dvi 模块开始工作,

做到这我没截图,所以下面图中还包含了一些下一步的连线。

将 v_tc_0 的 clk 端口连接到 axi_dynclk_0 的 PXL_CLK_O 端口上。同样,
将 v_axi4s_vid_out_0 的 vid_io_out_clk 也连接到 PXL_CLK_O 这一网络上,

中断合并

添加 GPIO 模块,用作 HDMI 的热拔插检测信号 HPD

双击 axi_gpio_0 进行配置,将其设为 1 位输入, 使能 Interrupt,点击 OK,
 

将端口名称修改为“ HDMI_HPD”,
 

添加一个 concat 模块,将所有中断信号集中起来,然后再连接到 Zynq 处理器的中断输入端口。
 

双击 xlconcat_0 进行配置。本例中有 3 个中断信号, 设置端口数为 3, 点击 OK, 如下图所示:
 

将 axi_gpio_0 的 中 断 端 口 ip2intc_irpt , axi_vdma_0 的 中 断 端 口mm2s_introut, v_tc_0 的中断端口 irq,分别连接到 xlconcat_0 的 In0, In1, In2上,并将 xlconcat_0 的输出端口 dout 连接到 Zynq 的 IRQ_F2P 上,

由于图比较大,可能脚比较远,也可以选择要连接的端口之一,比如axi_vdma_0 的 中 断 端 口mm2s_introut,然后右键选择 Make Connetion,出现可能的连线,然后做一个选择,下图中选择xlconc_0 的In1。看下图所示:

连接后是这样的,

硬件设计完成了,如果有 Run Block Automation 提示,那就点击 Run Block Automation,在弹出的对话框中全选所有信号端口,点击“ OK”。
 

点击 Regenerate Layout 生成标准布局如下图, 再点击 Validate Design 验证设计:
 

验证成功后, 弹窗点击 OK, Ctrl+S 保存设计。

我的在验证时出现如下错误:

[BD 41-1343] Reset pin /v_tc_0/resetn (associated clock /v_tc_0/clk) is connected to reset source /rst_ps7_0_100M/peripheral_aresetn (synchronous to clock source /processing_system7_0/FCLK_CLK0).
This may prevent design from meeting timing. Please add Processor System Reset module to create a reset that is synchronous to the associated clock source /axi_dynclk_0/PXL_CLK_O.

我对照一下,点击pin /v_tc_0/resetn, 然后右键 Disconnect Pin,再验证就好了。

来到 Source 窗口, 生成设计代码和顶层文件, 右键点击 hdmi_out 选择Generate Output Producs, 在弹出的窗口点击 Generate,点击 OK。 右键点击hdmi_out 选择 Create HDL Wrapper,在弹出的窗口点击 OK。

添加管脚约束,这里可以直接添加一个约束文件,然后把下面内容复制过去就可。

set_property IOSTANDARD LVCMOS33 [get_ports {HDMI_HPD_tri_i[0]}]
set_property PACKAGE_PIN P19 [get_ports {HDMI_HPD_tri_i[0]}]
set_property PACKAGE_PIN U18 [get_ports TMDS_0_clk_p]
set_property PACKAGE_PIN V20 [get_ports {TMDS_0_data_p[0]}]
set_property PACKAGE_PIN T20 [get_ports {TMDS_0_data_p[1]}]
set_property PACKAGE_PIN N20 [get_ports {TMDS_0_data_p[2]}]

这个约束文件很简单,看电路图,都用了脚对,虽然4个脚,实际是8个脚。

原文中有设置约束文件的方法,不错,值得学习。但复制约束文件比较简单。

产生比特流,输出硬件(要包含比特流),然后打开SDK,就开始软件设计部分了。

软件设计

在打开的 SDK 软件内点击 File > New > Application Project, 工程名填入“ hdmi_out”, 工程模板选择 Empty Application 工程, 点击 Finish。

创 建 完 成 后 , 打 开 资 料 目 录hdmi_out/hdmi_out.sdk/hdmi_out/src 文件夹,复制文件夹内的所有文件,粘贴到当前工程同样的目录下(注意,不要覆盖本工程下的 lscript.ld), 右键单击工程 hdmi_out
选择 Refresh,则开始自动编译。双击打开 src 目录底下的 display_demo.c, 这就是 SDK 主程序。
接下来讲解代码,展开工程下的 src 可以看到文件结构如下图所示, 其中display_ctrl 文件夹包含不同视频分辨率情况下的时序控制, dynclk 包含不同分辨率情况下的像素时钟和串行时钟生成。
 

打开 display_demo.c 文件,这就是本实验的主程序,我们分段讲解各部分的作用。

#include <stdio.h>
#include <math.h>
#include <ctype.h>
#include <stdlib.h>
#include "xil_types.h"
#include "xil_cache.h"
#include "xparameters.h"
#include "display_demo.h"
#include "display_ctrl/display_ctrl.h"
#include "display_ctrl/vga_modes.h"

// Image data for each resolution
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//#include "pic_800_600.h"
//#include "pic_1280_720.h"
//#include "pic_1280_1024.h"
#include "pic_1920_1080.h"
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

/*
 * XPAR redefines
 */
#define DYNCLK_BASEADDR     XPAR_AXI_DYNCLK_0_BASEADDR
#define VGA_VDMA_ID         XPAR_AXIVDMA_0_DEVICE_ID
#define DISP_VTC_ID         XPAR_VTC_0_DEVICE_ID

/* ------------------------------------------------------------ */
/*				Global Variables								*/
/* ------------------------------------------------------------ */

/*
 * Display Driver struct
 */
DisplayCtrl dispCtrl;
XAxiVdma vdma;

/*
 * Frame buffers for video data
 */
u8 frameBuf[DISPLAY_NUM_FRAMES][DEMO_MAX_FRAME];
u8 *pFrames[DISPLAY_NUM_FRAMES];  // array of pointers to the frame buffers

/* ------------------------------------------------------------ */
/*				Procedure Definitions							*/
/* ------------------------------------------------------------ */

int main(void)
{
	int i;
	int Status;
	XAxiVdma_Config *vdmaConfig;

	/*
	 * Initialize an array of pointers to the 3 frame buffers
	 */
	for (i = 0; i < DISPLAY_NUM_FRAMES; i++)
	{
		pFrames[i] = frameBuf[i];
	}

	/*
	 * Initialize VDMA driver, get the hardware VDMA configurations
	 */
	vdmaConfig = XAxiVdma_LookupConfig(VGA_VDMA_ID);
	if (vdmaConfig == NULL)
	{
		xil_printf("No video DMA found for ID %d\r\n", VGA_VDMA_ID);
	}

	/*
	 * Use hardware VDMA configurations to initialize the driver
	 */
	Status = XAxiVdma_CfgInitialize(&vdma, vdmaConfig, vdmaConfig->BaseAddress);
	if (Status != XST_SUCCESS)
	{
		xil_printf("VDMA Configuration Initialization failed %d\r\n", Status);
	}

	/*
	 * Initialize the Display controller and start it
	 */

	// Video Mode for each resolution
	//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
	//VideoMode VMODE = VMODE_800x600;
	//VideoMode VMODE = VMODE_1280x720;
	//VideoMode VMODE = VMODE_1280x1024;
	VideoMode VMODE = VMODE_1920x1080;
	//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

	Status = DisplayInitialize(&dispCtrl, &vdma, DISP_VTC_ID, DYNCLK_BASEADDR, pFrames, DEMO_STRIDE, VMODE);
	if (Status != XST_SUCCESS)
	{
		xil_printf("Display Ctrl initialization failed during demo initialization%d\r\n", Status);
	}

	Status = DisplayStart(&dispCtrl);
	if (Status != XST_SUCCESS)
	{
		xil_printf("Couldn't start display during demo initialization%d\r\n", Status);
	}

	DemoPrintTest(dispCtrl.framePtr[dispCtrl.curFrame], dispCtrl.vMode.width, dispCtrl.vMode.height, dispCtrl.stride);

	return 0;
}

void DemoPrintTest(u8 *frame, u32 width, u32 height, u32 stride)
{
	u32 xcoi, ycoi;
	u32 linesStart = 0;
	u32 pixelIdx = 0;

	for(ycoi = 0; ycoi < height; ycoi++)
	{
		for(xcoi = 0; xcoi < (width * 4); xcoi+=4)
		{

			//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

			/*
			// 800 x 600
			frame[linesStart + xcoi    ] = Pixel_800_600[pixelIdx++];  // Blue
			frame[linesStart + xcoi + 1] = Pixel_800_600[pixelIdx++];  // Green
			frame[linesStart + xcoi + 2] = Pixel_800_600[pixelIdx++];  // Red
			*/

			/*
			// 1280 x 720
			frame[linesStart + xcoi    ] = Pixel_1280_720[pixelIdx++];
			frame[linesStart + xcoi + 1] = Pixel_1280_720[pixelIdx++];
			frame[linesStart + xcoi + 2] = Pixel_1280_720[pixelIdx++];
			*/

			/*
			// 1280 x 1024
			frame[linesStart + xcoi    ] = Pixel_1280_1024[pixelIdx++];
			frame[linesStart + xcoi + 1] = Pixel_1280_1024[pixelIdx++];
			frame[linesStart + xcoi + 2] = Pixel_1280_1024[pixelIdx++];
			*/

			// 1920 x 1080
			frame[linesStart + xcoi    ] = Pixel_1920_1080[pixelIdx++];
			frame[linesStart + xcoi + 1] = Pixel_1920_1080[pixelIdx++];
			frame[linesStart + xcoi + 2] = Pixel_1920_1080[pixelIdx++];

			//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
		}

		linesStart += stride;
	}

	/*
	 * Flush the frame buffer memory range to ensure changes are written to the
	 * actual memory, and therefore accessible by the VDMA.
	 */
	Xil_DCacheFlushRange((unsigned int) frame, DEMO_MAX_FRAME);
}

这部分提供了 4 个不同分辨率图像的像素数据, 这些数据以头文件的形式包含在工程目录中,比如 pic_800_600.h。 以 800x600 图像像素为例,如下图,每个像素以三基色表示,一共 800x600x3 = 1440000 个数据点。屏幕左上角坐标(0,0)处第一个像素的蓝色,绿色,红色数值依次是 0xF1, 0xDC, 0xDB, 其它点的像素值依次类推。

pic_800_600.h 文件内容,其实就是定义图形常量:

const unsigned char Pixel_800_600[1440000] = {
0XF1,0XDC,0XDB,0XF1,0XDC,0XDB,0XF3,0XDE,0XDD,0XF4,0XDF,0XDE,0XF5,0XE0,0XDF,0XF6,0XE1,0XE0,
0XF7,0XE2,0XE1,0XF7,0XE2,0XE1,0XFC,0XE7,0XE6,0XFB,0XE7,0XE6,0XFB,0XE7,0XE6,0XFC,0XE8,0XE7,

...

};

主程序的开始部分是4个全局变量的定义

/*
 * Display Driver struct
 */
DisplayCtrl dispCtrl;
XAxiVdma vdma;

/*
 * Frame buffers for video data
 */
u8 frameBuf[DISPLAY_NUM_FRAMES][DEMO_MAX_FRAME];
u8 *pFrames[DISPLAY_NUM_FRAMES];  // array of pointers to the frame buffers

显示控制结构体 DisplayCtrl 和 VDMA 例化(读者可以按住 Ctrl 键的同时鼠标左键点进去看看这两个结构体包含的内容)。

然后是显示数据定义。

main 函数:

先初始化pFrames(frame buffers 的指针)

 get the hardware VDMA configurations  :vdmaConfig

初始化驱动

显示启动。

最后通过函数DemoPrintTest把显示数据复制到显示缓存,图像就显示了

程序结束

程序代码是针对1600x1200的,如果要更改方式,有3个地方需要修改:

1,#include "pic_1920_1080.h"

2:VideoMode VMODE = VMODE_1920x1080;

3:DemoPrintTest函数中

            // 1920 x 1080
            frame[linesStart + xcoi    ] = Pixel_1920_1080[pixelIdx++];
            frame[linesStart + xcoi + 1] = Pixel_1920_1080[pixelIdx++];
            frame[linesStart + xcoi + 2] = Pixel_1920_1080[pixelIdx++];

这3个地方都有//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

包括了,应该容易找到。

验证

编译好了,就可以验证显示了。

连接电脑 USB 到开发板 JTAG 口,连接电脑另一个 USB 到开发板 UART口,确保开发板启动模式设置为 JTAG,连接开发板 HDMI 口到 HDMI 显示器,给开发板上电。按照之前实验的操作方式连接串口(在本实验中,如果未发送错误则串口不打印信息)。其实每阶段运行完了,给个阶段显示更好,可以加点print。
Xilinx->program FPGA下载比特流,然后Run-> Run Configuration,做如下选择,然后Run

第一次的时候,我直接 Run As ->Launch on Hardware,结果没显示,按上面方法才显示,可能要等的时间长一点。我新做这个实验, Run As -> Launch on Hardware也能显示。

做这个实验还是花很多时间才完成,有点难。成功显示后还有点成就感,再做一遍,并写上此文。

猜你喜欢

转载自blog.csdn.net/leon_zeng0/article/details/113236364