ZYNQ基于 BRAM 的 PS 与 PL 数据交互

目录

  • 实验目的
  • 硬件设计
  • SDK 设计
  • 板级验证
  • 更多内容

在 ZYNQ 开发过程中,我们经常遇到需要 PS 和 PL 数据交互的场合,通常使用的方法有 DMA、BRAM 等。对于速度要求高、数据量大、地址连续的数据,可以通过 DMA 来实现,但对于地址不连续,数据量少的场合,DMA 就不适用了,针对这种情况,可以通过 BRAM 来实现。

BRAM(Block RAM)是 PL 端的存储器阵列,PL 可通过输出时钟、地址、读写控制等信号来对其进行读写操作,在 PS 端,处理器则可通过 AXI BRAM 控制器来实现对 BRAM 的读写,这样可以很方便的实现 PS 与 PL 之间的数据交互。

实验目的

  1. PS 端随机写入一些数据到 BRAM
  2. PS 端读取写入的数据并输出到串口
  3. 控制 PL 端开始、停止读取数据
  4. 实现 PL 连续、循环的读取数据
  5. 控制 PL 端读取的频率、数据量
  6. 通过 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 ---
");
    for(uint32_t i = 0; i < SIZE; i++)
    {
        kprintf("Address: %4ld 	 Data: %08lx 
", i, rd_value[i]); // 将读取的数据输出到串口
    }

    return 0;
}
  • 直接在主函数调用一次 bram_wr_test 函数即可。

板级验证

  • 串口终端:

  • ILA 捕获:

更多内容

  • CSDN博客:@Hello阿尔法
  • 哔哩哔哩:@Hello阿尔法
  • 知乎:@Hello阿尔法