14 基于BRAM的PS和PL数据交互实验

14.1 实验任务

本节的实验任务是PS将串口接收到的数据写入BRAM,然后从BRAM中读出数据,并通过串口打印出来;与此同时,PL从BRAM中同样读出数据,并通过ILA来观察读出的数据与串口打印的数据是否一致。

14.2 搭建Vivado工程(上半部)

首先,创建最小系统:

下一步,在最小系统基础上打开PS与PL之间的配置接口——GP接口

接着,打开PL侧的复位:

设置PL侧的时钟:

完成配置后,添加BRAM控制器:

对BRAM控制器进行配置:

接下来,添加BRAM的IP核:

对BRAM进行配置:

再接着,应该要添加我们自己编写的IP核实现PL侧从BRAM中同样读出数据,但是暂时先不添加了,先把通过官方的IP核读写BRAM的功能调试通过后,再来添加我们自己的IP核。

下面,先来进行连线:

如下图,自动连线完成后的框图:

如果想修改BRAM的存储深度(存储空间大小),则可以在下面的页面操作:

到此,基础Vivado工程构建完成,可导出硬件了。

14.3 搭建Vitis工程(上半部)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//----------------------------------------------------------------------------------------
// File name: main
// Last modified Date: 2023/5/12 17:25:36
// Last Version: V1.0
// Descriptions: PS读写BRAM
//----------------------------------------------------------------------------------------

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

#define START_ADDR 0 //BRAM起始地址 范围:0~1023
#define BRAM_DATA_BYTE 4 //BRAM数据位宽(32bit=4Byte)

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个字符
for(i=BRAM_DATA_BYTE*START_ADDR; i < BRAM_DATA_BYTE*(START_ADDR + ch_data_len);
i+=BRAM_DATA_BYTE)
{
// 调用库函数XBram_WriteReg
// 注1:XPAR_BRAM_0_BASEADDR:BRAM控制器的基地址,其实就是在Vivado构建中的0x4000_0000
// 注2:一次性写入4个字节,不是一个字节一个字节地写入
XBram_WriteReg(XPAR_BRAM_0_BASEADDR, i, ch_data[wr_cnt]) ;
wr_cnt++;
}
//设置BRAM写入的字符串长度
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_LEN, BRAM_DATA_BYTE*ch_data_len) ;
//设置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_START, 1) ;
//拉低BRAM开始信号
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START, 0) ;
}

//从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/4, read_data) ;
}
}

14.4 搭建Vivado工程(下半部)

为了实现实验任务的“与此同时,PL从BRAM中同样读出数据,并通过ILA来观察读出的数据与串口打印的数据是否一致”这部分功能,我们需要自定义IP核,下面来看一下步骤。

创建好之后,在左边IP库里面有刚才所创建的IP核,右击它选择edit对它进行编辑:

下面的界面就是创建的新IP核,它里面包含两个模块的代码(是官方写好的),它里面实现了AXI4-Light接口的协议,可以直接基于这个代码来添加自己的代码就可以了。

下面新建一个模块,这个模块实现的功能就是读BRAM端口的数据:

之后一路点击OK、Yes完成新文件的创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
module bram_rd(
input clk, //时钟信号
input rst_n, //复位信号

input start_rd, //读开始信号
input [31:0] start_addr, //读起始地址16
input [31:0] rd_len, //读长度,以字节为单位16
//RAM端口
output ram_clk, //RAM时钟
output ram_rst, //复位信号,高电平有效
output reg ram_en, //RAM使能信号,高电平有效
output [3:0] ram_we, //写使能信号,1:写 0:读
output reg [31:0] ram_addr, //读写地址
output [31:0] ram_we_data, //RAM写数据
input [31:0] ram_rd_data //RAM读数据
);

reg start_rd_d0;
reg start_rd_d1;
reg rd_flag; //读RAM的标志信号 0:没有读 1:正在读

wire pos_start_rd;

assign pos_start_rd = ~start_rd_d1 & start_rd_d0;

assign ram_clk = clk;
assign ram_rst = 1'b0;
assign ram_we = 4'b0; //固定读,不写
assign ram_we_data = 32'b0;

//对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
rd_flag <= 1'b0;
ram_en <= 1'b0;
ram_addr <= 32'b0;
end
else begin
if(rd_flag == 1'b0) begin
if(pos_start_rd) begin //判断读开始信号的上升沿
ram_en <= 1'b1;
ram_addr <= start_addr;
rd_flag <= 1'b1;
end
end
else begin
if(ram_addr == rd_len + start_addr - 32'd4) begin //读完成
ram_en <= 1'b0;
ram_addr <= 32'b0;
rd_flag <= 1'b0;
end
else
ram_addr <= ram_addr + 32'd4; //地址以字节为单位,每次累加4
end
end
end

endmodule

模块编写完成后,还需要例化:

例化之后,自定义的模块的输出信号要从当前例化的模块输出,因此要添加输出信号:

继续,上面还有一层,把输出端口继续加到上一层:

完成之后,就可以打包IP:

可以把相类似的功能的端口封装成一个接口,这样就不用一根线一根线地连接了。

首先,移除软件自动识别的端口设置:

移除后,添加总线接口:

可以继续修改参数值(这里暂未修改):

完成后,如下图所示:

最好,完成IP核:

返回主工程,把上面这个IP核添加到工程:

下一步,为了实现实验要求的ILA,现在需要添加ILA:

说明:直接右击然后debug就可以使用ILA进行观察了。这种方式虽然说简单,但它有个缺点——没有办法观察IP核内部的信号,它只能观察它所引出的端口。

所以,观察内部信号还有一种方案:

此外,还有一种方案,在之前的FPGA学习部分提到过:就是在自定义的IP核里面的代码添加mark_debug属性:(* mark_debug = "true" *)。

举例(注意,添加之后,还是要做上面截图中的setup debug中添加信号到ILA):

完成后,就可以Generate Output Products了:

最后,生成比特流文件,导出到硬件,Vivado部分就完成了。

补充:给工程添加自定义IP核路径的方法:

14.5 搭建Vitis工程(下半部)

由于我们更新了硬件,所以Vitis中也要同步更新:

接着更新板级支持包:

下面开始编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//----------------------------------------------------------------------------------------
// File name: main
// Last modified Date: 2023/5/12 17:25:36
// Last Version: V1.0
// Descriptions: PS读写BRAM
//----------------------------------------------------------------------------------------

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

// 添加自定义IP核的官方生成的头文件
#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 START_ADDR 0 //BRAM起始地址 范围:0~1023
#define BRAM_DATA_BYTE 4 //BRAM数据位宽(32bit=4Byte)

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

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

//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中
pl_bram_rd_cfg(); //配置PL BRAM RD IP核
str_rd_bram(); //从BRAM中读出数据
}
}

//将字符串写入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
// 注1:XPAR_BRAM_0_BASEADDR:BRAM控制器的基地址,其实就是在Vivado构建中的0x4000_0000
// 注2:一次性写入4个字节,不是一个字节一个字节地写入
XBram_WriteReg(XPAR_BRAM_0_BASEADDR, i, ch_data[wr_cnt]) ;
wr_cnt++;
}
//设置BRAM写入的字符串长度
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_LEN, BRAM_DATA_BYTE*ch_data_len) ;
//设置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_START, 1) ;
//拉低BRAM开始信号
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START, 0) ;
}

void plbram_rd_cfg()
{
//配置读起始地址
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE,PL_BRAM_START_ADDR,START_ADDR * BRAM_DATA_BYTE);
//配置读长度
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE,PL_BRAM_LEN,ch_data_len * BRAM_DATA_BYTE);
//开始读
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,1); // 高电平
PL_BRAM_RD_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0); // 低电平
}


//从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/BRAM_DATA_BYTE, read_data) ;
}
}

14.6 观察实验现象

主要是如何使用ILA观察:

启动调试后:

最后再来补充一下,当我们添加了ILA之后,肯定会影响到对整个资源的消耗,所以说当不需要再使用ILA之后就可以把ILA给它删除掉。

首先,使用可视化右击要观察的信号线添加的Debug,只需要再次右击选择Clear Debug就可以了。

其次,若还有使用(* mark_debug = "true" *)添加的内部信号的ILA,则需要把源代码中的(* mark_debug = "true" *)删除掉,同时还得把对应的.xdc文件删除,这样就行了。

14.7 实验总结

14.8 扩展实验

首先上面的pl_bram_rdIP核是从BRAM里面读出数据,下面需要实现的功能是BRAM里面写入数据,所以说需要再自定义一个IP核,名字就叫pl_bram_wr。创建过程和前面一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
module bram_rd(
input clk, //时钟信号
input rst_n, //复位信号

input start_wr, //写开始信号
input [31:0] start_addr, //写起始地址
input [31:0] wr_len, //写长度,以字节为单位
//RAM端口
output ram_clk, //RAM时钟
output ram_rst, //复位信号,高电平有效
output reg ram_en, //RAM使能信号,高电平有效
output [3:0] ram_we, //写使能信号,1:写 0:读
output reg [31:0] ram_addr, //读写地址
output [31:0] ram_we_data, //RAM写数据
input [31:0] ram_rd_data //RAM读数据
);

reg start_wr_d0;
reg start_wr_d1;
reg wr_flag; //写RAM的标志信号 0:没有写 1:正在写

wire pos_start_wr;

assign pos_start_wr = ~start_wr_d1 & start_wr_d0;

assign ram_clk = clk;
assign ram_rst = 1'b0;
assign ram_we = 4'b1111; // 固定写
assign ram_we_data = ram_addr; // 写入的数据正好等于地址(无意义,至少为了测试写入功能)

//对start_wr打两拍,采上升沿
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
start_wr_d0 <= 1'b0;
start_wr_d1 <= 1'b0;
end
else begin
start_wr_d0 <= start_wr;
start_wr_d1 <= start_wr_d0;
end
end

//根据写开始信号,向BRAM中写入数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_flag <= 1'b0;
ram_en <= 1'b0;
ram_addr <= 32'b0;
end
else begin
if(wr_flag == 1'b0) begin
if(pos_start_wr) begin //判断写开始信号的上升沿
ram_en <= 1'b1;
ram_addr <= start_addr;
wr_flag <= 1'b1;
end
end
else begin
if(ram_addr == wr_len + start_addr - 32'd4) begin
ram_en <= 1'b0;
ram_addr <= 32'b0;
wr_flag <= 1'b0;
end
else
ram_addr <= ram_addr + 32'd4; //地址以字节为单位,每次累加4
end
end
end

endmodule

之后,再对上层两个模块中的例化部分进行修改(这里就不展示了)。

剩下的构建工程过程和上面的实验一样。

接着,修改PS侧的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//----------------------------------------------------------------------------------------
// File name: main
// Last modified Date: 2023/5/12 17:25:36
// Last Version: V1.0
// Descriptions: PS读写BRAM
//----------------------------------------------------------------------------------------

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

// 添加自定义IP核的官方生成的头文件
#include "pl_bram_wr.h"

#define PL_BRAM_BASE XPAR_PL_BRAM_WR_0_S00_AXI_BASEADDR//PL BRAM WR IP核的基地址
#define PL_BRAM_START PL_BRAM_WR_S00_AXI_SLV_REG0_OFFSET//PL BRAM WR IP核写开始的寄存器
#define PL_BRAM_START_ADDR PL_BRAM_WR_S00_AXI_SLV_REG1_OFFSET //PL BRAM WR IP核写起始地址的寄存器
#define PL_BRAM_LEN PL_BRAM_WR_S00_AXI_SLV_REG2_OFFSET //PL BRAM WR IP核写长度的寄存器

#define START_ADDR 0 //BRAM起始地址 范围:0~1023
#define BRAM_DATA_BYTE 4 //BRAM数据位宽(32bit=4Byte)

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

int data_len;

int main()
{
while(1)
{
printf("Please enter the number of data to write BRAM \r\n");
scanf("%d", &data_len); //接收用户输入的写BRAM的数据个数
if(data_len > 1024)
continue;

str_rd_bram(); //从BRAM中读出数据
}
}

void p1_bram_wr_cfg()
{
//配置写起始地址
PL_BRAM_WR_mWriteReg(PL_BRAM_BASE,PL_BRAM_START_ADDR,START_ADDR *BRAM_DATA_BYTE);
//配置写长度
PL_BRAM_WR_mWriteReg(PL_BRAM_BASE,PL_BRAM_LEN,data_len * BRAM_DATA_BYTE);
//开始写
PL_BRAM_WR_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,1);
PL_BRAM_WR_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0);
}


//从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 + data_len);
i+=BRAM_DATA_BYTE)
{
read_data = XBram_ReadReg(XPAR_BRAM_0_BASEADDR, i) ;
printf("BRAM address is %d\t,Read data is %d\n", i/BRAM_DATA_BYTE, read_data) ;
}
}

15 AXI接口

15.1 AXI简介

15.1.1 为什么学习AXI

为什么要学习AXI呢,直接和上面使用AXI-Light一样会如何使用不就行了,反正官方会自动生成相应的协议程序,原因主要有以下几点:

  1. 使用官方IP核时,遇到问题,无法快速定位问题和解决问题
  2. 官方IP核无法满足项目需求或者消耗的资源过大时,需要自己编写AXI接口协议的代码
  3. 纯FPGA的开发,也需要熟悉AXI协议,如PCIe开发使用的AXI XDMA、PL侧DDR3、DDR4 IP核等

案例:基于DDR4的PCIe测试(纯FPGA)

左边是一个电脑主机,通过PCIE接口跟板卡进行连接,右边是一个XDMA IP核,它将PCIE复杂的协议转换成了一个AXI的接口,然后通过AXI SmartConnect IP这个互联接口(后面会讲)跟DDR4 MIG IP核进行连接(这个是PL侧的一个DDR4 IP核)。

然后下面还有一个AXI-LITE接口,同样接入AXI Interconnect IP核和一个AXI GPIO IP核连接,这样的话XDMA就可以控制LED灯或者是读取按键。

最下面是一个参考时钟,Utility Buffer IP核一个时钟缓冲,给XDMA提供时钟。

15.1.2 什么是AXI

AXI:Advanced Extensible Interface(高级可扩展总线)是ARM公司推出的高性能片上总线协议,属于AMBA规范的一部分,广泛应用于SoC(片上系统)和FPGA设计中,以满足高速、低延迟的数据传输需求。

AMBA:Advanced Microcontroller Bus Architecture (高级微控制器总线架构)是ARM公司推出的片上系统互联标准,旨在为系统级集成电路(SoC)设计提供高性能、可扩展和可重用的接口和总线结构。

  • AXI4三种接口类型
    • AXI4:高性能接口,适用于高性能存储器映射的需求,支持单次最高突发传输256个数据
    • AXI4-Lite:轻量级的AXI4接口,用于存储器器映射的单次数据通信,其优点是占用的PL资源较少
    • AXI4-Stream:传输时不指定地址,单次突发传输的数据个数没有限制,适用于高速流数据传输,如视频流、图像处理等

存储器映射:主机在对从机进行读写操作时,指定一个目标地址,这个地址对应系统存储空间的地址,表示对该空间进行读写操作。

突发传输:单个传输事务中连续传输多个数据,传输时仅需发送起始地址,后续地址由硬件自动推断或递增。