首先我们先创建工程
然后创建block design
添加PS处理器
自动配置 ZEDBOARD的预设。
好了以后我们创建一个新 IP 核
因为我们要创建一个AXI4总线标准的IP核,所以这里我们选择AXI4总线的外设
NEXT
NEXT
这一步要选择接口类型、接口模式、以及数据位宽还有寄存器个数。
这里因为我们是一个作为VGA显示的驱动,所以数据是由上层输入给这个IP核,然后由这个IP在输出VGA口,所以我们是从设备,这里接口模式就选择从设备(Slave),数据位宽保持默认就好,因为会比较方便和其他IP连接。但实际上我们只用到里面一些数据。
寄存器个数这里使用默认的4个就足够了。
NEXT
FINISH
进入IP工程编辑窗口
来到这个界面我们即可开始实现我们的IP核。
首先将我们上一节写的代码拿出来。添加到工程中
这里Bram没有实现可以先暂时不用处理,我们最后用【IP Catalog】生成一个bram就行。这是上次案例的东西。
现在我们假想AXI4外部顶层是一个统一的包装。但是核心是我们的VGA驱动,但是用这个核心的包装我们可以和其他模块进行交流。因此,我们将我们需要输入输出的端口 在这个“包装”里面声名。也就是在AXI4 IP 核自动生成的顶层.v文件下加入我们的接口。
也就是下面的接口。
我们在如图所示的区域声名,vivado已经帮我们留出了位置
接下来,在【zed_vga_v1_0_S00_AXI_inst】模块例化时也要加上。
因为【Zed_vga_v1_0】模块相当于是一个外面的包装,【zed_vga_v1_0_S00_AXI_inst】是内包装,我们核心的【vga_driver】才是核心。他们的区别:
【Zed_vga_v1_0】是单纯为了实现包装而存在,而
【zed_vga_v1_0_S00_AXI_inst】是实际实现了AXI总线传输协议时序的模块。用户一般实例化自己的模块也是在这个.v文件里面例化。
现在我们先把【zed_vga_v1_0_S00_AXI_inst】完整例化,
从某中角度上说,我们现在是一个自顶向下操作的。
然后在【zed_vga_v1_0_S00_AXI_inst】模块中声明端口
【zed_vga_v1_0_S00_AXI_inst】代码可能比较多,但是绝大部分你都不用关心,声名了端口后,滑动到最下方有一个 Add user logic here 的注释,在这里,我们完成我们的关于AXI4总线的逻辑和核心模块的例化,以及他们两个的交互关系。
这里,我们对每一个信号逐一分析。
【sys_clk】:是系统输入的时钟,是写入缓存的时钟,所以是100MHZ的系统时钟,在这里我们可以直接用AXI的时钟 【S_AXI_ACLK】
【rst_i】:复位信号高电平有效 ,不复位的话,直接可以拉低 为【1’b0】
【we_addr】( 是否写入地址 )
【we_data】( 是否写入数据 )
这里对于we信号我们可以做一个逻辑判断,将【slv_reg_wren】与【axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]】的值相遇,当都要写入的话,先判断【axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]】的值是0还是1,如果是0就是【we_data】有效,是1就是【we_addr】有效。这个功能将会与我们在驱动内部实现的一个小部分对应。
【din】( 将要写入的数据) 【S_AXI_WDATA】
最后的实现:
现在【ctrl+s】保存看看我们的整个IP核结构
一层套一层。底层很复杂,越到上层接口越少,逻辑实现也越简单越简洁。就像代码语言一样,从下到上封装得越来越好,高级语言越来越简单方便。
现在,我们注意!在vga_driver中外部数据输入部分的缓存时钟是我们的系统时钟100MHZ,这是个爱出bug的点。若采用25MHZ最后生成图像会花屏,很恐怖。
最后一步工作了,我们该例化我们的bram IP核了。这个上节我们应该讨论过了。
这里初始化文件可以选也可以不用
对了,这里B,读出端口的 第一季缓存寄存器我们可以不用设置
然后生成一下BRAM IP
最后好了以后,回到这个界面
点一下更新
最后重新打包一下
现在完成IP核的创建了,我们回到bd界面,可以添加我们的IP核了。
现在添加一个25MHZ的时钟
连接好以后就点击自动连线就好,最后的布局:
然后生成bd
生成顶层HDL文件
可以看到现在的工程结构
现在添加管脚约束
set_property PACKAGE_PIN Y19 [get_ports {v_sync}]
set_property IOSTANDARD LVCMOS33 [get_ports {v_sync}]
set_property PACKAGE_PIN AA19 [get_ports {h_sync}]
set_property IOSTANDARD LVCMOS33 [get_ports {h_sync}]
set_property PACKAGE_PIN V20 [get_ports {R[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {R[0]}]
set_property PACKAGE_PIN U20 [get_ports {R[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {R[1]}]
set_property PACKAGE_PIN V19 [get_ports {R[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {R[2]}]
set_property PACKAGE_PIN V18 [get_ports {R[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {R[3]}]
set_property PACKAGE_PIN AB22 [get_ports {G[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {G[0]}]
set_property PACKAGE_PIN AA22 [get_ports {G[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {G[1]}]
set_property PACKAGE_PIN AB21 [get_ports {G[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {G[2]}]
set_property PACKAGE_PIN AA21 [get_ports {G[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {G[3]}]
set_property PACKAGE_PIN Y21 [get_ports {B[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {B[0]}]
set_property PACKAGE_PIN Y20 [get_ports {B[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {B[1]}]
set_property PACKAGE_PIN AB20 [get_ports {B[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {B[2]}]
set_property PACKAGE_PIN AB19 [get_ports {B[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {B[3]}]
生成bit流
好了以后
点击cacel,然后去生成硬件顶层文件,接着Launch SDK
再创建我们的主程序,【vga_test】
模板用Helloworld就行,或者空项目也行。
现在我们先编写VGA显示部分的代码,首先先写一个【vga_utils.c】
vga_utils.c
宏定义类型
#include "stdlib.h"
#define u16 unsigned short
#define u32 unsigned int
#define u8 unsigned char
然后在 ASCII table网站上可以找到字符编码集合
我们取出一些常见的来使用
然后,我们 定义我们的地址和数据寄存器。
打开vivado,找到【address editior】
可以找到,我们的AXI的寄存器开始地址是 0x43C00000,当时我们只用到了2个寄存器对吧,一个传数据另一个传地址。因为是32位的寄存器,所以这里0号寄存器我们用来传数据,1号用来传地址它们相差4个字节(一个字节八位),
故有
#define VGA_DATA_REG *(volatile unsigned int * )(0x43c00000)
#define VGA_ADDR_REG*(volatile unsigned int *) (0x43c00004)
volatile是不自动优化寄存器。
现在和之前做VGA时序一样,我们要绘制VGA,
void VGA_DrawPoint(u16 x,u16 y,u16 dot)
{
VGA_ADDR_REG = y*640+x;
VGA_DATA_REG = dot ;
}
给对应的区域声名背景颜色
void VGA_Fill(u16 x1,u16 y1,u16 x2,u16 y2,u16 dot)
{
u16 x,y;
for(x=x1;x<=x2;x++)
{
for(y=y1;y<=y2;y++)
VGA_DrawPoint(x,y,dot);
}
}
在指定字符的位置显示一个char
void VGA_ShowChar(u16 x,u16 y,u8 chr,u16 dot,u16 bg)
{
u16 temp,t,t1;
u16 y0=y;
chr=chr-' ';//得到偏移后的值
for(t=0;t<16;t++)
{
temp=asc2_1608[chr][t]; //调用1608字体
for(t1=0;t1<8;t1++)
{
if(temp&0x80)VGA_DrawPoint(x,y,dot);
else VGA_DrawPoint(x,y,bg);
temp<<=1;
y++;
if((y-y0)==16)
{
y=y0;
x++;
break;
}
}
}
}
显示字符串
void VGA_ShowString(u16 x,u16 y,const u8 *p,u16 dot,u16 bg)
{
#define MAX_CHAR_POSX 640
#define MAX_CHAR_POSY 480
while(*p!='\0')
{
if(x>MAX_CHAR_POSX){x=0;y+=16;}//换行
if(y>MAX_CHAR_POSY){y=x=0;VGA_Fill( 0 ,0,640,480, 0 );}//清屏
VGA_ShowChar(x,y,*p,dot,bg);
x+=8;
p++;
}
}
显示一幅图像
void VGA_ShowBMP_640x480( u16 * p ){
int i = 640*480 ;
VGA_ADDR_REG = 0 ;
while(i--) VGA_DATA_REG = *p++ ;
}
写了这么多方法,应该非常足够我们调试了。
我们在main方法中,写一个测试方法看看是否能够完成我们预期的设想。
在坐标100,100的位置显示一串字符串
启动的配置
成功了!,背景的图片是我们默认在bram初始化的图像。
现在我们在ps上试试加载一幅图片。
1、先将VGA显示器默认填充为黑色
2、再读入图片
我们看看运行结果
现在按照我们的方法,可以随心所欲在VGA上成像了。如果能够编写一些复杂的程序甚至可以做一些图像小游戏。贪吃蛇,走迷宫什么的。。。
不过总而言之,这次实验比较复杂,PL+PS都涉及到了,比较综合,需要多多理解。