第17课: MIDI播放

音符序列ROM+MIDI消息解码+小星星旋律

🏆 播放MIDI文件音符序列 ✅ Verilator仿真验证通过

📖 核心概念

💡 关键思路:本课的核心是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&&note_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输出会拖慢仿真速度,验证通过后可以减少打印频率