简介:面向数字信号处理课程设计、期末大作业与项目开发的VHDL LMS自适应滤波器完整工程包,适合FPGA方向学生与开发者参考延展。工程基于LMS算法实现自适应滤波,输入为0.25MHz与7.5MHz混合余弦信号,经标准化后存为coe文件并例化ROM产生,滤波模块内含12位Booth补码乘法器、参数更新电路及参考信号生成等关键设计,代码均通过严格测试。资源共788个文件,以vhdl/v源码、coe/mif初始化文件、xdc约束、do仿真脚本、txt说明文档等为主,压缩包约115.98MB,目录结构清晰。目前已有77人学习下载,附带的md文档可帮助理解模块划分与工程搭建思路,便于在此基础上二次开发或直接用于课程答辩。
1. 为什么 VHDL 项目里要自己搭一个 LMS 滤波器
如果一个 0.25MHz 余弦和 7.5MHz 余弦叠在一起,直接做固定系数滤波当然可以,但一旦目标频率发生漂移或噪声特性变化,固定滤波器就要重新综合。LMS 自适应滤波器的好处是它用同一个权值更新电路去跟踪统计特性,工程里广泛用于回声消除、信道均衡和主动降噪。这个基于 VHDL 的课程设计资源包含 LMS 自适应滤波器模块、ROM 信号发生器、12 位 Booth 补码乘法器、Vivado xsim 仿真脚本和配套文档。对要做期末大作业或课程设计的人来说,可以直接打开源码先跑通仿真,再按自己的需求改输入频率、ROM 深度和权值位宽。下面按“算法设计 → RTL 实现 → 仿真验证 → 工程化排错”的顺序拆开讲。
2. 从最小均方到符号误差:参数更新与信号量化设计
2.1 先把更新公式写清楚
经典 LMS 的迭代公式是:
y(n) = W^T(n) X(n)
e(n) = d(n) - y(n)
W(n+1) = W(n) + μ e(n) X(n)
这个公式要求同时计算 μ、e(n)、X(n) 的乘积。在 FPGA 里做,如果 μ 是小数,还要把它定点化;如果运算路径稍长,更新周期还得和乘法器对齐。为了在有限的 12 位位宽下把电路做小,项目把步长固定为 1,把误差取符号,输入信号统一搬移到非负区间,于是参数更新就变成了项目正文里那一行:
W(n+1) = W(n) + sgn(e(n))
sgn(e(n)) 只取 1、0、-1:e(n) 大于 0 加 1,e(n) 小于 0 减 1,等于 0 保持不变。一个寄存器加 1 或减 1 就能完成权值更新,不需要额外的定点乘法器。这个做法本质上是符号误差符号输入 LMS 在输入恒正条件下的退化形式,不是完整版 LMS 的误差平方最小化,但对有限位宽下的正弦分离足够用。
2.2 为什么输入信号要映射到 0~512
输入信号是 0.25MHz 和 7.5MHz 两个余弦叠加出来的混合信号。如果直接量化成双极性 signed 信号,sign(x) 会不断翻转,简化公式的推导就不成立了。项目里采用的流程是:
- 把混合余弦信号做最大最小标准化,归一化到 0~1;
- 统一乘 512,映射到 0~512;
- 量化成 12 位,存成 COE 文件;
- 通过例化 ROM,每个时钟周期读出一个采样点给滤波器。
这样得到的数据全部为正,对符号误差更新来说,X(n) 的符号项可以看成恒为 1,于是 W(n+1)=W(n)+sgn(e(n)) 才立得住。选择乘 512 而不是直接用满 4095,是为了留出保护位。12 位用满的话,后续乘法器和误差累加器很容易溢出;0~512 只占低 9 位有效位,高 3 位作为保护位,乘法结果再宽也不会立刻污染符号位。参考信号单独存为一个 ROM,内容是 0.25MHz 的余弦信号,作为 LMS 的期望信号 d(n)。
2.3 位宽:每个信号占多少 bit 才够用
| 信号 | 位宽 | 范围 | 说明 |
|---|---|---|---|
| ROM 输入信号 | 12 bit | 0~4095 | 实际量化范围 0~512 |
| 参考信号 | 12 bit | 0~512 | 0.25MHz 余弦单独存 ROM |
| 乘法器输出 | 24 bit | signed | Booth 补码乘法结果,组合直接输出 |
| 误差 e | 13 bit | signed | d-y 需要先扩展到 24 位再截断 |
乘法器输出放到 24 bit,是为了避免乘法截断时把高频分量变成直流偏置。误差计算时d_in也要扩展成和y_mul相同的位宽,否则 VHDL 的-运算会按较窄位宽截断,仿真波形上会出现周期性毛刺。参考信号使用和输入同源的 0.25MHz 余弦,LMS 会不断把滤波器输出往 0.25MHz 方向拉,7.5MHz 分量会进入误差项并逐渐被权值更新压缩。
2.4 为什么 Booth 乘法器选组合实现
项目里的乘法器是 12 位 Booth 补码乘法器,“没有延时”指的是组合逻辑实现,不是流水线实现。组合 Booth 的好处是仿真波形里一拍就能看到乘法结果,适合课程设计逐周期核对状态机;缺点是综合后组合路径会变长,时钟频率不能提太高。项目跑的是 0.25MHz 和 7.5MHz 信号,即使采样时钟取 50MHz,组合路径压力也完全可以接受。VHDL 里的加法器直接写+号,比自己搭进位保存加法器少很多代码,也更适合在报告中展示逻辑结构;只有需要把采样率提高几倍时,才会考虑流水式 Booth 或树形加法器。
3. 按模块拆 VHDL:ROM 信号发生、Booth 乘法器与权值更新
3.1 模块划分与顶层端口
从顶层往下看,这个工程可以切成四个部分:
| 模块 | 作用 | 关键端口 |
|---|---|---|
| rom_signal | 读 COE 产生混合输入信号 | clk, addr, dout |
| rom_ref | 读参考信号 COE | clk, addr, dout |
| booth12 | 12 位补码 Booth 乘法器 | a, b, p |
| lms_core | 权值寄存器、误差计算、输出 | clk, rst_n, x_in, d_in, y_out, e_out |
打开源码时会看到顶层把rom_signal和rom_ref的地址同时递增,保证x_in与d_in对齐。很多课程设计里容易忽略这个对齐,导致仿真一直不收敛,因为输入和期望信号错开了几个采样周期。ROM 本身的输出延迟是固定的,只要两个 ROM 读取逻辑一致,地址同步就能保证数据在同一个时钟拍进入 LMS 核心。
3.2 用脚本生成两个 COE 文件
COE 文件是 Xilinx ROM 的初始化格式。项目正文里说信号值是最大最小标准化后乘 512,这一步通常用 Python 处理,避免手工算几百个采样点。下面这段脚本会生成 512 深度、12 位十六进制格式的input_rom.coe,参考信号 ROM 生成时把include_f2设为 False 即可。
import math FS = 50_000_000 N = 512 f1, f2 = 0.25e6, 7.5e6 def make_values(include_f2): raw = [] for n in range(N): t = n / FS v = math.cos(2 * math.pi * f1 * t) if include_f2: v += math.cos(2 * math.pi * f2 * t) raw.append(v) vmin, vmax = min(raw), max(raw) return [int(round((v - vmin) / (vmax - vmin) * 512)) for v in raw] def write_coe(samples, path): with open(path, "w") as f: f.write("memory_initialization_radix=16;\n") f.write("memory_initialization_vector=\n") for i, s in enumerate(samples): f.write(f"{s:03x}") f.write(";\n" if i == len(samples) - 1 else ",\n") write_coe(make_values(True), "input_rom.coe") write_coe(make_values(False), "ref_rom.coe")memory_initialization_radix=16告诉综合工具初始化数据按十六进制读取。s:03x把 0~512 的整数值格式化成三位十六进制,例如 512 会变成200。FS必须和仿真时钟一致,如果 VHDL 里时钟周期是 20ns,那 50MHz 的FS就是对的;后续想换成 100MHz,只需要把所有采样周期对应的FS改成100_000_000,信号频率保持不变即可。
3.3 12 位无延时 Booth 乘法器
Booth 乘法器在项目里的定位是一个组合逻辑模块,输入a、b都是 12 位 signed,输出p是 24 位。工程中完整实现会包含 Booth 编码、部分积生成和部分积压缩三个阶段,这里只给出端口和累加骨架:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity booth12 is port ( a : in signed(11 downto 0); b : in signed(11 downto 0); p : out signed(23 downto 0) ); end booth12; architecture combo of booth12 is type pp_array is array (0 to 5) of signed(23 downto 0); signal pp : pp_array; begin -- Booth 编码产生的 6 个部分积 -- 实际源码里通过 lookup table 从 0、±a、±2a 中选择 p <= (((((pp(0) + pp(1)) + pp(2)) + pp(3)) + pp(4)) + pp(5)); end combo;这个代码最需要注意的地方是p没有时钟驱动,输入a或b变化后,p只经过纯组合逻辑就会更新。因此它在仿真时不需要等待时钟周期,适合放在 LMS 更新路径上。副作用是综合后所有部分积加法会连成一条组合路径,如果后端时序报错,正确做法是在p输出端加一级寄存器,而不是把 Booth 编码改成超级复杂的多周期逻辑。
3.4 LMS 核心:误差计算和权值更新
LMS 核心每个采样周期做一次乘法和减法,然后根据误差符号更新权值。工程里乘法器通过例化booth12接入,误差计算和权值更新可以用一段很紧凑的 VHDL 描述:
architecture rtl of lms_core is signal w_reg : signed(11 downto 0) := (others => '0'); signal y_mul : signed(23 downto 0); signal e_int : signed(23 downto 0); begin comp_booth : entity work.booth12 port map(a => w_reg, b => x_in, p => y_mul); process(clk, rst_n) begin if rst_n = '0' then w_reg <= (others => '0'); elsif rising_edge(clk) then if e_int > 0 then w_reg <= w_reg + 1; elsif e_int < 0 then w_reg <= w_reg - 1; end if; end if; end process; e_int <= resize(d_in, 24) - y_mul; e_out <= e_int(12 downto 0); end rtl;w_reg初始为 0,每个时钟上升沿只看e_int的符号。e_int必须保留 24 位,因为y_mul已经是 24 位,d_in只有 12 位,直接相减会导致位宽匹配错误。e_out截断到 13 位,既保留符号位,又方便仿真观察。需要调整收敛速度时,把w_reg + 1改成w_reg + STEP,STEP是 VHDL 里定义的常量,例如constant STEP : integer := 2;。步长越大收敛越快,但稳态时权值抖动也更明显。
这段展示的是单权值结构,更贴合摘要里给出的简化公式。如果是多抽头 FIR 型 LMS,把w_reg和输入延迟寄存器都换成数组,每个抽头用同一个e_int的符号做加减,再将x_delay与w_array做点积。由于输入信号被设计为恒正区间,符号符号 LMS 的sign(x)项对该工程的每个抽头都近似成立,这正是项目能省略乘法更新项的直接原因。
4. 用 compile.bat 跑 Vivado xsim 仿真:从综合到波形验证
4.1 三个批处理脚本的分工
工程目录里的compile.bat、elaborate.bat、simulate.bat和runme.bat是标准的 Vivado xsim 命令行流程。VHDL 源文件用xvhdl编译,顶层测试台用xelab生成快照,最后用xsim跑仿真。三个脚本的职责可以对应成下面这张表:
| 脚本 | 执行命令 | 作用 |
|---|---|---|
| compile.bat | xvhdl --relax -work work | 编译所有 VHDL 源码到 work 库 |
| elaborate.bat | xelab -timescale 1ns/1ps tb_lms -snapshot lms_tb_snap | 生成可执行仿真快照 |
| simulate.bat | xsim lms_tb_snap -runall | 运行仿真并产生波形 |
实际脚本内容可能还会加--incr增量编译和-relax宽松检查。第一次在新机器上运行时,如果提示找不到work库,多半是xsim.ini指向了旧路径,这时可以参考目录里的xsim.ini.bak重建一份新的xsim.ini,再重新执行三个脚本。
4.2 Testbench 里的复位和收敛检查
Testbench 的作用不是只把时钟拉起来,还要在仿真结束后给出一个可读的收敛判断。常见的做法是统计最后一段窗口内误差绝对值的平均值,或者用断言检查误差均值是否落在阈值内。下面的测试台片段演示了这种验证方式:
architecture sim of tb_lms is signal clk, rst_n : std_logic; signal x_in, d_in : signed(11 downto 0); signal e_out : signed(12 downto 0); begin clk <= not clk after 10 ns; process begin rst_n <= '0'; wait for 100 ns; rst_n <= '1'; wait; end process; process(clk) variable sum : integer := 0; variable cnt : integer := 0; begin if rising_edge(clk) then if rst_n = '1' then sum := sum + to_integer(e_out); cnt := cnt + 1; if cnt = 1000 then assert abs(sum / 1000) < 20 report "error mean out of threshold" severity warning; cnt := 0; sum := 0; end if; end if; end if; end process; uut : entity work.lms_core port map(clk => clk, rst_n => rst_n, x_in => x_in, d_in => d_in, e_out => e_out); end sim;clk <= not clk after 10 ns生成 50MHz 时钟,和 COE 生成脚本里的FS一致。assert abs(sum/1000) < 20是窗口平均误差检查,阈值 20 需要根据输入信号幅度和步长调整;如果持续触发 warning,说明权值还没有稳定,需要延长仿真时间。cnt = 1000这个窗口对应 20 微秒,对 0.25MHz 信号已经是 5 个完整周期,足够看到均值趋势。
4.3 仿真波形上应该看到什么
跑完simulate.bat后,打开波形窗口添加x_in、d_in、y_out、e_out和w_reg。如果设计正确,理想结果是:
- 前几十微秒
e_out幅度较大,随后逐渐减小; y_out的包络向d_in靠拢;w_reg从 0 开始变化,最后在某个值附近上下抖动;- 7.5MHz 高频分量在
y_out中的占比明显低于原始x_in。
如果看不到这四点,优先检查 ROM 地址是否同步对齐,其次是e_int位宽是否足够。地址不对齐时,y_out和d_in永远差一个固定相位,e_out不会收敛。位宽不足时,w_reg会很快撞到上下限,仿真里看到的是权值一直不跳变。
5. 把课程设计再往前一步:量化灵敏度、收敛检查与 XSim 波形技巧
5.1 用 e_out 的符号抖动判断步长是否过大
权值更新公式只用了误差的符号,所以稳态时e_out不会保持在一个恒定不动的小数,而是会在 0 附近来回翻转。这个翻转幅度就是判断步长是否合适的最直观依据。把e_out的符号位拉出来做计数,统计单位时间内正负符号翻转次数,如果翻转频率过高,说明步长太大;如果翻转频率很低但e_out均值一直偏大,说明权值没有收敛到位。可以把e_out(12)接到一个计数器,在 testbench 里打印“正负翻转次数”,用这个指标替代肉眼看波形。
5.2 四个可以快速尝试的参数调整
| 参数 | 修改方式 | 效果 | 风险 |
|---|---|---|---|
| STEP | w_reg <= w_reg + STEP | 收敛更快 | 稳态抖动变大 |
| ROM 深度 | 改 Python 脚本里的N | 信号更连续 | ROM 资源占用增加 |
| 时钟频率 | 同步改FS和 TB 时钟 | 贴近实际采样 | 组合路径可能不满足时序 |
| 参考信号频率 | 把f1换成目标频率 | 跟踪不同频点 | 必须同步更新文档 |
改任意一个参数后,不要只跑一次仿真,至少对比三组 STEP 值下的e_out统计值。课程设计报告里如果能给出 STEP=1、2、4 三张波形截图,通常比写一大堆理论推导更能说明你真正理解了自适应滤波器的收敛和稳态矛盾。
5.3 一个实用的 XSim 调试技巧
当 LMS 在波形上看不出收敛时,很容易怀疑 Vivado 工程或综合结果,但这个项目绝大多数情况是 ROM 数据或位宽出了问题。一个快速定位技巧是用force命令在仿真中途替换期望信号,例如先强制d_in恒为 0,观察w_reg是否向 0 收敛;再强制d_in恒为最大值,观察w_reg是否持续增长。两次force的结果能直接把问题缩小到 ROM 读地址、乘法器或权值更新三个模块之一。
更直接的做法是在 testbench 里加一行打印,把权值实时输出:
report "w_reg = " & integer'image(to_integer(w_reg));配合xsim lms_tb_snap -gui打开图形界面,在仿真窗口执行add wave -radix signed /tb_lms/uut/w_reg,每次改动后重新跑脚本。等到w_reg不再单调变化,而是固定在一个窄区间内翻动时,这个自适应滤波器的核心环路就说明已经工作了。
本文还有配套的精品资源,点击获取