简介:一套基于MATLAB的VSB(残留边带)调制解调仿真代码,面向数字通信、广播电视技术方向的初学者和研究人员,用于理解VSB调制原理、解调方法及系统性能分析。压缩包内共3个m脚本文件,整体大小仅5KB,代码精简,包含从基带信号生成、VSB调制、加噪到匹配滤波解调与误码率统计的完整流程,适合作为课程实验或自学演示的起点。已有422人学习,资源结构清晰,便于对照描述中的理论步骤逐段运行调试。通过调整信噪比SNR等参数,可直观观察残留边带调制在不同信道条件下的解调效果与误码率变化,帮助读者将教材中的VSB公式快速转化为可运行的MATLAB实现。
1. VSB调制解调仿真:留多少边带,是滤波器设计决定的工程题
VSB(Vestigial Sideband,残留边带)调制解调仿真,看起来是教科书上的一道课后题,实际一跑起来马上会碰到真正的系统问题:滤波器怎么给、残留边带留多宽、解调噪声从哪里进来。如果在做数字电视8VSB的simulation链路,或者用软件无线电验证调制解调方案,就会发现VSB的价值不在理论曲线好看,而在它能用比DSB更窄的频谱、又用比SSB容易实现的滤波器换来可解调的信号。本文会用Python建一条能跑的VSB调制解调仿真链路,把滤波器设计、参数设置、解调恢复和验证方法串起来,适合正在做数字基带仿真、物理层算法或软件无线电验证的同学。
2. VSB调制原理:残留边带为什么必须对称互补
2.1 从DSB到VSB:省掉的那一半边带去哪了
幅度调制的常规起点是DSB,假设基带信号带宽为B,DSB占用2B的频谱。SSB只保留一条边带,频谱占用是B,但要求在载频处有极其陡峭的过渡带,工程滤波器很难做,相位失真也不可控。VSB取中间路线:保留一条边带的大部分,另一条边带只留下靠近载频的一小段,整体带宽为B + Δ,其中Δ就是残留边的宽度。
接收端做相干解调时,上下边带会被搬移到基带叠加。要让叠加结果还原出原始m(t),滤波器频率响应必须在载频两侧满足互补条件。写成公式就是:
$$s_{vsb}(t) = \left[ m(t) \cos(2\pi f_c t) \right] * h_{vsb}(t) $$
当接收端乘以同频同相的本振$\cos(2\pi f_c t)$,并滤除二倍频分量后,等效基带频谱正比于:
$$M(f) \cdot \left[ H_{vsb}(f_c + f) + H_{vsb}(f_c - f) \right] $$
要让传输无失真,必须让 $H_{vsb}(f_c + f) + H_{vsb}(f_c - f)$ 在信号带宽内接近常数。这说明VSB滤波器不是简单地把下边带“切一刀”,而是以载频为对称轴,左右两侧做成互补过渡带。
2.2 互补条件的具体形式:从0经0.5到1的过渡带
以保留上边带、下边带只留残边为例,理想的幅度响应可以写成:
- $f < f_c - \Delta$:完全抑制,幅度为0
- $f_c - \Delta < f < f_c$:从0平滑上升到0.5
- $f_c < f < f_c + \Delta$:从0.5平滑上升到1
- $f > f_c + \Delta$:保留上边带通带,幅度为1
这里Δ就是残留宽度。注意中心频率处的值是0.5,不是1。如果用一条从0到1的斜坡直接切到下边带,解调后基带低端会抬高,眼图出现固定的模式相关失真——这就是很多自作主张的VSB仿真“解调出来波形不对”的常见原因。
实际系统中,残留宽度Δ通常取符号率的0.1到0.25倍。ATSC 8VSB的符号率为10.762Msps,在6MHz频道内滚降因子大约是0.1152,残留宽度就是符号率的一半再乘滚降因子,对应约620kHz,读规范时看到的滚降系数一般指成形滤波器参数,不要和残留宽度直接画等号。
2.3 用firwin2设计留边滤波器:代码和参数说明
设计这种带互补过渡带的带通滤波器,最直接的方式是用scipy.signal.firwin2在频域给点。firwin2接受一组频率点和对应的幅度点,做线性插值后通过IFFT生成FIR系数,非常适合用来搭VSB频谱模板。
import numpy as np from scipy.signal import firwin2 def design_vsb_bp(fc, Rs, beta, fs, ntaps, rolloff=0.25): """设计保留上边带的VSB带通滤波器 fc 载波频率 Rs 符号率 beta 残留宽度因子,残留宽度 = beta * Rs / 2 fs 仿真采样率 ntaps FIR系数个数 rolloff 基带成型滚降系数,决定基带信号最大频率 """ fmax = 0.5 * (1 + rolloff) * Rs # 基带单边带宽 delta = beta * Rs / 2 # 残留边带宽度 f_pts = np.array([0, fc - fmax, fc - delta, fc, fc + delta, fc + fmax, fs / 2]) a_pts = np.array([0, 0, 0, 0.5, 1, 1, 0]) return firwin2(ntaps, f_pts, a_pts, fs=fs)这段代码里需要特别注意第5行和第6行:载频两侧各留了一条斜坡,斜坡中心落在fc处、幅度为0.5。如果没有这个0.5,而是直接从0跳到1,互补条件就不成立。firwin2本身只是按给定点插值,它不会替你保证VSB的对称性,频率点数组必须自己对好。
ntaps的取值也比较关键。ntaps太小,实际幅频响应跟模板偏差大,带内纹波会把互补条件破坏掉;ntaps太大,群延迟变大,接收端匹配滤波和对齐符号要做额外补偿。仿真里常用ntaps等于符号周期数的128到512倍,例如采样率960kHz、符号率40kHz时,300阶左右就够用。
3. 用Python搭VSB调制解调仿真链路:发端、信道、收端一次打通
3.1 仿真参数和信号链路总览
这一节给出一个能直接运行的闭环仿真。链路分四段:随机4ASK符号生成、匹配滤波成型、乘载波和VSB滤波、信道加噪。接收端依次做相干下变频、低通滤波、匹配滤波、符号定时和判决。选择4ASK而不是QPSK,是为了让残留边带的频谱在实数域中直观可见,解调后也不需要处理复数星座旋转,后续要扩成8VSB时只需把符号星座换成8电平即可。
仿真参数按下表设定。
| 参数 | 取值 | 说明 |
|---|---|---|
| 符号率 Rs | 40 kHz | 决定信号带宽 |
| 过采样倍数 SPS | 24 | fs = 960 kHz |
| 载波频率 fc | 200 kHz | 需大于信号带宽 |
| 符号数 Nsym | 512 | 样本总数12288 |
| 基带滚降 rolloff | 0.25 | 基带单边带宽25 kHz |
| 残留宽度因子 beta | 0.25 | 残留宽度5 kHz |
| VSB滤波器阶数 | 301 | 1.4个符号周期左右 |
3.2 发端代码:从比特到VSB调制波形
import numpy as np from scipy.signal import butter, lfilter def rrc_taps(beta, sps, span=12): """根升余弦成型滤波器""" n = int(sps * span) x = (np.arange(n) - (n - 1) / 2) / sps h = np.zeros(n) inv4b = 1 / (4 * beta) for k, t in enumerate(x): if np.isclose(t, 0): h[k] = 1 - beta + 4 * beta / np.pi elif np.isclose(np.abs(t), inv4b): h[k] = beta / np.sqrt(2) * ( (1 + 2 / np.pi) * np.sin(np.pi / (4 * beta)) + (1 - 2 / np.pi) * np.cos(np.pi / (4 * beta))) else: h[k] = (np.sin(np.pi * t * (1 - beta)) + 4 * beta * t * np.cos(np.pi * t * (1 + beta))) \ / (np.pi * t * (1 - (4 * beta * t) ** 2)) return h / np.sqrt(np.sum(h ** 2)) def run_vsb_link(snr_db=20, beta=0.25, seed=7): rng = np.random.default_rng(seed) Rs, SPS, fs = 40e3, 24, 960e3 fc, Nsym = 200e3, 512 rolloff, ntaps = 0.25, 301 # 4ASK符号映射到 -3, -1, 1, 3 bits = rng.integers(0, 4, Nsym) sym = 2 * bits - 3 # 上采样后通过RRC滤波器,生成限带基带信号 rrc = rrc_taps(rolloff, SPS) up = np.zeros(Nsym * SPS) up[::SPS] = sym base = np.convolve(up, rrc, mode="same") # 调制到载波后过VSB滤波器 t = np.arange(len(base)) / fs dsb = base * np.cos(2 * np.pi * fc * t) b_vsb = design_vsb_bp(fc, Rs, beta, fs, ntaps, rolloff) vsb_sig = lfilter(b_vsb, 1, dsb) # AWGN信道 rx_power = np.mean(vsb_sig ** 2) noise_std = np.sqrt(rx_power * 10 ** (-snr_db / 10)) rx = vsb_sig + rng.normal(0, noise_std, vsb_sig.shape) return rx, sym, SPS, fs, fc, rolloff调制端的核心逻辑在于先对符号做RRC成型,再经过VSB滤波器。RRC保证基带信号本身带宽受限,否则矩形脉冲的sinc频谱旁瓣会延伸到VSB滤波器通带外,残留边带波形被截断后,符号之间会产生不可控的码间串扰。VSB滤波器在这里起的是频带整形作用,互补过渡带内的对称性决定了解调后基带频谱是否平坦。
mode="same"会引入半个滤波器长度的群延迟,后面符号定时搜索峰值时会自动找到这个延迟,所以这里不需要手动补偿。噪声求法用的是实际发送信号功率,而不是符号幅度直接估算,这样不同beta值之间做性能对比时才公平。
3.3 收端代码:相干解调、匹配滤波和符号判决
def demod_vsb(rx, sym, SPS, fs, fc, rolloff): t = np.arange(len(rx)) / fs rrc = rrc_taps(rolloff, SPS) # 同频同相下变频,去除二倍频分量 baseband = rx * np.cos(2 * np.pi * fc * t) b_lp, a_lp = butter(3, (1 + rolloff) * 20e3 / (fs / 2), "low") baseband = lfilter(b_lp, a_lp, baseband) # 匹配滤波后,按峰值搜索符号最佳采样点 matched = np.convolve(baseband, rrc, mode="same") peak = np.argmax(np.abs(matched[:SPS])) idx = peak + np.arange(len(sym)) * SPS hat = matched[idx] # 4ASK阈值判决:-2, 0, 2 est = np.digitize(hat, [-2, 0, 2]) ser = np.mean(est != sym) return ser, hat解调逻辑里有几个关键点。第一,下变频后必须低通滤掉2倍载频分量,这里的三阶巴特沃斯截止频率取带内上限26.25kHz,保证信号通过又不引入过多带外噪声。第二,匹配滤波用的是同一个RRC系数,匹配滤波器输出峰值位置就是最佳采样点,直接用第一个符号周期内搜索最大幅值找相位,相当于做了一次粗定时同步。第三,np.digitize的阈值数组是[-2, 0, 2],返回值0到3正好对应4个ASK符号的电平区间。
3.4 跑一次并检查结果
rx, sym, SPS, fs, fc, rolloff = run_vsb_link(snr_db=20, beta=0.25, seed=7) ser, hat = demod_vsb(rx, sym, SPS, fs, fc, rolloff) print(f"SER = {ser:.4f}, 样例星座点 = {hat[:8]}")SNR为20dB、残留因子0.25时,4ASK的SER应该在10的负3次方量级附近。如果看到SER明显偏高,第一步不是调噪声,而是检查发射端RRC成型有没有生效,直接看np.mean(np.abs(base))的峰均比;第二步检查VSB滤波器的幅频响应在载频两侧是否满足互补条件,方法见第5章。
4. 仿真参数怎么调:残留宽度、滤波器阶数、载波失配对性能的影响
4.1 残留宽度beta:省带宽和抗失真的折中
在第3章的链路里,beta直接控制VSB滤波器过渡带宽度。beta越大,残留边带越宽,频谱占用越接近DSB,滤波器过渡带越缓和,解调输出越接近理想基带;beta越小,频谱越省,但过渡带越陡峭,FIR实现难度直线上升,且对群延迟误差更敏感。
用第3章的run_vsb_link函数做一次扫描就能看出趋势:
for beta in [0.1, 0.2, 0.3, 0.5]: rx, sym, SPS, fs, fc, rolloff = run_vsb_link(snr_db=20, beta=beta, seed=7) ser, _ = demod_vsb(rx, sym, SPS, fs, fc, rolloff) print(f"beta={beta:.1f}, SER={ser:.4f}")常见现象是:beta从0.1增加到0.3时SER下降明显,继续增大到0.5后SER改善放缓,但信号带宽从约27.5kHz增加到约35kHz。这就是ATSC选择8VSB而不是纯SSB的原因之一:真正限制系统效率的不是理想带宽,而是滤波器可实现性。如果你的仿真里beta降到0.1附近SER反而变差,先确认ntaps是不是太小,301阶在10kHz过渡带上并不算宽裕。
4.2 滤波器阶数和载波频率的相对关系
VSB滤波器阶数和载波频率有一个容易被忽略的约束:firwin2在频率响应过渡带上的实际斜率受窗函数长度限制,过渡带宽度不能远小于fs/ntaps。以第3章参数为例,fs=960kHz,ntaps=301,频率分辨率约为3.2kHz,而残留宽度delta=5kHz,勉强够用。如果把beta降到0.1,delta只有2kHz,这时就需要把ntaps提高到600以上,否则设计出的滤波器过渡带会变软,互补条件在载频附近不成立。
载波频率fc也不能定得太低。fc至少要大于基带最大频率与残留宽度之和,否则滤波器的负频率部分会卷到正频率区间。更安全的选择是让fc在fs/4到fs/3之间,这样VSB过渡带离0频和Nyquist频率都有足够距离,观察频谱时也方便。
4.3 载波相位误差和频偏:仿真里最常忽视的坑
相干解调要求本地载波和发送载波同频同相。仿真里直接复用np.cos(2 * np.pi * fc * t)太理想了,实际系统一定会有相位误差和频偏。把收端本振改成:
theta = 0.15 # 约8.6度 rx * np.cos(2 * np.pi * fc * t + theta)输出星座会整体旋转,4ASK还能勉强用幅度判决,但对8VSB这类多电平系统,相位误差超过几度就会让最小电平间隔被压掉。更隐蔽的是频偏,哪怕只有100Hz,匹配滤波峰值也会随时间偏移,固定峰值搜索的定时方法会失效,SER迅速恶化。这就是8VSB要在信号里插入导频的原因:导频不是用来传数据,而是给接收机做载波恢复和定时恢复。
5. 验证VSB仿真链路是否真的对:频谱互补性自检和延伸方向
5.1 用频响叠加检查滤波器是否满足VSB条件
与其盯着误码率猜问题,不如直接检查VSB滤波器的频域结构。补一个自检函数,逐个计算载频左右对称点的幅度之和:
from scipy.signal import freqz w, h = freqz(design_vsb_bp(200e3, 40e3, 0.25, 960e3, 301), 1, worN=4096, fs=960e3) idx_fc = np.argmin(np.abs(w - 200e3)) delta = 0.25 * 40e3 / 2 offsets = np.linspace(0.2e3, delta, 8) flatness = [] for df in offsets: i1 = np.argmin(np.abs(w - (200e3 + df))) i2 = np.argmin(np.abs(w - (200e3 - df))) flatness.append(np.abs(h[i1]) + np.abs(h[i2])) print(f"对称点幅度和: min={np.min(flatness):.4f}, max={np.max(flatness):.4f}")理想情况下,对称点幅度和应该恒为1.0。输出范围的极差在0.02以内说明滤波器设计合理,超过0.05就要检查频率点数组是否写错,最常见的问题是把中心点幅度写成了1而不是0.5。这个自检不依赖符号序列和噪声,任何VSB滤波器都可以先跑它,过了再进链路仿真。
5.2 一个实用技巧:用频域互补性定位解调失真来源
解调波形出现固定畸变时,先区分是滤波器问题还是定时问题。将接收序列的匹配滤波输出按符号周期抽取,画成叠加的眼图,如果眼睛张开但上下不对称,多半是VSB过渡带没有严格互补;如果眼睛里全是模糊的阴影,多半是定时偏移或RRC成型没有配对。验证定时偏移的一个小技巧是:用相关峰值搜索估计出的peak值对频率偏差做扫描,如果peak随仿真长度漂移,说明载波有残留频偏,而不是定时算法的问题。
5.3 向8VSB和硬件方向延伸的三个切入点
第3章的4ASK链路换成8VSB只需做三处改动:星座从4个幅度扩成8个电平;成型滤波器改用平方根升余弦并重启定时同步;在符号流中加入固定直流分量作为导频。更接近工程的做法是再给接收端加一个自适应均衡器,因为实际信道引入的多径会让VSB的互补对称性失效,均衡器的作用就是用抽头系数把基带频谱重新拉平。这些改动都建立在本文第5.1节的互补性自检能通过的前提下,先保证滤波器没有埋雷,再谈信道和硬件。
本文还有配套的精品资源,点击获取