
简介面向无线通信系统设计的高年级本科生与研究者这套MATLAB代码用于D2D设备直连信道仿真与资源分配研究重点展示启发式优化方法在解决频谱共享、功率控制与同频干扰问题时的实现思路。压缩包共22个文件核心是20个.m脚本另含2个.asv自动保存版本整体16KB体量小但模块划分明确适合精读与改造。脚本从main主函数开始依次完成路径损耗与瑞利衰落建模、用户及D2D对生成、平均干扰度量、随机分配与启发式分配、最优算法对比以及误码率与吞吐量统计等环节基本覆盖信道仿真到性能评估的完整链路。包内各函数以模块化方式组织便于单独提取复用也可作为后续扩展对比的基线对理解D2D信道特性与资源分配策略很有帮助。目前已有1214人学习下载值得需要物理层算法仿真的MATLAB用户收藏参考。1. D2D信道仿真真正耗时间的不是信道是干扰D2D信道仿真很多人以为是套几个路径损耗公式进MATLAB、跑一跑就能出图实际把代码跑通才发现信道建模只占三成工作量剩下七成都耗在干扰管理和资源分配上。这套MATLAB仿真工程做的就是这个完整链路从3GPP TR 36.843的近距离信道模型起步把大尺度衰落、瑞利快衰落、蜂窝用户同频干扰、D2D对之间的互干扰以及贪心资源分配和功率控制全部串成一条可跑的蒙特卡洛仿真线。它解决的是新手最头疼的问题信道算完了SINR怎么算、资源块怎么分、仿真结果为什么每次都不一样。适合两类人一类是毕设或课程论文里要跑D2D仿真的同学另一类是准备在MATLAB里验证新调度算法、但不想从零搭信道层的开发者。读完之后你手里会有一条能跑的仿真链路也知道每个参数该改哪里、算出来的数字怎么解释。2. 工程目录与技术选型一套配置驱动的仿真骨架2.1 目录结构从入口到可视化每一层在管什么拿到源码第一步不是急着运行而是先看目录。这套工程的模块划分遵循“信道层、算法层、场景层”三分离的原则所有仿真参数不散落在各个脚本里而是集中在配置文件里改动任何场景设定都不需要翻主循环代码。D2D_Channel_Sim/ ├── main.m # 总入口, 组装各个模块并执行蒙特卡洛循环 ├── config_default.m # 参数源, 区域大小/用户数量/功率/频段/种子 ├── channel_model/ │ ├── large_scale_ch.m # 大尺度衰落: 路径损耗 阴影衰落 │ ├── small_scale_ch.m # 小尺度衰落: 瑞利/莱斯快衰落 │ └── channel_gain.m # 合成函数: 把两种衰落合并成信道增益 ├── resource_alloc/ │ ├── greedy_alloc.m # 贪心RB分配, 核心调度算法 │ └── calc_power.m # 功率控制, 按目标SINR反推发射功率 ├── scenario/ │ └── gen_scenario.m # 生成一个快照: D2D对位置/蜂窝用户位置 ├── utils/ │ ├── dbm2w.m # dBm转瓦特 │ └── w2dbm.m # 瓦特转dBm ├── visualization/ │ └── plot_cdf.m # 绘制SINR/吞吐量的CDF对比曲线 └── results/ └── run_history.mat # 每次运行的完整结果存档main.m只做三件事读配置、循环跑快照、汇总画图。channel_model是整个仿真的地基所有链路增益都在这里算。resource_alloc是算法层greedy_alloc负责给D2D对分配资源块calc_power负责在满足SINR约束的前提下压低发射功率减少对蜂窝用户的干扰。utils里的两个单位转换函数看起来不起眼但后面会有无数地方用到相信我用惯了你会谢它的。visualization下的plot_cdf专门用来对比不同参数或不同算法下的性能分布论文里的那条CDF曲线就是从它这里出来的。2.2 技术选型为什么是MATLAB而不是NS-3或Python做D2D信道仿真可选的平台不少但不同阶段适合的工具完全不一样。NS-3适合协议级仿真跑完整的三层协议栈很在行但为了验证一个资源分配算法去搭一套NS-3环境学习成本就高了。Python配上SciPy也能做但信道增益矩阵的批量运算不如MATLAB原生顺手绘图倒是各有千秋。把三者的差异摊开看选型逻辑就很清楚了。特性MATLABNS-3Python SciPy信道建模上手速度快慢, 要学模块结构中等矩阵批量运算原生弱中等蒙特卡洛循环性能中快慢CDF/热力图绘制好弱好典型用途链路级算法验证全协议栈系统仿真原型验证这套工程瞄准的是“算法验证”所以MATLAB是正解。实际开发过程中我的习惯是先把所有功能写成独立函数输入输出全部用结构体传递不搞全局变量。结构体传参的好处是接口清晰后面想替换算法模块时只要新函数的输入输出结构体一致主循环一行都不用改。等到算法稳定了再考虑把结构体升级成classdef对象但一开始就用类是给自己找不痛快调试起来到处都是约束脚本阶段先把逻辑跑通才是正道。信道层、算法层、场景层各自独立还有一个额外的好处——任何一层出问题只需要单独测那一层的输入输出不用把整套仿真从头跑一遍。3. 核心模块拆解信道从大尺度衰减到快衰落的完整链路3.1 大尺度衰落3GPP TR 36.843近距离模型的MATLAB实现D2D通信的典型场景是几十米到一两百米距离这个范围用传统的COST231-Hata宏蜂窝模型并不合适Hata模型是从1km以上距离的场景拟合出来的硬套到50m的D2D链路上算出的路径损耗会偏离实际物理环境。常见做法是参考3GPP TR 36.843里定义的近距离模型分视距和非视距两套公式。function pl_db large_scale_ch(dist_m, fc_hz, link_type) % 大尺度衰落: 路径损耗 对数正态阴影衰落 % 输入: % dist_m - 收发距离(米), 标量或行向量 % fc_hz - 载频(Hz), 例如 3.5e9 % link_type - los 表示视距, nlos 表示非视距 % 输出: % pl_db - 路径损耗(dB), 不含阴影衰落 fc_ghz fc_hz / 1e9; if strcmp(link_type, los) % 视距场景: 对应广场、空旷楼道、视线无遮挡 pl_db 16.7 * log10(dist_m) 45.3 20 * log10(fc_ghz); else % 非视距场景: 对应隔墙、遮挡街道、拐角 pl_db 43.3 * log10(dist_m) 11.5 20 * log10(fc_ghz); end end两个模型在1GHz载频、20m距离下会有一个明显差异LOS模型算出的损耗远低于NLOS这符合直觉——近距离直射时波的衰减本来就不大一旦进入非视距多径折反射带来的损耗立刻上去。注意1800字后继续写。20*log10(fc_ghz)这一项是不能省的它把载频对传播损耗的影响折算进去载频翻一倍损耗大约增加6dB。仿真是3.5GHz频段和2.4GHz频段的结果直接差出好几dB不要拿一个频段的参数去套另一个场景。阴影衰落通常在外面再叠一层对数正态分布标准差按8dB处理。我会在gen_scenario里对每一条链路先用距离判定LOS概率常见的判定方式是距离小于20m直接判LOS大于100m判NLOS中间按线性概率过渡。这个判定阈值我习惯放在config_default里做成参数因为不同场景的遮挡情况差异很大写死会让后面换场景时很被动。还要注意一点大尺度衰落的变化速度远低于快衰落在单个快照内不应该每算一次SINR就重抽一次阴影衰落否则整个仿真就变成纯随机数生产器了。3.2 小尺度衰落瑞利信道生成与功率归一化小尺度衰落建模是另一个容易被忽视的黑匣子。D2D链路没有基站那类高架天线收发都在用户手里周围散射体复杂典型的衰落是瑞利分布。实现时每一条径的I、Q分量各自服从独立高斯分布再把所有径叠加幅度的包络自然呈现瑞利特性。function h_small small_scale_ch(num_paths, num_samples) % 生成瑞利快衰落信道系数 % 输入: % num_paths - 多径数目, 典型值8~12 % num_samples - 需要生成的采样点数 % 输出: % h_small - 1 x num_samples 复向量, 平均功率归一化为1 % 每条径的每个分量功率为 1/(2*num_paths) % 叠加后总功率 num_paths * (1/num_paths) 1 h_small (1 / sqrt(2 * num_paths)) * ... (randn(num_paths, num_samples) 1i * randn(num_paths, num_samples)); h_small sum(h_small, 1); end这里的功率归一化是关键。很多第一次做仿真的人忘了除以sqrt(2*num_paths)结果信道增益整体偏大算出来的SINR虚高整套仿真结论全部偏离。归一化之后|h_small|^2的平均值等于1它只刻画衰落波动不改变大尺度衰落确定的平均功率水平。如果需要莱斯信道就在复高斯变量上叠加一个确定性的直射分量K因子控制直射分量和散射分量的功率比改动量非常小。快衰落的使用方式也有讲究。我在这套仿真里按块衰落模型处理一个资源块内信道系数保持不变不同资源块之间重新生成。这对应现实里一个时频资源块的持续时间远小于信道相干时间但块与块之间的信道已经独立变化。如果你把快衰落放在最内层的SINR循环里每次重新生成你会得到一堆统计上毫无意义的噪声数据。3.3 SINR计算把信道增益翻译成系统级指标信道增益本身不是最终产出它要被折算成SINR才有业务意义。这一步的公式很简单但工程上的坑全在单位和干扰项的构成上。我的实现是先用合成函数把大尺度和小尺度衰落合并成线性域的信道增益再喂给SINR计算函数。function gain_lin channel_gain(dist_m, fc_hz, link_type) % 合成信道增益(线性域): 大尺度路径损耗取反 快衰落 % 返回线性功率增益, 可直接用于SINR分母/分子的计算 % 先取路径损耗(dB) pl_db large_scale_ch(dist_m, fc_hz, link_type); % 生成一个独立快衰落系数(块衰落模型) h_small small_scale_ch(1, 1); % 线性域: 10^(-pl/10) 是平均功率, 再乘以 |h|^2 得到瞬时功率 gain_lin 10.^(-pl_db / 10) .* abs(h_small).^2; endfunction sinr_db compute_sinr(p_tx_w, gain_lin, noise_w, i_w) % 计算单条链路的SINR(dB) % 输入: % p_tx_w - 发射功率(瓦特), 线性域 % gain_lin - 信道增益(线性), 由channel_gain得到 % noise_w - 热噪声功率(瓦特) % i_w - 同频干扰总功率(瓦特) % 输出: % sinr_db - SINR(dB) sinr_db 10 * log10((p_tx_w * gain_lin) / (noise_w i_w)); end热噪声功率按N kTB计算k是玻尔兹曼常数T取290开尔文B是资源块带宽再叠加接收机噪声系数。初学者常犯的错误是把噪声系数直接当成dB数加在噪声功率上正确的做法是把噪声系数经10*log10折算成线性倍数再乘进噪声功率。SINR计算函数本身纯粹是个公式翻译真正的复杂度在干扰功率i_w怎么聚合出来这就是资源分配模块要解决的事了。注意所有输入都在线性域不在函数内部再做单位换算单位转换统一由utils层的函数负责这样能避免一半以上的低级错误。4. 资源分配与功率控制干扰约束下的贪心调度4.1 RB复用与干扰矩阵预计算D2D仿真的核心矛盾在于复用蜂窝用户占用的资源块D2D对也可以用它来发送数据代价是互相干扰。一个D2D对收到的干扰来自两部分一部分是同频蜂窝用户的发射另一部分是复用了同一个资源块的其他D2D对的发射。建模的第一步是把这两类干扰链路全部预计算成矩阵这样算法层只需要做矩阵运算不必反复调用信道函数。% run_snapshot 中的信道预计算片段 % 输入: sc(场景结构体), params(仿真参数), fc(载频) for i 1:n_d2d % D2D本链路增益: 发射端到自己的接收端 d_self norm(sc.d2d(i).rx_pos - sc.d2d(i).tx_pos); G_d2d(i, 1) channel_gain(d_self, fc, los); % 蜂窝用户j对D2D对i接收端的干扰链路增益 for j 1:n_cue d_cue norm(sc.cue(j).pos - sc.d2d(i).rx_pos); G_c2d(i, j) channel_gain(d_cue, fc, nlos); end end % D2D发射端k对D2D对i接收端的干扰链路增益 for k 1:n_d2d for i 1:n_d2d if k ~ i d_inter norm(sc.d2d(k).tx_pos - sc.d2d(i).rx_pos); G_d2d_inter(i, k) channel_gain(d_inter, fc, nlos); end end end干扰链路用的是非视距模型因为D2D发射端到别的D2D接收端通常不存在直射路径这个细节决定了干扰功率的量级。预计算的好处很明显greedy_alloc里不需要再调channel_gain所有干扰关系都已经变成数值矩阵算法迭代快得多也方便后续加新的分配策略。这套缓存的思路移植到其他仿真场景同样成立信道相关计算放在场景生成阶段一次完成后续所有算法共享同一份信道实现能保证不同算法对比时基站条件完全一致统计上更公平。4.2 贪心资源分配按基准SINR排序再逐个填坑贪心分配算法的逻辑不复杂但执行顺序非常有讲究。我先计算每个D2D对在只有蜂窝干扰、没有D2D间干扰时的基准SINR然后按基准SINR从低到高排序——信道条件差的对优先选资源块之后每分配一个对都更新后续对的干扰环境。核心代码如下。function alloc greedy_alloc(result, rb_num) % 贪心RB分配 % 输入: % result - run_snapshot产出, 含G_d2d/G_c2d等预计算矩阵 % rb_num - 资源块数量 % 输出: % alloc - n_d2d x rb_num 逻辑矩阵, true表示该对占用该RB n_d2d length(result.G_d2d); % 蜂窝用户干扰: 简化为全频段满功率干扰 interf_cue sum(result.G_c2d, 2) * result.cue_power_w; % 基准SINR: 只考虑蜂窝干扰, 不算D2D间干扰 sinr_base (result.power_w * result.G_d2d) ./ ... (result.noise_w interf_cue); % 排序: 基准SINR低的优先分配, 保证公平性 [~, order] sort(sinr_base); alloc false(n_d2d, rb_num); for i order % 遍历每个RB, 计算该RB上已占用D2D对带来的额外干扰 for rb 1:rb_num occupied find(alloc(:, rb)); extra_i 0; for u occupied extra_i extra_i result.power_w * result.G_d2d_inter(i, u); end % 候选SINR 基准SINR衰减(额外干扰占比) sinr_cand(rb) sinr_base(i) / ... (1 extra_i / (result.noise_w interf_cue(i))); end % 选额外干扰最小的RB [~, best_rb] max(sinr_cand); alloc(i, best_rb) true; end end排序选“最差优先”的直觉在于信道好的对本身SINR高即使分配待干扰大的RB也能凑合满足业务需求而信道差的对如果还被强干扰压制整条链路就完全不可用。这其实是一套非常朴素的公平调度思想但它给后续算法留了比较基准。选RB时的策略也简单粗暴——遍历所有RB计算当前已占用该RB的D2D对会给新来者带来多少干扰挑干扰最小的那个。这里没考虑蜂窝用户在不同RB上的功率差异。如果要细化可以把每个RB绑定的蜂窝用户单独建模将interf_cue从向量扩展为矩阵sinr_base也变成n_d2d x rb_num算法的骨架不用动。4.3 开环功率控制用最小功率满足目标SINR资源分配定下来之后第二步就是给每个D2D对核定发射功率。这套工程里我用的是目标SINR反推功率的方式给定一个业务要求的目标SINR根据当前信道增益和干扰水平计算刚好满足需求的最小发射功率。function pw calc_power(target_sinr_db, gain_lin, noise_w, interf_w, p_max_w) % 功率控制: 用最小功率满足目标SINR % 输入: % target_sinr_db - 目标SINR(dB) % gain_lin - 本链路信道增益(线性) % noise_w - 热噪声功率(W) % interf_w - 本链路受到的干扰功率(W) % p_max_w - 最大发射功率约束(W) % 输出: % pw - 实际发射功率(W) target_lin 10^(target_sinr_db / 10); p_req target_lin * (noise_w interf_w) / gain_lin; pw min(p_max_w, p_req); end这个公式的前提是SINR和发射功率线性相关所以目标SINR转成线性后一乘就可以反推功率。interf_w要从资源分配的结果里聚合出来当前D2D对占用了哪些RB在每个RB上受到的蜂窝干扰和D2D间干扰之和是多少再对RB取平均。如果目标SINR达不到函数会返回p_max_w此时这条链路进入“尽力而为”模式实际SINR低于目标值业务可能中断这种链路在统计时要单独打标。提示固定功率发射是很多初版仿真的默认选择但它的代价是高载频下干扰被过度放大。功率控制加进来之后系统总吞吐量常常不升反降但单位功耗吞吐量会明显改善这在实际系统里才是有意义的优化方向。功率控制还有个迭代版本先固定发射功率算干扰再根据SINR调整功率反复迭代直到收敛。真实的蜂窝系统里确实有类似的控制环但仿真里做迭代会遇到收敛慢、震荡等问题做了三次以上的同学应该都见过功率在两个值之间反复横跳的情况这套工程暂时不做迭代一句话记住就行迭代功率控制需要加收敛保护否则就是无尽的震荡。5. 避坑指南D2D仿真中四个高频翻车现场5.1 随机数种子不固定结果无法复现现象是同样的参数跑两次SINR的CDF曲线对不上论文里画的图和二次验证时的数据总差一段。原因是每循环一次randn重新生成随机场景和信道整个仿真成了不可复现的黑匣子。解决方方法很简单main.m里第一行固定种子并且给每个快照派生独立子种子。我的习惯是主种子用2024第i个快照用rng(main_seed i)重置一遍这样不同快照之间统计独立而整次运行可以完整复现。结果存档时把种子也写进run_history.mat后面任何一次回放都拿得到现场。5.2 dBm、dB与线性域混用单位一塌糊涂现象是最常见也最隐蔽的SINR算出来数值离谱地高或低比如发射功率设成23但线性计算时直接把23当瓦特用干扰被放大了几个数量级。原因是dBm和W是两种不同的功率表示23dBm只等于0.2W左右不是23瓦特。还有个更隐蔽的坑是dB是比值单位dBm是绝对功率单位两者做加减合法但相乘除没有任何物理意义。我的工程里所有物理计算统一到线性域用户输入统一到dBm靠utils层做转换。转换关系并不复杂W 10^((dBm - 30)/10)随手写容易算错所以工具函数里封装好禁止在业务代码里直接写10^(x/10)这种裸计算。单位换算这件事属于典型的血泪教训我在初版仿真里因为把dBm当W用整套资源分配的结论都失效了从那以后每个新进项目的同学写的第一行工具函数必然是单位转换。5.3 隐式广播引发矩阵维度车祸现象是报错Matrix dimensions must agree或者更隐蔽的是不报错但速度突然变慢因为一个本应是向量的操作被广播成了大矩阵。MATLAB从2016b开始默认隐式扩展原来的维度不匹配报错变成自动广播。我之前排查过一次性能诡异下降最后发现是while循环里一个距离向量和自身转置相乘生成了n x n矩阵内存占用直接爆掉。解决的办法是在关键函数入口加防御性断言比如assert(isvector(dist_m) isscalar(fc_hz))并且约定所有向量的方向统一为行向量。这样即使调用方传错了错误会在第一时间暴露而不是在距离出错几层函数之后才浮现。5.4 平均SINR与平均容量的错位现象是平均SINR有18dB但用香农公式手算的吞吐量远低于仿真值或者换了调度算法之后平均SINR上升但总吞吐量反而下降。原因是E[log2(1SINR)]不等于log2(1E[SINR])对数函数是凹函数均值穿越和穿越均值是两回事。用dB平均再转线性的操作在处理容量统计时统计意义完全错乱。正确的统计口径是先对每个样本在线性域算容量再对所有样本求平均绝不能在dB域平均完再换算容量。我在plot_cdf之前的结果汇总模块里统一处理这层换算rate_slots变量存的永远是线性域的bps绘图之前才转成需要的单位。6. 二次开发把单场景脚本改造成可配置实验平台单跑一条蒙特卡洛仿真线只是起步真正有价值的是把它变成能批量做实验的平台。我的做法是先把所有参数收敛进一个配置文件main.m只做参数读取和结果归档不包含任何硬编码的常数。% config_default.m 的核心字段 params struct(); params.area [200, 200]; % 仿真区域尺寸(米) params.n_d2d 20; % D2D对数量 params.n_cue 10; % 蜂窝用户数量 params.rb_num 6; % 资源块数量 params.p_d2d_max_dbm 23; % D2D最大发射功率(dBm) params.p_cue_dbm 23; % 蜂窝用户发射功率(dBm) params.fc_hz 3.5e9; % 载频(Hz), 可切换到2.4e9 params.noise_fig_db 9; % 接收机噪声系数(dB) params.target_sinr_db 12; % 目标SINR(dB) params.seed 2024; % 随机种子主循环按“读配置、跑快照、聚合结果”三段式组织每个快照是一次独立的gen_scenario加一次greedy_alloc串起来就是整套蒙特卡洛实验。替换算法时不需要改主循环把新调度函数包装成和greedy_alloc相同签名的接口即可。信道模型层同理channel_model目录下新增一个同签名的mmwave_ch.m切换时只需要改channel_gain里的一行函数句柄。验证方法是每次改动前先跑一组baseline存档改动后重新运行用plot_cdf对比改动前后非目标模块的曲线是否完全重合。如果一次改动让信道模块的曲线也飘了说明改动污染了不该碰的部分需要回查。这套回归习惯帮我挡掉过至少三次“看似优化实则引入隐性bug”的改动。从那以后我每次动核心逻辑之前都强制先跑一遍baseline确认无关曲线不漂移才敢继续。希望帮到你。本文还有配套的精品资源点击获取