目录
1. 简介
本文通过调整 Vitis HLS 编译器的默认设置,选择FIFO或者PIPO作为通道缓存。首先,我们来复习一下这两种缓存的区别:
想象一下,你在一家快餐店,FIFO 缓冲器就像是排队等候的顾客队伍。第一个进入队伍的顾客会是第一个得到服务的人。这个系统的好处是,顾客(生产者)一旦排队,厨师(使用者)就可以开始准备他们的食物。但如果厨师做饭的速度赶不上顾客排队的速度,或者反过来,就会造成混乱,有可能导致整个队伍停滞不前,这就是所谓的“死锁”。
现在,让我们来看看乒乓缓冲器。这就像是有两个窗口的快餐店。当一个窗口正在为顾客准备食物时,另一个窗口就在接待新的顾客。一旦一个窗口的食物准备好了,顾客就可以从那里取餐,而厨师则转到另一个窗口继续工作。这样,即使顾客来得很快,也不会有人等得太久,因为总有一个窗口是在准备食物的。
PIPO 缓冲器的工作方式类似于一个自动调节的快餐店,无论顾客来得多快,厨师都能以相同的速度做饭,确保每个人都能及时得到食物,不会有任何拥堵或延误。
在这两种情况下,无论是 FIFO 还是 PIPO,关键点都是顾客(生产者)将订单(数据块)传递给厨师(使用者)。订单可以是一个汉堡包(单个值),也可以是一整个家庭套餐(一组 N 个值)。订单越大,厨师需要的准备空间就越多。
本文内容会参照《Vitis HLS 学习笔记--控制驱动TLP-处理deadlock_vitis hls数据流处理-CSDN博客》中对于 FIFO /PIPO 类型的分类。
2. 代码详解
2.1 FIFO 通道示例
2.1.1 配置默认通道
首先配置 FIFO 缓存,可以通过如下指令进行设定:
# Create a solution
open_solution -reset solution1 -flow_target vitis
config_dataflow -default_channel fifo -fifo_depth 2
或者通过 UI 界面设置:
2.1.2 kernel 代码
#include <hls_stream.h>
#include <hls_vector.h>
extern "C" {
void diamond(hls::vector<uint32_t, 16>* vecIn, hls::vector<uint32_t, 16>* vecOut, int size) {
// The depth setting is required for pointer to array in the interface.
#pragma HLS INTERFACE m_axi port = vecIn depth = 32
#pragma HLS INTERFACE m_axi port = vecOut depth = 32
hls::stream<hls::vector<uint32_t, 16>> c0, c1, c2, c3, c4, c5;
assert(size % 16 == 0);
#pragma HLS dataflow
load(vecIn, c0, size);
compute_A(c0, c1, c2, size);
compute_B(c1, c3, size);
compute_C(c2, c4, size);
compute_D(c3, c4, c5, size);
store(c5, vecOut, size);
}
}
void load(hls::vector<uint32_t, 16>* in, hls::stream<hls::vector<uint32_t, 16>>& out, int size) {
Loop_Ld:
for (int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
out.write(in[i]);
}
}
void compute_A(hls::stream<hls::vector<uint32_t, 16>>& in, hls::stream<hls::vector<uint32_t, 16>>& out1,
hls::stream<hls::vector<uint32_t, 16>>& out2, int size) {
Loop_A:
for (int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
hls::vector<uint32_t, 16> t = in.read();
out1.write(t * 3);
out2.write(t * 3);
}
}
void compute_B(hls::stream<hls::vector<uint32_t, 16>>& in, hls::stream<hls::vector<uint32_t, 16>>& out,
int size) {
Loop_B:
for (int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
out.write(in.read() + 25);
}
}
void compute_C(hls::stream<hls::vector<uint32_t, 16>>& in, hls::stream<hls::vector<uint32_t, 16>>& out,
int size) {
Loop_C:
for (unsigned int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
out.write(in.read() * 2);
}
}
void compute_D(hls::stream<hls::vector<uint32_t, 16>>& in1, hls::stream<hls::vector<uint32_t, 16>>& in2,
hls::stream<hls::vector<uint32_t, 16>>& out, int size) {
Loop_D:
for (unsigned int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
out.write(in1.read() + in2.read());
}
}
void store(hls::stream<hls::vector<uint32_t, 16>>& in, hls::vector<uint32_t, 16>* out, int size) {
Loop_St:
for (int i = 0; i < size; i++) {
#pragma HLS performance target_ti = 32
#pragma HLS LOOP_TRIPCOUNT max = 32
out[i] = in.read();
}
}
数据流说明:
- load:读取数组 vecIn -> c0;
- compute_A:从流 c0 读取数据,计算后写入 c0 × 3 -> c1, c0 ×3 -> c2;
- compute_B:从流 c1 读取数据,计算后写入 c1 + 25 -> c3;
- compute_C:从流 c2 读取数据,计算后写入 c2 × 2 -> c4;
- compute_D:从流 c3 和 c4 读取数据,计算后写入 c3 + c4 -> c5;
- store:数组 c5 -> vecOut。
2.1.3 综合报告
查看综合后的报告:
================================================================
== HW Interfaces
================================================================
* M_AXI
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| Interface | Data Width | Address Width | Latency | Offset | Register | Max Widen | Max Read | Max Write | Num Read | Num Write |
| | (SW->HW) | | | | | Bitwidth | Burst Length | Burst Length | Outstanding | Outstanding |
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| m_axi_gmem | 512 -> 512 | 64 | 64 | slave | 0 | 512 | 16 | 16 | 16 | 16 |
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
具有较大的位宽,512bit,符合 hls::vector<uint32_t, 16> 定义,512=32*16。
* SW-to-HW Mapping
+----------+---------------+-----------+----------+------------------------------------+
| Argument | HW Interface | HW Type | HW Usage | HW Info |
+----------+---------------+-----------+----------+------------------------------------+
| vecIn | m_axi_gmem | interface | | |
| vecIn | s_axi_control | register | offset | name=vecIn_1 offset=0x10 range=32 |
| vecIn | s_axi_control | register | offset | name=vecIn_2 offset=0x14 range=32 |
| vecOut | m_axi_gmem | interface | | |
| vecOut | s_axi_control | register | offset | name=vecOut_1 offset=0x1c range=32 |
| vecOut | s_axi_control | register | offset | name=vecOut_2 offset=0x20 range=32 |
| size | s_axi_control | register | | name=size offset=0x28 range=32 |
+----------+---------------+-----------+----------+------------------------------------+
vecIn 和 vecOut 均是绑定到 m_axi_gmem 的 AXI 主接口。
2.1.4 depth = 32 解析
void diamond(vecOf16Words* vecIn, vecOf16Words* vecOut, int size) {
#pragma HLS INTERFACE m_axi port = vecIn depth = 32
#pragma HLS INTERFACE m_axi port = vecOut depth = 32
hls::stream<vecOf16Words> c0, c1, c2, c3, c4, c5;
assert(size % 16 == 0);
#pragma HLS dataflow
load(vecIn, c0, size);
compute_A(c0, c1, c2, size);
compute_B(c1, c3, size);
compute_C(c2, c4, size);
compute_D(c3, c4, c5, size);
store(c5, vecOut, size);
}
#pragma HLS INTERFACE m_axi port = vecIn depth = 32 指令用于设置接口属性。这里的 depth=32 是一个指令选项,它指定了AXI接口的深度,也就是FPGA与外部内存交互时可以访问的数据量。这个数值应该与设计打算在单次事务中处理的数据量相匹配。由于 TB 中的 std::vector 容器 test 和 outcome 的大小都被初始化为32,这表明设计打算在一个事务中处理32个向量,因此 depth 被设置为32。
这个深度值对于优化数据传输非常重要,因为它影响了接口生成的FIFO缓冲区的大小,以及FPGA与外部内存之间的突发传输能力。正确设置深度可以帮助提高性能,减少延迟,并确保数据的连续流动。
2.1.5 FIFO 通道分类
FIFO 有三种类型:
- Streams (including hls::streams and streamed arrays),用户创建。
- Scalar propagation FIFOs,工具推断。
- Streams of blocks,用户创建。
对照 Dataflow 报告,本 kernel 的 FIFO 通道有两种类型 Stream 和 ScalarProp,从变量的名字也可以看出,c0、c1、c2、c3、c4、c5是由用户创建的,而其余的变量则由 HLS 工具推断生成。
2.2 PIPO
2.2.1 配置默认通道
首先配置 PIPO 缓存,可以通过如下指令进行设定:
# Create a solution
open_solution -reset solution1 -flow_target vitis
或者通过 UI 界面设置:
2.2.2 kernel 代码
void diamond(unsigned char vecIn[100], unsigned char vecOut[100]) {
unsigned char c1[100], c2[100], c3[100], c4[100];
#pragma HLS dataflow
funcA(vecIn, c1, c2);
funcB(c1, c3);
funcC(c2, c4);
funcD(c3, c4, vecOut);
}
void funcA(unsigned char* in, unsigned char* out1, unsigned char* out2) {
Loop0:
for (int i = 0; i < 100; i++) {
#pragma HLS pipeline rewind
#pragma HLS unroll factor = 2
unsigned char t = in[i] * 3;
out1[i] = t;
out2[i] = t;
}
}
void funcB(unsigned char* in, unsigned char* out) {
Loop0:
for (int i = 0; i < 100; i++) {
#pragma HLS pipeline rewind
#pragma HLS unroll factor = 2
out[i] = in[i] + 25;
}
}
void funcC(unsigned char* in, unsigned char* out) {
Loop0:
for (unsigned char i = 0; i < 100; i++) {
#pragma HLS pipeline rewind
#pragma HLS unroll factor = 2
out[i] = in[i] * 2;
}
}
void funcD(unsigned char* in1, unsigned char* in2, unsigned char* out) {
Loop0:
for (int i = 0; i < 100; i++) {
#pragma HLS pipeline rewind
#pragma HLS unroll factor = 2
out[i] = in1[i] + in2[i] * 2;
}
}
数据流说明:
- compute_A:计算后写入 vecIn × 3 -> c1, vecIn ×3 -> c2;
- compute_B:计算后写入 c1 + 25 -> c3;
- compute_C:计算后写入 c2 × 2 -> c4;
- compute_D:计算后写入 c3 + c4 × 2 -> vecOut.
2.2.3 综合报告
================================================================
== HW Interfaces
================================================================
* M_AXI
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| Interface | Data Width | Address Width | Latency | Offset | Register | Max Widen | Max Read | Max Write | Num Read | Num Write |
| | (SW->HW) | | | | | Bitwidth | Burst Length | Burst Length | Outstanding | Outstanding |
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
| m_axi_gmem | 512 -> 512 | 64 | 64 | slave | 0 | 512 | 16 | 16 | 16 | 16 |
+------------+------------+---------------+---------+--------+----------+-----------+--------------+--------------+-------------+-------------+
* SW-to-HW Mapping
+----------+---------------+-----------+----------+------------------------------------+
| Argument | HW Interface | HW Type | HW Usage | HW Info |
+----------+---------------+-----------+----------+------------------------------------+
| vecIn | m_axi_gmem | interface | | |
| vecIn | s_axi_control | register | offset | name=vecIn_1 offset=0x10 range=32 |
| vecIn | s_axi_control | register | offset | name=vecIn_2 offset=0x14 range=32 |
| vecOut | m_axi_gmem | interface | | |
| vecOut | s_axi_control | register | offset | name=vecOut_1 offset=0x1c range=32 |
| vecOut | s_axi_control | register | offset | name=vecOut_2 offset=0x20 range=32 |
| size | s_axi_control | register | | name=size offset=0x28 range=32 |
+----------+---------------+-----------+----------+------------------------------------+
报告中 HW Interface 与 Mapping 具有相同的内容,这里不再赘述。
2.2.4 PIPO 通道分类
PIPO 有三种类型:
- PIPO,用户创建。
- Task Level FIFOs (TLF),工具推断。
- Input and output ports to the upper level,用户创建。
对照 Dataflow 报告,本 kernel 的 PIPO 通道类型只有一种:PIPO,c1、c2、c3、c4均由用户创建。变量 vecOut_c 则由 HLS 工具推断生成为 FIFO 类型。
3. 综合对比
3.1 数据类型和接口
第一段代码:
- 使用 hls::vector<uint32_t, 16> 类型来表示向量。
- 使用 hls::stream 数据结构来处理数据流。
- 显式指定 AXI 主接口。
第二段代码:
- 使用基本的 unsigned char 类型的数组,每个数组包含 100 个元素。
- 没有使用 hls::stream,而是使用普通的数组传递数据。
- 输入输出接口使用普通的 C-style 指针和数组,没有显式的接口设置。
3.2 数据流和数据流控制
第一段代码:
- 使用 HLS dataflow pragma (#pragma HLS dataflow) 来启用数据流优化
- 显式指定使用 hls::stream 作为函数间的通信方式,函数之间的数据通过流进行传递。
第二段代码:
- 同样使用 HLS dataflow pragma (#pragma HLS dataflow) 来启用数据流优化,但数据传递是通过普通的数组来进行。
- 函数间的数据通过数组传递,没有使用 hls::stream。
3.3 计算函数和计算方式
第一段代码:
- 函数更为通用,接受 hls::vector 类型的数据,并且处理过程中使用了 HLS 的各种特性如 pipeline 和 unroll。
- 每个函数处理 hls::vector<uint32_t, 16> 类型的数据,并将结果写入另一个 hls::vector。
第二段代码:
- 函数接受基本类型的指针和数组作为输入输出,处理过程中使用 pipeline 和 unroll 进行优化。
- 每个函数处理 unsigned char 类型的数据,并将结果写入另一个 unsigned char 数组。
4. 总结
本文对比了在 Vitis HLS 编译器中配置 FIFO 和 PIPO 缓冲器的方法和效果。通过两段代码示例,我们展示了使用这两种通道缓存的不同实现方式。
在 FIFO 示例中,代码使用 hls::vector<uint32_t, 16> 和 hls::stream 数据结构,通过 AXI 主接口进行数据传输,并用 #pragma HLS dataflow 启用数据流优化,每个函数通过流进行通信。这种方式更通用,但需要显式指定流和接口设置。
在 PIPO 示例中,代码采用 unsigned char 数组进行数据传递,使用 C-style 指针和数组,没有显式接口设置,通过数组传递数据,同样使用 #pragma HLS dataflow 启用数据流优化。这种方式更简单直接,但缺乏流的灵活性。
PIPO 缓冲器可以自动调节,确保任务之间的重叠执行,而不会出现死锁。而显式手动串流的 FIFO 通道虽然可以更快地开始重叠执行,但需要小心调整队列大小,以避免死锁问题。
综合来看,FIFO 适用于复杂的通用数据处理场景,而 PIPO 更适合简单的数据传递需求。正确选择和配置这两种缓冲器,可以优化系统性能,减少延迟。本文帮助读者理解 FIFO 和 PIPO 的适用场景和配置方法,从而在设计中做出最佳选择。