
简介面向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调制阶数4QPSKn_symbol1000一帧内OFDM符号数snr_list0:2:20扫描的信噪比范围单位 dBh_path[1, 0.3, 0.1]多径信道复增益% setting.m 典型参数集合 N_sub 64; % 子载波数OFDM符号内有效数据子载波 cp_len 16; % 循环前缀长度单位采样点 M 4; % 4 QPSK16 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符号能传的比特数M4时每子载波2bitM16时每子载波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改成16qammod会自动按平方星座排布但要注意输入比特总数必须能被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_len1:end), x]; % 尾部复制到首部 else out x(cp_len1:end); % 丢弃循环前缀 end end发送端调用add模式后IFFT输出的N点序列变成Ncp_len点。接收端必须对称地丢弃前cp_len点再送入FFT。这样处理的价值在于经过多径信道后的线性卷积会被近似成循环卷积而循环卷积对应频域上每个子载波的乘积接收端就能用单抽头均衡器恢复信号。注意丢弃CP必须在同步完成之后如果符号起点偏了丢CP的位置就错了后面FFT得到的星座图会整个旋转。2.4 OFDM_Tx.mIFFT之前的子载波映射和窗函数叠加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:left1) mod_syms(1:left); % 正频部分 map_syms(end-left1:end) mod_syms(left1: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:left1, setting.fft_size-left1: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(:).; endfft输出除以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。beta0.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; endnover控制相邻两段数据的重叠程度比如fft_size256、nover192表示每段滑动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改成256IFFT点数跟着变此时必须同步检查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倍重跑平台通常能定位问题。本文还有配套的精品资源点击获取