一、项目任务介绍
本项目要求使用小脚丫FPGA电赛训练平台,完成基于麦克风的实时音量表与频谱显示。具体要求包括:
- 将麦克风放大模块的
Audio_Out接入 ADC 模块的模拟输入端,采样率设置为 8–20 kHz(FPGA 产生采样时钟并进行抽取)。 - 实现直流分量去除与简单预处理(如高通滤波或自动增益控制),并计算短时 RMS 或包络作为音量值。
- 通过核心板的 LED 实现音量条形显示,并用三色 LED 或 RGB LED 显示音量等级。
- 实现至少 4 个频段的能量分析(可使用小型 FFT 或多点 Goertzel),并通过 OLED 屏幕显示频谱柱状图。
- 支持按键切换“音量显示/频谱显示”模式,编码器用于调节增益和衰减时间常数。
- 完成任务时,需使用基于 FPGA 的电赛训练套件,具体版本核心板可根据需要选择。
二、项目介绍
本项目使用小脚丫 FPGA 核心板(Altera MAX10 系列 10M02SCM 芯片)完成任务,主要内容包括:
- 音量采集: 通过电赛板的 ADC 模块(3PA1030),以 20kHz 的采样率完成音量的实时采集。
- 数据预处理: FPGA 接收音频数据后,进行滤波与增益控制。
- 音量计算与显示: 使用短时 RMS 算法计算音量,并通过 LED 条、三色 LED 和 OLED 屏幕进行显示。
- 频谱计算与显示: 采用简单的频谱分析方法(方波相关检测),计算 8 个频段的能量并通过 OLED 屏幕显示。
- 用户交互: 通过编码器控制用户输入,调节增益与衰减,以及切换 OLED 屏幕的显示内容。


三、硬件介绍
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 接口。

2. ADC 模块:
- 采用 3PA1030 高速 ADC,10bit/50Msps,用于高速数据采集。

3. 麦克风模块:
- 模拟输出的麦克风,通过LM358运算放大器进行放大后输出至ADC。

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

四、方案框图与设计思路
1. 方案框图:
主要包括输入端、FPGA处理核心、输出设备三部分:

2. 设计思路
🎤 麦克风与ADC | 🎛️ 交互控制 - EC11 | ⚙️ 直流去除与增益 | 🔧 滤波处理(可选) | 📊 RMS计算 - 音量分析 |
|---|---|---|---|---|
采集环境中的音频信号 | 功能: | 处理步骤: | 高通滤波(HPF): | 功能:计算信号的均方根值 |
📈 频谱分析 | 🖥️ OLED显示驱动 | 🔌 LED与RGB输出 | 💻 UART串口调试输出 | ⚡ 关键性能指标 |
特性: | 双驱动模块: | LED条驱动(8个LED): | 功能:实时数据调试 | 采样性能: |
五、开发工具与编程语言
1. 开发环境:
- 使用 Intel Quartus Prime 25.1std 开发软件。
- 编程语言:Verilog。

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 % )。
按需在代码中分别启用两个功能,编译结果分别如下:


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

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

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

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

6. 调整增益:通过 EC11 旋钮改变增益大小,增益比例从1到4之间(实际测试,增益为3时,刚好将ADC采集的填充到±512之间)。该部分图片无法展示,可观看视频查看效果。
七、难题与解决方法
- 为了方便调试,加入了UART模块,但是数据偏移
通过检查代码,发现是模块间的信号同步上出问题,通过对UART模块增加跨时钟域同步,从而正确发送数据。同样的,对于其他模块,也需要特别注意时钟和时序。
- 频谱分析中,初期算法复杂度高
最开始考虑频谱计算,肯定就是FFT或Goertzel算法。然而,其耗费资源太高,单点的Goertzel算法就使用了大量的资源。为此,选择了耗费资源更少的方波相关检测算法,可轻松完成8个频段的计算。
八、心得体会
- 通过本次项目,我深入理解了 FPGA 编程的特点与挑战,尤其是在处理音频信号与频谱分析时,FPGA 的并行处理能力提供了巨大的性能提升。
- 此外,我对声音处理和频谱计算有了更深入的理解。
- 感谢主办方提供的平台,让我在实践中不断提高自己的技术水平。