音符序列器 + 节拍定时 + 多声道 — 用FPGA播放完整旋律!
🏆 成就:旋律编织者 ✅ Verilator验证通过
背景音乐播放的核心是序列器——按照节拍从ROM中读取音符,转换成频率,驱动NCO产生音频。整个流程完全硬件化,零CPU开销!
// 音符序列器 - 从ROM读取音符序列并播放
module music_sequencer #(
parameter CLK_FREQ = 50000000,
parameter BPM = 120 // 每分钟节拍数
)(
input wire clk,
input wire rst,
input wire play, // 1=播放
output wire [7:0] audio_out, // 音频输出
output wire [6:0] note_index // 当前音符序号(调试用)
);
// 节拍定时: BPM=120 → 每拍0.5s → 25000000个时钟周期
localparam BEAT_TICKS = CLK_FREQ * 60 / BPM / 2;
reg [24:0] beat_counter;
reg beat_trigger;
always @(posedge clk) begin
if (rst) begin
beat_counter <= 0;
beat_trigger <= 0;
end else begin
beat_trigger <= 0;
if (play && beat_counter >= BEAT_TICKS[24:0]) begin
beat_counter <= 0;
beat_trigger <= 1;
end else begin
beat_counter <= beat_counter + 1;
end
end
end
// 音符ROM: [7:4]=MIDI音符号(0~15映射C4~), [3:0]=时值(拍)
// 小星星旋律: C C G G A A G - F F E E D D C
reg [7:0] note_rom [0:31];
initial begin
// 小星星 (Twinkle Twinkle)
note_rom[ 0] = 8'h18; // C4, 1拍
note_rom[ 1] = 8'h18; // C4, 1拍
note_rom[ 2] = 8'h28; // G4, 1拍
note_rom[ 3] = 8'h28; // G4, 1拍
note_rom[ 4] = 8'h30; // A4, 1拍
note_rom[ 5] = 8'h30; // A4, 1拍
note_rom[ 6] = 8'h40; // G4, 2拍
note_rom[ 7] = 8'h24; // F4, 1拍
note_rom[ 8] = 8'h24; // F4, 1拍
note_rom[ 9] = 8'h14; // E4, 1拍
note_rom[10] = 8'h14; // E4, 1拍
note_rom[11] = 8'h0C; // D4, 1拍
note_rom[12] = 8'h0C; // D4, 1拍
note_rom[13] = 8'h08; // C4, 2拍
// 重复
note_rom[14] = 8'h18; note_rom[15] = 8'h18;
note_rom[16] = 8'h28; note_rom[17] = 8'h28;
note_rom[18] = 8'h30; note_rom[19] = 8'h30;
note_rom[20] = 8'h40; note_rom[21] = 8'h24;
note_rom[22] = 8'h24; note_rom[23] = 8'h14;
note_rom[24] = 8'h14; note_rom[25] = 8'h0C;
note_rom[26] = 8'h0C; note_rom[27] = 8'h08;
note_rom[28] = 8'h00; // 结束标记
note_rom[29] = 8'h00; note_rom[30] = 8'h00; note_rom[31] = 8'h00;
end
// 序列播放
reg [4:0] seq_pos;
reg [3:0] note_duration;
reg [3:0] duration_cnt;
reg [7:0] current_note;
always @(posedge clk) begin
if (rst) begin
seq_pos <= 0; duration_cnt <= 0;
current_note <= 0;
end else if (beat_trigger) begin
if (duration_cnt == 0) begin
current_note <= note_rom[seq_pos];
note_duration <= note_rom[seq_pos][3:0];
duration_cnt <= note_rom[seq_pos][3:0];
if (seq_pos < 27) seq_pos <= seq_pos + 1;
else seq_pos <= 0;
end else begin
duration_cnt <= duration_cnt - 1;
end
end
end
// MIDI音符→频率查找表
reg [19:0] freq_word;
always @(*) begin
case(current_note[7:4])
4'h0: freq_word = 20'd0; // 休止
4'h1: freq_word = 20'd343; // C4 ≈ 262Hz
4'h2: freq_word = 20'd0; // (reserved)
4'h3: freq_word = 20'd431; // D4 ≈ 294Hz
4'h4: freq_word = 20'h0; // reserved
4'h5: freq_word = 20'd513; // E4 ≈ 330Hz
4'h6: freq_word = 20'h0;
4'h7: freq_word = 20'd575; // F4 ≈ 349Hz
4'h8: freq_word = 20'h0;
4'h9: freq_word = 20'd645; // G4 ≈ 392Hz
4'hA: freq_word = 20'h0;
4'hB: freq_word = 20'd724; // A4 ≈ 440Hz
4'hC: freq_word = 20'h0;
4'hD: freq_word = 20'd813; // B4 ≈ 494Hz
default: freq_word = 20'd0;
endcase
end
// NCO生成音频
reg [15:0] phase;
always @(posedge clk) begin
if (rst) phase <= 0;
else phase <= phase + freq_word[15:0];
end
// 简单正弦近似
reg [7:0] sine_val;
always @(*) begin
case(phase[15:14])
2'b00: sine_val = phase[13:6];
2'b01: sine_val = 255 - phase[13:6];
2'b10: sine_val = 255 - phase[13:6];
2'b11: sine_val = phase[13:6];
endcase
end
assign audio_out = (play && current_note[7:4] != 0) ? sine_val : 8'd128;
assign note_index = seq_pos;
endmodulemodule music_sequencer_tb;
logic clk=0, rst=1, play=0;
logic [7:0] audio_out;
logic [6:0] note_index;
music_sequencer #(.CLK_FREQ(1000), .BPM(120)) uut(.*);
always #5 clk = ~clk; // 快速仿真
initial begin
rst=1; #50; rst=0;
play=1;
$display("--- 音乐序列器测试 ---");
// 快速仿真: 等几个beat
repeat(100) begin
@(posedge clk);
end
$display(" note_index=%0d audio=%0d", note_index, audio_out);
play=0; #100;
$display("音乐序列器测试完成 ✓");
$finish;
end
endmodule| 音符 | MIDI# | 频率(Hz) | NCO频率字(16bit@50M) |
|---|---|---|---|
| C4 | 60 | 261.6 | 343 |
| D4 | 62 | 293.7 | 385 |
| E4 | 64 | 329.6 | 432 |
| F4 | 65 | 349.2 | 458 |
| G4 | 67 | 392.0 | 514 |
| A4 | 69 | 440.0 | 576 |
| B4 | 71 | 493.9 | 647 |
| C5 | 72 | 523.3 | 686 |
💡 存储优化:8位编码[7:4]=音高+[3:0]=时值,一个字节存一个音符。32字节就能存一段完整旋律!用BRAM可以存储上千个音符的乐谱。
练习1:编写《欢乐颂》的音符ROM数据
练习2:实现双声道:旋律+低音伴奏同时播放
练习3:添加循环播放功能,歌曲结束自动重头
练习4:实现速度可调(BPM参数化)
练习5:用PS/2键盘实时切换不同曲目
步骤1:verilator --lint-only music_sequencer.v
步骤2:verilator --binary -j 0 music_sequencer.v music_sequencer_tb.sv
步骤3:./obj_dir/Vmusic_sequencer_tb
以下是几首经典旋律的ROM数据,可以直接替换使用:
《欢乐颂》(Ode to Joy):E E F G G F E D C C D E E D D
MIDI: 64 64 65 67 67 65 64 62 60 60 62 64 64 62 62
《生日快乐》:C C D C F E | C C D C G F
《两只老虎》:C D E C | C D E C | E F G | E F G
每行一个乐句,竖线分隔小节。用我们的8位编码[7:4]=音高+[3:0]=时值即可编码!
声道1(旋律):高音区,主旋律线,时值多变
声道2(低音):低音区,根音+五度,每拍1-2个音
声道3(和弦):中音区,长音和弦垫底,时值4拍
声道4(节奏):噪声通道,打击乐节拍
4个独立序列器,共享节拍时钟,各自ROM存储音符序列。混音器合并4路输出!
要让音乐好听,需要理解音程——两个音之间的距离。半音=最小单位,全音=两个半音。C到D是全音,E到F是半音。
大三和弦:根音+大三度+纯五度(如C-E-G),明亮开朗
小三和弦:根音+小三度+纯五度(如C-Eb-G),忧郁柔和
七和弦:三和弦+小七度(如C-E-G-Bb),爵士风味
FPGA实现:3个NCO同时发声,频率按和弦比例设置。C大三和弦=262+330+392Hz。