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

这篇具有很好参考价值的文章主要介绍了ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


前言

说起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 核之前,我们先来看下存储器的大致分类,如下图所示:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
由上图可知,常见的半导体存储器包括随机存储器和只读存储器,随机存储器包括静态 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 的特性与区别 ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
从上表可以看出,单端口 RAM 只有一个端口进行读写,即读/写只能通过这一个端口来进行。对于伪双端口 RAM 而言,其也有两个端口可以用于读写,但是其中一个端口只能读不能写,另一个端口只能写不能读;对于真双端口 RAM 而言,其有两个端口可以用于读写,且两个端口都可以进行读或写。

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

BMG IP 核配置成单端口 RAM如下图所示。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
BMG IP 核配置成伪双端口 RAM。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
与单端口 RAM 不同的是,伪双端口 RAM 输入有两路时钟信号 CLKA、CLKB(这里设置同步了一下);独立的两组地址信号ADDRA/ADDRB;Port A 仅提供 DINA 写数据总线,作为数据的写入口;Port B 仅提供数据读的功能,读出的数据为 DOUTB。

BMG IP 核配置成真双端口 RAM。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
真双端口 RAM 提供了两个独立的读写端口(A 和 B),既可以同时读,也可以同时写,也可以一个读一个写。通过框图对比可以发现,真双端口 RAM 只是将单端口 RAM 的所有信号做了一个复制处理,不同端口的同一信号以 A 和 B 作为区分。

三种静态 RAM 的端口对比表,让大家能更直观的看出各静态 RAM 的端口差异,其中 “√”表示有,“×”表示无。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
通过对比我们可以发现无论是哪种双端口 RAM,其地址线、时钟线和使能线等控制信号都有两组,所以双端口 RAM 可以实现在不同时钟域下的读/写,且可以同时对不同的地址进行读/写,这便大大提高了我们数据处理的灵活性。但是两组信号线也相应的加大了双端口 RAM 的使用难度,因为端口使能,读写使能,地址和写数据等控制信号我们都需要分别给出两组,这样才能驱使两个端口都处于我们需要的工作状态。

三、硬件设计

3.1 系统框图

根据实验任务我们可以画出本次实验的系统框图,如下图所示:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
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,其余配置同上一篇~
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
添加AXI BRAM Controller IP 核:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
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端的数据。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
BRAM IP 核内置了一个安全电路以降低 BRAM 数据出现错误的概率,如果勾选使能安全电路,BRAM端口会增加 rsta_busy 端口和 rstb_busy 端口,用于表示何时可以访问 BRAM。这里直接取消使能安全电路。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
其实在BRAM Controller模式下,BRAM的默认工作模式是WRITE_FIRST(写先于读)
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
我们可以来看一下在WRITE_FIRST(写先于读)工作模式下,BRAM各信号的时序图是什么样的
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
WRITE_FIRST的时序图,在红线以前,WE=0 ,即端口做的是读操作。在前一个CLK上升沿后,地址aa里的数据打到DO。在红线后,WE=1,即端口做的是写操作。在CLK上升沿后,DI的数据存入到地址bb里,与此同时输入的数据DI直接打到去输出DO,原先存在bb里的数据还没出来就被冲掉,这就是WRITE_FIRST的特性。

那READ_FIRST(读先于写)模式又是什么呢?其时序图如图:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
READ_FIRST的时序图,在红线以前,WE=0 ,即端口做的是读操作。在前一个CLK上升沿后,地址aa里的数据打到DO。在红线后,WE=1,即端口做的是写操作。在CLK上升沿后,DI的数据存入到地址bb里,与此同时原先存放在地址bb的数据输出到DO。

剩下的NO_CHANGE,其时序图如图:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
同样的在红线以前WE=0 ,即端口做的是读操作。在前一个CLK上升沿后,地址aa里的数据打到DO。在红线后,WE=1,即端口做的是写操作。在CLK上升沿后,DI的数据存入到地址bb里,与此同时输出DO的数据保持不变(依旧是地址aa里的数据)。

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

三个模块添加配置完成后依次点击~
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
基本框架搭建完成:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发

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:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
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:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
回到vivado我们可以看到截取的后六位(准确的显示1、3的ASCII所代表的2进制):
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
可以看到板子上LED灯按照预期正常亮起,
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
至此这个实验的设计需求咱们就完成了,那有没有改进的空间呢,或者说更规范一些,咱们可以接着往下做~

六、实验改进

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

6.1 硬件改进

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

实验回到3.2节搭好的基本框架,此时我们就要新建一个带AXI4接口的Bram_read IP核,点击Tools——Create and Package New IP
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
删除路径“/…/”中间的一个“.”符号,使路径改为工程目录下的 ip_repo 文件夹,其它的设置直接保持默认即可,点击“NEXT”,直到最后点击“Finish”按钮完成自定义 IP 核的创建。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
可以看到此时在IP Catalog中就有了刚才打包的一个带AXI接口的IP核。(VIVADO帮我们做好了AXI接口 )
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
在新打开的工程新建设计文件,添加一个读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”。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
点击“Ports and Interfaces”,对于我们新增的RAM端口,我们可以把它封装成一个总线。如果不封装,在Block design中就需要一个一个连线。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
设置BRAM 总线接口:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
端口映射
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
并在Parameters中加入主机类型,否则后续会报警告
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
后面重新封装整个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。
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
在原工程中去掉之前的IP核加入这个IP核,此时可以看见BRAM的端口封装好了,如图连接~
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发

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 改进结果

ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
由PS端直接给BRAM_READ模块要读取得地址~可以看到改进结果完全符合预期。

七、遇见的问题

1、在6.1小节,当我封装完IP核把IP核添加到工程后,验证设计跟生成Generate Output 都没报错,可是生成BIT文件时,Run synthesis一直出现错误:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
我刚开始猜测是bram_read模块的定义出了问题或者上层模块的例化写的不对,但是检查了好多遍程序都没意识到问题出在哪、后面转换思路,猜测可能是我在设计生成这个IP过程中遗漏了哪些步骤,果然发现在封装整个IP的前一步,我并没有在“Review and Package”页面,点击上侧的“IP has been modified”来更新 IP,也就是在配置完这个IP核后,并没有选择更新配置~(IP has been modified在页面左上角比较细小很不起眼,一定不要忘了点。)后面更新完配置后一切恢复正常。

2、当在SDK下载程序时,出现错误:
ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验,学习,笔记,fpga开发
这个也曾让我一度以为是程序出了问题,按理来说BRAM0地址它对应的是0x40000000,况且程序对应的是地址4,那怎么能写到0x100000呢?让我一度怀疑自己程序写错了,是不是函数用的不对,是不是少了头文件或者一些必须的定义声明等等~这个问题也是耽误了些时间,后来自己又仔细复盘了一下,终于发现了问题所在——DDR配置出现了错误,因为用的是火龙果的板子,一时间忘了更改它DDR3的配置,更改完成后一切回归正常。

总结

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

到了这里,关于ZYNQ学习笔记(四):PL与PS数据交互——基于BRAM IP 核的(PS端读写+PL端读)控制实验的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ZYNQ PS与PL通过AXI-LITE连接,在Linux下直接读写PL的物理地址,实现PS与PL的交互

    ZYNQ开发,如果PL与PS的交互方式仅为AXI-Lite总线的话,在Linux下可以通过直接访问PL的寄存器物理地址来实现PS-PL的数据交互。 测试代码的PC开发平台为Ubuntu18.04,QT5。 ZYNQ为7020,并移植了Linux系统和Ubuntu16.04的最小系统。 将PL的程序封装成IP核,通过AXI-LITE与PS连接,对外是18个寄

    2024年04月10日
    浏览(45)
  • FPGA----ZCU106基于axi-hp通道的pl与ps数据交互(全网唯一最详)

    1、大家好,今天给大家带来的内容是,基于AXI4协议的采用AXI-HP通道完成PL侧数据发送至PS侧(PS侧数据发送至PL侧并没有实现,但是保留了PL读取PS测数据的接口) 2、如果大家用到SoC这种高级功能,那大家应该对于AXI4协议已经很熟悉了,但本文侧重点为初学者直接提供可以上

    2023年04月24日
    浏览(45)
  • ZYNQ通过AXI DMA实现PL发送连续大量数据到PS DDR

    硬件:ZYNQ7100 软件:Vivado 2017.4、Xilinx SDK 2017.4   ZYNQ PL 和 PS 的通信方式有 AXI GPIO、BRAM、DDR等。对于数据量较少、地址不连续、长度规则的情况,BRAM 比较适用。而对于传输速度要求高、数据量大、地址连续的情况,比如 ADC,可以通过 AXI DMA 来完成。 1、硬件设计 1.1 ZYNQ7

    2024年02月04日
    浏览(40)
  • zynq pl访问ps ddr

    在 xilinx mpsoc 平台上进行 Linux 软件开发,不可避免的会涉及到 PS 与 PL 之间的数据交互。这个 系列 介绍一种 基于 DDR 的信息交互方式 。 这篇文章首先介绍下 如何从系统中“偷”内存 。 交互流程: PS 写入数据到 DDR 中,使用中断通知 PL,PL 从协商好的 DDR 中读取数据; PL 写

    2024年02月01日
    浏览(49)
  • zynq 使用AXI_dma 传输==pl到ps,linux驱动开发,应用层处理DMA数据

    在使用zynq输出处理时,会使用到pl和ps的数据传输,可供使用的方案有多种,由于我们的数据量较大打算,因此使用用以下两种方案处理: 1.使用pl直接写ddr3, 2.使用dma, 本次详细介绍使用axi_dma如何将pl的数据在linux应用层接收数据并处理,以及遇到的问题 fpga工程,我们使用

    2024年02月03日
    浏览(55)
  • zynq板上PS端给PL端复位

    最近接触zynq板-zcu104,记录一些实验笔记。 LED闪烁 在vivado中新建一个工程,命名为led-test。添加top.v文件。 其中,top模块接入zcu104开发板的clk_300_p/n信号,通过clock wizard转换为单端始终clock,接入到led闪烁模块led.v 其中,led模块通过一个24bit的计数器,经过相等间隔厚,让led灯

    2024年02月11日
    浏览(38)
  • zynq的PL向PS提供时钟和复位

    最近买了一块矿卡蚂蚁T9+,它的资源比EBAZ4205丰富。 需要矿卡资料包的朋友可以从这下载。里面包含蚂蚁T9+和EBAZ4205原理图和几个EBAZ4205例程,还有一些相关的pdf文档。 link 首先从fpga学起,可惜PL没有焊晶振,只好从PS端引,下面以点灯为例。 打开vivado,工具栏file -- project -

    2024年02月15日
    浏览(47)
  • 自定义ZYNQ的PL端数据处理器,通过DMA等进行交互(附GitHub源码)

    摘要:在ZYNQ中设计了自定义的PL端数据处理器,通过DMA连接到AXI总线,完成了PS和该PL端的数据交互等功能。 开发板型号:Zynq-7000 SoC XC7Z305 FPGA 开发平台:Vivado 2019.1; Vivado SDK 2019.1 Github源码 :https://github.com/CY0807/Vivado_FIFO_Test.git (1)vivado_project存放了vivado和sdk原始工程文件

    2024年01月21日
    浏览(44)
  • FPGA----UltraScale+系列的PS侧与PL侧通过AXI-HP交互(全网唯一最详)附带AXI4协议校验IP使用方法

    1、之前写过一篇关于ZYNQ系列通用的PS侧与PL侧通过AXI-HP通道的文档,下面是链接。 FPGA----ZCU106基于axi-hp通道的pl与ps数据交互(全网唯一最详)_zcu106调试_发光的沙子的博客-CSDN博客 大家好,今天给大家带来的内容是,基于AXI4协议的采用AXI-HP通道完成PL侧数据发送至PS侧(PS侧数

    2024年02月13日
    浏览(49)
  • Zynq-Linux移植学习笔记之62- PL挂载复旦微flash

    现在为了全国产化需要,之前所有的进口flash全部要换成国产flash 其中EFM25QU256和EFM25QL256对标winbond的w25q256 nor flash     复旦微flash只支持单线模式,当使用PL侧的IP核访问时,需要设置模式为standard   内核中修改m25p80.c,设置兼容的flash型号     同时复旦微flash推荐使用jffs2文件

    2024年02月15日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包