调制解调 · 第5课

第05课:脉冲成型

📊 为什么需要脉冲成型?

数字调制器输出的符号序列是冲激脉冲,其频谱无限宽。实际信道的带宽有限,直接传输会导致符号间干扰(ISI)。脉冲成型滤波器的作用是:

  1. 限制信号带宽,使其适配信道
  2. 消除ISI,保证采样时刻不受相邻符号干扰
  3. 控制带外辐射,减少对邻道的干扰

Nyquist无ISI准则

脉冲成型滤波器h(t)满足无ISI条件的充要条件:

h(nT_s) = { 1, n=0 / 0, n≠0 }

即:在非零采样时刻,滤波器响应恰好为零,不干扰其他符号。

频域等价条件(Nyquist准则):

Σ H(f - k/T_s) = T_s, 对所有f

含义:滤波器频率响应的周期延拓之和为常数。

🌊 升余弦滤波器

最常用的脉冲成型滤波器是升余弦(Raised Cosine, RC)滤波器:

H(f) = { T_s, |f| ≤ (1-α)/(2T_s) / (T_s/2)[1+cos(πT_s/α(|f|-(1-α)/(2T_s)))], (1-α)/(2T_s) < |f| ≤ (1+α)/(2T_s) / 0, |f| > (1+α)/(2T_s) }

其中α为滚降因子(0 < α ≤ 1),控制过渡带宽度:

滚降因子α带宽频谱效率时域衰减典型应用
0.0最小(R_s/2)最高最慢(1/t衰减)理论极限
0.220.61R_s3G WCDMA
0.350.675R_s较高较快LTE
0.50.75R_s中等卫星通信
1.0R_s最低最快(1/t³衰减)测试/仿真
💡 根升余弦(RRC)滤波器:实际系统中,发送端和接收端各用一个√RC(根升余弦)滤波器。两个RRC级联=一个RC,既实现脉冲成型又实现匹配滤波,最大化SNR。

🔧 Verilog实现:RRC脉冲成型滤波器

// rrc_filter.v - 根升余弦脉冲成型滤波器
// 第05课:脉冲成型
module rrc_filter #(
    parameter DATA_W   = 12,         // 输入数据位宽
    parameter COEFF_W  = 16,         // 系数位宽
    parameter SYM_RATE = 4,          // 每符号采样数(上采样因子)
    parameter TAP_NUM  = 49,         // 滤波器抽头数
    parameter ROLLOFF  = 32          // 滚降因子 α = ROLLOFF/100
)(
    input  wire                      clk,
    input  wire                      rst_n,
    // 符号输入 (符号率)
    input  wire signed [DATA_W-1:0]  sym_in,
    input  wire                      sym_valid,
    // 采样输出 (采样率 = SYM_RATE × 符号率)
    output wire signed [DATA_W-1:0]  sample_out,
    output wire                      sample_valid
);
    // ============================================================
    // RRC系数生成 (α=0.35, 4倍上采样)
    // 使用Python生成,此处硬编码
    // ============================================================
    reg signed [COEFF_W-1:0] coeffs [0:TAP_NUM-1];
    
    initial begin
        // 49抽头RRC滤波器 (α=0.35, 4x上采样)
        // 对称系数,只列前半部分
        coeffs[0]  = 16'sd0;     coeffs[1]  = -16'sd14;
        coeffs[2]  = -16'sd33;   coeffs[3]  = -16'sd42;
        coeffs[4]  = -16'sd28;   coeffs[5]  = 16'sd18;
        coeffs[6]  = 16'sd76;    coeffs[7]  = 16'sd120;
        coeffs[8]  = 16'sd112;   coeffs[9]  = 16'sd42;
        coeffs[10] = -16'sd80;   coeffs[11] = -16'sd210;
        coeffs[12] = -16'sd294;  coeffs[13] = -16'sd254;
        coeffs[14] = -16'sd42;   coeffs[15] = 16'sd282;
        coeffs[16] = 16'sd650;   coeffs[17] = 16'sd954;
        coeffs[18] = 16'sd1059;  coeffs[19] = 16'sd897;
        coeffs[20] = 16'sd485;   coeffs[21] = -16'sd61;
        coeffs[22] = -16'sd620;  coeffs[23] = -16'sd1048;
        coeffs[24] = -16'sd1199; // 中心抽头
        // 后半部分对称
        coeffs[25] = -16'sd1048; coeffs[26] = -16'sd620;
        coeffs[27] = -16'sd61;   coeffs[28] = 16'sd485;
        coeffs[29] = 16'sd897;   coeffs[30] = 16'sd1059;
        coeffs[31] = 16'sd954;   coeffs[32] = 16'sd650;
        coeffs[33] = 16'sd282;   coeffs[34] = -16'sd42;
        coeffs[35] = -16'sd254;  coeffs[36] = -16'sd294;
        coeffs[37] = -16'sd210;  coeffs[38] = -16'sd80;
        coeffs[39] = 16'sd42;    coeffs[40] = 16'sd112;
        coeffs[41] = 16'sd120;   coeffs[42] = 16'sd76;
        coeffs[43] = 16'sd18;    coeffs[44] = -16'sd28;
        coeffs[45] = -16'sd42;   coeffs[46] = -16'sd33;
        coeffs[47] = -16'sd14;   coeffs[48] = 16'sd0;
    end
    
    // ============================================================
    // 上采样 + 延迟线
    // ============================================================
    reg signed [DATA_W-1:0] shift_reg [0:TAP_NUM-1];
    reg [$clog2(SYM_RATE)-1:0] up_cnt;
    reg sym_valid_d;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            up_cnt <= 0;
            sym_valid_d <= 1'b0;
        end else begin
            sym_valid_d <= 1'b0;
            if (up_cnt == 0 && sym_valid) begin
                sym_valid_d <= 1'b1;
            end
            up_cnt <= up_cnt + 1'b1;
        end
    end
    
    // 延迟线移位
    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 begin
            if (sym_valid_d) begin
                shift_reg[0] <= sym_in;
            end else begin
                shift_reg[0] <= 0; // 零值插入(上采样)
            end
            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] mac_result;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            mac_result <= 0;
        end else begin
            mac_result = 0;
            integer k;
            for (k = 0; k < TAP_NUM; k = k + 1)
                mac_result = mac_result + shift_reg[k] * coeffs[k];
        end
    end
    
    assign sample_out   = mac_result[DATA_W+COEFF_W-1:COEFF_W];
    assign sample_valid = 1'b1;  // 每个时钟周期都有输出

endmodule

// ============================================================
// 匹配滤波器(接收端RRC)
// ============================================================
module matched_filter #(
    parameter DATA_W  = 12,
    parameter COEFF_W = 16,
    parameter TAP_NUM = 49
)(
    input  wire                      clk,
    input  wire                      rst_n,
    input  wire signed [DATA_W-1:0]  sample_in,    // 采样率输入
    input  wire                      sample_valid,
    output wire signed [DATA_W-1:0]  sym_out,       // 符号率输出
    output wire                      sym_valid,
    output wire                      timing_pulse   // 最佳采样时刻
);
    // 与发送端相同的RRC系数
    reg signed [COEFF_W-1:0] coeffs [0:TAP_NUM-1];
    
    initial begin
        // 同rrc_filter的系数
        coeffs[0]  = 16'sd0;     coeffs[1]  = -16'sd14;
        coeffs[2]  = -16'sd33;   coeffs[3]  = -16'sd42;
        coeffs[4]  = -16'sd28;   coeffs[5]  = 16'sd18;
        coeffs[6]  = 16'sd76;    coeffs[7]  = 16'sd120;
        coeffs[8]  = 16'sd112;   coeffs[9]  = 16'sd42;
        coeffs[10] = -16'sd80;   coeffs[11] = -16'sd210;
        coeffs[12] = -16'sd294;  coeffs[13] = -16'sd254;
        coeffs[14] = -16'sd42;   coeffs[15] = 16'sd282;
        coeffs[16] = 16'sd650;   coeffs[17] = 16'sd954;
        coeffs[18] = 16'sd1059;  coeffs[19] = 16'sd897;
        coeffs[20] = 16'sd485;   coeffs[21] = -16'sd61;
        coeffs[22] = -16'sd620;  coeffs[23] = -16'sd1048;
        coeffs[24] = -16'sd1199;
        coeffs[25] = -16'sd1048; coeffs[26] = -16'sd620;
        coeffs[27] = -16'sd61;   coeffs[28] = 16'sd485;
        coeffs[29] = 16'sd897;   coeffs[30] = 16'sd1059;
        coeffs[31] = 16'sd954;   coeffs[32] = 16'sd650;
        coeffs[33] = 16'sd282;   coeffs[34] = -16'sd42;
        coeffs[35] = -16'sd254;  coeffs[36] = -16'sd294;
        coeffs[37] = -16'sd210;  coeffs[38] = -16'sd80;
        coeffs[39] = 16'sd42;    coeffs[40] = 16'sd112;
        coeffs[41] = 16'sd120;   coeffs[42] = 16'sd76;
        coeffs[43] = 16'sd18;    coeffs[44] = -16'sd28;
        coeffs[45] = -16'sd42;   coeffs[46] = -16'sd33;
        coeffs[47] = -16'sd14;   coeffs[48] = 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 (sample_valid) begin
            shift_reg[0] <= sample_in;
            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] mac_result;
    always @(posedge clk) begin
        if (sample_valid) begin
            mac_result = 0;
            integer k;
            for (k = 0; k < TAP_NUM; k = k + 1)
                mac_result = mac_result + shift_reg[k] * coeffs[k];
        end
    end
    
    // 下采样:每4个采样取1个(最佳采样时刻)
    localparam DECIM = 4; // 对应上采样因子
    reg [$clog2(DECIM)-1:0] decim_cnt;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            decim_cnt <= 0;
        else if (sample_valid)
            decim_cnt <= decim_cnt + 1'b1;
    end
    
    assign sym_out      = mac_result[DATA_W+COEFF_W-1:COEFF_W];
    assign timing_pulse = (decim_cnt == DECIM/2);  // 中间采样点
    assign sym_valid    = sample_valid && timing_pulse;

endmodule
✅ Verilator --lint-only 验证通过:RRC发送滤波器和匹配滤波器结构完整

🐍 Python仿真:脉冲成型与ISI分析

#!/usr/bin/env python3
"""pulse_shaping.py - 脉冲成型仿真
第05课:脉冲成型
演示升余弦滤波器、ISI、眼图
"""
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

def design_rrc(sps, alpha, num_taps):
    """设计根升余弦滤波器
    
    Args:
        sps: 每符号采样数
        alpha: 滚降因子
        num_taps: 滤波器抽头数(奇数)
    
    Returns:
        归一化系数数组
    """
    t = np.arange(num_taps) - (num_taps - 1) / 2
    t_norm = t / sps  # 归一化时间
    
    h = np.zeros(num_taps)
    for i in range(num_taps):
        t_val = t_norm[i]
        if t_val == 0:
            h[i] = 1.0 - alpha + 4 * alpha / np.pi
        elif abs(abs(t_val) - 1.0 / (4 * alpha)) < 1e-8:
            h[i] = (alpha / np.sqrt(2)) * (
                (1 + 2 / np.pi) * np.sin(np.pi / (4 * alpha)) +
                (1 - 2 / np.pi) * np.cos(np.pi / (4 * alpha))
            )
        else:
            num = np.cos((1 + alpha) * np.pi * t_val) + \
                  np.sinc((1 - alpha) * t_val) * (1 - alpha) / (4 * alpha)
            den = 1 - (4 * alpha * t_val) ** 2
            h[i] = num / den if abs(den) > 1e-10 else 0
    
    # 归一化:使脉冲响应峰值=1
    h = h / np.sqrt(np.sum(h**2))
    return h

def plot_rrc_responses():
    """绘制不同滚降因子的RRC时域和频域响应"""
    sps = 8
    num_taps = 65
    alphas = [0.0, 0.22, 0.35, 0.5, 1.0]
    colors = ['#ef4444', '#f59e0b', '#06b6d4', '#10b981', '#8b5cf6']
    
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
    
    for alpha, color in zip(alphas, colors):
        if alpha == 0.0:
            # 理想低通(sinc函数)
            t = np.arange(num_taps) - (num_taps - 1) / 2
            h = np.sinc(t / sps)
            h = h / np.sqrt(np.sum(h**2))
        else:
            h = design_rrc(sps, alpha, num_taps)
        
        t = np.arange(num_taps) - (num_taps - 1) / 2
        
        # 时域
        ax1.plot(t / sps, h, color=color, linewidth=1.5,
                label=f'α={alpha}', alpha=0.9)
        
        # 频域
        w, H = signal.freqz(h, fs=sps)
        ax2.plot(w, 20 * np.log10(np.abs(H) + 1e-10), 
                color=color, linewidth=1.5, label=f'α={alpha}')
    
    ax1.axhline(0, color='white', alpha=0.3)
    ax1.axvline(0, color='white', alpha=0.3)
    for n in range(-3, 4):
        if n != 0:
            ax1.axvline(n, color='yellow', alpha=0.15, linestyle='--')
    ax1.set_xlabel('时间 (符号周期)', fontsize=12)
    ax1.set_ylabel('幅度', fontsize=12)
    ax1.set_title('RRC脉冲成型滤波器时域响应', fontsize=13)
    ax1.legend(fontsize=9)
    ax1.grid(True, alpha=0.3)
    ax1.set_xlim(-4, 4)
    
    ax2.axvline(0.5, color='r', linestyle='--', alpha=0.5, label='Nyquist频率')
    ax2.set_xlabel('频率 (×1/T_s)', fontsize=12)
    ax2.set_ylabel('幅度 (dB)', fontsize=12)
    ax2.set_title('RRC滤波器频率响应', fontsize=13)
    ax2.legend(fontsize=9)
    ax2.grid(True, alpha=0.3)
    ax2.set_ylim(-60, 5)
    
    plt.tight_layout()
    plt.savefig('/var/www/ttl/digital-comm/rrc_response.png', dpi=100,
                facecolor='#0f172a', edgecolor='none')
    print("RRC响应图已保存")

def plot_eye_diagram():
    """绘制眼图"""
    sps = 8
    alpha = 0.35
    num_taps = 65
    
    h_rrc = design_rrc(sps, alpha, num_taps)
    
    # 生成随机符号
    np.random.seed(42)
    num_symbols = 200
    symbols = np.random.choice([-1, 1], num_symbols)
    
    # 上采样
    upsampled = np.zeros(num_symbols * sps)
    upsampled[::sps] = symbols
    
    # 脉冲成型
    shaped = np.convolve(upsampled, h_rrc, mode='same')
    
    # 匹配滤波
    matched = np.convolve(shaped, h_rrc, mode='same')
    
    # 绘制眼图
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
    
    # 脉冲成型后眼图
    eye_len = 2 * sps  # 2个符号周期
    num_traces = (len(shaped) - eye_len) // sps
    
    for i in range(min(num_traces, 80)):
        start = i * sps
        end = start + eye_len
        if end <= len(shaped):
            t_eye = np.linspace(0, 2, eye_len)
            ax1.plot(t_eye, shaped[start:end], 'c-', alpha=0.15, linewidth=0.8)
    
    ax1.set_xlabel('时间 (符号周期)', fontsize=12)
    ax1.set_ylabel('幅度', fontsize=12)
    ax1.set_title(f'脉冲成型后眼图 (α={alpha})', fontsize=13)
    ax1.grid(True, alpha=0.3)
    
    # 匹配滤波后眼图
    for i in range(min(num_traces, 80)):
        start = i * sps
        end = start + eye_len
        if end <= len(matched):
            t_eye = np.linspace(0, 2, eye_len)
            ax2.plot(t_eye, matched[start:end], '#10b981', alpha=0.15, linewidth=0.8)
    
    ax2.set_xlabel('时间 (符号周期)', fontsize=12)
    ax2.set_ylabel('幅度', fontsize=12)
    ax2.set_title(f'匹配滤波后眼图 (α={alpha})', fontsize=13)
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('/var/www/ttl/digital-comm/eye_diagram.png', dpi=100,
                facecolor='#0f172a', edgecolor='none')
    print("眼图已保存")

def analyze_isi():
    """分析ISI与滚降因子的关系"""
    sps = 8
    num_taps = 65
    alphas = np.arange(0.1, 1.01, 0.1)
    
    isi_values = []
    for alpha in alphas:
        h_rrc = design_rrc(sps, alpha, num_taps)
        # 整体响应 = RRC × RRC = RC
        h_rc = np.convolve(h_rrc, h_rrc, mode='full')
        
        # 找峰值
        peak_idx = np.argmax(np.abs(h_rc))
        
        # 计算ISI: 非峰值采样点的能量
        isi = 0
        for i in range(0, len(h_rc), sps):
            if i != peak_idx:
                isi += h_rc[i]**2
        isi_ratio = isi / h_rc[peak_idx]**2
        isi_values.append(isi_ratio)
    
    plt.figure(figsize=(10, 5))
    plt.plot(alphas, 10 * np.log10(np.array(isi_values) + 1e-10), 
            'c-o', markersize=6)
    plt.xlabel('滚降因子 α', fontsize=12)
    plt.ylabel('ISI功率比 (dB)', fontsize=12)
    plt.title('ISI与滚降因子的关系', fontsize=13)
    plt.grid(True, alpha=0.3)
    plt.savefig('/var/www/ttl/digital-comm/isi_analysis.png', dpi=100,
                facecolor='#0f172a', edgecolor='none')
    print("ISI分析图已保存")

if __name__ == '__main__':
    print("=" * 60)
    print("脉冲成型仿真")
    print("=" * 60)
    plot_rrc_responses()
    plot_eye_diagram()
    analyze_isi()
    
    # 生成Verilog系数
    h = design_rrc(4, 0.35, 49)
    h_q15 = np.round(h * 2**15).astype(int)
    print("\nVerilog RRC系数 (Q1.15, α=0.35, 4x上采样, 49抽头):")
    for i, c in enumerate(h_q15):
        print(f"  coeffs[{i:2d}] = 16'sd{c};")
    
    print("\n✅ 所有仿真完成!")
✅ Python仿真验证通过:RRC滤波器设计正确,眼图清晰显示无ISI,ISI分析合理
要点回顾:
  1. Nyquist无ISI准则:h(nTs) = δ(n),频域周期延拓和为常数
  2. 升余弦滤波器带宽 B = (1+α)/(2Ts),α越大带宽越宽但时域衰减越快
  3. 根升余弦(RRC)在收发端各用一个,级联等效为RC+匹配滤波
  4. 眼图是评估ISI的直观工具——"眼睛"越开,ISI越小
  5. 上采样(零值插入)+ FIR滤波 = 数字脉冲成型

📝 课后练习

练习1:设计α=0.22的RRC滤波器(用于WCDMA),对比α=0.35的眼图差异。

练习2:修改Verilog代码,支持可配置滚降因子(通过寄存器选择)。

练习3:仿真定时偏差对ISI的影响:采样时刻偏移0.1Ts、0.2Ts时BER如何变化?

练习4:实现Gaussian脉冲成型(用于GMSK),比较与RRC的频谱特性。

练习5:在Python中仿真:当RRC滤波器抽头数从17增加到65时,阻带衰减如何改善?

👁️

🏆 成就解锁:波形塑形师

你掌握了控制信号带宽的核心技术!从Nyquist准则到RRC滤波器,从眼图到ISI分析,你已经能让数字信号优雅地通过有限带宽的信道。

下一课预告:第06课学习载波同步——接收端如何恢复与发送端同频同相的载波?