ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验

文章目录

  • 前言
  • 一、设计需求
  • 二、RAM是什么?
  • 三、硬件设计
    • 3.1 系统框图
    • 3.2 IP核配置
    • 3.3 自定义IP核
    • 3.4 其他
  • 四、软件设计
  • 五、下载验证
  • 六、实验改进
    • 6.1 硬件改进
    • 6.2 软件改进
    • 6.3 改进结果
  • 七、遇见的问题
  • 总结

前言

说起PS、PL数据交互,常见的有IO方式:MIO EMIO GPIO,还有利用BRAM或FIFO等,在上一篇文章ZYNQ学习笔记(三):PL与PS数据交互—— UART串口+AXI GPIO控制DDS IP核输出实验咱们学会了如何利用AXI GPIO IP核来实现PS(写)与PL(读)的数据交互,那么这篇文章来学习如何使用BRAM~

本次实验工程连接:ZYNQ通过PS访问PL端BRAM,与PL进行数据交互,基于BRAM IP 核的(PS端读写+PL端读)控制

一、设计需求

1.将 Xilinx BMG IP 核配置成一个真双端口的 RAM 并对其进行读写操作。
2.在PS端通过串口输入数据给BRAM,写操作完成后再把数据读回,在串口打印出来。
3.在PL端把RAM中的数据读出,将其输送给其他模块进行功能选择配置。
4.通过仿真观察数据是否正确,最后将设计下载到 FPGA 开发板中,并通过在线调试工具对实验结果进行验证。

二、RAM是什么?

RAM 的英文全称是 Random Access Memory,即随机存取存储器,简称随机存储器,它可以随时把数据 写入任一指定地址的存储单元,也可以随时从任一指定地址的存储单元中读出数据,其读写速度是由时钟频率决定的。
在了解 RAM IP 核之前,我们先来看下存储器的大致分类,如下图所示:

由上图可知,常见的半导体存储器包括随机存储器和只读存储器,随机存储器包括静态 RAM 和动态 RAM。静态 RAM 只要有供电,它保存的数据就不会丢失;而动态 RAM 在供电的情况下,还需要根据其要求的时间来对存储的数据进行刷新,才能保持存储的数据不会丢失。

静态 RAM 一般包括单端口 RAM(Single Port RAM,缩写为 SP RAM)、简单双端口 RAM(Simple Dual Port RAM,缩写为 SDP RAM,也叫伪双端口 RAM)和 真双端口 RAM(True Dual Port RAM,缩写为 TDP RAM)。静态 RAM 的特点是存储容量相对不是很大,但是读写速度非常高,其在 FPGA 或者 ASIC 设计中 都属于非常重要的器件,可以说查找表、寄存器、组合逻辑和静态 RAM 构成了整个数字电路体系,足见静 态 RAM 的重要性。

动态 RAM 一般包括 SDRAM 和 DDR SDRAM。目前 DDR SDRAM 已经从 DDR1 代发展到 DDR5 代 了,DDR3 和 DDR4 SDRAM 是目前非常主流的存储器,大量使用在电脑、嵌入式和 FPGA 板卡上面,其特点是存储容量非常大、但是读写速度相比于静态 RAM 会稍低一些,这一点在数据量较少的情况下尤为明显。

只读存储器一般包括 PROM、EPROM 和 EEPROM 等,是非易失性的存储器。目前使用率较高的是EEPROM,其特点是容量相对较小,存储的一般是器件的配置参数信息,例如 USB 2.0 芯片一般会配有一个 EEPROM 来存储相关的固件信息。

这次我们学习的 RAM 属于静态 RAM ,我们重点看下几种静态 RAM 的特性与区别
从上表可以看出,单端口 RAM 只有一个端口进行读写,即读/写只能通过这一个端口来进行。对于伪双端口 RAM 而言,其也有两个端口可以用于读写,但是其中一个端口只能读不能写,另一个端口只能写不能读;对于真双端口 RAM 而言,其有两个端口可以用于读写,且两个端口都可以进行读或写。

Vivado 软件自带的 Block Memory Generator IP 核(缩写为 BMG,中文名为块 RAM 生成器),可以用来配置生成 RAM 或者 ROM。需要注意的是,配置生成的 RAM 或者 ROM 使用的都是 FPGA 内部的 BRAM 资源(Block RAM,即块随机存储器,是 FPGA 厂商在逻辑资源之外,给 FPGA 加入的专用 RAM 块资源)。

BMG IP 核配置成单端口 RAM如下图所示。

BMG IP 核配置成伪双端口 RAM。

与单端口 RAM 不同的是,伪双端口 RAM 输入有两路时钟信号 CLKA、CLKB(这里设置同步了一下);独立的两组地址信号ADDRA/ADDRB;Port A 仅提供 DINA 写数据总线,作为数据的写入口;Port B 仅提供数据读的功能,读出的数据为 DOUTB。

BMG IP 核配置成真双端口 RAM。

真双端口 RAM 提供了两个独立的读写端口(A 和 B),既可以同时读,也可以同时写,也可以一个读一个写。通过框图对比可以发现,真双端口 RAM 只是将单端口 RAM 的所有信号做了一个复制处理,不同端口的同一信号以 A 和 B 作为区分。

三种静态 RAM 的端口对比表,让大家能更直观的看出各静态 RAM 的端口差异,其中 “√”表示有,“×”表示无。

通过对比我们可以发现无论是哪种双端口 RAM,其地址线、时钟线和使能线等控制信号都有两组,所以双端口 RAM 可以实现在不同时钟域下的读/写,且可以同时对不同的地址进行读/写,这便大大提高了我们数据处理的灵活性。但是两组信号线也相应的加大了双端口 RAM 的使用难度,因为端口使能,读写使能,地址和写数据等控制信号我们都需要分别给出两组,这样才能驱使两个端口都处于我们需要的工作状态。

三、硬件设计

3.1 系统框图

根据实验任务我们可以画出本次实验的系统框图,如下图所示:

AXI BRAM 控制器作为 PS 端读写 BRAM 的 IP 核;PL 读BRAM IP 核是我自定义的 IP 核,实现了 PL 端从 BRAM 中读出数据的功能。PS和PL通过对BRAM进行读写操作,来实现数据的交互。**在PL中,通过输出时钟、地址、读写控制等信号来对BRAM进行读写操作;但在PS中,处理器并不需要直接驱动BRAM的端口,而是通过AXI BRAM控制器来对BRAM进行读写操作。**对于少量、不连贯数据,使用BRAM是很好的选择,而对于大量连续的数据,ZYNQ7020只有4.9Mb的BRAM,使用DMA (直接内存访问)是更好的选择,这个以后也会学到。

3.2 IP核配置

ZYNQ7 Processing System 模块配置:
点击Peripheral IO Pins , 这里我们只需要配置UART0,其余配置同上一篇~
请添加图片描述
添加AXI BRAM Controller IP 核:

AXI Protocol(AXI 协议)选择的是 AXI4,对于本次实验来说,选择 AXI4 或者 AXI4-Lite没有影响;Data Width(数据位宽)选择最低的32 位,由于 AXI4 总线为字节寻址,因此在映射到 BRAM 地址时,需要按 4 字节寻址。
本次实验的 BRAM 控制器只需要读写 BRAM 的一个端口,因此将 BRAM 的总线个数设置为 1;ECC 选项用于数据错误纠正与检查,这里不使能。需要说明的是,Memory Depth(存储深度)在这里不可以设置,寻址 BRAM 的存储深度是在 Address Editor 里设置的

接下来在 Block Design 中添加 BRAM IP 核:

这里配置成真双端口 RAM,A端口为PS端提供读写,B端口则是PL端模块接收来自PS端的数据。

BRAM IP 核内置了一个安全电路以降低 BRAM 数据出现错误的概率,如果勾选使能安全电路,BRAM端口会增加 rsta_busy 端口和 rstb_busy 端口,用于表示何时可以访问 BRAM。这里直接取消使能安全电路。

其实在BRAM Controller模式下,BRAM的默认工作模式是WRITE_FIRST(写先于读)

我们可以来看一下在WRITE_FIRST(写先于读)工作模式下,BRAM各信号的时序图是什么样的

WRITE_FIRST的时序图,在红线以前,WE=0 ,即端口做的是读操作。在前一个CLK上升沿后,地址aa里的数据打到DO。在红线后,WE=1,即端口做的是写操作。在CLK上升沿后,DI的数据存入到地址bb里,与此同时输入的数据DI直接打到去输出DO,原先存在bb里的数据还没出来就被冲掉,这就是WRITE_FIRST的特性。

那READ_FIRST(读先于写)模式又是什么呢?其时序图如图:

READ_FIRST的时序图,在红线以前,WE=0 ,即端口做的是读操作。在前一个CLK上升沿后,地址aa里的数据打到DO。在红线后,WE=1,即端口做的是写操作。在CLK上升沿后,DI的数据存入到地址bb里,与此同时原先存放在地址bb的数据输出到DO。

剩下的NO_CHANGE,其时序图如图:

同样的在红线以前WE=0 ,即端口做的是读操作。在前一个CLK上升沿后,地址aa里的数据打到DO。在红线后,WE=1,即端口做的是写操作。在CLK上升沿后,DI的数据存入到地址bb里,与此同时输出DO的数据保持不变(依旧是地址aa里的数据)。

好了,介绍结束。回到正题~

三个模块添加配置完成后依次点击~
请添加图片描述
基本框架搭建完成:

3.3 自定义IP核

在前面关于BRAM IP核的基础知识应该说的差不多了,那么我该怎么去写一个模块,能让它读出BRAM 0地址处的32位(4字节)的数据呢?还是回归到WRITE_FIRST模式下的时序图,模块设计如下:

module Bram_read_only(
    input clk,
    input rst_n,
    output reg [31:0] read_b,//读到的数据输出
    input [31:0] doutb, // 来自BRAM的数据
    
    output  ram_clk, 
    // RAM时钟,注意:两路时钟信号 CLKA、CLKB我为了测试一下并没有在BRAM IP核里设置同步(Common Clock),如果设置了同步这个时钟信号可以去掉。
    output reg ram_en, // RAM使能信号
    output reg [31:0] ram_addr, // RAM地址
    output reg [3:0] ram_we// RAM读写控制信号
    
);

    reg [31:0] bram_data; // 用于存储从 BRAM 中读取的数据
    reg [31:0] addrb; // 用于存储 BRAM 地址
    assign  ram_clk = clk ; //

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin           //初始化一下
            addrb <= 32'd0;
            bram_data <= 32'd0;
            read_b <= 32'd0;
            ram_en <= 0;
            ram_we <= 4'b0000;
           
        end else begin

            if (addrb == 32'd0) begin
                ram_en <= 1;
                ram_addr <= addrb;
                ram_we <= 4'b0000;
                bram_data <= doutb;
                read_b <= bram_data;
            end
        end
    end

endmodule

注意,这个程序是对应在PS端输入的情况下写的,因为一方面AXI BRAM Controller IP 核它的Data Width(数据位宽)选择最低的32 位,由于 AXI4 总线为字节寻址,因此在映射到 BRAM 地址时,需要按 4 字节寻址。因此对应着的BRAM 控制器模式(BRAM Controller),在此模式下,地址和数据默认为 32 位。你如果想写一个PL输入,PL读,你可以在独立模式下把地址设置成对应正常的一个字节,也就是8位~

这个程序非常简单的实现了读取BRAM 0地址处的数据,但如果我想读连续多地址的数据,我就完全可以在这个程序上进行更改,比如加一个循环以及计数器、或者再加几个使能信号,当检测到使能信号满足某个条件,可进入循环状态下地址加4并输出读到的数据,不满足了则退出循环,并保存当前地址的状态等~等后面我遇到实际问题再把这部分补充更新一下

3.4 其他

后面我又加了一个Slice IP核,我把读出的32位数据截取了后六位来控制LED灯,最后的Block Design:

xdc文件:

set_property PACKAGE_PIN F16 [get_ports {Dout[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Dout[0]}]

set_property PACKAGE_PIN F17 [get_ports {Dout[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Dout[1]}]

set_property PACKAGE_PIN G15 [get_ports {Dout[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Dout[2]}]

set_property PACKAGE_PIN H15 [get_ports {Dout[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Dout[3]}]

set_property PACKAGE_PIN K14 [get_ports {Dout[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Dout[4]}]

set_property PACKAGE_PIN G14 [get_ports {Dout[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {Dout[5]}]

四、软件设计

在 SDK 软件中新建一个 BSP 工程和一个空的应用工程,然后为应用工程新建一个源文件“main.c”,我们在新建的 main.c 文件中输入本次实验的代码(具体怎么操作不再赘述,可参考上一篇文章)。代码的主体部分如下所示:

#include "xil_printf.h"
#include "stdio.h"
#include "xbram.h"
#include "xparameters.h"

#define START_ADDR          0  //RAM起始地址 范围:0~1023
#define BRAM_DATA_BYTE      4  //BRAM数据字节个数

char ch_data[1024];            //能写入BRAM的字符数组
int ch_data_len;               //写入BRAM的字符个数

//函数声明
void str_wr_bram();
void str_rd_bram();

//main函数
int main()
{
    while(1)
    {
        printf("Please enter data to read and write BRAM\n") ;
        scanf("%1024s", ch_data);        //用户输入字符串
        ch_data_len = strlen(ch_data);   //计算字符串的长度

        str_wr_bram();                   //将用户输入的字符串写入BRAM中
        str_rd_bram();                   //从BRAM中读出数据
    }
}

//将字符串写入BRAM
void str_wr_bram()
{
    int i=0,wr_cnt = 0;
    //每次循环向BRAM中写入1个字符,每个字符占4个字节
    for(i = BRAM_DATA_BYTE*START_ADDR ; i < BRAM_DATA_BYTE*(START_ADDR + ch_data_len) ;
            i += BRAM_DATA_BYTE){
        XBram_WriteReg(XPAR_BRAM_0_BASEADDR,i,ch_data[wr_cnt]) ;
        wr_cnt++;
    }
}

//从BRAM中读出数据
void str_rd_bram()
{
    int read_data=0,i=0;
    //循环从BRAM中读出数据
    for(i = BRAM_DATA_BYTE*START_ADDR ; i < BRAM_DATA_BYTE*(START_ADDR + ch_data_len) ;
            i += BRAM_DATA_BYTE){
        read_data = XBram_ReadReg(XPAR_BRAM_0_BASEADDR,i) ;
        printf("BRAM address is %d\t,Read data is %c\n",i ,read_data) ;
    }
}

这个程序并不复杂,配合注释我们应该都能明白代码意思,值得说的就是它的核心XBram_WriteReg、XBram_ReadReg函数,这两个函数我找到了它们的函数定义。

readreg函数定义为:
inline int readreg(struct net_ device *dev, int portno)
outw(portno, dev->base_ addr + ADD_ PORT); .
return inw(dev->base_ addr + DATA_ PORT);
}

参数 dev 是指向一个结构体的指针,该结构体存储了网络设备相关的信息,包括设备的基本地址等。
参数 portno 是要读取的寄存器的端口号。
outw(portno, dev->base_addr + ADD_PORT) 将 portno 写入到设备基地址加上 ADD_PORT 偏移量的寄存器中,用于指示要读取哪个寄存器的数据。
inw(dev->base_addr + DATA_PORT) 从设备基地址加上 DATA_PORT 偏移量的寄存器中读取数据,并将其返回作为函数的返回值。

writereg函数定义为:
inline void writereg(struct net_ device *dev, int portno, int value)
{
outw(portno, dev->base_ addr + ADD_ PORT);
outw(value, dev->base_ addr + DATA_ PORT);
}

参数 value 是要写入到寄存器中的值,outw(portno, dev->base_addr + ADD_PORT) 将 portno 写入到设备基地址加上 ADD_PORT 偏移量的寄存器中,用于指示要写入哪个寄存器。
outw(value, dev->base_addr + DATA_PORT) 将 value 写入到设备基地址加上 DATA_PORT 偏移量的寄存器中,完成寄存器的写入操作。

这两个函数在访问特定设备的寄存器时非常有用,它们使得通过指定端口号和设备基地址来读取和写入数据变得更加简便。在这个程序在写 BRAM 的过程中,通过 XBram_WriteReg()函数将接收到的数据按照起始地址,依次写入 BRAM。读取 BRAM 中的数据,通过XBram_ReadReg()函数按照 BRAM 的起始地址,依次从 BRAM 中读出数据,并通过串口打印出来。

五、下载验证

我们将下载器与火龙果板上的 JTAG 接口连接,下载器另外一端与电脑连接。然后使用 USB 连接线将 USB UART 接口与电脑连接,用于串口通信。最后连接开发板的电源,并打开电源开关。程序下载完毕后目光移至SDK Terminal,我们随便发送一个数字1、3:

回到vivado我们可以看到截取的后六位(准确的显示1、3的ASCII所代表的2进制):


可以看到板子上LED灯按照预期正常亮起,


至此这个实验的设计需求咱们就完成了,那有没有改进的空间呢,或者说更规范一些,咱们可以接着往下做~

六、实验改进

首先记得把上面的实验备份~

6.1 硬件改进

在上面实验中,Bram_read_only模块读BRAM的地址是咱们写到程序里的,那这个地址能不能由PS端给呢?比如说我PS端向某个或多个地址写入数据,在写操作完成后,给Bram_read_only模块一个使能信号还有写入数据的地址,通知它去读相应地址的数据,如果可以实现这样的情况,那以后完全可以只更改PS端代码。

实验回到3.2节搭好的基本框架,此时我们就要新建一个带AXI4接口的Bram_read IP核,点击Tools——Create and Package New IP


删除路径“/…/”中间的一个“.”符号,使路径改为工程目录下的 ip_repo 文件夹,其它的设置直接保持默认即可,点击“NEXT”,直到最后点击“Finish”按钮完成自定义 IP 核的创建。

可以看到此时在IP Catalog中就有了刚才打包的一个带AXI接口的IP核。(VIVADO帮我们做好了AXI接口 )

在新打开的工程新建设计文件,添加一个读BRAM模块bram_rd位于…/ip_repo/Bram_read_1.0/hdl 路径下 :

module bram_rd(
    input                clk        , //时钟信号
    input                rst_n      , //复位信号
    input                start_rd   , //读开始信号
    input        [31:0]  start_addr , //读开始地址
    input        [31:0]  rd_len     , //读数据的长度
    //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  reg  [3:0]   ram_we     , //RAM读写控制信号
    output  reg  [31:0]  ram_wr_data, //RAM写数据
    output               ram_rst      //RAM复位信号,高电平有效
);

//reg define
reg  [1:0]   flow_cnt;
reg          start_rd_d0;
reg          start_rd_d1;

//wire define
wire         pos_start_rd;

//**                  main code

assign  ram_rst = 1'b0;
assign  ram_clk = clk ;
assign pos_start_rd = ~start_rd_d1 & start_rd_d0;

//延时两拍,采start_rd信号的上升沿
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_rd;
        start_rd_d1 <= start_rd_d0;
    end
end

//根据读开始信号,从RAM中读出数据
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        flow_cnt <= 2'd0;
        ram_en <= 1'b0;
        ram_addr <= 32'd0;
        ram_we <= 4'd0;
    end
    else begin
        case(flow_cnt)
            2'd0 : begin
                if(pos_start_rd) begin//收到开始信号,读使能,并赋值开始读的地址,同时进入状态1
                    ram_en <= 1'b1;
                    ram_addr <= start_addr;
                    flow_cnt <= flow_cnt + 2'd1;
                end
            end
            2'd1 : begin
                if(ram_addr - start_addr == rd_len - 4) begin  //根据读数据长度的要求,数据读完后关闭读使能,进入状态1
                    ram_en <= 1'b0;
                    flow_cnt <= flow_cnt + 2'd1;
                end
                else
                    ram_addr <= ram_addr + 32'd4;          //地址累加4
            end
            2'd2 : begin
                ram_addr <= 32'd0; //地址清零,回归状态0
                flow_cnt <= 2'd0;
            end
        endcase
    end
end

endmodule

程序用 start_rd 信号的上升沿作为触发器,以捕获读取操作的开始,这样可以确保信号稳定后再开始执行读取操作。一旦检测到 start_rd 信号的上升沿,系统将启用读取操作。在启用读取操作时,ram_en 被置为1,允许从RAM中读取数据,并且 ram_addr 被设置为 start_addr,开始从指定地址读取数据。当读取数据的长度达到指定长度 rd_len 时,将关闭RAM的读取使能 (ram_en),来停止从RAM中读取数据。一旦读取操作完成,系统将重置地址并返回状态0,以准备进行下一次读取操作,这样就确保了下一次读取操作从指定地址开始。

下面我们需要把这个功能模块例化到刚才创建的带AXI接口的IP核里:
打开 Bram_read_v1_0.v 文件,在 Users to add ports here 和 User port ends 中间行添加如下代码,这些端口用于连接 BRAM 的B部分端口

	17行// Users to add ports here
       //RAM 端口
       output wire ram_clk , //RAM 时钟
       input wire [31:0] ram_rd_data, //RAM 中读出的数据
       output wire ram_en , //RAM 使能信号
       output wire [31:0] ram_addr , //RAM 地址
       output wire [3:0] ram_we , //RAM 读写控制信号
       output wire [31:0] ram_wr_data, //RAM 写数据
       output wire ram_rst , //RAM 复位信号,高电平有效
	26行// User ports ends
....
....
 79行//RAM 端口 
   .ram_clk (ram_clk ),
   .ram_rd_data (ram_rd_data),
   .ram_en (ram_en ),
   .ram_addr (ram_addr ),
   .ram_we (ram_we ),
   .ram_wr_data (ram_wr_data),
   .ram_rst (ram_rst )

打开Bram_read_v1_0_S00_AXI.v文件添加代码:

	17行// Users to add ports here
       //RAM 端口
       output wire ram_clk , //RAM 时钟
       input wire [31:0] ram_rd_data, //RAM 中读出的数据
       output wire ram_en , //RAM 使能信号
       output wire [31:0] ram_addr , //RAM 地址
       output wire [3:0] ram_we , //RAM 读写控制信号
       output wire [31:0] ram_wr_data, //RAM 写数据
       output wire ram_rst , //RAM 复位信号,高电平有效
	26行// User ports ends
	....
	....
	407行// Add user logic here
    bram_rd u_bram_rd(
   .clk (S_AXI_ACLK),
   .rst_n (S_AXI_ARESETN),
   .start_rd (slv_reg0[0]),
   .start_addr (slv_reg1),
   .rd_len (slv_reg2),
 //RAM 端口 
   .ram_clk (ram_clk ),
   .ram_rd_data (ram_rd_data),
   .ram_en (ram_en ),
   .ram_addr (ram_addr ),
   .ram_we (ram_we ),
   .ram_wr_data (ram_wr_data),
   .ram_rst (ram_rst )
 );
	423行// User logic ends

这段代码例化了 bram_rd 模块,其中==** start_rd 信号是开始读 BRAM 的开始信号,start_addr 是设置 BRAM的读起始地址,rd_len 是设置读 BRAM 的个数,分别连接到 AXI4-Lite 总线的寄存器地址 0、地址 1 和地址2 对应的数据。**==

点击“File Groups”一栏,随后点击界面上的“Merge changes from File Groups Wizard”;点击“Customization Parameters”
一栏,随后点击界面上的“Merge changes from Customization Parameters Wizard”。

点击“Ports and Interfaces”,对于我们新增的RAM端口,我们可以把它封装成一个总线。如果不封装,在Block design中就需要一个一个连线。

设置BRAM 总线接口:

端口映射

并在Parameters中加入主机类型,否则后续会报警告

后面重新封装整个IP,切换至“Review and Package”页面,点击上侧的“IP has been modified”来更新 IP,最后点击“Re-Package IP”完成 IP 核的封装。
完成后打开“Address Editor”页面,展开“processing_system7_0”下的“Data”,将范围设置成“4K”,由于 BRAM 的数据位宽是 32 位,因此 BRAM 的存储深度为 4K/4=1K。

在原工程中去掉之前的IP核加入这个IP核,此时可以看见BRAM的端口封装好了,如图连接~

6.2 软件改进

VIVADO重新导出硬件生成新的.sdk文件夹,Launch SDK~
新建一个 BSP 工程和一个空的应用工程,新建一个源文件“main.c”

#include <stdio.h>
#include "xil_printf.h"
#include "xbram_hw.h"
#include "bram_read.h"
#include "xparameters.h"

#define PL_BRAM_START 		        BRAM_READ_S00_AXI_SLV_REG0_OFFSET    //sreg0对应bram开始读信号
#define PL_BRAM_START_ADDR     BRAM_READ_S00_AXI_SLV_REG1_OFFSET    //sreg1对应读的起始地址
#define PL_BRAM_LEN		 	            BRAM_READ_S00_AXI_SLV_REG2_OFFSET   //sreg2对应读的长度
#define PL_BRAM_BASE_ADDR 	    XPAR_BRAM_READ_0_S00_AXI_BASEADDR  //PL_读bram IP核起始地址

#define START_ADDR 			    4
#define BRAM_DATA_BYTE      4
void str_wr_bram(void);
void str_rd_bram(void);

char ch_data[1024]; //写入 BRAM 的字符数组
int ch_data_len; //写入 BRAM 的字符个数
int main()
{
	while(1)
	{
		printf("Bram is running! Please input data to write bram.\n");
		scanf("%s",ch_data);
		ch_data_len = strlen(ch_data); //计算字符串的长度
		str_wr_bram();
		str_rd_bram();

	}
}
void str_wr_bram()
{
	int i=0,wr_cnt = 0;
	//每次循环向 BRAM 中写入 1 个字符
	for(i = BRAM_DATA_BYTE*START_ADDR ; i < BRAM_DATA_BYTE*(START_ADDR + ch_data_len) ;
			i += BRAM_DATA_BYTE)
	{
		XBram_WriteReg(XPAR_BRAM_0_BASEADDR,i,ch_data[wr_cnt]) ;
		wr_cnt++;
       //设置 BRAM 写入的字符串长度
       BRAM_READ_mWriteReg(PL_BRAM_BASE_ADDR, PL_BRAM_LEN , BRAM_DATA_BYTE*ch_data_len) ;
       //设置 BRAM 的起始地址
       BRAM_READ_mWriteReg(PL_BRAM_BASE_ADDR, PL_BRAM_START_ADDR, BRAM_DATA_BYTE*START_ADDR) ;
      //拉高 BRAM 开始信号
      BRAM_READ_mWriteReg(PL_BRAM_BASE_ADDR, PL_BRAM_START , 1) ;
      //拉低 BRAM 开始信号
      BRAM_READ_mWriteReg(PL_BRAM_BASE_ADDR, PL_BRAM_START , 0) ;
	}
}
 void str_rd_bram()
{
	 int read_data=0,i=0;
	 //循环从 BRAM 中读出数据
	 for(i = BRAM_DATA_BYTE*START_ADDR ; i < BRAM_DATA_BYTE*(START_ADDR + ch_data_len) ;
	i += BRAM_DATA_BYTE)
	 {
		 read_data = XBram_ReadReg(XPAR_BRAM_0_BASEADDR,i) ;
		 printf("BRAM address is %d\t,Read data is %c\n",i/BRAM_DATA_BYTE ,read_data) ;
	 }
} 

相较于之前的程序,改进的程序最主要的部分体现在这一部分:

//设置 BRAM 写入的字符串长度
BRAM_READ_mWriteReg(PL_BRAM_BASE_ADDR, PL_BRAM_LEN , BRAM_DATA_BYTE*ch_data_len) ;
//设置 BRAM 的起始地址
BRAM_READ_mWriteReg(PL_BRAM_BASE_ADDR, PL_BRAM_START_ADDR, BRAM_DATA_BYTE*START_ADDR) ;
//拉高 BRAM 开始信号
BRAM_READ_mWriteReg(PL_BRAM_BASE_ADDR, PL_BRAM_START , 1) ;
//拉低 BRAM 开始信号
BRAM_READ_mWriteReg(PL_BRAM_BASE_ADDR, PL_BRAM_START , 0) ;

咱们可以回顾前面已经说过的mWriteReg函数~
第一个参数是指向一个结构体的指针,包括BRAM_READ这个模块的基本地址。
第二个参数是要读取的寄存器的端口号。
第三个参数是要写入到寄存器中的值,mWriteReg函数通过指定端口号(偏移地址)和设备基地址来写入数据。
所以BRAM_READ_mWriteReg向寄存器2写入了用户写入的字符串长度,向寄存器1写入了读的起始地址,同时向寄存器0先写入了1后写入了0,这很好的对应了前面设计的 bram_rd 模块—— start_rd 信号是开始读 BRAM 的开始信号,连接到 AXI4-Lite 总线的寄存器 0对应的数据。start_addr 是设置 BRAM的读起始地址,连接到寄存器1 对应的数据。rd_len 是设置读 BRAM 的个数,连接到寄存器2 对应的数据。

程序将接收到的数据写入 BRAM 中,并配置 PL 端开始从 BRAM 中读取数据。在写 BRAM 的过程中,通过 XBram_WriteReg()函数将接收到的数据按照起始地址,依次写入 BRAM中。在数据写入完成后,通过 AXI 总线,配置 PL 端读取 BRAM 的数据个数和起始地址,并驱动 PL 读开始信号start_addr输出一个脉冲信号(每次循环相当于start_addr由0后1再0相当于一个脉冲信号)开始读BRAM相应地址中的数据。

6.3 改进结果



请添加图片描述
由PS端直接给BRAM_READ模块要读取得地址~可以看到改进结果完全符合预期。

七、遇见的问题

1、在6.1小节,当我封装完IP核把IP核添加到工程后,验证设计跟生成Generate Output 都没报错,可是生成BIT文件时,Run synthesis一直出现错误:

我刚开始猜测是bram_read模块的定义出了问题或者上层模块的例化写的不对,但是检查了好多遍程序都没意识到问题出在哪、后面转换思路,猜测可能是我在设计生成这个IP过程中遗漏了哪些步骤,果然发现在封装整个IP的前一步,我并没有在“Review and Package”页面,点击上侧的“IP has been modified”来更新 IP,也就是在配置完这个IP核后,并没有选择更新配置~(IP has been modified在页面左上角比较细小很不起眼,一定不要忘了点。)后面更新完配置后一切恢复正常。

2、当在SDK下载程序时,出现错误:

这个也曾让我一度以为是程序出了问题,按理来说BRAM0地址它对应的是0x40000000,况且程序对应的是地址4,那怎么能写到0x100000呢?让我一度怀疑自己程序写错了,是不是函数用的不对,是不是少了头文件或者一些必须的定义声明等等~这个问题也是耽误了些时间,后来自己又仔细复盘了一下,终于发现了问题所在——DDR配置出现了错误,因为用的是火龙果的板子,一时间忘了更改它DDR3的配置,更改完成后一切回归正常。

总结

回顾这篇文章,对自己这个初学者来说信息量还是比较大的,同样的自己在实验的过程中也在不断的学习与吸取教训,先不说程序带来的问题,其实很多问题的出现都是由自己粗心导致的,实现设计虽繁琐,但以后还要步步注意,步步细心,不能犯低级失误,路阻且长,还在路上~加油!

版权声明:本文为博主作者:星空lg原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/weixin_44852755/article/details/134223729

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2024年1月11日
下一篇 2024年1月11日

相关推荐