3. C/C++ 可综合语法基础:数据类型、循环与数组、函数与接口、指针与流

好,咱们直接进入正题。这一章讲的是HLS的“地基”——可综合语法。说白了,就是哪些C/C++代码能变成硬件,哪些不能。我见过不少新手,兴致勃勃写了一大段C代码,结果Vivado HLS一跑,全是“不可综合”的错误。嗯,那感觉确实挺崩溃的。

所以,这一章我会把最核心、最常用的可综合语法掰开揉碎了讲。你记住一个原则:HLS里的C/C++,不是写给CPU看的,是写给硬件看的。你的思维要从“软件顺序执行”切换到“硬件并行流水”。

核心思想: HLS综合器会把你的C代码翻译成RTL(寄存器传输级)电路。所以,代码风格直接决定了最终电路的面积、速度和功耗。

3.1 数据类型:选对类型,事半功倍

在HLS里,数据类型的选择比软件编程重要得多。为什么?因为每个bit都对应着真实的硬件寄存器或连线。

3.1.1 标准C类型与任意精度类型

标准C的 int、char、float 当然能用。但问题是,它们太“死板”了。比如你用 int(32位)去存一个0~15的值,那剩下的28位就白白浪费了。在FPGA上,这就是在浪费宝贵的LUT和寄存器资源。

我个人习惯,只要涉及数据路径,一律用任意精度类型。Vivado HLS提供了 ap_int 和 ap_uint 系列。用法很简单:

#include <ap_int.h>

// 定义一个10位有符号整数
ap_int<10> a;
// 定义一个8位无符号整数
ap_uint<8> b;

// 甚至可以定义奇数位宽
ap_uint<3> c;  // 3位,取值范围0~7

我在项目中遇到过,一个卷积层的权重用 ap_fixed<16,6>(16位总长,6位整数位)代替 float,面积直接缩小了4倍,速度还更快。定点数 ap_fixed 是卷积加速的利器,后面我们会专门讲。

我的建议: 能用 ap_uint 就别用 int,能用 ap_fixed 就别用 float。这不仅是面积问题,更是时序收敛的关键。

3.1.2 结构体与联合体

结构体在HLS里非常有用,尤其是当你需要把一组相关的数据打包成一个接口时。比如,一个像素点包含R、G、B三个分量:

struct pixel {
    ap_uint<8> r;
    ap_uint<8> g;
    ap_uint<8> b;
};

综合后,这个结构体会被映射成一个宽24位的总线。注意,结构体内部的成员顺序会影响布局,尽量按位宽从大到小排列,避免填充浪费。

注意: 联合体(union)在HLS中要慎用。我曾经因为union的字节序问题,调试了整整两天。不同编译器对union的布局理解不一样,综合出来的硬件可能跟你想象的不一样。能不用就不用。

3.2 循环与数组:性能的放大器

卷积加速的核心是什么?是循环的并行化和流水线化。你写一个三层嵌套的for循环,软件上跑得慢,但硬件上可以把它展开成一条超长的流水线。

3.2.1 循环的优化:PIPELINE与UNROLL

这是HLS最核心的两个pragma。我简单解释一下:

  • PIPELINE(流水线):让循环的多次迭代在时间上重叠执行。比如,第1次迭代还没算完,第2次迭代就开始取数了。这能大幅提升吞吐量。
  • UNROLL(展开):把循环体复制多份,并行执行。比如循环执行8次,展开后就是8个相同的硬件模块同时工作。这能大幅提升并行度,但面积也会成倍增加。

看个例子:

// 原始的累加循环
int sum = 0;
for (int i = 0; i < 8; i++) {
    sum += data[i];
}

// 加上流水线pragma
#pragma HLS PIPELINE
for (int i = 0; i < 8; i++) {
    sum += data[i];
}

// 完全展开
#pragma HLS UNROLL
for (int i = 0; i < 8; i++) {
    sum += data[i];
}

展开后的代码,综合出来就是8个加法器同时算,然后一个加法树把结果汇总。你想想看,这速度能不快吗?

避坑指南: 我曾经把一个循环完全展开,结果面积爆了,时序也跑不过。后来改成部分展开(factor=4),面积和速度达到了平衡。记住:不是展开得越多越好,要根据你的资源预算来。

3.2.2 数组的映射与分割

数组在HLS里会被综合成BRAM或LUTRAM。默认情况下,一个数组对应一个BRAM端口,读写速度有限。为了提升带宽,我们需要对数组进行“分割”。

常用的方法有三种:

方法 pragma 效果
分割 ARRAY_PARTITION 把一个BRAM拆成多个小BRAM,增加读写端口
重塑 ARRAY_RESHAPE 改变数组的维度,比如把二维数组变成一维
映射 ARRAY_MAP 把多个小数组合并成一个大数组,减少BRAM数量

举个例子,一个卷积核的权重数组,如果按行分割,就能同时读取多行数据:

int weights[3][3];
#pragma HLS ARRAY_PARTITION variable=weights dim=1 complete

这样,weights[0]、weights[1]、weights[2] 就变成了三个独立的BRAM,可以同时读取。

3.3 函数与接口:模块化的艺术

在HLS里,函数就是硬件模块。顶层函数的参数,就是模块的端口。

3.3.1 函数综合与内联

默认情况下,每个函数都会被综合成一个独立的硬件模块。但有时候,函数调用开销(比如握手信号)会拖慢性能。这时候可以用 INLINE 把函数内联掉:

#pragma HLS INLINE
void sub_func(...) {
    // 这个函数会被直接嵌入到调用者中,不产生独立的硬件模块
}

我个人的经验是:小函数(比如一个乘法加一个加法)直接内联,大函数(比如整个卷积层)保持模块化,方便复用和调试。

3.3.2 接口综合:从C到端口的映射

顶层函数的参数类型,决定了硬件端口的协议。最常用的接口类型有:

  • ap_none:无协议,直接连线。适合内部信号。
  • ap_vld:带valid信号,表示数据有效。
  • ap_ack:带acknowledge信号,表示接收方已准备好。
  • ap_hs:握手协议(valid + ready),最常用的流式接口。
  • axis:AXI4-Stream接口,用于高速数据传输。
  • m_axi:AXI4主接口,用于访问DDR等外部存储器。

看个例子,一个带握手协议的加法器:

void adder(ap_uint<8> a, ap_uint<8> b, ap_uint<9> *sum) {
    #pragma HLS INTERFACE ap_hs port=a
    #pragma HLS INTERFACE ap_hs port=b
    #pragma HLS INTERFACE ap_hs port=sum
    *sum = a + b;
}

综合后,a和b都会有对应的valid和ready信号,形成一个标准的流式接口。

小技巧: 如果你不确定用什么接口,就用 ap_hs。它是最通用的握手协议,几乎能跟任何模块对接。

3.4 指针与流:数据搬运的利器

指针在HLS里是个“双刃剑”。用好了,能实现高效的数据流;用不好,综合出来的电路可能完全不是你想要的。

3.4.1 指针的可综合性

HLS支持指针,但有限制。最重要的规则是:指针必须指向一个明确的、可综合的对象。比如:

int a = 10;
int *p = &a;  // 可以,指向一个局部变量

int arr[10];
int *q = arr; // 可以,指向一个数组

int *r;       // 不行!野指针,不可综合

另外,函数参数中的指针,会被综合成硬件端口。比如:

void func(int *in, int *out) {
    *out = *in + 1;
}

这里的 in 和 out 会被综合成两个独立的端口,每个端口都带地址和数据信号。

我曾经踩过的坑: 在循环里用指针做自增运算,比如 *p++。HLS综合器有时候会搞不清楚指针的指向范围,导致综合出复杂的地址计算逻辑。后来我改成用数组索引,代码更清晰,综合结果也更可控。

3.4.2 hls::stream:流式数据的正确打开方式

如果你要做数据流处理(比如卷积的输入特征图一行一行地进来),千万别用普通的数组或指针。用 hls::stream!

hls::stream 的行为像一个FIFO(先进先出队列)。它有几个好处:

  • 自动处理读写同步,不需要你手动控制valid/ready。
  • 综合后直接映射成硬件FIFO,性能极高。
  • 支持阻塞和非阻塞读写。

用法很简单:

#include <hls_stream.h>

void stream_example(hls::stream<ap_uint<8>> &in, hls::stream<ap_uint<8>> &out) {
    ap_uint<8> data;
    in.read(data);   // 从输入流读一个数据
    data = data + 1;
    out.write(data); // 写到输出流
}

综合后,in 和 out 就是两个带握手信号的硬件FIFO接口。你想想看,这比你自己写握手逻辑要省多少事?

核心建议: 在卷积加速器里,所有的数据通道(输入特征图、权重、输出特征图)都应该用 hls::stream 来连接。这是实现流水线并行和数据流驱动的关键。

本章知识体系

下面这张图,帮你把本章的核心知识点串起来:

HLS可综合语法核心知识体系 数据类型 循环与数组 函数与接口 指针与流 标准类型 vs 任意精度 ap_int / ap_uint / ap_fixed 结构体打包 PIPELINE 流水线 UNROLL 展开 ARRAY_PARTITION 分割 INLINE 内联 ap_hs / axis 接口 m_axi 存储器接口 指针可综合性规则 hls::stream FIFO 阻塞/非阻塞读写 核心原则 代码风格 = 硬件架构 用硬件思维写C代码,而不是用软件思维

好了,这一章的内容就到这里。数据类型、循环数组、函数接口、指针流——这四个东西,就是你用HLS做卷积加速的“四根柱子”。把它们搞明白了,后面的工程实战才能站得稳。

可可精品资料库