简介:WINNER2信道模型MATLAB实现代码包,面向无线通信仿真方向的研究生、科研人员与工程师,用于在3GPP SCM基础上开展更精细的MIMO信道建模与链路级仿真。包内共47个文件,以41个.m函数脚本为核心,覆盖场景参数配置、天线阵列与极化响应、路径损耗、时延扩展、角度扩展、信道矩阵生成等模块,另含4个.p加密文件、1份PDF实现说明文档与1份版本说明txt,整体约639KB,结构紧凑便于按模块调用。资源附有代码实现介绍文档,可帮助读者理解WINNER II各场景参数表与核心函数之间的调用关系,快速搭建从参数配置到信道系数输出的完整流程,并对照文档排查仿真中的参数设置问题。目前已有1655人学习下载,适合需要复现或二次开发WINNER信道模型的读者参考。
1. 从一次链路预算翻车说起:WINNER II 信道模型到底能干什么
去年帮一个做车联网的朋友看仿真结果,他信誓旦旦说链路预算留了 20 dB 余量,结果实测在城区高架场景下误码率直接飙到 1e-2,怎么调发射功率都压不下去。翻他的 MATLAB 脚本一看,信道部分用的是最基础的瑞利衰落加固定时延,连多径角度扩展都没建模。这就是典型的“仿真里信道太干净,一到真实场景就翻车”。WINNER II 信道模型就是解决这类问题的——它不是某一个公式,而是一整套面向 2 GHz 到 6 GHz 频段、覆盖城区微蜂窝到郊区宏蜂窝等多种场景的参数化信道建模框架。这套 MATLAB 代码包把 WINNER II 的几何随机信道模型(GSCM)实现成了可配置的函数,你给定场景类型、天线配置、移动速度,它就能生成带时延扩展、角度扩展、多普勒谱的 MIMO 信道系数矩阵。适合谁用?做 5G/6G 链路级仿真、车联网 V2X 信道建模、Massive MIMO 波束赋形验证的工程师,以及需要拿真实信道数据喂给深度学习模型的同学。如果你还在用rayleighchan或者comm.RicianChannel凑合,这套代码值得花时间拆一遍。
2. WINNER II 的几何随机建模逻辑与代码结构拆解
2.1 为什么不是“抽几个随机数乘一乘”那么简单
WINNER II 的核心思想是几何随机信道模型(Geometry-based Stochastic Channel Model, GSCM)。它不直接生成信道冲激响应,而是先在三维空间里随机撒一堆散射体簇(cluster),每个簇有随机的时延、功率、出发角(AoD)和到达角(AoA),然后根据收发端的天线阵列几何关系,把每个簇映射成天线阵元之间的复增益。这个过程决定了它比纯统计模型多出来的几个关键参数:时延扩展(DS)、角度扩展(AS)、阴影衰落(SF)以及簇的角度功率谱(PAS)。代码包里通常会有winner2_init或类似的入口函数来加载场景参数表,然后winner2_channel或wim函数负责生成信道矩阵。我拆过的几个版本里,目录结构大同小异:config/放场景配置文件,core/放 GSCM 核心计算,utils/放坐标变换和角度映射工具,demo/里有一两个跑通链路的示例脚本。先搞清楚这个分层,后面改参数才不会把整个包改崩。
2.2 场景配置文件怎么读、怎么改
WINNER II 定义了 A1、A2、B1、B2、B3、B4、C1、C2、C3、C4、D1、D2 共 12 种场景,从室内热点到郊区宏蜂窝全覆盖。代码包里一般用一个结构体数组存这些场景的统计参数,比如scenario_params.A1.DS_mean、scenario_params.B1.ASD之类的字段。你不需要背这些值,但要知道去哪里改。常见做法是:先跑demo里的默认脚本,确认能出图,然后找到config目录下对应的.m文件,把scenario字段从'B1'改成你需要的场景代号。注意,不同场景的载波频率和带宽默认值不一样,改场景后记得同步检查fc和bw两个变量,否则时延分辨率会对不上。
% 加载 WINNER II 场景参数并初始化信道对象 clear; clc; addpath(genpath('./winner2_core')); % 把核心函数目录加入搜索路径 scenario = 'B1'; % 城区微蜂窝,视距与非视距混合 fc = 2.6e9; % 载波频率 2.6 GHz,常见 5G 频段 bw = 20e6; % 带宽 20 MHz mobile_speed = 30; % 移动速度 30 km/h,约 8.33 m/s % 读取场景参数表,不同版本函数名可能略有差异 params = winner2_scenario_params(scenario); params.fc = fc; params.bandwidth = bw; params.velocity = mobile_speed / 3.6; % 换算成 m/s % 配置天线阵列:2x2 MIMO,半波长间距 params.tx_array = winner2_antenna_array('ula', 2, 0.5); params.rx_array = winner2_antenna_array('ula', 2, 0.5); fprintf('场景 %s 初始化完成,DS 均值 %.2e s\n', scenario, params.DS_mean);上面这段代码的逻辑是:先清空环境并加载核心函数路径,然后指定场景代号和射频参数。winner2_scenario_params返回一个结构体,里面包含该场景下时延扩展、角度扩展、阴影衰落标准差等统计量。winner2_antenna_array用来生成均匀线阵(ULA)的阵元坐标,第二个参数是阵元数,第三个是归一化间距。参数说明:fc影响多普勒频移计算,bw决定时延抽头的最小分辨率,mobile_speed直接进多普勒谱。如果你把mobile_speed设成 0,代码通常会自动退化成静态信道,但有些版本会报除零错误,建议至少给 0.1 m/s。
2.3 生成信道系数矩阵并验证维度
初始化完成后,下一步是调用信道生成函数。不同版本的函数签名可能不同,常见的是H = winner2_channel(params, num_snapshots),返回num_snapshots个时间快照上的信道矩阵。对于 2x2 MIMO,每个快照的H是 2x2 复矩阵。你要检查的维度是[rx_ant, tx_ant, num_snapshots]或者[num_snapshots, rx_ant, tx_ant],取决于代码包的排列习惯。我一般会先跑 100 个快照,画一下abs(H(:,:,1))的热力图,确认四个通道的幅度不是完全一样的——如果完全一样,说明角度参数没生效,大概率是天线阵列配置那一步被注释掉了。
num_snapshots = 1000; % 生成 1000 个时间采样点 H = winner2_channel(params, num_snapshots); % 检查输出维度 fprintf('信道矩阵维度: %s\n', mat2str(size(H))); % 提取第一个快照的幅度响应 H1 = H(:,:,1); figure; imagesc(abs(H1)); colorbar; title('第一个快照的 MIMO 信道幅度响应'); xlabel('发射天线索引'); ylabel('接收天线索引'); % 计算时延扩展验证:对功率时延谱做二阶矩 PDP = squeeze(mean(abs(H).^2, [1,2])); delay_bins = (0:length(PDP)-1) / bw; mean_delay = sum(PDP .* delay_bins.') / sum(PDP); rms_ds = sqrt(sum(PDP .* (delay_bins.' - mean_delay).^2) / sum(PDP)); fprintf('仿真 RMS 时延扩展: %.2e s,场景标称值: %.2e s\n', rms_ds, params.DS_mean);这段代码先生成 1000 个快照的信道矩阵,然后取第一个快照画幅度热力图。squeeze(mean(abs(H).^2, [1,2]))是对收发天线维度求平均,得到平均功率时延谱。delay_bins根据带宽算出每个时延抽头对应的时间。RMS 时延扩展的计算公式是功率加权二阶中心矩的平方根。如果算出来的rms_ds和params.DS_mean差一个数量级,先检查bw是否设错,再检查H的第三维是不是时间维——有些代码包把时间维放在第一维,squeeze的方向就要跟着改。
3. 把 WINNER II 接进链路仿真:从信道矩阵到误码率曲线
3.1 和 OFDM 调制解调器对接的三种方式
生成H只是第一步,真正要验证性能得把它塞进链路里。常见做法有三种:第一种是直接做卷积,把H当成时变滤波器,适用于单载波系统;第二种是转到频域,对每个子载波做逐元素乘,适用于 OFDM;第三种是提取统计量(如相关矩阵、特征值分布)喂给链路级抽象模型。我一般用第二种,因为 WINNER II 本身就是在时延-角度域建模,FFT 之后每个子载波上的等效信道正好是H在频域的采样。具体操作:对H的第三维做 FFT,得到H_freq,维度是[rx_ant, tx_ant, num_subcarriers]。然后对每个子载波做 MMSE 均衡,算误码率。
% 将时域信道矩阵转到频域,对接 OFDM 子载波 num_subcarriers = 64; H_freq = fft(H, num_subcarriers, 3); % 沿第三维做 FFT H_freq = H_freq(:, :, 1:num_subcarriers); % 取有效子载波 % 配置 OFDM 链路参数 mod_order = 4; % QPSK num_symbols = 1000; snr_db = 0:2:20; ber = zeros(size(snr_db)); for idx = 1:length(snr_db) snr_lin = 10^(snr_db(idx)/10); noise_var = 1 / snr_lin; err_count = 0; total_bits = 0; for sym = 1:num_symbols % 随机生成 QPSK 符号 tx_data = randi([0 mod_order-1], 2, 1); tx_sym = pskmod(tx_data, mod_order, pi/4); % 取对应快照的频域信道 H_snap = H_freq(:, :, mod(sym-1, num_subcarriers)+1); % 接收信号加噪声 rx_sym = H_snap * tx_sym + sqrt(noise_var/2) * (randn(2,1) + 1j*randn(2,1)); % MMSE 均衡 W = (H_snap' * H_snap + noise_var * eye(2)) \ H_snap'; eq_sym = W * rx_sym; % 解调并统计误码 rx_data = pskdemod(eq_sym, mod_order, pi/4); err_count = err_count + sum(rx_data ~= tx_data); total_bits = total_bits + 2; end ber(idx) = err_count / total_bits; end semilogy(snr_db, ber, 'b-o'); grid on; xlabel('SNR (dB)'); ylabel('BER'); title('WINNER II B1 场景下 QPSK OFDM 误码率曲线');这段代码把时域H通过 FFT 转到频域,然后对每个 OFDM 符号取一个子载波上的 2x2 信道矩阵。pskmod和pskdemod的第四个参数是星座图相位偏移,QPSK 常用pi/4。MMSE 均衡的权重矩阵公式是(H'*H + noise_var*I)^{-1} * H',其中noise_var根据 SNR 换算。注意H_freq的第三维索引用了mod(sym-1, num_subcarriers)+1,这是为了让每个符号经历不同的频域快照,模拟频率选择性。如果你发现 BER 曲线在 20 dB 还下不去 1e-3,先检查H_freq是不是没做归一化——有些代码包生成的H功率已经归一化到 1,有些没有,没归一化的话等效 SNR 会差很多。
3.2 参数扫描:时延扩展和角度扩展对 BER 的影响
WINNER II 的价值在于你能系统地扫参数。比如固定 SNR 为 15 dB,把场景从 B1 换到 B2(城区微蜂窝非视距),时延扩展均值会从约 100 ns 跳到 300 ns 以上,BER 曲线会明显右移。再比如把天线间距从 0.5 波长改成 4 波长,角度扩展带来的空间相关性会下降,MIMO 分集增益更明显。我一般会写一个外层循环,遍历scenario列表和antenna_spacing列表,把 BER 存成矩阵,最后画一组曲线对比。这个过程中最容易踩的坑是:换场景后忘了重新初始化params,导致DS_mean还是旧场景的值。解决办法是在循环体内每次调用winner2_scenario_params重新加载。
% 参数扫描:不同场景和天线间距下的 BER 对比 scenarios = {'B1', 'B2', 'C2'}; spacings = [0.5, 2, 4]; % 归一化波长间距 snr_fixed = 15; ber_matrix = zeros(length(scenarios), length(spacings)); for si = 1:length(scenarios) for ai = 1:length(spacings) p = winner2_scenario_params(scenarios{si}); p.fc = 2.6e9; p.bandwidth = 20e6; p.velocity = 30 / 3.6; p.tx_array = winner2_antenna_array('ula', 2, spacings(ai)); p.rx_array = winner2_antenna_array('ula', 2, spacings(ai)); H_scan = winner2_channel(p, 500); H_f = fft(H_scan, 64, 3); H_f = H_f(:, :, 1:64); % 简化的 BER 估算:用等效 SNR 近似 snr_lin = 10^(snr_fixed/10); noise_var = 1 / snr_lin; err = 0; total = 0; for sym = 1:500 tx = pskmod(randi([0 3], 2, 1), 4, pi/4); Hs = H_f(:, :, mod(sym-1, 64)+1); rx = Hs * tx + sqrt(noise_var/2) * (randn(2,1) + 1j*randn(2,1)); W = (Hs' * Hs + noise_var * eye(2)) \ Hs'; rx_d = pskdemod(W * rx, 4, pi/4); err = err + sum(rx_d ~= pskdemod(tx, 4, pi/4)); total = total + 2; end ber_matrix(si, ai) = err / total; end end % 画柱状图对比 bar(ber_matrix); set(gca, 'XTickLabel', scenarios); legend(arrayfun(@(x) sprintf('间距 %.1f\\lambda', x), spacings, 'UniformOutput', false)); ylabel('BER at SNR=15dB'); title('不同场景与天线间距下的误码率对比');这段扫描代码的关键点在于:每次循环都重新加载场景参数,避免参数残留。winner2_antenna_array的第三个参数是归一化间距,0.5 波长是典型半波阵子间距,2 到 4 波长则是为了获得空间分集。柱状图能直观看出 B2 场景(非视距)的 BER 明显高于 B1(视距为主),而增大天线间距在 C2 场景下改善最明显,因为 C2 的角度扩展更大。注意pskdemod(tx, 4, pi/4)那一步其实可以直接用tx_data变量,我这里为了代码紧凑重新解调了一次,实际跑的时候建议把原始比特存下来,避免不必要的计算。
4. 避坑与排查:WINNER II 代码包最容易翻车的五个地方
4.1 现象:生成的 H 矩阵所有元素一模一样
原因:天线阵列配置函数返回的阵元坐标全是零,或者params里根本没传tx_array和rx_array字段。有些代码包的默认天线配置是单天线,你如果不显式覆盖,它就会生成 1x1 信道然后广播到所有维度。解决:在调用winner2_channel之前,用disp(params.tx_array)确认阵元坐标矩阵不是全零,并且行数等于天线数。
4.2 现象:RMS 时延扩展算出来是标称值的 10 倍以上
原因:bw设成了 1 Hz 或者忘了设,导致delay_bins的时间刻度完全错误。另一个可能是H的第三维不是时间维而是天线维,fft做错了方向。解决:先size(H)确认维度顺序,然后检查bw是否等于实际系统带宽。如果H是[tx_ant, rx_ant, time]这种排列,fft的维度参数要改成 3 对应的位置。
4.3 现象:换场景后 BER 曲线完全不变
原因:winner2_scenario_params返回的结构体被缓存了,或者你在循环外初始化了params然后在循环内只改了scenario字符串但没重新加载。解决:把params = winner2_scenario_params(scenario)放在循环体内第一行,并且每次改完场景后clear params再重新赋值。有些版本的函数用了持久变量(persistent),需要clear函数名才能刷新。
4.4 现象:多普勒谱对称,正负频移幅度一样
原因:移动速度方向设成了 0 度或者 180 度,导致多普勒频移只有一个符号。WINNER II 的 GSCM 里,移动方向角phi_v决定了多普勒谱的形状。解决:在params里找到velocity_angle或phi_v字段,设成 30 到 60 度之间的值,这样正负频移都会出现,谱形更接近真实场景。
4.5 现象:代码跑着跑着报“矩阵维度不一致”
原因:winner2_channel返回的H维度是[rx, tx, time],但你在后面做H * tx_sym时tx_sym是[tx, 1],MATLAB 的*要求内维相等,[rx, tx, time]不能直接乘[tx, 1]。解决:用squeeze把时间维去掉,或者用H(:, :, k)取第 k 个快照。如果代码包里H是[time, rx, tx],那就用squeeze(H(k, :, :))。养成先size(H)再运算的习惯,能省很多调试时间。
5. 进阶技巧:用实测数据校准 WINNER II 参数
5.1 从信道冲激响应反推时延扩展和角度扩展
如果你手头有实测的 CIR 数据(比如用矢量网络分析仪或者软件无线电平台采的),可以反推 WINNER II 的统计参数,让仿真更贴近你的实际场景。具体做法:对实测 CIR 做功率时延谱,算 RMS 时延扩展;然后用多天线阵列的相位差算到达角,统计角度扩展。把这两个值填回params.DS_mean和params.ASD,再跑仿真,BER 曲线会和实测更吻合。我一般会写一个calibrate_params.m脚本,输入实测 CIR 矩阵和天线间距,输出校准后的params结构体。
function params = calibrate_params(cir_matrix, antenna_spacing, fc, bw) % cir_matrix: [rx_ant, tx_ant, time, delay_taps] % 计算实测 RMS 时延扩展 pdp = squeeze(mean(mean(mean(abs(cir_matrix).^2, 1), 2), 3)); delay_bins = (0:length(pdp)-1) / bw; mean_d = sum(pdp .* delay_bins.') / sum(pdp); rms_ds = sqrt(sum(pdp .* (delay_bins.' - mean_d).^2) / sum(pdp)); % 用相邻阵元相位差估算角度扩展 phase_diff = angle(cir_matrix(2,:,:,:) ./ cir_matrix(1,:,:,:)); phase_diff = phase_diff(:); phase_diff = phase_diff(~isnan(phase_diff)); asd_est = std(phase_diff) * 180 / pi; % 粗略估计,单位度 % 填充到 WINNER II 参数结构体 params.DS_mean = rms_ds; params.ASD = asd_est; params.fc = fc; params.bandwidth = bw; fprintf('校准完成: DS=%.2e s, ASD=%.1f deg\n', rms_ds, asd_est); end这个函数的输入cir_matrix是四维数组,维度顺序是收发天线、时间、时延抽头。pdp的计算对前三维求平均,得到平均功率时延谱。phase_diff取相邻接收阵元的相位差,std反映角度扩展的粗略大小。注意这个估计非常粗糙,只适合做量级校准,精确的角度扩展需要用 MUSIC 或 ESPRIT 算法。params.ASD字段名在不同版本里可能叫ASD或ASA,填之前先fieldnames(params)看一眼。
5.2 用校准后的参数跑闭环验证
校准完参数后,别急着下结论。我一般会做两步验证:第一步,用校准后的params重新生成H,算 RMS 时延扩展,看是否和实测值在 10% 以内;第二步,把校准前后的 BER 曲线画在一起,如果两条曲线在 15 dB 处差超过 3 dB,说明校准参数里有一个维度没对上。常见问题是角度扩展的单位——WINNER II 内部用弧度,你从实测数据算出来的是度,忘了换算就会导致角度参数大 57 倍,信道相关性完全错误。从那以后我每次校准完都强制走一遍单位检查:assert(params.ASD < pi, '角度扩展单位疑似未换算')。希望帮到你。
本文还有配套的精品资源,点击获取