第4章 简单信道仿真实现:生成瑞利衰落、时延扩展、功率延迟分布

信道仿真这事儿,说白了就是搭一个虚拟的无线环境。你想想看,真实的信道有多复杂——信号从发射机出去,碰到高楼、树木、车辆,反射、绕射、散射全来了。我们做仿真,就是要用数学模型把这些物理现象给“算出来”。

我记得在做一个 LTE 仿真项目时,一开始直接用理想信道,结果算法性能好得离谱。等我把瑞利衰落和时延扩展加进去,性能曲线一下就“真实”了。嗯,从那以后我养成了一个习惯:仿真里一定要有信道模型,否则就是在自欺欺人。

这一章,我们就来亲手实现三个最基础也最核心的信道特性:瑞利衰落、时延扩展、功率延迟分布。这三者构成了绝大多数无线信道仿真的底座。

信道仿真实现知识体系 信道仿真实现 瑞利衰落 时延扩展 功率延迟分布 正交高斯过程 幅度与相位 多径抽头模型 指数衰减 功率归一化 RMS 时延 核心:复高斯过程 + 指数衰减多径模型

4.1 瑞利衰落仿真

瑞利衰落,说白了就是信号在密集散射环境下的幅度波动。没有直射路径,信号全靠反射和散射到达接收端。幅度服从瑞利分布,相位服从均匀分布。怎么仿真?两个正交的高斯随机过程一组合,就成了。

我个人的习惯是先写一个极简版本:

import numpy as np

def generate_rayleigh(N):
    """
    生成 N 个瑞利衰落样本
    返回幅度和相位
    """
    # 两个正交的高斯过程
    x = np.random.randn(N)
    y = np.random.randn(N)
    # 归一化功率
    h = (x + 1j * y) / np.sqrt(2)
    # 幅度: 瑞利分布
    magnitude = np.abs(h)
    # 相位: 均匀分布
    phase = np.angle(h)
    return h, magnitude, phase
小技巧:除以 √2 是为了让平均功率为 1。我见过不少新手忘了这步,结果信道功率偏大或偏小,后面均衡器怎么调都调不对。

你看,核心就几行代码。但这里有个坑——随机种子。我曾经因为没固定种子,每次仿真的信道都不一样,算法的性能曲线抖得跟心电图似的。仿真时务必用 np.random.seed() 固定种子,否则你没法复现结果。

4.2 时延扩展仿真

多径环境里,信号走不同的路径到达时间不一样,这就是时延扩展。仿真时我们用 多径抽头模型 来表示:每条径有一个时延和一个功率衰减系数。

最常用的模型是指数衰减——功率随延时指数下降。为什么会这样?你想想看,路径越长,反射次数越多,能量损失就越大。

def generate_delays(num_taps, tau_rms, fs):
    """
    生成多径时延与功率
    num_taps : 抽头数
    tau_rms  : 均方根时延扩展(秒)
    fs       : 采样率(Hz)
    """
    # 时延,从 0 开始等间隔
    delays = np.arange(num_taps) / fs
    # 指数衰减
    powers = np.exp(-delays / tau_rms)
    # 归一化
    powers = powers / np.sum(powers)
    return delays, powers
注意:抽头数不要设太少,否则时延扩展体现不出来。我一般设 6~12 个抽头,具体看信道带宽和时延扩展量级。

tau_rms 这个参数怎么取?城区环境一般在 0.5~2 μs,郊区小一些,室内更小。我在项目中一般先查 3GPP 标准里的典型值,再根据实测数据微调。千万别拍脑袋乱设,否则仿真结果跟实测对不上。

4.3 功率延迟分布

功率延迟分布(PDP)描述的是不同延时路径上的功率分布情况。它直接决定了信道的频率选择性——PDP 越宽,频率选择性越强。

其实上一节生成的 powers 数组就是 PDP 的原始形式,只不过我们需要把它做进一步处理。下面这个表格列出了 PDP 的几个关键参数:

参数 符号 说明 典型取值
均方根时延扩展 τRMS PDP 的二阶中心矩的平方根 0.1~3 μs
平均时延 τmean PDP 的一阶矩 0.1~1 μs
最大时延 τmax 功率降到阈值以下的最后一个时延 1~10 μs
抽头数 L PDP 中有效的多径分量数 4~12

计算 PDP 的代码很简单,但要注意归一化:

def compute_pdp(delays, powers, fs):
    """
    计算功率延迟分布
    返回归一化 PDP 和累积功率
    """
    # 归一化总功率为 1
    pdp = powers / np.sum(powers)
    # 累积分布,用来判断时延扩展
    cum_power = np.cumsum(pdp)
    # 找到包含 90% 功率的时延区间
    idx_90 = np.where(cum_power >= 0.9)[0][0]
    t_90 = delays[idx_90]
    return pdp, cum_power, t_90
重点:归一化后的 PDP 总功率为 1,这样后面叠加噪声时 SNR 才好控制。我在项目中吃过亏——PDP 没归一化,结果信噪比算出来全是错的,均衡器参数怎么调都不收敛。

4.4 综合仿真实现

把三个模块串起来,就是一个完整的信道仿真器。下面这个函数把信号通过多径信道,每条径独立衰落,最后叠加输出:

def channel_simulator(signal, fs, num_taps, tau_rms):
    """
    完整信道仿真
    signal  : 发送信号(一维数组)
    fs      : 采样率
    num_taps: 多径抽头数
    tau_rms : RMS 时延扩展
    返回: 接收信号, 信道矩阵, PDP
    """
    N = len(signal)
    
    # 1. 生成 PDP
    delays, powers = generate_delays(num_taps, tau_rms, fs)
    pdp, _, _ = compute_pdp(delays, powers, fs)
    
    # 2. 每条径独立瑞利衰落
    h = np.zeros((num_taps, N), dtype=complex)
    for tap in range(num_taps):
        h[tap, :] = generate_rayleigh(N)[0] * np.sqrt(pdp[tap])
    
    # 3. 时延扩展:通过 FIR 滤波实现
    output = np.zeros(N, dtype=complex)
    for tap in range(num_taps):
        delayed = np.roll(signal, tap)
        delayed[:tap] = 0  # 去掉循环移位导致的头部数据
        output += delayed * h[tap, :]
    
    return output, h, pdp

使用的时候,你只需要调用 channel_simulator(signal, fs=1e6, num_taps=8, tau_rms=1e-6),就能得到一个经过真实信道污染的接收信号。我一般在跑 BER 仿真之前,先用这个函数把信道冲激响应打出来看一眼,确认 PDP 形状和衰落深度符合预期。

避坑指南:np.roll 会导致循环移位,前面补零要处理干净。我曾经漏了这一步,结果仿真出来的信道变成了“循环卷积”,自相关函数完全不对。排查了两天才发现问题。

说白了,信道仿真就是把物理世界的传播现象用数学语言翻译出来。瑞利衰落管幅度波动,时延扩展管多径结构,功率延迟分布管能量分配。这三样东西抓准了,你的仿真模型就八九不离十了。

嗯,我个人觉得,与其抄一堆复杂的信道模型代码,不如先把这三个基础模块亲手实现一遍。你把它们调通了、调透了,后面学 3GPP 的 CDL、TDL 模型,会发现其实就是这些基础模块的排列组合。