没有静态可计算的环路
L2循环条件不确定,只有运行时才知道
没有静态可计算的环路
L2循环条件不确定,只有运行时才知道
在线
边接收数据边排序
在这种情况下,我们不使用dependence指令来通知工具关于函数的先决条件,而是关于代码本身的属性。
代码本身无法直接推断动态变量old和val会不相同,因此必须加一句DEPENDENCE告诉HLS一定不会发生先写后读导致读到数据过期的情况,所以II可以是1,否则II必须为2
如果行k包含任何非0元素,那么rowPtr[k] 将包含当前行的第一个元素
例如,rowPtr[2]=4,在values[4]中为2,即原数组第2行第一个数
ap_ufixed<18,12> p = a*b;
从低往高截取
inline
这时候,inline(内联指令) 就派上用场了。 如图 2.11 所示,一旦你在子函数上加了 inline:
HLS 会直接抹除函数的物理边界。
所有的子逻辑全部暴露给顶层,融合成一坨巨大的组合逻辑。
好处: HLS 获得了“上帝视角”,它可以跨界进行资源共享和操作链接(Operation Chaining),从而缩短延迟(Latency),省下一些边界上的寄存器。
坏处与警告: 就像之前提到的“循环完全展开”一样,如果你的顶层逻辑极度复杂,你还把所有子模块都 inline 进来,这会生成一个无比庞大的数据流图。Vivado HLS 会被这海量的约束关系搞得内存溢出、耗费几个小时都综合不出来,甚至导致布线失败。
一句话总结: 层次化结构(不内联)是帮 EDA 工具减负,让代码结构模块化,但可能损失一点点跨界优化的性能;内联(Inline)是帮硬件提速,让逻辑融会贯通,但极容易让综合工具崩溃。合理的架构设计,就是在两者之间找平衡。
static
static 意味着这是必须保存状态的物理寄存器(D触发器)。你需要 4 个独立的滤波器同时运行,就必须在芯片上分配 4 套完全不同的物理寄存器。写 4 个不同的函数名,就是强迫 HLS 去生成 4 个独立的物理黑盒子。(注:在现代 HLS C++ 开发中,我们通常用面向对象的 Class/Template 或 HLS Allocation 指令来优雅地解决这个问题,而不需要傻傻地复制 4 遍代码)。
无符号:ap_uint有符号:ap_int
它是一个由 Xilinx 提供、专为硬件综合定制的 C++ 第三方类库。如果你试图脱离 Vivado HLS 环境,用纯 GCC 或 Clang 编译器去编译这段代码且不指定 Xilinx 头文件的路径,编译器是会报错找不到 ap_int 定义的。
虽然它是 Vivado HLS 专有的,但它完全符合 C++ 的语法规范。 实际上,ap_int 和 ap_uint 是 Xilinx 工程师用 C++ 写好的 模板类(Template Classes)。
当你 #include "ap_int.h" 时,你实际上引入了一大堆 C++ 的类定义。
这些类内部使用了 C++ 运算符重载(Operator Overloading) 技术,重载了 +、-、*、>>、== 等所有常用运算符。
这就使得你在写代码时,操作 ap_uint<12> a; 感觉就像在操作一个普通的 int 一样自然,但底层其实是在调用 Xilinx 写好的类方法进行位运算。
HLS通过数据输出就绪的时钟周期来计算延迟。在这种情况下,最后一个数据在第43周期准备好。也就等效于在第43周期结束第44个周期开始时写入一个寄存器。
在第 44 个时钟周期结束那一瞬间,最终的累加结果 acc 已经被成功写入了寄存器。至于第 45 个周期硬件还在那里傻傻地检查退出条件、清理状态机,这些动作对下游接收数据的模块来说毫无意义。 因为在第 44 拍末尾,结果已经有效(Valid)了。所以,HLS 的报告会减去最后那个无效等待周期,告诉你:“下游模块在第 44 个周期就能拿走数据了”。
完整的循环展开实现程序最大程度的并行性,这样的代价是需要很多资源。因此,可以在“较小”的循环上执行完整的循环。但是大迭代次数的循环展开(例如迭代一百万次)通常是不可行的。通常情况下,Vivado HLS将运行很长一段时间(并且往往在经过几个小时综合之后都会失败),如果这样的循环进行展开,它展开结果会是生成非常大的代码。
对于微小循环(Micro-loops),尽情使用“完全展开”来榨干延迟;但对于大循环(Macro-loops),必须老老实实使用带有特定因子的“部分展开(Partial Unroll)”再配合“流水线(Pipeline)”,这才是寻找最佳性价比硬件架构的核心能力。
shift_reg[−1]
硬件上会表现为全1(-1的补码),读出来的数可能全是XXX
操作链接
操作链接(Operation Chaining)是指在硬件电路中,将多个“组合逻辑(Combinational Logic)”首尾相连,中间不插入任何寄存器(Register/FF),从而让这些串联的计算在“同一个时钟周期内”一口气完成的综合技术。
Shift_Accum_Loop:
加上标签,可以在TCL和GUI中实现更明确的定位,如果不手动写标签,就会被HLS工具自动赋值,导致混乱和意义不明:
在 Tcl 脚本中精准,定位你可以直接在配套的 directives.tcl 文件里写:
set_directive_unroll "fir/Shift_Accum_Loop"
set_directive_pipeline -II 1 "fir/Shift_Accum_Loop"
data_t *y
输出端口 赋值参数值
也可以改成data_t &y 使用“引用”的方式接收参数
但是不能使用以下情况:
多级指针(ptr):硬件很难解析运行时的多级地址跳转。
动态指针(malloc/free):FPGA 的硬件资源(寄存器、连线)在编译(综合)时就必须固定下来,不可能在电路运行期间“动态生成”新的物理内存。
指针的随意转型(Casting):例如将 float * 强转为 int *,这会导致硬件总线位宽和内存对齐出现混乱。
data_t x
只读输入引脚
怎样将图1.1中的查找表编写成一个XOR门呢?一个OR门?我们需要一个几位输入的查找表?
修改真值表逻辑,根据实际需求设计输入的位数
一个2位输入的查找表最多可以被编写出多少种形态?一个n位输入的查找表呢?
4,2 ^n
来自于用户所给的限制,如果用户规定了在综合中可用的操作数,这其实是给处理率添加了限制条件
例如,资源DSP本身数量有限,用户给出使用DSP使用数量的限制,HLS工具就会在此限制下获得RTL硬件结构
限制来自于内存,因为大多数内存每周期只支持一定次数的访问
例如BRAM,一个时钟周期至多同时读写两个数,一旦超过2个数,就会受到资源的限制,无法展开循环,流水线并行
这里是指某个部件的计算需要这个部件之前一轮计算的结果
即数据依赖。例如求和累加,只有在当前运算数累加完成并反馈之后,才能计算下一个数,在顺序话结构中数据依赖不会影响数据效率,因为其本身就是一个一个实现的,而在流水线并行结构下,这种数据依赖的关系就很难实现了
默认
为了优先保证HLS的C代码可以转化成硬件,会以牺牲并行化结构为代价,用顺序化的状态机实现逻辑功能。这保证了功能实现的下限,无论怎样复杂的结构,都能实现。而加入HLS pipeline这样类似的指令后,可以通知HLS尽可能利用流水线和并行化,即函数流水,但不能保证所有代码都能被设计为并行结构
void fir(int input, int *output, int taps[NUM_TAPS]);
双文件结构。这里是main.c,用于测试输入输出是否正确,不会综合成硬件代码;fir.c中才会包含fir的具体实现
HLS中的流水和处理器中的流水概念相似,但是不再使用处理器中操作分5个阶段并把结果写入寄存器堆的方法,Vivado HLS工具构造的是一个只适用于特定板子,可以完成特定程序的电路,所以它能更好的调整流水的阶段数量,初始间隔(连续两组数据提供给流水之间的间隔),函数单位的数量和种类,还有所有部件之间的互联。
传统CPU: 固定的5级流水线(取指、译码、执行、访存、写回)。无论什么程序,都要硬塞进这个固定的物理结构中。
Vivado HLS: 它生成的是定制化电路。它可以根据你的C/C++代码逻辑,自由决定流水线的级数、并行展开的程度、以及各个计算单元之间的连线。它可以做到“因地制宜”。
生产力(throughput)
吞吐量 = 工作频率 / 任务间隔(II)
通过计算一个任务输出到输入之间这个过程需要最大的寄存器数来决定周期。因此,0周期的任务延迟是可以实现的,也就是组合逻辑下路径上没有任何寄存器。
HLS工具通过分析数据从输入到输出的最长路径(关键路径),并在中间插入寄存器(Registers)来打拍(切割时序)。插入的寄存器层数,决定了任务延迟的周期数。 “0周期任务延迟”: 意味着输入和输出之间没有任何寄存器,纯粹由组合逻辑(Combinational Logic)构成。信号像水流一样在一个周期内直接从输入端流到输出端。
32000 x 1比特,16000 x 2比特,8000 x 4比特
完整BRAM为36K,可配置为32K1,16K2,8K4,4K9,2K18,1K36,512*72。位宽大于4bit时,需要额外的校验位,8bit需要1位校验位,16bit需要2位校验位,以此类推
图1.1中的c部分就是由1个三位输入查找表和1个触发器组成的slice。slice可以变得更加复杂一点,比如加入全加器。FPGA内部通常有一些定义好的全加器slice,这看起来有点违背FPGA的"可编写性"--它只可以执行加法操作。但实际上使用全加器在硬件设计中太过于常见,把所有的全加器每次重新编写成一个slice会降低效率。灵活性和高效综合考虑,一些被配置好的slice是一个对整个系统有益的设计
加入固化的加法器结构减弱了slice的可编程性(灵活性),但在更多情况下,减少了由LUT组成加法器的消耗,提升了效率