一、FIR 滤波器基础理论
1. FIR 数学公式
有限长单位冲激响应滤波器(FIR)差分方程:
yn=∑k=0N−1hk⋅xn−k
- N:滤波器阶数(抽头数);
- hk:滤波器系数(固定,对称线性相位);
- xn−k:当前与过去 N 个输入采样;
- yn:滤波输出。
核心运算:乘累加 MAC (Multiply-Accumulate),每输出 1 个采样,需要 N 次乘法 + N 次加法。
2. 传统结构痛点
直接并行 MAC:N 个乘法器,阶数高时消耗大量 DSP48 资源; 串行分时 MAC:只用 1 个乘法器,分时复用,但需要时钟提速。 基于累加器的 FIR 是串行分时 MAC 的最简实现架构,核心只用一组乘法器 + 累加寄存器,适合资源受限 FPGA。
二、基于累加器的 FIR 核心原理
1. 架构核心思想
将求和运算拆分为逐次乘累加:
sum0sum1sum2yn=h0⋅xn=sum0+h1⋅xn−1=sum1+h2⋅xn−2...=sumN−1
流程拆解:
- 输入移位寄存器缓存 N 个历史采样 xn,xn−1,...,xn−N+1;
- 计数器遍历 0~N-1,依次取出系数hk和对应采样xn−k;
- 乘法器算出单路乘积,送入累加器持续叠加;
- 一轮 N 次循环结束,累加器输出滤波结果,随后清零准备下一组采样。
2. 硬件模块划分
整体分 5 个子模块:
- 输入移位寄存器组(Shift Register) 缓存连续 N 个输入采样,每次新采样到来右移 1 位,保存历史数据;
- 系数 ROM 预存 FIR 抽头系数h0∼hN−1,FPGA 用 Block ROM 实现,系数可定点量化;
- 循环计数器(Cnt) 0~N-1 循环计数,同时作为 ROM 读地址、移位寄存器选择地址;
- 单路乘法器(DSP48E1) 分时复用,同一时刻只计算一组hk×xn−k;
- 累加器(Accumulator) 带同步清零,每次乘积输入持续相加,计数到 N-1 时输出结果。
3. 时序工作逻辑(关键)
设滤波器阶数N=8,系统时钟远高于采样时钟(过采样分时运算):
- 采样时钟上升沿:采集新输入xnew,移位寄存器整体右移;复位计数器、累加器;
- 系统时钟连续 8 拍:
- 拍 0:cnt=0 → 读 h 0、取 x n → 乘积存入累加器;
- 拍 1:cnt=1 → 读 h 1、取 x n-1 → 累加器 = 原值 + 新乘积;
- ......
- 拍 7:cnt=7 → 最后一组乘加,累加器输出有效滤波值 y;
- 等待下一次采样触发,重复循环。
约束条件:系统时钟频率 ≥ N × 采样频率,保证在两次采样间隔内完成 N 次乘累加。
4. 定点量化说明(FPGA 必做)
浮点数系数无法直接硬件实现,定点化处理:
- MATLAB 生成浮点 h k;
- 放大2Q倍取整,Q 为小数位宽(常用 Q15、Q16);
- 输出累加后右移 Q 位还原幅值。
例:Q15 量化,系数范围 -1,1 → 乘以 32768 取 16 位整数存入 ROM。
三、线性相位 FIR 优化(节省资源)
绝大多数 FIR 采用对称系数 hk=hN−1−k,基于累加器架构可减半运算次数:
yn=∑k=0(N/2)−1hk⋅(xn−k+xn−(N−1−k))
硬件改动:
- 先将对称位置两个采样相加,再与系数相乘;
- 循环次数从 N 次变为 N/2 次,大幅降低运算时钟要求,累加器架构同样兼容。
四、Verilog 工程实例(8 阶低通 FIR,基于累加器串行 MAC)
1. 参数定义
- 阶数 N=8;
- 输入位宽:12bit 采样数据;
- 系数 Q15 定点 16bit;
- 累加器位宽:32bit(防止溢出);
- 系统时钟 50MHz,采样频率 5MHz(50M ≥ 8×5M,满足时序)。
2. 完整代码
verilog
module fir_accumulator
#(
parameter TAP_NUM = 8, // 滤波器阶数
parameter DATA_WIDTH = 12, // 输入采样位宽
parameter COE_WIDTH = 16, // 系数Q15位宽
parameter ACC_WIDTH = 32, // 累加器位宽防溢出
parameter Q_BIT = 15 // 定点小数位
)
(
input wire clk, // 50MHz系统时钟
input wire rst_n, // 低电平复位
input wire sample_en, // 采样使能5MHz
input wire [DATA_WIDTH-1:0] x_in, // 输入采样
output reg y_valid, // 输出有效标志
output reg [DATA_WIDTH-1:0] y_out // 滤波输出
);
// ====================== 1. 移位寄存器缓存采样 ======================
reg [DATA_WIDTH-1:0] shift_reg [0:TAP_NUM-1];
integer i;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
for(i=0; i<TAP_NUM; i=i+1) shift_reg[i] <= 'd0;
end
else if(sample_en) begin
shift_reg[0] <= x_in;
for(i=1; i<TAP_NUM; i=i+1) shift_reg[i] <= shift_reg[i-1];
end
end
// ====================== 2. 系数ROM 8阶低通FIR Q15定点 ======================
reg [COE_WIDTH-1:0] coe_rom [0:TAP_NUM-1];
initial begin
// MATLAB生成8阶低通系数 Q15量化
coe_rom[0] = 16'h0240;
coe_rom[1] = 16'h0780;
coe_rom[2] = 16'h0C80;
coe_rom[3] = 16'h1000;
coe_rom[4] = 16'h1000;
coe_rom[5] = 16'h0C80;
coe_rom[6] = 16'h0780;
coe_rom[7] = 16'h0240;
end
// ====================== 3. 循环计数器 0~7 ======================
reg [3:0] cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 'd0;
end
else if(sample_en) begin
cnt <= 'd0;
end
else if(cnt < TAP_NUM - 1) begin
cnt <= cnt + 1'b1;
end
end
// ====================== 4. 分时乘法器 ======================
wire [DATA_WIDTH-1:0] x_sel;
wire [COE_WIDTH-1:0] h_sel;
wire [ACC_WIDTH-1:0] mult_prod;
assign x_sel = shift_reg[cnt];
assign h_sel = coe_rom[cnt];
// 乘法器:有符号乘,自动调用DSP48
assign mult_prod = $signed(x_sel) * $signed(h_sel);
// ====================== 5. 累加器核心 ======================
reg signed [ACC_WIDTH-1:0] accum;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
accum <= 'd0;
end
else if(sample_en) begin
accum <= 'd0; // 新采样到来清空累加器
end
else begin
accum <= accum + mult_prod; // 逐次累加乘积
end
end
// ====================== 6. 输出锁存与有效标志 ======================
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
y_valid <= 1'b0;
y_out <= 'd0;
end
else if(cnt == TAP_NUM - 1) begin
y_valid <= 1'b1;
// 右移Q_BIT位,截断至输入位宽
y_out <= accum[ACC_WIDTH-1 : Q_BIT];
end
else begin
y_valid <= 1'b0;
end
end
endmodule
五、代码关键逻辑解读
- 移位寄存器
sample_en(采样时钟)到来时存入新采样,所有历史数据右移,shift_reg[k]对应xn−k; - 系数 ROM
initial块预存定点系数,高阶滤波器可改用 IP 核 Block ROM 节省逻辑; - 计数器时序 采样使能时 cnt 清零,之后每个系统时钟 + 1,依次读取每一组采样与系数;
- 累加器工作 每次采样刷新自动清零,每个时钟叠加一次乘积,完成全部 8 次乘加后输出;
- 定点缩放 累加结果右移 15 位抵消 Q15 放大,截取高位得到与输入同位宽输出。
六、仿真验证思路(ModelSim/MATLAB 联合)
- MATLAB 生成正弦叠加带噪输入波形,导出 txt 采样数据;
- 仿真激励读取采样,驱动
sample_en; - 抓取
y_out输出波形,与 MATLAB 浮点 FIR 滤波结果对比; - 观察
y_valid仅在每轮 8 次运算结束时拉高,输出平滑滤波波形,高频噪声被抑制。
七、架构优缺点分析
优点
- 资源占用极低:仅使用 1 个硬件乘法器(1 个 DSP48),阶数提升不增加乘法器;
- 结构简单易实现:无复杂并行布线,时序收敛容易;
- 系数修改方便:系数存在 ROM,可动态更新实现可调滤波。
缺点
- 吞吐率受限:完成一次输出需要 N 个系统时钟,采样频率受系统时钟约束;
- 高阶滤波器时钟压力大:N 很大时必须大幅提升系统时钟,无法满足高速采样场景;
适用场景
低速采集系统(温感、音频、低频传感器)、FPGA 逻辑资源紧张、滤波器阶数中等(N<128)。
八、拓展改进方案
- 对称系数减半运算 增加加法器先合并对称采样,循环次数减半,降低时钟频率要求;
- 多周期流水线累加 在乘法器后插入寄存器,切割关键路径,提升最高系统时钟;
- 多通道时分复用 多组输入共用同一套累加器 FIR,进一步节省 DSP;
- 分布式 RAM 存采样 高阶 FIR 时用 Block RAM 代替移位寄存器,减少大量寄存器消耗。
九、与并行 FIR 对比总结
表格
| 架构 | 乘法器数量 | 时钟要求 | 资源消耗 | 最高采样速率 |
|---|---|---|---|---|
| 并行直接型 FIR | N 个 | 采样时钟即可 | 极高 | 最高 |
| 基于累加器串行 FIR | 1 个 | N× 采样时钟 | 极低 | 低速 |
基于累加器的 FIR 是面积换取速度的典型串行架构,是 FPGA 入门 FIR 最经典、最容易理解的实现方式。
for(i=1; i<TAP_NUM; i=i+1) shift_regi <= shift_regi-1;什么意思
逐行拆解这条移位寄存器代码
verilog
for(i=1; i<TAP_NUM; i=i+1)
shift_reg[i] <= shift_reg[i-1];
1. 先看懂变量定义
shift_reg 是一个寄存器数组,用来存 FIR 滤波器连续 N 个采样点:
verilog
reg [DATA_WIDTH-1:0] shift_reg [0:TAP_NUM-1];
TAP_NUM:FIR 抽头总数(例子里是 8)- 数组下标:
shift_reg[0]、shift_reg[1] ... shift_reg[7] shift_reg[0]:最新进来的采样x[n]shift_reg[1]:上一拍采样x[n-1]shift_reg[2]:前两拍采样x[n-2]......shift_reg[7]:最早的旧采样x[n-7]
2. for 循环执行逻辑(硬件综合后等效移位链)
循环条件:
- 初始
i=1 - 只要
i < TAP_NUM就执行一次赋值,然后i=i+1
以 TAP_NUM=8 举例,循环会依次执行这 7 条并行赋值:
verilog
shift_reg[1] <= shift_reg[0];
shift_reg[2] <= shift_reg[1];
shift_reg[3] <= shift_reg[2];
shift_reg[4] <= shift_reg[3];
shift_reg[5] <= shift_reg[4];
shift_reg[6] <= shift_reg[5];
shift_reg[7] <= shift_reg[6];
3. 整体移位流程(配合前面一句)
完整移位逻辑是两段合在一起:
verilog
shift_reg[0] <= x_in; // 第1步:新数据放进最左边shift_reg[0]
for(i=1; i<TAP_NUM; i=i+1)
shift_reg[i] <= shift_reg[i-1]; // 第2步:所有旧数据整体右移一格
移位过程演示(8 阶 FIR)
假设移位前: [0]=A(新)、[1]=B、[2]=C、[3]=D、[4]=E、[5]=F、[6]=G、[7]=H
采样使能到来,先把新采样 x_in=X 送入 [0],再执行 for 循环移位:
shift_reg[1]取原来[0]的 Ashift_reg[2]取原来[1]的 B- ......
shift_reg[7]取原来[6]的 G
移位后状态: [0]=X、[1]=A、[2]=B、[3]=C、[4]=D、[5]=E、[6]=F、[7]=G 最末尾旧数据 H 被直接丢弃,实现先进先出的采样缓存。
4. 为什么 i 从 1 开始,不从 0?
shift_reg[0]单独赋值新输入x_in,不需要从左边拿数据;- 如果 i 从 0 开始,会出现
shift_reg[0] <= shift_reg[-1],下标越界,语法报错。
5. 硬件层面本质
Verilog 里always@(posedge clk)内部的 for 循环是并行执行 ,不是软件 C 语言串行循环: 综合后生成一条级联移位寄存器链 ,每来一次采样时钟,整组采样集体右移一位,专门给 FIR 提供延时序列 x[n],x[n-1],...,x[n-N+1],满足卷积公式的延时需求。
6. 一句话总结
这条 for 循环的作用: 当有新采样输入时,把移位寄存器里所有历史采样依次向右挪一格,腾出第一个位置存放最新采样,丢掉最老旧的采样,为 FIR 滤波提供不同延时的输入数据。