【正点原子FPGA连载】 第二十八章OV5640 DP显示实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1)实验平台:正点原子MPSoC开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=692450874670
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html

第二十八章OV5640 DP显示实验

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

28.1简介

AXI4接口总共有三种类型,它们分别是AXI4(AXI4-Full)、AXI4-Lite和AXI4-Stream,不同的接口类型适用于不同的应用场景,下面对这三个接口做简要说明。
AXI4-Full:最高性能的接口,适合存储器映射的通信,支持每个地址阶段最高256个数据传输周期的批量传输,适合于更需要持久、高速性能的IP。
AXI4-Lite:AXI4-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核未展时开如下图所示:
在这里插入图片描述

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

图 29.1.2 展开时通道和接口
在上图中,以m_axi_aw_为前缀的接口属于写地址通道;以m_axi_w_为前缀的接口属于写数据通道;以m_axi_b_为前缀的接口属于写响应通道;以m_axi_ar_为前缀的接口属于读地址通道;以m_axi_r_为前缀的接口属于读数据通道。
写地址通道包含的信号及信号含义如下表所示:
表29.1.1 通道信号
在这里插入图片描述

写响应通道包含的信号及信号含义如下表所示:
表29.1.3 通道信号
在这里插入图片描述

读地址通道包含的信号及信号含义如下表所示:
表29.1.4 通道信号
在这里插入图片描述

读数据通道包含的信号及信号含义如下表所示:
表29.1.5 通道信号
在这里插入图片描述
在进行数据传输的过程中,传输通道均使用valid/ready信号对传输过程的地址、数据、控制信号进行握手。使用双向握手机制,valid和ready都有效的时候表示握手成功。下面将以地址通道为例介绍几种常见的握手方式。
valid在ready前有效:主机先给出数据和控制信息,同时驱动valid为高电平。一旦主机驱动valid为高,地址将保持不变,直到从机驱动ready信号为高。一旦从机驱动ready为高,则握手成功,在时钟上升沿T3时刻开始进行地址。如下图所示:
在这里插入图片描述

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

图 29.1.4 valid在ready之后
valid和ready同时有效:主机驱动valid和从机驱动ready同时发生,在T2时刻开始传输地址。如下图所示:
在这里插入图片描述

图 29.1.5 valid和ready同时有效
AXI4读通道结构如下图所示:
在这里插入图片描述

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

图 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写通道结构如下图所示:
在这里插入图片描述

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

图 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协议的设备或模块可以通过互联模块进行数据的交互。如下图所示:
在这里插入图片描述

图 29.1.10 多机互联

28.2实验任务

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

28.3硬件设计

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

图 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模块读取到CPU。DP在读取数据时就可以根据指示位避开写操作帧,实现读写乒乓操作。
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摄像头的时序,每当一帧图片传输完成,下一帧图片开始传输之前都会有一个场同步信号,这个信号是高电平有效,如下图所示:
在这里插入图片描述

图 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的设置,如下图所示:
在这里插入图片描述

图 29.3.3 FIFO设置

在这里插入图片描述

图 29.3.4 FIFO设置

在这里插入图片描述

图 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核状态机示意图如下所示:
在这里插入图片描述

图 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开始往后分配25632/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在线调试图如下所示:
在这里插入图片描述

图 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语句块都是可以直接使用帧同步复位信号的,因为多复位一个时钟周期对整体功能并没用什么影响,当然你也可以使用其下降沿作为复位信号。
代码第393432行的作用就是为了产生axi_wlast信号,这个信号是写到最后一个数据的标志位。我们可以看到代码第420432行定义了一个计数器,这个计数器就是从零数到最大突发长度减一(因为是从零开始,所以数到最大突发长度减一刚好就是一次完整突发长度),而代码第393~415行就是根据这个计数器判断写突发是否写到最后一个数据了,如果写到最后一个数据就拉高axi_wlast信号。这段代码大家需要注意的就是代码第403行的判断条件,首先“write_index == C_M_AXI_BURST_LEN-2”这个条件是为了提前一个时钟开始拉高axi_wlast信号,这样刚好突发的最后一个数据和axi_wlast信号同步拉高(因为信号当前时钟周期做出改变,会在下一个时钟周期更新信号);其次是“C_M_AXI_BURST_LEN >= 2”,因为突发长度可以是1、2、4、8、16、32、64、128、256,当突发长度为1时axi_wlast信号应该始终为高。
写数据通道的ILA在线调试波形图如下所示:
在这里插入图片描述

图 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调试波形图如下所示:
在这里插入图片描述

图 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)的复位却是低电平有效,因此帧复位信号需要经过一个反相器,如下图所示。
在这里插入图片描述

图 29.3.10硬件平台
28.4软件设计
本节实验的软件设计结合了前面“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,因此需要将BUFFERSIZE、LINESIZE修改成对应的参数。
接下来我们来看一下主函数,如下所示:

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显示实验”两个例程的函数拼接在一起,其中代码第5363行是对摄像头寄存器进行配置,而代码第6671行是对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_flag、frame_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显示实验的软件代码就讲解完了,接下来就可以上板验证了。
28.5下载验证
首先我们将下载器与MPSOC开发板上的JTAG接口连接,下载器另外一端与电脑连接。然后使用USB连接线将开发板USB_UART (PS_PORT)接口与电脑连接,用于串口通信,然后将DP转接线连接到HDMI线,再连接至HDMI显示屏(这里需要使用主动式转换器,并不是所有的转接线都通用,请大家前往正点原子官方店铺购买),最后将开发板上四个启动模式开关均置为ON,并把双目摄像头插到扩展口上,如下图所示:
在这里插入图片描述

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

猜你喜欢

转载自blog.csdn.net/weixin_55796564/article/details/129436437