问答 店铺
热搜: ZYNQ FPGA discuz

QQ登录

只需一步,快速开始

微信登录

微信扫码,快速开始

微信扫一扫 分享朋友圈

已有 18 人浏览分享

开启左侧

第六章 第一个滤波器 - 移动平均滤波器

[复制链接]
18 0
       在第四章中,我们成功地生成了纯净的数字信号。然而,在真实世界中,信号总是伴随着各种噪声。滤波器的核心任务就是去伪存真,从含噪的信号中提取出我们感兴趣的部分。
       本章,我们将从最简单、最直观的滤波器——移动平均滤波器(Moving Average Filter)入手,学习如何设计一个基本的低通滤波器,并亲眼见证它平滑噪声、让信号“恢复平静”的神奇效果。
      项目目标: 设计一个N点移动平均滤波器,并用它来处理一个被高频噪声污染的正弦波信号。
1 项目目标
       欢迎来到你的第一个数字信号处理(DSP)项目!在本章中,我们将从零开始,在FPGA上设计、实现并验证一个基础但极为重要的滤波器——移动平均滤波器 (Moving Average Filter)。
这个项目的目标不仅仅是实现功能,更是要学习:
       硬件思维:如何将一个数学算法,从“软件思维”转化为适合FPGA流水线结构的“硬件思维”。
       高效实现:学习一种工业界常用的、高性能的滤波器实现结构,避免初学者常犯的时序错误。
       专业验证:建立一个从MATLAB生成测试数据,到Verilog Testbench进行自动化比对的完整、可靠的验证流程。

2 移动平均滤波器:从理论到硬件的思考

       移动平均滤波器的原理很简单:将最近N个采样点的值相加,然后除以N,得到当前点的输出。其数学公式为:
            y[n] = (x[n] + x[n-1] + ... + x[n-N+1]) / N
       其中,y[n]是当前时刻的输出,x[n]是当前时刻的输入,N是滤波器的“阶数”或“窗口大小”,我们称之为TAPS。
初学者的陷阱:直接翻译公式
       如果直接将公式翻译成Verilog,我们可能会写出这样的伪代码:
// 这是一个不好的设计,请勿使用!
always @(posedge clk) begin
    sum = x[n] + x[n-1] + ... + x[n-N+1]; // N输入加法器
    output = sum / N;
end

      这个设计的致命缺陷在于,N个输入的加法器是一个巨大的组合逻辑。综合工具会将其展开成一棵庞大的加法树。这个加法树的延迟非常长,它必须在一个时钟周期内计算完成,这会成为设计的关键路径,严重限制你的时钟频率。当N稍大时(例如16或32),几乎必然会导致时序违例。
专业的实现:利用递归思想
      一个高效的FPGA设计需要我们对算法进行转换。观察求和部分:
            Sum[n] = x[n] + x[n-1] + ... + x[n-N+1]
            Sum[n-1] = x[n-1] + x[n-2] + ... + x[n-N]
      将这两个式子相减,我们得到一个美妙的递归关系:
            Sum[n] - Sum[n-1] = x[n] - x[n-N]
            Sum[n] = Sum[n-1] + x[n] - x[n-N]
      这个公式是FPGA实现的黄金准则!它告诉我们,在每个时钟周期,我们不需要做N次加法,而只需要:
            1.拿到上一周期的和 (Sum[n-1])
            2.加上当前的新输入 (x[n])
            3.减去N个周期前最老的那个输入 (x[n-N])
这个计算路径非常短(一次加法和一次减法),与N的大小无关,可以运行在极高的时钟频率下,并且资源占用极少。这正是我们要在硬件中实现的结构。

3 步骤一:Verilog硬件设计
      根据上面的递归公式,我们来编写Verilog代码。我们需要三个主要部分:
            1.一个移位寄存器,用于存储历史数据,以便在需要时能提供最老的x[n-N]。
            2.一个累加器寄存器,用于实现 Sum[n] = Sum[n-1] + x[n] - x[n-N]。
            3.输出逻辑,将累加和除以N(通过右移实现)。
moving_average_filter_optimized.v
module moving_average_filter_optimized #(
    parameter DATA_WIDTH = 16,
    parameter TAPS = 8
)(
    input  wire                  I_clk,
    input  wire                  I_rst,
    input  wire signed [DATA_WIDTH-1:0] I_data_in,
    output wire signed [DATA_WIDTH-1:0] O_data_out
);

    localparam SHIFT_BITS = $clog2(TAPS);
    // 累加器的位宽需要足够大以防止溢出
    localparam ACCUM_WIDTH = DATA_WIDTH + SHIFT_BITS;

    // 内部信号
    reg signed [ACCUM_WIDTH-1:0] sum_reg;
    reg signed [DATA_WIDTH-1:0]  data_history [0:TAPS-1];
    wire signed [DATA_WIDTH-1:0] data_to_subtract;
    integer i;

    // 1. 移位寄存器链,用于得到 x[n-N]
    // 注意,我们需要一个完整的N级延迟链
    always @(posedge I_clk) begin
        if (I_rst) begin
            for (i = 0; i < TAPS; i = i + 1) begin
                data_history <= 0;
            end
        end else begin
            data_history[0] <= I_data_in;
            for (i = 1; i < TAPS; i = i + 1) begin
                data_history <= data_history[i-1];
            end
        end
    end

    // 要减去的最老的数据 x[n-N]
    assign data_to_subtract = data_history[TAPS-1];

    // 2. 累加器(核心部分)
    // Sum[n] = Sum[n-1] + x[n] - x[n-N]
    always @(posedge I_clk) begin
        if (I_rst) begin
            sum_reg <= 0;
        end else begin
            // 这是一个非常短的组合逻辑路径!
            sum_reg <= sum_reg + I_data_in - data_to_subtract;
        end
    end

    // 3. 除法 (通过算术右移实现)
    assign O_data_out = sum_reg >>> SHIFT_BITS;

endmodule


4 步骤二:MATLAB黄金参考模型
      为了验证我们的Verilog代码是否正确,我们需要一个“标准答案”,也就是黄金参考模型。MATLAB是完成这项任务的完美工具。
      下面的脚本将:
            1.生成一个混合了低频信号和高频噪声的测试输入。
            2.使用MATLAB内置的filter函数计算理想的滤波输出。
            3.将输入和输出数据转换为16位定点数,并以十六进制格式保存到.txt文件中。使用十六进制是FPGA仿真的最佳实践,因为它兼容性最好,且直接反映了硬件中的数据表示。
generate_stimulus_hex.m
% =========================================================================
% generate_stimulus_hex.m
%
% 功能:
% 1. 生成一个包含两个正弦波的复合信号作为滤波器输入。
% 2. 使用MATLAB的filter函数计算移动平均滤波的"黄金参考"输出。
% 3. 将输入信号和参考输出转换为16位定点数。
% 4. 将定点数结果以十六进制格式保存到 .txt 文件中,
% 用于Verilog Testbench的仿真。
% =========================================================================

clear; clc; close all;

% --- 参数定义 (必须与Verilog设计保持一致) ---
DATA_WIDTH = 16; % 数据位宽
TAPS = 8; % 滤波器阶数 (移动平均的点数)
NUM_POINTS = 1024; % 仿真数据点数

% --- 1. 生成输入信号 ---
% 创建一个复合信号:一个低频主信号 +一个高频噪声
fs = 1000; % 假设采样频率
t = (0:NUM_POINTS-1) / fs;
f1 = 10; % 低频信号 10Hz
f2 = 150; % 高频噪声 150Hz

% 生成浮点信号,并调整幅度使其在定点数范围内
signal_float = 0.6 * sin(2*pi*f1*t) + 0.2 * sin(2*pi*f2*t);

% --- 2. 转换为定点数 ---
% Q1.15 格式 (1个符号位, 15个小数位)
% 缩放因子为 2^(DATA_WIDTH-1)
scaling_factor = 2^(DATA_WIDTH-1);
input_fixed = round(signal_float * scaling_factor);

% 确保数据不会超出16位有符号数的范围
input_fixed(input_fixed > 32767) = 32767;
input_fixed(input_fixed < -32768) = -32768;

% --- 3. 计算黄金参考输出 ---
% 移动平均滤波器的系数是一个长度为TAPS,值为1/TAPS的数组
b = (1/TAPS) * ones(1, TAPS);
a = 1;

% 使用MATLAB的filter函数计算浮点输出
output_float_golden = filter(b, a, signal_float);

% 将黄金参考输出也转换为定点数
output_golden_fixed = round(output_float_golden * scaling_factor);
output_golden_fixed(output_golden_fixed > 32767) = 32767;
output_golden_fixed(output_golden_fixed < -32768) = -32768;


% --- 4. 将数据以十六进制格式写入文件 ---
fprintf('Writing stimulus and golden reference files...\n');

% -- 写入输入激励文件 --
fileID_in = fopen('input_stimulus.txt', 'w');
if fileID_in == -1
error('Cannot open input_stimulus.txt for writing.');
end
% 将有符号整数类型转换为无符号整数,以获得正确的二进制补码十六进制表示
input_unsigned = typecast(int16(input_fixed), 'uint16');
fprintf(fileID_in, '%x\n', input_unsigned);
fclose(fileID_in);

% -- 写入黄金参考文件 --
fileID_out = fopen('golden_reference.txt', 'w');
if fileID_out == -1
error('Cannot open golden_reference.txt for writing.');
end
output_golden_unsigned = typecast(int16(output_golden_fixed), 'uint16');
fprintf(fileID_out, '%x\n', output_golden_unsigned);
fclose(fileID_out);

fprintf('Files generated successfully!\n');

% --- 5. (可选) 绘图验证 ---
figure;
subplot(2,1,1);
plot(t, input_fixed);
title('Input Signal (Fixed-Point)');
xlabel('Time (s)');
ylabel('Amplitude');
grid on;

subplot(2,1,2);
plot(t, output_golden_fixed);
title('Golden Reference Output (Fixed-Point)');
xlabel('Time (s)');
ylabel('Amplitude');
grid on;

图示:
image.jpg

5 步骤三:编写专业的Testbench
      Testbench是连接我们的设计和MATLAB数据的桥梁。一个好的Testbench需要处理两个关键问题:延迟和精度。
      1.处理延迟:我们的硬件设计是流水线结构,存在延迟。当我们在时钟周期i给入stimulus时,data_out上出现的值是基于上一个输入stimulus[i-1]计算得到的。因此,在比对时,我们必须将当前的data_out与上一个黄金参考值golden_mem[i-1]进行比较。
      2.处理精度:MATLAB的浮点运算和Verilog的定点运算在舍入(Rounding)或截位(Truncation)上可能存在微小的差异,这可能导致结果有1个最低有效位(LSB)的误差。严格的 A === B 比较可能会因此失败。专业的做法是加入一个容差(Tolerance),允许±1的误差。
tb_moving_average_optimized.v

module tb_moving_average_optimized;

    // --- 参数定义 ---
    localparam DATA_WIDTH = 16;
    localparam TAPS       = 8;
    localparam SIM_POINTS = 1024;
    localparam CLK_PERIOD = 10;

    // --- 信号和变量 ---
    reg                          I_clk;
    reg                          I_rst;
    reg  signed [DATA_WIDTH-1:0] I_data_in;
    wire signed [DATA_WIDTH-1:0] O_data_out;
    integer i = 0;
    integer error_count = 0;
    integer comparison_start_index = TAPS;
    reg signed [DATA_WIDTH-1:0] stimulus_mem [0:SIM_POINTS-1];
    reg signed [DATA_WIDTH-1:0] golden_mem   [0:SIM_POINTS-1];

    // --- 实例化 DUT ---
    moving_average_filter_optimized #(
        .DATA_WIDTH(DATA_WIDTH),
        .TAPS(TAPS)
    ) uut (
        .I_clk(I_clk),
        .I_rst(I_rst),
        .I_data_in(I_data_in),
        .O_data_out(O_data_out)
    );

    // --- 时钟生成 ---
    always # (CLK_PERIOD / 2) I_clk = ~I_clk;

    // --- 主测试流程 ---
    initial begin
        $display("--- Starting Final Simulation (Latency Aligned & LSB Tolerant) ---");
        
        // 使用 $readmemh 读取十六进制文件
        $readmemh("input_stimulus.txt", stimulus_mem);
        $readmemh("golden_reference.txt", golden_mem);
        
        I_clk = 0;
        I_rst = 1;
        I_data_in = 0;
        # (CLK_PERIOD * 5);
        I_rst = 0;
        
        for (i = 0; i < SIM_POINTS; i = i + 1) begin
            @(posedge I_clk);
            I_data_in <= stimulus_mem;

            if (i >= comparison_start_index) begin
                #1; // 等待组合逻辑稳定
               
                // **核心验证逻辑**
                // 比较当前输出 O_data_out (由 i-1 的输入产生)
                // 和 golden_mem 中对应 i-1 的值,并允许 +/-1 的容差。
                if ( (O_data_out > golden_mem[i-1] + 1) || (O_data_out < golden_mem[i-1] - 1) ) begin
                    $display("ERROR @ input index %0d: DUT_output = %h, Mismatched Golden_ref(i-1) = %h",
                             i, O_data_out, golden_mem[i-1]);
                    error_count = error_count + 1;
                end
            end
        end

        # (CLK_PERIOD * (TAPS + 5)); // 等待所有数据流过

        if (error_count == 0) begin
            $display("--- [SUCCESS] Simulation passed! No errors found. ---");
        end else begin
            $display("--- [FAILURE] Simulation failed with %d errors. ---", error_count);
        end
        
        $stop;
    end

endmodule

6 步骤四:在Vivado中进行仿真
      1.创建工程:在Vivado中创建一个新工程,并将 moving_average_filter_optimized.v 添加为设计源(Design Source),将 tb_moving_average_optimized.v 添加为仿真源(Simulation Source)。
      2.添加数据文件:
            在 "Sources" 窗口,右键点击 "Simulation Sources"。
            选择 "Add Sources..." -> "Add or create simulation sources"。
            点击 "Add Files",找到你用MATLAB生成的 input_stimulus.txt 和 golden_reference.txt。
            关键:在文件选择对话框的右下角,将文件类型过滤器从 "Verilog" 改为 "All Files (.)",这样 .txt 文件才会显示出来。
            选中这两个文件,并确保 "Copy sources into project" 已勾选,然后完成添加。
      3.运行仿真:
            在左侧的 "Flow Navigator" 中,点击 "Run Simulation" -> "Run Behavioral Simulation"。
            仿真启动后,在Tcl Console窗口中,你应该会看到我们精心设计的打印信息,并最终显示 [SUCCESS] Simulation passed!。
            在波形窗口中,将 data_in 和 data_out 拖入。你可以将它们的显示格式改为 "Analog" 来直观地看到高频噪声是如何被滤除,波形是如何变得平滑的。
image.jpg

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

0

关注

10

粉丝

150

主题
精彩推荐
热门资讯
    网友晒图
      图文推荐
        
        • 微信公众平台

        • 扫描访问手机版