4. 高层次综合(HLS)入门:HLS概念与优势,使用Vitis HLS将C/C++代码转化为RTL,接口综合与优化指令
4.1 什么是HLS?说白了就是让C/C++也能写硬件
各位同学好。今天我们来聊聊HLS,也就是高层次综合。
我第一次接触HLS是在五年前的一个图像处理项目里。当时客户催得紧,用Verilog写一个简单的滤波算法就要折腾好几天。后来我试着用C语言写算法,再用Vitis HLS转成RTL——你猜怎么着?原来三天的活,半天就搞定了。
HLS的核心思想其实很简单:让你用C/C++这种高级语言来描述硬件行为,然后工具自动帮你生成对应的RTL代码。说白了,就是把软件思维映射到硬件实现上。
HLS的优势在哪里?
- 开发速度快:C/C++的抽象级别高,写起来比Verilog/VHDL快3-5倍
- 验证方便:C仿真比RTL仿真快几个数量级,调试也更容易
- 算法迭代快:改算法就像改软件,不用重新写RTL
- 复用性强:现有的C/C++算法库可以直接用
但要注意,HLS不是万能的。我曾经见过有人试图用HLS写一个复杂的状态机,结果折腾了两周还不如手写Verilog来得快。所以,HLS最适合的是数据流密集型的算法,比如图像处理、信号处理、神经网络推理这些。
4.2 Vitis HLS的工作流程:从C到RTL的魔法
Vitis HLS是AMD(原Xilinx)推出的HLS工具。它的工作流程大致是这样的:
- 编写C/C++代码:描述你的算法功能
- 编写Testbench:验证C代码的功能正确性
- C仿真:在软件层面跑通算法
- 综合(Synthesis):将C代码转化为RTL
- 分析报告:查看延迟、吞吐量、资源占用等指标
- 添加优化指令:用pragma或directive指导工具生成更好的硬件
- 导出IP:生成可以在Vivado中使用的IP核
嗯,这里要注意一点:C仿真通过不代表RTL一定正确。我踩过这个坑——C仿真跑得好好的,综合出来的RTL就是不对。后来发现是数据类型的问题,C语言里int是32位,但硬件里我用了16位,精度不够。
4.3 接口综合:让C函数和外部世界对话
写C代码的时候,函数参数就是输入输出。但在硬件里,这些参数需要映射到具体的物理接口上。这就是接口综合要做的事。
举个例子,假设你有这样一个C函数:
void fir_filter(int input[1024], int output[1024], int coeff[32]) {
// 滤波算法实现
for (int i = 0; i < 1024; i++) {
output[i] = 0;
for (int j = 0; j < 32; j++) {
if (i >= j)
output[i] += input[i-j] * coeff[j];
}
}
}
默认情况下,Vitis HLS会把数组参数综合成BRAM接口。但如果你希望用AXI Stream或者FIFO接口,就需要用指令来指定。
我的建议:接口类型的选择直接影响性能。如果是流式数据,用AXI Stream;如果是随机访问,用BRAM或AXI Master。我在一个视频处理项目里,就是因为选错了接口类型,导致吞吐量上不去,后来改成AXI Stream才解决问题。
4.4 优化指令:让工具听你的话
HLS工具虽然智能,但它不知道你的设计目标是什么。是追求高性能?还是低资源?还是低功耗?这就需要你通过优化指令来告诉它。
常用的优化指令有这些:
| 指令 | 作用 | 使用场景 |
|---|---|---|
#pragma HLS PIPELINE |
流水线化循环,提高吞吐量 | 循环体内部没有数据依赖时 |
#pragma HLS UNROLL |
展开循环,并行执行 | 需要大量并行计算时 |
#pragma HLS ARRAY_PARTITION |
拆分数组,增加读写端口 | 数组被多个并行访问时 |
#pragma HLS DATAFLOW |
任务级流水线 | 多个函数或循环之间有数据传递时 |
#pragma HLS INTERFACE |
指定接口协议 | 需要自定义接口时 |
拿上面的FIR滤波器来说,如果我们想提高性能,可以这样优化:
void fir_filter(int input[1024], int output[1024], int coeff[32]) {
#pragma HLS INTERFACE axis port=input
#pragma HLS INTERFACE axis port=output
#pragma HLS INTERFACE s_axilite port=coeff
for (int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
output[i] = 0;
for (int j = 0; j < 32; j++) {
#pragma HLS UNROLL
if (i >= j)
output[i] += input[i-j] * coeff[j];
}
}
}
注意:优化指令不是越多越好。我曾经在一个项目里,为了追求极致性能,把所有循环都展开了,结果资源爆了,综合都过不了。后来只展开了内层循环,外层用PIPELINE,效果反而更好。
4.5 实战经验:一个简单的例子
我们来做一个简单的向量加法,看看HLS到底怎么用。
首先,写C代码:
#include <hls_stream.h>
void vector_add(hls::stream<int> &in1, hls::stream<int> &in2, hls::stream<int> &out, int n) {
#pragma HLS INTERFACE axis port=in1
#pragma HLS INTERFACE axis port=in2
#pragma HLS INTERFACE axis port=out
#pragma HLS INTERFACE s_axilite port=n
for (int i = 0; i < n; i++) {
#pragma HLS PIPELINE II=1
int a = in1.read();
int b = in2.read();
out.write(a + b);
}
}
然后,写Testbench验证:
#include <hls_stream.h>
#include <iostream>
int main() {
hls::stream<int> in1, in2, out;
// 写入测试数据
for (int i = 0; i < 10; i++) {
in1.write(i);
in2.write(i * 2);
}
// 调用函数
vector_add(in1, in2, out, 10);
// 读取结果
for (int i = 0; i < 10; i++) {
int result = out.read();
std::cout << "result[" << i << "] = " << result << std::endl;
}
return 0;
}
跑完C仿真后,就可以综合了。综合完成后,你会看到一个报告,里面会告诉你:
- 延迟(Latency):多少个时钟周期完成一次计算
- 吞吐量(Throughput):每秒能处理多少数据
- 资源占用:用了多少LUT、FF、BRAM、DSP
如果结果不满意,就调整优化指令,重新综合。这就是HLS的迭代开发流程。
4.6 避坑指南
最后,分享几个我踩过的坑:
- 指针问题:C语言里用指针很常见,但HLS对指针的支持有限。尽量用数组或hls::stream代替。
- 动态内存分配:malloc、new这些在硬件里是不存在的。所有数组大小必须在编译时确定。
- 递归函数:HLS不支持递归,因为硬件里没有栈的概念。用循环代替递归。
- 浮点数:浮点运算会消耗大量资源。能用定点数就用定点数。
好了,这一章就到这里。下一章我们会深入讲解Vitis HLS的具体操作,包括如何创建工程、如何运行仿真、如何分析报告。到时候我会带大家一步步操作,保证你能上手。