FPGA 基于累加器的 FIR 滤波器原理与工程实例

一、FIR 滤波器基础理论

1. FIR 数学公式

有限长单位冲激响应滤波器(FIR)差分方程:

yn=∑k=0N−1​hk⋅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. 架构核心思想

将求和运算拆分为逐次乘累加

sum0​sum1​sum2​yn​=h0⋅xn=sum0​+h1⋅xn−1=sum1​+h2⋅xn−2...=sumN−1​​

流程拆解:

  1. 输入移位寄存器缓存 N 个历史采样 xn,xn−1,...,xn−N+1
  2. 计数器遍历 0~N-1,依次取出系数hk和对应采样xn−k
  3. 乘法器算出单路乘积,送入累加器持续叠加;
  4. 一轮 N 次循环结束,累加器输出滤波结果,随后清零准备下一组采样。

2. 硬件模块划分

整体分 5 个子模块:

  1. 输入移位寄存器组(Shift Register) 缓存连续 N 个输入采样,每次新采样到来右移 1 位,保存历史数据;
  2. 系数 ROM 预存 FIR 抽头系数h0∼hN−1,FPGA 用 Block ROM 实现,系数可定点量化;
  3. 循环计数器(Cnt) 0~N-1 循环计数,同时作为 ROM 读地址、移位寄存器选择地址;
  4. 单路乘法器(DSP48E1) 分时复用,同一时刻只计算一组hk×xn−k
  5. 累加器(Accumulator) 带同步清零,每次乘积输入持续相加,计数到 N-1 时输出结果。

3. 时序工作逻辑(关键)

设滤波器阶数N=8,系统时钟远高于采样时钟(过采样分时运算):

  1. 采样时钟上升沿:采集新输入xnew,移位寄存器整体右移;复位计数器、累加器;
  2. 系统时钟连续 8 拍:
    • 拍 0:cnt=0 → 读 h 0、取 x n → 乘积存入累加器;
    • 拍 1:cnt=1 → 读 h 1、取 x n-1 → 累加器 = 原值 + 新乘积;
    • ......
    • 拍 7:cnt=7 → 最后一组乘加,累加器输出有效滤波值 y;
  3. 等待下一次采样触发,重复循环。

约束条件:系统时钟频率 ≥ N × 采样频率,保证在两次采样间隔内完成 N 次乘累加。

4. 定点量化说明(FPGA 必做)

浮点数系数无法直接硬件实现,定点化处理:

  1. MATLAB 生成浮点 h k
  2. 放大2Q倍取整,Q 为小数位宽(常用 Q15、Q16);
  3. 输出累加后右移 Q 位还原幅值。

例:Q15 量化,系数范围 -1,1 → 乘以 32768 取 16 位整数存入 ROM。

三、线性相位 FIR 优化(节省资源)

绝大多数 FIR 采用对称系数 hk=hN−1−k,基于累加器架构可减半运算次数:

yn=∑k=0(N/2)−1​hk⋅(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

五、代码关键逻辑解读

  1. 移位寄存器 sample_en(采样时钟)到来时存入新采样,所有历史数据右移,shift_reg[k]对应xn−k
  2. 系数 ROM initial块预存定点系数,高阶滤波器可改用 IP 核 Block ROM 节省逻辑;
  3. 计数器时序 采样使能时 cnt 清零,之后每个系统时钟 + 1,依次读取每一组采样与系数;
  4. 累加器工作 每次采样刷新自动清零,每个时钟叠加一次乘积,完成全部 8 次乘加后输出;
  5. 定点缩放 累加结果右移 15 位抵消 Q15 放大,截取高位得到与输入同位宽输出。

六、仿真验证思路(ModelSim/MATLAB 联合)

  1. MATLAB 生成正弦叠加带噪输入波形,导出 txt 采样数据;
  2. 仿真激励读取采样,驱动sample_en
  3. 抓取y_out输出波形,与 MATLAB 浮点 FIR 滤波结果对比;
  4. 观察y_valid仅在每轮 8 次运算结束时拉高,输出平滑滤波波形,高频噪声被抑制。

七、架构优缺点分析

优点

  1. 资源占用极低:仅使用 1 个硬件乘法器(1 个 DSP48),阶数提升不增加乘法器;
  2. 结构简单易实现:无复杂并行布线,时序收敛容易;
  3. 系数修改方便:系数存在 ROM,可动态更新实现可调滤波。

缺点

  1. 吞吐率受限:完成一次输出需要 N 个系统时钟,采样频率受系统时钟约束;
  2. 高阶滤波器时钟压力大:N 很大时必须大幅提升系统时钟,无法满足高速采样场景;

适用场景

低速采集系统(温感、音频、低频传感器)、FPGA 逻辑资源紧张、滤波器阶数中等(N<128)。

八、拓展改进方案

  1. 对称系数减半运算 增加加法器先合并对称采样,循环次数减半,降低时钟频率要求;
  2. 多周期流水线累加 在乘法器后插入寄存器,切割关键路径,提升最高系统时钟;
  3. 多通道时分复用 多组输入共用同一套累加器 FIR,进一步节省 DSP;
  4. 分布式 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 循环移位:

  1. shift_reg[1] 取原来 [0] 的 A
  2. shift_reg[2] 取原来 [1] 的 B
  3. ......
  4. 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 滤波提供不同延时的输入数据。