简介:CPFSK在AWGN信道下的MATLAB仿真代码,面向通信原理与数字调制技术学习者,用于模拟连续相位频移键控的完整传输过程,并评估误码率性能。整个资源为1个m脚本构成的rar压缩包,仅4KB,代码结构紧凑,适合快速运行与二次修改。脚本实现了从二进制数据流生成、CPFSK连续相位调制、叠加AWGN噪声,到相关解调与BER统计的完整链路,并包含不同信噪比条件下的性能分析逻辑,可直接观察相位连续调制在抗噪声与抗多径衰落方面的特性。由于该代码对应信息技术领域的一项具体应用,对理解通信系统中信号处理与调制解调环节很有帮助。压缩包目前已有162人学习浏览,适合初学者结合教材或课程实验动手实践,也可作为通信系统设计中的基础参考模块。
1. 从berawgn.rar看 CPFSK 仿真该学什么
如果你在某个项目资料包或同学共享里解压过一个叫berawgn.rar的文件,里面多半是berawgn.m、berawgn_it.m这类脚本。名字拆开就是:CPFSK 调制,在 AWGN 信道下用蒙特卡洛法统计 BER,_it代表带迭代次数或逐步逼近的版本。这类脚本在通信算法验证里几乎是标准动作:先搭一个最小可仿真链路,然后把调制指数、过采样倍数、噪声功率这些参数来回调,看误码率曲线朝哪个方向移动。真正值得学的不是那份脚本本身,而是它背后的两个问题:一是 CPFSK 为什么不能像普通 FSK 那样直接用闭式误码率公式;二是当你自己写berawgn时,哪些参数设错了会让结果完全失真。适合看这篇内容的人包括做物理层预研的工程师、准备通信方向笔试或面试的学生,以及想从 BPSK/QPSK 仿真转向连续相位调制的算法人员。
2. 把 CPFSK 信号模型写成 MATLAB 代码:相位连续性和归一化噪声
2.1 CPFSK 的相位递推决定了它不能用解调端直接当 2FSK 处理
CPFSK 全称 Continuous-Phase Frequency Shift Keying,和普通频移键控最大的区别在于符号切换瞬间载波相位不跳变。信息承载在相位连续变化的路径上,而不是某个时刻的频率值上。复基带表达式可以写成:
$$s(t)=\sqrt{\frac{2E_b}{T}}\exp\left(j\left(2\pi h \sum_{k} a_k q(t-kT)+\phi_0\right)\right)$$
其中 $h$ 是调制指数,$a_k$ 是符号序列,$q(t)$ 是频率脉冲的积分。当 $q(t)$ 在符号周期内从 0 线性增长到完整值,就是常见的矩形频率脉冲 CPFSK。这个式子的关键点在求和项:当前符号的相位要在前一符号相位上累加,所以接收端的匹配需要有记忆。普通 2FSK 解调可以靠两个带通滤波器包络比较,但 CPFSK 这样做会损失欧氏距离,因此仿真里通常用基于相位网格的 Viterbi 序列检测。理解了这一点,你就明白berawgn脚本里必须用cpfskmod和cpfskdemod配合,而不是直接调fskmod。
2.2 最小链路:cpfskmod、AWGN、cpfskdemod三行代码
MATLAB 通信工具箱提供现成的 CPFSK 调制解调函数。最小可跑链路如下:
% 最小 CPFSK-AWGN BER 仿真链路 M = 2; % 二进制 CPFSK h = 0.5; % 调制指数,0.5 对应 MSK sps = 8; % 每符号采样数 dataLen = 10000; rng(1); data = randi([0 M-1], 1, dataLen); % 发送符号 0/1 x = cpfskmod(data, M, h, 'CONT', sps); % 连续相位调制 % 假设 Eb/No = 10 dB,折算成复基带噪声 EbNodB = 10; EbNolin = 10^(EbNodB/10); noiseVar = 1/(2*EbNolin*log2(M)*sps); noise = sqrt(noiseVar/2) * (randn(size(x)) + 1i*randn(size(x))); y = x + noise; est = cpfskdemod(y, M, h, 'CONT', sps); ber = sum(data ~= est) / dataLen;这段代码把cpfskdemod的输入输出长度默认当作对齐。逻辑上,cpfskmod的输入是符号序列,输出是复基带采样点;sps决定每个符号生成多少个采样。cpfskdemod使用 Viterbi 算法在整个序列上做最大似然序列检测,返回估计的符号序列。噪声生成那一行没有用awgn函数,原因是为了让噪声功率计算完全受控:cpfskmod输出的平均功率为 1,每个符号有sps个采样,因此把符号级 $E_b/N_0$ 折算成采样级噪声方差时需要除以sps和log2(M)。如果你用awgn(y, snr, 'measured')去试,会遇到信噪比定义不匹配的问题,最后画出来的 BER 曲线整体平移好几个 dB。
2.3 过采样倍数不是越大越好,但至少不能小于 4
cpfskdemod的 Viterbi 网格是在采样点上计算分支度量的。理论上过采样倍数越高,对连续相位路径的表征越精确,但仿真速度直线下降。工程上sps取 8 到 16 已经能覆盖绝大多数载波同步和多普勒场景。sps=1时虽然数学上仍然可用,但等效离散信道损失信息,BER 曲线在较高信噪比下会出现地板效应。另一个容易被忽略的点是cpfskmod的phaseType参数:'CONT'表示相位连续,'DIS'表示每个符号相位重置。做 CPFSK 仿真必须用'CONT',否则就退化成了普通 FSK。
3.berawgn_it主循环设计:错误数阈值、迭代上限与置信度
3.1 用while收集错误而不是固定发送 N bit
很多初学者写 BER 仿真时直接固定发 1e6 个比特,然后计算错误比例。这在低信噪比下没问题,但在高信噪比下可能出现错误数为零的情况,BER 曲线无法画出来。常见做法是设置错误数量阈值,例如累计到 100 个 bit 错误才停止。对应berawgn_it里的it语义:每个 SNR 点是一个迭代批次,批次内继续循环直到满足统计要求。
function ber = berawgn_cpfsk(h, sps, EbNodB, maxErr, maxBits) M = 2; blockLen = 20000; errors = 0; bits = 0; EbNolin = 10^(EbNodB/10); noiseVar = 1/(2*EbNolin*log2(M)*sps); while errors < maxErr && bits < maxBits data = randi([0 M-1], 1, blockLen); x = cpfskmod(data, M, h, 'CONT', sps); noise = sqrt(noiseVar/2) * (randn(size(x)) + 1i*randn(size(x))); est = cpfskdemod(x + noise, M, h, 'CONT', sps); errBlock = sum(data ~= est); errors = errors + errBlock; bits = bits + blockLen; end ber = errors / bits; end这里的循环逻辑是:每次生成固定 2 万个符号,做完调制、加噪、解调后统计错误。errors和bits都是累计量,达到任一上限就退出。maxBits作为安全阀,防止在极低信噪比下程序长时间跑不完。函数名berawgn_cpfsk可以看作是 RAR 包中berawgn主函数的一种整理形式。调用时把maxErr设为 100,得到的结果在工程上已经足够稳定。
3.2 置信度与表格:这个迭代上限该设多大
BER 估计本质上是对伯努利随机变量的均值估计,方差为 $p(1-p)/N$。相对标准差大约是 $\sqrt{(1-p)/(Np)}$。如果maxErr太小,相对误差会很大;如果太大,高信噪比点会跑很久。下表是常用参考值:
| 目标 BER | 最少错误数 | 至少需要的总比特数 | 相对标准差 |
|---|---|---|---|
| 1e-2 | 100 | 1e4 | 约 10% |
| 1e-3 | 100 | 1e5 | 约 10% |
| 1e-4 | 100 | 1e6 | 约 10% |
| 1e-4 | 400 | 4e6 | 约 5% |
| 1e-5 | 100 | 1e7 | 约 10% |
实际仿真时,每个信噪比点独立调用该函数,不同点之间因为随机数不同会有抖动。maxBits通常设为maxErr / 目标BER的 10 倍以上。如果你追求曲线平滑,可以在循环外固定随机种子,让每次运行可复现。
3.3 多信噪比扫描和曲线绘制
完成单点函数后,主脚本就非常简洁:
EbNodB = 0:2:12; BER = zeros(size(EbNodB)); maxErr = 100; maxBits = 2e7; for i = 1:length(EbNodB) BER(i) = berawgn_cpfsk(0.5, 8, EbNodB(i), maxErr, maxBits); end semilogy(EbNodB, BER, 'o-'); grid on; xlabel('E_b/N_0 (dB)'); ylabel('BER'); title('CPFSK over AWGN, h=0.5');绘制 BER 曲线时注意用semilogy而不是plot。横坐标已经是 $E_b/N_0$,没有额外换算,因为噪声功率在函数内部已经完成了从符号能量到采样能量的折算。观察曲线斜率是否符合香农限预期,是判断仿真是否可信的第一步。
4. 解调端与参数选择的三个硬核细节:Viterbi 对齐、调制指数与欠采样
4.1 相干解调里的符号对齐问题
cpfskdemod默认假设接收端已经完全知道初始相位和符号时序。但实际解调输出可能因为 Viterbi 网格的刷新长度引入延迟。收发序列没有对齐时,BER 会停在 0.5 左右,即使信噪比已经很高。用下面的方法检查对齐:
est = cpfskdemod(y, M, h, 'CONT', sps); % 做前后各 4 个符号的互相关,找出最佳位移 maxShift = 4; % 一般默认延迟就是 0,这里做防御 errHist = zeros(1, 2*maxShift+1); for shift = -maxShift : maxShift if shift >= 0 errHist(shift+maxShift+1) = sum(data(1:end-shift) ~= est(shift+1:end)); else errHist(shift+maxShift+1) = sum(data(1-shift:end) ~= est(1:end+shift)); end end [~, bestIdx] = min(errHist); bestShift = bestIdx - maxShift - 1;这段代码把解调结果的位移逐点搜索一遍,最佳位移处错误数最小。绝大多数正确配置的 CPFSK 链路延迟为 0,但你在整合多段仿真代码时,可能因为前一个模块引入了延迟块,导致这里出错。如果你的berawgn代码是从别人手里传下来的,先跑这个对齐检查比调一天噪声功率都有用。
4.2 调制指数 h 与 BER 性能之间的取舍
调制指数 $h$ 直接影响信号带宽和欧氏距离。$h=0.5$ 时是 MSK,频谱效率高,但最小欧氏距离小于正交信号;$h=1$ 时相邻频率间隔等于符号速率,接近正交 FSK。下表列出常见取值的工程倾向:
| h 值 | 谱效率 | 接收复杂度 | 相对性能 |
|---|---|---|---|
| 0.3 | 高 | 网格状态多,频偏敏感 | 较差 |
| 0.5 | 高 | 适中,最常用 | 接近 MSK 理论 |
| 0.7 | 中 | 适中 | 性能提升有限 |
| 1.0 | 低 | 可简化成频域能量检测 | 接近正交 FSK |
对做链路仿真的工程师,建议先用 $h=0.5$ 把整个流程跑通,再改成目标系统的指定值。不要同时调 $h$ 和sps,否则看到 BER 曲线变化很难定位是哪一项引起的。另外调制指数不是任意小数都行,考虑接收端载波恢复环路的拉普拉斯带宽,过小会让相位差分判决的容限急剧降低。
4.3 过采样不足带来的地板效应
sps至少要为信号最高频率分量的 2 倍以上。CPFSK 的瞬时频率与符号速率相关,当 $h=1$ 且符号速率为 $R_s$ 时,基带信号瞬时频率最大偏移约 $h R_s/2$,再加上残余相位抖动,sps=4是最低建议。如果sps=2,可以看到 BER 曲线在高信噪比下开始弯曲,这和调制本身无关,而是采样点不足导致 Viterbi 分支度量丢失了相位路径信息。修复方法很简单:把sps调到 8 或 16,观察曲线是否继续下降。如果你的berawgn脚本里有sps=1而 BER 又好得不正常,大概率是加噪声时把过采样因子遗漏了,功率算错会带来虚高的性能。
5. 用半解析界验证仿真曲线,并给berawgn_it加可复现保护
5.1 用最小欧氏距离近似做理论参照
CPFSK 的严格 BER 闭式表达式通常不存在,但可以用最小欧氏距离 $d_{\min}$ 构造上界。二进制 CPFSK 在高信噪比下有近似形式:
$$P_e \approx Q\left(\sqrt{d_{\min}^2 \cdot \frac{2E_b}{N_0}}\right)$$
其中 $d_{\min}^2$ 是归一化最小平方距离。对 $h=0.5$,数值约为 2.0,因此理论近似曲线是qfunc(sqrt(2*EbNolin*2))。把仿真曲线和这一近似画在同一张图上,如果高信噪比处两者斜率一致,说明仿真链路正确;如果差到 3 dB 以上,优先检查噪声功率折算。
EbNolin = 10.^(EbNodB/10); theoryMSK = qfunc(sqrt(2*EbNolin*2)); semilogy(EbNodB, BER, 'o-', EbNodB, theoryMSK, '--');这里theoryMSK对应的是 MSK 在高信噪比下的一个近似参照,不是精确值。仿真的二进制 CPFSK 在 $h=0.5$ 时理论曲线和这个近似会在信噪比大于 8 dB 后贴合。如果不贴合,而且永远偏差一个固定量,通常不是代码逻辑错,而是噪声方差里少除了sps。
5.2 随机种子管理和并行仿真的子流隔离
蒙特卡洛仿真可复现性依赖随机数序列。常见做法是在主脚本开头写rng(42, 'twister'),但这样做每个信噪比点使用同一随机流,高低信噪比之间的 BER 波动会被随机序列相关性污染。更稳妥的是给每个信噪比点分配独立分隔的流:
sc = parallel.pool.Constant(RandStream('Threefry')); for i = 1:length(EbNodB) stream = sc.Value; stream.Substream = i; BER(i) = berawgn_cpfsk_stream(stream, 0.5, 8, EbNodB(i), maxErr, maxBits); end用Substream隔离的好处是即使你把循环改成parfor,每个 worker 拿到的流仍然是确定性的,仿真结果不会因为现场调度不同而不可复现。对于标题里的_it版本,迭代次数和随机种子应该作为函数参数暴露出来,而不是写死在脚本里。
5.3 用berfit平滑离群点
仿真数据在低信噪比下通常很稳定,但是在目标误码率附近会有抖动。MATLAB 的berfit可以对 BER 曲线做最小二乘拟合,把离群值平滑掉,用于给链路预算提供一条单调的 BER 曲线。
berFit = berfit(EbNodB, BER); semilogy(EbNodB, berFit, 'k-', 'LineWidth', 1.5);注意berfit是经验拟合,不要把它当成理论结果。最合理的用法是:先用它判断哪些点没有落入预期斜率;然后把maxErr从 100 提高到 400,重新跑那几个异常点。最终你需要交付的不是一张好看的图,而是每个 SNR 点的总仿真比特数、错误数和随机种子,这才是berawgn_it这类迭代脚本里真正有价值的信息。
本文还有配套的精品资源,点击获取