目录
1. 简介
2. 示例代分析
2.1 源代码
2.2 URAM 不可用
2.3 代码功能解释
2.4 综合报告
2.4.1 顶层控制接口
2.4.2 软件 IO 信息
2.4.3 存储绑定
3. 对比两种 solution
3.1 solution_A
3.2 solution_B
4. 总结
1. 简介
在C++程序中,数组是一种基本的数据结构,用来存储一系列的数据。程序员可以动态地分配和释放数组的内存空间。
但是在将程序综合到硬件上时,就不能再动态地分配内存了,因为硬件需要提前知道需要多少内存来存储数组的数据。在FPGA上,有一个本地存储器,相比于全局存储器(比如DDR或HBM存储器),本地存储器的访问速度更快,通常只需要一个或多个周期。
本文例子展示了如何将全局数组映射到具有不同实现的RAM,并展示了它们如何初始化以及如何重置。
2. 示例代分析
2.1 源代码
#include <ap_int.h>ap_int<10> A[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
ap_int<10> B[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
ap_int<10> C[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};int example(int i) {
#pragma HLS BIND_STORAGE variable = A type = RAM_2P impl = BRAM
#pragma HLS BIND_STORAGE variable = B type = RAM_2P impl = LUTRAMA[i] += B[i] + C[i];B[i] += 5;C[i] += 10;int result = (A[i] + B[i] + C[i]).to_int();return result;
}
2.2 URAM 不可用
// #pragma HLS BIND_STORAGE variable=C type=RAM_2P impl=URAM
该指令不可用!
有用户提出了类似的问题,发现即使使用了 #pragma HLS BIND_STORAGE 指令指定将数组存储到 URAM 中,综合报告显示每个 URAM 单元只存储了一个数组元素,而没有充分利用 URAM 的容量。解决方法是在代码中手动对数据进行打包,将多个较小位宽的数打包成一个更大位宽的数后存入 URAM。
这表明,虽然 URAM 可以通过手动优化来使用,但 Vitis HLS 可能没有直接支持全局数组使用 URAM 的内置机制。
2.3 代码功能解释
首先定义三个数组:A、B 和 C,每个数组都包含了 10 个 ap_int<10> 类型的元素。这些数组在 HLS 中被绑定到不同的存储实现类型上。
#pragma HLS BIND_STORAGE 是用于指定存储实现类型的指令。在这里,我们有两个绑定:
- A 使用 BRAM(Block RAM)作为存储实现类型。
- B 使用 LUTRAM(Look-Up Table RAM)作为存储实现类型。
然后,对这些数组进行了一系列操作:
- A[i] += B[i] + C[i]:将 B[i] 和 C[i] 的值相加,并将结果加到 A[i] 上。
- B[i] += 5:将 B[i] 的值增加 5。
- C[i] += 10:将 C[i] 的值增加 10。
- 最后,计算了 A[i] + B[i] + C[i] 的和,并将其转换为整数类型,然后返回该结果。
2.4 综合报告
2.4.1 顶层控制接口
包含:块级控制协议、时钟、复位、中断;
* TOP LEVEL CONTROL
+-----------+---------------+-----------+
| Interface | Type | Ports |
+-----------+---------------+-----------+
| ap_clk | clock | ap_clk |
| ap_rst_n | reset | ap_rst_n |
| interrupt | interrupt | interrupt |
| ap_ctrl | ap_ctrl_chain | |
+-----------+---------------+-----------+
2.4.2 软件 IO 信息
================================================================
== SW I/O Information
================================================================
* Top Function Arguments
+----------+-----------+----------+
| Argument | Direction | Datatype |
+----------+-----------+----------+
| i | in | int |
| return | out | int |
+----------+-----------+----------+* SW-to-HW Mapping
+----------+---------------+----------+-------------------------------------+
| Argument | HW Interface | HW Type | HW Info |
+----------+---------------+----------+-------------------------------------+
| i | s_axi_control | register | name=i offset=0x18 range=32 |
| return | s_axi_control | register | name=ap_return offset=0x10 range=32 |
+----------+---------------+----------+-------------------------------------+
2.4.3 存储绑定
变量A、B、C和编译指令期望的输出是一致的:
================================================================
== Bind Storage Report
================================================================
+-----------+------+------+--------+----------+---------+--------+---------+
| Name | BRAM | URAM | Pragma | Variable | Storage | Impl | Latency |
+-----------+------+------+--------+----------+---------+--------+---------+
| + example | 1 | 0 | | | | | |
| B_V_U | - | - | pragma | B_V | ram_2p | lutram | 1 |
| C_V_U | - | - | | C_V | ram_1p | auto | 1 |
| A_V_U | 1 | - | pragma | A_V | ram_2p | bram | 1 |
+-----------+------+------+--------+----------+---------+--------+---------+
3. 对比两种 solution
3.1 solution_A
对 kernel 代码不做复位相关编译指令,进行综合后,查看图示文件:
在 HLS 设计中,为了初始化全局 RAM 数组(不包括URAM),会生成一种特定的结构,这种结构专门用于配置 BRAM/LUTRAM:
---
变量A
---
(* ram_style = "block" *)reg [DataWidth-1:0] ram[0:AddressRange-1];initial begin$readmemh("./example_A_V_RAM_2P_BRAM_1R1W.dat", ram);
end---
变量B
---
(* ram_style = "distributed" *)reg [DataWidth-1:0] ram[0:AddressRange-1];initial begin$readmemh("./example_B_V_RAM_2P_LUTRAM_1R1W.dat", ram);
end
*.dat文件包含相应数组的初始值。
3.2 solution_B
对 kernel 代码添加优化指令:
#pragma HLS reset variable=A
#pragma HLS reset variable=B
#pragma HLS reset variable=C
进行综合后,可以看到当对 BRAM/LUTRAM 使用复位指令生成的代码结构:
解释:
当在静态数组/全局数组(这里的A、B、C)上使用复位指令后,生成的RTL代码中,每个数组都会通过ROM和RAM来实现。数组的初始值只会被加载到ROM里面,这一点和solution_A是一样的。
但是,每当复位信号被激活,从数组读取的数据就默认会来自ROM。当然,如果有新数据写入了某个地址,该地址将被记录,滞后读取的数据就会转到RAM中。这样做的结果是,每次重置之后,数组都会回到它的初始状态,就像重新启动一样。
三个数组 A/B/C 的相同结构如下所示:
module example_A_V_RAM_2P_BRAM_1R1W
#(parameterDataWidth = 10,AddressWidth = 4,AddressRange = 10
)(input wire clk,input wire reset,input wire [AddressWidth-1:0] address0,input wire ce0,output wire [DataWidth-1:0] q0,input wire [AddressWidth-1:0] address1,input wire ce1,input wire we1,input wire [DataWidth-1:0] d1
);
//------------------------Local signal-------------------
reg [AddressRange-1:0] written = {AddressRange{1'b0}};
wire [DataWidth-1:0] q0_ram;
wire [DataWidth-1:0] q0_rom;
wire q0_sel;
reg [0:0] sel0_sr;
//------------------------Instantiation------------------
example_A_V_RAM_2P_BRAM_1R1W_ram #(.DataWidth(DataWidth),.AddressWidth(AddressWidth),.AddressRange(AddressRange))
example_A_V_RAM_2P_BRAM_1R1W_ram_u(.clk ( clk ),.reset ( reset ),.ce0 ( ce0 ),.address0 ( address0 ),.q0 ( q0_ram ),.ce1 ( ce1 ),.address1 ( address1 ),.we1 ( we1 ),.d1 ( d1 )
);example_A_V_RAM_2P_BRAM_1R1W_rom #(.DataWidth(DataWidth),.AddressWidth(AddressWidth),.AddressRange(AddressRange))
example_A_V_RAM_2P_BRAM_1R1W_rom_u(.clk ( clk ),.ce0 ( ce0 ),.address0 ( address0 ),.q0 ( q0_rom )
);
//------------------------Body---------------------------
assign q0 = q0_sel? q0_ram : q0_rom;
assign q0_sel = sel0_sr[0];always @(posedge clk) beginif (reset)written <= 1'b0;else beginif (ce1 & we1) beginwritten[address1] <= 1'b1;endend
endalways @(posedge clk) beginif (ce0) beginsel0_sr[0] <= written[address0];end
endendmodule
语句说明:
assign q0 = q0_sel? q0_ram : q0_rom;
用于选择输出数据来源于 RAM 或者 ROM。而 q0 来自于 written[address0]。
reg [AddressRange-1:0] written = {AddressRange{1'b0}};
用于记录某个地址是否被写入过,写入过的地址会被标记,下次读取时,就会从 RAM 中读取。
4. 总结
在本文中,我们探讨了如何在Vitis HLS中处理FPGA的全局数组映射和初始化问题。通过示例代码,我们了解了如何将C++数组映射到不同类型的RAM,并使用#pragma HLS BIND_STORAGE指令来指定存储实现。我们还讨论了URAM的使用限制和手动数据打包的解决方案。最后,我们比较了两种解决方案:一种是不使用复位指令的solution_A,另一种是使用复位指令的solution_B。solution_B通过ROM和RAM的结合,提供了一种在复位信号激活时能够将数组恢复到初始状态的方法。这些知识对于理解和优化FPGA设计中的内存管理至关重要。