ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LEO卫星网络仿真框架Hypatia源码解析:从ns-3到星间链路建模

LEO卫星网络仿真框架Hypatia源码解析:从ns-3到星间链路建模

简介:一套面向低地球轨道(LEO)卫星网络研究的高完整性仿真框架,适用对象为卫星通信领域的研究人员、工程师及高校相关专业学生,可支撑星座布局设计、链路级仿真、协议评估与覆盖规划等任务。包内共312个文件,以py脚本、plt绘图文件、h/cc/cpp源码、txt说明、sh脚本与md文档为主,深度覆盖Hypatia框架的轨道动力学、射频传播、网络协议、资源管理等模块;压缩包约33.63MB,目录结构清晰,便于直接复用和二次开发。已有449人学习/下载。借助该框架,用户可基于Hypatia示例场景快速开展卫星星座优化、干扰分析、通信协议对比等仿真实验,并结合辅助脚本完成参数调整与结果可视化;框架内置多种星座配置、TCP/IP及DVB-S2X协议模拟、频率分配与功率控制策略,适合需要系统理解LEO网络行为并开展定量研究的开发者。

1. 低地球轨道仿真为什么值得自己跑一遍:Hypatia 框架与这份源码能解决什么

低地球轨道(LEO)卫星网络这几年从论文一路走进了工程部署,星间激光链路、星地 Ka 链路、以及每几十秒就变一次的拓扑,让地面网络那套"配好静态路由然后压测"的仿真思路直接失效。Hypatia 是一个基于 ns-3 的开源 LEO 卫星网络仿真框架,这份 zip 里装的是它最核心的十几个源文件,覆盖了星座拓扑、星地链路、激光星间链路和路由仲裁四个层面。

它解决的问题很具体:我的星座在某个纬度覆盖怎么样、一条端到端路径的延迟是多少、星间链路带宽够不够、贪心路由会不会绕路。这些都不需要发射卫星,在仿真里跑一遍就有量化结果。适合的人群也清楚:做卫星通信的研究生、做星座规划的系统工程师、写星载路由协议的开发者。

下面我会按"文件分工 → 搭起来跑 → 调参 → 避坑 → 校验"的顺序展开,所有命令和参数都以能落地为准。

2. 先拆文件再看架构:十个源文件的分工与 ns-3 的接口设计

这份源码最大的特点是"麻雀虽小,五脏俱全"。拿到手不要急着编译,先把文件名的规律摸清楚,后面排查问题会省一半力气。这里先给一张速查表,再分三节细讲。

源文件职责关键点
iers-data.cc地球定向参数(EOP)数据dUT1、极移、UT1-UTC
julian-date.cc儒略日与时间换算JD、历元、时间系统
constants-gen.cc星座常量生成轨道根数、查表加速
satellite.cc卫星节点位置、轨道、转发
topology-satellite-network.cc星座拓扑构建轨道面×每轨卫星数
gsl-net-device.cc星地链路设备射频、可见性判断
gsl-channel.cc星地信道动态时延、衰减
point-to-point-laser-net-device.cc激光星间链路设备全双工、动态时延
point-to-point-laser-helper.ccISL 安装助手ns-3 helper 模式
arbiter-single-forward-helper.cc路由决策单跳贪心转发

2.1 时间与坐标是 LEO 仿真的地基:julian-date、iers-data 与 constants-gen 的作用

LEO 卫星的轨道位置通常在地心惯性系里描述,地面站坐标却在地球固连系里。两个坐标系之间的夹角由格林尼治恒星时决定,而恒星时要靠儒略日算出来。julian-date.cc 干的就是"公历时刻 ↔ 儒略日"的换算,它是整个坐标转换链的第一环,改错一个历元偏置,后面所有卫星位置都是偏的。

iers-data.cc 提供的是地球定向参数(EOP)。真实地球的自转轴会摆动(极移),自转速度也不均匀,UT1 和 UTC 之间最大能差到 0.9 秒。别小看这 0.9 秒,LEO 卫星速度约 7.8 公里每秒,乘起来就是约 7 公里的位置误差,足够让一个 30GHz 的窄波束星地链路彻底对不上。所以 iers-data 不是可选项,而是精度底线。我一般会在仿真初始化时一次性读入这张 EOP 表,按儒略日查表并做线性插值。

constants-gen.cc 则是一个生成器,它把"轨道面数、每轨卫星数、倾角、高度、相位因子"展开成每颗卫星的轨道六根数常量表。Hypatia 这么设计是为了运行时快:几百颗星的轨道不用现场解方程,查表就行。代价是改一次星座参数就要重新跑一次生成器。这里面最容易翻车的是相位因子,它决定相邻轨道面的卫星错开多少,直接影响星间链路能不能建立,别随手设成 0。

2.2 两条链路两类信道:GSL 射频链路与 point-to-point 激光 ISL

GSL(星地链路)走的是射频,典型工作在 Ka 频段。gsl-net-device.cc 是挂在卫星和地面站节点上的"网卡",gsl-channel.cc 是它的信道。每次发包,信道会拿着两端设备的位置算一次瞬时距离,用这个距离生成传播时延,再算接收功率。可见性判断用的是"地面站仰角大于最小仰角",低于阈值链路直接 down。

这个最小仰角是 GSL 仿真里最影响结果的参数之一。同样 1100 公里的轨道高度,仰角门限从 10° 提到 40°,单条链路的斜距能差出一倍以上——低仰角门限下链路多、时延大、信号弱,高仰角门限下链路变少但质量好。这就是星地链路的经典折中。

ISL(星间链路)走的是激光。point-to-point-laser-net-device.cc 在语义上继承了 ns-3 里 PointToPointNetDevice 的"无差错、固定速率、全双工"模型,但把固定链路时延替换成了由当前两星几何距离决定的动态时延。同一轨道面前后相邻的两颗星相对位置稳定,激光链路常年可用;相邻轨道面的两颗星相对速度大,距离和指向都在变,仿真里通常用最大链路距离来约束。

helper 文件在 ns-3 里承担"安装"职责:把设备属性设置好,再挂到信道上。point-to-point-laser-helper.cc 干的就是这件事。这里我想强调一个理解这份源码的关键:文件名里的 net-device、channel、helper 就是 ns-3 留给新链路的三层接口。网上常说的"卫星网络中的接口",指的就是这套抽象,搞懂了它,加一种新链路只是照葫芦画瓢。

2.3 卫星节点、星座拓扑与单跳转发路由:satellite.cc、topology 与 arbiter-helper

satellite.cc 实现的是轻量卫星节点,主要职责是"位置 + 转发",不跑完整协议栈。它保存轨道参数,提供按时刻查询位置和速度的接口。理解成"会移动的网络节点"就行,不要想复杂。

topology-satellite-network.cc 负责把 N 个轨道面 × M 颗卫星摆成一个星座,给每颗卫星算四个 ISL 邻居(同轨道面前后两颗、相邻轨道面各一颗),再根据地面站位置和最小仰角建立 GSL 连接关系。LEO 拓扑是随时间变化的,不能像地面网那样把邻居关系当常量,所以这个类同时维护拓扑的快照更新。

arbiter-single-forward-helper.cc 是路由决策的核心,名字里的 single forward 指的是"单跳贪心转发"。原理很朴素:卫星收到包以后,遍历自己的 ISL 邻居和自身 GSL,计算每个候选下一跳到目的地面站的距离,把包交给距离最近的那个。这样每颗星只靠本地邻居信息就能决策,复杂度是 O(邻居数),不需要任何全局状态,非常适合卫星上算力有限的场景。

代价是贪心不一定最优。在星座边缘、极区、或者相邻轨道面链路断开的时候,局部最近的选择可能把包带进死胡同,表现为跳数突增、时延变大。Hypatia 常见的做法是再提供一个 Dijkstra 风格的全局路由仲裁器做对比实验,这份 zip 里只带了 single-forward 这一种,够用来理解机制。

3. 把 zip 变成能跑的仿真:编译、星座配置与运行检查

这一章的目标只有一个:让这份源码在你的机器上编译通过,并且跑出带日志的仿真。先把环境说清楚,再给配置骨架,最后讲怎么确认仿真真的在动。

3.1 环境准备:目录放置与 waf 构建

这份源码是 ns-3 的模块,不是独立工程。先解压 zip,把整个 hypatia 目录放到 ns-3 的 src 目录下去。

unzip leo_hypatia.zip -d ~/ns3-workspace mv ~/ns3-workspace/hypatia ~/ns3-workspace/ns-3/src/hypatia cd ~/ns3-workspace/ns-3 ./waf configure --enable-examples --enable-tests ./waf build

这里有几个关键点。第一,解压路径别带中文和空格,ns-3 的构建系统对路径很敏感。第二,hypatia 目录必须落在 src/ 下面,构建系统只扫描这个目录下的模块,放错位置就会静默跳过。第三,这份源码的年代对应的是 waf 构建的 ns-3(3.3x 这一代),如果你手里是 3.37 之后的版本,构建系统已经换成了 CMake,编译方式会不一样。

./waf build跑完后,在输出里搜一下 hypatia 关键字,确认模块确实被编译进去了。这一步不查,后面所有缺头文件的报错都会让你怀疑人生。

3.2 定义星座参数:一个可改的 demo 骨架

在 scratch 目录下建一个 demo 文件,核心是把星座和链路参数暴露成变量。下面这个骨架的类名和方法名以你解压出来的头文件为准,我这里的重点是参数含义,不是 API 签名。

// scratch/leo-demo.cc —— LEO 星座仿真骨架(接口名以解压后的头文件为准) #include "ns3/core-module.h" #include "ns3/network-module.h" #include "ns3/hypatia-module.h" using namespace ns3; int main(int argc, char *argv[]) { uint32_t orbits = 6; // 轨道面数 uint32_t satsPerOrbit = 12; // 每轨道面卫星数,共 72 颗 double altitude = 1100e3; // 轨道高度 1100 km,单位米 double inclination = 53.0; // 轨道倾角 53 度 double phaseFactor = 0.5; // Walker 相位因子,错开相邻轨道面 DataRate gslRate("1000Mbps"); // 星地链路速率 1 Gbps DataRate islRate("10Gbps"); // 激光星间链路速率 10 Gbps double minElevation = 25.0; // GSL 最小仰角 25 度 // 创建拓扑对象并写入星座参数 Ptr<TopologySatelliteNetwork> topo = CreateObject<TopologySatelliteNetwork>(); topo->SetOrbits(orbits); topo->SetSatellitesPerOrbit(satsPerOrbit); topo->SetAltitude(altitude); topo->SetInclination(inclination); topo->SetPhaseFactor(phaseFactor); topo->SetGslRate(gslRate); topo->SetIslRate(islRate); topo->SetMinElevation(minElevation); topo->Build(); // 生成卫星、GSL 与 ISL 链路 Simulator::Run(); Simulator::Destroy(); return 0; }

参数里最需要解释的是两组。轨道面数 6 × 每轨 12 颗 = 72 颗,这是经典 Walker 星座规模的缩小版,够跑通流程又不至于让仿真慢到没法等。高度 1100 公里是 OneWeb 一类的典型值,单跳 GSL 时延在 6 毫秒量级;如果你改到 550 公里(Starlink 风格),单跳时延会降到约 3 毫秒,但覆盖半径也变小,需要的卫星总数会变多。

ISL 速率特意比 GSL 高一个数量级,这是符合现实的配置:星地链路才是瓶颈,星间链路要留足余量。如果你把两处速率设成一样,星座内部会先开始排队,你做延迟实验时会把排队延迟误当成传播延迟。

3.3 编译运行与日志检查

编译运行用 waf 的 run 命令,输出重定向到文件,再过滤关键日志。

./waf --run "scratch/leo-demo" 2>&1 | tee run.log grep -E "ISL|GSL|Delay" run.log | head -50

第一次跑通过后,至少要确认两件事。第一,ISS 链路日志里有没有 ISL up 的记录;72 颗星、每颗 4 条星间链路,理论上初始化时应该出现约 144 条 ISL 链路(每条链路会被两端各记录一次,所以看到 144 上下是正常的,数量对不上说明拓扑生成有问题)。第二,连续观察同一个 GSL 链路的时延数值在不在变化——如果从头到尾纹丝不动,说明卫星位置没有被更新,仿真可能只是搭了个静态拓扑,这对 LEO 来说等于没仿真。

如果运行时报段错误,优先怀疑星星座参数越界,比如轨道面数小于 3 或者相位因子导致链路重复建立。把参数换回上面的默认值再跑一次,能过就是参数问题。

4. 参数怎么调:高度、带宽与路由策略的三组取舍

跑通只是开始,真正花时间的是调参。LEO 仿真里参数互相牵连,我把最常见的三组取舍讲清楚,你改参数的时候心里就有数了。

4.1 轨道高度:延迟、覆盖与卫星数量的三角关系

高度是第一个要动的参数,因为它同时影响延迟、覆盖和卫星总数。下面这张表是用 30° 最小仰角算出来的典型值。

轨道高度 (km)单跳 GSL 斜距 (km)单跳 GSL 时延 (ms)同轨相邻星弧距 (km)星数需求
550约 10003.3约 1970高(需要上千颗星做全球覆盖)
1100约 18506.2约 3900中
1500约 24008.1约 5300低

这里有个经常被忽略的点:同轨相邻星的弧距在高轨道上会变得很大。1100 公里高度、每轨 12 颗星,同轨两颗相邻卫星的距离接近 3900 公里,激光链路单向时延就有 13 毫秒,这比单跳 GSL 时延还大。也就是说,跨多跳星间链路的延迟,主要贡献是 ISL 而不是 GSL。

所以调高度的逻辑不是"越低越好"或"越高越好",而是看你的仿真目标。做覆盖仿真,低轨高星数是优势;做端到端延迟仿真,要综合 GSL 和 ISL 两部分,把同轨星数加多(弧距变小)比单纯降高度更有效。

4.2 GSL 与 ISL 参数:带宽比、频率与最小仰角

链路参数我一般分成两类调:一类是速率,一类是物理层参数。

速率方面,ISL 和 GSL 的带宽比建议保持在 5:1 以上。现实中激光星间链路做到 10 Gbps 并不难,而星地 Ka 链路通常几百 MHz 带宽,换算成速率上限也就在 1 Gbps 量级。如果你在仿真里把它们设成等带宽,星间链路先变成瓶颈,端到端吞吐会虚低,这时候的延迟数据没有参考价值。

物理层参数里,GSL 重点是载波频率和最小仰角。Ka 频段一个典型配置是上行 28 GHz、下行 18 GHz,发射功率 10~30 dBW,天线增益 30~40 dBi。注意 Ka 频段雨衰非常可观,重雨条件下附加损耗可以到十几 dB,ns-3 默认模型里没有雨衰,常见做法是在 gsl-channel 外面再挂一个随降雨率变化的附加损耗函数。最小仰角我建议从 25° 起步,看覆盖结果再降到 20° 或升到 35°,每 5° 一档做对比。

ISL 方面,激光波长一般取 1550 纳米(光通信最成熟的窗口),链路速率按星座总吞吐反推。还有一个隐藏参数是最大链路距离,它限制了邻轨 ISL 能否建立。这个值必须覆盖同轨相邻两星的弧距,否则会出现"同轨链路都断了"的诡异现象。

4.3 路由策略:单跳转发的代价与观察手段

single-forward 路由是本地贪心,它没有全局视图,每个转发决策只保证"这一步更近",不保证"整体路径最优"。在仿真里观察这个代价最直接的手段是打印每个包的跳数。

做法是在路由仲裁器里加一行日志,输出当前包的下一跳选择和累计跳数。然后选一对距离很远的地面站,手算理论最小跳数:两点经度差除以单跳 ISL 的角跨度,得到下限。如果仿真出来的实际跳数比这个下限大一半以上,基本就是贪心绕路了——常见触发场景是极区,那里所有轨道面汇聚,本地最近的选择会把包带向极点再折回来。

想验证这个判断,可以把仲裁器换成 Dijkstra 风格的全路径路由做对比实验。同一场景下,Dijkstra 的跳数和时延会比 single-forward 低一截,这个差值就是贪心策略的真实代价。换路由仲裁器需要改代码并重新 waf build,但这是理解 LEO 路由问题最值得做的实验。

5. 避坑排查:Hypatia 的五个典型翻车点与定位方法

这部分内容来自我实际跑这类 ns-3 卫星模块的血泪经验。每一条都按"现象 → 原因 → 解决"写,你可以直接当排查手册用。

5.1 编译期找不到 gsl-net-device.h:模块没进构建系统

现象:./waf build报fatal error: ns3/gsl-net-device.h: No such file or directory。

原因:hypatia 模块没有被构建系统收录。最常见的是目录没有放在 src/ 下面,或者放在了 src/ 下面但 build 缓存没刷新。

解决:先确认目录结构,然后清掉 build 产物重新配置编译。

ls ~/ns3-workspace/ns-3/src/hypatia rm -rf build ./waf configure --enable-examples --enable-tests ./waf build 2>&1 | grep -i hypatia

如果 grep 没有输出,说明模块没被扫描到;如果编译报错但报错文件来自别的地方,那就是模块依赖顺序问题,把 hypatia 目录名改成字母序靠前的名字(比如 aaa-hypatia)可以绕过。

5.2 全是 GSL 没有 ISL:相位因子与最大链路距离

现象:日志里 GSL 链路频繁 up/down,但 ISL 从头到尾一条都没有。

原因:两个常见根因。其一是 Walker 相位因子设成了 0,相邻轨道面的卫星在相同时刻处于相同纬度,相对位置要么贴近要么错开,导致邻轨 ISL 被最大距离约束剪掉;其二是激光链路的最大距离参数没有跟着轨道高度调整,1100 公里高度下同轨相邻星弧距约 3900 公里,如果最大距离只有 2000 公里,ISL 全断。

解决:把相位因子设到 0.25~0.5 之间;然后按公式2*(地球半径+轨道高度)*sin(π/每轨星数)计算同轨弧距,把 ISL 最大距离设成这个值的 1.3~1.5 倍,留出轨道摄动余量。

5.3 端到端时延比手算高 40%:排队延迟和传播延迟先分开

现象:仿真输出的端到端时延是 15 毫秒,手算的几何传播时延只有 10 毫秒出头,怎么都对不上。

原因:排队延迟混进了你的统计里。链路速率设得太低、发包间隔太密时,包会在卫星节点的设备队列里排队,这个时间算进端到端时延,和传播时延是两码事。

解决:先找到日志里同时输出的传播时延和排队时延分量,把它们分开看。然后把发包间隔调大到 10 毫秒一个包,再看端到端时延是否回落到几何值附近。如果回落,说明传播建模没问题,是排队在干扰你的结论;如果没回落,才需要去查链路是不是绕路了。

5.4 卫星位置跳变:时间系统错位与 IERS 数据缺失

现象:打印同一颗星在相邻仿真时刻的位置,坐标跳了几百公里。

原因:时间系统没对齐。最常见的是儒略日换算里的历元偏置写错(J2000 历元对应的儒略日是 2451545.0,错一位就是几百年);其次是 IERS 数据表没有在初始化时加载,仿真只能用 UT1≈UTC 的近似,误差最多约 0.9 秒,折算成轨道位移约 7 公里。

解决:先查 julian-date.cc 里的历元常量;再确认 iers-data.cc 的数据表在初始化时被读入并做了插值。一个快速验证办法是打印某颗星在一个轨道周期(约 90 分钟)前后的位置,应该基本回到起点,偏差不超过几十公里才对。

5.5 内存涨到被杀死:日志、构建类型与分步定位

现象:仿真跑到第 60 秒左右内存持续上涨,最后被系统 OOM 杀掉。

原因:日志全开(NS_LOG 设为 ALL)、debug 构建下每个事件都带大量检查,或者 trace 回调在每个包上重复注册。LEO 仿真里包数量和链路变更频率都很高,小泄漏会被放大成 OOM。

解决:先把日志级别关到只剩错误级,换成 release 构建;然后把仿真时间拆成 10 秒一段跑,先确认无业务流量时内存稳定,再逐步叠加业务流量。如果加流量才涨,重点查 trace 回调注册;如果无流量也涨,重点查拓扑刷新的内存释放逻辑。

6. 结果校验与批量调参:把仿真跑成可信实验的两个习惯

仿真跑出来的数字,只有经过理论校验才值得写进报告。这一章讲两个我每次必做的动作,做完之后参数扫描的结果才敢往外拿。

6.1 用自由空间损耗公式做基线校验

第一个动作是手算一对链路的理论值,和仿真输出对一下。自由空间损耗公式在 30GHz、1850 公里斜距下的典型结果可以这样算:

import math c = 299792458.0 # 光速,m/s f_gsl = 30e9 # GSL 载波频率 30 GHz d_gsl = 1850e3 # 1100 km 高度、仰角 30° 的斜距 delay_gsl = d_gsl / c # 单向传播时延 fspl = 20 * math.log10(d_gsl) + 20 * math.log10(f_gsl) - 147.55 print(f"GSL 单向时延 {delay_gsl*1000:.2f} ms") print(f"自由空间损耗 {fspl:.1f} dB")

公式里的 147.55 是常数推导结果:20*log10(4π/c),它的作用是让距离以米、频率以赫兹为单位时直接相加得出 dB 值。这段代码跑出来,单向时延约 6.2 毫秒,损耗约 182 dB。然后去仿真日志里找对应的那条 GSL 链路的时延和接收功率,误差在 5% 以内才算链路建模可信。误差偏大时,先检查斜距是不是你预期的值——很多时候是仰角门限不一样导致配了另一条链路。

6.2 参数扫描脚本与小场景先行

校验通过后,批量调参才有意义。下面这个脚本扫描最小仰角对平均时延的影响,前提是你的 demo 用 CommandLine 接收 MinElevation 参数:

import subprocess def extract_avg_delay(log_text): # 假设日志里有一行 "AverageDelay = 6.21 ms" for line in log_text.splitlines(): if "AverageDelay" in line: return float(line.split("=")[1].strip().split()[0]) return None for elev in [20, 25, 30, 35, 40]: tmpl = f'--command-template="%s --MinElevation={elev}"' cmd = ["./waf", "--run", "scratch/leo-demo", tmpl] out = subprocess.run(cmd, capture_output=True, text=True).stdout print(f"elev={elev}, avg_delay={extract_avg_delay(out)} ms")

脚本本身很简单,但有两个习惯比脚本更重要。第一,每次改参数之前先跑一个 20 秒的小场景,确认时延和手算基线一致,再放大到完整仿真——在错误拓扑上调一整晚参数是最亏的事。第二,日志解析脚本要写稳定,碰到缺字段就报错而不是静默返回 None,否则你分析半天用的可能是不完整的数据。

从那以后,我每次拿到陌生的仿真源码包,第一天只做一件事:手算一对链路的时延和损耗,把仿真输出贴上去对。对得上才继续调参数,对不上就先查时间系统、查链路选择、查路由路径,直到理论值和仿真值咬合。这个习惯帮我挡掉了不少次在错误拓扑上浪费整晚的窘境。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进