HLS基础:从C语言到RTL的实现

0.前言

HLS相对于传统的硬件描述语言而言,有着独特的优势。HLS全称是High Level Synthesis,即高层次综合,基于C/C++的开发流程,可以极大地缩短IP开发周期。总的来说,这是一门人工引导加以优化的编程语言,可以方便地切数组、切流水,提高数据吞吐率与并发度,从而达到时间与空间、速度与面积的trade off。

一、基础元素、流程与指标介绍

1.基础元素到硬件资源的映射

基础元素硬件资源备注
主函数名顶层模块名唯一的,且需要额外声明顶层
子函数名子模块调用
顶层参数封装IO可定义类型、位宽等
数据类型与位宽IO口位宽可通过include<ap_fixed.h>自定义数据精度
数组BRAM、FIFO等数据的读写往往成为速度瓶颈,针对性优化
运算操作调用乘法器、与或非门等
循环以状态机控制完成

2.HLS设计:C到RTL调试顺序
以矩阵乘法为例,简单介绍HLS设计的关键步骤,顺序如下
(1)C设计
将电路功能用C语言描述,例如完成一个4*4的矩阵乘法

void matrix_mul(ap_int<8> A[4][4],ap_int<8>B[4][4],ap_int<16>C[4][4])
{
	for(int i=0;i<4;i++)
	{
		for(int j=0;j<4;j++)
		{
			C[i][j]=0;
			for(int k=0;k<4;k++)
			{
				C[i][j]=C[i][j]+A[i][k]*B[k][j];
			}
		}
	}
}

(2)C仿真
C仿真主要用于测试功能是否与预期一致。使用C语言完成testbench,同样地,C语言可以编译为对应的激励,驱动上一步骤完成的电路模块。它作为main函数存在,待测试电路作为例化的子模块调用,对应于C语言中的子函数调用。

int main()
{
	ap_int<8> A[4][4];
	ap_int<8> B[4][4];
	ap_int<16> C[4][4];

	//test data in
	for(int i=0;i<4;i++)
	{
		for(int j=0;j<4;j++)
		{
			A[i][j]=i*4+j;
			B[i][j]=A[i][j];
		}
	}
	//instance
	matrix_mul(A,B,C);

	//print the result
	for(int i=0;i<4;i++)
		{
			for(int j=0;j<4;j++)
			{
				std::cout<<"C["<<i<<","<<j<<"]="<<C[i][j]<<std::endl;
			}
		}
	return 0;
}

(3)RTL Sybthesis
这一步完成C代码到RTL代码的编译,即综合出对应的电路。查看综合后形成的报告,可以查看电路的延迟时间和资源使用情况。我们进行人工优化主要依照综合报告,针对性地加入优化选项。

(4)C-RTL Cosimulation
这一步完成C代码和RTL代码的联合仿真,作用是保证综合出来的电路功能与C代码描述出来的完全一致。为了方便观测波形,在dump时应该勾选所有的信号端口。

(5)Wave Viwer
顾名思义,这一步用于观测波形。默认情况下会启动Vivado自带的仿真器进行观测。当我们不清楚数据的读写时序时可以将信号抓出来逐个周期观察,并对应地切割数组、切割流水,以达到预期的性能指标。

3.HLS的关键指标:Latency与Througput
(1)Latency主要指顺序执行的组合逻辑电路所需要的延迟
Latency可以往大的延迟方向,往小的延迟约束则不一定能符合要求。例如100M时钟,latency最优化是10ns,可以约束到20ns,但约束为5ns则无法达到要求;
(2)Througput表现为interval,即发送相邻数据的间隔
interval可以做针对性优化,例如展开循环、切割数组、流水线执行等
下图可以说明两者的区别:
在这里插入图片描述

二、HLS语法基础

1.不支持的语法
(1)动态内存分配,如malloc(),calloc(),free();new(),delete()
因为硬件的大小是确定的,如果要使用较大的空间,可以声明为数组,映射为存储器。
(2)递归
模块调用是固定的,不能无限次反复调用

2.HLS指针
(1)含义完全明确时可用。
例如 int A[10]; int *pA; pA=A; 即把存储器A[0]地址传给pA
(2)外部存储器的调用

3.data packing
可以将数据打包为结构体,端口控制逻辑,且可以共享控制逻辑。例如调用100次,只需要一个结构体的端口控制逻辑。

4.directives
这是HLS最为独特的部分:人工引导。我们可以针对性地切割数组、展开循环、切流水。其难点是,在哪个地方加入引导,用哪种引导更为合适,从而完成更小的延迟,更小的电路面积。例如对矩阵乘法进行优化,directive选择了pipeline和unroll:

for(int i=0;i<4;i++)
	{
		for(int j=0;j<4;j++)
		{
		#pragma HLS LATENCY min=4 max=4
		#pragma HLS PIPELINE II=1
			C[i][j]=0;
			for(int k=0;k<4;k++)
			{
				//#pragma HLS UNROLL
				C[i][j]=C[i][j]+A[i][k]*B[k][j];
			}
		}
	}

三、数组操作

1.数组初始化
普通数组:做变量初始化,每次调用前会有一个初始化写入数据的状态机;
静态数组:相当于ROM,第一次写入的数据固定不变,不需要每次初始化;
备注:在AI中一般直接声明数组即可,不需初始化数值

2.移位寄存器
可变宽度的输出,常用于缓存。属于专用宏资源,需要添加头文件,ap_shift_reg.h

3.数组的优化
(1)partition,按照维度切分,切割为独立的存储器。
(2)reshape,按照维度重组,可以使得读出数据长度增加,减少读取周期。
当报warining时,提示状态机某一参数无法启动,带宽不够,可能需要切分数组。
对带宽的理解:可以执行完整功能的时钟频率。例如实现某功能要读取出ROM中所有数据,而读出数据需要10*10ns,则访问带宽为10M;若其他步骤更慢,则有效带宽由最慢的决定
对数组的访问往往会成为带宽的瓶颈

四、循环操作

1.循环的实现
在verilog中我曾经尝试用case的方法执行for循环内的每一条语句;HLS使用状态机控制。因此每个循环除了自身命令的执行时间外,还会增加两个时钟周期,用于状态机开始与结束的跳转。
以如下的循环加以说明:

 for (i=3;i>=0;i--) {
	b = a[i] + b;
  }

在这里插入图片描述
2.循环的展开
(1)unroll。循环形成的电路是由底层命令决定,如下图是一个组合逻辑电路。经过unroll,例如进行4次累加运算,则展开为4个加法器,一个周期可完成。
在这里插入图片描述
该优化策略只有在循环次数一定的情况下可用进行。循环N次,完全展开需要调用N个DSP的资源,因此循环次数不能为参数。
(2)flattening.将多重嵌套循环展开成一重循环。
例如二重循环,i=5;j=3,可以通过flattening,将3*5=15次循环展平。

for(i=0;i<5;i++){
	for(j=0;j<3;j++){
	func1();
	}
}

但是,该优化策略在只有一个底层循环执行体时才可用;例如以下情况则显然不适合做flattening:

for(i=0;i<5;i++){
	func2();
	for(j=0;j<3;j++){
	func1();
	}
}

3.pipeline
流水线是一种常用的加速方法。用两张图可以直观地表达其特点。
顺序执行:
在这里插入图片描述
流水执行:
在这里插入图片描述

  • 8
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值