OFDM链路级仿真:循环前缀与误码率实战解析
2026/9/14 13:10:12 网站建设 项目流程

简介:面向MATLAB初学者与无线通信学习者的OFDM链路级仿真程序包,覆盖从发射端调制、IFFT变换、加循环前缀,到接收端解调、信道估计与均衡的完整流程。包内共12个m文件,压缩后仅7KB,包含主程序、调制解调、加窗、参数设置等独立脚本,结构简洁,便于逐段阅读和修改参数,适合用直观方式理解OFDM核心原理。已有255人学习下载。通过调整子载波数量、循环前缀长度、调制方式或信噪比,可观察对误码性能与频谱效率的影响;仿真中还可选用AWGN、瑞利衰落等信道模型,帮助对比不同传输环境下的系统表现。对正在学习4G/5G物理层、准备课程设计或希望快速搭建可运行OFDM链路的读者来说,这份简洁代码是一个清晰的实践起点,既能辅助理论理解,也能为进一步研究提供参考。

1. 一套能跑的OFDM链路级仿真程序,到底拆开看是什么

日常要验证一个OFDM想法,比如循环前缀长度对误码率的影响,最快的方式不是翻标准文档,而是手头有一套参数外置、模块齐全的仿真程序。这个压缩包里没有几十个文件夹,只有十来个.m文件:setting.m集中管参数,OFDM_Tx.m和OFDM_dem.m分别对应发送与解调,pre_suf.m做循环前缀,Window.m与rcoswindow.m负责加窗,main.m把整个链路和数据统计串起来。它解决的是那种“原理背熟了但代码不知道从哪看起”的问题,适合正在学OFDM调制解调、准备做链路级对比实验的工程师。下文以这套程序为骨架,把发送链路、窗函数、信道与解调依次拆开,最后落到参数扫描和误码率曲线上的实战技巧。

2. 发送端链路:从比特流到OFDM符号的完整过程

2.1 setting.m里的参数设计决定仿真上限

打开压缩包第一个该看的是setting.m。这个文件虽然短,但所有仿真变量都集中在这里。调子载波数、改CP长度、换调制阶数,都改这一个文件,main.m和OFDM_Tx.m里只要读全局变量或调用函数入参即可。

参数典型值作用
N_sub64子载波总数,决定FFT点数
cp_len16循环前缀采样点数,需大于信道最大多径时延
M4 / 16 / 64调制阶数,4=QPSK
n_symbol1000一帧内OFDM符号数
snr_list0:2:20扫描的信噪比范围,单位 dB
h_path[1, 0.3, 0.1]多径信道复增益
% setting.m 典型参数集合 N_sub = 64; % 子载波数,OFDM符号内有效数据子载波 cp_len = 16; % 循环前缀长度,单位:采样点 M = 4; % 4 = QPSK,16 = 16QAM n_symbol = 1000; % 一个仿真帧内的OFDM符号数 snr_list = 0:2:20; % 扫描的信噪比范围,单位 dB h_path = [1, 0.3*exp(-1j*pi/3), 0.1*exp(-1j*pi/5)]; % 3径复信道 mod_order = log2(M); % 每个子载波承载的比特数 fft_size = N_sub; % 未做过采样时FFT点数等于子载波数

这里有几个容易忽略的点。cp_len的取值不能只看方便,它需要大于等于多径信道的最大时延扩展,否则前一个符号的拖尾会落到后一个符号里,形成符号间干扰。N_sub决定IFFT的点数,如果后面用来映射的频域符号长度不是N_sub,在OFDM_Tx.m里就会直接报维度错误。调制阶数M直接影响每个OFDM符号能传的比特数,M=4时每子载波2bit,M=16时每子载波4bit,链路级仿真跑完会看到误码率曲线随M增大整体上移。

2.2 bi2M.m:比特流到正交幅相调制符号

OFDM发送端的第一步是完成串行比特到频域符号的映射。bi2M.m在这套程序里承担这个角色,名字的意思是bit to M-ary symbol。

function sym_out = bi2M(bits_in, M) k = log2(M); % 每个调制符号对应的比特数 bits_mat = reshape(bits_in, k, []).'; dec_idx = bi2de(bits_mat, 'left-msb'); % 比特组转十进制 if M == 4 sym_out = pskmod(dec_idx, M, 0, 'gray'); % QPSK else sym_out = qammod(dec_idx, M, 'gray'); % 16QAM/64QAM end sym_out = sym_out(:).'; end

先按k个比特为一组,用bi2de把二进制向量转成十进制索引,再通过pskmod或qammod映射到星座点。pskmod里的第4个参数是初始相位,0就表示星座点从实轴开始,'gray'开启格雷映射,使相邻星座点只有1bit差异。这样即使发生误判,大概率只错1bit,从BER曲线看会更接近理论值。如果你把M改成16,qammod会自动按平方星座排布;但要注意输入比特总数必须能被log2(M)整除,否则reshape会报错。

这套程序里还有sig_OFDM.m,常见作用是生成一个未经IFFT的参考频域矩阵,用于和OFDM_Tx.m的中间结果对照,检测映射过程是否引入额外相偏。

2.3 pre_suf.m:循环前缀不只是补一段尾巴

在OFDM里加循环前缀,本质上是把符号尾部的样本复制到开头。pre_suf.m这个命名是prefix-suffix的缩写,既能在发送端加CP,也能在接收端把CP去掉。

function out = pre_suf(x, cp_len, mode) if strcmp(mode, 'add') out = [x(end-cp_len+1:end), x]; % 尾部复制到首部 else out = x(cp_len+1:end); % 丢弃循环前缀 end end

发送端调用add模式后,IFFT输出的N点序列变成N+cp_len点。接收端必须对称地丢弃前cp_len点,再送入FFT。这样处理的价值在于:经过多径信道后的线性卷积会被近似成循环卷积,而循环卷积对应频域上每个子载波的乘积,接收端就能用单抽头均衡器恢复信号。注意丢弃CP必须在同步完成之后,如果符号起点偏了,丢CP的位置就错了,后面FFT得到的星座图会整个旋转。

2.4 OFDM_Tx.m:IFFT之前的子载波映射和窗函数叠加

OFDM_Tx.m把前面的步骤串起来,完成频域符号到空口波形的变换。中间有两个细节:一是直流子载波通常不携带数据,二是IFFT输出经过加窗后才能让带外频谱更干净。

function tx_signal = OFDM_Tx(data_bits, setting) % 1. 比特 -> 频域调制符号 mod_syms = bi2M(data_bits, setting.M); % 2. 子载波映射,长度为fft_size map_syms = zeros(setting.fft_size, 1); n_active = length(mod_syms); % 有效数据子载波数 left = floor((n_active - 1) / 2); map_syms(2:left+1) = mod_syms(1:left); % 正频部分 map_syms(end-left+1:end) = mod_syms(left+1:end); % 分布在尾部 % 3. IFFT得到时域基带符号 ifft_out = ifft(ifftshift(map_syms), setting.fft_size) * sqrt(setting.fft_size); % 4. 加循环前缀 cp_out = pre_suf(ifft_out, setting.cp_len, 'add'); % 5. 乘上升余弦窗抑制带外泄漏 win = rcoswindow(setting.fft_size + setting.cp_len, 0.22); tx_signal = cp_out .* win; end

这里的ifftshift是必须的,它把负频率子载波挪到IFFT输入的正确位置。乘上sqrt(fft_size)是为了让能量不随IFFT点数变化,方便和理论误码率对比。rcoswindow生成的是升余弦窗,滚降系数0.22是5G NR里常见的数字。这样加窗后,符号头尾被平滑,频域旁瓣下降,代价是时域波形不再是严格的矩形OFDM符号,导致相邻符号边缘有交叠。

Window.m和rcoswindow.m在这套程序里很容易混。我的判断是,rcoswindow.m生成发送端用的升余弦滚降窗,而Window.m提供hann或hamming等一般窗,供后面频谱分析时使用。它们名字接近,但一个用于“成形”,一个用于“观测”。

3. 信道与接收端解调:从H文件到OFDM_dem.m的还原过程

3.1 H文件与多径信道模型在链路中的角色

压缩包里出现了一个没有后缀的H,我理解它是信道冲激响应的保存结果,可能是.mat,也可能是一个定义信道响应的脚本。OFDM仿真里,H常见有两种形态:一种是时域冲激响应h,长度等于多径抽头数;另一种是频域响应矩阵,长度等于子载波数,等于h的FFT。OFDM_dem.m里用哪个取决于它怎么和发送信号做卷积。

% 生成多径信道时域冲激响应,并转换到频域 h = [1, 0.3*exp(-1j*pi/3), 0.1*exp(-1j*pi/5)]; H_freq = fft(h, setting.fft_size); H_freq = H_freq(:).'; % 通过卷积模拟多径传播,注意加噪声在符号尾部 rx_signal = conv(tx_signal, h); % 实际仿真中应将rx_signal裁剪到与tx_signal等长,再进行接收处理

多径效应体现在每个到达路径有不同的幅度、时延和相位。链路上把发送信号和h做conv,等于制造了频率选择性衰落。但OFDM的CP会让线性卷积退化为循环卷积,因此在频域上每个子载波只受一个复系数H_freq(k)影响,这就是后面均衡的依据。实际仿真里,更重要的一步是让rx_signal和tx_signal等长,否则去CP时长度会对不上。

3.2 OFDM_dem.m:去CP、FFT和单抽头均衡

接收端的OFDM_dem.m与OFDM_Tx.m对称,顺序是去CP、FFT、逆映射、均衡、解调。因为有了CP,均衡只需要做一次复数除法。

function rx_bits = OFDM_dem(rx_signal, H_freq, setting) % 1. 丢弃循环前缀 cp_removed = pre_suf(rx_signal, setting.cp_len, 'remove'); % 2. FFT回到频域 rx_fft = fft(cp_removed, setting.fft_size) / sqrt(setting.fft_size); symbol_est = fftshift(rx_fft); symbol_est = symbol_est(:).'; % 3. 提取数据子载波并做单抽头均衡 n_active = setting.N_data; left = floor((n_active - 1) / 2); data_idx = [2:left+1, setting.fft_size-left+1:setting.fft_size]; rx_syms = symbol_est(data_idx); ch_est = H_freq(data_idx); % 每个子载波上的信道响应 eq_syms = rx_syms ./ ch_est; % 单抽头迫零均衡 % 4. 硬判决解调,这里以QPSK为例 rx_bits = pskdemod(eq_syms, setting.M, 0, 'gray'); rx_bits = de2bi(rx_bits, log2(setting.M), 'left-msb').'; rx_bits = rx_bits(:).'; end

fft输出除以sqrt(fft_size)是为了和发送端的ifft增益对齐,不这样做星座点的幅度会整体变化,均衡后误码率计算也会偏差。均衡用的H_freq如果是理想信道响应,这条链路实际上属于“理想信道估计”的范畴;如果想更真实,应该在数据子载波里插入导频,用LS估计出H_freq再插值。

这套程序里的OFDM.m与OFDM_dem.m的区别,我的理解是OFDM.m是一个更高层的封装函数,输入参数和输出参数都面向main.m;而OFDM_dem.m更纯粹的写解调算法本身。跑实验时如果只想调整均衡策略,改OFDM_dem.m就够了,不用碰外面的循环。

3.3 载波频率偏差与OFDM同步问题的仿真观察

在AWGN或理想同步下,OFDM仿真会太顺利。实际系统中,接收端本振和发射端本振不可能完全一致,一个很小的频率偏差就会破坏子载波正交性。

% 模拟CFO对单符号的影响 delta_f = 0.02; % 归一化频偏,单位是子载波间隔 n = 0:length(cp_removed)-1; cfo_phase = exp(1j*2*pi*delta_f*n/setting.fft_size).'; rx_cfo = cp_removed .* cfo_phase;

这个0.02看起来不大,但它会导致星座点围绕原点旋转,且子载波间干扰会在BER曲线底部形成平台,简单增大发送功率也无法消除。所以链路级仿真里如果发现BER曲线在高SNR下不下降,首先要怀疑不是噪声问题,而是同步偏差。OFDM的同步通常分两步:先用训练序列做符号定时对齐,再用循环前缀相关法或导频相位做频偏粗估计和细估计。这套程序里如果把同步模块替换成理想同步,main.m里的误码率结果可以作为其他算法的上限。

4. 窗函数与频谱成形:rcoswindow.m、Window.m和f_winNover.m 的三层关系

4.1 IFFT之后为什么要对OFDM符号边缘做滚降

OFDM信号在时域上由多个矩形子载波脉冲叠加,矩形脉冲的频谱是sinc函数,旁瓣衰减只有每倍频程6dB。如果不加窗,OFDM信号的带外辐射会干扰邻频段,4G/5G里到达-25dB以下都有可能。所以链路仿真里不能只看误码率,还要看发射信号的频谱成形效果。

4.2 rcoswindow.m 和 Window.m 的时域实现差别

为了降低带外泄漏,对每个OFDM符号在时域上乘一个滚降窗。rcoswindow.m实现的是升余弦窗,它的时域表达式可以写成主瓣与余弦滚降项的乘积,滚降系数beta越大,边缘越平滑,带外抑制越好,但有效保护间隔实际缩短。

function w = rcoswindow(N, beta) % N: 窗口长度, beta: 滚降系数 0~1 n = (0:N-1) - (N-1)/2; denom = 1 - (2 * beta * n / N).^2 + 1e-8; w = sin(pi * n / N) ./ (pi * n / N) .* cos(pi * beta * n / N) ./ denom; w(n == 0) = 1; end

这段代码是用频域升余弦滤波器的时域冲激响应来生成窗。denom里加1e-8是为了防止分母为0时产生NaN。beta=0.22时,符号边缘在大约15%长度的过渡带内平滑过渡。相比矩形窗,旁瓣可以从-13dB降到-30dB以下,但代价是循环前缀的有效长度损失了一部分,所以CP还要再留出一点富余量。

Window.m一般是通用窗函数入口,接收端频谱分析用它,比如hann(256),作用是对数据加窗防止截断泄漏。二者的差别,一张表看得更清楚。

函数使用侧窗的类型目的
rcoswindow.m发送链路升余弦窗OFDM符号成形,抑制带外辐射
Window.m接收/分析链路hann、hamming等频谱观测时抑制截断效应
f_winNover.m频域分析任意窗 + 重叠平均估计OFDM信号功率谱密度
f_NwinNover.m频域分析N段窗 + 重叠对比不同窗长/重叠率下的谱分辨率

这样分完后,发送端和接收端的“窗”职责就清楚了。

4.3 f_winNover.m 与 f_NwinNover.m:用重叠平均把带外特性看成曲线

这两个文件命名里都有Nover,我理解是No Overlap或N-overlap的简写。无论哪种,它们在仿真里的角色都是对发射信号做功率谱密度估计,用于观察加窗前后的频谱变化。

function [Pxx, freq] = f_winNover(sig, fft_size, nover, fs) % sig: 时域OFDM信号, fft_size: 每段FFT点数, nover: 重叠点数 hop = fft_size - nover; n_seg = floor((length(sig) - nover) / hop); win = hann(fft_size).'; Pxx = zeros(1, fft_size); for k = 0:n_seg-1 idx = k*hop + 1 : k*hop + fft_size; seg = sig(idx) .* win; Pxx = Pxx + abs(fft(seg, fft_size)).^2; end Pxx = Pxx / n_seg; freq = (0:fft_size-1) * fs / fft_size; end

nover控制相邻两段数据的重叠程度,比如fft_size=256、nover=192,表示每段滑动64点。重叠率越高,谱曲线越平滑,但计算量成倍增加。调用它之前,把前面OFDM_Tx输出的含CP符号连成一长串再传进来,就可以从频谱图上看到加窗后旁瓣下降的效果。f_NwinNover.m通常是它的变体,把多段窗长的平均结果再归一化,用于比较不同窗参数。注意功率谱密度估计的纵轴要除以窗能量和采样率,否则绝对值不反映真实带外功率,只适合看相对变化。

这里有个使用误区:有人会把发送端的rcoswindow.m和接收端的Window.m混用,导致星座点幅度被二次加权,解调后BER飘高。记住发送端只需要滚降一次,接收端如果要加窗,只能用于频域分析,不能进OFDM_dem的解调路径。

5. 从main.m跑通误码率仿真到参数扫描排错

5.1 main.m主循环:SNR扫描与BER统计

main.m是整套程序的入口,不管前面的发送、解调做成函数还是脚本,主循环一定是在不同SNR下重复“发射-过信道-加噪声-解调-统计错误比特”。下面这个骨架可以直接替换原工程里的主循环。

% main.m 骨架 settings = setting; ber_curve = zeros(size(settings.snr_list)); for idx_snr = 1:length(settings.snr_list) snr = settings.snr_list(idx_snr); tx_bits = randi([0 1], 1, settings.n_symbol * settings.N_data * settings.mod_order); tx_sig = OFDM_Tx(tx_bits, settings); rx_sig = filter(settings.h_path, 1, tx_sig); rx_sig = awgn(rx_sig, snr, 'measured'); rx_bits = OFDM_dem(rx_sig, settings.H_freq, settings); total_bit = length(tx_bits); total_err = sum(tx_bits ~= rx_bits(1:total_bit)); ber_curve(idx_snr) = total_err / total_bit; end semilogy(settings.snr_list, ber_curve, '-o');

awgn的'measured'选项会先测量信号功率,再按设置的SNR叠加噪声,这样避免因窗函数和滤波器带来的幅度缩放导致SNR失准。filter比conv更合适,因为conv会在处理时变信道时长度失控。统计误码率时,只取rx_bits前total_bit长度,防止解调函数输出长度因同步边界漂移而多出尾巴。

5.2 参数修改的连锁反应:子载波数、CP长度和调制阶数

子载波数从64改成256,IFFT点数跟着变,此时必须同步检查OFDM_Tx.m里map_syms的长度。CP长度改长,符号总长度变长,同样SNR下每比特能量会被拉低,需要重新用'measured'标定才能保证比较公平。调制阶数从QPSK换成16QAM,每符号比特数翻倍,同样帧长的总比特数也要翻倍,否则reshape不合法。我的习惯是准备一张参数对照表,把fft_size、N_data、导频位置、循环前缀逐项核对。链路级仿真里90%的报错都是这类长度不匹配,而不是算法本身的问题。

5.3 三个容易翻车的地方

第一,去CP后长度不等于fft_size。发送端用conv滤波后没有裁剪,接收端pre_suf会多丢几个点,FFT直接报错。解决方法是把rx_signal截断到与tx_signal等长,或用filter代替conv。

第二,星座图整体旋转。常见原因是ifftshift和fftshift使用不对称,或者均衡时把H_freq取错了索引。调试时把eq_syms用scatter画出来,QPSK应该落在四个象限中心;如果整体旋转45度,检查pskmod初始相位是否和pskdemod一致。

第三,BER曲线出现平台。先排除同步和直流子载波偏移,再把SNR推到很高区间观察。平台如果依然存在,大概率是载波频偏或窗函数导致循环前缀有效长度不足。把CP长度增大到最大多径时延的1.5倍重跑,平台通常能定位问题。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询