本章节将演示PL(FPGA)端读写PS端DDR的功能,从而实现FPGA端的大量数据缓存,以及PS与PL间大数据量的交互。
在做FPGA项目的时候,我们经常会遇到需要将大量的临时数据进行暂存的情况,数据量较少的时候,我们可以使用片内BRAM的资源来进行暂存,但是片内BRAM资源一般都非常有限,这个时候我们就需要考虑将数据暂存到外部ram,sdram,或者ddr上了,ram和sdram的存储容量通常有限,如果要存储的数据比较多,就该ddr上场了。
一些纯FPGA芯片,比方说artix 或者kintex 等平台上,DDR一般会直接接到FPGA端,这个时候只用在FPGA中生成对应的DDR控制器来对DDR进行读取, 但是在ZYNQ上这种情况就发生变化了。ZYNQ本身PS端自带硬件DDR控制器,并且PS的正常工作通常离不开DDR,所以大部分ZYNQ的板子都只有PS端接DDR,PL端是没有接DDR的, 这时候如果我们FPGA端需要访问DDR,就要通过AXI的方式来实现了。(包括之前提到过的VDMA的方式,其实最终也是通过AXI来访问DDR的)。
PL访问DDR的另一个好处是,可以方便PS和PL之间大数据的交互,PS端与PL端在硬件上是相互独立的,之前曾在 Smart ZYNQ(SP&SL 版) 工程十五 ZYNQ端PS 访问 PL端的reg 寄存器,实现PS与PL数据交互 章节中介绍过通过REG方式来实现数据的交互,但这种方式仅适合小数据量的通信,如果数据比较多,那通过将数据存储在DDR来进行分享将是一个很好的数据交互方式。
本文在 vivado2018.3版本上 演示, 其他版本请自行研究
(备注 此章节内容适用于 Smart ZYNQ 标准版,如果是Smart ZYNQ SP,SL或者SP2版,请看对应板子目录
下面将进行具体的演示
一. 创建工程
1)打开Vivado 新建一个项目, 新建一个VIVADO 工程,打开软件 选中Create Project, 如下图所示
2)点击NEXT ,在出现的第二个对话框“Project name”中输入工程名;在“Project location”中选择保存路径;勾选“Create project subdirectory”(默认),最后点击“Next” 备注,所有的路径均不能出现中文名称
3)点击 RTL PROJECT 选项,点击NEXT
4) 第四步Add Sources 选项直接留空,NEXT
5)第五步Add Constraints 选项直接留空,NEXT
6)选择芯片型号 (板子生产了多个芯片型号,请根据自己板子的型号来进行选择,下图以XA7Z020来进行演示,其余型号自行选择,必须要按照实际型号去填写)
- XA7Z020 (直接选择xc7z020CLG400-1,因为系统识别到的是xc7z020-1)
- XC7Z010-1 (xc7z010CLG400-1) (7010 芯片)
备注 (7020 车规级版在vivado2018.3识别出的是xc7z020,如果工程选择xa7z020 ,在SDK环境下有时候会弹出警告,所以这里直接选择Xc7z020CLG400-1)
7)确认所选信息 点击“Finish”,完成vivado的工程创建
之后 工程就新建好了, vivado 进入到开发界面
二、创建一个带AXI 接口的自定义IP
- 重新创建并封装一个带AXI 接口的IP 具体过程如下 ,TOOLS->Create and Package New IP
2.选择封装带AXI4总线的
3、next,填写名称等信息,注意IP保存路径(保存到工程目录下就可)。这里给IP取名 PL_DDR_RW
4、next,选择总线相关信息,这里AXI的 协议选择 FULL, 然后模式选择 Master模式,位宽保留默认的32位,4即代表有四个32bit的寄存器 最小选择是4个
5.选择 下一步 EDIT IP 并点FINISH完成(选择 EDIT IP 我们就可以对生成的IP进行修改)
6.之后系统会自动生成一个该IP的工程。 这里我们双击打开系统默认生成的IP代码
该默认生成的模块实现了PL端通过AXI4协议对PS端的DDR进行读写的测试。 因为我们本次的实验不需要其他功能,所以不需要修改代码直接关闭修改IP的这个工程即可(如果需要自己增加功能,则修改此IP中对应的代码即可)。
三、Vivado 工程设计
1.增加ZYNQ 模块
1)创建一个BLOCK设计
2)搜索并添加ZYNQ7 Processing System,添加ZYNQ7 PROCESSING SYSTEM模块
软件自动生成了一个 zynq的block 如下图所示,接下来要做一些相应的设置,双击下图中的ZYNQ核
3)依次在弹窗里找到DDR Configuration→DDR Controller Configuration→DDR3,在Memory Part下拉菜单中根据自己板子上的DDR来选择相应的DDR3,本实验所用到型号:MT41K256M16RE-125,数据位宽选择16bit 最后点击“OK”,如下图所示。
4) 移除M AXI GP0 interface 的勾, 改成勾选 S AXI HP0 interface
为什么是HP AXI 由下图就可以看到 HP AXI 可以同时访问 片内OCM 以及片外DDR3的资源。
5) 使能UART 0 并在IO选项力 选择MIO方式,下拉菜单里选择MIO50-51
6) 所有的都完成后点选 OK 退出 ZYNQ配置界面
7) 在Block Design 中同样添加我们刚才创建的IP PL_DDR_RW
之后我们便看到PL_DDR_RW IP 已经被添加
简单介绍下前面生成的这个IP模块的功能,模块一开始工作在IDLE状态,当检测到 axi_init_axi_txn信号出现高电平时, 模块将从IDLE进入到 INIT_WRITE 模式对DDR进行写入操作,当写入完成后,模块会进入到INIT_READ,将刚才写入到DDR的数据再次读回来。最后系统进入到INIT_COMPARE 模式,对读写数据进行比较。 随后拉高 axi_txn_done表示操作结束,并在axi_error端口输出对比的结果。如果读写的数据不一致 axi_error信号会输出高电平,代表读写出现错误。
- 1. IDLE (检测axi_init_axi_txn信号是否出现高电平,是则进入 INIT_WRITE 模式)
- 2. INIT_WRITE (对DDR进行数据写入操作,完成后进入INIT_READ 模式)
- 3. INIT_READ (对DDR进行数据读取的操作,完成后进入INIT_COMPARE 模式)
- 4. INIT_COMPARE (对读写的数据进行对比,完成后拉高axi_txn_done ,并在axi_error输出对比的结果)
8)双击PL_DDR_RW 模块打开配置界面(按图片中进行设置 ,之后点选OK 确定)
将寄存器AXI TARGET SLAVE BASE ADDR的值修改为0x10000000,这个地址将作为我们PL对DDR进行读写的起始地址,这里有个值得注意的地方,用0x10000000而不是0x00000000 是因为0x00000000这个地址属于OCM 片内存储器的, 又因为DDR的前面部分地址需要给程序堆栈使用,所以这里我们才选地址为0x10000000。
9) 之后点选 Run Block Automation 和Run Conection Automation (弹出来的对话框都保持默认即可)
10)系统会帮我自动连接好走线,还有增加需要的模块
11) 为了方便系统演示, 所以这里要认为的增加两个LED灯, 来分别连接axi_txn_done 和 axi_error。
分别引出这两个端口信号(右键端口选中 Make External)
将两个信号的名称分别修改为ERROR 和DONE
12) 为了方便系统演示 我们这边还需要增加一个按键连接axi_aresetn 信号,又因为axi_aresetn 检测到高电平即开始读写操作,所以这里我们要增加一个非门,以让按键按下后才开始进行读写操作。
如下图所示 添加utility vector logic 模块,并双击模块进入配置界面
双击模块并将模块修改成1个bit 的非门
连接非门与axi_aresetn信号。
修改信号的名称。
最后得到的结构如下图所示
13) 创建硬件描述,source→Design Source ,右键我们创建的BLOCK工程,点击create HDL wrapper如下图所示(这一步的作用相当于将图纸转换成对应的硬件描述语言的功能)
13)编写好代码后 对代码进行编译
14 ) 添加管脚约束 ,可以在图文界面里设置管脚定义
其中KEY1接在KEY[0] ,LED1接在DONE ,LED2接在ERROR。
之后点选保存 会弹出约束的保存窗口,填写要保存的约束文件的名字(自定义)即可。
15 )综合并生成 bit文件
成功后会出现下图对话框,点OK 确认就好
四 、PL部分访问DDR 实机演示
(DDR部分需要由PS初始化后才能正常访问,所以如果系统上电后PS有运行过代码(即使是测试固件),那可以按第四部分内容继续走下去,否则请先按第五部分流程走,之后感兴趣的再来做第四部分内容)
用TYPE C数据线将板子的JTAG口和电脑连接,给板子通上电源后,点击 PROGRAM AND DEBUG 中的OPEN TARGET 下的Auto Connect 来连接板子
如果连接正常会在右侧 硬件栏中显示设备,如下图所示
接下来下载bit文件,右键设备 然后选择 Program Device
默认会出现生成的BIT文件,如果没有的话需要手动添加路径 ,点击PROGRAM 下载
下载之后, 此时两个绿灯都是熄灭状态, 此时按下板子上的KEY1(HDMI接插件边上的这个) PL开始对DDR进行读写操作,之后LED1灯被点亮(程序里定义的DONE 灯,代表读写操作完成),LED2(ERROR)错误提示灯保持熄灭状态, 证明我们的DDR读写实验成功,从DDR读出的数据和写入的数据是一致的。
(很多朋友在这里做出来数据结果和我的情况不一致,那是因为DDR需要PS进行初始化导致的,所以如果大家遇到这里不成功,请继续按下面步骤做,让PS的代码运行一下,这样DDR就能被正确配置并被PL端访问了)
五、PS部分工程创建
添加PS代码有两个作用:
- 1. 运行PS代码对DDR进行初始化,以便DDR能被PL正常的读写访问。
- 2. 添加PS代码读取我们PL对DDR写入的地址,来验证PL对DDR的访问是否成功,同时也为以后我们PS和PL的交互做铺垫。
1)File→Export→Export hardware…,在弹出的对话框中勾选“include bitstream”,点击“OK”确认,如下图所示。
2)File→Lauch SDK,在弹出的对话框中,保存默认,点击“OK”,如下图所示。
系统将自动打开SDK开发环境
3)再创建一个APP 工程,负责PS读取DDR 部分的数据用以验证。
a. 创建一个新的空工程,可以取名叫PL_DDR_TEST
b.右键工程的SRC目录,然后新建一个SOURCE FILE
c.取名 main.c
六、 PS端代码编写
1.在main .c中添加如下代码
#include "stdio.h" #include "xil_cache.h" #include "xil_printf.h" #include "xil_io.h" #define DDR_BASEADDR 0X10000000 int main() { int i; char A; Xil_DCacheDisable(); print("AXI4 PL DDR TEST!\n\r"); print("Please input A to start\n\r"); while(1){ scanf("%c",&A); if(A=='A'||A=='a'){ printf("start\n\r"); for(i=0;i<4096;i=i+4){ printf("The data for the address %x is %d\n\r",DDR_BASEADDR+i,(int)Xil_In32(DDR_BASEADDR+i)); } } } return 0; }
这里有个地方比较难以理解 ,那就是为什么Xil_in32()中的地址每次是增加4的。因为Xil_in32每次都是读取32个bit数据, 同样的,我们上文中创建设计的AXI4 IP,数据位宽也是32个bit,而系统中内存的单位地址是以字节(8bit)作为计量的,相当于不论是读还是写一个32bit的数据都占4个内存地址。所以这里地址增加的是4而不是1。
2.下载程序到板子上验证 (备注 如果刚才vivado 里已经做过上文提到过的实验了,那此处需要断电 让DDR回到无序状态,必须是断电,复位键没用)
1) 先提前打开串口助手(这里用vivado SDK 自带的)
2) 打开Run as -> Run Configurations
如果是第一次打开此页面,之前没有debug过,则双击system debugger选项
3 ) 在右侧 的窗口勾选 Reset entire system ,以及Program FPGA, 这样每次debug 的时候都会预先加载并配置FPGA。
4 ) 之后系统就会自动运行了(fpga也会被自动加载) 。 如果修改程序后 再次要debug,就可以直接点绿色箭头,或者选 Run As –>Launch on Hardware (system debugger即可)
5) 程序工作后,会通过串口输出 AXI4 PL DDR TEST ! 以及 Please input A to start 的内容, 此时按下键盘上的A,并按下Send, 向系统发送开始命令
之后,系统通过串口将从DDR读取到的内容发送给PC, 此时读到的DDR的数据都是毫无规律的,无序的数据(因为PL 这个时候并未向DDR发送数据,而上电后DDR的内容本身是乱序的)。(备注如果读到的内容是有序的,那是因为刚才vivado 里已经做过上文提到过的实验后没有断电过DDR已经被PL写入了一遍,那此处必须要重新断电让DDR回到无序状态再重新上述操作否则影响实验的结果,必须是断电,复位键没用)
6) 此时我们按下板子上的KEY1 键(HDMI接插件边上的这个) 让PL对DDR进行读写操作,按下按键之后LED1灯被点亮(程序里定义的DONE 灯,代表读写操作完成),LED2(ERROR)错误提示灯保持熄灭状态, 证明我们的DDR读写实验成功,从DDR读出的数据和写入的数据是一致的。
7) 此时再回到串口界面, 再次发送字符A,可以看到从板子发回的DDR内容,由乱序变成1,2,3,4这样的顺序内容了, 10000000代表 0x10000000地址位,这个地址和PL端设置的相呼应。 PS端从DDR3中读出的数据为1到1024,与PL端IP写入的数据一致,说明实验成功了。
本文只是抛砖引玉, 如果要修改传输的内容,只需要修改PL端自动生成的AXI IP修改即可,大家自行研究。
写在后面:经过多次试验发现, 如果频繁的按下axi_init_axi_txn连接的按键,有概率会出现error 灯亮起的情况,一旦error出现后再按下按键,错误不能消除,并且将模块的rst信号引出手动控制模块复位发现出现问题后复位模块并不能恢复通讯,所以猜测导致这个情况的很大原因可能是因为axi4通讯的过程中被中断导致axi4部分卡住引起的,这部分可能需要在官方的axi4代码中增加判忙内容来解决。
我这边也用了另一个思路,就是只有在IDLE状态下,axi_init_axi_txn 的边沿跳变(即按键按下)才被判断为有效状态。这样 错误情况就再也没有出现过了,大家自行尝试,有问题可在下方反馈留言 20240614
把IP中的代码
assign init_txn_pulse = (!init_txn_ff2) && init_txn_ff;
修改成
assign init_txn_pulse = (!init_txn_ff2) && init_txn_ff && mst_exec_state==IDLE;