实验 11:蜂鸣器电子琴
🛒 开发板: EK301 GW5A FPGA 开发板 芯片: GW5A-LV25UG324C1/I0 (Arora V 系列)
一、实验目的
- 掌握无源蜂鸣器的 PWM 驱动原理——通过改变方波频率发出不同音高。
- 理解音阶频率与 PWM 周期之间的数学关系:
半周期计数值 = f_clk / (f_note × 2)。 - 学会组合键设计——KEY1+KEY2 同时按下触发高八度升调。
- 掌握
always @(*)组合逻辑与always @(posedge clk)时序逻辑的配合使用。
二、实验过程
2.1 硬件连接
| 信号 | 方向 | FPGA 引脚 | Bank | 说明 |
|---|---|---|---|---|
| CLK | 输入 | T9 | Bank 4 | 50MHz 系统时钟 |
| KEY1 | 输入 | C10 | Bank 1 | 弹奏 Do(按下=0) |
| KEY2 | 输入 | A10 | Bank 1 | 弹奏 Re |
| KEY3 | 输入 | B9 | Bank 0 | 弹奏 Mi |
| KEY4 | 输入 | A9 | Bank 0 | 弹奏 Fa |
| BEEP | 输出 | J1 | Bank 6 | 无源蜂鸣器 |
无源 vs 有源蜂鸣器:无源蜂鸣器没有内部振荡电路,必须外部提供方波信号才能发声。有源蜂鸣器内含振荡器,通电即响(单一频率)。
2.2 PWM 音频驱动原理
蜂鸣器的发声频率由 FPGA 输出方波的频率决定。50% 占空比的方波通过以下方式生成:
PWM 周期 = f_clk / f_target
例如:发声 262Hz (中音 Do)
周期 = 50,000,000 / 262 ≈ 190,840 个时钟周期
半周期翻转: cnt 从 0 计数到 95,420 → beep 翻转 → cnt 清零 → 重新计数
↑ 半周期 = 190,840 / 2 = 95,420
核心代码模式:
if (cnt >= half_period) begin
cnt <= 0;
beep <= ~beep; // 翻转输出,产生方波
end else begin
cnt <= cnt + 1;
end
2.3 音阶频率表
| 按键 | 音符 | 中音频率(Hz) | 半周期计数值 | 高八度频率(Hz) | 高八度半周期 |
|---|---|---|---|---|---|
| KEY1 | Do | 262 | 95,419 | 523 | 47,794 |
| KEY2 | Re | 294 | 85,034 | 588 | 42,517 |
| KEY3 | Mi | 330 | 75,757 | 660 | 37,879 |
| KEY4 | Fa | 349 | 71,633 | 698 | 35,816 |
计算公式:
半周期计数值 = 50,000,000 / (频率 × 2)
= 25,000,000 / 频率
Do(262Hz): 25,000,000 / 262 ≈ 95,419
Re(294Hz): 25,000,000 / 294 ≈ 85,034
高八度:频率翻倍(如 Do: 262Hz → 523Hz),半周期计数值减半。
2.4 组合键设计
| 按键组合 | 行为 |
|---|---|
| 仅 KEY1 | 中音 Do |
| 仅 KEY2 | 中音 Re |
| 仅 KEY3 | 中音 Mi |
| 仅 KEY4 | 中音 Fa |
| KEY1 + KEY2 | 高八度 Do(523Hz) |
| 同时多键 | KEY1 优先(note 编码为 KEY1 键值) |
音符优先级:KEY1 > KEY2 > KEY3 > KEY4
wire [1:0] note = kp[0] ? 0 : kp[1] ? 1 : kp[2] ? 2 : kp[3] ? 3 : 0;
wire oct_high = kp[0] && kp[1]; // KEY1+KEY2 同时按下 → 高八度
2.5 模块设计
模块名:beep_piano
端口:
| 端口 | 方向 | 位宽 | 说明 |
|---|---|---|---|
clk | input | 1 | 50MHz 系统时钟 |
key | input | [3:0] | 4 个按键(KEY1~3) |
beep | output | 1 | 蜂鸣器 PWM 输出 |
设计框图:
top.v (beep_piano)
├─ key_debounce × 4 ← 4 键消抖
├─ 音符译码 (always @*) ← kp → note编码 + oct_high
├─ 半周期 LUT (always @*) ← note+oct → half_period
└─ PWM 生成 ← cnt + beep 翻转
三、代码逐段讲解
3.1 按键消抖与音符编码
wire [3:0] kp;
key_debounce #(.MS(20)) u0(.clk(clk), .key_in(key[0]), .pressed(kp[0]));
// ... u1, u2, u3 同理
wire [1:0] note = kp[0] ? 0 : kp[1] ? 1 : kp[2] ? 2 : kp[3] ? 3 : 0;
wire any_key = |kp; // 按位或:任一键按下则为 1
wire oct_high = kp[0] && kp[1]; // KEY1+KEY2 同时按下
note使用嵌套三元运算符,实现优先编码——KEY1 优先级最高。any_key控制蜂鸣器是否发声:无按键时beep=0(静音)。oct_high检测组合键:kp[0] && kp[1]。
3.2 半周期查找表(组合逻辑)
reg [15:0] half_period;
always @(*) begin
case (note)
2'd0: half_period = oct_high ? 47794 : 95419; // Do
2'd1: half_period = oct_high ? 42517 : 85034; // Re
2'd2: half_period = oct_high ? 37879 : 75757; // Mi
2'd3: half_period = oct_high ? 35816 : 71633; // Fa
default: half_period = 0;
endcase
end
- 纯组合逻辑——
note或oct_high变化时,half_period立即更新。 - 使用
always @(*)而非查找表 ROM——综合为 LUT 实现。 oct_high为 1 时,半周期值取高八度(约减半)。
3.3 PWM 生成器
reg [15:0] cnt;
always @(posedge clk) begin
if (any_key) begin
if (cnt >= half_period) begin
cnt <= 0;
beep <= ~beep; // 翻转输出
end else begin
cnt <= cnt + 1;
end
end else begin
cnt <= 0;
beep <= 0; // 无按键→静音
end
end
工作流程:
- 有按键按下 (
any_key=1):cnt自增,到达half_period时翻转beep并清零cnt。 - 无按键:
cnt和beep都清零,蜂鸣器不发声。 beep翻转频率 =f_clk / (half_period × 2),即目标音符频率。
为什么无按键时置 0 而非保持:释放按键后,PWM 的最后一个周期可能未完成。直接清零 cnt 和 beep 确保快速静音,避免释放后短暂杂音。
四、实验现象
| 操作 | 预期结果 |
|---|---|
| 上电 | 蜂鸣器静音 |
| 按住 KEY1 | 发出 Do (262Hz) 持续音 |
| 按住 KEY2 | 发出 Re (294Hz) |
| 按住 KEY3 | 发出 Mi (330Hz) |
| 按住 KEY4 | 发出 Fa (349Hz) |
| 同时按 KEY1+KEY2 | 发出高八度 Do (523Hz),音调明显升高 |
| 释放所有按键 | 立即静音 |
| 快速切换按键 | 音高立即切换,无延迟 |
验证要点:
- ✅ 4 个基本音高清晰可辨,音阶呈上升趋势(Do < Re < Mi < Fa)。
- ✅ 高八度 Do 明显比中音 Do 高一个八度。
- ✅ 音高切换无延迟、无杂音。
- ✅ 可以用手机调音器 APP(如 gStrings)靠近蜂鸣器验证频率。
- ✅ 同时按 3 个以上按键 → 仅 KEY1 优先的音符发声。
五、掌握知识点
| 知识点 | 说明 | 在本实验中的体现 |
|---|---|---|
| 无源蜂鸣器 | 需外部方波驱动,频率=音高 | beep 输出 PWM 方波 |
| PWM 音频 | 50% 占空比方波 → 基频发声 | cnt >= half_period 翻转 |
| 音阶频率 | Do=262, Re=294, Mi=330, Fa=349 | 4 个半周期计数值 |
| 高八度 | 频率 × 2 = 半周期 ÷ 2 | oct_high 切换半周期值 |
| 组合键检测 | 同时判断多个按键状态 | kp[0] && kp[1] |
| 优先编码器 | 多输入中选最高优先级的有效输入 | 嵌套 ?: 实现 KEY1 最高 |
附录:源码
top.v
// EK301 实验09: 蜂鸣器驱动(简易电子琴)
// KEY1~3: Do/Re/Mi/Fa, 组合键+KEY1=高八度
module beep_piano (input wire clk, input wire [3:0] key, output reg beep);
wire [3:0] kf, ks, kp;
assign kp = kf & ~ks;
key_debounce #(.MS(20)) u0(.clk(clk),.reset_n(1'b1),.key_in(key[0]),.key_flag(kf[0]),.key_state(ks[0]));
key_debounce #(.MS(20)) u1(.clk(clk),.reset_n(1'b1),.key_in(key[1]),.key_flag(kf[1]),.key_state(ks[1]));
key_debounce #(.MS(20)) u2(.clk(clk),.reset_n(1'b1),.key_in(key[2]),.key_flag(kf[2]),.key_state(ks[2]));
key_debounce #(.MS(20)) u3(.clk(clk),.reset_n(1'b1),.key_in(key[3]),.key_flag(kf[3]),.key_state(ks[3]));
// 音阶频率: Do=262 Re=294 Mi=330 Fa=349 Sol=392 La=440 Si=494
// PWM周期 = 50MHz/freq, 占空比50% => 比较值=周期/2
// 按键按下=0, 松开=1. 用ks电平检测持续按压
wire any_key = !ks[0] || !ks[1] || !ks[2] || !ks[3];
wire oct_high = !ks[0] && !ks[1]; // KEY1+KEY2=高八度
wire [1:0] note = !ks[0]?0: !ks[1]?1: !ks[2]?2: !ks[3]?3: 0;
reg [15:0] half_period;
always @(*) case(note) // 50MHz / (freq*2) 半周期点数
0: half_period = oct_high ? 47794 : 95419; // Do: 262Hz/523Hz
1: half_period = oct_high ? 42517 : 85034; // Re: 294Hz/588Hz
2: half_period = oct_high ? 37879 : 75757; // Mi: 330Hz/660Hz
3: half_period = oct_high ? 35816 : 71633; // Fa: 349Hz/698Hz
default: half_period = 0;
endcase
reg [15:0] cnt;
always @(posedge clk) begin
if (any_key) begin
if (cnt >= half_period) begin cnt <= 0; beep <= ~beep; end
else cnt <= cnt + 1;
end else begin cnt <= 0; beep <= 0; end
end
endmodule