月见樽

日隐夜现,潜龙在渊


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 站点地图

  • 搜索

EIE结构与算法映射

发表于 2019-07-22 | 分类于 硬件设计

算法基础

EIE(Efficient Inference Engine)的算法基础是一种被称为Deep Compression的神经网络压缩算法。EIE可以说是为Deep Compression量身定制的硬件,Deep Compression的算法流程如下所示:

  1. 剪枝:将小于某个阈值的权值直接置为0,这一操作引入权值的稀疏性
  2. 量化:这里的量化是一种非线性量化,通过k近邻类聚算法确定量化中心和量化间隔
  3. 编码:原文中使用霍夫曼编码压缩权值的存储,EIE中使用CSC压缩存储方式

Deep Compression压缩

Deep Compression压缩分为剪枝、量化和编码操作。其中剪枝为对所有权值做以下操作:

其中T为剪枝阈值,该步骤将所有小于剪枝阈值T的权值置为0,引入了权值的稀疏性。原文中对于VGG结构的剪枝后,卷积层的非零参数量一般还剩原参数量的30%~60%中,全连接层的非零参数量一般仅剩5%以下,由于全连接层参数占参数的主要部分,因此全网络的非零参数量仅剩下原有的7.5%。考虑VGG是比较容易产生冗余的网络,因此对其他网络的剪枝效果可能差于VGG网络。剪枝阈值T在剪枝过程中为超参数,需要综合考虑剪枝效果和剪枝后网络的性能表现多次试验确定。

量化操作为对于每个层,使用k-近邻类聚算法类聚。类聚算法产生指定数量的类聚中心,所有属于某一类的权值都被直接赋予类聚中心的值。随后使用修改过的优化算法运行一定轮数的训练,调整类聚中心的值(权值从属关系不改变),具体过程参见Deep Compression论文,这里仅考虑结果,进行完量化后,每一层的权值张量变为一个同形状的标号张量和一个解码表。标号张量标记每个位置的元素属于的类别,一般仅有2~5bit(即分为4~32类);解码表标记每个类别的数据,如下图所示:

现在考虑量化对实现的影响。原有的高精度权值张量(取$D_H$bit)的非零参数量为M,则需要的存储空间为$M \times D_H$bit。量化后权值张量改为标号张量,标号的位数一般远远低于权值数据,取为$D_L$,需要存储空间为$M \times D_L$;另考虑编码表,编码表需要的bit数为$2^{D_L} \times D_H$。则量化后权值需要的存储空间占原有比例为:

$D_L$一般来说仅有5bit(VGG网络),因此有$M >> 2^{D_L}$,则可以发现将权值的存储空间降低到$\frac{5}{32} = 15.625\%$,有效的缓解了存储瓶颈。但是权值使用时,需要根据标号张量中的标号从编码表中查询权值,再将其与输入进行运算,比原有矩阵直接运算多一步查询,需要通过硬件查询。

Deep Compression论文中为了进一步压缩权值的存储,在量化后使用霍夫曼编码压缩矩阵的存储。EIE为了方便的硬件实现,使用CSC方法压缩稀疏权值矩阵。

CSC稀疏矩阵表示

CSC(compressed sparse column)为一种稀疏矩阵的表示方法,其将一个稀疏矩阵压缩表示为三个向量。首先考虑向量的压缩方法,每个稀疏向量被压缩为两个非稀疏向量,如下所示的向量:

将其压缩为两个长度相等的向量,第一个向量为按顺序排列的所有的非稀疏元素,第二个向量为对应位置的非稀疏元素与前面一个非稀疏元素中间的0数量,上述向量压缩完成如下所示:

u为非零元素,z为两个非零元素之间0的数量。例如$v[0]=1,z[0]=2$表示第一个非0元素为1,该元素之前有2个零;$v[1]=2,z[1]=0$表示第二个非0元素为2,该元素之前没有0(原向量中为$[0,0,1,2,…$)。由于这里的z向量使用的为int4类型数据,因此第三个非零数据3之前的18个零超出了表示范围,因此在v中添加一个0元素,即其中$v[2]=0,z[2]=15$表示第三个数据为0,之前有15个0。这个数据并不是非零数据,是为了能使用int4表示18而额外补充的数据。之后的$v[3]=3,z[3]=2$为要表示的数据3,之前有2个零,和前一条一起表示间隔18个零的情况,如下图所示:

随后考虑矩阵的表示方法,CSC稀疏表示将矩阵的每一列视为一个向量进行压缩,每一列都产生一个v向量和一个z向量,第i列产生的向量$v_i$和$z_i$向量的长度和其他列均可能不同。将每一列的v向量按列号依次连接,z向量按列号依次连接,获得矩阵的v和z向量,为了区分不同列,额外引入u向量,u向量长度为列数加1,表示每一列的v或z向量在矩阵v和z向量中的位置,即第i列的v和z向量在矩阵的v和z向量的第$u[i]$个到第$u[i+1] - 1$元素之间,u[0]固定为0。如下图所示:

最终,一个稀疏矩阵将被压缩到三个向量U、V和Z中,该方式仅保存非零数据(为了表示超过Z限制额外引入的0除外),同时Z和U向量使用的数据类型一般比U小,因此可以有效的压缩稀疏矩阵。

EIE结构

PE结构

EIE(Efficient Inference Engine)作为一种Engine,主要作为加速器系统组件使用,因此论文中并未提出明确的系统架构,而是重点描述了其PE的结构,PE结构图如下:

PE按功能为以下几个部分:

  • 蓝色底色部分为缓存部分,分布缓存了CSC格式表示矩阵方法下的U、V和Z向量以及Deep Compression产生的解码表和产生的部分和输出数据。
  • 紫色底色部分为标号处理部分,标号累加为一个累加器,通过累加一个向量CSC表示中之前的元素的z部分产生该元素在向量中的实际绝对位置;列地址生成从矩阵从U向量中获取某一列的数据在V和Z向量中的起始和结束位置。
  • 橙色底色部分为算数运算部分,输入数据和解码后的权值相乘并和之前的结构相加,结果保存在输出缓存中,当运算完成时,通过ReLu单元激活后输出。

该PE如何映射运算将在后续章节[算法映射]中表述。

CSC编码器

PE运算产生的结果并不是CSC方法表示。一般来说,在ReLU函数之前的输出数据并不具有稀疏性,但是ReLU函数将所有负数输出置为0,引入了输入\输出数据的稀疏性,因此需要将输出数据进行CSC编码,CSC编码器结构如下所示:

论文中PE以4个一组,每个PE输出一个输出数据及其绝对标号,非零数据检测器从PE0的输出数据开始依次检测,若发现非0数据,则通过绝对标号计算CSC格式的相对标号,同时输出器数据和相对标号,实现CSC编码。

算法映射

矩阵-向量乘法

原论文中以4个PE为一组,计算矩阵乘法。输入权值和输入数据以下图为例:

矩阵乘法计算的目标为:

上图中,有a=8、b=8。权值矩阵的第i行数据保存在标号为$i \% 4$的PE中并由该PE负责计算。第i个PE的所有权值行向量顺序堆叠组成一个新权值矩阵$W_i,W \in R^{(a//4) \times b}$,这里新权值矩阵为2行。标号为i的PE中存储的是新权值矩阵$W_i$的CSC表示。

EIE映射算法的原理如下图所示,综合考虑输入数据和权值的稀疏性,将矩阵-向量乘法分解为多个向量相乘,当且仅当对应位置上的元素均不为0时才进行计算,因此可以减少很多0之间的运算。

EIE的PE输入为一个CSC格式压缩的稀疏向量,将每个元素的数据和标号(v和z)依次输入数据队列和标号队列。处理一个数据时,从数据队列中取出数据D并从标号队列中取出标号$I_z$,标号$I_z$通过标号累加器变为向量的绝对坐标I。以上图中所述第一个数据X0为例,其相z元素为0,即之前没有0,因此X0的绝对位置为0。输入向量CSC格式累加过程如下所示:

随后通过$I//2$查询奇数U缓存,$I // 2 + I \%2$查询偶数缓存。分别从偶数U缓存和奇数U缓存中获取地址各一个:

  • 若I为奇数,则从奇数缓存中读取的数据为起始地址$U_s$,从偶数缓存中读取的数据为结束地址$U_e$
  • 若I为偶数,则从偶数缓存中读取的数据为起始地址$U_s$,从奇数缓存中读取的数据为结束地址$U_e$

对于X0而言,对应绝对位置为0,读出起始地址为0,结束地址为1;对于X2,读出起始地址为1,结束地址为2;对于X5,读取起始地址为3,读取终止地址为4。对于$U_s = U_e$的情况,说明该输入数据对应的列无非0数据,可直接跳过该输入数据的处理过程。随后使用$U_s$和$U_e$之间的值(不包括$U_e$,即$[U_s,U_e)$)从V缓存和Z缓存中读取权值。对于X0,读出权值$W_{0,0}$和相对位置0,对于X2,读取权值$W_{0,2}$和相对位置0;对于X5,读取权值$W_{4,5}$和相对位置1。根据这些权值从编码表中查询真实权值。相对位置进行与输入相同的权值累加计算真实权值WI,计算结果分别为0、0和1。

随后输入数据与读出的真实权值依次相乘,相乘的结果与输出缓存中位置为WI的数据累加,过程如下所示:

累加完成后,输出缓存每个地址存储的就是对应绝对位置的输出结果,完成矩阵-向量乘法映射。

卷积映射

卷积映射在原论文中没有提到,一下为基于结构对映射卷积方式的猜测,其映射卷积的方式可能为将卷积拆分为多个矩阵乘法实现,如下图所示:

PE的输入为广播输入,因此所有PE的输入数据必须相同,而所有权值均为本地存储,因此权值应当不在PE之间交换,由上推测出卷积的映射方法应当将一个$K \times K$的卷积变为$K \times K$个$1 \times 1$卷积实现。上图举出了一种$2 \times 2$卷积在EIE上实现的可能方案。每个PE计算一个输出通道为CO+1,输入通道为CI+1的$1 \times 1$卷积,所有PE计算完成后,将结果错位相加即可获得$2 \times 2$卷积的计算结果,错位相加过程如下所示:

基2FFT原理

发表于 2019-07-09 | 分类于 Verilog巩固手记

FFT前置知识

FT和DFT

傅里叶变换FT(fourier transform)用于将时域信号$x(t)$和频域信号$X(f)$之间变换,公式如下所示:

对于计算机系统中,无法处理连续的过程,因此离散化为离散傅里叶变换DFT(Discrete Fourier Transform):

取$W_N = e^{-\frac{2\pi}{N}j}$,可将DFT改写为以下公式:

DFT改进(削减计算量)

首先分析原始公式的计算量,取一个8点DFT算法,对于一个点:

  • 需要复数乘法N次,每次复数乘法由四次实数乘法和两次实数加法实现
  • 需要复数加法N-1次,每次复数加法由两次实数加法构成

因此,对于一个点,需要实数乘法共4N次,实数加法共(2N-2+2N)=4N-2次。削减计算量的主要重点在$W_N$上,使用欧拉公式有:

考虑$W_N^{k+\frac{N}{2}}$的情况,有以下公式:

同理有$W_N^{k+N} = W_N$,因此以一个4点DFT为例,有以下公式:

可减少所需要的复数乘法的次数,进而减少对应的实数乘法和加法的数量

FFT

基2FFT

基2FFT指点数为$2^n$的FFT变换,取$N = 2^n$的FFT变换如下所示:

将一个N点的FFT分解为两个FFT,一个为奇数项的FFT,另一个为偶数项的FFT。对于$W_N^{nk}$而言,考虑以下变化:

带入上式,有以下:

取$FFT_1(k) = \sum\limits^{\frac{N}{2}-1}_{n=0}x(2n)W_{\frac{N}{2}}^{kn} $和$FFT_2(k) = \sum\limits^{\frac{N}{2}-1}_{n=0}x(2n+1)W_{\frac{N}{2}}^{nk}$分别是两个长度为$\frac{N}{2}$的FFT运算,有:

上述有$n < \frac{N}{2}$,考虑后半段结果,有:

同理有$FFT_2(k+\frac{N}{2}) = FFT_2(k)$,因此当$n \geq \frac{N}{2}$时,考虑$W_N$的周期性,有:

综上所述对于一个N点的FFT运算,有

其中,$FFT_1$为对偶数序列的$\frac{N}{2}$点FFT;$FFT_2$为对应奇数序列的$\frac{N}{2}$点FFT。该操作将一个N点FFT分解为两个$\frac{N}{2}$点的FFT。

蝶形运算

蝶形运算为一个二输入二输出的运算,公式如下所示:

其中$X_1,X_2$为两个输入;$Y_1,Y_2$为两个输出;W为权值,均为复数。蝶形运算可以用于映射基2FFT,首先考虑2点FFT,两点FFT公式如下所示:

因此可以使用一个蝶形运算实现,权值为$W_2^k$,现考虑一个4点FFT,首先将其分解为2个两点FFT,分解的公式为

分解步骤也可以用蝶形运算实现,因此整体运算如下图所示:

更多点数的FFT可以类似的进行,即不断分解为长度为一半的奇偶序列的FFT变换分层实现。

浮点数处理

发表于 2019-05-28 | 分类于 硬件设计

浮点数表达

IEEE754标准是用于规范浮点数运算的IEEE标准,用于解决浮点数标准混乱的问题。其被认证后不久,几乎所有的处理器生产商都采用这一标准,极大的推动了软件的发展。浮点数存储的格式如下:

浮点数由符号位,指数位和尾数三个部分组成,表达公式如下式:

在IEEE754标准中,主要规定了单精度浮点(float)和双精度浮点(double)两种浮点数:

类型 符号位数 指数位数 尾数位数
单精度浮点(float) 1 8 23
双精度浮点(double) 1 11 52

首先考虑符号位,当该符号位为0时,表示该数为正数,符号位为1时,表示该数为负数。指数可以为负数,一般使用移码表示,移码表示为:

E为真实的指数,e为浮点数中存储的尾数,bias为移位,有$bias = 2^{len(e) - 1} - 1$。以单精度浮点为例,指数位数$len(e) = 8$,则有bias=127,真实指数和存储的关系为$E = e - 127$,表示范围为-126~127(e=0和e=255用于表示特殊字符)。尾数为规格化的尾数,即尾数的二进制表示$f_b$前有一个隐藏的二进制1,即如下表示:

当e=0时,该浮点数为非规格化数,表示的数如下所示:

该标准内还定义了几个特殊值:

特殊值 说明
0 指数部分和尾数部分均为1
无穷大 指数部分为$2^{len(e)} - 1$(指数最大值),尾数部分为0
NaN 指数部分为$2^{len(e)} - 1$(指数最大值),尾数部分不为0

浮点数计算

浮点数乘法

浮点数的乘法分为以下几个步骤:

  • 计算符号位:通过异或操作计算符号位,若两个操作数符号位相同,则结果符号位为0,否则结果符号为1
  • 计算原始尾数:两个操作数的尾数相乘,得到原始尾数
  • 计算原始指数:将两个操作数的指数相加,得到原始指数
  • 规格化与舍入:对原始尾数和原始指数进行规格化,获得结果的指数,再对尾数进行舍入,获得结果的尾数

对于科学计数法表示的乘法,有:

现考虑32位的单精度浮点数(float),其指数为8位,尾数为23位,获得原始指数和原始尾数为:

  • 原始指数:原始指数为两个8位的指数相加,共9位
  • 原始尾数:原始尾数为两个23位的尾数相乘,共46位

获得原始指数和尾数后进行规格化,若原始指数小于-126,则小于表示范围,将原始尾数右移,每右移一位,原始指数+1,直到原始指数到达-126,此时形成非规格化数。若原始尾数不小于-126,进行正常的标准化:

  • 两个规格化数相乘:$1.f_1 \times 1.f_2$,结果在1~4之间,即最高2位有以下几种可能性:
    • 最高2位为01:原始尾数向左移位2位(包括移除隐含的1),原始指数直接为获得规格化的指数,小数部分还剩44位,在舍入部分处理。若原始指数-2后为-127,则在移位后尾数前添加1,使用非规格化表示
    • 最高2位为10或11:原始尾数向左移位1位(移除隐含的1),原始指数+1获得规格化的指数,小数部分还剩45位,在舍入部分处理。
  • 非规格数和规格化相乘:$1.f_1 \times 0.f_2$,结果在0~2之间,操作方式与上述类似
  • 非规格化数和非规格化数相乘:原始指数为-252,尾数部分仅有46位,无论如何都不可能使指数规格化到-126,直接为0

进行规格化后,原始指数被修正为指数$e_3$,此时若尾数的位数超过23位,还需要进行舍入操作。将规格化后的尾数使用$sf$表示,$sf[h:h-23]$表示高23位的指数,$sf[h-24:0]$表示24位以后尾数。舍入使用“四舍六入”的方式,舍入规则如下所示:

  • 若$sf[h-24:0] < 1000…0$:抛弃,舍入结果为$sf[h:h-23]$(四舍)
  • 若$sf[h-24:0] > 1000…0$:进位,舍入结果为$sf[h:h-23]+1$(六入)
  • 若$sf[h-24:0] == 1000…0$:舍向偶数,即使$sf[h:h-23]$变为偶数($sf[h:h-23]$为奇数时进位,否则抛弃)

进行舍入后,原始尾数被修正为尾数$f_3$,乘法计算完成。

浮点数加法

浮点数的加法分为以下几个步骤:

  • 对阶:将指数较小的浮点数进行尾数向右移位,指数同步增大,直到两个操作数的指数等
  • 求和:对尾数进行求和
  • 规格化:对指数和尾数做规格化,并对尾数进行舍入

对于科学计数法表示的加法,有:

第一步为对阶,即将指数变为相同以实现加法,规定小阶向大阶对阶,即原始指数$e=\max\{e_1,e_2\}$,对于指数较小的操作数,需要将尾数向右移位,每移动一位,指数加1,移位直到阶数相等即完成对阶,对阶过程可表示为:

第二步为求和,即对阶完成后,两个尾数可以直接求和获得原始尾数,求和过程如下所示:

第三步为规格化和舍入,原始尾数$f = (-1)^{s_1} \times 1.f_1 \times 2^{e_1 - \max\{e_1,e_2\}} + (-1)^{s_2} \times 1.f_2 \times 2^{e_2 - \max\{e_1,e_2\}}$,原始指数$e=\max\{e_1,e_2\}$,对其进行规格化和舍入操作,获得新的指数$e_3$和尾数$f_3$,操作方式与乘法相同,即完成浮点数的加法。

异步FIFO设计

发表于 2019-05-06 | 分类于 硬件设计

1.设计目标

设计一个参数可配置的异步FIFO,要求:

  • FIFO深度从4开始在2的幂次方连续可配(4、8、16、……)
  • 读写时钟域相位差、频率差任意(同步器参数可配)

2.参数列表

名称 默认值 说明
DEPTH_LOG 4 FIFO容量为$2^{DEPTH_LOG}$
DATA_WIDTH 8 数据位宽

3.端口

3.1.端口列表

3.1.1.系统端口

名称 类型 位宽 说明
read_clk input 1 读时钟域时钟
write_clk input 1 写时钟域时钟
rst_n input 1 系统复位端口,低有效

3.1.2.读端口

名称 类型 位宽 说明
read_req input 1 读完成信号
read_valid output 1 读数据有效信号
read_data output DEPTH_LOG 读数据
fifo_empty output 1 FIFO空信号

3.1.3.写端口

名称 类型 位宽 说明
write_req input 1 写请求信号
write_data input DEPTH_LOG 写数据
fifo_full output 1 FIFO满信号

3.2.端口时序

3.2.1.读端口时序

read_req信号拉高表示请求读数据,若此时FIFO非空(fifo_empty为低),FIFO将会将数据置于read_data上,同时拉高read_valid信号。即当read_valid有效时,对应的read_data上的数据有效。fifo_empty拉高表示FIFO已空,当前数据输出端口上的数据无意义, 再拉高read_req将不会改变read_data上的数据。

3.2.2.写端口时序

写端口如上所示,当且仅当write_req信号高且fifo_full信号低时将write_data端口上的数据写入FIFO。

4.系统结构

4.1.结构框图

系统整体结构如上所示,分为两个时钟域——读时钟域和写时钟域。每个时钟域结构相互镜像:

  • 读/写指针:二进制的读写指针,用于SRAM的读/写地址
  • 二进制到格雷码转换器:将读/写指针从二进制转为格雷码,用于传递到下一个时钟域或生产空\满信号
  • 空/满信号生成:比对读指针和写指针的格雷码,生成空和满信号

其他还有跨时钟域的组件,分别为:

  • 双口SRAM:一个端口使用写时钟和写时钟域下的信号,另一个使用读时钟和读时钟域的信号
  • 同步器:两个同步器,分别将读指针同步到写时钟域和将写时钟同步到读时钟域

4.2.系统方法

4.2.1.二进制转格雷码

假设二进制码为每位为$bin[n]$,对应的格雷码每位为$gray[n]$,共N位,转换算法为:

例如二进制码011,共3位,则格雷码第2位为0,其他几位为10,对应格雷码为010,在具体实现时,可以参考下图的实现方法:

4.2.2.格雷码判空判满

对读指针和写指针有以下含义:

  • 读指针:指向当前正在读的地址
  • 写指针:指向下一次写入操作需要写入的地址

二进制下,对于地址位宽为N的SRAM,可以使用位宽为N+1的地址——低N位为地址,MSB为标志位,用于标记满和空:

  • 当低N位相等,MSB不相等时:FIFO满(写指针领先读指针“一圈”)
  • 当低N为相等,MSB相等时:FIFO空(读指针“追上”写指针)

转换到格雷码域,做相同判断,判空条件为两个指针相等,相等的二进制码对应格雷码相等,条件不变。对于判满,需要两个二进制仅有最高位不同,参考二进制转格雷码条件,判满条件如下:

  • 最高位不相等(格雷码MSB与二进制MSB相同)
  • 次高位不相等(次高位由二进制码的最高位和次高位异或,两指针二进制下最高位不同,次高位相同)
  • 其他位均相等(异或操作依赖的位数均相等)

由于同步器的同步需要消耗时钟周期,因此:

  • 判满:在写时钟域下生成满信号,读指针通过同步器,为若干个时钟周期之前的读指针。若在FIFO满的情况下,读操作发生,读指针的变化延迟传递到写时钟域,在传递的若干个周期内状态为“假满”
  • 判空:在读时钟域下生成空信号,写指针通过同步器,为若干个时钟周期之前的写指针。若在FIFO空的情况下,写操作发生,写指针的变化延迟传递到读时钟域,在传递的若干个周期内状态为“假空”

“假满”和“假空”状态均不影响异步FIFO的正常工作,仅为略微降低FIFO的工作效率

4.2.3.同步器

同步器是一种跨时钟域数据传输的方法,二级同步器结构如下所示:

从源时钟域下的源信号开始,依次通过多个时钟为目标时钟域时钟下的寄存器,即构成了多级同步器,寄存器的数量就是同步器的级数。一般的信号仅需要二级同步器,高速信号一般使用三级同步器。

5.实现细节

5.1.写FIFO部分

写FIFO部分包括以下几个组件:

  • 同步器:将读指针从读时钟域同步到写时钟域,使用两级同步器
  • 写指针:指示写入地址的指针,当满信号拉低且写请求拉高时加1
  • 写指针二进制转格雷码:将写指针从二进制转为格雷码,送到判满部分判满和向读时钟域的同步器
  • 满信号生成:当满足[4.2.2]的判满条件成立时,拉高满信号

5.1.1.需求

写FIFO部分的需求如下:

  • 产生写SRAM的相关信号,包括写请求信号,写地址信号和写数据信号
  • 同步内部读指针,配合写指针生成满信号。将写指针传递到读部分。

5.1.2.端口

名称 类型 位宽 说明
clk input 1 写部分时钟
rst_n input 1 系统复位
write_req input 1 写FIFO请求
fifo_full output 1 FIFO满信号
write_addr output DEPTH_LOG 写存储器地址
read_point_gray input DEPTH_LOG+1 读指针格雷码,未同步
write_point_gray output DEPTH_LOG+1 写指针格雷码

5.1.3.实现

上图为fifo_full生成部分的结构图,为了保证保证fifo_full拉高的及时性,设置next_write_point_gray寄存器,指示下一个格雷码的写地址。当一次is_write拉高时,每个部件的功能如下所示:

  • write_point:自增1
  • write_point_gray:从next_write_point_gray获取与write_point同步的格雷码
  • next_write_point_gray:取现在write_point加2后对应的格雷码,获得与write_point+1同步的格雷码

写产生FIFO满的波形如下所示:

当一次写请求使FIFO满时,由于写请求发生,因此使用next_write_point_gray进行判满操作,此时该信号与read_point_gray相等,因此在下一个时钟周期fifo_full拉高(图中a,b->c),同时,write_point和write_point_gray均完成更新。下一个时钟周期,无写请求发生,因此使用write_point_gray进行判满操作,此时write_point_gray更新后与read_point_gray相等,保持fifo满状态(图中e,d->f)。

对于数据部分,如下图所示:

该部分不包括在写控制模块中,系统输入的write_data端口直接连接到SRAM的写数据端口,写请求端口为write_req和fifo_full的组合逻辑与信号(write_req && !fifo_full)

5.2.读FIFO部分

读FIFO部分包括以下几个组件:

  • 同步器:将写指针从写时钟域同步到读时钟域,使用两级同步器
  • 读指针:指示读取地址的指针,当空信号拉低且读请求拉高时加1
  • 读指针二进制转格雷码:将读指针从二进制转为格雷码,送到判空部分判空和向写时钟域的同步器
  • 空信号生成:当满足[4.2.2]的判空条件成立时,拉高空信号

5.2.1.需求

读FIFO部分需要满足以下几个需求:

  • 产生读SRAM的相关信号,包括地址信号,数据有效信号
  • 同步内部写指针,配合读指针生成空信号。将读指针传递到读部分。

5.2.2.端口

名称 类型 位宽 说明
clk input 1 读时钟信号
rst_n input 1 系统复位信号,低有效
read_req input 1 读请求信号
fifo_empty output 1 FIFO空信号
memory_read_addr output DEPTH_LOG SRAM的读地址信号
read_point_gray output DEPTH_LOG+1 读指针格雷码
write_point_gray input DEPTH_LOG+1 写指针格雷码,未同步
read_valid output 1 读数据有效信号

5.2.3.实现

实现的方式与写部分类似,fifo_empty信号和读指针生成如下所示:

主要部件的功能如下所示:

  • read_point:读指针,产生读地址,每当读请求成立时自增1
  • read_point_gray:读指针的格雷码,用于产生空逻辑和向写时钟域传递
  • next_read_point_gray:下一个读指针的格雷码,用于空信号的及时性

读数据部分如上图所示,read_valid信号在read_req && !fifo_empty为真时拉高,表示当前read_data上的数据有效。read_data为输出数据端口直接连接到SRAM的输出端口(SRAM输出端口自带寄存器)。

Octave卷积学习笔记

发表于 2019-05-05 | 分类于 神经网络

Octave卷积

Octave卷积的主题思想来自于图片的分频思想,首先认为图像可进行分频:

  • 低频部分:图像低频部分保存图像的大体信息,信息数据量较少
  • 高频部分:图像高频部分保留图像的细节信息,信息数据量较大

由此,认为卷积神经网络中的feature map也可以进行分频,可按channel分为高频部分和低频部分,如图所示:

对于一个feature map,将其按通道分为两个部分,分别为低频通道和高频通道。随后将低频通道的长宽各缩减一半,则将一个feature map分为了高频和低频两个部分,即为Octave卷积处理的基本feature map,使用X表示,该类型X可表示为$X = [X^H,X^L]$,其中$X^H$为高频部分,$X^L$为低频部分。

为了处理这种结构的feature map,其使用了如下所示的Octave卷积操作:

首先考虑低频部分输入$X^L$,该部分进行两个部分的操作:

  • $X^L \to X^H$:从低频到高频,首先使用指定卷积核$W^{L \to H}$进行卷积,随后进行Upample操作生成与高频部分长宽相同的Tensor,最终产生$Y^{L\to H} = Upsample(Conv(X^L,W^{L \to H}),2)$
  • $X^L \to X^L$:从低频到低频,这个部分为直接进行卷积操作$Y^{L \to L} = Conv(X^L,W^{L \to L})$

随后考虑高频部分,与低频部分类似有两个部分的操作:

  • $X^H \to X^H$:从高频到高频,直接进行卷积操作$Y^{H \to H} = Conv(X^H,W^{H \to H})$
  • $X^H \to X^L$:从高频到低频,首先进行stride和kernel均为2的平均值池化,再进行卷积操作,生成与$Y^L$通道数相同的feature map,最终产生$Y^{H \to L} = conv(avgpool(X^H,2),W^{H \to L}))$

最终,有$Y^L = Y^{H \to L} + Y^{L \to L}$和$Y^H = Y^{H \to H} +Y^{L \to H}$,因此可以总结如下公式:

因此有四个部分的权值:

来源/去向 $\to H$ $\to L$
H $W^{H \to H}$ $W^{H \to L}$
L $W^{L \to H}$ $W^{L \to L}$

另外进行使用时,在网络的输入和输出需要将两个频率上的Tensor聚合,做法如下:

  • 输入部分,取$X = [X,0]$,即有$X^H = X$,$X^L = 0$,仅进行$H \to L$和$H \to H$操作,输出输出的低频仅有X生成,即$Y^H = Y^{H \to H}$和$Y^L = Y^{H \to L}$
  • 输出部分,取$X = [X^H,X^L]$,$\alpha = 0$。即仅进行$L \to H$和$H \to H$的操作,最终输出为$Y = Y^{L \to H} + Y^{H \to H}$

性能分析

以下计算均取原Tensor尺寸为$CI \times W \times H$,卷积尺寸为$CO \times CI \times K \times K$,输出Tensor尺寸为$CO \times W \times H$(stride=1,padding设置使feature map尺寸不变)。

计算量分析

Octave卷积的最大优势在于减小计算量,取参数$\alpha$为低频通道占总通道的比例。首先考虑直接卷积的计算量,对于输出feature map中的每个数据,需要进行$CI \times K \times K$次乘加计算,因此总的计算量为:

现考虑Octave卷积,有四个卷积操作:

  • $L \to L$卷积:$C_{L \to L} = \alpha^2 \times (CO \times \frac{W}{2} \times \frac{H}{2}) \times (CI \times K \times K) = \frac{\alpha^2}{4} \times C_{conv}$
  • $L \to H$卷积:$C_{L \to H} = ((1 - \alpha) \times CO \times \frac{W}{2} \times \frac{H}{2}) \times ( \alpha \times CI \times K \times K) = \frac{\alpha(1-\alpha)}{4} \times C_{conv}$
  • $H \to L$卷积:$C_{H \to L} = (\alpha \times CO \times \frac{W}{2} \times \frac{H}{2}) \times ((1 - \alpha) \times CI \times K \times K) = \frac{\alpha(1-\alpha)}{4} \times C_{conv}$
  • $H \to H$卷积:$C_{H \to H} = ((1 - \alpha) \times CO \times W \times H) \times ((1 - \alpha) \times CI \times K \times K) = (1 - \alpha)^2 \times C_{conv}$

总上,可以得出计算量有:

在$\alpha \in [0,1]$中单调递减,当取$\alpha = 1$时,有$\frac{C_{octave}}{C_{conv}} = \frac{1}{4}$。

参数量分析

原卷积的参数量为:

Octave卷积将该部分分为四个,对于每个卷积有:

  • $L \to L$卷积:$W_{L \to L} =(\alpha\times CO) \times (\alpha \times CI) \times K \times K = \alpha^2 \times W_{conv}$
  • $L \to H$卷积:$W_{L \to H} =((1-\alpha) \times CO) \times (\alpha \times CI) \times K \times K = \alpha(1 - \alpha) \times W_{conv}$
  • $H \to L$卷积:$W_{H \to L} =(\alpha \times CO) \times ((1-\alpha) \times CI) \times K \times K = \alpha(1 - \alpha) \times W_{conv}$
  • $H \to H$卷积:$W_{H \to L} =((1-\alpha) \times CO) \times ((1-\alpha) \times CI) \times K \times K = (1 - \alpha)^2 \times W_{conv}$

因此共有参数量:

由此,参数量没有发生变化,该方法无法减少参数量。

Octave卷积实现

Octave卷积模块

以下实现了一个兼容普通卷积的Octave卷积模块,针对不同的高频低频feature map的通道数,分为以下几种情况:

  • Lout_channel != 0 and Lin_channel != 0:通用Octave卷积,需要四个卷积参数
  • Lout_channel == 0 and Lin_channel != 0:输出Octave卷积,输入有低频部分,输出无低频部分,仅需要两个卷积参数
  • Lout_channel != 0 and Lin_channel == 0:输入Octave卷积,输入无低频部分,输出有低频部分,仅需要两个卷积参数
  • Lout_channel == 0 and Lin_channel == 0:退化为普通卷积,输入输出均无低频部分,仅有一个卷积参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class OctaveConv(pt.nn.Module):
def __init__(self,Lin_channel,Hin_channel,Lout_channel,Hout_channel,
kernel,stride,padding):
super(OctaveConv, self).__init__()
if Lout_channel != 0 and Lin_channel != 0:
self.convL2L = pt.nn.Conv2d(Lin_channel,Lout_channel, kernel,stride,padding)
self.convH2L = pt.nn.Conv2d(Hin_channel,Lout_channel, kernel,stride,padding)
self.convL2H = pt.nn.Conv2d(Lin_channel,Hout_channel, kernel,stride,padding)
self.convH2H = pt.nn.Conv2d(Hin_channel,Hout_channel, kernel,stride,padding)
elif Lout_channel == 0 and Lin_channel != 0:
self.convL2L = None
self.convH2L = None
self.convL2H = pt.nn.Conv2d(Lin_channel,Hout_channel, kernel,stride,padding)
self.convH2H = pt.nn.Conv2d(Hin_channel,Hout_channel, kernel,stride,padding)
elif Lout_channel != 0 and Lin_channel == 0:
self.convL2L = None
self.convH2L = pt.nn.Conv2d(Hin_channel,Lout_channel, kernel,stride,padding)
self.convL2H = None
self.convH2H = pt.nn.Conv2d(Hin_channel,Hout_channel, kernel,stride,padding)
else:
self.convL2L = None
self.convH2L = None
self.convL2H = None
self.convH2H = pt.nn.Conv2d(Hin_channel,Hout_channel, kernel,stride,padding)
self.upsample = pt.nn.Upsample(scale_factor=2)
self.pool = pt.nn.AvgPool2d(2)
def forward(self,Lx,Hx):
if self.convL2L is not None:
L2Ly = self.convL2L(Lx)
else:
L2Ly = 0
if self.convL2H is not None:
L2Hy = self.upsample(self.convL2H(Lx))
else:
L2Hy = 0
if self.convH2L is not None:
H2Ly = self.convH2L(self.pool(Hx))
else:
H2Ly = 0
if self.convH2H is not None:
H2Hy = self.convH2H(Hx)
else:
H2Hy = 0
return L2Ly+H2Ly,L2Hy+H2Hy

在前项传播的过程中,根据是否有对应的卷积操作参数判断是否进行卷积,若不进行卷积,将输出置为0。前向传播时,输入为低频和高频两个feature map,输出为低频和高频两个feature map,输入情况和参数配置应与通道数的配置匹配。

其他部分

使用MNIST数据集,构建了一个三层卷积+两层全连接层的神经网络,使用Adam优化器训练,代价函数使用交叉熵函数,训练3轮,最后在测试集上进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import torch as pt
import torchvision as ptv
# download dataset
train_dataset = ptv.datasets.MNIST("./",train=True,download=True,transform=ptv.transforms.ToTensor())
test_dataset = ptv.datasets.MNIST("./",train=False,download=True,transform=ptv.transforms.ToTensor())
train_loader = pt.utils.data.DataLoader(train_dataset,batch_size=64,shuffle=True)
test_loader = pt.utils.data.DataLoader(test_dataset,batch_size=64,shuffle=True)
# build network
class mnist_model(pt.nn.Module):
def __init__(self):
super(mnist_model, self).__init__()
self.conv1 = OctaveConv(0,1,8,8,kernel=3,stride=1,padding=1)
self.conv2 = OctaveConv(8,8,16,16,kernel=3,stride=1,padding=1)
self.conv3 = OctaveConv(16,16,0,64,kernel=3,stride=1,padding=1)
self.pool = pt.nn.MaxPool2d(2)
self.relu = pt.nn.ReLU()
self.fc1 = pt.nn.Linear(64*7*7,256)
self.fc2 = pt.nn.Linear(256,10)
def forward(self,x):
out = [self.pool(self.relu(i)) for i in self.conv1(0,x)]
out = self.conv2(*out)
_,out = self.conv3(*out)
out = self.fc1(self.pool(self.relu(out)).view(-1,64*7*7))
return self.fc2(out)
net = mnist_model().cuda()
# print(net)
# prepare training
def acc(outputs,label):
_,data = pt.max(outputs,dim=1)
return pt.mean((data.float()==label.float()).float()).item()
lossfunc = pt.nn.CrossEntropyLoss().cuda()
optimizer = pt.optim.Adam(net.parameters())
# train
for _ in range(3):
for i,(data,label) in enumerate(train_loader) :
optimizer.zero_grad()
# print(i,data,label)
data,label = data.cuda(),label.cuda()
outputs = net(data)
loss = lossfunc(outputs,label)
loss.backward()
optimizer.step()
if i % 100 == 0:
print(i,loss.cpu().data.item(),acc(outputs,label))
# test
acc_list = []
for i,(data,label) in enumerate(test_loader):
data,label = data.cuda(),label.cuda()
outputs = net(data)
acc_list.append(acc(outputs,label))
print("Test:",sum(acc_list)/len(acc_list))
# save
pt.save(net,"./model.pth")

最终获得模型的准确率为0.988

转置型FIR设计

发表于 2019-04-24 | 分类于 Verilog巩固手记

1.设计目标

设计基于单口SRAM的转置型FIR,半并行实现,要求满足:

  • 并行程度与串行程度参数可配置
  • 数据位宽可配置,支持负数,负数为补码类型

2.参数表

名称 默认值 说明
PALL_PAM 4 并行阶数
PALL_PAM_LOG 2 并行阶数LOG值
SERI_PAM 4 串行阶数
SERI_PAM_LOG 2 串行阶数LOG值
DATA_WIDTH 16 数据位宽

3.端口列表

3.1.系统端口

名称 类型 位宽 说明
clk input 1 系统时钟
rst_n input 1 系统复位信号,低有效

3.2.配置端口

名称 类型 位宽 说明
cfg_valid input 1 配置有效信号
cfg_addr input PALL_PAM_LOG*SERI_PAM_LOG 配置地址
cfg_data input DATA_WIDTH 配置数据

3.3.数据端口

名称 类型 位宽 说明
din_valid input 1 输入有效信号
din_busy output 1 输入忙信号
din_data input DATA_WIDTH 输入数据
dout_valid output 1 输出有效信号
dout_busy input 1 输出忙信号
dout_data output DATA_WIDTH 输出数据

4.系统结构

4.1.结构框图

该FIR共分为四个部分:

  • 输入部分:输入寄存器和单口RAM,用于控制输入端口信号,实现数据输入
  • 计算部分:由多个串行单元组成,每个串行单元 串行计算,多个串行单元之间并行计算
  • 输出部分:输出寄存器,用于控制输出端口信号实现结果输出功能
  • 控制部分:产生时序控制信号,控制输入部分、计算部分和输出部分的运行

4.2.系统算法

以一个六阶的FIR为例,并行度为2,串行度为3(每个串行处理单元串行处理3个乘加操作),整体有以下数据流:

可以发现,对于:

而言,前一部分的部分和在PE0的第0~2cycle中计算,后一部分的部分和在PE1的3~5cycle中计算,同时,PE0在第3~5个周期中计算$y(1)$的部分和。因此对于$m \times n$阶的FIR(并行度为m,串行度为n),每个串行单元负责一个FIR结果的n个乘法的计算。对于第i个串行单元,负责$h(j \times m + i),j= 0,1,…,n$和对应输入数据的乘法。现在考虑第k个输出$y(k)$,相关伪代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
k_result = 0;
for(int i = 0;i < m;i++) { // 不同PE分时并行实现
float this_result;
for(int j = 0;j < n;j++) { // 串行实现
if(j == 0) {
this_result = x(k+j*m+i) * h((n-j)*m-i) + k_result;
} else {
this_result = x(k+j*m+i) * h((n-j)*m-i) + this_result;
}
}
k_result = this_result;
}

对于第i个PE(PE的标号计算从1开始),在第j个周期(周期标号从0开始),输出的权值为$h[(n-j) \times m - i]$,每个PE的标号i是固定的,因此ROM对应的地址仅与当前串行周期数有关。对于第z个周期的输入(z计数从0开始,输出$x(k)$的周期为第0周期),对应的输入数据应为$x[k+m\times (z\%n) + z //n]$,因此对于数据RAM取数据的地址除了与周期数z有关外,还与k有关。

5.子模块设计

5.1.输入模块

5.1.1.需求

输入模块包括输入数据寄存器和数据RAM,需要实现以下功能:

  • 输入寄存器使用P2P接口,当且仅当P2P端口valid信号高且busy信号低时,输入有效
  • 数据寄存器中的数据在控制模块的控制下将数据输入到RAM中保存

5.1.2.端口

名称 类型 位宽 说明
clk input 1 系统时钟
rst_n input 1 系统复位信号,低有效
din_valid input 1 输入P2P接口有效信号
din_busy input 1 输入P2P接口忙信号,控制器生成
din_data input DATA_WIDTH 输入P2P数据信号
control_ram_addr input SERL_PRAM_LOG+PALL_PRAM_LOG 读写数据ram的地址
control_ram_write input 1 写ram请求信号
unit_din output DATA_WIDTH ram输出数据

5.1.3.实现

该部分设计如上图,共两个部分,如下所示:

  • 输入寄存器:P2P接口的输入寄存器,P2P接口的busy信号由控制器产生,该寄存器接收valid和busy信号,当valid为高且busy为低时,将输入数据data锁存到输入寄存器中
  • RAM:数据单口先读后写RAM,接收控制器的控制信号,写数据从输入寄存器获得,数据输出到内部端口

该部分不包括控制流部分,仅实现输入的数据流,控制流由控制器生成。输出端口的数据来源为RAM或输入寄存器。当执行RAM写入操作时,内部输出数据来源于输入寄存器,否则来源于数据RAM。

5.2.串行处理单元

5.2.1.需求

串行处理单元,实现串并行处理的串行部分,多个串行处理单元并行实现并行部分,单个单元的需求为:

  • 实现串行的相乘相加,一个操作数来自ROM,一个操作数来自输入模块的输出
  • 实现可选择的累加,选择控制信号由控制模块生成

5.2.2.端口

名称 类型 位宽 说明
clk input 1 系统时钟
rst_n input 1 系统复位信号,低有效
cfg_valid input 1 配置有效信号,高有效
cfg_addr input PALL_PAM_LOG+SERI_PAM_LOG 配置目标地址
cfg_data input DATA_WIDTH 配置数据
unit_din input DATA_WIDTH 乘法操作数,来自输入模块
unit_partsum_din input DATA_WIDTH*2 部分和累加操作数,来自上一个串行单元
unit_partsum_dout output DATA_WIDTH*2 部分和,输出到下一个串行单元
control_rom_addr input SERI_PAM_LOG 参数ROM地址,产生ROM的乘法操作数
control_mux_controller input 2 控制信号,控制累加器功能

5.2.3.实现

串行处理单元如上图所示,该部分仅包括数据流,控制流由控制器统一产生。分为以下几个部分:

  • ROM:存储当前单元的相关数据,可使用cfg_*接口进行参数配置。非配置时根据控制器提供的地址输出乘法操作数
  • 乘法器:带符号数乘法器,将ROM的数据输出和数据输入unit_din进行相乘
  • 累加部分:包括累加寄存器、加法器和Mux,可选择不执行操作、乘法结果与部分和输入相加和乘法结果累加三种操作

对于一次操作,数据输入和ROM地址对应的数据输出到乘法器完成乘法,根据控制信号加法器将乘法结果与部分和输入或累加结果进行相加,累加寄存器的值输出到部分和输出端口。其中的reg用于保证数据对齐。

5.3.控制器

5.3.1.需求

该设计使用中央控制的方式进行控制,所有控制信号均由控制器生成,包括:

  • 控制输入部分的busy信号和数据RAM的地址
  • 控制串行处理单元的ROM地址和操作方式
  • 控制输出部分的valid信号

5.3.2.端口

名称 类型 位宽 说明
clk input 1 系统时钟
rst_n input 1 系统复位,低有效
din_valid input 1 输入数据P2P端口有效信号
din_busy output 1 输入数据P2P端口忙信号
control_ram_addr output SERL_PRAM_LOG+PALL_PRAM_LOG 读写数据ram的地址
control_ram_write output 1 写数据ram请求信号
control_rom_addr output SERI_PAM_LOG 参数ROM地址,产生ROM的乘法操作数
control_mux_controller output 2 控制信号,控制累加器功能
dout_busy input 1 输出数据P2P端口忙信号
dout_valid output 1 输出数据P2P端口有效信号

5.3.3.实现

该部分的核心是一个状态机,该状态机控制所有部件的运行,状态机的流程图如下所示:

该状态机有四个状态:

  • INIT:初始待机状态,等待输入数据
  • READ:读取数据状态,当输入P2P传输发生时从INIT进入,下一时钟周期进入COMP状态
  • COMP:计算状态,从READ状态进入,SERI_PAM个时钟周期后进入WRITE状态
  • WRITE:输出状态,从COMP状态进入,3个时钟周期(等待计算全部完成)后控制P2P输出端口输出数据

5.3.3.1.输入端口控制实现

输入P2P端口需要控制的信号是din_busy信号,该信号仅在状态机状态为INIT时为低,否则为高。

输入部分RAM地址的控制信号为$k+i\times PALL_PAM,i<SERI_PAM$,其中:

  • k为基地址寄存器,每次WRITE状态结束时加1,范围为0~(PALL_PAM+SERI_PAM + 1)
  • i为偏移量,在COMP状态中每时钟周期加1,范围为0~SERI_PAM

输入部分RAM写请求信号在COMP的最后一个周期拉高,将数据写入RAM,同时将输入寄存器的值作为数据输出

5.3.3.2.串行处理单元控制实现

串行处理单元的ROM地址信号在COMP状态从SERI_PAM-1到0递减,每时钟周期减1

串行处理单元的MUX控制信号如下所示:

  • 在非COMP状态下为0,即加法器不工作
  • 在COMP的第一个时钟周期为1,为加法器实现乘法结果与部分和输入相加
  • 在COMP的其他时钟周期为3,为加法器实现乘法结果的累加操作

5.3.3.3.输出端口控制实现

输出部分控制信号为dout_valid,在进入WRITE状态3个时钟周期后将该信号拉高,退出WRITE状态时拉低

SystemC入门笔记

发表于 2019-01-22 | 分类于 硬件设计

变量说明

数据类型

SystemC为C++的一个库,因此C++的特性在SystemC中均可以使用,数据类型同理,除了C++中的数据类型外,SystemC也有一些自己的数据类型,如下所示:

  • 二值变量:sc_bit和sc_bv<n>(n为宽度)分别为二值(0、1)变量和任意位宽二值向量。
  • 四值变量:sc_logic和sc_lv<n>(n为宽度)分别为四值(0、1、x、z)变量和任意位宽四值向量
  • int型变量:sc_int<n>和sc_uint<n>(n为宽度)分别为有符号和无符号的不超过64位宽的整型变量
  • bigint变量:sc_bigint<n>和sc_bituing(n为宽度)分别为有符号和无符号任意位宽的整型变量

信号

信号使用sc_signal<type>声明,一般用于连接端口和进程通信(功能进程之间连接信号)

端口

SystemC中端口类型主要有sc_in<type>、sc_out<type>和sc_inout<type>,type中为端口的类型,可以使用C++自带的一些类型,也可以使用SystemC中的数据类型。

1
sc_out<sc_int<WIDTH * 2> > vec_o;

例如上面为一个输出端口例子,该输出端口名称为vec_o,类型为SystemC的数据类型sc_int<W>。

1
sc_in<sc_int<WIDTH> > vec1[VEC_WIDTH];

可以声明端口数组,如上所示,声明了一个宽度为VEC_WIDTH的端口数组,每个端口的类型是sc_int<W>。

模块设计——矩阵-向量乘法器

设计一个矩阵-向量乘法器用于熟悉语法,需要注意的是若要使用SystemC特性,需要使用#include "systemc.h"

系统结构

,.,.

该系统用于实现矩阵-向量乘法的行为级建模,包括以下几个部分:

  • 乘法器:实现矩阵-向量乘法功能,由多个向量-向量乘法器构成
  • 测试平台:激励生成器,用于产生指定尺寸的矩阵和向量以及时钟复位等控制信号

子模块设计

每个子模块在SystemC使用一个类描述,这个类使用宏SC_MODULE(<module name>)声明,这里的子模块是向量-向量乘法器,这一部分代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SC_MODULE(vector_mul) {
sc_in<bool> clk,rst_n;
sc_in<sc_int<WIDTH> > vec1[VEC_WIDTH],vec2[VEC_WIDTH];
sc_out<sc_int<WIDTH * 2> > vec_o;
void compute_vector_mul(void) {
int temp = 0;
if (rst_n.read() == false) {
vec_o.write(0);
return;
}
for (int i = 0; i < VEC_WIDTH; ++i) {
temp = temp + vec1[i].read() * vec2[i].read();
}
vec_o.write(temp);
};
SC_CTOR(vector_mul) {
SC_METHOD(compute_vector_mul);
sensitive_pos << clk;
sensitive_neg << rst_n;
};
};

一个子模块包括三个部分:端口定义、功能描述与构造函数。端口定义使用上述的端口定义,如下所示,该乘法器定义了两个类型为bool的控制端口:clk和rst_n。分别是时钟和复位端口;还定义了两个输入端口数组,分别是vec1和vec2,数组宽度为VEC_WIDTH(宏定义),类型为有符号整数类型sc_int<WIDTH>;此外还定义了一个输出端口vec_o,类型为指定位宽的整数类型sc_int<WIDTH * 2>。定义端口后,该部分框图如下所示:

1
2
3
sc_in<bool> clk,rst_n;
sc_in<sc_int<WIDTH> > vec1[VEC_WIDTH],vec2[VEC_WIDTH];
sc_out<sc_int<WIDTH * 2> > vec_o;

功能描述使用一个无输入参数无输出参数的方法描述,建议使用.read()读取输入端口的数据并使用.write()向输出端口写入数据。一个模块可以有多个功能描述,这里的功能描述功能类似于Verilog中的always块。SystemC中的赋值基本都是阻塞的,可以在这一函数中使用任意的C++特性和库等。定义了这一方法后,该部分框图如下所示:

1
2
3
4
5
6
7
8
9
10
11
void compute_vector_mul(void) {
int temp = 0;
if (rst_n.read() == false) {
vec_o.write(0);
return;
}
for (int i = 0; i < VEC_WIDTH; ++i) {
temp = temp + vec1[i].read() * vec2[i].read();
}
vec_o.write(temp);
};

最后一个部分是构造函数,这一部分是为了注册功能,描述连接关系和敏感列表等。无参数化的构造函数使用SC_CTOR(<module name>)定义,这里的module name必须与这个类的名称一致。一个类可能有很多方法,只有如下所示的方式定义为METHOD(或THREAD等)的方法才作为模块的一个功能,定义后需要立刻定义敏感列表,只有敏感列表中的变量发生改变时,功能才运行:sensitive <<表示事件(电平)敏感,一般用于组合逻辑建模;sensitive_pos <<和sensitive_neg <<分别为正跳变敏感和负跳变敏感,一般用于时序逻辑建模。完成以上所有部分的定义后,完成对一个子模块的构建。

METHOD是一种阻塞式的功能进程,当这个进程被敏感列表触发之后,获取仿真控制权开始运行,直到运行完成,将控制权返回SystemC仿真内核。使用METHOD注册的功能函数不能含有无限循环,这会导致仿真卡死在这个任务中,控制权无法返回仿真内核。

1
2
3
4
5
SC_CTOR(vector_mul) {
SC_METHOD(compute_vector_mul); // 注册为METHOD
sensitive_pos << clk; // clk为正跳变敏感信号
sensitive_neg << rst_n; // rst_n为负跳变敏感信号
};

顶层模块

顶层模块用于实现子模块的连接,代码实现如下所示。除了声明端口以外,还需要在构造函数中定义连接关系。连接关系的定义分为三个部分:模块指针声明、模块实例化和端口连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SC_MODULE(matrix_vector_mul) {
sc_in<bool> clk,rst_n;
sc_in<sc_int<WIDTH> > matrix[VEC_NUM][VEC_WIDTH];
sc_in<sc_int<WIDTH> > vector_in[VEC_WIDTH];
sc_out<sc_int<WIDTH * 2> > vector_out[VEC_NUM];
vector_mul *pe[VEC_NUM];
SC_CTOR(matrix_vector_mul) {
std::ostringstream pe_name;
for (int i = 0; i < VEC_NUM; ++i) {
pe_name << "pe" << i;
pe[i] = new vector_mul(pe_name.str().c_str());
pe[i]->clk(clk);
pe[i]->rst_n(rst_n);
for (int j = 0; j < VEC_WIDTH; ++j) {
pe[i]->vec1[j](matrix[i][j]);
pe[i]->vec2[j](vector_in[j]);
}
pe[i]->vec_o(vector_out[i]);
pe_name.str("");
}
};
};

模块指针声明如下所示,这里声明了VEC_NUM个子模块指针作为顶层模块的成员变量。

1
vector_mul *pe[VEC_NUM];

模块实例化如下所示,若使用SC_CTOR宏定义构造函数的子模块,需要使用一个唯一的字符串作为实例名。由于C++没有格式化字符串的功能,因此使用std::ostringstream生成唯一不重复的实例名,再用c_str()转为构造函数指定的类型。

1
pe[i] = new vector_mul(pe_name.str().c_str());

信号连接额部分使用如下所示的方式完成:<point>-><port name>(<signal name>)。需要注意的是若声明的是端口数组,则需要将每个数组中的每个端口拆分出来依次连接。

1
2
3
4
5
6
7
pe[i]->clk(clk); // clk端口与clk信号连接
pe[i]->rst_n(rst_n); // rst_n端口与rst_n信号连接
for (int j = 0; j < VEC_WIDTH; ++j) {
pe[i]->vec1[j](matrix[i][j]); // vec1[j]端口与matrix[i][j]信号连接
pe[i]->vec2[j](vector_in[j]); // vec2[j]端口与vector_in[j]信号连接
}
pe[i]->vec_o(vector_out[i]); // vec_o端口与vector_out[i]信号连接

测试平台设计

平台组件

这里实现的组件仅有激励生成器,该模块与上述模块没有太大的差别,该模块用于生成复位信号和数据信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
SC_MODULE(driver)
{
sc_in <bool> clk;
sc_out<bool> rst_n;
sc_out<sc_int<WIDTH> > mat[VEC_NUM][VEC_WIDTH];
sc_out<sc_int<WIDTH> > vec[VEC_WIDTH];
void generate_input(void) {
for (int i = 0; i < VEC_WIDTH; ++i) {
for (int j = 0; j < VEC_NUM; ++j) {
mat[j][i].write(rand() % ((int)pow(2,WIDTH) - 1));
}
vec[i].write(rand() % ((int)pow(2,WIDTH) - 1));
}
while(1) {
wait();
for (int i = 0; i < VEC_WIDTH; ++i) {
for (int j = 0; j < VEC_NUM; ++j) {
mat[j][i].write(rand() % ((int)pow(2,WIDTH) - 1));
}
vec[i].write(rand() % ((int)pow(2,WIDTH) - 1));
}
}
};
void generate_reset(void) {
rst_n.write(1);
wait(1,SC_NS);
rst_n.write(0);
wait(1,SC_NS);
rst_n.write(1);
};
SC_CTOR(driver) {
SC_THREAD(generate_input);
sensitive_neg << clk;
SC_THREAD(generate_reset);
};
};

需要注意的是这里的功能描述和构造函数,如下所示。构造函数中功能成员generate_input和generate_reset均使用THREAD宏注册为功能,这一宏与METHOD的区别是这一种功能进程在仿真开始时运行,碰到wait()跳出,直到敏感列表中的信号再次触发这一进程,从上次跳出的wait()处继续运行,因此这种进程可以使用循环体包括wait()的无限循环。

1
2
3
4
5
SC_CTOR(driver) {
SC_THREAD(generate_input);
sensitive_neg << clk;
SC_THREAD(generate_reset);
};

除了使用wait()阻塞运行外,还可以使用wait(<times>,SC_NS);将执行延迟指定的时钟周期,如rst_n信号的实现,使用多个wait(<times>,SC_NS)延迟执行。

1
2
3
4
5
6
7
void generate_reset(void) {
rst_n.write(1);
wait(1,SC_NS); // 延迟1ns
rst_n.write(0);
wait(1,SC_NS); // 延迟1ns
rst_n.write(1);
};

平台运行

最终的平台运行集成在main函数中,如下所示,分为以下几个步骤:信号声明,模块声明和端口连接,波形追踪和仿真运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "systemc.h"
int sc_main(int argc,char* argv[])
{
sc_clock clk("clk",10,SC_NS);
sc_signal<bool> rst_n;
sc_signal<sc_int<WIDTH> > mat[VEC_NUM][VEC_WIDTH],vec[VEC_WIDTH];
sc_signal<sc_int<WIDTH * 2> >vec_o[VEC_NUM];
sc_trace_file *fp; // Create VCD file
fp=sc_create_vcd_trace_file("wave");// open(fp), create wave.vcd file
fp->set_time_unit(1, SC_NS); // set tracing resolution to ns
matrix_vector_mul dut("dut");
dut.clk(clk);
dut.rst_n(rst_n);
for (int i = 0; i < VEC_NUM; ++i) {
for (int j = 0; j < VEC_WIDTH; ++j) {
dut.matrix[i][j](mat[i][j]);
}
}
for (int i = 0; i < VEC_WIDTH; ++i) {
dut.vector_in[i](vec[i]);
}
for (int i = 0; i < VEC_NUM; ++i) {
dut.vector_out[i](vec_o[i]);
}
driver d("dri");
d.clk(clk);
d.rst_n(rst_n);
for (int i = 0; i < VEC_WIDTH; ++i) {
for (int j = 0; j < VEC_NUM; ++j) {
d.mat[j][i](mat[j][i]);
}
d.vec[i](vec[i]);
}
sc_trace(fp,clk,"clk");
sc_trace(fp,rst_n,"rst_n");
for (int i = 0; i < VEC_NUM; ++i) {
for (int j = 0; j < VEC_WIDTH; ++j) {
std::ostringstream mat_name;
mat_name << "matrix(" << i << "," << j << ")";
sc_trace(fp,mat[i][j],mat_name.str());
mat_name.str("");
}
}
for (int i = 0; i < VEC_WIDTH; ++i) {
std::ostringstream stream1;
stream1 << "vec(" << i << ")";
sc_trace(fp,vec[i],stream1.str());
stream1.str("");
}
for (int i = 0; i < VEC_NUM; ++i) {
std::ostringstream out_name;
out_name << "dout(" << i << ")";
sc_trace(fp,vec_o[i],out_name.str());
out_name.str("");
}
sc_start(1000,SC_NS);
sc_close_vcd_trace_file(fp); // close(fp)
return 0;
};

第一个步骤是声明信号和模块,这一步用于声明连接需要的信号,这里的时钟信号使用SystemC的方法生成。用于连接的信号需要声明成sc_signal<>类型。

1
2
3
4
sc_clock clk("clk",10,SC_NS); // 时钟,周期为10ns
sc_signal<bool> rst_n;
sc_signal<sc_int<WIDTH> > mat[VEC_NUM][VEC_WIDTH],vec[VEC_WIDTH];
sc_signal<sc_int<WIDTH * 2> >vec_o[VEC_NUM];

第二步是声明模块并连接信号,这里的连接和声明与顶层模块中类似,只是这里直接声明对象而不是指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
matrix_vector_mul dut("dut");
dut.clk(clk);
dut.rst_n(rst_n);
for (int i = 0; i < VEC_NUM; ++i) {
for (int j = 0; j < VEC_WIDTH; ++j) {
dut.matrix[i][j](mat[i][j]);
}
}
for (int i = 0; i < VEC_WIDTH; ++i) {
dut.vector_in[i](vec[i]);
}
for (int i = 0; i < VEC_NUM; ++i) {
dut.vector_out[i](vec_o[i]);
}

第三步是实现波形跟踪与保存,首先定义一个sc_trace_file类型的指针,使用对应方法打开指定的文件类型和波形名称(fp=sc_create_vcd_trace_file("wave");保存vcd格式的波形)并进行配置。随后使用sc_trace(fp,signal,<signal name>);将需要观察的信号添加到波形跟踪中,其中<signal name>为波形文件中这一信号的名称,因此需要保证对于每一个信号该名称唯一。当仿真完成后,需要使用sc_close_vcd_trace_file(fp);关闭仿真文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sc_trace_file *fp; // Create VCD file
fp=sc_create_vcd_trace_file("wave");// open(fp), create wave.vcd file
fp->set_time_unit(1, SC_NS); // set tracing resolution to ns
......
sc_trace(fp,clk,"clk");
sc_trace(fp,rst_n,"rst_n");
for (int i = 0; i < VEC_NUM; ++i) {
for (int j = 0; j < VEC_WIDTH; ++j) {
std::ostringstream mat_name;
mat_name << "matrix(" << i << "," << j << ")";
sc_trace(fp,mat[i][j],mat_name.str());
mat_name.str("");
}
}
for (int i = 0; i < VEC_WIDTH; ++i) {
std::ostringstream stream1;
stream1 << "vec(" << i << ")";
sc_trace(fp,vec[i],stream1.str());
stream1.str("");
}
for (int i = 0; i < VEC_NUM; ++i) {
std::ostringstream out_name;
out_name << "dout(" << i << ")";
sc_trace(fp,vec_o[i],out_name.str());
out_name.str("");
}
......
sc_close_vcd_trace_file(fp); // close(fp)

第四个部分是启动仿真,使用sc_start()运行指定的时间长度。

1
sc_start(1000,SC_NS); // 运行1000ns

DianNao运算单元与体系结构分析

发表于 2018-12-24 | 分类于 硬件设计

运算单元

基本信息

名称 参数
数据输入位宽 $T_n \times DW$bit
权值输入位宽 $T_n \times T_n \times DW$bit
数据输出位宽 $T_n \times DW$bit
功能 矩阵乘法、最大值池化、平均值池化
乘法器数量 $T_n \times T_n \times T_n$
加法器数量 $T_n \times (T_n - 1)$

结构

NFU的整体结构如上所示,该部分分为三个部分,分别是NFU-1、NFU-2和NFU-3三个部分,分别是乘法器阵列,加法或最大值树和非线性函数部分。NFU-1由一些乘法器阵列构成,如下图所示。一个单元具有一个输入数据$I_i$和$T_n$个输入权值,一个单元中共有$T_n$个乘法器,分别计算$I_i \times W_{ji}$的值,具有$T_n$个输出。

$T_n$个输入数据和$T_n \times T_n$输入权值经过NFU-1处理后,变为$T_n \times T_n$个部分积,第i个乘法器单元的第j个输出为$P_{ij} = I_i \times W_{ji}$。所有部分积经过route分配给$T_n$NFU-2单元,分配规则如下所示,第i个NFU-2单元的输入是所有NFU-1单元的第i个输出。

NFU-2单元为加法/平均值(加法树前添加位移单元)/最大值(加法树的加法器可配置为取最大值)树,用于计算$T_n$个输入的和/平均值或最大值,如下所示:

NFU-2单元的输出为一个数据,整个NFU-2部分输出为$T_n$个部分操作数据。该输出可以流向NFU-3部分作为NFU-2的输出,也可以流向D-Reg作为部分和临时保存以节约带宽。NFU-3为一个加法器和一个非线性单元。非线性单元使用分段线性逼近非线性函数,分段线性逼近参数保存在RAM中,可通过更改该参数使该单元实现任意非线性函数。

运算映射

矩阵乘法/卷积

映射以下矩阵乘法:

有以下配置:

  • 数据输入:第i个NFU-1单元数据输入为$x_i$
  • 权值输入:第i个NFU-1单元的第j个权值输入为$w_{ji}$,即第i个NFU-1单元输入的数据为W矩阵的第i列
  • NFU-2:配置和实现加法树功能

池化

映射以下最大值操作:

有以下配置:

  • 数据输入:第i个NFU-1单元数据输入为$x_i$
  • 权值输入:所有权值配置为1
  • NFU-2:配置实现最大值树功能(若为求平均值,配置为平均值树)

对于x的维度小于$T_n$时,推测可以将权值部分设置为1部分设置为0作为掩码,同时计算多个最大值/平均值操作

系统结构

系统结构如上所述,各部分:

  • NFU:神经计算单元,已经加入compute_unit的pool中
  • 分裂缓存:按功能分裂为三个的缓存,已经加入memory的pool中
  • 控制模块CP:指令使控制,每个指令分为四个部分,分别是NBin指令,NBout指令,SB指令和NFU指令

系统使用指令控制,每条指令可以实现一次矩阵-向量乘法运算,每个指令的四个部分被解耦后发送给四个部分,因此存储器的load指令不需要等待NFU运算完成,对于三个缓存,执行完当前步骤后立刻执行下一个指令中对应部分的指令,可以实现数据的预取,但是考虑计算正确性,NFU必须等待运算所需要的数据预存完成后才能执行。

计算映射

对于一个矩阵乘法:

首先进行矩阵分块,参数矩阵W分块为$C^{T_n \times T_n}$的矩阵,输入向量x分块为$C^{T_n}$,再进行计算,如下图所示:

分块后,原论文给出的加速器参数为$T_n=16$,计算需要的权值矩阵有$W \in Q^{256 \times 8192}$,数据向量有$x \in Q^{8192}$,缓存载入的规则为:

  • Nbin:数据向量分块为$\frac{8192 \times 2B}{2KB} = 8$块,每一块数据大小为2KB=$16 \times 2B \times 64$,每次载入一块。即每次载入的输入数据包括64个逻辑块。
  • SB:每次载入32768B=$16 \times 16 \times 2B \times 64$,即每次载入的数据包括64个逻辑块。

映射一个矩阵乘法,步骤为:

  1. Nbin载入前四个逻辑块D1,D2,~,D64。SB载入与前四个输入逻辑块运算相关的64个数据块W11、W12、W13~W64,1(分块后W的前4列,前16行的块)。NFU计算对应乘法(例如$W_{11} \times D_1$,$W_{12} \times D_2$,…,$W_{1,64} \times D_{64}$),并将部分和存储在Nbout中
  2. Nbin载入第二块输入数据,包括D65~D128,SB继续载入与D1~D64运算相关的权值块W2,1~W2,64。NFU继续计算D1~D64对应乘法。直到将D1~D64相关的乘法计算完成以后,才进行D65~D128相关的乘法。
  3. …
  4. 当某个输出的计算完成后,Nbout将其输出到外部缓存中

复用策略为仅复用输入,仅当这一块输入数据需要参与的所有运算完成后才开始进行下一块输入相关的计算。对于每一块输入映射过程如下图所示:

AXI学习笔记-1

发表于 2018-11-18 | 分类于 硬件设计

1.AXI总线结构

AXI总线由5个通道构成:

通道名称 通道功能 数据流向
read address 读地址通道 主机->从机
read data 读数据通道(包括数据通道和读响应通道) 从机->主机
write address 写地址通道 主机->从机
write data 写数据通道(包括数据通道和每8bit一个byte的写数据有效信号) 主机->从机
write response 写响应通道 从机->主机

1.1.AXI通道

读操作的通道如下图所示

写操作的通道如下图所示

1.2.AXI系统

常见的标准AXI系统如下图所示,通常包括:

  • AXI master:AXI通信主机
  • AXI slave:AXI通信从机
  • AXI interconnect:AXI通信通路

AXI接口协议可用于:

  • AXI master - AXI interconnect的连接
  • AXI slave - AXI interconnect的连接
  • AXI master - AXI slave的连接

1.3.AXI接口

1.3.1.全局信号

信号名 来源 描述
ACLK system clock 全局时钟信号
ARESTn system reset 全局复位信号,低有效

1.3.2.写地址通道

信号名 来源 描述
AWID master 写地址ID(用于区分该地址属于哪个写地址组)
AWADDR master 写地址
AWLEN master 突发长度
AWSIZE master 突发尺寸(每次突发传输的最长byte数)
AWBURST master 突发方式(FIXED,INCR,WRAP)
AWCACHE master 存储类型(标记系统需要的传输类型)
AWPROT master 保护模式
AWQOS master QoS标识符
AWREGION master region标识符(当slave有多种逻辑接口时标识使用的逻辑接口)
AWUSER master 用户自定义信号
AWVALID master 写地址有效信号(有效时表示AWADDR上地址有效)
AWREADY master 写从机就绪信号(有效时表示从机准备好接收地址)

1.3.3.写数据通道

信号名 来源 描述
WDATA master 写数据
WSTRB master 数据段有效(标记写数据中哪几个8位字段有效)
WLAST master last信号(有效时表示当前为突发传输最后一个数据)
WUSER master 用户自定义信号
WVALID master 写有效信号(有效时表示WDATA上数据有效)
WREADY slave 写ready信号(有效时表示从机准备好接收数据)

1.3.4.写响应通道

信号名 来源 描述
BID slave 响应ID
BRESP slave 写响应
BUSER slave 用户自定义信号
BVALID slave 写响应信号有效
BREADY master 写响应ready(主机准备好接受写响应信号)

1.3.5.读地址通道

信号名 来源 描述
ARID master 读地址ID
ARADDR master 读地址
ARLEN master 突发长度
ARSIZE master 突发尺寸(每次突发传输的byte数)
ARBURST master 突发类型(FIXED,INCR,WRAP)
ARCACHE master 存储类型
ARPROT master 保护类型
ARQOS master QoS标识符
ARREGION master 区域标识符
ARUSER master 用户自定义
ARVALID master 读地址有效(有效时表示ARADDR上地址有效)
ARREADY slave 写有效信号(有效时表示从机准备好接收读地址)

1.3.6.读数据通道

信号名 来源 描述
RID slave 读ID标签
RDATA slave 读数据
RRESP slave 读响应
RLAST slave 有效时表示为突发传输的最后一个
RUSER slave 用户自定义
RVALID slave 读数据有效信号
RREADY master 主机就绪信号(有效时表示)

1.3.7.低功耗接口信号

信号名 来源 描述
CSYSREQ Clock controller 该信号有效时,系统退出低功耗模式
CSYSACK Peripheral device 退出低功耗模式应答信号
CACTIVE Peripheral device 外设申请时钟信号

2.AXI接口时序

2.1.复位

复位信号可以异步复位,但必须同步释放,复位时,信号要求如下:

  • 主机驱动的所有VALID信号(ARVALID, AWVALID和WVALID)必须被拉低
  • 从机驱动的所有VALID信号(RVALID和BVALID)必须被拉低
  • 其他信号无要求

2.2.基本传输

2.2.1.握手信号

握手信号包括VALID和READY信号,传输行为仅在VALID和READY同时有效时发生。其中:

  • VALID信号表示地址/数据/应答信号总线上的信号是有效的,由传输发起方控制
  • READY信号表示传输接收方已经准备好接收,由传输接收方控制

VALID和READY的先后关系具有三种情况:

  • VALID先有效,等待READY有效后完成传输(VALID一旦有效后在传输完成前不可取消)
  • READY先有效,等待VALID有效后完成传输(READY可以在VALID有效前撤销)
  • VALID和READY同时有效,立刻完成传输

此外,需要注意的是允许READY信号等待VALID信号再有效,即即使从机准备好,也可以不提供READY信号,等到主机发送VALID信号再提供READY信号。对应的VALID信号不允许等待READY信号,即不允许VALID等待READY信号拉高后再拉高,否则容易产生死锁现象。

2.2.1.1.命令通道握手(读地址,写地址,写响应)

  • 仅当地址等信息有效时,才拉高VALID,该VALID必须保持直到传输完成(READY置位)
  • READY默认状态不关心,仅当准备好接收时拉高READY

2.2.1.2数据通道握手(写数据和读地址)

  • 突发读写模式下,仅数据信息有效时才拉高VALID,该VALID必须保持直到传输完成。当突发传输最后一个数据发送时拉高LAST信号
  • READY默认状态不关心,仅当准备好接收时拉高READY

2.2.2.通道顺序

传输中,通道传输的先后有以下规定

  • 写响应通道传输必须在写操作完成以后进行
  • 读数据通道传输必须在读地址通道传输后进行
  • 必须遵循一系列的状态依赖关系

下文中会使用一些图描述依赖关系。图表中,单箭头表示可以等待有效再置位,双重箭头表示必须等待有效再置位

2.2.2.1.读操作顺序

上图为读操作的依赖关系,ARREADY可以等待ARVALID信号,RVALID必须等待ARVALID和ARREADY同时有效后(一次地址传输发生)才能能有效

2.2.2.2.写操作顺序

AXI3中写操作中唯一的强依赖关系是写响应通道BVALID,仅当WVALID和WREADY信号同时有效(数据传输完成)且WLAST信号有效(突发传输的最后一个数据传输完成)后才会被置位。

在AXI4中,定义了额外的依赖关系,即BVALID必须依赖AWVALID、AWREADY、WVALID和WREADY信号。

3.数据结构

3.1.地址通道数据结构

AXI总线是基于突发传输的总线,若主机要开始一次突发传输,需要传输一次地址和相关控制信号,之后从机自动计算地址,但一次突发传输的地址范围不能跨越4KB。

3.1.1.突发传输信息

3.1.1.1.突发长度(AxLEN)

突发长度为每次突发传输的传输次数,范围限制1~16(AXI4增量模式1~256)且不能跨越4kb的地址空间,每次突发传输不允许提前终止(可以通过关闭所有数据字段的方式使一段传输数据无效,但传输行为必须完成)。每次传输的突发长度为AxLEN[3:0] + 1(AXI增量模式AxLEN[7:0] + 1)

  • ARLEN[7:0]:读地址通道的突发长度接口
  • AWLEN[7:0]:写地址通道的突发长度接口

对于回卷模式突发传输,突发长度仅能是2,4,8或16。

3.1.1.2.突发尺寸(AxSIZE)

突发尺寸为每次传输的byte数量,与突发传输的地址预测相关性很强。每次的突发尺寸不能超过数据通道的宽度;若突发尺寸小于数据通道宽度,需要指定哪些位数是有效的。突发尺寸为2^AxSIZE[2:0]^。

  • ARSIZE[2:0]:读地址通道突发尺寸
  • AWSIZE[2:0]:写地址通道突发尺寸

3.1.1.3.突发类型(AxBURST)

AXI支持三种突发类型:

  • FIXED(AxBURST[1:0]=0b00):固定突发模式,每次突发传输的地址相同
  • INCR(AxBURST[1:0]=0b01):增量突发模式,突发传输地址递增,递增量与突发尺寸相关
  • WRAP(AxBURST[1:0]=0b10):回卷突发模式,突发传输地址可溢出性递增,突发长度仅支持2,4,8,16。地址空间被划分为长度【突发尺寸*突发长度】的块,传输地址不会超出起始地址所在的块,一旦递增超出,则回到该块的起始地址。

3.1.2.存储类型(AxCACHE)

AXI4可支持不同的存储类型,AxCACHE[3:0]用于描述不同的存储类型,如下图所示

ARCACHE[3:0] AWCACHE[3:0] Memory type
0000 0000 Device Non-bufferable
0001 0001 Device Bufferable
0010 0010 Normal Non-cacheable Non-bufferable
0011 0011 Normal Non-cacheable Bufferable
1010 0110 Write-through No-allocate
1110 (0110) 0110 Write-through Read-allocate
1010 1110 (1010) Write-through Write-allocate
1110 1110 Write-through Read and Write-allocate
1011 0111 Write-back No-allocate
1111 (0111) 0111 Write-back Read-allocate
1011 1111 (1011) Write-back Write-allocate
1111 1111 Write-back Read and Write-allocate

3.1.3.Qos标识符(AxQOS)

AXI4总线支持QoS,该标识符AxQOS[3:0]表示服务的优先级

3.1.4.REGION标识符(AxREGION)

region标识符用于指定选用的高级逻辑接口类型,当使用该标识符AxREGION[3:0]时,表示有多个逻辑接口共享该物理接口

3.1.5.权限标识符(AxPROT)

权限标识符AxPROT[2:0]用于防止非法传输

3.1.6.用户自定义(AxUSER)

用户自定义数据

3.2.数据通道数据结构

3.2.1.数据选通(WSTRB)

WSTRB的每一位对应数据中的8位(1字节),用于标志数据中的对应字节是否有效。即当WSTRB[n] = 1时,标志数据中WDATA[(8n)+7: (8n)]部分有效。

3.2.2.数据(xDATA)

3.2.2.1.窄带传输(Narrow transfers)

当传输的数据位宽小于xDATA总线带宽时,为窄带传输,每次使用的数据位数不同:

  • 固定地址的突发下,使用同一段数据信号线
  • 在递增地址和包装地址的突发下,使用不同段信号线

上图为地址递增突发下,在32位数据信号下使用8bit传输的窄带传输使用的位数图。第一次传输使用0~7位,第二次使用8~15位,依次递增;在第五次传输时回到开头使用0~7位

3.2.2.2.不对齐传输(Unaligned transfers)

当传输位宽超过1byte,起始地址不为数据总线硬件带宽(byte单位)整数倍时,为不对齐传输。不对齐传输的时候需要配合数据选通在第一次传输时将某几个byte置为无效,使第二次突发传输的起始地址(从机自动计算)为突发尺寸的整数倍。

如图,突发尺寸为4byte,若要对齐传输,起始地址要为4的整数倍。图中起始地址为0x07,因此为非对齐传输。第一次传输时,前3个数据为无效字段,可以使用数据选通WSTRB将前3个byte置为无效。

上图是在窄带传输下的非对齐传输启动。传输带宽为32bit,每次传输使用16bit,由于是窄带传输,因此每次交替使用低字节和高字节。现在关注启动状态,由于启动地址为7,而硬件带宽为8bit,则必须由地址0启动,设置前7个字段为无效,那么下次传输从地址8开始,满足不对齐传输。

3.2.3.用户自定义(*USER)

用户自定义数据

3.3.应答通道数据结构

3.3.1.响应信号(*RESP)

针对读和写均有响应的响应信号:

  • BRESP[1:0]写响应信号,每次突发传输完成后
  • RRESP[1:0]读响应信号(位于读数据通道)

响应信号含义如下:

  • OKAY(00):正常访问正确/特权访问失败/不支持特权访问
  • EXOKAY(01):特权访问成功
  • SLVERR(10):从机错误,传输失败
  • DECERR(11):互连解码错误,传输失败

3.3.2.用户自定义

用户自定义数据

4.传输特性

AXI从机分为两种:

  • 存储器从机(Memory Slave):需要支持所有传输特性
  • 外设从机(Peripheral Slave):仅需要支持指定的操作,但是可以保证所有类型的传输完成(不要求非指定的操作响应正确)

AxCACHE用于指定传输特性,传输特性用于标定传输如何在系统中进行和系统级缓存如何处理传输。

4.1.存储器特性

存储器特性包括4个位,如下所示:

  • AxCACHE[0](Bufferable):AxCACHE[0]表示传输过程中是否有缓存,当该位置为1时,表示表示传输路径上具有buffer(可延迟transaction到达最终点的时间)

  • AxCACHE[1](Modifiable):标记传输是否可以被修改/优化,当其置0时,每个传输将不会被更改,具体来说,AxADDR、AxSIZE、AxLEN、AxBURST、AxLOCK和AxPROT信号不会被修改(地址,突发传输信息,传输隐私信息不被修改)。但是AxCACHE[0]、ID和QoS可能被修改,同时,一个突发长度长于16的突发传输可能被切开,但是保证传输效果相同。当该位置1时,除了以上可能发生的改变,另外:

    • 多个传输可能被合并为一个传输,一个传输可能被切分为多个传输
    • 读传输在从机端读出的数据可能多于主机的请求(多的数据被保存在cache中用于优化数据访问)
    • 写传输可能访问到超过主机请求的地址范围,妥善使用WSTRB保证仅有需要的地址被覆盖

    另外,AxLOCK和AxPROT信号仍然不能被改变,同时需要注意的是:AxCACHE[0]=0,具有相同的AXI ID和指向相同的从机的一系列传输的顺序不能改变。

  • AxCACHE[2](Read-allocate)和AxCACHE[3](Write-allocate):读写操作前是否检查缓存以优化传输

4.2.存储器类型

通过ARCACHE和AWCACHE的不同定义不同的存储器类型

booth乘法器

发表于 2018-11-14 | 分类于 Verilog巩固手记

描述

Booth乘法器是一种使用移位实现的乘法器,实现过程如下,对于乘法:

扩展A的位数为n+1位,添加$a_{-1}=0$,则A变为:

从i=0开始,到i=n-1结束,依次考察$a_{i}a_{i-1}$的值,做如下操作:

  • 若$a_{i} = a_{i-1}$,不进行操作
  • 若$a_{i}a_{i-1} = 01$,$R = R + B << i$
  • 若$a_ia_{i-1}=10$,$R = R - B << i$

最后,舍弃R的最右端1位,即获得$R = A \times B$

原理

其原理比较容易理解,对于以上乘法,可以分解为:

以上是位移乘法器的原理,那么对于booth乘法器,添加了一条:

即有:

将移位乘法器原理式中$a_i$连续为1的部分使用两个减法代替,即形成booth乘法器

代码实现

这次实现了一个基于P2P接口的booth乘法器,位宽可配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module booth_mul #(
parameter DIN_WIDTH_LOG = 3
)(
input clk, // Clock
input rst_n, // Asynchronous reset active low
input din_valid,
output din_busy,
input [2 ** DIN_WIDTH_LOG-1:0] din_data_a,
input [2 ** DIN_WIDTH_LOG-1:0] din_data_b,
output reg dout_valid,
input dout_busy,
output [2 ** (DIN_WIDTH_LOG + 1) - 1:0]dout_data
);

首先定义控制流,控制流为一个状态机,分别为:

  • INIT:静默状态,等待输入,获得输入时,转向WORK状态
  • WORK:工作状态,进行booth乘法,过程中din_busy信号被拉高,当运算完成后,转向TRAN
  • TRAN:传输状态,进行P2P输出,输出完成后转向INIT状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
parameter INIT = 2'b00;
parameter WORK = 2'b01;
parameter TRAN = 2'b11;
reg [DIN_WIDTH_LOG-1:0]shifter_counter;
reg [1:0] status,next_status;
always @(posedge clk or negedge rst_n) begin : proc_status
if(~rst_n) begin
status <= 'b0;
end else begin
status <= next_status;
end
end
wire is_computed = (shifter_counter == 2 ** DIN_WIDTH_LOG - 1);
wire is_traned = dout_valid && !dout_busy;
always @(*) begin
case (status)
INIT:begin
if(din_valid) begin
next_status = WORK;
end else begin
next_status = INIT;
end
end
WORK:begin
if(is_computed) begin
next_status = TRAN;
end else begin
next_status = WORK;
end
end
TRAN:begin
if(is_traned) begin
next_status = INIT;
end else begin
next_status = TRAN;
end
end
default : next_status = INIT;
endcase
end
assign din_busy = status[0];
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
shifter_counter <= 'b0;
end else if(status == WORK) begin
shifter_counter <= shifter_counter + 1'b1;
end else begin
shifter_counter <= 'b0;
end
end
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
dout_valid <= 'b0;
end else if(is_computed) begin
dout_valid <= 1'b1;
end else if(is_traned) begin
dout_valid <= 'b0;
end
end

下面是数据流的部分,该部分实现了上述的booth乘法操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
reg [2 ** DIN_WIDTH_LOG:0]a_data;
wire is_read = !din_busy && din_valid;
always @(posedge clk or negedge rst_n) begin : proc_a_data
if(~rst_n) begin
a_data <= 0;
end else if(is_read) begin
a_data <= {din_data_a,1'b0};
end else if(status == WORK) begin
a_data <= a_data >> 1;
end
end
reg [2 ** (DIN_WIDTH_LOG + 1) - 1:0]b_data;
always @(posedge clk or negedge rst_n) begin : proc_b_data
if(~rst_n) begin
b_data <= 0;
end else if(is_read)begin
b_data <= {(2 ** DIN_WIDTH_LOG)'(0),din_data_b};
end else if(status == WORK) begin
b_data <= b_data << 1;
end
end
reg [2 ** (DIN_WIDTH_LOG + 1):0]temp_data,result_data;
always @(*) begin
case (a_data[1:0])
2'b01:temp_data = dout_data + b_data;
2'b10:temp_data = dout_data - b_data;
default:temp_data = dout_data;
endcase
end
always @(posedge clk or negedge rst_n) begin : proc_dout_data
if(~rst_n) begin
result_data <= 0;
end else if(is_read) begin
result_data <= 'b0;
end else if(status == WORK) begin
result_data <= temp_data;
end
end
assign dout_data = result_data;
endmodule
12…10

qiankun

Python,Verilog,IC设计,人工智能

96 日志
8 分类
14 标签
GitHub 简书
Links
  • Google
  • TensorFlow
  • PyTorch
  • GitHub
  • mxnet
  • FlightRadar
© 2019 qiankun
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.2