🎶 第9课:音乐播放器

音符序列 + 节拍控制 — 播放《小星星》

🏆 成就:作曲家 ✅ Verilator验证

🎶
作曲家
Verilator仿真播放《小星星》旋律

🎼 音乐=音符序列+节拍

乐ROM存储{note[3:0], duration[3:0]},节拍定时器控制每个音符的时长,BPM=120。

Verilogmusic_player.v核心
// 《小星星》: 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];

📐 节拍定时计算

BPM=120 (每分钟120拍) → 一拍 = 60/120 = 0.5s → 八分音符 = 0.25s → 四分音符 = 0.5s → 二分音符 = 1.0s 50MHz × 0.25s = 12,500,000 计数/八分音符 《小星星》时长: 14个音符 = 6个八分 + 2个四分 = 10个八分音符时长 = 2.5s 一遍 循环播放 → 无限循环的小星星! 乐曲ROM编码: {note[3:0], duration[3:0]} note: 0=rest, 1=C4, 2=D4, ... 8=C5 duration: 1=八分, 2=四分, 4=二分

🧪 Verilator测试台

SystemVerilogmusic_player_tb.sv
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数组就能切换歌曲!

📐 乐曲ROM编码详解

ROM编码: {note[3:0], duration[3:0]} = 8位/音符 note编码: 0 = 休止符(静音) 1 = C4 (262Hz) 中央C 2 = D4 (294Hz) 3 = E4 (330Hz) 4 = F4 (349Hz) 5 = G4 (392Hz) 6 = A4 (440Hz) 标准音高 7 = B4 (494Hz) 8 = C5 (523Hz) duration编码: 1 = 八分音符 (0.25s @BPM120) 2 = 四分音符 (0.50s) 3 = 附点四分 (0.75s) 4 = 二分音符 (1.00s) 《小星星》完整编码: C C G G A A G- F F E E D D C- 1C 1C 1G 1G 1A 1A 2G 1F 1F 1E 1E 1D 1D 2C 0x11 0x11 0x15 0x15 0x16 0x16 0x25 0x14 0x14 0x13 0x13 0x12 0x12 0x21

🔬 节拍定时器设计

Verilog节拍定时器详细实现
// 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;
end

🛠️ 编译运行

Bash编译运行步骤
verilator --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可能有瞬态,可在切换时短暂静音。

📏 音乐播放器关键参数

参数说明
BPM120每分钟120拍
一拍时长0.5s60/120
八分音符0.25s半拍
四分音符0.5s一拍
八分计数12,500,00050MHz×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编译提示

编译: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数组就能切换歌曲!

🧪 Verilator测试台

SystemVerilogmusic_player_tb.sv片段
// 追踪音符变化
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显示当前音符

📐 节拍定时计算

BPM=120 (每分钟120拍) → 一拍 = 60/120 = 0.5s → 八分音符 = 0.25s → 四分音符 = 0.5s → 二分音符 = 1.0s 50MHz × 0.25s = 12,500,000 计数/八分音符 《小星星》时长: 14个音符 = 6个八分 + 2个四分 = 8个八分音符时长 = 8 × 0.25s = 2.0s 一遍 循环播放 → 无限循环的小星星!