// SPDX-License-Identifier: GPL-2.0-only /* * Intel PPS signal Generator Driver * * Copyright (C) 2024 Intel Corporation */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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) { struct system_time_snapshot snap; ktime_get_snapshot(&snap); *time = ktime_to_timespec64(snap.real); 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 "); MODULE_AUTHOR("Lakshmi Sowjanya D "); MODULE_AUTHOR("Pandith N "); MODULE_AUTHOR("Thejesh Reddy T R "); MODULE_AUTHOR("Subramanian Mohan "); MODULE_DESCRIPTION("Intel PMC Time-Aware IO Generator Driver"); MODULE_LICENSE("GPL");