简介:围绕OFDM可见光通信系统的MATLAB仿真包,面向刚接触光通信与OFDM调制的本科生、研究生以及课程设计和毕业设计入门者,重点解决可见光无线信道建模与完整收发链路搭建两个问题。压缩包共10个文件,以9个.m脚本和1个.mat数据文件为主,整体约10KB;脚本涉及基于朗伯模型的可见光无线信道、OFDM发送与接收流程、QPSK和16QAM调制解调、升余弦窗处理等环节,数据文件保存信噪比相关信息,方便直接运行并对照分析。已有1978人学习,说明其在OFDM仿真与可见光通信仿真方面具有较强参考价值。代码注释较为详细,读者可据此理解信道冲激响应生成、子载波映射、加窗抑制带外泄漏以及信噪比与误码观察等关键步骤,也能在原有框架上修改参数开展扩展实验,适合作为课程作业、毕业设计前期验证和自学练手材料。
1. 从一盏灯到一条链路:OFDM可见光通信系统仿真先要解决什么
做 OFDM可见光通信系统仿真 的人,起点往往很具体:手边只有一盏大功率 LED、一个光电二极管和一台示波器,想验证一个峰均比抑制思路或者一个信道估计算法,总不能每次都重搭光路。一份能跑通的 matlab 仿真链路就成了刚需——它要能把比特流生成、OFDM 调制、LED 限幅与直流偏置、多径光信道、解调判决、误码率统计这几件事串成闭环。这套东西适合三类人:要交毕设或课设、需要完整链路和可解释结果的同学;想快速筛算法、不想一上来就动硬件的开发者;以及要给新人做入门材料的工程师。它不追求把可见光通信讲全,只求一条链路每一级都可调、可看、可复现。
2. 系统总体架构设计:发射端、信道、接收端与六个模块的职责划分
仿真平台最容易写歪的地方,不是算法难,而是边界没划清——发射端偷偷把信道的事做了,信道里又混进了接收端的均衡,最后哪一步出问题都查不出来。所以动手之前先把三段式结构钉死:发射端只负责把比特变成满足光域约束的实数序列,信道只负责电光转换、多径卷积和噪声叠加,接收端只负责同步、去 CP、FFT、均衡和判决。三段之间用明确的结构体传参,任何一段内部怎么改都不影响另外两段。这套分法看着朴素,但后面加算法、换信道模型、扫参数全靠它撑着。
2.1 一条完整链路的信号流向:从比特到光再到电流
按信号的物理形态走一遍,链路上依次经过六道工序。第一道是比特生成与星座映射,把 0/1 序列映射成 QAM 复数符号,同时记录原始比特用于后面算误码率。第二道是频域组装,把数据符号、导频、直流位置和 Hermitian 镜像拼成一个长度为 N 的频域向量。第三道是 IFFT 与加 CP,把频域向量变回时域实数序列,并在前面复制一段尾部样本抵消多径带来的符号间干扰。
第四道是光域适配,也就是加直流偏置并按 LED 的动态范围做削波,这一步是可见光 OFDM 区别于射频 OFDM 的核心。第五道是信道,光信号经 LED 发出、在空间传播、被光电二极管接收,数学上表现为多径卷积、乘性衰减和加性噪声。第六道是接收处理,去掉 CP、FFT 回到频域、用导频做信道估计与均衡、解映射回比特,最后和原始比特比对统计误码数。整条链路每一道工序都对应一个函数,输入输出都是纯数据,不共享全局变量。
这样切的好处是排查成本低。BER 曲线不对的时候,可以把某一级的中间结果单独抓出来看:星座图散不散、频谱有没有被削平、信道估计出来的幅频响应和设定的抽头对不对得上。我一般会在这六道工序之间各留一个可选的中间结果输出开关,调试时打开,出图时关掉,避免内存被一堆用不上的大矩阵撑爆。
2.2 信道建模的三层结构:电光转换、多径与加性噪声
可见光信道不能照搬射频的复基带模型,因为它多了两次物理转换。第一层是电到光的转换,LED 的输入是电流、输出是光功率,在正常工作区内近似线性,但上下都有硬边界:低于阈值不发光,高于上限进入饱和。第二层是空间传播,室内场景下直射路径加若干次墙面反射,等效成一个短抽头数的 FIR 滤波器,抽头数和时延扩展由房间尺寸和墙面反射率决定,通常取三到六径就够刻画主要失真。
第三层是光到电的转换与噪声。光电二极管把光功率线性转成电流,噪声主体是散粒噪声和热噪声,在仿真里合并成一个加性高斯项处理,方差由信噪比参数反推。这里有个容易忽略的细节:因为加了直流偏置,信号是单极性的,噪声叠加之后会出现负值,接收端需要先把偏置减掉再解调,否则星座点会整体平移,误码率怎么调都对不上。
提示:信道抽头的总能量要做归一化,否则扫信噪比的时候,噪声功率按什么基准算会变得含糊,出来的曲线不可比。
把这三层分开实现,好处是换场景只改一层。想测不同房间尺寸,改多径抽头和时延;想测不同 LED 线性度,改削波上下限;想测不同噪声环境,改信噪比数组。三层互不牵连,做对比实验的时候一次只动一个变量,结论才站得住。
2.3 模块划分与文件职责:六个函数撑起一套仿真
落到文件层面,我一般按职责切成六个部分,主脚本只做流程编排,不写任何算法细节。这样交作业的时候结构清楚,答辩被问「这一段在哪」也能立刻翻到。
| 模块 | 主要职责 | 关键输入 | 关键输出 |
|---|---|---|---|
| 参数配置 | 集中管理点数、阶数、偏置、信噪比、种子 | 无 | cfg 结构体 |
| 发射端 | 映射、导频插入、Hermitian 对称、IFFT、加 CP | cfg | 时域实数序列、原始比特 |
| 光域适配 | 加直流偏置、按 LED 范围削波 | 时域序列、偏置量 | 单极性驱动信号、削波比例 |
| 信道 | 多径卷积、功率归一、加噪 | 驱动信号、信噪比 | 接收信号 |
| 接收端 | 去偏置、去 CP、FFT、信道估计、均衡、解映射 | 接收信号、导频 | 判决比特 |
| 统计与出图 | 误码累计、误码率计算、曲线与星座图 | 比特对、中间量 | BER 数组、图窗 |
模块之间只通过参数和返回值通信,不读写 base 工作区的变量。这条纪律听起来刻板,但它是唯一能让「换一组参数结果就全变」这类问题变得可查的办法——所有影响结果的量都在 cfg 里,跑之前打印一遍 cfg 就知道这次到底用了什么配置。
3. 关键算法实现:OFDM 调制解调在光域上的三处改造
射频 OFDM 的教科书代码直接搬到可见光场景,跑出来的结果通常是错的,而且错得不明显:星座图看着还行,BER 曲线也能下降,但下降的位置和斜率都不对。原因在于光域对信号有三个硬约束——必须是实信号、必须是单极性、动态范围有限。这三条各自对应一处改造,缺一条整套仿真就失真。
3.1 Hermitian 对称:把复数 OFDM 变成能驱动 LED 的实信号
LED 的驱动电流是实数量,所以 IFFT 的输出必须是实序列。做法是在频域构造共轭对称:正频率放数据,负频率放它的共轭,直流和奈奎斯特位置留空不用。这样 IFFT 之后虚部理论上为零,只留实部即可。
% 频域组装:数据 + Hermitian 镜像,保证 IFFT 输出为实数 N = 64; % 子载波总数(2 的幂,便于 FFT) N_data = 31; % 单侧有效数据子载波数 M = 16; % 16QAM bits = randi([0 1], N_data*log2(M), 1); sym = qammod(bits, M, 'InputType','bit', 'UnitAveragePower', true); X = zeros(N, 1); idx = 2:N_data+1; % 跳过 DC(第 1 个)与奈奎斯特位置 X(idx) = sym; % 正频率放数据 X(N-idx+2) = conj(sym); % 负频率放共轭,形成对称 x = ifft(X, N) * N; % 乘 N 抵消 MATLAB ifft 的归一化这里有两个参数会直接影响结果。N_data 决定能装多少数据,取值上限是 N/2 减 1(去掉直流和奈奎斯特),取满了镜像就没地方放。乘 N 是为了让 IFFT 之后的功率与频域符号功率一一对应,不乘的话幅度会小 N 倍,后面按固定偏置削波时会被误判成「信号太弱」而白白加大偏置。
实际写的时候建议加一行断言,检查max(abs(imag(x)))是否小到可以忽略,比如小于 1e-10。有些版本的对称索引写错一位,虚部不会明显变大,但星座图会出现轻微的成对偏移,这种错靠看图很难发现,靠断言一行就抓住。
注意:奈奎斯特位置(第 N/2+1 个)必须置零。放数据进去会破坏对称性,IFFT 输出会出现无法消除的虚部交替。
3.2 直流偏置与削波:DCO-OFDM 的偏置系数怎么取
双极性实信号要驱动单极性 LED,最直接的办法是加一个直流偏置把整段波形抬到正区间,这就是 DCO-OFDM。偏置量太小,负半周被削掉的比例高,削波噪声大;偏置量太大,光功率大部分耗在直流上,有效信号占比低,同样掉性能。工程上常用的经验区间是信号均方根值的 7dB 到 13dB,仿真里可以从 7dB 起步往两边扫。
% 光域适配:加偏置后按 LED 线性区削波 bias_db = 7; % 偏置量(dB,相对信号 RMS) sigma = sqrt(mean(x.^2)); bias = sigma * 10^(bias_db/20); x_dc = x + bias; % 抬到正区间 led_min = 0; % LED 线性区下限 led_max = 2 * bias; % 以偏置为中心的对称上限 x_led = min(max(x_dc, led_min), led_max); clip_ratio = mean(x_dc ~= x_led); % 削波样本占比削波上限取 2 倍偏置,是为了让线性区相对偏置点对称,这样正负半周的削波概率一致,不会引入额外的直流漂移。削波比例 clip_ratio 是个非常有用的观测量:正常应该在千分之几到百分之一量级,超过 1% 基本可以判定偏置给少了或者信号功率估错了。我习惯把它打印在每一轮循环里,一旦某次实验这个数突然跳到 5% 以上,基本就是参数串了。
接收端要做的对称操作是减掉偏置。这一步的偏置值必须是发射端实际用的那个数,不能重新估。有些实现图省事用接收信号的均值代替,遇到多径造成的直流分量偏移就会减不干净,星座图整体平移一小截,BER 曲线在中高信噪比处出现平台——这个坑下面还会专门讲。
3.3 导频与 LS 信道估计:最小可用的均衡方案
多径会让不同子载波经历不同的幅度衰减和相位旋转,不做均衡直接解映射,高阶调制基本没法用。最小可用的方案是梳状导频加最小二乘估计:每隔固定间隔插一个已知符号,接收端用收到的值除以发端已知值得到该频点的信道响应,再插值到全部子载波。
% 梳状导频 + LS 估计 + 线性插值均衡 pilot_step = 8; % 每 8 个子载波插一个导频 pilot_pos = 2:pilot_step:N; % 跳过 DC Xpilot = ones(size(pilot_pos)); % 导频取单位幅度,便于反解 H_ls = Y(pilot_pos) ./ Xpilot; % 逐点最小二乘 H_est = interp1(pilot_pos, H_ls, (1:N).', 'linear', 'extrap'); X_eq = Y ./ H_est; % 迫零均衡pilot_step 是精度和开销的折中。间隔越小,插值越贴合真实的幅频响应,但占用的子载波越多,有效速率越低;间隔越大,遇到深衰落谷点容易插歪,均衡后那一带子载波的信噪比会明显低于别的频点。在四到六径的室内信道下,8 是个稳妥的起点,想对比可以试 4 和 16 看曲线差多少,这个对比本身就是毕设里很好写的一节。
导频取单位幅度是为了让除法退化成乘法,减少一次逐元素运算,同时也让 LS 估计的噪声特性更直观。如果导频带功率缩放,记得在反解时把缩放因子除回去,否则估计出的信道响应会整体差一个常数倍,均衡后星座图大小不对,解映射的门限全乱。
迫零均衡在深衰落处会放大噪声,这是 LS 方案的固有短板。仿真里如果要体现这一点,可以在均衡后加一句限幅:信道幅度低于某个阈值的子载波直接标记为不可靠并跳过,用相邻子载波的信息补。这不属于必须项,但答辩时被问「你为什么不用 MMSE」时,有这么一段实践记录会好接得多。
3.4 误码率统计与理论曲线对照
误码率是这套仿真的最终产出,也是最容易被糊弄过去的指标。可靠的统计至少要满足两条:单个信噪比点上累计的错误比特数超过 100 个,总比特数足够让最底部的点不至于只错一两个。常见做法是每个信噪比点跑固定帧数,帧数由目标误码率反推——想看 1e-4,那每个点至少得有 1e6 比特。
对照曲线怎么来?光域 OFDM 的理论误码率没有闭式解,工程上一般做两条参考线:一条是相同调制阶数下 AWGN 信道的理论 BER,用作下界;另一条是关掉削波和信道、只留噪声的仿真结果,用作自查。如果后者和理论线对不上,说明映射、对称或者解映射里有错,先别急着怀疑信道模型。
我一般会在图上同时画三条:理想 AWGN 理论线、无削波的仿真线、完整链路的仿真线。三条线的间距本身就说明了削波和信道估计各自损失了多少 dB,这个「损失分解」在答辩里比单画一条曲线有说服力得多。
4. 仿真平台的工程化落地:参数化配置、批量跑点与结果出图
算法写完只是半成品,能不能高效地跑几百组参数、能不能把结果整理成能放进报告的图,才是这套仿真好不好用的分水岭。见过太多版本是脚本里到处写死数字,想改个子载波数得全文搜索替换,改漏一处结果就悄悄变样,这种翻车现场排查起来极其费劲。
4.1 用一个 config 结构体管住所有参数
所有可调量收进一个函数返回的结构体,是投入产出比最高的一步改造。主脚本第一行拿到 cfg,之后所有函数都以 cfg 为入参,谁都不许在自己的作用域里重新定义点数、阶数或者偏置。
function cfg = vlc_config() % 全链路参数集中定义,改这里即可,不要在别处写死数字 cfg.N = 64; % FFT 点数 cfg.cp_len = 16; % 循环前缀长度,需大于信道最大时延扩展 cfg.mod_order = 16; % 调制阶数:4 / 16 / 64 cfg.n_ofdm = 2000; % 每帧 OFDM 符号数,决定蒙特卡洛精度 cfg.n_frame = 5; % 每个信噪比点重复帧数 cfg.snr_db = 0:2:30; % 信噪比扫描范围(dB) cfg.bias_db = 7; % 直流偏置(dB,相对信号 RMS) cfg.led_range = [0 2]; % 归一化后的 LED 线性区 cfg.n_tap = 4; % 多径抽头数 cfg.pilot_step= 8; % 导频间隔 cfg.seed = 20240501; % 随机种子,保证结果可复现 endcp_len 要大于信道最大时延扩展对应的样本数,否则 CP 起不到隔离作用,符号间干扰会直接体现在星座图的弥散上。拿不准的时候用「最大时延 × 采样率」估一下,往上取整再留一倍余量。种子固定这一条别省,同样的参数每次跑出不同的曲线,讨论结果时会非常被动。
把参数写成结构体还有一个隐性收益:做对比实验时可以直接把 cfg 存盘,图旁边附上对应的 cfg 文件名,半年后回头看还能还原当时的条件。手工记录参数表的做法,撑不过第三轮实验。
4.2 信噪比扫描与蒙特卡洛循环的写法
扫描循环的结构决定了运行时间和结果可信度。外层扫信噪比,内层跑帧做蒙特卡洛,每帧独立生成比特、独立过信道,错误累加后统一算比值。
ber = zeros(size(cfg.snr_db)); for k = 1:numel(cfg.snr_db) rng(cfg.seed); % 每个信噪比点重置种子,曲线才平滑 err = 0; nbit = 0; for f = 1:cfg.n_frame [tx_sig, tx_bits] = build_tx(cfg); rx_sig = pass_channel(tx_sig, cfg, cfg.snr_db(k)); [rx_bits, clip_ratio] = build_rx(rx_sig, cfg); err = err + sum(rx_bits ~= tx_bits); nbit = nbit + numel(tx_bits); end ber(k) = err / max(nbit, 1); fprintf('SNR=%2d dB BER=%.3e err=%d clip=%.2f%%\n', ... cfg.snr_db(k), ber(k), err, clip_ratio*100); end每个信噪比点重置种子这一步值得单说。不重置的话,不同信噪比点用的是随机流的不同段落,低信噪比处的偶然波动会带到高信噪比点的统计里,曲线看起来毛毛躁躁,做报告时不好解释。重置之后,各点之间的差异只来自噪声强度,曲线单调平滑,趋势一目了然。
打印那一行也是刻意加的。err 为 0 的点要单独标记——它不代表误码率真的是 0,只代表这一轮没抓到错误,画图时要么标成空心点,要么直接不画,别硬塞一个 1e-8 上去让曲线看起来特别漂亮。
4.3 结果出图:星座图、频谱图与 BER 曲线
三张图各有各的用途,缺一张答辩时就少一个抓手。星座图看均衡效果,频谱图看削波和带外泄漏,BER 曲线看整体性能。
星座图取均衡后、判决前的复数符号,画散点。看得清的标准是:低信噪比时云团状,高信噪比时聚成清晰的点阵;如果所有点整体偏移,是偏置没减干净;如果点阵被压扁成椭圆,是均衡的幅度估计有偏差。频谱图用pwelch或直接 FFT 取模,重点看两件事:带内是否平坦、带外是否快速滚降。削波严重时带外会明显抬起来,这是削波噪声的典型特征。
BER 曲线用半对数坐标,横轴信噪比、纵轴误码率,同时叠上理论参考线和无削波版本。图例写清楚每条线的条件,别只写「仿真」「理论」,半年后自己都看不懂。图窗保存成 pdf 或 png 时记得把分辨率调高,插进报告里不糊。
5. 避坑与常见问题排查:五个让仿真结果对不上的关键点
仿真跑不通的时候,报错反而是好事,最怕的是不报错但结果不对。下面这几条是我在调试这类链路时反复遇到过的,基本覆盖了「曲线形状怪异」的绝大多数情形。
5.1 现象:高信噪比处 BER 曲线突然变平,怎么加功率都压不下去
原因通常是残留的确定性误差,而不是噪声。最常见的两个来源:一是接收端减偏置用的是重估值而不是发射端的真实值,多径带来的直流分量残留成一个固定偏移;二是导频位置和均衡时的索引错位一位,导致部分子载波用了相邻频点的信道响应,形成固定的相位误差。这两类误差都不随信噪比减小,所以曲线到某个点就卡住。
解决的办法是把误差来源逐个隔离。先把信道设成单径、关掉削波,如果平台消失,说明问题在光域或信道;如果平台还在,问题在收发端的索引或偏置对齐。再逐段比对发射前的频域符号和接收均衡后的频域符号,理想情况下两者只差一个复系数,如果不是,就能定位到具体是哪几个子载波出错。
5.2 现象:星座图整体旋转一个角度,或者分裂成上下两团
整体旋转一般是相位没有对齐。OFDM 对公共相位误差很敏感,而梳状导频的 LS 估计是逐个频点独立做的,插值之后整条曲线的相位可能有缓慢漂移,表现为星座图被「拧」了一下。解决办法是加一级公共相位补偿:用所有导频估计出的相位取平均,作为一个全局旋转角补回去。
分裂成两团通常和 IFFT 长度或 CP 长度不匹配有关。CP 太短,前一个符号的尾巴压到当前符号上,前后符号的相位不同,星座点就会分成两组。把 CP 加长到明显够用,如果两团合并了,就确认是这个原因;随后再逐步缩短,找到刚好不出问题的临界长度,这个临界值本身就是报告里可以写的结论。
5.3 现象:加直流偏置削波之后,BER 反而比不削波还差
先确认「不削波」那一版是不是真的合法。很多实现里所谓不削波,是直接把双极性信号送进信道,接收端也没有减偏置这一步,等于省掉了一次变换,两者的比较基准根本不对等。正确的对比应该是:同一套收发流程,只把削波门限放开到足够宽,看性能差多少。
如果基准没问题,那多半是偏置量给错了方向。偏置太小会导致大量负半周被削,削波噪声集中在信号峰值处,等效于给高幅度符号加了额外扰动,高阶调制最先崩。把 bias_db 从 7 往下调到 4,削波比例会明显上升,BER 变差;往上调到 13,削波几乎消失但有效光功率占比下降,BER 也会变差。中间存在一个最优点,扫一遍就能看到,这个扫描曲线本身就是一个很漂亮的结果图。
5.4 现象:换一组参数结果全变,但完全找不到是哪一步的影响
这是典型的参数散落问题。表现是改了调制阶数之后,载波数、导频间隔、帧数全都跟着变了,或者根本没变但结果就是不一样,查半天查不出原因。根子在于某些参数写死在函数内部,改配置的时候漏掉了。
彻底的做法是把所有数字都收进 cfg,函数体内除了循环下标和数学常数,不出现任何字面量。改完之后做一次自检:把所有 cfg 字段打印出来,逐条对照论文或需求里的设定值。另外建议在每张图的标题或文件名里带上关键参数,比如调制阶数、偏置和导频间隔,出图之后不用回头翻日志。
5.5 现象:扫一个信噪比点要十几分钟,整套曲线跑一整晚
慢的原因九成在循环里反复做同一件事。常见的是每帧重新生成信道抽头、每帧重新算一次滤波器的频响、或者在符号级循环里用interp1逐点插值。这些计算和随机比特无关,应该提到循环外面做一次。
优化顺序建议这样:先把信道卷积改成频域相乘或filter一次成型,再把导频插值改成矩阵运算,最后调帧数——帧数是精度的直接来源,能不动尽量不动。如果确实要跑大量参数组合,把最外层改成parfor并行,注意随机种子要按信噪比点分别设置,否则并行出来的曲线会互相污染。
6. 验收与答辩:怎么证明这套仿真真的跑对了
仿真这类东西有个尴尬之处:它不产出实物,别人没法一眼判断对错。所以验收的核心不是「跑出图了」,而是「能证明每一步都对」。我一般按三层来查。
第一层是单元自检。每个模块单独跑一组确定输入,检查输出是否落在预期范围:IFFT 之后虚部是否可忽略、加 CP 之后长度是否等于 N 加 cp_len、削波比例是否在千分之几、LS 估计出的信道响应和设定抽头的频响是否吻合。这一层不需要图,打印几个数就够,但它能挡住八成以上的低级错误。
第二层是极限对照。把信道设成理想单径、关掉削波、只留高斯噪声,此时完整链路的 BER 应当与同阶调制在 AWGN 下的理论值基本重合,差距在 1dB 以内算合格。再单独打开削波,看曲线右移多少,这个位移量要和削波比例对得上——削波比例越大,位移越大,如果两者趋势相反,说明光域适配那一段的符号搞反了。最后打开多径,看损失是否符合时延扩展的预期。
第三层是敏感性检查。挑三到四个关键参数各扫一组:偏置从 4dB 到 13dB、导频间隔从 4 到 16、CP 长度从 8 到 32。合格的表现是曲线有明确的最优点或单调趋势,而不是忽高忽低。忽高忽低通常意味着统计量不够,把帧数翻十倍看是否收敛。
答辩时最容易被追问的三个问题,提前准备好答案:一是「你的信道模型依据是什么」——说清楚抽头数、时延和功率为什么这么取,能给出物理场景的对应关系最好;二是「为什么用 LS 不用 MMSE」——从复杂度、是否需要噪声先验、以及本场景下两者的实际差距三个角度答;三是「你的误码率可信吗」——把每个点的累计错误比特数拿出来,指出最底部的点也超过了三位数的错误数。
| 验收项 | 合格标准 | 不达标的典型原因 |
|---|---|---|
| 时域虚部 | 最大值小于 1e-10 | 对称索引写错、奈奎斯特位置未置零 |
| 削波比例 | 千分之几到 1% | 偏置量偏离最优区间 |
| 理想信道 BER | 与理论线差 1dB 以内 | 映射/解映射或偏置未对齐 |
| 参数敏感性 | 曲线单调或有明确最优点 | 统计帧数不足、种子未固定 |
| 运行时间 | 单点分钟级 | 循环内重复计算信道或插值 |
从那以后我每次开一套新仿真,都强制先把「理想信道能不能对上理论线」这一步走完,再谈别的模块。这一步不过,后面所有的曲线都不值得看——省下来的那几个小时,最后都会加倍还回去。希望这套结构能帮到你,把这套 OFDM可见光通信系统仿真 的 matlab 工程,直接落成一份能交、能讲、能复现的完整方案。
本文还有配套的精品资源,点击获取