Vivado HLS将C算法加速为FPGA IP核的流水线优化技巧
扫描二维码
随时随地手机看文章
在FPGA开发中,Vivado HLS(High-Level Synthesis)是将C/C++算法转化为硬件IP核的利器。然而,未经优化的C代码综合后往往性能低下。流水线(Pipelining)是打破顺序执行瓶颈、将吞吐量提升数倍的核心手段。本文将详解三种流水线实战技巧,帮你从“软件思维”转向“硬件思维”。
一、基础:理解流水线与启动间隔(II)
在HLS中,流水线的核心指标是启动间隔(Initiation Interval, II)。II=1是理想状态,表示每个时钟周期都能吃入新数据;II>1则意味着存在瓶颈(如资源冲突或数据依赖)。
原始串行代码(低效):
// 默认综合为状态机,需等待前一次计算完成才能开始下一次
void fir_filter(int *input, int *output, int len) {
static int shift_reg[16];
for(int i = 0; i < len; i++) {
// 移位操作
for(int j = 15; j > 0; j--) {
shift_reg[j] = shift_reg[j-1];
}
shift_reg[0] = input[i];
// 乘加操作(瓶颈)
int acc = 0;
for(int k = 0; k < 16; k++) {
acc += shift_reg[k] * coeff[k]; // 顺序执行,延迟高
}
output[i] = acc;
}
}
此代码即使综合成功,吞吐量也极低,因为内层循环是严格串行的。
二、技巧1:循环流水线(PIPELINE)与数组分割(ARRAY_PARTITION)
这是最常用的“组合拳”。直接对内层乘加循环应用流水线,往往因端口竞争导致II无法达到1(BRAM默认单端口,无法同时读写)。
优化步骤:
1. 分割数组:将系数数组coeff和移位寄存器shift_reg分割成多个独立寄存器,消除访问冲突。
2. 应用流水线:强制工具并行调度。
void fir_filter_optimized(int *input, int *output, int len) {
// 1. 数组完全分割为寄存器,解决端口竞争
#pragma HLS ARRAY_PARTITION variable=coeff complete dim=1
#pragma HLS ARRAY_PARTITION variable=shift_reg complete dim=1
for(int i = 0; i < len; i++) {
#pragma HLS PIPELINE II=1 // 2. 目标II=1
// 移位逻辑(需重构为并行或使用hls::stream)
// ...
// 乘加逻辑(现在可并行)
int acc = 0;
for(int k = 0; k < 16; k++) {
acc += shift_reg[k] * coeff[k];
}
output[i] = acc;
}
}
避坑:ARRAY_PARTITION complete会消耗大量FF资源,仅适用于中小数组(如深度<64)。对于大数组,改用cyclic或block分区,或使用双端口BRAM。
三、技巧2:任务级流水线(DATAFLOW)与数据流优化
当算法包含多个子函数(如Read→Process→Write)时,单纯循环流水线不够用。DATAFLOW指令实现任务级流水,让前后级像工厂流水线一样并发执行。
适用场景:图像处理中的“行缓存-滤波-输出”链路。
void image_pipeline(hls::stream<px_t> &in, hls::stream<px_t> &out, int rows, int cols) {
#pragma HLS DATAFLOW // 关键:使能数据流
hls::stream<window_t> buffer_stream;
// 三个任务并行执行,通过stream(FIFO)通信
read_and_buffer(in, buffer_stream, rows, cols);
process_window(buffer_stream, out, rows, cols);
// write_output(out_stream, out); // 通常合并到上一级
}
void read_and_buffer(hls::stream<px_t> &in, hls::stream<window_t> &out, int r, int c) {
#pragma HLS PIPELINE II=1
// 实现行缓存,输出3x3窗口
}
void process_window(hls::stream<window_t> &in, hls::stream<px_t> &out, int r, int c) {
#pragma HLS PIPELINE II=1
window_t win = in.read();
// 处理窗口...
out.write(result);
}
优势:DATAFLOW自动插入FIFO进行任务同步,极大减少了中间数据的缓存延迟,实现了真正的连续流处理。
四、技巧3:循环展开(UNROLL)与流水线的协同
UNROLL(展开)复制硬件资源以增加并行度,PIPELINE(流水)优化时序以提升吞吐率。两者需协同使用。
• 策略1(内层UNROLL,外层PIPELINE):适用于矩阵运算。
for (int i = 0; i < 64; i++) {
#pragma HLS PIPELINE II=1
for (int j = 0; j < 8; j++) {
#pragma HLS UNROLL // 复制8个乘法器并行计算
C[i][j] = A[i][j] + B[i][j];
}
}
• 策略2(部分UNROLL + PIPELINE):资源与性能的平衡。
for (int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
#pragma HLS UNROLL factor=4 // 仅复制4份,而非1024份
// ...
}
警告:盲目使用UNROLL factor=0(完全展开)会导致资源爆炸(LUT/FF/DSP激增),必须根据FPGA资源预算谨慎选择因子。
五、性能分析与避坑指南
1. II不收敛怎么办?
◦ 原因1:数据依赖。检查是否存在“真依赖”(True Dependency),如a[i] = a[i-1] + x,这种无法流水,需重构算法。
◦ 原因2:存储瓶颈。检查综合报告中的“Dependency Info”,若提示“Memory port limit”,必须使用ARRAY_PARTITION或改用hls::stream。
2. 接口协议选择
◦ 使用ap_ctrl_hs(握手协议)时,函数不能连续执行,会阻碍流水线。推荐使用ap_ctrl_none配合hls::stream(AXI-Stream),实现Free-running模式,数据源源不断。
3. 资源与时序权衡
◦ 流水线深度越深(级数越多),最高频率(Fmax)越高,但资源(FF)和延迟也越大。通常建议在时序紧张的路径上手动插入寄存器(如#pragma HLS EXPRESSION_BALANCE)来平衡。
六、结语
Vivado HLS的流水线优化,本质是用硬件并发性换取时间。记住三条铁律:
1. 数据流优先:多级处理用DATAFLOW,消除中间缓存。
2. 端口竞争必解:流水线遇阻,优先查数组访问,用ARRAY_PARTITION破局。
3. 资源可控:UNROLL不是越大越好,factor=4或8通常是性价比最高的选择。
掌握这些技巧,你就能将C算法的吞吐量推向FPGA的硬件极限。





