在 FPGA 上实现 50Hz 陷波器(Notch Filter)是一项经典且实用的数字信号处理任务,其核心目的是滤除无处不在的工频干扰。由于 50Hz 相对于 FPGA 常见的采样频率(如几 kHz 到几 MHz)而言属于低频信号,因此在实现时需要特别注意系数的精度与硬件资源的平衡。
陷波器的数学模型与参数计算
数字陷波器通常采用 IIR(无限脉冲响应)结构来实现,相较于 FIR 滤波器,IIR 能够以极少的阶数(计算量)实现非常窄的阻带带宽,这对于精准滤除单一频率干扰非常有利。
1. 传递函数
标准的二阶 IIR 陷波器在 Z 域的传递函数为:

其中,参数 r 控制陷波的带宽和深度。r 的值越接近 1,陷波器的带宽就越窄,频率选择性越好,但同时也会对计算的数值稳定性提出更高要求。
2. 系数计算示例
假设我们的目标采样率 fs = 1000Hz,需要滤除的频率 f0 = 50Hz:

3. 差分方程(用于 FPGA 编程)
将传递函数转换到时域,得到其差分方程,这将是我们在 FPGA 中进行逻辑实现的直接依据:

FPGA 逻辑实现
在 FPGA 的硬件逻辑中,我们无法直接处理浮点数,因此必须将计算好的浮点系数转换为定点数(Fixed-point)格式。
1. 定点化处理
我们需要将上述浮点系数放大 2^k 倍并取整。例如,当 k=16 时,放大倍数为 65536。
b1_fixed = round(-1.9021 × 65536)
- 计算完成后,最终结果需要再右移 16 位来还原真实值。
- 重要提示:对于 50Hz 这样的低频信号,如果系统采样率很高(例如 100kHz),计算出的系数会非常接近 2 或 -2。此时必须使用足够高的位宽(如 32 位或更高)来进行定点化,否则滤波器可能失效或产生振荡。
2. 定点系数(Q16.16 格式)
这里我们采用 Q16.16 格式(16位整数,16位小数)对系数进行定点化,得到用于硬件计算的整型系数:

3. Verilog 代码实现
以下模块采用直接 I 型(Direct Form I)结构,并加入了分时处理与饱和截断(Saturation)逻辑,有效防止运算溢出导致信号畸变。理解这类硬件描述语言是实现高效数字信号处理的基础,更多关于计算机体系结构和底层逻辑设计的知识可以在 云栈社区 的计算机基础板块找到深入讨论。
module notch_filter_50hz (
input wire clk, // 高速系统时钟 (如 50MHz)
input wire rst_n, // 异步复位
input wire vld_in, // 采样脉冲 (每当有新数据时拉高一个clk)
input wire signed [15:0] data_in, // 输入信号 (16位)
output reg signed [15:0] data_out, // 输出信号 (16位)
output reg vld_out // 输出有效标志
);
// --- 定点化系数 (Q16.16) ---
localparam signed [31:0] B0 = 32'sd65536;
localparam signed [31:0] B1 = -32'sd124657;
localparam signed [31:0] B2 = 32'sd65536;
localparam signed [31:0] A1 = -32'sd118423;
localparam signed [31:0] A2 = 32'sd59146;
// --- 状态寄存器 (延迟链) ---
reg signed [15:0] x_n1, x_n2; // 输入延迟
reg signed [15:0] y_n1, y_n2; // 输出延迟
// --- 乘累加中间变量 ---
// 16位数据 * 32位系数 = 48位结果
reg signed [47:0] sum;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
x_n1 <= 0; x_n2 <= 0;
y_n1 <= 0; y_n2 <= 0;
data_out <= 0;
vld_out <= 0;
end else if (vld_in) begin
// 核心差分方程计算:
// y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]
// 注意: 此处 A1, A2 已经在计算时带了负号,所以代码中用加法
sum <= (B0 * data_in) + (B1 * x_n1) + (B2 * x_n2)
- (A1 * y_n1) - (A2 * y_n2);
// 延迟链更新
x_n2 <= x_n1;
x_n1 <= data_in;
y_n2 <= y_n1;
// 结果截断与饱和处理
// 我们使用了 Q16.16,所以结果需要右移 16 位
// 如果结果超过 16 位有符号数的范围,则强行置为最大值/最小值
if (sum[47:16] > 32'sd32767)
y_n1 <= 16'sd32767;
else if (sum[47:16] < -32'sd32768)
y_n1 <= -16'sd32768;
else
y_n1 <= sum[31:16];
data_out <= y_n1;
vld_out <= 1'b1;
end else begin
vld_out <= 1'b0;
end
end
endmodule
实时采样与实现要点
- 时序同步:代码中的
vld_in 信号至关重要。滤波器不应在每个系统时钟周期都进行计算,而必须与 ADC 的采样率同步。例如,若 ADC 采样率为 1kHz,则 vld_in 应每 1ms 产生一个时钟周期的高电平脉冲,触发滤波器计算一次。
- 实时性:从
vld_in 有效到 vld_out 输出结果,仅有一个系统时钟周期的延迟(纳秒级)。这对于处理周期为 20ms 的 50Hz 信号而言,完全可以视为“零延迟”的实时处理。
|