用 HackRF + GNU Radio 制作 FM 发射机:从 WAV 到 103 MHz 空中信号
作者:charlie
关键词:HackRF、GNU Radio 3.10、SoapySDR、analog_wfm_tx、FM 广播调制、采样率链
摘要
本文记录如何用一块 HackRF One/Pro 配合 GNU Radio,把一个普通 WAV 音频文件调制成标准 FM 广播信号并发射到空中(默认 103 MHz),用普通收音机即可接收。文章给出完整可运行的流图 fm_tx.grc,并重点讲清三件事:(1) FM 调制与卡森带宽准则,为什么 max_dev=75 kHz、quad_rate 必须是 audio_rate 的整数倍;(2) GNU Radio analog_wfm_tx 的内部插值(×5)与预加重(tau=75 µs);(3) SoapySDR 下 HackRF 在 1 MHz 采样率发射会触发 TIMEOUT bug ,因此必须用 2 MHz 采样率,并用 rational_resampler 的 25/3 把 240 kHz 提到 2 MHz。文末附合规声明:本方案仅用于低功率近场教学测试。
1. 背景
FM 广播频段为 88--108 MHz,单声道基带音频带宽约 15 kHz,峰值频偏 75 kHz。HackRF 覆盖 1 MHz--6 GHz、半双工、8 位量化,既能接收也能发射,是做"软件定义 FM 发射机"的低成本平台。
目标链路非常短:
WAV 文件 (48k, 单声道)
│
▼ analog_wfm_tx (FM 调制, 插值 ×5)
240 kHz 复数基带
│
▼ rational_resampler_ccc (25/3)
2 MHz 复数基带
│
▼ soapy_hackrf_sink (TX, fc = freq×1e6, vga=16)
空中 103 MHz FM 信号
│
└─► qtgui_sink_x (本地频谱自检)
2. 理论基础
2.1 FM 调制与卡森带宽准则
在频率调制中,载波的瞬时频率正比于消息信号的幅度:
s(t) = A·cos( 2π·fc·t + 2π·kf·∫ m(τ)dτ )
-
kf决定频偏灵敏度;实际工程用峰值频偏 Δf 描述:max_dev = 75000Hz。 -
根据 Carson 带宽准则,已调信号 98% 功率落在:
BW ≈ 2·(Δf + f_m,max)
FM 广播 Δf = 75 kHz,基带最高 f_m,max ≈ 15 kHz,故 BW ≈ 2×(75+15) = 180 kHz。这就是 quad_rate = 240 kHz(留有余量,且为 audio_rate 的 5 倍)的来源。
2.2 预加重 / 去加重(tau = 75 µs)
高频噪声在 FM 解调后会被提升,因此发射端对高频做预加重 、接收端做去加重,以换得信噪比改善。时间常数分两派:
| 地区 | tau |
|---|---|
| 美国 / 日本 / 韩国等 | 75 µs |
| 欧洲 / 中国等 | 50 µs |
analog_wfm_tx 的 tau = 75e-6 对应 75 µs 预加重(美规)。若面向欧规设备,应改为 50e-6。fh = -1.0 表示关闭发射端的高通滤波器(不额外滤直流)。
2.3 GNU Radio analog_wfm_tx 原理
analog_wfm_tx 内部做四件事:
- 重采样 :内置插值因子固定为 5 ,所以要求
quad_rate = 5 × audio_rate(本例 48k → 240k)。它把音频上采样后再调制,保证每个音频样本有足够多的复数采样刻画频率变化。 - 预加重 :按
tau对音频做一阶高通式加权。 - 相位累加 FM 调制 :对每个输出采样累加相位增量
Δφ = 2π·(kf·m[n])/fs,再取cos/sin得到复数基带I + jQ。这正是"间接调频"------用积分+正交本振实现,避免直接调 VCO。 - 限幅/缩放:输出幅度归一化到适合 DAC 的范围。
2.4 采样率链与 HackRF 限制(关键坑)
HackRF 基带采样率规格为 2--20 MS/s 。1 MHz 在规格外,且 SoapySDR 的 HackRF TX 在 1 MHz 会触发 TIMEOUT bug (底层 hackrf 库在过低的采样率下 TX 超时,流图卡死/丢数)。因此:
wfm_tx自然产出 240 kHz;- 用
rational_resampler_ccc(interp=25, decim=3)把它提到240k × 25/3 = 2,000,000 = 2 MHz,正好落在 HackRF 合法区间下沿; - HackRF
samp_rate设为 2 MHz,center_freq = freq × 1e6。
若强行把
samp_rate设 1 MHz,流图能"跑起来"但发射会间歇卡死且频谱异常------这是 Soapy/HackRF 的已知问题,不是你流图写错。
2.5 法律与责任(务必阅读)
在 88--108 MHz 发射信号,绝大多数国家/地区都需要电台执照;未经许可的 FM 广播发射可能违法并干扰合法广播。本文设置:
amp = False(关闭 HackRF 板载放大器);vga = 16(适中增益,低功率);- 配合短天线、近场测试(发射机旁放一台收音机验证)。
请仅在合法授权、低功率、封闭/近场场景使用本方案,并遵守当地无线电法规。作者与本文不对任何违规使用负责。
3. 实践:流图搭建
3.1 音频准备
analog_wfm_tx 要求单声道且采样率与 audio_rate 一致。用 ffmpeg 把任意音频转成 48 kHz 单声道 WAV:
bash
ffmpeg -i in.wav -ar 48000 -ac 1 fm_input.wav
流图中 blocks_wavfile_source 指向 /path/to/fm_input.wav,repeat=True(循环播放,方便反复测试)。
3.2 流图关键参数
| 变量 / 模块 | 值 | 说明 |
|---|---|---|
freq |
88--108 MHz 滑杆,默认 103 | 发射中心频率 |
samp_rate |
2,000,000 | HackRF TX 采样率(≥2M,避开 1M bug) |
audio_rate |
48,000 | 必须与 WAV 一致 |
quad_rate |
240,000 | = 5 × audio_rate |
analog_wfm_tx.max_dev |
75,000 | 峰值频偏 75 kHz(FM 广播标准) |
analog_wfm_tx.tau |
75e-6 | 75 µs 预加重 |
rational_resampler |
interp=25, decim=3 | 240k → 2M |
soapy_hackrf_sink.vga |
16 | 低功率增益 |
soapy_hackrf_sink.amp |
False | 关闭板载放大 |
3.3 关键模块源码(来自 fm_tx.grc)
WAV 源(循环播放):
yaml
- name: wav_source_0
id: blocks_wavfile_source
parameters:
file: /path/to/fm_input.wav
nchan: '1'
repeat: 'True'
FM 调制核心:
yaml
- name: analog_wfm_tx_0
id: analog_wfm_tx
parameters:
audio_rate: audio_rate # 48000
quad_rate: quad_rate # 240000 (= 5 × audio_rate)
max_dev: '75000' # 峰值频偏 75 kHz
tau: 75e-6 # 75 µs 预加重
fh: '-1.0' # 关闭发射高通
采样率提升到 2 MHz(绕过 1M TX bug):
yaml
- name: rational_resampler_ccc_0
id: rational_resampler_xxx
parameters:
type: ccc
interp: '25' # 240000 × 25 / 3 = 2,000,000
decim: '3'
taps: '[]'
HackRF 发射与本地频谱自检:
yaml
- name: soapy_hackrf_sink_0
id: soapy_hackrf_sink
parameters:
type: fc32
center_freq: freq*1e6 # 103 MHz
samp_rate: samp_rate # 2,000,000
vga: '16'
amp: 'False' # 低功率,合规近场测试
bandwidth: '0'
- name: qtgui_sink_x_0
id: qtgui_sink_x
parameters:
type: complex
fc: freq*1e6
bw: samp_rate
fftsize: '1024'
plotfreq: 'True'
plotwaterfall: 'True'
连接关系(等价于生成的 fm_tx.py):
python
# 信号流:WAV → FM调制 → 重采样(→2M) → HackRF TX,并旁路一份给频谱仪
self.connect((self.wav_source_0, 0), (self.analog_wfm_tx_0, 0))
self.connect((self.analog_wfm_tx_0, 0), (self.rational_resampler_ccc_0, 0))
self.connect((self.rational_resampler_ccc_0, 0), (self.soapy_hackrf_sink_0, 0))
self.connect((self.rational_resampler_ccc_0, 0), (self.qtgui_sink_x_0, 0))
4. 运行与自检
启动(GRC 或 grcc 生成脚本):
bash
grcc -o /tmp fm_tx.grc && python3 /tmp/fm_tx.py
# 或直接用 GRC 的 "Run"
自检验证两步走:
- 本地频谱自检 :
qtgui_sink_x应显示以 103 MHz 为中心、宽约 180--200 kHz 的 FM 谱(典型"双驼峰" Carrier + 对称边带)。若谱很窄或卡死,多半是踩了 1M bug------确认samp_rate=2M。 - 真实接收 :把一台 FM 收音机调到 103.0 MHz,靠近 HackRF 天线,应清晰听到 WAV 里的声音(循环播放便于确认)。听感发闷/高频不足,检查
tau与接收机去加重是否匹配(美规 75 µs 对 75 µs)。
小贴士:
freq用counter_slider可在运行中平滑改频;改到 88--108 内任意值,收音机同步调台即可验证。
5. 总结与踩坑清单
核心结论
- FM 广播调制 = 相位累加的数字 FM;
quad_rate必须是audio_rate的 5 倍 (analog_wfm_tx内置插值 ×5)。 - 峰值频偏
75 kHz+ 卡森准则 ⇒ 占用带宽约 180 kHz,决定了quad_rate=240k的选取。 - HackRF TX 不能用 1 MHz 采样率 (Soapy TIMEOUT bug),必须 ≥2 MHz;用
rational_resampler25/3 把 240k 升到 2M 是标准解法。 - 预加重
tau=75 µs对应美规;面向欧规设备改50 µs。
踩坑清单
- 收音机收到但声音发尖/失真 →
max_dev与收音机期望不一致,或音频本身超出 15 kHz;确认max_dev=75k。 - 收音机高频发闷 →
tau不匹配(发射 75 µs 配欧规 50 µs 收音机会有差异)。 - 流图卡死 / 频谱断续 → 几乎都是
samp_rate=1M触发 Soapy/HackRF TX TIMEOUT;改成 2M。 - 完全收不到 → 先确认
amp=False时增益足够近场;天线用 88--108 MHz 鞭状天线;收音机与 HackRF 距离过远(低功率下十几厘米~一米内最佳)。 - 音频杂音大 → WAV 不是 48k 单声道;用 ffmpeg 重转,并保证
audio_rate与文件一致。
适用与局限
- 适用:FM 广播调制教学、近场音频链路演示、SDR 发射链路验证。
- 局限:HackRF 8 位 DAC,音质远不及专业 FM 激励器;半双工,不能收发同时;发射须合规、低功率、近场;立体声(MPX/Pilot 19 kHz)未实现,本文为单声道 FM。
合规声明(重申)
在 FM 广播频段发射通常需无线电执照。本方案默认 amp=False、vga=16、循环近场测试,仅供教学与合法授权场景。使用者须自行确保符合当地法规,作者对任何滥用后果不承担责任。
参考
- GNU Radio
analog.wfm_tx文档(插值因子、预加重、频偏参数) - Carson, J. R. --- "Notes on the Theory of Modulation" (1922),卡森带宽准则
- HackRF One 规格(采样率 2--20 MS/s)、SoapySDR HackRF 模块
- ITU / 当地无线电管理法规(FM 广播发射授权要求)