十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AIS信号解调实战:用Python和C协同处理RTL-SDR与HackRF数据

AIS信号解调实战:用Python和C协同处理RTL-SDR与HackRF数据 简介基于Python和C编写的AIS信号接收解调仿真项目配合RTL-SDR或HackRF等软件无线电设备使用可完成AIS基带信号的接收、解调并转换为NMEA语句适合通信、电子信息、数学等专业学习者参考。压缩包共85个文件包括23个Python脚本、8个C源文件、13个头文件以及CMAKE构建配置、GRC流图、YAML模块描述、原始数据test.raw与使用说明文档整体大小约199KB。目前已有828人浏览学习。内容覆盖调制映射、频率估计、MSK定时恢复、PDU转NMEA等关键解调模块并配有可独立运行的GNU Radio外置模块、示例数据及setup/构建脚本参照“使用说明.md”可快速配置环境观察AIS信号从采样到输出的完整链路适合课程设计、毕业课题或二次开发参考。1. 为什么AIS接收要同时用Python和CRTL-SDR与HackRF的工程定位港口监控、船舶追踪或者海事数据采集场景里AIS信号几乎是首选数据源——它工作在VHF频段公开、无授权要求而且一艘船的MMSI、航向、速度、船名都在里面。RTL-SDR几十块钱HackRF稍贵但频率范围和采样率都给得更足配合Python做原型验证、C来做高性能解调核心这套组合在无线电爱好者和海事物联网项目里非常常见。真正动手以后会发现解调链路比“收个FM广播”繁琐得多AIS用的是9600bps的GMSK不是简单的FM对讲机信号帧结构里NRZI编码、0比特插入、CRC-16一环扣一环任何一个环节对不齐出来的就是一串乱码。反直觉的地方在于射频前端反而不是瓶颈定时同步和位同步才是。本文顺着“采集→解调→解析→仿真验证”的顺序把RTL-SDR和HackRF统一到一条处理链路上Python负责看波形、调参数C负责把算法变成能扛住实时采样的核心。适合手里有SDR设备、想自己写AIS解调器而不是直接跑现成软件的人。2. 先把AIS信号解调链路拆开GMSK、NRZI和HDLC帧格式2.1 三个容易混淆的物理层参数AIS物理层经常被误当成“窄带FM”实际上它的调制方式是GMSK也就是高斯滤波后的MSK。理解AIS解调第一件事是把下面这张参数表记住后面的采样率、匹配滤波、定时同步全都从这些数字推导。参数取值说明信道A频率161.975 MHz民用船舶AIS 1信道信道B频率162.025 MHzAIS 2信道两信道间隔25 kHz数据速率9600 bps基带符号速率调制方式GMSK调制指数0.5等效频偏约2.4 kHz高斯滤波BT积约0.4发送端成型滤波接收端匹配时也按此设定发射带宽25 kHz信道间隔也有一部分区域用12.5 kHzGMSK的关键是把NRZ电平先过一级高斯低通滤波再做频率调制。这样做能压缩已调信号带宽但也意味着在接收端不能用“过零检测硬判决”简单打发必须做匹配滤波否则相邻符号间会互相干扰。RTL-SDR和HackRF的射频带宽都远超25 kHz所以采集时中心频率放两个信道中间比如162.000 MHz一个文件里同时包含AIS 1和AIS 2两路信号后续在数字域抽出来分别解调。这是和“.wav文件音频解调”完全不同的思路。2.2 解调链路的五个阶段从IQ到船舶信息完整链路通常拆成下面几个阶段每个阶段都对应一个可单独测试的模块正交鉴频对复数IQ流求瞬时相位差分输出一个近似“频率随时间变化”的浮点序列。匹配滤波与定时同步GMSK的符号间干扰靠高斯匹配滤波抵消定时恢复确定每个符号的最佳抽样点。NRZI解码AIS物理层用NRZI表示数据电平变化表示“0”电平不变表示“1”。HDLC帧同步找到0x7E标志位做0比特删除还原原始数据单元。CRC校验和报文解析CRC-16校验通过后按AIS二进制报文标准解析出MMSI、经纬度、船名等字段。前四步最容易出错的是第2步和第3步。定时同步偏差超过半个符号周期匹配滤波再准也没用NRZI方向搞反整帧解析全错但不会报任何运行时错误这种故障最让人头疼。2.3 帧同步里的0比特插入和CRCHDLC在AIS里使用0111 1110作为帧标志为了防止用户数据里出现连续6个1被误判成帧尾发送端会自动在连续5个1后面插入一个0。接收端对应要做删除操作。CRC用CRC-16-CCITT多项式0x1021初值0xFFFF收尾不需要异或0xFFFFAIS规范里校验值不是标准CCITT的非反转变体。static int hdlc_bit_unstuff(const uint8_t *in, int in_bits, uint8_t *out) { int ones 0, oi 0; // out 使用前必须清零本函数只按位置写位 for (int i 0; i in_bits; i) { int b (in[i / 8] (7 - (i % 8))) 1; if (ones 5 b 0) { ones 0; // 删除填充的 0 比特 continue; } if (b) ones; else ones 0; if (ones 6) break; // 遇到下一个帧标志 if (b) out[oi / 8] | (1 (7 - (oi % 8))); else out[oi / 8] ~(1 (7 - (oi % 8))); oi; } return oi; }这段代码里ones 5 b 0是删除填充0ones 6直接结束当前帧后续再回头找下一帧flag。位序约定是高位在前和AIS空口字节序一致。如果后面自己写字节组装这里顺序反了会直接导致CRC不过。3. 用Python RTL-SDR在本地跑通AIS最小接收链路3.1 采集数据RTL-SDR一条命令HackRF另一条先把设备驱动装好Windows下用Zadig给RTL-SDR装WinUSB驱动Linux下直接装librtlsdr-dev和libhackrf-devmacOS用Homebrew装rtl-sdr和hackrf。RTL-SDR采集AIS用下面这条命令# 中心频率 162.000 MHz2.4 MS/s 采样率两个 AIS 信道都在带宽内 rtl_sdr -f 162000000 -s 2400000 -g 40 -n 144000000 ais_2min.iq-f是中心频率-s是采样率-g 40是增益单位是0.1 dB也就是4 dB实际LNA、混频器、基带增益分段作用-n 144000000表示采集1.44亿个采样点。2.4 MS/s是RTL2832U比较稳的采样率数据量约5 MB/s录两分钟大约600 MB。用HackRF时命令略有不同# HackRF 采样率下限 2 MHz此处用 8 MHz 留出抽取余量 hackrf_transfer -f 162000000 -s 8000000 -g 40 -n 960000000 -r ais_hackrf.iqHackRF是半双工接收时把中心频率设为162.000 MHz同样能让两个AIS信道落在基带±25 kHz处避开本振泄露集中的DC附近。如果嫌文件太大后续可以先用CIC滤波器把8 MHz抽取到2.4 MHz再存盘。3.2 Python读IQ文件并做正交鉴频采集文件是8位有符号整数I/Q交错的裸数据读取方式见下面代码。这段代码同时在后面讲HackRF时复用不管设备是谁只要数据是“int8交错IQ”格式处理逻辑完全一样。import numpy as np def load_iq(path, dtypenp.int8): raw np.fromfile(path, dtypedtype).astype(np.int16) i raw[0::2].astype(np.float32) q raw[1::2].astype(np.float32) return (i 1j * q) / 128.0 def fm_disc(iq): # 相邻采样共轭相乘相位就是瞬时频偏 prod iq[1:] * np.conj(iq[:-1]) dphi np.unwrap(np.angle(prod)) kernel np.ones(4, dtypenp.float32) / 4.0 return np.convolve(dphi, kernel, modesame)raw[0::2]取I路raw[1::2]取Q路除以128是把int8映射到±1范围避免后续浮点乘法溢出。np.conj(iq[:-1]) * iq[1:]这一步里角度变化就是瞬时频率np.unwrap处理跨±π的相位跳变加不加这个函数结果完全不同——如果省掉你会看到解调波形出现大量尖峰。卷积核长度4对应约600 kHz等效窗口主要用来削掉DAC开关噪声和前端脉冲干扰窗口太大会把AIS的π/2频偏压平。3.3 匹配滤波、符号抽样和NRZI解码AIS符号周期是9600分之一秒RTL-SDR采样率2.4 MHz时每个符号有250个采样点。匹配滤波直接用“符号长度内平均”的矩形窗口即可效果够用比严格的高斯匹配滤波实现简单一个数量级def demod_ais(dphi, fs2_400_000.0, rate9600): nsps int(fs / rate) # 250 win np.ones(nsps, dtypenp.float32) / nsps matched np.convolve(dphi, win, modefull)[::nsps] # 符号抽样正负表示频偏方向即 GMSK 当前符号电平 bits (matched 0).astype(np.uint8) # NRZI 解码电平不变 原始比特 1电平变化 原始比特 0 nrz_output (bits[1:] bits[:-1]).astype(np.uint8) return nrz_output注意最后一句bits[1:] bits[:-1]的意思是“当前符号和上一个符号对比相同输出1不同输出0”。AIS的NRZI约定不同资料里出现过两种方向遇到解出来全是乱码时把这一行的比较方向反过来再试。定时同步方面上面的[::nsps]是从文件头开始硬取实际信号起始位置不一定刚好对上工程做法是先检测一段已知的帧头pattern确定偏移量再抽样。3.4 帧搜索从位流里找到7E标志位流里找0111 1110标志位用numpy写一个紧凑版本def find_flags(bits): window np.convolve(bits, np.array([1,1,1,1,1,1,1,1], np.int16), modevalid) # 7E 是 0111 1110窗口内1的个数为6且首尾为0 idx np.where(window 6)[0] flags [] for i in idx: seg bits[i:i8] if seg[0] 0 and seg[7] 0 and seg[1:7].all(): flags.append(i) return flags这个实现为了短牺牲了精度真正的0x7E是0111 1110而1011 1110也会被这个窗口捕获。所以后续必须做CRC过滤CRC不过的候选帧直接丢弃。用这个办法找flag一个几百兆的IQ文件能扫出上千个候选过滤后剩下的是真实AIS报文数目和附近船只有关。到这一步Python原型已经能出MMSI和报文类型。2.4 MS/s数据单纯做离线分析numpy向量化完全够用但接实时采集会掉帧原因和下一章讲的问题一致。4. 用C重写解调核心从文件仿真到实时采样的性能边界4.1 Python慢在哪里不是循环是内存分配rtl_sdr的USB传输块默认是512字节对齐的多个扇区每个块约几十KB回调频率每秒上百次。Python在GIL下做numpy切片、分配过渡数组每次回调都产生新对象数据量一旦到2.4 MS/s就会堆积。实测numpy向量化鉴频本身只花几十毫秒处理100万样本但加上文件I/O、逐帧扫描、字符串解析和进程内GC实时率很难压住。C重写核心不是要重写整个解析器而是把“鉴频→匹配滤波→位同步”这一段重写成无分配、顺序遍历的代码。解析帧字段和输出NMEA语句仍然留给Python做这样最省开发时间。4.2 正交鉴频的C实现和相位解缠绕#include stdint.h #include math.h void fm_disc_demod(const int8_t *iq, size_t n, float *out) { float prev_phase 0.0f; const float *last NULL; for (size_t i 1; i n; i) { float i_prev (float)iq[(i-1)*2]; float q_prev (float)iq[(i-1)*2 1]; float i_cur (float)iq[i*2]; float q_cur (float)iq[i*2 1]; float num q_cur * i_prev - i_cur * q_prev; float den i_cur * i_prev q_cur * q_prev; float phase atan2f(num, den); float delta phase - prev_phase; if (delta M_PI) delta - 2.0f * M_PI; if (delta -M_PI) delta 2.0f * M_PI; out[i - 1] delta; prev_phase phase; } }这段实现里num和den是上一节Python里共轭相乘后取角度的展开式避免构造复数对象。atan2f是单精度版本比双精度atan2快约一倍。手动做相位解缠绕是因为真实信号里噪声会让相位差接近±π跳到±π附近时不做修正会在输出里引入尖峰。位同步用开环法先对out做长度250的滑动平均然后每250个点抽一个。C实现里用一个累加器滚动更新窗口和避免每次重新加250个数。// 滑动窗口求和窗口宽度 nsps输出每一位 static void matched_filter_throttle(const float *in, size_t n, size_t nsps, float *out, size_t *out_n) { double acc 0.0; for (size_t i 0; i n; i) { acc in[i]; if (i nsps) acc - in[i - nsps]; if ((i % nsps) (nsps - 1)) { out[*out_n] (float)(acc / nsps); (*out_n); } } }这里用double累积是为了避免浮点误差在窗口求和里积累实际性能损失可以忽略。nsps太小会导致输出和输入长度不同步工程上建议把nsps定为常量并在程序入口检查整除关系。4.3 把C核心做成命令行工具先把解调核心编译成独立二进制输入输出都用文件方便离线调试gcc -O3 -marchnative -o ais_demod ais_demod.c -lm ./ais_demod -i ais_2min.iq -o demoded.bin -f 2400000 -r 9600-marchnative让编译器按当前CPU指令集做自动向量化这里是复用SSE/AVX的关键。输出demoded.bin存的是每一位的uint8值可以直接给Python做帧解析也可以继续在C里写帧搜索。这个阶段的主要目的是验证算法正确性因为文件可以反复跑波形能对比。处理环节Python numpyC -O2 按样本循环240万样本鉴频约30~80 ms约5~15 ms鉴频匹配滤波抽样约50~150 ms约10~30 ms帧搜索解析约150 ms~数秒与C中实现相关这张表不是精确基准量级关系可以参考C版本即使完全不用SIMD也能比Python流程快5到10倍且内存占用稳定在几个MB级别。4.4 实时回调和HackRF本振泄露的实际影响离线跑通后接实时设备RTL-SDR用rtlsdr_read_async注册回调函数每个回调拿到一个数据块直接丢进ring buffer。ring buffer长度设为采样率对应的1秒处理线程跟不上时丢弃旧数据而不是覆盖新数据。这样能保证实时性代价是高峰期丢帧。HackRF做AIS接收时有个更隐蔽的坑它采用零中频架构本振泄露会直接落在DC附近形成连续波尖峰。如果采集中心频率设为161.975 MHzAIS 1信道正对DC整段信号都会被本振泄漏抬高解调输出的直流偏置足以让符号判决全部偏向一边。常见做法是把中心频率偏离信道25~50 kHz让有用信号离开DC然后在数字域用低通滤波器恢复。所以前文采集命令特意用了162.000 MHz而不是161.975 MHz就是为了这个原因。5. 数据仿真与验证方法没有船也能压测你的AIS接收机5.1 自己生成一个带CRC的AIS帧测试接收机最麻烦的是附近不一定有船。用Python合成一段“假AIS”基带信号就能在桌面环境下验证解调链路每一环的参数。def crc16_ccitt(data: bytes) - int: crc 0xFFFF for byte in data: crc ^ byte 8 for _ in range(8): crc ((crc 1) ^ 0x1021) if (crc 0x8000) else (crc 1) crc 0xFFFF return crc # payload 是除了 flag 和 CRC 之外的完整 AIS 数据单元 payload bytes([0x00, 0x0b, 0x38, 0x00]) # 示例占位实际用报文生成器构造 frame b\x7e payload int.to_bytes(crc16_ccitt(payload), 2, big) b\x7e合成基带时把frame按位展开经过NRZI编码、双极性映射0对应11对应-1再用高斯低通滤波做成GMSK信号先让双极性序列通过BT0.4的高斯低通再积分成相位最后exp(j*phase)得到复数基带。加入高斯白噪声后直接喂给前面的解调代码统计CRC通过的帧数量和误码率。5.2 三个必调参数和它们的故障特征用仿真信号调参比用真实信号快得多因为真实信号里的干扰无法区分是信道问题还是算法问题。下面三个参数是我会优先调的参数范围调不准时的现象匹配滤波BT0.3~0.5眼图闭合符号判决抖动CRC通过率骤降符号定时偏移0~1个符号周期帧头能找到但数据位全错或CRC始终失败去直流偏移量自动或手动信号幅度正常但解调输出整体偏正/偏负仿真信号加噪声后如果CRC通过率从95%掉到50%先查定时偏差再查匹配滤波。GMSK信号对符号定时误差的容忍度大约在±25%符号周期超过这个范围基本无法解调。5.3 录真实信号做交叉验证仿真通过后录制一段时间真实信号rtl_sdr -f 162000000 -s 2400000 -g 40 -n 720000000 ais_5min.iq把同一份IQ文件同时喂给C解调器和GNURadio里现成的gr-ais模块对比两者输出的NMEA句子数量。两者报文基本一致说明解调方向、位同步和CRC逻辑都没问题。解调正确率不高时优先用HackRF的偏置供电给天线放大器供12V如果天线放大器支持供电RTL-SDR则先检查增益设置是否落在LNA压缩区。整个过程不需要依赖船只正好经过测试点只要把文件录制、仿真、回放这三个流程跑通AIS接收链路就算真正闭环了。本文还有配套的精品资源点击获取
返回列表