在 ZYNQ 开发过程中,我们经常遇到需要 PS 和 PL 数据交互的场合,通常使用的方法有 DMA、BRAM 等。对于速度要求高、数据量大、地址连续的数据,可以通过 DMA 来实现,但对于地址不连续,数据量少的场合,DMA 就不适用了,针对这种情况,可以通过 BRAM 来实现。
BRAM(Block RAM)是 PL 端的存储器阵列,PL 可通过输出时钟、地址、读写控制等信号来对其进行读写操作,在 PS 端,处理器则可通过 AXI BRAM 控制器来实现对 BRAM 的读写,这样可以很方便的实现 PS 与 PL 之间的数据交互。
实验目的
- PS 端随机写入一些数据到 BRAM
- PS 端读取写入的数据并输出到串口
- 控制 PL 端开始、停止读取数据
- 实现 PL 连续、循环的读取数据
- 控制 PL 端读取的频率、数据量
- 通过 ILA 来观察 PL 端读出的数据
硬件设计
- 创建 ZYNQ 工程,新建 Block Design,添加 ZYNQ IP:
- 打开 GP0 接口(默认是打开的):
- 添加 AXI BRAM Controller IP 核:
- 双击 axi_bram_ctrl_0 IP 打开配置:
- AXI Protocol(AXI 协议)选择 AXI4,Data Width(数据位宽)选择 32 位, BRAM 的总线个数设置为 1,点击 OK:
- 添加 Block Memory Generator IP 核:
- 双击 blk_mem_gen_0 IP 打开配置:
- Mode(模式)选择 BRAM Controller(BRAM 控制器模式),Memory Type(存储类型)设置为 True Dual Port RAM,即真双口 RAM:
- 切换至 Other Options 选项,取消使能安全电路:
- 点击 Run Block Automation:
- 勾选 All Automation,点击 OK:
- 自定义一个 PL 端读写 BRAM 的 IP 核, 命名为 pl_bram_rd,并封装 AXI 总线接口,可以很方便的实现调用,BRAM 读写实现代码如下:
module bram_rd(
input clk , //时钟信号
input rst_n , //复位信号
input start_rd , //读开始信号
input [31:0] start_addr , //读开始地址
input [31:0] rd_len , //读数据的长度
input [31:0] rd_freq , //读数据频率
//RAM端口
output ram_clk , //RAM时钟
input [31:0] ram_rd_data, //RAM中读出的数据
output reg ram_en , //RAM使能信号
output reg [31:0] ram_addr , //RAM地址
output [3:0] ram_we , //RAM读写控制信号
output reg [31:0] ram_wr_data, //RAM写数据
output ram_rst //RAM复位信号,高电平有效
);
wire pos_start_rd;
assign ram_rst = 1'b0;
assign ram_we = 4'b0;
assign pos_start_rd = start_rd;
assign ram_clk=clk;
wire add_cnt0 ;
wire end_cnt0 ;
wire add_cnt1 ;
wire end_cnt1 ;
reg [31:0] cnt0;
reg [31:0] cnt1;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
ram_en <= 1'b0;
end
else if(pos_start_rd)
ram_en <= 1'b1;
else if(!pos_start_rd)
ram_en <= 1'b0;
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n) begin
cnt0 <= 0;
end
else if(add_cnt0) begin
if(end_cnt0)
cnt0 <= 0;
else
cnt0 <= cnt0 + 1;
end
end
assign add_cnt0 =ram_en ;
assign end_cnt0 = add_cnt0 && cnt0==rd_freq-1;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt1 <= 0;
ram_addr <= start_addr;
end
else if(add_cnt1)begin
if(end_cnt1)begin
cnt1 <= 0;
ram_addr <=start_addr;
end
else begin
cnt1 <= cnt1 + 1;
ram_addr <= ram_addr + 'd4;
end
end
end
assign add_cnt1 = end_cnt0;
assign end_cnt1 = add_cnt1 && cnt1>=(rd_len>>2)-1;
endmodule
- 添加自定义 pl_bram_rd IP:
- 将 pl_bram_rd_0 IP 的 BRAM_PORT 端口导出:
- 将 blk_mem_gen_0 IP 的 BRAM_PORTB 端口也导出:
- 修改一下端口名字:
- 再次 Run Block Automation:
- 打开 Address Editor 页面,展开 processing_system7_0 下的 Data,将 BRAM 范围设置为 4K:
- 选择 Validate Design 验证设计:
- 设计无误,点击 OK 确认:
- 依次 Generate Output Products…、Create HDL Wrapper:
- 新建 system 顶层文件,例化 Block Design 顶层 HDL Wrapper,将 BRAM_PORT 端口和 BRAM_PORTB 端口连接:
- 添加 ILA IP 核,设置探针数量为 2,采样深度 65536:
- 探针位宽都设置为 32 位:
- 例化 ILA IP:
- 生成比特流:
- 导出硬件信息:
- 注意选中比特流一并导出:
SDK 设计
- 新建一个 Hello world SDK 工程,添加以下源文件:
- bram.c
/**
* Copyright (c) 2022-2023,HelloAlpha
*
* Change Logs:
* Date Author Notes
*/
#include "bram.h"
/* 将数据写入 BRAM */
int BramPsWrite_uint32(uint32_t *pdata, uint32_t offset, uint32_t length)
{
uint32_t i = 0, wr_cnt = 0;
/* 循环向 BRAM 中写入 */
for(i = BRAM_DATA_BYTE * (START_ADDR + offset) ; i < BRAM_DATA_BYTE * (START_ADDR + offset + length) ;
i += BRAM_DATA_BYTE) {
XBram_WriteReg(BRAM_BASE, i, pdata[wr_cnt]) ;
wr_cnt++;
}
return 0;
}
/* 从 BRAM 中读出数据 */
int BramPsRead_uint32(uint32_t *pbuff, uint32_t offset, uint32_t length)
{
uint32_t rd_cnt = 0, i = 0;
/* 循环从 BRAM 中读出数据 */
for(i = BRAM_DATA_BYTE * (START_ADDR + offset); i < BRAM_DATA_BYTE * (START_ADDR + offset + length) ;
i += BRAM_DATA_BYTE) {
pbuff[rd_cnt] = XBram_ReadReg(BRAM_BASE, i) ;
rd_cnt++;
}
return 0;
}
int BramPlReadSet(uint32_t length, uint32_t freq)
{
/* 设置 BRAM 读出的起始地址 */
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START_ADDR, BRAM_DATA_BYTE * START_ADDR) ;
/* 设置 BRAM 读出的字符串长度 */
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_LEN, BRAM_DATA_BYTE * length) ;
/* 设置 BRAM 读数据频率 */
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_RD_FREQ , freq) ;
return 0;
}
int BramPlReadStart(void)
{
/* 拉高 BRAM 读开始信号 */
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START , 1) ;
return 0;
}
int BramPlReadStop(void)
{
/* 拉低 BRAM 读开始信号 */
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START , 0) ;
return 0;
}
- bram.h
/**
* Copyright (c) 2022-2023,HelloAlpha
*
* Change Logs:
* Date Author Notes
*/
#ifndef __BRAM_H__
#define __BRAM_H__
#include "xbram.h"
#include "pl_bram_rd.h"
#define PL_BRAM_BASE XPAR_PL_BRAM_RD_0_S00_AXI_BASEADDR // PL_RAM_RD 基地址
#define PL_BRAM_START PL_BRAM_RD_S00_AXI_SLV_REG0_OFFSET // RAM 读开始寄存器地址
#define PL_BRAM_START_ADDR PL_BRAM_RD_S00_AXI_SLV_REG1_OFFSET // RAM 起始寄存器地址
#define PL_BRAM_LEN PL_BRAM_RD_S00_AXI_SLV_REG2_OFFSET // PL 读 RAM 的深度
#define PL_BRAM_RD_FREQ PL_BRAM_RD_S00_AXI_SLV_REG3_OFFSET // PL 读 RAM 的频率
#define BRAM_BASE XPAR_BRAM_0_BASEADDR
#define BRAM_HIGH XPAR_BRAM_0_HIGHADDR
#define START_ADDR 0 // RAM 起始地址 范围:0 ~ 4095
#define BRAM_DATA_BYTE 4 // BRAM 数据字节个数
//函数声明
int BramPsWrite_uint32(uint32_t *pdata, uint32_t offset, uint32_t length);
int BramPsRead_uint32(uint32_t *pbuff, uint32_t offset, uint32_t length);
int BramPlReadSet(uint32_t length, uint32_t freq);
int BramPlReadStart(void);
int BramPlReadStop(void);
#endif
- bram_test.c
#include "bram.h"
#include "xil_printf.h"
#define kprintf xil_printf
#define SIZE 16
#define BRAM_MAX_SIZE 4096
#define BRAM_OFFSET 0
int bram_wr_test(void)
{
uint32_t wr_value[SIZE], rd_value[SIZE];
for(uint32_t i = 0; i < SIZE; i++)
{
wr_value[i] = 0x01 << i;
}
BramPlReadStop(); // 停止 PL 读取数据
BramPsWrite_uint32(wr_value, BRAM_OFFSET, SIZE); // PS 写数据
BramPsRead_uint32(rd_value, BRAM_OFFSET, SIZE); // PS 读数据
BramPlReadSet(SIZE, 1); // PL 读 16 个数据,1 个时间单位(与时钟频率有关)读一次
BramPlReadStart(); // PL 开始连续、循环读取数据
kprintf("--- BRAM WR TEST ---\r\n");
for(uint32_t i = 0; i < SIZE; i++)
{
kprintf("Address: %4ld \t Data: %08lx \r\n", i, rd_value[i]); // 将读取的数据输出到串口
}
return 0;
}
- 直接在主函数调用一次 bram_wr_test 函数即可。
板级验证
- 串口终端:
- ILA 捕获: