本课实现NES 2A03芯片CH1通道的完整复刻——包含占空比选择、硬件包络、Sweep扫描和长度计数器。这是对真实声音芯片最忠实的硬件级模拟。
| 寄存器 | 位 | 功能 |
|---|---|---|
| $4000 | 7-6 | 占空比(00=12.5%,01=25%,10=50%,11=75%) |
| 5 | 包络循环标志 | |
| 4 | 包络禁用(1=使用固定音量) | |
| 3-0 | 包络周期/固定音量 | |
| $4001 | 7 | Sweep使能 |
| 6-4 | Sweep周期 | |
| 3 | Sweep方向(0=增/1=减) | |
| 2-0 | Sweep移位量 | |
| $4002-3 | 10-0 | 频率(11位半周期计数值) |
| $4003 | 7-3 | 长度计数器加载值 |
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)
NES方波的包络有两种工作模式:
长度计数器限制了音符的最大持续时间,提供32种预设长度值。这避免了一个音符"永远"响下去——即使CPU忘了更新,声音也会自动停止。
芯片考古学家 — 完整复刻NES CH1通道,理解2A03芯片的寄存器级设计,掌握Sweep和硬件包络!
// 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声音,需要理解所有寄存器:
| 地址 | 通道 | 位7-0 |
|---|---|---|
| $4000 | CH1 | DDLCVVVV (占空比/循环/禁用/音量) |
| $4001 | CH1 | ESPPPNNN (Sweep使能/周期/方向/移位) |
| $4002 | CH1 | TTTTTTTT (频率低8位) |
| $4003 | CH1 | LLLLLTTT (长度/频率高3位) |
| $4004 | CH2 | 同$4000 |
| $4005 | CH2 | 同$4001(但Sweep有差异) |
| $4006-7 | CH2 | 同$4002-3 |
| $4008 | CH3 | CLLLLLLL (控制/线性计数器) |
| $400A-B | CH3 | 频率(同CH1) |
| $400C | CH4 | 同$4000(但占空比=噪声模式) |
| $400E | CH4 | M---PPPP (模式/频率) |
| $400F | CH4 | LLLLL--- (长度) |
NES的帧计数器以~240Hz或~192Hz的频率产生中断,用于更新包络和Sweep:
帧计数器是NES音乐的"心跳"——所有定时相关的功能都依赖于它。
虽然CH1和CH2看起来完全相同,但有一个微妙差异:
NES的第5通道——DPCM/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声音,除了5个音频通道,还需要: