基于BRAM的PS和PL的数据交互
在MPSOC开发过程中,PL和PS之间经常需要做数据交互。对于传输速度要求较高、数据量大、地址连续的场合,可以通过AXI DMA来完成。而对于数据量较少、地址不连续、长度不规则的情况,此时AXI DMA便不再适用了。针对这种情况,可以通过BRAM来进行数据的交互。本章我们来学习下基于BRAM的PS和PL的数据交互。
本章包括以下几个部分:
- 简介
- 实验任务
- 硬件设计
- 软件设计
- 下载验证
简介
BRAM(Block RAM)是PL部分的存储器阵列,PS和PL通过对BRAM进行读写操作,来实现数据的交互。在PL中,通过输出时钟、地址、读写控制等信号来对BRAM进行读写操作(关于BRAM的操作时序,请参考“RAM IP核实验”);而在PS中,处理器并不需要直接驱动BRAM的端口,而是通过AXI BRAM控制器来对BRAM进行读写操作。AXI BRAM控制器是集成在Vivado设计软件中的软核,可以配置成AXI4-lite接口模式或者AXI4接口模式。AXI4-Lite 接口模式的框图如图18.1.1所示。
图18.1.1 AXI4-Lite BRAM控制器框图
AXI4接口模式的BRAM控制器支持的数据位宽为32位、64位、128位、512位和1024位,而AXI4-Lite接口仅支持32位数据位宽。由图18.1.1可知,PS通过AXI4-Lite接口访问BRAM,当使能ECC选项时,ECC允许AXI主接口检测和纠正BRAM块中的单位和双位错误。AXI BRAM控制器作为AXI总线的从接口,和AXI主接口实现互联,来对BRAM进行读写操作。针对不同的应用场合,该IP核支持单次传输和突发传输两种方式。
实验任务
本章的实验任务是PS将串口接收到的数据写入BRAM,然后从BRAM中读出数据,并通过串口打印出来;与此同时,PL从BRAM中同样读出数据,并通过ILA来观察读出的数据与串口打印的数据是否一致。
硬件设计
根据实验任务我们可以画出本次实验的系统框图,如下图所示:
图18.3.1 系统框图
在图18.3.1中,PS端的M_AXI_HPM作为主端口,与PL端的AXI BRAM控制器IP核和PL读BRAM IP核(pl_bram_rd)通过AXI4总线进行连接。其中,AXI互联IP(AXI Interconnect)用于连接AXI存储器映射(memory-mapped)的主器件和从器件;AXI BRAM控制器作为PS端读写BRAM的IP核;PL读BRAM IP核是我们自定义的IP核,实现了PL端从BRAM中读出数据的功能,除此之外,PS端通过AXI总线来配置该IP核读取BRAM的起始地址和个数等。
由框图可知,本次实验创建的BRAM为双端口的RAM,其中一个端口连接AXI BRAM控制器,另一个连接PL读BRAM IP核。
首先创建Vivado工程,工程名为“ps_pl_bram”,然后创建Block Design设计(design_1.bd)并添加Zynq UltraScale+ MPSOC模块。接下来按照《“Hello World”实验》中的步骤2-7、2-8分别配置PS的UART和DDR控制器。需要特别注意的是,我们在《“Hello World”实验》的步骤2-10中,移除了PS中与PL端交互的接口信号,这些接口在我们本次实验中需要予以保留。
最后点击右下角的“OK”,本次实验Zynq UltraScale+ MPSOC就配置完成了。
Zynq UltraScale+ MPSOC配置完成后其接口如图18.3.2所示:
图18.3.2 Zynq UltraScale+ MPSOC
接下来我们要在Block Design中添加AXI BRAM Controller IP核,在Diagram窗口空白位置右击,然后选择“Add IP”。在弹出的IP目录中搜索“AXI BRAM”,最后双击搜索结果中的“AXI BRAM Controller”将其添加到设计中,如下图所示:
图18.3.3 添加AXI BRAM Controller IP核
添加完成后,双击AXI BRAM Controller IP核,打开其配置界面如下图所示:
图18.3.4 BRAM控制器配置页面
在图18.3.4中,AXI Protocol(AXI 协议)选择的是AXI4,对于本次实验来说,选择AXI4或者AXI4-Lite没有影响;Data Width(数据位宽)选择32位,由于AXI4总线为字节寻址,因此在映射到BRAM地址时,需要按4字节寻址。本次实验的BRAM控制器只需要读写BRAM的一个端口,因此将BRAM的总线个数设置为1;ECC选项用于数据错误纠正与检查,这里不使能。
需要说明的是,Memory Depth(存储深度)在这里不可以设置,寻址BRAM的存储深度是在Address Editor里设置的,最后点击“OK”按钮完成设置。
接下来在Block Design中添加BRAM IP核,即Block Memory Generator IP核。在Diagram窗口空白位置右击,然后选择“Add IP”。在弹出的IP目录中搜索“Block Memory”,最后双击搜索结果中的“Block Memory Generator”将其添加到设计中,如下图所示:
图18.3.5 添加Block Memory Generator IP核
添加完成后,双击Block Memory Generator IP核,打开其配置界面如下图所示:
图18.3.6 Block Memory Generator IP核配置页面
BRAM IP核支持两种模式,一种是独立模式(Stand Alone),在此模式下,可以*配置RAM的数据深度和宽度;另一种是BRAM控制器模式(BRAM Controller),在此模式下,地址和数据默认为32位,由于本次实验添加了BRAM控制器IP核,因此BRAM模式选择BRAM控制器模式。
Memory Type(存储类型)设置为“True Dual Port RAM”,即真双口RAM。一端连接PL读BRAM IP核,另一端连接BRAM控制器。
接下来将页面切换至“Other Options”的选项,如下图所示:
图18.3.7 Block Memory Generator IP核“Other Options”配置页面
BRAM IP核内置了一个安全电路以降低BRAM数据出现错误的概率,如果勾选使能安全电路,BRAM端口会增加rsta_busy端口和rstb_busy端口,用于表示何时可以访问BRAM。这里直接取消使能安全电路,最后点击“OK”按钮完成设置。
添加完成后Diagram窗口如下图所示:
图18.3.8 Diagram窗口
接下来点击图18.3.8中箭头所指示的位置,弹出对话框如下图所示:
图18.3.9 自动连接
在左侧勾选“All Automation”,下面列出了会自动连接的模块及其接口。点击“OK”,工具会自动连接AXI BRAM Controller IP核的BRAM_PORTA接口和S_AXI接口。
连接完成后,在Diagram窗口空白处右击,然后选择“Regenerate Layout”对设计进行重新布局,布局后的界面如下图所示:
图18.3.10 重新布局后的设计界面
在图18.3.10中,箭头1指向的AXI SmartConnect是一种新型系统连接生成器,同样也是实现将一个或多个内存映射主设备连接到一个或多个内存映射从设备,一般可以作为AXI InterConnect的替代品,且具有更好的性能。箭头2指向的“Block Memory Generator”IP核,其中一个端口连接到了AXI BRAM控制器,另一个端口连接PL读BRAM IP核,这个IP核是我们自定义的IP核,接下来添加这个IP核。
自定义的IP核在例程工程目录下的ip_repo文件夹下,如图18.3.11所示。大家可以直接拷贝这个文件夹至工程目录下,然后通过菜单栏的Tool→Setting→IP→Repository,点击“ADD”图标来添加自定义的IP核。由于本次实验的自定义IP核的部分设置和“自定义IP核-呼吸灯实验”有些区别,因此我们接下来在本次实验工程的基础上,来向大家演示创建一个新的自定义IP核。
图18.3.11 ip_repo文件夹
点击菜单栏的“Tools”,选择“Creat and Package New IP…”,如下图所示:
图18.3.12 创建一个新的自定义IP核
此时弹出创建和封装新IP核的页面,点击“NEXT”,选择“Create a new AXI4 Peripheral”,再次点击“NEXT”,进入下图所示页面。
图18.3.13 自定义IP核设置页面
在Name一栏的名称改为“pl_bram_rd”,IP核的路径改为工程目录下的ip_repo文件夹,即删除路径“/../”中间的一个“.”符号,其它的设置直接保持默认即可,点击“NEXT”,直到最后点击“Finish”按钮完成自定义IP核的创建。
在“IP Catalog”界面下,依次展开User Repository→AXI Peripheral→“pl_bram_rd_v1.0”,右击“pl_bram_rd_v1.0”,选择“Edit in IP Packager”,如下图所示:
图18.3.14 编辑IP核
在弹出的页面中,点击“OK”,进入编辑自定义IP核的工程界面。
打开pl_bram_rd_v1_0.v文件,在Users to add ports here和User port ends中间行添加如下代码,这些端口用于连接BRAM端口的BRAM_PORTB。
在实例化pl_ram_rd_v1_0_S00_AXI模块的位置,添加以下代码。
打开pl_bram_rd_v1_0_S00_AXI.v文件,同样在Users to add ports here和User port ends中间行添加如下代码:
在程序最后Add user logic here和User logic ends的中间行,添加如下代码:
这段代码例化了bram_rd模块,其中start_rd信号是开始读BRAM的开始信号,start_addr是设置BRAM的读起始地址,rd_len是设置读BRAM的个数,分别连接到AXI4总线的寄存器地址0、地址1和地址2对应的数据。
接下来右击“Design Sources”,选择“Add Sources…”向工程中添加“bram_rd.v”文件,位于../ps_pl_bram/ip_repo/pl_bram_rd_1.0/hdl路径下,_rd模块的代码如下:
程序实现了根据输入的读开始信号(start_rd)、读起始地址(start_addr)和读数据长度(rd_len)从RAM中读出数据。
代码全部保存完成后,双击IP-XACT界面下的component.xml切换至Packaging Steps界面。点击“File Groups”一栏,随后点击界面上的“Merge changes from File Groups Wizard”;点击“Customization Parameters”一栏,随后点击界面上的“Merge changes from Customization Parameters Wizard”。
点击“Ports and Interfaces”一栏,可以看到该IP核顶层模块的端口,为了方便在Diagram界面中对自定义的IP核的BRAM端口进行连线,我们需要将BRAM相关的端口定义成总线接口的形式,方法如下。
点击“Add Bus Interfac”图标,如下图所示:
图18.3.15 添加总线接口
在弹出的页面中,在Name一栏,输入BRAM_PORT,然后点击Interface Definition(总线定义)右侧的“…”图标,如下图所示:
图18.3.16 选择一个总线接口
接下来在弹出页面的搜索框中输入“BRAM”,选中Advanced一栏下的“bram_rtl”,即BRAM总线接口。然后点击“OK”按钮,如下图所示:
图18.3.17 选择“bram_ctrl”
将页面切换至“Port Mapping”选项页,左侧窗口为总线逻辑端口,右侧窗口为IP核定义的物理端口,最下面的窗口是映射端口的总结页面,如下图所示:
图18.3.18 端口映射页面
点击左侧的“EN”端口,然后点击右侧的“ram_en”端口,此时页面上的“Map Ports”变成可以点击的按钮,点击这个按钮,此时在映射端口总结页面可以看到这两个端口映射到了一起,如下图所示:
图18.3.19 映射“EN”端口和“ram_en”端口
这个步骤是将顶层模块定义的“ram_en”端口和BRAM的“EN”端口映射到一起,我们接下来将其余接口分别一一映射,如图18.3.20和图所示:
图18.3.20 映射端口总结页面1
图18.3.21 映射端口总结页面2
接下来将页面切换至“Parameters”选项页,展开Auto-calculated,选中“MASTER_TYPE”,然后点击单个向右的箭头,如下图所示:
图18.3.22 添加“MASTER_TPYE”参数
此时,“MASTER_TYPE”参数会出现在右侧Overridden目录下,最后点击“OK”按钮完成BRAM接口的封装,如下图所示:
图18.3.23 BRAM接口封装完成页面
接下来切换至“Review and Package”页面,点击上侧的“IP has been modified”来更新IP,最后点击“Re-Package IP”完成IP核的封装,此时IP核的封装界面会自动关闭。
返回本次实验的工程界面,在Diagram界面中添加刚刚封装的IP核。在Diagram窗口空白位置右击,然后选择“Add IP”。在弹出的IP目录中搜索“pl_bram”,最后双击搜索结果中的“pl_bram_rd_v10”将其添加到设计中,如下图所示:
图18.3.24 添加“pl_bram_rd”IP核
添加完成后,点击界面上方的“Run Connection Automation”,在弹出页面的左侧勾选“All Automation”,下面列出了会自动连接的模块及其接口。点击“OK”,工具会自动连接PL读BRAM IP核的BRAM_PORT接口和BRAM IP核的BRAM_PORTB接口。
连接完成后,在Diagram窗口空白处右击,然后选择“Regenerate Layout”对设计进行重新布局,布局后的界面如下图所示:
图18.3.25 重新布局后的设计界面
接下来切换至“Address Editor”页面,展开“zynq_ultra_ps_e_0”下的“Data”,将范围设置成“4K”,如图18.3.26所示。由于BRAM的数据位宽是32位,因此BRAM的存储深度为1K。
图18.3.26 分配存储空间
到这里我们的Block Design就设计完成了,在Diagram窗口空白处右击,然后选择“Validate Design”验证设计。验证完成后弹出对话框提示“Validation Successful”表明设计无误,点击“OK”确认。最后按快捷键“Ctrl + S”保存设计。
接下来在Source窗口中右键点击Block Design设计文件“system.bd”,然后依次执行“Generate Output Products”和“Create HDL Wrapper”。
在左侧Flow Navigator导航栏中找到SYNTHESIS,点击该选项中的“Run Synthesis”,如下图所示:
图18.3.27 点击“Run Synthesis”
生成综合文件后,点击“Set Up Debug”来添加信号至ILA界面,如下图所示:
图18.3.28 点击“Set Up Debug”
在弹出的Set Up Debug窗口中点击“Next”,接下来的窗口中点击“Find Nets to Add”依次搜索并添加blk_mem_gen_0模块实例化U0模块下的addrb、doutb和enb信号,这三个信号分别对应BRAM PORTB端口的地址、读出的数据和RAM使能信号,如下图所示:
图18.3.29 搜索添加信号至ILA
然后分别设置三个信号的Clock Domain,设置方法为右击每个信号,选择Select Clock Domain,在弹出窗口的下拉栏中选择“LOCAL_CLOCK”,此时可以看到下面显示栏中出现“design1_1_i/zynq_ultra_ps_e_0/inst/pl_clk0”,然后点击“OK”,设置完成后如下图所示:
图18.3.30 添加待观察信号
点击Next按钮,接下来设置采样的深度,采样深度设置的值越大,采集到的数据越多。
图18.3.31 ILA设置选项
点击Next按钮,最后点击Finish按钮完成信号的添加,按快捷键“Ctrl + S”保存Synthesized Design,在弹出的Save窗口中输入约束文件名,这里直接输入工程名“ps_pl_bram”,然后点击“OK”。
最后在左侧Flow Navigator导航栏中找到PROGRAM AND DEBUG,点击该选项中的“Generate Bitstream”,对设计进行综合、实现、并生成Bitstream文件。
在生成Bitstream之后,在菜单栏中选择 File > Export > Export hardware导出硬件,并在弹出的对话框中,勾选“Include bitstream”。将导出的“design_1_wrapper.xsa”文件放到vitis文件夹,然后在菜单栏选择Tools > Launch Vitis,启动VITIS软件。
软件设计
在VITIS软件中新建一个空的应用工程,应用工程名为“pl_sysmon_vpvn”。然后为应用工程新建一个源文件“main.c”,我们在新建的main.c文件中输入本次实验的代码:
主函数实现了通过串口接收用户数据,然后将用户数据写入BRAM,并从BRAM中读出数据的功能。主函数包含一个无限循环while语句,程序首先打印一串字符串来提示用户开始输入数据,然后通过scanf函数来接收串口的数据,存放在字符串数组ch_data中。这里的“%1024s”是指单次最大接收1024个字符,这是因为BRAM的深度为1024,防止写入BRAM的数据个数超出BRAM的深度。
然后计算接收到的数据个数,并调用自己编写的str_wr_bram函数将数据写入BRAM。数据写完后,调用自己编写的str_rd_bram()函数从BRAM中读出数据。
接下来在程序的第35至53行,将接收到的数据写入BRAM中,并配置PL端开始从BRAM中读取数据。在写BRAM的过程中,通过XBram_WriteReg()函数将接收到的数据按照起始地址,依次写入BRAM中。在数据写入完成后,通过AXI总线,配置PL端读取BRAM的数据个数和起始地址,并驱动PL读开始信号输出一个脉冲信号。
在程序第55至65行PS端读取BRAM中的数据。通过XBram_ReadReg()函数按照BRAM的起始地址,依次从BRAM中读出数据,并通过串口打印出来。
下载验证
首先我们将下载器与开发板上的JTAG接口连接,下载器另外一端与电脑连接。然后使用USB连接线将USB_UART(开发板PS PORT)接口与电脑连接,用于串口通信。最后连接开发板的电源,给开发板上电。
打开Vitis Terminal终端,设置并连接串口。然后下载本次实验的程序,下载完成后,在下方的VITIS Terminal中可以看到应用程序打印的信息“Please enter data to read and write BRAM”。实验结果如下图所示:
图18.5.1 下载验证
此时返回到Vivado软件,在左侧Flow Navigator导航栏中找到PROGRAM AND DEBUG,展开“Open Hardware Manager”,点击该选项中的“Open Target”,选择“Auto Connect”,如下图所示:
图18.5.2自动连接器件
此时会打开ILA观察波形的界面。我们接下来设置触发条件,点击“Trigger Setup”界面下的“+”图标,选择enb信号,将Value的值设置为“R”,如下图所示:
图18.5.3 添加触发条件
点击工具栏的Run Trigger for this ILA core,此时界面上出现了等待触发的状态,如下图所示:
图18.5.4 等待触发
接下来打开VITIS的软件界面,在VITIS Terminal中点击箭头处的按钮打开输入框,输入www.openedv.com,并按回车键发送。此时可以看到串口中打印的信息,如图18.5.5和图18.5.6所示:
图18.5.5 发送数据界面
图18.5.6 窗口打印信息界面
由上图可知,PS写入BRAM的数据和从BRAM中读出的数据一致。接下来我们在ILA界面观察PL从BRAM中读出的数据是否正确。
返回到Vivado界面,可以看到ILA逻辑分析仪的界面已经显示触发信号的波形。对波形进行放大,将doutb的显示格式改成ASCII,如图18.5.7和图18.5.8所示:
图18.5.7 切换显示的进制
图18.5.8 ILA波形显示界面
由上图可知,在enb拉高期间,doutb输出的数据分别是“www.openedv.com”,和串口助手发送的数据和打印出的数据一致,说明本次实验验证成功。
在这里需要注意的是,如果大家使用其它串口调试助手的时候,需要勾选“发送新行”,否则PS的串口接收程序会一直等待接收串口的数据,串口调试助手的界面设置如下:
图18.5.9 串口助手设置界面