第1章:HLS基础回顾 — 开发流程、综合与接口
大家好,我是你们的FPGA讲师。今天咱们聊聊HLS的基础。
说实话,很多工程师一上来就急着写代码,结果综合出来的电路跑不动。我当年也犯过这毛病。所以这一章,咱们把HLS的底子打扎实。
1.1 Vivado HLS开发流程
Vivado HLS的开发流程,说白了就是三步走:
- C/C++建模 — 用高级语言描述算法
- 综合与验证 — 转成RTL,跑仿真
- 导出IP — 打包给Vivado用
我个人习惯把流程画成一张图,这样思路更清晰。你看下面这张:
嗯,这张图我用了很多年。每次带新人,我都会先让他们看这个流程。为什么?因为很多坑都藏在流程里。
核心要点:HLS不是写C代码,而是用C描述硬件行为。你写的每一行代码,最终都会变成门电路。
1.2 C/C++代码综合
综合,就是把C代码翻译成RTL。但这里有个关键区别:
- 软件思维:顺序执行,一条指令接一条
- 硬件思维:并行执行,所有操作同时发生
我记得刚学HLS时,写了个for循环,以为它会顺序执行。结果综合出来一堆并行加法器。嗯,这就是HLS的「魔法」所在。
看个简单例子:
// 软件风格
int sum = 0;
for (int i = 0; i < 4; i++) {
sum += data[i];
}
// HLS风格(带流水线)
int sum = 0;
for (int i = 0; i < 4; i++) {
#pragma HLS PIPELINE
sum += data[i];
}
加了 PIPELINE 指令后,综合工具会把循环展开成流水线。每个时钟周期都能处理一个新数据。你想想看,吞吐量直接翻倍。
我的经验:写HLS代码时,先别急着加优化指令。先把功能跑通,再用 PIPELINE、UNROLL 这些指令去压性能。我曾经一上来就加满优化,结果仿真死活不对,查了三天才发现是数据依赖没处理好。
1.3 接口综合
接口综合,说白了就是决定数据怎么进出你的IP核。HLS支持三种主流接口:
| 接口类型 | 适用场景 | 特点 |
|---|---|---|
ap_fifo |
流式数据,生产者-消费者 | 握手信号简单,适合连续数据流 |
ap_memory |
小容量数据,BRAM/URAM | 地址+数据总线,随机访问 |
m_axi |
大容量数据,DDR/HBM | AXI4协议,支持突发传输 |
咱们这门课主要关注 m_axi,因为DDR读写全靠它。但另外两个接口也很重要,我简单说说。
ap_fifo
这个接口最简单。你写数据,它自动帮你做握手。我在项目中常用它做模块间的数据传递。比如一个模块算完结果,直接塞进FIFO,下一个模块从FIFO里取。省心。
void dut(int in[100], int out[100]) {
#pragma HLS INTERFACE ap_fifo port=in
#pragma HLS INTERFACE ap_fifo port=out
for (int i = 0; i < 100; i++) {
out[i] = in[i] * 2;
}
}
ap_memory
这个接口适合小批量数据。综合工具会自动把它映射到BRAM上。我曾经用它做过一个查找表,256个系数,一个BRAM就搞定了。
注意:ap_memory 不支持随机访问的乱序读写。如果你需要频繁跳地址访问,还是用 m_axi 吧。
m_axi — 我们的主角
m_axi 是AXI4主接口。它支持:
- 突发传输(Burst)— 一次请求传多个数据
- 乱序传输(Out-of-order)— 提高总线利用率
- 数据宽度可配 — 32/64/128/512位
咱们后面几章会深入讲 m_axi 的调优。这里先记住一句话:DDR的性能瓶颈,90%在接口上。
1.4 流水线与数据流
这两个概念容易混淆。我当年也搞混过,后来在项目里吃了亏才分清楚。
流水线(PIPELINE)
流水线是针对单个循环或函数的优化。它让循环的每次迭代可以重叠执行。
举个例子:
// 无流水线:一次迭代3个时钟周期
for (int i = 0; i < 100; i++) {
a = data[i] + 1; // 1周期
b = a * 2; // 1周期
c = b >> 4; // 1周期
}
// 有流水线:每1个时钟周期输出一次结果
for (int i = 0; i < 100; i++) {
#pragma HLS PIPELINE II=1
a = data[i] + 1;
b = a * 2;
c = b >> 4;
}
II(Initiation Interval)表示启动间隔。II=1意味着每个时钟都能启动一次新的迭代。嗯,这是理想情况。实际能不能做到II=1,要看你的数据依赖和资源够不够。
数据流(DATAFLOW)
数据流是针对多个函数或模块的优化。它让不同模块可以并行执行。
看这个例子:
void top(int in[100], int out[100]) {
#pragma HLS DATAFLOW
int tmp1[100], tmp2[100];
func_A(in, tmp1); // 读入数据,做预处理
func_B(tmp1, tmp2); // 核心计算
func_C(tmp2, out); // 后处理
}
加了 DATAFLOW 后,func_A处理完第1个数据,func_B就可以开始处理了,不用等func_A全部算完。这就像工厂流水线,每个工位都在同时干活。
关键区别:
- 流水线(PIPELINE)— 让一个模块内部的循环重叠执行
- 数据流(DATAFLOW)— 让多个模块之间并行工作
我在项目中经常把两者结合使用。比如先对顶层函数加 DATAFLOW,再对内部每个循环加 PIPELINE。这样性能能翻好几倍。
避坑指南:我曾经在一个项目里对嵌套循环加了 PIPELINE,结果综合出来的面积大了10倍。后来发现内层循环有数据依赖,II根本压不下去。所以加优化指令前,一定要先分析数据依赖。
小结
这一章咱们把HLS的基础过了一遍。开发流程、代码综合、接口类型、流水线与数据流,这些都是后面调优的基石。
下一章开始,咱们就要进入DDR的世界了。我会带着大家一步步调优,把DDR的带宽榨干。
可可精品资料库