FPGA入门实战:按键控制蜂鸣器播放音乐,LED还能跟着节奏闪烁

FPGA入门实战:按键控制蜂鸣器播放音乐,LED还能跟着节奏闪烁
FPGA入门实战按键控制蜂鸣器播放音乐LED还能跟着节奏闪烁一、缘起从“让蜂鸣器响”到“让蜂鸣器唱歌”二、基础知识音符频率与PWM波2.1 音符与频率的对应关系2.2 用PLL产生精确时钟三、系统架构总览四、模块代码实现逐模块讲解4.1 模块一PLL时钟管理4.2 模块二按键消抖与长短按检测4.3 模块三旋律存储器ROM4.4 模块四音符发生器PWM方波4.5 模块五呼吸灯PWM4.6 模块六顶层模块五、总结与扩展本文适合已经学完LED闪烁、按键消抖、蜂鸣器驱动、呼吸灯和PLL时钟IP核的FPGA初学者想做一个综合性的小项目来巩固知识。一、缘起从“让蜂鸣器响”到“让蜂鸣器唱歌”学完蜂鸣器我知道了蜂鸣器可以发出不同频率的声音既然蜂鸣器能发出不同频率的声音那能不能让它播放一段旋律呢说干就干我决定做一个“FPGA电子门铃”功能规划如下操作反应上电呼吸灯柔和闪烁系统待机短按按键1蜂鸣器播放“叮咚~”门铃声LED闪两下短按按键2蜂鸣器播放“滴-滴-滴-”警报声LED快速闪烁长按按键13秒蜂鸣器播放《小星星》旋律二、基础知识音符频率与PWM波2.1 音符与频率的对应关系蜂鸣器发出不同音调的原理很简单不同频率的方波驱动蜂鸣器就会发出不同音高的声音。简谱中的 1Do、2Re、3Mi… 分别对应一个特定的频率简谱唱名频率Hz周期μs1Do52319122Re58717043Mi65915174Fa69814335Sol78412766La88011367Si9881012核心公式周期 1 / 频率。比如 523Hz 的周期 ≈ 1912μs。要让蜂鸣器输出 523Hz 的声音只需要让输出电平每 956μs 翻转一次半个周期翻转一次。2.2 用PLL产生精确时钟我的开发板输入时钟是 50MHz直接用它来产生音符频率时分频系数计算如下分频系数 50,000,000 / (频率 × 2) 25,000,000 / 频率 例如 Do523Hz25,000,000 / 523 ≈ 47,801为了让分频更精确我使用 PLLMMCM将 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~70休止符/结束 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) 4d1: div_max 32d95500; // Do 523Hz 4d2: div_max 32d85100; // Re 587Hz 4d3: div_max 32d75800; // Mi 659Hz 4d4: div_max 32d71600; // Fa 698Hz 4d5: div_max 32d63800; // Sol 784Hz 4d6: div_max 32d56800; // La 880Hz 4d7: div_max 32d50600; // 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占空比调制

最新新闻

日新闻

周新闻

月新闻