ZYNQ-PS与PL端BRAM数据交互

发布于:2025-06-03 ⋅ 阅读:(23) ⋅ 点赞:(0)

        在 ZYNQ SOC 开发过程中,PL 和 PS 之间经常需要做数据交互,对于传输速度要求较高、数据量大、地址连续的场合,可以通过 AXI DMA 来完成。而对于数据量较少、地址不连续、长度不规则的情况,则通过AXI4_LITE协议进行交互,通过生成一个带有AXI4-Lite接口的IP核,实现PS和PL的数据通信,即可以把不同类型的数据从PS传给PL,也可以从PL传给PS。

任务: PS 将串口接收到的数据写入 PL端BRAM,然后从 BRAM 中读出数据,并通过串口打印
出来;与此同时,PL 从 BRAM 中同样读出数据,并通过 ILA 来观察读出的数据与串口打印的数据是否一致。

一、硬件设计:

上图中:PS 端的 M_AXI_GP0 作为主端口,与 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 的起始地址和个数等。

 AXI BRAM Controller 是 Xilinx FPGA 和 Zynq SoC 中常用的 IP 核,用于通过 AXI 总线协议 访问 BRAM (Block RAM)。它允许 PS 端或 DMA 控制器通过标准 AXI 接口高效读写 BRAM,同时支持 PL(FPGA 逻辑)和 PS(处理器系统)之间的数据共享。

整个的数据回环如下:

PS端:除了基本的 DDR 和 UART 还使用到了 AXI 接口,因此如时钟、复位、AXI接口 都需要保留和配置。

PL 端:存储数据的 BRAM 的 IP 核、实现对 BRAM 写入数据的 AXI BRAM 控制器 IP 核、读取 BRAM IP 核数据的自定义的 IP 核(pl_bram_rd)。

具体详细配置可参考正点原子与XILINX官方例程,先看看效果:综合完成后set up debug需要观察的信号即PORTB的enb、addrb和doutb,PS端发送数据:xilinx

捕捉enb的上升沿(设为R值):

 可以看到读出来的数据和ILA抓出来的数据是一致的,说明符合任务说明。

 

二、上述是大部分厂家以及XILINX官方做的例程,但在学习过后发现其只是PS将数据读出来然后发送给PL后,PL并没有把数据读出来,而是做了个ILA抓取数据出来看。并没有实现PL将数据读出来过后然后再通过AXI总线将数据传输给PS端。

在网上阅读文章时学到了新的方法:可以给自定义PL端的IP核修改其功能,在bram_rd文件加入写的功能。同时给PL添加一个中断,使得PL数据给BRAM写好后触发中断给PS,PS捕获到该中断信号后开始读取BRAM的数据。ZYNQ—BRAM全双工PS_PL数据交互(开源)_zynq ps pl 数据传输-CSDN博客

实现捕获PS端输出的一个start脉冲,通过AXI-Lite接口得到start_addr起始地址和len数据长度,依次遍历后(保证数据的刷新),再进入后续状态机,将读数据暂存到reg变量,对读数据变量均+2,再写入到start_addr + len长度地址后的BRAM块,最终读写完成后,PL输出一个高电平脉冲intr触发PS中断。

记录一下过程:首先修改自定义IP核的三个.V文件:

pl_bram_rd_v1_0文件 :


`timescale 1 ns / 1 ps
  
	module pl_bram_rd_v1_0 #
	( 
		// Users to add parameters here

		// User parameters ends
		// Do not modify the parameters beyond this line


		// Parameters of Axi Slave Bus Interface S00_AXI
		parameter integer C_S00_AXI_DATA_WIDTH	= 32,
		parameter integer C_S00_AXI_ADDR_WIDTH	= 4
	)
	(
		// Users to add ports here
        //RAM端口
        input  wire [31:0]  din,	 
	    output wire [31:0]  dout,
	    output wire         en,
	    output wire [3:0]   we,
	    output wire [31:0]  addr,
	    output wire         intr,         //interrupt
	    output wire         bramclk,
	    output wire         bramrst_n,

		// User ports ends
		// Do not modify the ports beyond this line


		// Ports of Axi Slave Bus Interface S00_AXI
		input wire  s00_axi_aclk,
		input wire  s00_axi_aresetn,
		input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_awaddr,
		input wire [2 : 0] s00_axi_awprot,
		input wire  s00_axi_awvalid,
		output wire  s00_axi_awready,
		input wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_wdata,
		input wire [(C_S00_AXI_DATA_WIDTH/8)-1 : 0] s00_axi_wstrb,
		input wire  s00_axi_wvalid,
		output wire  s00_axi_wready,
		output wire [1 : 0] s00_axi_bresp,
		output wire  s00_axi_bvalid,
		input wire  s00_axi_bready,
		input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_araddr,
		input wire [2 : 0] s00_axi_arprot,
		input wire  s00_axi_arvalid,
		output wire  s00_axi_arready,
		output wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_rdata,
		output wire [1 : 0] s00_axi_rresp,
		output wire  s00_axi_rvalid,
		input wire  s00_axi_rready
	);
// Instantiation of Axi Bus Interface S00_AXI
	pl_bram_rd_v1_0_S00_AXI # ( 
		.C_S_AXI_DATA_WIDTH(C_S00_AXI_DATA_WIDTH),
		.C_S_AXI_ADDR_WIDTH(C_S00_AXI_ADDR_WIDTH)
	) pl_bram_rd_v1_0_S00_AXI_inst (
        //RAM端口   
        .din  (din),
        .en       (en     ),
        .addr     (addr   ),
        .we       (we     ),
        .dout  (dout),
	    .bramclk(bramclk),
	    .bramrst_n(bramrst_n),
        .intr(intr),       //start to read and write bram
		.S_AXI_ACLK(s00_axi_aclk),
		.S_AXI_ARESETN(s00_axi_aresetn),
		.S_AXI_AWADDR(s00_axi_awaddr),
		.S_AXI_AWPROT(s00_axi_awprot),
		.S_AXI_AWVALID(s00_axi_awvalid),
		.S_AXI_AWREADY(s00_axi_awready),
		.S_AXI_WDATA(s00_axi_wdata),
		.S_AXI_WSTRB(s00_axi_wstrb),
		.S_AXI_WVALID(s00_axi_wvalid),
		.S_AXI_WREADY(s00_axi_wready),
		.S_AXI_BRESP(s00_axi_bresp),
		.S_AXI_BVALID(s00_axi_bvalid),
		.S_AXI_BREADY(s00_axi_bready),
		.S_AXI_ARADDR(s00_axi_araddr),
		.S_AXI_ARPROT(s00_axi_arprot),
		.S_AXI_ARVALID(s00_axi_arvalid),
		.S_AXI_ARREADY(s00_axi_arready),
		.S_AXI_RDATA(s00_axi_rdata),
		.S_AXI_RRESP(s00_axi_rresp),
		.S_AXI_RVALID(s00_axi_rvalid),
		.S_AXI_RREADY(s00_axi_rready)
	);

	// Add user logic here

	// User logic ends

	endmodule

pl_bram_rd_v1_0_S00_AXI修改接口和例化模块:

// Users to add ports here
	 //bram port
     input  wire    [31:0]  din,	 //写入BRAM
	 output wire [31:0]  dout,//读出BRAM
	 output wire         en,//BRAM使能
	 output wire [3:0]   we,//写读选择
	 output wire [31:0]  addr,//地址
	 output wire         intr,         //interrupt输出给PS做中断
	 output   wire           bramclk,//bram时钟
	 output     wire         bramrst_n,//bram复位
	 
	// Add user logic here
    bram_rd u_bram_rd(
    .clk          (S_AXI_ACLK),
    .rst_n        (S_AXI_ARESETN),
    .start        (slv_reg0[0]),//PS写完数据后输出的一个脉冲触发
    .init_data    (slv_reg1),//未用到
    .len          (slv_reg2),//PS写入数据的长度
    .start_addr   (slv_reg3),    //PS写BRAM的起始地址
    //RAM端口   
    .din  (din),
    .en       (en     ),
    .addr     (addr   ),
    .we       (we     ),
    .dout  (dout),
	.bramclk(bramclk),
	.bramrst_n(bramrst_n),
	 //bram port	 
	 //control signal
	 .intr(intr)       //start to read and write bram
    );
	// User logic ends

 brambram_rd:

module bram_rd
    (
	 input              clk,
	 input              rst_n,
	 //bram port
     input      [31:0]  din,	 //read
	 output reg [31:0]  dout,    //write
	 output reg         en,
	 output reg [3:0]   we,
	 output reg [31:0]  addr,   //RAM地址
	 //control signal
	 input              start,       //start to read and write bram
	 input      [31:0]  init_data,   //没有用到
	 output reg         start_clr,   //没有用到
	 input      [31:0]  len,         //data count
	 input      [31:0]  start_addr,   //start bram address
	 //Interrupt
	 input              intr_clr,    //clear interrupt
	 output reg         intr,         //interrupt
	 output              bramclk,
	 output              bramrst_n
    );

assign bramclk = clk ;
assign bramrst_n = 1'b0 ;
	
localparam IDLE      = 4'd0 ;  //上电初始化
localparam READ_INIT      = 4'd1 ;   //每次循环读的初始化
localparam INIT      = 4'd2 ;   //每次循环的初始化
localparam READ_START      = 4'd3 ;  //准备读前的初始化
localparam READ_RAM  = 4'd4 ; //读
localparam READ_END  = 4'd5 ;//读结束
localparam WRITE_START  = 4'd6 ;//准备写的初始化
localparam WRITE_RAM = 4'd7 ; //写
localparam WRITE_END = 4'd8 ;//写结束
localparam END = 4'd9 ;//结束

reg [3:0] state ;
reg [31:0] len_tmp ;
reg [31:0] start_addr_tmp ;
reg [31:0] start_addr_tmp2 ;
reg [31:0] read_data_temp;
reg [31:0] read_addr;
reg [31:0] write_addr;

reg start_rd_d0;
reg start_rd_d1;
//wire define
wire pos_start_rd;
assign pos_start_rd = ~start_rd_d1 & start_rd_d0;
//延时两拍,采 start_rd 信号的上升沿  因为BRAM_B读取数据需要延迟两拍,即在PS写好数据,需要等一下才能读到RAM数据
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; 
         start_rd_d1 <= start_rd_d0; 
 end
 end

//Main statement
always @(posedge clk or negedge rst_n)
begin
  if (!rst_n)
  begin
    state      <= IDLE  ;
	dout       <= 32'd0 ;
	en         <= 1'b0  ;
	we         <= 4'd0  ;
	addr       <= 32'd0 ;
	intr       <= 1'b0  ;
	start_clr  <= 1'b0  ;
	len_tmp    <= 32'd0 ;						  
  end	
  else
  begin
    case(state)
	IDLE            : begin
			            if (pos_start_rd)
						begin
						  addr<=start_addr;
						  read_addr  <= start_addr;						  
						  start_addr_tmp <= start_addr ;
						  start_addr_tmp2<= start_addr+len ;  //从已有数据的后一位开始写
						  write_addr<=start_addr+len ;  //从已有数据的后一位开始写
						  len_tmp <= len ;	
						  intr       <= 1'b0  ;  //读取到后取消触发
						  state <= INIT     ; 
						  en    <= 1'b1;
	                      we   <= 4'd0;							  						  				  
						end			
						else begin 
			              state <= IDLE;
						  intr  <= 1'b0;			              		
						  en    <= 1'b0;
						  addr  <= addr;
	                      we   <= 4'd0;						  						  			              				
						end
			          end
	READ_INIT        : begin
		                if ((addr - start_addr_tmp) >= (len_tmp))   //当读取的遍历结束一遍
						begin
						  state <= INIT ;						  
						  en    <= 1'b0  ;
						  we    <= 4'd0  ;
						  addr<=start_addr_tmp;  //获取读地址  提前两个周期
						  read_addr<=start_addr_tmp; 
						end
						else begin
			              state <= READ_INIT;  //继续遍历
						  addr<=read_addr;  //获取读地址 遍历
						  read_addr<=read_addr+32'd4;
						  read_data_temp<=din; 	
						  end  
					  end
	INIT        : begin
			              state <= READ_START     ;
			              we    <= 4'b0000 ;
						  en    <= 1'b1 ;  //先en1
						  addr<=read_addr;  //获取读地址  提前两个周期
						  //read_data_temp<=din; 	  
					  end					  
    READ_START        : begin
						 en    <= en;
                         we    <= we;  //保持一个周期
                         //read_data_temp<=din;
					     state <= READ_RAM ;
					  end  
    READ_RAM        : begin	 
	                     read_data_temp<=din;                   
					     state <= READ_END  ;	                    					
						end					  
    READ_END        : begin
                        read_addr<=read_addr+32'd4;
                        en    <= 1'b0;		                                          
					    state <= WRITE_START  ;					    
					  end  
	WRITE_START        : begin
						 en    <= 1'b1;
						 we    <= 4'b1111;
					     state <= WRITE_RAM  ;	
					     addr  <= write_addr ;					     
					  end							   	  					   
	WRITE_RAM       : begin
	                    if ((addr - start_addr_tmp2) >= (len_tmp))   //write completed
						begin
						  state <= END ;						  
						  en    <= 1'b0  ;
						  we    <= 4'd0  ;
						end
						else
						begin
						   dout<=read_data_temp+32'd2; //到最后一位就不再写了  												  					  
						   state <= WRITE_END ;
						end
					  end
	WRITE_END       : begin
						write_addr  <= write_addr+32'd4 ;
						dout<=32'd0;
						addr<=read_addr;  //获取读地址  提前两个周期	
						en    <= 1'b0  ;
						we    <= 4'd0  ;
					    state <= INIT ;					    
					  end
					  					  					  
	END       : begin
	                    addr <= 32'd0 ;
	                  	dout  <= 32'd0; 
						intr <= 1'b1 ;
					    state <= IDLE ;					    
					  end	
	default         : state <= IDLE ;
	endcase
  end
end	
endmodule

block design参考正点原子的教程做修改,添加PL -> PS的中断:

添加一个EMIO,用 PL端做串口:

然后进行绑定管脚,用一个CH340的串口模块,通过连接GND、RX、TX后实现与电脑的通信 :

然后再生成bit流,并export,launch sdk。

sdk:

#include "xil_printf.h"
#include "xbram.h"
#include <stdio.h>
#include "pl_bram_rd.h"
#include "xscugic.h"

#define BRAM_CTRL_BASE      XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR //BRAM 控制器的 基地址,表示 BRAM 控制器在 AXI 总线上的起始地址
#define BRAM_CTRL_HIGH      XPAR_AXI_BRAM_CTRL_0_S_AXI_HIGHADDR //BRAM 控制器的 最高地址,表示 BRAM 控制器在 AXI 总线上的结束地址。
#define PL_RAM_BASE         XPAR_PL_BRAM_RD_0_S00_AXI_BASEADDR	//PL_RAM_RD基地址
#define PL_RAM_CTRL         PL_BRAM_RD_S00_AXI_SLV_REG0_OFFSET  //RAM读开始寄存器地址
#define PL_RAM_INIT_DATA    PL_BRAM_RD_S00_AXI_SLV_REG1_OFFSET  //RAM起始寄存器地址(没用到)
#define PL_RAM_LEN          PL_BRAM_RD_S00_AXI_SLV_REG2_OFFSET	//PL读RAM的深度
#define PL_RAM_ST_ADDR      PL_BRAM_RD_S00_AXI_SLV_REG3_OFFSET	//data

#define START_MASK   0x00000001   //b01  //表示启动信号的掩码
#define INTRCLR_MASK 0x00000002   //b10  //中断清除信号的掩码
#define INTC_DEVICE_ID	     XPAR_SCUGIC_SINGLE_DEVICE_ID        //用于标识PS端中断控制器(SCUGIC)的设备 ID
#define INTR_ID              XPAR_FABRIC_PL_BRAM_RD_0_INTR_INTR  //PL端的bram_rd模块产生的中断信号ID

#define TEST_START_VAL      0x0
/*
 * BRAM bytes number
 */
#define BRAM_BYTENUM        4   //每个数据占的字节大小,一般默认用4字节即32bit

XScuGic INTCInst;    //指向中断控制器SCUGIC实例的指针

char ch_data[1024];            //写入BRAM的字符数组
int Len=10  ;//单次写入长度
int Start_Addr=0 ;//写地址起始位即偏移0
int Intr_flag ;
/*
 * Function declaration
 */
int bram_read_write() ;
int IntrInitFuntion(u16 DeviceId);   //中断初始化
void IntrHandler(void *InstancePtr); //响应处理来自PL端的中断信号,

int main()
{
	int Status;
	Intr_flag = 1 ;
    IntrInitFuntion(INTC_DEVICE_ID) ;
	while(1)
	{
		if (Intr_flag)
		{
			Intr_flag = 0 ;
			Status = bram_read_write() ;
			if (Status != XST_SUCCESS)
			{
				xil_printf("Bram Test Failed!\r\n") ;
				xil_printf("******************************************\r\n");
				Intr_flag = 1 ;
			}
			sleep(2);
		}
	}
}

// 对BRAM的读写操作
int bram_read_write()
{
	u32 Write_Data = TEST_START_VAL ; // 要写入的数据
	int i ;
	/*
	 * if exceed BRAM address range, assert error
	 */
	if ((Start_Addr + Len) > (BRAM_CTRL_HIGH - BRAM_CTRL_BASE + 1)/4)
	{
		xil_printf("******************************************\r\n");
		xil_printf("Error! Exceed Bram Control Address Range!\r\n");
		return XST_FAILURE ;
	}
	/*
	 * Write data to BRAM
	 */ //写地址长度0-9
	for(i = BRAM_BYTENUM*Start_Addr ; i < BRAM_BYTENUM*(Start_Addr + Len) ; i += BRAM_BYTENUM)
	{
		XBram_WriteReg(XPAR_BRAM_0_BASEADDR, i , Write_Data) ;
		Write_Data += 1 ;  //写0-9
	}
	printf("完成PS写入BRAM\t\n等待捕获PL写BRAM结束中断\t\n");
	//Set ram read and write length
	PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_LEN , BRAM_BYTENUM*Len) ;//写寄存器,告诉PL数据长度
	//Set ram start address
	PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_ST_ADDR , BRAM_BYTENUM*Start_Addr) ;//写寄存器,告诉PL数据起始地址
	//Set pl initial data  没用到
	//PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_INIT_DATA , (Start_Addr+1)) ;
	//Set ram start signal
	PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_CTRL , START_MASK) ;  //输出高电平脉冲触发start:01
	return XST_SUCCESS ;
}

int IntrInitFuntion(u16 DeviceId)//接收PL端的intr中断
{
	XScuGic_Config *IntcConfig;
	int Status ;
	//check device id
	IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
	//intialization
	Status = XScuGic_CfgInitialize(&INTCInst, IntcConfig, IntcConfig->CpuBaseAddress) ;
	if (Status != XST_SUCCESS)
		return XST_FAILURE ;
	XScuGic_SetPriorityTriggerType(&INTCInst, INTR_ID,
			0xA0, 0x3);
	Status = XScuGic_Connect(&INTCInst, INTR_ID,
			(Xil_ExceptionHandler)IntrHandler,
			(void *)NULL) ;
	if (Status != XST_SUCCESS)
		return XST_FAILURE ;
	//启用PL端的中断响应
	XScuGic_Enable(&INTCInst, INTR_ID) ;

	Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
			(Xil_ExceptionHandler)XScuGic_InterruptHandler,
			&INTCInst);
	Xil_ExceptionEnable();
	return XST_SUCCESS ;

}

void IntrHandler(void *CallbackRef)//中断服务函数
{
	int Read_Data ;
	int i ;
	printf("捕获到PL写BRAM结束中断\t\n");
	//clear interrupt status
	PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_CTRL , INTRCLR_MASK) ;

	for(i = BRAM_BYTENUM*Start_Addr ; i < BRAM_BYTENUM*(Start_Addr + Len+15) ; i += BRAM_BYTENUM)  //len+10即可,只是多打几位,验证PL写的正确性
	{
		Read_Data = XBram_ReadReg(XPAR_BRAM_0_BASEADDR , i) ;
		printf("Address is %d\t Read data is %d\t\n",  i/BRAM_BYTENUM ,Read_Data) ;
	}
	Intr_flag = 1 ;
}


 效果:实现了PS写数据到BRAM的0-9位地址,触发PL读取,PL读取并+2分别写入到后面地址上(10-19位地址),触发PS中断读取。我觉得这才应该是PS --> PL\ PL --> PS的交互。

 


网站公告

今日签到

点亮在社区的每一天
去签到