简介:一份面向5G通信波形设计研究者与通信专业学生的MATLAB仿真包,聚焦OFDM、FBMC、UFMC三种多载波调制方式的对比分析。基于MATLAB 2022A平台,内含三个独立m脚本(Runme_OFDM.m、Runme_FBMC.m、Runme_UFMC.m),分别实现三种调制技术的基带仿真,运行后可输出误码率曲线与频谱对比图;另附一个avi格式操作录像,详细演示从路径设置到执行出图的全过程,零基础也能照做。压缩包共4个文件,整体仅2.32MB,轻量易用;代码注释完整,便于理解算法参数与信号处理流程。资源已有1174人学习下载,适合入门5G多载波技术、完成课程设计或快速验证想法时参照。执行时需确保MATLAB左侧当前文件夹与程序所在目录一致,视频中有针对性说明。
1. 三种波形摆在面前,先想清楚仿真为了回答什么问题
做 5G 物理层仿真的人,迟早会在 OFDM、FBMC、UFMC 这三个缩写面前停下来。OFDM 是 LTE 和 5G NR 真正用的波形,FBMC 和 UFMC 是学术界和工业界长期研究的替代方案。问题在于,很多人的第一次仿真只是把三个发射机分别跑出星座图,然后发现 FBMC 的误码率好像更差,就得出结论说 OFDM 最好。这个结论很危险,因为三种波形的差异根本不在星座图上,而在频谱利用率、同步鲁棒性和对异步用户的容忍度上。这篇文章的目标,是把你真正需要对比的维度拆开,用一套可复现的 MATLAB 代码把三条链路搭起来,并且把仿真操作录成视频,方便你回看参数改动前后的差异。适合系统算法工程师、物理层测试人员和做波形研究的同学,尤其是要在一个月内给出选型结论的人。
2. 先搭 OFDM 仿真框架:循环前缀、同步和信道估计一起考虑
2.1 OFDM 发送端:从比特流到带循环前缀的时域信号
OFDM 的发送端链路是另外两种波形的参照系,先把这条链路写清楚。下面这段代码实现一个完整的发送端,包括 QAM 映射、串并转换、IFFT、加循环前缀。这里直接把子载波数、循环前缀长度和调制阶数定义成参数,方便后面复用。
%% OFDM 发送端参数 Nfft = 64; % FFT 点数,等于子载波总数 CpLen = 16; % 循环前缀长度,按 1/4 符号长度设定 M = 4; % 4QAM,每符号 2 bit numSym = 100; % OFDM 符号数 dataLen = Nfft * numSym * log2(M); % 总比特数 %% 发送端 bits = randi([0 1], dataLen, 1); % 随机比特流 symbols = qammod(bits, M, 'InputType', 'bit'); % QAM 调制 symbols = reshape(symbols, Nfft, numSym); % 串并转换,每列一个 OFDM 符号 % IFFT 之后加循环前缀 ifftOut = ifft(symbols, Nfft); % 对每列做 IFFT txSignal = [ifftOut(end-CpLen+1:end, :); ifftOut]; % 把尾部复制到头部 txSignal = txSignal(:); % 并串转换代码里qammod的 ``InputType设置为'bit',直接吃比特流,省去手动分组。循环前缀是从每个符号尾部截取CpLen个采样点复制到头部,这样做保持了 IFFT 输出的周期性,在接收端做线性卷积时能把信道变成循环卷积,从而允许用单抽头均衡器恢复数据。注意CpLen` 必须大于信道的最大时延扩展,否则会产生符号间干扰和子载波间干扰。一般工程里 CP 长度取 FFT 点数的 1/8 到 1/4,5G NR 里普通子载波间隔是 1/16 左右,这里选 1/4 是为了在仿真里留足余量。
2.2 接收端对齐:符号定时与频偏校正的 MATLAB 实现
OFDM 接收端第一步不是解调,而是找到每个符号的边界。没有对齐的符号定时会让 FFT 窗口跨在两个符号之间,星座图直接糊掉。最常用的盲同步方法是利用循环前缀的相关性:因为 CP 是符号尾部的复制,所以滑动相关会在正确的符号边界处出现峰值。频偏估计则利用 CP 与尾部之间的相位差。
%% 基于循环前缀的符号定时与频偏估计 rxSignal = noisySignal; % 假设已经加完噪声 L = length(rxSignal); corr = zeros(L - Nfft - CpLen, 1); for n = 1:length(corr) % 计算相距 Nfft 的两个窗口的相关系数 windowA = rxSignal(n : n + CpLen - 1); windowB = rxSignal(n + Nfft : n + Nfft + CpLen - 1); corr(n) = abs(sum(windowA .* conj(windowB))); end [~, symStart] = max(corr); % 相关峰位置即符号起始点 % 频偏估计:利用 CP 与尾部间相位差 phaseDiff = angle(mean(conj(rxSignal(symStart:symStart+CpLen-1)) ... .* rxSignal(symStart+Nfft:symStart+Nfft+CpLen-1))); freqOffset = phaseDiff / (2 * pi * Nfft); % 归一化频偏 compSignal = rxSignal .* exp(-1j * 2 * pi * freqOffset * (0:L-1)');滑动相关窗口长度为CpLen,每次移动一个采样点,计算两个窗口的共轭相关。峰值位置symStart就是第一个符号的起始点。频偏估计的公式是利用相隔Nfft的两个采样点之间相位差正比于频偏的原理,理想情况下这个相位差为 0,有频偏时变成2 * pi * freqOffset * Nfft / Nfft,归一化后就得到频率偏移量。补偿时用复指数相乘,频率轴上整体搬移。这里必须注意,频偏估计算法在高信噪比下才接近 Cramer-Rao 下界,低信噪比时会出现大偏差,建议在仿真时加上限幅处理。
2.3 信道估计与均衡:LS 估计在 OFDM 里的代价
信道估计最常用的是最小二乘方法,在导频子载波上做除法得到信道频率响应,然后插值到全部子载波。实际系统中导频是离散分布的,这里为了简化,直接在全部子载波上放置导频符号便于展示原理。
%% LS 信道估计与均衡 pilotIdx = 1:4:Nfft; % 每隔 4 个子载波放一个导频 dataIdx = setdiff(1:Nfft, pilotIdx); % 其余子载波传数据 knownPilot = qammod(randi([0 1], length(pilotIdx)*numSym, 1), M, 'InputType', 'bit'); knownPilot = reshape(knownPilot, length(pilotIdx), numSym); % 假设收到的是经过信道的频域信号 freqRx pilotRx = freqRx(pilotIdx, :); pilotTx = knownPilot; H_est = pilotRx ./ pilotTx; % LS 估计 % 线性插值到全部子载波 H_full = interp1(pilotIdx, H_est, 1:Nfft, 'linear', 'extrap'); eqData = freqRx ./ H_full; % 零迫使均衡pilotIdx选 1:4:64,共 16 个导频子载波。LS 估计就是直接相除,没有噪声统计信息,因此噪声大时估计误差大。插值用interp1,'linear'是线性插值,'extrap'是外推超出范围的点。实际系统中常用二阶插值或基于 DFT 的插值,后者能平均掉部分噪声。均衡用的是迫零准则,即直接除以信道估计值。如果信道有深衰落导致H_full接近 0,均衡后噪声会被放大,这是零迫使均衡的固有缺陷,可以换成 MMSE 均衡器来折中,MMSE 需要估计噪声方差。
3. FBMC 仿真:滤波器组的核心是原型滤波器和 OQAM
3.1 为什么 FBMC 不需要循环前缀:频域交错的代价
FBMC 最大的卖点是没有循环前缀,而是用精心设计的滤波器组让每个子载波的旁瓣快速衰减。OFDM 使用矩形窗导致频域出现 sinc 形状的旁瓣,衰减速度只有约 13 dB 每倍频程,所以需要 CP 来对抗时延扩展。FBMC 把每个子载波都用长滤波器成形,频域旁瓣衰减可以达到 40 dB 以上。但代价是相邻子载波之间不再保持严格正交,只能用偏移 QAM,即实部和虚部错开半个符号周期,来换取实域正交性。
3.2 PHYDYAS 原型滤波器设计:重叠因子 K 如何决定旁瓣衰减
PHYDYAS 滤波器是 FBMC 里最常见的原型滤波器,设计时给定重叠因子 K,滤波器长度是 K 倍符号长度。K 越大滤波器频域越集中,但延迟越大。工程选型一般 K=4,因为 K=4 时旁瓣衰减已经超过 40 dB,再增大收益有限。
%% PHYDYAS 原型滤波器设计 K = 4; % 重叠因子,可选 2、3、4 N = Nfft; % 子载波数 taps = K * N; % 滤波器长度 % PHYDYAS 系数表 coefs = [1, sqrt(2)/2, 0, -sqrt(2)/2]; % K=4 时的前 4 个系数 p = zeros(taps, 1); p(1:N) = coefs(1); p(N+1:2*N) = coefs(2); p(2*N+1:3*N) = coefs(3); p(3*N+1:4*N) = coefs(4); p = p / sqrt(sum(p.^2)); % 归一化能量频域里观察这个滤波器,主瓣宽度约为单个子载波带宽,第一个旁瓣已经低于主瓣 40 dB。这个滤波器在每个子载波上都要用一次,所以发射机复杂度是 OFDM 的 K 倍。参数coefs的值取自 PHYDYAS 论文的频域采样系数,K=4 对应[1, 0.707, 0, -0.707],奇数位为 0 是因为滤波器在频域有对称性。后续设计 K=2 时系数换成[1, 1],K=3 时换成[1, sqrt(2)/2, 0]。
3.3 OQAM 调制与综合滤波器组:实部/虚部错开半个符号
FBMC 用 OQAM 把复符号拆成两路,分别调制实部和虚部,且虚部比实部延迟半个符号周期。这样在接收端提取实部时,来自相邻符号和相邻子载波的干扰是纯虚部,取实部就能消除。OQAM 映射的核心是符号分裂和时延。
%% OQAM 映射 numFBSym = 2 * numSym; % FBMC 符号数是 OFDM 的 2 倍 oqamSymbols = zeros(N, numFBSym); % 把复符号拆成实部和虚部,间隔放置 oqamSymbols(:, 1:2:end) = real(symbols); oqamSymbols(:, 2:2:end) = 1j * imag(symbols); % 相邻子载波的虚部乘以 (-1)^(n+m) 以维持实域正交 for m = 0:N-1 for n = 0:numFBSym-1 phase = (-1)^(m + n); oqamSymbols(m+1, n+1) = oqamSymbols(m+1, n+1) * phase; end end符号倍增意味着同样比特率下符号速率减半,滤波器组的输出逐符号叠加,重叠部分就是相邻符号的影响。乘以(-1)^(m+n)这步是 OQAM 的关键细节之一,它让相邻子载波之间的干扰在取实部时互相抵消。很多人 FBMC 仿真误码率高,八成是漏了这个符号因子。
用filter函数实现综合滤波器组,把每个子载波的数据经过原型滤波器,再频移相加。发射信号长度是普通 OFDM 的约 K 倍,因此仿真时要注意时间对齐。
%% 综合滤波器组(合成端) txFbmc = zeros(taps + (numFBSym-1)*N, 1); for m = 0:N-1 filterOut = filter(p, 1, oqamSymbols(m+1, :).'); % 把滤波后信号搬移到对应子载波频率 modSignal = filterOut .* exp(1j * 2 * pi * m / N * (0:length(filterOut)-1)'); txFbmc = txFbmc + modSignal; end重叠相加在循环里隐式完成,filter保留了滤波器内部的延迟状态,所以上一次的输出末尾和当前滤波输出自然叠加。如果手写重叠相加,需要自己维护一个累加器,这里 MATLAB 的签名状态设计刚好省掉这一步。exp(1j*2*pi*m/N*...)是每个子载波的频移,它和滤波一起等效于把原型滤波器搬到对应频段。
3.4 FBMC 接收端:匹配滤波与实部提取
接收端用同一个原型滤波器做匹配滤波,然后下变频、取实部。因为 OQAM 的虚部干扰在取实部时被消除,所以不需要进行复杂的均衡设计。下面给出核心解调代码。
%% FBMC 解调 rxFbmc = rxSignal; % 接收信号 rxSym = zeros(N, numFBSym); for m = 0:N-1 % 下变频到基带 downConv = rxFbmc .* conj(exp(1j * 2 * pi * m / N * (0:length(rxFbmc)-1)')); % 匹配滤波 filtOut = filter(flipud(p), 1, downConv); % 采样,采样点取每符号中间 idx = flow + round(taps/2) : N : length(filtOut); rxSym(m+1, :) = filtOut(idx(1:numFBSym)); end % 取实部并恢复 0/1 比特 rxReal = real(rxSym); % 去除相位因子 for m = 0:N-1 for n = 0:numFBSym-1 rxReal(m+1, n+1) = rxReal(m+1, n+1) * (-1)^(m+n); end end dataOut = [rxReal(:, 1:2:end); rxReal(:, 2:2:end)]; bitsOut = qamdemod(dataOut, M, 'OutputType', 'bit');匹配滤波用的是flipud(p),也就是原型滤波器的翻转版本。FBMC 的最佳接收应当是匹配滤波而不是直接用原型滤波,因为匹配滤波最大化信噪比。采样点取符号中心,是因为原型滤波器的群延迟特性让脉冲能量在中心最集中。实部提取前不要做均衡,FBMC 对频选信道的均衡要比 OFDM 复杂得多,这既是它的弱点也是研究热点,仿真里一般先假设理想同步和理想信道,把重点放在滤波器组本身。
4. UFMC 仿真:子带滤波是复杂度与性能的折中
4.1 UFMC 的系统结构:加窗不再需要 CP,但滤波器变短
UFMC 的思路介于 OFDM 和 FBMC 之间:把整个频带分成若干子带,每个子带用一个短滤波器滤波,而不是像 FBMC 那样每个子载波一个长滤波器。这样既抑制了旁瓣,又不需要 OQAM,复杂度也远低于 FBMC。UFMC 不需要 CP,但需要两个连续符号之间加一段保护间隔,通常是零填充,用来吸收滤波器延迟。
4.2 切比雪夫窗与子带滤波器设计
UFMC 的滤波器通常是基于窗函数设计,最常见的窗是切比雪夫窗,因为可以指定旁瓣衰减。滤波器长度为子带带宽的约 2 倍,太长会引入过多的符号间干扰,太短则旁瓣抑制不足。滤波器的通带和过渡带需要严格贴合子带宽度。
%% UFMC 子带滤波器设计 numSubbands = 4; % 子带数量 subbandSize = Nfft / numSubbands; % 每子带子载波数 filterLen = 32; % 滤波器长度,按经验取子带长度的一半以上 SidelobeAtten = 40; % 旁瓣衰减 dB % 切比雪夫窗做 FIR 设计 windowedFilter = zeros(filterLen, numSubbands); for sb = 1:numSubbands % 频域通带范围对应子带位置 cutoffLow = (sb-1) * subbandSize / Nfft; cutoffHigh = sb * subbandSize / Nfft; % 标准 FIR 设计 filt = fir1(filterLen-1, [cutoffLow cutoffHigh], chebwin(filterLen, SidelobeAtten)); % fir1 返回的是行向量,转置后存放 windowedFilter(:, sb) = filt(:); endfir1是标准基于窗函数的 FIR 设计函数,第三个参数传入切比雪夫窗作为窗函数。chebwin的SidelobeAtten参数直接控制旁瓣衰减,40 dB 相当于不影响相邻子带时能容忍约 1% 的功率泄漏。代码里每子带的滤波器频率范围按子带边界均分计算。实际中相邻子带之间的过渡带会有交叠,这个交叠是 UFMC 与 OFDM 相比额外获得的抑制效果。如果发现频谱旁瓣仍然偏高,优先增大SidelobeAtten,而不是增加滤波器长度,因为滤波器长度增加会扩展时延。
4.3 UFMC 发送与接收的 MATLAB 通路
UFMC 的发送端结构是:每个子带先做 IFFT,然后经过子带滤波器,最后所有子带累加。接收端做整个频段的 FFT,然后通过导频估计每个子带的频响。接收端不需要逐个滤波器匹配,这是和 FBMC 最大的区别。
%% UFMC 发送端 numUFMCSym = numSym; % 与 OFDM 符号数一致 txUfmc = zeros(Nfft + filterLen - 1, numUFMCSym); txUfmc = zeros(Nfft + filterLen - 1, numUFMCSym); for n = 1:numUFMCSym ufmcSymbol = zeros(Nfft, 1); for sb = 1:numSubbands % 取出该子带对应的频域数据 bandIdx = (sb-1)*subbandSize + 1 : sb*subbandSize; bandData = symbols(bandIdx, n); % 对应部分填入频域 ufmcSymbol(bandIdx) = bandData; % 然后做 IFFT 并滤波 timeBand = ifft(ufmcSymbol); % 该子带时域信号 filteredBand = conv(timeBand, windowedFilter(:, sb)); % 子带滤波 % 累加,长度对齐 txUfmc(1:length(filteredBand), n) = txUfmc(1:length(filteredBand), n) + filteredBand; end end txUfmc = txUfmc(:);这里用conv做滤波,输出长度是Nfft + filterLen - 1,所以每个符号之间不需要额外加 CP,符号间的前后延展就是滤波器长度造成的。接收端直接把整个数据做 FFT,因为滤波器长度远小于符号长度,可以近似认为子带滤波不影响子载波正交性,只需要做一步简单的频域均衡。
%% UFMC 接收端 rxUfmc = rxSignal; % 接收信号 % 分段,每段长度 Nfft,符号周期为 Nfft rxSeg = reshape(rxUfmc(1:numSym*Nfft), Nfft, numSym); freqRxUfmc = fft(rxSeg, Nfft); % 使用导频估计频响再均衡 pilotRxUfmc = freqRxUfmc(pilotIdx, :); H_ufmc = pilotRxUfmc ./ knownPilot; % 对每个子带做独立均衡 H_fullUfmc = interp1(pilotIdx, H_ufmc, 1:Nfft, 'linear', 'extrap'); eqUfmc = freqRxUfmc ./ H_fullUfmc; bitsOutUfmc = qamdemod(eqUfmc, M, 'OutputType', 'bit');UFMC 接收端的均衡与 OFDM 几乎一样,只是数据段长度不再包括 CP,而是靠滤波器自身吸收多径。因此低时延场景下 UFMC 比 OFDM 有明确的效率优势。注意这里没有做任何同步,真实环境中符号边界必须通过前导或者零填充段检测。
4.4 与 OFDM 共用信道估计:UFMC 的导频映射
UFMC 导频同样是插在特定子载波上,与 OFDM 兼容。但由于子带滤波会在导频周围产生频率扩展,导频子载波不能位于子带边缘,否则估计会有偏差。常见做法是每个子带内偏离 2-3 个子载波放置导频,这样能避开滤波器边缘的过渡带。在写导频图案时,把这个偏移写死,而不是简单用1:4:Nfft。
%% UFMC 导频放置:避开子带边缘 pilotIdxUfmc = []; for sb = 1:numSubbands bandStart = (sb-1)*subbandSize + 1; % 取子带中间位置放置导频 pilotInside = bandStart + floor(subbandSize/4) : subbandSize/2 : bandStart + subbandSize - floor(subbandSize/4); pilotIdxUfmc = [pilotIdxUfmc, pilotInside]; end这样设计导频图案后,在 10 MHz 带宽、30 kHz 子载波间隔下,子带边缘效应的影响可以忽略。如果强行用均匀导频,频谱泄漏会在导频位置引入额外误差,导致均衡后的星座点出现系统性偏转。
5. 同一套信噪比下对比三种波形:频谱、PAPR 与误码率
5.1 统一仿真基准:参数、信道和同步策略怎么对齐
三种波形仿真对比时,容易犯的错误是各自用各自觉得友好的参数,结果比出来没有说服力。我一般会把参数统一成同一组:子载波 64、调制方式 16QAM、信道用 5 径衰落模型、信噪比从 0 dB 扫到 20 dB。FBMC 使用 OQAM 时,实际数据速率和 OFDM 相同,因为符号率翻倍但每符号一半数据。UFMC 的滤波器长度为 32,FBMC 的 K=4,CP 长度为 16。
下面这张表给出推荐的统一参数,直接复制到仿真脚本顶部即可。
| 参数 | OFDM | FBMC | UFMC |
|---|---|---|---|
| FFT 点数 | 64 | 64 | 64 |
| 调制阶数 | 16QAM | 16QAM 使用 OQAM | 16QAM |
| 保护间隔 | CP 16 采样点 | 无(滤波器自然延展) | 零填充 16 采样点 |
| 滤波器长度 | 无 | 256 | 32 |
| 子带数 | 无(全频带) | 无(子载波级滤波) | 4 子带 |
| 等效频谱效率 | 64/(64+16)=0.8 | 1 | 64/(64+16)=0.8 |
| 同步复杂度 | 中 | 高 | 中 |
5.2 频谱泄漏与带外辐射的定量对比
频谱泄漏是三种波形最直观的差异。用pwelch估计功率谱密度,放在一张图里对比。
%% 频谱对比 fs = Nfft; % 采样率近似于子载波数 [psdOfdm, f] = pwelch(txSignal, 256, 128, 512, fs, 'centered'); [psdFbmc, ~] = pwelch(txFbmc, 256, 128, 512, fs, 'centered'); [psdUfmc, ~] = pwelch(txUfmc, 256, 128, 512, fs, 'centered'); plot(f, 10*log10(psdOfdm), 'b', f, 10*log10(psdFbmc), 'r', ... f, 10*log10(psdUfmc), 'g', 'LineWidth', 1.2); legend('OFDM', 'FBMC', 'UFMC'); xlabel('频率 (Hz)'); ylabel('PSD (dB)');从频谱图上可以更直观地看到几种差异。OFDM 在频带外有明显的旁瓣起伏,衰减慢,在归一化频率 ±0.5 附近仍有较高功率泄漏。FBMC 在主瓣外几乎没有能量,特别适合非连续频谱的场景。UFMC 介于两者之间,在子带边缘有明显过渡但不至于有长拖尾。如果在工程设计里决定用非连续频谱资源块,FBMC 可以省掉额外的滤波器硬件。
5.3 BER 曲线对比与复杂度估算
误码率曲线要跑蒙特卡洛仿真。OFDM 和 UFMC 的均衡方式相近,FBMC 需要理想信道假设才能保持性能。下面给出 BER 仿真主循环的骨架,使用统一信道。
%% 误码率仿真循环 snrRange = 0:5:20; berOfdm = zeros(size(snrRange)); berFbmc = zeros(size(snrRange)); berUfmc = zeros(size(snrRange)); numTrials = 50; for i = 1:length(snrRange) snr = snrRange(i); berSum = zeros(3, 1); for trial = 1:numTrials % 生成信道:5 径瑞利多径 chGain = (randn(1,5) + 1j*randn(1,5)) / sqrt(5); % 发送经过信道并加噪声 rxOfdm = filter(chGain, 1, txSignal); rxOfdm = awgn(rxOfdm, snr, 'measured'); % OFDM 解调并统计误码率 berSum(1) = berSum(1) + sum(bits ~= bitsOutOfdm) / length(bits); % 同样处理 FBMC 和 UFMC end berOfdm(i) = berSum(1) / numTrials; % ... end semilogy(snrRange, berOfdm, 'bo-', snrRange, berFbmc, 'r^-', snrRange, berUfmc, 'gs-');跑完可以看到,在没有同步误差的理想信道条件下,OFDM 和 UFMC 的 BER 接近,FBMC 则需要更高的信噪比才能达到同样误码率,因为 OQAM 的实部提取在频选信道下会产生额外干扰。但这不是说 FBMC 差,而是因为 FBMC 的均衡问题没有在仿真里解决。复杂度方面,在 MATLAB 里用tic/toc统计运行时间:OFDM 最快,UFMC 比 OFDM 慢约 1.5 倍,FBMC 慢 3-4 倍。
| 波形 | 相对运行时间 | 频谱效率 | 带外泄漏 | 同步难度 |
|---|---|---|---|---|
| OFDM | 1x | 0.8 | 高 | 低 |
| FBMC | 3-4x | 1.0 | 极低 | 高 |
| UFMC | 1.5x | 0.8 | 低 | 中 |
5.4 从仿真结论反推选型:什么场景用 FBMC,什么场景用 UFMC
从仿真结果看,三种波形的取舍其实很清楚。FBMC 适合频谱资源碎片化严重的场景,比如认知无线电或动态频谱共享,因为它的旁瓣抑制能有效保护相邻的空闲频段,不需要额外的频谱感知保护带。UFMC 适合机器通信和低时延场景,它不牺牲频谱效率但复杂度可控,尤其适合短包突发传输。OFDM 依然是最稳的选择,它对同步误差容忍度最高,所以 5G NR 才会继续用它。
对于具体工程落地,我更多会把仿真重心放在实际信道的鲁棒性检验上,而不是单纯比 BER。要加入 CFO、时偏、相位噪声等非理想因素,才能看出波形选型差异的真正价值。
6. 把仿真过程录成操作视频:MATLAB 录像与注释模板
6.1 用 VideoWriter 录制实时绘图
做仿真时,中途改动一个参数重新跑一遍,往往要几分钟甚至更久。把仿真过程录下来,尤其是波形对比和参数变化前后效果,能提升效率也方便多人协作。MATLAB 里用VideoWriter配合getframe录制实时图窗口变化。
%% 录制仿真过程 videoFile = VideoWriter('waveform_comparison.avi'); videoFile.FrameRate = 10; % 每秒钟 10 帧 open(videoFile); for snrIdx = 1:length(snrRange) % 更新当前图形 semilogy(snrRange(1:snrIdx), berOfdm(1:snrIdx), 'bo-', ... snrRange(1:snrIdx), berFbmc(1:snrIdx), 'r^-', ... snrRange(1:snrIdx), berUfmc(1:snrIdx), 'gs-'); xlim([0 max(snrRange)]); ylim([1e-4 0.5]); drawnow; % 捕获当前帧并写入视频 frame = getframe(gcf); writeVideo(videoFile, frame); end close(videoFile);把getframe放在绘图之后、drawnow之前容易录到旧画面,所以先drawnow再getframe。帧率不是越高越好,10 FPS 左右人眼看起来清晰且文件体积不会太大。录制结束后用close(videoFile)释放文件句柄,否则后续无法打开该文件。
6.2 代码注释模板:让别人不看录像也能复现
录像能辅助讲解,但代码注释才是长期可维护的关键。我习惯在每个函数头部写清楚参数单位、取值范围和由来,并在关键步骤标出如果修改会带来什么副作用。下面给出一个推荐的函数头注释模板。
function [ber, h] = compare_waveforms(snrDb, channelPath) % compare_waveforms 对比 OFDM/FBMC/UFMC 在指定信道下的误码率 % 输入参数: % snrDb - 信噪比,单位 dB,建议范围 0 到 20 % channelPath - 信道冲激响应,大小为 1xL,需归一化功率 % 输出参数: % ber - 1x3 数组,分别为 OFDM、FBMC、UFMC 的误码率 % h - 频域信道估计算法的句柄,便于后续扩展 % 依赖函数: % phydyas_filter,ufmc_filter_design % 版本: 2024-12-01 初版模板里写清楚版本日期,方便回溯。参数取值范围要精确,比如snrDb写建议范围 0 到 20,而不是“任意值”,这样使用者会主动考虑边界情况。仿真的核心是参数可复现,所以注释的优先级高于代码本身。
用 MATLAB 的live script导出 HTML 报告,也可以把录像、代码和结果整合在一个文件里分享给团队。做法是在实时编辑器里插入按钮运行不同参数组,脚本运行完后自动生成对比图和视频文件,这样团队其他人不需要手动改参数就能看到完整过程。
本文还有配套的精品资源,点击获取