img

对于 Vitis 嵌入式平台(zcu104) 的 Vitis HLS & Vitis 开发流程的简要介绍

写在文前

本文档需求关于 FPGA 开发、操作系统、编译原理、linux内核理解等前置知识,推荐在阅读本文档之前阅读 FPGA并行编程 与赛灵思官文档的 开发流程简介

以下大多数内容采样自官方文档,推荐阅读文档后查看官方文档进一步了解。

作者仅从零学习了四个月相关知识,如果存在与官方文档相悖部分请以官方文档为准。同时官方文档的技术分请尽可能参考英文版文档,中文版文档在部分专有名词命名方面可能存在偏差。

环境简介

zcu104 开发板简介

zcu104文档链接

Zynq UltraScale+ MPSoC ZCU104 Evaluation Kit(以下简称zcu104)是赛灵思 Zynq MPSoc系列的中等型号开发板,其上不仅具有主要的FPGA核心芯片,还具有完整的通用计算系统(由一个Arm核心与一定数量DDR组成,板载可由外置SD卡导入)。因此,此系列开发板的开发流程属于嵌入式开发流程,具有主机端与内核端两端开发流程。

zcu104 主要具有的资源如下图所示:

Vitis 嵌入式开发流程简介

Vitis 统一软件平台文档 应用加速开发 (UG1393)

VItis 嵌入式平台开发的简要流程如下图所示:

  • 嵌入式软件开发对应主机端。主机端主要为使用 C++/C 进行编写,依托板上带有的 Arm核运行 Unix base 系统。利用 XRT (Xilinx Run Time) 与 FPGA 内核进行沟通,完成数据传输与内核调用等主要任务。

    除了主要的执行代码外,主机端还包含完整的系统镜像用于在板上运行。因此主机端编译过程含有交叉编译,需要 Unix base 系统(推荐 Centos / RedHat 发行版)。

    主机端程序最后将被打包为可执行文件与系统镜像,通过 SD 卡加载到板卡上。

    请注意,上述提到的两次Unix base 系统并不是同一个,一个是开发过程中用户需要使用的Unix base 系统,Vitis 等软件将运行于其上;另一个是最后部署到 zcu104开发板上,开发板自主运行的一个Unix base 系统。

  • PL内核流程对应内核端。内核端可使用 High-Level Synthesis(以下简称 HLS)进行编写,或也可以使用更为基础的 Verilog 或 VHDL 进行编写。但本文主要聚焦于 HLS 开发。HLS 通过 V ++ 的编译链接后将生成 .xclbin 文件,代表烧录进FPGA核心的二进制流,其同样通过 SD 卡加载进板卡中。

后文将详细介绍 FPGA 与 HLS ,这里暂且略过。

Vitis 嵌入式开发环境简介

本文中我们主要使用的 Vitis 应用将包括:

  • Vivado ML

  • Vitis IDE

  • Vitis HLS

  • PetaLinux

其实理论上,仅仅使用 Vitis (其实也可以叫 Vitis 统一软件平台)就可以完成双端开发,调试与部署,因为 Vitis IDE相当于一个统一IDE帮助调用了开发流程中需要的各式软件。但是在 Vitis IDE上进行HLS开发调试的体验并不算完美,因此我们可以使用专为 HLS 设计的IDE:Vitis HLS 先进行内核端的开发调试,然后导出 .xo 包进入Vitis 进行主机端程序的编写与整体编译链接,最后部署。而 Vivado 与 PetaLinux主要由上述两个软件自动调用,除了查看部分调试中间信息外基本不用手动使用。

同时请注意本文档中将经常出现 Vitis 这个字眼,其狭义含义只指 Vitis IDE 这个软件,广义上则包含整个由赛灵思提供的 Vitis 工作流。请结合上下文理解(x。

Vitis 环境配置简述

安装流程文档

Vitis 统一平台应用安装

前往官网下载对应系统的 Vitis 统一平台安装程序 下载链接

下图是主要显示的是官方文档中的安装流程:

下载时勾选可参考下图:

安装完成后需要在 Xilinx (Vivado) License Manager (命令行名称 vlm/xlcm) 中申请赛灵思许可证,除了需要 Vitis 全套工具链的许可证外还需要对应开发板的解锁许可证(每个开发板仅有唯一一个许可证激活码,如果是二手开发板可以联系激活者将账号加入许可证)。

注意事项

  • 推荐下载 2022.1 或更新的版本,并在安装前在安装分区上预留 200GB 以上空间。

  • 如果需要使用虚拟机来安装 Vitis 系统,推荐使用 VMware,易于在调试过程中管理 USB 串口的透传,同时易于管理与宿主机间的共享文件夹。

  • 内核端开发的综合与实现所需时间对CPU性能与内存大小十分敏感。如果电脑内存小于等于 16GB,推荐在综合或实现期间关闭后台程序以空出足够性能与内存,同理,如果使 用虚拟机进行开发,推荐给虚拟机分配尽量多的CPU资源与内存资源。

  • 推荐将 Vitis 安装目录下的 setting64.sh 加入系统环境变量,之后从命令行启动对应ide,而不是从快捷方式开启,详细原因可参考文档解释,在此不赘述。

嵌入式平台需求环境

zcu104 嵌入式平台下载

嵌入式平台 (Vitis Embedded Base Platforms)是赛灵思提供的一个整体软件包,其中包含了用于嵌入式平台开发、调试所需要的软件配置,例如上文提到的 XRT 配置。

如果没有嵌入式平台,用户可能需要手动设计 主机端 与 内核端 的交互逻辑、板上存储与FPGA等等复杂交互逻辑,会极大的增大用户的开发成本。而利用赛灵思官方配置的嵌入式平台,我们可以集中精力开发应用逻辑代码。

嵌入式平台与 Vitis 版本与开发板型号强耦合,请在下载时一定要选择对应的平台。安装过程请参照下载页面中的指示。

下载页面内容随 VItis 版本变化会有变化,如 2022.2 版本下嵌入式平台主体已经包含在 VItis 安装程序中,而之前的版本则需要在此下载。

其中 ,Common Images for Embedded Vitis Platforms 为开发板上需要的系统镜像,可下载对应版本的 ZYNQMP common image,并运行 sdk.sh (需要petalinux)生成 sysroot。


FPGA 内核端 HLS 开发

Vitis 高层次综合用户指南 (UG1399)

info

本节主要集中于如何使用 Vitis HLS进行 HLS 开发,如果需要使用 Vitis 上的C++/C 开发可参考UG1393

Field-programmable gate array (FPGA)简介

info

如果读者对FPGA 开发不甚了解,推荐在查看本节之前先阅读电子书 FPGA并行编程 的第一章。

“ FPGA由一个可编程逻辑模块的矩阵和与之相连的内存组成,通常这些模块是以查找表(LUT)的形式存在,也就是说把地址信号输入进去,对应内存位置的内容会直接被输出出来。一个N位查找表可以以一个N位输入真值表的方式来表示。”

FPGA 中的主要资源有:

  • 查找表 LUT

    一连串互联的可编辑逻辑结构,可以组成出任意逻辑门,是 FPGA 实现灵活硬件实现的核心

  • 触发器 FF

    是FPGA最基本的内存单位,通常与 LUT 一起组成逻辑结构,也是理论上 FPGA 能实现的最快存储结构,可以实现接近1ns的读写延时,结构类似CPU上的寄存器,实际速率接近 CPU上的L1缓存速率。

  • 块存储 BREM

    一个支持多种内存形式和接口的可配置随机储存器,可以储存字节,对字,全字,双字等等,取决于具体的硬件实现。BRAM可以实现10ns级别的读写延迟,实际速率接近CPU上的L2缓存。

  • … 其他器件详见电子书。

FPGA 上的存储层次可以归纳为下表:

属性 外部内存 BRAM 触发器
数量 1-4 几千 几百万
单个大小 GB级 KB级 bit级
总量 GB级 MB级 100KB级
宽度 8-64 1-16 1
总带宽 GB每秒 TB每秒 100TB每秒

High-Level Synthesis (HLS)介绍

HLS 是一种高层次综合语言,允许用户通过编写 C++(兼容C++ 14特性)或 C 代码来实现硬件逻辑。Xilinx 提供的工具链将负责按照用户编写的代码与综合指令(下文会详细介绍)综合实现出 RTL 设计或 IP 核。相比 Verilog 等基础语言,可以自动实现很多优化范式,减少重复的心智负担较重的任务。用于在HLS下需要更多的考虑大的架构而非某个单独部件或逐周期运行。HLS需要注重的是系统的运行模式,HLS工具链会负责产生具体的RTL结构。

具体来说,HLS 可以自动完成以下任务:

  • 自动分析并利用一个算法中潜在的并发性
  • 自动在需要的路径上插入寄存器,并自动选择最理想的时钟
  • 自动产生控制数据在一个路径上出入方向的逻辑
  • 自动完成设计的部分与系统中其他部分的接口
  • 自动映射数据到储存单位以平衡资源使用与带宽
  • 自动将程序中计算的部分对应到逻辑单位,在实现等效计算的前提下自动选取最有效的实施方式

HLS 的学习除了推荐阅读 FPGA并行编程外,可详细参考Vitis HLS 硬件设计方法论HLS 编译指示文档学习HLS可进行的优化设计方法。在此仅列举一些常见的优化范例:

  • 流水线优化(pipeline)

    可以自动将循环流水线化,大幅减少循环需要的时间。

  • 流式数据处理(dataflow)

    通过典型的FIFO设计,让数据可以无延迟地在硬件设计中流动而不需要等待无关的执行单元。

对于 HLS 语言深入编译器与硬件的优化是写出合格硬件代码的关键,但是难以一蹴而就,需要熟练理解HLS的每一种优化策略并在开发中长期积累经验。

Vitis HLS 简介

“ Vitis HLS 是一种高层次综合工具,支持将 C、C++ 和 OpenCL™ 函数硬连线到器件逻辑互连结构和 RAM/DSP 块上。Vitis HLS 可在Vitis应用加速开发流程中实现硬件内核,并使用 C/C++ 语言代码在 Vivado 中为赛灵思器件设计开发 RTL IP。”

info

上文我们提到, Vitis HLS 将用于开发 HLS 内核代码,因此 Vitis HLS 中的所有操作将不涉及实际的主机端程序。

Vitis HLS 作为专为 HLS 设计的综合工具,可使用 C/C++ 语言开发, 此处仅列举 C++ 开发流程。其开发逻辑主要分为两部分:

  • 内核开发代码 HLS

    包含所有应该被综合为硬件的代码,应该遵守 HLS 范式进行开发。内核代码需要指定一个顶层函数作为实现后硬件的顶层入口。内核代码可以应用 HLS 约束。(详见HLS 编译指示文档

  • 测试代码 TestBench

    用以在开发期间测试内核代码,通过调用内核代码的顶层函数进行内核测试。其基本可按照普通 C++ 程序进行开发,可以进行系统调用等操作,不作为 HLS 代码进入硬件综合。但注意,这部分代码不会影响最终的硬件实现,也不是最终部署的主机端代码(主机端开发将在下文另外介绍),仅作为开发期间测试使用。

下图展示了 Vitis HLS 的主要设计流程:

同时, Vitis HLS 提供了 Solution 抽象管理层次,用以管理不同的约束与项目配置,但是所有 Solution 将共享代码。

Vitis HLS 使用&工作流

创建新的 Vitis HLS 工程 (UG1399)

如果配置了 vitis 全局环境变量,可以从命令行使用 vitis_hls 命令启动应用(注意 vitis_hls 启动脚本默认前台运行,运行期间不能关闭用于启动的命令行终端)。

按照上述链接 UG1399 文档中所述,新建 Vitis HLS 项目。

在新建过程中需要指定开发所用的硬件平台。可直接在 Borad 选项卡中搜索对应型号(如 zcu104)。

其他大多数选项会在后续开发中时常改变,保留默认设置即可。


在进入 Vitis HLS 后,默认界面应该如下图:

img

默认位于左下角的是 Flow Navigator 工作流导航,其中可控制进行 HLS 开发主要四个流程:

  1. C Simulation (C语言仿真)

    将 HLS 代码视为普通 C++ 代码进行编译执行,验证设计的基本正确性。 硬件实现不一定能实现与C++代码完全相同的语义(赛灵思工具链将尽可能满足这一点但是也有例外,详见官方文档),但是如果C++代码执行错误那么硬件设计必然错误。

    并且,在所有步骤中基本仅此步可以查看算法的输出结果,并利用gdb进行 debug。

    因此推荐算法改动后都先进进行此步骤验证正确性。

  2. C Synthesis (C 语言综合)

    此步将真正进行HLS部分代码的综合,应用HLS约束,导出对应的 verilog 实现并验证设计在硬件上的可行性与初步性能估计。下图展示了综合报告的一部分:

    img

    可以看到综合报告展示了预计可以实现的时钟频率与每个部分的预计延时,另外在未展示部分还含有各个接口的实现方式、dsp实现情况、存储具体实现等模块展示。

    如果此步骤无法完成,说明 HLS 部分代码或约束无法在硬件上实现或冲突,需要修改。实际情况视具体报错而定。

    相比最后实现电路,此处预计的组件延时可能较小(因为此时有诸多因素未考虑),而预计的资源利用量也可能较小(因为真正电路实现时会应用诸多优化手段减少资源占用)。

    需要注意的是,综合期间有两种方式可选:

    • Vivado IP Flow Target:作为纯 IP 核进行综合,将不会带有 Vitis 嵌入式平台的配置硬件,无法使用 Vitis 工作流进行后续部署,仅能作为 IP 核导入 Vivado 中进行设计。同时因为无法得知外界输入的接口,对于组件延时(特别是数据读取写入部分)的延时估计可能不准确。此不准确性会延续到C/RTL 协同仿真。

    • Vitis kernel Flow Target(推荐):作为 Vitis 工作流进行综合,会额外占用 量级的资源,但是能使用 Vitis 工作流进行部署,并且有更加精准的默认接口预测。

  3. Cosimulation (C/RTL 协同仿真)

    通过运行 RTL 实现(即综合后的实现)以验证 RTL 功能与 C ++代码是否完全相同。

    此步骤是选择 Vitis HLS 作为 HLS 开发的主要原因,因为 Vitis 开发流程中无法进行此步。此步骤中 Vitis HLS 可以将测试代码视为临时的主机端代码,尝试用测试代码运行 RTL 实现,输出较为真实的各组件延时。

    下图展示了一份协同仿真的报告:

    此处预计延时基本等同于实际部署后运行的延时,除非实际部署出现降频,意外硬件死锁等情况(因为输入导致的死锁此步骤能检测出来)。

  4. Implementation (实现)

    通过调用 Vivado 尝试实现最终硬件实现。如果需要导出本设计为Vivado IP,则需要运行此步。如果后续流程是导入 Vitis(推荐做法),则本步骤非必需,实现步骤将后移到 Vitis中进行。

    但作者仍推荐对设计进行实现尝试,可以最终查看各个资源的最终使用情况与硬件实现方式是否能符合代码意图。

  5. Export (导出)

    导出 HLS 设计为各种格式,以便后续工作流使用。

    • 导出 Vivado IP :选择导出为 Vivado IP (.zip),如上文所述,需要先进行硬件实现。

    • 导出为 Vitis 工作流:选择导出 Vitis kernel (.xo),需要在综合期间选择 Vitis Kernel Flow Target。


默认布局下,在页面右方 tab 页中可以找到 Directive 选项页。其中可以对 HLS 编译约束进行查看与编辑。

HLS 编译约束有两种存在形式,

  • 位于源代码中以宏的形式存在,如下图:

    此种约束可以通过直接在源代码中编辑或通过上述Directive 选项页进行添加。优点是约束直接存在于源代码中,无需额外文件,缺点是所有solution因为共用代码,无法使用不同约束。

  • 位于独立约束文件中,经由IDE 自动映射到对应的函数、作用域、变量。

    此种约束只能通过Directive 选项页进行添加与修改,但优点是可以在不同solution间尝试不同的约束(每个solution拥有自己的约束定义文件),IDE可以在综合后展示不同solution之间的报告对比。

可以视每种约束的实际情况选择放置方法。

XRT 主机端开发

XRT 文档

“ Xilinx® Runtime (XRT) is implemented as a combination of userspace and kernel driver components. XRT supports both PCIe based accelerator cards and MPSoC based embedded architecture provides standardized software interface to Xilinx® FPGA. The key user APIs are defined in xrt.h header file.”

Xilinx® Runtime (XRT) 允许我们使用C++ 甚至 Python 来简单地实现主机端与内核端的通信。对于 MPSoc 来说,XRT主要具有以下作用域(此处暂时不清楚具体域不要紧):

_images/XRT-Architecture-Edge.svg

以下给出一个典型的主机端调用内核执行代码的样例,具体API请参考上述文档:

以下假设内核代码的顶层函数输入两个整形数组,输出一个浮点类型返回值

即函数签名为 void testFunc (int ArrA[], int ArrB[], double* out)

#include "cmdlineparser.h"
#include <iostream>
#include <cstring>
// XRT includes
#include "experimental/xrt_bo.h"
#include "experimental/xrt_device.h"
#include "experimental/xrt_kernel.h"

int main(int argc, char** argv) {
    // Command Line Parser
    sda::utils::CmdLineParser parser;

    // 主机端程序需要指定 FPGA 硬件对应的二进制流 与 FPGA 硬件id
    //***//"<Full Arg>",  "<Short Arg>", "<Description>", "<Default>"
    parser.addSwitch("--xclbin_file", "-x", "input binary file string", "");
    parser.addSwitch("--device_id", "-d", "device index", "0");
    parser.parse(argc, argv);

    // Read settings
    std::string binaryFile = parser.value("xclbin_file");
    int device_index = stoi(parser.value("device_id"));

    if (argc < 3) {
        parser.printHelp();
        return EXIT_FAILURE;
    }

    std::cout << "Open the device" << device_index << std::endl;
    //  xrt::device 为 FPGA 硬件在主机端的抽象
    auto device = xrt::device(device_index);
    std::cout << "Load the xclbin" << binary_file << std::endl;、
    // device.load_xclbin 将二进制流烧录到FPGA中
    auto uuid = device.load_xclbin(binary_file);

    size_t vector_size_bytes = sizeof(int) * ARR_SIZE;

    // xrt::kernel 为二进制流中编写的内核的抽象,一个二进制流可能含有多个内核
    auto krnl = xrt::kernel(device, uuid, "testFunc");

    std::cout << "Allocate Buffer in Global Memory\n";
    // 将内核函数的参数映射为一段内核端的地址(抽象)
    auto bo0 = xrt::bo(device, vector_size_bytes, krnl.group_id(0));
    auto bo1 = xrt::bo(device, vector_size_bytes, krnl.group_id(1));
    auto bo2 = xrt::bo(device, sizeof(double), krnl.group_id(2));
    // 将内核端的地址映射到主机端
    auto bo0_map = bo0.map<int*>();
    auto bo1_map = bo1.map<int*>();
    auto bo2_m = bo2.map<double*>();

    // Create the test data
    std::cout << "Random Input Data\n";

    srand(114514); //change me if you want different numbers
    for (int i = 0; i < ARR_SIZE; i++) {
        bo0_map[i] = rand() % MAX_NUMBER + 1;
        bo1_map[i] = rand() % MAX_NUMBER + 1;
    }

    std::cout  << "generated input data\n";
    // Synchronize buffer content with device side
    std::cout << "synchronize buffer to device memory\n";

    // 通过同步信号等待数据输送到内核端
    bo0.sync(XCL_BO_SYNC_BO_TO_DEVICE);
    bo1.sync(XCL_BO_SYNC_BO_TO_DEVICE);

    std::cout << "Execution of the kernel\n";
    // 调用内核
    auto run = krnl(bo0,bo1, bo2);
    // 等待内核执行完成
    run.wait();

    // Get the output;
    std::cout << "Get the output data from the device" << std::endl;
    // 通过同步信号等待数据输送回主机端
    bo2.sync(XCL_BO_SYNC_BO_FROM_DEVICE);

    // Validate our results
    std::cout << "\n output: "<< *bo2_m<< "\n";
    std::cout << "\n";
    std::cout << "TEST PASSED\n";
    return 0;
}

主机端与部署工作流(Vitis)

Vitis IDE 开发工作流

Vitis 统一软件平台文档 应用加速开发 (UG1393)

如上文所述,我们从 Vitis HLS中导出 .xo 工作流文件,现在需要导入 Vitis 中进行主机端开发与最终整体打包。理论上 Vitis HLS 之后的流程可以直接通过纯命令行调用 V++ 完成,具体可参考构建和运行应用文档。但鉴于图形化 IDE 可能对新手较为友好,以下展示在 Vitis IDE 中的操作。

Vitis IDEs 使用文档
![
在 Vitis IDE 中创建新应用工程时,它将包含三个子工程:

  1. 顶层系统工程(嵌套在应用工程内用于主机代码)、

  2. 硬件内核工程(用于编译内核工程)

  3. hw_link 工程(用于将硬件内核链接到目标平台以及用于各硬件内核彼此间的链接)

打开 Vitis IDE (同样推荐使用命令行 vitis启动),选择New Application Project新建应用工程,同样在平台选择时搜索 对应开发板型号。

与 Vitis HLS 新建工程不同的是,需要选择系统域,如下图所示:

img

其中,

  • Sysroot:指用于开发板上系统交叉编译的根目录,应该选择嵌入式平台中由sdk.sh导出的目录。如果没有更改patalinux 导出目录,其应该为/opt/petalinux/<version>/sysroots/cortexa72-cortexa53-xilinx-linux

  • Root FS:板上系统根目录文件,后缀应该为 .ext4 。默认嵌入式平台下载的系统镜像中会包含。

  • Kernel Image: 板上系统内核镜像,默认嵌入式平台下载的系统镜像中会包含,文件名为 Image

之后,选择应用模板时,为了方便后续配置应该选择 XRT Native APIs 空模板。

img


下图展示了上述流程后的 Vitis IDE 默认界面。

img

在左上角的文件浏览器与左下角的视图中可以看到三个子项目。文件浏览器中每个子项目都具有一个 .prj文件,双击打开后可以对该项目进行构建配置。

我们应该将 Vitis HLS 导出的 .xo 文件添加进后缀为kernels 内核项目(右键 src 文件夹进行添加)。添加后进入内核项目的构建配置将 .xo 中的顶层函数设置为本内核项目的构建函数:

img

img

然后在顶层系统工程(无特殊后缀的工程)中添加自己的主机端代码,即上文使用 XRT 开发的主机端程序。与传统 C++ 项目相同,主机端程序的 main 函数为入口函数。

请注意 Vitis IDE 虽然具有软件仿真和硬件仿真两个仿真流程,但是使用 .xo 作为内核端后无法使用这两个仿真流程。用户应该在 Vitis HLS 中完成内核的完全开发与调试,Vitis IDE 将视 .xo 工作流为黑盒,仅进行实现工作。

构建部署简介

在完成主机端开发后,可以点击上方 Projectbuild all进行构建。

如果出现 .xo 不支持进行仿真的错误,请记得点击最外层的.sprj文件将构建目标设置为 Hardware 而非仿真。

构建成功后用户将可以在以下目录下找到构建目标:

img

图中 zcu104 为主机端可执行文件,kernel.xclbin 为内核端生成的二进制流,两者文件名取决于项目配置。其他文件为开发板系统文件。

将上述所有文件拷贝进 SD 卡中,插入开发板,如下图将开发板设置到从 SD 卡启动

img

使用USB线连接到电脑以及板上的 USB JTAG UART MicroUSB端口。

在 Vitis IDE 中开启 Vitis Serial Terminal(如果找不到可以在Windowshow view中进行搜索),添加串口连接以访问开发板系统:

img

如果使用虚拟机,需要将USB透传到虚拟机,实际操作取决于虚拟机软件。

如果出现多个USB可选,可以都试试。

将电源开关滑动到ON位置以打开电路板。一个红色 LED和一些其他黄色板LED会亮起,以确认该板已通电。几秒钟后,红色LED将变为黄色。这表明比特流已下载并且系统正在引导。同时Vitis Serial Terminal将有打印信息。

经过一段时间的系统启动时间后,Vitis Serial Terminal将进入正常的终端界面,此时说明板上系统已经正常启动(可以忽略打印出的警告,重点是可以进行终端操作即可)。

输入 cd /run/media/mmcblk0p1转到sd卡挂载目录,可以看到目录下有sd中的非系统文件。

如果主机端程序需要命令行指定二进制流文件,则输入./<可执行文件名> -x XXX.xclbin即可运行应用。

如果主机端程序有标准输出流或文件输出流,在终端中即可查看结果。