简介:这份资源面向无线通信方向的学生与研究人员,提供一套基于 MATLAB 的 D2D 信道仿真与资源分配代码,用于解决设备直连通信中信道建模、干扰分析与频谱资源优化等问题,适合具备一定 MATLAB 基础、正在学习或验证 D2D 系统性能的读者。压缩包共 22 个文件,以 20 个 .m 脚本为主体,辅以 2 个 .asv 自动备份文件,整体约 16KB,体积轻量便于直接运行与二次修改。代码覆盖信道建模、路径损耗计算、用户与 D2D 对生成、干扰距离统计等模块,并实现了随机算法、启发式算法与最优方案对比,可帮助读者复现吞吐量、覆盖率等性能评估流程,理解贪婪、遗传、粒子群等启发式思路在资源分配中的落地方式。目前已有 1214 人学习,适合作为 D2D 通信入门仿真与算法对照的实验素材。
1. 拿到这份 D2D 信道仿真 MATLAB 资源,先搞清楚它能替你省掉哪段苦力
如果你正在做 D2D 通信相关的课题,大概率经历过这个阶段:论文里的系统模型画得清清楚楚,公式也推完了,可真要跑出一条像样的 SINR 曲线或者资源分配对比图,就得从零搭一套 MATLAB 仿真环境——信道怎么建模、干扰怎么算、功率怎么配、调度怎么排,每一步都是坑。这份 D2D 信道仿真 MATLAB 资源,解决的就是这段“从公式到图”的苦力活。它把 D2D 通信场景下的信道建模、干扰计算和资源分配仿真串成了一条可运行的链路,适合通信方向的研究生、做链路级仿真的工程师,以及需要快速验证资源分配算法思路的开发者。你不需要从空白脚本开始,拿到手就能改参数、换算法、出对比图。
2. 项目结构与技术底座:这套仿真到底由哪几块拼起来
2.1 整体架构:三层结构撑起一条完整仿真链路
这套 MATLAB 仿真代码的组织方式,我拆下来看是典型的三层结构,和大多数通信链路级仿真的工程习惯一致:
- 场景配置层:负责定义小区半径、用户数量、D2D 对数量、带宽、噪声功率谱密度、路径损耗模型参数等。这一层通常是一个参数脚本或者结构体初始化函数,所有后续模块都从这里取参数。
- 信道与干扰计算层:根据用户位置计算大尺度衰落(路径损耗加阴影衰落),再叠加小尺度衰落,最终算出每个接收端的 SINR。D2D 场景的特殊之处在于,蜂窝链路和 D2D 链路共享频谱,干扰关系比纯蜂窝复杂得多——D2D 发射端干扰基站上行接收,D2D 接收端又被蜂窝用户干扰。
- 资源分配与调度层:决定哪些 D2D 对复用哪些蜂窝用户的资源块,以及功率怎么分配。这一层是大多数论文的核心创新点所在,也是你二次开发时改动最频繁的地方。
三层之间的数据流很清晰:配置层生成场景 → 信道层算出信道增益矩阵 → 分配层根据增益做决策 → 回到信道层算 SINR → 统计吞吐量或中断概率。理解这条链路,后面改代码就不会迷路。
2.2 目录与文件组织:常见做法是这样拆的
虽然具体文件名可能因版本而异,但这类仿真项目的文件组织通常遵循以下模式,你拿到资源后可以对照着找:
D2D_Simulation/ ├── main.m % 主入口,串起整个仿真流程 ├── config/ │ └── sim_params.m % 仿真参数集中管理 ├── channel/ │ ├── path_loss.m % 路径损耗计算 │ ├── shadowing.m % 阴影衰落生成 │ └── rayleigh_fading.m % 小尺度衰落 ├── resource/ │ ├── random_alloc.m % 随机资源分配基线 │ └── proposed_alloc.m % 核心分配算法 ├── utils/ │ ├── generate_positions.m % 用户位置生成 │ └── calc_sinr.m % SINR 统一计算入口 └── results/ └── plot_results.m % 结果可视化这种拆法的好处是每个文件职责单一,改信道模型只动channel/下的文件,换分配算法只动resource/下的文件,互不牵连。我一般建议先打开main.m从头到尾读一遍,看清楚调用顺序,再钻进具体模块。
2.3 参数配置:所有可调项集中在一处
参数集中管理是这套代码里最值得保留的设计习惯。典型配置长这样:
% sim_params.m - 仿真参数集中配置 function params = sim_params() params.numCells = 1; % 小区数量 params.cellRadius = 250; % 小区半径 (m) params.numCUE = 20; % 蜂窝用户数 params.numD2D = 10; % D2D 对数量 params.bandwidth = 10e6; % 系统带宽 (Hz) params.numRB = 50; % 资源块数量 params.noisePSD = -174; % 噪声功率谱密度 (dBm/Hz) params.maxTxPowerCUE = 23; % 蜂窝用户最大发射功率 (dBm) params.maxTxPowerD2D = 23; % D2D 最大发射功率 (dBm) params.pathLossExp = 3.5; % 路径损耗指数 params.shadowStd = 8; % 阴影衰落标准差 (dB) params.numRealizations = 1000; % 蒙特卡洛仿真次数 end这里每个参数都直接影响仿真结果的可信度。numRealizations设成 1000 意味着整个场景要重复生成 1000 次取平均,这是通信仿真里消除随机性影响的常规做法——单次 realization 的曲线抖动很大,取平均之后才能看出算法之间的真实差距。pathLossExp和shadowStd要和你参考的论文或标准对齐,否则算出来的 SINR 分布对不上,后面所有结论都站不住。
提示:改参数之前先把
numRealizations调小到 10 跑一遍,确认流程能走通、没有报错,再调回 1000 做正式仿真。1000 次跑下来可能要几分钟到十几分钟,别一上来就等。
3. 核心模块拆解:信道建模、干扰计算与资源分配怎么落地
3.1 信道增益计算:大尺度加小尺度,一步不能少
D2D 仿真里最基础也最容易写错的就是信道增益。完整做法是路径损耗、阴影衰落、小尺度衰落三者相乘(dB 域相加),少任何一项,SINR 分布都会偏。
% calc_channel_gain.m - 计算任意两点间的信道增益 function gain_dB = calc_channel_gain(dist, params, fadingType) % 路径损耗:对数距离模型 pathLoss_dB = 32.4 + 20*log10(2.1e9) + ... 10*params.pathLossExp*log10(max(dist, 1)); % 阴影衰落:对数正态分布 shadow_dB = params.shadowStd * randn(); % 小尺度衰落:瑞利或莱斯 if strcmp(fadingType, 'rayleigh') h = (randn() + 1j*randn()) / sqrt(2); fading_dB = 20*log10(abs(h)); else fading_dB = 0; % 简化场景可忽略小尺度 end gain_dB = -pathLoss_dB + shadow_dB + fading_dB; end逐段说清楚:pathLoss_dB用的是对数距离路径损耗模型,32.4 + 20*log10(f)是 2.1 GHz 载频下的自由空间参考损耗,后面那项按距离的pathLossExp次方衰减。max(dist, 1)是防止距离趋近于零时log10炸掉,这个保护在用户位置随机生成时非常必要——两个用户恰好靠得很近的概率虽小,但 1000 次 realization 里总会碰上。shadow_dB用randn()生成标准正态再乘标准差,这是对数正态阴影的标准做法。小尺度衰落用复高斯除以sqrt(2)保证平均功率归一化,取模再转 dB。
注意:
randn()每次调用都消耗随机数流,如果你在循环里反复调用而不控制随机种子,结果不可复现。正式跑之前用rng(seed)固定种子,方便对比不同算法时排除随机性干扰。
3.2 SINR 计算:D2D 场景的干扰关系是重点
SINR 计算是整套仿真的心脏。D2D 复用蜂窝上行资源时,接收端的干扰来源和纯蜂窝完全不同,写错了干扰项,后面资源分配算法再精巧也是白搭。
% calc_sinr_d2d.m - 计算 D2D 接收端的 SINR function sinr_dB = calc_sinr_d2d(d2dRx, d2dTx, cueTx, gainMatrix, params) % 有用信号:对应 D2D 发射端的信道增益 signal_dBm = params.maxTxPowerD2D + gainMatrix(d2dTx, d2dRx); % 干扰 1:复用同一 RB 的蜂窝用户 interference_cue = 0; for k = 1:length(cueTx) if is_reusing(cueTx(k), d2dRx) % 判断是否同 RB interference_cue = interference_cue + ... 10^( (params.maxTxPowerCUE + gainMatrix(cueTx(k), d2dRx))/10 ); end end % 干扰 2:其他复用同一 RB 的 D2D 发射端 interference_d2d = 0; for j = 1:length(d2dTx) if j ~= d2dTx && is_reusing(d2dTx(j), d2dRx) interference_d2d = interference_d2d + ... 10^( (params.maxTxPowerD2D + gainMatrix(d2dTx(j), d2dRx))/10 ); end end % 噪声 noise_mW = 10^( (params.noisePSD + 10*log10(params.bandwidth) - 30)/10 ); % 线性域合成后转 dB signal_mW = 10^(signal_dBm/10); sinr_dB = 10*log10( signal_mW / (interference_cue + interference_d2d + noise_mW) ); end这段代码的关键在于干扰项的枚举逻辑。is_reusing判断某个发射端是否和当前 D2D 接收端占用同一个资源块——这是 D2D 仿真的核心耦合点,资源分配算法改的就是这个映射关系。干扰功率在线性域(mW)累加,最后统一转 dB,这是标准做法;如果在 dB 域直接相加会得到错误结果,这是新手最容易翻车的地方之一。噪声功率由噪声谱密度加带宽再减 30(dBm 转 dBW 的换算)得到,量纲要对齐。
3.3 资源分配:从随机基线到你的算法
资源分配模块是二次开发的主战场。一套合格的仿真至少要有随机分配作为基线,否则你没法证明自己的算法“好”。
% random_alloc.m - 随机资源分配基线 function rbAssignment = random_alloc(numD2D, numRB) % 每个 D2D 对随机选一个 RB,允许复用 rbAssignment = randi(numRB, 1, numD2D); end % proposed_alloc.m - 基于信道质量的贪婪分配 function rbAssignment = proposed_alloc(gainMatrix, numD2D, numRB) rbAssignment = zeros(1, numD2D); rbLoad = zeros(1, numRB); % 记录每个 RB 被复用的次数 for d = 1:numD2D % 选信道增益最大且负载最轻的 RB score = gainMatrix(d, :) - 3 * rbLoad; [~, bestRB] = max(score); rbAssignment(d) = bestRB; rbLoad(bestRB) = rbLoad(bestRB) + 1; end endrandom_alloc就是一行randi,作为对照基线足够用了。proposed_alloc展示了一个典型的贪婪思路:score里第一项是信道增益,第二项-3*rbLoad是对负载的惩罚,系数 3 是我随手设的权重,你可以根据场景调——惩罚太轻会导致所有 D2D 挤在同一个 RB 上互相干扰,惩罚太重又会让信道好的 RB 利用率不足。这个权重本质上是吞吐量和公平性的折中旋钮,调参时建议画一条“D2D 数量 vs 平均吞吐量”的曲线来观察拐点。
提示:换算法时保持函数签名一致(输入增益矩阵和数量,输出 RB 分配向量),这样
main.m里只需要改一行函数名就能切换算法做对比,不用动其他代码。
4. 跑起来:从零到出图的完整操作路径
4.1 环境准备与首次运行
MATLAB 版本建议 R2018b 及以上,这套代码用到的函数都是基础通信和统计工具箱里的,不需要额外工具箱——但如果你后续要加优化求解,可能会用到 Optimization Toolbox。首次运行按这个顺序来:
- 把整个项目文件夹添加到 MATLAB 路径:
addpath(genpath('D2D_Simulation'))。 - 打开
main.m,把numRealizations临时改成 10。 - 直接运行
main.m,观察命令行输出有没有报错、有没有 NaN。 - 确认流程走通后,改回 1000,跑正式仿真。
main.m的典型结构是外层循环遍历 realization,内层依次调用位置生成、信道计算、资源分配、SINR 统计,最后把每次的结果存进数组,循环结束后取平均画图。第一次跑建议在main.m里加一句disp(['Realization ', num2str(r), '/', num2str(params.numRealizations)]),不然 1000 次跑下来你完全不知道进度到哪了,干等着心里没底。
4.2 结果验证:先看分布,再看曲线
跑出结果后别急着下结论,先做两个基本检查。第一,画 SINR 的 CDF 图,看分布形状是否合理——D2D 接收端的 SINR 通常集中在 0 到 20 dB 之间,如果出现大量负几十 dB 的值,大概率是干扰项算重了或者路径损耗参数不对。第二,对比随机分配和你的算法,在 D2D 数量较少时两者差距应该不大,随着 D2D 数量增加,好的算法优势才逐渐显现。如果一开始差距就巨大,反而要怀疑是不是基线写错了。
% plot_results.m - 画 SINR CDF 对比 figure; cdfplot(sinr_random); hold on; cdfplot(sinr_proposed); legend('Random', 'Proposed'); xlabel('SINR (dB)'); ylabel('CDF'); grid on;cdfplot是统计工具箱的函数,如果没有,用sort加linspace手写也就三行。这张图是你论文里最常出现的图之一,横轴 SINR、纵轴累积概率,两条曲线的水平间距就是算法增益的直观体现。
4.3 常见参数调整场景
不同课题对参数的需求不一样,这里列几个高频调整场景和对应改法:
| 调整目标 | 改哪个参数 | 注意事项 |
|---|---|---|
| 增大 D2D 密度 | numD2D | 同步观察干扰是否饱和,吞吐量曲线会出现平台 |
| 改变小区规模 | cellRadius | 路径损耗随之变化,SINR 整体平移 |
| 模拟不同衰落环境 | pathLossExp、shadowStd | 要和参考论文对齐,否则结论不可比 |
| 控制仿真时长 | numRealizations | 先小后大,正式结果不低于 500 |
| 调整功率约束 | maxTxPowerD2D | 功率增大干扰也增大,存在最优值 |
改numD2D时特别要注意:D2D 对数量增加后,如果资源块数量不变,复用程度加剧,干扰会快速上升,吞吐量曲线可能在某个点之后不升反降。这个拐点本身就是有价值的仿真结论,别把它当成 bug 去“修”。
5. 避坑与排查:这几处翻车点我替你踩过了
5.1 现象:SINR 全是负值或者低得离谱
原因:最常见的是 dB 和线性域混用。比如把 dBm 的功率直接和 dB 的增益相加得到“功率”,或者在线性域算完干扰又忘了转回 dB。另一个可能是噪声功率量纲错了,noisePSD是 dBm/Hz,加带宽时如果带宽用了 MHz 而没换算,噪声会小三个数量级,SINR 虚高;反过来如果多除了一个 1000,噪声偏大,SINR 就全负了。
解决:在calc_sinr_d2d里临时加disp打印信号功率、干扰功率、噪声功率三个中间量,逐个核对量纲。信号功率应该在 -20 到 0 dBm 量级,噪声在 -100 dBm 量级(10 MHz 带宽下),对不上就查换算。
5.2 现象:每次运行结果都不一样,没法对比算法
原因:随机数种子没固定。MATLAB 启动时默认用时间做种子,每次randn序列都不同,1000 次 realization 取平均虽然能压住大部分波动,但两种算法如果不在同一组随机场景下对比,差异里混着随机噪声。
解决:在main.m最开头加rng(42),并且在切换算法对比时,确保两种算法跑的是同一组信道实现——常见做法是把信道增益矩阵先全部生成好存起来,再分别喂给不同分配算法,而不是每个算法各自重新生成场景。
5.3 现象:用户位置生成后出现距离为零或极小的点
原因:均匀分布在圆内生成点时,如果直接用rand乘半径,点会向圆心聚集(面积元是r*dr不是dr),导致大量用户挤在基站附近,距离趋近零,路径损耗公式里的log10出问题。
解决:圆内均匀采样要用r = R*sqrt(rand()),开根号是为了抵消面积随半径增长的影响。同时在路径损耗函数里保留max(dist, 1)的保护,双保险。
5.4 现象:仿真跑得极慢,1000 次要几个小时
原因:循环里反复调用函数、动态扩容数组、或者在最内层做了本可以向量化的操作。MATLAB 的循环开销不小,尤其是嵌套循环里每次randn单独调用。
解决:把能向量化的都向量化——信道增益矩阵一次性算完,别在双重循环里逐个算;预分配结果数组sinr_all = zeros(1, numRealizations)而不是sinr_all(end+1) = ...;随机数一次性生成randn(N,1)而不是循环里调 N 次。这几招下来通常能快一个数量级。
5.5 现象:换了自己的分配算法后,结果反而不如随机基线
原因:大概率是算法输出的 RB 索引和仿真框架期望的格式不一致,比如你的算法返回 0 起始索引而框架用 1 起始,或者返回的是矩阵而框架期望向量,导致is_reusing判断全错,干扰算成了零或者全干扰。
解决:在main.m里分配完之后加一句assert(all(rbAssignment >= 1 & rbAssignment <= params.numRB)),先把索引范围卡死。再打印几个样本的分配结果人工核对,确认映射关系符合预期再跑全量。
6. 二次开发:把这份仿真改成你自己的算法验证平台
拿到这套代码之后,真正有价值的工作是把它变成你课题的验证平台。我的建议是从“替换一个模块、保持接口不变”开始,而不是推倒重写。具体来说,资源分配层是最容易替换的——保持rbAssignment = your_alloc(gainMatrix, numD2D, numRB)这个签名,内部随便你怎么写,main.m里改一行函数名就能跑对比。这样你可以在同一组信道实现下,把随机分配、贪婪分配、你的算法三条曲线画在一张图上,结论才有说服力。
如果你要做功率控制而不是资源分配,改动点在calc_sinr_d2d的功率取值上——把固定的maxTxPowerD2D换成一个由功率控制函数返回的向量,注意干扰项里其他 D2D 的功率也要同步更新,别只改了有用信号那一项。这是我在类似项目里翻过车的地方:只改发射功率忘了改干扰计算,结果功率越大 SINR 越高,物理上完全说不通,查了半天才发现是干扰项还用的旧功率。
验证方法上,我习惯做两个 sanity check。第一,把 D2D 数量设成 0,此时系统退化成纯蜂窝,SINR 分布应该和经典蜂窝仿真结果吻合,对不上说明信道或干扰模型有问题。第二,把路径损耗指数设成 2、阴影标准差设成 0,退化成自由空间,此时理论 SINR 可以手算,和仿真结果对比误差应该在 1 dB 以内。这两个检查花不了十分钟,但能帮你排除掉大部分模型层面的错误。
还有一个实用技巧:把每次仿真的关键参数和结果摘要存成一个结构体save('result_xxx.mat', 'params', 'sinr_all'),文件名带上日期和算法名。做对比实验时最怕的就是跑完忘了当时用的什么参数,回头想复现都复现不了。从那以后我每次跑正式仿真都强制走一遍“存参数加存结果”的流程,后悔药这东西,能提前吃就别等出事。
希望这份拆解能帮你少走几步弯路,把精力花在算法本身而不是环境搭建上。
本文还有配套的精品资源,点击获取