当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]在FPGA开发中,Vivado HLS(High-Level Synthesis)是将C/C++算法转化为硬件IP核的利器。然而,未经优化的C代码综合后往往性能低下。流水线(Pipelining)是打破顺序执行瓶颈、将吞吐量提升数倍的核心手段。本文将详解三种流水线实战技巧,帮你从“软件思维”转向“硬件思维”。



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的硬件极限。


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

在FPGA上构建RISC-V SoC时,从复位向量到串口打印“Hello World”的启动流程,是验证软核能否“自主呼吸”的关键。本文将基于常见的PicoRV32或VexRiscv软核,详解从硬件复位到软件驱动的完整链...

关键字: RISC-V FPGA

在软件无线电(SDR)和雷达接收机中,数字下变频(DDC)是连接高速ADC与基带处理的关键桥梁。其核心任务是将高频宽带信号搬移到基带,并降低数据率。本文将详解如何利用NCO(数控振荡器)生成正交载波,并结合CORDIC(...

关键字: FPGA DDC

在FPGA高速数据流设计中,AXI4-Stream(AXIS)是连接DMA、DSP和视频IP的“血管”。但很多工程师只关注TDATA和TLAST,却忽略了TKEEP信号,导致在非对齐数据传输时出现数据错位、CRC校验失败...

关键字: FPGA AXI4-Stream协议

在异构计算与高速数据采集领域,PCIe Gen3 x4 提供了接近 4GB/s 的理论带宽,是连接 FPGA 与 CPU 的“高速公路”。对于大多数开发者而言,XDMA(DMA/Bridge Subsystem for...

关键字: FPGA PCIe 异构计算

在FPGA开发中,时钟域交叉(CDC)是导致亚稳态和数据错乱的“头号杀手”。当信号从clk_a跨越到异步的clk_b时,若处理不当,轻则数据跳变,重则系统死锁。本文将对比4种最核心的CDC方案,帮你从“能用”进阶到“可靠...

关键字: FPGA CDC 时钟域交叉

在超大规模(SoC/NoC)设计中,传统解释型HDL仿真器(如ModelSim/VCS)往往成为验证瓶颈。Verilator作为一款高性能开源Verilog/SystemVerilog仿真器,通过将RTL代码编译为优化的...

关键字: Verilator C++

在多核处理器普及的今天,并行编程已成为提升程序性能的核心手段。C++作为系统级编程语言,通过标准库提供了丰富的并行编程工具,其中“锁”是保障多线程安全的基础机制。然而,锁的使用并非简单的“加锁-解锁”操作,开发者常常面临...

关键字: C++ 用户态锁

在资源受限和高可靠性要求的嵌入式系统中,C++常被误解为“只适合PC开发”。实际上,通过禁用运行时类型识别(RTTI)和异常处理(Exception Handling),并利用其编译期特性,C++能构建出比C更安全、更高...

关键字: C++ RTTI 嵌入式

在工业伺服驱动与多轴运动控制系统中,EtherCAT凭借其纳秒级同步精度成为主流。为了兼顾协议处理效率与复杂控制算法的实时性,“FPGA(ESC)+ MCU(应用)”的双核架构已成为高性能从站设计的标准范式。本文将深入探...

关键字: 工业总线协议 EtherCAT FPGA

在本教程中,我将展示如何创建并使用适用于 Zynq Ultrascale+ XCZU4EV 中 GTH 传输器的示例项目。

关键字: 处理器 收发器 FPGA
关闭