ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MATLAB FM立体声仿真:19 kHz导频与38 kHz副载波链路

MATLAB FM立体声仿真:19 kHz导频与38 kHz副载波链路

简介:这是一份面向通信原理学习者和MATLAB使用者的FM调制解调仿真资料,针对立体声广播场景,解决从原理理解到代码实现的完整验证问题,适合正在学习模拟调制、需要动手复现FM系统的本科高年级学生与工程入门人员。包内共2个文件,包含1个m脚本和1个m4a音频文件,前者承载完整的立体声FM调制与解调流程,后者可用于试听或对照验证处理结果,压缩包整体约88KB,体量轻便、便于快速运行与二次修改。代码配备详细注释,调制参数可自由调整,方便读者替换参数后观察效果差异;时域与频域展示完整,能帮助理解基带信号、载波调制与解调恢复各环节的波形特征。该资源已有868人学习下载,可作为课程实验、仿真练习或自学参考的实用素材。

1. 一段“左右拉不开”的波形,促使我排查 19 kHz 导频

接手 A 同学那份 MATLAB FM 系统仿真时,我起初以为只是滤波器没调好。左右声道送入两段完全不同的音频,播放出来却仍是单声道,人声固定在中间位置,前后拉不开。查了两小时才发现,整条链路里完全没有 19 kHz 导频和 38 kHz 副载波那一段,立体声编码被跳过,解调端只能拿到 L+R。这份 FM 调制解调仿真资源补的正是这段完整过程:预加重、和差矩阵、DSB-SC 副载波调制、调频、AWGN 信道、鉴频、导频恢复、和差还原、去加重,最后用分离度和信噪比把每一步量化。做通信原理课程设计的人,可以拿它把课本上的贝塞尔函数和频谱图对应到波形;想往上叠多径、RDS 这类功能的熟手,也能把它当作基础框架。

2. 链路总览与参数定标:把 FM 立体声拆成四段能单独验证的子链路

整条链路最麻烦的地方,是任何一段出错,最终表现都是“声音不对”。你听到失真,可能是编码端幅度超了,也可能是鉴频器进了非线性区,还可能是解码端本地载波相位歪了。所以我拆这类仿真包时,第一件事是把它切成能单独跑、单独看的四段,每段都有自己的验证手段,出问题能立刻定位到哪一段。

2.1 信号流:两路音频到左右声道复现要经过什么

按导频制(也叫 Zenith-GE 制式)走,发射端是这么几步。两路音频先做预加重,提升高频;再过一个和差矩阵变成 S=L+R 和 D=L−R;S 留在基带 0~15 kHz,D 搬到 38 kHz 副载波上做双边带抑制载波调制,占据 23~53 kHz;然后插一根 19 kHz 导频,代表“我是立体声台,副载波相位以此为准”;三块拼起来就是 MPX 复合基带信号,最后整体调频搬到射频载波上。

接收端反着来:鉴频器把瞬时频率还原成 MPX;窄带滤波提取 19 kHz 导频,倍频得到 38 kHz 本地载波;用它与 MPX 相乘解调出 D,低通取出 S;再做和差运算 L=S+D、R=S−D;最后去加重、降采样回音频率。

这条链上有三个必须自检的中间点,我一般在脚本里都会存一版波形和频谱:MPX 频谱是否呈现“基带 + 19 kHz 单峰 + 23~53 kHz 双边带”这三块;鉴频输出是否和原始 MPX 基本重合;解码出的 L/R 在只有左声道输入时,右声道是否足够小。三个点都对,剩下的调参才有意义。

2.2 采样率、载频与频偏:一组不能随意设定的参数

参数表这类东西,抄错一个数整篇都白跑。下面这组是我反复试下来比较稳的取值,也是这类仿真最常采用的一组:

参数取值为什么是这个数
音频采样率 fs_a48 kHz覆盖 15 kHz 音频,留足过渡带
仿真采样率 fs480 kHz见下面的带宽推算
中频载波 fc120 kHz避开直流,频谱画出来干净
峰值频偏 Δf75 kHz广播标准,调制度 100%
导频 fp19 kHz制式规定,不能改
副载波 fsc38 kHz严格等于 2×fp
预加重时间常数 τ50 μs国内调频广播常用,部分地区用 75 μs
音频带宽15 kHz主信道与差信道带宽相同
信道间隔200 kHz卡森带宽约 180 kHz,留保护带

采样率最容易出错。调频后信号占据 fc±(Δf+fm),取 fc=120 kHz、Δf=75 kHz、fm=15 kHz,最高到 210 kHz,最低到 30 kHz,所以 fs 至少要 420 kHz。很多人图省事用 400 kHz,Nyquist 只有 200 kHz,载频一抬到 120 kHz 立刻混叠,频谱上会多出一堆说不清来源的杂散。

提示:如果非要用 400 kHz,就把 fc 压到 100 kHz,此时最高边到 190 kHz,只剩 10 kHz 余量,滤波器的过渡带必须做得非常陡,稍有泄漏就会在解调输出里听到混叠噪声。

另外 fc 不要取 fs 的整数分之一,比如 fs=400 kHz、fc=100 kHz,两者刚好是 4 倍关系,采样后的频谱会周期性地重叠在一起,做频谱分析时很难看清副载波边带。避开整数比,图会干净很多。

2.3 目录结构:这类仿真包我一般怎么分文件

一个大 main.m 从头写到尾,跑通没问题,改起来非常难受。我习惯按链路分段分文件,每个文件只做一件事,函数签名统一成「输入信号 + 参数结构体,输出信号 + 可选调试量」:

fm_stereo_sim/ ├── main.m % 顶层脚本:串链路、出图、算指标 ├── params.m % 所有参数集中在这,改一处全链路生效 ├── gen_audio.m % 生成或读取双声道测试信号 ├── stereo_encode.m % 预加重 + 和差矩阵 + DSB-SC + 导频 ├── fm_modulate.m % 相位积分调频 ├── channel_awgn.m % 信道加噪与信噪比标定 ├── fm_demodulate.m % 鉴频,输出归一化 MPX ├── stereo_decode.m % 导频恢复 + 相干解调 + 和差 + 去加重 └── metrics.m % 分离度、SNR、THD

参数结构体这一层别省。我试过把 fs、fc、Δf 散在各个函数里,后来想把载频从 100 kHz 换成 120 kHz,翻了六个文件才改干净,还漏了一处滤波器归一化频率没跟着变,结果带通中心跑到 19 kHz 以外,导频直接提取不到。集中之后,改参数只动 params.m 一处,代价是每个函数多一个入参,非常划算。

3. 发射端拆解:预加重、和差矩阵与 38 kHz 副载波怎么拼出 MPX

发射端是整个仿真的地基。这里幅度一旦超了,后面鉴频器再好也救不回来——调频是非线性过程,过调制度会产生无法用滤波去掉的失真。所以这一段我会写得比接收端更谨慎,每一步都留一个可观测的量。

3.1 预加重与和差矩阵:先把 L/R 变成 S 和 D

预加重不是可有可无的装饰。调频鉴频之后,噪声功率随频率平方增长,高频段的信噪比会明显变差,所以在发射端把高频抬起来、接收端再压回去,这一抬一压噪声被压了两次,音频没变。时间常数 50 μs 对应的转折频率约 3.2 kHz,也就是说 3.2 kHz 以上开始以 6 dB/倍频程提升。

fs_a = 48e3; % 音频采样率 tau = 50e-6; % 预加重时间常数 % H(s) = 1 + s*tau,用双线性变换离散化 % 直接做差分近似在高频段相位误差偏大,双线性更稳 [b_pre, a_pre] = bilinear([tau 1], 1, fs_a); % 音频先统一到仿真采样率,后面所有模块共用 fs L = resample(L0, fs, fs_a); R = resample(R0, fs, fs_a); Lp = filter(b_pre, a_pre, L); % 预加重 Rp = filter(b_pre, a_pre, R); S = (Lp + Rp) / 2; % 和信号,留在基带 0~15 kHz D = (Lp - Rp) / 2; % 差信号,准备搬到 38 kHz

bilinear([tau 1], 1, fs_a)里分子系数是[τ, 1],对应 s 域的一阶零点在 −1/τ,分母是常数 1,所以离散化后是一个一阶高通型响应,直流增益为 1,高频增益随频率线性上升。resample自带抗混叠滤波,别用interp1硬插值,那种做法在 15 kHz 附近的镜像会直接混进主信道。

矩阵那里除 2 是为了让 S、D 的峰值不超过 1,后面 MPX 归一化时才有余量。如果这里不除 2,两路满幅音频相加峰值到 2,等到 MPX 归一化时会整体压下去,信噪比吃亏。

3.2 DSB-SC 调制与导频插入:MPX 的三块频谱

差信号搬到 38 kHz 用的是双边带抑制载波,不是普通 AM。抑制载波的原因很直接:载波本身不携带信息,却要占掉宝贵的频偏资源,而且会在解调端引入直流分量,破坏分离度。

fp = 19e3; fsc = 2 * fp; % 副载波必须是导频的二次谐波 t = (0:N-1).' / fs; pilot = cos(2*pi*fp*t); sub = cos(2*pi*fsc*t); % 与导频同源,不要另起一个相位 mpx = S + D .* sub; % 主信道 + 副信道 mpx = 0.9 * mpx / max(abs(mpx)); % 归一化,给导频留 10% 的频偏 mpx = mpx + 0.1 * pilot;

sub和pilot必须同源,这不是代码风格问题,是制式的硬约束。导频的全部意义就是告诉接收端“副载波的相位是 2φ”,接收端把导频平方一下,2cos²(x) = 1 + cos(2x),自动得到 2φ 相位的 38 kHz。如果你在发射端另起一个相位随机的 38 kHz,接收端恢复出来的载波和它对不上,相干解调出来的 D 就会带一个 cos(Δφ) 的衰减因子,极端情况下直接归零。

归一化到 0.9 再叠加 0.1 的导频,是为了保证 MPX 峰值不超 1。广播规范里导频占总频偏的 8%~10%,取 10% 是常见做法。如果这里贪图分离度把导频压到 3%,接收端的窄带滤波器就不好提取了。

编码完了一定要看一眼频谱,这是最省时间的自检:

[Pxx, f] = pwelch(mpx, hann(4096), 2048, 8192, fs); plot(f/1e3, 10*log10(Pxx)); xline(19); xline(38); % 标出导频和副载波位置 xlabel('频率 (kHz)'); ylabel('功率谱 (dB)');

正确的图应该是三块:0~15 kHz 一块连续的基带,19 kHz 一根又细又高的单峰,23~53 kHz 一块以 38 kHz 为中心、中间塌陷的双边带。15~19 kHz 和 19~23 kHz 这两段是保护带,必须是空的。如果 15~23 kHz 之间鼓起一片,说明和信号没滤干净,或者导频 waveform 不是纯正弦。

3.3 调频实现:为什么我用 cumsum 而不是 fmmod

调频公式本身简单,角频率是载频加上调制信号的线性项,相位就是它的积分。难点全在数值积分上。

fc = 120e3; % 中频载波 df = 75e3; % 峰值频偏 kf = df; % mpx 峰值已归一化到 1,kf 数值上等于峰值频偏 mpx_dc = mpx - mean(mpx); % 先去掉直流,否则积分后变成斜坡 int_m = cumsum(mpx_dc) / fs; % 数值积分 ∫m(τ)dτ s_tx = cos(2*pi*fc*t + 2*pi*kf*int_m);

mean(mpx)那一步很多人会漏。MPX 里只要有一点残余直流,cumsum就会把它积成一个随时间线性增长的斜坡,等效于载频在缓慢漂移,鉴频输出里会出现一条低频漂移的基线,去加重之后就是一阵“咚咚”的闷响。我在第一次跑的时候就遇到了这个问题,盯着时域波形看了半天,最后发现是导频的 cos 在截断时不是整数个周期,留下了直流量。

调频灵敏度kf直接取 75e3,是因为 mpx 峰值已经被归一化到 1,所以瞬时频偏峰值恰好等于 kf。如果你跳过了 3.2 节的归一化,这里就必须改成kf = df / max(abs(mpx)),否则一超频偏,鉴频输出会限幅,THD 立刻被拉起来。

不用fmmod的理由有三个:它内部同样是积分,但初相位不好控;它不方便在积分前加去直流这一步;还有就是调试时拿不到中间相位,出了问题只能当成无法观察的过程。自己写三行,什么都看得见。

4. 接收端拆解:鉴频、导频恢复与和差还原的完整链路

接收端的信息量比发射端大,因为它要在一个已经混了噪声的信号里,把三块频谱重新分开。这一段我按信号流顺序拆成四小节,每一节的输出都是下一节的输入,可以单独冻结下来看。

4.1 AWGN 信道与信噪比标定

snr_if = 20; % 中频带内信噪比,dB s_rx = awgn(s_tx, snr_if, 'measured');

'measured'这个参数不能省。不带它的时候,awgn假设输入信号功率为 0 dBW,如果你的s_tx是单位幅度余弦,实际功率只有 −3 dBW 左右,加出来的噪声功率会比预期高,最后算出的信噪比整体偏了 3 dB 还找不到原因。

更容易出错的是信噪比的定义。这里设置的 snr_if 是仿真带宽(fs/2)内的信噪比,而解调后音频带宽只有 15 kHz,两者相差10log10(240/15) ≈ 12 dB。也就是说中频 20 dB 的信噪比,落到音频带上大约是 32 dB 加上调频带来的处理增益。做理论对比的时候如果不换算,曲线会整体平移十几 dB,看着像“仿真错了”,其实只是定义没对齐。

提示:要跟教科书上的 C/N 曲线对比,就把目标载噪比先折算成仿真带内信噪比再喂给awgn,并在图例里写清楚是哪种定义,不然后面自己都分不清哪条是哪条。

4.2 鉴频器三种写法:相位差分、Hilbert 与 PLL 的取舍

鉴频的本质是求瞬时频率,也就是相位的导数。离散域里最稳的是相位差分法:

% 相邻样点相乘取共轭,角度就是相位差 d = angle(s_rx(2:end) .* conj(s_rx(1:end-1))); fm = d * fs / (2*pi); % 换算成瞬时频率(Hz) fm = fm - mean(fm); % 减掉载频对应的直流 mpx_hat = fm / kf; % 除以调频灵敏度,回到归一化 MPX

angle对幅度不敏感,这是它最大的好处——信道带来的幅度起伏、滤波器带来的增益变化,全都不影响结果。代价是它对相位跳变很敏感,一旦相邻样点相位差接近 ±π,angle会在 ±π 处来回翻,输出一片锯齿。

Hilbert 变换的写法更适合做理论分析:

z = hilbert(s_rx); % 构造解析信号 ph = unwrap(angle(z)); % 解卷绕得到连续相位 fm = [0; diff(ph)] * fs / (2*pi); % 差分求瞬时频率

它的问题是unwrap的可靠性和每样点相位增量直接挂钩。载频 120 kHz、采样率 480 kHz 时,每样点基础相位增量是 π/2,加上 75 kHz 频偏最多到 0.8π,还在安全区。但你要是把 fc 抬到 150 kHz,每样点增量就超过 π,unwrap会往反方向绕,输出变成一段一段的斜坡,看着像信号被“折叠”了。这个坑在 5.2 节还会展开。

PLL 是工程上真正在用的方案,仿真里也能写,核心就是一个相位检测器加一个环路滤波器:

% 二阶环,Kp/Ki 决定锁定速度和稳态相差 Kp = 0.05; Ki = 0.001; phase_hat = 0; err_int = 0; baseband = zeros(size(s_rx)); for n = 1:length(s_rx) err = s_rx(n) * sin(2*pi*fc*t(n) + phase_hat); err_int = err_int + Ki * err; phase_hat = phase_hat + Kp * err + err_int; baseband(n) = err; % 环路滤波后即为解调输出 end

选哪个?做单次仿真、参数不极端,相位差分三行搞定,够用。要做低信噪比下的门限效应分析,或者载频会漂移的场景,PLL 才值得写。Hilbert 我一般只拿来当交叉验证,两路结果对不上就说明有一路出问题了。

4.3 导频恢复与 38 kHz 相干解调

这一段是立体声解码的核心,也是分离度能不能上去的分水岭。

% 1) 窄带带通抠出 19 kHz 导频 bp19 = fir1(512, [18.4e3 19.6e3]/(fs/2), 'bandpass'); p = filtfilt(bp19, 1, mpx_hat); % 2) 平方产生 38 kHz,相位自动锁定 p2 = p.^2; % 2cos^2 = 1 + cos(2x) bp38 = fir1(512, [37e3 39e3]/(fs/2), 'bandpass'); c38 = filtfilt(bp38, 1, p2); % 3) 归一化到 ±1,否则解调出来的差信号幅度极小 c38 = c38 / max(abs(c38)); % 4) 相干解调出差信号,系数 2 补 cos*cos 的 1/2 lp15 = fir1(512, 15e3/(fs/2)); D_hat = 2 * filtfilt(lp15, 1, mpx_hat .* c38); % 5) 低通取出和信号 S_hat = filtfilt(lp15, 1, mpx_hat);

第 2 步是整段最精巧的地方。导频是cos(2πfp t + φ),平方之后是0.5 + 0.5cos(2π·2fp t + 2φ),滤掉直流剩下的就是 38 kHz、相位 2φ,和发射端那个sub完全同相。相位差为零,cos(Δφ)=1,差信号满幅解出来。如果改成本地振荡器cos(2π·38e3·t),只要信道或者滤波器引入一点群延迟,Δφ 就不是零,D 会被压掉一截,同时泄漏到和路里,分离度直接掉到 10 dB 以下。

第 3 步的归一化也容易被忽略。导频幅度只有 0.1,平方之后幅度约 0.005,不归一化的话后面D_hat小得可怜,和 S_hat 完全不是一个量级,和差矩阵一算,L 和 R 几乎相等,听起来就是单声道——这和开头 A 同学遇到的现象一模一样。

4.4 和差矩阵、去加重与分离度评估

% 和差还原 L_hat = S_hat + D_hat; R_hat = S_hat - D_hat; % 去加重:H(s) = 1/(1+s*tau) [b_dea, a_dea] = bilinear(1, [tau 1], fs_a); L_out = filter(b_dea, a_dea, resample(L_hat, fs_a, fs)); R_out = filter(b_dea, a_dea, resample(R_hat, fs_a, fs));

和差矩阵的系数取决于 3.1 节怎么定义 S 和 D。当时除过 2,所以这里直接加减就是原始 L 和 R。如果那一步没除 2,这里要补上 0.5,否则输出会大一倍,播放时直接削顶,THD 高得离谱还找不到原因。

去加重的bilinear(1, [tau 1], fs_a)是预加重那个传递函数的倒数,

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

返回列表