本课是第一个实战项目——将前面学到的所有模块整合为8位旋律播放器。这个系统能自动播放预编程的旋律,支持4种波形切换、BPM控制和简单的音量包络。
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ 节拍时钟 │───→│ 音符序列器 │───→│ 频率查找表 │
│ (BPM) │ │ (Step/Dur) │ │ (MIDI→DDS) │
└─────────────┘ └──────┬──────┘ └──────┬───────┘
│ │
trigger+vel freq_tune
│ │
▼ ▼
┌─────────────────────────────┐
│ 波形发生器 │
│ (方波/三角/锯齿/噪声) │
└──────────────┬──────────────┘
│
┌───────▼────────┐
│ 包络 × 波形 │
└───────┬────────┘
│
audio_out
将多个独立模块组合成完整系统时,需要注意:
让我们用这个播放器编程《小星星》的前两句:
// 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风格旋律 | 明亮、丰满 |
旋律演奏家 — 整合波形发生器、序列器、频率查找和包络,实现完整的8位旋律自动播放!第一个实战项目完成!
// 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中高效编码旋律数据:
// 方案:使用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
许多经典旋律有隐含的数学结构: