Ka 阵面星历下发与执行逻辑说明
一、系统总体架构
整个星历跟踪系统由三层组成:
上位机(PC,Python Flask Web 应用)负责轨道解算、星历点生成、NTP 时钟校正,并通过 UDP 把星历命令发给板子。
下位机(STM32,C 固件)负责接收并解析星历命令,存入点表,按 UTC 到点时刻逐点驱动两个阵面(收发各一路 SPI)的波束指向。
阵面硬件(接收阵面 + 发射阵面)接收下位机通过串口/SPI 下发的补角指令,完成物理波束偏转。
数据流方向:
轨道六根数 → 上位机解算 → UDP → 下位机解析 → 串口/SPI → 收发阵面
二、上位机侧逻辑(web_ctrl.py + orbit.py)
2.1 NTP 时钟校正(必须先做)
上位机的时间源是 PC 系统时钟,而 PC 时钟与真实 UTC 之间可能有几十到上百毫秒的偏差。
校正流程:
-
向 NTP 服务器(默认 ntp.aliyun.com)连续发 3 次查询
-
取 3 次偏差的中位数,滤除网络抖动
-
把偏差写入全局变量 `_clock_offset_s`
-
之后所有用到"当前时间"的地方调用 `utc_now()`,内部自动做 `系统时钟 - 偏差 = 真实UTC`
这样星历时间戳就对齐了真实 UTC,与板子的惯导 UTC 在同一个基准上。
2.2 轨道解算
输入参数:
-
历元 UTC(六根数对应的参考时刻)
-
轨道六根数:半长轴 a(米)、偏心率 e、倾角 i、近地点幅角 ω、升交点赤经 Ω、平近点角 M0(度)
-
台站经纬度、高程
-
载频 f0(仅用于计算多普勒,不影响角度)
解算链路:
-
用开普勒方程(牛顿迭代)从平近点角 M 求偏近点角 E
-
在轨道面内算出位置和速度
-
通过三次旋转(Ω, ω, i)变换到 J2000 惯性坐标系
-
经岁差矩阵 P、章动矩阵 N 旋转到瞬时真赤道系(TOD)
-
绕 Z 轴旋转格林威治真恒星时角(GAST),得到 ECEF 地固坐标
-
与台站 ECEF 坐标做差,投影到东北天(ENU)系,得到方位角 az 和俯仰角 el
可选的光行时修正:迭代 2 次求信号传播延迟 τ,用卫星在"发射时刻"而非"观测时刻"的位置来算角度,与 STK 结果对齐。
2.3 星历滚动下发主循环(EphemSim._loop)
核心参数:
| 参数 | 含义 | 典型值 |
|------------|------------------------------|-------------|
| count | 每组点数 | 20(固件上限)|
| step_ms | 相邻两点的时间间隔 | 50 ms |
| lead_s | 提前多久下发下一组 | 1.0 秒 |
| groups | 总组数,0 = 无限 | 0 |
| mask_deg | 仰角门限,低于此不发 | 10° |
一组的时间跨度 = count × step_ms = 20 × 50ms = 1.0 秒。
主循环每一轮做的事情:
-
取 group_start(这一组首点应该生效的 UTC 时刻)
-
算 group_start 和 group_end 两端的仰角
├─ 两端都 > mask_deg → 进入下发
└─ 任一端 ≤ mask_deg → 跳过,等待卫星升起
- 下发:
a. 对 group_start 起的 20 个时刻(间隔 50ms)逐个调用 obs_at() 解算 az/el
b. 拼成一条命令字符串,格式为:
$cmd,set ephem;20;YYYYMMDD,hhmm,ss.sss,az.xxxx,el.xxxx;...(重复20次)...
c. 通过 UDP 发送给板子(send_quiet,日志只记摘要不刷屏)
d. 同时把全部 20 个点的详细数据写入 info 日志,供人工核对
- 更新 group_start = 上一组 group_start + 组时长
计算下一组的唤醒时刻 = 新 group_start - lead_s
睡眠等待到唤醒时刻,然后回到第 1 步
时序示意(lead = 1.0s,组时长 = 1.0s):
```
时间轴 ───────────────────────────────────────────►
├── 第1组生效 ──┤├── 第2组生效 ──┤
t0 t0+1s t0+2s
▲发第1组 ▲发第2组 ▲发第3组
(t0-1s) (t0) (t0+1s)
```
每一组在"生效前 1 秒"发出,板子收到后存入 pending 槽,当前组发完自动切入。
2.4 角度约定
上位机下发的是**物理观测角**(也叫天文方位角/仰角):
-
方位角 az:北为 0°,顺时针增大,范围 0°\~360°
-
俯仰角 el:水平为 0°,天顶为 90°,范围 0°\~90°
补角转换由下位机完成,上位机不做任何转换。
2.5 命令格式
```
$cmd,set ephem;20;20260908,0425,58.000,295.7839,10.1234;20260908,0425,58.050,295.7854,10.1456;...;
```
各字段含义:
-
`$cmd,set ephem;` 固定前缀
-
`20` 点数(1\~20)
-
每个点用分号分隔,内部 5 个逗号分隔的字段:
-
`YYYYMMDD` 年月日
-
`hhmm` 时分
-
`ss.sss` 秒(含毫秒)
-
`az` 方位角(度)
-
`el` 俯仰角(度)
-
末尾可选 `*hh` 校验和(星历命令默认不加)
三、下位机侧逻辑(protocol.c 固件)
3.1 命令接收与解析(ParseEphemCommand)
收到 UDP 包或串口数据后,进入 `ParseCommand` 分发,匹配到 `$cmd,set ephem;` 前缀后调用 `ParseEphemCommand`。
解析过程:
-
跳过前缀,用 strtol 提取点数 count(1~20)
-
循环 count 次,每次:
a. strtol 取 YYYYMMDD → 拆出年月日
b. strtol 取 hhmm → 拆出时分
c. strtod 取 ss.sss → 秒
d. strtod 取 az → 方位角
e. strtod 取 el → 俯仰角
f. 合法性校验:年 2000~2099、月 1~12、日 1~31、时 0~23、分 0~59、秒 0~60、
方位 0~360、俯仰 0~90
g. 调用 EphemUtcToMs 把 UTC 转成距 2000-01-01 的毫秒数
h. 检查时间戳严格递增
i. 做补角转换:
beam_el = 90° - el(俯仰补角)
beam_az = (360° - az) mod 360°(方位补角)
乘以 100 取整,存为 0.01° 单位的整数
-
全部解析成功后,写入 g_ephem_pending 数组,置 pending_ready = 1
-
回复 "$cmd,set ephem,ack\r\n"
```
3.2 双缓冲机制(active + pending)
固件用两个数组实现无缝切换:
| 数组名 | 作用 |
|-------------------|--------------------------------------------|
| g_ephem_active | 正在逐点执行的当前组 |
| g_ephem_pending | 提前收到的下一组,等当前组发完后切入 |
`ParseEphemCommand` 每次只写 pending,不会打断正在执行的 active。
切换条件(在 `Ephem_Process` 中检查):
```
if (active_index >= active_count && pending_ready) {
把 pending 复制到 active
active_index = 0
pending_ready = 0
}
```
这样保证:当前组的 20 个点全部发完之后,才切到下一组,不会丢点也不会空档。
3.3 时基建立(Ephem_NowUtcMs)
板子的 UTC 来源是惯导(GNSS/INS),通过串口定时上报。
时间插值方法:
```
当前毫秒 = 惯导最近一帧的UTC毫秒 + (HAL_GetTick() - 收到那帧时的tick)
```
`HAL_GetTick()` 是 STM32 的 SysTick 计数器(1ms 分辨率),用来补偿"惯导帧到达"和"当前检查时刻"之间的延迟。
如果惯导 UTC 还没锁定(`g_gnss_time_valid = 0`),`Ephem_NowUtcMs` 返回 0,`Ephem_Process` 直接跳过不发,避免乱指。
3.4 到点执行(Ephem_Process,主循环中调用)
这个函数在 main 的 while(1) 里被高频调用(每轮主循环都跑一次):
```
- now_ms = Ephem_NowUtcMs()
如果 = 0,说明时基没建立,直接返回
-
检查是否需要切组(见 3.2)
-
while 循环:
只要 now_ms >= 当前点的 utc_ms,就立刻执行:
├─ 调用 XW_On_SetBeamAngles1(az, el) → 接收阵面
├─ 调用 XW_On_SetBeamAngles2(az, el) → 发射阵面
└─ active_index++,继续检查下一个点
用 while 而非 if,是为了补发:如果主循环某一轮延迟了,
导致连续多个点都已过期,用 while 可以按序全部补发,不跳点。
```
执行时序示意:
```
时间轴 ─────────────────────────────────────►
点表 P1 P2 P3 P4 ... P20
50ms 50ms 50ms 50ms 50ms
主循环 ↑检查 ↑检查 ↑检查 ↑检查
到P1 到P2 到P3 到P4
发P1 发P2 发P3 发P4
↑ 当 P20 发完后,pending → active,从下一组 P1 继续
3.5 补角与帧下发
存入点表的角度已经是补角(0.01° 单位整数):
```
beam_el_001deg = (int)((90.0 - el) × 100 + 0.5)
beam_az_001deg = (int)(((360.0 - az) % 360) × 100 + 0.5)
```
`XW_On_SetBeamAngles1/2` 把这两个整数打包成阵面控制帧,通过串口4(发射)和串口5(接收)分别下发给两个阵面。两个阵面收到的是同一组角度。
四、完整操作流程
第1步:连接
在网页上确认 IP=192.168.254.27、板子端口=5003、本机端口=6003
点"连接",左上绿灯亮
第2步:验证链路
点"天线姿态"按钮,观察日志有 rx 回包 → 双向通了
第3步:NTP 校正
在"时钟比对"区域点"对网络时间"
日志出现"已写入星历时钟校正"即可
第4步:填轨道参数
历元 UTC、六根数、台站经纬高、载频
第5步:预览确认
点"预览一组"
看俯仰角是否 > 门限(红字 = 低于门限,当前不可见)
看方位角范围是否合理
点"查下次过境"看卫星什么时候过来
第6步:开始滚动下发
确认参数后点"开始滚动下发"
如果卫星还没过境,状态栏显示"等待过境"
卫星升起后自动开始,日志里每组 20 个点逐条打印
第7步:观察
日志里看 tx 摘要和 info 逐点记录
可以开"波束统计"的自动查询,看 call 计数是否在涨
第8步:停止
卫星落下或不需要跟了,点"停止"即可
```
五、关键时序约束
| 约束 | 原因 |
|----------------------------|------------------------------------------------|
| 提前量 ≤ 组时长 | 固件只有一个 pending 槽,提前太多会覆盖还没执行完的组 |
| 点数 ≤ 20 | 固件 EPHEM_MAX_POINTS = 20,数组定长 |
| 时间戳严格递增 | 固件解析时校验,不递增直接拒绝整条命令 |
| 方位 0\~360°、俯仰 0\~90° | 固件解析范围限制,超出返回 ack error |
| 惯导 UTC 必须已锁 | 否则 Ephem_NowUtcMs 返回 0,不执行任何点 |
| NTP 必须先做 | 否则星历时间戳与板子 UTC 有几十毫秒偏差 |
六、故障排查要点
**日志里有 tx 但没有 rx 回包:** UDP 单向通了,检查板子 IP 和本机端口是否为 6003(固件校验源端口)。
**状态一直是"等待过境":** 当前仰角低于门限,看预览里的 el 值。如果历元太旧,累积误差可能导致过境时间偏差几分钟。
**板子回 ack error:** 命令格式不对,检查是否有字段超出范围(方位负数、俯仰负数、时间戳不递增等)。
**波束统计 call 不涨:** 星历点没送到阵面,可能是惯导 UTC 没锁(Ephem_NowUtcMs 返回 0)或者 pending 没切到 active。
**角度偏差大:** 检查历元是否过旧(二体模型不含 J2 摄动,5 天偏差可达 2° 升交点漂移)、NTP 校正是否做了、台站经纬度是否填对。