🎶 第20课:背景音乐播放

音符序列器 + 节拍定时 + 多声道 — 用FPGA播放完整旋律!

🏆 成就:旋律编织者 ✅ Verilator验证通过

🎶
旋律编织者
音符序列ROM + 节拍器 + 双声道旋律

🎶 音符序列器原理

背景音乐播放的核心是序列器——按照节拍从ROM中读取音符,转换成频率,驱动NCO产生音频。整个流程完全硬件化,零CPU开销!

音乐播放系统架构 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 音符ROM │──→│ 序列器 │──→│ NCO振荡 │──→ PWM/ DAC │ 小星星 │ │ 节拍定时 │ │ 正弦波 │ → 扬声器 └──────────┘ └──────────┘ └──────────┘ ↑ ↑ ↑ 音符+时值 beat_trigger freq_word [7:4]音高 每拍触发 MIDI→频率 [3:0]时值 双声道: 两个独立序列器 Ch1=旋律, Ch2=低音伴奏 输出 = (Ch1 + Ch2) / 2

📐 音符序列器实现

Verilogmusic_sequencer.v — 完整音乐播放器
// 音符序列器 - 从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;

endmodule

🧪 序列器测试台

SystemVerilogmusic_sequencer_tb.sv — 序列器测试
module 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音符→频率对照

音符MIDI#频率(Hz)NCO频率字(16bit@50M)
C460261.6343
D462293.7385
E464329.6432
F465349.2458
G467392.0514
A469440.0576
B471493.9647
C572523.3686

💡 存储优化:8位编码[7:4]=音高+[3:0]=时值,一个字节存一个音符。32字节就能存一段完整旋律!用BRAM可以存储上千个音符的乐谱。

🏋️ 练习

练习1:编写《欢乐颂》的音符ROM数据

练习2:实现双声道:旋律+低音伴奏同时播放

练习3:添加循环播放功能,歌曲结束自动重头

练习4:实现速度可调(BPM参数化)

练习5:用PS/2键盘实时切换不同曲目

🎶
旋律编织者
完成本课练习,掌握FPGA音乐播放!

🔧 Verilator验证步骤

步骤1verilator --lint-only music_sequencer.v

步骤2verilator --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。