🏠 课程总览 > 第17课
第17课: MIDI播放
音符序列ROM+MIDI消息解码+小星星旋律
🏆 播放MIDI文件音符序列
✅ Verilator仿真验证通过
📖 核心概念
- MIDI消息:Note On(0x9n)/Note Off(0x8n)+音符号+力度
- 音符序列ROM:{8bit音符, 8bit时值}格式存储旋律
- MIDI音符号:60=C4(中央C), 67=G4, 69=A4
- 时值编码:以1/16拍为单位,8=八分音符,16=四分音符
- 小星星:C C G G A A G- F F E E D D C- (14个音符)
💡 关键思路:本课的核心是MIDI消息——Note On(0x9n)/Note Off(0x8n)+音符号+力度。
💻 Verilog设计代码
设计模块源码——这是你真正要理解的硬件逻辑:
// 第17课: MIDI播放
module midi_player (
input wire clk, input wire rst_n, input wire play,
output reg [7:0] note, output reg note_on, output reg note_off,
output reg [7:0] velocity, output reg [6:0] tempo, output reg playing
);
reg [15:0] note_freq [0:11];
initial begin note_freq[0]=262;note_freq[1]=277;note_freq[2]=294;note_freq[3]=311;note_freq[4]=330;note_freq[5]=349;note_freq[6]=370;note_freq[7]=392;note_freq[8]=415;note_freq[9]=440;note_freq[10]=466;note_freq[11]=494; end
reg [15:0] seq_rom [0:31]; integer i;
initial begin
seq_rom[0]={8'd60,8'd8};seq_rom[1]={8'd60,8'd8};seq_rom[2]={8'd67,8'd8};seq_rom[3]={8'd67,8'd8};
seq_rom[4]={8'd69,8'd8};seq_rom[5]={8'd69,8'd8};seq_rom[6]={8'd67,8'd16};seq_rom[7]={8'd65,8'd8};
seq_rom[8]={8'd65,8'd8};seq_rom[9]={8'd64,8'd8};seq_rom[10]={8'd64,8'd8};seq_rom[11]={8'd62,8'd8};
seq_rom[12]={8'd62,8'd8};seq_rom[13]={8'd60,8'd16};
for(i=14;i<32;i=i+1) seq_rom[i]=16'h0000;
end
reg [4:0] seq_idx; reg [7:0] duration_cnt, cur_duration;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin note<=0;note_on<=0;note_off<=0;velocity<=0;tempo<=60;playing<=0;seq_idx<=0;duration_cnt<=0;cur_duration<=0; end
else begin note_on<=0;note_off<=0;
if(play&&!playing) begin playing<=1;seq_idx<=0;duration_cnt<=0; end
if(playing) begin
if(duration_cnt==0) begin note<=seq_rom[seq_idx][15:8];cur_duration<=seq_rom[seq_idx][7:0];velocity<=100;note_on<=1;duration_cnt<=1; end
else if(duration_cnt>=cur_duration) begin note_off<=1;duration_cnt<=0;seq_idx<=seq_idx+1;
if(seq_idx>=13||seq_rom[seq_idx+1]==16'h0000) playing<=0; end
else duration_cnt<=duration_cnt+1;
end
end
end
endmodule
🧪 测试平台(Testbench)
testbench = 你的"手柄+屏幕",模拟输入、验证输出:
/* verilator lint_off WIDTHEXPAND */
/* verilator lint_off WIDTHTRUNC */
/* verilator lint_off UNOPTFLAT */
module tb;
reg clk,rst_n,play; wire [7:0] note,velocity; wire note_on,note_off; wire [6:0] tempo; wire playing;
midi_player uut (.clk(clk),.rst_n(rst_n),.play(play),.note(note),.note_on(note_on),.note_off(note_off),.velocity(velocity),.tempo(tempo),.playing(playing));
always clk = #10 ~clk;
integer note_count; reg [7:0] played_notes [0:15];
initial begin
$dumpfile("midi.vcd");$dumpvars(0,tb);clk=0;rst_n=0;play=0;note_count=0;
repeat(5)@(posedge clk);rst_n=1;
$display("=== MIDI播放仿真 ===");$display("播放MIDI文件音符序列");$display("曲目: 小星星(Twinkle Twinkle Little Star)");$display("");
$display("--- MIDI消息格式 ---");$display(" Note On: 0x9n note velocity");$display(" Note Off: 0x8n note velocity");$display(" Tempo: 0xFF 0x51 0x03 tt tt tt");$display("");
$display("--- 开始播放 ---");play=1;@(posedge clk);play=0;
while(playing&¬e_count<15) begin @(posedge clk);
if(note_on) begin played_notes[note_count]=note;$display(" 🎵 Note On: MIDI#%0d (时值=%0d)",note,uut.cur_duration);note_count=note_count+1;end
if(note_off) $display(" 🎵 Note Off: MIDI#%0d",note);
end
repeat(50)@(posedge clk);
$display("");$display("--- 播放统计 ---");$display(" 总音符数: %0d",note_count);
$display(" 播放音符序列:");
begin integer j;$write(" ");for(j=0;j<note_count;j=j+1) begin case(played_notes[j]%12) 0:$write("C4 ");1:$write("C#4 ");2:$write("D4 ");3:$write("D#4 ");4:$write("E4 ");5:$write("F4 ");6:$write("F#4 ");7:$write("G4 ");8:$write("G#4 ");9:$write("A4 ");10:$write("A#4 ");11:$write("B4 ");default:$write("? ");endcase end end
$display("");
if(note_count>=13) begin $display(" ✅ 播放了完整的音符序列");
if(played_notes[0]==60&&played_notes[2]==67&&played_notes[4]==69) $display(" ✅ 小星星旋律正确: C C G G A A..."); end
$display("");$display("✅ 播放MIDI文件音符序列验证通过!");$display("🏆 成就解锁: 播放MIDI文件音符序列!");$finish;
end
endmodule
✅ 仿真输出
运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:
=== MIDI播放仿真 ===
播放MIDI文件音符序列
曲目: 小星星(Twinkle Twinkle Little Star)
--- MIDI消息格式 ---
Note On: 0x9n note velocity
Note Off: 0x8n note velocity
Tempo: 0xFF 0x51 0x03 tt tt tt
--- 开始播放 ---
🎵 Note On: MIDI#60 (时值=8)
🎵 Note Off: MIDI#60
🎵 Note On: MIDI#60 (时值=8)
🎵 Note Off: MIDI#60
🎵 Note On: MIDI#67 (时值=8)
🎵 Note Off: MIDI#67
🎵 Note On: MIDI#67 (时值=8)
🎵 Note Off: MIDI#67
🎵 Note On: MIDI#69 (时值=8)
🎵 Note Off: MIDI#69
🎵 Note On: MIDI#69 (时值=8)
🎵 Note Off: MIDI#69
🎵 Note On: MIDI#67 (时值=16)
🎵 Note Off: MIDI#67
🎵 Note On: MIDI#65 (时值=8)
🎵 Note Off: MIDI#65
🎵 Note On: MIDI#65 (时值=8)
🎵 Note Off: MIDI#65
🎵 Note On: MIDI#64 (时值=8)
🎵 Note Off: MIDI#64
🎵 Note On: MIDI#64 (时值=8)
🎵 Note Off: MIDI#64
🎵 Note On: MIDI#62 (时值=8)
🎵 Note Off: MIDI#62
🎵 Note On: MIDI#62 (时值=8)
🎵 Note Off: MIDI#62
🎵 Note On: MIDI#60 (时值=16)
🎵 Note Off: MIDI#60
--- 播放统计 ---
总音符数: 14
播放音符序列:
C4 C4 G4 G4 A4 A4 G4 F4 F4 E4 E4 D4 D4 C4
✅ 播放了完整的音符序列
✅ 小星星旋律正确: C C G G A A...
✅ 播放MIDI文件音符序列验证通过!
🏆 成就解锁: 播放MIDI文件音符序列!
- tb.sv:26: 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-BLKSEQ
# 运行
./obj_dir/sim
# 查看波形(可选)
gtkwave sim.vcd
🎮 实战步骤
1
定义音符序列:seq_rom存储{音符,时值}对。小星星旋律:C4(60) C4 G4(67) G4 A4(69) A4 G4-(长) F4(65) F4 E4(64) E4 D4(62) D4 C4-(长)。
2
播放状态机:duration_cnt==0时加载新音符,note_on脉冲触发声音。duration_cnt递增直到等于cur_duration时,note_off脉冲结束音符。
3
序列推进:每个音符结束后seq_idx递增。当seq_idx到达序列末尾或遇到0x0000时,playing清零,播放结束。
4
扩展思路:添加多轨道(track)支持→和弦。添加velocity控制→力度变化。添加pitch_bend→滑音效果。
🎵 MIDI与音乐理论
十二平均律:一个八度等分12个半音,相邻半音频率比为2^(1/12)≈1.0595。A4=440Hz,A5=880Hz。
小星星乐谱:1 1 5 5 | 6 6 5 - | 4 4 3 3 | 2 2 1 - | 这是C大调,全在白键上,是最简单的钢琴入门曲之一。
MIDI文件结构:MIDI文件由Header Chunk + Track Chunk组成。Header定义格式(0/1/2)和轨道数,Track包含事件序列(delta-time + event)。
🏆
播放MIDI文件音符序列
✅ Verilator仿真验证通过
🧠 知识扩展
MIDI协议:MIDI(Musical Instrument Digital Interface)是电子音乐标准协议。每个消息由状态字节+数据字节组成。Note On=0x9n,Note Off=0x8n,n=通道号(0-15)。
音符频率:A4=440Hz是国际标准音高。每升高一个半音,频率乘以2^(1/12)≈1.0595。C4=262Hz,中央C是钢琴键盘的中间位置。
⚡ 性能提示
• 使用--trace选项生成VCD波形文件,用GTKWave查看
• 使用-j 4选项并行编译,加快构建速度
• 使用--build选项让Verilator自动调用make
• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率