在 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 捕获:
更多内容
- CSDN博客:@Hello阿尔法
- 哔哩哔哩:@Hello阿尔法
- 知乎:@Hello阿尔法