OFDM-IM索引调制仿真:ML与LLR检测器实现与性能分析

发布时间:2026/9/15 2:40:11
OFDM-IM索引调制仿真:ML与LLR检测器实现与性能分析 简介压缩包内是一套面向无线通信研究的OFDM-IM正交频分复用索引调制MATLAB仿真源码适合通信工程专业学生、算法工程师以及对索引调制技术感兴趣的开发者。代码实现了OFDM-IM系统的完整链路包括子载波激活选择、信息比特映射、解调与检测环节并提供了ML检测器、LLR检测器及低复杂度近ML检测器等多种算法实现便于对比不同方案的性能与复杂度。资源共11个文件以MATLAB脚本.m为主辅以一份README说明文档整体包体仅8KB结构紧凑。其中OFDM_IM.m为主仿真入口ML_Detector_NearML.m、LLR_Detector.m等脚本分别对应不同接收检测策略Combin_Md.m、BitoDe.m等工具函数支撑比特与调制符号间的转换可帮助使用者快速理解OFDM-IM从发射端到接收端的完整流程。目前已有483人学习浏览适合作为研究生课题入门参考或数字通信课程设计的基本框架研究者可在现有代码基础上改进子载波选择策略或检测算法进一步探索IM-OFDM的节能与频谱效率优化。1. 为什么把子载波“关掉”反而能省出效率来——OFDM-IM 的索引调制思路我在跑 4G/5G 物理层仿真时第一次看到 OFDM-IM 项目以为是个噱头传统 OFDM 上每个子载波都该用来扛数据关掉一部分岂不是浪费频谱实际仿真后才知道OFDM-IM 的“关掉”不是浪费而是把子载波的激活状态本身当作一种调制维度。它把 N 个子载波分成若干子块每块只激活 k 个用“哪些位置被激活”这种排列组合去携带索引比特再在激活的子载波上用传统 QAM/PSK 携带符号比特。这套思路在低功耗物联网设备上很值钱因为被关闭的子载波不消耗发射功率而索引比特几乎不需要额外能量。这个 MATLAB 源码库正好把发射、组合表生成、三类 ML 检测器和两种 LLR 检测器都写全了适合想从原理落到 BER 曲线的人。2. 从 Combin_Md 到 OFDM_IM.m子载波激活组合表的生成与主循环2.1 索引调制要解决的首要问题怎么把“哪几个子载波开”编码成比特在一个 OFDM-IM 子块里假设子块大小为 n激活子载波数量为 k。那么可用的激活模式数就是组合数 C(n,k)。可以携带的索引比特数是 p floor(log2(C(n,k)))。这里注意C(n,k) 往往不是 2 的幂所以只能取其中 2^p 个模式丢掉的模式不用。这个取舍不是随便丢要尽量让丢掉的模式与用到的模式之间的汉明距离尽量小否则检测时索引比特错一个可能映射到完全不同的模式上。在代码库中Combin_Md.m 负责生成这 2^p 个模式。它的核心思路是递归或按字典序枚举组合再用 binomial.m 计算组合序数。很多工程做法是直接调用 nchoosek 列出所有组合然后取前 2^p 个。这样最简单但顺序固定且和后续查表必须一致。以下是按字典序枚举组合的简化实现function patterns GenerateIndexPatterns(n, k) % n: 子块大小 % k: 激活子载波数 % patterns: 行向量列表1 表示激活位置 allComb nchoosek(1:n, k); % 每行是激活位置的索引集合 numValid 2^floor(log2(size(allComb, 1))); patterns zeros(numValid, n); for idx 1:numValid comb allComb(idx, :); patterns(idx, comb) 1; % 把激活位置置 1 end end这个函数返回的 patterns 每一行就是一个合法的子载波开关状态。使用nchoosek(1:n, k)得到的是激活位置的编号集合比如 n4, k2 时第一行是 [1 2]表示第 1、2 个子载波激活。numValid是 2 的幂次个模式保证索引比特长度是整数。后面patterns(idx, comb) 1把对应列置 1形成稀疏的行向量。如果只想使用其中部分模式我一般会先检查numValid与组合总数的比值。例如 n4, k2 时C(4,2)6pfloor(log2(6)) 2所以只用其中 4 个模式剩下两个模式在查表时不会用到。这 4 个模式的选取建议从字典序中挑汉明距离较近的那一组因为实际信道下索引比特错误更多发生在相邻模式之间。2.2 主循环 OFDM_IM.m 中一个符号怎么走完发射链路OFDM_IM.m 是整个仿真的主函数它把比特流切成若干组每组再分成索引比特和符号比特。典型参数是子块大小 n4激活子载波数 k2调制阶数 M4QPSK。每个子块能传输的比特数索引比特p floor(log2(C(4,2))) 2 比特符号比特k * log2(M) 2 * 2 4 比特每个子块合计 6 比特发射端的关键步骤是先查索引比特对应的模式再把符号比特映射到激活位置上。下面是 OFDM_IM.m 中常见的核心循环伪代码% 发射端主循环 txBits randi([0 1], totalBits, 1); % 随机生成待发送比特 numBlocks length(txBits) / bitsPerBlock; txSig zeros(numBlocks, n); % 每个 OFDM-IM 子块 for b 1:numBlocks blockBits txBits((b-1)*bitsPerBlock (1:bitsPerBlock)); idxBits blockBits(1:p); % 前 p 位是索引比特 symBits blockBits(p1:end); % 剩余是符号比特 patternIdx bi2de(idxBits) 1; % 得到模式表中的行号 activePos find(patterns(patternIdx, :) 1); symbols qammod(symBits, M, InputType, bit, UnitAveragePower, true); txSig(b, activePos) symbols; % 填到激活子载波 end这里bi2de把索引比特转成十进制加 1 是因为 MATLAB 索引从 1 开始。qammod的参数InputType, bit让我可以直接把 bit 向量喂进去M4时输出长度是输入长度的一半。注意符号比特的数量是 k*log2(M)所以如果symBits长度是 4QPSK 调制后得到 2 个符号正好填到activePos的 2 个位置上。这里有一个常见坑不同 MATLAB 版本qammod的默认星座映射顺序不一致。低版本默认是二进制映射高版本默认是格雷映射。如果检测端按格雷去解但调制端用了二进制BER 会在末尾出现地板效应。我一般会显式指定Gray, true同时在接收端用同一个调制器解调避免版本差异。2.3 信道与接收端的整体数据流接收端拿到时域的 OFDM 符号后要去掉循环前缀、做 FFT再对子载波做信道均衡。这部分的代码通常不在 OFDM_IM.m 里而是放在主脚本中。OFDM_IM.m 里只保留从频域子载波到比特估计的逻辑。信道模型用简单的 AWGN 或频率选择性信道都行。如果用 AWGN接收信号就是rxSig txSig noise噪声方差根据信噪比 SNR 计算noiseVar 10^(-SNR/10)注意这里没有加归一化因子。如果用瑞利信道还需要对每个子载波的信道系数做 ZF 均衡然后把均衡后的符号和信道系数一起传给检测器。以下是我常用的 SNR 到噪声功率换算snr_db 0:2:16; for snr snr_db noiseVar 1 / (10^(snr/10)); % 单位能量符号下的噪声方差 rxSig txSig sqrt(noiseVar/2) * (randn(size(txSig)) 1j*randn(size(txSig))); % 之后调用检测器 end注意这里假设信号平均功率为 1。如果你的 QAM 调制没有归一化需要先对星座点做能量归一化否则 SNR 折算会偏差。在实际代码里我会在发射端注释里写明信号平均功率为 1这样检测器里的 LLR 计算才不会差好几倍。3. ML 检测器三兄弟Mary、NearML 与 LowC 的复杂度代价3.1 ML_Detector_Mary.m 的穷举逻辑与组合爆炸最优检测器是最大似然检测它把当前子块所有可能的激活模式和所有可能符号的组合都枚举一遍选择使欧氏距离最小的那组。ML_Detector_Mary.m里Mary代表 M 进制符号也就是每个激活子载波上可能有 M 种符号。给定 n4, k2, M4激活模式有 2^p4 个每个模式有 k 个符号每个符号有 M 种可能。总假设数是numPatterns * M^k 4 * 4^2 64。对于小参数来说完全可枚举。MATLAB 里典型实现function [estBits, estSym] ML_Detector_Mary(rx, patterns, M, noiseVar) % rx: 长度为 n 的接收子块 % patterns: 所有激活模式 % M: 调制阶数 numPatterns size(patterns, 1); qamSet qammod(0:M-1, M, UnitAveragePower, true); minDist inf; estBits []; for pi 1:numPatterns active logical(patterns(pi, :)); rxActive rx(active); % 枚举 k 个符号的所有 M^k 种组合 symbolCombs repmat(qamSet, k, 1); % 简化示意实际需要码本扩展 for combIdx 1:M^k cand codeCombinations(:, combIdx); dist sum(abs(rxActive - cand).^2); if dist minDist minDist dist; estBits patternBits(pi, :) DetoBit(cand, M); end end end end上面的代码为了可读性省掉了码本展开实际会用allcomb或ndgrid生成所有符号组合。注意noiseVar参数在这里没有用到因为 ML 不用先验概率只需要距离比较。这里有个细节如果信道不是 AWGN比如有频选衰落那么距离计算要变为sum(abs(rxActive - hActive .* cand).^2)也就是要先乘上信道系数。3.2 为什么要写 ML_Detector_NearML.m剪掉明显的坏假设ML_Detector_NearML.m做的事情是把穷举搜索裁剪掉一部分明显不可能的组合。最常见的是两步法先用 ZF 或匹配滤波估计每个激活位置上最可能的符号再只在符号候选集很小的前提下做模式搜索。实际实现中NearML 会把每个位置上的 M 个星座点按距离排序只保留前 L 个作为候选。L 通常取 2 或 4。这样总假设数从numPatterns * M^k降为numPatterns * L^k。对于 M16, k2, L4假设数从 16^2256 降到 64 个收益很明显。这种近似在高 SNR 下几乎无损因为噪声小时真实符号一定排在距离最近的前几个候选里。但在低 SNR 下会有些许损失好在这部分损失可以通过后续纠错码补偿。使用 NearML 时L 的取值和调制阶数 M 的比值很关键。L 小于等于 M/4 时复杂度下降明显但要注意检测性能曲线会在某个 SNR 点上出现平台。3.3 ML_Detector_LowC.m 是怎么利用索引调制结构降复杂度的LowC的思路和 NearML 不同它把检测拆成两层第一层对每个可能的激活模式做一次限带的最小二乘估计第二层只对模式间距离做比较。由于 OFDM-IM 的激活子载波数 k 远小于 n在非激活位置上的残留噪声被显式当作惩罚项计入距离。简化公式是score(mode) sum_active |rx - h*s_est|^2 alpha * sum_inactive |rx|^2其中s_est是对激活符号的 MMSE 估计alpha是一个与噪声方差相关的加权因子。当alpha1时这个评分就退化为对全向量的欧氏距离。alpha0时只关心激活位置会漏掉“假激活”产生的能量。实际代码中 alpha 一般取noiseVar/(noiseVar1)这是 MMSE 推导下的最优比例。这个检测器的实现开销主要在矩阵求逆但因为每个模式的结构是固定的可以预计算逆矩阵。我在做参数扫描时LowC 是三个 ML 里唯一能跑到 n8, k4, M64 参数的。用 2 个子块并行测试时时间开销比 Mary 低一个数量级。3.4 三种 ML 检测器的复杂度对比表检测器假设数乘法复杂度适合场景ML_Detector_MaryP * M^k约 5NPM^k小参数、做理论下界ML_Detector_NearMLP * L^k约 5NPL^k 排序开销中参数M16 时优先ML_Detector_LowCP约 3N*P 预计算n、k 较大的大参数仿真从表中可以看出LowC 的假设数降到 P 个也就是只跟激活模式数有关跟调制阶数无关。对高调制阶数系统比如 256QAMML_Detector_Mary 几乎跑不动但 LowC 依然能实时。不过要注意 LowC 不是严格最优它丢失了符号与模式之间的联合信息在低 SNR 下会比真 ML 差 0.5dB 左右。4. LLR 检测器与比特映射BitoDe、DetoBit 在软判决链路里的作用4.1 LLR 检测器为什么适合与 LDPC 码级联ML 检测器输出的是硬判决比特丢失了置信度信息。OFDM-IM 的索引比特错误通常不是均匀分布某些模式下激活位置相邻子载波间干扰会导致特定模式对更容易混淆。如果没有软信息LDPC 解码器无法利用这些置信度。LLR_Detector.m就是为软判决设计的。LLR 核心是计算每个比特为 1 与为 0 的对数似然比L(b) log( P(b1 | rx) / P(b0 | rx) )在 OFDM-IM 中这个值要对所有包含该比特为 1 的假设求和。设候选假设集合为 S比特 b 对应的软信息公式为L(b) log( sum_{(c in S, b(c)1)} exp(-||rx - h*c||^2 / noiseVar) / sum_{(c in S, b(c)0)} exp(-||rx - h*c||^2 / noiseVar) )直接用指数和对数实现数值不稳定代码里一般会用logsumexp技巧。我见过很多把实现写成max近似的版本那就是把上式换成最大项损失约 0.2dB但能省大量指数计算。以下是LLR_Detector.m的典型核心代码function llr LLR_Detector(rx, patterns, M, noiseVar) numBits log2(size(patterns,1)) log2(M); % 总比特数 llr zeros(1, numBits); for bi 1:numBits sumExp1 0; sumExp0 0; for pi 1:size(patterns,1) active logical(patterns(pi,:)); rxActive rx(active); for symComb allCombinations(M, sum(active)) bVec [de2bi(pi-1, log2(size(patterns,1))), ... % 索引比特 de2bi(symComb, log2(M))]; % 符号比特 if bVec(bi) 1 sumExp1 sumExp1 exp(-norm(rxActive - symNorm)^2 / noiseVar); else sumExp0 sumExp0 exp(-norm(rxActive - symNorm)^2 / noiseVar); end end end llr(bi) log(sumExp1 eps) - log(sumExp0 eps); end end注意这里de2bi的位序必须与发射端一致否则软信息会整个错位。我建议在BitoDe.m和DetoBit.m里固定使用 MSB 在前并在注释里标明。否则换一台机器跑结果就不一样。另一个容易踩的坑是noiseVar的尺度。如果接收信号没有归一化噪声方差要乘以符号能量。在 OFDM-IM 中激活子载波上的信号能量与传统 OFDM 相同但整体平均发射功率会因部分子载波关闭而降低。如果你用固定的发射功率约束噪声方差就是发射端激活功率与信道增益的比值这需要先算一个缩放系数。4.2 BitoDe 与 DetoBit索引比特与符号比特的互逆转换BitoDe.m和DetoBit.m是一对互逆函数。BitoDe把比特向量解释成十进制数DetoBit把十进制数转回比特向量。这里的关键是位序和位数的一致性。在组合表模式下索引比特数 p 不一定能被 8 整除所以不能直接用de2bi的默认位数。我一般这样封装function dec BitoDe(bits) % bits 是行向量第一位为最高位 dec sum(bits .* 2.^(numel(bits)-1:-1:0)); end function bits DetoBit(dec, numBits) % 把十进制转成定长比特首位为最高位 bits mod(floor(dec ./ 2.^(numBits-1:-1:0)), 2); end这两个函数不依赖 MATLAB 通信工具箱在纯源码环境下也能运行。很多初学者直接使用bi2de和de2bi结果在参数扫描时发现组合表索引对不上就是因为工具箱版本的位序默认值不同。用自己的封装至少能保证行为确定。4.3 为什么需要两个 LLR 检测器源码里有LLR_Detector.m和LLR_Detector2.m这两者的差异一般是对噪声方差估计方式不同。第一个假设已知noiseVar第二个从当前子块接收符号的剩余能量中估计噪声。第二种在信道变化较快的场景下更鲁棒但会有估计偏差。实际使用时我建议先跑LLR_Detector拿到理想性能下界再跑LLR_Detector2看估计误差带来的损失。如果两者差距小于 0.1dB那说明系统性已经足够好。4.4 检测性能的验证顺序拿到这个源码库我会按以下顺序验证第一步编辑OFDM_IM.m将 n4, k2, M4SNR 从 0 到 12dB 扫描。第二步分别调用ML_Detector_Mary和LLR_Detector对比硬判决 BER 与软判决 BER。第三步调整参数 n8, k4观察组合表数量指数增长后判断是否要切到LowC。第四步用BitoDe/DetoBit对随机比特做一致性测试确保在检测端能无误恢复。5. 把 OFDM-IM 仿真跑快的几个细节先查表再算 LLR最后这部分我想聊聊实际调参时容易忽略的三个点这些不是理论推导内容是跑仿真踩出来的。第一组合表生成顺序要固定。Combin_Md.m用nchoosek得到的组合顺序与 MATLAB 版本无关但如果你对组合按二进制索引排序就一定要在发射和接收端用同一套排序。一个很隐蔽的问题是patterns的每一行未必是字典序。我习惯在生成后加一行patterns sortrows(patterns)然后在注释里写明“索引比特 00 对应排序后第一行”。否则调试时检测器一直出错最后发现是模式顺序反了。第二LLR 计算中logsumexp替代直接取对数。直接把所有指数求和再取对数数值会下溢。例如 SNR20dB 时距离差可能超过 10exp(-10)接近 1e-5在浮点范围内没事但如果距离差到 30就会明显下溢。我用 max 近似误差很小但要注意归一化。如果要做精确软信息建议用以下稳定形式function lse logsumexp(v) m max(v); lse m log(sum(exp(v - m))); end把这个函数嵌入LLR_Detector.m可以避免指数溢出并且保持软信息的相对尺度正确。实测在 SNR 0-20dB 范围内logsumexp 版本与双精度直接计算版本完全一致但速度只慢约 5%。第三检查 BER 曲线在低 SNR 时的斜率。如果 BER 曲线在 10dB 以后变平多半不是检测器问题而是索引比特与符号比特的错误地板。OFDM-IM 在高信噪比下索引比特的错误概率受最小模式间距离决定。你可以算一下模式表中任意两行的汉明距离的最小值。如果最小汉明距离太小比如小于 2那么索引比特的错误地板就会抬高。这种情况下增加 p 的指数位数或改变模式选择策略比换更复杂的检测器更有效。最后给一个实用的自检函数随机生成一段比特流经过发射、AWGN、检测后比对BitoDe前后的比特是否一致。这一步能同时验证模式表、位序、调制映射和检测器的四条链路。源码里的 README.md 可能只写了参数定义但真正让代码跑通的关键往往就在这些细节里。本文还有配套的精品资源点击获取