root/drivers/ptp/ptp_dfl_tod.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * DFL device driver for Time-of-Day (ToD) private feature
 *
 * Copyright (C) 2023 Intel Corporation
 */

#include <linux/bitfield.h>
#include <linux/delay.h>
#include <linux/dfl.h>
#include <linux/gcd.h>
#include <linux/iopoll.h>
#include <linux/module.h>
#include <linux/ptp_clock_kernel.h>
#include <linux/spinlock.h>
#include <linux/units.h>

#define FME_FEATURE_ID_TOD              0x22

/* ToD clock register space. */
#define TOD_CLK_FREQ                    0x038

/*
 * The read sequence of ToD timestamp registers: TOD_NANOSEC, TOD_SECONDSL and
 * TOD_SECONDSH, because there is a hardware snapshot whenever the TOD_NANOSEC
 * register is read.
 *
 * The ToD IP requires writing registers in the reverse order to the read sequence.
 * The timestamp is corrected when the TOD_NANOSEC register is written, so the
 * sequence of write TOD registers: TOD_SECONDSH, TOD_SECONDSL and TOD_NANOSEC.
 */
#define TOD_SECONDSH                    0x100
#define TOD_SECONDSL                    0x104
#define TOD_NANOSEC                     0x108
#define TOD_PERIOD                      0x110
#define TOD_ADJUST_PERIOD               0x114
#define TOD_ADJUST_COUNT                0x118
#define TOD_DRIFT_ADJUST                0x11c
#define TOD_DRIFT_ADJUST_RATE           0x120
#define PERIOD_FRAC_OFFSET              16
#define SECONDS_MSB                     GENMASK_ULL(47, 32)
#define SECONDS_LSB                     GENMASK_ULL(31, 0)
#define TOD_SECONDSH_SEC_MSB            GENMASK_ULL(15, 0)

#define CAL_SECONDS(m, l)               ((FIELD_GET(TOD_SECONDSH_SEC_MSB, (m)) << 32) | (l))

#define TOD_PERIOD_MASK         GENMASK_ULL(19, 0)
#define TOD_PERIOD_MAX                  FIELD_MAX(TOD_PERIOD_MASK)
#define TOD_PERIOD_MIN                  0
#define TOD_DRIFT_ADJUST_MASK           GENMASK_ULL(15, 0)
#define TOD_DRIFT_ADJUST_FNS_MAX        FIELD_MAX(TOD_DRIFT_ADJUST_MASK)
#define TOD_DRIFT_ADJUST_RATE_MAX       TOD_DRIFT_ADJUST_FNS_MAX
#define TOD_ADJUST_COUNT_MASK           GENMASK_ULL(19, 0)
#define TOD_ADJUST_COUNT_MAX            FIELD_MAX(TOD_ADJUST_COUNT_MASK)
#define TOD_ADJUST_INTERVAL_US          10
#define TOD_ADJUST_MS                   \
                (((TOD_PERIOD_MAX >> 16) + 1) * (TOD_ADJUST_COUNT_MAX + 1))
#define TOD_ADJUST_MS_MAX               (TOD_ADJUST_MS / MICRO)
#define TOD_ADJUST_MAX_US               (TOD_ADJUST_MS_MAX * USEC_PER_MSEC)
#define TOD_MAX_ADJ                     (500 * MEGA)

struct dfl_tod {
        struct ptp_clock_info ptp_clock_ops;
        struct device *dev;
        struct ptp_clock *ptp_clock;

        /* ToD Clock address space */
        void __iomem *tod_ctrl;

        /* ToD clock registers protection */
        spinlock_t tod_lock;
};

/*
 * A fine ToD HW clock offset adjustment. To perform the fine offset adjustment, the
 * adjust_period and adjust_count argument are used to update the TOD_ADJUST_PERIOD
 * and TOD_ADJUST_COUNT register for in hardware. The dt->tod_lock spinlock must be
 * held when calling this function.
 */
static int fine_adjust_tod_clock(struct dfl_tod *dt, u32 adjust_period,
                                 u32 adjust_count)
{
        void __iomem *base = dt->tod_ctrl;
        u32 val;

        writel(adjust_period, base + TOD_ADJUST_PERIOD);
        writel(adjust_count, base + TOD_ADJUST_COUNT);

        /* Wait for present offset adjustment update to complete */
        return readl_poll_timeout_atomic(base + TOD_ADJUST_COUNT, val, !val, TOD_ADJUST_INTERVAL_US,
                                  TOD_ADJUST_MAX_US);
}

/*
 * A coarse ToD HW clock offset adjustment. The coarse time adjustment performs by
 * adding or subtracting the delta value from the current ToD HW clock time.
 */
static int coarse_adjust_tod_clock(struct dfl_tod *dt, s64 delta)
{
        u32 seconds_msb, seconds_lsb, nanosec;
        void __iomem *base = dt->tod_ctrl;
        u64 seconds, now;

        if (delta == 0)
                return 0;

        nanosec = readl(base + TOD_NANOSEC);
        seconds_lsb = readl(base + TOD_SECONDSL);
        seconds_msb = readl(base + TOD_SECONDSH);

        /* Calculate new time */
        seconds = CAL_SECONDS(seconds_msb, seconds_lsb);
        now = seconds * NSEC_PER_SEC + nanosec + delta;

        seconds = div_u64_rem(now, NSEC_PER_SEC, &nanosec);
        seconds_msb = FIELD_GET(SECONDS_MSB, seconds);
        seconds_lsb = FIELD_GET(SECONDS_LSB, seconds);

        writel(seconds_msb, base + TOD_SECONDSH);
        writel(seconds_lsb, base + TOD_SECONDSL);
        writel(nanosec, base + TOD_NANOSEC);

        return 0;
}

static int dfl_tod_adjust_fine(struct ptp_clock_info *ptp, long scaled_ppm)
{
        struct dfl_tod *dt = container_of(ptp, struct dfl_tod, ptp_clock_ops);
        u32 tod_period, tod_rem, tod_drift_adjust_fns, tod_drift_adjust_rate;
        void __iomem *base = dt->tod_ctrl;
        unsigned long flags, rate;
        u64 ppb;

        /* Get the clock rate from clock frequency register offset */
        rate = readl(base + TOD_CLK_FREQ);

        /* add GIGA as nominal ppb */
        ppb = scaled_ppm_to_ppb(scaled_ppm) + GIGA;

        tod_period = div_u64_rem(ppb << PERIOD_FRAC_OFFSET, rate, &tod_rem);
        if (tod_period > TOD_PERIOD_MAX)
                return -ERANGE;

        /*
         * The drift of ToD adjusted periodically by adding a drift_adjust_fns
         * correction value every drift_adjust_rate count of clock cycles.
         */
        tod_drift_adjust_fns = tod_rem / gcd(tod_rem, rate);
        tod_drift_adjust_rate = rate / gcd(tod_rem, rate);

        while ((tod_drift_adjust_fns > TOD_DRIFT_ADJUST_FNS_MAX) ||
               (tod_drift_adjust_rate > TOD_DRIFT_ADJUST_RATE_MAX)) {
                tod_drift_adjust_fns >>= 1;
                tod_drift_adjust_rate >>= 1;
        }

        if (tod_drift_adjust_fns == 0)
                tod_drift_adjust_rate = 0;

        spin_lock_irqsave(&dt->tod_lock, flags);
        writel(tod_period, base + TOD_PERIOD);
        writel(0, base + TOD_ADJUST_PERIOD);
        writel(0, base + TOD_ADJUST_COUNT);
        writel(tod_drift_adjust_fns, base + TOD_DRIFT_ADJUST);
        writel(tod_drift_adjust_rate, base + TOD_DRIFT_ADJUST_RATE);
        spin_unlock_irqrestore(&dt->tod_lock, flags);

        return 0;
}

static int dfl_tod_adjust_time(struct ptp_clock_info *ptp, s64 delta)
{
        struct dfl_tod *dt = container_of(ptp, struct dfl_tod, ptp_clock_ops);
        u32 period, diff, rem, rem_period, adj_period;
        void __iomem *base = dt->tod_ctrl;
        unsigned long flags;
        bool neg_adj;
        u64 count;
        int ret;

        neg_adj = delta < 0;
        if (neg_adj)
                delta = -delta;

        spin_lock_irqsave(&dt->tod_lock, flags);

        /*
         * Get the maximum possible value of the Period register offset
         * adjustment in nanoseconds scale. This depends on the current
         * Period register setting and the maximum and minimum possible
         * values of the Period register.
         */
        period = readl(base + TOD_PERIOD);

        if (neg_adj) {
                diff = (period - TOD_PERIOD_MIN) >> PERIOD_FRAC_OFFSET;
                adj_period = period - (diff << PERIOD_FRAC_OFFSET);
                count = div_u64_rem(delta, diff, &rem);
                rem_period = period - (rem << PERIOD_FRAC_OFFSET);
        } else {
                diff = (TOD_PERIOD_MAX - period) >> PERIOD_FRAC_OFFSET;
                adj_period = period + (diff << PERIOD_FRAC_OFFSET);
                count = div_u64_rem(delta, diff, &rem);
                rem_period = period + (rem << PERIOD_FRAC_OFFSET);
        }

        ret = 0;

        if (count > TOD_ADJUST_COUNT_MAX) {
                ret = coarse_adjust_tod_clock(dt, delta);
        } else {
                /* Adjust the period by count cycles to adjust the time */
                if (count)
                        ret = fine_adjust_tod_clock(dt, adj_period, count);

                /* If there is a remainder, adjust the period for an additional cycle */
                if (rem)
                        ret = fine_adjust_tod_clock(dt, rem_period, 1);
        }

        spin_unlock_irqrestore(&dt->tod_lock, flags);

        return ret;
}

static int dfl_tod_get_timex(struct ptp_clock_info *ptp, struct timespec64 *ts,
                             struct ptp_system_timestamp *sts)
{
        struct dfl_tod *dt = container_of(ptp, struct dfl_tod, ptp_clock_ops);
        u32 seconds_msb, seconds_lsb, nanosec;
        void __iomem *base = dt->tod_ctrl;
        unsigned long flags;
        u64 seconds;

        spin_lock_irqsave(&dt->tod_lock, flags);
        ptp_read_system_prets(sts);
        nanosec = readl(base + TOD_NANOSEC);
        seconds_lsb = readl(base + TOD_SECONDSL);
        seconds_msb = readl(base + TOD_SECONDSH);
        ptp_read_system_postts(sts);
        spin_unlock_irqrestore(&dt->tod_lock, flags);

        seconds = CAL_SECONDS(seconds_msb, seconds_lsb);

        ts->tv_nsec = nanosec;
        ts->tv_sec = seconds;

        return 0;
}

static int dfl_tod_set_time(struct ptp_clock_info *ptp,
                            const struct timespec64 *ts)
{
        struct dfl_tod *dt = container_of(ptp, struct dfl_tod, ptp_clock_ops);
        u32 seconds_msb = FIELD_GET(SECONDS_MSB, ts->tv_sec);
        u32 seconds_lsb = FIELD_GET(SECONDS_LSB, ts->tv_sec);
        u32 nanosec = FIELD_GET(SECONDS_LSB, ts->tv_nsec);
        void __iomem *base = dt->tod_ctrl;
        unsigned long flags;

        spin_lock_irqsave(&dt->tod_lock, flags);
        writel(seconds_msb, base + TOD_SECONDSH);
        writel(seconds_lsb, base + TOD_SECONDSL);
        writel(nanosec, base + TOD_NANOSEC);
        spin_unlock_irqrestore(&dt->tod_lock, flags);

        return 0;
}

static struct ptp_clock_info dfl_tod_clock_ops = {
        .owner = THIS_MODULE,
        .name = "dfl_tod",
        .max_adj = TOD_MAX_ADJ,
        .adjfine = dfl_tod_adjust_fine,
        .adjtime = dfl_tod_adjust_time,
        .gettimex64 = dfl_tod_get_timex,
        .settime64 = dfl_tod_set_time,
};

static int dfl_tod_probe(struct dfl_device *ddev)
{
        struct device *dev = &ddev->dev;
        struct dfl_tod *dt;

        dt = devm_kzalloc(dev, sizeof(*dt), GFP_KERNEL);
        if (!dt)
                return -ENOMEM;

        dt->tod_ctrl = devm_ioremap_resource(dev, &ddev->mmio_res);
        if (IS_ERR(dt->tod_ctrl))
                return PTR_ERR(dt->tod_ctrl);

        dt->dev = dev;
        spin_lock_init(&dt->tod_lock);
        dev_set_drvdata(dev, dt);

        dt->ptp_clock_ops = dfl_tod_clock_ops;

        dt->ptp_clock = ptp_clock_register(&dt->ptp_clock_ops, dev);
        if (IS_ERR(dt->ptp_clock))
                return dev_err_probe(dt->dev, PTR_ERR(dt->ptp_clock),
                                     "Unable to register PTP clock\n");

        return 0;
}

static void dfl_tod_remove(struct dfl_device *ddev)
{
        struct dfl_tod *dt = dev_get_drvdata(&ddev->dev);

        ptp_clock_unregister(dt->ptp_clock);
}

static const struct dfl_device_id dfl_tod_ids[] = {
        { FME_ID, FME_FEATURE_ID_TOD },
        { }
};
MODULE_DEVICE_TABLE(dfl, dfl_tod_ids);

static struct dfl_driver dfl_tod_driver = {
        .drv = {
                .name = "dfl-tod",
        },
        .id_table = dfl_tod_ids,
        .probe = dfl_tod_probe,
        .remove = dfl_tod_remove,
};
module_dfl_driver(dfl_tod_driver);

MODULE_DESCRIPTION("FPGA DFL ToD driver");
MODULE_AUTHOR("Intel Corporation");
MODULE_LICENSE("GPL");