简介:面向无线通信领域研究人员与MATLAB开发者,这套D2D信道仿真与资源分配代码包聚焦设备直连通信场景,覆盖信道建模、路径损耗计算、干扰协调及频谱资源分配等关键环节,并引入贪婪、遗传、粒子群等启发式算法对比,适合学习D2D系统仿真与优化算法实现。包体共22个文件,以20个m脚本为主、辅以2个asv自动备份文件,整体压缩包仅16KB,代码轻量、易于阅读和二次开发。核心模块包含主程序、用户与D2D对生成、干扰测量、Edmonds匹配、随机算法及启发式贪心算法等,便于按功能拆解运行。目前已有1214人学习该资源。通过运行这些代码,可直观理解D2D通信中资源分配与干扰抑制的权衡过程,掌握基于MATLAB的系统级仿真流程,为课程设计、毕业课题或论文复现提供可直接修改的参考实现。1. D2D信道仿真MATLAB:从一段能跑的代码到一整套可复现的工程我之前在某项目组做终端直通物理层预研时,最头疼的并不是算法推导,而是网上能找到的D2D信道仿真MATLAB工程大多只丢一个孤零零的函数,拿回来之后要自己拼配置、补随机种子、处理距离矩阵维度,经常因为一个行列方向的问题就绕进去一整天。模拟项目X不一样,它把信道建模和资源分配完整地串成了工程:从config参数、路径损耗、瑞利/莱斯小尺度衰落、干扰计算到RB资源分配,每个环节都是独立函数,主循环跑完直接画出SINR累计分布曲线。这个工程特别适合两类人:一是刚接触D2D物理层仿真、需要一个基线工程当作起点的研究生,二是在宏蜂窝场景下对比D2D资源分配算法效果的工程研究者。整个代码相当克制,没有依赖冷门工具箱,装好基础MATLAB就能跑。2. 工程解剖:目录结构、参数入口与技术选型2.1 文件布局:仿真工程应该怎么拆拿到工程压缩包之后,我习惯先把目录看一遍,确认它是能跑起来的完整工程,而不是一堆散落脚本。模拟项目X的目录结构大致是这样:D2D_channel_sim/ ├── config.m ├── main.m ├── src/ │ ├── channel/ │ │ ├── path_loss.m │ │ ├── fast_fading.m │ │ └── shadow_fading.m │ ├── resource/ │ │ ├── compute_sinr.m │ │ └── allocate_rb.m │ └── utils/ │ ├── distance_matrix.m │ └── plot_cdf.m └── results/这个划分很直接:channel目录只负责无线信道的物理过程,resource目录只处理干扰和资源块分配,utils目录放跨模块复用的工具函数。config.m是全局参数脚本,main.m是唯一入口,results目录用来落图和数据文件。我一般会坚持“入口只有一个”的原则,不要在多个脚本里各自定义参数,否则改起来会顾此失彼。channel和resource分离还有一个额外好处:如果你想把信道模型换成别的标准,只需要动channel下的文件,资源分配算法完全不用碰;反过来,如果你只想调资源分配策略,信道相关的文件可以原封不动。这种模块边界在工程代码里看不见摸不着,但在做算法对比实验时非常重要——它保证了你换了分配算法之后,信道条件完全相同,公平性才有保障。2.2 参数入口:config.m是改参数的第一站config.m本质上是一个MATLAB脚本,不是函数,所以它不返回任何东西,而是通过直接执行给当前工作区注入一堆变量。main.m第一行会调用它。这样做有一个很实际的好处:你在命令行里跑完main.m后,所有参数还留在工作区里,可以随时手工查看某个中间变量,这对于调试距离矩阵或者信道增益特别方便。工程里比较关键的参数集中在下面这组,我按照实际使用频率挑了主要的几个:参数名默认值作用area_size1000仿真区域边长,单位米,决定用户密度bs_pos[0 0]基站坐标,处理蜂窝链路时使用num_d2d10D2D用户对数量num_cellular5蜂窝用户数量,也是上行RB数量carrier_freq2e9载频,单位Hz,用于路径损耗模型p_d2d23D2D发射功率,单位dBmp_cellular23蜂窝用户发射功率,单位dBmnoise_dbm-104等效噪声功率,单位dBm,含带宽折算path_loss_model'UMa'蜂窝链路的路径损耗模型fading_type'Rayleigh'小尺度衰落类型,可选Ricianmc_trials500蒙特卡洛仿真次数sinr_threshold_dB0资源分配的最低SINR门限注意噪声功率noise_dbm这里直接写成了等效值,而不是让每个人再乘带宽算一次,这样能少踩一个单位换算的坑。如果你有特别想要的噪声系数或带宽,可以改成noise_dbm = -174 + 10*log10(bandwidth) + noise_figure,但默认值已经包含了20MHz带宽和适中的噪声系数。改参数操作本身没什么玄学,但有一点我吃过亏:不要在main.m里再写一遍参数覆盖config.m。原工程里除了config.m之外没有任何地方重新定义噪声功率或衰落类型,这是刻意保持的单一事实来源。如果你发现某次实验曲线不变化,先检查是不是自己手动在工作区里覆盖了某个变量,导致后续循环读的是旧值。2.3 为什么这个工程坚持用基础MATLAB函数很多D2D信道的例程会直接调用pdist2计算距离,或者用comm.RayleighChannel这种通信工具箱对象。模拟项目X刻意避开了这两招,数值上并没有区别,但换来了更高的可移植性。比如距离计算用的是两层循环,虽然代码看起来朴素一点,可是在几百个用户对场景里计算速度完全够用,而且不用依赖Statistics Toolbox;瑞利衰落是手动用randn构造复数序列,也不依赖额外工具箱。这意味着你在网上找一台只装了基础MATLAB的机器,把工程放进去就能直接跑,不会出现一打开就报“缺少工具箱”的尴尬。从二次开发角度看,基础函数还有一个容易被忽略的好处:你可以直接控制每一处实现的细节。用工具箱里的信道对象固然方便,但它是一个黑匣子,里面的归一化方式、滤波器尾长、相位更新逻辑都不透明,出了问题你只能改参数碰运气。手写公式虽然要多写几行,但每行都能解释,调试起来不用猜。模拟项目X这种“能看穿一切”的写法,其实更适合拿来做教学和算法对比,因为任何一个变量都能打印出来人工验证。3. 核心模块拆解:一个信号从发射机到SINR的完整链路3.1 距离矩阵:坐标索引是第一条坑D2D信道仿真的第一步永远是算距离。工程里没有直接用MATLAB自带的pdist2,而是写了一个distance_matrix函数放在utils目录下,代码如下:function distm = distance_matrix(tx_pos, rx_pos) % distance_matrix: 计算发射机到接收机的欧氏距离矩阵 % tx_pos: Nt x 2 矩阵,每一行是一个发射机坐标 % rx_pos: Nr x 2 矩阵,每一行是一个接收机坐标 % 输出 distm: Nt x Nr 矩阵,distm(i,j) 表示第 i 个发射机到第 j 个接收机的距离 Nt = size(tx_pos, 1); Nr = size(rx_pos, 1); distm = zeros(Nt, Nr); for i = 1:Nt for j = 1:Nr distm(i, j) = norm(tx_pos(i, :) - rx_pos(j, :)); end end end这个函数看着简单,但它的参数顺序非常容易写反。在D2D直通链路里,tx_pos传的是D2D发射机坐标集合,rx_pos传的是D2D接收机坐标集合;在蜂窝干扰链路里,tx_pos传的是蜂窝用户坐标,rx_pos传的是D2D接收机坐标。如果你把tx_pos和rx_pos传反了,矩阵形状并不一定会报错,因为只要两个维度恰好都是Nt和Nr,MATLAB照样能算出一个转置形状的结果,但是物理意义上“信号链路”和“干扰链路”就串位了,后面的SINR会变得完全无法解释。所以我拿到这个函数后做的第一件事,不是在主循环里闷头往下写,而是先构造两个极小的坐标矩阵做单元验证。比如让tx_pos只有两个点,rx_pos只有两个点,打印输出矩阵的对角元素,确认第一个发射机到第一个接收机的距离是符合直觉的值,再放大规模。这种验证方法虽然老土,但能保证后面整个链路不因为一个索引问题崩掉。距离矩阵的单位也值得注意,工程里默认全部用米,path_loss内部会自己对极小距离做保护,避免距离为零导致的log10除零。3.2 路径损耗:3GPP模型与D2D直通模型的差异距离算完之后,接下来就是把距离折成路径损耗。模拟项目X把路径损耗单独封装在path_loss.m中,支持两种模式,一种给宏蜂窝链路用,一种给D2D直通链路用:function pl = path_loss(dist_m, fc, model) % path_loss: 路径损耗计算 % dist_m: 距离,单位米,可以是矩阵 % fc: 载频,单位Hz,当前版本暂时只在UMa公式中使用 % model: 选用的模型字符串,'UMa' 或 'D2D' % 输出 pl: 路径损耗,单位dB,与输入dist_m同维度 if strcmp(model, 'UMa') pl = 128.1 + 37.6 * log10(max(dist_m, 1e-3)); else pl = 148 + 40 * log10(max(dist_m, 1e-3)); end endUMa代表的是城郊宏基站场景,公式里的常数128.1包含了载频等于2GHz时的参考路径损耗,37.6是路径损耗指数,它描述的是非视距宏蜂窝链路。D2D直通链路的默认写法把指数拉高到40,相当于用更快的损耗速度来模拟终端直连时周围物体遮挡更严重的情况。这里我在读代码时专门看了一眼它对dist_m为零的保护:max(dist_m, 1e-3)这一步很有必要,因为D2D接收机位置可能刚好和发射机坐标碰上,如果不过滤,log10(0)会直接给负无穷,后面信道增益就全是零。工程里没有把fc真正代入D2D分支,这是刻意的简化。实际项目中如果你想支持5G频段的3.5GHz或者毫米波,可以把fc加进公式,比如在UMa分支里加入20*log10(fc/2e9)之类的频率修正项。我建议在二次开发时把这个参数留好,因为换频段是D2D参数研究中很常见的调整方向。路径损耗计算完之后,一般会转成线性增益再参与信道增益叠加,也就是后面代码里会出现sqrt(10.^(-pl/10))这种写法,注意单位是dB,运算顺序是先取负再除10最后乘方,这个换算我在调试时经常看到有人写反。3.3 小尺度衰落:瑞利和莱斯到底差在哪路径损耗描述的是大尺度趋势,真实无线环境还需要加入小尺度衰落。工程里的fast_fading.m主要做了两种模式:瑞利和莱斯。核心代码如下:function h = fast_fading(num_samples, fading_type, K_factor) % fast_fading: 生成小尺度复数信道系数 % num_samples: 需要生成的样本数量 % fading_type: 'Rayleigh' 或 'Rician' % K_factor: 莱斯K因子,单位是线性值,仅在Rician时使用 % 输出 h: 1 x num_samples 复数序列,平均功率归一化为1 if strcmp(fading_type, 'Rayleigh') h = sqrt(0.5) * (randn(1, num_samples) + 1i * randn(1, num_samples)); elseif strcmp(fading_type, 'Rician') mu = sqrt(K_factor / (K_factor + 1)); sigma = sqrt(1 / (2 * (K_factor + 1))); h = mu + sigma * (randn(1, num_samples) + 1i * randn(1, num_samples)); else error('未知衰落类型,请使用 Rayleigh 或 Rician'); end end瑞利衰落看起来只是两个高斯随机变量的组合,但里面那个sqrt(0.5)非常关键。一个标准的复高斯变量如果实部和虚部都是标准正态分布,那么它的模平方期望值是2,显然不符合信道系数功率归一化到1的约定。加上sqrt(0.5)之后,实部虚部功率各为0.5,合成功率正好是1。很多初学者在仿真时忘记这一步,导致最后SINR整体偏高,还以为是系统设计得好,实际上是衰落功率被放大了3dB。这是我见过最多的一次性翻车点。莱斯模型则是在瑞利基础上加了一条固定视距路径,mu是视距分量的幅度,sigma是散射分量的标准差。从公式推导看,K因子越大,视距分量占比越高,衰落波动越小。工程里默认只用瑞利,但如果你把config.m里的fading_type改成'Rician',需要在main.m调用时多传一个K_factor参数,这个参数在config.m中没有默认值,我一般会把它设成10,对应10dB的莱斯K因子。注意这里的K因子是线性值,不是dB,如果你从论文里读到K=10dB,需要先转成10再传进来。3.4 干扰计算与资源分配:启发式算法的工程实现D2D用户复用蜂窝用户的上行RB时,干扰主要来自使用同一RB的蜂窝用户。模拟项目X用compute_sinr.m把信号和干扰折算成SINR矩阵,然后交给allocate_rb.m做分配。先看SINR怎么算:function sinr_mat = compute_sinr(H_d2d, H_interf, p_d2d, p_cellular, noise_power) % compute_sinr: 计算D2D链路的SINR线性值 % H_d2d: K x M 矩阵,D2D链路信道增益 % H_interf: K x M 矩阵,蜂窝用户到D2D接收机的干扰链路增益 % p_d2d: D2D发射功率,单位瓦特 % p_cellular: 蜂窝用户发射功率,单位瓦特 % noise_power: 噪声功率,单位瓦特 % 输出 sinr_mat: K x M 矩阵,线性SINR值 sinr_mat = p_d2d * H_d2d ./ (p_cellular * H_interf + noise_power); % 防止极端情况下的除零 sinr_mat(isinf(sinr_mat)) = 1e6; end这里H_d2d的行对应D2D用户,列对应蜂窝用户占用的RB。H_d2d(k,m)代表第k个D2D对在第m个RB上的等效信道增益,H_interf(k,m)代表第m个蜂窝用户对第k个D2D接收机造成的干扰链路增益。分母里蜂窝用户发射功率乘以干扰链路增益,再加上噪声功率,这是上行链路最标准的SINR表达式。需要注意的是功率单位:config.m里给的是dBm,在main.m中调用时需要用10^((p_d2d-30)/10)转成瓦特,如果偷懒直接用dBm数值参与计算,SINR会被放大1e3倍,曲线直接飞上天。接下来是资源分配。这个工程没有走复杂的迭代注水算法,而是用一个带SINR门限的贪心策略,我在阅读时把它单独抽出来看:function rb_alloc = allocate_rb(sinr_mat, sinr_threshold_lin) % allocate_rb: 给每个D2D用户分配一个RB,使用保守的两用户复用策略 % sinr_mat: K x M 矩阵,线性SINR值 % sinr_threshold_lin: 线性SINR门限,低于该值的RB不能被分配 % 输出 rb_alloc: K x 1 向量,取值0..M,0表示未分配 K = size(sinr_mat, 1); M = size(sinr_mat, 2); rb_alloc = zeros(K, 1); n_assign = zeros(1, M); [~, order] = sort(max(sinr_mat, [], 2), 'descend'); for idx = 1:K u = order(idx); feasible = true(1, M); for m = 1:M % 一个RB最多服务2个D2D用户,超过则不可行 if n_assign(m) >= 2 feasible(m) = false; end % SINR低于门限的RB不可行 if sinr_mat(u, m) < sinr_threshold_lin feasible(m) = false; end end if any(feasible) [~, best] = max(sinr_mat(u, :) .* feasible); rb_alloc(u) = best; n_assign(best) = n_assign(best) + 1; else rb_alloc(u) = 0; end end end这个分配策略的核心思想是“先来后到加门限控制”。sort那一行先按每个用户在所有RB上的最大SINR降序排序,让信道条件好的用户优先生成分配决策。比如第k个D2D用户在某个RB上的SINR很高,就先让它选走,避免它后来被迫去跟强干扰用户挤同一个RB。feasible数组用来记录每个RB是否允许分给当前用户,一个RB最多复用两个D2D用户,这是在仿真自由度上是刻意的保守选择——如果允许无限制复用,需要考虑同RB用户之间的互干扰,代码要加一层迭代循环,会让初版工程复杂不少。门限sinr_threshold_lin的作用是实现“宁可不分配也不要强干扰”,如果一个RB的SINR低于门限,就算它是唯一空闲的RB,也直接标记为不可用。这种做法在很多论文里叫“门限调度”,实际系统里也是为了避免边缘用户拖垮整体性能。如果你把门限设得太高,比如调成10dB以上,会发现D2D用户被强制未分配的比例明显上升,甚至出现某个RB始终空置的情况,这不是bug,是策略正确工作。用户等级order计算也隐含了一个公平性问题:SINR高的用户占便宜,SINR低的用户只能在剩下的RB里选,这种“机会主义”调度在对比算法时是常见的参照系。4. 跑通仿真:从main.m到SINR累计分布图4.1 主流程:一次仿真发生了什么工程把整个蒙特卡洛循环放在main.m里,第一次打开这个文件时,我原以为会有几百行,实际上主干很短。核心循环大概可以浓缩成下面这个结构:% main.m 核心流程 rng(42); % 固定随机流,保证结果可复现 config; % 加载所有配置参数 sinr_all = []; for trial = 1:mc_trials % 生成随机拓扑 d2d_tx = area_size * rand(num_d2d, 2) - area_size/2; d2d_rx = area_size * rand(num_d2d, 2) - area_size/2; cellular_pos = area_size * rand(num_cellular, 2) - area_size/2; % 信号链路信道增益 dist_d2d = distance_matrix(d2d_tx, d2d_rx); pl_d2d = path_loss(dist_d2d, carrier_freq, 'D2D'); h_fast = fast_fading(num_d2d, fading_type, 10); H_d2d = sqrt(10.^(-pl_d2d/10)) .* h_fast; % 干扰链路信道增益 dist_interf = distance_matrix(cellular_pos, d2d_rx); pl_interf = path_loss(dist_interf, carrier_freq, path_loss_model); H_interf = sqrt(10.^(-pl_interf/10)); % 功率与噪声换算 p_d2d_w = 10^((p_d2d - 30) / 10); p_cell_w = 10^((p_cellular - 30) / 10); noise_w = 10^((noise_dbm - 30) / 10); % 计算SINR并分配RB sinr_lin = compute_sinr(H_d2d, H_interf, p_d2d_w, p_cell_w, noise_w); ...... % 这里调用 allocate_rb 获得 rb_alloc,然后只保留已分配用户的SINR sinr_all = [sinr_all; sinr_dB_allocated(:)]; end plot_cdf(sinr_all, 'results/sinr_cdf.png');我故意在中间用省略号跳过了一个细节,因为在阅读原工程时我发现它还有一步:分配完RB后,需要对sinr_lin做一次“按rb_alloc过滤”,把未被分配的用户从统计样本里删掉。否则CDF曲线会混入大量低于门限的值,掩盖真实调度的性能。这一步逻辑虽然简单,但它直接决定了曲线的物理意义——是“所有D2D用户的SINR分布”还是“成功调度D2D用户的SINR分布”,论文图里的曲线必须说清楚。我在自己的实验里一般保留后者,并会在图例或者表格里注明样本数。整个运行过程在MC次数等于500时,普通笔记本电脑大概要跑十几秒,瓶颈主要不在距离循环,而在SINR矩阵的逐次拼接。如果你嫌慢,可以在循环开始前预先分配一个大数组,每次往固定索引填充,循环结束后再截断,这是MATLAB性能优化最见效的一种方式。我改了之后,500次仿真时间缩短了大概一半,但代价是代码可读性稍微下降了一点。4.2 结果验证:CDF曲线怎么看才可信跑通之后别急着截图,先问自己三个问题。第一,曲线取值范围是不是合理的物理范围?如果sinr_dB的中位数在-5dB以下,多半是噪声功率设置过高或者路径损耗指数取得太大;如果中位数超过了20dB,极其有可能是功率单位没换算或者衰落归一化出了问题。第二,曲线形状是否符合预期?瑞利衰落下的SINR CDF一般来说会比较平滑地穿过中值附近,如果曲线出现明显台阶或者跳变,可能是因为RB分配后样本数太少,或者门限边缘条件把一堆用户切在了同一个整数值上。第三,重复运行结果是否稳定?由于main.m第一行固定了rng(42),在同一个环境下多次运行应该得到几乎完全一样的曲线,只有随机流没有被重置时才应该有波动。我一般会连跑三次看一下三条曲线是否重叠,如果三条线完全分离,就说明有变量没有被重置。CDF还有一种快速验证方法:把fading_type改成'Rician'并且把K因子调大,比如设成100,曲线会明显变得更集中在中高SINR区域。如果你看不到这个变化趋势,那么大概率是fast_fading里的K因子没传对,或者功率归一化被覆盖。这条验证并不严谨,但它能快速暴露出代码里“衰落类型切换”是否真的生效。4.3 参数与结果的对应关系做资源分配方向的人最关心的就是参数变化如何影响曲线。我把常用调整和预期效果整理成下面这个简要对应表:调参动作预期曲线变化关键原因调大p_d2dSINR曲线整体向右移动信号功率增大,干扰项不变调大p_cellular干扰增大,曲线向左移动D2D接收机收到的蜂窝干扰变强调低sinr_threshold_dB更多用户被调度,低SINR样本变多门限放宽,边缘用户也获得RB增加num_d2d单RB复用压力增大,部分用户未分配可用RB数不变,竞争加剧改area_size为200用户间距缩小,路径损耗降低距离项直接影响信道增益这个表基本上覆盖了最常用的几个调节方向。如果你发现调了某个参数但曲线完全不变,不用怀疑是自己操作不对,大概率是参数在config.m中的名称没对上,或者在main.m里有一处硬编码覆盖了它。我就在调p_cellular时遇到过这个问题,因为main.m里还有一行临时变量p_cell_w = 10^((p_cellular - 30)/10),看起来没问题,但我在另一个函数里又自己定义了一遍,结果改了半天没效果。这种同名覆盖问题在不写全局参数的工程里特别容易发生,所以我在二次开发时会主动搜索所有出现某个参数的地方,确认只有config.m一处定义。5. 避坑记录:D2D仿真里最隐蔽的4个坑5.1 距离矩阵行列写反,SINR数据一塌糊涂现象:SINR曲线出现很多负20dB以下的样本,甚至混入NaN,检查路径损耗公式也找不到明显错误。原因:调用distance_matrix(cellular_pos, d2d_rx)时,把蜂窝用户坐标和D2D接收端坐标传反了,导致干扰链路增益被写进信号链路,信号链路增益被当作干扰,两个角色互换。解决:在distance_matrix函数头加一个参数维度断言,如果输入矩阵的行数不符合当前链路的定义就报错;另外在主脚本中每个trial开始时用size()打印关键矩阵维度,人工看一眼是否与预期一致。从那以后我每次写带链路区分的仿真,都在函数名后缀上注明信号链路还是干扰链路,比如distance_matrix_signal和distance_matrix_interference,避免语义模糊。5.2 瑞利衰落少乘sqrt(0.5),信道功率无故翻倍现象:所有SINR曲线比同配置的理论值高约3dB,且用户间相对差异不大,整体看着“过于乐观”。原因:randn生成的标准正态变量实部虚部功率各为1,直接组合成复数后平均功率为2,没有归一化到1。很多人只用randn(1,N) + 1i*randn(1,N),忘记了系数。解决:在fast_fading.m的瑞利分支中乘以sqrt(0.5),并在函数说明里明确标注“平均功率归一化为1”。如果你拿到的信道数据来自工具箱对象,也要先看它的输出功率约定,很多通信工具箱默认输出的是单位功率,不需要你再手动缩放,但有些模型需要额外配置。5.3 资源分配出现死循环,CPU直接拉满现象:运行allocate_rb时程序长时间不退出,任务管理器里MATLAB CPU占用100%,只能强制终止。原因:我把sinr_threshold_dB设成了10dB,在稀疏场景下大量用户找不到任何可行RB,而原工程某个初版实现用了while循环等待用户找到可用RB,找不到就永远空转。解决:将RB搜索改成for循环,并且增加最大尝试次数上限;同时在进入分配之前判断max(sinr_mat(u,:))是否小于门限,如果连最大SINR都低于门限就直接标记为未分配,不再做内部扫描。我在二次开发后把分配函数改成“即使无可行RB也必须返回0”的约定,主循环就永远不会卡死。5.4 随机种子不固定,曲线复现成为玄学现象:同一份代码隔天跑出来的CDF曲线形状有差异,甚至两次运行中某个点的概率值能差5个百分点。原因:rand和randn使用的全局随机流在每次MATLAB启动时都不同,脚本没有重置,不同机器上表现更明显。解决:main.m第一行写死rng(42),如果做批量实验,则使用for seed = 1:100循环,并将seed作为变量传入配置,最后把每个seed对应的结果文件单独保存。固定随机种子还能辅助调试:当你发现一条曲线很奇怪时,可以回头用同一个seed重新运行,复现同一组用户位置,逐步打印中间变量定位问题。6. 二次开发:把静态拓扑改成动态移动模型这个工程目前的拓扑是每轮蒙特卡洛重新随机生成,相当于每个时刻的快照之间没有任何相关性。如果你想研究D2D用户在移动过程中的切换或者长时平均性能,最简单的一步改造是加入随机游走模型。核心思路是在每个时间片t里更新坐标位置,同时给快衰落加时间相关性。坐标更新可以写成一个很小的函数:% 随机游走位置更新 function pos = random_walk_update(pos, velocity, dt, area_size) % pos: N x 2 矩阵,当前时刻坐标 % velocity: 平均移动速度,单位m/s % dt: 仿真步长,单位s step = velocity * dt; pos = pos + step * randn(size(pos)); pos = max(min(pos, area_size/2), -area_size/2); end调用时,只需要把原来的rand生成位置改成先初始化位置再循环调用random_walk_update,每个时刻重新计算距离矩阵和SINR。这里有个关键技术点:快衰落不能每时刻重新生成独立随机数,否则时间相关性会彻底丢失,移动模型就变成了“位置漂移但信道快速随机闪变”的混合体,物理上不自洽。我一般会给每个D2D链路预先存一个固定相位偏移量,在每个时刻用相位旋转来模拟时间相关性,比如h_t = h_t_prev * exp(1i * delta_phase),其中delta_phase由移动速度和载频折算。改造完成后的验证方法很简单:把velocity分别设成1m/s和5m/s,运行两次仿真,画出SINR CDF或者平均吞吐量随时间的变化曲线。如果两条曲线几乎完全一致,说明移动模型并没有影响信道的时间相关性——这时候要回头检查是不是每个时刻都重新调用了fast_fading生成独立序列,而不是做相位连续演化。如果你研究的是资源分配策略在移动场景下的切换开销,还可以在分配函数里记录RB切换次数,输出一个额外指标。最后我想说一个我自己的习惯:拿到任何别人的仿真代码,都先固定随机种子,打印一遍关键矩阵的维度,确认能复现再动算法。这个习惯挽救过我好几个下午的时间,每次改动前也能有一个可信的baseline。希望帮到你。本文还有配套的精品资源,点击获取
简介:面向无线通信领域研究人员与MATLAB开发者,这套D2D信道仿真与资源分配代码包聚焦设备直连通信场景,覆盖信道建模、路径损耗计算、干扰协调及频谱资源分配等关键环节,并引入贪婪、遗传、粒子群等启发式算法对比,适合学习D2D系统仿真与优化算法实现。包体共22个文件,以20个m脚本为主、辅以2个asv自动备份文件,整体压缩包仅16KB,代码轻量、易于阅读和二次开发。核心模块包含主程序、用户与D2D对生成、干扰测量、Edmonds匹配、随机算法及启发式贪心算法等,便于按功能拆解运行。目前已有1214人学习该资源。通过运行这些代码,可直观理解D2D通信中资源分配与干扰抑制的权衡过程,掌握基于MATLAB的系统级仿真流程,为课程设计、毕业课题或论文复现提供可直接修改的参考实现。1. D2D信道仿真MATLAB:从一段能跑的代码到一整套可复现的工程我之前在某项目组做终端直通物理层预研时,最头疼的并不是算法推导,而是网上能找到的D2D信道仿真MATLAB工程大多只丢一个孤零零的函数,拿回来之后要自己拼配置、补随机种子、处理距离矩阵维度,经常因为一个行列方向的问题就绕进去一整天。模拟项目X不一样,它把信道建模和资源分配完整地串成了工程:从config参数、路径损耗、瑞利/莱斯小尺度衰落、干扰计算到RB资源分配,每个环节都是独立函数,主循环跑完直接画出SINR累计分布曲线。这个工程特别适合两类人:一是刚接触D2D物理层仿真、需要一个基线工程当作起点的研究生,二是在宏蜂窝场景下对比D2D资源分配算法效果的工程研究者。整个代码相当克制,没有依赖冷门工具箱,装好基础MATLAB就能跑。2. 工程解剖:目录结构、参数入口与技术选型2.1 文件布局:仿真工程应该怎么拆拿到工程压缩包之后,我习惯先把目录看一遍,确认它是能跑起来的完整工程,而不是一堆散落脚本。模拟项目X的目录结构大致是这样:D2D_channel_sim/ ├── config.m ├── main.m ├── src/ │ ├── channel/ │ │ ├── path_loss.m │ │ ├── fast_fading.m │ │ └── shadow_fading.m │ ├── resource/ │ │ ├── compute_sinr.m │ │ └── allocate_rb.m │ └── utils/ │ ├── distance_matrix.m │ └── plot_cdf.m └── results/这个划分很直接:channel目录只负责无线信道的物理过程,resource目录只处理干扰和资源块分配,utils目录放跨模块复用的工具函数。config.m是全局参数脚本,main.m是唯一入口,results目录用来落图和数据文件。我一般会坚持“入口只有一个”的原则,不要在多个脚本里各自定义参数,否则改起来会顾此失彼。channel和resource分离还有一个额外好处:如果你想把信道模型换成别的标准,只需要动channel下的文件,资源分配算法完全不用碰;反过来,如果你只想调资源分配策略,信道相关的文件可以原封不动。这种模块边界在工程代码里看不见摸不着,但在做算法对比实验时非常重要——它保证了你换了分配算法之后,信道条件完全相同,公平性才有保障。2.2 参数入口:config.m是改参数的第一站config.m本质上是一个MATLAB脚本,不是函数,所以它不返回任何东西,而是通过直接执行给当前工作区注入一堆变量。main.m第一行会调用它。这样做有一个很实际的好处:你在命令行里跑完main.m后,所有参数还留在工作区里,可以随时手工查看某个中间变量,这对于调试距离矩阵或者信道增益特别方便。工程里比较关键的参数集中在下面这组,我按照实际使用频率挑了主要的几个:参数名默认值作用area_size1000仿真区域边长,单位米,决定用户密度bs_pos[0 0]基站坐标,处理蜂窝链路时使用num_d2d10D2D用户对数量num_cellular5蜂窝用户数量,也是上行RB数量carrier_freq2e9载频,单位Hz,用于路径损耗模型p_d2d23D2D发射功率,单位dBmp_cellular23蜂窝用户发射功率,单位dBmnoise_dbm-104等效噪声功率,单位dBm,含带宽折算path_loss_model'UMa'蜂窝链路的路径损耗模型fading_type'Rayleigh'小尺度衰落类型,可选Ricianmc_trials500蒙特卡洛仿真次数sinr_threshold_dB0资源分配的最低SINR门限注意噪声功率noise_dbm这里直接写成了等效值,而不是让每个人再乘带宽算一次,这样能少踩一个单位换算的坑。如果你有特别想要的噪声系数或带宽,可以改成noise_dbm = -174 + 10*log10(bandwidth) + noise_figure,但默认值已经包含了20MHz带宽和适中的噪声系数。改参数操作本身没什么玄学,但有一点我吃过亏:不要在main.m里再写一遍参数覆盖config.m。原工程里除了config.m之外没有任何地方重新定义噪声功率或衰落类型,这是刻意保持的单一事实来源。如果你发现某次实验曲线不变化,先检查是不是自己手动在工作区里覆盖了某个变量,导致后续循环读的是旧值。2.3 为什么这个工程坚持用基础MATLAB函数很多D2D信道的例程会直接调用pdist2计算距离,或者用comm.RayleighChannel这种通信工具箱对象。模拟项目X刻意避开了这两招,数值上并没有区别,但换来了更高的可移植性。比如距离计算用的是两层循环,虽然代码看起来朴素一点,可是在几百个用户对场景里计算速度完全够用,而且不用依赖Statistics Toolbox;瑞利衰落是手动用randn构造复数序列,也不依赖额外工具箱。这意味着你在网上找一台只装了基础MATLAB的机器,把工程放进去就能直接跑,不会出现一打开就报“缺少工具箱”的尴尬。从二次开发角度看,基础函数还有一个容易被忽略的好处:你可以直接控制每一处实现的细节。用工具箱里的信道对象固然方便,但它是一个黑匣子,里面的归一化方式、滤波器尾长、相位更新逻辑都不透明,出了问题你只能改参数碰运气。手写公式虽然要多写几行,但每行都能解释,调试起来不用猜。模拟项目X这种“能看穿一切”的写法,其实更适合拿来做教学和算法对比,因为任何一个变量都能打印出来人工验证。3. 核心模块拆解:一个信号从发射机到SINR的完整链路3.1 距离矩阵:坐标索引是第一条坑D2D信道仿真的第一步永远是算距离。工程里没有直接用MATLAB自带的pdist2,而是写了一个distance_matrix函数放在utils目录下,代码如下:function distm = distance_matrix(tx_pos, rx_pos) % distance_matrix: 计算发射机到接收机的欧氏距离矩阵 % tx_pos: Nt x 2 矩阵,每一行是一个发射机坐标 % rx_pos: Nr x 2 矩阵,每一行是一个接收机坐标 % 输出 distm: Nt x Nr 矩阵,distm(i,j) 表示第 i 个发射机到第 j 个接收机的距离 Nt = size(tx_pos, 1); Nr = size(rx_pos, 1); distm = zeros(Nt, Nr); for i = 1:Nt for j = 1:Nr distm(i, j) = norm(tx_pos(i, :) - rx_pos(j, :)); end end end这个函数看着简单,但它的参数顺序非常容易写反。在D2D直通链路里,tx_pos传的是D2D发射机坐标集合,rx_pos传的是D2D接收机坐标集合;在蜂窝干扰链路里,tx_pos传的是蜂窝用户坐标,rx_pos传的是D2D接收机坐标。如果你把tx_pos和rx_pos传反了,矩阵形状并不一定会报错,因为只要两个维度恰好都是Nt和Nr,MATLAB照样能算出一个转置形状的结果,但是物理意义上“信号链路”和“干扰链路”就串位了,后面的SINR会变得完全无法解释。所以我拿到这个函数后做的第一件事,不是在主循环里闷头往下写,而是先构造两个极小的坐标矩阵做单元验证。比如让tx_pos只有两个点,rx_pos只有两个点,打印输出矩阵的对角元素,确认第一个发射机到第一个接收机的距离是符合直觉的值,再放大规模。这种验证方法虽然老土,但能保证后面整个链路不因为一个索引问题崩掉。距离矩阵的单位也值得注意,工程里默认全部用米,path_loss内部会自己对极小距离做保护,避免距离为零导致的log10除零。3.2 路径损耗:3GPP模型与D2D直通模型的差异距离算完之后,接下来就是把距离折成路径损耗。模拟项目X把路径损耗单独封装在path_loss.m中,支持两种模式,一种给宏蜂窝链路用,一种给D2D直通链路用:function pl = path_loss(dist_m, fc, model) % path_loss: 路径损耗计算 % dist_m: 距离,单位米,可以是矩阵 % fc: 载频,单位Hz,当前版本暂时只在UMa公式中使用 % model: 选用的模型字符串,'UMa' 或 'D2D' % 输出 pl: 路径损耗,单位dB,与输入dist_m同维度 if strcmp(model, 'UMa') pl = 128.1 + 37.6 * log10(max(dist_m, 1e-3)); else pl = 148 + 40 * log10(max(dist_m, 1e-3)); end endUMa代表的是城郊宏基站场景,公式里的常数128.1包含了载频等于2GHz时的参考路径损耗,37.6是路径损耗指数,它描述的是非视距宏蜂窝链路。D2D直通链路的默认写法把指数拉高到40,相当于用更快的损耗速度来模拟终端直连时周围物体遮挡更严重的情况。这里我在读代码时专门看了一眼它对dist_m为零的保护:max(dist_m, 1e-3)这一步很有必要,因为D2D接收机位置可能刚好和发射机坐标碰上,如果不过滤,log10(0)会直接给负无穷,后面信道增益就全是零。工程里没有把fc真正代入D2D分支,这是刻意的简化。实际项目中如果你想支持5G频段的3.5GHz或者毫米波,可以把fc加进公式,比如在UMa分支里加入20*log10(fc/2e9)之类的频率修正项。我建议在二次开发时把这个参数留好,因为换频段是D2D参数研究中很常见的调整方向。路径损耗计算完之后,一般会转成线性增益再参与信道增益叠加,也就是后面代码里会出现sqrt(10.^(-pl/10))这种写法,注意单位是dB,运算顺序是先取负再除10最后乘方,这个换算我在调试时经常看到有人写反。3.3 小尺度衰落:瑞利和莱斯到底差在哪路径损耗描述的是大尺度趋势,真实无线环境还需要加入小尺度衰落。工程里的fast_fading.m主要做了两种模式:瑞利和莱斯。核心代码如下:function h = fast_fading(num_samples, fading_type, K_factor) % fast_fading: 生成小尺度复数信道系数 % num_samples: 需要生成的样本数量 % fading_type: 'Rayleigh' 或 'Rician' % K_factor: 莱斯K因子,单位是线性值,仅在Rician时使用 % 输出 h: 1 x num_samples 复数序列,平均功率归一化为1 if strcmp(fading_type, 'Rayleigh') h = sqrt(0.5) * (randn(1, num_samples) + 1i * randn(1, num_samples)); elseif strcmp(fading_type, 'Rician') mu = sqrt(K_factor / (K_factor + 1)); sigma = sqrt(1 / (2 * (K_factor + 1))); h = mu + sigma * (randn(1, num_samples) + 1i * randn(1, num_samples)); else error('未知衰落类型,请使用 Rayleigh 或 Rician'); end end瑞利衰落看起来只是两个高斯随机变量的组合,但里面那个sqrt(0.5)非常关键。一个标准的复高斯变量如果实部和虚部都是标准正态分布,那么它的模平方期望值是2,显然不符合信道系数功率归一化到1的约定。加上sqrt(0.5)之后,实部虚部功率各为0.5,合成功率正好是1。很多初学者在仿真时忘记这一步,导致最后SINR整体偏高,还以为是系统设计得好,实际上是衰落功率被放大了3dB。这是我见过最多的一次性翻车点。莱斯模型则是在瑞利基础上加了一条固定视距路径,mu是视距分量的幅度,sigma是散射分量的标准差。从公式推导看,K因子越大,视距分量占比越高,衰落波动越小。工程里默认只用瑞利,但如果你把config.m里的fading_type改成'Rician',需要在main.m调用时多传一个K_factor参数,这个参数在config.m中没有默认值,我一般会把它设成10,对应10dB的莱斯K因子。注意这里的K因子是线性值,不是dB,如果你从论文里读到K=10dB,需要先转成10再传进来。3.4 干扰计算与资源分配:启发式算法的工程实现D2D用户复用蜂窝用户的上行RB时,干扰主要来自使用同一RB的蜂窝用户。模拟项目X用compute_sinr.m把信号和干扰折算成SINR矩阵,然后交给allocate_rb.m做分配。先看SINR怎么算:function sinr_mat = compute_sinr(H_d2d, H_interf, p_d2d, p_cellular, noise_power) % compute_sinr: 计算D2D链路的SINR线性值 % H_d2d: K x M 矩阵,D2D链路信道增益 % H_interf: K x M 矩阵,蜂窝用户到D2D接收机的干扰链路增益 % p_d2d: D2D发射功率,单位瓦特 % p_cellular: 蜂窝用户发射功率,单位瓦特 % noise_power: 噪声功率,单位瓦特 % 输出 sinr_mat: K x M 矩阵,线性SINR值 sinr_mat = p_d2d * H_d2d ./ (p_cellular * H_interf + noise_power); % 防止极端情况下的除零 sinr_mat(isinf(sinr_mat)) = 1e6; end这里H_d2d的行对应D2D用户,列对应蜂窝用户占用的RB。H_d2d(k,m)代表第k个D2D对在第m个RB上的等效信道增益,H_interf(k,m)代表第m个蜂窝用户对第k个D2D接收机造成的干扰链路增益。分母里蜂窝用户发射功率乘以干扰链路增益,再加上噪声功率,这是上行链路最标准的SINR表达式。需要注意的是功率单位:config.m里给的是dBm,在main.m中调用时需要用10^((p_d2d-30)/10)转成瓦特,如果偷懒直接用dBm数值参与计算,SINR会被放大1e3倍,曲线直接飞上天。接下来是资源分配。这个工程没有走复杂的迭代注水算法,而是用一个带SINR门限的贪心策略,我在阅读时把它单独抽出来看:function rb_alloc = allocate_rb(sinr_mat, sinr_threshold_lin) % allocate_rb: 给每个D2D用户分配一个RB,使用保守的两用户复用策略 % sinr_mat: K x M 矩阵,线性SINR值 % sinr_threshold_lin: 线性SINR门限,低于该值的RB不能被分配 % 输出 rb_alloc: K x 1 向量,取值0..M,0表示未分配 K = size(sinr_mat, 1); M = size(sinr_mat, 2); rb_alloc = zeros(K, 1); n_assign = zeros(1, M); [~, order] = sort(max(sinr_mat, [], 2), 'descend'); for idx = 1:K u = order(idx); feasible = true(1, M); for m = 1:M % 一个RB最多服务2个D2D用户,超过则不可行 if n_assign(m) >= 2 feasible(m) = false; end % SINR低于门限的RB不可行 if sinr_mat(u, m) < sinr_threshold_lin feasible(m) = false; end end if any(feasible) [~, best] = max(sinr_mat(u, :) .* feasible); rb_alloc(u) = best; n_assign(best) = n_assign(best) + 1; else rb_alloc(u) = 0; end end end这个分配策略的核心思想是“先来后到加门限控制”。sort那一行先按每个用户在所有RB上的最大SINR降序排序,让信道条件好的用户优先生成分配决策。比如第k个D2D用户在某个RB上的SINR很高,就先让它选走,避免它后来被迫去跟强干扰用户挤同一个RB。feasible数组用来记录每个RB是否允许分给当前用户,一个RB最多复用两个D2D用户,这是在仿真自由度上是刻意的保守选择——如果允许无限制复用,需要考虑同RB用户之间的互干扰,代码要加一层迭代循环,会让初版工程复杂不少。门限sinr_threshold_lin的作用是实现“宁可不分配也不要强干扰”,如果一个RB的SINR低于门限,就算它是唯一空闲的RB,也直接标记为不可用。这种做法在很多论文里叫“门限调度”,实际系统里也是为了避免边缘用户拖垮整体性能。如果你把门限设得太高,比如调成10dB以上,会发现D2D用户被强制未分配的比例明显上升,甚至出现某个RB始终空置的情况,这不是bug,是策略正确工作。用户等级order计算也隐含了一个公平性问题:SINR高的用户占便宜,SINR低的用户只能在剩下的RB里选,这种“机会主义”调度在对比算法时是常见的参照系。4. 跑通仿真:从main.m到SINR累计分布图4.1 主流程:一次仿真发生了什么工程把整个蒙特卡洛循环放在main.m里,第一次打开这个文件时,我原以为会有几百行,实际上主干很短。核心循环大概可以浓缩成下面这个结构:% main.m 核心流程 rng(42); % 固定随机流,保证结果可复现 config; % 加载所有配置参数 sinr_all = []; for trial = 1:mc_trials % 生成随机拓扑 d2d_tx = area_size * rand(num_d2d, 2) - area_size/2; d2d_rx = area_size * rand(num_d2d, 2) - area_size/2; cellular_pos = area_size * rand(num_cellular, 2) - area_size/2; % 信号链路信道增益 dist_d2d = distance_matrix(d2d_tx, d2d_rx); pl_d2d = path_loss(dist_d2d, carrier_freq, 'D2D'); h_fast = fast_fading(num_d2d, fading_type, 10); H_d2d = sqrt(10.^(-pl_d2d/10)) .* h_fast; % 干扰链路信道增益 dist_interf = distance_matrix(cellular_pos, d2d_rx); pl_interf = path_loss(dist_interf, carrier_freq, path_loss_model); H_interf = sqrt(10.^(-pl_interf/10)); % 功率与噪声换算 p_d2d_w = 10^((p_d2d - 30) / 10); p_cell_w = 10^((p_cellular - 30) / 10); noise_w = 10^((noise_dbm - 30) / 10); % 计算SINR并分配RB sinr_lin = compute_sinr(H_d2d, H_interf, p_d2d_w, p_cell_w, noise_w); ...... % 这里调用 allocate_rb 获得 rb_alloc,然后只保留已分配用户的SINR sinr_all = [sinr_all; sinr_dB_allocated(:)]; end plot_cdf(sinr_all, 'results/sinr_cdf.png');我故意在中间用省略号跳过了一个细节,因为在阅读原工程时我发现它还有一步:分配完RB后,需要对sinr_lin做一次“按rb_alloc过滤”,把未被分配的用户从统计样本里删掉。否则CDF曲线会混入大量低于门限的值,掩盖真实调度的性能。这一步逻辑虽然简单,但它直接决定了曲线的物理意义——是“所有D2D用户的SINR分布”还是“成功调度D2D用户的SINR分布”,论文图里的曲线必须说清楚。我在自己的实验里一般保留后者,并会在图例或者表格里注明样本数。整个运行过程在MC次数等于500时,普通笔记本电脑大概要跑十几秒,瓶颈主要不在距离循环,而在SINR矩阵的逐次拼接。如果你嫌慢,可以在循环开始前预先分配一个大数组,每次往固定索引填充,循环结束后再截断,这是MATLAB性能优化最见效的一种方式。我改了之后,500次仿真时间缩短了大概一半,但代价是代码可读性稍微下降了一点。4.2 结果验证:CDF曲线怎么看才可信跑通之后别急着截图,先问自己三个问题。第一,曲线取值范围是不是合理的物理范围?如果sinr_dB的中位数在-5dB以下,多半是噪声功率设置过高或者路径损耗指数取得太大;如果中位数超过了20dB,极其有可能是功率单位没换算或者衰落归一化出了问题。第二,曲线形状是否符合预期?瑞利衰落下的SINR CDF一般来说会比较平滑地穿过中值附近,如果曲线出现明显台阶或者跳变,可能是因为RB分配后样本数太少,或者门限边缘条件把一堆用户切在了同一个整数值上。第三,重复运行结果是否稳定?由于main.m第一行固定了rng(42),在同一个环境下多次运行应该得到几乎完全一样的曲线,只有随机流没有被重置时才应该有波动。我一般会连跑三次看一下三条曲线是否重叠,如果三条线完全分离,就说明有变量没有被重置。CDF还有一种快速验证方法:把fading_type改成'Rician'并且把K因子调大,比如设成100,曲线会明显变得更集中在中高SINR区域。如果你看不到这个变化趋势,那么大概率是fast_fading里的K因子没传对,或者功率归一化被覆盖。这条验证并不严谨,但它能快速暴露出代码里“衰落类型切换”是否真的生效。4.3 参数与结果的对应关系做资源分配方向的人最关心的就是参数变化如何影响曲线。我把常用调整和预期效果整理成下面这个简要对应表:调参动作预期曲线变化关键原因调大p_d2dSINR曲线整体向右移动信号功率增大,干扰项不变调大p_cellular干扰增大,曲线向左移动D2D接收机收到的蜂窝干扰变强调低sinr_threshold_dB更多用户被调度,低SINR样本变多门限放宽,边缘用户也获得RB增加num_d2d单RB复用压力增大,部分用户未分配可用RB数不变,竞争加剧改area_size为200用户间距缩小,路径损耗降低距离项直接影响信道增益这个表基本上覆盖了最常用的几个调节方向。如果你发现调了某个参数但曲线完全不变,不用怀疑是自己操作不对,大概率是参数在config.m中的名称没对上,或者在main.m里有一处硬编码覆盖了它。我就在调p_cellular时遇到过这个问题,因为main.m里还有一行临时变量p_cell_w = 10^((p_cellular - 30)/10),看起来没问题,但我在另一个函数里又自己定义了一遍,结果改了半天没效果。这种同名覆盖问题在不写全局参数的工程里特别容易发生,所以我在二次开发时会主动搜索所有出现某个参数的地方,确认只有config.m一处定义。5. 避坑记录:D2D仿真里最隐蔽的4个坑5.1 距离矩阵行列写反,SINR数据一塌糊涂现象:SINR曲线出现很多负20dB以下的样本,甚至混入NaN,检查路径损耗公式也找不到明显错误。原因:调用distance_matrix(cellular_pos, d2d_rx)时,把蜂窝用户坐标和D2D接收端坐标传反了,导致干扰链路增益被写进信号链路,信号链路增益被当作干扰,两个角色互换。解决:在distance_matrix函数头加一个参数维度断言,如果输入矩阵的行数不符合当前链路的定义就报错;另外在主脚本中每个trial开始时用size()打印关键矩阵维度,人工看一眼是否与预期一致。从那以后我每次写带链路区分的仿真,都在函数名后缀上注明信号链路还是干扰链路,比如distance_matrix_signal和distance_matrix_interference,避免语义模糊。5.2 瑞利衰落少乘sqrt(0.5),信道功率无故翻倍现象:所有SINR曲线比同配置的理论值高约3dB,且用户间相对差异不大,整体看着“过于乐观”。原因:randn生成的标准正态变量实部虚部功率各为1,直接组合成复数后平均功率为2,没有归一化到1。很多人只用randn(1,N) + 1i*randn(1,N),忘记了系数。解决:在fast_fading.m的瑞利分支中乘以sqrt(0.5),并在函数说明里明确标注“平均功率归一化为1”。如果你拿到的信道数据来自工具箱对象,也要先看它的输出功率约定,很多通信工具箱默认输出的是单位功率,不需要你再手动缩放,但有些模型需要额外配置。5.3 资源分配出现死循环,CPU直接拉满现象:运行allocate_rb时程序长时间不退出,任务管理器里MATLAB CPU占用100%,只能强制终止。原因:我把sinr_threshold_dB设成了10dB,在稀疏场景下大量用户找不到任何可行RB,而原工程某个初版实现用了while循环等待用户找到可用RB,找不到就永远空转。解决:将RB搜索改成for循环,并且增加最大尝试次数上限;同时在进入分配之前判断max(sinr_mat(u,:))是否小于门限,如果连最大SINR都低于门限就直接标记为未分配,不再做内部扫描。我在二次开发后把分配函数改成“即使无可行RB也必须返回0”的约定,主循环就永远不会卡死。5.4 随机种子不固定,曲线复现成为玄学现象:同一份代码隔天跑出来的CDF曲线形状有差异,甚至两次运行中某个点的概率值能差5个百分点。原因:rand和randn使用的全局随机流在每次MATLAB启动时都不同,脚本没有重置,不同机器上表现更明显。解决:main.m第一行写死rng(42),如果做批量实验,则使用for seed = 1:100循环,并将seed作为变量传入配置,最后把每个seed对应的结果文件单独保存。固定随机种子还能辅助调试:当你发现一条曲线很奇怪时,可以回头用同一个seed重新运行,复现同一组用户位置,逐步打印中间变量定位问题。6. 二次开发:把静态拓扑改成动态移动模型这个工程目前的拓扑是每轮蒙特卡洛重新随机生成,相当于每个时刻的快照之间没有任何相关性。如果你想研究D2D用户在移动过程中的切换或者长时平均性能,最简单的一步改造是加入随机游走模型。核心思路是在每个时间片t里更新坐标位置,同时给快衰落加时间相关性。坐标更新可以写成一个很小的函数:% 随机游走位置更新 function pos = random_walk_update(pos, velocity, dt, area_size) % pos: N x 2 矩阵,当前时刻坐标 % velocity: 平均移动速度,单位m/s % dt: 仿真步长,单位s step = velocity * dt; pos = pos + step * randn(size(pos)); pos = max(min(pos, area_size/2), -area_size/2); end调用时,只需要把原来的rand生成位置改成先初始化位置再循环调用random_walk_update,每个时刻重新计算距离矩阵和SINR。这里有个关键技术点:快衰落不能每时刻重新生成独立随机数,否则时间相关性会彻底丢失,移动模型就变成了“位置漂移但信道快速随机闪变”的混合体,物理上不自洽。我一般会给每个D2D链路预先存一个固定相位偏移量,在每个时刻用相位旋转来模拟时间相关性,比如h_t = h_t_prev * exp(1i * delta_phase),其中delta_phase由移动速度和载频折算。改造完成后的验证方法很简单:把velocity分别设成1m/s和5m/s,运行两次仿真,画出SINR CDF或者平均吞吐量随时间的变化曲线。如果两条曲线几乎完全一致,说明移动模型并没有影响信道的时间相关性——这时候要回头检查是不是每个时刻都重新调用了fast_fading生成独立序列,而不是做相位连续演化。如果你研究的是资源分配策略在移动场景下的切换开销,还可以在分配函数里记录RB切换次数,输出一个额外指标。最后我想说一个我自己的习惯:拿到任何别人的仿真代码,都先固定随机种子,打印一遍关键矩阵的维度,确认能复现再动算法。这个习惯挽救过我好几个下午的时间,每次改动前也能有一个可信的baseline。希望帮到你。本文还有配套的精品资源,点击获取