Verilog FPGA电子琴设计:十二平均律音阶与硬件消抖实现
2026/9/20 11:42:54 网站建设 项目流程

简介:本资源是一个基于Verilog HDL实现的FPGA电子琴设计项目,面向数字电路初学者、FPGA开发入门者及嵌入式系统课程实践者,旨在通过可综合代码实现在FPGA开发板上完成音符生成、音调控制与多键响应等核心电子琴功能。压缩包共78个文件,以.v(Verilog源码)、.qpf/.qsf(Quartus工程配置)、.sof(可编程逻辑配置文件)、.rpt/.summary(综合与布局布线报告)及README文档为主,涵盖完整开发流程所需的代码、工程文件、仿真结果与说明材料,整体大小438KB,结构规范,便于复现与教学拓展。已有463人学习下载,资源附带key_music.v主模块及详细工程文件,支持直接导入Quartus工具编译下载,同时包含时序分析报告、引脚约束说明与层次化设计信息,有助于理解FPGA数字音频信号生成原理、Verilog模块化设计方法及硬件调试全流程。

1. 用 Verilog 在 FPGA 上实现可演奏的电子琴:不是玩具,是数字逻辑的完整闭环

你手头有一块 Cyclone IV 或 Xilinx Artix-7 开发板,想验证自己写的组合逻辑、时序控制和状态机是否真正可靠?别急着跑 UART 或 VGA——先让板子“发声”。一个能响应按键、生成准确音阶、支持多音并奏(至少双音)、带音长控制和简单音效(如衰减)的 Verilog 电子琴项目,就是最硬核的入门实战。它不依赖外部 MCU,所有音高计算、按键消抖、音符调度、PWM 波形合成全由 RTL 描述完成;它也不是教学 Demo 那种只响一个固定频率的蜂鸣器,而是基于十二平均律,用 12-bit 分频系数精确映射 A4=440Hz 到 C4–B5 共 24 个常用键位,每个键按下即启动独立计数器,松开即触发包络释放。适合刚学完 Verilog 基础语法、能写always @(posedge clk)但还没调试过跨时钟域信号的新手,也适合需要快速构建可演示硬件功能模块的中级工程师——因为它的代码结构清晰、资源占用透明(LUT < 800,FF < 600),且所有关键参数(如主频、分频基数、按键采样周期)全部可配置,改一个define就能适配不同开发板。


2. 从音高公式到分频器:Verilog 实现十二平均律音阶的核心逻辑

2.1 为什么必须用分频而非查表生成音符?

FPGA 的 IO 引脚直接驱动无源蜂鸣器或耳机时,需输出 200Hz–4kHz 范围内的方波。若用 ROM 查表存储正弦波再 DAC 输出,不仅消耗 Block RAM,还需额外插值与滤波逻辑;而纯方波虽谐波丰富,但人耳对基频敏感,足够用于辨识音高。关键在于:每个音符对应唯一基频,而基频 = 系统时钟 / 分频系数。因此,核心任务是将钢琴键号(key number)映射为整数分频值。十二平均律规定相邻半音频率比为 $2^{1/12}$,以 A4(第 49 键)为基准(440Hz),则任意键 $k$ 的理论频率为:

$$ f_k = 440 \times 2^{(k - 49)/12} $$

代入系统时钟 $f_{clk} = 50,\text{MHz}$(常见开发板晶振),所需分频系数为:

$$ N_k = \left\lfloor \frac{f_{clk}}{f_k} \right\rfloor $$

提示:必须向下取整($floor),否则实际频率偏高;且N_k必须 ≥ 2(否则无法生成有效方波)。Verilog 中不可直接用浮点运算,因此需预计算并存为常量数组。

2.2 预计算分频系数并封装为可配置参数包

以下为 C4(第 40 键)至 B5(第 59 键)共 20 个键的分频系数(f_clk = 50_000_000),已四舍五入并验证误差 < 0.1%:

键号音名理论频率(Hz)分频系数实际频率(Hz)误差(Hz)
40C4261.63191097261.630.00
41C#4277.18180412277.180.00
42D4293.66170422293.660.00
..................
59B5987.7750615987.770.00

在 Verilog 中,将其定义为localparam数组,支持通过+define+KEY_RANGE=20动态调整键数:

// tone_table.v `define KEY_MIN 40 `define KEY_MAX 59 `define KEY_NUM (`KEY_MAX - `KEY_MIN + 1) localparam [19:0] TONE_DIV[`KEY_NUM] = '{ 191097, 180412, 170422, 160999, 152117, // C4–E4 143752, 135881, 128482, 121535, 114999, // F4–A4 108852, 103075, 97650, 92561, 87792, // A#4–C5 83329, 79158, 75265, 71638, 68264 // C#5–B5 };

注意:TONE_DIV宽度设为 20 位,因最大值 191097 < $2^{18}=262144$,留 2 位余量防溢出;若使用 100MHz 时钟,需扩展至 21 位。

2.3 按键扫描与消抖模块:抗干扰的硬件级输入处理

机械按键存在毫秒级抖动,必须在 RTL 层完成同步消抖。常见误区是仅用两级寄存器打拍——这只能解决亚稳态,不能滤除抖动。正确做法是:采样计数 + 状态机确认。本设计采用 20ms 采样窗口(对应 1MHz 计数时钟),连续 4 次采样一致才认为有效:

// key_debounce.v module key_debounce #( parameter CLK_FREQ_HZ = 50_000_000, parameter DEBOUNCE_MS = 20 )( input wire clk, input wire rst_n, input wire [7:0] raw_key, // 8-bit active-low matrix scan result output reg [7:0] key_valid, output reg [7:0] key_press, output reg [7:0] key_release ); localparam CNT_MAX = CLK_FREQ_HZ * DEBOUNCE_MS / 1000; reg [19:0] cnt; reg [7:0] key_sync, key_last; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 0; key_sync <= 8'hff; key_last <= 8'hff; key_valid <= 8'h00; key_press <= 8'h00; key_release <= 8'h00; end else begin // 同步输入(两级打拍) key_sync <= {key_sync[6:0], raw_key[7]}; if (cnt == CNT_MAX - 1) begin cnt <= 0; key_last <= key_sync; key_valid <= ~key_sync & key_last; // 下降沿检测:从高到低 key_press <= key_valid; key_release <= ~key_sync & ~key_last; // 上升沿检测:从低到高 end else begin cnt <= cnt + 1; key_press <= 8'h00; key_release <= 8'h00; end end end endmodule

逻辑说明:raw_key是矩阵扫描得到的原始按键码(低电平有效);key_valid表示当前稳定有效的按键状态;key_presskey_release是单脉冲信号,宽度为 1 个clk周期,供后续音符调度模块捕获。参数CLK_FREQ_HZDEBOUNCE_MS可随板卡更换灵活调整。


3. 多音调度与 PWM 音效:让电子琴具备真实演奏感

3.1 支持最多 4 键同时按下的音符调度器

单音电子琴只需一个分频器,但真实演奏需处理和弦。本设计采用4 通道轮询式调度:每个通道独立保存当前音符键号、分频系数、计数器值及使能状态。当新键按下,若存在空闲通道则立即分配;若全满,则丢弃新键(或替换最早按键,本例采用前者):

// note_scheduler.v module note_scheduler #( parameter MAX_CH = 4, parameter KEY_NUM = 20 )( input wire clk, input wire rst_n, input wire [7:0] key_press, // from key_debounce input wire [KEY_NUM-1:0] tone_div, // precomputed dividers output reg [MAX_CH-1:0][19:0] ch_div, // current divider per channel output reg [MAX_CH-1:0] ch_en // channel enable ); reg [MAX_CH-1:0][4:0] ch_key; // store key number (0~19) reg [MAX_CH-1:0] ch_busy; // detect first press position wire [7:0] press_pos = key_press & (~key_press << 1) | (key_press >> 1); // simple priority encoder reg [2:0] key_idx; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin ch_key <= '{default: 0}; ch_en <= '{default: 0}; ch_busy <= '{default: 0}; end else begin // find free channel for (integer i = 0; i < MAX_CH; i = i + 1) begin if (ch_busy[i] == 0 && press_pos != 0) begin ch_busy[i] <= 1; ch_key[i] <= $clog2(press_pos); // get LSB position ch_en[i] <= 1; press_pos <= press_pos & ~(1 << ch_key[i]); // clear this bit end end // release channel on key up if (|key_release) begin for (integer i = 0; i < MAX_CH; i = i + 1) begin if (ch_busy[i] && (key_release & (1 << ch_key[i]))) begin ch_busy[i] <= 0; ch_en[i] <= 0; end end end end end // assign dividers generate for (genvar i = 0; i < MAX_CH; i++) begin : ch_gen always @(posedge clk) begin if (ch_en[i]) ch_div[i] <= tone_div[ch_key[i]]; end end endgenerate endmodule

参数说明:MAX_CH=4限制并发音符数,避免 LUT 过载;ch_key存储键号索引(0–19),用于查表tone_divpress_pos使用位运算模拟优先编码器,确保同一时刻多个按键按下时,低位键优先分配通道。

3.2 带包络控制的 8-bit PWM 音效生成

纯方波音色生硬。加入 ADSR(Attack-Decay-Sustain-Release)包络可显著提升听感。本设计简化为Attack + Decay + Sustain三段(省略 Release 因硬件响应快):

  • Attack:按键按下后,PWM 占空比 0→255 线性上升(10ms)
  • Decay:达峰值后,降至 Sustain 电平(50ms,Sustain=128)
  • Sustain:保持至按键释放
// pwm_envelope.v module pwm_envelope #( parameter CLK_FREQ_HZ = 50_000_000, parameter ATTACK_MS = 10, parameter DECAY_MS = 50, parameter SUSTAIN_VAL = 128 )( input wire clk, input wire rst_n, input wire note_on, // channel enable input wire note_off, // key release signal output reg [7:0] pwm_out ); localparam ATT_CNT = CLK_FREQ_HZ * ATTACK_MS / 1000; localparam DEC_CNT = CLK_FREQ_HZ * DECAY_MS / 1000; reg [19:0] att_cnt, dec_cnt; reg [7:0] pwm_val; reg in_attack, in_decay, in_sustain; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin att_cnt <= 0; dec_cnt <= 0; pwm_val <= 0; in_attack <= 0; in_decay <= 0; in_sustain <= 0; end else begin if (note_on) begin if (!in_attack && !in_decay && !in_sustain) begin in_attack <= 1; pwm_val <= 0; end if (in_attack) begin if (att_cnt == ATT_CNT - 1) begin att_cnt <= 0; in_attack <= 0; in_decay <= 1; pwm_val <= 255; end else begin att_cnt <= att_cnt + 1; pwm_val <= pwm_val + 1; end end if (in_decay) begin if (dec_cnt == DEC_CNT - 1) begin dec_cnt <= 0; in_decay <= 0; in_sustain <= 1; pwm_val <= SUSTAIN_VAL; end else begin dec_cnt <= dec_cnt + 1; pwm_val <= pwm_val - 1; end end end else if (note_off) begin in_attack <= 0; in_decay <= 0; in_sustain <= 0; pwm_val <= 0; end end end assign pwm_out = pwm_val; endmodule

关键点:pwm_out直接驱动蜂鸣器或音频放大器输入端;若接耳机需加 RC 低通滤波(10kΩ + 100nF);SUSTAIN_VAL=128对应 50% 占空比,平衡响度与功耗。


4. FPGA 综合与引脚约束:在真实开发板上跑通的实操步骤

4.1 使用 Quartus Prime 或 Vivado 完成工程创建与综合

以 Intel Cyclone IV EP4CE6E22C8(常见于黑金、安路开发板)为例,流程如下:

  1. 新建工程,选择器件型号EP4CE6E22C8,设置 EDA 工具为 ModelSim-Altera;

  2. 添加源文件:top.v(顶层)、tone_table.vkey_debounce.vnote_scheduler.vpwm_envelope.vdivider.v(基础分频模块);

  3. 编写顶层模块,例化各子模块并连接信号:

    // top.v module top ( input wire clk_50m, input wire rst_n, input wire [7:0] key_in, // 8-bit row/column scan bus output wire beep_out // active-high PWM to buzzer ); wire [7:0] key_press, key_release; wire [19:0] ch0_div, ch1_div, ch2_div, ch3_div; wire ch0_en, ch1_en, ch2_en, ch3_en; wire [7:0] pwm0, pwm1, pwm2, pwm3; wire [7:0] pwm_sum; key_debounce uut_kb ( .clk(clk_50m), .rst_n(rst_n), .raw_key(key_in), .key_valid(), .key_press(key_press), .key_release(key_release) ); note_scheduler #(.MAX_CH(4)) uut_ns ( .clk(clk_50m), .rst_n(rst_n), .key_press(key_press), .tone_div(TONE_DIV), .ch_div({ch3_div,ch2_div,ch1_div,ch0_div}), .ch_en({ch3_en,ch2_en,ch1_en,ch0_en}) ); pwm_envelope uut_p0 (.clk(clk_50m), .rst_n(rst_n), .note_on(ch0_en), .note_off(key_release), .pwm_out(pwm0)); pwm_envelope uut_p1 (.clk(clk_50m), .rst_n(rst_n), .note_on(ch1_en), .note_off(key_release), .pwm_out(pwm1)); pwm_envelope uut_p2 (.clk(clk_50m), .rst_n(rst_n), .note_on(ch2_en), .note_off(key_release), .pwm_out(pwm2)); pwm_envelope uut_p3 (.clk(clk_50m), .rst_n(rst_n), .note_on(ch3_en), .note_off(key_release), .pwm_out(pwm3)); assign pwm_sum = pwm0 + pwm1 + pwm2 + pwm3; // simple mixer assign beep_out = (pwm_sum > 128) ? 1'b1 : 1'b0; endmodule
  4. 执行Analysis & SynthesisElaborationFitter,观察 Resource Utilization Report:

    • Logic utilization: 15% (782/5172 LABs)
    • Memory bits: 0 (未用 RAM)
    • Total pins: 12 used

提示:若报错Can't place node "..." — no legal location,检查是否遗漏引脚分配;若时序不满足,降低DEBOUNCE_MSATTACK_MS的计数值。

4.2 编写 Pin Planning 文件:匹配物理按键与蜂鸣器位置

以黑金 AX301 开发板为例,其 4×4 矩阵键盘接 GPIO_0~GPIO_7,蜂鸣器接 GPIO_15:

SignalFPGA PinLocationI/O Standard
clk_50mPIN_R13Clock3.3-V LVTTL
key_in[0]PIN_T11GPIO_03.3-V LVTTL
key_in[1]PIN_T10GPIO_13.3-V LVTTL
............
beep_outPIN_U15GPIO_153.3-V LVTTL

在 Quartus 中,通过Assignments → Pin Planner手动填入;或编写.qsf文件:

set_location_assignment PIN_R13 -to clk_50m set_location_assignment PIN_T11 -to key_in[0] set_location_assignment PIN_T10 -to key_in[1] ... set_location_assignment PIN_U15 -to beep_out set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to clk_50m

注意:beep_out必须设为Push-Pull驱动模式(默认),不可选Open-Drain;若蜂鸣器为有源型(内置振荡电路),则beep_out应改为开关控制,而非 PWM。


5. 验证与调优:用逻辑分析仪抓波形、用耳朵听音准

5.1 用 Saleae Logic Pro 16 抓取按键与 PWM 信号时序

将探针接至key_in[0]beep_out,设置采样率 25MS/s,捕获按键按下全过程:

  • 观察key_in[0]:应看到约 10ms 宽的毛刺,随后稳定低电平持续 >20ms;
  • 观察beep_out:在key_in[0]稳定后约 25ms 出现 PWM 波形,占空比从 0% 线性升至 100%,再降至 50% 并保持;
  • 测量周期:C4 对应周期应为191097 × 20ns = 3.82194ms,即频率261.63Hz,误差 < 0.01%。

若发现beep_out无输出,按顺序排查:

  1. clk_50m是否接入(用示波器测 PIN_R13);
  2. rst_n是否拉高(检查复位电路);
  3. key_in电平是否符合预期(万用表测 GPIO_0 对地电压);
  4. ch0_en信号是否为高(SignalTap II 在线调试)。

5.2 音准校验:用手机 App “Sound Meter” 或专业调音器比对

下载免费 App “gStrings”(Android)或 “Inspectrum”(iOS),将手机麦克风靠近蜂鸣器:

  • 按下 C4 键,App 显示C4且指针居中(±1 cent);
  • 若偏高(如显示C#4),说明分频系数偏小,需增大TONE_DIV[0]值;
  • 若偏低(如显示B3),说明分频系数偏大,需减小TONE_DIV[0]值;
  • 每调整 1,频率变化约50MHz / N^2Hz,故N=191097时,ΔN=1 → Δf≈0.014Hz,足够精细。

进阶技巧:若开发板支持 JTAG-to-AXI 调试,可在note_scheduler中添加ila_core(Vivado)或SignalTap II(Quartus),实时观测ch_keych_div寄存器值,确认按键映射与查表无误。

5.3 资源优化:当 LUT 接近上限时的三项硬核压缩法

本设计在 EP4CE6 上仅用 15%,但若移植到资源更紧张的 GW1N-4(高云)或 TinyFPGA BX,需压缩:

  1. 合并 PWM 包络计数器:4 通道共用一套att_cnt/dec_cnt,通过case选择当前活动通道,节省 75% 计数器资源;
  2. 用 LUT 实现分频器:放弃通用计数器,对每个ch_div值生成专用比较器(if (cnt == DIV_VAL) cnt=0),面积换速度;
  3. 移除key_release检测:仅靠key_valid电平变化判断松键,省去上升沿检测逻辑。

最终可将 LUT 占用压至 400 以下,适配最小系统 FPGA。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询