第6课:频率控制(音符映射)

阶段2:音调控制

到目前为止,我们通过手动计算freq_tune值来控制频率。本课将实现音符-频率查找表,让模块直接接受MIDI音符编号,自动转换为DDS频率调谐字。这是从"波形发生器"到"音乐合成器"的关键一步。

MIDI音符编号体系

MIDI(Musical Instrument Digital Interface)使用0-127的编号来表示音高:

🎹 MIDI音符对照表

MIDI#音符频率(Hz)说明
21A027.50钢琴最低A
36C265.41低音C
48C3130.81低音区起点
60C4261.63中央C
69A4440.00标准音(调音基准)
84C61046.50高音C
108C84186.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 ✅

查找表 vs 实时计算

在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

误差来自取整,在音乐应用中可以忽略。移位操作在硬件中零成本。

NES 2A03的频率寄存器是11位,范围0-2047。它存储的是"半周期时钟数"而非频率调谐字——值越小频率越高。这与DDS的"值越大频率越高"正好相反。两种方案各有优劣,DDS方案频率分辨率更均匀。

Verilog实现解析

模块接受7位MIDI音符编号,输出32位freq_tune:

  1. 查找表:36个常量,覆盖C3(MIDI48)到B5(MIDI83)
  2. 范围检查:超出范围的音符映射到C4
  3. 八度移位:通过右移处理低于C3的音符
  1. 验证:计算E4(MIDI64)的freq_tune值,与查找表对比
  2. 扩展:将查找表范围扩展到C2-B6(5个八度,60个表项)
  3. 写testbench:遍历所有MIDI音符,输出频率和freq_tune值
  4. 挑战:用CORDIC算法实时计算freq_tune,替代查找表

音律掌控者 — 掌握MIDI音符到DDS频率的映射方法,实现查找表+八度移位的混合方案,理解十二平均律在硬件中的实现!

Verilog 实现

note_freq_lut.v
// 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硬件接口:

🔧 MIDI UART规范

// 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-TET12等分所有调等价西方音乐标准
纯律(Just)整数比和弦最纯合唱、弦乐
24-TET24等分四分之一音阿拉伯音乐
五声音阶5音/八度无半音中国、日本传统

在Verilog中实现不同音律只需要更换查找表——这是查找表方案优于实时计算的另一个优势。

微调(Detune)与合唱效果

通过给两个通道略微不同的频率(差0.5-2Hz),可以产生合唱(Chorus)效果——声音变得更厚更宽。这是芯片音乐中常用的"扩声"技巧:

// 合唱效果:两个方波,频率差1Hz
CH1: A4 = 440.00Hz
CH2: A4 = 440.50Hz  // 偏移0.5Hz
→ 声音在左右声道间缓慢"游走"
《时空之轮》的弦乐音色就使用了微调技巧——两个方波通道演奏相同旋律但频率略微不同,产生温暖宽厚的合唱效果。这在硬件上零成本——只是freq_tune值差1而已。

频率精度的系统分析

不同相位位宽下的频率精度对比:

📐 频率分辨率 vs 相位位宽

相位位宽 | 频率分辨率(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位是音乐应用的最低可接受位宽

频率查找表的ROM优化

128个音符的查找表占用的ROM空间:

// 方案1:直接存储128个32位值
128 × 4字节 = 512字节

// 方案2:只存储1个八度(12个值) + 八度移位
12 × 4字节 = 48字节 + 移位逻辑
→ 节省90%ROM!

// 方案3:只存储8个半音 + 插值
8 × 4字节 = 32字节 + 插值逻辑
→ 更小,但精度下降

我们选择方案2——12个值+八度移位,这是精度和面积的最佳平衡。