目录
1. 简介
2. 基本指针
3. 算术指针
4. 疑点解答
4.1 疑点1
4.2 疑点2
5. 总结
1. 简介
在 C/C++ 语言中,指针被广泛用来表示内存中的地址信息,它们是理解和使用这些语言的核心概念之一。然而,在 Vitis HLS 中,指针的使用有着显著的差异。尽管 Vitis HLS 支持指针的综合,但在某些情况下,使用指针可能会导致综合后的 RTL 接口与设计时的预期不符,尤其是在以下情况下:
- 在同一函数内多次访问(读取或写入)指针时。
- 使用指针数组时,每个指针都必须指向一个标量或标量数组(而不是另一个指针)。
- 仅支持标准 C/C++ 语言类型间的指针强制转换。
虽然 Vitis HLS 支持指针的使用,但通常建议避免在代码中使用指针,尤其是在需要高效硬件实现的情况下。
在顶层函数的接口上使用指针时,应该特别注意指针的实现方式,以确保综合结果符合设计要求。在某些情况下,可能需要通过修改代码或选择不同的接口类型来解决由指针使用引起的问题。
2. 基本指针
基本指针,指的是直接指向数据的指针,而不需要复杂的指针操作,如指针数组或指针算术。
在 Vitis HLS 中,如果一个函数的顶层接口使用了基本指针,这通常不会对硬件综合产生负面影响。
用一个简单的比喻来理解这个概念:
你有一栋大楼(这里指的是你的硬件设计),而指针就像是大楼里的电梯。基本指针就像是一个直达特定楼层的电梯,它可以直接带你到达你想去的地方(即直接访问数据)。在 Vitis HLS 中,这样的电梯(基本指针)是可以被很好地管理和综合,因为它们的行为很直接,很容易预测。
现在,有两种方式可以让这些电梯(指针)工作:
- 有线接口:这就像是一个普通的电梯,它在你按下按钮时就开始工作。在硬件中,有线接口意味着数据可以直接从一个地方传输到另一个地方,没有复杂的控制逻辑。
- 握手接口协议:这更像是一个智能电梯,它会在确认你准备好了之后才开始运行。在硬件中,握手接口会在收到一个特定的信号(ready)后才开始数据传输。这种方式允许更精细的控制数据流,但也更复杂一些。
只要你的指针像一个直达电梯那样简单直接,Vitis HLS 就能够很好地处理它,无论是通过有线接口还是握手接口协议。这样的指针不会给硬件综合带来问题,因为它们的行为很容易在硬件中实现。
void example(int *d) {
static int acc = 0;
acc += *d;
*d = acc;
}
测试代码:
#include <stdio.h>
extern void example(int *d);
int main() {
int d;
int i;
printf(" Din Dout\n");
// Create input data
// Call the function to operate on the data
for (i = 0; i < 4; i++) {
d = i;
example(&d);
printf(" %d %d\n", i, d);
}
// Return 0 if the test passed
return 0;
}
输出的结果:
Call Din Dout
1st 0 0
2nd 1 1
3rd 2 3
4th 3 6
查看综合报告 HW Interfaces 部分:
================================================================
== HW Interfaces
================================================================
* REGISTER
+-----------+---------+----------+
| Interface | Mode | Bitwidth |
+-----------+---------+----------+
| d_i | ap_none | 32 |
| d_o | ap_none | 32 |
+-----------+---------+----------+
可以看到,指针d是通过 ap_none 接口实现的。
3. 算术指针
在 Vitis HLS 中,指针算术的使用与传统的连线、握手或 FIFO 接口的数据访问方式存在显著差异。
算术指针允许开发者进行无序访问,即可以直接通过地址运算来访问任意位置的数据,如下代码:
void example(int *d) {
static int acc = 0;
int i;
for (i = 0; i < 4; i++) {
acc += *(d + i + 1);
*(d + i) = acc;
}
}
在这段代码中,指针 d 被用来访问和修改数组中的元素,且访问顺序不是连续的。
对应生成的IP:
对应的测试代码:
#include <stdio.h>
extern void example(int *d);
int main() {
int d[5], ref[5];
int i;
// Create input data
for (i = 0; i < 5; i++) {
d[i] = i;
ref[i] = i;
}
// Call the function to operate on the data
example(d);
printf(" Din Dout\n");
for (i = 0; i < 4; i++) {
printf(" %d %d\n", ref[i], d[i]);
}
return 0;
}
如果直接运行 C Simulation,那么仿真是通过的。
这种方式在软件编程中非常灵活,但在硬件设计中可能会引入问题。
相比之下,连线、握手或 FIFO 接口则要求数据必须按照固定的顺序进行访问。这意味着数据的读取和写入操作必须从数组的第一个元素开始,然后依次进行。这种顺序性要求与算术指针的灵活性相冲突。
- 连线接口:当数据准备就绪时,接口会读取数据。这种方式适合于数据流的连续处理。
- 握手和 FIFO 接口:当控制信号允许时,接口会执行读写操作。这种方式适合于需要同步控制的场景。
由于 Vitis HLS 不支持对连线、握手或 FIFO 接口建立数据索引,因此在硬件实现中,如果使用了算术指针,就需要某种形式的数据索引机制。为了解决这个问题,可以将指针替换为数组,并通过 RAM (ap_memory) 接口来实现代码,如下所示:
void example(int d[5]) {
static int acc = 0;
int i;
for (i=0; i<4; i++) {
acc += d[i+1];
d[i] = acc;
}
}
查看综合报告 HW Interfaces 部分:
================================================================
== HW Interfaces
================================================================
* AP_MEMORY
+------------+----------+
| Interface | Bitwidth |
+------------+----------+
| d_address0 | 3 |
| d_address1 | 3 |
| d_d0 | 32 |
| d_q1 | 32 |
+------------+----------+
可以看到,数组d已经通过 ap_memory 接口来实现了。
4. 疑点解答
4.1 疑点1
基本指针代码输出的IP如下,这个模块是在每个周期执行一次吗?
答:不是。执行状态受块级控制协议 ap_hs 控制,在使能期间,每个周期可以执行一次。对应到仿真程序上,调用 example(&d) 则执行一次。
4.2 疑点2
void example(int d[5]) {
static int acc = 0;
int i;
for (i=0; i<4; i++) {
acc += d[i+1];
d[i] = acc;
}
}
对应的IP如下,通过块级控制协议启动一次,就能实现对存储器中4个元素进行操作吗?
答:是的。启动该IP后, HLS core 会自动操作存储器4次,完成数据的读写,完毕后,使能 ap_done信号。
5. 总结
在Vitis HLS中,指针的使用与传统的C/C++环境存在显著差异,尤其是在涉及硬件综合时。基本指针可以直接指向数据,并且通常不会对硬件综合产生负面影响,但在顶层函数的接口中使用时需要注意指针的实现方式。另一方面,算术指针的灵活性在软件编程中很有用,但在硬件设计中可能引入问题,因为硬件需要按照固定顺序访问数据。为了克服这些问题,可以通过使用数组和RAM接口来替代指针,以更好地满足硬件设计的需求。总之,在进行Vitis HLS设计时,需要仔细考虑指针的使用方式,以确保最终的硬件实现符合设计要求。