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 #include "xil_printf.h" #include "stdio.h" #include "xbram.h" #define START_ADDR 0 #define BRAM_DATA_BYTE 4 char ch_data[1024 ]; int ch_data_len; void str_wr_bram () ;void str_rd_bram () ;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(); str_rd_bram(); } } void str_wr_bram () { int i=0 , wr_cnt = 0 ; 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++; } PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_LEN, BRAM_DATA_BYTE*ch_data_len) ; PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START_ADDR, BRAM_DATA_BYTE*START_ADDR) ; PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START, 1 ) ; PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START, 0 ) ; } void str_rd_bram () { int read_data=0 ,i=0 ; 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, input [31 :0 ] rd_len, output ram_clk, output ram_rst, output reg ram_en, output [3 :0 ] ram_we, output reg [31 :0 ] ram_addr, output [31 :0 ] ram_we_data, input [31 :0 ] ram_rd_data ); reg start_rd_d0;reg start_rd_d1;reg rd_flag; 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 ; 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 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 ; 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 #include "xil_printf.h" #include "stdio.h" #include "xbram.h" #include "pl_bram_rd.h" #define PL_BRAM_BASE XPAR_PL_BRAM_RD_0_S00_AXI_BASEADDR #define PL_BRAM_START PL_BRAM_RD_S00_AXI_SLV_REG0_OFFSET #define PL_BRAM_START_ADDR PL_BRAM_RD_S00_AXI_SLV_REG1_OFFSET #define PL_BRAM_LEN PL_BRAM_RD_S00_AXI_SLV_REG2_OFFSET #define START_ADDR 0 #define BRAM_DATA_BYTE 4 char ch_data[1024 ]; int ch_data_len; void str_wr_bram () ;void str_rd_bram () ;void plbram_rd_cfg () ;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(); pl_bram_rd_cfg(); str_rd_bram(); } } void str_wr_bram () { int i=0 , wr_cnt = 0 ; 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++; } PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_LEN, BRAM_DATA_BYTE*ch_data_len) ; PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START_ADDR, BRAM_DATA_BYTE*START_ADDR) ; PL_BRAM_RD_mWriteReg(PL_BRAM_BASE, PL_BRAM_START, 1 ) ; 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 ); } void str_rd_bram () { int read_data=0 ,i=0 ; 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, output ram_clk, output ram_rst, output reg ram_en, output [3 :0 ] ram_we, output reg [31 :0 ] ram_addr, output [31 :0 ] ram_we_data, input [31 :0 ] ram_rd_data ); reg start_wr_d0;reg start_wr_d1;reg wr_flag; 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; 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 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 ; 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 #include "xil_printf.h" #include "stdio.h" #include "xbram.h" #include "pl_bram_wr.h" #define PL_BRAM_BASE XPAR_PL_BRAM_WR_0_S00_AXI_BASEADDR #define PL_BRAM_START PL_BRAM_WR_S00_AXI_SLV_REG0_OFFSET #define PL_BRAM_START_ADDR PL_BRAM_WR_S00_AXI_SLV_REG1_OFFSET #define PL_BRAM_LEN PL_BRAM_WR_S00_AXI_SLV_REG2_OFFSET #define START_ADDR 0 #define BRAM_DATA_BYTE 4 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); if (data_len > 1024 ) continue ; str_rd_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 ); } void str_rd_bram () { int read_data=0 ,i=0 ; 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一样会如何使用不就行了,反正官方会自动生成相应的协议程序,原因主要有以下几点:
使用官方IP核时,遇到问题,无法快速定位问题和解决问题
官方IP核无法满足项目需求或者消耗的资源过大时,需要自己编写AXI接口协议的代码
纯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:传输时不指定地址,单次突发传输的数据个数没有限制,适用于高速流数据传输,如视频流、图像处理等
存储器映射 :主机在对从机进行读写操作时,指定一个目标地址,这个地址对应系统存储空间的地址,表示对该空间进行读写操作。
突发传输 :单个传输事务中连续传输多个数据,传输时仅需发送起始地址,后续地址由硬件自动推断或递增。