20个点位下发该如何正常操作?

一、整体架构:他其实是"预加载 + 时间触发",而不是你的"实时解算"

你之前的方案(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 量级,取决于:

  1. 时间分辨率被 TIM4 的 50ms 锁死------软件钟只有50ms一跳,所以判断"到点没"最坏晚 50ms。
  2. 主循环轮询延迟 ------Ephem_Process 不是中断里发,是 while 里发,主循环有多长就有多少额外延迟。
  3. 对时误差 ------依赖 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 累加软件钟。

这里有两个关键设计点,我先讲清楚,否则你照抄会踩坑:

  1. 必须做双缓冲。 你现在这套"收到新表就 g_ephem_next_index=0 覆盖旧表"的写法,在"提前1秒发下一组"的场景下会丢一整秒的点。因为下一组在 t=10.0 到达时,当前组的 10.0~10.95 这 20 个点还没发呢,一覆盖就没了,产生 1 秒空档。所以要"当前组发完才切下一组"。
  2. 表里存的是绝对时刻,点要"到时间才发"。 提前 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;

注意:ParseEphemCommandEphem_Process 都在主循环里跑 ,互不抢占(UDP 回调只置旗,真正解析在主循环)。所以这两块缓冲之间不需要关中断保护 ,比原来简单。原来的 g_ephem_now_utc_ms / clock_valid / 50ms_pending 三个变量以及 Ephem_Timer50msISREphem_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 被连灌两组冲掉中间一组。

验证清单(按这个顺序测)

  1. 先用选项A 。灌一组 20 点,串口/网口打印 Ephem_Process 里实际触发时刻 vs 表里 utc_ms,看误差是不是稳定在十几 ms 内。
  2. 测切换无缝:连灌 3~4 组首尾相连的表,确认组与组交界处(如 10.95→11.00)没有丢点、没有重复、没有 1 秒空档。
  3. 测坐标补角对不对 (这个和时间无关但同样致命):beam_el = 90-elbeam_az = (360-az) mod 360 这两个转换要和你阵面实际定义核对,指向错这一步就白搭。
  4. 选项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() 直接写全给你。

相关推荐
jianqiang.xue2 小时前
ESP-IDF保姆级入门23|MQTT物联网通信全解:发布订阅模式/QoS等级/遗嘱消息/主题设计/断线重连,掌握工业级标准消息通信
stm32·单片机·mcu·物联网·51单片机·iot
HRTOS3 小时前
HRTOS 4.0 Driver Library:03_Sensor 传感器驱动
驱动开发·单片机·嵌入式硬件·51单片机
小麦嵌入式3 小时前
FPGA入门(十一):受控线性序列机 UART 发送升级
stm32·单片机·嵌入式硬件·mcu·fpga开发·硬件工程
jianqiang.xue4 小时前
ESP-IDF保姆级入门29|FreeRTOS任务管理与调度全解:任务创建/状态切换/优先级调度/双核负载均衡/实时性优化,掌握嵌入式多任务编程核心
stm32·单片机·mcu·物联网·51单片机·iot
自小吃多4 小时前
PCB Editor软件走线、修线操作笔记
笔记·嵌入式硬件
新晨单片机设计5 小时前
S008A-基于STM32单片机八路抢答器(数码管)【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus
米饭不加菜5 小时前
STM32——数码管显示
stm32·单片机·嵌入式硬件
梵亿6 小时前
基于 STM32F103C8T6 的无人机低功耗设计与实现
stm32·嵌入式硬件·无人机·低功耗
hrw_embedded6 小时前
Keil开发与STM32Cube开发的差异点总结(未完待续)
stm32·单片机·嵌入式硬件