34

碎碎思 · 2022年09月02日 · 北京市

HLS最全知识库

HLS最全知识库

副标题-FPGA高层次综合HLS(二)-Vitis HLS知识库

高层次综合(High-level Synthesis)简称HLS,指的是将高层次语言描述的逻辑结构,自动转换成低抽象级语言描述的电路模型的过程。

对于AMD Xilinx而言,Vivado 2019.1之前(包括),HLS工具叫Vivado HLS,之后为了统一将HLS集成到Vitis里了,集成之后增加了一些功能,同时将这部分开源出来了。Vitis HLS是Vitis AI重要组成部分,所以我们将重点介绍Vitis HLS。

官方指南:

https://docs.xilinx.com/r/_lSn47LKK31fyYQ_PRDoIQ/root

重要术语

LUT 或 SICE

LUT 或 SICE是构成了 FPGA 的区域。它的数量有限,当它用完时,意味着您的设计太大了!

BRAM 或 Block RAM

FPGA中的内存。在 Z-7010 FPGA上,有 120 个,每个都是 2KiB(实际上是 18 kb)。

Latency延迟

  • 设计产生结果所需的时钟周期数。
  • 循环的延迟是一次迭代所需的时钟周期数。

Initiation Interval (or II, or Interval间隔)

  • 在接受新数据之前必须执行的时钟周期数。

这与延迟不同!如果函数是流水线的,许多数据项会同时流过它。延迟是一个数据项被推入后弹出的时间,而时间间隔决定了数据可以被推入的速率。

  • 循环的间隔是可以开始循环迭代的最大速率,以时钟周期为单位。

image.png

上图中,左边是函数右边是循环,左边的时间间隔(接收新数据之前)是3个时钟周期,右边循环的间隔则是一个时钟周期;对于左边的延迟是这个函数产生结果的时钟周期数,是func_C运行完毕产生的周期数,为5个时钟周期,右边循环的延迟是一次迭代所需的时钟数,是4个时钟周期。

上面的概念非常重要,要不然下面的一些指令作用也看不懂~

重要的指令

这是在实际使用过程中重要的指令列表(不是全部)。

  • Functions-函数
  • loops-循环
  • Various-所有都适合
  • Arrays-数组
  • parameters-参数

image.png
image.png

任意精度类型

可以在 HLS 中使用普通的 C 类型(int、 char等)变量。但是,设计中的常用的寄存器并不完全需要 4、8 或 16 位宽,那么可以使用任意精度类型来准确定义需要多宽的数据类型,而不是接受这种低效率的通用定义。

下面展示了如何使用 C 和 C++ 风格的任意精度类型。我们建议使用 C++,除非有特定的理由不这样做。

在 C 中:

包含 <ap_cint.h> 头文件。然后,可以声明具有如下类型的变量:

image.png

在 C++ 中:

包含 <ap_int.h> 头文件。然后,可以声明具有如下类型的变量:

image.png

按照上面的设置应该能够正常打印任意精度类型,但是如果在调试过程中得到奇怪的值,请先使用printf调用to_int():

ap_uint<23> myAP;
printf("%d\n", myAP.to_int());

复位行为

在 HLS 中,所有静态和全局变量都被初始化为零(如果给定了初始化值,则初始化为其他值)。这包括 RAM,其中每个元素都被清除为零。然而,这种初始化只发生在 FPGA 首次编程时。任何后续处理器复位都不会触发初始化过程。

如果需要清除设备的内部状态,那么应该包含某种复位协议(根据复位状态处理所需要的程序)。

AXI 从接口和 AXI 主接口

可以在 HLS 组件中使用两个接口,即 AXI Slave 和 AXI Master。

  • AXI Slave:ARM 内核使用此接口来启动和停止 HLS 组件。他们还可以使用此接口来读取和写入相对少量的用户定义值。
  • AXI Master:如果需要更大量的共享数据,HLS 组件可以使用 AXI Master 接口启动事务以从主系统内存读取和写入数据。

可以通过toplevel在 HLS 组件中为函数指定参数并将指令附加到这些参数来定义所需的接口。下面显示了一个只有从接口的组件:

带有AXI Slave的 HLS 组件

uint32 toplevel(uint32 *arg1, uint32 *arg2, uint32 *arg3, uint32 *arg4) {
    #pragma HLS INTERFACE s_axilite port=arg1 bundle=AXILiteS register
    #pragma HLS INTERFACE s_axilite port=arg2 bundle=AXILiteS register
    #pragma HLS INTERFACE s_axilite port=arg3 bundle=AXILiteS register
    #pragma HLS INTERFACE s_axilite port=arg4 bundle=AXILiteS register
    #pragma HLS INTERFACE s_axilite port=return bundle=AXILiteS register
}

而下面是一个同时具有从接口和主接口的组件:

具有从属和主接口的 HLS 组件

uint32 toplevel(uint32 *ram, uint32 *arg1, uint32 *arg2, uint32 *arg3, uint32 *arg4) {
    #pragma HLS INTERFACE m_axi port=ram offset=slave bundle=MAXI
    #pragma HLS INTERFACE s_axilite port=arg1 bundle=AXILiteS register
    #pragma HLS INTERFACE s_axilite port=arg2 bundle=AXILiteS register
    #pragma HLS INTERFACE s_axilite port=arg3 bundle=AXILiteS register
    #pragma HLS INTERFACE s_axilite port=arg4 bundle=AXILiteS register
    #pragma HLS INTERFACE s_axilite port=return bundle=AXILiteS register
}

请注意,可以为从接口添加和删除参数,并更改它们的数据类型,只需记住也要更新关联#pragmaS。HLS 将相应地更新组件的驱动程序。

PS:主数据类型:由于 AXI 主接口会连接到 32 位宽的 RAM,因此在指定 AXI 主接口时应始终使用 32 位数据类型。

一旦决定了的接口,应该能够依靠 Vivado 自动化连线来连接一切。

请注意,返回端口的 pragma 很重要!

#pragma HLS INTERFACE s_axilite port=return bundle=AXILiteS register

//端口=返回  包=AXILiteS 寄存器

即使不使用函数的返回值,此 pragma 也会告诉 HLS 将 start、stop、done 和 reset 信号捆绑到 AXI Slave 接口中的控制寄存器中。因此,这将生成相应的驱动程序函数来启动和停止生成的 IP 内核。如果不包含此 pragma,则 HLS 将为这些信号生成简单的连线,并且 IP 内核将无法直接被 ARM 内核控制。

多种类型的 AXI Master

Vitis HLS在从同一主AXI端口复制值并将其解释为不同类型时非常挑剔。

例如,以下 memcpy 可能会导致“Stored value type does not match pointer operand type! (存储值类型与指针操作数类型不匹配!)” ,尝试将 RAM 视为uint32 和float类型时,综合过程中将会产生 LLVM 错误:

void toplevel(uint32 *ram) {
#pragma HLS INTERFACE m_axi port=ram offset=slave bundle=MAXI
    uint32 u_values[10];
    float f_values[10];
  
    memcpy(u_values, ram, 40);
    memcpy(f_values, ram+10, 40);
}

为了正确强制从 RAM 中复制数据的类型信息,可以使用union,如下所示:

typedef union {
    uint32 u;
    float f;
} ram_t;
  
void toplevel(ram_t *ram) {
#pragma HLS INTERFACE m_axi port=ram offset=slave bundle=MAXI
    uint32 u_values[10];
    float f_values[10];
  
    for (int i = 0; i < 10; i++) {
        ram_t data = ram[i];
        u_values[i] = data.u;
    }
    for (int i = 0; i < 10; i++) {
        ram_t data = ram[i+10];
        f_values[i] = data.f;
    }
}

此外,只要循环边界从零开始(并且是固定的),HLS应该足够聪明,将其视为类似于memcpy的突发传输-在综合过程中查找“推断MAXI端口上长度为X的总线突发读取”来证实这一点。

强制和阻止使用 Block RAM

HLS 会自动将大部分ARRAY转换为 BRAM。这通常很有用,因为寄存器ARRAY在 LUT(FPGA 空间)方面非常昂贵。但是,FPGA 的 BRAM 数量有限。BRAM 也只有 2 个访问端口。这意味着在任何时候最多有两个并行进程可以访问 RAM。这可能会限制设计的并行性潜力。

如果HLS使用的是不希望使用的BRAM,则将类型设置为COMPLETE且维度设置为1的指令array_PARTITION应用于数组。这将迫使它从寄存器中生成数组。这会占用大量的FPGA空间(LUT),所以要节约!

要强制 HLS 使用 BRAM,请将指令BIND\_STORAGE集应用到 RAM\_2P。(添加时按下帮助按钮可查看所有各种选项的说明)。

该 ARRAY_MAP 指令(见上文)可以通过自动将多个较小的数组放入一个较大的数组来帮助节省 Block RAM。

当更改 HLS 时

当更改 HLS 代码时,请执行以下步骤以确保bitfile已更新,方便进行正确地测试。

  • 1、重新运行综合。
  • 2、重新导出 IP 核。
  • 3、在 Vivado 中,它应该已经注意到了变化,并且会出现一条消息说“IP Catalog is out-of-date”。a、如果没有,请单击 IP Status,然后单击重新运行报告

b、单击刷新 IP 目录

c、在 IP Status面板中,应选择 toplevel  IP。单击 Upgrade 选项。

  • 4、在“Generate Output Products”对话框中,单击“Generate”。
  • 5、单击生成比特流。
  • 6、导出硬件到 Vitis。
  • 7、在 Vitis 中重新编程 FPGA 并运行软件。

现在应该明白了为什么测试和仿真如此重要了!

循环优化

在 HLS 中,可以将指令应用于循环以指示它展开或流水线。考虑以下循环:

myloop: for(int i = 0; i < 3; i++) {
    doSomething(X[i]);
}

默认情况下,HLS 将按顺序执行循环的每次迭代。它的执行将如下所示:

image.png

如果循环的每次迭代需要 10 个时钟周期,那么循环总共需要 30 个周期才能完成。

如果我们给这个循环 PIPELINE 指令,那么 HLS 将尝试在元素 0 完成之前开始计算元素 1,从而创建一个PIPELINE。这意味着循环的整体执行时间会更短,但代价是更复杂的控制逻辑和更多的寄存器来存储中间数据。循环如下所示:

image.png

只有在没有阻止此优化的依赖项时,它才能执行此操作。考虑以下代码:

int lastVal;
  
for(int i = 0; i < 50; i++) {
    lastVal = calculateAValue(lastVal);
}

在此示例中,循环被迫按顺序执行,因为在下一次循环迭代开始时需要在循环体末尾使用计算出的值。PIPELINE 仍然会试图加快速度,但不会大幅加快。

最后,如果我们给循环 UNROLL 指令,那么 HLS 将尝试并行执行循环的迭代。这需要更多的硬件,但速度非常快。在我们的示例中,整个循环只需要 10 个周期。

image.png

这要求循环的元素之间没有数据依赖关系。例如,如果 doSomething() 保留一个执行次数的全局计数器,则此依赖项将阻止 UNROLL 指令工作。

请注意,UNROLL默认情况下会尝试展开循环的所有迭代。这可能会导致非常大的设计!为了使事情更合理,可以设置UNROLL的FACTOR参数来告诉工具要创建多少副本。

应用UNROLL后,最好在分析视图中查看它是否实际应用。成功展开的设计在分析视图中将非常“垂直”,表示同一列中的操作同时发生。如果视图仍然非常“水平”且有很多列,那么很可能是数据依赖项阻止了展开。可以尝试通过单击操作来确定是什么阻止了展开。该工具将绘制箭头以显示输入的内容和输出的内容。请记住,BlockRAM 一次只能进行两次访问,因此,如果有一个大型ARRAY,而这些工具是从 BlockRAM 制作的,则展开或流水线操作最多只能创建 2 个副本。可以告诉工具不要使用带有ARRAY_PARTITION指令的块RAM。这可以快得多,但要使用更多的硬件资源。

数据流优化

如果没有使用限制资源的指令(例如 ALLOCATION 指令),HLS 会寻求最小化延迟并提高并发性。但是数据依赖性可以限制这一点。例如,访问数组的函数或循环必须在完成之前完成对数组的所有读/写访问,这就阻止了下一个消耗数据的函数或循环启动。

函数或循环中的操作可能会 在前一个函数或循环完成其所有操作之前开始操作。

HLS指定数据流优化时:

  • 分析顺序函数或循环之间的数据流。

这允许函数或循环并行运行,从而减少延迟并提高 RTL 设计的吞吐量,但以增加硬件资源为代价。尝试一下DATAFLOW ,看看它是否对设计有帮助。

找不到 'crt1.o' 错误

当试图在实验室硬件以外的机器上运行测试时,可能会收到一个错误,抱怨它找不到“crt1.o”。如果是这样,就需要为项目设置自定义链接器标志。

单击顶部菜单中的“Project”,然后单击Project Settings。在此框中,单击左侧的“Simulation”,然后将以下内容粘贴到“Linker Flags”框中:

-B"/usr/lib/x86_64-linux-gnu/"

我的循环有???latency估计!

有时,HLS 综合报告将包含?而不是给出最小和最大延迟的值。这是因为设计中至少有一个循环是数据相关的,即它循环的次数取决于 HLS 无法知道的数据值。

例如,下面的代码:

image.png

当综合在综合报告中给出以下内容:

image.png

如果我们检查代码,它将来自ram的元素相加,但要相加的元素的确切数量来自用户,作为arg1参数输入。因此,HLS无法提前知道该硬件执行需要多长时间,因为每次运行时它都是可变的。这就是上面我们说的运行时依赖于数据。生成的硬件将正常工作,我们只是无法预测运行需要多长时间。查看循环的细节,HLS仍然可以告诉我们循环的延迟是2,换句话说,它不知道它将迭代多少次,但每次迭代将花费2个时钟周期。

一般来说,应该尽量避免这种情况。如果 HLS 无法预测最坏的情况,那么它会过于“谨慎”,并且它可能会制造比我们需要的更大的硬件。此外,不能展开具有可变循环边界的循环。

一些算法从根本上是依赖于数据的,如果这种情况无法避免,那么可以通过将LOOP_TRIPCOUNT指令添加到循环中来告诉 HLS ,假设循环将进行给定次数的迭代,但这仅用于报告目的。生成的硬件将完全相同,但HLS将在循环迭代该次数的假设下生成延迟数。这意味着延迟数字不“正确”,但这仍然有助于了解其他优化是否具有总体积极效果。

定点类型

当需要使用小数运算但又不想支付使用浮点的大量硬件成本时,定点类型很有用。Vitis HLS 用户指南(https://www.xilinx.com/support/documentation/sw\_manuals/xilinx2020_2/ug1399-vitis-hls.pdf)中详细描述了定点类型,下面是一个简短示例:

定点示例

#include <iostream>
#include <ap_fixed.h>
  
ap_fixed<15, 5> a = 3.45;
ap_fixed<15, 5> b = 9.645;
ap_fixed<20, 6> c = a / b * 2;
std::cout << c;
//Prints 0.7148. The accurate answer is 0.7154. More bits can be allocated to the types if more accuracy is required.

C标准数学函数(在math.h中)仅针对浮点实现,但Xilinx在hls_math.h中提供了某些函数的定点实现。在hls::命名空间下;例如:hls::sqrt()、hls::cos()和hls::sin()。

此外,以下赛灵思示例代码显示了另一种定点平方根实现,在某些情况下可能更有效。

fxp_sqrt.h

#ifndef __FXP_SQRT_H__
#define __FXP_SQRT_H__
#include <cassert>
#include <ap_fixed.h>
using namespace std;
 
/*
 * Provides a fixed point implementation of sqrt()
 * Must be called with unsigned fixed point numbers so convert before calling, follows:
 * ap_ufixed<32, 20> in = input_number;
 * ap_ufixed<32, 20> out;
 * fxp_sqrt(out, in);
 */
template <int W2, int IW2, int W1, int IW1>
void fxp_sqrt(ap_ufixed<W2,IW2>& result, ap_ufixed<W1,IW1>& in_val)
{
   enum { QW = (IW1+1)/2 + (W2-IW2) + 1 }; // derive max root width
   enum { SCALE = (W2 - W1) - (IW2 - (IW1+1)/2) }; // scale (shift) to adj initial remainer value
   enum { ROOT_PREC = QW - (IW1 % 2) };
   assert((IW1+1)/2 <= IW2); // Check that output format can accommodate full result
   ap_uint<QW> q      = 0;   // partial sqrt
   ap_uint<QW> q_star = 0;   // diminished partial sqrt
   ap_int<QW+2> s; // scaled remainder initialized to extracted input bits
   if (SCALE >= 0)
      s = in_val.range(W1-1,0) << (SCALE);
   else
      s = ((in_val.range(W1-1,0) >> (0 - (SCALE + 1))) + 1) >> 1;
   // Non-restoring square-root algorithm
   for (int i = 0; i <= ROOT_PREC; i++) {
      if (s >= 0) {
         s = 2 * s - (((ap_int<QW+2>(q) << 2) | 1) << (ROOT_PREC - i));
         q_star = q << 1;
         q = (q << 1) | 1;
      } else {
         s = 2 * s + (((ap_int<QW+2>(q_star) << 2) | 3) << (ROOT_PREC - i));
         q = (q_star << 1) | 1;
         q_star <<= 1;
      }
   }
   // Round result by "extra iteration" method
   if (s > 0)
      q = q + 1;
   // Truncate excess bit and assign to output format
   result.range(W2-1,0) = ap_uint<W2>(q >> 1);
}
#endif

总结

这是《FPGA高层次综合HLS》系列教程第二篇,后面会按照专题继续更新,文章有什么问题,欢迎大家批评指正~感谢大家支持。

原文:OpenFPGA
作者:碎碎思

相关文章推荐

更多FPGA技术干货请关注FPGA 的逻辑技术专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
10605
内容数
562
FPGA Logic 二三事
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息