简介面向无线通信与OFDM-IM技术研究者这份源代码包完整实现了OFDM-IM及其改进型IM-OFDM的仿真流程涵盖子载波激活模式选择、信息嵌入、LLR检测、ML检测及误码率评估等核心环节。压缩包内共11个文件以10个MATLAB脚本和1个README说明文档为主OFDM_IM.m为系统主程序LLR_Detector系列与ML_Detector系列分别对应不同复杂度的接收端检测算法Combin_Md、BitoDe等工具函数完成比特与子载波索引的双向转换结构清晰便于按模块学习。已有483人学习下载适合通信工程专业学生、科研人员及对索引调制感兴趣的开发者。通过研读代码可掌握OFDM-IM从发射端子载波激活到接收端检测解调的完整工作原理理解节能型OFDM-IM在IoT等低功耗场景中的设计优势并在此基础上扩展信道模型、比较各检测算法性能是快速上手OFDM-IM仿真的实用样例。1. OFDM-IM 仿真这份 MATLAB 源码把索引调制从理论拉到了 BER 曲线OFDM-IM索引调制 OFDM这几年在低功耗通信方向被频繁提起核心思路和传统 OFDM 正好相反传统系统恨不得每个子载波都满载OFDM-IM 反而主动关掉一部分子载波让“哪些子载波处于活动状态”这件事本身携带信息。功率降了频谱效率不一定吃亏因为索引信息不占传统调制符号。这份 ofdm_im-master 源码包覆盖了组合数映射、比特拆分、三种检测器和完整 BER 统计链路适合两类人刚接触索引调制、想搞懂活动子载波索引如何编解码的研究生需要拿 OFDM-IM 做对比实验、想快速拿到可用基准曲线的工程师。源码按入口脚本、映射函数、检测器分文件组织改参数就能跑不同配置不用从零搭链路。后面按“原理 → 映射 → 检测器 → 跑通 → 踩坑 → 进阶改造”的顺序拆这份代码重点讲清楚三个检测器的性能差异和几个容易被忽略的边界细节。2. 入口与映射逻辑OFDM_IM.m 怎么把比特变成活动子载波图案2.1 OFDM-IM 发端的三段式比特处理先明确 OFDM-IM 的一帧数据怎么组织。假设每个子块有 n 个子载波其中 k 个处于活动状态调制阶数为 M。每个子块能携带的比特分成两部分索引比特p1 floor(log2(C(n,k)))决定哪 k 个子载波活动符号比特p2 k × log2(M)由活动子载波上的 M 进制调制符号承载。C(n,k) 是组合数。这里要注意 p1 取的是 floor因为组合数不一定是 2 的整数次幂多出来的组合必须丢弃这是后面避坑章要讲的第一个问题。发端流程在 OFDM_IM.m 主循环里大致是信息比特流 → BitoDe.m 切分成索引比特和符号比特 → 查 Combin_Md.m 生成的映射表得到活动子载波位置 → 符号比特映射成调制符号放到活动子载波上 → IFFT 进入时域。整个“比特分组 → 子载波索引 → 调制符号”的对应关系是理解这份代码的主线。2.2 文件调用关系与各自职责这份压缩包是典型的 MATLAB 函数式组织没有复杂的面向对象封装适合直接打断点跟读。我按调用顺序整理了一张表文件职责调用方OFDM_IM.m主仿真参数初始化、信道循环、BER 统计无入口binomial.m组合数 C(n,k) 计算Combin_Md.m 等Combin_Md.m生成十进制索引→活动子载波组合的查找表OFDM_IM.mBitoDe.m比特序列转十进制索引值OFDM_IM.mDetoBit.m十进制索引值转回比特序列接收端解码ML_Detector_Mary.m全搜索最大似然检测OFDM_IM.mML_Detector_NearML.m低复杂度近 ML 检测OFDM_IM.mLLR_Detector.m / LLR_Detector2.m对数似然比检测两个版本OFDM_IM.mREADME.md 里应该有基础的参数说明但据我拆包的经验这类源码的 README 往往只写了运行方式真正的边界条件都在函数体的注释里。建议把 OFDM_IM.m 里的参数区当成第一个阅读重点先把 n、k、M、子块数这几个变量找出来。2.3 Combin_Md.m 的查找表逻辑Combin_Md.m 做的事情本质上是构建一个组合数系统combinatorial number system的映射把 0 到 C(n,k)-1 的十进制索引映射到具体的 k 个活动子载波位置。常见做法是预先穷举所有组合按字典序或格雷码序排好存成查找表% Combin_Md.m 核心逻辑示意生成所有组合并按序编号 function Comb Combin_Md(n, k) % n: 子块子载波总数, k: 活动子载波数 allComb nchoosek(1:n, k); % 所有 C(n,k) 种组合按字典序 numComb size(allComb, 1); Comb zeros(numComb, k); for idx 1:numComb Comb(idx, :) allComb(idx, :); % 第 idx 行对应索引 idx-1 end end关键点是索引从 0 开始还是从 1 开始。MATLAB 数组下标从 1 开始但索引比特转出来的十进制数通常从 0 开始这里一旦偏移一位整个映射表就错位BER 会直接掉到 0.5 附近。发端查表用 Combin_Md收端做 ML 穷举或 LLR 计算时也要用同一张表两边的组合排序必须完全一致。另一个细节是 nchoosek 在 n 较大时计算量会迅速膨胀。n4、k2 只有 6 种组合n128、k64 就是天文数字所以这个查找表方案只适合子块尺寸较小的配置一般 n 不超过 16。如果要做大子块就得改成实时计算组合序号的算法这是后话。3. 三种检测器拆解ML、Near-ML、LLR 的性能与复杂度取舍3.1 ML_Detector_Mary.m穷举搜索的参考基准ML 检测的思想是对每个子块遍历所有可能的索引组合和所有可能的调制符号组合找到让接收信号欧氏距离最小的一组。ML_Detector_Mary.m 里的“Mary”指的是多进制调制M-ary即它同时处理索引维度和符号维度% ML_Detector_Mary.m 结构示意 function [bitSeq, symIdx] ML_Detector_Mary(rx, Comb, M, k) % rx: 接收到的子块信号频域已知信道 % Comb: Combin_Md 生成的索引查找表 % M: 调制阶数 (2 BPSK, 4 QPSK, 16 16QAM) numComb size(Comb, 1); bestDist inf; bestCombIdx 1; % 遍历所有活动子载波组合 for cIdx 1:numComb activePos Comb(cIdx, :); % 对每个活动位置穷举 M 进制符号 % 用 meshgrid 或 ndgrid 生成 M^k 种符号组合 % 计算 || rx(activePos) - symVec .* H(activePos) ||^2 % 记录最小距离对应的组合 end end逻辑说明外层循环遍历 C(n,k) 种活动组合内层遍历 M^k 种符号组合总复杂度是 C(n,k) × M^k 次欧氏距离计算。n4、k2、M4 时是 6×1696 次完全能接受但 n 和 k 一涨就是指数爆炸所以 ML 在这份代码里更多是作为性能上界存在用来衡量其他检测器的损失。参数说明M 的取值直接影响内层符号组合数M4 换到 M16内层组合数从 16 涨到 256复杂度翻 16 倍。实际跑仿真时我一般先固定 QPSK 把链路调通确认 BER 曲线形状正常后再换高阶调制。3.2 LLR_Detector.m软信息的近似计算路径LLR 检测不做穷举而是对每个子载波计算一个活跃度度量。对第 j 个子载波计算它属于活动子载波的对数似然比% LLR_Detector.m 核心计算示意 function llr LLR_Detector(rx, H, M, sigma2) % rx: 频域接收信号, H: 信道频响, sigma2: 噪声方差 % 对每个子载波 j计算 LLR(j) % 利用 max-log 近似只保留每项的最大值 % LLR(j) max_{s in 星座} ( -|rx(j) - H(j)*s|^2 / sigma2 ) % - max_{s in 星座} ( -|rx(j)|^2 / sigma2 ) 常数 end逻辑说明第一项是“该子载波上有某个调制符号”时的最大后验概率第二项是“该子载波空闲”时的概率只有噪声。两者相减LLR 越大说明越可能是活动子载波。max-log 近似把求和变成取最大省掉了大量指数运算代价是软信息精度略微下降。参数说明sigma2 是噪声方差必须和仿真里加的高斯噪声功率对齐。噪声功率算错是最常见的坑——LLR 里的 sigma2 偏大或偏小检测器会把判据整体平移导致活动子载波判错。LLR_Detector2.m 应该是这个思路的另一个变体比如换了归一化方式或加了信道增益补偿可以对比看两版在多径信道下的差异。3.3 ML_Detector_NearML.m牺牲少量性能换速度NearML 的思路是先用粗筛缩减候选集合再在缩减后的集合上做近似 ML 搜索。常见做法是先按每个子载波接收功率排序只保留功率最大的 k 个子载波作为候选活动位置然后只对这些候选做符号层 ML% ML_Detector_NearML.m 结构示意 function [bitSeq] ML_Detector_NearML(rx, Comb, M, k) % 步骤1: 按 |rx|.^2 对子载波降序排序取前 k 个作为候选 [~, sortIdx] sort(abs(rx).^2, descend); candPos sortIdx(1:k); % 步骤2: 只在 candPos 上做 M^k 穷举 % 步骤3: 把最优符号组合映射回比特 end逻辑说明这个启发式的依据是活动子载波上有真实信号加噪声能量通常高于纯噪声的空闲子载波。但在低 SNR 或两个子载波能量接近时粗筛会选错位置性能损失就显现出来了。实测中 NearML 在低 SNR 区比 ML 差 1~2 dB 是正常的换取的是把组合维度从 C(n,k) 压到 1速度提升明显。参数说明k 的取值在这里既是活动子载波数也是候选数不能改小否则直接破坏了 OFDM-IM 的信息结构。真正可以调的是排序用的度量——有些实现用 |rx|²/|H|² 做归一化来对抗频率选择性衰落效果会更好。4. 跑通主仿真OFDM_IM.m 的参数设置与一帧数据的完整旅程4.1 参数区子载波数、活动数、调制阶数怎么配OFDM_IM.m 开头一般是参数区我拆到的典型配置是每子块 n4活动 k2调制 QPSK。这几个参数决定了一帧能传多少比特p1 floor(log2(C(4,2))) floor(log2(6)) 2 p2 2 × log2(4) 4 每子块 6 bit其中 2 bit 是索引信息。如果改成 n8、k4、16QAMp1 floor(log2(70)) 6p2 4×4 16每子块 22 bit频谱效率明显提升但检测复杂度暴涨ML 基本跑不动只能靠 NearML 或 LLR。4.2 一帧数据的完整旅程主循环里一帧数据走的路是信息比特生成 → BitoDe 切分为索引比特和符号比特 → 查 Combin_Md 表确定活动子载波位置 → QPSK 映射符号 → 分配到 N 个子载波上非活动位置置 0→ IFFT 得到时域信号 → 加循环前缀 → 过信道 → 加噪声 → 去 CP → FFT → 频域信号 → 三种检测器之一 → DetoBit 恢复比特 → 和原始比特对比统计 BER。这里贴一段主仿真骨架的伪代码方便对照源码找位置% OFDM_IM.m 主循环骨架 for snrIdx 1:length(SNRdB_list) totalErr 0; totalBits 0; for frameIdx 1:numFrames % 1. 生成随机比特并做 OFDM-IM 调制 txBits randi([0 1], bitsPerFrame, 1); [txSymbols, activeIdx] OFDM_IM_mod(txBits, ...); % 2. IFFT / 加CP / 信道 / 噪声 % 3. 接收端 FFT 后送入检测器 rxBits ML_Detector_Mary(rxFreq, ...); % 或 LLR_Detector % 4. 统计误比特 totalErr totalErr sum(txBits ~ rxBits); totalBits totalBits length(txBits); end BER(snrIdx) totalErr / totalBits; end逻辑说明每帧的比特数必须能被 p1p2 整除否则最后一组比特凑不齐BitoDe 会报错或补零补零方式不统一会引入额外误码。信道部分常见的是高斯白噪声信道和瑞利衰落信道两种代码里通常用 if 分支切换。参数说明numFrames 决定了曲线平滑度——帧数太少 BER 曲线抖动厉害帧数太多仿真时间受不了。我一般先跑 100 帧看个大概确认没问题再加大到 1000 帧出正式曲线。SNR 扫描范围建议从 0 dB 到 20 dB步进 2 dB覆盖 BER 从 0.1 到 1e-4 的区间。4.3 BitoDe.m 与 DetoBit.m比特拆分的边界条件BitoDe.m 把比特序列拆成索引比特和符号比特DetoBit.m 在接收端做逆操作。这两个函数看着简单实际是整条链路最容易出 bug 的地方。常见做法是% BitoDe.m 示意按 p1 和 p2 拆分比特 function [idxBits, symBits] BitoDe(bits, p1, p2) idxBits bits(1:p1); % 前 p1 位给索引 symBits bits(p11:p1p2); % 后面 p2 位给符号 end逻辑说明切分顺序必须和发端约定一致。如果这里用列向量DetoBit 拼接时用行向量或者索引比特走高位、符号比特走低位的顺序反了BER 会固定在 0.5 左右且不随 SNR 改善。排查这类问题最简单的办法是先把噪声设为 0 跑一帧逐比特对比收发两端。参数说明p1 和 p2 由 n、k、M 唯一确定必须作为参数从主函数传入不要在子函数里重新计算。两个地方算出的 p1 不一致索引映射表会错位这种错位不会报错只会表现为 BER 曲线异常非常隐蔽。5. 避坑指南OFDM-IM 仿真里五个高频翻车点5.1 索引映射表错位BER 直接掉到 0.5现象改了几行代码后 BER 曲线整体抬升到 0.4~0.5且不随 SNR 改善。 原因Combin_Md 生成的查找表在发端和收端排序不一致常见于把字典序改成自定义序时只改了一边。 解决把 Combin_Md 的输出做成全局只读变量发端和收端都引用同一份加一行断言检查表的总行数等于 C(n,k)并在自测时把噪声设 0 逐帧对比。5.2 噪声方差 sigma2 与 LLR 不匹配现象LLR 检测器 BER 曲线在低 SNR 区曲率不对高 SNR 区出现地板。 原因LLR_Detector 里的 sigma2 在点数、噪声功率、频谱归一化之间的换算搞错了。OFDM 的 IFFT 输出有功率缩放因子时域噪声功率和频域噪声功率差一个 FFT 长度。 解决先在无信道、无衰落、已知符号的情况下单独测噪声方差用实测值替换理论值多径信道下还要把每路子载波的信道增益乘进去。5.3 NearML 在低 SNR 下选错活动子载波现象NearML 和 ML 的 BER 差在低 SNR 区超过 3 dB。 原因粗筛用 |rx|² 排序低 SNR 下噪声功率和信号功率同量级空闲子载波能量可能超过活动子载波。 解决把排序度量改成 |rx|² / |H|²用信道增益归一化如果还是不行把候选集合从 k 扩大到 k1 或 k2在粗筛结果上补几个次优候选再搜索。5.4 组合数不是 2 的整数次幂时吞比特现象仿真能跑但理论吞吐和实际统计的比特数对不上。 原因p1 floor(log2(C(n,k))) 会丢弃无效组合而 DetoBit 解出来的索引可能落在无效区。如果代码没做无效索引丢弃发端和收端的比特对齐就乱了。 解决查表只保留 2^p1 个有效组合顺序生成后直接截断多余行判决出的索引如果落在无效区按最近有效索引处理或标记为擦除。5.5 平均功率归一化导致 SNR 定义混乱现象同一个代码在不同 SNR 定义下曲线差 3 dB不同文章结果对不上。 原因OFDM-IM 里有大量空闲子载波平均功率和峰值功率的比值与普通 OFDM 不同。E_b/N0 和 E_s/N0 之间的换算要用“实际传输比特数”而不是“载波数×每符号比特”。 解决在代码开头明确 SNR 定义把 E_s 用活动子载波平均功率计算注释里写清公式对比外部结果时先校准到同样定义。6. 进阶改造把 QPSK 换成 16QAM顺手验证频谱效率变化6.1 调制阶数升级的三个改动点把 M4 换成 M16 不是简单改一个变量。第一p2 从 4 变 8每子块比特数变了帧长和 padding 逻辑要跟着改第二星座映射函数要支持 16QAM且解映射时软信息的星座点集合换成 16 点第三LLR 里的 max-log 近似在 16QAM 下的近似误差比 QPSK 大必要时要把 max-log 换成真正的 log-sum-exp。具体的改动位置在 ML_Detector_Mary.m 的符号生成部分% 16QAM 星座点定义示意 M 16; constellation (1/sqrt(10)) * ... [-3-3i, -3-1i, -33i, -31i, -1-3i, -1-1i, -13i, -11i, ... 3-3i, 3-1i, 33i, 31i, 1-3i, 1-1i, 13i, 11i];参数说明16QAM 星座平均功率归一化系数是 1/sqrt(10)QPSK 是 1/sqrt(2)。归一化不对会让信道增益整体偏差BER 曲线左右平移这个坑我踩过不止一次。6.2 验证频谱效率与经典 OFDM 的对比OFDM-IM 的价值在能耗和频谱效率的权衡。跑完 16QAM 配置后可以做一组对比同子载波数、同信道条件下经典 OFDM 用满所有子载波OFDM-IM 只开一半比较两者的 BER 和有效吞吐。你会发现 OFDM-IM 在相同 BER 目标下平均发射功率低 2~3 dB这就是“关闭子载波”换来的节能收益。验证方法是把两种系统的 BER 曲线画在同一张图上横轴用 E_b/N0纵轴用 BER并标注各自的有效比特率。从那以后我每次改参数都强制走一遍全流程先零噪声自测一帧逐比特比对再跑低帧数快速验证曲线形状最后才上高帧数正式曲线。这套流程帮我少踩了很多隐性 bug希望也能帮到你。本文还有配套的精品资源点击获取