简介面向数字信号处理课程设计、期末大作业与项目开发的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(n1) W(n) μ e(n) X(n)这个公式要求同时计算 μ、e(n)、X(n) 的乘积。在 FPGA 里做如果 μ 是小数还要把它定点化如果运算路径稍长更新周期还得和乘法器对齐。为了在有限的 12 位位宽下把电路做小项目把步长固定为 1把误差取符号输入信号统一搬移到非负区间于是参数更新就变成了项目正文里那一行W(n1) W(n) sgn(e(n))sgn(e(n)) 只取 1、0、-1e(n) 大于 0 加 1e(n) 小于 0 减 1等于 0 保持不变。一个寄存器加 1 或减 1 就能完成权值更新不需要额外的定点乘法器。这个做法本质上是符号误差符号输入 LMS 在输入恒正条件下的退化形式不是完整版 LMS 的误差平方最小化但对有限位宽下的正弦分离足够用。2.2 为什么输入信号要映射到 0512输入信号是 0.25MHz 和 7.5MHz 两个余弦叠加出来的混合信号。如果直接量化成双极性 signed 信号sign(x) 会不断翻转简化公式的推导就不成立了。项目里采用的流程是把混合余弦信号做最大最小标准化归一化到 01统一乘 512映射到 0512量化成 12 位存成 COE 文件通过例化 ROM每个时钟周期读出一个采样点给滤波器。这样得到的数据全部为正对符号误差更新来说X(n) 的符号项可以看成恒为 1于是 W(n1)W(n)sgn(e(n)) 才立得住。选择乘 512 而不是直接用满 4095是为了留出保护位。12 位用满的话后续乘法器和误差累加器很容易溢出0512 只占低 9 位有效位高 3 位作为保护位乘法结果再宽也不会立刻污染符号位。参考信号单独存为一个 ROM内容是 0.25MHz 的余弦信号作为 LMS 的期望信号 d(n)。2.3 位宽每个信号占多少 bit 才够用信号位宽范围说明ROM 输入信号12 bit04095实际量化范围 0512参考信号12 bit05120.25MHz 余弦单独存 ROM乘法器输出24 bitsignedBooth 补码乘法结果组合直接输出误差 e13 bitsignedd-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. 按模块拆 VHDLROM 信号发生、Booth 乘法器与权值更新3.1 模块划分与顶层端口从顶层往下看这个工程可以切成四个部分模块作用关键端口rom_signal读 COE 产生混合输入信号clk, addr, doutrom_ref读参考信号 COEclk, addr, doutbooth1212 位补码 Booth 乘法器a, b, plms_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_radix16;\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_radix16告诉综合工具初始化数据按十六进制读取。s:03x把 0512 的整数值格式化成三位十六进制例如 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 STEPSTEP是 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.batxvhdl --relax -work work编译所有 VHDL 源码到 work 库elaborate.batxelab -timescale 1ns/1ps tb_lms -snapshot lms_tb_snap生成可执行仿真快照simulate.batxsim 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 四个可以快速尝试的参数调整参数修改方式效果风险STEPw_reg w_reg STEP收敛更快稳态抖动变大ROM 深度改 Python 脚本里的N信号更连续ROM 资源占用增加时钟频率同步改FS和 TB 时钟贴近实际采样组合路径可能不满足时序参考信号频率把f1换成目标频率跟踪不同频点必须同步更新文档改任意一个参数后不要只跑一次仿真至少对比三组 STEP 值下的e_out统计值。课程设计报告里如果能给出 STEP1、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 integerimage(to_integer(w_reg));配合xsim lms_tb_snap -gui打开图形界面在仿真窗口执行add wave -radix signed /tb_lms/uut/w_reg每次改动后重新跑脚本。等到w_reg不再单调变化而是固定在一个窄区间内翻动时这个自适应滤波器的核心环路就说明已经工作了。本文还有配套的精品资源点击获取