简介:本资源是一个基于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) |
|---|---|---|---|---|---|
| 40 | C4 | 261.63 | 191097 | 261.63 | 0.00 |
| 41 | C#4 | 277.18 | 180412 | 277.18 | 0.00 |
| 42 | D4 | 293.66 | 170422 | 293.66 | 0.00 |
| ... | ... | ... | ... | ... | ... |
| 59 | B5 | 987.77 | 50615 | 987.77 | 0.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_press和key_release是单脉冲信号,宽度为 1 个clk周期,供后续音符调度模块捕获。参数CLK_FREQ_HZ和DEBOUNCE_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_div;press_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(常见于黑金、安路开发板)为例,流程如下:
新建工程,选择器件型号
EP4CE6E22C8,设置 EDA 工具为 ModelSim-Altera;添加源文件:
top.v(顶层)、tone_table.v、key_debounce.v、note_scheduler.v、pwm_envelope.v、divider.v(基础分频模块);编写顶层模块,例化各子模块并连接信号:
// 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执行
Analysis & Synthesis→Elaboration→Fitter,观察 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_MS或ATTACK_MS的计数值。
4.2 编写 Pin Planning 文件:匹配物理按键与蜂鸣器位置
以黑金 AX301 开发板为例,其 4×4 矩阵键盘接 GPIO_0~GPIO_7,蜂鸣器接 GPIO_15:
| Signal | FPGA Pin | Location | I/O Standard |
|---|---|---|---|
clk_50m | PIN_R13 | Clock | 3.3-V LVTTL |
key_in[0] | PIN_T11 | GPIO_0 | 3.3-V LVTTL |
key_in[1] | PIN_T10 | GPIO_1 | 3.3-V LVTTL |
| ... | ... | ... | ... |
beep_out | PIN_U15 | GPIO_15 | 3.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无输出,按顺序排查:
clk_50m是否接入(用示波器测 PIN_R13);rst_n是否拉高(检查复位电路);key_in电平是否符合预期(万用表测 GPIO_0 对地电压);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_key和ch_div寄存器值,确认按键映射与查表无误。
5.3 资源优化:当 LUT 接近上限时的三项硬核压缩法
本设计在 EP4CE6 上仅用 15%,但若移植到资源更紧张的 GW1N-4(高云)或 TinyFPGA BX,需压缩:
- 合并 PWM 包络计数器:4 通道共用一套
att_cnt/dec_cnt,通过case选择当前活动通道,节省 75% 计数器资源; - 用 LUT 实现分频器:放弃通用计数器,对每个
ch_div值生成专用比较器(if (cnt == DIV_VAL) cnt=0),面积换速度; - 移除
key_release检测:仅靠key_valid电平变化判断松键,省去上升沿检测逻辑。
最终可将 LUT 占用压至 400 以下,适配最小系统 FPGA。
本文还有配套的精品资源,点击获取