Skip to content

File syn_timesync.c

File List > drivers > syn_timesync.c

Go to the documentation of this file

#if __has_include("syn_config.h")
#include "syn_config.h"
#endif

#if !defined(SYN_USE_TIMESYNC) || SYN_USE_TIMESYNC

#include "../util/syn_assert.h"
#include "syn_timesync.h"

#include <stdlib.h>
#include <string.h>

/* ── Init ───────────────────────────────────────────────────────────────── */

void syn_timesync_init(SYN_TimeSync *tsync)
{
    SYN_ASSERT(tsync != NULL);
    if (tsync == NULL) {
        return;
    }

    memset(tsync, 0, sizeof(SYN_TimeSync));
    tsync->base_jitter_ns = SYN_TIMESYNC_DEFAULT_JITTER_NS;
    tsync->max_holdover_s = SYN_TIMESYNC_DEFAULT_HOLDOVER_S;

#if SYN_TIMESYNC_HAS_RTC
    /* If hardware RTC is configured and valid, mark RTC as available */
    SYN_RTC_DateTime dt;
    if (syn_rtc_get(&dt) == SYN_OK && syn_rtc_is_valid(&dt)) {
        tsync->rtc_synced = true;
    }
#endif
}

/* ── Bind PPS ───────────────────────────────────────────────────────────── */

SYN_Status syn_timesync_bind_pps(SYN_TimeSync *tsync, const SYN_HPTimestamp *pps_ts,
                                 uint64_t utc_sec)
{
    if (tsync == NULL || pps_ts == NULL) {
        return SYN_INVALID_PARAM;
    }

    uint64_t curr_ticks = syn_hpclock_resolve(pps_ts);
    uint32_t nominal_freq = syn_port_hpclock_freq_hz();

    if (tsync->has_pps_lock && tsync->pps_count > 0 && nominal_freq > 0) {
        /*
         * Measure actual ticks elapsed between PPS pulses.
         * For a 1-second interval, expected ticks = nominal_freq.
         * Calculate crystal drift PPM = ((actual - expected) * 1,000,000) / expected.
         */
        uint64_t actual_ticks = curr_ticks - tsync->last_pps_ticks;
        int64_t diff_ticks = (int64_t)actual_ticks - (int64_t)nominal_freq;

        /* Convert tick difference to Parts-Per-Million */
        tsync->drift_ppm = (int32_t)((diff_ticks * 1000000LL) / (int64_t)nominal_freq);
    }

    /* Update anchors */
    tsync->prev_pps_ticks = tsync->last_pps_ticks;
    tsync->last_pps_ts = *pps_ts;
    tsync->last_pps_ticks = curr_ticks;
    tsync->last_utc_sec = utc_sec;
    tsync->pps_count++;
    tsync->has_pps_lock = true;

#if SYN_TIMESYNC_HAS_RTC
    /* Discipline hardware RTC to GPS time if RTC is enabled */
    SYN_RTC_DateTime dt;
    syn_rtc_from_epoch((uint32_t)utc_sec, &dt);
    if (syn_rtc_set(&dt) == SYN_OK) {
        tsync->rtc_synced = true;
    }
#endif

    return SYN_OK;
}

/* ── Resolve UTC ────────────────────────────────────────────────────────── */

SYN_Status syn_timesync_resolve_utc(const SYN_TimeSync *tsync, const SYN_HPTimestamp *event_ts,
                                    SYN_UTCTimestamp *out_utc)
{
    if (tsync == NULL || event_ts == NULL || out_utc == NULL) {
        return SYN_INVALID_PARAM;
    }

    uint64_t event_ticks = syn_hpclock_resolve(event_ts);
    uint32_t freq_hz = syn_port_hpclock_freq_hz();

    if (!tsync->has_pps_lock || freq_hz == 0) {
        /*
         * Tier 4 / Tier 3 Fallback: No active PPS lock ever acquired.
         */
#if SYN_TIMESYNC_HAS_RTC
        if (tsync->rtc_synced) {
            SYN_RTC_DateTime dt;
            if (syn_rtc_get(&dt) == SYN_OK) {
                out_utc->sec = syn_rtc_to_epoch(&dt);
                out_utc->nsec = 0;
                out_utc->uncertainty_ns = 1000000000U; /* ±1 second RTC precision */
                out_utc->source = SYN_TIME_SOURCE_RTC_SYNCED;
                return SYN_OK;
            }
        }
#endif
        /* Unsynced fallback: Monotonic ticks converted to nanoseconds */
        uint64_t total_ns = syn_hpclock_ticks_to_ns(event_ticks);
        out_utc->sec = total_ns / 1000000000ULL;
        out_utc->nsec = (uint32_t)(total_ns % 1000000000ULL);
        out_utc->uncertainty_ns = 0xFFFFFFFFU; /* Infinite / undefined error */
        out_utc->source = SYN_TIME_SOURCE_UNSYNCED;
        return SYN_OK;
    }

    /* Compute tick offset relative to last PPS anchor */
    int64_t tick_delta = (int64_t)event_ticks - (int64_t)tsync->last_pps_ticks;
    int64_t ns_delta;

    if (tick_delta >= 0) {
        ns_delta = (int64_t)syn_hpclock_ticks_to_ns((uint64_t)tick_delta);
    } else {
        ns_delta = -(int64_t)syn_hpclock_ticks_to_ns((uint64_t)(-tick_delta));
    }

    /* Compute total seconds and sub-second nanoseconds */
    int64_t sec_offset = ns_delta / 1000000000LL;
    int64_t nsec_rem = ns_delta % 1000000000LL;

    if (nsec_rem < 0) {
        sec_offset -= 1;
        nsec_rem += 1000000000LL;
    }

    out_utc->sec = (uint64_t)((int64_t)tsync->last_utc_sec + sec_offset);
    out_utc->nsec = (uint32_t)nsec_rem;

    /*
     * Quality Tier & Uncertainty Evaluation
     */
    uint64_t elapsed_s = (tick_delta >= 0) ? (uint64_t)tick_delta / freq_hz : 0;

    if (elapsed_s <= 1) {
        /* Tier 1: Active PPS lock within 1 second */
        out_utc->source = SYN_TIME_SOURCE_GPS_PPS;
        out_utc->uncertainty_ns = tsync->base_jitter_ns;
    } else if (elapsed_s <= tsync->max_holdover_s) {
        /* Tier 2: PPS holdover — error accumulates with drift */
        out_utc->source = SYN_TIME_SOURCE_GPS_HOLDOVER;
        uint32_t abs_drift = (uint32_t)abs(tsync->drift_ppm);
        out_utc->uncertainty_ns = tsync->base_jitter_ns + (abs_drift * (uint32_t)elapsed_s * 1000U);
    } else {
        /* Tier 3 or 4: Holdover expired */
#if SYN_TIMESYNC_HAS_RTC
        if (tsync->rtc_synced) {
            out_utc->source = SYN_TIME_SOURCE_RTC_SYNCED;
            out_utc->uncertainty_ns = 1000000000U;
        } else {
            out_utc->source = SYN_TIME_SOURCE_UNSYNCED;
            out_utc->uncertainty_ns = 0xFFFFFFFFU;
        }
#else
        out_utc->source = SYN_TIME_SOURCE_UNSYNCED;
        out_utc->uncertainty_ns = 0xFFFFFFFFU;
#endif
    }

    return SYN_OK;
}

/* ── Epoch Nanoseconds Helper ───────────────────────────────────────────── */

uint64_t syn_timesync_to_epoch_ns(const SYN_TimeSync *tsync, const SYN_HPTimestamp *event_ts)
{
    SYN_UTCTimestamp utc;
    if (syn_timesync_resolve_utc(tsync, event_ts, &utc) != SYN_OK) {
        return 0;
    }
    return utc.sec * 1000000000ULL + (uint64_t)utc.nsec;
}

/* ── PPS Lock Check ─────────────────────────────────────────────────────── */

bool syn_timesync_is_pps_locked(const SYN_TimeSync *tsync)
{
    if (tsync == NULL || !tsync->has_pps_lock) {
        return false;
    }
    /* Simple sanity check: has at least one PPS been bound */
    return true;
}

#endif /* SYN_USE_TIMESYNC */