第4章 简单信道仿真实现:生成瑞利衰落、时延扩展、功率延迟分布
信道仿真这事儿,说白了就是搭一个虚拟的无线环境。你想想看,真实的信道有多复杂——信号从发射机出去,碰到高楼、树木、车辆,反射、绕射、散射全来了。我们做仿真,就是要用数学模型把这些物理现象给“算出来”。
我记得在做一个 LTE 仿真项目时,一开始直接用理想信道,结果算法性能好得离谱。等我把瑞利衰落和时延扩展加进去,性能曲线一下就“真实”了。嗯,从那以后我养成了一个习惯:仿真里一定要有信道模型,否则就是在自欺欺人。
这一章,我们就来亲手实现三个最基础也最核心的信道特性:瑞利衰落、时延扩展、功率延迟分布。这三者构成了绝大多数无线信道仿真的底座。
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
你看,核心就几行代码。但这里有个坑——随机种子。我曾经因为没固定种子,每次仿真的信道都不一样,算法的性能曲线抖得跟心电图似的。仿真时务必用 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
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
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 模型,会发现其实就是这些基础模块的排列组合。