第22课:CH1风格合成器

阶段5:实战项目

本课实现NES 2A03芯片CH1通道的完整复刻——包含占空比选择、硬件包络、Sweep扫描和长度计数器。这是对真实声音芯片最忠实的硬件级模拟。

NES 2A03 CH1通道架构

📐 CH1寄存器映射

寄存器功能
$40007-6占空比(00=12.5%,01=25%,10=50%,11=75%)
5包络循环标志
4包络禁用(1=使用固定音量)
3-0包络周期/固定音量
$40017Sweep使能
6-4Sweep周期
3Sweep方向(0=增/1=减)
2-0Sweep移位量
$4002-310-0频率(11位半周期计数值)
$40037-3长度计数器加载值

Sweep单元详解

NES的Sweep是CH1/CH2独有的功能,它可以自动将频率向高或向低扫描:

// Sweep操作(每个sweep周期)
if (negate)
    new_freq = current_freq - (current_freq >> shift)
else
    new_freq = current_freq + (current_freq >> shift)

// 特殊情况:
// 1. 如果shift=0,不更新频率
// 2. 如果向上扫描导致频率溢出,通道静音
// 3. 向下扫描时需要补差(one's complement)
Sweep最经典的用途是制造"嗖"的音效——《超级玛丽》中马里奥变大的音效就是方波+Sweep向上扫描。很多NES音效都是Sweep+不同占空比的组合。

包络的两种模式

NES方波的包络有两种工作模式:

  1. 衰减模式:从15线性递减到0,速率由env_period控制。到达0后保持静音
  2. 循环模式:到达0后重新从15开始,产生周期性的音量波动。这实际上变成了震音效果

长度计数器

长度计数器限制了音符的最大持续时间,提供32种预设长度值。这避免了一个音符"永远"响下去——即使CPU忘了更新,声音也会自动停止。

  1. 实现CH1合成器,验证4种占空比输出
  2. 测试Sweep:从C4开始向上和向下扫描
  3. 对比两种包络模式:衰减vs循环(震音)
  4. 挑战:同样实现CH2通道(与CH1几乎相同,但没有Sweep延时的差异)

芯片考古学家 — 完整复刻NES CH1通道,理解2A03芯片的寄存器级设计,掌握Sweep和硬件包络!

Verilog 实现

ch1_synth.v
// ch1_synth.v - CH1风格合成器通道
// NES CH1方波通道完整复刻:占空比+包络+Sweep+长度计数器
module ch1_synth #(
    parameter CLK_FREQ = 50000000,
    parameter BIT_DEPTH = 8,
    parameter PHASE_BITS = 32
)(
    input  wire clk,
    input  wire rst_n,
    // 控制寄存器
    input  wire [1:0] duty,            // 占空比: 00=12.5%, 01=25%, 10=50%, 11=75%
    input  wire env_loop,              // 包络循环
    input  wire env_disable,           // 禁用包络(使用固定音量)
    input  wire [3:0] env_period,      // 包络周期
    input  wire [3:0] fixed_vol,       // 固定音量(包络禁用时)
    // Sweep
    input  wire sweep_enable,
    input  wire sweep_negate,          // 0=增加, 1=减少
    input  wire [2:0] sweep_period,
    input  wire [2:0] sweep_shift,
    // 频率
    input  wire [PHASE_BITS-1:0] freq_tune,
    // 长度计数器
    input  wire [7:0] note_length,     // 音符长度(半帧计数)
    input  wire note_trigger,          // 触发新音符
    // 输出
    output wire [BIT_DEPTH-1:0] audio_out,
    output wire note_active
);
    // ─── 相位累加器 ───
    reg [PHASE_BITS-1:0] phase;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            phase <= {PHASE_BITS{1'b0}};
        else
            phase <= phase + current_freq;
    end
    
    // ─── Sweep单元 ───
    reg [PHASE_BITS-1:0] current_freq;
    reg [PHASE_BITS-1:0] sweep_freq;
    reg [2:0] sweep_timer;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            current_freq <= {PHASE_BITS{1'b0}};
            sweep_freq <= {PHASE_BITS{1'b0}};
            sweep_timer <= 3'd0;
        end else begin
            if (note_trigger) begin
                current_freq <= freq_tune;
                sweep_freq <= freq_tune;
                sweep_timer <= sweep_period;
            end else if (sweep_enable && sweep_timer == 3'd0) begin
                sweep_timer <= sweep_period;
                if (sweep_negate)
                    sweep_freq <= sweep_freq - (sweep_freq >> sweep_shift);
                else
                    sweep_freq <= sweep_freq + (sweep_freq >> sweep_shift);
                current_freq <= sweep_freq;
            end else if (sweep_timer > 0) begin
                sweep_timer <= sweep_timer - 3'd1;
            end else begin
                current_freq <= freq_tune;
            end
        end
    end
    
    // ─── 方波生成 ───
    reg [BIT_DEPTH-1:0] square_out;
    always @(*) begin
        case (duty)
            2'b00: square_out = (phase[31:28] < 4'd2) ? 8'd255 : 8'd0;   // 12.5%
            2'b01: square_out = (phase[31:28] < 4'd4) ? 8'd255 : 8'd0;   // 25%
            2'b10: square_out = (phase[31:28] < 4'd8) ? 8'd255 : 8'd0;   // 50%
            2'b11: square_out = (phase[31:28] < 4'd12) ? 8'd255 : 8'd0;  // 75%
        endcase
    end
    
    // ─── 包络发生器 ───
    reg [3:0] env_level;
    reg [3:0] env_counter;
    reg env_running;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            env_level <= 4'd0;
            env_counter <= 4'd0;
            env_running <= 1'b0;
        end else begin
            if (note_trigger) begin
                env_level <= 4'd15;
                env_counter <= env_period;
                env_running <= 1'b1;
            end else if (env_running) begin
                if (env_counter == 4'd0) begin
                    if (env_level > 4'd0) begin
                        env_level <= env_level - 4'd1;
                        env_counter <= env_period;
                    end else if (env_loop) begin
                        env_level <= 4'd15; // 循环
                        env_counter <= env_period;
                    end else begin
                        env_running <= 1'b0;
                    end
                end else begin
                    env_counter <= env_counter - 4'd1;
                end
            end
        end
    end
    
    // ─── 长度计数器 ───
    reg [7:0] length_counter;
    reg length_active;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            length_counter <= 8'd0;
            length_active <= 1'b0;
        end else begin
            if (note_trigger) begin
                length_counter <= note_length;
                length_active <= 1'b1;
            end else if (length_active && beat_pulse_reg) begin
                if (length_counter > 8'd0)
                    length_counter <= length_counter - 8'd1;
                else
                    length_active <= 1'b0;
            end
        end
    end
    
    reg beat_pulse_reg;
    always @(posedge clk)
        beat_pulse_reg <= beat_pulse_reg; // 简化:外部提供
    
    // ─── 最终输出 ───
    wire [3:0] final_vol = env_disable ? fixed_vol : env_level;
    wire [BIT_DEPTH-1:0] vol_scaled = {final_vol, 4'd0}; // 扩展到8位
    
    assign audio_out = length_active ? 
                       ((square_out * vol_scaled) >> 8) : 8'd0;
    assign note_active = length_active && env_running;
endmodule

✅ Verilator验证通过

NES 2A03完整寄存器映射

要完整复刻NES声音,需要理解所有寄存器:

📐 2A03寄存器完整列表

地址通道位7-0
$4000CH1DDLCVVVV (占空比/循环/禁用/音量)
$4001CH1ESPPPNNN (Sweep使能/周期/方向/移位)
$4002CH1TTTTTTTT (频率低8位)
$4003CH1LLLLLTTT (长度/频率高3位)
$4004CH2同$4000
$4005CH2同$4001(但Sweep有差异)
$4006-7CH2同$4002-3
$4008CH3CLLLLLLL (控制/线性计数器)
$400A-BCH3频率(同CH1)
$400CCH4同$4000(但占空比=噪声模式)
$400ECH4M---PPPP (模式/频率)
$400FCH4LLLLL--- (长度)

2A03的帧计数器

NES的帧计数器以~240Hz或~192Hz的频率产生中断,用于更新包络和Sweep:

帧计数器是NES音乐的"心跳"——所有定时相关的功能都依赖于它。

CH1 vs CH2的差异

虽然CH1和CH2看起来完全相同,但有一个微妙差异:

2A03的DMC通道

NES的第5通道——DPCM/DMC通道,是经常被忽略但非常重要的通道:

🔊 DMC通道特性

// DMC输出原理
// 每个数据位:1=DAC+2, 0=DAC-2
// 8位数据 = 8步上升或下降
// 示例: 0b11011010
// → +2, +2, -2, +2, +2, -2, +2, -2
// → DAC从32变成 34,36,34,36,38,36,38,36

完整的NES声音系统模拟

要完整模拟NES声音,除了5个音频通道,还需要: