🏠 课程总览 > 第11课
第11课: 音乐播放
小星星旋律播放,音符序列+自动推进
🏆 小星星旋律正确
✅ Verilator仿真验证通过
📖 核心概念
- 音符编码:C4=262Hz, D4=294, E4=330, F4=349, G4=392, A4=440。每个音符对应一个相位步进值
- 旋律序列:小星星=CCGGAAG-FFEEDDC,14个音符。melody数组存储音符索引
- 自动推进:每个音符持续note_duration个时钟周期后自动切换到下一个音符
💡 关键思路:本课的核心是音符编码——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输出会拖慢仿真速度,验证通过后可以减少打印频率