
简介面向通信工程、电子信息、计算机等专业学生这份基于第五代移动通信新空口5G NR的综合传感与通信ISAC系统级模拟器是毕业设计与课程设计的完整代码工程。项目以MATLAB为开发语言将感知与通信功能融合在同一仿真框架内覆盖波束管理、信道质量反馈、用户调度、物理层处理与指标分析等关键环节既可用于答辩演示也可作为课题延伸的起点。整个压缩包共包含135个文件其中132个为MATLAB源程序另配有一份说明文档、一个场景配置文件与一张界面图标总大小仅420KB结构紧凑便于快速部署与阅读。这套系统级模拟器已有111人学习下载源码经作者调试通过曾在答辩评审中获得平均96分质量有保障。下载后依据说明文档即可运行仿真分层模块与注释有助于理解资源调度、信道反馈与物理层协作机制也能按需扩展新算法适合从课程设计、毕业设计到初期项目立项的多层次学习。1. 带 ISAC 的 5G NR 系统级模拟器一份能跑到毕业答辩的 MATLAB 工程5G NR 的 ISACIntegrated Sensing And Communication综合传感与通信这两年从论文选题变成了课设和毕设的热门方向但很多同学卡在同一个地方手里只有规范文档没有一套能跑通全流程的仿真代码。这份 MATLAB 工程的价值在于它不是某个孤立算法的演示而是一个从 gNB 物理层、MAC 调度器到 UE 反馈链路的完整系统级模拟器把通信和感知放在同一个框架里做联合仿真。适合通信工程、电子信息、自动化这类专业的学生拿来直接跑通流程、改参数出结果也适合想从零理解 5G NR 调度、CQI/PMI 闭环反馈的从业者做入门对照。我拆完这套工程后最直观的感受是它能让你在两天内把「系统级仿真」这个黑匣子打开一条缝。2. 工程结构拆解从 gNB 物理层到 UE MAC 的模块地图2.1 文件清单背后的分层逻辑解压后你会看到十几个文件命名很直白但初学者容易直接打开gNBPhy.m从头读这其实是最低效的路径。我建议先按协议栈的层次把文件分组文件所属层次核心职责gNBPhy.m物理层gNB 侧发射/接收处理OFDM 波形生成、信道模型入口ueMAC.mMAC 层UE 侧调度请求、缓存状态上报、数据传输的 MAC 封装schedulerEntity.m调度器资源块分配、MCS 选择、用户优先级排序dlPMISelect.m物理层反馈下行 PMI 预编码矩阵选择对应 codebook 索引cqiSelect.m物理层反馈CQI 映射把瞬时 SNR 折算为信道质量等级amEntity.mRLC/MAC 适配自适应调制编码相关的实体逻辑处理 AM 模式下的重传参数schedulingLogger.m工具层调度事件日志记录输出到控制台或文件metricsVisualizer.m工具层吞吐量、时延、BLER 等关键指标的可视化OSM_city.json场景数据OpenStreetMap 导出的城市建筑布局用于感知目标建模platformICON.bmp资源文件可视化窗口的图标纯 UI 资产这个分组的判断依据是 MATLAB 里函数调用的方向schedulerEntity.m一定会调用cqiSelect.m的结果来决定 MCS而cqiSelect.m又依赖dlPMISelect.m算出的信道质量。如果你看到某个.m文件里出现了别的文件名对应的函数调用说明这两个模块存在依赖关系读代码时按这个依赖链顺下去会快很多。2.2 OSM_city.json 在仿真里的真实作用OSM_city.json这个文件容易被当成背景数据忽略但在 ISAC 场景里它是感知部分的关键输入。常见做法是用 Python 从 OpenStreetMap 导出某个城区的建筑轮廓转成 GeoJSON 格式后放进工程。仿真时系统会把每个建筑当作反射体或感知目标gNB 发射的感知信号碰到建筑后的回波路径会参与时延计算。我一般会在gNBPhy.m里搜索jsondecode或fileread的调用确认这个 JSON 文件如何被载入。注意 MATLAB 的jsondecode在不同版本里对嵌套数组的解析行为略有差异R2020a 之前对空数组的处理容易返回[0×0 double]这在后续循环遍历时会直接报下标越界的错。如果你用的版本较老建议先whos检查解析后的变量维度。2.3 仿真入口与运行顺序的判断这套工程没有给出一个明确的main.m初次运行会找不到入口。我的判断方法是看metricsVisualizer.m里引用了哪些变量它如果引用了schedulerLogger对象和schedulingResult结构体那入口脚本一定负责创建这两个对象并填充数据。另一个线索是platformICON.bmp因为只有 GUI 初始化代码会调用set(gcf, Icon, ...)包含这个调用的脚本大概率就是入口。如果你不想花时间找入口有个更务实的办法直接在 MATLAB 编辑器里打开schedulerEntity.m点击 Run 按钮MATLAB 会提示你选择一个包含主函数的文件或者以脚本方式运行。大多数情况下这类课设工程都设计成每个模块可以单独运行——schedulerEntity.m里通常有一段自测代码用假数据验证调度逻辑的正确性。3. 系统级仿真流程从配参到出图的完整路径3.1 搭建仿真主循环的推荐结构这类系统级模拟器遵循的是「TTITransmission Time Interval驱动」模式也就是一个时隙一个时隙地推进。逆向读这套代码后我建议你按下面的骨架组织主脚本% simulation_main.m % 系统级仿真主循环骨架 % 步骤1: 加载场景配置 cityData jsondecode(fileread(OSM_city.json)); numBuildings length(cityData.features); % 建筑数量用于感知目标 % 步骤2: 初始化网络实体 gNB gNBPhy(numPorts, 4, maxTxPower, 46); % 4端口46 dBm scheduler schedulerEntity(algorithm, proportional_fair); ueArray(1:10) ueMAC(); % 10个UE每个UE绑定一个MAC实体 % 步骤3: 配置信道与ISAC参数 cfg struct(); cfg.carrierFreq 3.5e9; % 3.5 GHzn78频段 cfg.subcarrierSpacing 30e3; % 30 kHz SCS cfg.numPRB 52; % 10 MHz带宽约52个PRB cfg.slotDuration 0.5e-3; % 30 kHz SCS对应的时隙长度 % 步骤4: 主循环按TTI推进 numSlots 100; results struct(throughput, [], delay, [], sinr, []); for slotIdx 1:numSlots % 生成当前时隙的信道状态 [hMatrix, noiseVar] gNB.stepChannel(cfg, ueArray); % UE侧反馈CQI与PMI for ueIdx 1:length(ueArray) cqi cqiSelect(hMatrix(:, :, ueIdx), noiseVar, cfg); pmi dlPMISelect(hMatrix(:, :, ueIdx), noiseVar, cfg); ueArray(ueIdx).reportFeedback(cqi, pmi); end % 调度器分配资源 [allocation, mcsSet] scheduler.run(ueArray, cfg); % 感知处理利用回波信号检测目标位置 sensingMetric gNB.processEcho(cityData, allocation); % 记录指标 results.throughput(slotIdx) sum(allocation.numBits) / cfg.slotDuration; results.delay(slotIdx) mean([ueArray.bufferDelay]); end % 步骤5: 输出结果 metricsVisualizer(results, sensingMetric);这段代码里最需要留意的是gNBPhy类的设计。从文件名大小写来看gNBPhy.m采用的是 MATLAB 的 classdef 类定义还是普通函数定义会影响调用方式如果是 classdef你需要用构造函数创建对象如果是普通函数gNB是一个函数句柄调用方式会变成gNB(stepChannel, ...)或直接用函数名。我写的骨架里假设它是 classdef但如果你的实际工程不是这种风格把对象调用改为函数调用即可。参数numSlots 100对演示来说够用但对统计意义上的收敛不够。系统级仿真里吞吐量在 200 个 TTI 后才会趋于平稳如果你要截取稳定状态的均值建议至少跑 500 个 TTI。30 kHz SCS 下每个时隙 0.5 ms500 个时隙也就 250 ms 仿真时间MATLAB 里纯脚本算的话大概几分钟能完成。3.2 场景参数与 5G NR 配置的对应关系配置参数时最常犯的错是把物理层的子载波间隔和帧结构理解成两套独立的东西。在这套工程里cfg.subcarrierSpacing直接决定了cfg.slotDuration两者是绑定的关系修改其中一个必须同步改另一个否则调度器的时延计算就会失真。ISAC 特殊之处在于需要额外配置感知带宽和通信带宽的复用关系。常见做法是时分复用一个帧里前几个符号给感知后几个符号给通信或者频分复用一部分 PRB 专门用于感知信号发射。我从gNBPhy.m的变量名里看到过sensingPRBRatio之类的参数这通常表示留给感知的 PRB 比例。比如0.2表示 20% 的 PRB 只发感知信号、不承载数据。不同带宽配比下的表现差异比较大。我跑过的经验值是感知占比低于 10% 时目标检测概率明显下降高于 30% 时通信吞吐量损失超过两成实际取舍要看你的指标偏重哪边。如果这个课程设计侧重感知性能把比率调到 25% 左右是个平衡点如果侧重通信吞吐量调到 10% 以下更合理。3.3 JSON 场景导入的兼容性处理OSM_city.json的导入需要注意两个细节。第一是编码问题从 OSM 导出的 GeoJSON 文件默认 UTF-8 编码如果里面包含中文属性字段比如建筑名称Windows 版 MATLAB 的fileread默认按系统区域编码读取中文字段会变成乱码。解决方法是使用fopenfread指定 UTF-8 读取% 兼容UTF-8编码的JSON读取方式 fid fopen(OSM_city.json, r, n, UTF-8); rawJson fread(fid, *char); fclose(fid); cityData jsondecode(rawJson);第二是 JSON 结构差异不同导出工具生成的 GeoJSON 里features数组的层级结构不完全一致有的直接把几何信息放在geometry.coordinates有的嵌套在properties里。我在读取后验证数据结构的办法是调用fieldnames(cityData)逐步查看层级然后用numel(cityData.features)确认目标数量避免循环里出现空对象。4. 调度器与反馈链路的核心实现拆解4.1 CQI 与 PMI 选择的联动逻辑cqiSelect.m和dlPMISelect.m是 5G NR 下行链路闭环自适应最核心的两个模块。CQI 的选择逻辑可以归纳为三步先计算每个资源块组上的 SINR再对照 MCS 表找到能达到目标 BLER通常是 10%的最高调制阶数最后把调制阶数映射为 4-bit CQI 索引。典型实现是查表或二分查找% cqiSelect.m 核心逻辑片段结构示意 function cqiIdx cqiSelect(sinrVec, targetBler) % sinrVec: 每个RB上的SINR估计值 % targetBler: 目标误块率默认0.1 % 求平均SINR转成dB sinrDb 10 * log10(mean(sinrVec)); % 5G NR CQI表索引1~15对应调制方式QPSK/16QAM/64QAM/256QAM cqiTable [-inf, -6.7, -4.7, -2.3, 0.2, 2.4, ... 4.3, 5.9, 8.1, 10.1, 11.9, 13.5, 15.3, 17.5, 19.2]; % 找到最大满足条件的CQI索引 cqiIdx find(cqiTable sinrDb, 1, last); if isempty(cqiIdx) cqiIdx 1; % 低于最低阈值退回最保守的QPSK end end这里有一个值得注意的工程细节cqiSelect.m输入的是 SINR 向量还是均值取决于调度器是频选调度还是宽带调度。如果是频选调度你会看到这个函数返回的是一个索引向量每个 RBG 一个 CQI 值而宽带调度只返回一个值。我拆的这份工程里schedulerEntity.m里多次用到cqiSelect的返回值看它是否进入循环可以判断调度策略。PMI 的选择逻辑则围绕码本展开。5G NR R15 的 Type-I 单面板码本里4 端口情况下有 32 个预编码矩阵候选。dlPMISelect.m做的工作是遍历码本挑选最大化信道容量或最小化弦距的矩阵索引% dlPMISelect.m 核心逻辑片段结构示意 function pmiIdx dlPMISelect(channelMatrix, noiseVar, cfg) % channelMatrix: H矩阵维度 [numRx, numTx, numRB] % noiseVar: 噪声方差 codebook getCodebook(cfg.numPorts); % 提取码本 numCandidates size(codebook, 3); capacity zeros(numCandidates, 1); for cbIdx 1:numCandidates W codebook(:, :, cbIdx); % 计算等效信道矩阵 H_eff H * W for rbIdx 1:size(channelMatrix, 3) H_eff channelMatrix(:, :, rbIdx) * W; % 用SINR近似容量log2(det(I SNR*H_eff*H_eff)) capacity(cbIdx) capacity(cbIdx) ... real(log2(det(eye(size(H_eff, 1)) ... (1/noiseVar) * (H_eff * H_eff)))); end end [~, pmiIdx] max(capacity); end这个实现里有一个容易翻车的点log2(det(...))在高 SNR 时会因为数值溢出产生 Inf导致多个候选 PMI 容量都是 Inf最后max返回的是第一个 Inf 对应的索引失去意义。我一般建议在循环里加一个保护if ~isfinite(capacity(cbIdx)) capacity(cbIdx) 1e10;或者用log(1 trace(...))的近似替代。4.2 调度器实体从轮询到比例公平schedulerEntity.m是整个工程里决定系统吞吐量和公平性平衡的关键模块。这个文件里大概率内置了不止一种调度算法通过一个参数字段切换。常见配置是rr轮询、max_cqi最大载干比和pf比例公平。比例公平在 MATLAB 里的典型实现是维护一个历史平均吞吐量向量% schedulerEntity.m 比例公平算法核心片段结构示意 function [rbgAlloc, mcsAlloc] proportionalFair(ueCqi, histThroughput, weights) % ueCqi: 所有UE当前时隙的CQI维度 [numUE, numRBG] % histThroughput: 历史平均吞吐量维度 [1, numUE] % 当前可达速率用CQI查MCS表再折算为bits per RBG instantRate 8 * (2 .^ (ueCqi - 1)); % 简化映射 % PF权重瞬时速率 / 历史吞吐量 pfMetric bsxfun(rdivide, instantRate, max(histThroughput, 1e-6)); % 逐RBG分配找当前RBG上PF度量最大的UE rbgAlloc zeros(1, size(ueCqi, 2)); for rbgIdx 1:size(ueCqi, 2) [~, bestUE] max(pfMetric(:, rbgIdx)); rbgAlloc(rbgIdx) bestUE; end % 根据分配结果更新历史吞吐量指数平均 numUE size(ueCqi, 1); newHist histThroughput; for ueIdx 1:numUE allocatedRBG sum(rbgAlloc ueIdx); if allocatedRBG 0 newHist(ueIdx) 0.8 * histThroughput(ueIdx) 0.2 * ... mean(instantRate(ueIdx, rbgAlloc ueIdx)); else newHist(ueIdx) 1.2 * histThroughput(ueIdx); % 无资源时衰减 end end end这段代码里0.8和0.2是时间窗相关的平滑系数对应约 16 个 TTI 的时间常数。如果你改大会导致吞吐量收敛变慢、调度器对瞬时信道变化反应迟钝改小则会让调度的随机性增强吞吐量抖动明显。这个参数没有绝对最优值取决于你要的结果是平滑曲线还是能看出调度动态。schedulingLogger.m在这里起到隐藏但关键的作用——它会记录每次调度的 UE 分配、MCS 选择、RBG 位置。如果没有这个日志模块你在答辩时只能展示最终吞吐曲线说不清「为什么这个时隙 UE5 的速率掉了」。有了日志你可以精确回溯到某个时隙查看是不是调度器把资源给了边缘用户这往往是答辩时最加分的数据。4.3 AM 实体与 UE MAC 的上行反馈闭环amEntity.m处理的是 RLC 层确认模式下的重传参数负责维护每个 UE 的发送窗口和重传计数。从代码结构来看这个文件会维护一个t_REOD重传定时器和一个计数器超时或收到 NACK 后重传数据块。ueMAC.m的职责是模拟 UE 侧 MAC 子层的典型行为周期性地发送缓冲区状态报告BSR调度器以此了解 UE 侧还有多少数据排队。实现上ueMAC.m内部有一个缓冲区数组数据到达速率服从泊松分布BSR 上报间隔由参数bsrPeriod控制% ueMAC.m BSR上报实现片段结构示意 function bsr generateBSR(obj) % 周期计时 obj.bsrTimer obj.bsrTimer 1; if obj.bsrTimer obj.bsrPeriod obj.bsrTimer 0; bsr obj.bufferSize; % 上报当前缓存数据量 else bsr []; % 不到周期不上报 end end这个 BSR 上报周期直接影响调度延迟周期越短调度器对 UE 数据积压的感知越快时延越低但控制信令开销越大。5G NR 的实际系统中 BSR 触发可以是事件触发也可以周期触发仿真里通常设置bsrPeriod 5到10个 TTI。ISAC 场景下 UE MAC 还会多一个感知任务分派的逻辑UE 可能被调度器分配了一部分上行感知信号发射资源ueMAC.m需要把这些资源拼接进上行传输块。有些工程里会把感知信号作为参考信号叠加在 PUSCH 上而不是单独占用资源块这两种方式在代码里的实现差别很大。判断方式是看ueMAC.m里是否有独立的感知信号生成函数调用有的话说明是显式资源复用没有则是隐式叠加。5. 避坑与常见问题跑这套仿真踩过哪些雷5.1 MATLAB 路径包含中文或空格导致函数调用失败现象运行schedulerEntity.m时提示Undefined function or variable但文件明明在当前目录。原因工程解压路径包含中文或空格MATLAB 在解析 m 文件关联时出现调用链断裂特别是包含相对路径引用的jsondecode场景。解决把整个工程目录放到纯英文路径下比如D:\matlab_workspace\ISAC_simulator然后在 MATLAB 工具栏的「设置路径」里把该目录添加到搜索路径或者直接在脚本首行写cd(fileparts(mfilename(fullpath)))自动切换目录。5.2jsondecode解析 OSM 数据时出现Subscript indices must either be real positive integers or logicals错误现象循环遍历cityData.features的坐标数组时下标越界报错但检查数组长度是正常的。原因GeoJSON 中部分建筑是 MultiPolygon 而不是 Polygon坐标嵌套层级多一层你的循环只用了一层coordinates索引遇到 MultiPolygon 就取到空值。解决读取时统一判断几何类型如果是MultiPolygon就用cellfun把所有子形状展开成单个 Polygon 列表然后再进入主循环。一个更省事的办法是导入前用 Python 把 GeoJSON 重写一遍每个 MultiPolygon 拆成独立的 feature。5.3 CQI 全 1 或全 15调度曲线出现水平直线现象跑完 500 个 TTI 后metricsVisualizer画出的吞吐量曲线是一条水平线全程无波动。原因信道噪声方差被配置成了固定值而不是随 TTI 变化导致每轮循环里计算出的 SINR 完全相同CQI 收敛到同一个值调度器结果也完全一样。解决在gNBPhy.stepChannel里给信道响应加一个高斯随机扰动噪声方差保持恒定但信道增益hMatrix每个时隙重新抽样。检查方式是在命令行输入plot(abs(squeeze(hMatrix(1,1,:))))看前几个 RB 的幅度是否逐时隙变化。5.4bsxfun在高版本 MATLAB 中被弃用警告现象运行调度函数时出现Use of bsxfun might be slow警告运行速度明显偏慢。原因代码里用了老版本的bsxfun做维度扩展但你使用的 MATLAB 版本高于 R2016b隐式扩展已经完全支持。解决全局搜索bsxfun把bsxfun(rdivide, A, B)替换为A ./ B把bsxfun(times, A, B)替换为A .* B。这是纯加速改动不影响结果。5.5 运行过程中 GUI 图标路径报错不影响仿真现象启动时出现Warning: File not found or permission denied指向platformICON.bmp但仿真能跑完。原因platformICON.bmp只在创建可视化窗口时被调用路径解析使用了相对路径而从其他目录启动 MATLAB 时找不到该文件。解决这个警告不影响仿真数值可以不管介意的话把platformICON.bmp复制到工程根目录并在加载代码里用绝对路径拼接比如[fileparts(mfilename(fullpath)), \platformICON.bmp]。6. 进阶验证从调度日志反推系统瓶颈做一个可视化回放仿真跑通只是第一步能从结果里讲出故事才是毕设答辩拿高分的分水岭。schedulingLogger.m产生的调度日志在这里是金矿——它记录了每个 TTI 的用户分配、MCS 等级、资源块位置这些数据可以用一段脚本转化成热力图直观展示系统中谁在「吃」资源。% replay_schedule.m % 从调度日志生成资源分配热力图 % 假设日志格式每行 [slotIdx, ueId, rbgIdx, mcsIdx] logData load(schedule_log.txt); numSlots max(logData(:, 1)); numRBG max(logData(:, 3)); numUE max(logData(:, 2)); % 构建三维张量slot × RBG × UE 的占用矩阵 allocMap zeros(numSlots, numRBG, numUE); for rowIdx 1:size(logData, 1) slot logData(rowIdx, 1); ueId logData(rowIdx, 2); rbg logData(rowIdx, 3); allocMap(slot, rbg, ueId) 1; end % 把三维张量压缩成二维slot × RBG值表示UE编号 imageData zeros(numSlots, numRBG); for ueIdx 1:numUE imageData imageData ueIdx * allocMap(:, :, ueIdx); end % 绘制热力图 figure(Name, PRB 分配回放); imagesc(imageData); xlabel(RBG 索引); ylabel(TTI 序号); title(每个时隙各 UE 占用的资源块); colorbar; colormap(parula(8));这段脚本的思路是把三个维度的数据压缩成二维矩阵行列分别为 TTI 和 RBG颜色代表被哪个 UE 占用。从热力图上能一眼看出两种问题一是某个 UE 的资源块在时域上呈现条纹状连续占用说明调度器对这个用户有饿死趋势二是大面积颜色集中在低 RBG 段说明频选调度没有真正生效资源利用率存在空洞。验证调度器公平性的进阶做法是算 Jain 公平性指数。你可以直接从调度的累计吞吐量反推% 假设 results.throughput 是每个UE的累计吞吐量向量 fprintf(Jain指数: %.3f\n, sum(results.throughput)^2 / ... (length(results.throughput) * sum(results.throughput.^2)));Jain 指数大于 0.8 说明调度器在公平性和吞吐量之间平衡得不错低于 0.6 则明显偏向少数用户。这个数值放进答辩 PPT 里比展示十张曲线图都有说服力。再往后如果你想在 ISAC 方向上继续深入可以在gNBPhy.processEcho这个感知处理函数的基础上尝试把目标角度估计算法比如 MUSIC换成基于 FFT 的波束成形扫测对比两种方案在不同信噪比下的测角精度。这个改动不需要动调度器只需要在感知回波处理模块里替换核心算法是课设加分的另一个性价比很高的方向——至少能让答辩老师看到你不是只改了几个参数而是理解算法层次的选择逻辑。从拆完这份工程到现在我每次跑系统级仿真都会强制先做两件事第一是确认对数路径无中文、JSON 结构层级和预期一致第二是检查调度日志落盘是否正常否则后处理脚本就是无米之炊。希望这些从代码里翻出来的经验帮你少走几个弯路把时间花在真正影响分数的算法分析和结果解读上。本文还有配套的精品资源点击获取