root/drivers/pps/generators/pps_gen_tio.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Intel PPS signal Generator Driver
 *
 * Copyright (C) 2024 Intel Corporation
 */

#include <linux/bitfield.h>
#include <linux/bits.h>
#include <linux/cleanup.h>
#include <linux/container_of.h>
#include <linux/device.h>
#include <linux/hrtimer.h>
#include <linux/io-64-nonatomic-hi-lo.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pps_gen_kernel.h>
#include <linux/timekeeping.h>
#include <linux/types.h>

#include <asm/cpu_device_id.h>

#define TIOCTL                  0x00
#define TIOCOMPV                0x10
#define TIOEC                   0x30

/* Control Register */
#define TIOCTL_EN                       BIT(0)
#define TIOCTL_DIR                      BIT(1)
#define TIOCTL_EP                       GENMASK(3, 2)
#define TIOCTL_EP_RISING_EDGE           FIELD_PREP(TIOCTL_EP, 0)
#define TIOCTL_EP_FALLING_EDGE          FIELD_PREP(TIOCTL_EP, 1)
#define TIOCTL_EP_TOGGLE_EDGE           FIELD_PREP(TIOCTL_EP, 2)

/* Safety time to set hrtimer early */
#define SAFE_TIME_NS                    (10 * NSEC_PER_MSEC)

#define MAGIC_CONST                     (NSEC_PER_SEC - SAFE_TIME_NS)
#define ART_HW_DELAY_CYCLES             2

struct pps_tio {
        struct pps_gen_source_info gen_info;
        struct pps_gen_device *pps_gen;
        struct hrtimer timer;
        void __iomem *base;
        u32 prev_count;
        spinlock_t lock;
        struct device *dev;
};

static inline u32 pps_tio_read(u32 offset, struct pps_tio *tio)
{
        return readl(tio->base + offset);
}

static inline void pps_ctl_write(u32 value, struct pps_tio *tio)
{
        writel(value, tio->base + TIOCTL);
}

/*
 * For COMPV register, It's safer to write
 * higher 32-bit followed by lower 32-bit
 */
static inline void pps_compv_write(u64 value, struct pps_tio *tio)
{
        hi_lo_writeq(value, tio->base + TIOCOMPV);
}

static inline ktime_t first_event(struct pps_tio *tio)
{
        return ktime_set(ktime_get_real_seconds() + 1, MAGIC_CONST);
}

static u32 pps_tio_disable(struct pps_tio *tio)
{
        u32 ctrl;

        ctrl = pps_tio_read(TIOCTL, tio);
        pps_compv_write(0, tio);

        ctrl &= ~TIOCTL_EN;
        pps_ctl_write(ctrl, tio);
        tio->pps_gen->enabled = false;
        tio->prev_count = 0;
        return ctrl;
}

static void pps_tio_enable(struct pps_tio *tio)
{
        u32 ctrl;

        ctrl = pps_tio_read(TIOCTL, tio);
        ctrl |= TIOCTL_EN;
        pps_ctl_write(ctrl, tio);
        tio->pps_gen->enabled = true;
}

static void pps_tio_direction_output(struct pps_tio *tio)
{
        u32 ctrl;

        ctrl = pps_tio_disable(tio);

        /*
         * We enable the device, be sure that the
         * 'compare' value is invalid
         */
        pps_compv_write(0, tio);

        ctrl &= ~(TIOCTL_DIR | TIOCTL_EP);
        ctrl |= TIOCTL_EP_TOGGLE_EDGE;
        pps_ctl_write(ctrl, tio);
        pps_tio_enable(tio);
}

static bool pps_generate_next_pulse(ktime_t expires, struct pps_tio *tio)
{
        u64 art;

        if (!ktime_real_to_base_clock(expires, CSID_X86_ART, &art)) {
                pps_tio_disable(tio);
                return false;
        }

        pps_compv_write(art - ART_HW_DELAY_CYCLES, tio);
        return true;
}

static enum hrtimer_restart hrtimer_callback(struct hrtimer *timer)
{
        ktime_t expires, now;
        u32 event_count;
        struct pps_tio *tio = container_of(timer, struct pps_tio, timer);

        guard(spinlock)(&tio->lock);

        /*
         * Check if any event is missed.
         * If an event is missed, TIO will be disabled.
         */
        event_count = pps_tio_read(TIOEC, tio);
        if (tio->prev_count && tio->prev_count == event_count)
                goto err;
        tio->prev_count = event_count;

        expires = hrtimer_get_expires(timer);

        now = ktime_get_real();
        if (now - expires >= SAFE_TIME_NS)
                goto err;

        tio->pps_gen->enabled = pps_generate_next_pulse(expires + SAFE_TIME_NS, tio);
        if (!tio->pps_gen->enabled)
                return HRTIMER_NORESTART;

        hrtimer_forward(timer, now, NSEC_PER_SEC / 2);
        return HRTIMER_RESTART;

err:
        dev_err(tio->dev, "Event missed, Disabling Timed I/O");
        pps_tio_disable(tio);
        pps_gen_event(tio->pps_gen, PPS_GEN_EVENT_MISSEDPULSE, NULL);
        return HRTIMER_NORESTART;
}

static int pps_tio_gen_enable(struct pps_gen_device *pps_gen, bool enable)
{
        struct pps_tio *tio = container_of(pps_gen->info, struct pps_tio, gen_info);

        if (!timekeeping_clocksource_has_base(CSID_X86_ART)) {
                dev_err_once(tio->dev, "PPS cannot be used as clock is not related to ART");
                return -ENODEV;
        }

        guard(spinlock_irqsave)(&tio->lock);
        if (enable && !pps_gen->enabled) {
                pps_tio_direction_output(tio);
                hrtimer_start(&tio->timer, first_event(tio), HRTIMER_MODE_ABS);
        } else if (!enable && pps_gen->enabled) {
                hrtimer_cancel(&tio->timer);
                pps_tio_disable(tio);
        }

        return 0;
}

static int pps_tio_get_time(struct pps_gen_device *pps_gen,
                            struct timespec64 *time)
{
        ktime_get_real_ts64(time);
        return 0;
}

static int pps_gen_tio_probe(struct platform_device *pdev)
{
        struct device *dev = &pdev->dev;
        struct pps_tio *tio;

        if (!(cpu_feature_enabled(X86_FEATURE_TSC_KNOWN_FREQ) &&
              cpu_feature_enabled(X86_FEATURE_ART))) {
                dev_warn(dev, "TSC/ART is not enabled");
                return -ENODEV;
        }

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

        tio->gen_info.use_system_clock = true;
        tio->gen_info.enable = pps_tio_gen_enable;
        tio->gen_info.get_time = pps_tio_get_time;
        tio->gen_info.owner = THIS_MODULE;

        tio->pps_gen = pps_gen_register_source(&tio->gen_info);
        if (IS_ERR(tio->pps_gen))
                return PTR_ERR(tio->pps_gen);

        tio->dev = dev;
        tio->base = devm_platform_ioremap_resource(pdev, 0);
        if (IS_ERR(tio->base))
                return PTR_ERR(tio->base);

        pps_tio_disable(tio);
        hrtimer_setup(&tio->timer, hrtimer_callback, CLOCK_REALTIME,
                      HRTIMER_MODE_ABS);
        spin_lock_init(&tio->lock);
        platform_set_drvdata(pdev, tio);

        return 0;
}

static void pps_gen_tio_remove(struct platform_device *pdev)
{
        struct pps_tio *tio = platform_get_drvdata(pdev);

        hrtimer_cancel(&tio->timer);
        pps_tio_disable(tio);
        pps_gen_unregister_source(tio->pps_gen);
}

static const struct acpi_device_id intel_pmc_tio_acpi_match[] = {
        { "INTC1021" },
        { "INTC1022" },
        { "INTC1023" },
        { "INTC1024" },
        {}
};
MODULE_DEVICE_TABLE(acpi, intel_pmc_tio_acpi_match);

static struct platform_driver pps_gen_tio_driver = {
        .probe          = pps_gen_tio_probe,
        .remove         = pps_gen_tio_remove,
        .driver         = {
                .name                   = "intel-pps-gen-tio",
                .acpi_match_table       = intel_pmc_tio_acpi_match,
        },
};
module_platform_driver(pps_gen_tio_driver);

MODULE_AUTHOR("Christopher Hall <christopher.s.hall@intel.com>");
MODULE_AUTHOR("Lakshmi Sowjanya D <lakshmi.sowjanya.d@intel.com>");
MODULE_AUTHOR("Pandith N <pandith.n@intel.com>");
MODULE_AUTHOR("Thejesh Reddy T R <thejesh.reddy.t.r@intel.com>");
MODULE_AUTHOR("Subramanian Mohan <subramanian.mohan@intel.com>");
MODULE_DESCRIPTION("Intel PMC Time-Aware IO Generator Driver");
MODULE_LICENSE("GPL");