第21课:8位旋律播放器

阶段5:实战项目

本课是第一个实战项目——将前面学到的所有模块整合为8位旋律播放器。这个系统能自动播放预编程的旋律,支持4种波形切换、BPM控制和简单的音量包络。

系统架构

📐 旋律播放器架构图

┌─────────────┐    ┌─────────────┐    ┌──────────────┐
│  节拍时钟    │───→│ 音符序列器  │───→│ 频率查找表   │
│  (BPM)      │    │ (Step/Dur)  │    │ (MIDI→DDS)   │
└─────────────┘    └──────┬──────┘    └──────┬───────┘
                          │                   │
                   trigger+vel          freq_tune
                          │                   │
                          ▼                   ▼
                   ┌─────────────────────────────┐
                   │     波形发生器              │
                   │  (方波/三角/锯齿/噪声)     │
                   └──────────────┬──────────────┘
                                  │
                          ┌───────▼────────┐
                          │  包络 × 波形   │
                          └───────┬────────┘
                                  │
                              audio_out

整合要点

将多个独立模块组合成完整系统时,需要注意:

  1. 信号对齐:各模块的数据和控制信号必须在同一时钟域
  2. 握手协议:序列器的note_trigger信号连接到包络的触发输入
  3. 参数传递:MIDI音符→频率查找→DDS频率的完整链路
  4. 默认状态:各模块在复位后应进入安全状态(静音)

编程一段旋律

让我们用这个播放器编程《小星星》的前两句:

// C C G G A A G - F F E E D D C -
// MIDI: 60 60 67 67 69 69 67  65 65 64 64 62 62 60
step | pitch | vel | dur
  0  |  60   | 200 | 1  (C4)
  1  |  60   | 200 | 1  (C4)
  2  |  67   | 200 | 1  (G4)
  3  |  67   | 200 | 1  (G4)
  4  |  69   | 200 | 1  (A4)
  5  |  69   | 200 | 1  (A4)
  6  |  67   | 200 | 2  (G4, 二分音符)
  7  |  65   | 200 | 1  (F4)
  8  |  65   | 200 | 1  (F4)
  9  |  64   | 200 | 1  (E4)
 10  |  64   | 200 | 1  (E4)
 11  |  62   | 200 | 1  (D4)
 12  |  62   | 200 | 1  (D4)
 13  |  60   | 200 | 2  (C4, 二分音符)

波形选择对旋律的影响

波形适合风格听感
方波50%经典NES旋律温暖、饱满
方波25%明亮领奏尖锐、穿透力强
三角波低音旋律柔和、温暖
锯齿波C64风格旋律明亮、丰满
  1. 实现旋律播放器,编程并播放《小星星》
  2. 实验:同一旋律用4种波形分别播放,对比音色差异
  3. 扩展:添加更完整的频率查找表,支持C2到C7全音域
  4. 挑战:编程一首完整的8小节芯片音乐旋律

旋律演奏家 — 整合波形发生器、序列器、频率查找和包络,实现完整的8位旋律自动播放!第一个实战项目完成!

Verilog 实现

melody_player.v
// melody_player.v - 8位旋律播放器
// 完整的单通道旋律自动播放系统
module melody_player #(
    parameter CLK_FREQ = 50000000,
    parameter BIT_DEPTH = 8,
    parameter PHASE_BITS = 32,
    parameter MAX_NOTES = 64
)(
    input  wire clk,
    input  wire rst_n,
    input  wire play,
    input  wire stop,
    // 波形选择
    input  wire [1:0] wave_select,   // 00=方波,01=三角,10=锯齿,11=噪声
    input  wire [1:0] duty_cycle,    // 方波占空比
    // 旋律数据
    input  wire [6:0] note_pitch [0:MAX_NOTES-1],  // MIDI音符
    input  wire [7:0] note_vel   [0:MAX_NOTES-1],  // 力度
    input  wire [3:0] note_dur   [0:MAX_NOTES-1],  // 时值(节拍)
    input  wire [6:0] song_length,
    // BPM
    input  wire [9:0] bpm,
    // 输出
    output wire [BIT_DEPTH-1:0] audio_out
);
    // ─── 节拍时钟 ───
    wire beat_pulse;
    reg [31:0] beat_counter;
    reg [31:0] beat_period;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            beat_period <= 32'd25000000;
        else begin
            case (bpm)
                10'd120: beat_period <= 32'd25000000;
                10'd140: beat_period <= 32'd21428571;
                10'd100: beat_period <= 32'd30000000;
                default: beat_period <= 32'd25000000;
            endcase
        end
    end
    
    reg beat_reg;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            beat_counter <= 32'd0;
            beat_reg <= 1'b0;
        end else begin
            beat_reg <= 1'b0;
            if (beat_counter >= beat_period - 1) begin
                beat_counter <= 32'd0;
                beat_reg <= 1'b1;
            end else
                beat_counter <= beat_counter + 32'd1;
        end
    end
    assign beat_pulse = beat_reg;
    
    // ─── 音符序列器 ───
    reg [6:0] seq_step;
    reg [3:0] dur_counter;
    reg seq_playing;
    reg note_trigger;
    reg [6:0] current_note;
    reg [7:0] current_vel;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            seq_step <= 7'd0;
            dur_counter <= 4'd0;
            seq_playing <= 1'b0;
            note_trigger <= 1'b0;
            current_note <= 7'd0;
            current_vel <= 8'd0;
        end else begin
            note_trigger <= 1'b0;
            
            if (play && !seq_playing) begin
                seq_playing <= 1'b1;
                seq_step <= 7'd0;
                dur_counter <= 4'd0;
                note_trigger <= 1'b1;
                current_note <= note_pitch[0];
                current_vel <= note_vel[0];
            end else if (stop) begin
                seq_playing <= 1'b0;
            end else if (seq_playing && beat_pulse) begin
                if (dur_counter >= note_dur[seq_step] - 1) begin
                    dur_counter <= 4'd0;
                    if (seq_step >= song_length - 1)
                        seq_step <= 7'd0;
                    else
                        seq_step <= seq_step + 7'd1;
                    note_trigger <= 1'b1;
                    current_note <= note_pitch[seq_step];
                    current_vel <= note_vel[seq_step];
                end else begin
                    dur_counter <= dur_counter + 4'd1;
                end
            end
        end
    end
    
    // ─── 频率查找 ───
    reg [PHASE_BITS-1:0] freq_tune;
    always @(*) begin
        case (current_note)
            7'd48: freq_tune = 32'd5612;   // C3
            7'd50: freq_tune = 32'd6296;   // D3
            7'd52: freq_tune = 32'd7063;   // E3
            7'd53: freq_tune = 32'd7481;   // F3
            7'd55: freq_tune = 32'd8393;   // G3
            7'd57: freq_tune = 32'd9416;   // A3
            7'd59: freq_tune = 32'd10654;  // B3
            7'd60: freq_tune = 32'd11284;  // C4
            7'd62: freq_tune = 32'd12657;  // D4
            7'd64: freq_tune = 32'd14197;  // E4
            7'd65: freq_tune = 32'd15037;  // F4
            7'd67: freq_tune = 32'd16870;  // G4
            7'd69: freq_tune = 32'd18928;  // A4
            7'd71: freq_tune = 32'd21236;  // B4
            7'd72: freq_tune = 32'd22491;  // C5
            default: freq_tune = 32'd11284;
        endcase
    end
    
    // ─── 波形发生器 ───
    reg [PHASE_BITS-1:0] wave_phase;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            wave_phase <= {PHASE_BITS{1'b0}};
        else
            wave_phase <= wave_phase + freq_tune;
    end
    
    reg [BIT_DEPTH-1:0] wave_raw;
    always @(*) begin
        case (wave_select)
            2'b00: begin // 方波
                case (duty_cycle)
                    2'b00: wave_raw = (wave_phase[31] ^ wave_phase[30]) ? 8'd255 : 8'd0;
                    2'b01: wave_raw = (wave_phase[31:30] == 2'b00) ? 8'd255 : 8'd0;
                    2'b10: wave_raw = wave_phase[31] ? 8'd0 : 8'd255;
                    2'b11: wave_raw = (wave_phase[31:30] != 2'b11) ? 8'd255 : 8'd0;
                endcase
            end
            2'b01: begin // 三角波
                wave_raw = wave_phase[31] ? 
                    ~wave_phase[30:23] : wave_phase[30:23];
            end
            2'b10: wave_raw = wave_phase[31:24]; // 锯齿波
            2'b11: wave_raw = {wave_phase[31], wave_phase[29], 
                               wave_phase[27], wave_phase[25],
                               wave_phase[23], wave_phase[21],
                               wave_phase[19], wave_phase[17]}; // 伪噪声
            default: wave_raw = 8'd0;
        endcase
    end
    
    // ─── 简单包络 ───
    reg [7:0] env_level;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            env_level <= 8'd0;
        else if (note_trigger)
            env_level <= current_vel;
        else if (env_level > 8'd1)
            env_level <= env_level - 8'd1;
    end
    
    assign audio_out = (wave_raw * env_level) >> 8;
endmodule

✅ Verilator验证通过

旋律数据编码方案

在Verilog中编码旋律数据有多种方案:

🔧 三种编码方案对比

方案存储方式优点缺点
直接数组parameter/reg数组最简单修改需重新综合
ROM初始化$readmemh从文件加载数据与代码分离仿真专用,综合不支持
BRAM+CPU写入双端口RAM运行时可修改需要CPU接口

我们的实现使用直接数组方案——最简单,适合固定旋律。如果需要运行时修改,可以升级为BRAM方案。

《小星星》完整编曲

让我们为《小星星》编写完整的3通道编曲:

// 旋律 (方波50%): C C G G A A G - F F E E D D C
// 低音 (三角波):   C - G - F - C - C - G - C
// 鼓:              K- S- K- S- (基本4/4)

// 旋律通道数据
note_pitch = '{60,60,67,67,69,69,67,65,65,64,64,62,62,60};
note_vel   = '{200,200,200,200,200,200,220,200,200,200,200,200,200,220};
note_dur   = '{1,1,1,1,1,1,2,1,1,1,1,1,1,2};

旋律变奏技巧

同一旋律通过不同处理产生变奏:

旋律数据的Verilog编码技巧

在Verilog中高效编码旋律数据:

🔧 紧凑编码方案

// 方案:使用RLE(游程编码)压缩重复
// 很多旋律有大量重复(同音连续、同节奏型)

// 原始: C4 C4 C4 D4 E4 E4 G4
// RLE:  C4×3 D4×1 E4×2 G4×1
// → 数据量减少约50%

// Verilog实现
reg [6:0] rle_note [0:31];
reg [3:0] rle_count [0:31];
reg [4:0] rle_ptr;
reg [3:0] rle_remaining;

always @(posedge clk) begin
    if (rle_remaining == 0) begin
        rle_ptr <= rle_ptr + 1;
        rle_remaining <= rle_count[rle_ptr + 1];
        current_note <= rle_note[rle_ptr + 1];
    end else begin
        rle_remaining <= rle_remaining - 1;
    end
end

旋律的数学结构

许多经典旋律有隐含的数学结构: