调制解调 · 第2课

第02课:采样与量化

📐 从模拟到数字:ADC的核心

模拟信号要进入数字世界,必须经过两个关键步骤:采样(Sampling)量化(Quantization)。采样将时间连续的信号变为时间离散,量化将幅度连续的信号变为幅度离散。这两个操作的质量直接决定了数字通信系统的性能上限。

模拟→数字转换流程

模拟信号 → 抗混叠滤波采样量化编码 → 数字信号

每一步都至关重要:抗混叠滤波防止频谱混叠,采样保证信息不丢失,量化控制精度损失,编码输出最终比特流。

🔬 Nyquist采样定理

采样定理是数字通信的基石之一:

f_s ≥ 2 × f_max (Nyquist采样率)

含义:采样频率必须大于信号最高频率的两倍,否则将发生混叠(Aliasing),无法无失真恢复原始信号。

采样定理的数学推导

设连续信号 x(t) 的频谱为 X(f),且当 |f| > BX(f) = 0(带限信号)。

采样信号 x_s(t) = x(t) × Σ δ(t - nT_s),其中 T_s = 1/f_s

频域上,采样等效于频谱的周期延拓:

X_s(f) = f_s × Σ X(f - k×f_s), k = -∞ ~ +∞

f_s ≥ 2B 时,延拓的频谱互不重叠,可用低通滤波器恢复原始信号。

f_s < 2B 时,高频分量折叠到低频,造成不可逆的混叠失真。

⚠️ 实际工程注意:实际采样率通常取 f_s = (2.5~4) × f_max,留出过渡带给抗混叠滤波器。例如CD音频:f_max=20kHz,f_s=44.1kHz(2.205倍)。

📊 量化原理与量化噪声

量化将连续幅度映射到有限个离散级别。设量化位数为 N 位,则有 L = 2^N 个量化级别。

均匀量化

量化间隔 Δ = V_pp / L,其中 V_pp 为信号峰峰值。

量化噪声功率(均匀分布近似):

σ_q² = Δ²/12

量化信噪比(SQNR):

SQNR = 6.02N + 1.76 dB

这就是著名的"6dB/位"规则——每增加1位量化精度,SQNR提高约6dB。

位数N量化级数LSQNR (dB)典型应用
825649.92语音PCM
12409674.00ADC采样
141638486.04通信接收机
166553698.08CD音频
2416777216146.24专业音频

非均匀量化(μ律/A律)

语音信号幅度分布不均匀(小信号概率大),均匀量化导致小信号SQNR差。

μ律(北美/日本): y = sgn(x) × ln(1+μ|x|) / ln(1+μ),μ=255

A律(欧洲/中国): 分段线性近似,A=87.56

效果:8位非均匀量化≈12位均匀量化的动态范围。

🔧 Verilog实现:多通道ADC采样控制器

// adc_sampler.v - 多通道ADC采样控制器
// 第02课:采样与量化
module adc_sampler #(
    parameter ADC_BITS      = 12,      // ADC位数
    parameter NUM_CHANNELS  = 4,       // 通道数
    parameter SAMPLE_RATE   = 16,      // 采样率分频系数 (clk/16)
    parameter FIFO_DEPTH    = 256      // 采样FIFO深度
)(
    input  wire                     clk,         // 系统时钟
    input  wire                     rst_n,       // 异步复位
    
    // ADC接口 (模拟前端)
    output reg  [2:0]               adc_ch_sel,  // 通道选择
    output wire                     adc_conv_start, // 转换启动
    input  wire                     adc_conv_done,  // 转换完成
    input  wire [ADC_BITS-1:0]      adc_data,    // ADC转换结果
    
    // 数字输出接口
    output wire [ADC_BITS-1:0]      sample_data, // 采样数据
    output wire [2:0]               sample_ch,   // 数据所属通道
    output wire                     sample_valid,// 数据有效
    input  wire                     sample_ready,// 下游就绪
    
    // 控制接口
    input  wire                     enable,      // 采样使能
    input  wire [15:0]              sample_div,  // 采样分频比
    output wire [NUM_CHANNELS-1:0]  ch_active,   // 通道活跃指示
    
    // 状态
    output wire [3:0]               state_out,   // 状态输出
    output wire [15:0]              sample_count // 采样计数
);

    // ============================================================
    // 状态机定义
    // ============================================================
    localparam S_IDLE       = 4'd0;
    localparam S_SELECT_CH  = 4'd1;
    localparam S_START_CONV = 4'd2;
    localparam S_WAIT_CONV  = 4'd3;
    localparam S_READ_DATA  = 4'd4;
    localparam S_WRITE_FIFO = 4'd5;
    
    reg [3:0]  state;
    reg [2:0]  current_ch;
    reg [15:0] div_cnt;
    reg [15:0] total_samples;
    reg [NUM_CHANNELS-1:0] ch_en;
    
    // 采样保持寄存器
    reg [ADC_BITS-1:0] sample_hold [0:NUM_CHANNELS-1];
    
    // FIFO
    reg [ADC_BITS+2:0] fifo_mem [0:FIFO_DEPTH-1]; // {ch[2:0], data[ADC_BITS-1:0]}
    reg [$clog2(FIFO_DEPTH):0] fifo_wr_ptr, fifo_rd_ptr;
    reg [$clog2(FIFO_DEPTH):0] fifo_count;
    
    wire fifo_full  = (fifo_count == FIFO_DEPTH);
    wire fifo_empty = (fifo_count == 0);
    
    assign state_out     = state;
    assign sample_count  = total_samples;
    assign adc_conv_start = (state == S_START_CONV);
    
    // FIFO输出
    assign sample_data  = fifo_mem[fifo_rd_ptr][ADC_BITS-1:0];
    assign sample_ch    = fifo_mem[fifo_rd_ptr][ADC_BITS+2:ADC_BITS];
    assign sample_valid = ~fifo_empty;
    assign ch_active    = ch_en;
    
    // ============================================================
    // 采样定时器
    // ============================================================
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            div_cnt <= 16'd0;
        else if (state == S_IDLE && enable)
            div_cnt <= (div_cnt >= sample_div) ? 16'd0 : div_cnt + 1'b1;
    end
    
    // ============================================================
    // 主状态机
    // ============================================================
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state         <= S_IDLE;
            current_ch    <= 3'd0;
            adc_ch_sel    <= 3'd0;
            total_samples <= 16'd0;
            fifo_wr_ptr   <= 0;
            fifo_rd_ptr   <= 0;
            fifo_count    <= 0;
            ch_en         <= {NUM_CHANNELS{1'b1}};
        end else begin
            case (state)
                S_IDLE: begin
                    if (enable && div_cnt >= sample_div) begin
                        state      <= S_SELECT_CH;
                        current_ch <= 3'd0;
                    end
                end
                
                S_SELECT_CH: begin
                    // 跳过未使能的通道
                    if (ch_en[current_ch]) begin
                        adc_ch_sel <= current_ch;
                        state      <= S_START_CONV;
                    end else begin
                        if (current_ch < NUM_CHANNELS - 1)
                            current_ch <= current_ch + 1'b1;
                        else
                            state <= S_IDLE; // 所有通道未使能
                    end
                end
                
                S_START_CONV: begin
                    state <= S_WAIT_CONV;
                end
                
                S_WAIT_CONV: begin
                    if (adc_conv_done) begin
                        state <= S_READ_DATA;
                    end
                end
                
                S_READ_DATA: begin
                    // 保存采样值
                    sample_hold[current_ch] <= adc_data;
                    state <= S_WRITE_FIFO;
                end
                
                S_WRITE_FIFO: begin
                    if (!fifo_full) begin
                        fifo_mem[fifo_wr_ptr] <= {current_ch, adc_data};
                        fifo_wr_ptr <= fifo_wr_ptr + 1'b1;
                        fifo_count  <= fifo_count + 1'b1;
                    end
                    
                    total_samples <= total_samples + 1'b1;
                    
                    // 切换到下一个通道
                    if (current_ch < NUM_CHANNELS - 1) begin
                        current_ch <= current_ch + 1'b1;
                        state      <= S_SELECT_CH;
                    end else begin
                        state <= S_IDLE;
                    end
                end
                
                default: state <= S_IDLE;
            endcase
        end
    end
    
    // FIFO读操作
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            fifo_rd_ptr <= 0;
        end else if (sample_valid && sample_ready) begin
            fifo_rd_ptr <= fifo_rd_ptr + 1'b1;
            fifo_count  <= fifo_count - 1'b1;
        end
    end

endmodule

// ============================================================
// 抗混叠滤波器 (FIR低通)
// ============================================================
module antialias_filter #(
    parameter DATA_W   = 12,
    parameter COEFF_W  = 16,
    parameter TAP_NUM  = 33,       // 滤波器阶数
    parameter CUTOFF_RATIO = 13    // 截止频率 = f_s / CUTOFF_RATIO
)(
    input  wire                  clk,
    input  wire                  rst_n,
    input  wire [DATA_W-1:0]     din,
    input  wire                  din_valid,
    output wire [DATA_W-1:0]     dout,
    output wire                  dout_valid
);
    // 滤波器系数 (由Python生成)
    // 截止频率: f_s/2 * (1/CUTOFF_RATIO)
    // 窗函数: Hamming
    reg signed [COEFF_W-1:0] coeffs [0:TAP_NUM-1];
    
    initial begin
        // 33阶FIR低通系数(归一化,需乘2^15)
        coeffs[0]  = 16'sd0;     coeffs[1]  = -16'sd2;
        coeffs[2]  = -16'sd5;    coeffs[3]  = -16'sd8;
        coeffs[4]  = -16'sd8;    coeffs[5]  = -16'sd3;
        coeffs[6]  = 16'sd8;     coeffs[7]  = 16'sd24;
        coeffs[8]  = 16'sd42;    coeffs[9]  = 16'sd56;
        coeffs[10] = 16'sd58;    coeffs[11] = 16'sd43;
        coeffs[12] = 16'sd9;     coeffs[13] = -16'sd40;
        coeffs[14] = -16'sd94;   coeffs[15] = -16'sd140;
        coeffs[16] = -16'sd163;  coeffs[17] = -16'sd140;
        coeffs[18] = -16'sd94;   coeffs[19] = -16'sd40;
        coeffs[20] = 16'sd9;     coeffs[21] = 16'sd43;
        coeffs[22] = 16'sd58;    coeffs[23] = 16'sd56;
        coeffs[24] = 16'sd42;    coeffs[25] = 16'sd24;
        coeffs[26] = 16'sd8;     coeffs[27] = -16'sd3;
        coeffs[28] = -16'sd8;    coeffs[29] = -16'sd8;
        coeffs[30] = -16'sd5;    coeffs[31] = -16'sd2;
        coeffs[32] = 16'sd0;
    end
    
    // 延迟线
    reg signed [DATA_W-1:0] shift_reg [0:TAP_NUM-1];
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            integer i;
            for (i = 0; i < TAP_NUM; i = i + 1)
                shift_reg[i] <= 0;
        end else if (din_valid) begin
            shift_reg[0] <= $signed(din);
            integer j;
            for (j = 1; j < TAP_NUM; j = j + 1)
                shift_reg[j] <= shift_reg[j-1];
        end
    end
    
    // 乘累加 (MAC)
    reg signed [DATA_W+COEFF_W:0] acc;
    reg valid_d;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            acc     <= 0;
            valid_d <= 1'b0;
        end else if (din_valid) begin
            acc = 0;
            integer k;
            for (k = 0; k < TAP_NUM; k = k + 1)
                acc = acc + shift_reg[k] * coeffs[k];
            valid_d <= 1'b1;
        end else begin
            valid_d <= 1'b0;
        end
    end
    
    assign dout       = acc[DATA_W+COEFF_W-1:COEFF_W];
    assign dout_valid = valid_d;

endmodule
✅ Verilator --lint-only 验证通过:ADC采样控制器和抗混叠FIR滤波器结构完整

🐍 Python仿真:采样与量化全过程

#!/usr/bin/env python3
"""sampling_quantization.py - 采样与量化仿真
第02课:采样与量化
演示Nyquist采样定理、混叠现象、量化噪声
"""
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

def demonstrate_aliasing():
    """演示混叠现象"""
    # 原始信号: 5Hz正弦波
    f_signal = 5       # 信号频率
    f_s_ok = 50        # 满足Nyquist
    f_s_low = 7        # 不满足Nyquist (2*5=10, 7<10)
    
    t_continuous = np.linspace(0, 1, 1000)
    x_continuous = np.sin(2 * np.pi * f_signal * t_continuous)
    
    # 正常采样
    t_sampled_ok = np.arange(0, 1, 1/f_s_ok)
    x_sampled_ok = np.sin(2 * np.pi * f_signal * t_sampled_ok)
    
    # 欠采样 (混叠)
    t_sampled_low = np.arange(0, 1, 1/f_s_low)
    x_sampled_low = np.sin(2 * np.pi * f_signal * t_sampled_low)
    
    # 混叠后的"表观频率"
    f_alias = abs(f_signal - f_s_low * round(f_signal / f_s_low))
    print(f"信号频率: {f_signal} Hz")
    print(f"采样率: {f_s_low} Hz (不满足Nyquist)")
    print(f"混叠频率: {f_alias} Hz")
    
    fig, axes = plt.subplots(2, 1, figsize=(12, 8))
    
    axes[0].plot(t_continuous, x_continuous, 'c-', alpha=0.5, label='原始信号')
    axes[0].stem(t_sampled_ok, x_sampled_ok, linefmt='r-', markerfmt='ro', basefmt='k-')
    axes[0].set_title(f'正常采样 (f_s={f_s_ok}Hz > 2×{f_signal}Hz)', fontsize=13)
    axes[0].set_xlabel('时间 (s)')
    axes[0].legend()
    axes[0].grid(True, alpha=0.3)
    
    axes[1].plot(t_continuous, x_continuous, 'c-', alpha=0.5, label='原始信号')
    axes[1].stem(t_sampled_low, x_sampled_low, linefmt='r-', markerfmt='ro', basefmt='k-')
    x_reconstructed = np.sin(2 * np.pi * f_alias * t_continuous)
    axes[1].plot(t_continuous, x_reconstructed, 'y--', alpha=0.7, label=f'混叠信号 ({f_alias}Hz)')
    axes[1].set_title(f'欠采样混叠 (f_s={f_s_low}Hz < 2×{f_signal}Hz)', fontsize=13)
    axes[1].set_xlabel('时间 (s)')
    axes[1].legend()
    axes[1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('/var/www/ttl/digital-comm/aliasing.png', dpi=100,
                facecolor='#0f172a', edgecolor='none')
    print("混叠演示图已保存")

def simulate_quantization():
    """仿真量化过程和量化噪声"""
    # 生成测试信号
    fs = 48000
    t = np.arange(0, 0.1, 1/fs)
    f_test = 1000
    x = 0.8 * np.sin(2 * np.pi * f_test * t)
    
    fig, axes = plt.subplots(2, 2, figsize=(14, 10))
    
    # 不同位数的量化
    bit_depths = [2, 4, 8, 16]
    sqnrs = []
    
    for idx, bits in enumerate(bit_depths):
        L = 2 ** bits
        # 均匀量化
        x_clipped = np.clip(x, -1, 1)
        q_step = 2.0 / L
        x_quantized = np.round((x_clipped + 1) / q_step) * q_step - 1
        
        # 量化噪声
        q_noise = x_quantized - x_clipped
        noise_power = np.mean(q_noise ** 2)
        signal_power = np.mean(x_clipped ** 2)
        sqnr = 10 * np.log10(signal_power / noise_power) if noise_power > 0 else float('inf')
        sqnrs.append(sqnr)
        
        row, col = idx // 2, idx % 2
        t_short = t[:500]
        axes[row, col].plot(t_short * 1000, x_clipped[:500], 'c-', alpha=0.5, label='原始')
        axes[row, col].plot(t_short * 1000, x_quantized[:500], 'r.', markersize=2, label='量化')
        axes[row, col].set_title(f'{bits}位量化 (L={L}, SQNR={sqnr:.1f}dB)', fontsize=12)
        axes[row, col].set_xlabel('时间 (ms)')
        axes[row, col].legend(fontsize=9)
        axes[row, col].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('/var/www/ttl/digital-comm/quantization.png', dpi=100,
                facecolor='#0f172a', edgecolor='none')
    
    # 验证6.02N + 1.76公式
    print("\n量化SQNR验证:")
    print(f"{'位数':>6} {'仿真SQNR':>12} {'理论SQNR':>12} {'误差':>8}")
    for bits, sqnr_sim in zip(bit_depths, sqnrs):
        sqnr_theory = 6.02 * bits + 1.76
        print(f"{bits:>6} {sqnr_sim:>12.2f} {sqnr_theory:>12.2f} {sqnr_sim-sqnr_theory:>8.2f}")
    print("量化仿真图已保存")

def design_fir_filter():
    """设计抗混叠FIR滤波器"""
    fs = 48000       # 采样率
    f_cutoff = 8000  # 截止频率
    numtaps = 33     # 滤波器阶数
    
    # Hamming窗FIR
    coeffs = signal.firwin(numtaps, f_cutoff, fs=fs, window='hamming')
    
    # 频率响应
    w, h = signal.freqz(coeffs, fs=fs)
    
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
    
    # 幅度响应
    ax1.plot(w, 20 * np.log10(np.abs(h) + 1e-10), 'c-', linewidth=1.5)
    ax1.axvline(f_cutoff, color='r', linestyle='--', alpha=0.7, label=f'截止频率 {f_cutoff}Hz')
    ax1.axvline(fs/2, color='y', linestyle='--', alpha=0.7, label=f'Nyquist {fs/2}Hz')
    ax1.set_xlabel('频率 (Hz)')
    ax1.set_ylabel('幅度 (dB)')
    ax1.set_title('抗混叠FIR滤波器频率响应', fontsize=13)
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    ax1.set_ylim(-80, 5)
    
    # 冲激响应
    ax2.stem(range(numtaps), coeffs, linefmt='c-', markerfmt='co', basefmt='k-')
    ax2.set_xlabel('抽头序号')
    ax2.set_ylabel('系数值')
    ax2.set_title('FIR滤波器冲激响应', fontsize=13)
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('/var/www/ttl/digital-comm/fir_filter.png', dpi=100,
                facecolor='#0f172a', edgecolor='none')
    
    # 输出Verilog系数
    print("\nFIR系数 (Q1.15格式):")
    for i, c in enumerate(coeffs):
        c_fixed = int(round(c * 2**15))
        print(f"  coeffs[{i:2d}] = 16'sd{c_fixed};")
    
    print("FIR滤波器设计图已保存")
    return coeffs

def simulate_mu_law():
    """仿真μ律压扩"""
    mu = 255
    x = np.linspace(-1, 1, 1000)
    
    # μ律压缩
    y_compress = np.sign(x) * np.log(1 + mu * np.abs(x)) / np.log(1 + mu)
    
    # μ律扩展
    y_expand = np.sign(y_compress) * (1/mu) * ((1 + mu)**np.abs(y_compress) - 1)
    
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
    
    ax1.plot(x, y_compress, 'c-', linewidth=2, label=f'μ律压缩 (μ={mu})')
    ax1.plot(x, x, 'r--', alpha=0.5, label='线性')
    ax1.set_xlabel('输入幅度')
    ax1.set_ylabel('压缩输出')
    ax1.set_title('μ律压缩特性', fontsize=13)
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # 8位μ律量化的SQNR vs 信号幅度
    amplitudes = np.logspace(-3, 0, 100)
    sqnr_linear = []
    sqnr_mulaw = []
    
    for amp in amplitudes:
        t = np.arange(0, 1000) / 8000
        sig = amp * np.sin(2 * np.pi * 1000 * t)
        
        # 线性8位量化
        L = 256
        q_step = 2.0 / L
        sig_lin = np.round((np.clip(sig, -1, 1) + 1) / q_step) * q_step - 1
        n_lin = sig_lin - sig
        sqnr_l = 10 * np.log10(np.mean(sig**2) / (np.mean(n_lin**2) + 1e-20))
        sqnr_linear.append(sqnr_l)
        
        # μ律8位量化
        sig_comp = np.sign(sig) * np.log(1 + mu * np.abs(sig)) / np.log(1 + mu)
        sig_comp_q = np.round((np.clip(sig_comp, -1, 1) + 1) / q_step) * q_step - 1
        sig_exp = np.sign(sig_comp_q) * (1/mu) * ((1 + mu)**np.abs(sig_comp_q) - 1)
        n_mu = sig_exp - sig
        sqnr_m = 10 * np.log10(np.mean(sig**2) / (np.mean(n_mu**2) + 1e-20))
        sqnr_mulaw.append(sqnr_m)
    
    ax2.plot(20*np.log10(amplitudes), sqnr_linear, 'r-', label='线性8位')
    ax2.plot(20*np.log10(amplitudes), sqnr_mulaw, 'c-', linewidth=2, label='μ律8位')
    ax2.set_xlabel('信号幅度 (dB)')
    ax2.set_ylabel('SQNR (dB)')
    ax2.set_title('线性 vs μ律量化 SQNR对比', fontsize=13)
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('/var/www/ttl/digital-comm/mulaw.png', dpi=100,
                facecolor='#0f172a', edgecolor='none')
    print("μ律压扩仿真图已保存")

if __name__ == '__main__':
    print("=" * 60)
    print("采样与量化 - 全流程仿真")
    print("=" * 60)
    demonstrate_aliasing()
    simulate_quantization()
    design_fir_filter()
    simulate_mu_law()
    print("\n✅ 所有仿真完成!")
✅ Python仿真验证通过:混叠演示、量化SQNR验证、FIR滤波器设计、μ律压扩分析均正确

💡 核心要点

要点回顾:
  1. Nyquist采样定理:f_s ≥ 2f_max,否则发生混叠
  2. 量化噪声功率 = Δ²/12,SQNR = 6.02N + 1.76 dB
  3. μ律/A律压扩改善小信号SQNR,8位≈12位线性效果
  4. 抗混叠FIR滤波器在采样前滤除高频分量
  5. Verilog多通道ADC控制器实现了轮转采样和FIFO缓冲

📝 课后练习

练习1:一个4kHz语音信号,分别用8kHz和6kHz采样,结果有何不同?用Python验证。

练习2:设计一个12位ADC的量化器,计算满量程2V时的量化间隔和SQNR。

练习3:修改FIR滤波器系数,设计一个截止频率为4kHz(f_s=48kHz)的低通滤波器。

练习4:在Verilog中添加FIFO溢出检测和中断机制。

练习5:对比μ律和A律在小信号(-40dB)时的SQNR差异。

🎚️

🏆 成就解锁:数字化大师

你掌握了模拟世界与数字世界的桥梁!从Nyquist定理到量化噪声,从FIR滤波器到μ律压扩,你已经能够设计完整的ADC前端。

下一课预告:第03课将进入调制世界,学习ASK/FSK/PSK三种基本数字调制方式。