1.惯导提供当前时刻的UTC时间,星历包含星历时间,轨迹文件生成时的第一个起始点的时间是怎么确定的?把这几个时间搞清楚
未移植前:
历元时间算出历元儒略日
cs
sat.epoch_jd = jday_utc(epoch.year, epoch.month, epoch.day,
epoch.hour, epoch.minute, epoch.second);
根据时间偏移量算出当前时刻的儒略日
cs
double t_offset = start + i * step; /* 当前时刻的时间偏移 */
double jd_current = sat->epoch_jd + t_offset / 86400.0; /* 当前时刻儒略日 */
这里传入t_offset==t_offset_receive偏移时间指的是接收时刻的偏移时间
根据因为发射时间偏移量+光行时(在空间中传播的时间)=接收时间偏移量
那么发射时间偏移量=接收时间偏移量-光行时
t_tx_off = t_offset_receive - tau;
再算出发射时刻的儒略日jd_tx
根据t_tx_off发射时刻的偏移量计算卫星J2000坐标系下的速度与位置
根据jd_tx当前发射时刻的儒略日计算卫星在ECEF坐标系下的速度和位置再转换到ENU坐标系下计算俯仰角和方位角
cs
ObsGeometry obs = obs_geometry_lighttime(sat, t_offset,
sat->epoch_jd, &fac, f0_hz, enable_light_time);
tatic ObsGeometry obs_geometry_lighttime(const Satellite* sat,
double t_offset_receive,
double sat_epoch_jd,
const Facility* fac,
double f0_hz,
int enable_light_time) {
double t_tx_off = t_offset_receive - tau; /* 发射时刻的时间偏移 */
double jd_tx = sat_epoch_jd + t_tx_off / 86400.0; /* 发射时刻儒略日 */
/* 计算发射时刻的卫星 J2000 状态,再旋转到发射时刻的 ECEF */
State st = orbital_to_j2000_state(sat, t_tx_off);
j2k_state_to_ecef(st, jd_tx, &r_sat_e, &v_sat_e, NULL, NULL);
这里jd_display用作时间显示,time_display_offset取为0,所以 jd_display =jd_current
cs
double jd_display = jd_current + time_display_offset / 86400.0;
DateTime dt;
jd_to_utc(jd_display, &dt);
算法大致流程:
(1)根据历元时间计算出历元儒略日
cs
sat.epoch_jd = jday_utc(epoch.year, epoch.month, epoch.day,
epoch.hour, epoch.minute, epoch.second);
(2)根据start/step算出接收时间偏移量t_offset/t_offset_receive
cs
double t_offset = start + i * step; /* 当前时刻的时间偏移 *
(3)根据接收时刻偏移量计算出发射时刻偏移量t_tx_off
cs
double t_tx_off = t_offset_receive - tau; /* 发射时刻的时间偏移 */
(4)根据t_tx_off计算当前发射时刻的儒略日jd_tx
cs
double jd_tx = sat_epoch_jd + t_tx_off / 86400.0; /* 发射时刻儒略日 */
(5)发射时间偏移量t_tx_off计算J2000坐标系下的位置和速度
cs
State st = orbital_to_j2000_state(sat, t_tx_off);
(6)发射时刻的儒略日jd_tx计算ECEF坐标系的位置和速度
cs
j2k_state_to_ecef(st, jd_tx, &r_sat_e, &v_sat_e, NULL, NULL);
移植到stm32F427算法应该怎么做?首先我们也要提取到这两个时间参数t_tx_off和jd_tx用于计算
stm32F427每1s通过串口2接收惯导传来的数据包:数据包含每个时刻的utc时间和当前所处位置的经纬度信息,那么每刻实时上传的utc_now时间就可以转为jd_now,也就是上述所说的jd_current
且jd_current = sat->epoch_jd + t_offset / 86400.0; /* 当前时刻儒略日 */,我们已知道jd_current,就可以通过jd_current减去sat->epoch_jd=t_offset,t_offset算出来以后就可以很快的计算出t_tx_off和jd_tx
算法移植到stm32F427整体流程
(1)通过定时器每1s实时接收来自惯导的数据包,数据包包含每个时刻的年月日时分秒
cs
if(htim->Instance == TIM4)
{
if(usart2_rx_flag==1)
{
usart2_rx_flag = 0;
tim4_parse_flag = 1;
}
tim4_track_flag = 1; /* ← 确认存在,每秒无条件置位 */
}
}
(2)将每个时刻的UTC时间转为当前时刻的儒略日
cs
* ===== 新增:1Hz 卫星跟踪块 ===== */
if (tim4_track_flag == 1)
{
tim4_track_flag = 0;
HAL_IWDG_Refresh(&hiwdg);
/* 前置条件1:轨道六根数已注入 */
if (!tracker_orbit_valid())
{
/* 还没有轨道数据,等待上位机发 $cmd,set orbit */
}
/* 前置条件2:PBSOL已有有效定位(mode非0,年份合理) */
else if (nav.mode == 0 || nav.year < 2024)
{
/* INS尚未定位,跳过本次跟踪 */
Debug_printf3("$track,wait,INS not ready\r\n");
}
else
{
/* --- 1. 拼当前UTC -> 儒略日 ---
PBSOL的millisecond字段:当前分钟内的毫秒数(0~59999)
换算成秒直接除以1000即可 */
double sec = nav.millisecond / 1000.0;
double jd_now = tracker_utc_to_jd(
nav.year, nav.month, nav.day,
nav.hour, nav.minute, sec);
(3)再进行轨迹跟踪计算tracker_tick
cs
tracker_tick(
nav.latitude,
nav.longitude,
nav.height_msl,
0.0f, /* heading:固定朝北,不旋转 */
0.0f, /* pitch:阵面水平放置 */
0.0f, /* roll:无倾斜 */
jd_now,
g_carrier_f0_hz,
&o);
int tracker_tick(double lat_deg, double lon_deg, double alt_m,
float heading_deg, float pitch_deg, float roll_deg,
double utc_jd, double f0_hz,
Tracker_Out *out)
(4)根据jd_now=utc_jd计算接收时刻的时间偏移_ t_off_recv
cs
/* ===== 2. 接收时刻相对历元的偏移 ===== */
double t_off_recv = (utc_jd - g_sat.epoch_jd) * 86400.0;
(5)根据接收时刻的时间偏移计算发射时刻的时间偏移t_tx
cs
double t_tx = t_off_recv - tau;
(6)根据发射时刻的时间偏移计算发射时刻的儒略日
cs
double jd_tx = g_sat.epoch_jd + t_tx / 86400.0;
(7)发射时间偏移量t_tx计算J2000坐标系下的位置和速度
发射时刻的儒略日jd_tx计算ECEF坐标系的位置和速度
cs
State st = orbital_to_j2000_state(&g_sat, t_tx);
j2k_state_to_ecef(st, jd_tx, &r_sat, &v_sat);

2.惯导部分代码如何写,起到软件部分设置起始时间,持续时间,步长start, dur, step?
惯导部分不像软件部分,自动设置start,dur和step,start和step的目的是计算t_offset,而是直接给出当前时刻的时间,当前时刻的时间减去历元时间获取t_offset,
纯代码根据星历时间去预测接下来每个时间点的俯仰角和方位角去追踪,角度预测出来以后到点了发送数据再追踪
算法移植到stm32F427以后,就不再是先预测再追踪,而是实时地根据惯导传来的数据,实时地去计算追踪
关于 "dur(捕获时长)" 在嵌入式上怎么体现
PC 版里 dur 只是仿真循环边界------"我要打印多少秒的数据"。
嵌入式上根本不需要 dur,因为:
TIM4每秒置位tim4_track_flag;- 主循环每秒算一次、下发一次指向;
- 只要
tracker_orbit_valid()为真 + INS 有效,就一直跟踪下去。(轨道六根数+惯导数据有效)
3.指令发送时间要求
指令可以提前发送,直到惯导
cs
$cmd,set orbit,6840390.297,0.0010217,54.854500,142.808300,305.082600,296.4902,2026,4,15,8,41,56,17799960000,38.0559231,114.3554102,101.5

前提条件(非常重要):你的 INS 输出的 UTC 时间必须是真实的、准确到秒以内的 UTC。 如果 PBSOL 的时间偏了 1 秒,卫星位置会偏大约 7 km(LEO 速度 ~7.5 km/s),相控阵就指偏了
3、移植后要确保能捕获信号,必须检查的清单
下面这些坑你代码里有的没处理、有的处理得不完整,按重要程度排:
(1). INS 时间精度。
这是头号杀手。nav.millisecond 你写的是"当前分钟内的毫秒数",但 LEO 卫星 1 ms 偏差就是 7.5 m 距离误差、对应几十 Hz 多普勒误差。务必确认:
- PBSOL 时间是 UTC 还是 GPST?(差 18 秒!)
- 时间戳是"这一帧解算时刻"还是"上一秒整秒标记"?延迟多少?
- 你解析后再传给
tracker_utc_to_jd时,要不要加 PPS 补偿?
(2). 阵面安装方位/姿态约定。
main.c 里你传了 0,0,0 给 heading/pitch/roll,注释说"阵面固定朝北、水平"。这只在阵面机械正北 0°、水平放置时才对。实际安装时哪怕偏 1° 都会显著掉增益。建议留个静态校准接口。
(3). ELEV_MASK_DEG 对齐。
PC 是 0,嵌入式是 5,自己确定一个数值并文档化。
(4). 时间系统:TLE 推算的有效期。
你的根数是开普勒六根数(不是 TLE,没有 SGP4 摄动模型)。对 LEO 来说,纯开普勒推算超过几小时位置误差就会上百米。要么:
- 上位机定期(每几小时)下发新历元的根数;
- 或者改用 SGP4(推荐,TLE 7 天内精度 ~1 km)。
(5). tracker_tick 计算耗时。
你在 TIM4 中断里只置标志位,计算在主循环跑,这是对的。但主循环里 MX_LWIP_Process 阻塞可能很久,导致 1Hz 节奏被破坏。建议测一下最坏情况下两次 tracker_tick 之间的真实间隔------如果偶尔到 2-3 秒,多普勒预测就会有突跳,基带要能容忍。
(6). 多普勒预测给基带的接口延迟。
o.doppler_hz 是**"接收时刻"的瞬时多普勒**。从 MCU 算出来 → UART 串到基带 → 基带应用,至少有几十毫秒延迟。LEO 多普勒变化率高达几百 Hz/s,所以基带的搜索带宽必须 ≥ ±2 kHz 才稳。
(7). 看门狗。
你 IWDG reload = 3000、prescaler = 32,约 3 秒超时。主循环里 LWIP 慢的时候要小心,建议在 tracker_tick 调用前也喂一次。
4.惯导直接获取的经纬度信息以及高度信息不一定准确,可以通过串口3直接外部输入吗?代码要怎么改?
(1). Tracker.h 加两个函数声明
/* 由上位机指令注入固定基站位置(已知精确坐标) */
void tracker_set_facility(double lat_deg, double lon_deg, double alt_m);
/* 是否已经注入过基站位置 */
uint8_t tracker_facility_valid(void);
(2). Tracker.h 里 tracker_tick 接口去掉 lat/lon/alt 参数(或者保留作为后备)
我建议保留接口不变,避免大改 main.c。改成:内部如果发现 facility 已设置,就用注入的坐标,否则用传进来的参数。
(3). tracker.c 里加全局变量和函数
/* 在文件顶部,g_sat 旁边加 */
static struct {
double lat_deg;
double lon_deg;
double alt_m;
uint8_t valid;
} g_facility = {0};
/* 公共接口 */
void tracker_set_facility(double lat_deg, double lon_deg, double alt_m)
{
g_facility.lat_deg = lat_deg;
g_facility.lon_deg = lon_deg;
g_facility.alt_m = alt_m;
g_facility.valid = 1;
}
uint8_t tracker_facility_valid(void)
{
return g_facility.valid;
}
(4). 改 tracker_tick,让它优先用注入的坐标
把这一段:
/* ===== 1. 地面点 ECEF(每 tick 都算,载体是动的) ===== */
double fx, fy, fz;
llh2ecef(lat_deg, lon_deg, alt_m, &fx, &fy, &fz);
改成:
/* ===== 1. 地面点 ECEF =====
如果上位机已经下发了固定基站坐标,优先用那个(更准确、不受惯导漂移影响)
否则才用传进来的实时坐标(兼容载体移动场景) */
double fx, fy, fz;
if (g_facility.valid) {
llh2ecef(g_facility.lat_deg, g_facility.lon_deg, g_facility.alt_m,
&fx, &fy, &fz);
} else {
llh2ecef(lat_deg, lon_deg, alt_m, &fx, &fy, &fz);
}
关键点:ENU 投影那一段也要用同一个 lat/lon(用来算方位角的"本地正北"方向),所以下面这段也要改:
/* ===== 5. ENU 方位/俯仰(地理) ===== */
double lat_r, lon_r;
if (g_facility.valid) {
lat_r = deg2rad(g_facility.lat_deg);
lon_r = deg2rad(g_facility.lon_deg);
} else {
lat_r = deg2rad(lat_deg);
lon_r = deg2rad(lon_deg);
}
double sl = sin(lat_r), cl = cos(lat_r);
double so = sin(lon_r), co = cos(lon_r);
/* ......后面不变...... */
(5).改 $cmd,set orbit 指令解析
在 protocol.c 你现在的 $cmd,set orbit 分支里,最后加上三个参数解析:
else if (strstr(command, "$cmd,set orbit") != NULL)
{
/* 格式:$cmd,set orbit,<a_m>,<e>,<i_deg>,<RAAN_deg>,
<argp_deg>,<M0_deg>,
<YYYY>,<MM>,<DD>,<hh>,<mm>,<ss>,<f0_Hz>,
<lat_deg>,<lon_deg>,<alt_m>
示例:
$cmd,set orbit,6840390.297,0.0010217,54.854500,142.808300,
305.082600,296.4902,
2026,4,15,8,41,56,17799960000,
38.0559231,114.3554102,101.5
*/
char buf[256];
strncpy(buf, command, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *p = strchr(buf, ','); if (!p) return; p++; /* 跳过$cmd */
p = strchr(p, ','); if (!p) return; p++; /* 跳过set orbit */
double a = strtod(p, &p); if (*p == ',') p++;
double e = strtod(p, &p); if (*p == ',') p++;
double id = strtod(p, &p); if (*p == ',') p++;
double Od = strtod(p, &p); if (*p == ',') p++;
double wd = strtod(p, &p); if (*p == ',') p++;
double M0d = strtod(p, &p); if (*p == ',') p++;
int yr = (int)strtol(p, &p, 10); if (*p == ',') p++;
int mo = (int)strtol(p, &p, 10); if (*p == ',') p++;
int da = (int)strtol(p, &p, 10); if (*p == ',') p++;
int hr = (int)strtol(p, &p, 10); if (*p == ',') p++;
int mi = (int)strtol(p, &p, 10); if (*p == ',') p++;
double s = strtod(p, &p); if (*p == ',') p++;
extern double g_carrier_f0_hz;
g_carrier_f0_hz = strtod(p, &p); /* ← 改成带 &p,方便继续解析 */
/* ====== 新增:解析基站经纬度高度 ====== */
double lat_deg = 0.0, lon_deg = 0.0, alt_m = 0.0;
int has_facility = 0;
if (*p == ',') {
p++;
lat_deg = strtod(p, &p);
if (*p == ',') {
p++;
lon_deg = strtod(p, &p);
if (*p == ',') {
p++;
alt_m = strtod(p, NULL);
has_facility = 1;
}
}
}
/* ====== 注入轨道根数 ====== */
tracker_set_orbit(
a, e,
id * 3.14159265358979 / 180.0,
Od * 3.14159265358979 / 180.0,
wd * 3.14159265358979 / 180.0,
M0d * 3.14159265358979 / 180.0,
yr, mo, da, hr, mi, s);
/* ====== 注入基站位置(如果指令里带了) ====== */
if (has_facility) {
tracker_set_facility(lat_deg, lon_deg, alt_m);
Debug_printf3("$cmd,set orbit ok,facility=%.7f,%.7f,%.2f\r\n",
lat_deg, lon_deg, alt_m);
} else {
Debug_printf3("$cmd,set orbit ok,facility=ins\r\n");
}
}
注意一个小修正 :原来你写的是 g_carrier_f0_hz = strtod(p, NULL);,NULL 会让 p 不再前进,后面就解析不到经纬度了。要改成 strtod(p, &p);,让指针继续往前走。
5.根据星网的时间比如说是2026-04-15 08:42:00实际计算的时候是要减去4s,输入的时间2026-04-15 08:41:56去计算,同时还要注意的是根据算法算出来的轨迹文件,实际追踪的过程中,还要加上8小时,才是真正的追踪时间,通过串口输入时,我们可以手动减去4秒输入,但是控制的时候实际推算出来的时间还要加上8小时那么这样的话代码要怎么改?
代码是不用改的,PC版本追踪使用的是北京时间,MCU内部计算的话使用的是utc时间
首先要搞清楚两个时间一个是北京时间,一个是utc时间,utc+8=北京时间,惯导模块获取的是utc时间
上位机代码是"离线计算 + 上位机定时下发"方案,MCU 只是一个"指令转发器",本身不做星历计算。所有方位/俯仰角都是 PC 提前算好的,上位机按北京时间到点发指令。
"嵌入式版"(前面我们一直在改的 tracker.c + main.c)是"MCU 自主跟踪"方案,MCU 实时算方位/俯仰角并下发到相控阵。
上位机方案为什么要加 8:
- 离线生成轨迹文件 → 文件使用的是北京时间字
- 上位机读到当前时间UTC → 也要北京时间才能和文件比较
- 所以 GNSS 时间 +8 转北京时间是为了两个时间能对上
嵌入式方案根本没有"两个时间要对上"这回事:
- 惯导给 MCU UTC 时间
tracker_tick用这个 UTC 直接算- 算出的方位俯仰角就是此刻卫星的真实位置
- 立刻下发相控阵
- 抓到信号

比如说PC 版输出 "19:07 UTC" 对应实际追踪在 "03:07 北京时间",下意识觉得"算法是不是要把 19:07 改成 03:07 才能追到"。
这是一个误解。 UTC 19:07 和北京时间 03:07 不是"两个时间需要换算后再用",而是同一瞬间的两个名字。算法用任何一个名字都能算出同一个结果------只要保证名字和公式一致(天文公式认 UTC 这个名字)。
惯导给你的就是 UTC 这个名字,算法也认 UTC 这个名字,两边名字对得上,直接用即可。
移植到 STM32F427 后捕捉低轨信号:算法不加 8,相控阵下发不加 8
6.如何根据星网给的时间下发指令?
星网时间是2026-04-15 08:42:00
转UTC减4s:2026-04-15 08:41:56
MCU内部计算的话也是按照这个UTC时间去计算
实际指令发送的话要加8在北京时间这个点去发送:2026-4-15 16:41:56这个和UTC时间是等效的只不过是两种叫法,指令发送以后,MCU内部计算还是用的UTC的

7.上位机PC控制惯导起的作用是什么?MCU惯导起的作用是什么?
惯导给 UTC → 上位机收到 → 加 8 小时变北京时间 → 和 PC 本地时间(也是北京时间)相减得到时差 → 这个时差用于后续等待"轨迹文件北京时间"到达。
上位机惯导的作用:仅仅是"对一次表",让 PC 知道"GNSS 的时间和我自己的本地时间差多少秒",之后整个跟踪过程 PC 用"本地时间 + 时差"来判断什么时候发哪条指令。
惯导(沙谷 LaneTo4)
│
│ PBSOL 帧里的 UTC 字段
│ year=2026, month=1, day=10
│ hour=19, minute=07, ms=10500
▼
MCU 收到 PBSOL
│
│ $cmd,get gnss time 触发
│ 回复:"UTC: 2026-01-10 19:07:10.500 Lon: ... Lat: ..."
▼
上位机收到这一行字符串
│
│ 正则解析 → utc_time = 2026-01-10 19:07:10.500
▼
【加 8 小时】
│
│ beijing_time = utc_time + 8h
│ = 2026-01-11 03:07:10.500
▼
存为 gnss_base_time ← 这是北京时间
│
│ 同一瞬间记下:
│ gnss_local_saved = datetime.now() ← PC 本地时间
▼
【时差 = gnss_base_time - gnss_local_saved】
│
│ 例:GNSS 北京时间 03:07:10.500
│ PC 本地时间 03:07:09.000
│ 时差 = +1.500 秒(PC 比 GNSS 慢 1.5 秒)
▼
保存这个时差,后续跟踪一直用
MCU中惯导起到的作用是实时提供准确的UTC数据,定时器每一秒上报惯导数据信息(UTC年月日时分秒)--------再将这个时间送入到星历算法-------解析出这个时间点的俯仰角和方位角------------再把俯仰角方位角送入阵面进行控制------------------再接收下一秒的惯导数据信息----------再将当前UTC时间送入到星历算法--------再解析出这个时间点的俯仰角和方位角------再把俯仰角方位角送入阵面进行控制--------依次循环
PC是代码手动控制时间间隔设置俯仰角和方位角
MCU是定时器控制每1s接收惯导数据信息送入算法解析出俯仰角和方位角去控制