4、第一个Vitis程序:创建Vitis项目、编写简单的向量加法内核、编写主机端代码、编译与运行、性能分析

好,咱们正式开始动手了。

前面几章聊了不少概念,什么HLS、什么Zynq架构、什么软硬件协同。说实话,光听不练容易晕。我自己的经验是——跑通第一个程序,比看十遍文档都管用。

这一章,我们就来写一个最经典的入门例子:向量加法。别小看它,麻雀虽小五脏俱全。从创建项目到跑出结果,再到看性能数据,整个Vitis开发流程你都能走一遍。

4.1 创建Vitis项目

打开Vitis IDE,你会看到一个熟悉的Eclipse风格界面。嗯,别被它吓到,其实操作逻辑很清晰。

第一步:创建平台工程

  • 点击 File → New → Platform Project
  • 给平台起个名字,比如 zcu102_platform
  • 选择你的硬件描述文件(XSA)。这个文件一般由Vivado生成,包含了PS和PL的配置信息
  • 我习惯勾上 Generate boot components,这样后面省事

第二步:创建应用工程

  • 点击 File → New → Application Project
  • 选择刚才建好的平台
  • 给应用起名,比如 vadd_example
  • 系统语言选 C++
  • 模板先选 Empty Application,咱们自己写代码
小提示: 我第一次用Vitis时,总搞混“平台”和“应用”的关系。说白了,平台就是你的硬件板子,应用就是跑在板子上的程序。一个平台可以跑多个应用。

4.2 编写向量加法内核

内核(Kernel)就是跑在FPGA上的加速逻辑。我们用HLS C++来写。

在项目中新建一个源文件,叫 vadd.cpp:

#include <ap_int.h>
#include <hls_stream.h>

#define DATA_SIZE 4096

extern "C" {
void vadd(
    const int *a,      // 输入向量A
    const int *b,      // 输入向量B
    int *c,            // 输出向量C
    int size           // 向量长度
) {
#pragma HLS INTERFACE m_axi port=a offset=slave bundle=gmem0
#pragma HLS INTERFACE m_axi port=b offset=slave bundle=gmem1
#pragma HLS INTERFACE m_axi port=c offset=slave bundle=gmem2
#pragma HLS INTERFACE s_axilite port=size
#pragma HLS INTERFACE s_axilite port=return

    for (int i = 0; i < size; i++) {
#pragma HLS PIPELINE II=1
        c[i] = a[i] + b[i];
    }
}
}

这段代码看起来简单,但有几个关键点我得提醒你:

  • extern "C":这是必须的,告诉编译器用C的命名规则,不然主机端找不到函数
  • #pragma HLS INTERFACE:定义了内核和外部怎么通信。m_axi 是走DDR内存,s_axilite 是走控制总线
  • #pragma HLS PIPELINE II=1:这个最狠。它告诉编译器,每个时钟周期处理一个数据。II=1意味着流水线全开
核心理解: 这个内核本质上就是一个硬件电路。for循环会被综合成一个流水线,每个时钟周期读两个数、加一次、写一个结果。CPU要一条条指令执行,而FPGA是硬件并行——这就是加速的根源。

4.3 编写主机端代码

主机端代码跑在ARM处理器上,负责调用内核、搬运数据。新建 main.cpp:

#include <stdio.h>
#include <stdlib.h>
#include <xrt/xrt_bo.h>
#include <xrt/xrt_device.h>
#include <xrt/xrt_kernel.h>

#define DATA_SIZE 4096

int main() {
    // 1. 打开设备
    xrt::device device = xrt::device(0);
    xrt::uuid xclbin_uuid = device.load_xclbin("vadd.xclbin");

    // 2. 创建内核对象
    xrt::kernel kernel = xrt::kernel(device, xclbin_uuid, "vadd");

    // 3. 分配缓冲区
    size_t size_in_bytes = DATA_SIZE * sizeof(int);
    xrt::bo bo_a = xrt::bo(device, size_in_bytes, kernel.group_id(0));
    xrt::bo bo_b = xrt::bo(device, size_in_bytes, kernel.group_id(1));
    xrt::bo bo_c = xrt::bo(device, size_in_bytes, kernel.group_id(2));

    // 4. 准备数据
    int *a = bo_a.map<int*>();
    int *b = bo_b.map<int*>();
    int *c = bo_c.map<int*>();
    for (int i = 0; i < DATA_SIZE; i++) {
        a[i] = i;
        b[i] = i * 2;
    }

    // 5. 数据从主机传到FPGA
    bo_a.sync(XCL_BO_SYNC_BO_TO_DEVICE);
    bo_b.sync(XCL_BO_SYNC_BO_TO_DEVICE);

    // 6. 运行内核
    auto run = kernel(a, b, c, DATA_SIZE);
    run.wait();

    // 7. 数据从FPGA传回主机
    bo_c.sync(XCL_BO_SYNC_BO_FROM_DEVICE);

    // 8. 验证结果
    int errors = 0;
    for (int i = 0; i < DATA_SIZE; i++) {
        if (c[i] != a[i] + b[i]) errors++;
    }
    printf("Test %s with %d errors\n", errors ? "FAILED" : "PASSED", errors);

    return 0;
}

这段代码的流程很清晰:

  1. 打开设备,加载比特流(xclbin)
  2. 创建内核句柄
  3. 分配三块缓冲区:输入A、输入B、输出C
  4. 准备测试数据
  5. 把数据从DDR搬到FPGA的全局内存
  6. 启动内核,等待完成
  7. 把结果从FPGA搬回DDR
  8. 验证结果
注意: 我曾经犯过一个低级错误——忘了调用 sync()。结果数据根本没传过去,内核算了个寂寞。记住:数据不会自己飞过去,必须显式同步。

4.4 编译与运行

编译分两步:先编译内核,再编译主机程序。

编译内核:

v++ -t hw --platform xilinx_zcu102_base_202410_1 \
    -c -k vadd -o vadd.xo vadd.cpp
v++ -t hw --platform xilinx_zcu102_base_202410_1 \
    -l -o vadd.xclbin vadd.xo

编译主机程序:

aarch64-linux-gnu-g++ -std=c++17 \
    -I$XILINX_XRT/include \
    -L$XILINX_XRT/lib \
    -lxrt_coreutil -o vadd_host main.cpp

运行:

把 vadd_host 和 vadd.xclbin 拷贝到板子上,然后:

./vadd_host vadd.xclbin

如果一切顺利,你会看到:

Test PASSED with 0 errors
避坑指南: 我第一次编译时卡了半小时,后来发现是 -t hw 没加。不加这个选项,默认走软件仿真,根本不会生成真正的比特流。嗯,这个坑我替你们踩过了。

4.5 性能分析

跑通只是第一步。我们得看看它到底快不快。

Vitis提供了性能分析工具。在运行程序后,会生成一个 xrt.run_summary 文件。用Vitis Analyzer打开:

vitis_analyzer xrt.run_summary

你会看到几个关键指标:

指标 说明 我们的例子
Kernel Execution Time 内核实际运行时间 约 2.5 μs(4096个元素)
Data Transfer Time 数据传输时间 约 15 μs
Total Wall Time 总耗时 约 20 μs
Throughput 吞吐量 约 1.6 GB/s

看到没?数据传输时间比计算时间还长。这是FPGA加速的典型特征——计算极快,但数据搬运是瓶颈。所以实际项目中,我们往往要设计流水线,让计算和传输重叠起来。

我的经验: 做性能优化时,别光盯着内核的II(初始间隔)。先看看数据传输是不是瓶颈。很多时候,把数据搬好,比把计算做快更有效。

另外,Vitis Analyzer还能看到内核内部的流水线情况。比如我们的向量加法,如果II=1,那每个时钟周期都在干活。如果II>1,说明有资源冲突,需要优化。

嗯,第一个程序就到这里。你可能会觉得:就这么点东西?对,就这么点。但相信我,能跑通这个例子,你已经掌握了Vitis开发的80%的流程。剩下的,无非是换更复杂的内核、调更多的参数。

Vitis向量加法程序流程 主机端 (ARM) FPGA内核 (vadd) DDR 全局内存 sync() 写入数据 内核读取数据 内核写回结果 sync() 读取结果 验证结果 数据流方向:主机 → DDR → 内核 → DDR → 主机

这张图把整个数据流画清楚了。你写代码时,脑子里要有这张图——数据从哪里来,到哪里去,谁在什么时候干活。搞明白这个,Vitis开发就不再是黑盒了。

可可精品资料库