使用BRAM进行PL与PS通信及ILA使用实例

前言

在FPGA开发板中,实现PL端与PS端的通信是一个常见的话题。在这里,为实现PYNQ-Z2的PL与PS端通信,使用BRAM作为解决方案。

方案构想

这里参考了一个教程自定义IP实现PL处理PS输入数据(不过这个教程太坑了,过于理论,实际是有问题,稍后会提到),使用AXI BRAM Controller进行PS端的BRAM读写,创建一个AXI4 IP,进行PL端的读写。

这里硬件部分不作过多的讲述,详细的硬件部分可以参考上面的链接,但代码部分请使用本文提供的替代。

以下为方案实施

IP核的创建

我们将要创建一个IP核,功能是用于读取BRAM中的数据并加一后写回。

这里通过Create and Package new IP向导,创建一个新的AXI4 Peripheral,这里创建8个寄存器,寄存器可用于传递状态、简单数据,部分寄存器分别用于PS端传递在BRAM中的基地址、BRAM读开始信号、起始地址、数据长度、读写完成标识及PL端的操作完成标识,剩下的寄存器预留给以后的拓展,完成后选择Edit IP进入IP核的编辑。IP核创建

IP核内容处理

我们需要添加一个可连接BRAM的端口用于PL端与BRAM的通信,因此在顶层文件中添加以下端口

output wire ram_clk ,                //RAM 时钟
input wire [31:0] ram_rd_data,       //RAM 中读出的数据
output wire ram_en ,                 //RAM 使能信号
output wire [31:0] ram_addr ,        //RAM 地址
output wire [3:0] ram_we ,           //RAM 读写控制信号
output wire [31:0] ram_wr_data,      //RAM 写数据
output wire ram_rst ,                //RAM 复位信号,高电平有

同时在模块中([你的IP核名称]_S00_AXI_inst)添加以上端口的实例化

.ram_clk (ram_clk ),
.ram_rd_data (ram_rd_data),
.ram_en (ram_en ),
.ram_addr (ram_addr ),
.ram_we (ram_we ),
.ram_wr_data (ram_wr_data),
.ram_rst (ram_rst )

然后打开[你的IP核名称]_S00_AXI.v文件,实例化以下接口

bram_wr inst_bram_wr(
.clk (S_AXI_ACLK),
.rst_n (S_AXI_ARESETN),
.start_rd (slv_reg0[0]),
.start_addr (slv_reg1),
.rd_len (slv_reg2),
.pl_wr_done (pl_wr_done[0]),//用于传递PL操作完成标识
.ps_rd_start (slv_reg4),//用于传递PS开始读取标识
//RAM 端口
.ram_clk (ram_clk ),
.ram_rd_data (ram_rd_data),
.ram_en (ram_en ),
.ram_addr (ram_addr ),
.ram_we (ram_we ),
.ram_wr_data (ram_wr_data),
.ram_rst (ram_rst )
);

并添加以下端口

output wire ram_clk ,                //RAM 时钟
input wire [31:0] ram_rd_data,       //RAM 中读出的数据
output wire ram_en ,                 //RAM 使能信号
output wire [31:0] ram_addr ,        //RAM 地址
output wire [3:0] ram_we ,           //RAM 读写控制信号
output wire [31:0] ram_wr_data,      //RAM 写数据
output wire ram_rst ,                //RAM 复位信号,高电平有

同时将Address decoding for reading registers下的slv_reg3修改为pl_wr_done,并在逻辑寄存空间下添加wire [C_S_AXI_DATA_WIDTH-1:0] pl_wr_done;以完成PL传递操作完成标识的连线。
最后创建一个新的wr_bram模块,添加以下内容

module bram_wr(
	input clk , //时钟信号
	input rst_n , //复位信号
	input start_rd , //读开始信号
	input [31:0] start_addr , //读起始地址
	input [31:0] rd_len , //读数据的长度
	output reg pl_wr_done ,    //pl端写出完毕
	input ps_rd_start ,    //ps端开始读取
	//RAM 端口
	output ram_clk , //RAM 时钟
	input [31:0] ram_rd_data, //RAM 中读出的数据
	output reg ram_en , //RAM 使能信号
	output reg [31:0] ram_addr , //RAM 地址
	output reg [3:0] ram_we , //RAM 读写控制信号
	output reg [31:0] ram_wr_data, //RAM 写数据
	output ram_rst //RAM 复位信号,高电平有效
	);
	
	 //reg define
	reg [2:0] flow_cnt;
	reg start_rd_d0;
	reg start_rd_d1;
	//wire define
	
	//*****************************************************
	//** main code
	//*****************************************************
	
	assign ram_rst = 1'b0;
	assign ram_clk = clk ;         //时钟clk
	assign pos_start_rd = ~start_rd_d1 & start_rd_d0;  //PL端操作开始信号
	
	//延时两拍,采 start_rd 信号的上升沿
	always @(posedge clk or negedge rst_n) begin
	if(!rst_n) begin
		start_rd_d0 <= 1'b0;
		start_rd_d1 <= 1'b0;
	end
	else begin
		start_rd_d0 <= start_rd;
		start_rd_d1 <= start_rd_d0;
		end
	end
	
	//根据读开始信号,从 RAM 中读出数据
	always @(posedge clk or negedge rst_n) begin
	   if(!rst_n) begin
		  flow_cnt <= 3'd0;
		  ram_en <= 1'b0;
		  ram_addr <= 32'd0;
		  ram_we <= 4'd0;
		  end
	   else begin
	       case(flow_cnt)
	           3'd0 : begin
	               if(ps_rd_start) begin
	                   flow_cnt <= 3'd0;   //正在被ps端占用
	               end
	               else if(pos_start_rd) begin
		              ram_en <= 1'b1;
		              ram_addr <= start_addr;
		              flow_cnt <= flow_cnt + 3'd1;
	               end
	           end
	           3'd1 : begin
	               flow_cnt <= flow_cnt + 3'd1;    //延迟一个时钟周期,原代码的修复一
	           end
	           3'd2 : begin
	               ram_we <= 4'b1111;
	               ram_wr_data <= ram_rd_data + 32'd1;    //内容加一并写回
	               flow_cnt <= flow_cnt + 3'd1;
	           end 
	           3'd3:begin
		          if(ram_addr - start_addr == rd_len - 4) begin //数据读完
			         flow_cnt <= flow_cnt + 3'd1;
			         ram_we <= 4'b0000;
		          end
		          else begin
			         ram_addr <= ram_addr + 32'd4; //地址累加 4
			         ram_we <= 4'b0000;      //BRAM写入信号拉低,原代码的修复二
			         flow_cnt <= flow_cnt - 3'd2;
		          end
	           end
	           3'd4 : begin           //完成状态
	               ram_addr <= 32'd0;  //地址还原为0
	               flow_cnt <= 3'd5;
	               ram_en <= 1'b0;      //取消bram使能
	               pl_wr_done <= 1'b1;  //PL端完成标识
	           end
	           3'd5 : begin           //传递PL端完成标识
	               if (ps_rd_start) begin     //PS端开始读取,完成PL端所有操作
	                   flow_cnt <= 3'd0;
	                   pl_wr_done <= 1'b0;
	               end
	               else begin
	                   flow_cnt <= 3'd5;      //PS端开始读取,PL端所有操作完成
	               end
	           end
	       endcase
	   end
	 end
endmodule

接下来,我们需要在Package IP中先点击File Groups然后应用文件变更,之后在Ports and Interfaces中添加一个Interface,General、Port Mapping、Parameters分别按下图方式设置
现在Repackage ip,IP核设计至此完成。

测试该IP核

为做一个简单的Testbench,这里进行如图的设计,原生的AXI BRAM Controller用于PS端BRAM读写,自定义IP核用于PL端的操作
然后生成顶层、创建HDL Wrapper之后生成比特流并导出硬件,硬件部分至此完成。

SDK部分

创建一个新的Application Project,并使用C语言编写,可以创建一个hello world示例,并使用以下TestBench粘贴替换

#include "xil_printf.h"
#include "stdio.h"
#include "bram_wr.h"
#include "xbram.h"
#include "xparameters.h"

#define PL_BRAM_BASE XPAR_BRAM_WR_0_S00_AXI_BASEADDR //PL_RAM_RD 基地址
#define PL_BRAM_START BRAM_WR_S00_AXI_SLV_REG0_OFFSET //RAM 读开始寄存器信号
#define PL_BRAM_START_ADDR BRAM_WR_S00_AXI_SLV_REG1_OFFSET //RAM 起始寄存器地址
#define PL_BRAM_LEN BRAM_WR_S00_AXI_SLV_REG2_OFFSET //PL 读 RAM 的深度

#define START_ADDR 0 //RAM 起始地址 范围:0~1023
#define BRAM_DATA_BYTE 4 //BRAM 数据字节个数

int data[1024]; //写入 BRAM 的字符数组
int data_len; //写入 BRAM 的字符个数

//main 函数
 int main()
 {
	    int i=0,wr_cnt = 0,k=0;
	    int read_data=0, rd_data;
		for(int k=0;k<1024;k++)
		{
			data[k] = k;
		}
		data_len = 1024;
		//将用户输入的字符串写入 BRAM 中
		getchar();//方便调试
		//每次循环向 BRAM 中写入 1 个整数(4字节宽度)
		for(i = BRAM_DATA_BYTE*START_ADDR ; i < BRAM_DATA_BYTE*(START_ADDR + data_len) ;i += BRAM_DATA_BYTE)
		{
			XBram_WriteReg(XPAR_BRAM_0_BASEADDR,i,data[wr_cnt]);
			wr_cnt++;
		}
		//设置 BRAM 写入的字符串长度
		BRAM_WR_mWriteReg(PL_BRAM_BASE, PL_BRAM_LEN , BRAM_DATA_BYTE*data_len);
		//设置 BRAM 的起始地址
		BRAM_WR_mWriteReg(PL_BRAM_BASE, PL_BRAM_START_ADDR, BRAM_DATA_BYTE*START_ADDR);
		//拉高 BRAM 开始信号
		BRAM_WR_mWriteReg(PL_BRAM_BASE, PL_BRAM_START , 1);
		//拉低 BRAM 开始信号
		BRAM_WR_mWriteReg(PL_BRAM_BASE, PL_BRAM_START , 0);
		//从 BRAM 中读出数据

		//检查可读状态
		while(BRAM_WR_mReadReg(XPAR_BRAM_WR_0_S00_AXI_BASEADDR, 12)==0){
			printf("not_ready\n");
		}

		i = 0;
		printf("done");//可读
		//ps读开始
		BRAM_WR_mWriteReg(PL_BRAM_BASE, 16 , 1);
		//拉低信号,pl端恢复状态
		BRAM_WR_mWriteReg(PL_BRAM_BASE, 16 , 0);

		//循环从 BRAM 中读出数据
		for(i = BRAM_DATA_BYTE*START_ADDR ; i < BRAM_DATA_BYTE*(START_ADDR + data_len) ;i += BRAM_DATA_BYTE)
		{
			read_data = XBram_ReadReg(XPAR_BRAM_0_BASEADDR,i) ;
			printf("BRAM address is %d\t,Read data is %d\n",i/BRAM_DATA_BYTE ,read_data) ;
		}
 }

至此,所有工作完成,可以烧写比特流并运行查看效果

效果演示

PS端向BRAM中写入一个0-1023的递增数列,PL端遍历BRAM,将读入的数据加一并写回,得到以下结果。

ILA使用及原博主代码错误

这里使用原作者提供的代码,然后在自定义IP核中,使用IP Catalog,搜索ILA,并添加ILA (Integrated Logic Analyzer),根据自己想debug的端口数,修改Number of Probes,并修改另一标签页的Probe宽度以对应debug的端口,以下是我的设置

然后在bram_wr文件下,添加ila模块并将其实例化

ila_0 ila_inst (//ila部分,用于DEBUG,不调试可忽略这部分
    .clk(ram_clk),          //时钟
    .probe0(ps_rd_start),   //ps端开始读取标志
    .probe1(pos_start_rd),  //pl端开始操作
    .probe2(ram_wr_data),   //pl端写入bram数据
    .probe3(ram_en),        //bram enable
    .probe4(ram_rd_data),   //pl端读bram数据
    .probe5(ram_we),        //bram write enable
    .probe6(ram_addr),      //bram address
    .probe7(flow_cnt)       //状态机
);

之后按照流程,跑出比特流,导出硬件,并在SDK中进行Program FPGA(PS端与PL端同时调试的要求),然后运行,注意,这里运行需要Run as Launch at Hardware(System Debugger),才能继续进行调试,之后在Vivado中,打开Hardware Manager,刷新设备,这时可以看到设备下有一个ILA核心,这时可以添加需要监视的端口,并在右下的窗口中指定端口电平变化触发的条件,并运行等待触发,之后可在Waveform窗口中得到以下结果

我们发现BRAM的写入数据比读出数据明显快了一个电平,也就是说,原作者代码中读出的数据,实际是BRAM上一个地址中的内容,而在Vivado中,Block Memory Generator也有对此的描述,会延迟一个时钟周期。

而作者原代码如下,读取和写入同时发生,这就是为什么我们使用原作者的代码时,PS端不管写入什么数据,永远只能得到一个递增数列结果的原因,因此,需要在原代码的状态机中,添加一个延迟,消耗一个时钟周期,以此读取正确的BRAM输出的内容

然后我们再看到原作者的代码中,还有一个致命错误,如图,在这个状态机中,当写入到BRAM指定末地址时,没有将bram的write enable信号拉低电平,因此,PL端在下一次运行时极有可能写入上一次的读取的内容,从而影响结果

总结

本文通过学习一篇来自CSDN的文章并且修正其中的错误,达成了PL端与PS端通过BRAM传输数据的目的。

所见非所得,在试验自己的代码时,一定要使用多组测试数据,必要时使用ILA进行debug工作,才能更好地完成在FPGA开发板上的工作。

发表评论

评论内容会被上传至 百度智能云 审核,电子邮件地址不会被公开。必填项已用 * 标注