用USRP解码飞机的ADS-B信号
0. 前言
USRP(Universal Software Radio Peripheral,通用软件无线电外设)是Ettus Research公司的一款高性能软件定义无线电(Software Defined Radio, SDR)硬件平台,广泛应用于通信原型验证与信号处理研究。
捕获与解码飞机的ADS-B信号是学习SDR应用的一个经典入门级项目。ADS-B(Automatic Dependent Surveillance – Broadcast,广播式自动相关监视)是现代航空监视的核心技术,飞机以1090MHz频率广播其身份、位置、高度等信息,信号格式公开,非常适合用于SDR技术实践。
本文将介绍如何基于USRP设备完成ADS-B信号的接收与解析。先讲一讲ADS-B信号的报文格式与解码原理,然后实现一个基于GNU Radio的从信号捕获到报文解析的接收机。我用的硬件是入门级的USRP B200,开发环境是WSL下的Ubuntu 22.04系统+GNU Radio+UHD Radioconda环境下的GNU Radio + UHD,环境配置会在附录里介绍。
1. ADS-B介绍
ADS-B是现代航空监视的核心技术。与传统依赖地面雷达询问、飞机被动应答的模式不同,ADS-B是飞机主动、定期广播的。它通过Mode S(S模式) 扩展电文格式,将自身的身份、位置、高度、速度等关键信息以非加密的明文形式向外界广播,任何具备接收设备的地面站或其他飞机都能直接接收,从而实现更精确、高效的空中交通管理。著名的飞行轨迹追踪网站FlightAware就是通过解码ADS-B包来获得飞机的位置信息。
我们的应用仅限于被动接收这些公开的ADS-B广播信号用于学习与研究,不涉及任何信号发射,因此是完全合法的。
调制方式
Mode S采用的是PPM调制(Pulse Position Modulation)。在一个时钟周期里,用脉冲出现的位置来传递信息。在一些光通信和UWB (Ultra Wide Band)应用里,一个时钟周期会被划分为4份或8份,称为4PPM,8PPM,一个符号能传递2个或3个比特。但是Mode S用的比较简单,一个时钟周期只分为2份:高电平出现在前半段表示1,出现在后半段表示0。
如图所示,从8us开始是数据包,每1us传递1比特,总共持续56us或112us。在开始传输数据之前,从0us到8us是前导码,前导码包括4个0.5us宽的脉冲,分别位于第0us,第1us,第3.5us和第4.5us。因为正常的数据包不会出现这样的形状,所以接收机可以侦测有没有收到前导码,以便告诉后续的信号处理模块“数据包要来了,准备好接收”。

解码
ADS-B其实是Mode S格式的数据的一个子集,那些56us长的数据包就是Mode S格式但非ADS-B的,是飞机收到地面站询问之后回复的各种应答消息,而非主动发出的ADS-B广播。
一个112位的ADS-B包包括下面这些信息,其中前5个比特表示数据格式DF,DF=17,18,19是“长的包”,即112位。其中DF=17最常见的ADS-B广播数据,解码器会继续解析后面的TC等字段,提取身份、位置、速度等信息。绝大多数商用客机和通用航空飞机都会使用全球唯一的 24位 ICAO 地址来标识一架飞机。DF=18和19来自军用和匿名物体,不会有ICAO身份标识。
其他DF则是“短的包”,即56位,后续包含的信息各不相同。我们暂时只关注DF=17。

2. 基于USRP的接收机实现
系统框图
为了实现一个接收机,硬件上需要一根1090MHz的天线,一台USRP,一台PC。基于GNU Radio的软件解码器会从USRP收到的基带信号里解析出飞机的数据,并通过TCP传输给客户端。

gr-adsb
开源社区里有很多现成的ads-b的解码器,比如dump1090,可以从接收到的基带信号恢复出时钟,进行PPM解调,并解码飞机信息。我这里选用的是gr-adsb (https://github.com/mhostetter/gr-adsb.git),因为它可以作为模块直接在GNU Radio里调用。
安装很简单,直接编译就行。
$ cd gr-adsb/
$ mkdir build
$ cd build/
$ cmake ../
$ make
$ sudo make install
$ sudo ldconfig
它最核心的模块是framer.py,demod.py,decoder.py这三样。
Framer根据用户设定的阈值,判断有没有接收到前导码,如果接收到会输出一个flag给Demodulator。
在收到前导码后,Demoulator利用概率函数判断后续的比特是0还是1。
Decoder解析整个包得到速度、高度、位置等信息。
流图
在GNU Radio Companion的GUI界面里,绘制这样的流图。ADS-B信号严格一定发射在1090MHz,所以接收频率设定在1090M。采样率需要是2MHz的整数倍,2M效果就很好。我在Complex to Mag之前还放了一个DC Blocker用来消除DC Offset。最后解析出ADS-B信息后通过TCP端口发布消息。

TCP接收端
我写了一个简单的Python接收端用来从TCP端口接收消息并打印。
import zmq
import pmt
def zmq_pdu_subscriber():
context = zmq.Context()
socket = context.socket(zmq.SUB)
# 连接到GNU Radio的ZMQ Pub Sink
# 地址和端口需要与GNU Radio流图中的配置一致
socket.connect("tcp://localhost:5001")
socket.setsockopt_string(zmq.SUBSCRIBE, "")
print("等待接收ADS-B PDU消息...")
try:
while True:
# 接收二进制PDU数据
pdu_bin = socket.recv()
# 反序列化PDU
pdu = pmt.deserialize_str(pdu_bin)
# 提取数据部分(pmt.car获取PDU的数据)
plane = pmt.to_python(pmt.car(pdu))
# 处理ADS-B飞机数据
print("收到飞机数据:")
print(f" ICAO地址: {plane.get('icao', 'N/A')}")
print(f" 时间: {plane.get('datetime', 'N/A')}")
print(f" 经度: {plane.get('longitude', 'N/A')}")
print(f" 纬度: {plane.get('latitude', 'N/A')}")
print(f" 高度: {plane.get('altitude', 'N/A')} 英尺")
print(f" 速度: {plane.get('speed', 'N/A')} 节")
print(f" 航向: {plane.get('heading', 'N/A')}°")
print("-" * 50)
except KeyboardInterrupt:
print("\n接收端停止")
except Exception as e:
print(f"处理数据时出错: {e}")
finally:
socket.close()
context.term()
if __name__ == "__main__":
zmq_pdu_subscriber()
实验结果
同时运行流图和接收端,时域和频域的图像可以帮助调试,通过观察有没有收到长度在120us左右的burst,确认硬件是否连接良好。如果硬件ok并顺利解析,Python接收端就会打印出收到的飞机数据。

3. 总结
解析ADS-B是个很好的软件无线电入门项目,可以从物理层开始一层一层把想要的数据解出来。下一步的学习方向可以包括:
优化Framer和Demodulator的算法,让它对噪声更robust
利用USRP和GNU Radio做更多有意思的事
参考资料
环境配置
ADS-B原理
文档手册
附录A 环境配置
我在配环境时踩了一些坑,包括但不限于:1)MacOS没有合适的安装包 2)Ubuntu 24.04的各种不兼容 3)把设备挂载到WSL而不是Windows。
其实Windows下安装是挺顺利的,见下文,但是我喜欢WSL那种工作环境和娱乐环境分开的设计,所以都装在WSL里了。
踩完坑后发现Ubuntu 22.04是用着最舒服的,安装流程也是最丝滑的,所以下面统一基于Ubuntu 22.04来介绍。
【更新: 不要用WSL!!!】
我后来在试图解决连接USRP时数据溢出的问题的时候折腾了好久,最后发现瓶颈在于WSL的虚拟USB接口的传输速率,所以要避免在虚拟机里运行GNU Radio。
最佳方案是在conda环境里装radioconda,直接装好GNU Radio + UHD 以及 gr-adsb,gr-ieee802.11等扩展模块。
不管在Windows本体还是Mac本体还是Ubuntu本体里,都可以在装了miniconda以后:
conda create -n radioconda -c conda-forge -c ryanvolz --only-deps radioconda
本附录剩下的内容仅供参考,以防偶尔需要手动管理UHD和GNU Radio的安装,或用来快速验证硬件是否正常工作。
Ubuntu驱动
UHD (USRP Hardware Driver)是Ettus Research官方出品的硬件驱动,参考[官方文档] (https://files.ettus.com/manual/page_install.html),用apt-get安装即可。
sudo add-apt-repository ppa:ettusresearch/uhd
sudo apt-get update
sudo apt-get install libuhd-dev uhd-host
装完UHD第一次使用之前要下载给设备的FPGA用的固件,下载完的固件会保存在/usr/share/uhd/images:
sudo uhd_images_downloader
Windows驱动
然而这是WSL特有的一个坑,只装ubuntu版的UHD是不够的,Windows上也需要安装UHD,否则没法把usb设备挂载到WSL,会识别不出USRP设备。
准确地说Windows上需要安装的是LibUSBx驱动,用来支持USB3,但是这个驱动自从UHD4.8开始已经集成进了UHD,所以直接去这里下载官方的UHD安装包是最简单的。
安装完成后在C:\Program Files\UHD\bin目录下启动命令行,运行uhd_usrp_probe,就会检测有没有已经连接的设备,输出如下:
C:\Program Files\UHD\bin>uhd_usrp_probe
[INFO] [UHD] Win32; Microsoft Visual C++ version 1944; Boost_108500; UHD_4.9.0.0-release
[INFO] [B200] Loading firmware image: C:\Program Files\UHD\share/uhd\images\usrp_b200_fw.hex...
[INFO] [B200] Detected Device: B200
[INFO] [B200] Loading FPGA image: C:\Program Files\UHD\share/uhd\images\usrp_b200_fpga.bin...
[INFO] [B200] Operating over USB 3.
[INFO] [B200] Detecting internal GPSDO....
[INFO] [GPS] No GPSDO found
[INFO] [B200] Initialize CODEC control...
[INFO] [B200] Initialize Radio control...
[INFO] [B200] Performing register loopback test...
[INFO] [B200] Register loopback test passed
[INFO] [B200] Setting master clock rate selection to 'automatic'.
[INFO] [B200] Asking for clock rate 16.000000 MHz...
[INFO] [B200] Actually got clock rate 16.000000 MHz.
_____________________________________________________
/
| Device: B-Series Device
| _____________________________________________________
| /
| | Mboard: B200
| | revision: 5
| | product: 1
| | name: MyB200
| | serial: 31DFF0E
| | FW Version: 8.0
| | FPGA Version: 16.0
| |
| | Time sources: none, internal, external, gpsdo
| | Clock sources: internal, external, gpsdo
| | Sensors: ref_locked
| | _____________________________________________________
| | /
| | | RX DSP: 0
| | |
| | | Freq range: -8.000 to 8.000 MHz
| | _____________________________________________________
| | /
| | | RX Dboard: A
| | | _____________________________________________________
| | | /
| | | | RX Frontend: A
| | | | Name: FE-RX1
| | | | Antennas: TX/RX, RX2
| | | | Sensors: temp, rssi, lo_locked
| | | | Freq range: 50.000 to 6000.000 MHz
| | | | Gain range PGA: 0.0 to 76.0 step 1.0 dB
| | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz
| | | | Connection Type: IQ
| | | | Uses LO offset: No
| | | _____________________________________________________
| | | /
| | | | RX Codec: A
| | | | Name: B200 RX dual ADC
| | | | Gain Elements: None
| | _____________________________________________________
| | /
| | | TX DSP: 0
| | |
| | | Freq range: -8.000 to 8.000 MHz
| | _____________________________________________________
| | /
| | | TX Dboard: A
| | | _____________________________________________________
| | | /
| | | | TX Frontend: A
| | | | Name: FE-TX1
| | | | Antennas: TX/RX
| | | | Sensors: temp, lo_locked
| | | | Freq range: 50.000 to 6000.000 MHz
| | | | Gain range PGA: 0.0 to 89.8 step 0.2 dB
| | | | Bandwidth range: 200000.0 to 56000000.0 step 0.0 Hz
| | | | Connection Type: IQ
| | | | Uses LO offset: No
| | | _____________________________________________________
| | | /
| | | | TX Codec: A
| | | | Name: B200 TX dual DAC
| | | | Gain Elements: None
接着用usbipd把设备挂载到WSL下,在WSL里同样运行
uhd_usrp_probe
会输出一样的结果,这就说明UHD硬件驱动已经装好了。
GNU Radio安装
GNU Radio是一款开源软件定义无线电工具包,可以通过其图形化界面(GUI)以拖放“流图”方式快速搭建通信系统,支持Python自定义模块;既能进行离线仿真验证,也能直接驱动USRP等硬件设备进行实时信号收发,大幅简化从设计到实现的流程。
按照官方指示,同样用apt-get安装即可。
sudo apt-get install gnuradio
验证
GNU Radio自带的fft频谱仪是个很有用的小工具:
uhd_fft --freq 2402M
其中freq用来指定频率,还有一些其他参数可以用来调整采样率、窗口等,这些参数在GUI下也都可以调,几乎就是个虚拟频谱仪。
把频率调到2402MHz,也就是蓝牙的Channel 1,就能捕捉到电脑发出的蓝牙低功耗(BLE)广播的Beacon信号。

附录B Downlink Format
虽然我们只关注DF=17,但是gr-adsb的decoder模块其实对各种DF都能解析,在顶层流图里可以设定只解析“长的包” (Extended Squitter Only)或是所有包。其他DF的简介请看Amazon Q的解释。
The decoder handles different DF values by routing them through specific decoding paths in the `decode_message()` method. Here's what happens for each DF value:
## Key DF Values and Their Processing:
**DF 0 & 16** - Air-Air Surveillance (ACAS):
- Decodes vertical status, reply information, and altitude
- DF 0: Also decodes crosslink capability
- DF 16: Also decodes 56-bit message value (MV)
**DF 4, 5, 20, 21** - Surveillance and Comm-B Messages:
- Decodes flight status, downlink request, and utility message fields
- DF 4/20: Altitude replies - extracts 13-bit altitude code
- DF 5/21: Identity replies - extracts 13-bit identity code
- DF 20/21: Also decode 56-bit Comm-B message
**DF 11** - All-Call Reply:
- Decodes capability field and ICAO address
- Updates plane dictionary with basic aircraft info
**DF 17** - Extended Squitter (ADS-B):
- Decodes capability and ICAO address
- Calls `decode_me()` to process the 56-bit ADS-B message containing:
- Aircraft identification/callsign
- Position data (airborne/surface)
- Velocity information
- Status messages
**DF 18** - Extended Squitter/Non-Transponder:
- Decodes CF (Control Field) to determine message type
- Routes to appropriate decoder based on CF value:
- CF 0,1,6: Standard ADS-B messages
- CF 2,3,5: TIS-B messages
- CF 4: Management messages
**DF 19** - Military Extended Squitter:
- Decodes Application Field (AF)
- AF 0: Standard ADS-B messages
- AF 1-7: Reserved for military use
## Message Filtering:
The decoder respects the `msg_filter` setting:
- "All Messages": Processes all DF types
- "Extended Squitter Only": Only processes DF 17, 18, 19
## Error Handling:
- Unknown DF values are logged as debug messages
- Unsupported formats publish "unknown" PDUs
- CRC validation varies by DF type (some use address/parity, others use parity/interrogator ID)
The decoder essentially acts as a router, using the DF value to determine which specific decoding algorithm to apply to extract the relevant aviation data from each message type.