简介:面向码分多址通信与移动通信课程学习者,这份资源聚焦IS-95前向链路系统仿真实现,完整覆盖扩频调制、伪随机序列生成、匹配滤波、软硬判决译码及RAKE接收分集等关键模块,适合需要理解第三代移动通信基础理论或复现链路仿真的学生与研究人员。压缩包共13个文件,含10个MATLAB脚本、2个仿真结果数据文件和1份说明文档,整体大小3.83MB,脚本覆盖调制、解调、维特比译码、RAKE合并等子功能,文档辅助梳理算法流程,目录分类存放便于检索。已有148人浏览学习。通过运行主仿真脚本,可直观观察直接序列扩频、多径合并和高斯白噪声信道下的误码率表现,也能结合文档核对算法实现,支持调整参数以对比不同信噪比下的接收性能,用于课程设计或毕业设计参考,价值较高。
1. IS95前向链路系统到底在仿真什么:一个RAKE接收机的最低可用闭环
第一次拿到 IS95前向链路系统这套仿真时,大多数人的关注点是 CDMA 的扩频与解扩,真正拉开差距的,是一个能处理多径的 RAKE 接收机。IS-95 前向链路之所以把导频信道单独设出来,就是为了让接收端可以从多条时延路径里分别做信道估计,再把能量合并起来。对刚接触无线物理层仿真的从业者而言,这套系统最适合用来回答三个问题:正交 Walsh 码为什么在前向链路里成立;多径为什么不全都是坏事;以及 RAKE 的合并增益到底是怎么算出来的。下面不展开协议帧格式,按发射端、信道、接收端 RAKE 这条链,把可复现的仿真结构、关键参数和容易踩的坑一次讲透。
2. 前向链路信道结构与RAKE接收机原理:先从码域和多径说起
2.1 四种信道:导频、同步、寻呼、业务,谁的功率大
IS-95 前向链路是从基站到手机的方向,采用 1.2288 Mcps 码片速率,信道化用的是 64 阶 Walsh 码,也就是每个 Walsh 码字占用 64 个码片来表示一个符号。码域上被分成若干并行信道:导频信道固定用 W0,同步信道用 W32,寻呼信道常用 W1,其余编号分给业务信道。一个仿真里最容易被忽略的动作,是给这些信道分配发射功率。导频不是“加一点意思一下”,它的功率占比直接影响 RAKE 的信道估计质量。
在前向链路里,导频信道通常占总功率的 15% 到 20%,同步信道和寻呼信道各占几个百分点,剩余功率几乎全部给业务信道。导频功率占比过小,手机在小区边缘就搜不到你,RAKE 合并时的信道估计会跟着抖动;占比过大,业务信道被压低,用户速率和容量都上不去。仿真参数表里常见的做法是设置“导频增益”而不仅仅是“导频功率占比”,因为代码里经常拿幅度相加,幅度和功率差着一个平方关系。下表是我习惯上先填入仿真的一组基准值,实际项目里应根据覆盖场景再调。
| 信道 | Walsh 码字 | 典型功率占比 | 在 RAKE 里的用途 |
|---|---|---|---|
| 导频 | W0 | 15% ~ 20% | 路径搜索、时延锁定、信道估计 |
| 同步 | W32 | 2% ~ 5% | 帧定时和系统参数 |
| 寻呼 | W1 | 5% ~ 10% | 呼叫控制 |
| 业务 | 按分配 | 剩余功率 | 语音/数据复用 |
功率分配在仿真里也不是一次性到位的。先固定导频和业务的最小幅度,把其他信道全部暂时关掉,等 RAKE 把单用户链路跑通,再逐步加入同步、寻呼和其他业务信道。前向链路正交性的前提是所有信道码片严格对齐,这个对齐在仿真里靠 Walsh 矩阵生成顺序和切片位置保证,一旦 Walsh 编号写错,后续所有解扩都会跟着错。
2.2 RAKE的三个手指:相关器、搜索器、合并器
RAKE 接收机名字很形象,但“手指”不是物理天线,而是接收端里一组并行工作的相关器。常见配置是三个手指,每个手指负责锁定一条多径:先让本地短 PN 码对准这条多径的时延,然后用对应的 Walsh 码做解扩,得到一个带信道幅相信息的数据符号,最后由合并器把这些符号做最大比合并。
如果只讲数学,一条多径的基带形式可以写成 h_l·x(t-τ_l)。三根手指做的事情就是分别估算 (h_0, τ_0)、(h_1, τ_1)、(h_2, τ_2),再把解扩后的结果按权重叠加。最大比合并的标准形式是 y = Σ h_l^* · r_l,这里 h_l 来自导频信道估计,r_l 是业务信道解扩后的符号。很多第一次做仿真的人把“合并”理解成直接把几个相关性求和,忽略权重中的共轭,导致相位没有对齐,合出来还不如单径。
搜索器是 RAKE 里容易被忽略的模块。接收端不可能知道多径时延是多少,需要先用导频信道做滑动相关,找到相关峰的位置。前向链路因为所有基站短 PN 码相同,只是相位偏移不同,搜索器要处理的时延范围通常不会太大,一段 64 到 256 码片的搜索窗就够。仿真里如果直接用已知时延喂给接收机,能快速验证算法,但做完一定要回到搜索器结构,否则你看不到“路径分配”这个环节给实际接收机带来的差异。
2.3 为什么前向链路比反向链路更适合用RAKE
IS-95 反向链路是多用户共享同一频率,每个用户由长 PN 码区分,基站接收端需要做非相干或导频辅助解调,RAKE 也能用,但训练和功率控制限制更多。前向链路则天然适合 RAKE,原因是基站只发一套公共导频,所有用户共享,手机的 RAKE 可以直接拿导频做相干信道估计。同时前向链路靠短 PN 码加 Walsh 码区分信道,只要路径时延对准,路径间正交性就能恢复。
多径在这个系统里实际上是资源,不是纯粹的干扰。没有 RAKE 时,多径会造成符号间干扰;有 RAKE 时,每条路径都被解扩出来,变成对判决有用的能量。这也是为什么在 IS95 前向链路仿真里,增加多径数量经常能提升 BER,而不是恶化 BER,前提是 RAKE 的搜索器和 finger 数量跟得上。理解了这一点,再看后面仿真里的各种“诡异”现象就会轻松一些。
3. 把IS95前向链路系统跑起来:最小可复现的Python仿真
3.1 参数清单:码片速率、Walsh长度、RAKE路数
一套能说明问题的前向链路仿真不需要把 IS-95 协议全部实现。我习惯先按物理层最小闭环来搭:一个业务信道、一个导频信道、三条多径、一个三 finger RAKE。先把这套基带数字模型跑通,再往里面补同步、寻呼、功率控制、帧结构。下面是仿真里的基础参数,可以直接抄走。
| 参数 | 典型值 | 本仿真取值 | 说明 |
|---|---|---|---|
| 码片速率 | 1.2288 Mcps | 1.2288e6 | IS-95 固定值,仿真可归一化 |
| Walsh 长度 | 64 | 64 | 前向链路信道化码长 |
| 业务 Walsh 编号 | 10 | 10 | 不与导频、寻呼冲突即可 |
| 导频增益 | 幅度比 0.2~0.5 | 0.3 | 对应功率占比约 9%,更接近低导频配置 |
| 多径时延 | 0~5 码片 | [0, 1, 3] | 城市环境典型时延扩展 |
| 多径幅度 | 随距离衰减 | [0.9, 0.45, 0.25] | 幅度比,不是功率比 |
| RAKE finger 数 | 2~4 | 3 | 与多径数保持一致 |
码片速率在 Python 仿真里通常不需要真的每秒生成 1.2288e6 个点,关键是把码片速率和时间关系保持正确。比如 64 个码片承载一个符号,符号周期就是 64 个码片;多径时延写成“几个码片”,而不是绝对时间,后面调整时延扩展时更直观。先用随机数据跑,不能直接说明 BER 就达标,但能验证所有模块连接是否完整。
3.2 发射端代码:利用Walsh正交性与PN扩频
发射端先做 Walsh 正交调制,再做 PN 扩频。注意 Walsh 码和 PN 码是两次不同的操作:Walsh 用来区分前向信道,PN 用来区分基站相位并抹平频谱。仿真里最容易出错的是把两者顺序颠倒,或者把 Walsh 码当成扩频码。下面这段是发射端的核心代码。
import numpy as np SF = 64 # Walsh 码长 NUM_SYM = 800 # 仿真符号数 N_CHIPS = SF * NUM_SYM # 总码片数 TRAFFIC_WALSH = 10 # 业务信道使用的 Walsh 编号 PILOT_GAIN = 0.3 # 导频幅度增益 # 生成 Hadamard-Walsh 矩阵 walsh = np.array([[1, 1], [1, -1]]) while walsh.shape[0] < SF: walsh = np.block([[walsh, walsh], [walsh, -walsh]]) w_pilot = walsh[0] # 导频信道 W0,全 1 w_traf = walsh[TRAFFIC_WALSH] # 业务信道 # 简化版 15 位 m 序列,用于仿真;与 IS-95 标准抽头不同, # 但结构一致:发射端和接收端共用同一个序列即可复现 def lfsr_pn(nchips, seed=0x7fff): reg = seed out = np.zeros(nchips, dtype=np.int8) for n in range(nchips): out[n] = reg & 1 # m 序列反馈抽头,15 位长度 fb = ((reg >> 14) & 1) ^ ((reg >> 13) & 1) reg = ((reg << 1) | fb) & 0x7fff return 2.0 * out - 1.0 pn = lfsr_pn(N_CHIPS) rng = np.random.default_rng(42) bit = rng.choice([-1.0, 1.0], size=NUM_SYM) # 发射基带:业务信道 + 导频信道 tx_traffic = np.zeros(N_CHIPS) tx_pilot = np.zeros(N_CHIPS) for i, b in enumerate(bit): base = i * SF tx_traffic[base:base+SF] = b * w_traf tx_pilot[base:base+SF] = w_pilot # 导频符号固定为 +1 # 叠加导频并按总功率归一化 tx_unscaled = tx_traffic + PILOT_GAIN * tx_pilot tx_unscaled = tx_unscaled / np.sqrt(np.mean(tx_unscaled**2)) tx = tx_unscaled * pn # PN 扩频逻辑说明:tx_traffic把每个业务符号先复制成 64 个码片,再逐位乘上 Walsh 码字;tx_pilot直接铺 W0。W0 全 1,所以导频部分没有编码数据,只提供参考相位。tx_unscaled做完功率归一化后,再乘 PN 序列,这一步必须放在 Walsh 调制之后。参数说明:PILOT_GAIN=0.3是幅度,0.3平方对应约 9% 的功率占比;如果项目里要求导频功率 20%,幅度应设置为np.sqrt(0.2)左右,不要直接用功率值加在幅度上。
这段里的 PN 用的是简化 m 序列,不是 IS-95 标准短码。标准的 15 位短码还包含一个特殊补零逻辑,周期接近 2^15 码片,仿真里用同样结构的 PRBS 足以验证 RAKE 算法。真正做设备级仿真时,再把生成多项式换成 3GPP2 文档里的抽头定义,其他代码结构不用变。
3.3 多径信道代码:给RAKE提供分集素材
信道模块把发射码片序列分别延迟、衰减、叠加,再加入白噪声。多径延迟用码片整数倍来切,是最直接的做法;实际信道里时延不是码片整数倍,通常还要过一组分数时延滤波器或重采样器。仿真初期用整数倍最容易排错,跑通后再换分数时延,效果差异主要反映在 finger 抽头位置的精度上。
# 多径条件:时延单位是码片,幅度是线性幅度 DELAYS = [0, 1, 3] GAINS = [0.9, 0.45, 0.25] rx = np.zeros(N_CHIPS) for d, g in zip(DELAYS, GAINS): if d == 0: rx += g * tx else: rx[d:] += g * tx[:-d] # 右移 d 个码片,等价于时延 # 加白噪声:这里按码片级 SNR 粗略设置 SNR_DB = 2.0 noise_power = np.mean(rx**2) / (10 ** (SNR_DB / 10)) rx += np.sqrt(noise_power) * rng.standard_normal(N_CHIPS)逻辑说明:rx[d:] += g * tx[:-d]是纯 Python 数组右移,表示路径 d 的输出到达时间比主径晚 d 个码片。写这段时要注意发射序列末尾的截断,真实仿真里应在帧头留一段保护间隔,否则最后一条路径会截掉尾部符号。参数说明:GAINS是幅度,不是功率,三路功率关系是 0.9²、0.45²、0.25²,这样第一条路径占绝对主导,适合先验证三 finger 是否能把小径能量也合并进来。
加噪基准SNR_DB=2.0看起来偏低,但注意这是码片级 SNR。IS-95 处理增益是 64 倍,一个符号解扩后信噪比会提高大约 18 dB,所以码片级 2 dB 对应符号级约 20 dB,对 BPSK 来说已经能跑出接近无误码的结果。如果没有这个概念,直接把符号级 SNR 填到码片上,你会发现输出 BER 全零,以为系统没问题,实际上是在掩盖信道损伤。
3.4 接收端RAKE代码:导频估计与最大比合并
接收端要做三件事:去掉 PN、解 Walsh、从导频里提取信道估计然后合并。下面这段是三 finger RAKE 的实现骨架,采用与发射端相同结构的 PN 序列。注意这里先固定多径时延已知,省去搜索器,目的是把合并算法单独验证一遍。
# RAKE 接收机:每个 finger 对齐一条多径 sym_data = np.zeros((NUM_SYM, len(DELAYS))) sym_pilot = np.zeros((NUM_SYM, len(DELAYS))) for f, d in enumerate(DELAYS): # 本地 PN 对齐时延;np.roll 是简化写法,边缘有 wrap pn_local = np.roll(pn, -d) rxd = rx * pn_local for i in range(NUM_SYM): seg = rxd[i*SF + d : i*SF + d + SF] sym_data[i, f] = np.dot(seg, w_traf) # 业务解扩 sym_pilot[i, f] = np.dot(seg, w_pilot) # 导频解扩 # 最大比合并:导频解扩结果作为信道估计权重 hard = np.zeros(NUM_SYM) for i in range(NUM_SYM): weight = sym_pilot[i, :] # 复数场景取 conj() comb = np.sum(weight * sym_data[i, :]) hard[i] = 1.0 if comb > 0 else -1.0 BER = np.mean(hard != bit) print(f"RAKE BER = {BER:.4f}")逻辑说明:每个 finger 先乘本地 PN 码把扰码去掉,切片位置从i*SF+d开始,这样符号边界对齐的是这条路径的到达时刻;然后分别与业务 Walsh 码和导频 Walsh 码做相关。导频解扩结果在路径增益和相位已知的情况下就代表信道系数 h_l,用它去乘业务解扩结果,就是最大比合并的权重相乘。最后按符号判决并统计 BER。
参数说明:np.roll(pn, -d)是循环移位,对多径时延长到接近帧长度时不安全,正式仿真应在发射数据尾部丢弃最后max(DELAYS)个码片,接收也只用有效区间。复数场景里,权重需要取共轭,本文里是实数信道所以省略。把这三段代码连起来,保存为同一脚本就能得到一个完整的前向链路 RAKE 最小仿真。如果 BER 在 0.01 以下,基本说明发射、信道、接收这条链路是通的,可以进入参数调优阶段。
4. 把RAKE接收机调好:搜索窗口、finger数与导频滤波系数
4.1 搜索器如何决定finger分配:搜索窗和门限
实际 RAKE 不知道路径时延,需要靠搜索器在导频信道里做相关峰扫描。搜索窗长度要根据环境时延扩展来定,城区环境下前向链路多径时延扩展通常在几微秒量级,对应到 1.2288 Mcps 就是 3 到 10 个码片。我一般先开 64 码片的窗,半码片步进扫描,这样能用 128 个相关值覆盖整段多径。找到相关峰后,不是把前三个最大的峰直接分配,而是要加一个门限,比如峰值的十分之一。低于门限的峰大多是噪声或旁瓣,强行分配给 finger 只会往合并器里灌噪声。
路径分配还有个常见原则:任意两个 finger 的时延差不能小于一个码片。因为 RAKE 的分集收益来自不相关性,隔得太近的路径本质上是同一条路径的旁瓣,分配过去只是浪费资源。如果搜索器扫出两个相距 0.7 码片的峰,直接保留能量更高那个,另一个舍掉。这个“最小间隔”参数在仿真里一般设为 64 个码片样本的若干倍,具体值取决于你仿真采用的过采样倍数。
4.2 finger数多少合适:仿真和工程的不一样的取舍
仿真里很多人一上来就把 finger 数设为 6,其实没这个必要。IS-95 前向链路的典型移动台用 3 个 finger,基站侧也不会把所有路径都分给一个用户。分集增益随着 finger 数增加是递减的:主径占 80% 能量时,加第二根手指能带来明显收益,加到第三根手指边际变小,再加第四根基本属于浪费硬件和功耗。做设计时先看你的多径能量分布,用搜索器统计出强路径数量,再定 finger 数。
如果我的仿真多径参数是 [0.9, 0.45, 0.25],三根手指就是合适的:第一根吃主径,第二根吃中等路径,第三根收小路径能量。如果把 finger 改成两根,第三径完全丢失,BER 会有可见恶化;改成四根,MERGE 时第四根可能只收到噪声,甚至拉低性能。对比试验很有说服力:在相同信道下分别跑 1 finger、2 finger、3 finger,你会看到 BER 差异非常直观,这就是给你的 RAKE 必要性留下证据。
| finger 数 | 对应配置 | 预期效果 |
|---|---|---|
| 1 | 只收主径 | 受多径衰落影响最明显 |
| 2 | 主径 + 一条强径 | 增益明显,BER 降低一个量级 |
| 3 | 主径 + 两条强径 | 适合 3 条路径的仿真 |
| 4 | 多收噪声 | 对三径而言几无增益 |
4.3 导频估计的平滑系数:对移动速度很敏感
RAKE 里最大比合并的权重来自导频信道估计,而单次导频解扩的瞬时值噪声很大,需要平滑。常见的做法是一阶 IIR 滤波:h_est = (1-α) * h_est + α * h_instant。α 选大选小是个纠结点:α 太大,估计值追得上快变信道,但噪声滤不干净;α 太小,估计跟不上,移动速度一快就出现相位滞后,合并权重错位。
在 IS-95 前向链路里,导频符号率是 19.2 ksymbol/s,一个导频符号持续 64 个码片,约 52 μs。低速场景我用 α=0.1 到 0.2,高速场景会提高到 0.3 到 0.5,具体要看 BER 随速度的拐点。还有一种做法是滑动窗平均,窗口长度取 4 到 8 个导频符号,简单且不容易出现 IIR 滤波器的初始化问题。注意做相干合并时,信道估计不要做太深的滤波,否则路径相位在快衰落里已经漂移了,你还在用上一轮的估计去合并。
5. 避坑与排查:IS95前向链路仿真中最常见的6个翻车点
5.1 现象:把所有信道的 BER 都统计成业务信道,结果差异巨大
原因:没有先确认业务 Walsh 编号和接收端解扩用的是同一个编号。Walsh 矩阵生成方式不一致,尤其在使用np.block拼接 Hadamard 矩阵时,矩阵的行顺序和 IS-95 标准定义不一定相同,W10 在不同矩阵里对应的码字完全不同。解决:在发射端和接收端之间只传w_traf这个向量,不要传“10”这个编号,跑通后再去核对标准 Walsh 序号。
5.2 现象:RAKE 的 BER 比单径 RAKE 还差
原因:导频功率设置过高或过低。如果导频幅度达到 0.5,业务信号被压缩,数据解扩的 SNR 下降,最大比合并权重虽然更准,但数据符号本身太弱,合并后还是不如单径。解决:把导频幅度和业务幅度一起归一到总功率,不要分别给两个信道都按绝对幅度叠加。常见做法是先固定数据幅度为 1,导频幅度在 0.2 到 0.4 之间扫一遍,找到 BER 最低点。
5.3 现象:时延改成 0.5 码片之后,RAKE 合并性能剧烈下降
原因:整数码片延时代码只能平移整数倍样本,0.5 码片需要过采样后做多相滤波或重采样,直接用整点采样代替分数时延会丢失能量。解决:先对基带波形做 4 倍或 8 倍过采样,再把“延迟 d 个码片”换算成“延迟 d*采样倍数 个采样点”。过采样同时也解决了匹配滤波和指纹抽头精度问题,代价是仿真长度增加,但这是值得的。
5.4 现象:明明信噪比很高,BER 却下不来,看起来像有误码地板
原因:没有把信道估计和业务路径对齐,或者np.roll循环移位导致第一条路径的尾部数据卷到了帧头。循环移位在短帧里会把末尾码片挪到开头,相关器跳到错误符号边界。解决:发射时留出max(DELAYS)码片的前导和尾随零,接收只统计有效段,不让 wrap-around 进入符号窗。
5.5 现象:复数信道下把权重直接乘业务符号,星座图乱转
原因:最大比合并要求权重取信道估计的共轭。很多人在实数仿真里写weight * data,复数场景忘了加np.conj(weight),结果两条路径相位的共轭不匹配,叠加后互相抵消。解决:统一在合并器里写成np.sum(np.conj(weight) * data),实数信道下结果不变,复数信道下自然正确。
5.6 现象:加寻呼和同步信道后,业务信道 PERFORMANCE 突然下降
原因:这几路信道叠加到发射端以后,总功率归一化改变了导频功率占比。发射端在加法后直接除均方根,导致导频实际功率低于预期,接收端导频估计变差。解决:先把所有信道的幅度配置好,统一设计成目标功率比,再做归一化,不要临时往加法器里面塞没有功率预算的信道。
6. 进阶验证:用BER曲线和finger开关验证你的RAKE实现
验证一套 RAKE 仿真,光看一个 BER 数值不够。我习惯做一组“finger 开关”实验:在同一个信道条件下,分别关闭第 2、3 个 finger,记录 BER。如果打开第 3 个 finger 后 BER 没有明显降低,要么是这条路径在该 SNR 下已低于合并门限,要么是搜索器把路径分配错了。这个对照实验能直接反映你的 RAKE 资源是不是都用在刀刃上。
# finger 开关对照:只有第一根手指参与合并 ber_1 = [] for trial in range(10): # 重新生成同一信道参数,只保留 DELAYS[0] # 合并时 sym_pilot[:, 1:] 和 sym_data[:, 1:] 置零即可 pass # 实际脚本里用循环参数化完成代码示意里不需要真去重写整个链路,而是在合并循环里加一个启用掩码。比如active = [True, True, False],合并时只把active[f]对应的权重和数据纳入。这样能快速看到手指增多带来的增益曲线。如果三 finger 比单 finger 在 BER=1e-3 附近能省出大约 3 dB 到 5 dB,你的 RAKE 实现就是合格的。
最后的习惯性检查:把发射端 PN 序列和接收端本地 PN 序列随机错开几个码片,看 BER 是不是立刻变成 0.5。这个测试能避免你在把难度逐步加到 4 倍过采样、64 个业务信道、动态信道估计之后,突然发现基本功没打牢。先跑通最小闭环,再做复杂度扩展,这套顺序我用了很多年,希望帮到你少踩几个坑。
本文还有配套的精品资源,点击获取