ZYNQ笔记(十四):基于 BRAM 的 PS、PL 数据交互
版本:Vivado2020.2(Vitis)
实验任务:
PS 将字符串数据写入BRAM,再将数据读取出来;PL 从 BRAM 中读取数据,bing。通过 ILA 来观察读出的数据,与前面串口打印的数据进行对照(检查是否正确读出)。
目录
一、介绍
1. BRAM(Block RAM)
2. AXI BRAM Controller
二、硬件设计
1. 整体系统框图
2. 配置 BRAM
3. 配置 AXI BRAM Controller
4. 自定义IP核(pl_bram_rd)
5. 最终 bd 设计
三、软件设计
四、效果
一、介绍
1. BRAM(Block RAM)
BRAM(Block RAM) 是 Xilinx Zynq SoC 内部的嵌入式高速静态存储器(SRAM),分布在 FPGA 可编程逻辑(PL)部分,但也可被处理器系统(PS)直接访问。主要特点:
特性 | 说明 |
---|---|
高速访问 | 1~2 个时钟周期完成读写,远快于外部 DDR(通常需数十周期) |
低功耗 | 低功耗,片上存储无需外部总线交互 |
双端口架构 | 支持双端口(可独立读写),可配置为单端口或真双端口模式 |
存储容量 | 每块 36Kb(可拆分为 2×18Kb),器件集成数量:几十至数百块 |
初始化方式 | 支持 COE 文件预加载或运行时写入 |
典型用途 | 高速缓存、查找表、PS-PL 数据共享、跨时钟域同步 |
ECC 支持 | 可选校验功能 |
2. AXI BRAM Controller
AXI BRAM Controller 是 Xilinx FPGA 和 Zynq SoC 中常用的 IP 核,用于通过 AXI 总线协议 访问 BRAM (Block RAM)。它允许 PS 端或 DMA 控制器通过标准 AXI 接口高效读写 BRAM,同时支持 PL(FPGA 逻辑)和 PS(处理器系统)之间的数据共享。
特性 | 说明 |
---|---|
支持的 AXI 协议 | AXI4(高性能)、AXI4-Lite(简化版) |
数据位宽 | 32/64/128/256/512 位(需匹配 BRAM 配置) |
突发传输 | AXI4 支持突发(Burst)读写,提升吞吐量 |
ECC 支持 | 可选,增加数据校验功能 |
低延迟配置 | 可配置访问延迟 |
多主设备支持 | 多个 Master(如 CPU + DMA)可共享同一 BRAM |
二、硬件设计
1. 整体系统框图
PS端:除了 基本的 DDR 和 UART 还使用到了 AXI 接口,因此如时钟、复位、AXI接口 都需要保留和配置。
PL 端:存储数据的 BRAM 的 IP 核、实现对 BRAM 写入数据的 AXI BRAM 控制器 IP 核、读取 BRAM IP 核数据的自定义的 IP 核(pl_bram_rd)。
2. 配置 BRAM
即添加 Block Memory Generator IP 核,配置如下:
BRAM IP 核支持两种模式,一种是独立模式(Stand Alone),在此模式下,可以自由配置 RAM 的数 据深度和宽度;另一种是 BRAM 控制器模式(BRAM Controller),在此模式下,地址和数据默认为 32 位, 由于本次实验添加了 BRAM 控制器 IP 核,因此 BRAM 模式选择 BRAM 控制器模式。
存储类型(Memory Type)设置为“True Dual Port RAM” 真双端口 RAM。一端连接 PL 读 BRAM IP 核,另一端连接 BRAM 控制器。
来到其他选项:取消使能安全电路(如果勾选使能安全电路,BRAM 端口会增加 rst_busy 端口,用于表示何时可以访问 BRAM。)
在 AXI BRAM Controller 模式下,BRAM 的 we
写使能信号为 4 位,这是为了支持 按字节写入(Byte-Write)功能,允许对 32 位数据(4 字节)中的特定字节进行单独写入,而无需覆盖整个字。以下是详细说明:
-
AXI 总线特性:AXI 协议支持字节级写入(通过
WSTRB
信号),因此 BRAM Controller 需将 AXI 的字节使能信号转换为 BRAM 的we[3:0]
。 -
数据对齐:32 位 BRAM 数据(4 字节)中,每个
we
位对应一个字节的写使能:-
we[0]
:控制 字节 0(数据位[7:0]
) -
we[1]
:控制 字节 1(数据位[15:8]
) -
we[2]
:控制 字节 2(数据位[23:16]
) -
we[3]
:控制 字节 3(数据位[31:24]
)
-
3. 配置 AXI BRAM Controller
AXI Protocol(AXI 协议)选择的是 AXI4_Lite,本实验只需发送一段开源电子网网址的字符串,因此选择适用于低吞吐率存储映射的 AXI4_Lite 接口即可满足传输需求。
Data Width(数据位宽)固定为默认的 32 位,由于 AXI4 总线为字节寻址,因此在映射到 BRAM 地址时,需要按 4 字节寻址。
BRAM 接口数 1,本次 BRAM 控制器只需要读写 BRAM 的一个端口,
此外 Memory Depth(存储深度)不可以设置,寻址 BRAM 的存储深度是在 Address Editor 里设置。读取延时默认1个时钟周期。ECC 用于数据错误纠正与检查,不使能。所有配置如下:
4. 自定义IP核(pl_bram_rd)
自定义 IP 核的设计和封装可参考:ZYNQ笔记(六):自定义IP核-LED呼吸灯 。可以直接点击菜单栏的 “Tools”,选择 “Creat and Package New IP…” 在当前工程目录快速进行自定义 IP 核的创建,如下图所示:
需要注意创建的是 带 AXI 接口的 IP
IP 默认在工程目录的上一级目录创建目录ip_repo (/../ip_repo) ,这里去掉一个点让他创建在当前工程目录下(/./ip_repo) :
AXI 接口类型设置为 Lite 和前面保持一致,接口模式为从。Next、Finish。
在IP Catolog 内找到 整个自定义 IP pl_bram_rd,右键进行编辑。pl_bram_rd 里面所添加的自定义verilog模块是直接使用的正点原子的 bram_rd 模块,模块引出了 bram 接口并实现了对 bram 的读操作,控制信号在例化时相依分配寄存器即可,这样 PS 端通过读相应寄存器实现对 PL 端 自定义IP核 pl_bram_rd 的控制。
bram_rd 模块
/*BRAM读数据 模块例化bram_rd bram_rd(.clk (S_AXI_ACLK ), //时钟信号.rst_n (S_AXI_ARESETN), //复位信号//PS端输入的控制信号(分配三个寄存器进行控制).start_rd (slv_reg0[0]), //读开始信号(上升沿有效).start_addr (slv_reg1 ), //读开始地址 .rd_len (slv_reg2 ), //读数据的长度//BRAM端口 .ram_clk (ram_clk ), //RAM时钟.ram_rst (ram_rst ), //RAM复位信号,高有效.ram_en (ram_en ), //RAM工作使能信号.ram_we (ram_we ), //RAM写使能信号.ram_addr (ram_addr ), //RAM地址.ram_wr_data (ram_wr_data), //RAM要写入的数据.ram_rd_data (ram_rd_data) //RAM中读出的数据);
*///模块实现对BRAM的读取操作(BRAM为控制模式为 BRAM Contrulor 控制)
module bram_rd(input clk , //时钟信号input rst_n , //复位信号//控制信号input start_rd , //读开始信号(上升沿有效)input [31:0] start_addr , //读开始地址 input [31:0] rd_len , //读数据的长度//BRAM端口output ram_clk , //RAM时钟output ram_rst , //RAM复位信号,高有效output reg ram_en , //RAM工作使能信号output reg [3:0] ram_we , //RAM写使能信号output reg [31:0] ram_addr , //RAM地址output reg [31:0] ram_wr_data, //RAM要写入的数据input [31:0] ram_rd_data //RAM中读出的数据
);
reg [1:0] flow_cnt;//流程计数
reg start_rd_d0;
reg start_rd_d1;assign ram_rst = 1'b0;
assign ram_clk = clk ;//读开始信号上升沿标志信号
wire pos_start_rd;
assign pos_start_rd = ~start_rd_d1 & start_rd_d0;//延时两拍,采start_rd信号的上升沿
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_rd; start_rd_d1 <= start_rd_d0; end
end//判断读开始信号,从RAM中读出数据
always @(posedge clk or negedge rst_n) beginif(!rst_n) beginflow_cnt <= 2'd0;ram_en <= 1'b0;ram_we <= 4'd0;ram_addr <= 32'd0;end//通过计数实现的简单状态机else begincase(flow_cnt)2'd0 : beginif(pos_start_rd) beginram_en <= 1'b1;ram_addr <= start_addr;flow_cnt <= flow_cnt + 2'd1;endend2'd1 : beginif(ram_addr - start_addr == rd_len - 4) begin //数据读完(读完差值为rd_len-4)ram_en <= 1'b0;flow_cnt <= flow_cnt + 2'd1;endelseram_addr <= ram_addr + 32'd4; //地址累加4(,一次读32位4个字节,一个字节对应一个地址)end2'd2 : beginram_addr <= 32'd0; flow_cnt <= 2'd0;endendcaseend
endendmodule
接着就是把 bram_rd 模块例化到 官方AXI 接口模块中并添加端口定义:
AXI接口模块例化、添加端口定义:
AXI 接口顶层模块添加端口定义:
接着保存后综合,并打包IP核(参考:ZYNQ笔记(六):自定义IP核-LED呼吸灯 )。注意再PORT 栏部分,为了使 IP 核最后在 bd 视图简洁规范,这里需要对自定义的端口封装成一个BRAM的总线接口并匹配端口类型(也可以不设置,不过结果就是db视图中IP核自定义端口全部罗列在视图上不能缩略为接口,同时运行自动连线时不能自动连接BRAM,需要手动连线):
找到端口,可能有端口被纳入到其他组里面,选中右键 Remove Interface 把它移出来即可:
选中所有自定义端口右键添加总线接口,接口定义设置为官方的bram接口,自己进行命名,模式选“Master”主模式(因为是要读写BRAM模块)
为自定义的端口添加接口映射(左右对应选中点击Map Ports):
最后添加自动计算参数,这里直接全部添加过去(Vivado 根据连接的总线或关联 IP 自动推导,这里没有进行特定的设置,直接选“自动”即可)
完成后PORT栏可以查看接口情况,GUI视图栏也可以看到这些端口被封装为一个接口了:
最后打包IP即可。注意 : 在Vitis 开发环境下,需要修改自定义IP核的 Makefile 文件,如果不修改,当包含该 IP 的硬件(xsa)文件导出 到 vitis 后,对 vitis 工程进行编译就会报错,报错信息为“xxx.h: No such file or directory”。因此需要在使用该 IP 前完成修改: Makefile 修改方法
5. 最终 bd 设计
添加完自定义IP核后可以运行自动连接,为了保险起见手动设置要自定义IP核要连接的 BRAM 避免出错。
接着需要设置地址映射,设置地址范围也会间接定义 BRAM 的大小。这里读/写同一 bram 就都设置为 4K(单位字节),也定义 BRAM 深度大小为 1K (BRAM 数据位宽32位 4字节)。BRAM 控制器 和 自定义 IP(BRAM读控制器) 的地址范围不能重合。
除了定义 BRAM 大小,两个 bram 控制器需要设置映射地址和范围是为了定义可访问的 BRAM 地址,确保模块能够准确读写数据。尽管已经通过连接明确了物理关系(接口连接),地址映射仍然是必要的,实际设置的是 BRAM 的映射地址,而非 BRAM 控制器本身,以确保控制器能正确进行数据访问。
最后整体 bd 设计部分如图所示:设计检查、Generate Output Products、 Create HDL Wrapper、管脚约束(没有PL管脚,忽略这一步)、Gnerate Bitstream、Export Hardware(包含比特流文件)、启动Vitis
三、软件设计
#include "xil_printf.h"
#include "xparameters.h"
#include "pl_bram_rd.h" //自定义IP头文件(包含对IP寄存器进行读写的函数)
#include "xbram.h" //AXI BRAM Controller IP头文件(包含使用该IP进行BRAM读写的函数)
#include "stdio.h"
#include "sleep.h"//===================用户自定义宏===================//
#define BRAM_BASEADDR XPAR_BRAM_0_BASEADDR //BRAM器件(BRAM IP核) 基地址#define BRAM_RD_IP XPAR_PL_BRAM_RD_0_S00_AXI_BASEADDR //自定义IP核(pl_bram_rd)寄存器基地址
#define BRAM_RD_START_RD PL_BRAM_RD_S00_AXI_SLV_REG0_OFFSET //start_rd 对应寄存器(slv_reg0)地址偏移量(pl_bram_rd.h中查看)
#define BRAM_RD_STRAT_ADDR PL_BRAM_RD_S00_AXI_SLV_REG1_OFFSET //start_addr对应寄存器(slv_reg1)地址偏移量
#define BRAM_RD_RD_LEN PL_BRAM_RD_S00_AXI_SLV_REG2_OFFSET //rd_len 对应寄存器(slv_reg2)地址偏移量#define STRAT_ADDR 0 //读起始地址(从0开始)
#define BRAM_DATA_BYTE 4 //BRAM 数据字节数(32位宽 4字节)//===================函数变量声明===================//
void BRAM_Ctrl_Wr(); //BRAM控制器写数据
void BRAM_Ctrl_Rd(); //BRAM控制器读数据
void PL_BRAM_RD_Rd(); //自定义IP核(pl_bram_rd)读数据int wr_len; //写入数据长度
char wr_buff[50] = "Hello"; //写入数据(字符数组)
char rd_buff[50] = ""; //读出数据//======================主函数======================//
int main()
{while(1){xil_printf("Write Char Data : %s\r\n", wr_buff); // 打印待写入数据数据wr_len = strlen(wr_buff); //获取字符串长度BRAM_Ctrl_Wr(); //BRAM控制器写数据BRAM_Ctrl_Rd(); //BRAM控制器读数据PL_BRAM_RD_Rd(); //自定义IP核(pl_bram_rd)读数据sleep(3); //每隔3秒执行一次}return 0;
}//================BRAM控制器写数据================//
void BRAM_Ctrl_Wr()
{//用XBram_WriteReg一次写入一个字符,用for循环遍历写入所有字符for(int i=0; i<wr_len; i++){//XBram_WriteReg(BRAM基地址,偏移量,写入数据),这里一次偏移4字节是因为BRAM数据位宽32位4字节,不过每次只写1个字符XBram_WriteReg(BRAM_BASEADDR, i*BRAM_DATA_BYTE, wr_buff[i]);}xil_printf("BRAM Write done! \r\n");
}//================BRAM控制器读数据================//
void BRAM_Ctrl_Rd()
{xil_printf("Read Char Data : ");//用XBram_WriteReg一次读取一个字符,用for循环遍历读取所有字符for(int i=0; i<wr_len; i++){//XBram_ReadReg(BRAM基地址,偏移量)rd_buff[i] = XBram_ReadReg(BRAM_BASEADDR, i*BRAM_DATA_BYTE);xil_printf("%c",rd_buff[i]);}xil_printf("\r\n");
}//===========自定义IP核(pl_bram_rd)读数据===========//
void PL_BRAM_RD_Rd()
{PL_BRAM_RD_mWriteReg(BRAM_RD_IP, BRAM_RD_RD_LEN, wr_len*BRAM_DATA_BYTE); //设置读数据长度PL_BRAM_RD_mWriteReg(BRAM_RD_IP, BRAM_RD_STRAT_ADDR, STRAT_ADDR*BRAM_DATA_BYTE); //设置读起始地址//产生读开始信号有效上升沿PL_BRAM_RD_mWriteReg(BRAM_RD_IP, BRAM_RD_START_RD, 1); //拉高读开始信号PL_BRAM_RD_mWriteReg(BRAM_RD_IP, BRAM_RD_START_RD, 0); //拉低读开始信号
}
四、效果
PS 端:先打印待写入数据,写入后将数据读取并打印。可看到前后一致说明写入数据无误
PL 端:要用到 ILA 查看 自定义IP 核读取的数据是否正确,手动添加一个 ILA IP 核并在system_warper 中进行例化(修改保存后需要重新走一遍导出xac文件之前的流程)。
不过这样到最后并没有实现PL到PS的数据传输,可以考虑自定义IP核加一个中断输出,并重新修改功能,实现读取数据后再接着最后一个地址开始,再把数据写到BRAM上,然后产生一个高脉冲信号(中断信号)时PS端得到中断开读数据检查两端数据是否一致:参考文章
ZYNQ—BRAM全双工PS_PL数据交互(开源)
关于自定义IP核的中断(BD设计里面需要给ZYNQ添加中断端口),我这里也有一个可参考的模板,中断类型位上升沿中断:
#include "xil_printf.h"
#include "xparameters.h"
#include "pl_bram_rd.h" //自定义IP头文件(包含对IP寄存器进行读写的函数)
#include "xbram.h" //AXI BRAM Controller IP头文件(包含使用该IP进行BRAM读写的函数)
#include "xscugic.h"
#include "stdio.h"//===================用户自定义宏===================//
#define BRAM_BASEADDR XPAR_BRAM_0_BASEADDR //BRAM器件(BRAM IP核) 基地址#define BRAM_RD_IP XPAR_PL_BRAM_RD_0_S00_AXI_BASEADDR //自定义IP核(pl_bram_rd)寄存器基地址
#define BRAM_RD_START_RD PL_BRAM_RD_S00_AXI_SLV_REG0_OFFSET //start_rd 对应寄存器(slv_reg0)地址偏移量(pl_bram_rd.h中查看)
#define BRAM_RD_STRAT_ADDR PL_BRAM_RD_S00_AXI_SLV_REG1_OFFSET //start_addr对应寄存器(slv_reg1)地址偏移量
#define BRAM_RD_RD_LEN PL_BRAM_RD_S00_AXI_SLV_REG2_OFFSET //rd_len 对应寄存器(slv_reg2)地址偏移量#define STRAT_ADDR 0 //读起始地址(从0开始)
#define BRAM_DATA_BYTE 4 //BRAM 数据字节数(32位宽 4字节)#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID //宏定义中断控制器(GIC)ID
#define BRAM_RD_IP_ID XPAR_PL_BRAM_RD_0_DEVICE_ID //自定义IP核(pl_bram_rd)器件ID
#define BRAM_RD_IP_INTR_ID XPAR_FABRIC_PL_BRAM_RD_0_INTR_INTR //宏定义GPIO中断号(每个器件都有中断号,xparameters.h查询)//===================函数变量声明===================//
XScuGic Intc; //中断控制器驱动实例void BRAM_Ctrl_Wr (); //BRAM控制器写数据
void BRAM_Ctrl_Rd (); //BRAM控制器读数据
void PL_BRAM_RD_Rd(); //自定义IP核(pl_bram_rd)读数据
void Set_Intr_Sys(XScuGic *GicInstancePtr, u16 DeviceIntrID); //建立中断系统
void IntrHandler(); //中断处理函数int wr_len; //写入数据长度
char wr_buff[] = "ABC"; //写入数据(字符数组)//======================主函数======================//
int main()
{//建立中断系统Set_Intr_Sys(&Intc, BRAM_RD_IP_INTR_ID);wr_len = strlen(wr_buff); //获取字符串长度BRAM_Ctrl_Wr (); //BRAM控制器写数据BRAM_Ctrl_Rd (); //BRAM控制器读数据PL_BRAM_RD_Rd(); //自定义IP核(pl_bram_rd)读写数据while(1)return 0;
}//================BRAM控制器写数据================//
void BRAM_Ctrl_Wr()
{//用XBram_WriteReg一次写入一个字符,用for循环遍历写入所有字符for(int i=0; i<wr_len; i++){//XBram_WriteReg(BRAM基地址,偏移量,写入数据),这里一次偏移4字节是因为BRAM数据位宽32位4字节,不过每次只写1个字符//读写地址=基地址+偏移量XBram_WriteReg(BRAM_BASEADDR, i*BRAM_DATA_BYTE, wr_buff[i]);}xil_printf("BRAM Write Finish! \r\n");
}//================BRAM控制器读数据================//
void BRAM_Ctrl_Rd()
{int r_data;xil_printf("Read the Written Data : ");//用XBram_WriteReg一次读取一个字符,用for循环遍历读取所有字符for(int i=0; i<wr_len; i++){//XBram_ReadReg(BRAM基地址,偏移量)//读写地址=基地址+偏移量r_data = XBram_ReadReg(BRAM_BASEADDR, i*BRAM_DATA_BYTE);xil_printf("%c",r_data);}xil_printf("\r\n");
}//===========自定义IP核(pl_bram_rd)读数据===========//
void PL_BRAM_RD_Rd(u32 addr)
{xil_printf("START the custom IP: \r\n");PL_BRAM_RD_mWriteReg(BRAM_RD_IP, BRAM_RD_RD_LEN, wr_len*BRAM_DATA_BYTE); //设置读数据长度PL_BRAM_RD_mWriteReg(BRAM_RD_IP, BRAM_RD_STRAT_ADDR, STRAT_ADDR*BRAM_DATA_BYTE); //设置读起始地址//产生读开始信号有效上升沿PL_BRAM_RD_mWriteReg(BRAM_RD_IP, BRAM_RD_START_RD, 1); //拉高读开始信号PL_BRAM_RD_mWriteReg(BRAM_RD_IP, BRAM_RD_START_RD, 0); //拉低读开始信号
}//===========================中断处理函数===========================//
void IntrHandler(void *CallbackRef)
{int r_data;xil_printf("Interrupt Detected , Start Read BRAM :\r\n");//clear interrupt status//PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_CTRL , INTRCLR_MASK) ;//BRAM控制器读数据for(int i=0; i<wr_len*2; i++){//XBram_ReadReg(BRAM基地址,偏移量)//读写地址=基地址+偏移量r_data = XBram_ReadReg(BRAM_BASEADDR, i*BRAM_DATA_BYTE);xil_printf("Address:%d Data:%c \r\n", i*BRAM_DATA_BYTE, r_data);}xil_printf("\r\n");
}//===========================建立中断系统===========================//
/* 建立中断系统,使能自定义IP核终端输出信号的上升沿产生中断* @param GicInstancePtr 是指向 XScuGic 驱动实例的指针* @param DeviceIntrID 是器件中断 ID*/
void Set_Intr_Sys(XScuGic *GicInstancePtr, u16 DeviceIntrID)
{//定义中断控制器配置信息(指针类型)XScuGic_Config * IntcConfig;//根据中断控制器ID,查找GIC配置信息(Generic Interrupt Controller(通用)中断控制器)IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);//初始化中断控制器驱动XScuGic_CfgInitialize(GicInstancePtr, IntcConfig, IntcConfig->CpuBaseAddress);//设置并打开中断异常处理功能Xil_ExceptionInit();Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,(Xil_ExceptionHandler)XScuGic_InterruptHandler,GicInstancePtr);//为GPIO中断设置中断处理函数(IntrHandler为自己编写的中断函数)XScuGic_Connect(GicInstancePtr, DeviceIntrID,(Xil_ExceptionHandler)IntrHandler, (void *) NULL); //自定义IP核无实例NULL//使能处理器中断Xil_ExceptionEnable();//设置中断优先级、触发类型(优先级从最高0开始步长8最大为248,0xA0=160优先级中等;触发类型:上升沿)XScuGic_SetPriorityTriggerType(GicInstancePtr, DeviceIntrID, 0xA0 , 0x3);//使能来自于器件的中断XScuGic_Enable(GicInstancePtr, DeviceIntrID);
}