《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​
  95kVyaJuybju 2023年11月12日 17 0

OV5640 DP显示实验​

在前面的例程中大家学习了DP的彩条显示和从SD卡中读取图片显示都是比较简单的例程。本节实验将在前面的基础上增加难度带领大家学习如何使用DP接口去显示OV5640摄像头采集到的视频流数据。本节实验还会用到自定义AXI4接口的IP核,所以在本节实验中还会给大家详细讲解AXI4协议相关的知识。

  1. 简介
  2. 实验任务
  3. 硬件设计
  4. 软件设计
  5. 下载验证



简介

AXI4接口总共有三种类型,它们分别是AXI4(AXI4-Full)AXI4-LiteAXI4-Stream,不同的接口类型适用于不同的应用场景,下面对这三个接口做简要说明。

AXI4-Full最高性能的接口适合存储器映射的通信支持每个地址阶段最高256个数据传输周期的批量传输适合于更需要持久、高速性能的IP

AXI4-LiteAXI4-Full接口的轻量级版本用于存储器映射的单次数据通信会话。这个版本的好处是简化了的接口占用较少的逻辑部分面积。这个版本不支持批量数据只支持每次传输单个数据适合于需要最小硬件消耗的较低性能的IP。

AXI4-Stream它没有地址阶段因此不是存储器映射为流式数据的传输定义了单个通道支持数量限制的批量传输最适合于需要持续固定数据流的应用。连接只能是从主机到从机所以如果需要双向传输的话两个外围设备都必须是主机/从机兼容类型的

本次实验需要通过AXI4接口源源不断地把摄像头中的数据存储到DDR中去,因此数据量较大,由于AXI4-Lite速度稍慢,不适合本实验;AX14-Stream虽然本身占用资源不多,但是为了和AXI互联模块连接,需要接AXI DMA或者其它模块,然而本节实验不需要DMA等其他模块(之所以不采用VDMA来传输数据是因为DP采用的是直接读取内存作为数据源的模式,而VDMA在往DDR中写入数据的时候我们不知道其当前帧在DDR的什么位置。本节实验的数据源来自摄像头,输出是DP接口,这二者的数据传输速度不一致,因此我们需要做一个帧的乒乓操作,防止读写重叠到同一帧,读写重叠就会造成画面有撕裂感,尤其是画面快速晃动的时候。而要想做到读写帧乒乓切换就必须要知道写数据进行到DDR的什么位置,读数据进行到DDR的什么位置,这样才能让二者错开,由于VDMA的自动运行机制不知道当前写的帧进行到DDR的具体位置因此不适合在本节实验使用,当然如果数据输出是可以直接连接VDMA的数据输出AXI接口那么使用VDMA还是比较方便的,例如前面的“OV5640 HDMI实验”),因此AXI4-Stream接口也不适合;AXI4(AXI4-Full)接口无论在资源占用还是速度方面均能满足本实验的需求,故本实验采用的接口为AXI4

AXI4协议具有5个独立的通道,分别为:读地址通道读数据通道写地址通道写数据通道和写响应通道,通道之间相互独立且存在差别。通信是由机发起的,机可以对机进行数据的读或写操作。每次读或写操作都需要相应的读地址通道或写地址通道传输一个地址。数据传输使用写数据通道来实现到从写数据传输,数据传输使数据通道用来实现从到主的读数据传输。下面以AXI4 IP核为例,详细介绍AXI4协议的各通道和通道接口。

在本实验中FPGA对外部DDR4写入数据,可视为主机,所在例化、封装IP时应选择“Master”作为主机,接口名字以m_为前缀(具体步骤会在本章中详细描述,我们先直接拿来为大家讲解相关的通道和接口)。AXI4 IP核未展时开如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP


图29.1.1 未展开时接口

上图红标1处为IP核的时钟和复位信号,红标2处M_AXI为通道接口,其他为数据验证信号。下面展开红标2处,介绍M_AXI接口,如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_数据_02


图29.1.2 展开时通道和接口

在上图中,以m_axi_aw_为前缀的接口属于写地址通道;以m_axi_w_为前缀的接口属于写数据通道;以m_axi_b_为前缀的接口属于写响应通道;以m_axi_ar_为前缀的接口属于读地址通道;以m_axi_r_为前缀的接口属于读数据通道。

写地址通道包含的信号及信号含义如下表所示:

29.1.1 通道信号

信号名程

来源

描述

m_axi_awid

写地址ID,是写地址信号组的标记(值通常为0)

m_axi_awaddr

写地址,一次突发数据传输的地址

m_axi_awlen

突发长度,一次突发数据传输的准确个数(INCR下最多256

m_axi_awsize

突发大小,突发中每次传输的大小

m_axi_awburst

突发类型(值通常为2'b01INCR

m_axi_awlock

锁类型(值通常为0)

m_axi_awcache

缓存类型(值通常为4'b0010

m_axi_awprot

保护类型(值通常为0)

m_axi_awqos

质量服务信号,可作为安全级标志(值通常为0)

m_axi_awuser

用户定义信号(值通常为1

m_axi_awvalid

写地址有效,表明该通道的地址和控制信息有效

m_axi_awready

从机准备好,表明从机可以接收地址和控制信息

写数据通道包含的信号及信号含义如下表所示:

29.1.2 通道信号

信号名程

来源

描述

m_axi_wdata

突发写数据

m_axi_wstrb

写选通,表示突发写数据中有效的字节,一位对应一个字节

m_axi_wlast

一次突发写数据传输的最后一个数据

m_axi_wuser

用户定义信号(值通常为0

m_axi_wvalid

写数据有效,表明该通道的写数据有效

m_axi_wready

从机准备好,表明从机可以接收写数据

写响应通道包含的信号及信号含义如下表所示:

29.1.3 通道信号

信号名程

来源

描述

m_axi_bid

突发写响应ID,写响应识别的标记

m_axi_bresp

突发写响应,表明写事务的状态

m_axi_buser

用户定义信号

m_axi_bvalid

突发写响应有效,表明该通道的写数据有效

m_axi_bready

主机准备好,表明主机可以接收写数据

读地址通道包含的信号及信号含义如下表所示:

29.1.4 通道信号

信号名程

来源

描述

m_axi_arid

读地址ID,是读地址信号组的标记(值通常为0)

m_axi_araddr

读地址,一次突发数据传输的地址

m_axi_arlen

突发长度,一次突发数据传输的准确个数(INCR下最多256

m_axi_arsize

突发大小,突发中每次传输的大小

m_axi_arburst

突发类型(值通常为2'b01INCR

m_axi_arlock

锁类型(值通常为0)

m_axi_arcache

缓存类型(值通常为4'b0010

m_axi_arprot

保护类型(值通常为0)

m_axi_arqos

质量服务信号,可作为安全级标志(值通常为0)

m_axi_aruser

用户定义信号(值通常为1

m_axi_arvalid

读地址有效,表明该通道的地址和控制信息有效

m_axi_arready

从机准备好,表明从机可以接收地址和控制信息

读数据通道包含的信号及信号含义如下表所示:

29.1.5 通道信号

信号名程

来源

描述

m_axi_rid

一次突发读传输ID

m_axi_rdata

突发读数据

m_axi_rresp

读响应,表明突发读传输的状态

m_axi_rlast

一次突发读数据传输的最后一个数据

m_axi_ruser

用户定义信号

m_axi_rvalid

读数据有效,表明该通道的读数据有效

m_axi_rready

从机准备好,表明从机可以接收读数据

在进行数据传输的过程中,传输通道均使用valid/ready信号对传输过程的地址、数据、控制信号进行握手。使用双向握手机制,valid和ready有效的时候表示握手成功。下面将以地址通道为例介绍几种常见的握手方式。

validready前有效:主机先给出数据和控制信,同时驱动valid为高电平。一旦主机驱动valid为高,地址将保持不变,直到从机驱动ready信号为高。一旦从机驱动ready为高,则握手成功,在时钟上升沿T3时刻开始进行地址如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_03


图29.1.3 valid在ready之前

validready后有效:从机在主机驱动valid前,就驱动了ready信号为高。一旦主机确定valid信号为高,则握手成功,在T3时刻开始传输地址。如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_写数据_04


图29.1.4 valid在ready之后

validready同时有效:主机驱动valid和从机驱动ready同时发生,在T2时刻开始传输地址。如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_数据_05


图29.1.5 valid和ready同时有效

AXI4读通道结构如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_06


图29.1.6读通道结构

由上图可知,主机先传递地址和控制信息给从机之后从机将有效的地址上对应的读数据批量读数据发送给主机。在突发读过程中,读通道上典型的信号交互过程如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_数据_07


图29.1.7 突发读信号交互

由上图可知在读地址通道中:主机在T0时间段内先提供地址m_axi_araddr,同时将m_axi_arvalid拉高;从机在T1时间段内将m_axi_arready拉高,表明从机可以接收地址;m_axi_aradd和m_axi_arready均为高时则握手成功,在T2时钟上升沿时刻,开始传输地址。

在读数据通道中:主机在T3时间段内将m_axi_rready拉高,表明主机可以接收数据;从机在T5时间段内提供读数据同时将m_axi_rvalid拉高,表明数据D0有效可以读出;m_axi_rvalid和m_axi_rready均为高时则握手成功,在T6时钟上升沿时刻,开始传输数据D0。同理在T9、T10时钟上升沿时刻,分别输数据D1、D2。在T13时钟上升沿时刻,从机拉高m_axi_rlast,表明D3是此次突发最后一个需要传输的数据。

AXI4写通道结构如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_08


图29.1.8写通道结构

由上图可知,主机首先传递地址和控制信息,再发送批量写数据给从机。从机接收完所有的数据后,从机发送一个写响应信号给主机突发写过程中写通道上典型的信号交互过程如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_数据_09


图29.1.9 突发写信号交互

由上图可知在写地址通道中:主机在T0时间段内先提供地址m_axi_awaddr,同时将m_axi_awvalid拉高;从机在T1时间段内将m_axi_awready拉高,表明从机可以接收地址;m_axi_awaddr和m_axi_awready均为高时则握手成功,在T2时钟上升沿时刻,开始传输地址。

在写数据通道中:主机在T2时间段内先提供写数据D0,同时将m_axi_awvalid拉高,表明该数据有效;从机在T3时间段内将m_axi_wready拉高,表明从机可以接收数据;m_axi_awvalid和m_axi_wready均为高时则握手成功,T4时钟上升沿时刻,开始传输数据D0。同理,在T6、T8、T9时钟上升沿时刻,分别传输数据D1、D2、D3;

在写响应通道中:主机在T2时间段内将m_axi_bready拉高,表示主机可以接收来自从机的写响应信号;从机接收此次突发最后一个传输的写数据D3时,在T9时间段提供些响应信号OKAY,同时将m_axi_bvalid拉高,表明写响应信号有效。m_axi_bvalid和m_axi_bready均为高时则握手成功,T10时钟上升沿时刻,开始传输OKAY。注意:写响应信号必须跟随最后一次突发的写传输数据。

多个具有AXI4协议的设备或模块可以通过互联模块进行数据的交互。如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_10


图29.1.10 多机互联

实验任务

本节实验是通过自定义一个AXI4接口的IP核将OV5640摄像头的数据写入PS端的DDR4,然后通过MPSOC自带的DPDMA将DDR的数据读取出来并打包成DP格式的数据流发送出去,最终通过DP转HDMI线实现HDMI显示屏显示DP图像的功能。我们使用双目OV5640摄像头中的COMOS1摄像头完成本实验,双目摄像头插在MPSOC扩展板的J19扩展口,摄像头朝外。

硬件设计

根据实验任务可知本次实验的数据流是从OV5640摄像头进入DDR4,再从DDR4读取出来从DP接口输出,其中从摄像头到DDR需要由自定义AXI4 IP核去执行,我们画出如下框图:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_11


29.3.1 OV5640 DP显示结构框图

从上图中我们可以很清晰的看到本次实验整个数据流的走向了,下面我们对每个模块的功能做一下详细的讲解。

OV5640控制模块:这个模块在前面的“OV5640 LCD显示实验”中大家已经了解它的作用,这里不再重复赘述,但是需要注意的是本次实验在原来的基础上将像素数据拼接成32bit的RGBA数据,并生成一个帧复位信号,详细代码在后文会具体讲解。

FIFO缓冲:本节实验需要用到一个FIFO来缓冲数据。FIFO中每当存满256个数据,AXI4_RW模块就会将这256个数据读取出来写入DDR4

AXI4_RW模块:AXI4_RW模块是本节实验最重要的一个模块也是最难的一个模块。上文说到FIFO中每当存满256个数据,AXI4_RW模块就会将这256个数据读取出来写入DDR4。那么为什么要存满256个数据就写入DDR一次呢?这主要取决于AXI4 IP核本身的性质,通过前文简介部分我们可以知道AXI4 协议在读写数据时采用的是突发模式。它支持2的整数次方突发长度去读写数据,但是最大长度为256,也就是说我们自定义的AXI4_RW模块最多一次性可以写入DDR 256个数据(注意这里是256个数据不是256bit数据,一个数据可以是8bit、16bit、32bit等等,本节实验一个数据是32bit)。因此我们每当FIFO中存够256个数据就完成一次突发写操作。

这里还有一点要说明就是大家不要让FIFO存储超过256个数据,因为AXI4_RW模块的写突发开启条件用的是FIFO中数据量大于一定值就完成一次写突发(当然如果大家自己改写代码采用其他触发机制就另当别论)。这样的机制下一旦FIFO存储的数据超过256个那么当摄像头一帧数据传输到最后一行后一定会有剩余数据写不进DDR,要等到下一帧数据来才能把这剩余的数据压入DDR,这样的操作不好。例如我每当FIFO大于512个数据触发一次AXI4_RW模块写突发,那么最后一次触发肯定是一帧数据最后一行的最后512个数据,此时一次写突发最大只能将256个数据写入DDR,那么必然会剩下256个数据留在FIFO中无法写入DDR,因为此时摄像头传输完一帧数据后进入消隐时刻,这段时间没有数据传出,那么FIFO中剩下的256个数据就只能等到下一帧数据来临再次凑够512个数据,开启一次AXI4_RW模块写突发,才能将上一帧的最后256个数据写入DDR,这样的操作就很不合适了,因此我们本节实验采用的就是当FIFO中存入的数据够256个就开启一次突发,这样最后一次突发刚好可以将一帧数据完整写入DDR

AXI GPIO模块:用来读取帧指示位,为了实现读写地址不冲突,在DDR中开辟了两帧存储区域,写0帧就读1帧,写1帧就读0帧,并通过AXI4_RW模块去控制读写地址。因此需要引出一个帧指示位用来指示当前写到了哪一帧存储区,这个指示位就可以通过AXI GPIO模块读取到CPUDP在读取数据时就可以根据指示位避开写操作帧,实现读写乒乓操作。

DDR控制器:PS端的DDR控制器是MPSOC自带的硬核资源主要是用来实现DDR的通讯协议和数据交互,我们直接在ZYNQ UltraSCALE中勾选DDR就可以了。

GPIO模块:主要用来配置OV5640摄像头寄存器。

DP控制器:DP控制器主要用来读取DDR的图像数据,并打包成符合DP传输协议的数据包发送出去。

了解完每个模块的作用后下面我们来带领大家分析一下代码,首先我们来看一下OV5640控制模块(ov5640_capture_data)的代码作了哪些修改。第一个修改的地方就是数据拼接,如下所示:

assign cmos_frame_data = wait_done ?

{8'hff, cmos_data_16b[4:0],3'd0 , cmos_data_16b[10:5],2'd0 ,cmos_data_16b[15:11],3'd0 }

: 32'd0; //输出数据

摄像头的数据是RGB565格式的,在前面有关OV5640摄像头的实验中都是通过将RGB565格式补零转换成RGB888格式。本节实验在RGB888格式的基础上还要补一个“8'hff”来当alpha 值来用,因为最终DP数据是RGBA格式,这里建议大家就将alpha 值设置为“8'hff”,如果之后有需要修改alpha值的可以直接在DP配置函数中修改,不需要再动OV5640控制模块的代码。并且因为AXI4在将数据写入DDR的时候是按照字节写入的,低位字节在前,所以我们还需要将摄像头的数据高低位换一下,将RGBA格式数据调整为ABGR,这样写入DDR后刚好是RGBA,否则DP读取DDR时会发生数据高低位反过来的错误。

另一个改动的地方就是产生一个场同步信号下降沿,并把这个下降沿输出到其他模块当复位信号使用,如下所示:

assign neg_vsync = wait_done ? (cam_vsync_d1 & (~cam_vsync_d0)):1'b0 ;

通过PL端的学习我们已经了解OV5640摄像头的时序,每当一帧图片传输完成,下一帧图片开始传输之前都会有一个场同步信号,这个信号是高电平有效,如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_12


29.3.2 OV5640帧时序

从上图中可以看到OV5640的消隐时间还是挺久的,光场同步前沿加上场同步信号本身就有14544+5688=20232个tp,这么长的一段时间我们不能白白浪费,因此我们把场同步信号下降沿抓取出来。

当我们最后一次突发开始发生时OV5640摄像头刚好进入消隐期间,因为消隐时间足够长,在这段时间内足够AXI4_RW模块把最后256个数据写入DDR了,那么当抓取到场同步信号下降沿后我们用这个脉冲当复位信号,将FIFO和AXI4_RW模块统统复位,这样做的好处就是即使因为外界因素导致MPSOC运行出错,导致读写数据丢失或者错误都不要紧,因为在下一帧到来前FIFO和AXI4_RW模块都进行了复位,确保新的一帧数据不会出错。

OV5640控制模块(ov5640_capture_data)的代码修改完成后就可以将它封装成IP核了,关于自定义IP核的调用步骤在前面“自定义IP核呼吸灯实验”中有详细的讲解这里就不再赘述了。接下来我们来看一下FIFO的设置,如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_写数据_13


29.3.3 FIFO设置


《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_14


29.3.4 FIFO设置


《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_数据_15


29.3.5 FIFO设置

设置好FIFO后接下来就是本节实验最重要的自定义AXI4接口IP核了(axi4_rw),这个IP核的创建方式和前面的“AXI4 读写DDR实验”一模一样,都是先创建一个官方的模板,然后在这个模板上修改。接下我们来给大家讲解一下都修改了哪些内容。

首先我们要改的就是复位信号、写操作帧指示标志以及突发的开启条件,如下所示:

284 assign init_txn_pulse = (!init_txn_ff2) && init_txn_ff;​
285 assign M_AXI_ARESETN_NEG = (!M_AXI_ARESETN_0) && M_AXI_ARESETN_1;//帧复位信号的下降沿​
286​
287 always @(posedge M_AXI_ACLK) ​
288 begin ​
289 M_AXI_ARESETN_0 <= M_AXI_ARESETN;​
290 M_AXI_ARESETN_1 <= M_AXI_ARESETN_0; ​
291 end ​
292 ​
293 always @(posedge M_AXI_ACLK) //产生突发脉冲信号 ​
294 begin ​
295 if (M_AXI_ARESETN_NEG ) ​
296 bank_flag <= ~bank_flag;​
297 else ​
298 bank_flag <= bank_flag;​
299 end​
300 ​
301 always @(posedge M_AXI_ACLK) //产生突发脉冲信号 ​
302 begin ​
303 if (M_AXI_ARESETN == 0 ) ​
304 INIT_AXI_TXN <= 1'b0;​
305 else if(fifo_count>253)​
306 INIT_AXI_TXN <= 1'b1; ​
307 else ​
308 INIT_AXI_TXN <= 1'b0; ​
309 end​
310 //Generate a pulse to initiate AXI transaction.​
311 always @(posedge M_AXI_ACLK) ​
312 begin ​
313 // Initiates AXI transaction delay ​
314 if (M_AXI_ARESETN == 0 ) ​
315 begin ​
316 init_txn_ff <= 1'b0; ​
317 init_txn_ff2 <= 1'b0; ​
318 end ​
319 else ​
320 begin ​
321 init_txn_ff <= INIT_AXI_TXN;​
322 init_txn_ff2 <= init_txn_ff; ​
323 end ​
324 end

我们先看代码第285~291行复位信号,大家可能在这里会有疑惑,在上文提到要用OV5640控制模块(ov5640_capture_data)的场同步信号下降沿作为FIFO和axi_rw模块的复位信号,为什么这里又要对这个复位信号再一次打拍和抓取下降沿呢?其实这里抓取的复位信号下降沿是为了给写操作帧指示标志(bank_flag)以及后面的写地址使用的。

我们可以看到代码第293~299行,每当产生一次帧复位下降沿就对帧指示标志(bank_flag)取反一次,这本身很好理解,就是写完一帧图像,标志位就取反一次,用来指示当前是写0帧区域还是写1帧区域,方便DP在读取DDR的时候跟写操作错开。但是有的同学会问了,我干嘛多此一举呢?直接用帧复位信号不行吗?来一次帧复位信号bank_flag取反一次不也可以指示当前是写0帧区域还是写1帧区域吗?这其实还涉及到信号的跨时钟域处理问题,在OV5640控制模块(ov5640_capture_data)中抓取的场同步信号下降沿即帧复位信号是在OV5640的时钟域下操作的,它的一个脉冲是48Mhz,但是在axi4_rw模块中时钟域变了,一个脉冲是100Mhz。也就是说在axi4_rw模块中检测到的帧复位信号其实持续了两个时钟周期还多,那么直接用帧复位信号作为bank_flag取反的判断条件就会造成bank_flag连续取反两次,显然和预期要求不符。因此我们抓取帧复位信号的下降沿作为bank_flag取反的判断条件。

再往下我们看代码的第301~324行,这段代码就是为了产生突发写DDR的触发条件,在官方生成的模板当中触发条件(INIT_AXI_TXN)是一个输入信号由外部产生触发条件。在本次实验中我们给它改一下,改成根据FIFO中存储的数据量来产生触发条件,每当FIFO中存储的数据达到256个的时候我们就把数据写入DDR。但是大家注意看代码第305行,这里判断条件是FIFO中的数据量计数器大于253而不是255,因为FIFO中计数是从0开始的,当FIFO中存满256个数据其实FIFO中的数据量计数器显示的是255,而如果你使用的是First Word Fall Through模式那么计数器则显示254,而本节实验使用的就是First Word Fall Through模式。因此当FIFO存满256个数据时其实FIFO中的数据量计数器显示的是254,所以判断条件设置为FIFO中的数据量计数器大于253开启一次突发。

接下来我们继续往后面看写地址通道的代码,如下所示:

328 always @(posedge M_AXI_ACLK) ​
329 begin ​
330 ​
331 if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 ) ​
332 begin ​
333 axi_awvalid <= 1'b0; ​
334 end ​
335 // If previously not valid , start next transaction ​
336 else if (~axi_awvalid && start_single_burst_write) ​
337 begin ​
338 axi_awvalid <= 1'b1; ​
339 end ​
340 /* Once asserted, VALIDs cannot be deasserted, so axi_awvalid ​
341 must wait until transaction is accepted */ ​
342 else if (M_AXI_AWREADY && axi_awvalid) ​
343 begin ​
344 axi_awvalid <= 1'b0; ​
345 end ​
346 else ​
347 axi_awvalid <= axi_awvalid; ​
348 end ​
349 ​
350 ​
351 // Next address after AWREADY indicates previous address acceptance ​
352 always @(posedge M_AXI_ACLK) ​
353 begin​
354 if (M_AXI_ARESETN_NEG && bank_flag)​
355 axi_awaddr <= 'b0; ​
356 else if (M_AXI_AWREADY && axi_awvalid) ​
357 ​
358 axi_awaddr <= axi_awaddr + burst_size_bytes; ​
359 ​
360 else ​
361 axi_awaddr <= axi_awaddr; ​
362 end

这一段代码其实就是官方自动帮我们生成的AXI4总线协议中关于写地址的内容。通过简介部分的内容我们已经知道整个AXI4协议分为5个通道,上面这段代码就是写地址通道的代码实现。先看代码第328~348行,这个语句块是为了产生写地址有效信号axi_awvalid。当复位信号或者写操作触发信号来临时axi_awvalid清零,然后等待状态机进入写状态,本节实验自定义的AXI4 IP核状态机示意图如下所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_数据_16


29.3.6 AXI4 IP核状态机示意图

从上图可以看出,本节实验的状态机是非常简单的,满足写突发条件就开启一次写突发,写突发完成后回到空闲状态等待下一次突发条件满足(其实官方生成的模板中状态机还包括读数据状态和比较数据状态,但是本节实验并没有用到)。

当突发条件满足,即FIFO中存储数据达到了256个就进入写状态,进入写状态会拉高start_single_burst_write信号,此时axi_awvalid信号拉高,之后就是等待从机给出M_AXI_AWREADY信号,当M_AXI_AWREADY信号也拉高后代表主机从机握手成功,此时地址线上就可以给出有效地址数据了,然后立刻拉低axi_awvalid信号(也就是说axi_awvalid信号和M_AXI_AWREADY信号同时为高电平只持续一个时钟周期)。为什么要立刻拉低axi_awvalid信号呢?因为在突发模式下写数据地址只需要给出首地址就行了,内部DDR控制器会自动按照当前首地址往后累加,每次累加一个字节。例如我现在要写入256个32位数据(32位数据即每个数据占用4个字节的地址空间),在开启写地址时只需要给出首地址,假如首地址为0,那么DDR控制器会从0开始往后分配256*32/8=1024个字节的地址空间来存放这256个数据。正是由于这种机制我们可以看代码第352~362行,每次突发只写入一个突发首地址,然后下一次写突发首地址时就累加一个突发长度尺寸(这个尺寸的计算为:assign burst_size_bytes = C_M_AXI_BURST_LEN * C_M_AXI_DATA_WIDTH/8;即突发长度*单个数据位宽再除以8),这样就可以保证每一次突发的数据存储地址是紧挨着上一次突发的地址,达到数据连续的目的。

这里尤其需要注意的是写地址的清零操作,在代码的第354行我们可以看到当帧复位信号下降沿来临且bank_flag信号为1时地址清空,这就意味着我们整个写地址是每写入两帧数据地址清零一次,相当于开辟了两帧的存储空间(两帧数据在地址上是紧挨着的),这样就方便DP去读取DDR数据了,确保写帧操作和读帧操作不会重叠。

整个地址通道的ILA在线调试图如下所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_17


29.3.7 写地址通道ILA在线调试图

从上图中可以看出当FIFO中存储数据量够256个数据时开启一次突发,接着等待axi_awvalid信号和M_AXI_AWREADY信号同时为高时写入一个突发首地址,然后地址自加一个突发尺寸(这里的突发尺寸是16进制400即1024个字节刚好对应256个32bit数据),等待下一次突发写地址到来。

说完了写地址通道我们再来看看写数据通道,其代码如下:

367 assign wnext = M_AXI_WREADY & axi_wvalid; ​
368 ​
369 // WVALID logic, similar to the axi_awvalid always block above ​
370 always @(posedge M_AXI_ACLK) ​
371 begin ​
372 if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 ) ​
373 begin ​
374 axi_wvalid <= 1'b0; ​
375 end ​
376 // If previously not valid, start next transaction ​
377 else if (~axi_wvalid && start_single_burst_write) ​
378 begin ​
379 axi_wvalid <= 1'b1; ​
380 end ​
381 /* If WREADY and too many writes, throttle WVALID ​
382 Once asserted, VALIDs cannot be deasserted, so WVALID ​
383 must wait until burst is complete with WLAST */ ​
384 else if (wnext && axi_wlast) ​
385 axi_wvalid <= 1'b0; ​
386 else ​
387 axi_wvalid <= axi_wvalid; ​
388 end ​
389 ​
390 ​
391 //WLAST generation on the MSB of a counter underflow ​
392 // WVALID logic, similar to the axi_awvalid always block above ​
393 always @(posedge M_AXI_ACLK) ​
394 begin ​
395 if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 ) ​
396 begin ​
397 axi_wlast <= 1'b0; ​
398 end ​
399 // axi_wlast is asserted when the write index ​
400 // count reaches the penultimate count to synchronize ​
401 // with the last write data when write_index is b1111 ​
402 // else if (&(write_index[C_TRANSACTIONS_NUM-1:1])&& ~write_index[0] && wnext) ​
403 else if (((write_index == C_M_AXI_BURST_LEN-2 && C_M_AXI_BURST_LEN >= 2) && wnext)​
|| (C_M_AXI_BURST_LEN == 1 ))​
404 begin ​
405 axi_wlast <= 1'b1; ​
406 end ​
407 // Deassrt axi_wlast when the last write data has been ​
408 // accepted by the slave with a valid response ​
409 else if (wnext) ​
410 axi_wlast <= 1'b0; ​
411 else if (axi_wlast && C_M_AXI_BURST_LEN == 1) ​
412 axi_wlast <= 1'b0; ​
413 else ​
414 axi_wlast <= axi_wlast; ​
415 end ​
416 ​
417 ​
418 /* Burst length counter. Uses extra counter register bit to indicate terminal ​
419 count to reduce decode logic */ ​
420 always @(posedge M_AXI_ACLK) ​
421 begin ​
422 if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 || start_single_burst_write == 1'b1) ​
423 begin ​
424 write_index <= 0; ​
425 end ​
426 else if (wnext && (write_index != C_M_AXI_BURST_LEN-1)) ​
427 begin ​
428 write_index <= write_index + 1; ​
429 end ​
430 else ​
431 write_index <= write_index; ​
432 end ​
433 ​
434 ​
435 /* Write Data Generator ​
436 Data pattern is only a simple incrementing count from 0 for each burst */ ​
437 always @(posedge M_AXI_ACLK) ​
438 begin ​
439 if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) ​
440 axi_wdata <= 'b1; ​
441 //else if (wnext && axi_wlast) ​
442 // axi_wdata <= 'b0; ​
443 else if (wnext) ​
444 axi_wdata <= axi_wdata + 1; ​
445 else ​
446 axi_wdata <= axi_wdata; ​
447 end

先来看代码第367行定义了一个wnext信号,当M_AXI_WREADY 信号和 axi_wvalid同时为高时wnext信号拉高,其实就是写数据有效信号。

代码第370~388行是为了生成axi_wvalid信号,当状态机进入写状态后除了写地址有效信号要拉高之外写数据有效信号也要拉高,只不过写地址有效信号在和从机握手成功后要立刻拉低,但是写数据有效信号不一样,它要持续到一次写突发全部完成才能拉低。因此代码第384、385行,直到一次突发写到最后一个数据时(axi_wlast信号是写到最后一个数据的标志位)将axi_wvalid信号拉低。还有一点需要注意,大家可以看到代码第372行,这里的复位信号用的就是帧同步复位信号而不是用它的下降沿,其实除了写地址清零因为要配合bank_flag来使用,必须要用帧同步复位信号下降沿之外,其他的always语句块都是可以直接使用帧同步复位信号的,因为多复位一个时钟周期对整体功能并没用什么影响,当然你也可以使用其下降沿作为复位信号。

代码第393~432行的作用就是为了产生axi_wlast信号,这个信号是写到最后一个数据的标志位。我们可以看到代码第420~432行定义了一个计数器,这个计数器就是从零数到最大突发长度减一(因为是从零开始,所以数到最大突发长度减一刚好就是一次完整突发长度),而代码第393~415行就是根据这个计数器判断写突发是否写到最后一个数据了,如果写到最后一个数据就拉高axi_wlast信号。这段代码大家需要注意的就是代码第403行的判断条件,首先“write_index == C_M_AXI_BURST_LEN-2”这个条件是为了提前一个时钟开始拉高axi_wlast信号,这样刚好突发的最后一个数据和axi_wlast信号同步拉高(因为信号当前时钟周期做出改变,会在下一个时钟周期更新信号);其次是“C_M_AXI_BURST_LEN >= 2”,因为突发长度可以是1248163264128256,当突发长度为1时axi_wlast信号应该始终为高。

写数据通道的ILA在线调试波形图如下所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_18


29.3.8写数据通道ILA在线调试图

从上图可以看到当M_AXI_WREADY 信号和 axi_wvalid同时为高时数据开始有效,当数据传输到最后一个数据时axi_wlast信号拉高,完全符合我们的预期结果。

接着我们再来看写应答通道的代码,如下所示:

451 always @(posedge M_AXI_ACLK) ​
452 begin ​
453 if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 ) ​
454 begin ​
455 axi_bready <= 1'b0; ​
456 end ​
457 // accept/acknowledge bresp with axi_bready by the master ​
458 // when M_AXI_BVALID is asserted by slave ​
459 else if (M_AXI_BVALID && ~axi_bready) ​
460 begin ​
461 axi_bready <= 1'b1; ​
462 end ​
463 // deassert after one clock cycle ​
464 else if (axi_bready) ​
465 begin ​
466 axi_bready <= 1'b0; ​
467 end ​
468 // retain the previous value ​
469 else ​
470 axi_bready <= axi_bready; ​
471 end ​
472 ​
473 ​
474 //Flag any write response errors ​
475 assign write_resp_error = axi_bready & M_AXI_BVALID & M_AXI_BRESP[1];

写应答的代码是比较简单的,他是主机回复从机的一种机制,当数据传输完成后从机会拉高M_AXI_BVALID信号,主机在检测到M_AXI_BVALID信号后会拉高axi_bready信号告诉从机我准备好了你可以给我应答了,这个时候从机会给出一个应答信号M_AXI_BRESP。我们看代码第475行就是应答错误标志位的生成,满足这个条件则代表应答失败,拉高write_resp_error信号,反之则代表应答正确write_resp_error信号保持低电平。

写应答通道的ILA调试波形图如下所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_IP_19


29.3.9 写应答通道ILA波形图

最后我们再来看一下状态机的代码,如下所示:

667 always @ ( posedge M_AXI_ACLK) ​
668 begin ​
669 if (M_AXI_ARESETN == 1'b0 ) ​
670 begin ​
671 // reset condition ​
672 // All the signals are assigned default values under reset condition ​
673 mst_exec_state <= IDLE; ​
674 start_single_burst_write <= 1'b0; ​
675 start_single_burst_read <= 1'b0; ​
676 compare_done <= 1'b0; ​
677 ERROR <= 1'b0; ​
678 end ​
679 else ​
680 begin ​
681 ​
682 // state transition ​
683 case (mst_exec_state) ​
684 ​
685 IDLE: ​
686 // This state is responsible to wait for user defined C_M_START_COUNT ​
687 // number of clock cycles. ​
688 if ( init_txn_pulse == 1'b1) ​
689 begin ​
690 mst_exec_state <= INIT_WRITE; ​
691 ERROR <= 1'b0;​
692 compare_done <= 1'b0;​
693 end ​
694 else ​
695 begin ​
696 mst_exec_state <= IDLE; ​
697 end ​
698 ​
699 INIT_WRITE: ​
700 // This state is responsible to issue start_single_write pulse to ​
701 // initiate a write transaction. Write transactions will be ​
702 // issued until burst_write_active signal is asserted. ​
703 // write controller ​
704 if (writes_done) ​
705 begin ​
706 mst_exec_state <= IDLE;//本节实验只用到写功能,写完就回到空闲状态 ​
707 end ​
708 else ​
709 begin ​
710 mst_exec_state <= INIT_WRITE; ​
711 ​
712 if (~axi_awvalid && ~start_single_burst_write && ~burst_write_active&&INIT_AXI_TXN) ​
713 begin ​
714 start_single_burst_write <= 1'b1; ​
715 end ​
716 else ​
717 begin ​
718 start_single_burst_write <= 1'b0; //Negate to generate a pulse ​
719 end ​
720 end

在前文中其实都已经给大家画出状态机的状态图了,本节实验的状态机是在官方的模板上修改过来的,大家注意代码第706行,原本的模板是写完数据后进入读状态,之后再进入数据比较状态,而本节实验只需要写状态,所以在第706行我们让状态完成一次写突发就回到空闲状态等待下一次突发。

到这里我们整个自定义AXI4接口IP核(axi4_rw)的代码就给大家讲解完了,这个模块其实还有读地址、读数据、读应答、比较数据以及状态机跳转到对应读状态和比较状态的内容,只不过本节实验用不到所以就没有跟大家讲解了,当然在例程中这部分代码我并没有删掉也给大家保留了,官方也都给出详细注释了,感兴趣的同学可以自己去了解一下。

最后还有一个要注意的点就是在搭建硬件平台的时候FIFO的复位是高电平有效而自定义AXI4接口IP核(axi4_rw)的复位却是低电平有效,因此帧复位信号需要经过一个反相器,如下图所示。

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_写数据_20


29.3.10硬件平台

软件设计

本节实验的软件设计结合了前面“axi_gpio按键控制LED实验”、“DP彩条显示实验”以及“OV5640 LCD显示实验”例程,将axi_gpio、DP显示、emio以及OV5640配置全部集中到一个工程中了,所以建议大家在学习本节实验软件设计前先学习前面几个实验例程。

关于本节实验软件代码我们也只讲解修改的部分内容,对于前面已经讲解过内容不再重复赘述。首先我们先来看DP显示的基本参量修改如下所示:

29 #define BUFFERSIZE 1280 * 720 * 4 /* HTotal * VTotal * BPP */​
30 #define LINESIZE 1280 * 4 /* HTotal * BPP */​
31 #define STRIDE LINESIZE /* The stride value should​
32 be aligned to 256*/

本节实验DP的分辨率设置成720p,因此需要将BUFFERSIZELINESIZE修改成对应的参数。

接下来我们来看一下主函数,如下所示:

38 int main(void)​
39 {​
40 u16 cmos_h_pixel; //ov5640 DVP 输出水平像素点数​
41 u16 cmos_v_pixel; //ov5640 DVP 输出垂直像素点数​
42 u16 total_h_pixel; //ov5640 水平总像素大小​
43 u16 total_v_pixel; //ov5640 垂直总像素大小​
44 ​
45 cmos_h_pixel = 1280; //设置OV5640输出分辨率为1280*720​
46 cmos_v_pixel = 720;​
47 total_h_pixel = 2570;​
48 total_v_pixel = 980;​
49 int Status;​
50 Xil_DCacheDisable();​
51 Xil_ICacheDisable();​
52 ​
53 emio_init(); //初始化EMIO​
54 Status = ov5640_init( cmos_h_pixel, //初始化ov5640​
55 cmos_v_pixel,​
56 total_h_pixel,​
57 total_v_pixel);​
58 if(Status == 0)​
59 xil_printf("OV5640 detected successful!\r\n");​
60 else​
61 xil_printf("OV5640 detected failed!\r\n");​
62 ​
63 axi_gpio_init();​
64 ​
65 //.............DP显示.................​
66 xil_printf("DPDMA Generic Video Example Test \r\n");​
67 Status = DpdmaVideoExample(&RunCfg);​
68 if (Status != XST_SUCCESS) {​
69 xil_printf("DPDMA Video Example Test Failed\r\n");​
70 return XST_FAILURE;​
71 }​
72 ​
73 xil_printf("Successfully ran DPDMA Video Example Test\r\n");​
74 ​
75 return XST_SUCCESS;​
76 }

主函数大家看起来应该很熟悉,其实就是将“DP彩条显示实验”和“OV5640 LCD显示实验”两个例程的函数拼接在一起,其中代码第53~63行是对摄像头寄存器进行配置,而代码第66~71行是对DP显示进行配置。其中对摄像头寄存器进行配置与“DP彩条显示实验”一模一样,这里不再讲解,而DP显示的配置进行了不少修改需要我们重点来学习。

DP显示配置内容主要放在了DpdmaVideoExample函数当中,其代码如下:

88 int DpdmaVideoExample(Run_Config *RunCfgPtr)​
89 ​
90 {​
91 u32 Status;​
92 int i=0;​
93 int bank_flag;​
94 /* Initialize the application configuration */​
95 InitRunConfig(RunCfgPtr);​
96 Status = InitDpDmaSubsystem(RunCfgPtr);​
97 if (Status != XST_SUCCESS) {​
98 return XST_FAILURE;​
99 }​
100 ​
101 xil_printf("Generating Overlay.....\n\r");​
102 ​
103 while(1){​
104 ​
105 bank_flag = get_bank_flag();​
106 xil_printf("bank_flag=%d\r\n",bank_flag);​
107 if(bank_flag==1&&frame_flag)​
108 {​
109 ​
110 frame_flag=0;​
111 frame_buffer_addr=0x40384000;​
112 }​
113 else if(bank_flag==0&&frame_flag)​
114 {​
115 ​
116 frame_flag=0;​
117 frame_buffer_addr=0x40000000;​
118 }​
119 ​
120 FrameBuffer.Address = (INTPTR)frame_buffer_addr;​
121 FrameBuffer.Stride = STRIDE;​
122 FrameBuffer.LineSize = LINESIZE;​
123 FrameBuffer.Size = BUFFERSIZE;​
124 XDpDma_DisplayGfxFrameBuffer(RunCfgPtr->DpDmaPtr, &FrameBuffer);​
125 if(i<1){​
126 SetupInterrupts(RunCfgPtr);​
127 i=i+1;​
128 }​
129 }​
130 //return XST_SUCCESS;​
131 }

大家可以看到相比较于“DP彩条显示实验” DpdmaVideoExample函数中少了一个彩条函数多了一个while循环。在这个循环里首先调用了一个get_bank_flag函数,我们进入get_bank_flag函数,如下所示:

u32 get_bank_flag(void)​
{​
u32 fifo_count = 0;​
fifo_count = XGpio_DiscreteRead(&axi_gpio_inst0, AXI_GPIO_0_CHANEL);​
return fifo_count;​
}

可以看到get_bank_flag函数的主要功能就是调用XGpio_DiscreteRead函数读取axi_gpio的值,而这个axi_gpio是连接到bank_flag上的,因此通过get_bank_flag函数我们就可以知道当前写操作进行到哪一帧了。

接下来代码第107~118行是用来判断写操作在哪一帧,对应的就将读操作跳到另外一帧。其中bank_flag是写帧指示位,frame_buffer_addr是一帧的起始地址,我们把0帧的起始地址放在0x40000000,而1帧的起始地址放在0x40384000。这里还有一个非常关键的标志frame_flag,这个标志是DP新一帧开始的标志。frame_flag是一个全局变量,在DP中断函数中有一个中断回调处理函数如下所示:

XScuGic_Connect(IntrPtr, DPDMA_INTR_ID,​
(Xil_ExceptionHandler)XDpDma_InterruptHandler, RunCfgPtr->DpDmaPtr);​
进入这个中断回调处理函数,如下所示:​
1 void XDpDma_InterruptHandler(XDpDma *InstancePtr)​
2 {​
3 u32 RegVal;​
4 RegVal = XDpDma_ReadReg(InstancePtr->Config.BaseAddr,​
5 XDPDMA_ISR);​
6 if(RegVal & XDPDMA_ISR_VSYNC_INT_MASK) {​
7 frame_flag=1;​
8 XDpDma_VSyncHandler(InstancePtr);​
9 }​
10 ​
11 if(RegVal & XDPDMA_ISR_DSCR_DONE4_MASK) {​
12 XDpDma_SetChannelState(InstancePtr, AudioChan0, XDPDMA_DISABLE);​
13 InstancePtr->Audio[0].Current = NULL;​
14 XDpDma_WriteReg(InstancePtr->Config.BaseAddr, XDPDMA_ISR,​
15 XDPDMA_ISR_DSCR_DONE4_MASK);​
16 }​
17 ​
18 if(RegVal & XDPDMA_ISR_DSCR_DONE5_MASK) {​
19 XDpDma_SetChannelState(InstancePtr, AudioChan1, XDPDMA_DISABLE);​
20 InstancePtr->Audio[1].Current = NULL;​
21 XDpDma_WriteReg(InstancePtr->Config.BaseAddr, XDPDMA_ISR,​
22 XDPDMA_ISR_DSCR_DONE5_MASK);​
23 }​
24 }

每一次DP显示开始新一帧前都会执行一次帧中断函数(XDpDma_VSyncHandler),我们在执行帧中断函数之前添加了一个“frame_flag=1;”语句,这样每当执行帧中断前都会拉高frame_flag用来指示DP显示新一帧开始。

了解了bank_flagframe_flag以及frame_buffer_addr这三个变量的含义后再来看代码第107~118行就比较容易了,当bank_flag等于1时代表写操作处于0帧区域(例如一开始bank_flag值为0,当写第一帧数据时,帧同步信号先到来,bank_flag取反等于1,此时是把第一帧数据写入0帧区域),当bank_flag等于0时代表写操作处于1帧区域(注意这里的0帧和1帧代表存储区域编号)。这样每当DP开始读取新一帧时(frame_flag=1代表新一帧开始)判断当前写操作在哪一帧区域,我们将对应非写操作区域的缓存数据传给DP显示,并且要把frame_flag清零,这样当frame_flag下一次拉高时代表DP新的一帧又来了。

代码第125~127行是中断函数配置,因为只需要配置一次所以我们用了一个if语句,配置完一次后就不再配置了。

到这里整个OV5640 DP显示实验的软件代码就讲解完了,接下来就可以上板验证了。

下载验证

首先我们将下载器与MPSOC开发板上的JTAG接口连接,下载器另外一端与电脑连接。然后使用USB连接线将开发板USB_UART接口与电脑连接,用于串口通信,然后将DP转接线连接到HDMI线,再连接至HDMI显示屏(这里需要使用主动式转换器,并不是所有的转接线都通用,请大家前往正点原子官方店铺购买),最后开发板上四个启动模式开置为ON,并把双目摄像头插到扩展口上,如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南》第十九章 OV5640 DP显示实验​_数据_21


29.5.1 硬件连接图

之后就可以下载代码观察HDMI显示屏是否能正常显示图像了,如果可以正常显示则代表实验成功。


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月12日 0

暂无评论

推荐阅读
95kVyaJuybju
最新推荐 更多