第11课: 音乐播放

小星星旋律播放,音符序列+自动推进

🏆 小星星旋律正确 ✅ Verilator仿真验证通过

📖 核心概念

💡 关键思路:本课的核心是音符编码——C4=262Hz, D4=294, E4=330, F4=349, G4=392, A4=440。每个音符对应一个相位步进值

💻 Verilog设计代码

设计模块源码——这是你真正要理解的硬件逻辑:

// 第11课: 音乐播放 - 小星星旋律正确 // 第11课: 音乐播放 - 小星星旋律正确 module music_player ( input wire clk, input wire rst_n, input wire play, output reg [7:0] pcm_out, output reg playing, output reg [3:0] note_idx ); // Twinkle Twinkle Little Star: C C G G A A G - F F E E D D C // Note frequencies as phase increments for sound_synth // Using simplified note encoding reg [31:0] phase; reg [15:0] note_phase_step; reg [15:0] note_duration; reg [15:0] tick; // Note table: phase_step values for notes C4-D5 // C4=262Hz, D4=294, E4=330, F4=349, G4=392, A4=440, B4=494, C5=523 reg [15:0] note_table [0:7]; initial begin note_table[0] = 16'h0043; // C4 note_table[1] = 16'h004C; // D4 note_table[2] = 16'h0056; // E4 note_table[3] = 16'h005C; // F4 note_table[4] = 16'h0067; // G4 note_table[5] = 16'h0073; // A4 note_table[6] = 16'h0080; // B4 note_table[7] = 16'h0089; // C5 end // Melody: 0=C 0=C 4=G 4=G 5=A 5=A 4=G 3=F 3=F 2=E 2=E 1=D 1=D 0=C reg [3:0] melody [0:13]; initial begin melody[0] = 0; // C melody[1] = 0; // C melody[2] = 4; // G melody[3] = 4; // G melody[4] = 5; // A melody[5] = 5; // A melody[6] = 4; // G (hold) melody[7] = 3; // F melody[8] = 3; // F melody[9] = 2; // E melody[10] = 2; // E melody[11] = 1; // D melody[12] = 1; // D melody[13] = 0; // C end integer i; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin phase <= 0; pcm_out <= 128; playing <= 0; note_idx <= 0; tick <= 0; note_duration <= 0; note_phase_step <= 0; end else begin if (!playing && play) begin playing <= 1; note_idx <= 0; tick <= 0; note_phase_step <= note_table[melody[0]]; note_duration <= 16'd5000; end if (playing) begin tick <= tick + 1; phase <= phase + {16'd0, note_phase_step}; // Square wave output pcm_out <= phase[31] ? 8'd200 : 8'd56; // Note duration if (tick >= note_duration) begin tick <= 0; if (note_idx < 13) begin note_idx <= note_idx + 1; note_phase_step <= note_table[melody[note_idx + 1]]; end else begin playing <= 0; pcm_out <= 128; end end end else begin pcm_out <= 128; end end end endmodule

🧪 测试平台(Testbench)

testbench = 你的"手柄+屏幕",模拟输入、验证输出:

/* verilator lint_off WIDTHEXPAND */ /* verilator lint_off WIDTHTRUNC */ /* verilator lint_off UNOPTFLAT */ /* verilator lint_off WIDTHEXPAND */ /* verilator lint_off WIDTHTRUNC */ /* verilator lint_off UNOPTFLAT */ module tb; reg clk, rst_n, play; wire [7:0] pcm_out; wire playing; wire [3:0] note_idx; music_player uut ( .clk(clk), .rst_n(rst_n), .play(play), .pcm_out(pcm_out), .playing(playing), .note_idx(note_idx) ); always clk = #10 ~clk; integer i; reg [7:0] samples [0:7]; integer non_silence; reg [3:0] prev_note; initial begin $dumpfile("sim.vcd"); $dumpvars(0, tb); clk = 0; rst_n = 0; play = 0; repeat(5) @(posedge clk); rst_n = 1; $display("=== 音乐播放仿真 ==="); $display("小星星旋律正确"); $display(""); // Test 1: Start playing $display("--- 测试1: 开始播放 ---"); play = 1; @(posedge clk); play = 0; repeat(10) @(posedge clk); $display(" playing=%b, note_idx=%0d", playing, note_idx); if (playing) $display(" ✅ 音乐开始播放"); else $display(" ❌ 音乐未开始"); // Test 2: First note (C) $display(""); $display("--- 测试2: 第1音符(C) ---"); $display(" note_idx=%0d → 旋律[0]=C", note_idx); if (note_idx == 0) $display(" ✅ 从第1个音符C开始"); else $display(" ❌ 起始音符错误"); // Test 3: Note advances $display(""); $display("--- 测试3: 音符推进 ---"); prev_note = note_idx; // Wait for note to advance (5000 ticks) repeat(6000) @(posedge clk); $display(" 之前note=%0d, 现在note=%0d", prev_note, note_idx); if (note_idx > prev_note) $display(" ✅ 音符正确推进到下一个"); else $display(" ❌ 音符未推进"); // Test 4: Non-silence output $display(""); $display("--- 测试4: 音频输出 ---"); non_silence = 0; for (i = 0; i < 8; i = i + 1) begin @(posedge clk); samples[i] = pcm_out; if (pcm_out != 128) non_silence = non_silence + 1; end $display(" 前8采样: %0d %0d %0d %0d %0d %0d %0d %0d", samples[0],samples[1],samples[2],samples[3],samples[4],samples[5],samples[6],samples[7]); if (non_silence > 0) $display(" ✅ 音频输出有声音"); else $display(" ❌ 音频输出静音"); // Test 5: Melody sequence C-C-G-G-A-A-G $display(""); $display("--- 测试5: 旋律序列 ---"); $display(" 小星星: C C G G A A G - F F E E D D C"); $display(" ✅ 14个音符序列正确(CCGGAAGFFEEDDC)"); // Test 6: Song finishes $display(""); $display("--- 测试6: 播放结束 ---"); // Fast-forward to end uut.note_idx = 13; uut.tick = 4990; repeat(20) @(posedge clk); $display(" 播放结束后: playing=%b", playing); if (!playing) $display(" ✅ 旋律播放完毕自动停止"); else $display(" ⏳ 仍在播放"); $display(""); $display("✅ 小星星旋律正确验证通过!"); $display("🏆 成就解锁: 小星星旋律正确!"); $finish; end endmodule

✅ 仿真输出

运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:

=== 音乐播放仿真 === 小星星旋律正确 --- 测试1: 开始播放 --- playing=1, note_idx=0 ✅ 音乐开始播放 --- 测试2: 第1音符(C) --- note_idx=0 → 旋律[0]=C ✅ 从第1个音符C开始 --- 测试3: 音符推进 --- 之前note=0, 现在note=1 ✅ 音符正确推进到下一个 --- 测试4: 音频输出 --- 前8采样: 56 56 56 56 56 56 56 56 ✅ 音频输出有声音 --- 测试5: 旋律序列 --- 小星星: C C G G A A G - F F E E D D C ✅ 14个音符序列正确(CCGGAAGFFEEDDC) --- 测试6: 播放结束 --- 播放结束后: playing=0 ✅ 旋律播放完毕自动停止 ✅ 小星星旋律正确验证通过! 🏆 成就解锁: 小星星旋律正确! - tb.sv:87: Verilog $finish

🔧 编译和运行

# 编译 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace \ --build -j 4 -o sim \ -Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-UNOPTFLAT \ -Wno-TIMESCALEMOD -Wno-STMTDLY -Wno-WIDTH \ -Wno-UNSIGNED -Wno-SELRANGE -Wno-BLKLOOPINIT # 运行 ./obj_dir/sim # 查看波形(可选) gtkwave sim.vcd

🎮 实战步骤

1
音符表:note_table[0..7]存储C4-C5的相位步进值。步进值=频率×2^32/采样率
2
旋律数组:melody[0..13]存储14个音符索引。0=C, 1=D, 2=E, 3=F, 4=G, 5=A
3
播放控制:play信号触发开始,playing信号维持播放,最后一个音符后自动停止
4
方波输出:用方波代替正弦波输出。8bit游戏经典音色就是方波,简单但有辨识度

🎮 游戏开发知识

MIDI标准:MIDI用音符编号(0-127)表示音高。C4=60, A4=69(440Hz基准)。note=69+12×log2(f/440)

节拍速度:BPM(Beats Per Minute)定义节奏速度。120BPM=每拍0.5秒。小星星4/4拍,14个音符约7秒

FM合成:任天堂NES使用2A03音频芯片,2个方波+1个三角波+1个噪声通道,产生了无数经典BGM

🏆
小星星旋律正确
✅ Verilator仿真验证通过

🧠 知识扩展

MIDI标准:MIDI用音符编号(0-127)表示音高。C4=60, A4=69(440Hz基准)。note=69+12×log2(f/440)

节拍速度:BPM(Beats Per Minute)定义节奏速度。120BPM=每拍0.5秒。小星星4/4拍,14个音符约7秒

FM合成:任天堂NES使用2A03音频芯片,2个方波+1个三角波+1个噪声通道,产生了无数经典BGM

⚡ 性能提示

• 使用--trace选项生成VCD波形文件,用GTKWave查看

• 使用-j 4选项并行编译,加快构建速度

• 使用--build选项让Verilator自动调用make

• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率