root/drivers/net/wireless/intel/iwlwifi/mld/ptp.c
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
 * Copyright (C) 2025 Intel Corporation
 */

#include "mld.h"
#include "iwl-debug.h"
#include "hcmd.h"
#include "ptp.h"
#include <linux/timekeeping.h>

/* The scaled_ppm parameter is ppm (parts per million) with a 16-bit fractional
 * part, which means that a value of 1 in one of those fields actually means
 * 2^-16 ppm, and 2^16=65536 is 1 ppm.
 */
#define PTP_SCALE_FACTOR        65536000000ULL

#define IWL_PTP_GP2_WRAP        0x100000000ULL
#define IWL_PTP_WRAP_TIME       (3600 * HZ)
#define IWL_PTP_WRAP_THRESHOLD_USEC     (5000)

static int iwl_mld_get_systime(struct iwl_mld *mld, u32 *gp2)
{
        *gp2 = iwl_read_prph(mld->trans, mld->trans->mac_cfg->base->gp2_reg_addr);

        if (*gp2 == 0x5a5a5a5a)
                return -EINVAL;

        return 0;
}

static void iwl_mld_ptp_update_new_read(struct iwl_mld *mld, u32 gp2)
{
        IWL_DEBUG_PTP(mld, "PTP: last_gp2=%u, new gp2 read=%u\n",
                      mld->ptp_data.last_gp2, gp2);

        /* If the difference is above the threshold, assume it's a wraparound.
         * Otherwise assume it's an old read and ignore it.
         */
        if (gp2 < mld->ptp_data.last_gp2) {
                if (mld->ptp_data.last_gp2 - gp2 <
                    IWL_PTP_WRAP_THRESHOLD_USEC) {
                        IWL_DEBUG_PTP(mld,
                                      "PTP: ignore old read (gp2=%u, last_gp2=%u)\n",
                                      gp2, mld->ptp_data.last_gp2);
                        return;
                }

                mld->ptp_data.wrap_counter++;
                IWL_DEBUG_PTP(mld,
                              "PTP: wraparound detected (new counter=%u)\n",
                              mld->ptp_data.wrap_counter);
        }

        mld->ptp_data.last_gp2 = gp2;
        schedule_delayed_work(&mld->ptp_data.dwork, IWL_PTP_WRAP_TIME);
}

u64 iwl_mld_ptp_get_adj_time(struct iwl_mld *mld, u64 base_time_ns)
{
        struct ptp_data *data = &mld->ptp_data;
        u64 scale_time_gp2_ns = mld->ptp_data.scale_update_gp2 * NSEC_PER_USEC;
        u64 res;
        u64 diff;
        s64 scaled_diff;

        lockdep_assert_held(&data->lock);

        iwl_mld_ptp_update_new_read(mld,
                                    div64_u64(base_time_ns, NSEC_PER_USEC));

        base_time_ns = base_time_ns +
                (data->wrap_counter * IWL_PTP_GP2_WRAP * NSEC_PER_USEC);

        /* It is possible that a GP2 timestamp was received from fw before the
         * last scale update.
         */
        if (base_time_ns < scale_time_gp2_ns) {
                diff = scale_time_gp2_ns - base_time_ns;
                scaled_diff = -mul_u64_u64_div_u64(diff,
                                                   data->scaled_freq,
                                                   PTP_SCALE_FACTOR);
        } else {
                diff = base_time_ns - scale_time_gp2_ns;
                scaled_diff = mul_u64_u64_div_u64(diff,
                                                  data->scaled_freq,
                                                  PTP_SCALE_FACTOR);
        }

        IWL_DEBUG_PTP(mld, "base_time=%llu diff ns=%llu scaled_diff_ns=%lld\n",
                      (unsigned long long)base_time_ns,
                      (unsigned long long)diff, (long long)scaled_diff);

        res = data->scale_update_adj_time_ns + data->delta + scaled_diff;

        IWL_DEBUG_PTP(mld, "scale_update_ns=%llu delta=%lld adj=%llu\n",
                      (unsigned long long)data->scale_update_adj_time_ns,
                      (long long)data->delta, (unsigned long long)res);
        return res;
}

static int iwl_mld_ptp_gettime(struct ptp_clock_info *ptp,
                               struct timespec64 *ts)
{
        struct iwl_mld *mld = container_of(ptp, struct iwl_mld,
                                           ptp_data.ptp_clock_info);
        struct ptp_data *data = &mld->ptp_data;
        u32 gp2;
        u64 ns;

        if (iwl_mld_get_systime(mld, &gp2)) {
                IWL_DEBUG_PTP(mld, "PTP: gettime: failed to read systime\n");
                return -EIO;
        }

        spin_lock_bh(&data->lock);
        ns = iwl_mld_ptp_get_adj_time(mld, (u64)gp2 * NSEC_PER_USEC);
        spin_unlock_bh(&data->lock);

        *ts = ns_to_timespec64(ns);
        return 0;
}

static int iwl_mld_ptp_settime(struct ptp_clock_info *ptp,
                               const struct timespec64 *ts)
{
        return -EOPNOTSUPP;
}

static int iwl_mld_ptp_adjtime(struct ptp_clock_info *ptp, s64 delta)
{
        struct iwl_mld *mld = container_of(ptp, struct iwl_mld,
                                           ptp_data.ptp_clock_info);
        struct ptp_data *data = &mld->ptp_data;

        spin_lock_bh(&data->lock);
        data->delta += delta;
        IWL_DEBUG_PTP(mld, "delta=%lld, new delta=%lld\n", (long long)delta,
                      (long long)data->delta);
        spin_unlock_bh(&data->lock);
        return 0;
}

static int iwl_mld_ptp_adjfine(struct ptp_clock_info *ptp, long scaled_ppm)
{
        struct iwl_mld *mld = container_of(ptp, struct iwl_mld,
                                           ptp_data.ptp_clock_info);
        struct ptp_data *data = &mld->ptp_data;
        u32 gp2;

        /* Must call iwl_mld_ptp_get_adj_time() before updating
         * data->scale_update_gp2 or data->scaled_freq since
         * scale_update_adj_time_ns should reflect the previous scaled_freq.
         */
        if (iwl_mld_get_systime(mld, &gp2)) {
                IWL_DEBUG_PTP(mld, "adjfine: failed to read systime\n");
                return -EBUSY;
        }

        spin_lock_bh(&data->lock);
        data->scale_update_adj_time_ns =
                iwl_mld_ptp_get_adj_time(mld, gp2 * NSEC_PER_USEC);
        data->scale_update_gp2 = gp2;

        /* scale_update_adj_time_ns now relects the configured delta, the
         * wrap_counter and the previous scaled frequency. Thus delta and
         * wrap_counter should be reset, and the scale frequency is updated
         * to the new frequency.
         */
        data->delta = 0;
        data->wrap_counter = 0;
        data->scaled_freq = PTP_SCALE_FACTOR + scaled_ppm;
        IWL_DEBUG_PTP(mld, "adjfine: scaled_ppm=%ld new=%llu\n",
                      scaled_ppm, (unsigned long long)data->scaled_freq);
        spin_unlock_bh(&data->lock);
        return 0;
}

static void iwl_mld_ptp_work(struct work_struct *wk)
{
        struct iwl_mld *mld = container_of(wk, struct iwl_mld,
                                           ptp_data.dwork.work);
        struct ptp_data *data = &mld->ptp_data;
        u32 gp2;

        spin_lock_bh(&data->lock);
        if (!iwl_mld_get_systime(mld, &gp2))
                iwl_mld_ptp_update_new_read(mld, gp2);
        else
                IWL_DEBUG_PTP(mld, "PTP work: failed to read GP2\n");
        spin_unlock_bh(&data->lock);
}

static int
iwl_mld_get_crosstimestamp_fw(struct iwl_mld *mld, u32 *gp2, u64 *sys_time)
{
        struct iwl_synced_time_cmd synced_time_cmd = {
                .operation = cpu_to_le32(IWL_SYNCED_TIME_OPERATION_READ_BOTH)
        };
        struct iwl_host_cmd cmd = {
                .id = WIDE_ID(DATA_PATH_GROUP, WNM_PLATFORM_PTM_REQUEST_CMD),
                .flags = CMD_WANT_SKB,
                .data[0] = &synced_time_cmd,
                .len[0] = sizeof(synced_time_cmd),
        };
        struct iwl_synced_time_rsp *resp;
        struct iwl_rx_packet *pkt;
        int ret;
        u64 gp2_10ns;

        wiphy_lock(mld->wiphy);
        ret = iwl_mld_send_cmd(mld, &cmd);
        wiphy_unlock(mld->wiphy);
        if (ret)
                return ret;

        pkt = cmd.resp_pkt;

        if (iwl_rx_packet_payload_len(pkt) != sizeof(*resp)) {
                IWL_DEBUG_PTP(mld, "PTP: Invalid PTM command response\n");
                iwl_free_resp(&cmd);
                return -EIO;
        }

        resp = (void *)pkt->data;

        gp2_10ns = (u64)le32_to_cpu(resp->gp2_timestamp_hi) << 32 |
                le32_to_cpu(resp->gp2_timestamp_lo);
        *gp2 = div_u64(gp2_10ns, 100);

        *sys_time = (u64)le32_to_cpu(resp->platform_timestamp_hi) << 32 |
                le32_to_cpu(resp->platform_timestamp_lo);

        iwl_free_resp(&cmd);
        return ret;
}

static int
iwl_mld_phc_get_crosstimestamp(struct ptp_clock_info *ptp,
                               struct system_device_crosststamp *xtstamp)
{
        struct iwl_mld *mld = container_of(ptp, struct iwl_mld,
                                           ptp_data.ptp_clock_info);
        struct ptp_data *data = &mld->ptp_data;
        int ret = 0;
        /* Raw value read from GP2 register in usec */
        u32 gp2;
        /* GP2 value in ns*/
        s64 gp2_ns;
        /* System (wall) time */
        ktime_t sys_time;

        memset(xtstamp, 0, sizeof(struct system_device_crosststamp));

        ret = iwl_mld_get_crosstimestamp_fw(mld, &gp2, &sys_time);
        if (ret) {
                IWL_DEBUG_PTP(mld,
                              "PTP: fw get_crosstimestamp failed (ret=%d)\n",
                              ret);
                return ret;
        }

        spin_lock_bh(&data->lock);
        gp2_ns = iwl_mld_ptp_get_adj_time(mld, (u64)gp2 * NSEC_PER_USEC);
        spin_unlock_bh(&data->lock);

        IWL_DEBUG_PTP(mld,
                      "Got Sync Time: GP2:%u, last_GP2: %u, GP2_ns: %lld, sys_time: %lld\n",
                      gp2, mld->ptp_data.last_gp2, gp2_ns, (s64)sys_time);

        /* System monotonic raw time is not used */
        xtstamp->device = ns_to_ktime(gp2_ns);
        xtstamp->sys_realtime = sys_time;

        return ret;
}

void iwl_mld_ptp_init(struct iwl_mld *mld)
{
        if (WARN_ON(mld->ptp_data.ptp_clock))
                return;

        spin_lock_init(&mld->ptp_data.lock);
        INIT_DELAYED_WORK(&mld->ptp_data.dwork, iwl_mld_ptp_work);

        mld->ptp_data.ptp_clock_info.owner = THIS_MODULE;
        mld->ptp_data.ptp_clock_info.gettime64 = iwl_mld_ptp_gettime;
        mld->ptp_data.ptp_clock_info.settime64 = iwl_mld_ptp_settime;
        mld->ptp_data.ptp_clock_info.max_adj = 0x7fffffff;
        mld->ptp_data.ptp_clock_info.adjtime = iwl_mld_ptp_adjtime;
        mld->ptp_data.ptp_clock_info.adjfine = iwl_mld_ptp_adjfine;
        mld->ptp_data.scaled_freq = PTP_SCALE_FACTOR;
        mld->ptp_data.ptp_clock_info.getcrosststamp =
                                        iwl_mld_phc_get_crosstimestamp;

        /* Give a short 'friendly name' to identify the PHC clock */
        snprintf(mld->ptp_data.ptp_clock_info.name,
                 sizeof(mld->ptp_data.ptp_clock_info.name),
                 "%s", "iwlwifi-PTP");

        mld->ptp_data.ptp_clock =
                ptp_clock_register(&mld->ptp_data.ptp_clock_info, mld->dev);

        if (IS_ERR_OR_NULL(mld->ptp_data.ptp_clock)) {
                IWL_ERR(mld, "Failed to register PHC clock (%ld)\n",
                        PTR_ERR(mld->ptp_data.ptp_clock));
                mld->ptp_data.ptp_clock = NULL;
        } else {
                IWL_DEBUG_INFO(mld, "Registered PHC clock: %s, with index: %d\n",
                               mld->ptp_data.ptp_clock_info.name,
                               ptp_clock_index(mld->ptp_data.ptp_clock));
        }
}

void iwl_mld_ptp_remove(struct iwl_mld *mld)
{
        if (mld->ptp_data.ptp_clock) {
                IWL_DEBUG_INFO(mld, "Unregistering PHC clock: %s, with index: %d\n",
                               mld->ptp_data.ptp_clock_info.name,
                               ptp_clock_index(mld->ptp_data.ptp_clock));

                ptp_clock_unregister(mld->ptp_data.ptp_clock);
                mld->ptp_data.ptp_clock = NULL;
                mld->ptp_data.last_gp2 = 0;
                mld->ptp_data.wrap_counter = 0;
                cancel_delayed_work_sync(&mld->ptp_data.dwork);
        }
}