阶段一:信号基础

🔬 第04课:离散傅里叶变换(DFT)

频域分析的核心——从时域采样到频域离散谱

1. DFT的定义与意义

离散傅里叶变换(DFT)将有限长离散信号变换到离散频域,是数字信号处理中最重要的变换。它使得我们可以在频域分析和处理信号。

正变换:X[k] = Σ(n=0 to N-1) x[n] · W_N^(nk),k = 0,1,...,N-1
逆变换:x[n] = (1/N) Σ(k=0 to N-1) X[k] · W_N^(-nk),n = 0,1,...,N-1
其中 W_N = e^(-j2π/N) 称为旋转因子

1.1 DFT与DTFT的关系

DFT是DTFT在频率上的等间隔采样:

X[k] = X(e^jω)|_{ω=2πk/N},即对DTFT在[0,2π)上均匀采N个点

这意味着DFT的频率分辨率为 Δf = fs/N Hz,增加N可提高频率分辨率。

1.2 DFT的基本性质

性质时域频域(DFT)
线性a·x₁[n]+b·x₂[n]a·X₁[k]+b·X₂[k]
循环移位x[(n-m)_N]W_N^(km)·X[k]
循环卷积x[n] ⊛ h[n]X[k]·H[k]
Parseval定理Σ|x[n]|²(1/N)Σ|X[k]|²
对称性实序列x[n]X[k] = X*[N-k](共轭对称)

2. 窗函数与频谱泄漏

对无限长信号截断等价于加窗,窗函数的旁瓣会导致频谱泄漏:

常见窗函数特性

窗类型主瓣宽度旁瓣衰减(dB)应用场景
矩形窗4π/N-13频率分辨率优先
Hanning窗8π/N-31通用频谱分析
Hamming窗8π/N-41语音处理
Blackman窗12π/N-57高动态范围
Kaiser窗可调可调灵活设计

3. Verilog实现:DFT计算引擎

实现一个N点DFT的直接计算引擎,使用CORDIC旋转因子生成器:

//=============================================
// dsp_dft_engine.v
// N点DFT直接计算引擎
// X[k] = Σ x[n] * W_N^(nk)
// W_N = e^(-j2π/N) = cos(2πnk/N) - j*sin(2πnk/N)
//=============================================
module dsp_dft_engine #(
    parameter N = 16,               // DFT点数
    parameter DATA_WIDTH = 16,      // 输入数据位宽
    parameter OUT_WIDTH = 32        // 输出位宽
)(
    input  wire                          clk,
    input  wire                          rst_n,
    input  wire                          start,
    input  wire signed [DATA_WIDTH-1:0]  x_in [0:N-1],  // 输入采样
    output reg  signed [OUT_WIDTH-1:0]   X_re [0:N-1],  // DFT实部输出
    output reg  signed [OUT_WIDTH-1:0]   X_im [0:N-1],  // DFT虚部输出
    output reg  [OUT_WIDTH-1:0]          X_mag [0:N-1], // 幅度谱
    output reg                           done
);

    // 旋转因子查找表
    reg signed [DATA_WIDTH-1:0] wn_cos_table [0:N*N-1];
    reg signed [DATA_WIDTH-1:0] wn_sin_table [0:N*N-1];

    initial begin
        integer idx, n_idx, k_idx;
        for (k_idx = 0; k_idx < N; k_idx = k_idx + 1) begin
            for (n_idx = 0; n_idx < N; n_idx = n_idx + 1) begin
                idx = k_idx * N + n_idx;
                wn_cos_table[idx] = $rtoi(
                    (2.0**(DATA_WIDTH-1)-1) * $cos(2.0*3.14159265*n_idx*k_idx/N)
                );
                wn_sin_table[idx] = $rtoi(
                    (2.0**(DATA_WIDTH-1)-1) * $sin(2.0*3.14159265*n_idx*k_idx/N)
                );
            end
        end
    end

    // 计算状态机
    localparam IDLE    = 3'd0;
    localparam COMPUTE = 3'd1;
    localparam MAGNITUDE = 3'd2;
    localparam FINISH  = 3'd3;

    reg [2:0] state;
    reg [$clog2(N)-1:0] k_idx, n_idx;
    reg signed [OUT_WIDTH-1:0] re_acc, im_acc;

    integer i;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE;
            k_idx <= 0; n_idx <= 0;
            done <= 1'b0;
            for (i = 0; i < N; i = i + 1) begin
                X_re[i] <= 0; X_im[i] <= 0; X_mag[i] <= 0;
            end
        end else begin
            case (state)
                IDLE: begin
                    done <= 1'b0;
                    if (start) begin
                        k_idx <= 0;
                        n_idx <= 0;
                        re_acc <= 0;
                        im_acc <= 0;
                        state <= COMPUTE;
                    end
                end

                COMPUTE: begin
                    // X[k] += x[n] * (cos - j*sin)
                    re_acc <= re_acc + (x_in[n_idx] * wn_cos_table[k_idx*N+n_idx]);
                    im_acc <= im_acc - (x_in[n_idx] * wn_sin_table[k_idx*N+n_idx]);

                    if (n_idx == N - 1) begin
                        X_re[k_idx] <= re_acc;
                        X_im[k_idx] <= im_acc;
                        re_acc <= 0;
                        im_acc <= 0;
                        n_idx <= 0;

                        if (k_idx == N - 1) begin
                            k_idx <= 0;
                            state <= MAGNITUDE;
                        end else begin
                            k_idx <= k_idx + 1'b1;
                        end
                    end else begin
                        n_idx <= n_idx + 1'b1;
                    end
                end

                MAGNITUDE: begin
                    // 幅度谱:|X[k]| ≈ |X_re| + |X_im|(近似计算)
                    X_mag[k_idx] <= (X_re[k_idx][OUT_WIDTH-1] ? -X_re[k_idx] : X_re[k_idx])
                                   + (X_im[k_idx][OUT_WIDTH-1] ? -X_im[k_idx] : X_im[k_idx]);

                    if (k_idx == N - 1) begin
                        state <= FINISH;
                    end else begin
                        k_idx <= k_idx + 1'b1;
                    end
                end

                FINISH: begin
                    done <= 1'b1;
                    state <= IDLE;
                end
            endcase
        end
    end

endmodule

4. Verilog实现:窗函数生成器

//=============================================
// dsp_window_generator.v
// 可配置窗函数生成器
// 支持:矩形/Hanning/Hamming/Blackman窗
//=============================================
module dsp_window_generator #(
    parameter N = 256,
    parameter DATA_WIDTH = 16
)(
    input  wire                    clk,
    input  wire                    rst_n,
    input  wire [1:0]              window_type,  // 00=Rect 01=Hanning 10=Hamming 11=Blackman
    input  wire                    en,
    input  wire [$clog2(N)-1:0]    index,        // 当前采样索引 n
    output reg  [DATA_WIDTH-1:0]   window_out    // 窗函数值
);

    // 窗函数系数查找表(归一化到DATA_WIDTH位宽)
    reg [DATA_WIDTH-1:0] hanning_lut [0:N-1];
    reg [DATA_WIDTH-1:0] hamming_lut [0:N-1];
    reg [DATA_WIDTH-1:0] blackman_lut [0:N-1];

    initial begin
        integer i;
        for (i = 0; i < N; i = i + 1) begin
            // Hanning: 0.5 * (1 - cos(2πn/(N-1)))
            hanning_lut[i] = $rtoi(
                (2.0**(DATA_WIDTH-1)-1) * 0.5 * (1.0 - $cos(2.0*3.14159265*i/(N-1)))
            );
            // Hamming: 0.54 - 0.46 * cos(2πn/(N-1))
            hamming_lut[i] = $rtoi(
                (2.0**(DATA_WIDTH-1)-1) * (0.54 - 0.46*$cos(2.0*3.14159265*i/(N-1)))
            );
            // Blackman: 0.42 - 0.5*cos(...) + 0.08*cos(...)
            blackman_lut[i] = $rtoi(
                (2.0**(DATA_WIDTH-1)-1) * (0.42 - 0.5*$cos(2.0*3.14159265*i/(N-1))
                + 0.08*$cos(4.0*3.14159265*i/(N-1)))
            );
        end
    end

    always @(*) begin
        case (window_type)
            2'b00: window_out = {1'b0, {(DATA_WIDTH-1){1'b1}}};  // 矩形窗=1
            2'b01: window_out = hanning_lut[index];
            2'b10: window_out = hamming_lut[index];
            2'b11: window_out = blackman_lut[index];
        endcase
    end

endmodule

5. Verilog实现:频谱峰值检测

//=============================================
// dsp_peak_detector.v
// 频谱峰值检测器
// 检测DFT输出中的频谱峰值位置和幅度
//=============================================
module dsp_peak_detector #(
    parameter N = 16,
    parameter DATA_WIDTH = 32
)(
    input  wire                      clk,
    input  wire                      rst_n,
    input  wire                      start,
    input  wire [DATA_WIDTH-1:0]     spectrum [0:N-1],  // 幅度谱输入
    output reg  [$clog2(N)-1:0]      peak_index,         // 峰值频率索引
    output reg  [DATA_WIDTH-1:0]     peak_value,         // 峰值幅度
    output reg  [$clog2(N)-1:0]      second_peak_index,  // 第二峰值索引
    output reg  [DATA_WIDTH-1:0]     second_peak_value,  // 第二峰值幅度
    output reg                       done
);

    localparam IDLE = 0, SCAN = 1, DONE = 2;
    reg [1:0] state;
    reg [$clog2(N)-1:0] scan_idx;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE;
            peak_index <= 0; peak_value <= 0;
            second_peak_index <= 0; second_peak_value <= 0;
            done <= 1'b0;
        end else begin
            case (state)
                IDLE: begin
                    done <= 1'b0;
                    if (start) begin
                        peak_index <= 0; peak_value <= 0;
                        second_peak_index <= 0; second_peak_value <= 0;
                        scan_idx <= 0;
                        state <= SCAN;
                    end
                end
                SCAN: begin
                    if (spectrum[scan_idx] > peak_value) begin
                        second_peak_value <= peak_value;
                        second_peak_index <= peak_index;
                        peak_value <= spectrum[scan_idx];
                        peak_index <= scan_idx;
                    end else if (spectrum[scan_idx] > second_peak_value) begin
                        second_peak_value <= spectrum[scan_idx];
                        second_peak_index <= scan_idx;
                    end

                    if (scan_idx == N - 1)
                        state <= DONE;
                    else
                        scan_idx <= scan_idx + 1'b1;
                end
                DONE: begin
                    done <= 1'b1;
                    state <= IDLE;
                end
            endcase
        end
    end

endmodule

6. DFT频谱分辨率分析

📌 频谱分辨率与参数选择

频率分辨率 Δf = fs/N,意味着:

物理频率分辨率 = fs / N_actual(N_actual为有效数据点数)
频谱采样间隔 = fs / N_fft(N_fft ≥ N_actual,含零填充)

7. 练习

📝 练习1:手工计算DFT

计算4点DFT:x[n] = {1, 2, 3, 4},求X[k](k=0,1,2,3)

📝 练习2:频谱泄漏观察

对x[n] = cos(2π·3.5/16·n),n=0..15,分别用矩形窗和Hanning窗做16点DFT,比较频谱泄漏情况。

📝 练习3:流水线DFT

修改 dsp_dft_engine.v,实现全流水线结构,使得每个时钟周期都能输出一个频点的计算结果。

📝 练习4:频率估计

使用峰值检测器的结果,实现频率插值算法(如抛物线插值),提高频率估计精度到子 bin 级别。

🏆 成就解锁:频域分析师

✅ 掌握了DFT的定义与计算方法

✅ 理解了DFT性质与循环卷积

✅ 实现了DFT计算引擎(Verilog)

✅ 实现了窗函数生成器与峰值检测

✅ 理解了频谱泄漏与分辨率的关系

11. 补充:离散傅里叶变换(DFT)的进阶主题

离散傅里叶变换(DFT)在进阶应用中还有以下重要主题值得深入研究:

📚 进阶研究方向

12. 与前后课程的关联

本课内容在整个DSP课程体系中处于承上启下的位置:

🔗 课程关联图

13. 设计经验总结

在实际工程中,离散傅里叶变换(DFT)的设计经验总结如下:(1)始终从系统级需求出发,不要过早陷入实现细节;(2)先用浮点仿真验证算法正确性,再转为定点;(3)系数量化后必须重新验证频率响应;(4)硬件实现前用C/MATLAB模型作为参考;(5)综合后检查时序和资源是否符合预期;(6)板级验证时用已知的测试信号对比参考输出。每一步验证都是必要的,跳过任何一步都可能导致最终的调试时间成倍增加。

📌 关键设计检查清单

13. 设计经验总结

在实际工程中,离散傅里叶变换(DFT)的设计经验总结如下:(1)始终从系统级需求出发,不要过早陷入实现细节;(2)先用浮点仿真验证算法正确性,再转为定点;(3)系数量化后必须重新验证频率响应;(4)硬件实现前用C/MATLAB模型作为参考;(5)综合后检查时序和资源是否符合预期;(6)板级验证时用已知的测试信号对比参考输出。每一步验证都是必要的,跳过任何一步都可能导致最终的调试时间成倍增加。

📌 关键设计检查清单

13. 设计经验总结

在实际工程中,离散傅里叶变换(DFT)的设计经验总结如下:(1)始终从系统级需求出发,不要过早陷入实现细节;(2)先用浮点仿真验证算法正确性,再转为定点;(3)系数量化后必须重新验证频率响应;(4)硬件实现前用C/MATLAB模型作为参考;(5)综合后检查时序和资源是否符合预期;(6)板级验证时用已知的测试信号对比参考输出。每一步验证都是必要的,跳过任何一步都可能导致最终的调试时间成倍增加。

📌 关键设计检查清单