简介:这份资源面向无线通信方向的学生与研究人员,聚焦 Device-to-Device 通信中的信道仿真与频谱资源分配问题,适合具备一定 MATLAB 基础、希望深入理解 D2D 系统建模与启发式算法实现的读者。压缩包共 22 个文件,以 20 个 .m 脚本为主,辅以 2 个 .asv 自动备份文件,整体约 16KB,体积轻量但结构完整。内容围绕信道建模、系统模型、启发式算法实现、性能评估、干扰管理与结果分析等环节展开,脚本涵盖路径损耗计算、用户与 D2D 对生成、干扰距离与干扰功率统计、随机分配与最优分配对比,以及基于 Edmonds 等方法的资源匹配,可帮助读者快速复现 D2D 信道仿真流程,对比不同资源分配策略在吞吐量、误码率与覆盖率上的表现,并在此基础上开展功率控制与干扰协调的扩展实验。目前已有 1214 人学习下载,适合作为课程设计、毕业设计或相关课题的参考代码。
1. 从一次链路预算翻车说起:D2D 信道仿真到底在算什么
做 D2D 资源分配的同学大概率都遇到过这种场景:算法在论文里跑出来吞吐量漂亮得不行,换一组路损参数、把小区半径从 500 米改成 200 米,曲线立刻塌成一条直线。问题往往不在调度算法本身,而在信道模型这一层被当成了黑匣子。D2D 信道仿真要解决的核心,就是在 MATLAB 里把发射端到接收端的路径损耗、阴影衰落、快衰落、干扰耦合关系算清楚,让上层资源分配算法拿到一份可信的 SINR 输入。它适合两类人:一类是刚接手 D2D 复用蜂窝频谱课题、需要快速搭出可跑通链路级仿真的新手;另一类是已经写完分配算法、但发现性能对信道参数极度敏感、想回头校准模型的老手。这份资源围绕的就是这套 MATLAB 仿真链路,从场景搭建到干扰计算再到结果验证,把每一步的参数含义和踩坑点摊开讲。
2. 场景搭建与信道模型选型:为什么不能直接套用蜂窝路损
2.1 D2D 与蜂窝链路的信道差异
很多人上手第一反应是把蜂窝上行路损公式直接复制给 D2D 链路,这是最常见的翻车起点。D2D 通信的两端都是终端,天线高度通常在 1.5 米左右,而蜂窝链路里基站天线挂在 25 米以上,两者的传播环境根本不是一个量级。蜂窝链路可以近似看成一条主导的视距或准视距路径,而 D2D 链路在城市环境里更接近终端到终端的街道峡谷传播,绕射和散射占比高得多。
常见的做法是采用 3GPP 的 D2D 信道模型框架,把路损拆成两部分:一段是距离相关的幂律衰减,另一段是穿透损耗和阴影衰落。对于复用蜂窝上行频谱的 underlay 模式,还要额外考虑 D2D 对基站的干扰和蜂窝用户对 D2D 接收端的干扰,这两条干扰链路的信道增益必须单独建模,不能和有用信号共用一套参数。
选型上我一般会区分三种场景:开放区域、城市街道和室内。开放区域用接近自由空间的路径损耗指数 2.0 到 2.2,城市街道取 3.5 到 4.0,室内因为墙体遮挡要再叠加穿透损耗项。这个指数选错,后面所有 SINR 计算都会系统性偏移,而且偏移量随距离放大,越远越离谱。
2.2 用 MATLAB 搭出可复现的仿真场景
下面这段代码搭的是一个单小区场景,中心一个基站,若干蜂窝用户和若干 D2D 对随机分布,重点是把信道增益矩阵算出来。参数我按城市街道场景设的,你可以按自己的场景改。
% D2D 信道仿真场景初始化 rng(42); % 固定随机种子,保证结果可复现 R_cell = 500; % 小区半径,单位米 N_cell = 10; % 蜂窝用户数 N_d2d = 15; % D2D 对数 fc = 2e9; % 载波频率 2GHz alpha_cell = 3.7; % 蜂窝链路路损指数 alpha_d2d = 4.0; % D2D 链路路损指数 shadow_sigma = 8; % 阴影衰落标准差,单位 dB % 极坐标方式撒点,保证蜂窝用户落在小区内 r_cell = R_cell * sqrt(rand(N_cell,1)); theta_cell = 2*pi*rand(N_cell,1); pos_cell = [r_cell.*cos(theta_cell), r_cell.*sin(theta_cell)]; % D2D 对:先撒发射端,再在给定距离内撒接收端 d_d2d = 20 + 30*rand(N_d2d,1); % D2D 通信距离 20~50 米 r_tx = R_cell * sqrt(rand(N_d2d,1)); theta_tx = 2*pi*rand(N_d2d,1); pos_tx = [r_tx.*cos(theta_tx), r_tx.*sin(theta_tx)]; theta_rx = 2*pi*rand(N_d2d,1); pos_rx = pos_tx + [d_d2d.*cos(theta_rx), d_d2d.*sin(theta_rx)]; % 路损计算函数句柄,距离单位米,结果单位 dB pl_cell = @(d) 32.4 + 10*alpha_cell*log10(fc/1e9) + 10*alpha_cell*log10(d); pl_d2d = @(d) 32.4 + 10*alpha_d2d*log10(fc/1e9) + 10*alpha_d2d*log10(d);这段代码的关键点有三个。第一,rng(42)固定种子,否则每次跑出来的拓扑都不一样,调算法时根本分不清性能变化是算法改的还是随机撒点改的。第二,撒点用sqrt(rand)而不是直接rand,因为极坐标下均匀分布的面积元正比于半径,不取平方根会导致点往圆心堆。第三,路损公式里的常数项 32.4 是自由空间在 1 米处的参考损耗,配合log10(fc/1e9)把频率折算进去,这样换载波频率时不用重推公式。
2.3 信道增益矩阵的组装
有了位置和路损函数,下一步是把所有链路的信道增益算成矩阵。这里要区分四类链路:基站到蜂窝用户、基站到 D2D 接收端、D2D 发射端到蜂窝用户、D2D 发射端到 D2D 接收端。后两类是干扰链路,最容易漏。
% 计算各类链路的距离 d_bs_cell = sqrt(sum(pos_cell.^2, 2)); % 基站到蜂窝用户 d_bs_rx = sqrt(sum(pos_rx.^2, 2)); % 基站到 D2D 接收端 d_tx_cell = pdist2(pos_tx, pos_cell); % D2D 发射到蜂窝用户 d_tx_rx = zeros(N_d2d, N_d2d); for i = 1:N_d2d for j = 1:N_d2d d_tx_rx(i,j) = norm(pos_tx(i,:) - pos_rx(j,:)); end end % 转成线性增益,阴影衰落按 dB 正态叠加 G_bs_cell = 10.^(-(pl_cell(d_bs_cell) + shadow_sigma*randn(N_cell,1))/10); G_bs_rx = 10.^(-(pl_cell(d_bs_rx) + shadow_sigma*randn(N_d2d,1))/10); G_tx_cell = 10.^(-(pl_d2d(d_tx_cell) + shadow_sigma*randn(N_d2d,N_cell))/10); G_tx_rx = 10.^(-(pl_d2d(d_tx_rx) + shadow_sigma*randn(N_d2d,N_d2d))/10);逻辑说明:所有增益统一转成线性值再进 SINR 计算,因为 dB 域不能直接做加减以外的运算。阴影衰落用randn生成零均值正态分布,标准差取 8 dB 是城市环境的常见值,开放环境可以降到 4 到 6。G_tx_rx的对角线是每个 D2D 对的有用信道,非对角线是其他 D2D 对带来的同频干扰,这个矩阵是后面资源分配算法的核心输入。参数上要留意d_tx_rx的对角线元素应该等于d_d2d,如果对不上说明撒点逻辑写错了。
3. SINR 计算与资源分配接口:把信道矩阵喂给调度算法
3.1 干扰项的完整枚举
SINR 算错,九成是干扰项漏了。在 underlay D2D 复用蜂窝上行频谱的场景里,一个 D2D 接收端受到的干扰至少来自三处:复用同一资源块的蜂窝用户、其他复用同一资源块的 D2D 发射端、以及基站的下行泄漏(如果考虑交叉时隙)。很多简化模型只算前两项,结果在高密度场景下 SINR 偏高,算法误以为资源够用。
我一般会把干扰写成矩阵形式,方便后面按资源块索引。假设资源块分配用一个 0/1 矩阵X表示,X(c,r)=1表示蜂窝用户 c 占用资源块 r,Y(d,r)=1表示 D2D 对 d 占用资源块 r。
% 资源分配矩阵示例:每个蜂窝用户独占一个资源块,D2D 复用 N_rb = N_cell; % 资源块数等于蜂窝用户数 X = eye(N_cell, N_rb); % 蜂窝用户一一对应 Y = zeros(N_d2d, N_rb); for d = 1:N_d2d Y(d, randi(N_rb)) = 1; % 随机复用,实际由算法决定 end % 计算每个 D2D 对在每个资源块上的 SINR P_cell = 0.1; % 蜂窝用户发射功率,单位瓦 P_d2d = 0.01; % D2D 发射功率 N0 = 1e-13; % 噪声功率谱密度对应功率 sinr_d2d = zeros(N_d2d, N_rb); for r = 1:N_rb interf_cell = sum(X(:,r) .* P_cell .* G_tx_cell(:, :), 2); interf_d2d = (Y(:,r) .* P_d2d)' * G_tx_rx; signal = P_d2d * diag(G_tx_rx); sinr_d2d(:,r) = signal ./ (interf_cell + interf_d2d' + N0); end逻辑说明:interf_cell是复用同一资源块的蜂窝用户对每个 D2D 接收端的干扰之和,interf_d2d是其他 D2D 发射端的干扰,注意这里要把自身排除掉,否则会把自己的信号当成干扰。signal取G_tx_rx的对角线,就是每个 D2D 对的有用信道增益。参数上P_d2d通常比P_cell低一个数量级,这是为了控制 D2D 对基站的干扰,具体值要按你的功率控制策略调。
3.2 把信道矩阵封装成算法可调用的接口
资源分配算法通常不关心信道怎么来的,它只要一个 SINR 查询函数。我习惯把信道相关的东西封成一个结构体,算法只跟结构体打交道,这样换信道模型时算法代码一行不用改。
% 封装信道结构体 channel.N_d2d = N_d2d; channel.N_rb = N_rb; channel.G_tx_rx = G_tx_rx; channel.G_tx_cell = G_tx_cell; channel.G_bs_rx = G_bs_rx; channel.P_d2d = P_d2d; channel.P_cell = P_cell; channel.N0 = N0; % SINR 查询函数,输入资源分配矩阵,输出 SINR 矩阵 get_sinr = @(Y) compute_sinr(Y, channel);compute_sinr里就是上面那段干扰枚举的逻辑。这样做的价值在于:当你从随机复用换成贪婪算法或者图着色算法时,只需要改Y的生成方式,SINR 计算和信道模型完全不动。参数说明:channel结构体里所有增益都是线性值,功率单位统一用瓦,噪声用总功率而不是谱密度,避免单位混用导致的量级错误。
3.3 一个最小可跑的分配算法验证
为了确认信道接口没问题,我会先跑一个最简单的随机分配,看 SINR 分布是否合理。如果大部分 D2D 对的 SINR 低于 0 dB,说明干扰建模过重或者功率设置有问题;如果普遍高于 30 dB,多半是干扰项漏算了。
% 随机分配跑 100 次,统计 SINR 分布 sinr_all = []; for trial = 1:100 Y_tmp = zeros(N_d2d, N_rb); for d = 1:N_d2d Y_tmp(d, randi(N_rb)) = 1; end sinr_tmp = get_sinr(Y_tmp); sinr_all = [sinr_all; sinr_tmp(Y_tmp > 0)]; end fprintf('SINR 中位数: %.2f dB\n', 10*log10(median(sinr_all))); fprintf('SINR 5%% 分位: %.2f dB\n', 10*log10(prctile(sinr_all, 5)));这段代码的作用是给信道模型做一次体检。中位数落在 5 到 15 dB 之间通常比较合理,5% 分位如果低于 -10 dB,说明边缘用户的干扰太严重,实际系统里这些链路会直接掉线,算法需要考虑中断概率而不是只看平均吞吐。
4. 避坑与排查:D2D 信道仿真里最容易翻车的五件事
4.1 现象:SINR 随距离增大不降反升
原因:路损公式里距离用了公里而代码里传的是米,或者log10的参数写反了。还有一种情况是阴影衰落的随机数每次调用都重新生成,导致同一链路在不同资源块上的增益不一致,看起来像是不降反升。
解决:在路损函数入口加一句断言,检查距离范围是否在合理区间。阴影衰落要在场景初始化时一次性生成并固定下来,不能每次算 SINR 都重新抽。我一般会把所有链路的阴影值存成一个矩阵,整个仿真周期内不变。
4.2 现象:D2D 对的有用信号被自己干扰
原因:在计算interf_d2d时用了完整的G_tx_rx矩阵,没有把对角线置零。Y(:,r)' * G_tx_rx这个表达式里,如果Y(d,r)=1,那么第 d 个 D2D 自己的发射端也会通过G_tx_rx(d,d)贡献一份“干扰”,而这份其实是有用信号。
解决:在计算干扰前把G_tx_rx的对角线临时置零,或者用G_tx_rx - diag(diag(G_tx_rx))构造干扰矩阵。这个坑非常隐蔽,因为 SINR 看起来只是偏低一点,不会报错,但算法会误判资源紧张。
4.3 现象:换一组随机种子后算法性能排名完全颠倒
原因:D2D 对的位置和阴影衰落都是随机的,单次仿真的结果方差很大。如果只跑一次就下结论,等于在噪声上做比较。
解决:固定多组种子,每组跑完整的资源分配,最后对性能指标取平均和置信区间。我一般至少跑 50 组种子,报告里给出均值和标准差。如果两组算法的置信区间重叠,就不能说谁更好。
4.4 现象:高密度场景下 SINR 矩阵出现 NaN
原因:干扰功率累加后超过了realmax或者出现了除零。常见于 D2D 对数很多、资源块很少的情况,多个 D2D 复用同一资源块,干扰项累加后数值爆炸。另一种可能是某些 D2D 对的距离为零,路损公式里log10(0)返回负无穷。
解决:在撒点阶段加最小距离约束,D2D 通信距离不能小于 1 米。干扰累加前先检查是否有 Inf 或 NaN,用isnan和isinf做过滤。如果确实是密度过高导致的,说明场景本身不合理,需要限制每个资源块复用的 D2D 对数上限。
4.5 现象:蜂窝用户吞吐量在加入 D2D 后反而上升
原因:这通常不是算法好,而是蜂窝用户的 SINR 计算里漏掉了 D2D 对基站的干扰。在 underlay 模式下,D2D 发射端对基站的干扰是实打实存在的,如果只算 D2D 接收端的 SINR 而不管基站侧,就会得出 D2D 无害的错误结论。
解决:蜂窝链路和 D2D 链路的 SINR 必须联合计算,基站侧的干扰项要包含所有复用同一资源块的 D2D 发射端。验证方法是把 D2D 发射功率调到很大,如果蜂窝吞吐量不降,说明干扰链路没接上。
5. 进阶技巧:用蒙特卡洛加速和中断概率验证信道模型
信道模型搭完只是开始,真正要让它可信,得做两件事:一是把仿真速度提上来,二是用中断概率而不是平均 SINR 来验证。
先说加速。D2D 信道仿真里最耗时的部分是pdist2和双重循环算距离,当 D2D 对数上百时,每次资源分配都重算一遍距离矩阵是浪费。我的习惯是把所有链路的距离和增益在场景初始化时一次算完,存成矩阵,后面算法迭代只做索引和累加。下面这个对比表是我在某次仿真里记录的时间开销,场景是 50 个 D2D 对、20 个资源块、跑 1000 次分配。
| 做法 | 单次分配耗时 | 说明 |
|---|---|---|
| 每次重算距离和路损 | 约 18 ms | 距离矩阵重复计算,占了大头 |
| 预存增益矩阵,只做索引 | 约 2.3 ms | 内存换时间,50 对场景内存增加可忽略 |
| 预存 + 向量化干扰累加 | 约 0.8 ms | 去掉内层循环,用矩阵乘法替代 |
向量化的关键是把干扰累加写成矩阵形式。上面interf_d2d那行(Y(:,r) .* P_d2d)' * G_tx_rx就是向量化写法,比for循环快一个数量级。注意G_tx_rx的对角线要提前置零,否则向量化会把有用信号也算进去。
再说中断概率验证。平均 SINR 好看不代表系统可用,因为资源分配算法往往牺牲边缘用户来抬高平均值。我一般会画一条 SINR 的累积分布曲线,看 5% 分位对应的值,这个值低于解调门限的比例就是中断概率。
% 中断概率验证:跑多组种子,统计 SINR 低于门限的比例 threshold_dB = 0; % 解调门限 0 dB outage_count = 0; total_count = 0; for seed = 1:50 rng(seed); % 重新撒点、算增益、跑分配,得到 sinr_vec % sinr_vec 是当前种子下所有 D2D 链路的 SINR 线性值 outage_count = outage_count + sum(10*log10(sinr_vec) < threshold_dB); total_count = total_count + length(sinr_vec); end fprintf('中断概率: %.2f%%\n', 100*outage_count/total_count);这段代码的价值在于,它把信道模型和资源分配算法放在同一个统计框架下评估。如果中断概率超过 10%,说明要么信道模型太悲观,要么分配算法没有保护边缘链路。我一般会先调信道参数让中断概率落在 1% 到 5% 之间,再在这个基础上比较不同算法的吞吐量,否则算法比较没有意义。
还有一个容易被忽略的点是阴影衰落的相关性。真实环境里相邻链路的阴影衰落不是独立的,如果仿真里假设完全独立,会低估干扰的波动。常见做法是给阴影衰落加一个空间相关矩阵,用 Cholesky 分解生成相关随机变量。这个进阶操作在 D2D 密度高的时候影响明显,密度低时可以先用独立假设跑通再说。
从那以后我每次搭完信道模型,都强制先跑一遍中断概率和 SINR 分布,确认量级合理了再往上接算法,不然调算法调到怀疑人生,最后发现是信道参数错了。希望帮到你。
本文还有配套的精品资源,点击获取