
D2D通信这几年在学术界和工业界都是个绕不开的热点尤其是Underlay模式下复用蜂窝频谱的那套玩法。我断断续续把这个方向的研究做了快两年从最开始只会调别人的MATLAB脚本到现在能在GNU Octave里从零搭出一整套完整仿真链路中间踩过的坑、推翻过的设计思路都值得好好写一写。这篇文章不打算堆公式而是老老实实讲清楚Underlay模式下D2D通信仿真的整体框架怎么搭、资源分配和功率控制这两个核心模块怎么实现、用GNU Octave做通信仿真时有哪些坑以及最终结果怎么分析和调优。无论你是刚接触D2D方向的研究生还是工作中需要快速验证算法性能的工程师这套经过反复验证的仿真思路应该都能直接帮到你。1. 场景建模与方案选型思路1.1 Underlay模式的核心逻辑与干扰约束先聊透一个基本问题Underlay模式下D2D通信到底在解决什么。简单说D2D通信让两个距离很近的用户设备不经过基站直接交换数据好处是降低时延、减轻基站负载、提升频谱利用率。但如果D2D用户直接使用蜂窝用户的频谱两对链路之间就会产生同频干扰蜂窝用户作为频谱的原主必须优先保证它的通信质量不低于设定阈值。Underlay模式最核心的设计约束就是在给D2D用户分配资源时必须同时满足两个目标一是D2D链路自身能获得可用的信干噪比SINR二是它对蜂窝用户的干扰不能超过蜂窝用户能承受的底限。这个底限通常用蜂窝用户目标SINR对应的干扰上限来表示。资源分配解决的是D2D用户该复用哪个蜂窝用户的RB资源块功率控制则解决在这个RB上D2D该用多大功率发射。两个问题强耦合分开做会损失性能联合做又增加计算复杂度这是整个仿真的核心矛盾。我在最初建模时犯过一个典型错误把蜂窝用户当成固定干扰源只优化D2D链路自身的SINR完全没考虑D2D对蜂窝链路的反向干扰。结果仿真出来的D2D吞吐量很漂亮但一检查蜂窝链路性能发现SINR跌破了目标值一大截整个网络处于不可用状态。后来我把约束条件改写成了蜂窝用户实际SINR不低于目标SINR的硬约束仿真才真正意义。1.2 为什么选择GNU Octave而不是MATLAB工具选型这块我纠结过一阵子。MATLAB的通信工具箱确实好用但License成本高换设备后在课题组内共享也麻烦。GNU Octave作为开源替代品语法和MATLAB高度兼容90%的通信仿真代码可以无痛迁移。对D2D仿真这种以矩阵运算、循环迭代、随机数统计为主的场景Octave的性能完全够用。有人可能担心Octave的画图质量和运行速度。以我在四核笔记本上的实测数据为例一个包含50个蜂窝用户、30对D2D用户、1000次蒙特卡洛快照的仿真Octave跑完大约需要20到30分钟MATLAB大概快30%左右。对于科研验证来说这个差距完全可以接受。画图方面Octave生成的图虽然默认样式朴素但通过右键菜单或set()命令调整后插入论文完全没问题。还有一点很关键Octave的随机数生成机制默认不同版本间保持一致这对仿真结果的可复现性非常重要。我在写仿真环境时入口处固定了随机数种子rand(seed, 42)这样每次重跑实验、调参、debug对照的都是同一套信道实例定位问题效率高很多。这一点在MATLAB里反而容易被忽视。2. 系统模型搭建与参数预设2.1 仿真场景布局与拓扑生成仿真场景我采用的是单小区六边形模型半径500米基站eNB位于中心。蜂窝用户CUE在小区范围内均匀撒点D2D用户对DUE按发送端-接收端配对生成发送端随机分布接收端在以发送端为中心、半径50米范围内随机分布。这样设置的逻辑是模拟真实场景中D2D通常用于近距离通信的特点同时避免D2D收发端距离过远导致链路根本无法建立。用户数量这个参数直接决定仿真的规模感。我默认设置30个蜂窝用户、40对D2D用户这个规模既能体现资源复用带来的密度压力又不至于让仿真跑太久。蜂窝用户和D2D用户都是均匀撒点的但这里有个细节D2D接收端要以发送端为条件做二次撒点不能用独立均匀分布否则会出现收发端相距数百米的荒谬配置。拓扑生成还有一个容易忽略的点就是基站覆盖半径与用户距离的统计特征。如果蜂窝用户距离基站太远它本身的路径损耗就大SINR也会偏低对D2D干扰的容忍度就很差。所以我在生成拓扑后会做一个校验把距离基站超过450米的蜂窝用户重新撒点确保所有蜂窝用户都处于小区有效覆盖范围内避免极端位置点对统计结果造成偏差。2.2 信道模型与链路预算关键参数信道模型我采用标准的大尺度衰落加小尺度衰落混合形式。大尺度衰落包含路径损耗和阴影衰落路径损耗公式使用PL 128.1 37.6 * log10(d)其中d的单位是km。这个公式是3GPP TR 36.843里推荐的城区宏站场景参数我直接沿用。阴影衰落则用对数正态分布模拟标准差8dB。小尺度衰落简化为单位功率的瑞利衰落这在OFDM系统的每个子载波上都是合理假设。关于功率参数基站发射功率设为46dBm蜂窝用户发射功率为24dBmD2D发送端初始配置为23dBm后面会通过功率控制算法动态调整。噪声功率谱密度设定为-174dBm/Hz结合180kHz的RB带宽单RB噪声功率可以算出来N -174 10 * log10(180000) -174 52.55 -121.45 dBm这个数值是整个SNR计算链路的基准我在仿真代码里直接作为一个常量定义避免每次调用时重复计算。天线模型采用全向天线增益统一0dBi不做方向性差异。3. 资源分配与功率控制算法实现3.1 资源分配方案从随机复用走向干扰感知贪心资源分配最简单的方案是随机复用每个D2D用户随机挑一个蜂窝用户占用的RB不管干扰关系。这种方案实现非常容易但性能波动大而且对蜂窝链路的SINR破坏性极强。我在仿真一开始先跑这个baseline模型并不是为了用它做最终结果而是作为后续算法对比的下界参考。真正主用的算法是干扰感知的贪心匹配。它的核心逻辑是逐个处理D2D用户优先让D2D复用不会对蜂窝造成明显干扰的RB这个优先级通过一个度量函数来决定Metric(i,j) SINR_d2d(i,j) / (1 I_cellular(i,j))其中SINR_d2d(i,j)表示第i对D2D用户复用第j个蜂窝用户RB时的D2D链路SINRI_cellular(i,j)表示这个复用决定对蜂窝用户j造成的SINR损失比。这个度量兼顾了D2D自身的收益和对蜂窝的伤害比值越大越应该优先分配。贪心匹配的过程中有一个实际工程问题D2D用户逐个选择时先被选择的用户可能抢占了最优RB导致全局性能不是最优。我采用了一个简易的公平性修正先按所有D2D候选RB上的SINR均值从高到低排序SINR均值高的D2D用户优先选择。这样保证信道条件好的用户不会和信道差的用户抢同一个RB同时让信道差、资源稀缺的用户有机会拿到相对较好的RB。实测下来这种排序分配相比随机顺序D2D整体的频谱效率提升大约15%到20%。3.2 功率控制固定功率与迭代优化的对比资源分配决定了复用谁功率控制则要回答用多大功率发射。最基础的方案是固定功率D2D发送端统一用某个固定值比如10dBm。固定功率的好处是零计算开销但问题在于如果固定功率太高靠近基站的蜂窝用户会被干扰崩掉如果太低远离基站的D2D链路又达不到最低SINR需求。我在Code里实现了两种功率控制算法第一种是开环功率控制参考LTE上行PUSCH的机制P_tx min(P_max, P_0 10 * log10(M) alpha * PL)其中M是分配的RB数alpha是路损补偿因子默认0.8P_0是目标接收功率PL是D2D链路自身的路径损耗P_max设为23dBm上限。这个算法的逻辑很直接如果D2D收发端距离远、路径损耗大就自动提高发射功率反之则降低节省能量也减少干扰。第二种是基于迭代的干扰约束功率控制。思路是给定蜂窝用户的目标SINR阈值反推蜂窝用户能够容忍的最大干扰功率再把这个干扰预算分摊到复用它频谱的D2D用户上逐个更新D2D的发射功率。这个更新过程用公式表达就是P_d2d_new min(P_d2d_old, I_max / G_d2d_to_cellular)其中I_max是蜂窝用户能容忍的额外干扰上限G_d2d_to_cellular是D2D发送端到蜂窝用户接收端的信道增益。逐轮迭代直到所有D2D发射功率的变化小于某个收敛阈值比如0.1dB为止。这个算法在Octave里实现起来就是一个while循环关键在于每次迭代要重新计算所有D2D对蜂窝的干扰贡献还要处理多个D2D复用同一个RB的情况——此时这个RB上的干扰预算要被均分。实测下来迭代功率控制能让D2D总吞吐量在满足蜂窝SINR约束前提下提升大约30%到40%代价是每次仿真快照多出十几毫秒的计算时间。对整体仿真时长的影响完全可以接受。个人建议在科研初期先用固定功率和开环控制搭通全链路确认整体逻辑无误后再切换到迭代功率控制。4. 基于GNU Octave的仿真代码实现4.1 仿真主流程与模块化设计Octave代码的模块化设计直接影响后期调参效率。我把整个仿真拆成了五个脚本文件分别负责参数定义、拓扑生成、信道计算、资源分配与功率控制、性能统计。主入口脚本main.m只负责按顺序调用各模块并控制蒙特卡洛循环。模块结构如下d2d_sim/ ├── main.m // 主入口蒙特卡洛循环 ├── generate_params.m // 全局参数定义 ├── generate_topology.m // 用户分布拓扑生成 ├── compute_channel.m // 信道增益矩阵计算 ├── resource_allocate.m // 资源分配算法 ├── power_control.m // 功率控制算法 └── calculate_metrics.m // 性能指标统计这种一台磁带机式的结构虽然简单但每个模块都可以单独调试。我刚开始是写了一个巨大的main.m所有代码堆在一起结果每次改一个参数都要小心翼翼地往下翻还容易改错作用域。拆成独立函数后每个函数用nargin/nargout控制参数传递配合Octave的dbstop if error调试模式出问题时可以直接定位到具体函数具体行。主循环的关键伪代码如下% main.m (Octave) rand(seed, 42); for snapshot 1:num_snapshots [cue_pos, due_tx_pos, due_rx_pos] generate_topology(params); [channel_gain] compute_channel(params, cue_pos, due_tx_pos, due_rx_pos); [rb_alloc] resource_allocate(params, channel_gain); [tx_power] power_control(params, rb_alloc, channel_gain); [metrics(snapshot)] calculate_metrics(params, rb_alloc, tx_power, channel_gain); end4.2 关键代码实现与参数计算细节最值得展示的是信道矩阵的计算逻辑。在我的实现里channel_gain是一个三维矩阵维度分别是蜂窝用户数、D2D对数和链路类型其中链路类型包括D2D链路、D2D到蜂窝基站、D2D发送端到蜂窝接收端、蜂窝发送端到D2D接收端四类。这样设计的好处是后续算SINR和干扰时可以直接索引不用重复计算。一个典型的信道计算代码如下% compute_channel.m function gain compute_channel(params, cue_pos, due_tx_pos, due_rx_pos) num_cue size(cue_pos, 1); num_due size(due_tx_pos, 1); gain zeros(num_cue, num_due, 4); for i 1:num_cue for j 1:num_due % 链路1: D2D链路增益 d_d2d norm(due_tx_pos(j,:) - due_rx_pos(j,:)) / 1000; % km gain(i,j,1) path_loss(d_d2d) * shadow_fading(); % 链路2: D2D发送端到基站 d_t2b norm(due_tx_pos(j,:) - params.bs_pos) / 1000; gain(i,j,2) path_loss(d_t2b) * shadow_fading(); % 链路3: D2D发送端到蜂窝用户接收端 d_t2c norm(due_tx_pos(j,:) - cue_pos(i,:)) / 1000; gain(i,j,3) path_loss(d_t2c) * shadow_fading(); % 链路4: 蜂窝用户发送端到D2D接收端 d_c2r norm(cue_pos(i,:) - due_rx_pos(j,:)) / 1000; gain(i,j,4) path_loss(d_c2r) * shadow_fading(); end end end function pl path_loss(d_km) pl_dB 128.1 37.6 * log10(d_km); pl 10^(-pl_dB / 10); end这里有个需要提醒的细节所有距离单位在处理路径损耗前必须统一换算成km因为128.1这个常数的量纲就是基于km的。我之前有一次直接用了以米为单位的距离代入算出来的信道增益偏差了几个数量级整个SINR计算全乱套。这个坑我花了一个下午才排查出来原因就是单纯看了眼公式没注意单位。4.3 蒙特卡洛快照与统计置信度仿真实验要得到稳定的统计结果不能只跑一两次就下结论。我默认设置1000次快照每次独立生成拓扑和信道完成资源分配与功率控制后记录指标最后对1000组结果做平均。但这里有个关键问题1000次快照下整个数据量的存储和读取也需要留意。我的做法是每次快照结束后只把核心指标D2D总吞吐量、蜂窝SINR分布、功率均值追加到一个矩阵里而不是保存每个用户的全程轨迹这样内存占用从几GB降到几十MB。还有一点不是所有快照都有效。如果随机撒点时出现了极端拓扑比如某对D2D收发端距离小于5米导致信道增益异常大或者蜂窝用户紧贴基站导致干扰预算异常充裕这都会让单次快照的指标出现极端值。我加了一个数据清洗逻辑如果某次快照的蜂窝用户平均SINR低于预设下限就忽略这次快照的统计结果不纳入最终平均从而避免极端值把平均值拉偏。5. 结果分析与性能评估5.1 关键性能指标定义与解读性能评估主要看三个指标蜂窝用户的SINR保持情况、D2D链路的SINR分布、以及整体频谱效率。蜂窝用户的SINR保持情况是硬约束必须保证95%以上的蜂窝用户SINR不低于目标值我设为8dB否则说明D2D干扰失控。D2D链路的SINR分布则反映D2D通信自身的质量我统计的是CDF曲线和均值。频谱效率方面我计算D2D总吞吐量除以总占用带宽。这三个指标必须放在一起看单看任何一个都会得出片面结论。比如只优化D2D吞吐量蜂窝SINR可能崩掉反过来只保护蜂窝用户D2D可能根本没法工作。我在仿真结果展示中通常画三张图蜂窝SINR的CDF、D2D SINR的CDF、以及不同算法下D2D总吞吐量的柱状对比图。5.2 不同方案对比与参数灵敏度分析我将四种方案做了完整对比随机复用固定功率、干扰感知贪心分配固定功率、干扰感知贪心分配开环功率控制、干扰感知贪心分配迭代功率控制。结果非常规律方案D2D平均吞吐量 (Mbps)蜂窝SINR达标率蜂窝平均SINR (dB)随机复用固定功率3.8278%5.2贪心分配固定功率4.6191%7.1贪心分配开环功率控制5.4294%7.8贪心分配迭代功率控制6.3596%8.3从这个结果能看出一个有意思的现象蜂窝SINR达标率和D2D吞吐量并不是必然矛盾的优化资源分配和功率控制后两者可以同时提升。原因在于固定功率在大部分场景下其实功率过高导致干扰失控功率控制把功率降下来之后蜂窝用户受到的保护反而增强同时D2D在低功率下因为干扰减少SINR反而变好。参数灵敏度上我专门测试了D2D用户数从20对增加到60对时的系统表现。趋势是D2D用户越多复用同一RB的碰撞概率增大边缘用户的SINR明显下降。这说明在仿真中资产分配算法在高负载场景下需要考虑拒绝接入机制不能无条件让每个D2D复用RB。我后续给资源分配加了一个前置条件如果复用某RB时蜂窝用户的SINR预测值低于约束阈值则放弃这次复用尝试D2D保持等待状态。这个改动让高负载场景下的蜂窝SINR达标率重新回到95%以上。6. 常见问题与调试实录6.1 干扰计算中常见的几种错误模式写D2D仿真最容易出错的地方就是干扰链路的配对。我在排查自己复现文献结果时遇到过这样一个诡异问题蜂窝SINR达标率始终在90%左右徘徊怎么调功率参数都上不去。最后逐行检查才发现我在计算蜂窝用户SINR时把D2D发送端到蜂窝用户接收端的干扰链路和蜂窝用户到基站的有用链路搞混了方向导致干扰增益矩阵的对应用错算出来的干扰值比实际高了几个量级。还有一种常见的错误模式是忘记考虑同频复用的叠加干扰。如果三个D2D用户复用同一个蜂窝用户的RB那么蜂窝用户受到的干扰应该是三个D2D发送端贡献之和。我早期版本只取了最大的一个干扰项导致蜂窝SINR被大幅低估。正确写法是用sum函数把所有D2D在同RB上的干扰功率相加再代入SINR公式计算。另外单位换算永远是重灾区。dBm和mW之间的转换我在脚本里统一用10^(x/10)和10*log10(y)处理并写了一个简单函数封装避免到处手动转换。所有功率值在进入SINR公式之前必须统一成线性单位不能混用dBm和瓦特。6.2 Octave性能瓶颈与优化技巧Octave跑大循环的性能确实需要关注。D2D信道计算是个双重循环蜂窝用户数和D2D对数的乘积就是循环次数当两者都到50时就是2500次迭代。虽然在Octave里可以接受但如果加进1000次蒙特卡洛快照总迭代次数会膨胀到250万次这时候性能就吃紧了。我做过的有效优化有几种。第一把信道计算中的路径损耗部分改成矩阵运算用meshgrid生成距离矩阵后直接对矩阵做公式运算避免逐点循环。第二蒙特卡洛循环内尽量避免动态增加矩阵大小提前用零矩阵初始化用索引赋值减少内存重分配的开销。第三绘图只在外层循环完成后进行不要在每次快照内画图否则输出设备的刷新会让仿真时间翻倍。关于调试我强烈建议在开发阶段把蒙特卡洛快照数设成50次甚至更少。跑通逻辑后再恢复到完整实验。很多新手在写大仿真时调一次参数就要重跑1000次快照一次30分钟改10次就是5个小时效率极低。我自己的开发循环是10次快照跑通流程、100次快照验证趋势、1000次快照出正式结果三档递进非常有用。6.3 资源分配的死锁与公平性问题协作式干扰感知贪心分配在实现上有一个隐藏问题多个D2D用户对同一个RB的Metric值都很高竞争激烈时可能让部分D2D永远选不到资源也就是所谓的饿死现象。我第一次仿真时D2D接入率只有82%查了半天代码没看出算法错误后来才意识到是公平性问题。从算法设计上讲这个问题可以通过引入接入门限解决如果某个D2D在所有RB上的Metric值都低于阈值说明它和所有蜂窝用户都八字不合不允许接入反而对系统更好。此外迭代功率控制算法在极端场景下可能不收敛。我遇到过的场景是某个D2D同时复用了两个蜂窝用户的RB两个蜂窝用户对它都有干扰约束而且两个约束算出的功率上限互相矛盾导致循环震荡。解决办法是在每轮迭代后加一个功率变化限制也就是阻尼因子让功率每次最多调整0.5dB保证迭代平滑收敛。这个技巧在最开始的文献里没提到过但实测效果非常好收敛速度只慢了一点点稳定性却大幅提升。最后再分享一个个人建议仿真代码里的所有随机数和固定参数最好在脚本开头集中管理并且给出一份参数说明注释。这个习惯看起来微不足道但当论文需要补充仿真参数、或者两三个月后重新看自己的代码时好处就体现出来了。至少我自己的经验是这帮我避免了无数次我当初这个值到底是怎么定的的灵魂拷问。