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