简介:这套MATLAB仿真代码面向无线通信学习者与3G系统研究者,围绕WCDMA小区搜索流程提供可运行的分析素材,帮助理解移动台入网时的时间同步、频率同步与码同步机制。压缩包共16个文件,以13个.m脚本为主,辅以2个.bin数据文件和1个.txt说明,整体约323KB,便于在MATLAB中直接复现实验并观察各阶段中间结果。已有162人学习,内容涵盖主同步信道与辅同步信道搜索、公共导频信道功率控制检测,以及下行链路信号生成、扰码、脉冲成形和浮点转定点等辅助环节,并配有结果可视化对比脚本。读者可据此梳理同步序列匹配、小区ID解析与相关峰检测思路,搭建从信号生成到搜索验证的完整仿真链路,适合作为通信原理课程实验、3G技术研究或算法复现的参考材料。
1. 从开机扫网说起:WCDMA 小区搜索为什么要拆成三步
做过终端协议栈的人都清楚,开机扫频扫到能量够的频点之后,第一件事不是解系统消息,而是 WCDMA 小区搜索——找时隙、找帧头、找扰码,这三件事做完才谈得上读广播信道。这套 MATLAB 仿真代码做的正是这件事:发射端把 PSC、SSC、CPICH 和 Gold 扰码拼成一帧下行信号,接收端用滑动相关一步步把时隙边界、码组号、主扰码号提取出来。它适合两类人:一类是通信、电子方向要做课程设计或毕设,需要一份能跑出检测概率曲线的同学;另一类是在基带侧写 C 代码,想先用 MATLAB 把算法验证一遍再落地的工程师。整个工程纯 MATLAB 实现,没有依赖 Communications Toolbox 这类插件里的通信函数,几百行就能跑通。
2. 工程骨架:目录、参数表与一条 38400 chip 的帧流水线
2.1 目录按发射、信道、接收三段切
这个项目的目录结构没什么花活,就是老老实实按信号流向分。之所以不按「算法」分而按「方向」分,是因为小区搜索本质是一条链路:发射端造信号、信道加损伤、接收端做检测。改代码的时候,你脑子里想的是「我要给信道加个频偏」,而不是「我要改一个函数」,按方向切目录能省掉一半找文件的时间。
wcdma_cell_search/ ├── config/ │ └── sim_params.m % 所有可调参数集中在这 ├── tx/ │ ├── gen_psc.m % 主同步码 │ ├── gen_ssc.m % 16 个辅同步码 │ ├── gen_ssc_pattern.m % 64 组码字表 │ ├── gen_scrambling_code.m % Gold 扰码 │ └── build_tx_frame.m % 组一帧:CPICH + SCH ├── channel/ │ ├── add_awgn.m % 高斯白噪声 │ ├── add_cfo.m % 载波频偏 │ └── rayleigh_fading.m % 多径瑞利 ├── rx/ │ ├── step1_slot_sync.m % 第一步:时隙同步 │ ├── step2_frame_sync.m % 第二步:帧同步 + 码组识别 │ ├── step3_scrambling_id.m % 第三步:扰码识别 │ └── cell_search.m % 三步骤串联 ├── utils/ │ ├── upsample_chips.m % chip 级到采样级 │ └── plot_metric.m % 画相关度量曲线 ├── run_demo.m % 单次跑通,出图 └── run_snr_sweep.m % 扫 SNR,出检测概率曲线config/sim_params.m是整个工程的唯一入口参数,把它单独拎出来是有原因的:小区搜索里最容易被改动的就是采样率、过采样倍数和 SNR 定义,散在各个函数里迟早出事。utils/upsample_chips.m看着不起眼,其实是把 chip 级序列搬到采样级的关键一环,所有相关器都靠它对齐采样点。
2.2 参数表:38400 chip 一帧是怎么定下来的
WCDMA FDD 的帧结构是硬约束,参数不能乱设。下表是这版仿真里所有跟物理层相关的东西,改动任何一项都要检查相关性:
| 参数 | 取值 | 说明 |
|---|---|---|
chipRate | 3.84e6 | 码片速率,3.84 Mcps |
os | 4 | 每 chip 采样点数,等效采样率 15.36 MHz |
slotChips | 2560 | 一个时隙 2560 chip |
slotsPerFrame | 15 | 一帧 15 个时隙 |
frameChips | 38400 | 10 ms 对应 38400 chip |
sfCPICH | 256 | CPICH 扩频因子 |
numSC | 512 | 主扰码总数,64 组 × 每组 8 个 |
ampCPICH | 1.0 | CPICH 幅度基准 |
ampSCH | 0.7 | SCH 相对 CPICH 约 -3 dB |
os × frameChips | 153600 | 一帧的采样点数 |
os取 4 是个折中。取 1 的话每个 chip 一个点,匹配滤波的峰位只有 chip 级精度,时隙同步误差能到几百个采样点;取 8 精度更好但相关运算量翻倍,扫 200 次蒙特卡洛要等很久。4 倍过采样下单帧采样点 153600,一个filter调用在普通笔记本上几十毫秒就出结果,适合反复扫参数。
提示:
ampSCH这一项很容易被当成无关紧要的归一化系数,其实它直接决定了你算出来的 SNR 对应的是哪个信道。后面第 5 章会专门讲这个坑。
2.3 流水线:一帧信号从生成到出结果的完整链路
把run_demo.m拆开看,一次完整的仿真就五步,每步的输出都是下一步的输入,链路很清晰:
% run_demo.m 主干 cfg = sim_params(); % 1. 载入参数 [tx, meta] = build_tx_frame(cfg); % 2. 发射端组一帧 rx = add_awgn(tx, cfg.snrDb); % 3. 过信道 res = cell_search(rx, cfg); % 4. 三步骤搜索 fprintf('group=%d, scCode=%d, 正确=%d\n', ... res.codeGroup, res.scCode, res.scCode == meta.scCode);build_tx_frame返回的meta里存着这一帧真实用的码组号和扰码号,是后面对答案用的。cell_search返回的res里除了三个检测结果,还带着第一步的折叠度量metricStep1,画出来能直接看到时隙同步的峰有多干净——调试的时候我基本只看这张图,峰不明显就说明前面某一环出问题了。
链路里有一个容易被忽略的点:rx的长度。第一步要在整段信号上折叠出 15 个时隙做非相干累加,第二步又要取满 15 个时隙的 SSC,所以rx至少要覆盖两帧,也就是 307200 个采样点。短了会在step2_frame_sync里break掉,度量矩阵缺行,码组匹配直接算错,而且不报错——这种静默失败最难查。
3. 发射端建模:PSC/SSC/CPICH 与 Gold 扰码怎么生成
3.1 PSC:256 chip 的 Golay 序列
主同步码是所有小区共用的,不加密、不加扰,手机在不知道任何信息的情况下就靠它找时隙边界。它的结构很规整:一个 16 长的 a 序列,按固定的 16 段符号拼接,总长 256 chip。
function psc = gen_psc() % 生成主同步码 PSC:256 chip 的 Golay 序列 % 结构:16 长的 a 序列按 pat 给出的段符号拼接 a = [1 1 1 1 1 1 -1 -1 1 -1 1 -1 1 -1 -1 1].'; pat = [1 1 1 -1 -1 1 -1 -1 1 1 1 -1 1 -1 1 1].'; psc = kron(pat, a); % 16 段 x 16 chip = 256 chip psc = (1 + 1j) * psc; % 实部虚部同号,等价于 QPSK 映射 endkron(pat, a)是这段代码的核心:pat的每个元素乘以整段a,拼出来正好 256 个 chip,第 i 段就是pat(i)*a。这么写比手写 256 个数的数组可读得多,也方便对照规范检查。末尾乘(1+1j)是因为 PSC 在复基带上实部虚部相同,等效于在 I、Q 两路发同样的序列。
选 Golay 结构不是为了好看,是因为它有很好的非周期自相关特性:除了零时延附近,旁瓣压得很低。这一步的重要性在于,第一步时隙同步就是拿 PSC 做匹配滤波,如果序列自相关旁瓣高,折叠累加之后会出现多个高度接近的峰,时隙边界就定不准。
3.2 SSC 与码组:16 个码 + comma-free 表
辅同步码有 16 个,每个也是 256 chip,构造方式是在 a 序列上做位置调制:把 16 阶 Hadamard 矩阵的第 k 行取出来,第 i 个符号乘到第 i 段 a 上。
function ssc = gen_ssc() % 生成 16 个辅同步码,每个 256 chip a = [1 1 1 1 1 1 -1 -1 1 -1 1 -1 1 -1 -1 1].'; H = 1; while size(H, 1) < 16 % 手写 Sylvester 构造,避免依赖工具箱 H = [H H; H -H]; end ssc = zeros(256, 16); for k = 1:16 ssc(:, k) = (1 + 1j) * kron(H(k, :).', a); % 位置调制 end end这里没用hadamard(16),是因为那个函数在某些 MATLAB 安装里属于工具箱插件,换台机器就跑不了。手写一个 4 层递归的 Sylvester 构造只要 5 行,结果完全一样,Hadamard 行之间严格正交,16 个 SSC 在理想信道下互不干扰。
码组表是第二步的关键。64 个码组,每组对应一个长度 15 的 SSC 索引序列,要求是 comma-free:任意一组的任意循环移位都不能等于另一组。这个性质保证了帧同步能唯一确定,不然 15 个时隙转一圈会撞车。
function pat = gen_ssc_pattern() % 64 组码字,每组 15 个 SSC 索引,强制满足 comma-free rng(2024); % 固定种子,保证结果可复现 pat = zeros(64, 15); for g = 1:64 ok = false; while ~ok cand = randperm(16, 15); ok = true; for h = 1:g-1 for s = 0:14 if isequal(cand, circshift(pat(h,:), [0 s])) ok = false; break; end end if ~ok, break; end end end pat(g, :) = cand; end end这段生成逻辑跑起来大概几十毫秒,代价可以接受。需要说明的是:真实系统里这 64 行是标准文档附录里硬编码的表,仿真用可复现的随机生成替代,只要满足 comma-free 性质,帧同步算法的行为和用真表完全一致。真要跟标准逐位对齐,把pat换成从表格里抄下来的 64×15 矩阵就行,其余代码一行不用改。
3.3 扰码:x^18+x^7+1 的 Gold 序列
主扰码负责区分小区,512 个,分成 64 组、每组 8 个。生成方式是两条 18 阶 m 序列异或,生成多项式 x^18 + x^7 + 1,第二条支路做相位偏移。
function c = gen_scrambling_code(n, len) % 生成第 n 号主扰码(n = 0..511),长度 len chip,输出 ±1 实数 x = ones(1, 18); y = ones(1, 18); for i = 1:n % y 支路相位偏移 n 拍 y = [mod(y(7) + y(18), 2), y(1:17)]; end c = zeros(len, 1); for i = 1:len c(i) = 1 - 2 * mod(x(1) + y(1), 2); % 异或后映射到 ±1 x = [mod(x(7) + x(18), 2), x(1:17)]; % 两个寄存器同步推进 y = [mod(y(7) + y(18), 2), y(1:17)]; end endn的取值范围是 0 到 511,对应 512 个主扰码;len一般传frameChips(38400,一帧)或者slotChips(2560,一个时隙)。寄存器用行向量、新比特拼在左边,等价于右移,只是写起来短一些。mod(x(7)+x(18), 2)对应 x^18 + x^7 + 1 的抽头位置,改多项式的话这里要一起改。
这个函数是整份代码里调用最频繁的:第三步要对 8 个候选扰码各生成一帧长度的序列,扫 200 次蒙特卡洛就是 1600 次调用。如果嫌慢,可以把 512 个扰码预生成一次存成矩阵,用n+1直接索引——这也是我后来改的第一处。
3.4 组帧:CPICH 与 SCH 的功率叠加
前面几个都是零部件,build_tx_frame把它们装成一帧。CPICH 用 SF=256 扩频、符号恒为 1、用主扰码加扰;SCH 不加扰不扩频,只占每时隙前 256 chip。
function [tx, meta] = build_tx_frame(cfg) tx = zeros(cfg.frameChips, 1); % --- P-CPICH:SF=256,导频符号恒为 1 c = gen_scrambling_code(cfg.scCode, cfg.frameChips); nSym = cfg.frameChips / cfg.sfCPICH; % 150 个符号 cpich = reshape(repmat(ones(1, nSym), cfg.sfCPICH, 1), [], 1) .* c; tx = tx + cfg.ampCPICH * cpich; % --- SCH:PSC + SSC,只在每时隙前 256 chip psc = gen_psc(); ssc = gen_ssc(); sch = zeros(cfg.frameChips, 1); for s = 0:cfg.slotsPerFrame-1 base = s * cfg.slotChips; sch(base + (1:256)) = psc + ssc(:, cfg.pattern(cfg.codeGroup, s+1)); end tx = tx + cfg.ampSCH * sch; meta.scCode = cfg.scCode; meta.codeGroup = cfg.codeGroup; endrepmat加reshape那行就是扩频:一个符号重复 256 次,再逐 chip 乘扰码。SF=256 意味着每帧 150 个 CPICH 符号,这个数字后面第三步要用到。SCH 部分是把 PSC 和当前时隙对应的 SSC 直接相加,两者在同一个 256 chip 窗口内码分复用,接收端靠正交性分开。
meta只回传了两个号,其实可以多存点东西,比如用的哪组pattern、ampSCH是多少。做对比实验的时候多存一份元信息,能省掉不少「当时到底跑的是哪版参数」的回忆。
4. 接收端三步骤:滑动相关、码组匹配与扰码识别
4.1 第一步时隙同步:匹配滤波 + 周期折叠
时隙同步干的事很朴素:拿本地 PSC 跟接收信号逐点做相关,相关值大的地方就是 PSC 出现的地方,也就是时隙起点。单次相关噪声太大,所以把整段信号按 2560 chip 的周期折叠起来累加。
function [peakPos, acc] = step1_slot_sync(rx, cfg, psc) psc_os = kron(psc, ones(cfg.os, 1)); % chip 级上采样到 4 倍 mf = conj(flipud(psc_os)); % 匹配滤波器 y = filter(mf, 1, rx); % 滑动相关 slotLen = cfg.slotChips * cfg.os; % 10240 个采样点 nSlot = floor(length(y) / slotLen); acc = zeros(slotLen, 1 <p> <a href="https://download.csdn.net/download/yaomly/89363690" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>