简介:OFDM通信中的信道模拟是无线通信教学与科研中的常见需求。这套基于MATLAB的OFDM仿真代码面向通信工程专业学生及入门研究者,用于对比分析AWGN信道与瑞利衰落信道下的系统性能。压缩包共收录25个文件,包括8个.m源码脚本、8个.fig仿真图、8个.dat数据文件及1张说明图片,整体大小约252KB,结构紧凑,便于本地运行与二次修改。代码覆盖OFDM基础传输、保护间隔CP/ZP设置、不同保护间隔长度(如3与16)的对比、BER误码率统计及正交性测试等模块,并配有对应仿真图与数据文件,可直接复现实验结果,帮助读者快速理解OFDM关键参数对系统误码性能的影响。已有200人学习下载,适合作为课程设计、毕业设计或科研预研阶段的参考工具。
1. 从 IFFT 到 BER:这套 OFDM 仿真代码在验证什么
OFDM 的教学例程在 MATLAB 里并不少见,但绝大多数要么只画星座图,要么只给 AWGN 理论公式,能把发送端 IFFT、保护间隔插入、信道叠加、接收端 FFT、误码率统计整条链路串成一套可运行脚本的,其实没有那么多。这套代码的价值在于它一次性覆盖了三组对照:CP 与 ZP 两种保护间隔类型、GL3 与 GL16 两种保护间隔长度、AWGN 与瑞利衰落两类信道条件。你不仅可以复现 BER 曲线,还能直接回答课程答辩里常被追问的问题——保护间隔到底在什么时候起作用、起多大作用。对刚开始接触 OFDM 的人,它是能逐行读懂原理的起点;对已经在做链路仿真的人,它则是做参数化改造和批量实验的基底。
2. OFDM 发送端设计:IFFT 调制与 CP/ZP 保护间隔的源码对照
2.1 OFDM_basic.m 的主流程:数据生成到 IFFT 调制
OFDM 的发送端本质上是把高速串行数据流拆到多个低速并行子载波上,MATLAB 里最常见的做法是把调制后的频域符号矩阵直接喂给ifft,沿列方向完成多载波调制。OFDM_basic.m 的主流程大致可以压缩成下面几步:
% OFDM_basic.m 发送端核心流程 M = 4; % 4QAM,每符号2比特 N = 64; % 子载波数 GI_len = 16; % 保护间隔长度,可选 3 或 16 num_sym = 1000; % OFDM 符号个数 data = randi([0 M-1], N, num_sym); % 随机整数序列 mod_sym = qammod(data, M, 'gray', 'InputType', 'integer'); % IFFT:频域符号 -> 时域波形,每列独立完成 ifft_out = ifft(mod_sym, N, 1); % 插入保护间隔,type 可选 'CP' 或 'ZP' tx_signal = guard_interval(ifft_out, GI_len, 'CP');这里randi生成 0 到 M-1 的整数序列,qammod负责 QAM 映射并用 Gray 编码保证相邻星座点只差一比特。ifft沿第一维操作,等同于对每个 OFDM 符号单独做 N 点 IFFT。需要注意的是从 R2017a 开始,qammod要求显式指定'InputType',否则会报参数错误,这是旧脚本迁移到新版 MATLAB 时最常见的报错点。
IFFT 之后得到的是时域波形,如果直接发射,符号之间没有任何隔离,多径信道会直接把相邻符号搅在一起。接下来guard_interval要解决的就是这个问题。
2.2 guard_interval.m 与 remove_GI.m:CP 和 ZP 的实现差异
保护间隔的两种实现,对应着两种工程取舍。CP(循环前缀)把符号末端的几个采样点复制到符号前面,好处是让多径信道的线性卷积变成循环卷积,代价是重复数据占据了一部分发射功率。ZP(补零)在符号后面补一段零,省下了这部分功率,但接收端不能只靠直接剥零就恢复正交性。guard_interval.m 的逻辑非常直接:
function tx_out = guard_interval(signal, GI_len, type) % 在 OFDM 时域符号上加入保护间隔 % signal: N x num_sym 的时域符号矩阵 % type: 'CP' 或 'ZP' switch type case 'CP' % 复制符号尾部 GI_len 个采样到前面 tx_out = [signal(end-GI_len+1:end, :); signal]; case 'ZP' % 在符号末尾补 GI_len 个零 tx_out = [signal; zeros(GI_len, size(signal, 2))]; otherwise error('未知的保护间隔类型'); end endremove_GI.m 在接收端做的是完全对应的逆操作:
function rx_sym = remove_GI(rx_time_signal, GI_len, type) % 去除保护间隔,只保留 N 个有效采样点 switch type case 'CP' rx_sym = rx_time_signal(GI_len+1:end, :); case 'ZP' rx_sym = rx_time_signal(1:end-GI_len, :); otherwise error('未知的保护间隔类型'); end endCP 的接收端处理只有一步:丢掉前 GI_len 个采样,这就是 LTE 和 WiFi 都选 CP 的原因——接收机足够简单。ZP 这样直接切掉也能用,但多径信道带来的符号间拖尾会被当成噪声丢掉;要在 ZP 下拿回完整性能,需要引入重叠相加处理,这一点在第 4 章详细展开。
2.3 GL3 与 GL16:保护间隔长度的频谱效率账
文件名里的 GL3 和 GL16,指的是保护间隔长度分别为 3 个采样点和 16 个采样点。GL 的选择直接影响两个指标:频谱效率和抗多径时延能力。
| 保护间隔长度 | 有效数据占比(N=64) | 最大容忍时延 | 典型适用场景 |
|---|---|---|---|
| GL3 | 64/67 ≈ 95.5% | 3 个采样周期 | AWGN、弱多径 |
| GL16 | 64/80 = 80% | 16 个采样周期 | 瑞利衰落、多径明显 |
GL3 的频谱效率接近 96%,但一旦信道时延扩展超过 3 个采样点,保护间隔就形同虚设;GL16 虽然损失 20% 的吞吐,却能容纳 16 个采样周期的时延扩展。在纯 AWGN 信道下没有多径时延,GL3 和 GL16 的 BER 几乎完全重合,所以AWGN_CP_GL3.fig和AWGN_CP_GL16.fig两张图里的曲线会叠在一起;真正拉开差距的是瑞利衰落信道下的CH_CP_GL3与CH_CP_GL16对比。这套代码最有教学价值的地方,正在于它把两个“看起来应该差不多”的参数放到一起,用仿真结果告诉你什么时候差不多、什么时候差很多。
提示:文件名中 CH 表示瑞利衰落信道,AWGN 表示高斯信道,CP/ZP 是保护间隔类型,GL 后数字是采样点数。这套命名规则本身就是实验记录范本。
3. AWGN 信道下的 BER 仿真:理论公式、噪声功率换算与数据解析
3.1 Q.m 与 ber_QAM.m:理论误码率从哪里来
把仿真结果和理论基准对齐,需要 Q 函数和 QAM 的理论误码率公式。Q.m 实现的是高斯 Q 函数,也就是标准正态分布的右尾概率:
function y = Q(x) % Q(x) = P(Z > x),Z 为标准正态随机变量 % 用 erfc 展开是数值上最稳定的实现 y = 0.5 * erfc(x / sqrt(2)); end直接用数值积分定义算 Q 函数,在自变量较大时误差会非常明显,用erfc的恒等式转换是最稳妥的写法。有了 Q 函数,M-QAM 在 AWGN 下的理论 BER 可以用标准公式计算,ber_QAM.m 的典型实现是:
function Pe = ber_QAM(EbN0_dB, M) % M-QAM 在 AWGN 下的理论误比特率(格雷编码近似) k = log2(M); EbN0 = 10.^(EbN0_dB / 10); % 符号错误率近似公式 Ps = 4 * (1 - 1/sqrt(M)) * Q(sqrt(3 * k * EbN0 / (M - 1))); Pe = Ps / k; % 格雷编码下 BER 约等于 SER / k end这个公式适用于矩形 M-QAM、M 为 2 的偶数次幂的情况,星座点采用格雷编码。输入EbN0_dB是分贝值,函数内部先换算成线性比值。系数 4 来自星座边缘点错误概率的加权,对 16QAM 以上会略微偏保守,但工程误差在可接受范围。不同调制阶数的理论曲线差异,可以直接用这张表做初步判断:
| 调制方式 | 理论 BER 近似公式 | 达到 10^-3 所需 EbN0 |
|---|---|---|
| QPSK | Q(sqrt(2·EbN0)) | 约 6.8 dB |
| 16QAM | (3/4)·Q(sqrt(0.8·EbN0)) | 约 12.1 dB |
| 64QAM | (7/12)·Q(sqrt(0.43·EbN0)) | 约 17.6 dB |
3.2 AWGN 仿真链路里的 EbN0 到噪声功率换算
AWGN 信道模拟本身只是叠加复高斯噪声,但 EbN0 和实际噪声功率之间的换算才是最容易出错的地方。噪声加在整个时域信号上,而 EbN0 是每比特能量与噪声谱密度之比,中间隔着保护间隔开销、调制阶数等条件:
% AWGN 信道叠加噪声的功率换算 Es = mean(abs(tx_signal(:)).^2); % 时域符号平均功率 Eb = Es / (log2(M) * N) * (N + GI_len) / N; % 每比特能量(含GI开销) N0 = Eb / (10^(EbN0_dB / 10)); % 噪声谱密度 noise_power = N0 * (N + GI_len) / N; % 折算到时域全块 rx_signal = tx_signal + sqrt(noise_power/2) * ... (randn(size(tx_signal)) + 1j * randn(size(tx_signal)));这里的randn生成实部和虚部独立的复高斯噪声,sqrt(noise_power/2)保证复噪声总功率等于noise_power。EbN0 折算中容易被忽略的是(N+GI_len)/N这一项,不把保护间隔开销算进去,仿真曲线会和理论曲线错开约10*log10(80/64) ≈ 0.97 dB。如果仿真点全部平行偏移理论曲线,先查这里。
3.3 读取 .dat 结果文件与 plot_ber.m 绘图
项目里的.dat文件是保存下来的 BER 仿真结果,比如OFDM_BER_AWGN_CP_GL16.dat。这类文件在 MATLAB 里直接用load读取:
data = load('OFDM_BER_AWGN_CP_GL16.dat'); EbN0 = data(:, 1); % 第一列是信噪比 BER = data(:, 2); % 第二列是对应误码率plot_ber.m 负责把不同配置的 BER 曲线画到同一张坐标下。BER 跨越多个数量级,必须用半对数坐标semilogy,线性坐标下 10^-3 和 10^-5 的差异根本看不出:
semilogy(EbN0, BER, 'b-o', 'LineWidth', 1.2); hold on; grid on; xlabel('E_b/N_0 (dB)'); ylabel('误比特率 (BER)'); legend('仿真值', 'Location', 'southwest');画图时如果仿真曲线在低 EbN0 区间贴着理论线、到高 EbN0 区间开始偏离,优先检查前面说的噪声功率折算;如果从头到尾对不上,则检查qammod的 Gray 编码参数是否生效。
4. 瑞利衰落信道模拟:多径时延如何改变 CP 与 ZP 的选择
4.1 用抽头延迟线模型模拟频率选择性瑞利衰落
瑞利信道在基带仿真里最常用的模型是抽头延迟线,每条路径对应一个时延和独立的复高斯增益,幅度服从瑞利分布。当多径时延超过保护间隔长度,前一个符号的拖尾会落入当前符号的 FFT 窗口,造成符号间干扰。下面是典型 3 径信道建模:
% 3径瑞利信道建模:时延单位是采样点 path_delay = [0, 1, 3]; % 相对时延,第3径刚好等于GL3 path_power = [0, -3, -6]; % 路径功率(dB) Lh = max(path_delay) + 1; h = zeros(1, Lh); for p = 1:length(path_delay) h(path_delay(p) + 1) = 10^(path_power(p) / 20) * ... (randn + 1j * randn) / sqrt(2); end % 时域线性卷积模拟多径传播 rx_signal_chan = filter(h, 1, tx_signal); % 之后再按第3章公式叠加 AWGN10^(path_power/20)把 dB 单位功率换算成幅度增益,(randn + 1j*randn)/sqrt(2)保证每径增益的模服从瑞利分布、相位均匀分布。第三径时延为 3 个采样点,恰好等于 GL3 的容忍极限——搭配 GL3 时信道时延刚好在边界上,搭配 GL16 则完全在保护范围内。这套参数设计不是巧合,它就是用来制造“GL3 失败、GL16 成功”的对照实验。
4.2 频域选择性衰落对子载波的影响与 ZP 的重叠相加处理
OFDM 把宽带信道切成多个窄带子信道,每个子信道上的衰落近似平坦,但不同子载波经历的信道增益 H(k) 各不相同。有的子载波被深度衰减,有的几乎不受影响。CP 让多径线性卷积变成循环卷积,接收端 FFT 之后每个子载波只乘一个复数增益,均衡只需一次复数除法。ZP 没有这个性质,直接剥掉补零再做 FFT 会留下残余的线性卷积效应,因此需要重叠相加把信道重新“折叠”回循环卷积:
function rx_fft_ready = zp_overlap_add(rx_time, N, GI_len) % ZP 接收端的重叠相加处理 % rx_time: 接收时域符号矩阵,每列长度 N+GI_len % 返回: 可直接做 FFT 的有效符号矩阵 num_sym = size(rx_time, 2); rx_fft_ready = zeros(N, num_sym); tail = zeros(GI_len, 1); for m = 1:num_sym sym = rx_time(:, m); % 当前符号(N+GI_len 点) seg = sym(1:N) + [tail; zeros(N - GI_len, 1)]; rx_fft_ready(:, m) = seg; tail = sym(N+1:N+GI_len); % 保存尾部给下一个符号 end end核心思想是:上一段符号的拖尾经过多径信道后会叠加到当前符号头部,把它加回来之后,等效于把线性卷积补偿成循环卷积。之后再做 FFT,ZP 就和 CP 一样只剩下每个子载波的复增益。代价是接收机复杂度上升,这也是 ZP 在实际系统里不如 CP 普及的原因。
4.3 从 CH 数据文件看误差平台
对比OFDM_BER_CH_CP_GL3.dat和OFDM_BER_CH_CP_GL16.dat两组数据,瑞利信道下的差异非常直观。GL3 的 BER 在高 EbN0 区间进入误差平台,继续增加信噪比也降不下去,因为此时误码的主导因素不再是高斯噪声,而是多径时延超过保护间隔后产生的符号间干扰。GL16 因为容纳了 3 径信道的所有时延,BER 曲线随 EbN0 持续下降。
| 仿真配置 | 低 EbN0 区间 | 高 EbN0 区间 |
|---|---|---|
| CH_CP_GL3 | 与 GL16 差异小 | 出现误差平台 |
| CH_CP_GL16 | 与 GL3 差异小 | BER 持续下降 |
| CH_ZP_GL3 | 与 GL16 差异小 | 误差平台更高(未做重叠相加时) |
提示:看到 BER 曲线在高 EbN0 处不再下降,第一步查信道最大时延是否超过保护间隔,第二步查 IFFT/FFT 的归一化因子是否一致。这两个原因占了误差平台问题的大多数。
5. 进阶:先用 test_orthogonality 自检,再参数化改造仿真链路
5.1 test_orthogonality.m 在验证什么
OFDM 的根基是子载波正交性。实现上的任何错误——IFFT 归一化不对、数据维度不匹配、保护间隔类型和接收端不一致——最终都会破坏正交性。test_orthogonality.m 的做法很直接:用单位矩阵作为频域输入,经过 IFFT 再 FFT,观察输出是否能恢复为单位阵:
N = 64; X = eye(N); % 每列是一个单位冲激的频域表示 x = ifft(X, N, 1); % IFFT 到时域 Y = fft(x, N, 1); % 再 FFT 回来 err = max(abs(Y(:) - X(:))); fprintf('最大正交性误差:%.2e\n', err);误差在 1e-14 量级说明 IFFT/FFT 对完全对称;如果到 1e-8 甚至更大,基本可以断定归一化因子或矩阵维度出了问题。把这一小段验证放在任何 BER 仿真之前,能省掉大量盲调参数的时间。
5.2 把单文件脚本改成统一调用入口
原始脚本的参数散落在各行,换一组配置就得改代码。工程上常见的做法是封装成一个带参函数,把所有 OFDM 链路参数作为输入:
function ber = run_ofdm_link(N, GI_len, GI_type, channel_type, EbN0_dB) % 统一 OFDM 链路仿真入口 % N: 子载波数 % GI_len: 保护间隔长度(采样点) % GI_type: 'CP' 或 'ZP' % channel_type: 'AWGN' 或 'Rayleigh' % EbN0_dB: 信噪比数组 % ber: 对应 BER 输出数组 % 内部顺序:调制 -> IFFT -> 加GI -> 过信道 -> 去GI -> FFT -> 解调 -> 统计BER end参数化之后,批量跑完下载包里出现的所有文件组合就只是一层循环:
channels = {'AWGN', 'Rayleigh'}; gi_types = {'CP', 'ZP'}; gl_values = [3, 16]; for c = 1:2 for g = 1:2 for l = 1:2 ber = run_ofdm_link(64, gl_values(l), gi_types{g}, channels{c}, 0:2:20); fname = sprintf('OFDM_BER_%s_%s_GL%d.dat', ... channels{c}, gi_types{g}, gl_values(l)); save(fname, 'ber', '-ascii'); end end end命名沿用OFDM_BER_信道_保护间隔_GL长度.dat的规则,和下载包里的文件保持同一体系。每次实验的结果都被文件系统完整记录,事后分析 CP 与 ZP 的差异、GL3 与 GL16 的差异,都只是按文件名搜索数据的问题。
注意:
save用-ascii选项时只写数值矩阵不存变量名,读取端load拿到的就是纯数组,与原有的.dat文件格式完全兼容。
本文还有配套的精品资源,点击获取