在 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的交互。