第二讲:HLS 数据类型与接口综合
各位同学好,今天我们来聊聊HLS里最基础也最容易踩坑的部分——数据类型和接口综合。说实话,我刚开始用Vitis HLS那会儿,觉得数据类型不就是C语言那套吗?结果综合出来的硬件要么面积爆炸,要么时序跑不过。后来才明白,HLS里的数据类型,跟软件C完全是两码事。
一、任意精度整数:别再用int了
先问大家一个问题:你写C代码时,是不是习惯用int、short、char?但在硬件里,这些类型其实很浪费。比如你只需要一个10位的计数器,用int就是32位,白白多出22个触发器。我做过一个图像处理项目,里面全是8位像素值,结果有人全用int,综合出来面积大了三倍。
HLS提供了ap_int和ap_uint,前者带符号,后者无符号。用法很简单:
#include <ap_int.h>
ap_uint<8> pixel; // 8位无符号,范围0-255
ap_int<10> offset; // 10位有符号,范围-512到511
ap_uint<1> flag; // 1位,其实就是个wire
为什么要用这个? 说白了,就是省资源。你想想看,一个8位的加法器和一个32位的加法器,面积能一样吗?而且位宽越小,时序越好跑。我个人的习惯是:能用ap_uint<N>绝不用int,除非你真的需要32位。
二、定点数:浮点数的替代方案
图像处理里经常要算一些系数,比如高斯滤波的权重。用float当然方便,但综合出来的硬件又大又慢。这时候定点数就派上用场了。
HLS的定点数类型是ap_fixed和ap_ufixed。它的格式是ap_fixed<W, I>,W是总位宽,I是整数部分位数。小数部分就是W-I位。
#include <ap_fixed.h>
// 总16位,整数8位,小数8位
ap_fixed<16, 8> coeff = 1.5;
// 无符号版本,总12位,整数4位,小数8位
ap_ufixed<12, 4> weight = 0.625;
我记得有一次做HDR图像融合,需要计算多个曝光图像的加权平均。用float综合出来延迟很大,换成ap_fixed<16, 8>后,不仅面积小了,吞吐量也上去了。当然,定点数有精度损失,你得自己评估误差是否在可接受范围内。
AP_RND(四舍五入)或AP_TRN(截断)。默认是截断,如果你需要更高精度,记得显式指定。我曾经因为默认截断导致图像出现条纹,排查了半天才发现是量化问题。
三、流接口(axis):数据搬运的利器
图像处理里,数据通常是一帧一帧流式传输的。HLS提供了hls::stream和AXIS接口来支持这种模式。说白了,就是数据像流水一样,一拍一拍地往前走。
定义AXIS接口很简单:
#include <hls_stream.h>
#include <ap_axi_sdata.h>
// 定义AXIS数据类型,比如32位数据+1位用户+1位最后
typedef ap_axis<32, 1, 1, 1> axis_pixel;
void image_filter(hls::stream<axis_pixel> &input,
hls::stream<axis_pixel> &output) {
#pragma HLS INTERFACE axis port=input
#pragma HLS INTERFACE axis port=output
axis_pixel p;
for (int i = 0; i < 1024; i++) {
p = input.read();
// 处理像素...
p.data = p.data * 2; // 简单处理
output.write(p);
}
}
为什么要用stream? 因为stream有反压机制。如果下游处理不过来,stream会自动阻塞上游。这比用FIFO手动管理方便多了。我做过一个项目,用stream做流水线,代码写起来跟串行一样简单,但综合出来是并行流水线。
四、内存接口(m_axi):大块数据的搬运
有时候数据量太大,不能全部放在片上BRAM里。比如一张1080p的图像,光像素数据就2MB多,片上根本放不下。这时候就需要用m_axi接口,直接读写DDR。
m_axi接口的定义方式:
void ddr_access(ap_uint<64> *input, ap_uint<64> *output, int size) {
#pragma HLS INTERFACE m_axi port=input offset=slave depth=1024
#pragma HLS INTERFACE m_axi port=output offset=slave depth=1024
#pragma HLS INTERFACE s_axilite port=size
for (int i = 0; i < size; i++) {
output[i] = input[i] * 2;
}
}
这里要注意几个点:
- depth参数: 告诉HLS这个接口需要多大的FIFO深度。设小了可能丢数据,设大了浪费资源。
- offset=slave: 表示地址由外部通过AXI-Lite配置。
- 突发传输: m_axi支持突发模式,一次可以连续读写多个数据。但需要你的循环是连续访问的,不能跳来跳去。
我曾经遇到过一个坑:m_axi接口的带宽利用率很低。后来发现是因为循环里有条件分支,导致访问地址不连续,无法触发突发传输。解决办法是把数据重新排列,保证连续访问。
#pragma HLS ARRAY_PARTITION把数据分到多个BRAM里,或者用#pragma HLS DATA_PACK打包数据。
五、综合对比:什么时候用什么接口?
我整理了一个表格,方便大家对比:
| 接口类型 | 适用场景 | 带宽 | 资源消耗 | 典型应用 |
|---|---|---|---|---|
| axis | 流式数据,逐像素处理 | 高(一拍一个数据) | 低(只需FIFO) | 图像滤波、缩放 |
| m_axi | 大块数据,随机访问 | 高(支持突发) | 中(需要地址生成) | 帧缓存、查找表 |
| bram | 小数据,片上存储 | 极高(零延迟) | 低(BRAM资源) | 行缓存、系数表 |
说白了,选接口就是看你的数据流模式。如果是流水线式的,用axis;如果是随机访问大块内存,用m_axi;如果是小数据频繁访问,用BRAM。
六、知识体系总览
下面这张图总结了本章的核心内容,大家可以对照着理解:
嗯,这张图基本把本章的内容串起来了。数据类型决定你用什么位宽和精度,接口类型决定数据怎么进出,综合优化则是让你在资源、带宽、延迟之间找到平衡点。
最后说一句:HLS开发,说白了就是C代码的硬件化。但你不能用软件的思维去写硬件代码。数据类型和接口的选择,直接决定了最终硬件的质量。多动手,多综合,慢慢就有感觉了。
可可精品资料库