ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MATLAB实现OFDM可见光通信系统仿真:DCO链路与BER曲线

MATLAB实现OFDM可见光通信系统仿真:DCO链路与BER曲线

简介:这是一套面向可见光通信与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/DD),LED 发的是光功率,光电探测器收到的是光强,光强不可能为负。所以发射端的时域波形必须是实数且非负,这两个约束分别由 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_f
ber_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/2+2:N) = conj(flipud(sym)); % 负频率做共轭镜像 % X(1) 与 X(N/2+1) 保持为 0,不携带任何信息 x = ifft(X, N) * sqrt(N); % 乘 sqrt(N) 抵消 ifft 的 1/N x = [x(N-Ncp+1:N); x]; % 循环前缀 x_dc = x + bias_ratio * std(x); % 抬到非负 x_out = min(max(x_dc, 0), led_max); % 双端限幅,模拟 LED 动态范围 end

N取 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、N=64 的情况下,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 = ((m+1)*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) 配套 end

d是通信距离,单位米;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(Ncp+1 : Ncp+N); % 丢掉 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); end

rx = 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 米距离翻倍,接收光功率按平方衰减

第一次跑建议就用N=64, Ncp=16, M=16, bias_ratio=7,这组参数收敛快、图也好看。确认流程通了之后再把 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:N+Ncp); % 单径下等价于乘一个系数 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:N+Ncp); [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:N+Ncp); 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(Ncp+1:Ncp+N)再减均值。挤在一条线上则是另一回事,通常是把实部虚部搞混了——光通信是实信号,IFFT 出来取实部,接收端 FFT 之后的正频率那一半本身就是复数,这一步没问题,问题往往出在有人为了“对称”又取了实部。

现象三:BER 曲线在 1e-3 附近压不下去,出现平台。这是限幅失真造成的误差平台,不是代码 bug,是参数问题。bias_ratio太小、led_max太紧、或者N太大导致 PAPR 高,三者任何一个都能单独造成平台。解决方法是把 3.2 那段削波比例统计跑一遍,如果下削波超过 2%,就往上加偏置;如果上削波超过 2%,就抬高led_max或者降低M。也可以直接对比M=4和M=16的曲线,如果 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 end

p这个平滑因子决定压缩拐点的陡峭程度,取 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 文件和图片放在同一个目录下,文件名带上时间戳和关键参数,宁可多占几兆磁盘,也不留这种说不清的账。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进