简介:面向无线通信与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.m |
| BitoDe.m | 比特序列转十进制索引值 | OFDM_IM.m |
| DetoBit.m | 十进制索引值转回比特序列 | 接收端解码 |
| ML_Detector_Mary.m | 全搜索最大似然检测 | OFDM_IM.m |
| ML_Detector_NearML.m | 低复杂度近 ML 检测 | OFDM_IM.m |
| LLR_Detector.m / LLR_Detector2.m | 对数似然比检测两个版本 | OFDM_IM.m |
README.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 较大时计算量会迅速膨胀。n=4、k=2 只有 6 种组合,n=128、k=64 就是天文数字,所以这个查找表方案只适合子块尺寸较小的配置,一般 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 次欧氏距离计算。n=4、k=2、M=4 时是 6×16=96 次,完全能接受;但 n 和 k 一涨就是指数爆炸,所以 ML 在这份代码里更多是作为性能上界存在,用来衡量其他检测器的损失。
参数说明:M 的取值直接影响内层符号组合数,M=4 换到 M=16,内层组合数从 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 开头一般是参数区,我拆到的典型配置是每子块 n=4,活动 k=2,调制 QPSK。这几个参数决定了一帧能传多少比特:
p1 = floor(log2(C(4,2))) = floor(log2(6)) = 2 p2 = 2 × log2(4) = 4 每子块 6 bit,其中 2 bit 是索引信息。
如果改成 n=8、k=4、16QAM,p1 = floor(log2(70)) = 6,p2 = 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逻辑说明:每帧的比特数必须能被 p1+p2 整除,否则最后一组比特凑不齐,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(p1+1:p1+p2); % 后面 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 扩大到 k+1 或 k+2,在粗筛结果上补几个次优候选再搜索。
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 调制阶数升级的三个改动点
把 M=4 换成 M=16 不是简单改一个变量。第一,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, -3+3i, -3+1i, -1-3i, -1-1i, -1+3i, -1+1i, ... 3-3i, 3-1i, 3+3i, 3+1i, 1-3i, 1-1i, 1+3i, 1+1i];参数说明: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,希望也能帮到你。
本文还有配套的精品资源,点击获取