本帖最后由 正点原子01 于 2020-9-8 11:00 编辑
1)实验平台:正点原子领航者ZYNQ开发板
2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-301505-1-1.html
4)对正点原子FPGA感兴趣的同学可以加群讨论:905624739 点击加入群聊:
5)关注正点原子公众号,获取最新资料更新
第十七章PS通过VDMA驱动LCD显示实验
AXI VDMA是Xilinx专门针对视频应用提供的一种高带宽的解决方案,旨在实现AXI4-Stream视频接口和AXI4接口之间的高带宽接入,可以方便地实现双缓冲和多缓冲机制。 本章我们将在PL端搭建VDMA的使用框架,并通过VDMA将PS端需要显示的数据显示在LCD上。本章包括以下几个部分: 1 1.1 简介 1.2 实验任务 1.3 硬件设计 1.4 软件设计 1.5 下载验证
1.1 简介AXI VDMA(AXI Video Direct Memory Access,以下简称VDMA),是Xilinx提供的软核IP。其功能和AXIDMA(以下简称DMA)有些类似,都可以为存储器或者AXI4-Stream类目标外设之间提供高带宽直接存储器存取。和DMA相比,VDMA增加了帧缓存的缓冲机制和同步锁相(GenLock)等功能,是为了针对视频图像应用而做的升级版的DMA。VDMA集成了视频专用功能,如帧同步和2D DMA传输等,非常适合基于ZYNQ架构上的图像和视频处理,缩短了开发周期。 DMA和VDMA的对比如表18.1.1所示。 表 15.1.1 DMA和VDMA的对比 对比项\IP核 | | | | 为存储器或者AXI4-Stream类目标外设提供高带宽存取 | 为存储器或者AXI4-Stream类目标外设提供高带宽存取;帧缓存和多种缓存机制; 帧同步等 | | | | | | |
这里我们引入了帧缓存的缓冲机制和动态同步锁相两个概念,接下来向大家详细介绍这两个概念。 帧缓存 首先我们来看下什么是帧缓存。帧缓存存储器(Frame Buffer),简称帧缓存,也常被称作显存,是为显示设备(如HDMI显示器、RGB LCD液晶屏等)提供数据缓存的一片存储区域。一般图像输入源和图像显示的传输速率不匹配(如图像输入源传输速度较快或者图像显示端传输速度较快),这个时候需要一片存储区域来缓存输入的数据,以便显示设备读取数据,同时也方便后续对视频数据做图像处理。帧缓存的每一个存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。 在实际应用中,一般选择外置的DDR3存储器作为帧缓存,而不是选择片内的BRAM,这是由于ZYNQ片内的BRAM存储容量非常小。我们知道,ZYNQ7010的BRAM存储容量为2.1Mbit,ZYNQ7020的存储容量为4.9Mbit,假设RGB LCD屏的分辨率为800*480,那么存储一帧图像需要的存储容量为(如:RGB888数据格式=24位)800*480*24=9216000bit≈8.79Mbit,整个BRAM的存储容量不足以存储单帧图像,更何况有时需要更多个显存的情况,因此使用外置的DDR3存储器作为帧缓存。 对于使用帧缓存存储器来缓存图像数据来说,可以采用单帧缓存或者多帧缓存的方案。单帧缓存是指图像的输入和图像的显示都是通过读写同一片存储区域来实现的。虽然也可以实现显示设备显示图像的功能,但这会带来一个问题,即读出的单帧图像是输入的两帧图像或者更多帧图像数据叠加在一起的结果,可能会导致显示设备显示的图像出现割裂的现象。单帧缓存方案的读写示意图如下图所示:
图 15.1.1 单帧缓存方案读写示意图 由上图可知,随着输入数据源的不断写入,单帧存储区域会出现单帧图像数据或者两帧图像数据叠加在一起的情况。当然,如果读出速度比写入速度慢很多的话,也可能出现更多帧图像叠加在一起,从而导致显示设备显示的图像出现割裂的现象。 对于本次RGB LCD屏彩条显示实验来说,由于彩条只需要写入一次,写入完成后,后续只需要进行读操作,因此本次实验使用单帧缓存的方案即可,那么采用DMA或者VDMA的方案区别不大。而对于需要频繁写入和读出的应用来说,比如摄像头的图像显示,如果仅仅使用单个帧缓存的话,势必会造成图像叠加的情况,这个时候就要采用VDMA的多帧缓存的方案了。本次实验旨在通过VDMA的RGB LCD屏显示彩条的实验,让大家熟悉并掌握VDMA IP核的使用方法,为后续学习摄像头的图像显示实验打下基础。 在VDMA IP核中,通过配置VDMA的同步锁相模式来配置帧缓存的缓冲进制,接下来我们向大家介绍VDMA的同步锁相模式。 同步锁相(Genlock) 在很多的视频应用中,图像输入端和输出端的数据速率不匹配,通常使用帧缓存来避免因速率不匹配而导致的潜在错误。为了解决单帧缓存区域带来的图像叠加问题,通过分配多个帧缓存区域保存数据,图像输入端在写入其中一个帧缓存时,输出端读取其它的帧缓存。 VDMA支持四种同步锁相模式,分别是Genlock Master(同步锁相主模式)、Genlock Slave(同步锁从模式)、Dynamic Genlock Master(动态同步锁相主模式)和DynamicGenlock Slave(动态同步锁相从模式)。 VDMA有一个写通道(S2MM)和一个读通道(MM2S),用户通过写通道将输入端数据写入帧缓存,通过读通道将从帧缓存中读出数据。VDMA的每一个通道都可以选择以上四种模式中的一种,接下来我们分别向大家介绍这四种同步模式。 Genlock Master 当写通道(S2MM)或者读通道(MM2S)配置为Genlock Master时,该通道不会跳过或者重复任一帧缓存区域,按照帧缓存顺序读出数据。配置为Genlock Slave的通道应当紧跟Genlock Master通道变化,但有一定的延迟,延迟的大小在寄存器(*frmdly_stride[28:24])中配置。 Genlock Slave 当写通道(S2MM)或者读通道(MM2S)配置为Genlock Slave时,该通道会通过跳过或者重复一些帧缓存区域的方式,尝试与Genlock Master通道同步。 Dynamic GenlockMaster 当写通道(S2MM)或者读通道(MM2S)配置为Dynamic Genlock Master时,该通道会跳过DynamicGenlock Slave通道正在操作的帧缓存,通过跳过或者重复一些帧缓存区域的方式来完成。 这里以分配三个帧缓存为例。当配置为Dynamic Genlock Master的通道访问帧缓存时,没有检测到Slave通道访问的帧缓存,那么它会循环访问帧缓存0 1 2 0 1 2;而如果检测到Slave访问的帧缓存区域,它们它会跳过该区域并开始访问下一帧缓存。因此,如果Slave通道长时间访问帧缓存1,则Master会循环访问帧缓存2和3。 Dynamic GenlockSlave 当写通道(S2MM)或者读通道(MM2S)配置为Dynamic Genlock Slave时,该通道会操作 Dynamic Genlock Master通道上一周期操作的帧。 下面以配置为Genlock模式为例,写通道(S2MM)配置为Genlock Master模式,读通道(MM2S)配置为Genlock Slave模式,操作示意图如图18.1.2所示:
图 15.1.2 Genlock Master/Slave示意图 s2mm_frame_ref:写通道访问的帧缓存; s2mm_fsync:写通道帧同步信号,下降沿表示VDMA开始进行写操作; mm2s_frame_ref:读通道访问的帧缓存; mm2s_fsync:读通道帧同步信号,下降沿表示VDMA开始进行读操作; 由上图可知,写通道在循环访问帧缓存0,1,2;而由于读通道的帧速率比写通道慢,读通道会紧跟写通道,在操作0之后,跳过1,而去处理2。 下面以配置为Dynamic Genlock模式为例,写通道(S2MM)配置为Dynamic Genlock Master模式,读通道(MM2S)配置为Dynamic Genlock Slave模式,操作示意图18.1.3所示:
图 15.1.3 DynamicGenlock Master/Slave示意图 在动态同步锁相模式下,Master会跳过Slave当前操作的帧缓存,Slave工作在Master上一次操作过的帧缓存。在上图中,写通道循环访问0,1,2,当再次返回至0时,由于Slave在操作0,所以Master跳过0,而去访问1。而读通道操作Master上一次访问过的帧缓存。 由此我们可以得出结论,如果想要避开读通道和写通道同时访问同一帧缓存,那么VMDA必须配置成动态同步锁相的模式,且帧缓存数量要大于等于3。由于分配过多的帧缓存区域对效率的提升已经微乎其微,且会占用更多的存储空间和消耗CPU的时间,因此在摄像头图像显示的应用中,帧缓存空间一般设置为3,并采用动态同步锁相的模式。 需要注意的是,VDMA只是Xilinx提供的IP核,本身不提供存储的功能,帧缓存一般设置在外置的存储器(如DDR3)中,VDMA只是提供了用于访问DDR3的接口。 VDMA概述 VDMA用于将AXI Stream格式的数据流转换为Memory Map格式或将Memory Map格式的数据转换为AXI Stream数据流,也就是说VDMA内核旨在提供从AXI4域到AXI4-Stream域的视频读/写传输功能,反之亦然,从而实现系统内存(主要指DDR3)和基于AXI4-Stream的目标视频IP之间的高速数据移动。VDMA的框图如下图所示:
图 15.1.4 VDMA的框图 AXI4-Lite可以对寄存器进行编程(配置),从而实现软件动态配置VDMA的功能。通过AXI4-Lite接口对寄存器进行编程后,控制/状态逻辑块会为DataMover生成适当的命令,以在AXI4主接口上启动写入和读取命令。可配置的异步line buffer用于在将像素数据写入AXI4-Memory Map接口或AXI4-Stream接口之前临时保存像素数据。 VDMA数据接口可以分为读、写两个通道,且写入和读取独立运行。用户可以通过写通道将AXI-Stream类型的数据流写入系统存储器(主要指DDR3)。在读通道中,VDMA使用AXI4主接口从系统存储器读取数据并在AXI4-Stream主接口上输出。可以看到,VDMA本质上是一个数据搬运的IP,可以看作是为视频图像处理做特殊优化的带有帧缓冲功能的高性能DMA,为数据进出系统存储器提供了一种便捷的方案。 VDMA内核不仅具有帧缓冲功能,而且集成了视频专用功能,如Gen-Lock和帧同步,用于完全同步的帧DMA操作和2D DMA传输。除了同步之外,还可以使用帧存储编号和scatter gather或寄存器直接模式操作,以便中央处理器控制。在本设计中,不使用VDMA scatter gather功能,因为可以使用VDMA的更简单的寄存器直接模式充分实现系统,从而避免实现scatter gather功能带来的面积成本。只有在系统需要对VDMA进行相对复杂的软件控制时,才应启用scatter gather。 1.2 实验任务本章的实验任务是PS写彩条数据至DDR3内存中,然后通过VDMA IP核将彩条数据显示在RGB LCD液晶屏上。 1.3 硬件设计VDMA IP核为存储器和AXI4-Stream类目标外设之间提供高带宽直接存储器存取,本身不产生RGB LCD的接口时序,因此本次实验除添加VDMA IP核外,还需要添加用于产生RGB LCD接口时序的IP核。ZYNQ中提供了AXI4-Streamto Video Out IP核,可以将VDMA输出的AXI4-Stream数据流转换成视频协议的数据流(包括并行数据、视频同步信号等),转换后的数据流符合RGB LCD接口的时序。另外,在添加AXI4-Stream to Video OutIP核之后,还需要添加Video Timing Controller(VTC)IP核,这个IP核为AXI4-Stream接口和视频输出接口提供了一个桥,用于控制视频输出的时序参数。 本次实验需要兼容正点原子推出的所有RGB LCD液晶屏,因此本次实验需要先获取LCD屏ID,然后对RGB LCD屏接口时序相关的IP核进行动态配置,以实现兼容所有RGB LCD屏的功能。 图18.3.1为本次实验的系统框图,粉红色的走线是控制流,绿色的走线是数据流。我们先来看数据流,彩条数据由PS端产生,产生的数据写入系统存储器DDR3内存中;VDMA通过AXI Smartconnect IP核与AXI_HP端口进行连接,从而高效访问DDR3。VDMA将从DDR3中读取的视频或图像数据传输给AXI4-Stream to Video Out IP核。AXI4-Stream toVideo Out IP核在VTC IP核的控制下,把AXI4-Stream格式的数据转换成视频输出的数据格式(如RGB888),并将输出的视频数据流连接至RGB2LCD IP核(rgb2lcd)的输入端。RGB2LCD IP核是本次实验自定义的IP核,实现了获取LCD屏的ID,以及将LCD屏的引脚封装到总线接口上,以方便将LCD引脚引出至顶层模块端口上。 最后我们再来看控制流,AXI Interconnect实现M_AXI_GP接口与外设配置接口的互联,从而实现PS控制PL端的外设。PS通过AXI GPIO IP核获取LCD屏ID,并根据获取到的ID,配置VDMA IP核的帧缓存空间大小、读通道等以及配置VTC IP核的输出的时序参数。由于不同分辨率的LCD屏其驱动时钟不一样,因此本次实验添加了AXI dynclk(动态时钟配置)的IP核,PS根据LCD屏的ID配置AXI dynclk输出不同的时钟。
图 15.3.1 VDMA系统框 首先创建Vivado工程,工程名为“vdma_lcd”。 1 配置ZYNQ Processing System 添加ZYNQ ProcessingSystem,完成UART和DDR3的基本配置外,本实验我们还需要使用S_AXI_HP0_Interface和PL到PS的中断。 首先在PS-PL Configuration栏开启S_AXI_HP0_Interface,如下图所示:
图 15.3.2 开启S_AXI_HP0_Interface 接下来配置时钟,FCLK_CLK0配置为100Mhz。需要说明的是,VDMA端口的各种总线都有自己的时钟,这些时钟是异步的,可以使用同一个时钟,可以使用不同的时钟。为了降低设计的复杂度,本次统一使用100Mhz的时钟,对于一些有特殊需求的应用,可以使用不同的时钟。时钟配置界面如下图所示:
图 15.3.3 配置时钟 配置完成后的ZYNQ ProcessingSystem IP如下图所示:
图 15.3.4 ZYNQ7 框图 2 添加并配置VDMA IP核。 2-1 添加VDMA IP。 点击“+”图标,在搜索框中输入“vdma”,添加该IP,如下图所示:
图 15.3.5 添加VDMA IP 2-2 配置VDMA。 双击打开VDMA配置界面,在Basic界面,Address Width保持默认32即可。Frame Buffers选项可以选择AXI VDMA要处理的帧缓冲存储位置的数量。对于大于32位的地址空间,最多允许8个帧缓存。由于对于本次彩条显示实验来说,数据只需要写入一次,因此不需要设置多个帧缓存区域,这里我们设置为1。因为本实验是从DDR3中读取数据输出给LCD,所以只需要勾选Enable Read Channel就可以了,无需勾选Enable Write Channel。 Memory Map DataWidth选项可以为MM2S通道选择所需的AXI4数据宽度。有效值为32、64、128、256、512和1024。此处保持默认64即可。 Read Burst Size用于指定突发读的大小,此处选择64。 Stream DataWidth选项可以选择MM2S通道的AXI4-Stream数据宽度。有效值是8的倍数,最大到1024。必须注意的是该值必须小于或等于Memory Map Data Width。此处因输出数据格式为RGB888,设置为24。 Line BufferDepth选项可以选择MM2S通道的行缓冲深度(行缓冲区宽度为streamdata 的大小),此处设置为2048即可。 Advanced界面用于配置读写通道的同步锁相模式,由于本次实验只有一个帧缓存,且只开启了读通道,多以同步锁相模式无需配置,保持默认就可以了。VDMA的配置如下图所示:
图 15.3.6 配置VDMA 配置完成后,VDMA的框图如下图所示:
图 15.3.7 AXI VDMA框图 AXI VDMA部分接口说明: S_AXI_LITE:AXI-Lite接口,PS通过这个接口配置VDMA的寄存器; s_axi_lite_aclk:AXI VDMA AXI4-Lite接口时钟; M_AXI_MM2S:读通道存储器端映射的AXI4接口,提供对存储器(DDR3)的访问; m_axi_mm2s_aclk:AXI VDMA MM2S时钟; M_AXIS_MM2S:读通道AXI-Stream端映射的AXI4接口,用于输出到外设; m_axis_mm2s_aclk:AXI VDMA MM2S AXIS时钟; mm2s_introut:读通道中断输出信号。 3 添加视频时序控制器Video Timing Controller 3-1 添加Video Timing Controller,如下图所示:
图 15.3.8 添加视频时序控制器 3-2 配置Video Timing Controller 在配置界面,确保勾选IncludeAXI4-Lite Interface,该选项将使用AXI4-Lite接口,该接口可以动态访问程序并更改处理参数,也就是说可以动态配置Video Timing Controller。勾选Enable Generation,使能Video Timing Controller生成并输出视频时序。因为本实验通过PS动态配置生成时序,所以无需勾选Enable Detection使能检测,其它选项保持默认就可以了。
图 15.3.9 配置视频时序控制器 配置完成后,VideoTiming Controller的框图如下图所示:
图 15.3.10 Video Timing Controller框图 Video TimingController部分接口说明: ctrl:Video Timing Controller配置接口,PS通过这个接口配置VTC的寄存器; clk:Video核时钟; gen_clken:Video Timing Generator时钟使能信号,高电平有效; vtiming_out:Video时序输出接口。 4 添加视频输出控制器Video Out 4-1 添加Video Out,如下图所示:
图 15.3.11 添加视频输出控制器 4-2 配置Video Out Clock Mode时钟模式用于指定AXI4-Stream输入和视频输出信号是使用公共时钟还是独立时钟进行时钟控制,此处我们使用独立时钟进行控制,即勾选Independent,取消勾选Common,其它选项保持默认即可。
图 15.3.12 配置视频输出控制器 配置完成后,Video Out的框图如下图所示:
图 15.3.13 AXI4-Stream to Video Out框图 AXI4-Stream toVideo Out部分接口说明: video_in:AXI-Stream接口输入数据流; vtiming_in:Video时序输入接口; vid_io_out:Video数据流输出接口; vtg_ce:VTC时钟使能信号。 5 添加自定义IP。 由于RGB LCD液晶屏有多种分辨率,所以我们需要一个获取LCD屏ID的模块,我们自定义的rgb2lcdIP核可以实现该功能,且该IP核把LCD引脚封装到一起,方便引出至端口。 不同的分辨率的LCD屏,其驱动时钟频率不同,所以需要使用一个动态时钟控制器来控制时钟的输出,来自开源软件的axi_dynclk IP可以实现该功能。这两个IP我们放在例程(vdma_lcd)的ip_repo目录,大家可以将ip_repo文件夹拷贝到自己的工程目录下,如下图所示:
图 15.3.14 自定义IP存放目录 拷贝完成后将IP核添加至工程中。打开设置界面,如下图所示:
图 15.3.15 点击设置按钮 在弹出的界面中,点击左侧的IP->Repository,单击右侧的“+”按钮,添加自定义IP库,如下图所示:
图 15.3.16 修改宏定义 在弹出的界面中选择自定义IP所存放的目录,本实验为F:\ZYNQ\Embedded_System\vdma_lcd\ip_repo,添加完成后,如下图所示,在弹出的“Add Repository”界面中直接单击“OK”按钮即可。
图 15.3.17 确认添加 6 添加rgb2lcd IP,如下图所示: 图 15.3.18 添加RGB转LCD输出的IP 双击打开配置界面,其中Reddepth、Greendepth和Buledepth用于设置输出的Red、Green和Blue颜色的位数,这里我们使用RGB888,所以,分别设置为8、8、8。Vid In Data Width用于指定输入的RGB总线宽度,为24位。
图 15.3.19 rgb2lcd IP核配置 rgb2lcd IP核将rgb2lcd.v文件封装成自定义的IP核,其模块代码如下: - module rgb2lcd #(
- //parameter define
- parameter VID_IN_DATA_WIDTH = 24,
- parameter REDDEPTH = 8,
- parameter GREENDEPTH = 8,
- parameter BULEDEPTH = 8,
- parameter VID_OUT_DATA_WIDTH = REDDEPTH + GREENDEPTH + BULEDEPTH
- )(
- //VID_OUT
- input [VID_IN_DATA_WIDTH-1:0 rgb_data ,
- input rgb_vde ,
- input rgb_hsync,
- input rgb_vsync,
- input pixel_clk,
- input vid_rst ,
-
- //RGBLCD
- output lcd_pclk ,
- output lcd_rst ,
- output reg lcd_hs ,
- output reg lcd_vs ,
- output reg lcd_de ,
- output lcd_bl ,
-
- //LCDID
- output reg [2:0 lcd_id ,
-
- //LCD数据引脚为双向引脚,改成三态引脚的形式
- input [VID_OUT_DATA_WIDTH-1:0 lcd_rgb_i,
- output [VID_OUT_DATA_WIDTH-1:0 lcd_rgb_o,
- output [VID_OUT_DATA_WIDTH-1:0 lcd_rgb_t
- );
- //reg define
- reg [VID_IN_DATA_WIDTH-1:0 int_pData ;
- reg rd_id_flag=1'b0;
- //wire define
- wire [REDDEPTH-1:0 lcd_red ;
- wire [GREENDEPTH-1:0 lcd_green ;
- wire [BULEDEPTH-1:0 lcd_blue ;
- //*****************************************************
- //** main code
- //*****************************************************
- //LCD 数据赋值
- always @(posedge pixel_clk or negedge vid_rst) begin
- if(!vid_rst)
- int_pData <= 0;
- else if (rgb_vde == 1'b1)
- int_pData <= rgb_data;
- else
- int_pData <= {VID_IN_DATA_WIDTH{1'b0}};
- end
- //LCD控制信号赋值
- always @(posedge pixel_clk or negedge vid_rst) begin
- if(!vid_rst) begin
- lcd_de <= 1'b0 ;
- lcd_hs <= 1'b0;
- lcd_vs <= 1'b0;
- end
- else begin
- lcd_de <= rgb_vde ;
- lcd_hs <= rgb_hsync;
- lcd_vs <= rgb_vsync;
- end
- end
- //读取LCD屏ID
- always @(posedge pixel_clk or negedge vid_rst) begin
- if(!vid_rst) begin
- lcd_id <= 3'd0;
- rd_id_flag <= 1'b0;
- end
- else if(!rd_id_flag) begin
- lcd_id <= {lcd_rgb_i[BULEDEPTH - 1],lcd_rgb_i[GREENDEPTH+ BULEDEPTH - 1],
- lcd_rgb_i[REDDEPTH +GREENDEPTH +BULEDEPTH - 1]};
- rd_id_flag <= 1'b1;
- end
- end
- assign lcd_pclk = pixel_clk;
- assign lcd_rst = vid_rst;
- assign lcd_bl = 1'b1;
- assign lcd_red =int_pData[VID_IN_DATA_WIDTH - 1:VID_IN_DATA_WIDTH - REDDEPTH];
- assign lcd_green = int_pData[VID_IN_DATA_WIDTH/3 * 2 - 1:
- VID_IN_DATA_WIDTH/3 * 2 - GREENDEPTH];
- assign lcd_blue = int_pData[VID_IN_DATA_WIDTH/3 - 1:VID_IN_DATA_WIDTH/3 - BULEDEPTH];
- assign lcd_rgb_o = rd_id_flag ? {lcd_red,lcd_green,lcd_blue}
- : {VID_OUT_DATA_WIDTH{1'bz}};
- assign lcd_rgb_t = ~{VID_OUT_DATA_WIDTH{rd_id_flag}};
- endmodule
复制代码这个模块的代码比较简单,这里有几点需要注意。首先LCD屏的数据引脚是双向的引脚,由于在封装IP核的时候不能直接定义成inout信号(软件最终会综合成输出的引脚),因此这里需要将LCD的数据引脚定义成三个引脚。lcd_rgb_i为输入的24位LCD数据信号,用于获取LCD屏的ID;lcd_rgb_o为输出到LCD屏的像素数据;lcd_rgb_t用于控制LCD数据引脚的方向。这三个信号在自定义IP核的时候增加一个GPIO的总线,即可在端口上以一个双向引脚的形式存在。 程序中的rd_id_flag信号为读ID的标志,初始值为0。复位信号结束后,在第一个时钟的上升沿时刻,lcd_rgb_t的值为1,表示双向引脚作为输入,此时读取LCD屏的ID,LCD屏的ID实际上只需读取RGB颜色分量的高位数据,并将rd_id_flag信号赋值为1。在rd_id_flag的值为高电平之后,lcd_rgb_t的值为0,双向引脚用做输出,开始输出LCD屏的像素数据。 7 添加动态时钟控制器,如下图所示。无需配置。
图 15.3.20 添加动态时钟控制器 8 添加GPIO IP核 PS通过AXI GPIO获取LCD屏的ID,接下来添加AXI GPIO IP核。点击Add IP按钮,在搜索框中输入“AXI GPIO”,双击AXI GPIO IP核,将IP核添加进来,该IP核配置界面如下:
图 15.3.21 配置AXI GIPIO 将GPIO设置为输入,位宽设置为3位即可。 9 连线。 因为Vivado的自动连线功能不可能完全按照我们的需求进行连接,我们在进行自动连线之前我们先手动连接部分接口,避免Vivado错误连接,手动连线如下图所示:
图 15.3.22 手动连线 这里是将动态时钟IP核输出的时钟连接至LCD屏时序控制相关的IP核,另外将rgb2lcdIP核输出的lcd_id端口连接至AXI GPIO IP核,以供PS读取LCD屏的ID。 接下来点击界面上方“RunBlock Automation”,在弹出的对话框左侧确认勾选processing_system7_0,然后点击“OK”。这一步骤会将ZYNQ7 PS模块的DDR和FIXED_IO端口引出。 然后点击“RunConnnection Automation”,下面列出了会自动连接的模块及其接口,勾选“All Automation”,然后点击“OK”按钮。 此时系统会自动生成AXIInterconnect和AXI Smartconnect。AXIInterconnect(ps7_0_axi_periph)用于桥接ZYNQ处理器M_AXI_GP0总线和外部低速外设的AXI_LITE总线;AXI Smartconnect(axi_smc)用于连接ZYNQ处理器的HP0接口和VDMA的M_AXI_MM2S 总线。另外系统也自动生成了1个reset模块(rst_ps7_0_100M),用于复位AXI_Lite总线上的外设。 Vivado自动连线完成后,有部分信号Vivado无法自动连接,需我们手动连接,如下图所示: 图 15.3.23 需手动连接的信号 10 引出rgb2lcd的输出引脚。由于在硬件上,LCD复位信号和PL复位信号直接连接在一起,因此LCD的复位端口信号不用引出,其余的LCD端口信号需要引出,如图 18.3.24所示。
图 15.3.24 引出引脚 整体系统架构图如下:
图 15.3.25 整体系统架构连接图 到这里我们的Block Design就设计完成了,在Diagram窗口空白处右击,然后选择“Validate Design”验证设计。验证完成后弹出对话框提示“ValidationSuccessful”表明设计无误,点击“OK”确认。最后按快捷键“Ctrl + S”保存设计。 接下来在Source窗口中右键点击Block Design设计文件“system.bd”,然后依次执行“Generate Output Products”和“Create HDLWrapper”。 11 添加引脚约束。 引脚约束如下: - #----------------------LCD接口---------------------------
- set_property-dict {PACKAGE_PIN Y18 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[0]}]
- set_property-dict {PACKAGE_PIN Y19 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[1]}]
- set_property-dict {PACKAGE_PIN W20 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[2]}]
- set_property-dict {PACKAGE_PIN V20 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[3]}]
- set_property-dict {PACKAGE_PIN U14 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[4]}]
- set_property-dict {PACKAGE_PIN U15 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[5]}]
- set_property-dict {PACKAGE_PIN T20 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[6]}]
- set_property-dict {PACKAGE_PIN U20 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[7]}]
- set_property-dict {PACKAGE_PIN W14 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[8]}]
- set_property-dict {PACKAGE_PIN Y14 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[9]}]
- set_property-dict {PACKAGE_PIN N15 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[10]}]
- set_property-dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[11]}]
- set_property-dict {PACKAGE_PIN V16 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[12]}]
- set_property-dict {PACKAGE_PIN W16 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[13]}]
- set_property-dict {PACKAGE_PIN W18 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[14]}]
- set_property-dict {PACKAGE_PIN W19 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[15]}]
- set_property-dict {PACKAGE_PIN T10 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[16]}]
- set_property-dict {PACKAGE_PIN T11 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[17]}]
- set_property-dict {PACKAGE_PIN P14 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[18]}]
- set_property-dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[19]}]
- set_property-dict {PACKAGE_PIN V13 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[20]}]
- set_property-dict {PACKAGE_PIN U13 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[21]}]
- set_property-dict {PACKAGE_PIN G15 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[22]}]
- set_property-dict {PACKAGE_PIN H15 IOSTANDARD LVCMOS33} [get_ports {lcd_rgb_tri_io[23]}]
- set_property-dict {PACKAGE_PIN U17 IOSTANDARD LVCMOS33} [get_ports lcd_hs]
- set_property-dict {PACKAGE_PIN P20 IOSTANDARD LVCMOS33} [get_ports lcd_vs]
- set_property-dict {PACKAGE_PIN N20 IOSTANDARD LVCMOS33} [get_ports lcd_de]
- set_property-dict {PACKAGE_PIN Y16 IOSTANDARD LVCMOS33} [get_ports lcd_bl]
- set_property-dict {PACKAGE_PIN T16 IOSTANDARD LVCMOS33} [get_ports lcd_clk]
复制代码最后在左侧FlowNavigator导航栏中找到PROGRAM AND DEBUG,点击该选项中的“Generate Bitstream”,对设计进行综合、实现、并生成Bitstream文件。 在生成Bitstream之后,在菜单栏中选择 File > Export > Export hardware导出硬件,并在弹出的对话框中,勾选“Include bitstream”。然后在菜单栏选择File > LaunchSDK,启动SDK软件。 1.4 软件设计在将硬件导出至 SDK,并打开 SDK 开发环境后,创建应用工程的步骤都是一样的,这里不再赘述,新创建的空白应用工程命名为vdma_lcd。 在SDK软件中新建一个BSP工程和一个空的应用工程,应用工程名为“vdma_lcd”。SDK中需要添加的源代码较多,大家可以从提供的例程中拷贝SDK的源文件,需拷贝的文件如下:
图 15.4.1 SDK源文件 在硬件设计部分我们添加了动态时钟控制器axi_dynclk IP核,该IP核来自Digilent的开源文件,这里我们手动添加配置该IP核的C源码文件,从而控制该IP核生成不同频率的时钟信号以驱动不同分辨率的LCD。由于开源文件display_ctrl里的VDMA配置只支持VDMA的读通道,不支持写通道,为了兼容后面摄像头的图像显示实验,本次实验对开源文件做了简单的修改,把文件中VDMA相关的代码删除,将VDMA相关API函数放在单独的vdma_api文件夹内。除此之外,display_ctrl文件夹内的函数也实现了获取LCD屏ID的功能。 vdma_api文件夹存放了对VDMA相关操作的API函数,该文件来源于Xilinx官方提供的VDMA模板,如下图所示:
图 15.4.2 VDMA模块例程 默认提供的模板同时打开写通道和读通道的,由于本次试验不需要打开写通道,所以对文件内的函数作了修改,通过函数输入的形参来判断是否打开读通道或者写通道。 main.c文件中的宏定义、全局变量等相关内容,代码如下: - #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include "xil_types.h"
- #include "xil_cache.h"
- #include "xparameters.h"
- #include "xgpio.h"
- #include "xaxivdma.h"
- #include "xaxivdma_i.h"
- #include "display_ctrl/display_ctrl.h"
- #include "vdma_api/vdma_api.h"
- //宏定义
- #define BYTES_PIXEL 3 //像素字节数,RGB888占3个字节
- #define DYNCLK_BASEADDR XPAR_AXI_DYNCLK_0_BASEADDR //动态时钟基地址
- #define VDMA_ID XPAR_AXIVDMA_0_DEVICE_ID //VDMA器件ID
- #define DISP_VTC_ID XPAR_VTC_0_DEVICE_ID //VTC器件ID
- #define AXI_GPIO_0_ID XPAR_AXI_GPIO_0_DEVICE_ID //PL端 AXI GPIO 0(lcd_id)器件ID
- #define AXI_GPIO_0_CHANEL 1 //PL按键使用AXI GPIO(lcd_id)通道1
- //函数声明
- void colorbar(u8 *frame, u32 width, u32 height, u32 stride);
- //全局变量
- XAxiVdma vdma;
- DisplayCtrl dispCtrl;
- XGpio axi_gpio_inst; //PL端 AXI GPIO 驱动实例
- VideoMode vd_mode;
- //frame buffer的起始地址
- unsigned int const frame_buffer_addr = (XPAR_PS7_DDR_0_S_AXI_BASEADDR+ 0x1000000);
- unsigned int lcd_id=0; //LCD ID
复制代码在代码第26行, 我们定义了一个DisplayCtrl 结构体变量 dispctrl,DisplayCtrl是在displya_ctrl.h文件下定义的结构体,包含 dynclk IP的基地址、vtc结构体,视频分辨率video mode等信息,用于显示控制,具体定义如下图所示:
图 15.4.3 DisplayCtrl 结构体 在代码的第28行,定义了一个VideoMode结构体变量vd_mode,VideoMode是在lcd_modes.h文件下定义的结构体,如下图所示:
图 15.4.4 VideoMode结构体 不同分辨率的LCD屏,需要为这个结构体所包含的变量赋不同的值,具体变量的赋值方法在注释中已给出,如hps的值为LCD水平方向有效像素+行显示前沿。为了方便赋值,我们在lcd_modes.h文件下针对不同分辨率的LCD屏,分别赋值了不同的参数,在获取到LCD屏的ID直接选择对应的参数即可,如7寸LCD屏(分辨率800*480)的参数如下图所示:
图 15.4.5 7寸LCD屏(分辨率800*480)的参数 在代码的第30行定义了VDMA的帧缓存区域位于DDR3中的起始地址(frame_buffer_addr),写彩条和VDMA的读通道都是从这个起始地址开始操作的。 main.c文件中的函数代码如下: - int main(void)
- {
- //获取LCD的ID
- XGpio_Initialize(&axi_gpio_inst,AXI_GPIO_0_ID);
- lcd_id = LTDC_PanelID_Read(&axi_gpio_inst,AXI_GPIO_0_CHANEL);
- xil_printf("LCDID: %x\r\n",lcd_id);
- //根据获取的LCD的ID号来进行video参数的选择
- switch(lcd_id){
- case 0x4342 : vd_mode =VMODE_480x272; break; //4.3寸屏,480*272分辨率
- case 0x4384 : vd_mode =VMODE_800x480; break; //4.3寸屏,800*480分辨率
- case 0x7084 : vd_mode =VMODE_800x480; break; //7寸屏,800*480分辨率
- case 0x7016 : vd_mode =VMODE_1024x600; break; //7寸屏,1024*600分辨率
- case 0x1018 : vd_mode =VMODE_1280x800; break; //10.1寸屏,1280*800分辨率
- default : vd_mode = VMODE_800x480; break;
- }
- //配置VDMA
- run_vdma_frame_buffer(&vdma, VDMA_ID, vd_mode.width, vd_mode.height,
- frame_buffer_addr,0, 0,ONLY_READ);
- //初始化Displaycontroller
- DisplayInitialize(&dispCtrl, DISP_VTC_ID, DYNCLK_BASEADDR);
- //设置VideoMode
- DisplaySetMode(&dispCtrl, &vd_mode);
- DisplayStart(&dispCtrl);
- //写彩条
- colorbar((u8*)frame_buffer_addr, vd_mode.width,
- vd_mode.height, vd_mode.width*BYTES_PIXEL);
- return 0;
- }
复制代码在main函数中,我们首先需要获取LCD屏的ID,才能对VDMA、VTC、动态时钟做配置,如代码中第35行至第38行代码所示。PS通过AXI GPIO获取LCD屏的ID,因此先对AXI GPIO做初始化,LTDC_PanelID_Read()函数返回的值即为LCD屏的ID。LTDC_PanelID_Read()函数根据读取到的AXI GPIO的引脚数据映射成16位的值,以方便指示当前的LCD屏尺寸和分辨率。当AXI GPIO引脚为000时,函数返回0x4342,表示LCD屏为4.3寸屏,分辨率为480*272;001对应0x7084;010对应0x7016;100对应0x4384;101对应0x1018。 在main函数的第40行至第48行代码,根据不同的LCD屏ID,为vd_mode结构体变量赋不同的值。接下来通过run_vdma_frame_buffer()函数配置VDMA,包括配置帧缓存地址、读通道配置、开启读通道等,如程序中第51行和第52行代码所示。需要说明的是,该函数输入的最后一个参数用来配置VDMA的读写通道,当该参数为ONLY_READ时表示只开启读通道;ONLY_WRITE表示只开启写通道;BOTH表示既开启读通道,也开启写通道。 在main函数的第54行至第58行代码对VTC和动态时钟进行配置,并启动VTC开始运行。在main函数的第61行,通过colorbar()函数向帧缓存区域中写入彩条数据,写彩条的函数同样位于main.c文件中,代码如下: - //写彩条函数(彩虹色)
- void colorbar(u8 *frame, u32 width, u32 height, u32 stride)
- {
- u32 color_edge;
- u32 x_pos, y_pos;
- u32 y_stride = 0;
- u8 rgb_r, rgb_b, rgb_g;
- color_edge = width * BYTES_PIXEL / 7;
- for (y_pos = 0; y_pos < height; y_pos++) {
- for (x_pos = 0; x_pos < (width * BYTES_PIXEL); x_pos += BYTES_PIXEL) {
- if (x_pos < color_edge) { //红色
- rgb_r = 0xFF;
- rgb_g = 0;
- rgb_b = 0;
- } else if ((x_pos >= color_edge) && (x_pos < color_edge * 2)) { //橙色
- rgb_r = 0xFF;
- rgb_g = 0x7F;
- rgb_b = 0;
- } else if ((x_pos >= color_edge * 2) && (x_pos < color_edge * 3)) { //黄色
- rgb_r = 0xFF;
- rgb_g = 0xFF;
- rgb_b = 0;
- } else if ((x_pos >= color_edge * 3) && (x_pos < color_edge * 4)) { //绿色
- rgb_r = 0;
- rgb_g = 0xFF;
- rgb_b = 0;
- } else if ((x_pos >= color_edge * 4) && (x_pos < color_edge * 5)) { //青色
- rgb_r = 0;
- rgb_g = 0xFF;
- rgb_b = 0xFF;
- } else if ((x_pos >= color_edge * 5) && (x_pos < color_edge * 6)) { //蓝色
- rgb_r = 0;
- rgb_g = 0;
- rgb_b = 0xFF;
- } else if ((x_pos >= color_edge * 6) && (x_pos < color_edge * 7)) { //紫色
- rgb_r = 0x8B;
- rgb_g = 0;
- rgb_b = 0xFF;
- }
- frame[x_pos + y_stride + 0 = rgb_b;
- frame[x_pos + y_stride + 1 = rgb_g;
- frame[x_pos + y_stride + 2 = rgb_r;
- }
- y_stride += stride;
- }
- Xil_DCacheFlush(); //刷新Cache,数据更新至DDR3中
- xil_printf("showcolor bar\r\n");
- }
复制代码colorbar()函数实现了一个彩虹色(红橙黄绿青蓝紫)的七色彩条。在函数的最后通过调用Xil_DCacheFlush()函数将缓存在DataCache中的数据刷新到DDR3中,以便VDMA进行读取。 程序设计完成后,按快捷键Ctrl+S保存main.c文件,工具会自动进行编译。编译完成后控制台(Console)中会出现提示信息“Build Finished”,同时在应用工程的Binaries目录下可以看到生成的elf文件。 1.5 下载验证首先我们将下载器与领航者底板上的JTAG接口连接,下载器另外一端与电脑连接。然后使用Mini USB连接线将USB UART接口与电脑连接,用于串口通信。接下来使用FPC排线一端与RGB LCD液晶屏上的接口连接,另一端连接领航者底板上的RGB LCD接口,如图 18.5.1和图 18.5.2所示。连接时,先掀开FPC连接器上的黑色翻盖,将FPC排线蓝色面朝上插入连接器,最后将黑色盖压下以固定FPC排线。
图 15.5.1 RGB LCD液晶屏
图 15.5.2 开发板连接图 在SDK软件下方的SDK Terminal窗口中点击右上角的加号设置并连接串口。然后下载本次实验硬件设计过程中所生成的BIT文件,来对PL进行配置。最后下载软件程序,下载完成后,下载完成后,在下方的SDK Terminal中可以看到应用程序打印的信息,如下图所示:
图 15.5.3 Terimnal打印的信息 由于本次实验连接的是7寸液晶屏(分辨率:800*480),因此串口打印的LCD ID为7084。接下来观察RGB LCD液晶屏,可以看到,LCD上显示出彩条,彩条颜色从左到右依次为红橙黄绿青蓝紫,与写入的彩虹色数据相同,如图 18.5.4所示。
图 15.5.4 RGB LCD液晶屏显示彩条
|