
简介面向通信工程、电子信息及软件无线电方向的学生这套课程设计项目完整演示了从RTL-SDR或HackRF等软件无线电设备接收原始射频信号到完成AIS船舶自动识别信号解调的全过程包含Python与C两套可运行仿真代码覆盖信号捕获、GMSK解调、位定时恢复、帧同步和NMEA报文输出等关键环节。压缩包共包含88个文件包括23个Python脚本、8个C/C源文件及对应头文件另有文本说明、配置清单、GNU Radio流图、原始测试数据与构建脚本等其中Python脚本便于快速验证思路C/C源码利于性能优化与移植源码、测试与文档三层内容齐全。整体仅347KB轻量易部署目前已有241人浏览学习与下载使用。项目经导师指导获得97分结构完整、开箱即用内附真实AIS数据便于对照验证既适合直接作为课程设计或期末大作业也可作为SDR解调链路二次开发的基础参考框架。1. 把AIS信号从噪声里捞出来不只是接一根天线的事AIS船舶自动识别系统报文在VHF频段上以9600bps的GMSK调制广播调制本身并不复杂真正让人头疼的是解调链路里的频偏估计、时序恢复和HDLC帧同步。RTL-SDR和HackRF这类软件无线电设备把射频前端变成了几行Python代码就能驾驭的IQ数据流但拿到原始IQ采样后如何从噪声和频偏中准确提取AIS报文才是区分“能出声音”和“能解出船位”的分水岭。这套课程设计源码包的核心价值在于它把完整的GNU Radio OOT模块、C底层实现、Python仿真脚本以及实测IQ数据文件打包在一起构成了一条从文件源到NMEA标准句子的完整处理链。无论你是准备课程设计答辩还是想把SDR接收AIS做成一个长期运行的监控节点都可以直接在这个框架上做二次开发而不是从零造轮子。2. AIS信号处理链路GNU Radio OOT模块的角色分工2.1 GMSK调制与AIS物理层的关键参数AIS信号采用GMSK调制波特率9600bps调制指数BT0.4信道带宽25kHz。与普通MSK相比GMSK高斯滤波让频谱旁瓣更低但代价是引入了码间串扰接收端的匹配滤波和时序同步必须足够精准。从RTL-SDR或HackRF捕获到的原始IQ数据实际上包含了中心频率偏移、采样时钟偏差和多径衰落等非理想因素因此解调链路不能只做简单的鉴频器或正交解调。从源码包的文件结构能看出设计者对信号链路的清晰划分。lib目录下既有freqest_impl.cc这类频偏估计模块也有msk_timing_recovery_cc_impl.cc用于符号定时恢复还有corr_est_cc_impl.cc做相关估计pdu_to_nmea_impl.cc把解出的PDU转成标准NMEA语句。这五个模块各司其职构成了一个完整的同步解调框架。模块功能输入/输出freqest估计并校正残留频偏IQ数据 → 频偏补偿后的IQ数据invert处理极性翻转GMSK信号极性不确定IQ数据 → 极性修正后的IQ数据msk_timing_recovery符号定时恢复与重采样过采样IQ → 每符号单点输出corr_est_cc基于训练序列的相关估计完成帧同步符号流 → 突发检测与帧起始位置pdu_to_nmea将HDLC帧解析并转为NMEA 0183格式PDU字节流 → AIS NMEA句子每个模块都实现了GNU Radio的gr::block接口在python目录下也有对应的qa_*.py测试文件可以独立验证各模块的输入输出是否符合预期。2.2 频偏估计模块的工作机制freqest_impl.cc的核心思路是基于AIS突发信号前面的训练序列来估计频偏。AIS帧格式里有一段固定的24bit训练序列“01010101...”在GMSK调制后呈现为明显的单音特征。设计上先对IQ数据做延迟自相关再通过反正切提取相位增量从而得到频偏估计值。代码关键部分如下// freqest_impl.cc 核心逻辑示意 for (int i 0; i noutput_items; i) { // 延迟自相关conj(in[i]) * in[i - delay] gr_complex corr in[i] * conj(in[i - delay]); // 相位增量反映频偏大小 float phase_inc arg(corr); // 累加并求平均得到稳定的频偏估计 freq_est freq_est * 0.99f phase_inc * 0.01f; // 补偿当前样本 out[i] in[i] * exp(-gr_complex(0, freq_est * i)); }这里delay参数通常配置为符号周期的整数倍arg(corr)计算的是相邻延迟样本间的相位差这个相位差正比于频偏值。0.99/0.01的系数构成了一阶低通滤波平滑频偏估计的抖动。实际使用时如果AIS突发间隔较大可以考虑增大低通时间常数让频偏估计更稳定但代价是响应速度变慢。HackRF的本振偏差通常在ppm量级RTL-SDR的R820T2 tuner偏差也类似这个模块在接收前需要设置合理的freq_est带宽我一般把模块参数里的bandwidth设为10kHz左右太小了跟不上信号变化太大了噪声会被放大。2.3 GMSK信号的符号定时恢复从过采样到符号判定msk_timing_recovery_cc_impl.cc实现的是基于Gardner算法的定时恢复。AIS信号每个符号采样多个点恢复模块要找到最佳采样时刻并输出每符号一个点。Gardner算法不需要训练序列直接利用相邻符号的过零幅度来误差检测非常适合突发式AIS信号的盲同步需求。// msk_timing_recovery_cc_impl.cc 误差检测部分 float err 0.0f; if (nid % 2 0) { // 偶数符号索引处做鉴相 err real(out[nid - 1]) * (real(out[nid]) - real(out[nid - 2])); }代码里的判决逻辑是经典的数字锁相环结构定时误差信号经过环路滤波器后驱动一个插值器调整采样相位。需要注意mu是小数间隔取值范围0到1之间表示当前采样点距离理想符号时刻的相对位置。GNU Radio的clock_recovery_mm与这个模块的区别在于msk_timing_recovery是针对MSK/GMSK信号优化过的版本支持配置omega符号周期采样数和gain_omega这两个参数必须和射频前端的采样率严格匹配。比如用2.4MHz采样率接收AISomega就等于2502.4M/9600。3. 从源码构建到数据驱动运行CMake、SWIG与GRC链路3.1 构建环境准备与编译安装这套源码基于GNU Radio 3.8/3.9框架构建方式采用标准CMake流程。先把zip包解压到工作目录确认系统已经安装好GNU Radio开发环境和SWIG# 安装依赖Ubuntu/Debian示例 sudo apt-get install gnuradio-dev swig cmake build-essential # 进入源码根目录创建构建目录 mkdir build cd build # 配置并编译 cmake .. -DCMAKE_INSTALL_PREFIX/usr make -j$(nproc) # 安装OOT模块到GNU Radio sudo make install sudo ldconfigcmake ..阶段会自动生成SWIG接口文件把C实现的模块绑定到Python层。构建完成后运行gnuradio-companion打开python/ais_demod2.grc就能看到可视化流程图。apps/ais_rx是一个可以直接执行的命令行接收程序脚本内部调用了ais_demod.py。如果编译过程中报找不到gnuradio-runtime的头文件多半是gnuradio-dev没装好可以检查一下pkg-config --modversion gnuradio-runtime的输出。3.2 用自带的IQ数据文件跑通解调链路源码包里的data目录提供了两组真实采集的AIS信号数据test_250k.raw和test.raw。test_250k.raw是250kHz采样率采集的窄带数据test.raw则可能是不同前端采集的完整AIS信道数据。用GNU Radio的File Source直接读取配合ais_demod2.grc里的采样率设置就能在GRC窗口中看到解调结果。如果你习惯用命令行验证可以直接跑# 在源码根目录下 python3 python/ais_demod.py --input data/test_250k.raw --rate 250000源码里ais_demod.py封装了完整的解调流程输出会是标准的AIS NMEA句子。命令里的--input指定了文件路径--rate是IQ数据采样率必须与采集时保持一致。如果你用的是test.raw里的数据需要确认文件格式是short还是float存储可以用ls -l先看一下文件大小再估算一下数据时长判断采样率配置是否匹配。从test_250k.raw文件名可以看出这个文件是250kHz采样率下采集的每个样本通常是8位有符号整数也就是两字节IQ文件时长约等于文件大小 / (采样率 * 2)。运行时会打印QPSK/GMSK解调过程中的中间值包括频偏估计结果、同步状态寄存器等。如果你看到AIS: NMEA sentence output这样的日志行说明链路已经通了。检查输出NMEA句子的格式可以用Python快速验证# 验证NMEA句子合法性的简单脚本 import re with open(ais_output.txt, r) as f: for line in f: line line.strip() if not line.startswith(!AIVDM): continue # 拆解字段!AIVDM,条数,序号,串ID,信道,消息类型,... parts line.split(,) if len(parts) 7: msg_type int(parts[5]) print(f有效AIS消息类型{msg_type}位置字段{parts[6]})!AIVDM是AIS NMEA句子的标准前缀msg_type对应AIS消息类型比如类型1是位置报告类型5是静态与航次数据。如果输出的是!AIVDO说明是本船发送的句子在接收场景下更常见的是!AIVDM。如果一直没有有效的!AIVDM输出问题可能出在SDR前端频率偏差未校正或时序恢复模块的增益参数不匹配。3.3 GRC流程图的信号走向与Python仿真测试打开ais_demod2.grc你会看到一条清晰的流水线File Source → freqest → invert → msk_timing_recovery → corr_est_cc → pdu_to_nmea → Socket PDU/File Sink。每个模块的Python wrapper都由ais_swig.i自动生成因此GRC里修改参数会直接映射到C底层。这种设计的好处是调试时在GRC里调整环路带宽线上部署时则可以只依赖C实现去掉GRC的Python开销。python/目录里还有一套完整的qa_*.py单元测试脚本例如qa_freqest.py验证频偏估计模块能否在给定频偏下恢复信号qa_pdu_to_nmea.py验证PDU解析逻辑。我之前做GNU Radio的OOT开发时习惯先在Python层把单模块行为用合成数据验证一遍再接入真实IQ数据。一个典型测试命令是python3 -m pytest python/qa_freqest.py -v --tbshort3.4 C与Python的分工边界这套项目中C承担了所有实时信号处理任务Python则负责流程编排和仿真验证。lib目录下的test_ais.cc是C层面的自测程序需要链接到对应的库才能运行它主要验证了各模块在C API下的输入输出一致性。而python/__init__.py里的ais_demod类则是一个更上层的封装适合快速做离线数据处理。如果你需要在嵌入式设备上部署AIS接收机通常只保留C模块用gnuradio-companion生成Python流程或直接用gr::basic_block构建流程图。如果你想提升接收灵敏度可以在信号链路的freqest模块之前插入一个low_pass_filter带宽设为25kHz左右抑制带外干扰。4. 参数调优与排错频偏带宽、时序恢复环路与射频前端4.1 常见故障现象与排查手段这套源码包虽然可以“下载即用”但换一个环境、换一台SDR设备参数往往需要重新标定。我梳理了三个最容易出问题的环节频偏估计器带宽、时序恢复环路增益、以及射频前端的采样率设置。下面这张表总结了典型现象和对应解法故障现象可能原因检查/修改参数解调输出大量乱码无NMEA频偏估计带宽过大或过小检查freqest的bandwidthRTL-SDR建议5~15kHzHackRF建议10~20kHz输出NMEA句子中有CRC错误符号定时同步点偏移减小msk_timing_recovery的gain_omega重新校准omega只有极少数AIS消息被解出采样率与符号率不匹配确认文件源rate参数标准AIS波特率9600bps采样率应为波特率整数倍verbose模式无任何输出信号极性反转导致相关峰值过低检查invert模块状态尝试手动切换极性实时接收时频繁丢包USB传输或CPU占用率过高降低采样率使用float类型接收减少转换开销4.2 用Python辅助定位频偏估计是否收敛如果你怀疑频偏估计模块不收敛可以用下面的Python脚本直接读取关键中间量。在实际项目中我会在freqest_impl.cc里加一行fprintf输出当前的频偏估计值编译后重新运行数据。这种方法比在GRC里加探针更直接尤其在处理长时间采集文件时能快速判断频偏估计是否在训练序列期间完成收敛。也可以借助GNU Radio的probe_signal块把频偏估计算子实时推到QT GUI的数值窗口里但文件离线调试时日志更直观。import numpy as np # 读取test_250k.raw假设为int16 IQ交织 raw np.fromfile(data/test_250k.raw, dtypenp.int16) iq raw[0::2] 1j * raw[1::2] iq iq.astype(np.float32) / 32768.0 # 训练序列的频谱特征24个0101交替bit在GMSK调制后呈现单音 # 计算短时傅里叶变换观察训练序列位置的谱峰 from scipy.signal import spectrogram f, t, Sxx spectrogram(iq, fs250000, nperseg256, noverlap128) # 找能量最强的时频点 idx np.unravel_index(np.argmax(Sxx), Sxx.shape) print(f峰值频偏约 {f[idx[0]]} Hz出现在 {t[idx[1]]:.2f}s)spectrogram计算的时频图里如果训练序列位置出现明显的单一频率峰值说明频偏估计模块有明确的校正目标。如果峰值是分散的那多半是前置滤波没做好或者是文件数据本身就是多信道混叠的。4.3 HackRF本振泄露与invert模块的正确使用HackRF的常见问题之一是本振泄露LO leakage表现为频谱中心出现DC分量对AIS这类低信噪比信号会造成一定干扰。另外GMSK信号本身存在极性模糊问题——接收端不知道发送端的初始相位状态invert模块就是用来消除这种不确定性的。源码包里的invert_impl.cc核心就是对IQ数据的虚部取反或实部取反相当于把星座图翻转。在AIS场景下如果你发现信号链路的corr_est_cc相关峰始终低于阈值尝试切换invert的invert参数。测试时可以通过qa_invert.py快速验证python3 python/qa_invert.py -v该测试会生成一组已知的GMSK符号序列分别以两种极性送入invert模块确认输出一致。这也是为什么在GRC里你会看到invert被放在freqest后面、msk_timing_recovery前面——它需要在符号恢复之前把极性纠正过来否则后面的相关峰会被削弱一半。在射频前端方面RTL-SDR的镜像抑制和DC偏差通常比HackRF更明显建议先用gr-osmosdr的rtl_sdr源加一个高通滤波器截止频率约1kHz把DC分量滤掉再进入freqest。HackRF接收AIS时本振泄露会造成中心频点附近大约几百赫兹的干扰如果AIS信号中心频率正好落在DC附近解调质量会显著下降这时可以把射频中心频率偏移几百赫兹然后在数字域补偿回来。4.4 数据文件与采样率匹配的常见陷阱很多人拿到test.raw后直接套用test_250k.raw的参数导致解调失败。这里的关键是test.raw的采样率不一定是250kHz要看采集时的配置。从文件大小和时长反推采样率是一个常用技巧但更直接的办法是看python/目录下是否有对应的配置文件。如果ais_demod.py里写死了采样率你需要手动确认# 假设test.raw有60MBint16 IQ持续时长约120秒则采样率约 python3 -c import os; sizeos.path.getsize(data/test.raw); print(f采样率約 {size/2/120:.0f} Hz)这里用size除以2是因为每个IQ样本是int16模式下的两个字节I和Q各一个字节。如果算出来是41667左右那说明采集时用的是48kHz音频前端而非SDR直接采样。实际上AIS信号带宽只有25kHz用48kHz采样率也能完整捕获但GNU Radio信号链所有模块的omega参数必须对应改成548000/9600否则时序恢复完全不工作。这个点非常隐蔽因为grc里的File Source没报错但输出就是一片噪声。5. 把仿真链路改写为实时接收RTL-SDR与HackRF接入的工程化细节5.1 用OsmoSDR源替换文件源的最小改动方案仿真链路跑通后接着做的是把它变成实时接收机。在GRC里把File Source替换成OSMOCOM Source设备类型选择rtl0或hackrf0采样率按设备能力设定。RTL-SDR的采样率来自R820T2/RTL2832U的组合常见的是2.4MHz或2.048MHzHackRF支持到20MHz但AIS接收不需要那么高的采样率4MHz或8MHz就足够了。GRC里设置完成后实际生成的Python代码核心是osmosdr.source的配置from gnuradio import osmosdr from gnuradio import blocks # 初始化RTL-SDR源 sdr osmosdr.source(argsnumchan1) sdr.set_sample_rate(2.4e6) # 采样率2.4MHz sdr.set_center_freq(161.975e6) # AIS频道AIS1 sdr.set_freq_corr(0, 0) sdr.set_gain(40, 0) # 增益40dB可根据信号强度调整 sdr.set_if_gain(20, 0) # RTL-SDR的IF增益 sdr.set_bb_gain(20, 0) # 基带增益这段代码里set_sample_rate必须与后面所有同步模块的内存参数匹配即msk_timing_recovery中的omega 待采样率/9600。set_center_freq设置接收频点AIS有两个主要频点AIS1是161.975MHzAIS2是162.025MHz可以交替监听也可以把中心频率设在162.000MHz让两个信道同时落在带宽范围内。RTL-SDR的射频增益分为三段LNA/IF/BB通常把LNA设为自动或中等值30dB左右IF和BB设20dB以上增益太大会引入交调太小则信噪比不足。HackRF的增益结构更简单直接set_gain(30)即可但要注意HackRF的接收前端在低频段可能存在本振泄露建议在freqest之前加一个Freq XLating FIR滤波把零频附近的干扰带宽剔除掉。5.2 实时接收时的CPU占用与数据流转优化实时接收时CPU占用率是首要关注指标。GNU Radio的Python调度机制有一定开销如果希望最大化吞吐量一个更好的方案是用gr::flowgraph的C API构建链路或者用gnuradio-companion的Generate Options选为No GUI无GUI模式。在纯命令行环境下apps/ais_rx使用了类似策略运行后直接输出NMEA句子到stdout。另一个并行思路是保留Python流图但采用blocks.vector_sink批量收集数据避免每个样本都经过Python回调这在信号输入非常密集时收益明显。从实测来看RTL-SDR使用2.4MHz采样率接收AIS时GNU Radio的全链路CPU占用约30%~40%i5四核HackRF使用8MHz采样率时占用会升高到60%左右。若CPU资源紧张可以降低采样倍数。AIS信号带宽25kHz理论上最小采样率50kHz即可无误接收但考虑滤波滚降实际至少取采样率200kHz以上。与其用高采样率来换滤波余量不如在射频前端直接加SAW滤波器或LC带通采样率设2.4MHz或4MHz就足够信号质量这能显著降低后续FFT和定时恢复的算力压力。用HackRF接收时还有一个实用技巧把采样率设置为能被符号率整除的值。例如用2.048MHz210×9600这样omega就是213.33虽然非整数但比直接用2.5MHz更接近整数倍能减轻定时恢复环路的收敛负担。如果主板支持PPS同步还可以用uhd源配合DOA到达方向估计模块做信源测向但这已经超出AIS解码本身的范围。工程上最常见的做法是保持msk_timing_recovery模块的omega可微调因为SDR设备实际时钟与标称值总存在ppm级偏差运行时间长后采样点会逐渐漂移。我通常把gain_omega设为0.02左右omega设为理论值运行15分钟后观察解调误码率如有恶化再微调omega的初始值。5.3 验证实时接收效果的三个信号指标没有频谱仪的情况下判断AIS接收机是否正常工作可以关注三个指标解出的NMEA句子中!AIVDM消息的数量正常繁忙航道每分钟几条到几十条不等消息类型分布是否合理类型1、2、3位置报告占绝大多数类型5静态信息偶尔出现corr_est_cc输出的相关峰幅度是否稳定如果相关峰时强时弱说明天线位置存在多径衰落尝试移动天线位置如果在室内接收不到信号不要急着调算法参数先检查天线是否放到窗边、射频增益是否太小。AIS信号从船载天线发出频率在VHF段穿墙损耗较大通常室外天线或至少靠窗位置才能稳定接收。实际场景中一条船距你5海里时信号强度约-90dBm到-70dBmRTL-SDR的灵敏度足够应付但如果船在港口内被建筑物遮挡信号起伏会很明显。这中间还有一个常被忽略的细节pdu_to_nmea模块输出的NMEA句子包含六位填充位字段通过!AIVDM后的第一个数字标识消息类型1表示单条消息2表示多条分帧。调试时如果只看到分帧消息而没有完整重组可以在下游加一个简单的NMEA消息重组器按消息ID和帧号拼接分帧内容这样解析AIS类型5消息含船名、呼号、船型等静态数据更完整。结尾留一个建议把pdu_to_nmea解析出的原生句子上抛到InfluxDB或Redis配合Grafana做时间线分布比单纯打印更容易观察AIS实时信号特征与信号质量变化趋势。本文还有配套的精品资源点击获取