基于Smart ZYNQ (SP/SP2/SL 版) 的PS实验 二十一 5寸RGB屏实验:用VDMA模块来缓存图像并在LCD上显示(一)PS端彩条纹的显示

本文介绍如何在ZYNQ上增加VDMA模块,来作为图像的缓存并最终点亮我们的LCD屏

  • 此章节内容适用于Smart ZYNQ SP SP2和 SL 版的板子 ( 不包含Smart ZYNQ 标准版 ),如是标准版或本站其他板子请看对应板子目录
  • 本文在 vivado2018.3版本上演示

本次实验需要外接本站的5寸IPS 屏幕模块,有关本次实验的 RGB屏的原理图手册以及其他例程资料可以参看下面汇总贴:5寸IPS 800X480 RGB接口LCD屏幕模块

另外 为了方便之后教程的演示,这里在设置的过程中也同时打开了SD卡的功能(后续教程会跟进SD卡的数据读取显示功能,这里仅作准备工作)

本文中RGB888驱动部分的内容和上之前的VDMA点亮HDMI的工程有部分类似,大家可以选看。其实关于RGB的时序都是一致的,只要设置好其中的参数就可以了。另外和HDMI 输出不同,这里的RGB 数据是可以直接点LCD屏幕的,不需要额外再增加RGB2DVI模块了。

一、背景

之前FPGA的5INCH LCD彩色条纹显示的demo是由FPGA部分的逻辑实时生成的,所以数据流是根据RGB的时序直接给到了LCD,整个显示过程未开辟任何缓存,适合比较简单应用的场合。 但是实际的项目中,我们需要对摄像头传递过来的图像进行实时的处理,但图像的处理速度或者摄像头传递图像的速度和FPGA输出显示的速率不同步,所以需要对处理好的帧进行缓存,又或者当我们在PS上跑LINUX需要带高分辨率的图形界面时,或想用PS控制PL端LCD显示的内容时,这种情况下,需要我们在系统上开辟一个缓存区域以用于图像的暂存用。

FPGA开辟缓存的方式有很多种,1)可以用FPGA芯片内部的资源开辟BLOCKRAM(优点是实现方便,缺点是FPGA内部的资源有限仅仅可以缓存小分辨率图像) 2)可以是FPGA芯片外部连接的 SDRAM, SRAM,纯FPGA连接的DDR,或者ZYNQ的PL部分连接的DDR 3)如果是ZYNQ平台,也可以是PS端的DDR芯片

我们Smart Zynq板子上硬件电路上有一块256M 16bit (即512MB)的DDR芯片,连接在主芯片的PS部分,也就是上述第三种情况。

PL访问PS上DDR缓存的方式有很多种,其中有一种专门为图像传输而开发的方式是VDMA, 这也就是我们需要用到VDMA的原因

VDMA 用于 AXI Stream 格式的数据 和 Memory Map 格式相互转换, 也就是说 VDMA 提供从 AXI4 域到 AXI4-Stream 域的视频读/写传输功能

芯片内部架构:因为板子上的DDR是直接物理连接到ZYNQ的PS部分上的,所以PS对DDR的访问控制只需要映射DDR的地址,然后对DDR的映射地址进行读写即可,但是如果ZYNQ的PL(FPGA)部分需要访问DDR,则必须要通过AXI_HP端口。

二、实验内容:

接下来用实际工程演示,PS部分通过软件对DDR进行彩色条纹的写入,并使用VDMA模块,让DDR内存中的彩色条纹通过LCD屏显示出来

三、Vivado工程创建

工程创建的过程可以参考实验一中的内容,这里不详细描述了。基于Smart ZYNQ (SP/SP2/SL 版) 的PS实验一 GPIO之用EMIO方式点亮LED(完整图文) (芯片型号选XC7Z020CLG484-1)

四、Vivado 中的设置

1)IP INTEGRATOR→Create Block Design,在弹出的对话框中输入设计名,最后点击“OK”,如下图所示

2)在右侧的窗口里 ,点击加号,在选择框里搜索ZYNQ,并找到ZYNQ7 PROCESSING SYSTEM ,双击并打开

3) 软件自动生成了一个 zynq的block 如下图所示,接下来要做一些相应的设置,双击下图中的ZYNQ核

4)依次在弹窗里找到DDR Configuration→DDR Controller Configuration→DDR3,在Memory Part下拉菜单中根据自己板子上的DDR来选择相应的DDR3,本实验所用到型号:MT41K256M16RE-125,数据位宽选择16bit 最后点击“OK”,如下图所示。

5)在ZYNQ中设置时钟功能:

找到 设置项目中的 Clock Configuration 选项, 在PL Fabric Clocks 设置自己需要的时钟频率,这里一共有4种频率可以设置 类似于我们的PLL功能。这里FCLK_CLK0我们设置成25M(用于LCD 25M DCLK用)FCLK_CLK1设置成50M时钟用于AXI_LITE基础通信,然后增加FCLK_CLK2 150M(用于AXI4 HP高速接口用)

6) 在PS-PL接口部分,增加 AXI HP 高速接口

7)为了 方便后续 例程的展示,这里增加一个SD的功能(为接下来 TF图片直接在 LCD 屏上显示做准备工作,本章节用不到)

8)添加一个VDMA模块,搜索并添加VDMA模块,并双击打开这个模块的配置页

本次工程只用到了 VDMA的 读功能,所以这里勾除了Enable Write Channel功能,帧缓存保留默认的3(本例程只是静态图的显示 ,如果是动态的图片显示,需要多帧缓存,画面才不会有撕裂感), 因为我们显示的内容是RGB888,所以 将系统默认的stream Data Width 从32位改成24位。 Line Buffer Depth默认512(只要AXI总线的速率大于显示的数据流的速率,缓存可以改更小),Read Burst Size 修改成64

在系统里增加video out 模块,这个模块的作用是将AXI4-Stream 的数据流转换成标准的RGB888视频格式,可以和我们最后的屏幕LCD RGB888输入接口相对应

参数设置除了clock mode 修改成独立,其余都保留默认值

Clock Mode代表aclk与vid_io_out_clk两个时钟是否同源。因为HDMI 的显示和aclk的输入这里的时钟域并不同(HDMI显示的像素时钟和ACLK的数据流时钟不同),所以这里选择independent。让两个时钟相互独立。

Timing Mode:因为后面我们要用video timing controller 模块来提供视频时序,所以这里选择slave模式

9)增加Video Timing Controller 模块

video Timing Controller是视频时序控制器,支持AXI-Lite接口协议(可通过该接口在PS端动态调整分辨率,如果是固定分辨率则不需要该接口,这里演示我们不需要动态调整)

在block design图形编辑器里搜索并添加 video timing controller模块,并双击打开配置页

因为我们不需要动态配置 分辨率,所以这里把LITE端口关闭,以及取消数据监视功能

在第二页我们设置分辨率,这里我们设置800X480测试用,因为我们的LCD屏幕的800×480分辨率不在预设分辨率之中,所以这里我们手动设置参数。 选择CUSTOM,其他的参数按下图所示填写即可(下列参数是我根据LCD屏幕手册推荐的参数修改得到,DCLK时钟25M情况下可以输出约每秒60帧

五、在Block Design 中对之前添加的模块进行连接

这时我们已经得到了所需要的各个模块(还有些复位模块后面系统会自动添加 先忽略),接下来的工作就是连接各个模块

1) 首先像下图(黄色部分)这样完成 图像数据流部分的连接

2)左键 video out 模块的vid_io_out 端口的“加号”,展开端口的内容,并选中vid_active_video,vid_data,vid_hsync,vid_vsync这几个信号,并右键make external 将信号引出

分别将这些信号重命名为lcd_de,lcd_data,lcd_hsync,lcd_vsync。 (选中信号,在左侧窗格的name 中修改)

3)引出我们的FCLK_CLK0信号作为lcd的lcd_clk信号: 右键ZYNQ模块的 FCLK_CLK0,并选中Make External 引出这个信号

并选中输出的port,将名字修改成lcd_clk

4) 将VTC(Video Timing Controller)模块的clk与25M的DCLK像素时钟(通过ZYNQ的FCLK_CLK0产生)链接,因为这个clk和像素时钟需要高度同步,同理Axi4-Stream to video_out模块的clk也应该和这个像素时钟连接。

5) 将AXI4 LITE低速通信的时钟连接到FCLK_CLK1的50M时钟上

6)将和数据流有关部分的时钟与FCLK_CLK2(150M)连接如下图所示

7) 增加video timing 模块的GEN_CLKEN连线如下图所示

8) 点 Run connection automation 来自动连接剩下的走线

在弹出的设置对话框里勾选内容,如下图所示

之后系统将自动帮我们连接好剩下的信号线以及添加需要的模块

9) 连接各个模块的使能和复位功能

a ) 新添加一个 Processor System Reset 模块(系统之前已经帮我们添加了两个时钟的复位模块(150M的和50M的),但是我们的VTC 和 Video out 等模块都是基于CLK_0也就是25M的,如果这时候把复位信号接到 150M或者50M的复位模块上,系统也能使用,但是会弹出警告以及timing 警告),所以这里才需要额外添加一个基于CLK_0的 复位模块。

这里将Processor System Reset的 slowest_sync_clk连接到 ZYNQ的 FCLK_CLK0上也就是我们的DCLK相关的25M上, ext_reset_in接到ZYNQ的复位输出上。

b )将VTC ,Video out模块的 rst_n复位信号接到我们刚刚添加的process system reset的peripheral_aresetn信号上(低电平复位)

由于video out模块的vid_io_out_reset是高电平复位,我们将这个信号接到刚刚添加的process system reset的peripheral_reset信号上(高电平复位)

c )我们添加一个constant模块来添加一个常量1,用于连接我们各个模块的ce以及aclken 使能信号

之后将这个constant 像下图一样,连接video out 的aclken信号,vid_io_out_ce信号,以及VTC的clken信号

最终的连接图如下(看似很复杂,其实连线的时候 只需要分清楚各个模块是从高速接口拉出 还是低速的接口,传输的是指令AXI LITE 还是数据流 AXI4 STREAM 还是 像素PIX,熟悉后再连就不会有问题了) 其中很多模块都是自动生成的 图片可能会看不清楚,可以在示例工程里查看

10 ) 保存工程,然后点击source→Design Source ,右键我们创建的BLOCK工程,点击create HDL wrapper,打包BLOCK文件并生成.v代码

软件自动为我们生成HDL文件

至此,Block Design 部分已经设置完成。

六、添加我们的FPGA部分顶层TOP_module代码,并例化ZYNQ模块

其实这里我们不增加顶层模块,直接编译综合也是可以,但是考虑到我们的工程后续可能会添加PL部分(即FPGA)的代码,所以这里我们还是增加了顶层模块。

1)在主界面点击左侧 Add Sources ,点击 复选框的Add or create design sources 选项 并点击NEXT

此图片的 alt 属性为空;文件名为 image-5.png

2)在出现的Add Sources 中 选择创建新文件 Create FILE 如下图所示,并在弹出的窗口中 选择类别为Verilog ,在FILE name中填写文件的名称,这里用TopModule代替,点击OK 并点击FINISH

3)在跳出的窗口中可以填写模块的输入输出信号,由于这部分工作在代码中可以完成,所以这里直接点OK 完成VERILOG 文件的创建。

4)双击打开刚才创建的Top_Module文件 ,这里我们可以添加我们的FPGA代码,这里我们暂时不在工程中添加任何其他功能,所以我们只需要对刚刚定义的Block design中创建的ZYNQ模块进行例化即可。

5) 在TopModule程序中例化ZYNQ模块

打开刚才的Top_Module.v,并添加下列代码,里面包括ZYNQ模块的例化部分,以及lcd信号的连接,注意这里的背光控制LCD_BL 是直接给的1,也就是背光永远是打开状态。

//www.hellofpga.com
`timescale 1ns / 1ps
module Top_Module(
output [7:0] LCD_R,
output [7:0] LCD_G,
output [7:0] LCD_B,
output LCD_HS,
output LCD_VS,
output LCD_DE,
output LCD_CLK,
output LCD_BL
);

assign LCD_BL=1'b1;

ZYNQ_CORE_wrapper u1(
.lcd_clk(LCD_CLK),
.lcd_data({LCD_R,LCD_G,LCD_B}),
.lcd_de(LCD_DE),
.lcd_hsync(LCD_HS),
.lcd_vsync(LCD_VS)
);


endmodule

肯定有人会问 ZYNQ_CORE 里明明有那么多信号,为啥我都留空了只保留了lcd控制相关信号,其实这里我也偷懒了,因为本章中ZYNQ只额外添加了LCD部分的接口,而剩下诸如DDR FIX_IO这种信号线因为本身是硬件连接的,所以即使程序例化的时候留空,这些信号线仍然是硬件同外部连接的。当然你也可以将所有的信号添加全。(备注 如果有其他的EMIO 或者AXI 等涉及到和FPGA通讯或者映射的信号,则这里例化的时候必须添加信号上去)

这样我们就在Top_Module.v中增加了ZYNQ部分了。 (这里容易产生误区,ZYNQ模块是硬件存在的,并不是实例化凭空出现的,只是通过例化这种方式将PS和顶层模块PL两部分结合在一起了)

七、添加约束并对工程进行编译和综合

1) 新增约束文件 用约束文件方式添加管脚定义(本文以Smart ZYNQ SP SL SP2的管脚为例)

set_property PACKAGE_PIN U22 [get_ports LCD_VS]
set_property PACKAGE_PIN T22 [get_ports LCD_DE]
set_property PACKAGE_PIN W22 [get_ports LCD_CLK]
set_property PACKAGE_PIN V22 [get_ports LCD_HS]
set_property PACKAGE_PIN Y16 [get_ports LCD_BL]
set_property PACKAGE_PIN Y20 [get_ports {LCD_B[7]}]
set_property PACKAGE_PIN Y21 [get_ports {LCD_B[6]}]
set_property PACKAGE_PIN AA22 [get_ports {LCD_B[5]}]
set_property PACKAGE_PIN AB22 [get_ports {LCD_B[4]}]
set_property PACKAGE_PIN AA21 [get_ports {LCD_B[3]}]
set_property PACKAGE_PIN AB21 [get_ports {LCD_B[2]}]
set_property PACKAGE_PIN AB20 [get_ports {LCD_B[1]}]
set_property PACKAGE_PIN AB19 [get_ports {LCD_B[0]}]
set_property PACKAGE_PIN Y19 [get_ports {LCD_G[7]}]
set_property PACKAGE_PIN AA19 [get_ports {LCD_G[6]}]
set_property PACKAGE_PIN AA16 [get_ports {LCD_G[5]}]
set_property PACKAGE_PIN AB16 [get_ports {LCD_G[4]}]
set_property PACKAGE_PIN AA18 [get_ports {LCD_G[3]}]
set_property PACKAGE_PIN Y18 [get_ports {LCD_G[2]}]
set_property PACKAGE_PIN AB15 [get_ports {LCD_G[1]}]
set_property PACKAGE_PIN AB14 [get_ports {LCD_G[0]}]
set_property PACKAGE_PIN AA13 [get_ports {LCD_R[7]}]
set_property PACKAGE_PIN Y13 [get_ports {LCD_R[6]}]
set_property PACKAGE_PIN W13 [get_ports {LCD_R[5]}]
set_property PACKAGE_PIN V13 [get_ports {LCD_R[4]}]
set_property PACKAGE_PIN W17 [get_ports {LCD_R[3]}]
set_property PACKAGE_PIN W18 [get_ports {LCD_R[2]}]
set_property PACKAGE_PIN AB17 [get_ports {LCD_R[1]}]
set_property PACKAGE_PIN AA17 [get_ports {LCD_R[0]}]

set_property IOSTANDARD LVCMOS33 [get_ports LCD_VS]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_HS]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_DE]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_CLK]
set_property IOSTANDARD LVCMOS33 [get_ports LCD_BL]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_R[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_G[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {LCD_B[0]}]

2) 点击绿色箭头RUN 对代码进行编译

3) 生成bit文件 :按下Generate Bitstream 完成综合以及生成bit文件,等待弹出综合完成的窗口

八、SDK部分配置

1)File→Export→Export hardware…,在弹出的对话框中勾选“include bitstream”,点击“OK”确认,如下图所示。

2)File→Lauch SDK,在弹出的对话框中,保存默认,点击“OK”,如下图所示。

系统将自动打开SDK开发环境

3)创建一个新的空工程,可以取名叫LCD_VDMA_TEST

4)右键工程 的SRC目录,然后新建一个SOURCE FILE,并取名main.c

5)接下来在我们创建的main.c中 复制以下代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "xil_types.h"
#include "xil_cache.h"
#include "xparameters.h"
#include "xaxivdma.h"
#include "xaxivdma_i.h"
#include "sleep.h"

#define WIDTH 800
#define HEIGHT 480

#define VDMA_BASEADDR XPAR_AXI_VDMA_0_BASEADDR

#define VIDEO_BASEADDR0 0x01000000
#define VIDEO_BASEADDR1 VIDEO_BASEADDR0+WIDTH*HEIGHT*3

uint8_t *frame_buffer0 = (uint8_t *)VIDEO_BASEADDR0;
uint8_t *frame_buffer1 = (uint8_t *)VIDEO_BASEADDR1;

int main(void) {

Xil_Out32((VDMA_BASEADDR + 0x000), 0x00000001);
Xil_Out32((VDMA_BASEADDR + 0x05c), (uint32_t)frame_buffer0);
Xil_Out32((VDMA_BASEADDR + 0x060), (uint32_t)frame_buffer1);
Xil_Out32((VDMA_BASEADDR + 0x058), (WIDTH*3));
Xil_Out32((VDMA_BASEADDR + 0x054), (WIDTH*3));
Xil_Out32((VDMA_BASEADDR + 0x050), HEIGHT);
Xil_Out32((VDMA_BASEADDR + 0x028), 0X00000000);

unsigned int i, j;

for (i = 0; i < WIDTH; i++) {
for (j = 0; j < HEIGHT; j++) {
if(i<=WIDTH/4){
frame_buffer0[(j * WIDTH + i) * 3 + 2] = 0xFF;
frame_buffer0[(j * WIDTH + i) * 3 + 1] = 0x00;
frame_buffer0[(j * WIDTH + i) * 3 + 0] = 0x00;
}
else if(i<=WIDTH/4*2){
frame_buffer0[(j * WIDTH + i) * 3 + 2] = 0x00;
frame_buffer0[(j * WIDTH + i) * 3 + 1] = 0xFF;
frame_buffer0[(j * WIDTH + i) * 3 + 0] = 0x00;
}
else if(i<=WIDTH/4*3){
frame_buffer0[(j * WIDTH + i) * 3 + 2] = 0x00;
frame_buffer0[(j * WIDTH + i) * 3 + 1] = 0x00;
frame_buffer0[(j * WIDTH + i) * 3 + 0] = 0xFF;
}
else {
frame_buffer0[(j * WIDTH + i) * 3 + 2] = 0xFF;
frame_buffer0[(j * WIDTH + i) * 3 + 1] = 0xFF;
frame_buffer0[(j * WIDTH + i) * 3 + 0] = 0xFF;
}
}
}

for (i = 0; i < WIDTH; i++) {
for (j = 0; j < HEIGHT; j++) {
if(j<=HEIGHT/4){
frame_buffer1[(j * WIDTH + i) * 3 + 2] = 0xFF;
frame_buffer1[(j * WIDTH + i) * 3 + 1] = 0x00;
frame_buffer1[(j * WIDTH + i) * 3 + 0] = 0x00;
}
else if(j<=HEIGHT/4*2){
frame_buffer1[(j * WIDTH + i) * 3 + 2] = 0x00;
frame_buffer1[(j * WIDTH + i) * 3 + 1] = 0xFF;
frame_buffer1[(j * WIDTH + i) * 3 + 0] = 0x00;
}
else if(j<=HEIGHT/4*3){
frame_buffer1[(j * WIDTH + i) * 3 + 2] = 0x00;
frame_buffer1[(j * WIDTH + i) * 3 + 1] = 0x00;
frame_buffer1[(j * WIDTH + i) * 3 + 0] = 0xFF;
}
else {
frame_buffer1[(j * WIDTH + i) * 3 + 2] = 0xFF;
frame_buffer1[(j * WIDTH + i) * 3 + 1] = 0xFF;
frame_buffer1[(j * WIDTH + i) * 3 + 0] = 0xFF;
}
}
}

Xil_DCacheFlush();

while(1){

Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000000);
sleep(1);
Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000001);
sleep(1);

}

}

VDMA配置部分代码解读:

本文中我们的VDMA 并没有用库函数来进行配置,而是用更简洁的Xil_Out32方式来进行初始化和控制,我们在BLOCK DESIGN 中定义了VDMA的缓存有3个,这里我们只用两个缓存来进行演示。

1)VDMA功能使能(Start VDMA)

Xil_Out32((VDMA_BASEADDR + 0x000), 0x00000001);

多缓存区存在的目的是为了防止画面的撕裂,正常情况下图像会在不同的缓存中来回切换循环,这里为了方便演示,我们用上面的代码来使能VDMA功能,但是不对缓存图像进行循环,如需要循环,可以把这里的BIT1拉高,即整个。(这个寄存器的其他的详细功能可以查看VDMA的手册PG020)

2) 缓存区映射地址设置

Xil_Out32((VDMA_BASEADDR + 0x05c), (uint32_t)frame_buffer0);
Xil_Out32((VDMA_BASEADDR + 0x060), (uint32_t)frame_buffer1);

frame_buffer0frame_buffer1是一个指向内存中图像数据缓冲区的指针。VDMA_BASEADDR +0x05c 是VDMA_BASEADDR +0x060 用来设置 源缓冲区0和1的起始地址。这里的 frame_buffer0frame_buffer1相当于图像数据的起始地址,设置后意味着 VDMA 将从这个缓冲区读取数据。

3)配置图像宽高和每行字节数

Xil_Out32((VDMA_BASEADDR + 0x058), (WIDTH*3));
Xil_Out32((VDMA_BASEADDR + 0x054), (WIDTH*3));
Xil_Out32((VDMA_BASEADDR + 0x050),  HEIGHT);

这三句代码配置了 VDMA 控制器的图像尺寸和数据传输参数。首先,VDMA_BASEADDR + 0x058VDMA_BASEADDR + 0x054 被设置为 WIDTH * 3,指定每一行图像数据的字节数(因为每个像素包含 3 个颜色通道:RGB,所以这里要x3)。然后,VDMA_BASEADDR + 0x050 被设置为图像的高度 HEIGHT,确定图像的行数。这些设置确保 VDMA 控制器可以正确地按行读取和传输图像数据。

VDMA图像缓冲区的填充

for (i = 0; i < WIDTH; i++) {
for (j = 0; j < HEIGHT; j++) {
if (i <= WIDTH / 4) {
frame_buffer0[(j * WIDTH + i) * 3 + 2] = 0xFF;
frame_buffer0[(j * WIDTH + i) * 3 + 1] = 0x00;
frame_buffer0[(j * WIDTH + i) * 3 + 0] = 0x00;
} else if (i <= WIDTH / 4 * 2) {
frame_buffer0[(j * WIDTH + i) * 3 + 2] = 0x00;
frame_buffer0[(j * WIDTH + i) * 3 + 1] = 0xFF;
frame_buffer0[(j * WIDTH + i) * 3 + 0] = 0x00;
} else if (i <= WIDTH / 4 * 3) {
frame_buffer0[(j * WIDTH + i) * 3 + 2] = 0x00;
frame_buffer0[(j * WIDTH + i) * 3 + 1] = 0x00;
frame_buffer0[(j * WIDTH + i) * 3 + 0] = 0xFF;
} else {
frame_buffer0[(j * WIDTH + i) * 3 + 2] = 0xFF;
frame_buffer0[(j * WIDTH + i) * 3 + 1] = 0xFF;
frame_buffer0[(j * WIDTH + i) * 3 + 0] = 0xFF;
}
}
}

for (i = 0; i < WIDTH; i++) {
for (j = 0; j < HEIGHT; j++) {
if (j <= HEIGHT / 4) {
frame_buffer1[(j * WIDTH + i) * 3 + 2] = 0xFF;
frame_buffer1[(j * WIDTH + i) * 3 + 1] = 0x00;
frame_buffer1[(j * WIDTH + i) * 3 + 0] = 0x00;
} else if (j <= HEIGHT / 4 * 2) {
frame_buffer1[(j * WIDTH + i) * 3 + 2] = 0x00;
frame_buffer1[(j * WIDTH + i) * 3 + 1] = 0xFF;
frame_buffer1[(j * WIDTH + i) * 3 + 0] = 0x00;
} else if (j <= HEIGHT / 4 * 3) {
frame_buffer1[(j * WIDTH + i) * 3 + 2] = 0x00;
frame_buffer1[(j * WIDTH + i) * 3 + 1] = 0x00;
frame_buffer1[(j * WIDTH + i) * 3 + 0] = 0xFF;
} else {
frame_buffer1[(j * WIDTH + i) * 3 + 2] = 0xFF;
frame_buffer1[(j * WIDTH + i) * 3 + 1] = 0xFF;
frame_buffer1[(j * WIDTH + i) * 3 + 0] = 0xFF;
}
}
}

这两段代码分别填充了 frame_buffer0frame_buffer1 两个图像缓冲区,用于生成两个不同的颜色区域。

  • 在第一段代码中,frame_buffer0 被分为四个垂直的区域:左侧 1/4 的区域为红色,接下来的 1/4 为绿色,接下来的 1/4 为蓝色,最后的 1/4 为白色。
  • 第二段代码则为 frame_buffer1 填充类似的颜色区域,但这次是按水平分割的:上方 1/4 为红色,下方的 1/4 为绿色,接下来的 1/4 为蓝色,最后 1/4 为白色。

因此,最终 frame_buffer0 显示的是一个由四个竖直色块组成的图像,而 frame_buffer1 显示的是一个由四个水平色块组成的图像。

主循环解读

  while(1){
Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000000);
sleep(1);
Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000001);
sleep(1);
}

主循环很容易理解,前面我们已经对缓存帧0和缓存帧1分别填了不同的图像,这里我们通过配置028h的4-0bit来让vdma停留在我们指定的缓存帧上(前文中已将vdma 配置在非循环状态下,Park Mode)

结合while(1)以及sleep,最终我们实现了让屏幕在缓存帧1和缓存帧0中以1秒为间隔来回切换。

备注,关于VDMA切换,这里直接用了Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000000);或者Xil_Out32((VDMA_BASEADDR + 0x028), 0x00000001);来进行切换,实际VDMA 28h这个寄存器还包含很多内容,因为我们整个硬件构架只用了VDMA读取功能,所以这里可以偷懒使用这种方法来进行帧的切换,详细内容可以看VDMA的官方手册(如果有VDMA写的部分,这里就不能直接这么操作,而是需要先将当前28h的寄存器内容读出,再把5-0位修改成目标帧号,再将修改后的数据写入回去)。

十、下载到板子上进行验证

选中工程中的硬件平台,并点击右键→Program FPGA,在弹出的对话框中选择默认,点击“program”,完成FPGA PL部分的Program工作

2)选中我们的工程 展开绿色箭头(RUN)右边的图标,选择Run As→1 Launch on Hardware(System Debugger)

正常情况可以看到LCD上显示着RGBW彩色条纹,并在横竖两种状态下不停切换证明我们的VDMA 已经工作正常了

备注 :为了方便调试可以按照下列描述进行勾选,这样每次debug的时候就自动重新对PL部分进行配置了(强烈推荐把每个工程都这样设置)

之后点 APPLY 然后 再选择Run As→1 Launch on Hardware(System Debugger)就可

写在后面:

网络上有部分教程导入了第三方 dynamic clock generator 模块来产生动态时钟,大家有兴趣的自行尝试

  • 本文的完整工程下载:21_VDMA_5INCH_LCD_COLOR_BAR
  • VIVADO的版本:2018.3
  • 工程创建目录:E:\Smart_ZYNQ_SP_SL\SDK\21_VDMA_5INCH_LCD_COLOR_BAR
  • 工程适用主板: Smart ZYNQ (SP / SP2 / SL) (不适用于Smart ZYNQ 标准版 

发表回复

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