news 2026/9/15 4:39:45

基于GNU Radio的AIS信号GMSK解调:频偏估计与定时恢复实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于GNU Radio的AIS信号GMSK解调:频偏估计与定时恢复实战

简介:面向通信工程、电子信息及软件无线电方向的学生,这套课程设计项目完整演示了从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,调制指数BT=0.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采样率接收AIS,omega就等于250(2.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 ldconfig

cmake ..阶段会自动生成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.rawtest.rawtest_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 --tb=short

3.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频偏估计带宽过大或过小检查freqestbandwidth,RTL-SDR建议5~15kHz;HackRF建议10~20kHz
输出NMEA句子中有CRC错误符号定时同步点偏移减小msk_timing_recoverygain_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', dtype=np.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, fs=250000, nperseg=256, noverlap=128) # 找能量最强的时频点 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相关峰始终低于阈值,尝试切换invertinvert参数。测试时可以通过qa_invert.py快速验证:

python3 python/qa_invert.py -v

该测试会生成一组已知的GMSK符号序列,分别以两种极性送入invert模块,确认输出一致。这也是为什么在GRC里你会看到invert被放在freqest后面、msk_timing_recovery前面——它需要在符号恢复之前把极性纠正过来,否则后面的相关峰会被削弱一半。

在射频前端方面,RTL-SDR的镜像抑制和DC偏差通常比HackRF更明显,建议先用gr-osmosdrrtl_sdr源加一个高通滤波器(截止频率约1kHz)把DC分量滤掉,再进入freqest。HackRF接收AIS时,本振泄露会造成中心频点附近大约几百赫兹的干扰,如果AIS信号中心频率正好落在DC附近,解调质量会显著下降,这时可以把射频中心频率偏移几百赫兹,然后在数字域补偿回来。

4.4 数据文件与采样率匹配的常见陷阱

很多人拿到test.raw后直接套用test_250k.raw的参数导致解调失败。这里的关键是:test.raw的采样率不一定是250kHz,要看采集时的配置。从文件大小和时长反推采样率是一个常用技巧,但更直接的办法是看python/目录下是否有对应的配置文件。如果ais_demod.py里写死了采样率,你需要手动确认:

# 假设test.raw有60MB,int16 IQ,持续时长约120秒,则采样率约 python3 -c "import os; size=os.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参数必须对应改成5(48000/9600),否则时序恢复完全不工作。这个点非常隐蔽,因为grc里的File Source没报错,但输出就是一片噪声。

5. 把仿真链路改写为实时接收:RTL-SDR与HackRF接入的工程化细节

5.1 用OsmoSDR源替换文件源的最小改动方案

仿真链路跑通后,接着做的是把它变成实时接收机。在GRC里,把File Source替换成OSMOCOM Source,设备类型选择rtl=0hackrf=0,采样率按设备能力设定。RTL-SDR的采样率来自R820T2/RTL2832U的组合,常见的是2.4MHz或2.048MHz;HackRF支持到20MHz,但AIS接收不需要那么高的采样率,4MHz或8MHz就足够了。

GRC里设置完成后,实际生成的Python代码核心是osmosdr.source的配置:

from gnuradio import osmosdr from gnuradio import blocks # 初始化RTL-SDR源 sdr = osmosdr.source(args="numchan=1") 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 = 待采样率/9600set_center_freq设置接收频点,AIS有两个主要频点:AIS1是161.975MHz,AIS2是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-companionGenerate 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.048MHz(210×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到-70dBm,RTL-SDR的灵敏度足够应付,但如果船在港口内被建筑物遮挡,信号起伏会很明显。

这中间还有一个常被忽略的细节:pdu_to_nmea模块输出的NMEA句子包含六位填充位字段,通过!AIVDM后的第一个数字标识消息类型,1表示单条消息,2表示多条分帧。调试时如果只看到分帧消息而没有完整重组,可以在下游加一个简单的NMEA消息重组器,按消息ID帧号拼接分帧内容,这样解析AIS类型5消息(含船名、呼号、船型等静态数据)更完整。结尾留一个建议:把pdu_to_nmea解析出的原生句子上抛到InfluxDB或Redis,配合Grafana做时间线分布,比单纯打印更容易观察AIS实时信号特征与信号质量变化趋势。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 4:38:30

美国野火烟雾数据集解析与应用实践

1. 项目背景与数据价值美国作为全球野火高发地区&#xff0c;其烟雾扩散数据对气候研究、公共健康和政策制定具有重要价值。CnOpenData最新发布的2003-2025年美国每日野火烟雾数据集&#xff0c;填补了中长期环境监测数据的空白。这个数据集最显著的特点是实现了三个维度的突破…

作者头像 李华
网站建设 2026/9/15 4:38:15

Doris数据倾斜治理:分区分桶设计与查询优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 4:36:30

2D游戏法线贴图程序化生成工作流:五分钟从平涂到动态受光

1. 从一张平涂到动态受光&#xff1a;这条工作流到底改变了什么先说我自己的处境。前阵子帮朋友的项目补一批2D道具资源&#xff0c;数量不大&#xff0c;也就二十来件&#xff1a;剑、盾、药水瓶、木箱、卷轴、一小堆杂物。原画早就画完了&#xff0c;每一张都是干干净净的平涂…

作者头像 李华
网站建设 2026/9/15 4:32:25

当用户加微信提需求:浏览器插件从自用到维护的实战笔记

昨天下午微信突然弹出一条好友申请&#xff0c;备注只有一行字&#xff1a;“作者你好&#xff0c;我用了你的XX插件&#xff0c;想问下能不能加个功能。”我盯着那条验证信息愣了大概十秒——半年前把这个插件发布到商店之后&#xff0c;我就再没主动维护过它&#xff0c;偶尔…

作者头像 李华
网站建设 2026/9/15 4:32:17

STM32CubeMX安装:嵌入式AI编程的硬件语义起点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 4:32:17

微信小程序天气预报源码拆解:定位、接口与页面适配全流程

简介&#xff1a;一份提供完整项目文件和效果截图的微信小程序天气预报源码&#xff0c;适合小程序初学者、前端开发者以及正在做课程设计或毕业设计的学生使用&#xff0c;能够帮助读者直观理解天气类小程序的页面组织、样式设计与数据交互流程。rar压缩包共收录47个文件&…

作者头像 李华