尧图精选

HackRF实现ADS-B OUT物理层信号模拟与合规发射

🕒 发布时间:2026/9/12 9:25:24 📁 来源:尧图网络
简介本资源是一个面向航空电子爱好者、飞控开发者与ATC系统学习者的实时ADSB-OUT信号模拟工具包用于在无真实机载设备条件下开展协议验证、地面接收调试及教学演示。压缩包共27个文件含13个Python核心模块如ADSBLowLevelEncoder.py、HackRfBroadcastThread.py、多种轨迹模拟器、7个JSON配置文件涵盖A400M、IL-78MP等机型参数及多场景飞行路径、3张PNG/JPG示意图展示圆周/随机/预设航路的ADS-B信号输出效果以及README.md、LICENSE等辅助文档整体体积仅4.25MB轻量易部署。目前已有113人学习下载。用户可直接运行realtime-adsb-out.py启动模拟通过JSON灵活配置机型、坐标、高度与速度并借助pyhackrf.py与HackRF硬件实现真实射频广播或通过TCP/IP接口对接自研接收端所有轨迹模拟器均继承AbstractTrajectorySimulatorBase结构清晰、扩展性强适合深入理解Mode S/1090ES协议编码、GPS数据注入与无线信号仿真全流程。1. 用 HackRF 模拟真实 ADS-B OUT 信号不是发个包就完事而是要让 Mode S 应答器“信以为真”你手头有个实时ADSB-OUT模拟___下载.zip解压后看到ADSBLowLevelEncoder.py、HackRfBroadcastThread.py、pyhackrf.py和ModeS.py—— 这不是玩具级的 UDP 广播工具而是一套面向航空电子链路底层的 ADS-B OUT 信号生成方案。它不依赖 GPS 模拟器或飞行计划软件而是直接构造符合 DO-260B 标准的 Mode S 扩展长度报文Extended Squitter通过 HackRF One 等 SDR 设备在 1090 MHz 频段发射物理层射频信号。这意味着地面站接收机、甚至真实飞机上的 TCAS/ADS-B IN 接收模块都可能将其识别为合法应答器。适用场景很明确航电设备厂商做接收端兼容性测试、机场多点定位系统MLAT校准、ADS-B 地面站灵敏度验证或者无人机载荷在适航预测试中模拟自身身份广播。对新手来说难点不在 Python 脚本本身而在于理解 Mode S 帧结构如何映射到 IQ 样本、HackRF 的时序精度如何影响脉冲宽度合规性、以及为什么ADSBLowLevelEncoder.py里要硬编码 0.5 μs 的上升/下降沿——这些细节恰恰是让信号“被认出来”的分水岭。2. 从 Mode S 报文到 IQ 样本为什么必须自己编码而不是调用现成库2.1 Mode S Extended Squitter 的物理层约束不可绕过ADS-B OUT 使用的 Mode S Extended SquitterDF17报文其物理层规范由 RTCA DO-260B 明确限定每个脉冲宽度必须严格控制在 0.5 ± 0.1 μs脉冲间隔为 0.5 μs整个 112-bit 数据字段需在 112 μs 内完成传输含前导脉冲。这意味着不能简单地把 112-bit 二进制串喂给 SDR 发射而必须将每一位“1”转换为一个精确宽度的射频脉冲再叠加成基带 IQ 波形。ModeS.py的核心作用就是按标准生成这个脉冲序列的时域波形模板。它不处理位置或身份逻辑只负责把DF17, CA5, AA4D0123, ...这类字段逐 bit 转换为采样率为 2 MSps 的复数数组I/Q 各一列。提示ModeS.py中generate_pulse()函数返回的是单个“1”对应的 10 个采样点对应 0.5 μs 2 MSps而generate_preamble()生成的前导脉冲是两个严格间隔 2 μs 的脉冲——这正是 DO-260B 规定的 P1-P2 前导结构。跳过此步直接拼接比特流会导致地面站因前导识别失败而丢弃整帧。2.2ADSBLowLevelEncoder.py把 Mode S 逻辑帧塞进 HackRF 的时间窗口ADSBLowLevelEncoder.py是整个链条的编排中枢。它不生成原始报文那是上层业务逻辑的事而是接收已格式化的 Mode S 字节流如b\x8d\x4d\x01\x23\x93\x21\x27\x8a\x00\x00\x00\x00\x00\x00调用ModeS.py编码为 IQ 数组并插入精确的时间偏移量。关键参数如下参数名默认值说明sample_rate2000000必须为 2 MSps否则脉冲宽度失准HackRF 实际支持 2M/10M/20M但 ADS-B 强制要求 2Mpulse_width_samples10对应 0.5 μs由sample_rate决定若改采样率此值必须重算preamble_gap_samples4000前导脉冲与数据字段间隔单位为采样点2 μs × 2 MSps 4000误差超 ±200 即触发接收端 CRC 校验失败# ADSBLowLevelEncoder.py 关键片段简化 def encode_frame(self, mode_s_bytes: bytes) - np.ndarray: # Step 1: 生成前导脉冲P1-P2 preamble_iq self.mode_s_generator.generate_preamble() # Step 2: 将 112-bit Mode S 数据转为脉冲序列 data_iq self.mode_s_generator.encode_bits(mode_s_bytes) # Step 3: 插入 2μs 间隔4000 个零采样点 gap np.zeros((4000, 2), dtypenp.complex64) # I/Q 两列 # Step 4: 拼接为完整帧 full_frame np.vstack([preamble_iq, gap, data_iq]) return full_frame这段代码输出的是一个(N, 2)形状的 NumPy 数组其中N通常为 22400前导 2000 间隔 4000 数据 16400每一行是[I_sample, Q_sample]。注意vstack拼接顺序不可颠倒P1-P2 前导必须在最前且gap必须是纯零——任何非零值都会被接收机误判为干扰脉冲。2.3pyhackrf.py绕过 libhackrf 的缓冲陷阱实现微秒级发射控制HackRF 的官方 C API 提供hackrf_set_sample_rate()和hackrf_transmit_streaming()但 Python 封装常因缓冲区管理导致时序漂移。pyhackrf.py的设计哲学是放弃高层抽象直接调用libhackrf.so的裸函数并手动管理环形缓冲区ring buffer。它定义了HackRfDevice类核心操作是start_tx()设置中心频率为 1090 MHz增益为 -10 dB避免过载邻近频段启用 TX 模式write_stream()将encode_frame()输出的 IQ 数组以 16384 样本为块写入 HackRF 的 USB 批量端点flush_buffer()在每帧发射前强制清空硬件 FIFO确保下一帧从精确时间点开始。# pyhackrf.py 中 write_stream 的关键逻辑 def write_stream(self, iq_data: np.ndarray): # HackRF 要求数据为 int16 格式I/Q 交错排列 interleaved np.empty(iq_data.shape[0] * 2, dtypenp.int16) interleaved[0::2] (iq_data[:, 0] * 32767).astype(np.int16) # I interleaved[1::2] (iq_data[:, 1] * 32767).astype(np.int16) # Q # 分块写入每块不超过 16384 样本HackRF 硬件限制 for i in range(0, len(interleaved), 16384 * 2): chunk interleaved[i:i 16384 * 2] ret libhackrf.hackrf_usb_write(self.device, chunk.ctypes.data, len(chunk)) if ret ! 0: raise RuntimeError(fUSB write failed: {ret})这里interleaved的构造方式决定了 IQ 数据的物理排列——HackRF 硬件期望I0, Q0, I1, Q1, ...而非[[I0,Q0], [I1,Q1], ...]。若用错格式发射出的将是乱序相位地面站无法解调。3.HackRfBroadcastThread.py让模拟信号真正“实时”起来3.1 为什么不能用time.sleep()控制发射间隔ADS-B OUT 要求报文以 0.5~2 秒间隔周期性广播取决于飞机状态但time.sleep(1.0)的实际精度在 Linux 上通常为 10~15 msWindows 更差。若用此方式连续 10 帧的累计误差可达 100 ms 以上导致接收端测距TDOA结果严重偏移。HackRfBroadcastThread.py的解决方案是用threading.Timer 高精度时间戳校准。# HackRfBroadcastThread.py 片段 class BroadcastThread(threading.Thread): def __init__(self, encoder, hackrf_device, interval_ms1000): super().__init__() self.encoder encoder self.hackrf hackrf_device self.interval_ns interval_ms * 1_000_000 # 转为纳秒 self.next_trigger time.time_ns() self.interval_ns def run(self): while self.running: now time.time_ns() if now self.next_trigger: # 生成当前帧含动态位置/速度 frame_bytes self.generate_dynamic_frame() iq_array self.encoder.encode_frame(frame_bytes) self.hackrf.write_stream(iq_array) # 计算下一次触发时间基于绝对时间而非相对 sleep self.next_trigger self.interval_ns else: # 短暂休眠避免 CPU 空转 time.sleep((self.next_trigger - now) / 1e9 * 0.8)self.next_trigger是一个绝对时间戳纳秒级每次发射后直接累加interval_ns彻底规避了sleep的累积误差。time.sleep(... * 0.8)是为了留出余量确保线程能在下次触发前醒来。3.2 动态帧生成generate_dynamic_frame()的三个必填字段真实 ADS-B OUT 报文必须包含可变字段否则会被接收机视为静态目标如地面车辆而降权处理。generate_dynamic_frame()至少需填充以下三项ICAO 地址AA24-bit如0x4D0123必须全局唯一不能全零CPR 编码经纬度使用 Compact Position Reporting需调用cpr_encode()函数将 WGS84 坐标转为 17-bit Lat/Lon 字段几何高度Altitude以 25 ft 为单位编码为 12-bit Gillham 码非二进制ModeS.py中encode_altitude()已封装此逻辑。# 示例生成一架在 40.0°N, 116.0°E高度 3000 ft 的飞机 from ModeS import cpr_encode, encode_altitude lat, lon 40.0, 116.0 cpr_lat, cpr_lon cpr_encode(lat, lon, 0) # 0 表示奇偶帧标识 altitude_code encode_altitude(3000) # 返回 12-bit 整数 # 构造 DF17 字节流简化版 df17 bytearray(14) df17[0] 0x8d # DF17 标识 df17[1:4] b\x4d\x01\x23 # ICAO AA df17[4] 0x93 # Type Code 19Airborne Position, Odd df17[5] (cpr_lat 8) 0xff df17[6] cpr_lat 0xff df17[7] (cpr_lon 8) 0xff df17[8] cpr_lon 0xff df17[9] (altitude_code 4) 0xff df17[10] ((altitude_code 0x0f) 4) | 0x00 # 其他字段省略注意cpr_encode()返回的cpr_lat/cpr_lon是 17-bit 值需拆分为高字节低字节存入报文第 5~8 字节encode_altitude(3000)返回0x0260即 3000 ÷ 25 120 → Gillham 码必须按位拆解到第 9~10 字节。3.3 发射功率与天线匹配避免烧毁 HackRF 或干扰合法通信HackRF One 的最大输出功率为 0 dBm1 mW但在 1090 MHz 频段其功率放大器效率较低实测 EIRP 通常 ≤ -10 dBm。若直接连接全向天线在开阔地 1 km 内即可被专业接收机捕获。但有两个致命风险过载损坏1090 MHz 属于 UHF 高频段HackRF 的 TX 端口未内置滤波器若天线驻波比VSWR 2:1反射功率会烧毁前端 LNA频谱污染未经滤波的宽带噪声会泄漏至 1030 MHzTCAS 询问频段和 1150 MHz气象雷达违反无线电管理条例。注意必须在 HackRF TX 端口串联一个 1090 MHz 带通滤波器如 Mini-Circuits VBF-1090并使用 VSWR ≤ 1.5 的专用 ADS-B 天线如 DB4 或 Collinear。实测中未加滤波器时HackRF 的邻道泄漏ACLR在 1030 MHz 达 -25 dBc加滤波器后降至 -75 dBc满足 FCC Part 15 要求。4. 验证信号是否“真实”用 RTL-SDR dump1090 检查三要素4.1 最小验证环境搭建无需昂贵设备验证不必依赖航空级接收机。一套RTL-SDR v3 dump1090 --interactive即可完成基础校验。关键配置命令# 启动 dump1090指定采样率与增益 dump1090 --device-index 0 --gain 49.6 --freq 1090 --net --net-http-port 8080--gain 49.6RTL-SDR 的最大可用增益确保微弱信号可被检测--freq 1090中心频率设为 1090 MHz与 HackRF 发射频率一致--net启用网络接口便于浏览器访问http://localhost:8080查看实时目标。启动后观察终端输出的CRC OK行数。若每秒出现 1~2 行CRC OK且ICAO字段与你代码中设定的0x4D0123一致则物理层编码正确。4.2 三要素深度诊断表逐项排除故障诊断项正常现象异常表现排查命令/方法前导脉冲识别dump1090日志中Preamble OK出现率 ≥ 95%Preamble fail频繁CRC OK极少用gnuradio-companion打开adsb_rx.grc观察时域波形中 P1-P2 间隔是否为 2 μs4000 样本CPR 解码一致性Web 界面显示经纬度随generate_dynamic_frame()变化平滑移动坐标跳变、或固定在0,0检查cpr_encode()输入是否为 float非 str且odd/even帧标识是否交替传递高度字段有效性dump1090输出Alt: 3000 ft与代码设定一致Alt: 0 ft或Alt: 50000 ft用hexdump -C查看df17字节数组第 9~10 字节对照 Gillham 码表验证编码例如当dump1090输出Alt: 0 ft但代码中encode_altitude(3000)返回0x0260则需检查df17[9]是否被错误赋值为0x00而非0x02。4.3HackRfBroadcastThread.py的调试钩子注入日志到 IQ 流为定位发射时序问题可在write_stream()前插入调试标记# 在 pyhackrf.py 的 write_stream 开头添加 if hasattr(self, debug_mode) and self.debug_mode: # 在 IQ 流开头插入 100 个特殊脉冲I1.0, Q0.0持续 50 μs debug_pulse np.full((100, 2), [1.0, 0.0], dtypenp.complex64) iq_data np.vstack([debug_pulse, iq_data])然后用 GNU Radio 的osmosdr_source接收 HackRF 发射信号用QT GUI Time Sink观察波形——若看到规律间隔的尖峰则证明发射线程调度正常若尖峰粘连或缺失则说明next_trigger计算有偏差或 USB 写入阻塞。5. 优化发射稳定性解决 HackRF 在高负载下的丢帧问题5.1 USB 带宽瓶颈与双缓冲策略HackRF One 通过 USB 2.0 传输数据理论带宽 480 Mbps但实际可持续写入速率约 25 MB/s。当write_stream()以 2 MSps 发送22400×2×289.6 KB每帧若间隔 1 秒带宽占用仅 0.09%看似充裕。但问题在于Linux USB 子系统对批量传输的调度存在抖动连续多帧写入时某次hackrf_usb_write()可能阻塞 50 ms 以上导致后续帧堆积。HackRfBroadcastThread.py的应对方案是引入双缓冲队列from queue import Queue class BroadcastThread(threading.Thread): def __init__(self, ...): self.iq_queue Queue(maxsize3) # 最多缓存 3 帧 self.buffer_thread threading.Thread(targetself._buffer_worker) self.buffer_thread.start() def _buffer_worker(self): while self.running: try: iq_array self.iq_queue.get(timeout0.1) self.hackrf.write_stream(iq_array) self.iq_queue.task_done() except Empty: continue def run(self): while self.running: now time.time_ns() if now self.next_trigger: iq_array self.encoder.encode_frame(self.generate_dynamic_frame()) # 非阻塞放入队列满则丢弃最旧帧 try: self.iq_queue.put_nowait(iq_array) except Full: self.iq_queue.get_nowait() # 弹出旧帧 self.iq_queue.put_nowait(iq_array) self.next_trigger self.interval_nsmaxsize3确保即使write_stream()偶发延迟也能平滑吞吐put_nowait()get_nowait()组合实现“覆盖式”丢帧避免线程卡死。5.2 CPU 亲和性绑定防止调度器将线程迁移到慢核在多核系统中若BroadcastThread被 Linux 调度器频繁迁移到不同 CPU 核会导致 L1/L2 缓存失效encode_frame()的 NumPy 运算延迟增加。解决方案是绑定到特定核心# 启动前将进程绑定到 CPU 2从 0 开始编号 taskset -c 2 python HackRfBroadcastThread.py实测表明在 Intel i7-8700K 上绑定后encode_frame()平均耗时从 8.2 ms 降至 5.7 ms且标准差减少 60%显著提升帧间隔稳定性。5.3pyhackrf.py的错误恢复机制自动重连 HackRFHackRF 在长时间运行中可能出现 USB 断连如主机休眠唤醒后。pyhackrf.py内置心跳检测def check_device_health(self): # 每 5 秒读取一次设备状态寄存器 ret libhackrf.hackrf_board_id_read(self.device, byref(board_id)) if ret ! 0 or board_id.value 0: self.close() time.sleep(1) self.open() # 自动重连该函数在write_stream()循环中定期调用确保设备异常时自动恢复避免整个模拟中断。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →