ZYNQ-PS与PL端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 来观察读出的数据与串口打印的数据是否一致。
一、硬件设计:
上图中: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 psmodule 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, //interruptoutput 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 portinput wire [31:0] din, //写入BRAMoutput wire [31:0] dout,//读出BRAMoutput 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 herebram_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 portinput [31:0] din, //readoutput reg [31:0] dout, //writeoutput reg en,output reg [3:0] we,output reg [31:0] addr, //RAM地址//control signalinput start, //start to read and write braminput [31:0] init_data, //没有用到output reg start_clr, //没有用到input [31:0] len, //data countinput [31:0] start_addr, //start bram address//Interruptinput intr_clr, //clear interruptoutput reg intr, //interruptoutput 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) beginif(!rst_n) beginstart_rd_d0 <= 1'b0; start_rd_d1 <= 1'b0;endelse beginstart_rd_d0 <= start; start_rd_d1 <= start_rd_d0; endend//Main statement
always @(posedge clk or negedge rst_n)
beginif (!rst_n)beginstate <= 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 elsebegincase(state)IDLE : beginif (pos_start_rd)beginaddr<=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; endendREAD_INIT : beginif ((addr - start_addr_tmp) >= (len_tmp)) //当读取的遍历结束一遍beginstate <= INIT ; en <= 1'b0 ;we <= 4'd0 ;addr<=start_addr_tmp; //获取读地址 提前两个周期read_addr<=start_addr_tmp; endelse beginstate <= READ_INIT; //继续遍历addr<=read_addr; //获取读地址 遍历read_addr<=read_addr+32'd4;read_data_temp<=din; end endINIT : beginstate <= READ_START ;we <= 4'b0000 ;en <= 1'b1 ; //先en1addr<=read_addr; //获取读地址 提前两个周期//read_data_temp<=din; end READ_START : beginen <= en;we <= we; //保持一个周期//read_data_temp<=din;state <= READ_RAM ;end READ_RAM : begin read_data_temp<=din; state <= READ_END ; end READ_END : beginread_addr<=read_addr+32'd4;en <= 1'b0; state <= WRITE_START ; end WRITE_START : beginen <= 1'b1;we <= 4'b1111;state <= WRITE_RAM ; addr <= write_addr ; end WRITE_RAM : beginif ((addr - start_addr_tmp2) >= (len_tmp)) //write completedbeginstate <= END ; en <= 1'b0 ;we <= 4'd0 ;endelsebegindout<=read_data_temp+32'd2; //到最后一位就不再写了 state <= WRITE_END ;endendWRITE_END : beginwrite_addr <= write_addr+32'd4 ;dout<=32'd0;addr<=read_addr; //获取读地址 提前两个周期 en <= 1'b0 ;we <= 4'd0 ;state <= INIT ; endEND : beginaddr <= 32'd0 ;dout <= 32'd0; intr <= 1'b1 ;state <= IDLE ; end default : state <= IDLE ;endcaseend
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字节即32bitXScuGic 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-9for(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 lengthPL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_LEN , BRAM_BYTENUM*Len) ;//写寄存器,告诉PL数据长度//Set ram start addressPL_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 signalPL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_CTRL , START_MASK) ; //输出高电平脉冲触发start:01return XST_SUCCESS ;
}int IntrInitFuntion(u16 DeviceId)//接收PL端的intr中断
{XScuGic_Config *IntcConfig;int Status ;//check device idIntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);//intializationStatus = 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 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的交互。