第16课:节拍时钟

阶段4:音序器

节拍时钟是音序器的"心跳"——它定义了音乐的速度(BPM)节拍细分。本课实现精确的节拍时钟发生器,从系统时钟生成稳定的BPM脉冲,这是自动演奏系统的基础。

BPM与音乐速度

BPM(Beats Per Minute,每分钟拍数)是音乐速度的标准度量:

🎵 速度分类

BPM范围术语音乐风格
40-60Grave/Largo缓慢、庄重
60-80Adagio柔板、抒情
80-100Andante行板、中等
100-120Moderato中板、流行
120-140Allegro快板、舞曲
140-160Vivace活泼、电子
160-200Presto极快、Drum'n'Bass
200+Prestissimo极速、Hardcore

节拍细分(Subdivision)

一个四分音符可以细分为更小的时值:

全音符 = 4拍
二分音符 = 2拍  
四分音符 = 1拍
八分音符 = 1/2拍
十六分音符 = 1/4拍
三十二分音符 = 1/8拍

在芯片音乐中,最常用的是八分和十六分音符细分,这决定了音序器的最小时间分辨率。

时钟精度问题

从50MHz系统时钟生成BPM脉冲需要大计数器。120BPM下,每拍25,000,000个时钟周期。32位计数器最大可计数~42.9亿,足够覆盖30-300BPM范围。

BPM精度问题:50MHz/120BPM = 25,000,000(精确),但50MHz/140BPM = 21,428,571.43...(不精确)。取整误差导致BPM有微小偏差,但在音乐中可忽略。如果需要精确BPM,可以增大系统时钟频率。

NES的帧时钟

NES CPU运行在~1.79MHz,每帧(~60Hz)产生一个IRQ中断。音乐引擎通常每帧更新一次,这意味着:

帧率的量化是NES音乐节奏感独特的来源——它不完全是均匀的!

  1. 实现节拍时钟,验证不同BPM下的脉冲间隔
  2. 计算:150BPM十六分音符的时钟周期数
  3. 添加Swing功能:奇数拍和偶数拍使用不同长度
  4. 挑战:实现Tap Tempo——通过按键间隔自动计算BPM

节奏基石 — 实现BPM节拍时钟,理解节拍细分,掌握时钟到音乐时值的映射!

Verilog 实现

beat_clock.v
// beat_clock.v - 节拍时钟发生器
// 从系统时钟生成BPM节拍脉冲
module beat_clock #(
    parameter CLK_FREQ = 50000000,
    parameter BPM_BITS = 10
)(
    input  wire clk,
    input  wire rst_n,
    input  wire [BPM_BITS-1:0] bpm,          // BPM值(30-300)
    input  wire [3:0] subdivision,            // 细分: 1=全音符,2=半,4=四分,8=八分...
    output reg  beat_pulse,                   // 节拍脉冲(1周期宽)
    output reg  [3:0] beat_count,             // 当前节拍计数(0-15)
    output reg  [3:0] measure_count,          // 小节计数(0-15)
    output wire running
);
    // 计算每拍时钟周期数
    // clocks_per_beat = CLK_FREQ × 60 / BPM
    // 对于120BPM: 50M × 60 / 120 = 25,000,000
    reg [31:0] clocks_per_beat;
    reg [31:0] beat_counter;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            clocks_per_beat <= 32'd25000000; // 默认120BPM
        end else begin
            // 简化BPM到时钟数的映射
            // 使用查找表近似避免除法
            case (bpm)
                10'd60:  clocks_per_beat <= 32'd50000000;
                10'd80:  clocks_per_beat <= 32'd37500000;
                10'd100: clocks_per_beat <= 32'd30000000;
                10'd120: clocks_per_beat <= 32'd25000000;
                10'd140: clocks_per_beat <= 32'd21428571;
                10'd160: clocks_per_beat <= 32'd18750000;
                10'd180: clocks_per_beat <= 32'd16666667;
                10'd200: clocks_per_beat <= 32'd15000000;
                default: clocks_per_beat <= 32'd25000000;
            endcase
        end
    end
    
    // 细分:将每拍进一步分割
    wire [31:0] subdivided_period;
    assign subdivided_period = clocks_per_beat >> subdivision;
    
    // 节拍脉冲生成
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            beat_counter <= 32'd0;
            beat_pulse <= 1'b0;
            beat_count <= 4'd0;
            measure_count <= 4'd0;
        end else begin
            beat_pulse <= 1'b0; // 默认低
            
            if (beat_counter >= subdivided_period - 1) begin
                beat_counter <= 32'd0;
                beat_pulse <= 1'b1;
                
                if (beat_count >= 15) begin
                    beat_count <= 4'd0;
                    measure_count <= measure_count + 4'd1;
                end else begin
                    beat_count <= beat_count + 4'd1;
                end
            end else begin
                beat_counter <= beat_counter + 32'd1;
            end
        end
    end
    
    assign running = 1'b1;
endmodule

✅ Verilator验证通过

Swing/Shuffle节奏

不是所有音乐都使用均匀的节拍。Swing(也称为Shuffle)让奇数拍比偶数拍稍长,产生"摇摆"的节奏感:

🎵 Swing的数学

标准4/4拍(无Swing):
|X...|X...|X...|X...|
 1   2   3   4  (等长)

Swing 50% (标准):
|X...|X...|X...|X...|
 1   2   3   4  (等长,无swing)

Swing 60%:
|X....|X..|X....|X..|
 1    2   3    4  (奇数拍60%,偶数拍40%)

Swing 67% (经典Hip-hop):
|X.....|X.|X.....|X.|
 1     2  3     4  (2:1比例)

在硬件中实现Swing只需修改奇数步和偶数步的beat_period值:

// Swing实现
wire [31:0] swing_period = (step[0] == 1'b0) ? 
    (beat_period + beat_period >> 2) :  // 奇数步: +25%
    (beat_period - beat_period >> 2);   // 偶数步: -25%

多速率节拍

有时需要不同的时值分辨率——旋律用十六分音符,鼓用八分音符:

// 节拍分频器
beat_pulse_16th = base_pulse;            // 十六分音符
beat_pulse_8th  = base_pulse && step[0]; // 八分音符(每2个)
beat_pulse_qtr  = base_pulse && (step[1:0] == 2'b00); // 四分音符(每4个)

Tap Tempo实现

Tap Tempo通过测量两次按键的间隔来计算BPM:

// Tap Tempo逻辑
reg [31:0] tap_time_1, tap_time_2;
reg tap_count;

always @(posedge clk) begin
    if (tap_button && !tap_button_prev) begin
        if (!tap_count) begin
            tap_time_1 <= current_time;
            tap_count <= 1'b1;
        end else begin
            tap_time_2 <= current_time;
            tap_count <= 1'b0;
            // BPM = 60 × CLK_FREQ / (tap_time_2 - tap_time_1)
            bpm <= calculate_bpm(tap_time_2 - tap_time_1);
        end
    end
end

全球音乐速度统计

不同音乐类型的典型BPM:

🎵 BPM分类统计

BPM风格芯片音乐代表
60-80Dub, Ambient探索类游戏BGM
80-100Hip-hop, R&BRPG村庄音乐
100-120Pop, House平台游戏主旋律
120-140Techno, Trance动作游戏战斗
140-160Drum'n'Bass, Jungle射击游戏
160-180Hardcore, Gabber极速游戏
180+Speedcore极端场景

节拍时钟的精度要求

人耳对节奏精度的感知:

50MHz时钟下,1ms = 50,000个时钟周期,远超精度要求。

节拍时钟的变体实现

除了固定BPM,还有多种节拍时钟变体:

🔧 变体实现方案

// 变体1:可编程BPM(精确计算)
// 不使用查找表,用乘法器实时计算
// beat_period = CLK_FREQ * 60 / (BPM * subdivision)
// 避免除法:beat_period = (CLK_FREQ * 60) >> log2(BPM)

// 变体2:Swing时钟
// 奇偶步使用不同周期
reg [31:0] swing_period;
always @(*) begin
    if (step[0] == 1'b0)  // 偶数步
        swing_period = beat_period + (beat_period >> 2);  // +25%
    else                   // 奇数步
        swing_period = beat_period - (beat_period >> 2);  // -25%
end

// 变体3:加速/减速
// BPM随时间线性变化
reg [9:0] current_bpm;
always @(posedge clk) begin
    if (accelerating)
        current_bpm <= current_bpm + 1;  // 逐渐加速
    else if (decelerating)
        current_bpm <= current_bpm - 1;  // 逐渐减速
end

📐 节拍时钟参数计算器

50MHz时钟下的BPM→时钟周期数计算:

// 公式:beat_period = CLK_FREQ * 60 / (BPM * subdivision)
// 十六分音符( subdivision=4 ):
// 60BPM: 50M*60/(60*4) = 12,500,000
// 80BPM: 50M*60/(80*4) = 9,375,000
// 100BPM: 50M*60/(100*4) = 7,500,000
// 120BPM: 50M*60/(120*4) = 6,250,000
// 140BPM: 50M*60/(140*4) = 5,357,143
// 160BPM: 50M*60/(160*4) = 4,687,500
// 180BPM: 50M*60/(180*4) = 4,166,667
// 200BPM: 50M*60/(200*4) = 3,750,000

// 八分音符( subdivision=2 )只需将上述值×2
// 四分音符( subdivision=1 )只需将上述值×4

节拍时钟的精确度分析

节拍时钟的精确度直接影响音乐的节奏感。让我们深入分析:

📐 BPM精度量化分析

// 50MHz时钟下,120BPM四分音符:
// 理想周期 = 500ms = 25,000,000时钟周期
// 实际周期 = 25,000,000 × 20ns = 500.000ms ✅

// 121BPM四分音符:
// 理想周期 = 495.867ms
// 计数器值 = 24,793,388
// 实际周期 = 24,793,388 × 20ns = 495.868ms ✅

// 关键问题:当BPM不能整除时
// 123BPM四分音符:
// 理想 = 487.805ms
// 计数器值 = round(24,390,244) = 24,390,244
// 实际 = 487.805ms ✅
// 精度:亚微秒级,完全可接受

结论:50MHz时钟下BPM精度远超需求。在更低的时钟频率(如1MHz)下,精度会下降,但对于8位系统仍然足够。

节拍时钟的测试方法

验证节拍时钟的正确性:

// 测试1: 测量实际BPM
// 计数beat_pulse脉冲数和总时钟周期数
// actual_bpm = pulse_count * 60 * CLK_FREQ / total_clocks

// 测试2: 抖动测试
// 测量相邻beat_pulse之间的间隔变化
// 任何大于1个时钟周期的变化都是bug

// 测试3: Swing精度
// 验证奇偶步的周期比是否正确
// 67% swing: 奇数步/偶数步 = 2:1