在上一期的 Q01 中我们成功解调了单声道 FM 广播。但现实世界中的无线电信号往往更为复杂为了在一个载波上传输更多信息频分复用FDM技术应运而生。本期Q02我们将踏入立体声 FM 广播的领域。本题在 SDR CTF 体系中属于模拟广播层的进阶难度 ★2。它不仅引入了频分复用和相干解调的概念更是一个绝佳的“信号诊断”实战案例——当实际跑出的频谱与教科书理论严重不符时我们该如何由表及里地排查问题而不是对着代码怀疑人生。一、 题目设定在 CTF 竞赛或真实的无线电逆向中拿到一个未知的 IQ 文件我们不应主观臆断它的类型。以下是本题的题干设定截获文件Q02_stereo_fm.iq复基带信号叠加了 SNR25dB 的 AWGN 噪声下载地址。已知参数初始告知采样率Fs 240 kHz有个小陷阱信号为恒包络调制载波已下变频至基带。挑战目标成功分离出立体声的左声道(L)和右声道®。Flag 载体Flag 隐藏在右声道®音频中以 800 Hz 载波承载的简化摩斯电码ON/OFF 序列[1 0 1 1 1 0 1 1 1 0 0 0]形式发送。预期 FlagFLAG{FM_STEREO_R}二、 盲解第一步频谱“指纹”失效与诊断面对未知的 IQ 文件标准的盲分析流程是“时域看包络 → 频域看轮廓 → 鉴频转基带 → 功率谱看指纹”。1. 常规操作与意外的频谱读取文件后我们先观察时域包络发现幅度恒定确认是恒包络调制FM/FSK/PSK。于是顺理成章地进行 FM 鉴频共轭相乘法提取基带信号并使用 Welch 法绘制平滑功率谱% 读取 IQ 文件fidfopen(Q02_stereo_fm.iq,r);rawfread(fid,float32);fclose(fid);iqcomplex(raw(1:2:end),raw(2:2:end));Fs240e3;% 初始给定的采样率% FM 鉴频 (共轭相乘法)iqiq-mean(iq);% 去除直流偏置fm_demodangle(iq(2:end).*conj(iq(1:end-1)));basebandfm_demod/(2*pi)*Fs;% 转换为 Hz 量级basebandbaseband-mean(baseband);% 去除鉴频器输出的残余直流% 绘制功率谱密度 (Welch 法)[pxx,f]pwelch(baseband,hann(4096),2048,4096,Fs);figure;plot(f/1e3,10*log10(pxx));xlabel(kHz);ylabel(dB/Hz);title(Fs240kHz 时的基带功率谱);按照理论鉴频后的立体声基带频谱应该呈现标准的三段式“指纹”0~15kHz 的音频、19kHz 的孤立尖峰、23~53kHz 的对称能量块。2. 奇怪图像分析中高频段爆发与采样率混叠图像表现频谱图呈现出“两头高中间低”的怪异特征极低频区域 (0 - 5 kHz)出现极高能量的尖峰群瞬间达到约 60 dB/Hz。中频区域 (5 - 65 kHz)看似平坦能量极低完全没有导频和副载波的影子。中高频爆发 (70 - 85 kHz)在76 kHz 附近出现了极强的高能“爆发”峰值超过 60 dB/Hz几乎与低频尖峰持平随后迅速衰减。由表及里的根因剖析为什么理论与实测对不上核心问题出在采样率不足导致的频谱混叠。理论带宽FM 信号的带宽遵循卡森公式B 2 ( Δ f f m ) B 2(\Delta f f_m)B2(Δffm)。本题最大频偏Δ f 75 \Delta f 75Δf75kHz复合基带最高频率f m 53 f_m 53fm53kHz。实际带宽约2 × ( 75 53 ) 256 2 \times (75 53) 2562×(7553)256kHz。混叠发生初始采样率 240 kHz低于 256 kHz 的信号带宽。在数字域生成信号时超出± 120 \pm 120±120kHz奈奎斯特频率的频谱分量被折叠回来。非线性恶化混叠破坏了恒包络特性。当混叠信号进入鉴频器非线性操作时原本折叠的高频分量与 38kHz 副载波发生交调在 76kHz 产生了极强的杂散峰彻底掩盖了微弱的 19kHz 导频。解决方案必须提升采样率。我们将发送端与接收端的采样率统一下调至原设计的合理值例如重新提供F s 500 Fs 500Fs500kHz 的数据或按比例重采样确保奈奎斯特带宽覆盖所有信号分量。真实数据下载地址。三、 盲解第二步修正采样率与发现真实频谱1. 修正代码与图像表现在 500 kHz 采样率下奈奎斯特带宽达到 250 kHz彻底消除了混叠。再次运行上述pwelch代码频谱基底噪声变得平稳约 35 dB/Hz真实的信号结构显现出来极低频 (0-5 kHz) 强信号 (54-56 dB/Hz)这是完全正常的 (LR) 主信道因为左右声道的音频440Hz, 880Hz, 800Hz都集中于此。核心峰值 (36.5 kHz 附近, 55 dB/Hz)这是 (L-R) 副信道38 kHz DSB-SC 调制的能量。由于左右声道差异大且 FM 调制的非线性交调能量在此处高度集中。15 - 20 kHz 的轻微隆起 (43 dB/Hz)这是 19 kHz 导频。2. 奇怪图像分析导频为何变成了“小山包”图像表现为什么 19 kHz 导频在频谱上只是一个微弱的“隆起”而不是教科书里孤立高耸的“尖峰”如果仅凭这张图很容易误判该信号不是立体声。由表及里的根因剖析三角噪声效应FM 鉴频器有一个固有的物理特性——输出噪声功率谱密度与频率的平方成正比即高频噪声远大于低频噪声。导频幅度极小在发送端导频的幅度被特意设为复合基带最大幅度的 10% 左右以节省频偏资源。在 19 kHz 这个相对较高的频率上噪声底已经被三角噪声抬高微弱的导频信号在 Welch 功率谱上就被“糊”成了一个隆起的包而不是一根清晰的针。解决方案既然直接看频谱看不清我们就不能仅凭肉眼看频谱必须用极窄的带通滤波器去把导频“挖”出来作为证据。四、 盲解第三步用带通滤波验证隐藏的导频既然 19kHz 导频在频谱上不明显我们设计一个极窄的零相位带通滤波器将其从噪声中强制提取出来并观察时域波形。% 设计 19kHz 窄带带通滤波器 (零相位)bpFilt_19kdesignfilt(bandpassfir,FilterOrder,500,...CutoffFrequency1,18.8e3,CutoffFrequency2,19.2e3,SampleRate,Fs);pilot_19kfiltfilt(bpFilt_19k,baseband);% 必须用 filtfilt 保证零相位% 观察导频时域波形figure;t_ms(0:4999)/Fs*1000;% 取前 10ms 数据plot(t_ms,pilot_19k(1:5000));xlabel(时间);ylabel(幅度);title(提取出的 19kHz 导频时域波形);奇怪图像分析时域波形的“视觉陷阱”图像表现运行代码后你会看到一张布满密集振荡线条的图幅度在 -6000 到 6000 之间剧烈波动看起来像是一个“实心的蓝色填充区域”。波形在某些时间点达到最大振幅如 X≈9.8 处峰值 5800在另一些时间点振幅变得很小甚至接近零如 X≈7 处幅度收缩。这看起来像是导频受到了低频调制或发生了拍频由表及里的根因剖析这是一种典型的视觉错觉。19kHz 是高频信号每毫秒包含 19 个完整周期。当我们在一个较宽的时间窗口如 10ms内绘制几百个周期时波形密度极高相邻周期的波峰和波谷在屏幕像素级渲染时会相互重叠产生类似摩尔纹的干涉现象。图中看到的“包络上下边界”其实就是正弦波本身的等幅上下限在像素上的压缩表现。看似“剧烈波动的包络”实际上是高频正弦波在渲染时的混叠视觉效应。19kHz 导频在发送端是未调制的单频正弦波理论上是严格等幅的。解决方案缩短时间窗口验证。若想验证其稳定性只需将时间轴缩短到 1ms 以内约 19 个周期就能看到完美的等幅正弦波。figure;t_ms_short(0:499)/Fs*1000;% 取前 1ms 数据plot(t_ms_short,pilot_19k(1:500));xlabel(时间);ylabel(幅度);title(19kHz 导频时域波形短时间窗口验证);验证成功在短窗口下代码中窗口还是有些大了导频呈现出完美的等幅正弦波。至此“标准 FM 立体声广播”的判定才有了充分的局部证据。五、 解调实现中的三大“致命陷阱”判明信号类型后进入解调阶段。这里同样有三个易错点需要严格规避否则前功尽弃。陷阱一数学相位的暗度陈仓sin 与 cos 之争现象接收端提取 19 kHz 导频平方后得到 38 kHz 副载波将其与 DSB-SC 信号相乘并低通滤波结果 (L-R) 信号全是 0。剖析如果发射端导频和副载波使用了sin提取导频并平方P² sin²(2π·19k·t) 0.5 - 0.5·cos(2π·38k·t)38 kHz 带通滤波后本地副载波变成了-cos(2π·38k·t)发射端的 DSB-SC 信号是(L-R) · sin(2π·38k·t)相乘解调[(L-R) · sin(38k)] · [-cos(38k)] -0.5 · (L-R) · sin(76k)乘积变为高频项76kHz被低通滤波器彻底滤除避坑指南在涉及倍频和相干解调的系统中导频和副载波必须全部使用cos。平方后得到同相cos(38k)相乘后产生直流分量0.5(L-R)才能被低通滤波器保留。陷阱二滤波器的群延迟失配现象修正了 sin/cos 问题后解调出来的 L-R 依然是一片噪声摩斯电码能量检测全为 0。剖析FIR 滤波器具有群延迟阶数为N NN的滤波器会使信号延迟N / 2 N/2N/2个采样点。DSB-SC 信号经过 23-53kHz 带通滤波延迟了N 1 / 2 N_1/2N1/2。导频经过 19kHz 带通滤波平方后再次经过 38kHz 带通滤波累计延迟了N 2 / 2 N 3 / 2 N_2/2 N_3/2N2/2N3/2。两路信号在时间轴上错位。高频周期信号即使错开几个采样点也会导致巨大的相位差如从0 ∘ 0^\circ0∘变成90 ∘ 90^\circ90∘相干解调输出幅度趋近于 0。避坑指南绝不能使用单向的filter函数必须使用filtfilt零相位滤波。正向和反向各滤波一次相互抵消群延迟保证相位严格对齐。陷阱三硬件声卡的“降维打击”采样率不支持现象解调成功后用sound(R, Fs)播放报错Device Error: Unanticipated host error。剖析500 kHz 的采样率远超普通 PC 声卡支持范围通常最高 48kHz 或 192kHz底层音频 API 拒绝接受过高采样率的音频流。避坑指南输出到声卡前必须降采样。使用resample(R, 48000, Fs)将其转换为 48 kHz即可顺利播放。六、 核心解调逻辑演示基于上述避坑经验我们提取 L-R 信号并进行立体声解码的核心逻辑如下非完整脚本仅展示关键环节% 1. 设计零相位滤波器 (此处省略 designfilt 细节)% lpFilt: 15kHz 低通 | bpFilt_19k | bpFilt_38k | bpFilt_23_53% 2. 提取主信道 (LR)L_plus_Rfiltfilt(lpFilt,baseband);% 3. 提取 19kHz 导频并倍频恢复 38kHz 副载波pilot_19kfiltfilt(bpFilt_19k,baseband);subcarrier_38kpilot_19k.*pilot_19k;subcarrier_38kfiltfilt(bpFilt_38k,subcarrier_38k)*2;% 乘2补偿幅度% 4. 相干解调副信道dsb_scfiltfilt(bpFilt_23_53,baseband);L_minus_Rfiltfilt(lpFilt,dsb_sc.*subcarrier_38k);% 5. 立体声矩阵解码L(L_plus_RL_minus_R)/2;R(L_plus_R-L_minus_R)/2;% 6. 针对 R 通道进行 800Hz 摩斯电码能量检测% 对 R 进行 800Hz 带通滤波按符号长度计算短时能量并阈值判决% 最终检测到序列 [1 0 1 1 1 0 1 1 1 0 0 0]% 解出 Flag: FLAG{FM_STEREO_R}完整代码如下% Q02_stereo_fm_rx.m - 解题脚本零相位滤波 音频降采样clear;clc;% 1. 读取 IQ 文件fidfopen(Q02_stereo_fm.iq,r);rawfread(fid,float32);fclose(fid);iq_datacomplex(raw(1:2:end),raw(2:2:end));Fs500e3;% 采样率% 2. FM 鉴频使用更稳健的共轭相乘法iq_dataiq_data-mean(iq_data);% 去除直流% 角度差分即瞬时频率fm_demodangle(iq_data(2:end).*conj(iq_data(1:end-1)));basebandfm_demod/(2*pi)*Fs;basebandbaseband/max(abs(baseband));% 归一化幅度便于后续处理% 3. 设计滤波器lpFiltdesignfilt(lowpassfir,FilterOrder,100,...CutoffFrequency,15e3,SampleRate,Fs);bpFilt_19kdesignfilt(bandpassfir,FilterOrder,200,...CutoffFrequency1,18.8e3,CutoffFrequency2,19.2e3,...SampleRate,Fs);bpFilt_38kdesignfilt(bandpassfir,FilterOrder,200,...CutoffFrequency1,37.8e3,CutoffFrequency2,38.2e3,...SampleRate,Fs);bpFilt_23_53designfilt(bandpassfir,FilterOrder,200,...CutoffFrequency1,23e3,CutoffFrequency2,53e3,...SampleRate,Fs);% 4. 提取各分量 (使用 filtfilt 进行零相位滤波消除群延迟导致的相位失配)% 提取 LRL_plus_Rfiltfilt(lpFilt,baseband);% 提取 19kHz 导频pilot_19kfiltfilt(bpFilt_19k,baseband);% 恢复 38kHz 副载波平方 带通滤波subcarrier_38kpilot_19k.*pilot_19k;subcarrier_38kfiltfilt(bpFilt_38k,subcarrier_38k)*2;% 乘2补偿幅度衰减% 提取并解调 L-R23-53 kHz 带通 → 乘以副载波 → 低通dsb_scfiltfilt(bpFilt_23_53,baseband);L_minus_Rfiltfilt(lpFilt,dsb_sc.*subcarrier_38k);% 5. 立体声矩阵解码L(L_plus_RL_minus_R)/2;R(L_plus_R-L_minus_R)/2;% 6. 归一化右声道RR/max(abs(R));% 7. 降采样到 48kHz 并播放解决高采样率导致的声卡设备错误Fs_play48e3;R_playresample(R,Fs_play,Fs);fprintf(准备播放右声道...\n);trysound(R_play,Fs_play);pause(length(R_play)/Fs_play0.5);% 等待播放完毕catchMEfprintf(音频播放失败%s\n,ME.message);end% 无论如何都保存一份 wav 方便试听audiowrite(R_audio.wav,R_play,Fs_play);fprintf(右声道音频已保存为 R_audio.wav (48kHz)\n);% 8. 自动解码摩斯电码能量检测法fprintf(\n--- 自动解码摩斯码 ---\n);% 发送端参数morse_symbol_lenfloor(Fs*0.15);% 每个符号的采样点数36000% 对右声道信号进行 800 Hz 带通滤波抑制带外噪声bp800designfilt(bandpassiir,FilterOrder,4,...HalfPowerFrequency1,750,HalfPowerFrequency2,850,...SampleRate,Fs);R_filtfiltfilt(bp800,R);% 零相位滤波% 计算短时能量每符号长度一个窗口num_symbols12;% 已知摩斯码共 12 个符号energyzeros(num_symbols,1);fork1:num_symbols start_idx(k-1)*morse_symbol_len1;end_idxmin(k*morse_symbol_len,length(R_filt));segmentR_filt(start_idx:end_idx);energy(k)mean(segment.^2);% 平均功率end% 动态阈值thresholdmax(energy)*0.75;detecteddouble(energythreshold);% 1: 有声, 0: 无声fprintf(检测到的摩斯码 ON/OFF 序列);fprintf(%d ,detected);fprintf(\n);% 发送端原始模式作为验证expected_pattern[101110111000];ifisequal(detected(:),expected_pattern(:))fprintf(摩斯码匹配隐藏的 Flag 是FLAG{FM_STEREO_R}\n);elsefprintf(模式不完全匹配请人工收听 R_audio.wav 确认。\n);end% 绘制能量图用于观察figure;bar(energy);hold on;yline(threshold,r--,Threshold);xlabel(Symbol index);ylabel(Energy);title(R 声道 800 Hz 能量分布);七、 总结与预告本题的真正难点不在于解调公式而在于当实测频谱与理论描述严重不符时能否系统性地诊断出差异来源。从采样率不足导致的混叠爆发到三角噪声掩盖导频再到高频时域波形的视觉错觉每一个坑都可能让人误入歧途。正确的做法是计算理论带宽验证采样率、用pwelch平滑频谱、用带通滤波做局部验证让多个独立证据相互印证。这套“频谱给方向、带通给证据”的方法论在后续题目中会反复用到。下一题Q03 FM RDS中57 kHz 的 BPSK 副载波功率比 19 kHz 导频还低单凭 FFT 几乎看不到。届时我们将引入锁相环PLL / Costas Loop——它不仅能从噪声中提取纯净相位还能为 BPSK 的相干解调提供本地载波是从“看谱”迈向“闭环跟踪”的关键一步。敬请期待。