简介这是一套面向可见光通信与OFDM仿真初学者的MATLAB完整代码包围绕基于OFDM的可见光通信链路展开可作为课程设计、毕业设计或入门科研的参考实现。压缩包共10个文件约10KB其中9个为.m脚本与函数1个为.mat数据文件脚本与函数覆盖基于朗伯模型的可见光无线信道建模含视距与反射路径、QPSK与16QAM调制解调、OFDM收发链路及升余弦窗处理.mat文件存放信噪比相关数据便于复现与对比实验。目前已有1978人学习下载在同类入门资料中关注度较高。代码几乎逐行附有中文详解注释可直接运行并观察不同参数下的波形与性能变化帮助读者理解信道建模、子载波映射、加窗抑制带外泄漏等关键环节。对希望快速搭建OFDM-VLC仿真框架、理清收发流程与参数设置的读者这份体量小、结构清晰的代码包能省去从零摸索的时间。1. 一套能跑出 BER 曲线的 OFDM 可见光通信系统仿真先别急着看公式解压之后没有模型文件全是 .m 脚本——这套 OFDM 可见光通信系统仿真用 matlab 把「比特流 → QAM → Hermitian 对称 → IFFT → 加直流偏置 → LED 限幅 → 朗伯信道 → 迫零均衡 → 误码统计」整条链路串了起来跑一次就能拿到 BER-SNR 曲线和星座图。它解决的是那种“论文公式看得懂、一上手不知道第一行写什么”的困境共轭镜像该放在哪几个下标、偏置加几倍标准差、限幅之后误码率为什么在高 SNR 就下不去这些细节它都替你试过一遍。适合通信、光电方向做课设和毕设开题的人也适合需要一个可复现基线、拿去和自己算法对比的开发者。2. 整体链路拆解从比特到光强再到判决的完整闭环这套仿真的骨架其实很短短到可以画在一张纸上发射端把随机比特映射成复星座点塞进 OFDM 的子载波里用共轭镜像把频谱掰成实信号IFFT 之后加循环前缀抬直流、削顶喂给 LED接收端用光电探测器把光强变回电流去直流、FFT、迫零均衡、QAM 解调、比对误码。真正花时间的不是这条主线而是每一段里那些“不做就出不来正确曲线”的处理。这里要先说清楚一个和射频 OFDM 最大的差别可见光通信是强度调制直接检测IM/DDLED 发的是光功率光电探测器收到的是光强光强不可能为负。所以发射端的时域波形必须是实数且非负这两个约束分别由 Hermitian 对称和直流偏置来满足少了任何一个后面整条链路都是错的。2.1 发射端QAM 映射、共轭镜像和循环前缀OFDM 本身产生的是复数基带信号扔进 IFFT 出来还是复数。要让它变实数标准做法是让频域序列满足共轭对称正频率放数据负频率放它的共轭镜像直流分量和奈奎斯特分量置零。这样 IFFT 的虚部理论上恒为零取实部不会丢任何信息。可用的数据子载波数量因此只有 N/2-1 个N 是 FFT 点数这一点在新手代码里最容易写错——很多人按 N 个子载波算比特数结果发端收端长度对不上误码率直接停在 0.5。循环前缀的作用和射频里一样把信道卷积的瞬态挡在 CP 里接收端丢掉就不产生 ISI单径信道下它看起来毫无用处但只要换成多径或者加一个 LED 响应拖尾没有 CP 曲线立刻崩。2.2 信道与噪声光通信里没有“负光”也没有复相位射频信道是复基带冲激响应可见光信道是实值、非负的。视距场景下最常用的是朗伯模型LED 半功率角决定朗伯阶数 m接收端 PD 的面积、视场角、滤光片增益、聚光器增益一起决定直流增益 H(0)。这个增益是个纯实数量纲上把光功率折算成光电流所以均衡的时候除的就是它不用考虑相位旋转。噪声部分通常不逐项建模散粒噪声和热噪声的物理过程而是合并成一个加性高斯白噪声方差由接收光功率、带宽、PD 响应度折算。这么做在 BER 曲线的定性趋势上是够用的也是绝大多数课程仿真采用的方式。真要抠散粒噪声随信号功率变化的那点非线性得改噪声生成那几行后面二次开发那章会说。2.3 接收端与文件组织一份典型工程的目录长什么样这类仿真工程的文件划分一般遵循“一个函数干一件事”的原则主脚本负责参数和流程功能函数各管一段。下面是常见的一种组织方式实际压缩包里命名可能略有出入但职责划分基本一致文件职责关键输入main_sim.m参数面板 单帧全流程演示 出图无dco_tx.m比特生成、QAM、共轭镜像、IFFT、CP、偏置限幅N、Ncp、M、偏置比vlc_channel.m朗伯模型信道增益、时域抽头、频域响应距离、半功率角、PD 面积dco_rx.m去 CP、去直流、FFT、迫零均衡、解调接收序列、H_fber_sweep.mSNR 循环、蒙特卡洛统计、画 BER 曲线SNR 范围和帧数plot_const.m均衡前后星座图对比均衡输出这个划分的好处是想单独测信道只调vlc_channel.m想换调制阶数只改主脚本里的 M。最怕的是把所有东西塞进一个几百行的脚本里改一个参数要在三个地方同步那才是真正的血泪经验。3. 核心模块拆解四段代码决定仿真结果对不对这一章把上面那条链路拆成四段可抄的代码每段都给出参数含义和改法。代码风格按 matlab 的常规写法来函数式封装主脚本调用。3.1 发射端共轭镜像与归一化对齐function [x_out, h_taps, H_f] dco_tx(N, Ncp, M, bias_ratio, led_max) % DCO-OFDM 发射机比特 - QAM - Hermitian 对称 - IFFT - CP - 偏置限幅 k log2(M); nData N/2 - 1; % 去掉 DC 和 Nyquist 后的可用子载波 bits randi([0 1], nData*k, 1); sym qammod(bits, M, InputType, bit, UnitAveragePower, true); X zeros(N, 1); X(2:N/2) sym; % 正频率承载数据 X(N/22:N) conj(flipud(sym)); % 负频率做共轭镜像 % X(1) 与 X(N/21) 保持为 0不携带任何信息 x ifft(X, N) * sqrt(N); % 乘 sqrt(N) 抵消 ifft 的 1/N x [x(N-Ncp1:N); x]; % 循环前缀 x_dc x bias_ratio * std(x); % 抬到非负 x_out min(max(x_dc, 0), led_max); % 双端限幅模拟 LED 动态范围 endN取 64、128、256 都行越大峰均比越高、限幅失真越明显课程仿真一般用 64 就够看出趋势。M是 QAM 阶数4/16/64 都能跑阶数越高对偏置和 SNR 的要求越苛刻。bias_ratio是最关键的一个旋钮后面 3.2 会专门讲怎么定。led_max是 LED 线性区上限设得太大等于只有下削波设得太小上削波会很严重一般取偏置后信号均值再加两到三倍标准差。ifft(X, N) * sqrt(N)这一行的乘法不是装饰。matlab 的ifft自带 1/N如果不补回来发端幅度会随 N 变化接收端解调出来的星座点半径就跟着 N 跑SNR 定义全乱。同理qammod用了UnitAveragePower保证平均符号功率为 1这样 SNR 才好按每样本功率来定义。归一化这条线上任何一处不统一最后的 BER 曲线都会是错的而且错得很隐蔽——曲线形状可能还挺像样。3.2 直流偏置与双端限幅把双极性信号塞进 LED 的线性区偏置加多少没有闭式解靠的是看削波比例。经验做法是先扫一遍bias_ratio统计两个方向的削波样本占比选一个两者都在 1% 以内的点bias_list 4:1:10; for b bias_list x_dc x b * std(x); p_low mean(x_dc 0); % 下削波信号被压成 0 p_high mean(x_dc led_max); % 上削波超过线性区上限 fprintf(bias%2d 下削波 %.2f%% 上削波 %.2f%%\n, b, p_low*100, p_high*100); end偏置太小负半周被硬砍削波噪声和子载波上的数据混在一起表现为高 SNR 处 BER 曲线不再下降出现误差平台偏置太大信号整体抬高同样的噪声方差下有效 SNR 反而变差而且 LED 更容易顶到上限。这是一对矛盾bias_ratio就是在这对矛盾里找平衡点。16QAM、N64 的情况下6 到 8 之间通常是个不错的起点64QAM 就得往 8 以上走。提示削波后的信号均值不再是零接收端一定要先减均值再 FFT。这一步漏掉直流分量会泄漏到相邻子载波上星座图整体偏移看起来像“均衡没做对”实际是偏置没去掉。限幅本身是非线性操作会带来带内失真和带外辐射。仿真里通常不建模带外辐射只关心带内失真对 BER 的影响所以用min(max(...))这种理想硬限幅就够了。如果要模拟真实 LED 的软压缩特性把这一行换成多项式或者 Rapp 模型具体在最后一章展开。3.3 信道朗伯模型的时域抽头和频域响应function [h, H_f] vlc_los_channel(N, Ncp, d, phi_half, psi_cone, Apd, Ts, g) % 视距朗伯信道输出时域抽头 h 和用于均衡的频域响应 H_f m -log(2) / log(cos(phi_half)); % 朗伯阶数 phi atan(Apd / (2*d)); % 发射角近似 psi phi; % 接收角对称布置时取相等 if psi psi_cone h0 0; % 超出视场角收不到 else h0 ((m1)*Apd) / (2*pi*d^2) * cos(phi)^m * Ts * g * cos(psi); end h [h0; zeros(Ncp, 1)]; % 单径抽头长度不超过 CP H_f fft(h, N); % 与接收端 fft/sqrt(N) 配套 endd是通信距离单位米phi_half是 LED 半功率角常见取值对应 60 度左右psi_cone是 PD 视场角超出就直接判为收不到光这个截断在多灯场景里很关键。Apd是 PD 有效面积Ts是滤光片增益g是聚光器增益这三个一起决定接收光功率的量级。注意h后面补了一串零到Ncp长度。单径信道其实不需要补但补上之后conv和fft的行为与多径场景完全一致后面想把信道换成两径、三径只要改这一个向量主流程一行都不用动。H_f用fft(h, N)而不是除以 N是为了和接收端fft(y, N)/sqrt(N)的归一化对齐——发送端乘了sqrt(N)两边正好抵消。这种归一化约定必须在动手前定死中途改必翻车。注意h0的数量级往往在 1e-5 到 1e-4直接拿去做均衡会让星座点缩成一个点。要么在接收端先按1/h0归一要么在噪声生成时按同样的量级折算别让“数值太小”伪装成“算法有问题”。3.4 接收端去 CP、去直流、迫零均衡与解调function [bits_hat, Yeq] dco_rx(y, N, Ncp, M, H_f) % 接收机去 CP - 去直流 - FFT - 迫零均衡 - QAM 解调 y y(:); y y(Ncp1 : NcpN); % 丢掉 CP取一个完整 OFDM 符号 y y - mean(y); % 去掉直流偏置偏置不携带信息 Y fft(y, N) / sqrt(N); % 与发端 ifft*sqrt(N) 配套 Yeq Y ./ H_f; % 迫零均衡H_f 为信道频响 rx Yeq(2:N/2); % 只取正频率的数据子载波 bits_hat qamdemod(rx, M, OutputType, bit, UnitAveragePower, true); endrx Yeq(2:N/2)这个下标区间必须和发端的X(2:N/2) sym严格对应多一个少一个都会让解调出来的比特整体错位BER 稳定在 0.5 附近而波形看上去一切正常。迫零均衡在 SNR 高的时候够用低 SNR 时噪声被1/H_f放大如果H_f在某些子载波上接近零比如多径造成的深衰落这几路子载波会贡献大量错误这时换成 MMSE 均衡会好一些。跑完一帧之后画两张图是最直观的检查手段均衡前的星座图应该是一个模糊的圆环被信道增益缩放过均衡后应该回到标准的 16 点方格。如果均衡后还是糊的问题在信道估计或者归一化如果均衡后点是对的但 BER 还是很高问题在比特映射或者解调的输入类型上。4. 跑起来参数面板、执行顺序和批量扫点代码看懂只是一半能不能在十分钟内跑出第一张自己的曲线才是这套东西值不值的问题。下面按实际动手的顺序走一遍。4.1 主脚本的参数区改哪几个数就够绝大多数调整都集中在主脚本开头那十几行其余函数不用碰。下面这张表是几个真正会被改到的参数以及改动之后的连锁反应参数典型取值改动影响N64 / 128 / 256越大 PAPR 越高、限幅失真越重、单帧耗时越长NcpN/4 或 N/8小于信道时延扩展会引入 ISI大于则浪费功率M4 / 16 / 64阶数越高同 SNR 下 BER 越差对偏置越敏感bias_ratio6 ~ 8小了下削波大了浪费动态范围led_max均值 2~3 倍标准差小了上削波大了等于不限幅d0.5 ~ 3 米距离翻倍接收光功率按平方衰减第一次跑建议就用N64, Ncp16, M16, bias_ratio7这组参数收敛快、图也好看。确认流程通了之后再把 N 往上加观察 PAPR 和误差平台怎么变化这个对比过程本身比结果更有价值。4.2 一次完整运行的执行顺序打开 matlab把工程目录设为当前路径然后分三步走每步都有明确的产出物出问题能立刻定位到哪一段% 第 1 步跑单帧看波形和星座图确认链路通 params; % 载入参数 [x, h, H_f] dco_tx(N, Ncp, M, bias_ratio, led_max); y conv(x, h); y y(1:NNcp); % 单径下等价于乘一个系数 y y randn(size(y)) * std(y) / 10^(15/20); % 固定 15 dB 看看 [bits_hat, Yeq] dco_rx(y, N, Ncp, M, H_f); plot_const(Yeq, M); % 均衡后星座图应该清清楚楚 % 第 2 步单独验证信道去掉噪声看能不能完美恢复 y_clean conv(x, h); y_clean y_clean(1:NNcp); [b2, ~] dco_rx(y_clean, N, Ncp, M, H_f); fprintf(无噪声误码数%d\n, sum(b2 ~ bits_ref)); % 第 3 步确认无误后跑 SNR 扫描 ber_sweep;第 2 步是最容易被跳过但最有用的一个自检无噪声情况下误码数必须严格为零。只要这里不是零就别急着加噪声跑曲线先回头查共轭镜像的下标、CP 长度、均衡时用的H_f是不是和信道完全一致。这一步几分钟能省掉后面几小时对着一条诡异的曲线发呆。4.3 批量扫 SNR蒙特卡洛点数决定曲线能不能看SNR_dB 0:2:20; ber zeros(size(SNR_dB)); for i 1:numel(SNR_dB) err 0; total 0; for frame 1:300 % 每点 300 帧低于 100 帧曲线全是锯齿 [x, h, H_f] dco_tx(N, Ncp, M, bias_ratio, led_max); y conv(x, h); y y(1:NNcp); y y randn(size(y)) * std(y) / 10^(SNR_dB(i)/20); [bh, ~] dco_rx(y, N, Ncp, M, H_f); err err sum(bh ~ bits_ref); total total numel(bits_ref); end ber(i) err / total; fprintf(SNR%2d dB BER%.3e\n, SNR_dB(i), ber(i)); end semilogy(SNR_dB, ber, o-); grid on; xlabel(SNR (dB)); ylabel(BER); ylim([1e-5 1]);帧数这一项直接决定曲线的可用性。16QAM 在 15 dB 附近 BER 已经在 1e-4 量级要统计到这么多错误至少需要 1e5 到 1e6 个比特也就是几百帧。只跑十帧的人经常会发现“曲线怎么忽高忽低、20 dB 反而比 18 dB 还差”那不是算法问题是统计样本不够随机波动盖过了真实趋势。std(y) / 10^(SNR/20)这种加噪方式假设信号零均值但y里其实还带着偏置std会把偏置算进去。更严谨的写法是对信号交流部分求标准差或者直接按已知噪声方差生成。这一点差异不大但如果你发现实测 BER 和理论曲线整体偏移了两三个 dB先回来查这里。5. 避坑与常见问题BER 恒为 0.5 的五种真实原因仿真类项目最折磨人的地方是“代码不报错结果全错”。下面这五条是我在拆这类工程时反复遇到的每条按现象、原因、解决三步写照着排查基本能覆盖八成的问题。现象一误码率稳定在 0.5 左右怎么调 SNR 都不动。原因基本是比特映射错位。发端的X(2:N/2)和收端的Yeq(2:N/2)看起来对称但中间经过了 IFFT、卷积、FFT只要任何一步的归一化或者索引差一位解调出来的符号序列就整体平移。另一种可能是qammod用了默认的二进制映射而qamdemod用了格雷映射两边的星座点编号对不上。解决方式是做那个“无噪声自检”把噪声设为零打印发端前 10 个比特和收端前 10 个比特一个一个对。对不上就从头顺索引别靠猜。现象二星座图整体偏移或者所有点挤在一条线上。直流没去干净或者去了两次。y - mean(y)这一步在含 CP 的序列上做和在去 CP 之后做结果是不一样的因为 CP 是前一符号尾巴的复制它会影响均值。正确顺序是先截取y(Ncp1:NcpN)再减均值。挤在一条线上则是另一回事通常是把实部虚部搞混了——光通信是实信号IFFT 出来取实部接收端 FFT 之后的正频率那一半本身就是复数这一步没问题问题往往出在有人为了“对称”又取了实部。现象三BER 曲线在 1e-3 附近压不下去出现平台。这是限幅失真造成的误差平台不是代码 bug是参数问题。bias_ratio太小、led_max太紧、或者N太大导致 PAPR 高三者任何一个都能单独造成平台。解决方法是把 3.2 那段削波比例统计跑一遍如果下削波超过 2%就往上加偏置如果上削波超过 2%就抬高led_max或者降低M。也可以直接对比M4和M16的曲线如果 4QAM 没有平台而 16QAM 有那基本可以确定是削波而非信道的问题。现象四曲线锯齿严重相邻 SNR 点之间 BER 跳动超过一个数量级。蒙特卡洛点数不够。低 SNR 时错误多几百帧就够高 SNR 时 BER 到 1e-4同样帧数下只有几个错误比特统计方差极大。解决办法是让帧数随 SNR 自适应增长或者在 SNR 高于 15 dB 之后把帧数提到 2000 以上。另一种省时间的做法是只统计到 BER 1e-4 就停再往下意义不大除非你的应用真的关心那个量级。现象五换成多径信道之后 BER 突然崩到 0.5单径时好好的。两个原因。一是 CP 长度不够信道时延扩展超过Ncp就会产生符号间干扰把Ncp从 N/8 加到 N/4 试试二是加了多径之后H_f在部分子载波上接近零迫零均衡把噪声放大了几个数量级这时候换 MMSE 均衡Yeq Y .* conj(H_f) ./ (abs(H_f).^2 1./snr_lin)能救回来一截。多径场景还有个容易忽略的点h的能量要归一化否则H_f的整体幅度变了SNR 定义也跟着变。提示这五条里的前三条占了实际问题的绝大多数。养成一个习惯——每次改完参数先跑无噪声自检、再看均衡前后星座图两个都过了再跑曲线。这个顺序能把排查时间从几小时压缩到几分钟。6. 二次开发把单链路脚本改成可比对的实验平台这套仿真的初始形态是“一条链路、一组参数、一张图”拿来做课设够用但要做算法对比就不够了。我一般的改造顺序是先把发射机变成可插拔的再把评估流程变成可复现的。第一步把 DCO 和 ACO 拆成两个同签名的函数主脚本用一个字符串选路。ACO-OFDM 的关键差异是只用奇数子载波、不需要直流偏置、直接在下削波到零信号功率全部来自奇子载波频谱效率只有 DCO 的一半但省掉了偏置带来的功率浪费。有了统一接口切换只要改一行scheme aco两条曲线就能画在同一张图上对比。第二步把 LED 的非线性换成可开关的模型。硬限幅太理想真实器件的压缩是渐变的用一段多项式或者 Rapp 模型更接近function y led_model(x, mode, led_max) % 可切换的 LED 非线性模型方便做消融对比 switch mode case clip % 理想硬限幅 y min(max(x, 0), led_max); case rapp % Rapp 模型软压缩 p 3; % 平滑因子越大越接近硬限幅 y x ./ (1 (x./led_max).^(2*p)).^(1/(2*p)); y max(y, 0); case poly % 三阶多项式 a1 1; a3 -0.15 / led_max^2; y a1*x a3*x.^3; y min(max(y, 0), led_max); end endp这个平滑因子决定压缩拐点的陡峭程度取 3 到 5 之间和实测器件的拟合度通常还可以。把mode传进主脚本跑一遍clip和rapp的对比就能量化“理想限幅高估了多少性能”这个差距在论文里经常是半页讨论的素材。第三步把评估流程封装成可复现的函数。参数扫描最容易出的问题是“上次跑出来的图找不到了”所以我会强制把 SNR 范围、帧数、随机种子、削波比例一起写进结果文件改造点改造前改造后调制方案硬编码 DCO字符串选路DCO/ACO 并存LED 模型硬限幅clip/rapp/poly 三选一结果输出只有图图 参数结构体存档随机性每次不同rng(seed)固定可复现第四步加一个 PAPR 的 CCDF 统计。可见光 OFDM 里限幅失真的根源就是高峰均比把 CCDF 曲线和 BER 曲线放在一起看能很直观地解释“为什么 64QAM 的误差平台来得更早”。这段代码不长papr 10*log10(max(abs(x).^2)/mean(abs(x).^2))跑上千个符号统计超出门限的概率就行。验证改造有没有改对我用的方法还是那个最笨的任何新方案上线先用无噪声跑一遍确认误码为零再用单径信道跑一遍确认 BER 曲线和理论 QAM 曲线只差一个常数偏置这个偏置就是限幅损失最后才换多径、换非线性。跳过前面两步直接看多径结果出问题时你根本分不清是信道建的错还是调制改错了。说个我自己的教训。早年做这类对比实验图一多就懒得记参数结果两组曲线的差别到底是算法带来的还是bias_ratio差了 1 造成的完全说不清只能把两周的实验全部重跑一遍。从那以后我每次开新的参数扫描第一件事一定是把参数字典存成 mat 文件和图片放在同一个目录下文件名带上时间戳和关键参数宁可多占几兆磁盘也不留这种说不清的账。希望帮到你。本文还有配套的精品资源点击获取