音符序列 + 节拍控制 — 播放《小星星》
🏆 成就:作曲家 ✅ Verilator验证
乐ROM存储{note[3:0], duration[3:0]},节拍定时器控制每个音符的时长,BPM=120。
// 《小星星》: C C G G A A G- F F E E D D C-
reg [7:0] melody [0:13];
initial begin
melody[0]={4'd1,4'd1}; melody[1]={4'd1,4'd1};
melody[2]={4'd5,4'd1}; melody[3]={4'd5,4'd1};
melody[4]={4'd6,4'd1}; melody[5]={4'd6,4'd1};
melody[6]={4'd5,4'd2}; // G4四分
melody[7]={4'd4,4'd1}; melody[8]={4'd4,4'd1};
melody[9]={4'd3,4'd1}; melody[10]={4'd3,4'd1};
melody[11]={4'd2,4'd1}; melody[12]={4'd2,4'd1};
melody[13]={4'd1,4'd2}; // C4四分
end
// 每帧检查节拍定时器,到时读下一个音符
wire [3:0] current_note=melody[seq_ptr][7:4];
wire [3:0] current_dur=melody[seq_ptr][3:0];module music_player_tb;
logic clk,rst_n; logic pwm_out;
music_player uut(.*);
initial clk=0; always #10 clk=~clk;
logic [3:0] last_note; int note_changes;
always @(posedge clk)
if(uut.current_note!=last_note) begin
$display("[%0t] 音符:%0d (时值=%0d)",
$time,uut.current_note,uut.current_duration);
last_note<=uut.current_note; note_changes++;
end
initial begin
rst_n=0;#100;rst_n=1;
$display("========== 音乐播放器验证 ==========");
$display("旋律: C C G G A A G- F F E E D D C-");
#100_000_000;
$display("音符变化次数: %0d",note_changes);
$display("==========================================");
$finish;
end
initial #200_000_000 $finish;
endmodule🎵 生日快乐:C4 C4 D4 C4 F4 E4 | C4 C4 D4 C4 G4 F4
🎵 两只老虎:C4 D4 E4 C4 | C4 D4 E4 C4 | E4 F4 G4- | E4 F4 G4-
🎵 欢乐颂:E4 E4 F4 G4 | G4 F4 E4 D4 | C4 C4 D4 E4 | E4- D4 D4-
只需修改melody数组就能切换歌曲!
// BPM=120 → 一拍=0.5s → 八分=0.25s
// 50MHz × 0.25s = 12,500,000
localparam EIGHTH_TICKS = 25'd12_500_000;
reg [24:0] beat_counter;
reg [3:0] current_duration;
wire beat_done = (beat_counter >= EIGHTH_TICKS * current_duration - 1);
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
seq_ptr <= 0; beat_counter <= 0;
current_duration <= 1;
end else if(beat_done) begin
beat_counter <= 0;
if(seq_ptr >= MELODY_LEN - 1)
seq_ptr <= 0; // 循环播放
else
seq_ptr <= seq_ptr + 1;
current_duration <= melody[seq_ptr][3:0];
end else
beat_counter <= beat_counter + 1;
endverilator --binary -j 0 --trace music_player.v music_player_tb.sv ./obj_dir/Vmusic_player_tb # 预期输出: # ========== 音乐播放器验证 ========== # [xxx] 音符:1 (时值=1) → C4八分 # [xxx] 音符:1 (时值=1) → C4八分 # [xxx] 音符:5 (时值=1) → G4八分 # [xxx] 音符:5 (时值=1) → G4八分 # [xxx] 音符:6 (时值=1) → A4八分 # [xxx] 音符:6 (时值=1) → A4八分 # [xxx] 音符:5 (时值=2) → G4四分 # ... (F F E E D D C-) # 循环播放 → 无限循环的小星星!
💡 扩展方向:添加多首歌曲用开关选择 | LED随节拍闪烁 | 合并VGA显示当前音符 | 快进/暂停功能
✅ 乐曲ROM的数据格式如何设计?
✅ 节拍定时器的计数周期如何计算?
✅ 如何实现循环播放?
✅ 休止符(rest)如何处理?
✅ 如何添加新曲目?
✅ BPM和音符时值的关系?
下一课开始学习通信协议!UART是最经典的串行协议——只需2根线,无需时钟。先学发送,仿真发送"Hello"字符串!
Q: 旋律不对? 逐条检查melody数组的音符编码和时值。
Q: 节奏不对? 检查EIGHTH_TICKS计算:50MHz×0.25s=12,500,000
Q: 不循环? 确认seq_ptr重置逻辑:if(seq_ptr>=MELODY_LEN-1) seq_ptr<=0;
Q: 音符之间有杂音? 切换音符时PWM可能有瞬态,可在切换时短暂静音。
| 参数 | 值 | 说明 |
|---|---|---|
| BPM | 120 | 每分钟120拍 |
| 一拍时长 | 0.5s | 60/120 |
| 八分音符 | 0.25s | 半拍 |
| 四分音符 | 0.5s | 一拍 |
| 八分计数 | 12,500,000 | 50MHz×0.25s |
| 音符编码 | 4位 | 0=rest,1=C4,...,8=C5 |
| 时值编码 | 4位 | 1=八分,2=四分,4=二分 |
| ROM条目 | 8位/条 | {note,duration} |
💡 音乐播放器扩展方向:①添加多首歌曲用开关选择 ②LED指示灯随节拍闪烁 ③合并VGA显示当前播放的音符 ④实现快进/暂停功能 ⑤添加音量控制(PWM占空比调节)⑥支持多音轨同时播放(和弦)
钢琴上相邻两个键的频率比 = 2^(1/12) ≈ 1.059463
A4 = 440Hz(国际标准音高)
A4# = 440 × 1.059463 = 466.16Hz
B4 = 466.16 × 1.059463 = 493.88Hz
C5 = 493.88 × 1.059463 = 523.25Hz
每高一个八度,频率翻倍:A5 = 880Hz, A6 = 1760Hz
FPGA计数器值 = 50,000,000 / 频率 / 2(半周期)
编译:verilator --binary -j 0 --trace music_player.v music_player_tb.sv
运行:./obj_dir/Vmusic_player_tb
波形:gtkwave music_player.vcd
提示:仿真速度很快,完整播放一遍《小星星》约2秒仿真时间,但需要等约200ms的仿真运行时间
💡 音符频率速查:C4=262 D4=294 E4=330 F4=349 G4=392 A4=440 B4=494 C5=523 Hz。这些是十二平均律下最常用的音符频率,FPGA用计数器分频产生方波即可播放。
🎵 生日快乐:C4 C4 D4 C4 F4 E4 | C4 C4 D4 C4 G4 F4
🎵 两只老虎:C4 D4 E4 C4 | C4 D4 E4 C4 | E4 F4 G4- | E4 F4 G4-
🎵 欢乐颂:E4 E4 F4 G4 | G4 F4 E4 D4 | C4 C4 D4 E4 | E4- D4 D4-
只需修改melody数组就能切换歌曲!
// 追踪音符变化
logic [3:0] last_note;
always @(posedge clk)
if(uut.current_note!=last_note) begin
$display("[%0t] 音符: %s (时值=%0d)",
$time, note_name(uut.current_note), uut.current_duration);
last_note <= uut.current_note;
end• 添加多首歌曲,用开关选择
• LED指示灯随节拍闪烁
• 合并VGA显示当前音符