第1章:HLS基础回顾 — 开发流程、综合与接口

大家好,我是你们的FPGA讲师。今天咱们聊聊HLS的基础。

说实话,很多工程师一上来就急着写代码,结果综合出来的电路跑不动。我当年也犯过这毛病。所以这一章,咱们把HLS的底子打扎实。

1.1 Vivado HLS开发流程

Vivado HLS的开发流程,说白了就是三步走:

  1. C/C++建模 — 用高级语言描述算法
  2. 综合与验证 — 转成RTL,跑仿真
  3. 导出IP — 打包给Vivado用

我个人习惯把流程画成一张图,这样思路更清晰。你看下面这张:

C/C++ 源码 testbench + 设计 C 仿真 验证算法正确性 综合 C→RTL RTL 仿真 验证时序与功能 导出 IP 打包给Vivado 集成到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的带宽榨干。

可可精品资料库