> 技术文档 > ZYNQ-PS与PL端BRAM数据交互_zynq bram

ZYNQ-PS与PL端BRAM数据交互_zynq bram

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

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

一、硬件设计:ZYNQ-PS与PL端BRAM数据交互_zynq bram

上图中: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(处理器系统)之间的数据共享。

整个的数据回环如下:ZYNQ-PS与PL端BRAM数据交互_zynq bram

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

ZYNQ-PS与PL端BRAM数据交互_zynq bram

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

ZYNQ-PS与PL端BRAM数据交互_zynq bram 可以看到读出来的数据和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_AXIparameter 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_AXIinput 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_AXIpl_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 endsendmodule

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 definewire 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 statementalways @(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; endelse begin  state <= IDLE; intr <= 1\'b0; en <= 1\'b0; addr <= addr;we = (len_tmp)) //当读取的遍历结束一遍begin state <= INIT ; en <= 1\'b0 ; we <= 4\'d0 ; addr<=start_addr_tmp; //获取读地址 提前两个周期 read_addr<=start_addr_tmp; endelse begin  state <= READ_INIT; //继续遍历 addr<=read_addr; //获取读地址 遍历 read_addr<=read_addr+32\'d4; read_data_temp<=din; end endINIT : 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 : beginread_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 = (len_tmp)) //write completedbegin state <= END ; en <= 1\'b0 ; we <= 4\'d0 ;endelsebegin dout<=read_data_temp+32\'d2; //到最后一位就不再写了 state <= WRITE_END ;end endWRITE_END : beginwrite_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 ; enddefault : state <= IDLE ;endcase endendendmodule

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

ZYNQ-PS与PL端BRAM数据交互_zynq bram

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

ZYNQ-PS与PL端BRAM数据交互_zynq bram

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

ZYNQ-PS与PL端BRAM数据交互_zynq bram

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

sdk:

#include \"xil_printf.h\"#include \"xbram.h\"#include #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字节即32bitXScuGic INTCInst; //指向中断控制器SCUGIC实例的指针char ch_data[1024]; //写入BRAM的字符数组int Len=10 ;//单次写入长度int Start_Addr=0 ;//写地址起始位即偏移0int 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-9for(i = BRAM_BYTENUM*Start_Addr ; i 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 statusPL_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的交互。

ZYNQ-PS与PL端BRAM数据交互_zynq bram