1 概述
1 滤波器的两个基本应用:信号重建和信号分离。
信号分离更常用到将输入信号分离到不同部分。或者设计低通滤波、带通滤波器来筛选特定信号频率。
信号重建时指滤除可能混入有用信号的噪声和其他失真。例如通过无线信道传输数据。信号重建包括平滑信号和移除直流分量。
2 数字FIR滤波器
数字FIR通常处理由采样连续信号产生的离散信号。采样的数据格式取决于应用场景。数字通信领域通常使用复数inphase和quadrature或I/Q值来白哦是一个采样数据。
2 背景
1 脉冲响应
对滤波器输入脉冲信号得到的输出信号为该滤波器FF的脉冲响应。可以通过卷积的方法算出该滤波器任意输入的输出响应。该运算过程结合滤波器脉冲响应(系数或阶)和输入信号来计算输出信号。滤波器的输出可以通过时域或频域的方式进行计算,本章重点关注时域计算。
(1)卷积运算 卷积核size及其放置位置;对应元素相乘后相加;
(2)N-阶FIR滤波器的系数与输入信号之间的卷积运算可以表示为:
PS: 对于一个N-阶滤波器的输出值,需要N个乘法和N-1个加法。
(3)滑动均值滤波器
滑动均值滤波器是低通FIR滤波器的一种简单形式,其所有系数都是相同的且和为1. 简单来说,一个滑动均值滤波器,它将输入信号的几个相邻样本相加,再求平均值。 可以用1/N取代上式中的h[j] 系数矩阵。
这里N个因素的计算,共进行了N次加法和1次乘法。(滑动均值滤波器比常规的FIR滤波器更简单)这个过滤器是因果系统,这意味着输出数据与当前输入值及以前的数据有关。
(巧妙地表达:虽然从根本上来说因果特性是系统分析地一个重要属性,但是对于硬件实现来说,一个有限非因果滤波器可以通过数据缓冲或则和重排列来实现转因果系统地转换。)
滑动均值滤波器的作用:平滑信号(去除随机噪声); N值得增大等效于减小输出信号带宽;平滑等同于降低高频分量。滑动滤波器是最优得减少白噪声同时保持最陡峭阶跃响应得滤波器,即再给定边缘锐度得情况下把噪声压到最低。
PS:通常滤波器系数可以用来精确得创建许多不同类型得滤波器:低通、高通、带通等;一般情况下,设计滤波器时,阶数越大提供的自由度越多,设计滤波器的性能越好。可以忽略系数本身是如何求得的,但滤波器的结构和特定系数对实现该滤波器需要执行的操作数可能会产生很大的影响。
3 FIR结构基础
代码对不同变量类型使用typedef(更大限度的使数据类型的定义更加灵活)。 以便后续方便对数据类型的更改。
#define N 11
#include "ap_int.h"
typedef int coef_t;
typedef int data_t;
typedef int acc_t;
void fir(data_t *y, data_t x){
coef_t C[N] ={
53,0,-91,0,315,500,313,0,-91,0,53
};
static
data_t shift_reg[N];
axx_t acc;
int i;
acc=0;
shift_Accum_loop; //benefit for the compile
for(i=N-1; i>=0; i--){
if(i==0){
acc+=x*C[0];
shift_reg[0]=X;
}else{
shift_reg[i]=shift_reg[i-1];
acc+=shift_reg[i]*C[i];
}
}
*y=acc;
}
4 计算性能
当评价设计性能时,必须说明度量。针对FIR滤波器的运行速度度量方法为滤波操作数/秒,或者Y操作/秒。另一种度量方法是乘累加操作:MAC/s。从滤波数/秒等效为位/秒需要了解关于输入和输出数据位宽的信息。
5 操作链接
较低的时钟频率为工具在单个周期中组合多个相关操作提供了更多的时间余量,这个过程叫做 操作链接。
6 代码提升
for 循环内部的if/else语句效率很低。在代码中每个控制结构,VHLS会生成硬件逻辑电路来检查条件是否满足,这个检查在每个循环中都执行。此外,这种条件结构限制了if或else分支中语句的执行,这些语句只有在解决了if条件语句之后才能执行。 在循环中删除if/else控制流。
7 循环拆分
在for循环中执行两个基本操作,一个是通过shift_reg数组进行数据移位;另一个是进行乘累加运算来计算输出样本。
循环分裂是分别在两个循环中实现各自操作。这样做允许我们在每个循环上分别进行优化。
每个循环单独拆分往往不能提高硬件实现效率,但是它可以实现每个循环独立地进行优化。
TDL:
for(i=N-1; i>1; i=i-2){
shift_reg[i] = shift_reg[i-1];
shift_reg[i-1] = shift_reg[i-2];
}
if(i==1){
shift_reg[1] = shift_reg[0];
}
shift_reg[0] = x;
8 循环展开
默认情况下,HLS会将循环综合成顺序执行方式。数据路径顺序执行每次循环迭代运算,从而创建了一个有效的区域架构,但是它限制了在循环迭代中可能出现的并行运算。
循环展开会通过循环次数来复制循环的主体。每次循环迭代循环次数减少相同因子。在最好情况下,当循环中没有任何语句依赖于前一次迭代生成的任何数据时,循环主题可以大大增加并行性,从而使系统运行速度更快。
对于上述for循环,每次迭代要求我们从shift_reg数组中读取两个值,而且要在同一个数组上写上两个值。因此如果希望并行的执行这两个语句,必须能够在相同的周期内对shift_reg数组执行两个读操作和两个写操作。
可以使用#pragma HLS array_partition variable = shift_reg complete 设置VHLS使其将所有放在shift_reg数组中的值放到寄存器中。
可以使用unroll指令,使VHLS自动循环展开:#pragma HLS unroll factor = 2 放入for循环之后。
acc = 0;
MAC:
for(i=N-1;i>=3;i-=4){
#pragma HLS unroll factor = 4
acc+=shift_reg[i] * c[i]+
shift_reg[i-1]*c[i-1]+
shift_reg[i-2]*c[i-2]+
shift_reg[i-3]*c[i-3];
}
for(;i>=0;i--){
acc+=shift_reg[i]*c[i];
}
完整的循环展开实现程序最大的并行性,这样的代价是需要更多的资源。故可以在较小的循环上执行完整的循环。但是大迭代次数的循环展开通常不行。通常情况下,HLS会运行很长一段时间并且往往在经过几个小时综合之后都会失败,如果这样的循环进行展开,它展开结果会是生成非常大的代码。
PS:如果设计15min内无法综合完成,则应该考虑优化效果。
9 循环流水
循环流水:允许同时执行多个循环迭代运算。
读取操作需要两个时钟周期,第一个周期提供内存地址,第二个时钟周期完成数据传递。由于二者间无依赖关系,可以并行执行。
与for循环相关的性能指标:
迭代延迟:执行一次循环运算所需要的时钟周期数。for循环延迟是完成整个循环所需要的时钟周期数。
循环流水线是将for循环多个迭代运算进行重叠的优化。
循环起始间隔是另一个重要的性能度量。它定义为本次循环到下一次循环开始的时钟周期数。
#pragma HLS pipeline II=2 设置Vivado HLS工具II = 2。 设置循环起始间隔的指令
任何for循环都可以进行流水操作,每次循环有两个操作数:读,写。循环延迟为2个时钟周期。读操作需要2周期,写操作在第2周期末开始执行。for循环按照非流水操作需要20个时钟周期。
通过在循环头部后面插入指令#pragma HLS pipeline II=1将循环进行流水优化。综合的结果是循环间隔为1个时钟周期。即每个时钟周期都可以开始循环迭代运算。
使用指令指定内存类型
#pragma HLS resource variable = shift_reg core = RAM 1P 强制HLS工具使用单端口RAM。当使用该指令与流水优化进行结合时,HLS工具将无法使用II=1来连接此循环。
在第2个时钟周期需要在迭代1中完成对数据shift_reg的写操作,在迭代2中对相同的数组进行读操作。可以删除II=1的确定优化如#pragma HLS pipeline,从而允许HLS有更多调整自由度。 在这种情况下,HLS将自动增加初始间隔,知道找到可行的流程表。
10 位宽优化
所有C语言的数据类型位宽都是2的幂次。在许多情况下为了优化硬件资源,需要处理位宽不是2的幂次的数据。比如10bits, 12bits,14bits,我们可以将其映射到16bits,但这可能会降低处理性能并增加资源消耗。 为了准确表达这些值,HLS提供了任意精度数据类型,这些数据类型可以表示为有符号或者无符号任意位宽数据。
#include "ap_int.cpp"
无符号:ap_unit 32位
有符号:ap_int 8位
数据类型coef_t被定义为int,是指我们有32位精度。 coef_t可以声明为 ap_int
一般来说,在做加法时,运算结果要比两个加数中最大数值的位宽还要多1位。
11 复数FIR滤波器
函数充当接口,HLS工具不能跨越函数边界进行优化。即每个fir函数都被独立综合,并且在cmoplexFIR函数中被当作一个黑盒来处理。 VHLS工具可以自动进行函数内联。inline指令去除了函数边界,使得VHLS对代码可以进行额外优化,但是这种额外优化的方式可能带来复杂的综合问题,比如使得编译时间变长。同时,它在执行函数调用时将排除顶层关联。在inline指令中,也有递归参数,即在inline函数中,其调用的子函数也inline处理,即所有子函数都会把代码展开到母函数中。这个可能会导致代码膨胀,且一个inline函数不会有单独报告。