☰
2FSK调制解调Python仿真:参数设计、误码率与解调边界
2026/10/31 13:14:21 网站建设 项目流程

简介:这份面向通信工程与电子信息类学生的课程设计文档,围绕2FSK调制与解调系统展开,重点解决MATLAB7.0环境下数字通信链路建模、相干解调实现与性能评估问题。文档按信源、调制、发送滤波、信道、接收滤波、解调、信宿等模块组织,给出参数设置思路,并采用开关法产生2FSK信号、加性高斯白噪声信道和相干解调方案,最终通过仿真波形、眼图以及信噪比与误码率曲线分析系统性能。压缩包仅含1个doc文件,大小约579KB,篇幅紧凑,适合作为课程设计报告模板与实验参考。已有855人学习,说明其在同类课程设计中具有一定参考价值。读者可从中获取完整的系统框图、调制解调原理说明、MATLAB源程序片段与仿真结果分析,便于对照搭建仿真模型、理解关键参数含义,并排查波形异常与误码率曲线偏差等问题。

1. 一个答辩现场的2FSK:仿真跑通和讲清楚之间差着什么

某高校通信原理课程设计的验收现场,A同学把2FSK调制与解调系统的时域波形和频谱投到屏幕上,图很干净,误码率曲线也画了。老师只追问了两句:调制指数取了多少、你的Eb/N0是怎么和噪声方差对上的。答不上来,代码跑得再顺也证明不了信号链是通的。

2FSK这门课设真正的难点不在敲代码,而在四笔账:参数怎么定、调制器怎么保证相位连续、解调器怎么在噪声里判对、仿真曲线怎么和理论值对上。适合刚接手课设、想一次做对的同学,也适合手上有一套跑不通的旧代码、想找出问题出在哪一段的人。

2. 2FSK的参数账:调制指数、频偏和采样率先算清楚

动手之前先把参数定死,是省掉后面一大半返工的唯一办法。我见过太多人上来就写np.cos(2*np.pi*f*t),跑出来频谱一团糊,然后再回头猜是频率错了还是采样率不够。这一章把2FSK设计里必须显式给出的量列清楚:相位形态、调制指数、频偏、载波频率、采样率、每码元点数,以及它们之间互相牵制的约束。

2.1 相位不连续拼接和连续相位,先决定你要哪一种

2FSK的实现有两条完全不同的路子,选错了后面解调、频谱、带宽分析全部对不上。

第一条是相位不连续(也叫相位跳变型)。做法是准备两个独立振荡器,频率分别是 $f_1$ 和 $f_2$,来一个比特就切换一次。数学上写成:

$$s(t)=\sum_n \cos(2\pi f_{i_n} t)\cdot \mathrm{rect}(t-nT_b),\quad i_n\in{1,2}$$

关键在于每个码元内部都是 $\cos(2\pi f t)$ 从相位 0 重新起跳,码元交界处相位会突变。这最贴近“两个硬件振荡器加一个开关”的物理模型,也是大多数教材画原理图时的那一版。

第二条是连续相位(CPFSK)。把比特映射成一段频率序列,对频率积分得到相位:

$$\phi(t)=2\pi\int_0^t f(\tau),d\tau,\quad s(t)=\cos\phi(t)$$

相位是连续的,频谱滚降比跳变型快得多,带外泄漏小。代价是你没法用“两路独立振荡器”去解释它,仿真时也不能按码元分块独立生成。

课程设计该选哪个?我一般建议:主报告用相位不连续版本,因为它和解调端的“两路相关器/两路带通滤波器”结构一一对应,讲得通;进阶部分补一段连续相位对比,用频谱图说明旁瓣抑制的差异,这一页在报告里很值钱。两套代码都要写,但花的时间差不多,第二套只是换了个相位生成方式。

2.2 调制指数 h 与频偏 Δf:正交条件与带宽的取舍

调制指数定义成 $h = 2\Delta f / R_b$,其中 $\Delta f=|f_2-f_1|/2$ 是相对中心频率的频偏,$R_b$ 是码元速率。这个量是2FSK的“玄学参数”,几乎所有关于带宽、正交性、误码率的讨论都绕不开它。

先说正交性。相干解调下,两路信号要在一个码元内正交,需要满足

$$2\Delta f = n R_b,\quad n=1,2,3\dots$$

取 $n=1$ 是最小频偏,也就是 $h=1$。这时候两路波形在一个码元内的相关系数为零,相干解调器能完全分离两路,误码率最低。取 $h<1$ 会出现相关性,误码率上升;取 $h$ 更大(比如 2、3),正交性依然成立,但占用带宽成倍增加。

再说带宽。2FSK的经验带宽估算用卡森公式:

$$B \approx 2\Delta f + 2R_b = (h+2)R_b$$

$h=1$ 时 $B\approx 3R_b$,这是性价比最高的点。有些同学为了让时域波形“看起来两个频率差得明显”,直接把 $\Delta f$ 拉到 $5R_b$,结果频谱占了半个奈奎斯特区,后面加个带通滤波器都难做。

提示:课程设计报告里一定要把 $h$ 的值写出来并说明取值理由。$h=1$ 对应最小正交频差,是最标准的答案;如果老师要求做对比,就再加一组 $h=0.5$(非正交)和 $h=2$(宽带正交),三组误码率曲线放一张图里,说服力完全不同。

2.3 采样率、载波频率、每码元点数的一张参数表

参数之间互相咬合,最好是先定 $R_b$ 和 $f_s$,再推其他。我常用的一套参数如下,量级小、跑得快、频谱看得清楚:

参数符号取值取值依据
码元速率$R_b$1000 bit/s课程设计常用量级,1 ms 一个码元
采样率$f_s$100 kHz每码元 100 点,$f_s \ge 8f_{\max}$
中心载频$f_c$10 kHz约为 $f_s/10$,每载波周期 10 点
频偏$\Delta f$500 Hz配合 $R_b$ 使 $h=1$
两个频率$f_1,f_2$9.5 kHz / 10.5 kHz$f_c \mp \Delta f$
每码元点数$SPS$100$f_s/R_b$

几个校验点。第一,最高频率分量是 $f_2+\text{旁瓣}$,采样率至少要给到 $2f_2$ 以上,实际取 8~10 倍最稳妥,这里 100 kHz 对 10.5 kHz 约 9.5 倍,够用。第二,每码元 100 个采样点,意味着一路相关器就是一个 100 点的乘加,数值上非常干净,也不会有累积误差。第三,$f_1$ 和 $f_2$ 都在 $f_s/2$ 之下很远,不存在混叠风险。

如果老师给的指标是固定的(比如 $R_b=2400$ bit/s),那就按同样比例缩放:$f_s$ 取 $R_b$ 的整数倍(64 或 100 倍都行),$f_c$ 取 $f_s/10$ 附近,$\Delta f=R_b/2$。唯一不能动的是 $h=1$ 这条约束,它决定了 $\Delta f$ 和 $R_b$ 的绑定关系。

3. 从比特流到2FSK波形:Python调制的三套写法

参数定了,接下来把比特流变成可以直接拿去加噪声、做FFT的数组。这一章的三段代码是我反复用过的骨架:比特生成、两种相位形态的调制、频谱核对。每段都能单独跑,也都能拼起来。

3.1 比特流与码元索引:先把时间轴对齐

最容易被忽略的一步是时间轴。采样点总数必须是 $N_{\text{bit}}\times SPS$,多一个少一个点,后面reshape就会错位,误码率凭空变高,还查不出来。先把常量集中定义,再用整数除法算 $SPS$,不要用浮点。

import numpy as np RB = 1000 # 码元速率 bit/s FS = 100_000 # 采样率 Hz FC = 10_000 # 中心载频 Hz DF = 500 # 频偏 Hz SPS = FS // RB # 每码元采样点数,必须整除 F1, F2 = FC - DF, FC + DF assert FS % RB == 0, "采样率必须是码元速率的整数倍" assert FS > 8 * F2, "采样率相对最高频率余量不足" def gen_bits(n, seed=7): # 固定种子保证每次跑出来的比特流一致,便于复现和对比 return np.random.default_rng(seed).integers(0, 2, n)

FS % RB == 0这条断言看着多余,实际能救你:一旦有人把 $R_b$ 改成 2400 而 $f_s$ 还是 100 kHz,整除立刻失败,你能第一时间发现,而不是等到误码率对不上再去猜。gen_bits里固定种子是为了让“同一组比特、不同信噪比”的对比成立,扫描误码率时尤其重要——换种子会让两条曲线抖动得看不出趋势。

3.2 相位不连续实现:两个振荡器按位切换

按位循环,每来一个比特就选一个频率,码元内部从相位 0 重新起跳。这种写法和硬件框图一一对应,讲课设时最好讲。

def fsk_discontinuous(bits, fs=FS, f1=F1, f2=F2, sps=SPS): n = len(bits) out = np.empty(n * sps) t = np.arange(sps) / fs # 一个码元内的时间轴,长度 sps for i, b in enumerate(bits): f = f2 if b == 1 else f1 # 每个码元都从相位 0 起跳,这就是"相位不连续"的来源 out[i * sps:(i + 1) * sps] = np.cos(2 * np.pi * f * t) return out

t只算一次并复用,避免在循环里重复构造数组。out用np.empty预分配,长度严格等于 $n\times sps$。循环体里没有累加相位,所以码元边界一定跳变——这不是bug,是特性,频谱图上的旁瓣就是这么来的。

顺带说个血泪经验:如果写成np.cos(2*np.pi*f*np.arange(n*sps)/fs)一次性生成,虽然更快,但那是“两个频率各自铺满全时长再拼接”的写法,边界相位同样跳变,效果一样,只是可读性差、不好按码元做门控。我建议保留按位循环的版本,报告里贴代码也更好讲。

3.3 连续相位实现:频率序列做相位累加

连续相位版本的核心是把频率展开成与采样点等长的序列,然后做累积求和得到相位。这里没有任何按码元分块的操作,整条波形是一个整体。

def fsk_continuous(bits, fs=FS, f1=F1, f2=F2, sps=SPS): # 每个比特复制 sps 份,得到逐采样点的瞬时频率 freq = np.repeat(np.where(bits == 1, f2, f1), sps) # 相位是频率的积分:phi[k] = 2*pi*sum(freq[0..k])/fs phase = 2 * np.pi * np.cumsum(freq) / fs return np.cos(phase) def fsk_continuous_baseband(bits, f1=F1, f2=F2, sps=SPS): # 复基带版本:只保留相位,去掉载波,后面扫BER时直接用 freq = np.repeat(np.where(bits == 1, f2 - FC, f1 - FC), sps) phase = 2 * np.pi * np.cumsum(freq) / SPS # 归一化到码元速率 return np.exp(1j * phase)

np.cumsum(freq)/fs这一步就是在做数值积分,离散形式是 $\phi[k]=\phi[k-1]+2\pi f[k]/f_s$,cumsum恰好等价。要注意freq里存的是绝对频率,累积出来的相位绝对值很大(几万弧度),但np.cos只关心相对值,浮点精度在双精度下完全够,不用担心。

第二个函数是给后面第 6 章准备的:把载波去掉,只留 ±$\Delta f$ 的频偏,这样仿真步长可以放大、点数可以减到几千,扫 10 个信噪比点几秒钟就跑完。带通仿真和基带等效仿真的误码率应该重合,这本身就是一次很好的验证。

3.4 FFT核对:两个峰必须落在 f1 和 f2

波形生成完别急着往下走,先做一次频谱核对。这一步花 30 秒,能提前发现 80% 的参数错误。

import matplotlib.pyplot as plt def spectrum_db(x, fs=FS): w = np.hanning(len(x)) # 加窗压旁瓣,避免强信号盖住弱分量 X = np.fft.rfft(x * w) f = np.fft.rfftfreq(len(x), 1 / fs) mag = 20 * np.log10(np.abs(X) / np.max(np.abs(X)) + 1e-12) return f, mag bits = gen_bits(200) x_nc = fsk_discontinuous(bits) x_cp = fsk_continuous(bits) f, m1 = spectrum_db(x_nc) _, m2 = spectrum_db(x_cp) plt.plot(f, m1, label="phase-discontinuous") plt.plot(f, m2, label="CPFSK") plt.xlim(0, 25_000); plt.ylim(-60, 3) plt.xlabel("Hz"); plt.ylabel("dB"); plt.legend() plt.show()

正确的结果是:主瓣出现在 9.5 kHz 和 10.5 kHz 附近,不连续版本在两侧还有明显的旁瓣拖着,连续相位版本滚降快至少十几 dB。如果峰跑到 5 kHz 或 20 kHz,先查F1/F2有没有被后面的代码覆盖;如果只有一个峰,多半是比特流全是 0 或全是 1,gen_bits的种子给错了;如果峰很宽糊成一片,那就是采样率不够或者点数太少导致频率分辨率不足。

rfftfreq(len(x), 1/fs)的分辨率是 $f_s/N$,200 个比特 20000 点,分辨率 5 Hz,分辨 1 kHz 的频差绰绰有余。做报告图时把点数提到 2000 比特以上,频谱会更干净。

4. 解调端:相干、非相干和过零检测的代码与边界

调制出来的波形谁都会写,真正拉开差距的是解调。三种做法在课设里都会被提到,但它们的适用条件和代码结构差别很大,混着写就会得到一个“看起来能跑、换个信噪比就崩”的解调器。

4.1 相干相关器:结构最简单,前提最苛刻

相干解调的前提是收发端载波同频同相。课程设计里我们一般直接假设这个条件成立(不做载波恢复),只验证“给定理想同步,误码率能不能达到理论值”。结构上就是两个相关器:把接收信号分别与 $\cos(2\pi f_1 t)$、$\cos(2\pi f_2 t)$ 相乘并在一个码元内积分,比大小。

def demod_coherent(rx, fs=FS, f1=F1, f2=F2, sps=SPS): n = len(rx) // sps blocks = rx[:n * sps].reshape(n, sps) # 每个码元一行 t = np.arange(sps) / fs c1 = np.cos(2 * np.pi * f1 * t) c2 = np.cos(2 * np.pi * f2 * t) y1 = blocks @ c1 # 相关器:乘加,等价于积分 y2 = blocks @ c2 return (y1 < y2).astype(int) # y2 大判 1,否则判 0

blocks @ c1用的是矩阵乘,等价于对每一行做内积,也就是“相乘后求和”。这一步同时完成了混频、低通和抽样三个动作,比真的去调scipy.signal.butter再滤波干净得多,也没有群时延问题。n = len(rx)//sps保证 reshape 不会报错,最后不足一个码元的尾巴直接丢掉。

判决策略是y1 < y2时判 1。注意这是相对比较,不是拿某个绝对门限去卡,所以信号整体放大缩小不影响结果。这一点在第 5 章讲门限坑的时候还会回来。

4.2 非相干正交下变频取包络:工程上最常用

相干解调要求相位对齐,实际系统里很难做到。非相干解调只看包络,对相位不敏感,代价是同样的信噪比下误码率略高(大约差 1 dB)。实现方式是用复指数做下变频再取模:

def demod_noncoherent(rx, fs=FS, f1=F1, f2=F2, sps=SPS): n = len(rx) // sps blocks = rx[:n * sps].reshape(n, sps) t = np.arange(sps) / fs e1 = np.exp(-2j * np.pi * f1 * t) e2 = np.exp(-2j * np.pi * f2 * t) y1 = np.abs(blocks @ e1) # 取模 = 正交两路包络合成 y2 = np.abs(blocks @ e2) return (y1 < y2).astype(int)

np.exp(-2j*pi*f*t)是复本振,实部对应 I 路、虚部对应 Q 路,blocks @ e得到的是一个复数,取np.abs就是两路平方和开根号,正是包络检波的结果。和 4.1 的代码相比只多了一个abs和复指数,但鲁棒性完全不是一个量级:给接收信号乘任意相位因子 $e^{j\theta}$,4.1 的结果会乱,4.2 的结果纹丝不动。

这个性质在第 6 章的相位旋转实验里会被用来做对照,也是报告里“为什么选非相干”的有力论据。

4.3 过零检测:能过答辩,但只在干净信道里成立

过零检测是老教材里必提的一种方法:2FSK 两个频率对应不同的过零密度,数一个码元内过零次数就能判决。实现起来只有三行:

def demod_zero_cross(rx, sps=SPS, thresh=20): n = len(rx) // sps blocks = rx[:n * sps].reshape(n, sps) # signbit 转 0/1,相邻差分绝对值求和就是过零次数 zc = np.sum(np.abs(np.diff(np.signbit(blocks).astype(int), axis=1)), axis=1) return (zc > thresh).astype(int)

门限 20 是这么来的:$f_1=9.5$ kHz,码元 1 ms,约 9.5 个周期,双向过零约 19 次;$f_2=10.5$ kHz 约 21 次。取中间值 20 最合理。

但这个方法非常脆弱。加一点点噪声,过零点就会在真实零点附近来回抖,一次抖动就是两次额外的过零计数,判错是必然的。我实测下来,信噪比低于 15 dB 之后误码率就奔着 10⁻¹ 去了,而且它不是平滑恶化,是断崖式失效。所以我的建议是:过零检测只用来做原理演示,报告里写清楚它的适用条件是高信噪比、无滤波、无多径,然后主体误码率曲线用 4.1 或 4.2 的结果。把这条边界写明白,反而是加分项。

4.4 判决门限和抽样时刻:两个必须显式写出来的量

上面三段代码有个共同点:抽样时刻都取在码元块的末尾(因为reshape之后整块做完相关就出结果,相当于在码元内做了完整积分)。这在理想情况下没问题,但如果中途加了滤波器,输出会整体后移,抽样点就落在了码元边缘甚至下一个码元上,误码率立刻抬高一个数量级。

解决办法是显式引入抽样偏移:

def sample_with_offset(y, offset=0): # y: 每个码元的判决量;offset: 向后偏移的码元数(可为小数) idx = np.arange(len(y)) + int(round(offset)) idx = np.clip(idx, 0, len(y) - 1) return y[idx]

更稳妥的做法是在相关器之前就对blocks做滑动窗口,让窗口中心对齐码元中心。课程设计里如果不加滤波器,直接用整块相关就够了;一旦加了带通或低通,就必须在报告里写一句“已补偿滤波器群时延 $\tau_g$,抽样点后移 $\tau_g f_s$ 个采样点”。这句话写出来,说明你知道问题在哪。

判决门限同理。别用固定值 0.5 去卡y1,要用y1和y2的相对大小,或者用y1 - y2的符号。前者对幅度变化免疫,后者还顺带把直流偏置抵消了。这个习惯从课设带到工程里都不会错。

5. 五个真实翻车现场:2FSK仿真避坑与排查

以下五条是我在帮人看课设代码时反复遇到的,基本覆盖了 2FSK 仿真里 90% 的“跑不通”。每条按现象、原因、解决来写,对着查就行。

5.1 误码率曲线卡在 1e-2 不再下降

现象:Eb/N0 从 0 dB 扫到 10 dB,误码率一路下降,到 8 dB 附近就趴在 1e-2 上下不动了,再往上加信噪比也没用,曲线是一条水平线。

原因:这是典型的误码平台,八成是抽样时刻偏了。加噪声前波形是对的,加噪声后解调器里如果用了scipy.signal.lfilter做带通或低通,滤波器的群时延会把整个信号推后几十个采样点,而你还在按原来的码元边界切块,等于每个码元都取到了相邻码元的尾巴。信噪比越高,这个错位造成的错误占比越大,所以曲线会收敛到一个常数。

解决:先不加滤波器,用 4.1 或 4.2 的相关器直接跑一遍,看曲线能不能贴到理论值。如果能,说明问题确实在滤波器,那就查滤波器的群时延:butter设计的 FIR 用filtfilt做零相位滤波,IIR 就老老实实算出(N-1)/2的延迟并对blocks做偏移。还有一个更省事的办法:把滤波器的通带做宽一点,或者干脆改用相关器,因为相关器本身就是最优匹配滤波器,根本不需要额外滤波。

5.2 频谱上只有一个峰,或者峰的位置对不上

现象:FFT 出来只有一个明显的峰,或者两个峰挤在 10 kHz 附近分不开,跟参数表上写的 9.5/10.5 kHz 对不上。

原因:三种可能。一是比特流退化,全是 0 或者全是 1,那就只有一个频率;二是F1、F2在别处被重新赋值了,比如后面为了“让波形好看”改成了 1000/2000 Hz,而绘图时还按原参数标注;三是 FFT 点数太少,频率分辨率 $f_s/N$ 大于 1 kHz,两个峰糊成一个。

解决:先print(np.unique(bits, return_counts=True))确认比特流是均衡的;再print(F1, F2)确认常量没被覆盖;最后把len(x)提到 20000 点以上。还有一个隐蔽的坑:画频谱时如果没加窗,强信号(比如直流分量或者载波泄漏)的旁瓣会盖住另一路,看起来像只有一个峰——np.hanning那一行别省。

5.3 相干解调一加信道就崩

现象:不加噪声时相干解调误码率为 0,加一点点噪声之后误码率直接飙到 0.3 以上,比随机猜还差。

原因:相干解调假设收发载波同相。你的调制端用np.cos(2*np.pi*f*t),解调端也用同一个表达式,看起来是同步的。但只要中间经过了任何带相位响应的环节(滤波器、延迟、重采样),或者你为了“更真实”给信号乘了一个 $e^{j\theta}$,相位就偏了。相位偏 180° 时判决完全反过来,误码率接近 1,表现出来就是“比随机猜还差”。

解决:两个方向。要么老老实实做载波恢复(科斯塔斯环或者判决反馈),课设里工作量大,一般不做;要么就用非相干解调,并且把“相干解调需要理想同步”这句话写进报告的假设条件里。我一般会在报告里加一张对比图:理想相干、非相干、相位偏 30° 的相干,三条线放一起,一眼就能看出相干解调对相位有多敏感。这张图比写十行文字都管用。

5.4 过零检测在低信噪比下彻底失效

现象:高信噪比时过零检测误码率为 0,信噪比一降到 15 dB 以下,误码率突然跳到 0.2 以上,而且再怎么调门限都救不回来。

原因:这不是实现问题,是原理性的。过零检测本质上是在数频率,噪声会让信号在真实过零点附近反复穿越零轴,一次抖动贡献两个额外计数。码元内过零次数从 19/21 变成 25/17 这种离谱值,判决必错。而且这个误差不是高斯的,不会随信噪比平滑下降。

解决:接受它,别硬救。报告里把过零检测定位成“高信噪比下的原理验证”,误码率曲线只画到 15 dB,然后明确写“低信噪比下应采用相干或非相干相关解调”。如果老师要求必须有一条完整的过零检测曲线,那就在检测前加一级窄带带通滤波把噪声压下去,曲线能好看一些,但代价是引入了群时延,回到 5.1 那个坑。

5.5 加了滤波器之后判决点整体后移

现象:单独测滤波器输出是对的,波形完整、没有失真,但一接上解调器误码率就高,把判决点往后挪 20~30 个采样点就正常了。

原因:因果滤波器的群时延。scipy.signal.butter(4, ...)设计出来的 4 阶巴特沃斯,群时延大约几个到十几个采样点,阶数越高延迟越大。你按blocks = rx.reshape(n, sps)切块,切到的是延迟后的波形,码元边界全错位了。

解决:三个选项,按推荐顺序排。第一,用scipy.signal.filtfilt做零相位滤波,前后各滤一遍,相位响应抵消,延迟为零,代价是需要整段数据,不能流式处理——课设场景完全够用。第二,手动补偿:delay = (N-1)//2,切块时rx[delay:]。第三,也是最省事的,压根不用滤波器,相关器自带带通特性,匹配滤波就是最优的。

6. 把曲线推到能写进报告:基带等效扫BER和相位鲁棒性验证

最后一章讲两件让报告从“能跑”变成“有说服力”的事:用基带等效快速扫出误码率曲线并与理论值对照,以及用相位旋转实验定量说明相干解调的脆弱边界。

6.1 基带等效:省掉载波,扫一遍Eb/N0

带通仿真每码元 100 点,扫 10 个信噪比、每个信噪比 20000 比特,跑下来要几十秒。把载波去掉、只保留 ±500 Hz 的频偏,每码元 8 点就够,速度快一个数量级,结果完全一致。

from scipy.special import erfc import numpy as np def qfunc(x): return 0.5 * erfc(x / np.sqrt(2)) def ber_2fsk(ebn0_db_list, nbits=20000, sps=8, seed=1): rng = np.random.default_rng(seed) res = [] for db in ebn0_db_list: bits = rng.integers(0, 2, nbits) # 基带等效:只保留 +/-df 的相位旋转 df_norm = 0.5 # h=1 时 df*Tb = 0.5 freq = np.repeat(np.where(bits == 1, df_norm, -df_norm), sps) phase = 2 * np.pi * np.cumsum(freq) / sps tx = np.exp(1j * phase) # 每比特能量归一化为 1,噪声方差 = 1/(2*Eb/N0) 每维 ebn0 = 10 ** (db / 10) sigma = np.sqrt(1 / (2 * ebn0)) rx = tx + sigma * (rng.normal(size=len(tx)) + 1j * rng.normal(size=len(tx))) # 非相干解调:按码元取模比较 blk = rx.reshape(nbits, sps) t = np.arange(sps) / sps y1 = np.abs(blk @ np.exp(-2j * np.pi * (-df_norm) * t)) y2 = np.abs(blk @ np.exp(-2j * np.pi * (df_norm) * t)) ber_sim = np.mean((y1 < y2).astype(int) != bits) res.append((db, ber_sim)) return res

关键在噪声方差:每比特能量归一化成 1 之后,复高斯噪声每一维的方差是 $1/(2\cdot E_b/N_0)$,两维加起来正好是 $1/(E_b/N_0)$,信噪比才对得上。这一步错了,整条曲线会整体平移好几 dB,而且看不出来。

理论值方面,相干 2FSK 是 $P_b=Q(\sqrt{E_b/N_0})$,非相干 2FSK 是 $P_b=\tfrac12 e^{-E_b/(2N_0)}$。把仿真点和这两条理论线画在一张半对数图上,相干曲线应该贴着 $Q$ 函数,非相干曲线在相同 $E_b/N_0$ 下高出约 1 dB,这是教科书结论,对上了就说明整条链路是通的。

6.2 相位旋转实验:相干解调的脆弱点在哪

第二个实验更直观:给接收信号乘一个相位因子 $e^{j\theta}$,$\theta$ 从 0 扫到 $\pi$,看相干解调和非相干解调的误码率怎么变。

def phase_sweep_demo(theta_list, ebn0_db=8, nbits=20000, sps=8, seed=2): rng = np.random.default_rng(seed) out = [] df_norm, ebn0 = 0.5, 10 ** (ebn0_db / 10) bits = rng.integers(0, 2, nbits) freq = np.repeat(np.where(bits == 1, df_norm, -df_norm), sps) tx = np.exp(2j * np.pi * np.cumsum(freq) / sps) sigma = np.sqrt(1 / (2 * ebn0)) rx0 = tx + sigma * (rng.normal(size=len(tx)) + 1j * rng.normal(size=len(tx))) for th in theta_list: rx = rx0 * np.exp(1j * th) blk = rx.reshape(nbits, sps) t = np.arange(sps) / sps # 相干:取实部,对相位敏感 c1 = np.real(blk @ np.exp(-2j * np.pi * (-df_norm) * t)) c2 = np.real(blk @ np.exp(-2j * np.pi * (df_norm) * t)) ber_coh = np.mean((c1 < c2).astype(int) != bits) # 非相干:取模,对相位免疫 y1 = np.abs(blk @ np.exp(-2j * np.pi * (-df_norm) * t)) y2 = np.abs(blk @ np.exp(-2j * np.pi * (df_norm) * t)) ber_nc = np.mean((y1 < y2).astype(int) != bits) out.append((th, ber_coh, ber_nc)) return out

跑出来的形状很有意思:非相干误码率在整条 $\theta$ 轴上几乎是一条平线,相干误码率在 $\theta=0$ 附近最低,到 $\pi/2$ 附近恶化到 0.5,然后继续变差,到 $\pi$ 附近反过来变成“几乎全错”。这条曲线放在报告里,比任何文字说明都有力——它直接告诉你为什么实际系统不用理想相干解调,也告诉你为什么“相干解调假设收发载波同相”这句话必须写在假设条件里。

我自己踩过最深的一个坑,是刚开始做这个课设时把相干解调的相关器写成了np.real(blk @ np.exp(-2j*pi*f*t))却忘了相位,跑出来误码率一直是 0.5,查了整整一晚上,最后发现调制端用的是np.cos(实信号),解调端用复指数下变频后取了实部,两边的相位基准差了 90°。从那以后我养成了一个习惯:任何解调器的第一版,先不加噪声,用单频正弦喂进去,看判决量是不是干净的两个电平。这一步花五分钟,能省掉后面所有的猜谜时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询