ZYNQ PS读写PL BRAM
一、实验室任务
本章的实验任务是 PS 将数据写入BRAM,然后从 BRAM 中读出数据,并通过串口打印出来;与此同时,PL 从通过自定义ip核从BRAM中同样读出数据,并通过ILA 来观察读出的数据与串口打印的数据是否一致。这里是通过PS写入进的只有数据信息,而无法读数据,因此通过ILA观察数据。
二、实验整体架构
三、自定义ip写进ram的控制ip核
这个ip核呢是将我们通过AXI输入进BRAM的数据通过自定义的ip核转为合适的ram接口信息接到ramip核上,这样就可以通过ps端输入的读使能,读地址读数据长度信息转化为ram的读使能,读地址,保证时序正确。
四、设计流程
一、自定义ip核。
1、创建IP管理器
2、选择AXI接口的ip核,然后一直保持默认最后点击完成,
3、编辑这个ip,右键ip核,然后选择编辑选项,进入编辑页面,然后默认ok就行
4、添加一个控制文件(.v)如下
为什么要写这个模块呢?为什么不能通过ps侧输入读使能,读数据直接连接到ram的ip核上呢?在我看来这是因为ps侧的逻辑无法精准控制时钟逻辑,也就是ps侧无法做到像pl侧的严格时序控制。pl侧的逻辑是在每个时钟内做自己的事情,每个时钟周期都有严格的要求,因此我们在PS侧无法做到如此严格的时序要求。所以通过一个自定义控制模块对输入的信号进行处理,使得时序要求满足我们pl侧的逻辑。
module ram_ip
(input pl_rst_n ,input pl_clk ,input ps_read_en ,//开始读的使能input [31:0] ps_start_addar ,//开始读的起始地址input [31:0] ps_data_num ,//需要读取的数量//bram接口output ram_clk ,//RAM时钟input [31:0] ram_rd_data ,//RAM中读出的数据,这个接口是读出来的数据需要我们通过该控制器输出到ps端。output reg ram_en ,//RAM使能信号,这个接口是读使能信号,需要根据我们ps侧输入的几个信息给出合适的读信号。output reg [31:0] ram_addr ,//RAM地址,这个也需要通过ps侧输入的起始地址和需要读取的数量来生成的。output reg [3 :0] ram_we ,//RAM读写控制信号output reg [31:0] ram_wr_data ,//RAM写数据output ram_rst //RAM复位信号,高电平有效);assign ram_clk=pl_clk;
assign ram_rst=1'b0;always @(posedge pl_clk)
if(!pl_rst_n)ram_we<=4'd0;
else ram_we<=ram_we;always @(posedge pl_clk)
if(!pl_rst_n)ram_wr_data<=32'd0;
else ram_wr_data<=ram_wr_data;reg ps_read_en_reg;
always @(posedge pl_clk)
if(!pl_rst_n)ps_read_en_reg<=1'b0;
else ps_read_en_reg<=ps_read_en;wire ps_read_en_pos=ps_read_en&~ps_read_en_reg;parameter A_EDA=3'd0,A_ADD=3'd1,A_DOW=3'd2;reg [2:0] state;
always @(posedge pl_clk)if(!pl_rst_n)state<=A_EDA;else case(state)A_EDA:state<=(ps_read_en_pos) ? A_ADD : A_EDA;A_ADD:state<=(ram_addr-ps_start_addar+2'd2==ps_data_num) ? A_DOW : A_ADD;A_DOW:state<=A_EDA;default:state<=A_EDA;endcasealways @(posedge pl_clk)if(!pl_rst_n)ram_en<=1'b0;else case(state)A_EDA:if(ps_read_en_pos) ram_en<=1'b1;A_ADD:ram_en<=1'b1;default:ram_en<=1'b0; endcasealways @(posedge pl_clk)if(!pl_rst_n)ram_addr<=32'd0;else case(state)A_EDA:if(ps_read_en_pos) ram_addr<=ps_start_addar;A_ADD:ram_addr<=ram_addr+1'b1;default:ram_addr<=32'd0;endcaseendmodule
上面是我们的需要添加进我们ip核的verilog代码。下面是systemverilog仿真代码和仿真图。由图可知在我们的pl侧受到来自ps的使能和读数据信息时候,会产生一个高脉冲,然后通过状态机完成对读数据信息的转化,转化成我们pl侧ram接口可以识别的信息,比如读使能,使能对应的读地址。由仿真图也可以看出:当我们的pl侧检测到来自ps侧的上升沿时就会立马进入ram接口时序生成状态,依据起始地址,读数量等生成我们需要的接口时序。
`timescale 1ns/1ns
module ram_ip_sim();reg pl_rst_n ;
reg pl_clk ;
reg ps_read_en ;
reg [31:0] ps_start_addar ;
reg [31:0] ps_data_num ;//bram接口
wire ram_clk ;
reg [31:0] ram_rd_data ;
wire ram_en ;
wire [31:0] ram_addr ;
wire [3 :0] ram_we ;
wire [31:0] ram_wr_data ;
wire ram_rst ;initial beginpl_clk<=1'b0;forever #10 pl_clk=~pl_clk;
endinitial beginpl_rst_n=1'b0;repeat(3) @(posedge pl_clk);pl_rst_n=1'b1;
endinitial beginps_read_en =1'b0 ;ps_start_addar =32'd0;ps_data_num =32'd0;ram_rd_data =32'd0;
endtask data_send;repeat(3) @(posedge pl_clk);ps_read_en =1'b1 ;ps_start_addar =32'd0 ;ps_data_num =32'd10;repeat(15) @(posedge pl_clk);ps_read_en =1'b0 ;ps_start_addar =32'd0;ps_data_num =32'd0;endtask : data_send;initial beginwait(pl_rst_n);fork beginrepeat(1) @(posedge pl_clk);data_send;endjoin
endram_ip ram_ip_un
(.pl_rst_n (pl_rst_n ),.pl_clk (pl_clk ),.ps_read_en (ps_read_en ),//开始读的使能.ps_start_addar (ps_start_addar ),//开始读的起始地址.ps_data_num (ps_data_num ),//需要读取的数量.ram_clk (ram_clk ),//RAM时钟.ram_rd_data (ram_rd_data ),//RAM中读出的数据,这个接口是读出来的数据需要我们通过该控制器输出到ps端。.ram_en (ram_en ),//RAM使能信号,这个接口是读使能信号,需要根据我们ps侧输入的几个信息给出合适的读信号。.ram_addr (ram_addr ),//RAM地址,这个也需要通过ps侧输入的起始地址和需要读取的数量来生成的。.ram_we (ram_we ),//RAM读写控制信号.ram_wr_data (ram_wr_data ),//RAM写数据.ram_rst (ram_rst ) //RAM复位信号,高电平有效);endmodule
5、需要修改的地方
在顶层模块添加输出端口
在顶层例化的模块添加例化模块修改的端口。
在AXI接口模块添加输出接口,然后例化我们的bram控制器
在例化我们添加的模块可知,我们例化模块的使能信号,我存在寄存器0的第一位,因此我们需要在ps侧C语言代码中操作该寄存器就可以给该bram控制模块输出想要的信息,同理我们用到了寄存器1和2分别接收来自ps侧的起始地址和读数据量,然后由该信息就可以让bram控制模块输出满足时序要求的读使能端口和读地址。
6、点击文件组,这里就是我们添加和修改文件后需要刷新
7、这里是常量刷新(如果你是替换的代码就先进代码随便打个空格然后保存就会刷新)
8、按照如下操作分别将信号接口从时钟和复位接口移出去ram_rst和ram_clk
9、这里有两个警告,这是因为我们顶层的输出端口没有和其他的ip核端口和形成映射关系,因此这里会有警告不知道对应的接口是哪个,因此我们添加输出端口映射。
10、添加输出端口映射。右键随便一个端口,然后点击add bus interface。
11、先修改我们要对接的端口,这里我们选择bram端口。然后添加该接口映射的总线名字,然后选择主端口,因为这个模块控制的是ram ip的读数据信息。
12、添加端口映射,点一下需要映射的端口然后点击映射即可,按照此流程将所有端口映射。这样做的目的是为了我们软件可以自动帮我们连线,不需要我们一个一个连。
13、将端口的属性添加到自适应列。这样我们连接端口时,端口的信号会自动和从机端口属性保持一致,这样不需要我们一个一个的设置。
14、点击提示信息
15、点击下面的生成ip核
二、IP组装
1、先修改我们的最小系统IP,因为在hello_word实验时将有些端口取消了,这里我们重新添加回来,如下:GP接口和时钟复位添加进来
2、添加AXI bram控制器,并按照如图修改
3、添加bram ip核,并按照如图修改两个地方
4、添加我们自己写的ip控制器,不需要修改
5、点击自动连接,然后全部选中,然后为这两个ip核的端口设置连接的ip核接口,这样这两个ip核的接口就会自动连接到我们的双端口bram上去,实现一个ip核写,一个ip核读。然后点击确定后点击重新布局。
6、验证设计,然后验证无误点击ok即可。
7、修改ram容量大小,这里可以将两个都改为4k也可以不改。
8、生成输出文件,然后ok即可
9、生成顶层文件,这里需要记住的是在原来的hello的工程基础上的顶层没有改动,可以不生成,如果是重新建立的工程要生成顶层文件。以下是已经生成过的顶层文件重新生成,默认点击ok即可
三、添加ILA
1、点击综合
2、点击综合设计->点击step up debug
3、next后添加我们要探针的信号,如图,导航到ram ip核的U0下添加我们要探查的信号
4、为探查信号添加时钟,搜索选择ALL_CLOCK,然后选择下列时钟后点击🆗。重复刚才步骤为这三个信号都添加时钟。然后一直默认点击ok。
5、然后点击实现,然后点击生成bit流
6、导出设计文件,勾选bit流后点击🆗即可,一定要勾选bit流,因为这里用到了pl端的设计。
7、打开SDK
四、SDK设计
1、新建空项目。
2、建立一个源文件,命名main.c
3、复制如下代码
#include "stdio.h"
#include "xbram_hw.h"
#include "stdio.h"
#include "custom_pl_bram_ps.h"
#include "xparameters.h"
#include "sleep.h"#define PL_BRAM_START CUSTOM_PL_BRAM_PS_S00_AXI_SLV_REG0_OFFSET//寄存器1输出使能信号
#define PL_BRAM_START_ADDR CUSTOM_PL_BRAM_PS_S00_AXI_SLV_REG1_OFFSET//寄存器2输出起始地址
#define PL_BRAM_LEN CUSTOM_PL_BRAM_PS_S00_AXI_SLV_REG2_OFFSET//寄存器3输出数据长度#define PL_BRAM_BASE XPAR_CUSTOM_PL_BRAM_PS_0_S00_AXI_BASEADDR //自定义ip的地址#define START_ADDAR 0//
#define PL_BRAM_DATA_LEN 4//每个bram的深度为4个字节char input_data[1024]="www.com.lzs" ;
int num_data ;int main()
{//通过调试发现如果没初值,那会将数据写入到系统分配的初始地址位置。int wr_cnt=START_ADDAR;printf("test start!");sleep(5);//while(1)//{//scanf("please input data: %s",input_data);num_data=strlen(input_data);for(int i=START_ADDAR*PL_BRAM_DATA_LEN;i<(START_ADDAR+num_data)*PL_BRAM_DATA_LEN;i+=PL_BRAM_DATA_LEN){//第一个参数为写入AXIip核的地址XBram_WriteReg(XPAR_BRAM_0_BASEADDR,i,input_data[wr_cnt]);printf("w_data:%c,odata : %d\n",input_data[wr_cnt],input_data[wr_cnt]);wr_cnt++;}//}//设备ip,ip映射(寄存器2),写入数据的bram起始地址。其实就是像我们自定义的ip核里面的AXI寄存器写入数据。//这个寄存器就是连接我们自己编写的bram读控制文件的输入端口。CUSTOM_PL_BRAM_PS_mWriteReg(PL_BRAM_BASE,PL_BRAM_START_ADDR,START_ADDAR*PL_BRAM_DATA_LEN);//配置长度CUSTOM_PL_BRAM_PS_mWriteReg(PL_BRAM_BASE,PL_BRAM_LEN,(START_ADDAR+num_data)*PL_BRAM_DATA_LEN);//配置使能,让我们的自定义ip采集上升沿CUSTOM_PL_BRAM_PS_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0x00000001);CUSTOM_PL_BRAM_PS_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0x00000000);printf("\naddares,data\n");int read_data;for(int i=START_ADDAR*PL_BRAM_DATA_LEN;i<(START_ADDAR+num_data)*PL_BRAM_DATA_LEN;i+=PL_BRAM_DATA_LEN){read_data=XBram_ReadReg(XPAR_BRAM_0_BASEADDR,i);printf("addares: %d, data: %c, odata : %d\n",i/PL_BRAM_DATA_LEN,read_data,read_data);}return 0;}
4、修改报错信息,这里是因为系统生成的库是按照我们自定义ip核名字生成的,如果你是按照我的步骤来的就复制最后修改好的代码。
5、找到对应的库文件,然后找到这几个参数复制改变定义
6、找到这几个库文件的函数修改为以下函数
7、修改完后保存,如果你的自定义ip核的命名也为pl_ps_bram就可以直接复制下面的
#include "stdio.h"
#include "xbram_hw.h"
#include "stdio.h"
#include "pl_ps_bram.h"
#include "xparameters.h"
#include "sleep.h"#define PL_BRAM_START PL_PS_BRAM_S00_AXI_SLV_REG0_OFFSET//寄存器1输出使能信号
#define PL_BRAM_START_ADDR PL_PS_BRAM_S00_AXI_SLV_REG1_OFFSET//寄存器2输出起始地址
#define PL_BRAM_LEN PL_PS_BRAM_S00_AXI_SLV_REG2_OFFSET//寄存器3输出数据长度#define PL_BRAM_BASE XPAR_PL_PS_BRAM_0_S00_AXI_BASEADDR //自定义ip的地址#define START_ADDAR 0//
#define PL_BRAM_DATA_LEN 4//每个bram的深度为4个字节char input_data[1024]="www.com.lzs" ;
int num_data ;int main()
{//通过调试发现如果没初值,那会将数据写入到系统分配的初始地址位置。int wr_cnt=START_ADDAR;printf("test start!");sleep(5);//while(1)//{//scanf("please input data: %s",input_data);num_data=strlen(input_data);for(int i=START_ADDAR*PL_BRAM_DATA_LEN;i<(START_ADDAR+num_data)*PL_BRAM_DATA_LEN;i+=PL_BRAM_DATA_LEN){//第一个参数为写入AXIip核的地址XBram_WriteReg(XPAR_BRAM_0_BASEADDR,i,input_data[wr_cnt]);printf("w_data:%c,odata : %d\n",input_data[wr_cnt],input_data[wr_cnt]);wr_cnt++;}//}//设备ip,ip映射(寄存器2),写入数据的bram起始地址。其实就是像我们自定义的ip核里面的AXI寄存器写入数据。//这个寄存器就是连接我们自己编写的bram读控制文件的输入端口。PL_PS_BRAM_mWriteReg(PL_BRAM_BASE,PL_BRAM_START_ADDR,START_ADDAR*PL_BRAM_DATA_LEN);//配置长度PL_PS_BRAM_mWriteReg(PL_BRAM_BASE,PL_BRAM_LEN,(START_ADDAR+num_data)*PL_BRAM_DATA_LEN);//配置使能,让我们的自定义ip采集上升沿PL_PS_BRAM_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0x00000001);PL_PS_BRAM_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0x00000000);printf("\naddares,data\n");int read_data;for(int i=START_ADDAR*PL_BRAM_DATA_LEN;i<(START_ADDAR+num_data)*PL_BRAM_DATA_LEN;i+=PL_BRAM_DATA_LEN){read_data=XBram_ReadReg(XPAR_BRAM_0_BASEADDR,i);printf("addares: %d, data: %c, odata : %d\n",i/PL_BRAM_DATA_LEN,read_data,read_data);}return 0;}
五、SDK验证
1、打开板子供电、连接串口
2、右键项目,打开运行配置
3、双击GDB,打开烧录bit流和复位
4、点击运行
5、我们发现AXIbram控制器ip核读写一致
六、ILA验证
这里我们需要注意,由于我的设计用不了scanf输入函数,因此我加了一个延迟然后马上回到ILA界面运行,等待enb拉高,触发逻辑分析仪。如果你的scanf能用就不用加延迟,就可以通过串口输入写入信息后,SDK程序会立马运行到拉高enb的信号函数。
还有一点就是如果你调不出ILA界面请点击以下链接,查看原因和解决方法
vivado 下载程序后没有ILA界面
1、点击设备管理器后点击自动连接,进入ila界面
2、设置触发条件,选择enb,选择上升沿触发。
3、回到SDK烧录界面,烧录后马上回到ILA界面点击运行。这里一定要马上回到这个界面点运行,然后等待触发。前面说过如果你的scanf能用就可以在SDK 终端输入后这里就会触发。而这里由于我的scanf用不了,我就加了个延迟,等几秒钟后会按顺序执行到SDK设置上升沿的使能程序。然后ILA界面就会触发,如下图
4、对比写入数据。将数据以ASCLL码的形式展现
5、对比发现我们逻辑分析仪捕捉到的自定义ip核读数据转为ASCLL码后和我们SDK PS侧写进PS侧RAM的数据是一致的。因此我们的设计正确。