2026寒假练 - 基于小脚丫FPGA实现实时音量表与频谱显示
该项目使用了小脚丫FPGA,实现了实时音量表与频谱显示的设计,它的主要功能为:ADC采集麦克风数据,使用短时RMS和频谱算法,得到音量表和频谱,并通过LED和屏幕显示。
标签
嵌入式系统
FPGA
2026年寒假练
小小洋洋
更新2026-03-24
同济大学
56

一、项目任务介绍

本项目要求使用小脚丫FPGA电赛训练平台,完成基于麦克风的实时音量表与频谱显示。具体要求包括:

  1. 将麦克风放大模块的 Audio_Out 接入 ADC 模块的模拟输入端,采样率设置为 8–20 kHz(FPGA 产生采样时钟并进行抽取)。
  2. 实现直流分量去除与简单预处理(如高通滤波或自动增益控制),并计算短时 RMS 或包络作为音量值。
  3. 通过核心板的 LED 实现音量条形显示,并用三色 LED 或 RGB LED 显示音量等级。
  4. 实现至少 4 个频段的能量分析(可使用小型 FFT 或多点 Goertzel),并通过 OLED 屏幕显示频谱柱状图。
  5. 支持按键切换“音量显示/频谱显示”模式,编码器用于调节增益和衰减时间常数。
  6. 完成任务时,需使用基于 FPGA 的电赛训练套件,具体版本核心板可根据需要选择。


二、项目介绍

本项目使用小脚丫 FPGA 核心板(Altera MAX10 系列 10M02SCM 芯片)完成任务,主要内容包括:

  1. 音量采集: 通过电赛板的 ADC 模块(3PA1030),以 20kHz 的采样率完成音量的实时采集。
  2. 数据预处理: FPGA 接收音频数据后,进行滤波与增益控制。
  3. 音量计算与显示: 使用短时 RMS 算法计算音量,并通过 LED 条、三色 LED 和 OLED 屏幕进行显示。
  4. 频谱计算与显示: 采用简单的频谱分析方法(方波相关检测),计算 8 个频段的能量并通过 OLED 屏幕显示。
  5. 用户交互: 通过编码器控制用户输入,调节增益与衰减,以及切换 OLED 屏幕的显示内容。

image.png image.png

image.png image.png


三、硬件介绍

1. 核心模块:

  • 小脚丫 FPGA 核心板 ,基于 Intel MAX10 系列,包含 Lattice MachX02 系列(LATTICE LCMXO2-4000HC-4MG132)。
  • 板载资源:4 路按键、8 路用户 LED、4 路拨码开关、2 个 RGB 三色 LED、1 路 12M 有源晶振等。
  • 核心器件:支持 DDR/DDR2/LPDDR 存储器,96KBIT 用户闪存,92KBIT RAM,具有 SPI、I2C 接口。

image.png

2. ADC 模块:

  • 采用 3PA1030 高速 ADC,10bit/50Msps,用于高速数据采集。

image.png

3. 麦克风模块:

  • 模拟输出的麦克风,通过LM358运算放大器进行放大后输出至ADC。

image.png

4. 组装合并:

  • 将麦克风模块与ADC输入引脚相连,并接上3.3V和GND。
  • 特别注意,需要一个端接帽将麦克风的AIN与MIC相连,让麦克风数据通过运算放大器后输出。

image.png


四、方案框图与设计思路

1. 方案框图:

主要包括输入端、FPGA处理核心、输出设备三部分:

image.png


2. 设计思路

🎤 麦克风与ADC


🎛️ 交互控制 - EC11


⚙️ 直流去除与增益


🔧 滤波处理(可选)


📊 RMS计算 - 音量分析


采集环境中的音频信号

AD1030 ADC:
• 10位分辨率
• 20kHz采样率
• 输出范围: 0~1023(无符号)
• 采样周期: 50μs

功能:
• 旋转调节增益(±50步长)
• 增益范围: 250~1000(Q8.8格式)
• 对应倍数: 0.98x ~ 3.91x
• 按键切换显示模式
• 模式: 频谱/幅度折线/幅度填充

处理步骤:
1. ADC输入(0~1023) 减去512
2. 转换为有符号(-512~511)
3. 应用EC11增益系数
4. 自适应DC追踪

输出:有符号数据可供后续处理

高通滤波(HPF):
• 去除低频干扰(风声、50Hz等)
• 截止频率 ~400Hz
• 可选单级或级联配置

低通滤波(LPF):
• 平滑高频噪声
• 可选IIR(快速)或FIR(线性相位)

功能:计算信号的均方根值

• 输入: 处理后音频样本
• 窗口: 4096样本(204.8ms)
• 输出: 0~255(8-bit无符号)
• 含义: 音量大小指示
• 用途: LED显示、热图映射

📈 频谱分析

🖥️ OLED显示驱动


🔌 LED与RGB输出


💻 UART串口调试输出


⚡ 关键性能指标


特性:
• 轻量级算法(相比FFT节省>80%资源)
• 8个独立频段分析
• 输出精度: 6-bit/频段(0~63)
• 更新周期: 256样本
• 用途: 频谱柱状图显示

双驱动模块:
• 频谱驱动: 8通道柱状频谱图
• 幅度驱动: 实时RMS滚动折线

显示模式(EC11切换):
• 频谱柱状图(8列)
• 幅度折线图(实时曲线)
• 幅度填充图(柱状幅度)

LED条驱动(8个LED):
• PWM驱动,柱状音量显示
• 音量越大点亮越多LED
• 刷新频率: 1kHz

RGB LED热图:
• 冷色(蓝) → 热色(红)渐变
• RMS值映射颜色
• 三路独立PWM输出

功能:实时数据调试

调试模式选择:
• 模式0: 处理后数据(512±值)
• 模式1: 原始ADC(0~1023)
• 模式2: DC估计值(观察漂移)

PC端可视化分析

采样性能:
• 采样率: 20kHz
• ADC精度: 10-bit
• 处理延迟: <10ms

显示性能:
• OLED刷新: 30Hz
• LED更新: 1kHz
• 实时性: 完全实时处理


五、开发工具与编程语言

1. 开发环境:

  • 使用 Intel Quartus Prime 25.1std 开发软件。
  • 编程语言:Verilog。

image.png


2. 关键代码:

  • ADC采集: 使用 FPGA 通过 ADC 模块进行音频数据采集,根据参数ADC_FRE 设定分频系统,按照设定的ADC_FRE 采样
module ad1030_top #(
    parameter CLK_FRE = 12,      // MHz
    parameter ADC_FRE = 20_000,    // Hz
    parameter GATED_MODE = 1     // 1=随 start 开启/关闭时钟 ; 0=时钟常开
)(
    input           clk,
    input           rst_n,

    input           ad1030_start,   // 高电平启动 ADC 采样
    output  [9:0]   ad1030_data,    // ADC 10位输出数据,用于后续处理

    input  [9:0]    ad1030_db,  // ADC 10位输入原始数据
    output          ad1030_clk  // ADC 采样时钟
);

// ================= 分频参数保护 =================
localparam integer DIV_MAX_RAW =
    (CLK_FRE*1000_000) / (ADC_FRE*2);

localparam integer DIV_MAX =
    (DIV_MAX_RAW > 1) ? (DIV_MAX_RAW-1) : 1;

// ================= 采样时钟(改进的分频逻辑) =====================
// 使用积分分频法,降低时钟抖动
reg [15:0] clk_cnt = 0;
reg [15:0] clk_acc = 0;      // 累加器,用于更精确的分频
reg        ad1030_clk_r = 0;
assign     ad1030_clk   = ad1030_clk_r;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        clk_cnt      <= 0;
        clk_acc      <= 0;
        ad1030_clk_r <= 0;
    end
    else if (!GATED_MODE || ad1030_start) begin
        clk_acc <= clk_acc + 1;
       
        if (clk_acc >= DIV_MAX) begin
            clk_acc      <= 0;
            ad1030_clk_r <= ~ad1030_clk_r;
        end
    end
    else begin
        clk_acc      <= 0;
        ad1030_clk_r <= 0;
    end
end

// ================= 数据采样(ADC 时钟域) =======
// 下降沿采样:ADC在CLK下降沿输出新数据,此时数据最稳定
reg [9:0] ad1030_db_r1, ad1030_db_r2;

always @(negedge ad1030_clk_r or negedge rst_n) begin
    if (!rst_n) begin
        ad1030_db_r1 <= 10'd0;
        ad1030_db_r2 <= 10'd0;
    end else begin
        ad1030_db_r1 <= ad1030_db;
        ad1030_db_r2 <= ad1030_db_r1;
    end
end

assign ad1030_data = ad1030_db_r2;

endmodule
  • DC滤除与增益控制:在上电后 1s 内累加采样点,计算直流分量移除之后输出有符号的音频信号;增益系数gain_value为Q8.8格式,运行时可调。
module dc_add_and_remove #(
    parameter integer DATA_WIDTH = 10,          // ADC 位宽(如 10 位、12 位等)
    parameter integer SAMPLE_RATE = 20000,      // 采样率(Hz),默认 20kHz
    parameter integer LOCK_TIME = 1,            // 锁定时间(秒),默认 1s
    // 固定增益参数
    parameter         GAIN_EN     = 1'b1,       // 固定增益使能
    parameter integer FRAC_BITS   = 8           // 定点数小数位宽
)(
    input                              clk,
    input                              rst_n,
    input                              data_valid,
    input  [DATA_WIDTH-1:0]            data_in,           // 无符号输入(来自 ADC)
    input  [15:0]                      gain_value,        // 增益系数(Q8.8格式,运行时可调)
    output reg                         data_out_valid,
    output reg signed [DATA_WIDTH-1:0] data_out,          // 有符号输出
    output      signed [DATA_WIDTH-1:0] dc_level,
    output reg                         dc_locked           // DC 值是否已锁定
);
    // ================================================================
    // 参数定义
    // ================================================================
    localparam integer ACC_WIDTH = DATA_WIDTH + 20;     // 累加器位宽(防止溢出)
    localparam integer LOCK_SAMPLES = SAMPLE_RATE * LOCK_TIME;  // 锁定时间对应的采样点数
   
    // ================================================================
    // 累加器及计数器
    // ================================================================
    reg [ACC_WIDTH-1:0] dc_accumulator;          // 样本累加器
    reg [31:0] sample_counter;                   // 计数器:0 到 LOCK_SAMPLES
    reg signed [DATA_WIDTH-1:0] dc_value_locked; // 锁定的 DC 值
   
    // ================================================================
    // 直流分量估计(除以采样数得到平均值)
    // ================================================================
    wire signed [DATA_WIDTH-1:0] dc_est = dc_accumulator / LOCK_SAMPLES;
   
    // 使用锁定的 DC 值
    assign dc_level = dc_value_locked;

    // ================================================================
    // 数据路径:无符号转有符号,然后去除直流
    // ================================================================
    wire signed [DATA_WIDTH:0] data_in_signed = $signed({1'b0, data_in});  // 无符号扩展到有符号
    wire signed [DATA_WIDTH:0] data_centered = data_in_signed - $signed({1'b0, dc_value_locked});  // 减去锁定的 DC
   
    // 饱和裁剪到 DATA_WIDTH 位
    wire signed [DATA_WIDTH-1:0] data_clipped;
    assign data_clipped = (data_centered > $signed({1'b0, {(DATA_WIDTH-1){1'b1}}})) ?
                          {1'b0, {(DATA_WIDTH-1){1'b1}}} :  // 正饱和
                          (data_centered < $signed({1'b1, {(DATA_WIDTH-1){1'b0}}})) ?
                          {1'b1, {(DATA_WIDTH-1){1'b0}}} :  // 负饱和
                          data_centered[DATA_WIDTH-1:0];    // 正常范围

    // ================================================================
    // 增益处理(可选)
    // ================================================================
    // 扩展位宽确保乘法不溢出:需要 DATA_WIDTH + log2(GAIN) 位
    wire signed [DATA_WIDTH+FRAC_BITS+1:0] gain_product;  // 额外增加2位防止溢出
    wire signed [DATA_WIDTH-1:0] data_with_gain;
   
    generate
        if (GAIN_EN) begin : gen_gain
            // 乘以增益系数(扩展到足够位宽)
            assign gain_product = $signed(data_clipped) * $signed(gain_value);
            // 右移小数位并饱和限位
            wire signed [DATA_WIDTH+FRAC_BITS+1:0] gain_shifted = gain_product >>> FRAC_BITS;
           
            // 饱和限幅(确保使用足够位宽进行比较)
            wire signed [DATA_WIDTH+FRAC_BITS+1:0] max_pos = {{(FRAC_BITS+2){1'b0}}, {1'b0, {(DATA_WIDTH-1){1'b1}}}};
            wire signed [DATA_WIDTH+FRAC_BITS+1:0] max_neg = {{(FRAC_BITS+2){1'b1}}, {1'b1, {(DATA_WIDTH-1){1'b0}}}};
           
            assign data_with_gain =
                (gain_shifted > max_pos) ? {1'b0, {(DATA_WIDTH-1){1'b1}}} :  // 正饱和到 +511
                (gain_shifted < max_neg) ? {1'b1, {(DATA_WIDTH-1){1'b0}}} :  // 负饱和到 -512
                gain_shifted[DATA_WIDTH-1:0];
        end else begin : gen_no_gain
            assign gain_product = 0;
            assign data_with_gain = data_clipped;
        end
    endgenerate

    // ================================================================
    // 时序逻辑
    // ================================================================
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            data_out_valid <= 1'b0;
            data_out <= 0;
            dc_accumulator <= 0;
            sample_counter <= 0;
            dc_value_locked <= 0;
            dc_locked <= 1'b0;
        end else begin
            data_out_valid <= data_valid;
           
            if (data_valid) begin
                // 输出路径(应用增益处理)
                data_out <= data_with_gain;
               
                // DC 值锁定逻辑
                if (!dc_locked) begin
                    // 未锁定:继续累加
                    if (sample_counter < LOCK_SAMPLES) begin
                        dc_accumulator <= dc_accumulator + $unsigned({4'b0, data_in});
                        sample_counter <= sample_counter + 1'b1;
                    end else begin
                        // 到达锁定时间:计算并锁定 DC 值
                        dc_value_locked <= dc_est;
                        dc_locked <= 1'b1;
                    end
                end else begin
                    // 已锁定:不再更新 DC 值和累加器
                    // DC 值保持不变,继续处理输入数据
                end
            end
        end
    end

endmodule
  • RMS计算: 使用短时 RMS 算法计算音量。
module audio_rms #(
    parameter DATA_WIDTH  = 10,   // 输入数据位宽
    parameter AVG_SHIFT   = 10,   // 平均窗口移位量
    parameter SQRT_EN     = 0,    // 开方使能(1=真 RMS,0=平方均值)
    parameter OUTPUT_WIDTH = 8    // 输出位宽,默认 8 位(0-255,用于 LED)
)(
    input                           clk,
    input                           rst_n,
   
    // 输入接口
    input                           data_valid,  // 输入数据有效
    input  signed [DATA_WIDTH-1:0]  data_in,     // 音频数据(有符号)
   
    // 输出接口
    output reg                      rms_valid,   // RMS 值更新有效
    output reg [OUTPUT_WIDTH-1:0]   rms_value    // RMS 值(0-255 用于 LED)
);

    // ======================================================================
    // 位宽与缩放计算
    // ======================================================================
    localparam integer SQ_WIDTH   = DATA_WIDTH * 2;               // 平方位宽
    localparam integer ACC_WIDTH  = SQ_WIDTH + AVG_SHIFT;         // 累加器位宽
    localparam integer SCALE_SHIFT = (SQ_WIDTH > OUTPUT_WIDTH) ?  // 输出缩放
                                    (SQ_WIDTH - OUTPUT_WIDTH) : 0;

    // ======================================================================
    // 平方与指数滑动平均(简易 RMS:平方+均值)
    // ======================================================================
    reg [SQ_WIDTH-1:0] square;
    reg [ACC_WIDTH-1:0] acc;
    reg [ACC_WIDTH-1:0] temp_acc;  // 临时变量用于溢出检测
   
    wire [SQ_WIDTH-1:0] avg_sq = acc >> AVG_SHIFT;  // 平方均值
    wire [SQ_WIDTH-1:0] avg_sq_scaled = avg_sq >> SCALE_SHIFT;   // 缩放后的平方均值
   
    // 饱和限幅输出
    wire [OUTPUT_WIDTH-1:0] max_val = {OUTPUT_WIDTH{1'b1}};  // 最大值(全 1)
    wire [OUTPUT_WIDTH-1:0] rms_saturated = (avg_sq_scaled > max_val) ? max_val : avg_sq_scaled[OUTPUT_WIDTH-1:0];

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            square    <= {SQ_WIDTH{1'b0}};
            acc       <= {ACC_WIDTH{1'b0}};
            temp_acc  <= {ACC_WIDTH{1'b0}};
            rms_valid <= 1'b0;
            rms_value <= {OUTPUT_WIDTH{1'b0}};
        end else begin
            rms_valid <= data_valid;
            if (data_valid) begin
                // 平方(有符号乘法)
                square <= $unsigned($signed(data_in) * $signed(data_in));
               
                // 指数滑动平均:acc = acc - acc/2^AVG_SHIFT + square
                // 防溢出保护:检查 square 左移后是否会溢出
                if (square > ({ACC_WIDTH{1'b1}} >> AVG_SHIFT)) begin
                    // 输入信号过大,饱和到最大值
                    acc <= {ACC_WIDTH{1'b1}};
                end else begin
                    // 正常更新:acc = acc - (acc >> AVG_SHIFT) + (square << AVG_SHIFT)
                    // 先计算减去衰减项
                    temp_acc = acc - (acc >> AVG_SHIFT);
                    // 检查加上新值是否溢出
                    if (temp_acc > ({ACC_WIDTH{1'b1}} - (square << AVG_SHIFT))) begin
                        // 加上新值会溢出,饱和
                        acc <= {ACC_WIDTH{1'b1}};
                    end else begin
                        acc <= temp_acc + (square << AVG_SHIFT);
                    end
                end
                // 缩放到输出位宽,加饱和限幅
                rms_value <= rms_saturated;
            end
        end
    end
endmodule
  • 频谱分析: 这个并不是标准的Goertzel算法,而是一个针对FPGA的优化版本,用更简单的方式达到类似Goertzel的目的。
  • 此算法用高频方波与输入信号做相关运算(只做±累加,无乘法)
  • 标准的Goertzel算法资源太高,附加代码中有一个检测2.5kHz的单点Goertzel算法,文件名为audio_spectrum_goertzel_2k5.v,可按需启用。
// =============================================================================
// Module: audio_spectrum_goertzel  (接口保持不变,超省资源版)
// Function: 8频点能量检测(方波相关法,无乘法)
// Fs: 20kHz (由12MHz分频得到)
// Output: spectrum_ch0~7 = 8个频点幅度(0~63)
// =============================================================================
module audio_spectrum_goertzel #(
    parameter integer DATA_WIDTH     = 10,
    parameter integer CLK_HZ         = 12_000_000,
    parameter integer FS_HZ          = 20_000,
    parameter integer VALID_HOLD_CYC = 12_000,
    // ===== 8个频点 (100~10k) =====
    // half period samples: H = Fs/(2f)
    parameter integer H0_100   = 100,  // 100Hz
    parameter integer H1_250   = 40,   // 250Hz
    parameter integer H2_500   = 20,   // 500Hz
    parameter integer H3_1K    = 10,   // 1kHz
    parameter integer H4_2K    = 5,    // 2kHz
    parameter integer H5_4K    = 2,    // 4kHz (近似)
    parameter integer H6_8K    = 1,    // 8kHz
    parameter integer H7_10K   = 1,    // 10kHz (Nyquist)
    // ===== 输出缩放 shift(可调)=====
    // 低频累加多 -> shift大,高频累加少 -> shift小
    parameter integer SHIFT0_100 = 7,
    parameter integer SHIFT1_250 = 7,
    parameter integer SHIFT2_500 = 6,
    parameter integer SHIFT3_1K  = 6,
    parameter integer SHIFT4_2K  = 5,
    parameter integer SHIFT5_4K  = 4,
    parameter integer SHIFT6_8K  = 4,
    parameter integer SHIFT7_10K = 4
)(
    input                              clk,
    input                              rst_n,
    input                              data_valid,   // unused OK
    input  signed [DATA_WIDTH-1:0]     data_in,
    output reg                         spectrum_valid,
    output reg [5:0] spectrum_ch0,
    output reg [5:0] spectrum_ch1,
    output reg [5:0] spectrum_ch2,
    output reg [5:0] spectrum_ch3,
    output reg [5:0] spectrum_ch4,
    output reg [5:0] spectrum_ch5,
    output reg [5:0] spectrum_ch6,
    output reg [5:0] spectrum_ch7
);

    // ============================================================
    // 1) 12MHz -> 20kHz sample_en
    // ============================================================
    localparam integer DIV = (CLK_HZ / FS_HZ); // 600
    reg [$clog2(DIV)-1:0] div_cnt;
    wire sample_en = (div_cnt == DIV-1);

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n) div_cnt <= 0;
        else begin
            if(sample_en) div_cnt <= 0;
            else          div_cnt <= div_cnt + 1'b1;
        end
    end

    // ============================================================
    // 2) latch sample
    // ============================================================
    reg signed [DATA_WIDTH-1:0] x_sample;
    wire signed [DATA_WIDTH:0] x_ext = {x_sample[DATA_WIDTH-1], x_sample};

    // ============================================================
    // 3) square-wave toggles (8 tones)
    // ============================================================
    reg [$clog2(H0_100)-1:0] c0;
    reg [$clog2(H1_250)-1:0] c1;
    reg [$clog2(H2_500)-1:0] c2;
    reg [$clog2(H3_1K )-1:0] c3;
    reg [$clog2(H4_2K )-1:0] c4;
    reg [$clog2(H5_4K )-1:0] c5;
    reg [$clog2(H6_8K )-1:0] c6;
    reg [$clog2(H7_10K)-1:0] c7;
    reg s0, s1, s2, s3, s4, s5, s6, s7;
    // accumulators (pure add/sub)
    reg signed [19:0] a0, a1, a2, a3, a4, a5, a6, a7;

    // ============================================================
    // 4) output window: use 100Hz period (200 samples -> 10ms)
    // ============================================================
    localparam integer OUT_N = (FS_HZ / 100); // 200
    reg [$clog2(OUT_N)-1:0] out_cnt;

    // ============================================================
    // 5) spectrum_valid hold
    // ============================================================
    reg [$clog2(VALID_HOLD_CYC+1)-1:0] valid_cnt;

    // ============================================================
    // 6) saturate function
    // ============================================================
    function automatic [5:0] sat6_abs_shift;
        input signed [19:0] v;
        input integer sh;
        reg [19:0] abs_v;
        reg [19:0] scaled;
        begin
            abs_v = v[19] ? (~v + 1'b1) : v;
            scaled = abs_v >> sh;
            if(scaled >= 20'd63) sat6_abs_shift = 6'd63;
            else                 sat6_abs_shift = scaled[5:0];
        end
    endfunction

    // ============================================================
    // 7) sequential
    // ============================================================
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            x_sample <= 0;
            c0<=0; c1<=0; c2<=0; c3<=0; c4<=0; c5<=0; c6<=0; c7<=0;
            s0<=0; s1<=0; s2<=0; s3<=0; s4<=0; s5<=0; s6<=0; s7<=0;
            a0<=0; a1<=0; a2<=0; a3<=0; a4<=0; a5<=0; a6<=0; a7<=0;
            out_cnt <= 0;
            spectrum_valid <= 1'b0;
            valid_cnt <= 0;
            spectrum_ch0<=0; spectrum_ch1<=0; spectrum_ch2<=0; spectrum_ch3<=0;
            spectrum_ch4<=0; spectrum_ch5<=0; spectrum_ch6<=0; spectrum_ch7<=0;
        end else begin
            // valid hold
            if(valid_cnt != 0) begin
                valid_cnt <= valid_cnt - 1'b1;
                spectrum_valid <= 1'b1;
            end else begin
                spectrum_valid <= 1'b0;
            end

            if(sample_en) begin
                x_sample <= data_in;

                // -------- toggle signs --------
                if(c0 == H0_100-1) begin c0<=0; s0<=~s0; end else c0<=c0+1'b1;
                if(c1 == H1_250-1) begin c1<=0; s1<=~s1; end else c1<=c1+1'b1;
                if(c2 == H2_500-1) begin c2<=0; s2<=~s2; end else c2<=c2+1'b1;
                if(c3 == H3_1K -1) begin c3<=0; s3<=~s3; end else c3<=c3+1'b1;
                if(c4 == H4_2K -1) begin c4<=0; s4<=~s4; end else c4<=c4+1'b1;
                if(c5 == H5_4K -1) begin c5<=0; s5<=~s5; end else c5<=c5+1'b1;
                if(c6 == H6_8K -1) begin c6<=0; s6<=~s6; end else c6<=c6+1'b1;
                if(c7 == H7_10K-1) begin c7<=0; s7<=~s7; end else c7<=c7+1'b1;

                // -------- accumulate (+x / -x) --------
                a0 <= s0 ? (a0 - x_ext) : (a0 + x_ext);
                a1 <= s1 ? (a1 - x_ext) : (a1 + x_ext);
                a2 <= s2 ? (a2 - x_ext) : (a2 + x_ext);
                a3 <= s3 ? (a3 - x_ext) : (a3 + x_ext);
                a4 <= s4 ? (a4 - x_ext) : (a4 + x_ext);
                a5 <= s5 ? (a5 - x_ext) : (a5 + x_ext);
                a6 <= s6 ? (a6 - x_ext) : (a6 + x_ext);
                a7 <= s7 ? (a7 - x_ext) : (a7 + x_ext);

                // -------- output update every 10ms --------
                if(out_cnt == OUT_N-1) begin
                    out_cnt <= 0;
                    spectrum_ch0 <= sat6_abs_shift(a0, SHIFT0_100);
                    spectrum_ch1 <= sat6_abs_shift(a1, SHIFT1_250);
                    spectrum_ch2 <= sat6_abs_shift(a2, SHIFT2_500);
                    spectrum_ch3 <= sat6_abs_shift(a3, SHIFT3_1K);
                    spectrum_ch4 <= sat6_abs_shift(a4, SHIFT4_2K);
                    spectrum_ch5 <= sat6_abs_shift(a5, SHIFT5_4K);
                    spectrum_ch6 <= sat6_abs_shift(a6, SHIFT6_8K);
                    spectrum_ch7 <= sat6_abs_shift(a7, SHIFT7_10K);

                    // reset acc
                    a0<=0; a1<=0; a2<=0; a3<=0; a4<=0; a5<=0; a6<=0; a7<=0;

                    // pulse hold
                    valid_cnt <= VALID_HOLD_CYC[$clog2(VALID_HOLD_CYC+1)-1:0];
                end else begin
                    out_cnt <= out_cnt + 1'b1;
                end
            end
        end
    end
endmodule
  • 显示与交互: 控制 LED 条与 OLED 屏幕显示,编码器用于调节增益。以下代码为利用8个LED,通过PWM控制显示256级的的显示,用于声音强度显示。
// ============================================================================
// LED PWM 驱动模块
// 功能:根据输入的 0-255 亮度值,控制 8 个 LED 以柱状图+PWM 渐变方式显示
// LED 类型:共阳极(低电平点亮)
// 原理:将 0-255 分为 8 个档位(每档 32 级),前面的 LED 全亮,当前 LED 根据余数进行 PWM 调制
// ============================================================================

module led_pwm_driver #(
    parameter LED_NUM = 8,           // LED 数量
    parameter PWM_BITS = 5           // PWM 分辨率(5位=32级,对应每个LED的亮度范围)
)(
    input  wire clk,                 // 系统时钟
    input  wire rst_n,               // 复位信号(低电平有效)
    input  wire [7:0] brightness,    // 输入亮度值 0-255
    output wire [LED_NUM-1:0] led_out // LED 输出(共阳极,低电平点亮)
);

    // ================================================================
    // PWM 计数器:用于生成 PWM 波形
    // ================================================================
    reg [PWM_BITS-1:0] pwm_counter;
   
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            pwm_counter <= 0;
        else
            pwm_counter <= pwm_counter + 1'b1;  // 循环计数 0~31
    end

    // ================================================================
    // 将 0-255 分解为:完全点亮的 LED 数量 + 当前 LED 的 PWM 占空比
    // brightness[7:5] → 决定前面有几个 LED 全亮(0~7)
    // brightness[4:0] → 决定当前 LED 的 PWM 占空比(0~31)
    // ================================================================
    wire [2:0] full_leds = brightness[7:5];        // 完全点亮的 LED 数量(0-7)
    wire [PWM_BITS-1:0] pwm_duty = brightness[4:0]; // 当前 LED 的 PWM 占空比(0-31)

    // ================================================================
    // 生成 8 个 LED 的 PWM 输出
    // ================================================================
    wire [LED_NUM-1:0] led_pwm;
   
    genvar i;
    generate
        for (i = 0; i < LED_NUM; i = i + 1) begin : led_gen
            assign led_pwm[i] = (i < full_leds) ? 1'b1 :              // 前面的 LED 全亮
                                (i == full_leds) ? (pwm_counter < pwm_duty) : // 当前 LED 使用 PWM
                                1'b0;                                  // 后面的 LED 全灭
        end
    endgenerate

    // ================================================================
    // 共阳极转换:1 表示亮 → 0 表示亮(取反)
    // ================================================================
    assign led_out = ~led_pwm;

endmodule


六、功能展示

1. 编译结果:由于开发板资源有限,无法同时启用 OLED 屏幕切换显示频谱分布和音量曲线。两个都启用时:Total logic elements 3,823 / 2,304 ( 166 % )

按需在代码中分别启用两个功能,编译结果分别如下:

image.png

image.png

2. 引脚配置:根据开发板手册,配置外设引脚,具体如下:

image.png

3. 音量显示:通过8颗 LED 和RGB 三色LED 显示音量大小,LED点亮数量表示强度、三色LED从蓝到红表示强度。

image.png image.png image.png

4. 频谱显示通过 OLED 屏幕显示频谱分布,共显示8个频率点的强度,分别为100、250、500、1k、2k、4k、8k、10k。

image.png

5. 音量曲线显示:通过 OLED 屏幕显示音量曲线,音量更新频率为30Hz,整个屏幕更新约为4s。显示模式包括未填充折线图和柱状图,通过按压 EC11 旋钮切换。

image.png image.png

6. 调整增益:通过 EC11 旋钮改变增益大小,增益比例从1到4之间(实际测试,增益为3时,刚好将ADC采集的填充到±512之间)。该部分图片无法展示,可观看视频查看效果。


七、难题与解决方法

  1. 为了方便调试,加入了UART模块,但是数据偏移

通过检查代码,发现是模块间的信号同步上出问题,通过对UART模块增加跨时钟域同步,从而正确发送数据。同样的,对于其他模块,也需要特别注意时钟和时序。

  1. 频谱分析中,初期算法复杂度高

最开始考虑频谱计算,肯定就是FFT或Goertzel算法。然而,其耗费资源太高,单点的Goertzel算法就使用了大量的资源。为此,选择了耗费资源更少的方波相关检测算法,可轻松完成8个频段的计算。


八、心得体会

  1. 通过本次项目,我深入理解了 FPGA 编程的特点与挑战,尤其是在处理音频信号与频谱分析时,FPGA 的并行处理能力提供了巨大的性能提升。
  2. 此外,我对声音处理和频谱计算有了更深入的理解。
  3. 感谢主办方提供的平台,让我在实践中不断提高自己的技术水平。


附件下载
FPGA_Sound_Amplitude_Spectrum_OLED_Pro.zip
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号