// 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");