FPGA入门实战:按键控制蜂鸣器播放音乐,LED还能跟着节奏闪烁
- 一、缘起:从“让蜂鸣器响”到“让蜂鸣器唱歌”
- 二、基础知识:音符频率与PWM波
- 2.1 音符与频率的对应关系
- 2.2 用PLL产生精确时钟
- 三、系统架构总览
- 四、模块代码实现(逐模块讲解)
- 4.1 模块一:PLL时钟管理
- 4.2 模块二:按键消抖与长短按检测
- 4.3 模块三:旋律存储器(ROM)
- 4.4 模块四:音符发生器(PWM方波)
- 4.5 模块五:呼吸灯PWM
- 4.6 模块六:顶层模块
- 五、总结与扩展
本文适合:已经学完LED闪烁、按键消抖、蜂鸣器驱动、呼吸灯和PLL时钟IP核的FPGA初学者,想做一个综合性的小项目来巩固知识。
一、缘起:从“让蜂鸣器响”到“让蜂鸣器唱歌”
学完蜂鸣器,我知道了蜂鸣器可以发出不同频率的声音,既然蜂鸣器能发出不同频率的声音,那能不能让它播放一段旋律呢?
说干就干!我决定做一个“FPGA电子门铃”,功能规划如下:
| 操作 | 反应 |
|---|---|
| 上电 | 呼吸灯柔和闪烁,系统待机 |
| 短按按键1 | 蜂鸣器播放“叮咚~”门铃声,LED闪两下 |
| 短按按键2 | 蜂鸣器播放“滴-滴-滴-”警报声,LED快速闪烁 |
| 长按按键1(3秒) | 蜂鸣器播放《小星星》旋律 |
二、基础知识:音符频率与PWM波
2.1 音符与频率的对应关系
蜂鸣器发出不同音调的原理很简单:不同频率的方波驱动蜂鸣器,就会发出不同音高的声音。
简谱中的 1(Do)、2(Re)、3(Mi)… 分别对应一个特定的频率:
| 简谱 | 唱名 | 频率(Hz) | 周期(μs) |
|---|---|---|---|
| 1 | Do | 523 | 1912 |
| 2 | Re | 587 | 1704 |
| 3 | Mi | 659 | 1517 |
| 4 | Fa | 698 | 1433 |
| 5 | Sol | 784 | 1276 |
| 6 | La | 880 | 1136 |
| 7 | Si | 988 | 1012 |
📌核心公式:周期 = 1 / 频率。比如 523Hz 的周期 ≈ 1912μs。要让蜂鸣器输出 523Hz 的声音,只需要让输出电平每 956μs 翻转一次(半个周期翻转一次)。
2.2 用PLL产生精确时钟
我的开发板输入时钟是 50MHz,直接用它来产生音符频率时,分频系数计算如下:
分频系数 = 50,000,000 / (频率 × 2) = 25,000,000 / 频率 例如 Do(523Hz):25,000,000 / 523 ≈ 47,801为了让分频更精确,我使用 PLL(MMCM)将 50MHz 倍频到 100MHz,这样分频步长更小,音符频率更准:
分频系数 = 50,000,000 / 频率 (100MHz下)三、系统架构总览
在动手写代码之前,先画出整个系统的架构图:
四、模块代码实现(逐模块讲解)
4.1 模块一:PLL时钟管理
使用Vivado的Clock IP核生成100MHz时钟。操作步骤:
- 点击 IP Catalog → 搜索 “Clocking Wizard”
- 输入时钟 50MHz,输出时钟 100MHz
- 点击 Generate
生成的PLL模块例化代码:
clk_wiz_0 u_pll ( .clk_out1(clk_100m), // 100MHz 输出 .clk_in1(clk_50m), // 50MHz 输入 .reset(~rst_n), .locked(pll_locked) );4.2 模块二:按键消抖与长短按检测
功能说明:
- 检测按键按下(下降沿)
- 消除按键抖动
- 区分短按(<1秒)和长按(>3秒)
module key_detect ( input clk, // 100MHz input rst_n, input key, output reg key_short, // 短按脉冲 output reg key_long // 长按脉冲 ); // 按键消抖:采样20ms reg [20:0] cnt_20ms; reg key_sync1, key_sync2; wire key_falling; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync1 <= 1; key_sync2 <= 1; end else begin key_sync1 <= key; key_sync2 <= key_sync1; end end assign key_falling = key_sync2 & ~key_sync1; // 下降沿检测 // 长短按判断 reg [31:0] press_cnt; reg key_down; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin press_cnt <= 0; key_down <= 0; key_short <= 0; key_long <= 0; end else begin key_short <= 0; key_long <= 0; if (key_falling) begin key_down <= 1; press_cnt <= 0; end else if (key_down && key_sync2) begin // 按键持续按下,开始计数 press_cnt <= press_cnt + 1; // 3秒 = 300,000,000 个周期(100MHz) if (press_cnt == 300_000_000) begin key_long <= 1; key_down <= 0; // 长按触发后不再响应短按 end end else if (key_down && ~key_sync2) begin // 按键释放 if (press_cnt < 100_000_000) begin // <1秒为短按 key_short <= 1; end key_down <= 0; end end end endmodule4.3 模块三:旋律存储器(ROM)
把乐谱存储为ROM数组,用索引读取:
module melody_rom ( input clk, input [4:0] addr, // 地址 0~31 output reg [3:0] note,// 音符 1~7,0=休止符/结束 output reg [3:0] dur // 节拍时长 ); // "叮咚" 门铃声 always @(posedge clk) begin case (addr) 0: begin note = 3; dur = 4; end // Mi 1: begin note = 1; dur = 6; end // Do(长音) default: begin note = 0; dur = 0; end endcase end endmodule4.4 模块四:音符发生器(PWM方波)
根据音符频率产生对应频率的方波信号:
module note_generator ( input clk, // 100MHz input rst_n, input [3:0] note, // 当前音符 1~7 input play_en, // 播放使能 output reg pwm_out, // 蜂鸣器驱动 output reg note_done // 当前音符播放完成 ); // 音符频率查找表(100MHz时钟下的分频系数) reg [31:0] div_max; always @(*) begin case (note) 4'd1: div_max = 32'd95500; // Do 523Hz 4'd2: div_max = 32'd85100; // Re 587Hz 4'd3: div_max = 32'd75800; // Mi 659Hz 4'd4: div_max = 32'd71600; // Fa 698Hz 4'd5: div_max = 32'd63800; // Sol 784Hz 4'd6: div_max = 32'd56800; // La 880Hz 4'd7: div_max = 32'd50600; // Si 988Hz default: div_max = 0; // 0=静音 endcase end reg [31:0] cnt; reg [31:0] duration_cnt; reg playing; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 0; duration_cnt <= 0; pwm_out <= 0; note_done <= 0; playing <= 0; end else begin note_done <= 0; if (play_en && !playing) begin playing <= 1; duration_cnt <= 0; end if (playing) begin if (div_max == 0) begin // 休止符:输出低电平 pwm_out <= 0; duration_cnt <= duration_cnt + 1; // 休止符持续0.2秒(100MHz计数) if (duration_cnt >= 20_000_000) begin note_done <= 1; playing <= 0; end end else begin // 产生PWM方波(50%占空比) cnt <= cnt + 1; if (cnt >= div_max) begin cnt <= 0; pwm_out <= ~pwm_out; end // 每个音符持续0.4秒(100MHz计数) duration_cnt <= duration_cnt + 1; if (duration_cnt >= 40_000_000) begin note_done <= 1; playing <= 0; duration_cnt <= 0; end end end else begin pwm_out <= 0; cnt <= 0; end end end endmodule4.5 模块五:呼吸灯PWM
用占空比周期性变化的PWM驱动LED,实现柔和呼吸效果:
module breath_led ( input clk, input rst_n, input beat_signal, // 播放音乐时随节拍闪烁 input play_mode, // 1=播放模式,0=待机 output reg led ); reg [15:0] pwm_cnt; reg [15:0] duty_cycle; reg [24:0] slow_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin pwm_cnt <= 0; duty_cycle <= 0; slow_cnt <= 0; led <= 0; end else begin if (play_mode) begin // 播放模式:随节拍闪烁 led <= beat_signal; end else begin // 待机模式:呼吸灯(2秒一个周期) slow_cnt <= slow_cnt + 1; // 占空比从0→65535→0变化(PWM周期约0.4ms) if (slow_cnt < 25_000_000) begin duty_cycle <= slow_cnt[15:0]; end else if (slow_cnt < 50_000_000) begin duty_cycle <= 65535 - (slow_cnt - 25_000_000); end else begin slow_cnt <= 0; end pwm_cnt <= pwm_cnt + 1; led <= (pwm_cnt < duty_cycle) ? 1 : 0; end end end endmodule4.6 模块六:顶层模块
将以上模块连接起来:
module top ( input clk_50m, // 50MHz输入 input rst_n, // 复位 input key1, // 按键1(门铃) input key2, // 按键2(警报) output buzzer, // 蜂鸣器 output led // 呼吸灯/状态指示 ); // 信号定义 wire clk_100m; wire pll_locked; wire key1_short, key1_long; wire key2_short; wire [3:0] note; wire [3:0] dur; wire note_done; wire pwm_out; wire play_en; wire beat_signal; // 1. PLL实例化 clk_wiz_0 u_pll ( .clk_out1(clk_100m), .clk_in1(clk_50m), .reset(~rst_n), .locked(pll_locked) ); // 2. 按键检测 key_detect u_key1 ( .clk(clk_100m), .rst_n(rst_n & pll_locked), .key(key1), .key_short(key1_short), .key_long(key1_long) ); key_detect u_key2 ( .clk(clk_100m), .rst_n(rst_n & pll_locked), .key(key2), .key_short(key2_short), .key_long() ); // 3. 旋律选择与地址控制 reg [4:0] melody_addr; reg playing_mode; wire [3:0] note_out; wire [3:0] dur_out; // 旋律ROM:根据按键选择不同旋律 melody_rom u_melody ( .clk(clk_100m), .addr(melody_addr), .note(note_out), .dur(dur_out) ); // 4. 播放控制状态机 reg [4:0] addr_cnt; reg play_enable; reg play_busy; always @(posedge clk_100m or negedge rst_n) begin if (!rst_n) begin addr_cnt <= 0; play_enable <= 0; play_busy <= 0; playing_mode <= 0; end else begin // 按键触发播放 if (key1_short && !play_busy) begin addr_cnt <= 0; play_enable <= 1; play_busy <= 1; playing_mode <= 1; end else if (key2_short && !play_busy) begin addr_cnt <= 8; // 第二段旋律起始地址 play_enable <= 1; play_busy <= 1; playing_mode <= 1; end // 播放进行中 if (play_busy) begin if (note_done) begin if (note_out == 0) begin // 遇到0表示旋律结束 play_busy <= 0; play_enable <= 0; playing_mode <= 0; end else begin addr_cnt <= addr_cnt + 1; end end end else begin play_enable <= 0; end end end // 5. 音符发生器 note_generator u_note ( .clk(clk_100m), .rst_n(rst_n & pll_locked), .note(note_out), .play_en(play_enable & pll_locked), .pwm_out(pwm_out), .note_done(note_done) ); // 6. 呼吸灯 assign beat_signal = pwm_out & play_busy; // 播放时LED随声音节奏闪烁 breath_led u_breath ( .clk(clk_100m), .rst_n(rst_n & pll_locked), .beat_signal(beat_signal), .play_mode(play_busy), .led(led) ); // 输出蜂鸣器 assign buzzer = pwm_out & pll_locked; endmodule五、总结与扩展
通过这个项目,我们综合运用了:
- PLL时钟管理:倍频获得更精确的分频
- 按键消抖与长短按检测:状态机设计
- ROM存储:乐谱数据存储
- PWM方波生成:音符频率控制
- 呼吸灯:占空比调制
扩展思路:
- 增加更多旋律,用更大的ROM存储
- 加入数码管显示当前播放的音符
- 用SD卡存储MIDI文件,播放任意音乐
- 增加音量控制(PWM占空比调制)