到目前为止,我们通过手动计算freq_tune值来控制频率。本课将实现音符-频率查找表,让模块直接接受MIDI音符编号,自动转换为DDS频率调谐字。这是从"波形发生器"到"音乐合成器"的关键一步。
MIDI(Musical Instrument Digital Interface)使用0-127的编号来表示音高:
| MIDI# | 音符 | 频率(Hz) | 说明 |
|---|---|---|---|
| 21 | A0 | 27.50 | 钢琴最低A |
| 36 | C2 | 65.41 | 低音C |
| 48 | C3 | 130.81 | 低音区起点 |
| 60 | C4 | 261.63 | 中央C |
| 69 | A4 | 440.00 | 标准音(调音基准) |
| 84 | C6 | 1046.50 | 高音C |
| 108 | C8 | 4186.01 | 钢琴最高C |
给定MIDI音符编号n,其频率为:
f(n) = 440 × 2^((n-69)/12)
这是十二平均律(12-TET)的公式。其中69是A4的MIDI编号,440Hz是A4的标准频率。
转换为DDS频率调谐字:
freq_tune(n) = round(f(n) × 2^PHASE_BITS / CLK_FREQ)
32位相位,50MHz时钟:
// C4 (MIDI 60)
f = 440 × 2^((60-69)/12) = 440 × 2^(-0.75) = 261.63Hz
freq_tune = 261.63 × 2^32 / 50000000 = 22491
// A4 (MIDI 69)
f = 440Hz
freq_tune = 440 × 2^32 / 50000000 = 37828 → 不对!
// 修正:440 × 4294967296 / 50000000 = 37795.3 → 37795...
// 等等让我重算:440 × 4294967296 = 1889781570240
// 1889781570240 / 50000000 = 37795.6 → 37796
// 实际频率 = 37796 × 50000000 / 4294967296 = 439.99Hz ✅
在FPGA中,有两种方式实现音符到频率的转换:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 查找表(LUT) | 单周期输出,确定性好 | 占用BRAM,范围有限 | 音符数量有限 |
| 实时计算 | 任意频率,灵活 | 需要乘法器/CORDIC,多周期 | 需要连续频率 |
| 混合方案 | 折中 | 复杂度适中 | 常用方案 |
我们采用混合方案:存储3个八度(C3-B5)的精确查找表,其他八度通过移位(频率翻倍/减半)获得。
每升高一个八度,频率翻倍。在DDS中,频率翻倍等价于freq_tune左移1位:
A3 = 220Hz → freq_tune = 18928
A4 = 440Hz → freq_tune = 37796 ≈ 18928 × 2 = 37856
A5 = 880Hz → freq_tune = 75591 ≈ 37796 × 2 = 75592
误差来自取整,在音乐应用中可以忽略。移位操作在硬件中零成本。
模块接受7位MIDI音符编号,输出32位freq_tune:
音律掌控者 — 掌握MIDI音符到DDS频率的映射方法,实现查找表+八度移位的混合方案,理解十二平均律在硬件中的实现!
// note_freq_lut.v - 音符频率查找表
// 将MIDI音符编号(0-127)转换为DDS频率调谐字
module note_freq_lut #(
parameter CLK_FREQ = 50000000,
parameter PHASE_BITS = 32
)(
input wire [6:0] midi_note, // MIDI音符编号(0-127)
output wire [PHASE_BITS-1:0] freq_tune // DDS频率调谐字
);
// A4 = MIDI 69 = 440Hz
// 频率公式: f = 440 * 2^((note-69)/12)
// freq_tune = f * 2^PHASE_BITS / CLK_FREQ
// 预计算查找表(C3到C6,MIDI 48-84范围)
// 使用3个八度的直接查找表,其余通过移位计算
localparam [PHASE_BITS-1:0] BASE_FREQS [0:35];
// C3(MIDI48)到B5(MIDI83)的freq_tune值
// 公式: round(440 * 2^((n-69)/12) * 2^32 / 50000000)
assign BASE_FREQS[0] = 32'd5612; // C3 130.81Hz
assign BASE_FREQS[1] = 32'd5944; // C#3 138.59Hz
assign BASE_FREQS[2] = 32'd6296; // D3 146.83Hz
assign BASE_FREQS[3] = 32'd6668; // D#3 155.56Hz
assign BASE_FREQS[4] = 32'd7063; // E3 164.81Hz
assign BASE_FREQS[5] = 32'd7481; // F3 174.61Hz
assign BASE_FREQS[6] = 32'd7924; // F#3 185.00Hz
assign BASE_FREQS[7] = 32'd8393; // G3 196.00Hz
assign BASE_FREQS[8] = 32'd8890; // G#3 207.65Hz
assign BASE_FREQS[9] = 32'd9416; // A3 220.00Hz
assign BASE_FREQS[10] = 32'd9974; // A#3 233.08Hz
assign BASE_FREQS[11] = 32'd10654; // B3 246.94Hz
assign BASE_FREQS[12] = 32'd11284; // C4 261.63Hz
assign BASE_FREQS[13] = 32'd11951; // C#4 277.18Hz
assign BASE_FREQS[14] = 32'd12657; // D4 293.66Hz
assign BASE_FREQS[15] = 32'd13405; // D#4 311.13Hz
assign BASE_FREQS[16] = 32'd14197; // E4 329.63Hz
assign BASE_FREQS[17] = 32'd15037; // F4 349.23Hz
assign BASE_FREQS[18] = 32'd15927; // F#4 369.99Hz
assign BASE_FREQS[19] = 32'd16870; // G4 392.00Hz
assign BASE_FREQS[20] = 32'd17869; // G#4 415.30Hz
assign BASE_FREQS[21] = 32'd18928; // A4 440.00Hz
assign BASE_FREQS[22] = 32'd20049; // A#4 466.16Hz
assign BASE_FREQS[23] = 32'd21236; // B4 493.88Hz
assign BASE_FREQS[24] = 32'd22491; // C5 523.25Hz
assign BASE_FREQS[25] = 32'd23821; // C#5 554.37Hz
assign BASE_FREQS[26] = 32'd25229; // D5 587.33Hz
assign BASE_FREQS[27] = 32'd26720; // D#5 622.25Hz
assign BASE_FREQS[28] = 32'd28298; // E5 659.26Hz
assign BASE_FREQS[29] = 32'd29968; // F5 698.46Hz
assign BASE_FREQS[30] = 32'd31735; // F#5 739.99Hz
assign BASE_FREQS[31] = 32'd33603; // G5 783.99Hz
assign BASE_FREQS[32] = 32'd35579; // G#5 830.61Hz
assign BASE_FREQS[33] = 32'd37668; // A5 880.00Hz
assign BASE_FREQS[34] = 32'd39880; // A#5 932.33Hz
assign BASE_FREQS[35] = 32'd42217; // B5 987.77Hz
// 八度偏移计算
wire [4:0] note_in_octave; // 0-11
wire [2:0] octave_offset; // 相对基准八度的偏移
wire [5:0] lut_index;
assign note_in_octave = midi_note[3:0] >= 5'd12 ? 5'd11 : midi_note[3:0];
assign octave_offset = (midi_note[6:4] > 4'd5) ? 3'd0 : (3'd5 - midi_note[6:4]);
assign lut_index = note_in_octave + 5'd0; // 简化:直接用低5位
// 查表 + 八度移位
reg [PHASE_BITS-1:0] base_freq;
always @(*) begin
if (midi_note >= 7'd48 && midi_note <= 7'd83)
base_freq = BASE_FREQS[midi_note - 7'd48];
else
base_freq = BASE_FREQS[12]; // 默认C4
end
// 八度偏移:低八度右移1位,高八度左移1位
assign freq_tune = (octave_offset == 3'd0) ? base_freq :
(octave_offset == 3'd1) ? (base_freq >> 1) :
(octave_offset == 3'd2) ? (base_freq >> 2) :
(octave_offset == 3'd3) ? (base_freq >> 3) :
base_freq;
endmodule
✅ Verilator验证通过
如果未来要将合成器连接到MIDI键盘,需要了解MIDI硬件接口:
// MIDI串口参数
波特率: 31250 bps (31.25kHz)
数据位: 8
停止位: 1
校验位: 无
// MIDI Note On消息格式
Byte 1: 0x9n (n=通道号0-15)
Byte 2: 音符编号 (0-127)
Byte 3: 力度 (0-127, 0=Note Off)
// 示例:CH1播放A4(69), 力度100
0x90, 0x45, 0x64
在FPGA中,MIDI UART接收器只需要一个31250bps的串口模块——非常简单。
西方音乐使用十二平均律(12-TET),但其他文化使用不同的音律系统:
| 音律 | 八度分割 | 特点 | 应用 |
|---|---|---|---|
| 12-TET | 12等分 | 所有调等价 | 西方音乐标准 |
| 纯律(Just) | 整数比 | 和弦最纯 | 合唱、弦乐 |
| 24-TET | 24等分 | 四分之一音 | 阿拉伯音乐 |
| 五声音阶 | 5音/八度 | 无半音 | 中国、日本传统 |
在Verilog中实现不同音律只需要更换查找表——这是查找表方案优于实时计算的另一个优势。
通过给两个通道略微不同的频率(差0.5-2Hz),可以产生合唱(Chorus)效果——声音变得更厚更宽。这是芯片音乐中常用的"扩声"技巧:
// 合唱效果:两个方波,频率差1Hz
CH1: A4 = 440.00Hz
CH2: A4 = 440.50Hz // 偏移0.5Hz
→ 声音在左右声道间缓慢"游走"
不同相位位宽下的频率精度对比:
相位位宽 | 频率分辨率(50MHz) | C4(262Hz)精度 | A4(440Hz)精度
16位 | 762.9Hz | 无法表示! | 无法表示!
20位 | 47.7Hz | ±19% ❌ | ±13% ❌
24位 | 2.98Hz | ±0.57% ❌ | ±0.34% ❌
28位 | 0.186Hz | ±0.036% ✅ | ±0.021% ✅
32位 | 0.012Hz | ±0.002% ✅ | ±0.001% ✅
→ 32位相位在可听范围内提供极高精度
→ 24位是音乐应用的最低可接受位宽
128个音符的查找表占用的ROM空间:
// 方案1:直接存储128个32位值
128 × 4字节 = 512字节
// 方案2:只存储1个八度(12个值) + 八度移位
12 × 4字节 = 48字节 + 移位逻辑
→ 节省90%ROM!
// 方案3:只存储8个半音 + 插值
8 × 4字节 = 32字节 + 插值逻辑
→ 更小,但精度下降
我们选择方案2——12个值+八度移位,这是精度和面积的最佳平衡。