0
点赞
收藏
分享

微信扫一扫

HLS实践 - 03 - 接口优化设计


写在前面

本文使用UG871的示例工程,第一个工程说明什么是块级 I/O 协议以及如何控制它们;第二个工程进行设置端口IO协议;第三个工程对数组接口进行设计;第四个工程使用AXI接口设计。

块级接口协议

新建工程并添加文件

添加UG871提供的文件如下:

将adders.c和adders.h添加到source。

adders.c

#include "adders.h"

int adders(int in1, int in2, int in3) {

// Prevent IO protocols on all input ports
#pragma HLS INTERFACE ap_none port=in3
#pragma HLS INTERFACE ap_none port=in2
#pragma HLS INTERFACE ap_none port=in1

int sum;
sum = in1 + in2 + in3;
return sum;

}

adders.h

#ifndef ADDERS_H_
#define ADDERS_H_
int adders(int in1, int in2, int in3);
#endif

完成添加后进行c综合。

此时生成的报告中的接口列表如下:

HLS实践 - 03 - 接口优化设计_#define

此时可以看到,在接口列表中,出现了start、done、idle、ready、return等信号,这些信号也可以称为块级IO的控制信号,用于对数据传输的控制。块级 I/O 协议允许 RTL 设计由独立于数据 I/O 端口的附加端口控制。 此 I/O 协议与功能本身相关联,而不与任何数据端口相关联。 默认的块级 I/O 协议称为 ap_ctrl_hs。

下表分析了ap_ctrl_hs中各个信号的功能。

Signals

Description

ap_start

该信号控制模块执行,必须置为逻辑 1,设计才能开始运行。 它应该保持在逻辑 1,直到相关的输出握手 ap_ready 被断言。 当 ap_ready 变高时,可以决定是保持 ap_start 置位并执行另一个事务,还是将 ap_start 设置为逻辑 0 并允许设计在当前事务结束时暂停。

如果 ap_start 在 ap_ready 为高电平之前被置为低电平,则设计可能没有读取所有输入端口,并且可能会在下一次输入读取时停止操作。

ap_ready

此输出信号指示设计何时准备好接受新输入。

当设计准备好接受新输入时,ap_ready 信号设置为逻辑 1,表明该事务的所有输入读取都已完成。如果设计没有流水线操作,则在下一个事务开始之前不会执行新的读取。

该信号用于决定何时将新值应用于输入端口以及是否应使用 ap_start 输入信号开始新事务。

如果 ap_start 信号未置为高电平,则当设计完成当前事务中的所有操作时,该信号变为低电平。

ap_done

该信号指示设计何时完成了当前事务中的所有操作。 此输出上的逻辑 1 表示设计已完成此事务中的所有操作。 由于这是事务的结束,因此该信号上的逻辑 1 也表示 ap_return 端口上的数据有效。 并非所有函数都有函数返回参数,因此并非所有 RTL 设计都有 ap_return 端口。

ap_idle

该信号指示设计是在运行还是空闲(无运行)。 空闲状态由该输出端口上的逻辑 1 指示。 一旦设计开始运行,该信号就会被置为低电平。 当设计完成操作且不再执行进一步操作时,该信号被置为高电平。

从波形中也可以看出每个信号的功能

HLS实践 - 03 - 接口优化设计_数组_02

修改块级接口

在directive中,点击函数名,然后进行添加指令。

HLS实践 - 03 - 接口优化设计_数组_03

  • ap_ctrl_none:无块级 I/O 控制协议。
  • ap_ctrl_hs:块级I/O 控制握手协议。
  • ap_ctrl_chain:用于控制链接的块级I/O协议。这个I/O协议主要用于将流水线块链接在一起。
  • s_axilite: 可以应用在ap_ctrl_hs或ap_ctrl_chain之外,作为AXI Slave Lite接口来实现块级I/O协议,以代替单独的离散I/O端口。

如果选择无块级 I/O 控制协议,将不会出现相应的IO控制接口信号。如下图:

HLS实践 - 03 - 接口优化设计_数据_04

端口IO协议

新建工程并添加文件

新建工程并添加文件,添加UG871提供的文件如下: 将adders_io.c和adders_io.h添加到source。

adders_io.c

#include "adders_io.h"
void adders_io(int in1, int in2, int *in_out1) {
*in_out1 = in1 + in2 + *in_out1;
}

adders_io.h

#ifndef ADDERS_IO_H_
#define ADDERS_IO_H_

void adders_io(int in1, int in2, int *in_out1);
#endif

完成添加后进行c综合。

此时默认生成的端口列表如下:

HLS实践 - 03 - 接口优化设计_#define_05

修改端口IO接口

在directive中,点击需要修改端口的变量,然后进行添加指令。

HLS实践 - 03 - 接口优化设计_数据_06

ap_none:指定端口不添加I/0协议。这个端口的参数被实现为一个没有其他相关信号的数据端口。ap none模式是标量输入的默认模式。
ap_stable模式:用于只在设备处于重置模式时才会改变的配置输入。
ap_ovld与in-out参数一起使用。当将in-out分为单独的输入端口和输出端口时,ap_none应用于输入端口,ap_vld应用于输出端口。
ap_ovld:这是指针实参的默认值,可读可写。

如果把in1修改成ap_vld,in2修改成ap_ack。综合生成的信号列表将会出现一个in1的有效信号,和一个in2的响应信号。如下图:

HLS实践 - 03 - 接口优化设计_数据_07

数组接口设计

新建工程并添加文件

添加UG871提供的文件如下: 将array_io.c和array_io.h添加到source。

array_io.c

#include "array_io.h"
void array_io (dout_t d_o[N], din_t d_i[N]) {
int i, rem;

// Store accumulated data
static dacc_t acc[CHANNELS];
dacc_t temp;

// Accumulate each channel
For_Loop: for (i=0;i<N;i++) {
rem=i%CHANNELS;
temp = acc[rem] + d_i[i];
acc[rem] = temp;
d_o[i] = acc[rem];
}
}

array_io.h

#ifndef ARRAY_IO_H_
#define ARRAY_IO_H_

#include <stdio.h>

typedef short din_t;
typedef short dout_t;
typedef int dacc_t;

#define CHANNELS 8
#define SAMPLES 4
#define N CHANNELS *

void array_io (dout_t d_o[N], din_t d_i[N]);

#endif

完成添加后进行c综合。
此时默认生成的端口列表如下:

HLS实践 - 03 - 接口优化设计_数据_08

默认情况下,只要数组在顶级函数上,就会使用ap_memory。无论数组的类型是什么(input, output, in/out), ap_memory都是默认值。

修改数组接口设计

将数组参数合成到 RAM 端口是默认设置。 可以使用许多其他选项来控制这些端口的实现方式。 如使用单端口或双端口 RAM 接口;使用FIFO接口;划分为离散端口。

HLS将 RAM 接口指定为单端口或双端口。 如果不进行选择,Vivado HLS 会自动分析设计并选择端口数量以最大化数据速率。

在这个设计中,如果想使用多个 RTL 端口实现一个数组参数,必须进行展开 for 循环并允许所有内部操作并行发生,否则多个端口没有任何好处。for 循环确保一次只能读取(或写入)一个数据样本。

所以为了将数组接口转换为双端口,首先要进行数组的循环展开优化。

在directive界面选择loop的标签进行添加循环展开优化。

HLS实践 - 03 - 接口优化设计_数据_09

尝试将端口设置为双端口RAM和FIFO,假设设置d_i为双端口RAM接口,d_o为FIFO接口。同样的方法进行插入相关指令。

HLS实践 - 03 - 接口优化设计_数组_10

数组参数d_o 已实现为具有16 位数据端口(d_o_din) 和相关输出写入(d_o_write) 和输入FIFO 满(d_o_full_n) 端口的FIFO 接口。 参数d_i 已实现为双端口RAM 接口。

HLS实践 - 03 - 接口优化设计_数组_11

性能报告:

HLS实践 - 03 - 接口优化设计_数组_12

通过使用双端口 RAM 接口,该设计可以以两倍于先前设计的速率接受输入数据。 由于 for 循环已展开,因此循环中的逻辑能够以该速率消耗数据。 默认情况下,每个循环迭代都会依次执行。 此实现代码将逻辑限制为在每次迭代中对 d_i 进行一次读取。 展开循环允许执行更多读取(但会创建 N 个逻辑副本)。 但是,在输出上使用单端口 FIFO 接口,输出数据速率与以前相同

分割RAM和FIFO接口

三种数组分割的方法 进行数组分割的三种方法如下,

Block:可以进行设置Block的方式进行分割,该分组方式是顺序分割的。
Cycllc:可以进行设置Cycllc的方式进行分割,该分组方式是轮序分割的。
Register:设置为register发方式,循环将会展开成完全并行。指令的参数设置为complete

性能评估对比:Block、Cycllc的性能基本一致,而Register(complete)的方式是最佳的。

将设计中的d_o进行分割,设置分割类型为Block,在 Vivado HLS Directive Editor 对话框中,将因子设置为 4。点击OK完成设置。

HLS实践 - 03 - 接口优化设计_数组_13

也可以对d_i进行添加分割指令,设置分割类型为Block,在 Vivado HLS Directive Editor 对话框中,将因子设置为2。点击OK完成设置。

HLS实践 - 03 - 接口优化设计_#define_14

完成插入指令后进行C综合。接口列表如下:

HLS实践 - 03 - 接口优化设计_数据_15

性能报告:

HLS实践 - 03 - 接口优化设计_#define_16

经过优化设计后,整体的执行延时降低,接口根据分割指令增加了相应的个数。

修改进行全分割

新建解决方案,修改成分割指令为完全分割。

HLS实践 - 03 - 接口优化设计_数组_17

在 Directive 选项卡中,选择 d_i 上的 RESOURCE 指令,用鼠标右键单击并选择 Remove Directive。 如果数组被划分为单独的元素,则不能将其分配给块 RAM。

完成插入指令后进行C综合。接口列表如下:

HLS实践 - 03 - 接口优化设计_数据_18

性能报告:

HLS实践 - 03 - 接口优化设计_数据_19

对比资源和性能分析:

HLS实践 - 03 - 接口优化设计_数组_20

从该图中可以看出,对数组进行展开设计可以优化性能,在一定情况下可以优化资源的使用。

AXI接口设计

新建工程并添加文件

添加UG871提供的文件如下: 将axi_interfaces.c和axi_interfaces.h添加到source,函数和上个设计相同。

axi_interfaces.c

#include "axi_interfaces.h"
// The data comes in organized in a single array.
// - The first sample for the first channel (CHAN)
// - Then the first sample for the 2nd channel etc.
// The channels are accumulated independently
// E.g. For 8 channels:
// Array Order : 0 1 2 3 4 5 6 7 8 9 10 etc. 16 etc...
// Sample Order: A0 B0 C0 D0 E0 F0 G0 H0 A1 B1 C2 etc. A2 etc...
// Output Order: A0 B0 C0 D0 E0 F0 G0 H0 A0+A1 B0+B1 C0+C2 etc. A0+A1+A2 etc...
void axi_interfaces (dout_t d_o[N], din_t d_i[N]) {
int i, rem;

// Store accumulated data
static dacc_t acc[CHANNELS];

// Accumulate each channel
For_Loop: for (i=0;i<N;i++) {
rem=i%CHANNELS;
acc[rem] = acc[rem] + d_i[i];
d_o[i] = acc[rem];
}
}

axi_interfaces.h

#ifndef AXI_INTERFACES_H_
#define AXI_INTERFACES_H_

#include <stdio.h>

typedef short din_t;
typedef short dout_t;
typedef int dacc_t;

#define CHANNELS 8
#define SAMPLES 4
#define N CHANNELS * SAMPLES

void axi_interfaces (dout_t d_o[N], din_t d_i[N]);

#endif

使用 AXI4-Stream 接口创建优化设计

在本工程中,指定要实现为 AXI4-Stream 接口的数组参数。 如果阵列被划分为通道,您可以通过设计并行传输每个通道的样本。如果 I/O 端口被配置为提供和使用单独的通道数据流,部分展开 for 循环可以确保专用硬件处理每个通道。

首先进行数据通道展开,也就是使用数组分割进行优化输出,确保可以同时输出8个通道的数据。对d_o进行添加分割指令,设置分割类型为cyclic,在 Vivado HLS Directive Editor 对话框中,将因子设置为8。点击OK完成设置。

HLS实践 - 03 - 接口优化设计_数组_21

同时设置d_o的接口为axis类型的,也就是axi-stream流的接口。

HLS实践 - 03 - 接口优化设计_#define_22

同理对输入的d_i进行设置,设置完成后在指令辅助界面如下所示:

HLS实践 - 03 - 接口优化设计_#define_23

然后对循环进行优化处理,期望把循环进行展开8份,所以使用UNROLL进行展开,部分展开的系数设置为8。

HLS实践 - 03 - 接口优化设计_数据_24

然后为了进一步并行化,可以使用pipeline指令进行流水展开操作。并且勾选上rewind操作。

在未使用rewind操作时,每执行一次for循环中间会有一个时钟周期的延时,在使用了rewind操作后,执行完上一个循环后,下一个for循环直接进流水,而不需要等待一个时钟周期的延迟。

指令优化界面此时如下图所示:

HLS实践 - 03 - 接口优化设计_数组_25

因为使用AXI协议,所以在层级接口可以使用AXI-Lite接口进行对模块进行控制。在指令控制界面添加块级的接口,声明为s_axilite。

运行c综合后出现对应的AXI总线接口,

HLS实践 - 03 - 接口优化设计_#define_26

并会自动综合生成相关驱动文件,以方便调用。

HLS实践 - 03 - 接口优化设计_#define_27

reference

  1. UG871


举报

相关推荐

0 条评论