目录
1.什么是FPGA?
1.1 简介
1.2 架构
1.3 FPGA并行方式与处理器对比
2.硬件设计基本概念
2.1 时钟频率
2.2 延迟
2.3 吞吐率
2.4 存储布局
3.高层次综合
3.1 概述
3.2 运算
3.3 条件语句
3.3循环
3.5 函数
3.6 动态内存申请
3.7 指针
4.以计算为中心的算法
5.以控制为中心的算法
6. Vivado HLS中的仿真
6.1 概述
6.2 软件测试台
6.3 代码覆盖率
6.4 未初始化变量
6.5 内存越界
6.6 C-RTL 联合仿真
6.7 C++仿真无法适用的情况
7.多个程序的集成
7.1 概述
7.2 AXI
8. 完整应用的验证
8.1 Overview
8.2 Standalone Compute Systems
8.2.1 Module Verification
8.2.2 Connectivity Verification
8.2.3 Application Verification
8.2.4 Device Validation
8.3 Processor-Based Systems
8.3.1 Hardware in the Loop Verification
1.什么是FPGA?
1.1 简介
FPGA是一种可编程实现不同算法的集成电路。现代FPGA设备包括多达百万个逻辑单元,可被配置实现多种不同的软件算法。FPGA的优势是动态可编程。在使用FPGA时,需要了解各种硬件资源以达到充分利用硬件特性的目的。
1.2 架构
FPGA的基本组成如下:
-
Look-up table(LUT):执行逻辑操作
-
Flip-Flop(FF):存储LUT计算结果的寄存器单元
-
Wires:连接各个单元
-
Input/Output(I/O)pads:FPGA的输入输出数据端口
这些元素的组合构成了FPGA的基础架构, 这些结构可以实现任何算法,但是执行效率仍然受到计算吞吐量、需要的资源和时钟频率的制约。
当代FPGA架构包含基本元素以及额外的计算和数据存储块,从而提高设备的计算密度和效率。
-
用于分布式数据存储的嵌入式存储器
-
用于以不同时钟速率驱动FPGA结构的锁相环(PLL)
-
高速串行收发器
-
片外存储器控制器
-
乘加块
这些元素的组合为FPGA提供了实现在处理器上运行任何软件算法的灵活性,并形成了当代FPGA体系结构。
LUT
LUT是FPGA的基本构建块,可以实现N个boolean变量的任意逻辑函数。本质上,这个元素是一个真值表,其中不同的输入组合实现不同的函数以产生输出值。真值表的大小限制为N,其中N表示LUT的输入数。对于N输入的LUT,表的存储地址是 2N ,这使得该表可以实现 2NN 种功能。Xilinx FPGA设备的N的典型值是6。
LUT的硬件实现可以看作是连接到一组多路复用器的存储单元的集合。LUT的输入充当多路复用器上的选择器位,以选择给定时间点的结果。记住这种表示很重要,因为LUT既可以用作函数计算引擎,也可以用作数据存储元素。FPGA没有采用门的方式,而是利用查找表得到逻辑函数的输出,即LUT的本质就是存放了真值表的一块存储。假设有6位输入和1位输出,该输出与输入之间的关系用26就可以完全表示,因此生成64位宽度,1位深度的查找表就可以完全表示该逻辑函数关系。
Flip-Flop
flip-flop是FPGA的基本存储单元。这个单元总是和一个LUT结对来辅助逻辑流水线和数据存储。flip-flop的基本结构包括一个数据输入、时钟输入、时钟使能信号、复位信号和数据输出。在正常操作期间,数据输入端口上的任何值都被锁存,并在时钟的每个脉冲上传递到输出。时钟使能引脚的用途是允许触发器为多个时钟脉冲保持特定值。当时钟使能信号为0时,数据会一直锁存。当时钟和时钟使能均等于1时,数据被传递到数据输出端口。
DSP
Xilinx FPGA中最复杂的计算块是DSP块。DSP模块是嵌入FPGA结构中的算术逻辑单元(ALU),由三个不同模块组成的链组成。DSP中的计算链由一个加法/减法单元组成,该加法/减法单元连接到一个乘法器,该乘法器连接到一个最终的加法/减法/累加引擎。此链允许单个DSP单元实现以下形式的功能: 或
FPGA设备包括可用作随机存取存储器(RAM)、只读存储器(ROM)或移位寄存器的嵌入式存储器元件。这些元件是块RAM(BRAM)、UltraRAM块(URAM)、LUT和移位寄存器(SRL)。
-
BRAM BRAM是一个双端口RAM模块,实例化到FPGA结构中,为相对较大的数据集提供片上存储。两种类型的BRAM存储器在该设备可容纳18K或36K bit。这些可用存储器的数量是特定于设备的。这些存储器的双端口特性允许对不同位置进行并行、相同的时钟周期访问。就数组在C/C++代码中的表示方式而言,BRAM可以实现RAM或ROM。唯一的区别是数据写入存储元素的时间。在RAM中,数据在运行的任意时间都可读可写。相反,在ROM配置中,只能在电路运行时读取数据。ROM的数据是作为FPGA配置的一部分写入的,并且不能被以任何方式修改。
-
UltraRAM UltraRAM块是双端口、同步288 Kb RAM,具有4096位深和72位宽的固定配置。它们可在UltraScale+设备上使用,提供的存储容量是BRAM的8倍。
-
LUT 如前所述,LUT是在设备配置期间写入真值表内容的小型存储器。由于Xilinx FPGA中LUT结构的灵活性,这些块可用作64位存储器,通常称为分布式存储器。这是FPGA设备上可用的最快的内存,因为它可以在结构的任何部分实例化,从而提高实现电路的性能。
-
SRL 移位寄存器是相互连接的寄存器链。此结构的目的是提供沿计算路径的数据重用,例如使用filter。例如,基本滤波器由一系列乘法器组成,这些乘法器将数据样本与一组系数相乘。通过使用移位寄存器存储输入数据,内置数据传输结构在每个时钟周期将数据样本移动到链中的下一个乘法器。图2-6显示了移位寄存器的示例。
1.3 FPGA并行方式与处理器对比
与处理器体系结构相比,构成FPGA的结构能够在应用程序执行中实现高度并行。Vivado HLS编译器为软件程序生成的自定义处理体系结构提供了不同的执行范例,在决定将应用程序从处理器移植到FPGA时,必须考虑到这一点。
处理器中的程序执行
任何类型的处理器,执行程序时都是以指令序列的形式,该序列是被翻译成和软件应用相关的计算。指令序列是处理器编译器工具生成的,例如GCC将c/c++代码翻译为汇编语言。处理器编译器的工作是将如下形式的c函数:
z=a+b
翻译为汇编代码:
ADD $R1,$R2,$R3
汇编代码以处理器内部寄存器的形式定义操作来计算输出值。上述代码只定义了计算部分,完整的加载-计算-写回的汇编如下:
LD a,$R1
LD b,$R2
ADD $R1,$R2,$R3
ST $R3,c
即使一个简单的操作如加法也会生成多条汇编指令。每一条指令的计算延迟并不相等,取决于指令的类型。
例如,根据a和b的位置,LD操作需要不同的时钟周期数来完成。如果这些值在cache中,这些加载操作将在几十个时钟周期内完成。如果这些值位于主存中,则操作需要数百到数千个时钟周期才能完成。如果这些值位于硬盘驱动器中,则加载操作需要更长的时间才能完成。这就是为什么软件工程师需要缓存命中,花费大量时间重新构造算法,以增加内存中数据的空间局部性,从而提高缓存命中率并减少每条指令花费的处理器时间。
FPGA中的程序执行
FPGA是一种固有的并行处理结构,能够实现任何逻辑功能以及可以在处理器上运行的算术功能。主要区别是vivado用于将软件描述转换为RTL(register transfer level)的HLS编译器不受cache和统一内存空间的约束。
z的计算由Vivado HLS编译成实现输出操作数大小所需的LUT。例如,假设程序中变量a、b、z的数据类型是16bit的short类型,则有vivado hls实现为16个LUT。通常情况下,1bit计算由1个LUT负责。
计算z的LUT只能用于该操作,不具有共享性。在处理器中,所有的操作共享相同的ALU。FPGA实现为软件算法中的每次计算实例化独立的LUT集合。
为了给每个计算赋予独一无二的LUT,FPGA的内存架构与访存开销与处理器大不相同。在FPGA中,vivado hls编译器将内存排列到多个靠近操作中使用点的存储库中。这会导致瞬时的内存带宽,远远超过处理器的能力。例如Xilinx Kintex®-7 410T设备共有1590个18 k位内存可用。在内存带宽方面,该设备的内存布局为软件工程师提供了寄存器级每秒0.5M位和BRAM级每秒23T位的容量。
关于计算吞吐量和内存带宽,Vivado HLS编译器通过调度、流水线和数据流过程来实现FPGA结构的功能。虽然对用户透明,但这些过程是软件编译过程中不可或缺的阶段,可以提取软件应用程序的最佳电路级实现。
-
调度
调度是在不同操作间确定数据和控制依赖以决定执行时间的过程。在传统FPGA设计中,这是一个手动过程,也称为硬件的软件算法并行化。
Vivado HLS分析相邻操作之间以及跨时间的依赖关系。这允许编译器将操作分组以在同一时钟周期内执行,并设置硬件以允许函数调用重叠。函数调用执行的重叠消除了处理器限制,处理器限制要求当前函数调用在同一组操作的下一个函数调用开始之前完全完成。这一过程称为流水线。
-
流水线
流水线是一种数字设计技术,它允许设计者在算法硬件实现中避免数据依赖并提高并行度。原始软件实现中的数据依赖性保留为功能等效,但所需的电路被划分为一系列独立的级。链中的所有级在相同的时钟周期上并行运行。唯一的区别是每个阶段的数据来源。计算中的每一级从前一级在前一时钟周期内计算的结果接收其数据值。例如为了计算 ,Vivado HLS编译器实例化一个乘法器和两个加法器块:
矩形盒子代表由flip-flop实现的寄存器。每一个盒子可以算作一个时钟周期。在流水化版本中,计算y的值总共需要花费3个时钟周期。通过添加寄存器,每个块在时间上被隔离为单独的计算部分。
这意味着带有乘法器的部分和带有两个加法器的部分可以并行运行,并减少函数的总体计算延迟。通过并行运行数据路径的两个部分,块基本上并行计算值y和y',其中y'是下一次执行等式的结果。y的初始计算(也称为管道填充时间)需要三个时钟周期。该初始计算之后,每个时钟周期的输出上都有一个新的y值,因为计算管道包含用于当前和后续y计算的重叠数据集。
piplining通常在执行多个操作时才有优势。如上图,执行多次的相同操作,最左边是新加载的本次计算的数据,中间是上一次的数据,最右边是上两次的数据。每一个运算部件一直处于运转中,并不会等一次操作彻底完成后才开始新的任务,只要不产生依赖,运算单位就会像工厂中的流水线一样运转。
-
数据流
数据流的目标是在粗粒度级别上表示并行性。就软件执行而言,这种转换适用于单个程序中函数的并行执行。
Vivado HLS通过基于输入和输出评估程序不同功能之间的交互来提取这种级别的并行性。 最简单的并行情况是函数在不同的数据集上工作并且彼此不通信。 在这种情况下,Vivado HLS为每个函数分配FPGA逻辑资源,然后独立地运行模块。更复杂的情况是,一个函数为另一个函数提供结果,这是软件程序中的典型情况。这种情况称为消费者-生产者情景。
Vivado HLS支持消费者-生产者场景的两种使用模型。在第一个使用模型中,生产者在消费者开始操作之前创建一个完整的数据集。并行性是通过实例化一对排列为内存库ping和pong的BRAM内存来实现的。在函数调用期间,每个函数只能访问一个内存库(ping或pong)。当新的函数调用开始时,HLS生成的电路切换生产者和消费者的内存连接。这种方法保证了函数的正确性,但限制了跨函数调用的可实现并行性级别
在第二种使用模式中,消费者可以开始使用生产者的部分结果,并且,可实现的并行级别被扩展为包括函数调用中的执行。Vivado HLS为这两个功能生成的模块通过使用先进先出(FIFO)内存电路连接。这个存储器电路,作为一个队列在软件编程中,提供模块之间的数据级同步。在函数调用期间的任何时候,两个硬件模块都在执行其编程。唯一的例外是消费者模块在开始计算之前等待生产者提供一些数据。在Vivado HLS术语中耗电元件模块的等待时间称为间隔或启动间隔(II)。
2.硬件设计基本概念
FPGA和处理器之间的一个关键区别是处理架构是否固定。这种差异直接影响每个目标的编译器的工作方式。在处理器中,计算架构是固定的。编译器的工作是在可用处理架构中决定如何最好满足软件应用需求。性能是应用程序映射到处理器功能的程度以及正确执行所需的处理器指令数的函数。
相比之下,FPGA类似于一块有一盒构建块的白板。Vivado HLS编译器的工作是从最适合软件程序的构建块盒中创建处理架构。指导Vivado HLS编译器创建最佳处理体系结构的过程需要硬件设计概念的基础知识。
2.1 时钟频率
时钟频率是确定特定算法的执行平台时要考虑的首要项。
这一通用指南之所以具有误导性,与处理器和FPGA之间时钟频率的标称差异有关。
Porcessor | FPGA |
---|---|
2GHz | 500 MHz |
对上表的简要分析很容易得出一个不正确的结论:处理器的性能是FPGA的4倍。但两个平台的差异并不仅在于时钟频率。
最主要的差异是软件程序如何执行的。处理器能够在通用硬件平台上执行任何程序。通用平台是处理器的核心,定义了一个固定的体系结构,所有软件都必须安装在该体系结构上。编译器内置对处理器体系结构的理解,将用户软件编译成一组指令。
指令执行总是以相同的顺序:
取指(IF):从存储中获取指令
-
译码(ID):以事先规定好的格式解析指令
-
执行(EXE):在ALU或FPU中进行计算。在指令处理的这一阶段,专用处理器将固定功能加速器添加到标准处理器的功能上。
-
存储操作(MEM):使用存储操作为下一条指令获取数据。
-
写回(WB):将结果写回寄存器或内存
以上5级流水线类似于MIPS五级流水线。
大多数现代处理器包含指令执行路径的多个副本,并且能够以一定程度的重叠运行指令。 由于处理器中的指令通常相互依赖,指令执行硬件副本之间的重叠并不完美。 在最好的情况下,只有使用处理器引入的开销阶段可以重叠。 负责应用程序计算的EXE阶段按顺序执行。这种顺序执行的原因与EXE阶段的资源有限以及指令之间的依赖性有关。
如下是最完美的重叠执行方式:
FPGA并不是在一个通用计算平台上执行所有软件。它在该程序的自定义电路上一次执行一个程序。因此,改变用户应用程序改变了FPGA中的电路。
鉴于这种灵活性,Vivado HLS编译器不需要考虑平台中的开销阶段,并且可以找到最大化指令并行性的方法。
与处理器相比,FPGA具有9倍的标称性能优势。实际数字总是特定于应用程序,但FPGA通常在计算密集型应用中的性能至少是处理器的10倍。
仅关注时钟频率而隐藏的另一个问题是软件程序的功耗。功耗的近似值如下所示:
在相同的计算工作量下,处理器的功耗高于FPGA。通过为每个软件程序创建自定义电路,FPGA能够以较低的时钟频率运行,操作之间具有最大的并行性,并且没有处理器中的指令解释开销。
2.2 延迟
延迟是完成一条或一组指令以生成应用程序结果值所需的时钟周期数。使用串行的基本处理器架构,指令的延迟为五个时钟周期。如果应用程序总共有五条指令,那么这个简单模型的总延迟为25个时钟周期。也就是说,在25个时钟周期到期之前,应用程序的结果不可用。
延迟是一个关键性能。延迟问题是通过使用流水线来解决的。在处理器中,流水线意味着在当前指令彻底完成之前就发射执行下一条指令。这允许指令集处理中所需的开销阶段重叠。通过重叠指令的执行,处理器为五条指令应用程序实现九个时钟周期的延迟。每一条指令的执行时间没有变,但是通过流水线方式,总的执行时间减小了,因为部分操作重叠执行。
在FPGA中,不存在与指令处理相关的开销周期。延迟通过运行原始处理器指令的EXE阶段所需的时钟周期来测量。对于一条指令,延迟为一个时钟周期。并行性在延迟中也起着重要作用。对于完整的五条指令应用,FPGA延迟也是一个时钟周期,如图3-4所示。由于FPGA只有一个时钟周期的延迟,所以可能不清楚为什么流水线是有利的。然而,在FPGA中进行流水线处理的原因与在处理器中进行流水线处理的原因相同,即,为了提高应用程序性能
如前所述,FPGA是一块白板,其中包含必须连接以实现应用程序的构建块。Vivado HLS编译器可以直接或通过寄存器连接块。
FPGA中的操作定时是信号从source寄存器传输到sink寄存器所需的时间长度。假设图3-5中的每个构建块需要2 ns。执行时,当前设计需要10 ns来实现该功能。
延迟仍然是一个时钟周期,但时钟频率限制为100 MHz。100 MHz频率限制源自FPGA中时钟频率的定义。对于FPGA电路,时钟频率定义为source寄存器和sink寄存器之间的最长信号传输时间。
FPGA中的流水线是插入更多寄存器以将大型计算块分解为较小段的过程。这种计算分区增加了绝对时钟周期数的延迟,但通过允许自定义电路以更高的时钟频率运行来提高性能。
下图是完成流水化后的处理架构。完全流水线意味着在FPGA电路中的每个构建块之间插入一个寄存器。寄存器的添加将电路的时间要求从10纳秒降低到2纳秒,从而使最大时钟频率达到500 MHz。 此外,通过将计算划分为单独的寄存器边界区域,每个块都可以始终处于繁忙状态,这对应用程序吞吐量产生了积极影响。 流水线的一个问题是电路的延迟。图3-5的原始电路具有一个时钟周期的延迟,但牺牲了较低的时钟频率。相反,图3-6中的电路在更高的时钟频率下有五个时钟周期的延迟。
2.3 吞吐率
吞吐量是用于确定实现的总体性能的另一个指标。
例如,图3-5和图3-6都显示了在输入数据样本之间需要一个时钟周期的实现。关键区别在于,图3-5中的实现需要输入样本之间的10ns,而图3-6中的电路只需要输入数据样本之间的2ns。在知道时基之后,第二个实现显然具有更高的性能,因为它可以接受更高的输入数据速率。
2.4 存储布局
所选实现平台的内存体系结构是可能影响软件应用程序性能的物理元素之一。内存体系结构决定了可实现性能的上限。在某些性能点上,处理器或FPGA上的所有应用程序都会受到内存限制,而不管可用计算资源的类型和数量如何。FPGA设计中的一个策略是了解内存绑定的位置,以及数据布局和内存组织如何影响内存绑定。
在基于处理器的系统中,无论处理器的具体类型如何,软件工程师都必须将应用程序安装在基本相同的内存体系结构上。这种通用性以牺牲性能为代价简化了应用程序迁移过程。
存储层次结构通常是金字塔型的,数据在其中流动对用户是透明的。为了增强程序的性能,要尽可能的利用快速的存储如cache。
为了实现这一目标,软件工程师必须花费大量时间查看缓存跟踪,重新构造软件算法以增加数据局部性,并管理内存分配以最小化程序的瞬时内存占用。 尽管所有这些技术都可以跨处理器移植,但结果并非如此。软件程序必须针对其运行的每个处理器进行调优,以最大限度地提高性能。
FPGA的不同是没有固定的片上内存架构。基于FPGA的系统可以连接到慢速和中型存储器,但在可用的快速存储器差异最大。也就是说,Vivado HLS编译器构建了一个快速内存体系结构,以最适合算法中的数据布局,而不是重新构造软件。
FPGA没有动态内存分配。
Vivado HLS编译器构建了一个针对应用程序定制的内存体系结构。这种定制的内存体系结构是由程序中内存块的大小以及在整个程序执行过程中数据的使用方式决定的。当前最先进的FPGA编译器,如Vivado HLS,要求应用程序的内存需求在编译时完全可分析。基于此,FPGA的内存只能在编译时就完全确定。
静态内存分配的好处是Vivado HLS可以以不同的方式实现数组A的内存。根据算法中的计算,Vivado HLS编译器可以将A的内存实现为寄存器、移位寄存器、FIFO或BRAM。
3.高层次综合
3.1 概述
Xilinx®Vivado®高级综合(HLS)编译器提供的编程环境与标准处理器和专用处理器上的应用程序开发环境类似。Vivado HLS与处理器编译器共享用于解释、分析和优化C/C++程序的关键技术。主要区别在于应用程序的执行目标。
通过将FPGA作为执行结构,Vivado HLS使软件工程师能够优化代码的吞吐量、功耗和延迟,而无需解决单个内存空间和有限计算资源的性能瓶颈。
3.2 运算
运算是指计算结果值所涉及的应用程序的算术和逻辑组件。此定义有意排除比较语句,因为它们是在条件语句中处理的。
在运算方面,vivado hls和其他编译器的区别主要是对设计师的限制。对于处理器编译器,固定的处理体系结构意味着用户只能通过限制操作依赖性和操纵内存布局来影响性能,从而最大限度地提高缓存性能。而vivado hls不被固定的处理平台限制,而是基于用户输入构建一个特定算法平台。这使得HLS设计器可以在吞吐量、延迟和功率方面影响应用程序性能。
对于下图所示的三条操作:
处理器的运算流程类似于下:
FPGA的默认实现类似于下:
默认生成的FPGA执行性能已经优于处理器。在处理器中数组A、B、C、D、E和F存储在单个内存空间中,一次只能访问一个。相反,HLS检测这些内存并为每个数组创建独立的内存库,这导致数组B和数组C的读取操作重叠。
在clock=1时对数组E的读取操作显示vivado hls的资源优化调度。对于存储操作,vivado hls分析包含数据的仓库和计算中值被使用的地方。虽然数组E的读取操作在可以在clock=0时发生,vivado hls自动调整将存储操作放在尽可能靠近数据使用的地方,这样可以减少临时数据的存储。在该例中,E的值在clock=2时才会使用,因此只要在clock=1时读取就可以。
vivado hls控制生成电路大小的另一种方法是提供用于调整变量大小的数据类型。vivado提供了integer、单精度和双精度的数据类型。这使得软件能够快速迁移到FPGA上,但可能会掩盖算法效率低下的问题,这是处理器中可用的32位和64位数据路径造成的。
例如figure4-1的代码只需要数组B、C和E中的20bit值。在原始的处理器代码中,这些位大小要求A、D和F能够存储64bit值来避免精度损失。Vivado HLS可以按原样编译代码,但这会导致效率低下的64位数据路径,消耗的资源超过算法所需的资源。
下图4-4显示如何使用vivado hls任意精度类型重写图4-1。这些数据类型可以在软件级别快速探索和验证算法正确性所需的最低精度。除了减少实施项目所需的资源数量,在计算中使用任意精度的数据类型可以减少完成操作所需的逻辑级别数。这反过来又减少了设计的延迟。
3.3 条件语句
条件语句是通常作为if、if-else或case语句实现的程序控制流语句。
在处理器编译器中,条件语句被转换为分支操作,这些分支操作可能导致也可能不会导致上下文切换。分支引入了影响下一步从内存中提取哪个指令的依赖关系。这种不确定性导致处理器执行流水线中出现stall,并直接影响程序性能。
在FPGA中,条件语句对性能的潜在影响与处理器中的不同。Vivado HLS创建由条件语句的每个分支描述的所有电路。因此,条件软件语句的运行时执行涉及两个可能结果之间的选择,而不是上下文切换。
3.3循环
假设无论实现平台如何,循环每次迭代都需要四个时钟周期。在处理器上,编译器被迫按顺序安排循环迭代,总运行时间为40个周期,
HLS没有这个限制,HLS为算法创建硬件,所以可以通过流水线迭代来改变循环的执行配置文件。循环迭代流水线将操作并行化的概念从循环内迭代扩展到跨迭代。
为了减小迭代延迟,vivado hls的首个动态优化是循环迭代体的操作并行。第二个优化是循环迭代流水线。这种优化需要用户输入,因为它会影响FPGA实现的资源消耗和输入数据速率。
HLS的默认执行配置和处理器的类似,执行整个循环延迟是40 clocks。
用户通过设置循环初始化间隔(II)来控制迭代流水线的级别。循环的II指定连续循环迭代开始时间之间的时钟周期数。图4-7显示了将II值设置为1后产生的调度。
为了达成这个优化目标,HLS需要在循环迭代0和1之间分析数据依赖关系和资源冲突。
-
为了解决数据依赖关系,HLS更改循环体中的一个操作。
-
为了解决资源冲突,HLS实例化资源的更多副本。
3.5 函数
函数是一种编程层次结构,可以包含运算符、循环和其他函数。HLS和处理器编译器中函数的处理与循环的处理类似。
函数调用执行的优化称为数据流优化。数据流优化指示HLS为给定程序层次结构级别的所有函数创建独立的硬件模块。这些独立的硬件模块能够在数据传输期间并发执行和自同步。
3.6 动态内存申请
动态内存申请是c/c++的一种内存管理技术,可以在程序运行时申请内存。
FPGA的内存申请必须在编译时就完全确定,用户不能使用在c/c++中的动态内存申请,HLS在综合时会忽略类似的代码。
int *A=malloc(10*sizeof(int)); //or new
上面的代码在HLS是不允许的。
int A[10];
上面的代码是自动内存分配。A数组所在内存仅在包含该数组的函数调用期间才会存储有效的值。因此函数调用负责在开始使用前填充有效数据。
static int A[10];
上面的代码表示静态内存分配。数组A的内容在函数调用时有效,一直保持到程序完全关闭。在HLS中意味着,只要电路通电,为数组A实现的内存就包含有效内容。
自动和静态内存分配技术都会增加处理器上运行的算法的总体软件内存占用。在用C/C++为FPGA实现指定算法时,最重要的考虑是用户应用程序的总体目标。也就是说,编译到FPGA时的主要目标不是创建最佳的软件算法实现。相反,当使用诸如HLS之类的工具时,目标是以一种允许工具推断最佳硬件架构的方式捕获算法,从而产生最佳实现。
3.7 指针
指针就是一块内存的地址。C/C++程序中指针的一些常见用途是函数参数、数组处理、指针到指针和类型转换。HLS支持在编译时完全可分析的指针用法。可分析的指针用法指不需要运行时信息就能完全表达和计算。
int *A=malloc(10*sizeof(int)); //不支持 int A[10]; int *pA; pA=A; //支持
第一个动态分配内存是不支持的,指针指向的地址仅在运行时才能获取。第二个是支持的,编译时A的地址就已知,因此可以用指针来指向它。
另一个受支持的内存和指针模型是访问外部内存。使用HLS时,对函数参数的任何指针访问都意味着变量或外部内存。HLS将外部内存定义为编译器生成的RTL范围之外的任何内存。这意味着存储器可能位于FPGA中的另一个函数(function)中,或者位于片外存储器的一部分,例如DDR
如上图。函数foo是hls的一个顶层模块,具有data_in输入参数。
基于对data_in的多个指针访问,hls推断该函数参数是外部内存模块,必须在硬件级别通过总线协议访问。总线协议如高级可扩展接口(AXI)协议,指定多个功能如何相互连接和通信。
4.以计算为中心的算法
以计算为中心的算法是每个任务配置一次的算法,并且在任务期间不能更改其行为。硬件中的任务与C/C++程序中的函数调用相同。任务的大小由HLS用户控制。为了正确优化这个算法,设计者必须首先决定任务的大小。任务的大小决定了生成的硬件模块需要配置的频率以及需要接收新批数据的频率。
如下是sobel边缘检测的代码:
该代码整体可以作为划分任务1:
划分任务2:
划分任务3:
在HLS编译器中,代码优化从基线编译开始。基线编译的目的是确定实现瓶颈所在的位置,并为测量不同优化的效果设置一个参考点。基线编译以尽可能少的FPGA资源和最低的输入数据速率构建算法实现。在本章中的示例中,基线编译导致每40个时钟周期1个像素的传入数据速率。
在使用HLS编译器时,流水线优化是提高输入数据速率和实现并行化的方法。流水线将大型计算划分为可并发执行的较小阶段。当应用于循环时,流水线设置循环的启动间隔。
循环II 通过影响开始i+1迭代所需的时钟周期数来控制循环的输入数据速率。设计者可以选择在算法代码中应用流水线优化的位置。
5.以控制为中心的算法
以控制为中心的算法是一种可以在任务执行期间根据系统级因素进行更改的算法。以计算为中心的算法在任务期间对所有输入数据值应用相同的操作,而以控制为中心的算法则基于当前输入端口状态确定其操作。
HLS支持for-loops、while-loops和do-while loops。
VivadoHLS编译器不区分控制语言构造和计算语言构造。对于本图中的代码,HLS为循环中的数学操作生成一个流水线数据路径。这种实现通过在循环迭代内和跨循环迭代中并行化计算来减少执行延迟。除了这个逻辑之外,VivadoHLS实现还嵌入了循环控制器逻辑。循环控制器逻辑规定了执行硬件的次数来计算y的值。
条件语句
条件语句通常在C和C++中表示为if-else语句。在硬件实现中,这将导致基于触发器值在两个结果或两条执行路径之间进行选择。这个有用的构造允许设计者在变量或函数级别上对算法施加控制。HLS编译器都完全支持这两个用例。
图6-2显示了一个if-else语句的示例,其中if语句在算法中的两个不同函数之间进行选择。VivadoHLS编译器生成的实现同时为function_a和function_b分配FPGA资源。这两个硬件电路并行运行,并在同一时钟周期内进行平衡以产生结果。原始源代码中的条件触发器用于在两个计算结果之间进行选择。
开关语句
图6-3显示了一个case用例声明和使用VivadoHLS的编译结果。编译器将case语句转换为硬件有限状态机(FSM)。FSM的数组表示状态之间的转换,并对应于代码示例中的情况转换。FSM中的每个状态还包括程序控制区域内的计算逻辑。
控制系统分类:
对于需要非常慢的响应时间的设计,最好的实现选择是处理器。这种选择允许为以计算为中心的算法编译到FPGA结构中提供更多的空间。图6-4显示了一个控制响应时间非常慢的系统的示例。
对于需要中等速度水平的设计,如慢速或中等速度类别所示,实现选择可以是更多的处理器或FPGA结构中的自定义逻辑。在这种情况下,控制算法具有一个必须作为一个硬件模块来实现的关键功能。对于这些类型的系统,硬件协同处理器的目的是为了弥补控制处理器中的通信延迟或处理速度的不足。图6-5显示了一个需要硬件协同处理元素的系统的示例。
最后一类以控制为中心的应用程序是快速响应时间类别。此类别指的是需要高于处理器能够提供的响应时间和计算吞吐量的控制应用程序。自引入HLS编译器以来,属于这类类别的算法范围已经扩大。例如,HLS编译器越来越多地用于为Zynq-7000SoC生成处理器加速器模块。
6. Vivado HLS中的仿真
6.1 概述
与处理器编译器一样,Vivado®HLS编译器输出的质量和正确性取决于输入软件。本章回顾了适用于Vivado®HLS编译器的推荐的软件质量保证技术。它给出了典型的编码错误及其对HLS编译的影响,以及每个问题的可能解决方案。它还包括一个部分,内容是关于当程序的行为不能在C/C++仿真级别上得到完全验证时该做什么。
6.2 软件测试台
验证任何hls生成的模块都需要一个software test bench:
-
为了证明针对FPGA实现的软件可以运行,且不会产生 segmentation fault
-
证明算法的功能正确性
segmentation fault:在基于处理器的执行中,segmentation fault是由一个程序试图访问一个处理器不知道的内存位置而造成的。造成此错误的最常见原因是,用户程序试图在内存分配并连接到指针之前访问内存中与指针地址相关联的位置。探测该错误的流程通常如下:
-
处理器检测到内存访问冲突,并通知操作系统(OS)。
-
操作系统向导致该错误的程序或进程发出信号。
-
在接收到来自操作系统的错误信号后,程序终止并生成一个核心转储文件进行分析
在hls生成的实现中,很难检测到segmentation fault,因为没有处理器,也没有操作系统监控程序的执行。segmentation fault的唯一指示是电路产生的不正确结果值。仅凭这一点还不足以确定分割故障的根本原因,因为有多个问题可能会触发不正确的结果计算。
在使用HLS时,建议设计人员确保software test bench在处理器上编译和执行该功能而无问题。这就保证了hls生成的实现不会导致segmentation fault。
一个良好的software test bench的特点是对一个算法的软件实现执行成千上万或数以百万计的数据集测试。这允许设计者高度自信地断言算法已被正确捕获。然而,即使有许多测试向量,有时仍然可以在FPGA设计的硬件验证期间检测到hls生成的输出中的错误。在硬件验证过程中检测到功能错误意味着software test bench不完整。将违规测试向量应用于C/C++执行中会显示不正确的语句。
不能在生成的RTL中直接修复错误。任何关于功能正确性的问题都是软件算法功能正确性的直接结果。
6.3 代码覆盖率
代码覆盖范围表示test bench代码执行的设计中的语句的百分比。这个度量可以由gcov等工具生成。
test bench必须至少获得90%的代码覆盖率分数,才能被认为是对算法的充分测试。这意味着测试向量会在case语句、条件if-else语句和循环中触发所有分支。除了总体覆盖率度量之外,由代码覆盖率工具生成的报告还提供了对函数的哪些部分被执行,而哪些部分没有被执行的洞察力
使用gcov可以展示每行代码被执行的次数:
结果表明,在为循环中发生的分配A=0从未被执行。此语句提醒用户注意条件语句可能出现的问题。条件语句,i==11,对于图7-1中表示的循环边界永远不能成立。算法必须检查这是否是预期的行为。HLS检测C/C++中不可到达的语句,如A赋值为0,作用是从电路中消除死代码。
6.4 未初始化变量
int A; int B; ... A=b*100;
变量B在未经初始化就使用,这在C++中属于未定义行为,部分编译器可能会自动赋初值为0.但HLS编译器不会这样,需要使用代码分析工具消除这种未初始化变量的行为。
6.5 内存越界
在HLS中,内存访问可以表示为数组上的操作,或者通过指针表示为外部内存上的操作。
使用HLS,访问无效地址会触发一系列事件,导致生成电路中不可恢复的运行时错误。由于HLS实现假设软件算法得到了正确的验证,因此在生成的FPGA实现中不包含错误恢复逻辑。因此,通过将图7-4中的代码实现到存储数组A值的BRAM资源元素,可以生成一个无效的内存地址。然后,BRAM发出HLS实现不期望的错误条件,并且错误无人注意。来自BRAM的无人值守错误会导致系统挂起,并且只能通过设备重新启动来解决。
为了避免上述错误的发生,推荐使用c++/c代码检查工具如valgrind来保证代码质量。Valgrind是一套旨在检查和配置C/C++程序质量的工具。该工具执行已编译的C/C++程序,并在执行过程中监视所有内存操作。此工具将标记以下关键问题:
-
未初始化变量的使用
-
非法内存访问
6.6 C-RTL 联合仿真
用于C/C++程序分析和功能测试的工具捕获了影响HLS实现的大多数问题。但是,这些工具无法验证一个连续的C/C++程序在并行化后是否保持了功能的正确性。该问题在HLS编译器中通过协同仿真的过程得到了解决。
协同仿真是指生成的FPGA实现由软件仿真过程中使用的相同的C/C++ test bench 执行的过程。HLS以一种对用户透明的方式处理C/C++ test bench与生成的RTL之间的通信。作为这个过程的一部分,HLS调用一个硬件模拟器,如Vivado模拟器,来模拟RTL在设备上的功能。该仿真的主要目的是检查用户提供的并行化指导是否没有破坏算法的功能正确性。
默认情况下,HLS在并行化之前遵循所有的算法依赖关系,以确保与原始C/C++表示的功能等价性。在不能完全分析算法依赖性的情况下,HLS采用保守的方法并服从依赖性。这可能会导致编译器生成一个无法实现应用程序的目标性能目标的保守实现。下面显示了一个在HLS中触发保守性行为的代码示例。
for(i=0;i<M;i++)
{
A[k+i]=A[i]+...;
B[i]=A[i]*...;
}
上述代码出现的问题在于数组A。对于数组A的索引由i和k控制,在该例中变量k是在编译时未知的函数参数。因此HLS不能证明写入A[k+i]和在计算B[i]时使用的A[i]是在不同的地址。基于这种不确定性,HLS假设了一种算法依赖性,迫使A[k+i]和B[i]以顺序计算,如原始C/C++中表示的那样。用户有能力覆盖这种依赖关系,并强制HLS生成一个电路,其中a[k+i]和B[i]并行计算。这种覆盖的影响只影响所产生的电路,因此只能通过协同仿真来验证。
在使用协同仿真时,必须记住,这是对在处理器上执行的并行硬件的仿真。因此,它比C/C++仿真要慢大约1万倍。同样重要的是要记住,协同仿真的目的不是为了验证算法的功能正确性。相反,其目的是检查该算法是否没有被对HLS编译器的用户指导所破坏。
6.7 C++仿真无法适用的情况
case IDLE :
*dma_start = false;
*rx_lock_page = false;
*rx_irq_ack = false;
*cs_trigger = false;
if(*tx_rts) state = TXFIFO_0;
else if(*rx_irq)
{ switch(*rx_status){
case 0x00: state = UDP_0; break;
case 0x40: state = DHCP_0; break;
case 0x20: state = ARP_0; break;
default: state = ERROR_0; break; }
}
break;
此代码显示了C中描述的UDP包处理引擎的片段。在本例中,所有指针都使用volatile关键字声明。在设备驱动程序开发中常见的volatile关键字的使用会提醒编译器指针连接到在执行函数期间可能发生变化的存储元素。每次在源代码中指定这类指针时,都必须读写这类指针。对合并指针访问的传统编译器优化也会被volatile关键字关闭。
volatile数据的问题是代码在C/C++模拟无法完全验证的行为。C/C++模拟不能在被测试函数的执行中间更改指针的值。因此,这种类型的代码只能在HLS编译后的RTL模拟中得到完全验证。用户必须编写一个RTL测试,以测试C/C++源中的每个volatile指针在所有可能的情况下所生成的电路。在这种情况下,使用协同模拟并不适用,因为它受到可用于C/C++模拟的测试向量的限制。
7.多个程序的集成
7.1 概述
就像大多数处理器运行多个程序来执行一个应用一样,FPGA实例化多个程序或模块来执行一个应用。本章主要介绍如何连接FPGA中的多个模块,以及如何使用处理器来控制这些模块。本章中的示例使用Xilinx®Zynq®-7000SoC来演示处理器和FPGA结构之间的互连。
Zynq-7000SoC是针对低功耗软件执行的新设备中的第一个。该设备将Arm®Cortex™-A9多核处理器与FPGA结构结合在单个芯片上。该设备中的集成级别消除了与协同处理器或加速解决方案相关的通信延迟和瓶颈。该设备还不需要PCIe®桥来在处理器上运行的代码和Vivado®HLS为FPGA编译的代码之间传输数据。相反,这两个计算域的互连是通过使用高级可扩展接口(AXI)协议。
7.2 AXI
AXI是Arm高级单控制器总线架构(AMBA®)系列微控制器总线的一部分。本标准定义了系统中的模块如何在彼此之间传输数据。适用于运行在Zynq-7000SoC上的应用程序的AXI通信用例是:
-
内存映射从属(Memory Mapped Slave)
-
内存映射主机( Memory Mapped Master)
-
直接点对点流(Direct Point-to-Point Stream)
AXI Reference Guide(UG761)
8. 完整应用的验证
8.1 Overview
在FPGA设计中,一个完整的应用程序是指一个硬件系统,它实现了由设计的软件表示的功能。使用FPGA的HLS主要构建两类系统:
-
独立计算系统
-
基于处理器的系统
8.2 Standalone Compute Systems
独立的计算系统是由一个或多个由hls生成的模块连接在一起以实现一个软件应用程序而创建的FPGA实现。在这些类型的系统中,算法的配置是固定的,并在设备配置期间进行加载。由HLS编译器生成的模块连接到外部FPGA引脚,用于数据传输和接受事务。一个独立系统的验证分为以下阶段:
-
模块验证
-
连接性验证
-
应用程序验证
-
设备验证
8.2.1 Module Verification
HLS生成块的模块验证在section 6进行了详细介绍。在软件协同仿真中,块的功能正确性得到充分验证后,设计者必须对块进行系统容错测试。
软件仿真和协同仿真都集中于孤立地测试算法的功能正确性。也就是说,对算法和编译模块进行测试,以确保在以理想的方式处理所有输入和输出时具有正确的功能。这种全面的测试级别有助于确保将数据提供给模块后的正确性。通过消除模块的内部处理核心作为一个可能的错误来源,它还有助于减少后期阶段的验证负担。此方法无法处理的唯一模块级问题是验证该模块是否可以从其界面上的不正确handshake中完全恢复。
系统内测试测试hls生成模块对输入和输出端口的反应。本测试的目的是为了消除作为可能崩溃或对被测模块产生不利影响的错误源的I/O行为。本方法中测试的不当用例类型有:
-
不稳定时钟信号切换
-
复位操作和随机复位脉冲
-
以不同的速率接收数据的输入端口
-
以不同速率采样的输出端口
-
接口协议违反
这些测试是系统级行为的示例,它们确保hls生成的模块在所有情况下都按预期工作。本阶段所需的测试量取决于接口的类型和集成方法。通过使用HLS默认设置来生成符合axi标准的接口,设计者可以避免编写一个包含不正确的系统级行为的详尽test bench。符合axi的接口由HLS编译器的开发人员完全测试和验证。
8.2.2 Connectivity Verification
连接性验证是检查应用程序中的模块是否正确连接的一系列测试。与模块验证一样,所需的测试量取决于系统集成方法。
手动集成流程要求用户在RTL中编写一个应用程序顶级模块,并手动连接组成应用程序的每个模块的RTL端口。这是最容易出错的流程,必须进行验证。
通过使用HLS编译器默认值和为每个模块端口生成AXI接口,可以减少所需的测试量。对于围绕AXI接口构建的系统,可以通过使用总线功能模型(BFM)来验证其连通性。BFM提供了经过xilinx验证的AXI总线和点对点连接的行为。这些模型可以用于流量发生器,这有助于证明hLS生成的RTL仿真模块的正确连接。
8.2.3 Application Verification
应用程序验证是在FPGA设备上运行该应用程序之前的最后一步。流程中的前面步骤集中于检查组成应用程序的单个算法的质量,以及检查所有算法是否正确连接。应用程序验证的重点是检查原始软件模型是否与FPGA实现的结果相匹配。如果应用程序由单个hls生成的模块组成,则此阶段与模块验证相同。如果应用程序由两个或两个以上hls生成的模块组成,验证过程从原始软件模型开始。设计者必须从要用于RTL仿真的软件模型中提取应用程序的输入和输出测试向量。由于硬件实现的构造是在多个阶段进行验证的,因此应用程序验证不需要是一个详尽的仿真。仿真可以运行尽可能多的测试向量,以便设计者对FPGA实现有信心
8.2.4 Device Validation
在使用自动化或手动集成流在RTL中组装应用程序后,设计将经过一个额外的编译阶段,以生成编写FPGA所需的二进制或位流。在FPGA设计的术语中,将RTL编译成位流被称为逻辑合成、实现和位流生成。在生成位流后,可以对FPGA设备进行编程。硬件在设计人员指定的时间内正确运行后,应用程序将被验证。
8.3 Processor-Based Systems
对于模块和连接性验证阶段,基于处理器的系统的验证流程与独立系统相同。主要的区别是,应用程序的一部分在处理器上运行。在Zynq®-7000SoC中,这意味着部分应用程序运行在嵌入式Arm®Cortex™-A9处理器上,部分由HLS编译以在FPGA结构上执行。此分区提出了一个验证挑战,可以通过使用以下技术来解决:
-
循环中的硬件(HIL)验证
-
虚拟平台(VP)验证
8.3.1 Hardware in the Loop Verification
HIL验证是一种在FPGA结构中对被测系统的部分进行仿真的验证方法。在Zynq-7000SoC中,针对该处理器的应用程序代码是在设备中实际的ArmCortex-A9处理器上执行的。用HLS编译的代码在RTL模拟中执行。图9-1显示了对Zynq-7000设备的HIL验证的概述。图中的系统是一个实验装置,包括目前可用的商业板ZC702评估板和Vivado模拟器。该图还介绍了处理系统(PS)和可编程逻辑(PL)单元的概念。PS是指 dual Arm Cortex-A9处理器,也称为处理子系统。PL指的是Zynq-7000设备内部的FPGA逻辑,它是由hls生成的模块被映射到的设备的部分。
HIL验证的主要优点是:
-
处理器模型与实际处理器之间没有模拟不一致
-
在处理器上运行的代码以FPGA设备的速度执行
-
通过RTL模拟,充分了解每个生成的模块如何操作
以上内容整理自Introduction to FPGA Design with Vivado High-Level Synthesis
相关阅读
HLS(一)Vivado高层次综合概述 - 守夜人的文章 - 知乎 https://zhuanlan.zhihu.com/p/578586431
硬件友好的高效softmax函数实现调研与分析 - 守夜人的文章 - 知乎 https://zhuanlan.zhihu.com/p/577554331
TVM第三方论文调研(一) - 守夜人的文章 - 知乎 https://zhuanlan.zhihu.com/p/573512820
VTA专题内容(二) 第三方改进调研 - 守夜人的文章 - 知乎 https://zhuanlan.zhihu.com/p/572522290
VTA专题内容(一):VTA(Versatile Tensor Accelerator)介绍 - 守夜人的文章 - 知乎 https://zhuanlan.zhihu.com/p/572468377
AI编译器中常见CPU运行优化--以TVM为例 - 守夜人的文章 - 知乎 https://zhuanlan.zhihu.com/p/570814540
基于HLS(High-level synthesis)的开源CNN加速库调研 - 守夜人的文章 - 知乎 https://zhuanlan.zhihu.com/p/569488705