news 2026/9/16 4:09:45

FPGA中DA分布式算法实现FIR滤波器,用查找表替代乘法器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA中DA分布式算法实现FIR滤波器,用查找表替代乘法器

简介:DA分布式FIR滤波器基于Verilog硬件描述语言实现,在Xilinx Vivado 2019.2中完成开发,采用纯Verilog设计、不依赖特定IP核,可方便移植到Quartus II或ISE环境,适合FPGA开发者与数字信号处理工程师学习高性能滤波器实现与跨平台代码迁移。资源共88个文件、约1.55MB,除核心DA_table.v和da_fir.v源文件外,还包含Vivado工程文件(.xpr)、综合与实现报告(.rpt)、IP核配置及仿真相关文件,覆盖从设计输入到布局布线的完整流程。同时附有FPGA与MATLAB说明文档,用于了解滤波器系数生成与响应验证方法,帮助理解分布式算法如何将乘法转化为移位加法、降低硬件资源消耗。目前已有907人学习下载,对希望掌握DA算法、熟悉Vivado工具链并具备Quartus/ISE移植能力的工程人员颇具参考价值。

1. 做FIR滤波时,为什么我放弃了乘法器改用DA分布式算法

做 FIR 滤波器,很多设计者第一反应是“抽头乘系数再加起来”,于是 16 阶 8bit 输入的滤波器在 FPGA 上要占用十几个 DSP Slice。资源不紧张还行,一旦要把同一片器件塞进多路滤波、FFT 或解调逻辑,乘法器就成了瓶颈。DA 分布式算法思路不一样:它把固定系数的乘累加拆成查找表和移位累加,用 LUT 换 DSP,特别适合 16 阶左右、系数固定且对称的 FIR。这个工程跑在 Vivado 2019.2 上,纯 Verilog 编写,核心文件只有 DA_table.v 和 da_fir.v,一个生成查找表,一个做移位累加。因为没有使用 Xilinx 原语,工程可以直接搬到 Quartus II 或 ISE,对做 FPGA 数字信号处理的人来说,等于拿到了一套跨工具链的分布式 FIR 模板。

2. DA 算法基础:FIR 的乘法为什么能换成查表

2.1 从乘累加公式到位平面分解

一个 N 阶 FIR 输出公式是 y[n] = sum(h[k] * x[n-k]),其中 k 从 0 到 N-1,h[k] 固定。硬件常规做法是例化乘法器,每个抽头一个乘加器;换成 DA 之后,整个乘累加过程可以被“位平面”重写。

把输入 x[k] 看成二进制补码,宽度为 W:x[k] = -b_{MSB} * 2^{W-1} + sum(b_j * 2^j),其中 b_j 是第 j bit。代入 FIR 公式后交换求和次序,得到:

y[n] = sum_{j=0}^{W-1} (2^j * F(b_{j,n}))

这里 F(b_{j,n}) 只与同一时刻所有抽头信号的同一个 bit 有关:给定当前 16 个抽头各自第 j bit 组成的 16 位向量,F 就固定了。这个 F 可以预先算好存进查找表,输入地址是 16 位,查找表输出是系数加权和。对每一位查一次表,再按位权移位累加,W 次完成一个输出采样。

这段推导中容易出错的是补码符号位。最高位的权重是负的,所以符号位查出来的部分和应该做减法,而不是加法。很多初版 DA 实现仿真波形尾部不对,基本都是这个地方加了正号。

2.2 用 MATLAB 生成系数与 DA_table.v

工程里的 fpga&matlab.txt 就是这个用途:先用 fir1 设计低通系数,再定点量化。常见做法是把系数放大到 12bit 有符号范围,也就是 [-2048, 2047],输入数据按 8bit 有符号补码进入。下方脚本可直接生成 Verilog 查找表文件。

% fpga_matlab_da_gen.m N = 16; % 抽头数 h = fir1(N-1, 0.2); % 归一化通带 0.2 pi 的低通系数 coef_width = 12; hq = round(h * (2^(coef_width-1) - 1)); hq = max(-2^(coef_width-1), min(2^(coef_width-1)-1, hq)); fid = fopen('DA_table.v', 'w'); fprintf(fid, '// DA lookup table, N=%d, coef_width=%d\n', N, coef_width); fprintf(fid, 'module DA_table (\n'); fprintf(fid, ' input [%d:0] addr,\n', N-1); fprintf(fid, ' output reg [%d:0] sum\n', coef_width+3); fprintf(fid, ');\n'); fprintf(fid, ' always @(*) begin\n'); fprintf(fid, ' case (addr)\n'); for addr = 0:2^N-1 bits = dec2bin(addr, N) - '0'; sum_val = hq * bits(:); fprintf(fid, ' %d\'d%d: sum = %d\'d%d;\n', N, addr, coef_width+4, sum_val); end fprintf(fid, ' endcase\n'); fprintf(fid, ' end\n'); fprintf(fid, 'endmodule\n'); fclose(fid);

逻辑说明:hq * bits(:)计算“当前抽头 bit 向量”对应的系数和,例子里 16 个抽头会生成 65536 行 case。查找表位宽取 16bit,是因为 12bit 系数 16 项求和,最大绝对值约 12+4=16bit,留了 4bit 裕量,防止中间累加溢出。

合成选项:如果仅做验证,65536 行的 case 也能综合,就是 LUT 占用偏大;若做对称优化,表规模会小很多,细节放到第三章。这里的coef_width最后如果改到 16bit,位宽要同步改,否则综合工具会报 truncation warning。

2.3 查找表里到底存了什么

用一个 4 抽头例子直观理解。假设四个系数分别是 h0、h1、h2、h3,地址最低位对应最旧抽头还是最新抽头需要写代码时统一,这里定义 bit0 对应 h0。

addr (bin)参与求和的系数查找表输出 sum
00000
0001h0h0
0010h1h1
0100h2h2
1000h3h3
1011h0 + h2 + h3h0+h2+h3

可以看到每一项都是纯加法,没有乘法。DA“分布式”的含义就是把系数分散到输入数据的每一位上,每次只查当前位的组合结果,最后的完整输出靠移位累加完成。

3. da_fir.v 实现:移位、查表、累加的时序细节

3.1 顶层接口与参数设计

da_fir.v 是整个工程的核心,建议把它当成一个可复用 IP 来读。我通常这样定义端口:

信号方向位宽说明
clkinput1系统时钟
rst_ninput1低有效复位
dininput8有符号输入,补码
din_validinput1输入有效脉冲
doutoutput16滤波结果
dout_validoutput1输出有效脉冲

参数可以留成parameter N=16, INPUT_W=8, COEF_W=12。这样做的好处是换一个滤波器规格时,不需要改模块内部逻辑,只改参数并重新生成 DA_table.v。纯 Verilog 实现里没有使用 Xilinx RAM 或 DSP 原语,所以综合时不依赖 Vivado 自带的 IP,这也是能移植到 Quartus II 的基础。

3.2 移位寄存器与按位查表

输入数据首先要对齐到 16 个抽头窗口。我习惯用一个数组做移位寄存器,而不是写 16 行dout[0] <= din。这样代码短,也方便后续改成任意抽头数。

reg [7:0] shift_reg [0:15]; integer i; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < 16; i = i + 1) shift_reg[i] <= 8'd0; end else if (din_valid) begin shift_reg[0] <= din; for (i = 1; i < 16; i = i + 1) shift_reg[i] <= shift_reg[i-1]; end end reg [15:0] raddr; always @(*) begin raddr = 16'd0; for (i = 0; i < 16; i = i + 1) raddr[i] = shift_reg[i][bit_index]; end

逻辑说明:shift_reg[0]是最新输入,shift_reg[15]是最旧采样。bit_index由后级状态机提供,从 0 递增到 7。raddr的第 i bit 对应第 i 个抽头的第bit_indexbit,addr 的顺序必须和 MATLAB 生成查找表时定义的 bit 顺序一致,否则系数会错位,滤波结果会变成乱序加权。

这个组合逻辑块的输出会送到 DA_table 的 addr 端口。如果表规模较大,查表输出本身有组合延迟,配合 100MHz 左右时钟时建议在 DA_table 输出后加一级寄存器。

3.3 补码符号位必须做减法

查表得到 F(b_j) 之后,需要逐位累加。下面是一个可读性优先的累加状态机,实际工程中可改成流水线覆盖输出。

localparam IDLE = 3'd0; localparam ACC = 3'd1; localparam SUB = 3'd2; reg [2:0] state; reg [15:0] acc; reg [2:0] bit_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin acc <= 16'd0; bit_cnt <= 3'd0; state <= IDLE; dout_valid <= 1'b0; end else begin dout_valid <= 1'b0; case (state) IDLE: begin if (din_valid) begin acc <= 16'd0; bit_cnt <= 3'd0; state <= ACC; end end ACC: begin if (bit_cnt == 3'd7) begin acc <= acc - table_out; // bit7 是符号位 state <= SUB; end else begin acc <= acc + (table_out << bit_cnt); bit_cnt <= bit_cnt + 1'b1; end end SUB: begin dout <= acc; dout_valid <= 1'b1; state <= IDLE; end endcase end end

逻辑说明:ACC状态里对 bit0 到 bit6 做算术左移累加,table_out << bit_cnt表示乘上 2^bit_cnt。等 bit_cnt 到 7 时,当前查的是补码符号位,权重是 -2^7,所以执行acc - table_outSUB状态只用来寄存最终结果,让dout保持一个完整时钟周期,方便下游模块采样。

如果在这里把符号位处理成加法,会导致直流分量错误。验证方法:给一个恒定的 8’d10 输入,正确输出应该是系数和乘以 10,负权重加错会导致输出明显偏小。

3.4 用 FIR 对称性把查找表从 65536 行降到 256 行

16 阶线性相位 FIR 满足 h[k] = h[15-k],因此可以先合并对称抽头再做查表。这样查表地址从 16 位降到 8 位,规模从 65536 降到 256,代价是多了 8 个对称加法器。对于 Artix-7 这类 LUT 富裕、乘法器紧张的器件,这种交换通常很划算。

wire signed [8:0] pre_sum[0:7]; assign pre_sum[0] = {shift_reg[0][7], shift_reg[0]} + {shift_reg[15][7], shift_reg[15]}; assign pre_sum[1] = {shift_reg[1][7], shift_reg[1]} + {shift_reg[14][7], shift_reg[14]}; // 其余抽头类似

说明:两个 8bit 补码相加可能产生 9bit 结果,所以pre_sum用 9bit 有符号数。{shift_reg[0][7], shift_reg[0]}是符号扩展。对称合并之后,查表表的地址就是pre_sum[0..7],而查表结果仍是 12bit 系数乘 8bit 输入求和,数值范围没有变化。

资源消耗大概是:

实现方式查找表规模额外加法器综合资源预期
直接 DA2^16 = 65536 行LUT 多
对称 DA2^8 = 256 行8 个 9bit 加法器LUT 少

4. 在 Vivado 2019.2 中跑通工程与仿真

4.1 project_1 工程文件结构说明

解压后看到project_1.xprproject_1.srcsproject_1.runsproject_1.ip_user_files等,这是 Vivado 标准工程目录。各目录作用如下:

路径作用
project_1.xprVivado 工程主文件,记录源文件、约束、器件型号
project_1.srcs/sources用户源码,包括 da_fir.v、DA_table.v
project_1.srcs/sim仿真测试文件
project_1.runs/synth_1综合结果与 runme.log
project_1.runs/impl_1布局布线结果与时序报告
fpga&matlab.txt系数设计说明与 MATLAB 对比信息
4.1.1 用 Tcl 重建,避免路径漂移

如果打开 xpr 时提示文件路径错误,通常是因为工程压缩包在其他机器解压后路径变化。最省事的做法是不改工程文件,用 Vivado Tcl Console 重建。

create_project da_fir_proj ./da_fir_proj -part xc7a35tcsg324-1 add_files -norecurse ./src/da_fir.v ./src/DA_table.v add_files -fileset sim_1 ./src/tb_da_fir.v set_property top da_fir [current_fileset] set_property top tb_da_fir [get_filesets sim_1] launch_simulation -mode behavioral run 20 us

参数说明:-part xc7a35tcsg324-1指定 Artix-7,如果实际板卡是其他型号,改成对应 part name,DA 逻辑本身不依赖具体器件。-fileset sim_1把 testbench 加入仿真文件集。run 20 us行为仿真跑 20 微秒,假设时钟周期 10ns,能覆盖 2000 个输入采样。

4.2 测试平台:把硬件输出导出成文本

要验证滤波器行为,建议用 testbench 把dout写进文本文件,再和 MATLAB 结果对比。下面是一个能直接跑的仿真框架:

module tb_da_fir; reg clk = 0; reg rst_n = 0; reg din_valid = 0; reg signed [7:0] din = 0; wire signed [15:0] dout; wire dout_valid; integer fd, i; always #5 clk = ~clk; initial begin fd = $fopen("fir_out.txt", "w"); rst_n = 0; #30 rst_n = 1; for (i = 0; i < 500; i = i + 1) begin @(posedge clk); din_valid = 1; din = $random; end @(posedge clk); din_valid = 0; #200; $fclose(fd); $finish; end always @(posedge clk) begin if (dout_valid) $fwrite(fd, "%0d %0d\n", $time, dout); end da_fir dut( .clk(clk), .rst_n(rst_n), .din(din), .din_valid(din_valid), .dout(dout), .dout_valid(dout_valid) ); endmodule

说明:din = $random生成的是 32bit 随机数,赋值给 8bit 端口会自动截断,产生噪声输入;如果想对比特定频率响应,可以在 MATLAB 中生成正弦波量化后写入文件,再用$readmemb读到 tb 中。$fwrite只记录dout_valid为高的时刻,避免在流水线中间采样。最后用 MATLAB 把filter(hq,1,din)的定点结果与fir_out.txt比较。

4.3 仿真结果异常时先看哪几个信号

行为仿真失败,优先查三处:

  1. raddr在 bit_index 变化时是否毛刺。Vivado 的 xsim 在组合逻辑仿真里通常不会像门级仿真那样产生 glitch,但如果 data type 错误会直接 X。
  2. table_out是否在bit_cnt=7前稳定。如果accACC状态比预期多一个时钟,检查查表模块是否有额外寄存器。
  3. 输出dout是否为有符号值。如果只看[15:0]的十进制,补码负数会被当成大正数,改用$signed(dout)显示。

综合后的报告也要看。report_utilization里如果 DSP48E 数量不是 0,说明代码中存在*运算并被工具推断为硬件乘法器。想确认是否保持了 DA 结构,在综合后原理图里搜索LUTMULT的实例数量即可。

5. 移植到 Quartus II 或 ISE 的差异与验证技巧

5.1 工具链差异判断

工程里没有 Xilinx 原语,所以移植第一步不是改代码,而是确认目标工具的 Verilog 版本。ISE 对 Verilog-2001 支持稳定,代码里建议避免使用++-:等扩展写法,本工程写法能直接兼容。Quartus II 对signed表达式的推导比 Vivado 严格,常见报错是“non-constant operand”,出现这种问题时把算术表达式写成完整的补码加法和移位,不要用隐式 signed 运算。

检查项Vivado 2019.2Quartus IIISE
工程文件.xpr.qpf.xise
约束文件.xdc.sdc.ucf
仿真工具xsimModelSimiSim/ModelSim
initial支持综合忽略,行为仿真生效基本忽略忽略

5.2 移植中最容易踩的两个坑

第一个是DA_table.v的 65536 行 case。Quartus 综合时可能因为case太长产生 memory inference 警告,甚至把查找表综合成冗余逻辑。常见做法是对称合并后只保留 256 行表,或者在生成时把大表拆成两个 8bit 地址的小表级联。

第二个是寄存器初始值。Vivado 和 Quartus 行为仿真允许initial给 reg 赋初值,但上板后这些初值会被忽略。复位分支必须把所有accshift_reg、状态机寄存器全部归零。我的习惯是写一个synch_reset内部复位,在rst_n拉低后的下一个时钟沿让状态机回到IDLE,避免异步复位释放时产生亚稳态。

reg rst_n_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) rst_n_r <= 1'b0; else rst_n_r <= 1'b1; end

5.3 一个实用的移植验证技巧

移植后不要急着上板,先把 Vivado 仿真导出的fir_out.txt和 Quartus/ISE 仿真导出的结果做逐点对比。两份数据如果是纯组合路径加流水,应该完全一致;如果只在最高位差 1,检查是不是仿真时把table_out从 16bit 截断到 8bit;如果数据逐点相差固定倍数,通常是符号位加减方向搞反了。

更快的定位方法是让 MATLAB 脚本直接生成lookup.hex,Verilog testbench 用$readmemh加载同一个查找表,这样三套 FPGA 工具共享同一份系数,可以排除“表内容不一致”这个变量。把$readmemh写进 DA_table 例化位置,替换掉原来的组合逻辑 case,就能在不重新运行 MATLAB 的情况下验证不同综合器的实现是否一致。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 4:09:28

嵌入式固件下载与OTA升级全链路故障排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 4:08:49

AGV核心岗位深度拆解:从伺服控制到物联网路径规划

最近我手上有好几个朋友来问同一件事&#xff0c;说收到了“必高具身智能猎头推荐职位”里关于AGV方向的岗位推送&#xff0c;职位名称一长串&#xff1a;“伺服软件工程师、伺服算法工程师、物联网路径工程师、物联网导航定位工程师、控制技术工程师”。大部分人第一反应是&am…

作者头像 李华
网站建设 2026/9/16 4:08:39

拖把更名器深度指南:批量文件重命名工具的功能与应用

拖把更名器&#xff0c;这个名字在不少老网民的记忆里&#xff0c;几乎和“批量改名”划等号。我第一次用它的时间&#xff0c;大概要追溯到Windows XP还很流行的年代&#xff0c;那时候电脑里的下载文件、照片、MP3&#xff0c;动不动就是“新建文件夹(2)”“00(1).jpg”这种乱…

作者头像 李华
网站建设 2026/9/16 4:08:37

商用饮水机选购全攻略:类型、品牌、成本与维护一次讲清

又到了换饮水机的季节&#xff0c;后台和微信里隔三差五就有人问我&#xff1a;商用饮水机到底怎么选&#xff1f;哪个牌子更靠谱&#xff1f;说实话&#xff0c;这个问题我接了不下几十次。我自己经营过两个办公室&#xff0c;前前后后换过好几台商用饮水机&#xff0c;也帮朋…

作者头像 李华
网站建设 2026/9/16 4:08:20

通义千问接入飞书机器人:从长连接到上下文管理的实战指南

上周末我把这个机器人从本地测试群推到部门大群之后&#xff0c;半小时内被同事了二十几次。有人问它能不能写周报&#xff0c;有人让它解释一段线上日志里的报错&#xff0c;还有人直接扔了个需求文档链接过来。那一刻我才觉得&#xff0c;这个"通义千问对接飞书机器人&q…

作者头像 李华
网站建设 2026/9/16 4:08:03

激光SLAM入门实战指南:从原理到代码的完整学习路径

先说我自己的结论&#xff1a;激光SLAM 不是一门“看完就会”的课&#xff0c;而是一条必须亲手跑通代码、亲手调过一版参数才算入门的技能线。最近这套号称“清华大佬”一口气讲完的 47 集激光SLAM入门教程在圈子里传得很广&#xff0c;标题里又是“3天速通”、又是“具身智能…

作者头像 李华