简介:这套Matlab仿真程序聚焦多径衰落信道下的OFDM系统,面向无线通信方向的学生和入门研究者,解决从理论公式到仿真验证的落地问题。程序完整覆盖发射、导频插入、星座映射与解调链路,包含循环前缀、FFT/IFFT等关键环节,可对比瑞利衰落与莱斯衰落信道下的传输差异,直观观察深衰落对误码率和星座图的影响。资源共4个m文件,压缩包仅2KB,文件结构清晰,发射、导频、星座和解调模块相互独立,方便按需修改子载波数量、CP长度、信噪比等核心参数。已有2639人学习下载。借助该仿真,读者可系统理解OFDM抗多径衰落的三种策略:时间分集、频率分集和循环前缀防ISI,并通过误码率曲线评估系统性能。无论是课程设计、毕业设计还是技术预研,都能作为快速上手和深入学习的实用参考。
1. 多径衰落信道下的 OFDM 仿真:一份能直接跑通的 MATLAB 程序
做 OFDM 系统仿真的人,十有八九绕不开多径衰落信道。这套 MATLAB 仿真程序把“发送端 QPSK 映射 → IFFT 加循环前缀 → 多径信道 → 接收端 FFT → 信道估计均衡 → 误码率统计”整条链路都串起来了,参数全部集中在一个文件里改,跑一次就能看到 BER 曲线和星座图。它适合两类人:一类是刚接触 OFDM 想把手动链路搞明白的学生,另一类是已经做过 AWGN 仿真、现在想验证多径信道下循环前缀和导频设计到底怎么影响性能的工程师。我拆这份资源时最直接的感受是:代码量不大,但每一段都能对上教科书里的公式,改参数后的现象也和理论对得上。
2. 从发射到接收:OFDM 仿真链路与多径信道模型
2.1 发送端:调制、IFFT 与循环前缀
OFDM 的核心思想是把一个宽频带分成 N 个正交子载波,串行高速数据流变成 N 路并行低速数据流。这样做的好处是符号周期拉长 N 倍,多径时延造成的码间串扰(ISI)被稀释;再配上循环前缀(CP),多径带来的符号间干扰直接被吸收掉。这套程序里默认用 QPSK,每个符号 2 bit,子载波总数N_subcar = 64,其中数据子载波 48 个、导频 4 个,其余留空。为什么要留空子载波?因为 FFT 输出的边缘子载波受频谱泄漏影响严重,实际系统里都是留空的,仿真里保留这个细节,后面观察频谱时才不会出现边缘畸变。
发送端的标准流程是:先把 bit 流按 QPSK 映射成复数符号,再把这些符号和导频一起放到对应的子载波索引上,组成一个N_fft × N_symbols的频域矩阵;接着对每一列做 IFFT,把频域符号变回时域波形;最后把每个符号块末尾CP_len个采样点复制到块开头,加上循环前缀后再并串输出。IFFT 在这里的正交性很关键,N_fft 个等间隔子载波在时域上正好是一个完整周期,接收端做 FFT 时只要采样点对齐,子载波之间就不会串扰。
% 发送端:QPSK 映射 -> 子载波映射 -> IFFT -> 加 CP N_symbols = 200; % 每个蒙特卡洛 trial 的 OFDM 符号数 data_bits = randi([0 1], N_data * N_symbols * 2, 1); % 随机 bit 流 tx_sym = qammod(data_bits, M, 'InputType', 'bit', ... 'UnitAveragePower', true); % QPSK 复数符号 tx_sym = reshape(tx_sym, N_data, N_symbols); % 按子载波 x 符号排矩阵 % 频域资源格:0 表示空子载波 ifft_in = zeros(N_fft, N_symbols); ifft_in(pilot_idx, :) = repmat(pilot_sym, 1, N_symbols); % 导频子载波 ifft_in(data_idx, :) = tx_sym; % 数据子载波 % IFFT 到时域,每一列是一个 OFDM 符号 tx_time = ifft(ifft_in, N_fft, 1); % 按列做 N_fft 点 IFFT % 加循环前缀:把每个符号末尾 CP_len 个点复制到前面 tx_time = [tx_time(end - CP_len + 1 : end, :); tx_time]; tx_wave = tx_time(:); % 并串变换,形成发射波形 tx_wave = tx_wave / sqrt(mean(abs(tx_wave).^2)); % 功率归一化这里qammod的InputType设为'bit'表示直接输入 bit 流,UnitAveragePower为true保证星座图平均功率为 1,这样后面算 SNR 时不用再做功率归一化。ifft(ifft_in, N_fft, 1)的第三个参数1表示按第一维(列方向)做变换,每一列对应一个 OFDM 符号。加 CP 时直接把每列最后 16 个点拼到列首,tx_time(end - CP_len + 1 : end, :)取的就是每列尾部,这一步写错了会直接导致后面解调全部错位。最后一行功率归一化很关键,它让时域波形平均功率变成 1,后续由 Eb/N0 换算噪声功率时才不会算错。
有人会问:为什么不直接在频域做高斯白噪声叠加?因为多径信道的本质是时域卷积,发射波形先通过多径信道、再在接收端做 FFT,噪声叠加在时域波形上才符合实际物理过程。如果只弄一个频域平坦衰落的等价模型,CP 和信道估计部分就失去了训练意义。
2.2 信道建模:瑞利多径衰落与 Jakes 模型
多径衰落信道在 MATLAB 仿真里有两级复杂度:最简单的是给每条路径一个固定复增益,相位在每次蒙特卡洛循环里随机生成;再往上一步是用 Jakes 模型或滤波白噪声法给每条路径加上多普勒谱,让信道系数随时间连续变化。这套程序用的是第二种思路,因为只有时变信道才能看出导频密度和信道估计的作用。射频信号经过反射、绕射、散射后到达接收端,每一条路径的幅度服从瑞利分布,相位均匀分布,这就是瑞利衰落;如果存在一条直射径则变成莱斯衰落,仿真里默认按瑞利处理。
这里有个很实用的简化:不用去实现完整的 Jakes 正弦叠加公式,而是用“白噪声激励 + 低通滤波器”来逼近多普勒谱,滤波器截止频率就是最大多普勒频移fd。虽然相比标准 Jakes 模型在功率谱形状上略有差异,但相干时间和电平通过率的统计特性基本正确,对验证 OFDM 链路足够用了。
% 多径信道系数生成:滤波白噪声法逼近多普勒谱 rng(2024); % 固定随机种子,结果可复现 fs = 1e6; % 基带采样率 1 MHz delay = [0 5 10 15]; % 多径时延,单位:采样点 power = [0 -3 -6 -9]; % 各径平均功率,单位:dB fd = 50; % 最大多普勒频移,单位:Hz path_amp = sqrt(10.^(power / 10)); % dB 转线性幅度 h_time = zeros(N_symbols, max(delay) + 1); % 每行对应一个 OFDM 符号时刻 for p = 1:length(delay) raw = randn(N_symbols, 1) + 1j * randn(N_symbols, 1); % 复高斯白噪声 [b, a] = butter(1, fd / (fs / 2), 'low'); % 一阶低通,截止在 fd h_p = filter(b, a, raw); h_p = h_p / sqrt(mean(abs(h_p).^2)) * path_amp(p); % 归一化后乘路径幅度 h_time(:, delay(p) + 1) = h_p; endbutter(1, fd/(fs/2), 'low')的第二个参数是归一化截止频率,fs/2 = 500k是奈奎斯特频率。低通滤波器让衰落系数的变化速率被限制在fd以内,等效于信道相干时间约为1/fd;fd=50Hz时相干时间约 20ms,而一个 OFDM 符号周期约 80us,说明信道在一个符号内几乎不变、在多个符号间缓慢变化,这正是慢衰落信道的典型状态。每条路径滤波后都要归一化到单位功率,再乘以path_amp(p)控制平均功率,否则滤波器增益会让低径功率偏大。
delay 的单位是采样点,不是秒。fs=1MHz时delay=[0 5 10 15]对应 0us 到 15us 的时延扩展。最大时延 15 个采样点小于CP_len=16,这是故意设置为“CP 足够长”的理想情况;如果你把 delay 改成 20 或者把 CP_len 改小,就会在 BER 曲线上看到地板效应,这个问题我在第 5 章会专门拆。
2.3 接收端:定时对齐、去 CP 与 FFT 解调
接收端的第一步是把自己发射的波形找回来。仿真里假设接收机已知符号边界,所以代码直接用reshape把串行波形恢复成(N_fft+CP_len) × N_symbols的矩阵,然后丢掉每个符号前面CP_len个采样点。真实接收机里这一步对应定时同步,要做滑动相关或者用前导序列找到符号起始点,仿真里把它省掉是为了聚焦信道和均衡问题。
% 接收端:串并 -> 去 CP -> FFT -> 提取子载波 -> 均衡 rx_time = reshape(rx_wave, N_fft + CP_len, N_symbols); % 按符号长度分段 rx_time = rx_time(CP_len + 1 : end, :); % 去掉循环前缀 rx_freq = fft(rx_time, N_fft, 1); % 按列 FFT 回频域 % 导频子载波处用已知导频做信道估计 h_est_pilot = rx_freq(pilot_idx, :) ./ pilot_sym; % LS 估计 % 按子载波索引线性插值到所有数据子载波 h_est_data = interp1(pilot_idx, h_est_pilot, data_idx, ... 'linear', 'extrap'); rx_data = rx_freq(data_idx, :); rx_eq = rx_data ./ h_est_data; % 单抽头均衡去 CP 后剩下的采样点就只承载有效数据了。注意reshape能用,是因为发射端tx_wave = tx_time(:)按列展开,MATLAB 矩阵默认按列优先存储,所以reshape(rx_wave, N_fft+CP_len, N_symbols)能正好还原出“每列一个 OFDM 符号”的矩阵,这个隐式约定很容易被忽略,第 5 章的避坑里还会提。rx_freq(pilot_idx, :) ./ pilot_sym是典型的 LS 信道估计:接收导频除以已知导频,得到导频位置的信道频率响应,然后interp1在频域上插值到数据子载波。这种线性插值在子载波间隔较小、频选衰落平缓时够用;如果多径时延扩展变大,频域信道波动剧烈,就要换 MMSE 估计或者加密集导频,否则均衡残留会很明显。
3. 核心代码拆解:怎么把链路完整跑起来
3.1 参数化配置与主循环结构
这套仿真程序的优点之一是参数没有散落在各处,整个文件开头集中定义。主循环的结构是:外层遍历 SNR 点,内层做固定次数的蒙特卡洛仿真,每次随机生成 bit、随机生成信道系数、过信道、解调、统计错误比特数,最后用所有蒙特卡洛循环的累计错误数除以累计比特数得到 BER。这种做法的好处是统计稳定性好,不会因为某次信道生成特别深衰落导致曲线抖动剧烈。
% 主循环:遍历 SNR 做蒙特卡洛仿真 snr_dB = 0:2:30; % 仿真 Eb/N0 范围 N_trials = 200; % 每个 SNR 点的蒙特卡洛次数 ber = zeros(size(snr_dB)); for s = 1:length(snr_dB) EbN0 = 10^(snr_dB(s) / 10); Es = N_fft + CP_len; % 发射波形已归一化,符号能量即采样点数 Eb = Es / (N_data * log2(M)); % 摊到每个数据比特 No = Eb / EbN0; % 单边噪声谱密度 total_err = 0; total_bit = 0; for trial = 1:N_trials [err, bit_num] = run_ofdm_link(M, N_fft, CP_len, ... data_idx, pilot_idx, pilot_sym, delay, power, fd, No); total_err = total_err + err; total_bit = total_bit + bit_num; end ber(s) = total_err / total_bit; end这段代码里最容易抄错的是Es和Eb的换算。tx_wave在发射端已经归一化为单位功率,因此每个采样点能量是 1,一个含 CP 的 OFDM 符号总能量就是N_fft+CP_len;N_data * log2(M)是每符号携带的数据比特数,两者相除得到每个比特的平均能量Eb,再除以目标EbN0就得到No。这里把导频和 CP 的功率摊进了 Eb,属于工程上的等效定义,和理论曲线叠图时记得做开销修正,后面第 6 章会算这个数。
3.2 信道卷积与噪声注入实现
发射波形进入多径信道时,代码没有用conv一次性卷积,而是逐径移位叠加。这么做的原因很实际:如果直接conv(tx_wave, h_impulse),线性卷积会把波形拉长,截断处理稍有不慎就会在符号边界引入假误差;而逐径叠加更贴近物理过程,也方便逐符号乘上时变衰落系数。每一径的衰落系数在 OFDM 符号之间是变化的,但在一个符号内部近似不变,这个假设对应慢衰落信道。
% 多径信道时域通过:逐径延迟、叠加,再注入 AWGN tx_symbols = reshape(tx_wave, N_fft + CP_len, N_symbols); % 还原成符号矩阵 rx_wave = zeros(size(tx_wave)); for p = 1:length(delay) gain_p = h_time(:, delay(p) + 1); % 该径逐符号衰落系数 path_block = tx_symbols .* gain_p.'; % 每个符号乘各自的衰落系数 path_wave = zeros(size(tx_wave)); path_wave(delay(p) + 1 : end) = path_block(:); % 延迟 delay(p) 个采样点 rx_wave = rx_wave + path_wave; % 复数域求和叠加 end % 加性高斯白噪声,时域复数噪声 rx_wave = rx_wave + sqrt(No / 2) * (randn(size(rx_wave)) ... + 1j * randn(size(rx_wave)));gain_p.'用的是普通转置而不是共轭转置,因为这里只是想把列向量变成行向量参与.乘法,做共轭会引入相位错误。path_wave(delay(p)+1 : end)模拟信号晚到delay(p)个采样点,晚到的径和早到的径在接收端叠加,形成频率选择性衰落。当delay(p)超过 CP 长度时,前一个符号的尾巴会落到当前符号的 FFT 窗口里,这就是第 5 章要讲的地板效应的根源。噪声注入的位置也值得注意:噪声加在时域波形上,而不是加在频域子载波上。实际接收机的热噪声本来就是宽带噪声,经过 FFT 之后每个子载波上的噪声功率才是白噪声谱密度;反过来如果先在频域加噪声再做 IFFT,统计特性等价,但会绕过时域信道的卷积过程,达不到训练目的。
3.3 误码率统计与结果输出
误码率统计要区分“比特误码率”和“符号误码率”。程序里统计的是 BER,所以发射端把随机 bit 流交给qammod,接收端把均衡后的复数符号交给qamdemod并指定OutputType='bit',从而直接得到 bit 判决结果,再用逻辑异或统计错误 bit 数。很多人图省事直接比较星座点符号,得到的是 SER,和理论 BER 曲线对不上,会误以为程序有 bug。
% 误码统计:硬判决 -> 逐 bit 比对 rx_bits = qamdemod(rx_eq, M, 'OutputType', 'bit', ... 'UnitAveragePower', true); tx_bits = reshape(data_bits, N_data * 2, N_symbols); bit_err = sum(sum(rx_bits ~= tx_bits)); % 非零表示有 bit 错误 bit_num = numel(tx_bits); % 本次传输总 bit 数 % SNR 循环结束后统一绘图 semilogy(snr_dB, ber, 'b-o', 'LineWidth', 1.5); grid on; xlabel('Eb/N0 (dB)'); ylabel('BER'); title('多径衰落信道下 OFDM - QPSK 误码率曲线'); legend('OFDM over Rayleigh multipath');rx_bits ~= tx_bits得到的逻辑矩阵,sum两次分别是按列求和再对行求和,必须用numel取总 bit 数作为分母,否则 SNR 越高、错误越少时,统计出的 BER 波动会越大。还有一个隐藏细节:qamdemod返回的 bit 排列顺序必须和qammod输入 bit 顺序一致,都是按格雷码映射,这个由 MATLAB 内部保证;如果你自己写硬判决函数,要按星座点的真实 bit 标签去查表,不能想当然地按 0 1 2 3 的顺序排。跑完主程序,你会得到一条随 SNR 增大而下降的 BER 曲线。把它和理论瑞利信道 BER 曲线放到同一张图里,正常情况两者相差在 1dB 以内;如果差得多了,直接跳到第 5 章,大概率是 CP、信道估计或者缩放因子里的某一个出了问题。
4. 参数设置实战:子载波数、CP 长度、导频密度与多普勒频移
4.1 子载波数与 FFT 点数的匹配关系
子载波数N_subcar和 FFT 点数N_fft不一定相等。程序里 64 个子载波中只有 48 个用作数据、4 个用作导频,其余 12 个留空,这对应实际 OFDM 系统中数据子载波、导频子载波和空子载波的概念。子载波间隔Δf = fs / N_fft,在fs=1MHz、N_fft=64时子载波间隔是 15.625kHz,OFDM 符号周期(不含 CP)约 64us。如果要仿真带宽 20MHz、子载波间隔 15kHz 的 LTE 类系统,就得把fs和N_fft成比例放大,而不是只改一个参数。
参数之间的联动关系是最容易踩坑的地方。例如把N_fft从 64 改成 128,子载波间隔减半,符号周期翻倍;此时如果fd还保持 50Hz,相干时间占符号周期的比例变小,信道在符号内的变化更明显,需要重新审视信道估计的更新速率。常见做法是先确定子载波间隔、再反推 FFT 点数和采样率,而不是随手填一个好看的整数。我整理了一份参数对应表,照着调不容易乱:
| 参数 | 典型值 | 与哪些参数联动 |
|---|---|---|
| N_fft | 64 / 128 / 256 | 子载波间隔、符号周期、FFT 复杂度 |
| 子载波间隔 Δf | 15.625kHz(fs=1MHz, N_fft=64) | fs / N_fft |
| CP 长度 | 16 采样点 ≈ 16us | 多径时延扩展 max(delay) |
| 导频间隔 | 每隔 12~16 个子载波插一个 | 信道频选衰落深度、多普勒频移 |
| fd | 50Hz 低速移动 / 500Hz 高速 | 符号周期、导频时域密度 |
| 调制阶数 M | 4(QPSK)/ 16(16QAM) | 每符号 bit 数、信道估计精度要求 |
4.2 CP 长度与多径时延扩展的匹配
CP 长度设计的唯一硬约束是:CP_len必须大于最大多径时延(以采样点计),否则前一个符号的拖尾会污染当前符号的 FFT 窗口。程序里delay=[0 5 10 15]最大时延 15 点,CP_len=16正好覆盖。如果改成delay=20而 CP 还是 16,性能就会明显恶化;反过来 CP 过长又会浪费发射功率和带宽,因为 CP 本身不携带新信息。
多径时延扩展的物理含义是最大路径与最短路径的到达时间差。仿真里用采样点表示时延,要注意采样率变了时延数值也要跟着变。比如delay=15在 1MHz 采样率下是 15us,如果fs改成 5MHz,同样的 15us 时延要写成 75 个采样点,否则实际时延就缩短成 3us 了。这个换算关系是参数化仿真里最基础的功课,也是很多“换个采样率结果完全变样”问题的根源。
% 校验 CP 长度是否覆盖多径时延:仿真开始前主动断言 assert(CP_len > max(delay), 'CP太短: 最大时延 %d 必须小于 CP_len=%d', ... max(delay), CP_len);我把这行assert加在了程序开头。它不改变任何仿真逻辑,但能在你手滑改大delay时立刻报错,而不是让你对着一条 BER 地板曲线发呆半小时。程序里建议保留这行检查。
4.3 导频密度与多普勒频移的约束
导频的作用是让接收端获得信道的频域采样值。频域上,导频间隔必须小于信道相干带宽,否则信道在两个导频之间剧烈变化,线性插值会失真;时域上,导频符号的更新速率必须快于信道相干时间,否则信道在两次导频之间已经变了。这两个约束一个由多径时延扩展决定,一个由多普勒频移决定。
程序中 4 个导频均匀分布在 64 个子载波里,平均 16 个子载波一个导频。当delay最大 15 点时,相干带宽约为1/(15us) ≈ 66kHz,而 16 个子载波的跨度是16×15.625kHz = 250kHz,已经略大于相干带宽。严格来说这种导频密度在深频选衰落时插值误差会偏大;但代码里为了演示要求并不苛刻,因为模拟信道是 4 条离散径,频域信道虽有起伏,还不至于在两个导频之间出现完全独立的深衰落。如果你要仿真连续多径功率谱(比如指数衰减的 20 径信道),建议把导频密度提高到每 8 个子载波一个。
时域上的导频插入同样重要。如果程序只做一次信道估计就用完整帧,那要求整帧时长远小于相干时间;当fd增大到 500Hz 时,一帧 1000 个符号已经跨越了 4 个相干时间,这时候要么把帧拆短,要么在时域上每隔若干符号插入一个导频符号做插值。标准做法是维护一个二维导频栅格——频域按子载波间隔、时域按符号间隔——接收端用二维插值恢复整个资源格的信道响应。这套程序为了保持入门复杂度没写时域插值,但你改fd时会撞上这个边界。
5. 仿真踩坑实录:这份程序里最容易翻车的五个位置
5.1 IFFT/FFT 缩放因子导致 BER 系统性偏移
现象:在 AWGN 单径信道下跑 BER,曲线形状和理论一致,但整体差了一个固定的功率因子,大约 1~2dB。
原因:MATLAB 自带ifft是 1/N 缩放、fft是 1 缩放,两者级联后恰好恢复原信号,所以程序里不需要额外归一化。但很多从 C 或者 Python 转过来的朋友习惯手写 FFT 或者用1/sqrt(N)的对称缩放版本,这样发射和接收都会多出1/sqrt(N)的幅度缩放,等效于噪声功率相对信号被放大,BER 曲线就整体右移。
解决:用 MATLAB 内置ifft/fft时不要加任何额外系数;如果自己写 FFT 内核,统一约定 IFFT 带1/N、FFT 不带系数。验证方法也很简单:在无噪声、单径信道下解调,星座图必须和发射端完全重合,任何幅度或相位偏差都说明缩放或转置有问题。
5.2 导频索引与 FFT 输出排列错位
现象:均衡之后的星座图整体旋转了一个相位,而且 SNR 越高旋转角度越固定,低 SNR 时直接被噪声盖住看不出来。
原因:MATLAB 的fft输出是 DC 分量在第 1 个位置、正频率在前、负频率在后。如果导频索引按“先正后负”的直觉写,比如把负频率子载波排在矩阵后半段,而插值时又用连续索引去对应,导频位置就整体错位了。导频位置错了,估计出的信道响应等于乘了一个exp(-j2πkΔ/N)的线性相位,均衡后星座图自然旋转。
解决:定义子载波索引时要不就用0:N_fft-1的常规顺序并手动映射,要不统一用fftshift把 DC 移到中间再按物理频率排列。我建议在程序开头用一个subcarrier_idx = (-N_fft/2:N_fft/2-1).'这样的向量做参照,所有数据、导频和空子载波都基于它来选,避免正负频率混淆。
5.3 高 SNR 时 BER 曲线出现地板
现象:多径信道下 SNR 超过 20dB 后 BER 不再下降,曲线像被什么东西托住了。
原因:这是多径仿真中最经典的现象。第一嫌疑是 CP 长度小于最大时延,造成符号间干扰;第二嫌疑是导频密度不足,信道估计插值残留误差;第三嫌疑是信道估计用的线性插值本身在复平面上不对。程序里如果delay最大值接近CP_len,即使没超过,信道尾部拖进 FFT 窗口的残余也会限制高 SNR 性能。
解决:先用assert(CP_len > max(delay))排除 CP 问题,再把导频密度从每 16 个一组改成每 8 个一组对比跑一次;如果曲线明显变化,说明是信道估计精度不够。还有一种更隐蔽的可能性:interp1对复数向量做线性插值时,实部和虚部各插一次,这在数学上没有问题,但当导频间距超过相干带宽时,插值结果在深衰落附近会有较大偏差,换成'nearest'或spline对比跑一版就能定位。
5.4 采样率改变后结果完全变样
现象:把fs从 1MHz 改成 5MHz,其他参数没动,BER 曲线突然变差一大截。
原因:delay、CP_len的单位都是采样点。fs变成 5 倍后,同样的采样点数值对应的物理时间变成原来的 1/5,也就是说实际多径时延缩短了、CP 的实际时长也缩短了,但fd仍然是绝对频率(Hz),它相对采样率的关系也变了。这一连串连锁反应会让整个链路在物理上变成另一套系统。
解决:参数化设计时分清哪些是物理量、哪些是采样域数值。物理量用秒、赫兹定义,采样域数值用采样点定义,两者之间只差一个fs。我在改采样率时会统一走一遍换算:delay_samples = round(delay_us * fs / 1e6),CP_samples = round(CP_us * fs / 1e6),而不是直接改一个fs就跑。
5.5 复数转置与列优先展开的隐藏坑
现象:程序跑到一半报矩阵维度不匹配,或者星座图整体上下镜像,看似数据反了,但 BER 统计出来又偶然落在合理范围,很难一眼定位。
原因:MATLAB 里'是共轭转置,.''才是普通转置。复数矩阵如果该用.''的地方用了',所有符号的虚部相位都会翻转,星座图表现为上下镜像;另外tx_time(:)是按列优先展开,如果接收端reshape时行列数写反,数据顺序就完全错乱,但概率统计上 BER 可能仍然是个合理的值,属于最阴险的一类 bug。
解决:涉及复数矩阵方向变换时一律用.'';发射端并串展开用了(:),接收端恢复矩阵就必须用相同列数去reshape。我在代码里把发射端tx_wave = tx_time(:)和接收端reshape(rx_wave, N_fft+CP_len, N_symbols)写成了位置紧邻的两段,就是强迫自己检查它们的行列约定是否一致。
5.6 低 SNR 时瑞利衰落系数没归一化
现象:BER 曲线整体比理论值差很多,低 SNR 时尤其明显,曲线看起来“掉得太快”。
原因:h_time的每一径在滤波后虽然做了功率归一化,但如果你把path_amp直接乘在归一化系数上、却没重新检查总功率,多条路径叠加后的总接收功率可能大于 1。瑞利信道下各径功率求和应为sum(10.^(power/10)),如果写成sum(path_amp),相当于把线性幅度当成功率相加,总功率被放大了,等效 SNR 降低,BER 自然变差。
解决:验证信道总功率用sum(10.^(power/10))是否等于 1;如果不等于 1,需要在信道输出后做一个功率归一化。程序里常见的做法是rx_wave = rx_wave / sqrt(mean(abs(rx_wave).^2)),但这会连噪声一起归一化,必须在加噪声之前做。更稳妥的是在生成h_time后统一除以sqrt(sum(path_amp.^2)),保证各径叠加后的平均功率为 1。
6. 最后的验证技巧:用理论 BER 曲线给仿真校准
6.1 理论 BER 闭式解
多径瑞利信道下 QPSK 的理论 BER 闭式解是Ber = 0.5 * (1 - sqrt(EbN0 / (1 + EbN0))),AWGN 信道下是Q(sqrt(2 * EbN0))。把两条曲线放一起就能直观看到多径衰落带来的性能代价:在 BER=1e-3 时,瑞利信道比 AWGN 信道大约要多 17~20dB。如果你的仿真跑完连这个差距都没有,那多半不是信道没生效,就是衰落系数生成有误。这里的关键是确认程序横轴的定义——程序把导频和 CP 功率摊进了 Eb,这套参数的开销因子约 0.8dB,叠图时把理论曲线右移 0.79dB 再对比更公平。
6.2 单径 AWGN 对照实验
我拆这份程序时养成的习惯是:调多径参数前先把delay=[0]、power=[0]跑一遍,让信道退化成纯 AWGN。如果 AWGN 下 BER 和理论对不上,说明链路本身有问题——调制、IFFT、FFT、均衡、解调先接受检验;确认无误后再加第二条路径,把时延从 1 个采样点逐步增大,看 BER 曲线随频率选择性加深是否平缓恶化。具体做法是把发射接收部分包成一个函数,信道固定为h=[1],每次改完fs、N_fft或调制阶数先跑它,十几秒出整条曲线,比任何断言都有效。
6.3 快速验证星座图与信道估计的一致性
最后一招是“关掉噪声看痕迹”:把No设成 0,只保留信道,直接打印均衡前后的星座图。均衡前的星座图会呈现一个扭曲的团簇,这是多径信道频率选择性衰落的直观体现;均衡后如果星座点清晰落在 QPSK 的 4 个理想位置上,说明 CP 和信道估计一起工作正常。如果均衡后星座点聚成 2 个团而不是 4 个,多半是导频映射或转置出了问题;如果 4 个团有旋转,多半是导频索引错位。我自己有一次这样排查问题:当时调fd=500Hz时曲线掉得特别快,关掉噪声后发现均衡后的星座图虽然也有 4 个团,但每个团内部拉成了一条短线,说明信道在一个 OFDM 符号内已经从“常值”变成了“线性变化”,而我用的还是每符号一个信道系数的慢衰落模型——模型本身在高多普勒下就不成立。从那以后我每次改fd都会强制过一遍星座图检查,确认信道在一个符号内的变化量可以忽略,再开始统计 BER。这份程序拿回去直接跑就行,跑通之后再按这套验证流程改参数,基本不会翻车。希望帮到你。
本文还有配套的精品资源,点击获取