一、整体架构:他其实是"预加载 + 时间触发",而不是你的"实时解算"
你之前的方案(Tracker.c)是:MCU 拿轨道根数,每个时刻现场解算方位/俯仰。
你同事的方案是:上位机把算好的20个 (UTC时间, 方位角, 俯仰角) 点一次性灌进来,MCU 存成一张表,然后靠一个自走时钟,到点就发 。两套逻辑完全独立,走的是完全不同的代码路径。Tracker.c 在新方案里基本是"挂着但不调用"的状态(Ephem_Process 根本不碰它)。
二、代码位置对照表
| 阶段 | 函数 / 位置 | 文件 |
|---|---|---|
| ① 上位机发命令,网口收包 | udp_receive_callback → 置 udp_cmd_flag |
myudp.c |
| ② 主循环取命令 | main while 里 if (udp_cmd_flag) → ParseCommand |
main.c |
| ③ 解析20个点存表 | ParseEphemCommand |
protocol.c |
| ④ 建立时基(对时) | Ephem_SyncToCurrentUTC |
protocol.c |
| ⑤ 时钟推进 | Ephem_Timer50msISR(TIM4 中断里调) |
protocol.c |
| ⑥ 到点下发 | Ephem_Process(main while 每圈调) |
protocol.c |
| ⑦ 真正驱动阵面 | XW_On_SetBeamAngles1 |
xw_protocol_user.c |
三、逐段拆解他的思路
① 上位机命令格式
从 ParseEphemCommand 的前缀能反推出上位机发的字符串长这样:
$cmd,set ephem;20;20260906,1230,15.5,123.45,45.67;20260906,1230,15.9,...;*hh
└点数┘ └───YYYYMMDD──┘└hhmm┘└ss.s┘└方位az┘└俯仰el┘ ...重复20次...
注意分隔符:点数和各点之间用分号 ;,一个点内部的5个字段用逗号 ,。这是他刻意设计的,避免和字段内的逗号冲突。
② 网口收包(myudp.c)
udp_receive_callback 里这段是关键分流:
c
if (udp_len >= 4U && memcmp(lwip_rx_buffer, "$cmd", 4U) == 0) {
if (!udp_cmd_flag) { // 上一条还没处理完就丢弃,防覆盖
memcpy(udp_cmd_buffer, lwip_rx_buffer, n);
udp_cmd_flag = 1U; // 只置标志,不在中断里解析
}
}
思路:中断/回调里只做搬运+置旗,绝不解析。 解析放到主循环。这是嵌入式里正确的做法------lwIP 回调上下文不适合跑重逻辑。
③ 解析存表(ParseEphemCommand)
他把20个点解析进 g_ephem_points[20],每个点存三样东西:
c
typedef struct {
uint64_t utc_ms; // 绝对毫秒(2000-01-01起点)
int32_t beam_az_001deg; // 波束方位补角 ×100
int32_t beam_el_001deg; // 波束俯仰补角 ×100
} EphemPoint;
这里有两个关键的坐标变换,你要特别注意:
c
beam_el_deg = 90.0 - el_deg; // 俯仰 → 天顶补角
beam_az_deg = fmod(360.0 - az_deg, 360.0); // 方位取反(镜像)
也就是说,上位机发的是"物理观测角",存进表里的是"阵面需要的补角" 。阵面波控要的不是"抬头45°",而是"离天顶45°",且方位方向是反的。这个转换如果和你阵面实际定义对不上,指向就会错------这是你要第一个核对的地方。
他还做了严格的时间单调性检查(utc_ms <= 上一点 直接返回0),保证20个点时间递增。
④ 建立时基 ------ 整套方案的灵魂(Ephem_SyncToCurrentUTC)
这是最巧妙也最脆弱的一环。到点下发的前提是 MCU 得知道"现在几点"。他的做法:
c
utc_ms = EphemUtcToMs(g_ins_year, ... g_ins_second); // 惯导给的UTC整秒
age_ms = HAL_GetTick() - g_gnss_time_update_tick_ms; // 距上次收到UTC过了多久
g_ephem_now_utc_ms = utc_ms + age_ms; // 补上这段延迟 = 当前真实UTC
g_ephem_clock_valid = 1U;
思路:GNSS/惯导的UTC是"慢更新"的(几百ms才来一帧),不能直接用。他用 HAL_GetTick()(1ms 系统滴答)来补偿"从收到那帧UTC到现在"经过的时间,拼出一个当前时刻。之后这个软件钟就靠 TIM4 自己走。
触发时机:这个函数在 ReplyAntennaParameters 里被调用,也就是上位机每次发 $cmd,get ant para 查询时,顺手对一次时。
⑤ 时钟推进(Ephem_Timer50msISR)
c
void Ephem_Timer50msISR(void) {
if (g_ephem_clock_valid) {
g_ephem_now_utc_ms += 50U; // 每次TIM4中断+50ms
g_ephem_50ms_pending = 1U;
}
}
TIM4 配成 50ms 周期,中断里只干两件事:软件钟 +50ms、置"该检查了"标志。中断极短,不占波控时间,这个原则贯穿全代码。
⑥ 到点下发(Ephem_Process)------ 主循环每圈调
c
while (g_ephem_next_index < g_ephem_count &&
now_ms >= g_ephem_points[g_ephem_next_index].utc_ms) {
EphemPoint *point = &g_ephem_points[g_ephem_next_index];
GNSS_SetAntennaAzimuth(...);
XW_On_SetBeamAngles1(point->beam_az_001deg, point->beam_el_001deg);
g_ephem_next_index++;
}
思路里最重要的一句是这个 while(不是 if) :如果主循环某圈卡了(比如去处理别的命令耽误了),软件钟已经涨过了好几个点,这个 while 会把落下的点一次性补发完 ,用 g_ephem_next_index 做游标,保证按顺序、不跳点。这是防抖动的设计。
它读软件钟时用 __disable_irq() 保护,因为 g_ephem_now_utc_ms 是64位,在32位MCU上读写不是原子的,不关中断可能读到"高32位新、低32位旧"的撕裂值。这个细节他处理对了。
四、准点性到底能做到多准?------ 时间精度链路分析
真实UTC ──惯导串口(几百ms延迟,抖动)──► utc_ms
│
HAL_GetTick()(±1ms) ────────┤ 补偿age_ms
▼
软件钟起点(误差≈惯导时标不确定度)
│
TIM4 50ms累加(晶振ppm级) ───┤ 长期缓慢漂移
▼
主循环轮询(每圈几ms) ────────► 实际发出时刻
结论:这套方案的准点精度大约在 ±50ms ~ ±100ms 量级,取决于:
- 时间分辨率被 TIM4 的 50ms 锁死------软件钟只有50ms一跳,所以判断"到点没"最坏晚 50ms。
- 主循环轮询延迟 ------
Ephem_Process不是中断里发,是 while 里发,主循环有多长就有多少额外延迟。 - 对时误差 ------依赖
get ant para什么时候来,如果长时间不查询,软件钟只靠晶振自走会缓慢漂移。
五、可行性评估与隐患(重点,请务必核对)
可以用,架构是合理的,但有几处我必须给你标出来:
🔴 隐患1:PPS 明明写好了却没启用。 main.c 里 PPS_Sync_Init() 和 MX_IWDG_Init() 都被注释掉了。pps_sync.c 那套硬件授时(TIM2 微秒计数 + PPS 上升沿锚定)精度是微秒级 ,远高于现在这套 50ms 的软件钟。你同事写了高精度方案但没接上 ,实际跑的是精度低两三个数量级的降级版。如果你要"真准点",应该把星历下发的时基从 g_ephem_now_utc_ms 切到 PPS_GetJDNow()。
🔴 隐患2:对时依赖"上位机来查询"这个副作用。 Ephem_SyncToCurrentUTC() 只在收到 get ant para 时调用。如果上位机灌完星历后不发查询 ,软件钟可能根本没建立(g_ephem_clock_valid 还是0),或者建立后长时间不校准而漂移。这个耦合很隐蔽,建议:灌表成功后立即自动对一次时,且周期性(比如每秒)重新对时。
🟡 隐患3:TIM4 中断里是否真的调了 Ephem_Timer50msISR? 我在给的文件里没看到 TIM4 的回调函数体,只看到 main 里 HAL_TIM_Base_Start_IT(&htim4)。你要确认 HAL_TIM_PeriodElapsedCallback 里确实调用了 Ephem_Timer50msISR(),并且 TIM4 周期确实配成了 50ms------否则软件钟根本不走,一个点都发不出去 。同时也要确认 TIM4 周期和代码里写死的 += 50U 一致。
🟡 隐患4:20点最多覆盖多久要算清楚。 EPHEM_MAX_POINTS = 20。如果上位机每 0.4s 一个点,20个点只覆盖 8 秒,必须持续补发新表。这是个运行约束,不是bug,但要确认上位机的补表节奏跟得上。
🟢 隐患5(次要): 存表用局部数组 parsed[20] 再 memcpy 到全局,是为了"解析失败不破坏正在用的旧表",这个设计是对的,可放心。
六。修改方案
改造方案:查表 + 绝对时间触发(去掉 TIM4 软件钟)
先把目标架构一句话说清楚,你后面每一步都围绕它:
上位机提前 1 秒把 20 个"绝对UTC时刻 + 方位/俯仰"灌进来,MCU 存两块缓冲,靠一个"当前真实UTC"去比对,到点就发;不再用 TIM4 累加软件钟。
这里有两个关键设计点,我先讲清楚,否则你照抄会踩坑:
- 必须做双缓冲。 你现在这套"收到新表就
g_ephem_next_index=0覆盖旧表"的写法,在"提前1秒发下一组"的场景下会丢一整秒的点。因为下一组在 t=10.0 到达时,当前组的 10.0~10.95 这 20 个点还没发呢,一覆盖就没了,产生 1 秒空档。所以要"当前组发完才切下一组"。 - 表里存的是绝对时刻,点要"到时间才发"。 提前 1 秒收到不等于提前发------
now >= point->utc_ms这个判断天然保证了"表早到了也压着不发,各点等各自的绝对时刻"。
下面是完整步骤。
第 1 步:把单缓冲换成双缓冲(protocol.c)
把原来的:
static EphemPoint g_ephem_points[EPHEM_MAX_POINTS];
static uint8_t g_ephem_count = 0U;
static uint8_t g_ephem_next_index = 0U;
static volatile uint64_t g_ephem_now_utc_ms = 0U;
static volatile uint8_t g_ephem_clock_valid = 0U;
static volatile uint8_t g_ephem_50ms_pending = 0U;
替换为:
/* active = 正在下发的组;pending = 提前1秒收到、等待切换的下一组 */
static EphemPoint g_ephem_active[EPHEM_MAX_POINTS];
static uint8_t g_ephem_active_count = 0U;
static uint8_t g_ephem_active_index = 0U;
static EphemPoint g_ephem_pending[EPHEM_MAX_POINTS];
static uint8_t g_ephem_pending_count = 0U;
static uint8_t g_ephem_pending_ready = 0U;
注意:
ParseEphemCommand和Ephem_Process都在主循环里跑 ,互不抢占(UDP 回调只置旗,真正解析在主循环)。所以这两块缓冲之间不需要关中断保护 ,比原来简单。原来的g_ephem_now_utc_ms / clock_valid / 50ms_pending三个变量以及Ephem_Timer50msISR、Ephem_SyncToCurrentUTC全部作废,后面会删掉调用。
第 2 步:解析后写入 pending(改 ParseEphemCommand 结尾)
ParseEphemCommand 前面的解析逻辑(分号分组、坐标补角转换、时间单调性检查)全部保留不动,只改最后写入的三行:
/* 原来是写 g_ephem_points,现在写入 pending,等当前组发完再切 */
memcpy(g_ephem_pending, parsed, (size_t)count * sizeof(parsed[0]));
g_ephem_pending_count = (uint8_t)count;
g_ephem_pending_ready = 1U;
return 1;
这样"收到就存进候补区",不碰正在跑的组。
第 3 步:重写 Ephem_Process(切换 + 时基)
void Ephem_Process(void)
{
uint64_t now_ms;
/* ① 取当前真实UTC(毫秒)。时基见第5步,两种实现二选一 */
now_ms = Ephem_NowUtcMs();
if (now_ms == 0U) return; /* 时基还没建立,不发 */
/* ② 当前组发完 → 把提前收到的下一组切进来(关键:不丢点、不空档) */
if (g_ephem_active_index >= g_ephem_active_count && g_ephem_pending_ready) {
memcpy(g_ephem_active, g_ephem_pending,
(size_t)g_ephem_pending_count * sizeof(g_ephem_active[0]));
g_ephem_active_count = g_ephem_pending_count;
g_ephem_active_index = 0U;
g_ephem_pending_ready = 0U;
}
if (g_ephem_active_count == 0U ||
g_ephem_active_index >= g_ephem_active_count) return;
/* ③ 到点就发;用 while 补发落下的点,保证按序不跳点 */
while (g_ephem_active_index < g_ephem_active_count &&
now_ms >= g_ephem_active[g_ephem_active_index].utc_ms) {
EphemPoint *point = &g_ephem_active[g_ephem_active_index];
GNSS_SetAntennaAzimuth((double)point->beam_az_001deg / 100.0);
XW_On_SetBeamAngles1(point->beam_az_001deg, point->beam_el_001deg);
g_ephem_active_index++;
}
}
"提前1秒"在这里如何生效: 下一组在 t≈10.0 到达进 pending;当前组在 t≈10.95 发完最后一点、active_index>=count,②立刻把 pending 切成 active,其首点时刻 11.0 仍大于当前 now,压着等到 11.0 才发。无缝、无空档、无丢点------那 1 秒提前量就是用来把 pending 提前备好的。
第 4 步:拆掉 TIM4 软件钟(tim.c)
HAL_TIM_PeriodElapsedCallback 里删掉 Ephem_Timer50msISR() 调用:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM4)
{
tim4_track_flag = 1;
/* Ephem_Timer50msISR(); ← 删除:不再用TIM4推软件钟 */
}
}
TIM4 可以留着空转(无害),也可以把 main.c 里 HAL_TIM_Base_Start_IT(&htim4) 一并注释掉。顺便说一句:你之前发现的 "Period=3999 是400ms 但代码加50ms" 的那个不匹配 bug,这套改完后就彻底不相关了------因为时间不再来自 TIM4。
ReplyAntennaParameters 里那句 Ephem_SyncToCurrentUTC(); 也删掉(时基改为按需计算,不需要它了)。
第 5 步:时基二选一(重点,决定准点精度)
新增 Ephem_NowUtcMs(),返回"此刻绝对UTC毫秒"。这里选哪个,直接决定你的准点精度。
选项 A(推荐先上):GNSS UTC + HAL_GetTick 插值,无硬件依赖
static uint64_t Ephem_NowUtcMs(void)
{
uint64_t base_ms;
uint32_t age_ms;
if (!g_gnss_time_valid) return 0U; /* 惯导UTC还没来 */
base_ms = EphemUtcToMs(g_ins_year, g_ins_month, g_ins_day,
g_ins_hour, g_ins_minute, (double)g_ins_second);
if (base_ms == 0U) return 0U;
age_ms = HAL_GetTick() - g_gnss_time_update_tick_ms; /* 补上惯导帧到现在的延迟 */
return base_ms + age_ms;
}
- 直接解决隐患2 :每次调用都用最新惯导UTC重新算,
get ant para来不来都不影响,不会漂。 - 精度:≈ 惯导报文延迟抖动 + HAL_GetTick 的 1ms。对 50ms 的点间隔够用(前提是惯导UTC本身准、报文延迟稳定)。
- 零硬件风险,建议先用这个把整条链路跑通、验证正确性。
选项 B(精度升级):PPS 硬件授时,微秒级
只有当选项A精度不够、且确认硬件真的接了 PPS 秒脉冲时才上。需要三处改动:
(B1) main.c 打开 PPS:
PPS_Sync_Init(); /* 取消注释 */
(B2) gnss.c 里,惯导解析出有效UTC后锚定 PPS (在 parse_gnss_frame 设完 g_gnss_time_valid=1U 那几行后面加):
#include "pps_sync.h"
...
/* 用GNSS整秒 + 秒内小数(ms)锚定最近一个PPS上升沿 */
PPS_Anchor(year, month, day, hour, minute,
(uint32_t)(second * 1000.0f + 0.5f));
(B3) Ephem_NowUtcMs() 改成读 PPS:
static uint64_t Ephem_NowUtcMs(void)
{
double jd;
if (!PPS_TimeValid()) return 0U;
jd = PPS_GetJDNow();
if (jd == 0.0) return 0U;
/* JD → 绝对毫秒,起点2000-01-01,和 EphemUtcToMs 完全一致 */
return (uint64_t)((jd - 2451544.5) * 86400000.0 + 0.5);
}
⚠️ PPS 上线前必须在台架上调这两件事,否则可能一个点都发不出去:
pps_sync.h里的PPS_LAT_MIN_MS / PPS_LAT_MAX_MS(管线延迟窗口)要用实测值。代码里已经留了调试计数器g_dbg_diff_min / g_dbg_diff_max / g_dbg_frac_hist[],跑起来看这几个值落在哪,再把窗口设进去。窗口设错,PPS_Anchor会一直拒绝锚定,PPS_TimeValid()永远为假。- 确认 PPS 秒脉冲物理上真的接到了
PPS_GPIO_PIN对应的 EXTI 引脚。
第 6 步:上位机侧约定(隐患4,必须对齐)
- 每组 20 点、每秒一组、提前 1 秒发 :那么 20 点应覆盖整整 1 秒 (即 50ms 一个点,
10.000, 10.050, ..., 10.950),下一组接着11.000开始,首尾相连不留缝。这样第3步的"发完即切"才无缝。 - 组内时刻必须严格递增 (
ParseEphemCommand已校验,不满足直接回ack error)。 - 建议上位机每灌一组、收到
$cmd,set ephem,ack再发下一组,避免 pending 被连灌两组冲掉中间一组。
验证清单(按这个顺序测)
- 先用选项A 。灌一组 20 点,串口/网口打印
Ephem_Process里实际触发时刻 vs 表里utc_ms,看误差是不是稳定在十几 ms 内。 - 测切换无缝:连灌 3~4 组首尾相连的表,确认组与组交界处(如 10.95→11.00)没有丢点、没有重复、没有 1 秒空档。
- 测坐标补角对不对 (这个和时间无关但同样致命):
beam_el = 90-el、beam_az = (360-az) mod 360这两个转换要和你阵面实际定义核对,指向错这一步就白搭。 - 选项A 稳了、且需要更高精度时,再切选项B,先在台架上用调试计数器把 PPS 窗口调好。
残留风险两条
XW_On_SetBeamAngles1内部有HAL_Delay(5)阻塞等回包。正常一圈发一个点没问题;但首次锚定或时钟跳变时,while 可能一次补发很多点,每点阻塞几 ms 会短暂占住主循环。如果介意,可以在 while 里限制"每次 Process 最多补发 N 个点"。- 选项B 下,
PPS_TimeValid()为假时整个下发停摆 (不像选项A有惯导兜底)。稳妥做法:保留选项A的Ephem_NowUtcMs,PPS 无效时自动回退到 GNSS+HAL_GetTick,二者取其一,既准又不至于哑火。
需要的话,我可以把"PPS 有效用PPS、无效自动回退到GNSS插值"的那个融合版 Ephem_NowUtcMs() 直接写全给你。