跳到主要内容

实验 11:蜂鸣器电子琴

🛒 开发板: EK301 GW5A FPGA 开发板 芯片: GW5A-LV25UG324C1/I0 (Arora V 系列)


一、实验目的

  1. 掌握无源蜂鸣器的 PWM 驱动原理——通过改变方波频率发出不同音高。
  2. 理解音阶频率与 PWM 周期之间的数学关系:半周期计数值 = f_clk / (f_note × 2)
  3. 学会组合键设计——KEY1+KEY2 同时按下触发高八度升调。
  4. 掌握 always @(*) 组合逻辑与 always @(posedge clk) 时序逻辑的配合使用。

二、实验过程

2.1 硬件连接

信号方向FPGA 引脚Bank说明
CLK输入T9Bank 450MHz 系统时钟
KEY1输入C10Bank 1弹奏 Do(按下=0)
KEY2输入A10Bank 1弹奏 Re
KEY3输入B9Bank 0弹奏 Mi
KEY4输入A9Bank 0弹奏 Fa
BEEP输出J1Bank 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)高八度半周期
KEY1Do26295,41952347,794
KEY2Re29485,03458842,517
KEY3Mi33075,75766037,879
KEY4Fa34971,63369835,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

端口

端口方向位宽说明
clkinput150MHz 系统时钟
keyinput[3:0]4 个按键(KEY1~3)
beepoutput1蜂鸣器 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
  • 纯组合逻辑——noteoct_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

工作流程

  1. 有按键按下 (any_key=1):cnt 自增,到达 half_period 时翻转 beep 并清零 cnt
  2. 无按键:cntbeep 都清零,蜂鸣器不发声。
  3. beep 翻转频率 = f_clk / (half_period × 2),即目标音符频率。

为什么无按键时置 0 而非保持:释放按键后,PWM 的最后一个周期可能未完成。直接清零 cntbeep 确保快速静音,避免释放后短暂杂音。


四、实验现象

操作预期结果
上电蜂鸣器静音
按住 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=3494 个半周期计数值
高八度频率 × 2 = 半周期 ÷ 2oct_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