diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
commit | 2c3c1048746a4622d8c89a29670120dc8fab93c4 (patch) | |
tree | 848558de17fb3008cdf4d861b01ac7781903ce39 /drivers/pps | |
parent | Initial commit. (diff) | |
download | linux-upstream.tar.xz linux-upstream.zip |
Adding upstream version 6.1.76.upstream/6.1.76upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/pps')
-rw-r--r-- | drivers/pps/Kconfig | 45 | ||||
-rw-r--r-- | drivers/pps/Makefile | 11 | ||||
-rw-r--r-- | drivers/pps/clients/Kconfig | 37 | ||||
-rw-r--r-- | drivers/pps/clients/Makefile | 11 | ||||
-rw-r--r-- | drivers/pps/clients/pps-gpio.c | 255 | ||||
-rw-r--r-- | drivers/pps/clients/pps-ktimer.c | 87 | ||||
-rw-r--r-- | drivers/pps/clients/pps-ldisc.c | 141 | ||||
-rw-r--r-- | drivers/pps/clients/pps_parport.c | 225 | ||||
-rw-r--r-- | drivers/pps/generators/Kconfig | 14 | ||||
-rw-r--r-- | drivers/pps/generators/Makefile | 10 | ||||
-rw-r--r-- | drivers/pps/generators/pps_gen_parport.c | 241 | ||||
-rw-r--r-- | drivers/pps/kapi.c | 223 | ||||
-rw-r--r-- | drivers/pps/kc.c | 109 | ||||
-rw-r--r-- | drivers/pps/kc.h | 33 | ||||
-rw-r--r-- | drivers/pps/pps.c | 489 | ||||
-rw-r--r-- | drivers/pps/sysfs.c | 99 |
16 files changed, 2030 insertions, 0 deletions
diff --git a/drivers/pps/Kconfig b/drivers/pps/Kconfig new file mode 100644 index 000000000..e1651d51c --- /dev/null +++ b/drivers/pps/Kconfig @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# PPS support configuration +# + +menuconfig PPS + tristate "PPS support" + help + PPS (Pulse Per Second) is a special pulse provided by some GPS + antennae. Userland can use it to get a high-precision time + reference. + + Some antennae's PPS signals are connected with the CD (Carrier + Detect) pin of the serial line they use to communicate with the + host. In this case use the SERIAL_LINE client support. + + Some antennae's PPS signals are connected with some special host + inputs so you have to enable the corresponding client support. + + To compile this driver as a module, choose M here: the module + will be called pps_core.ko. + +if PPS + +config PPS_DEBUG + bool "PPS debugging messages" + help + Say Y here if you want the PPS support to produce a bunch of debug + messages to the system log. Select this if you are having a + problem with PPS support and want to see more of what is going on. + +config NTP_PPS + bool "PPS kernel consumer support" + depends on !NO_HZ_COMMON + help + This option adds support for direct in-kernel time + synchronization using an external PPS signal. + + It doesn't work on tickless systems at the moment. + +source "drivers/pps/clients/Kconfig" + +source "drivers/pps/generators/Kconfig" + +endif # PPS diff --git a/drivers/pps/Makefile b/drivers/pps/Makefile new file mode 100644 index 000000000..ceaf65cc1 --- /dev/null +++ b/drivers/pps/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Makefile for the PPS core. +# + +pps_core-y := pps.o kapi.o sysfs.o +pps_core-$(CONFIG_NTP_PPS) += kc.o +obj-$(CONFIG_PPS) := pps_core.o +obj-y += clients/ generators/ + +ccflags-$(CONFIG_PPS_DEBUG) := -DDEBUG diff --git a/drivers/pps/clients/Kconfig b/drivers/pps/clients/Kconfig new file mode 100644 index 000000000..4f3244e17 --- /dev/null +++ b/drivers/pps/clients/Kconfig @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# PPS clients configuration +# + +comment "PPS clients support" + +config PPS_CLIENT_KTIMER + tristate "Kernel timer client (Testing client, use for debug)" + help + If you say yes here you get support for a PPS debugging client + which uses a kernel timer to generate the PPS signal. + + This driver can also be built as a module. If so, the module + will be called pps-ktimer. + +config PPS_CLIENT_LDISC + tristate "PPS line discipline" + depends on TTY + help + If you say yes here you get support for a PPS source connected + with the CD (Carrier Detect) pin of your serial port. + +config PPS_CLIENT_PARPORT + tristate "Parallel port PPS client" + depends on PARPORT + help + If you say yes here you get support for a PPS source connected + with the interrupt pin of your parallel port. + +config PPS_CLIENT_GPIO + tristate "PPS client using GPIO" + help + If you say yes here you get support for a PPS source using + GPIO. To be useful you must also register a platform device + specifying the GPIO pin and other options, usually in your board + setup. diff --git a/drivers/pps/clients/Makefile b/drivers/pps/clients/Makefile new file mode 100644 index 000000000..7a3807e57 --- /dev/null +++ b/drivers/pps/clients/Makefile @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Makefile for PPS clients. +# + +obj-$(CONFIG_PPS_CLIENT_KTIMER) += pps-ktimer.o +obj-$(CONFIG_PPS_CLIENT_LDISC) += pps-ldisc.o +obj-$(CONFIG_PPS_CLIENT_PARPORT) += pps_parport.o +obj-$(CONFIG_PPS_CLIENT_GPIO) += pps-gpio.o + +ccflags-$(CONFIG_PPS_DEBUG) := -DDEBUG diff --git a/drivers/pps/clients/pps-gpio.c b/drivers/pps/clients/pps-gpio.c new file mode 100644 index 000000000..2f4b11b4d --- /dev/null +++ b/drivers/pps/clients/pps-gpio.c @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * pps-gpio.c -- PPS client driver using GPIO + * + * Copyright (C) 2010 Ricardo Martins <rasm@fe.up.pt> + * Copyright (C) 2011 James Nuss <jamesnuss@nanometrics.ca> + */ + +#define PPS_GPIO_NAME "pps-gpio" +#define pr_fmt(fmt) PPS_GPIO_NAME ": " fmt + +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/interrupt.h> +#include <linux/mod_devicetable.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/pps_kernel.h> +#include <linux/gpio/consumer.h> +#include <linux/list.h> +#include <linux/property.h> +#include <linux/timer.h> +#include <linux/jiffies.h> + +/* Info for each registered platform device */ +struct pps_gpio_device_data { + int irq; /* IRQ used as PPS source */ + struct pps_device *pps; /* PPS source device */ + struct pps_source_info info; /* PPS source information */ + struct gpio_desc *gpio_pin; /* GPIO port descriptors */ + struct gpio_desc *echo_pin; + struct timer_list echo_timer; /* timer to reset echo active state */ + bool assert_falling_edge; + bool capture_clear; + unsigned int echo_active_ms; /* PPS echo active duration */ + unsigned long echo_timeout; /* timer timeout value in jiffies */ +}; + +/* + * Report the PPS event + */ + +static irqreturn_t pps_gpio_irq_handler(int irq, void *data) +{ + const struct pps_gpio_device_data *info; + struct pps_event_time ts; + int rising_edge; + + /* Get the time stamp first */ + pps_get_ts(&ts); + + info = data; + + rising_edge = gpiod_get_value(info->gpio_pin); + if ((rising_edge && !info->assert_falling_edge) || + (!rising_edge && info->assert_falling_edge)) + pps_event(info->pps, &ts, PPS_CAPTUREASSERT, data); + else if (info->capture_clear && + ((rising_edge && info->assert_falling_edge) || + (!rising_edge && !info->assert_falling_edge))) + pps_event(info->pps, &ts, PPS_CAPTURECLEAR, data); + + return IRQ_HANDLED; +} + +/* This function will only be called when an ECHO GPIO is defined */ +static void pps_gpio_echo(struct pps_device *pps, int event, void *data) +{ + /* add_timer() needs to write into info->echo_timer */ + struct pps_gpio_device_data *info = data; + + switch (event) { + case PPS_CAPTUREASSERT: + if (pps->params.mode & PPS_ECHOASSERT) + gpiod_set_value(info->echo_pin, 1); + break; + + case PPS_CAPTURECLEAR: + if (pps->params.mode & PPS_ECHOCLEAR) + gpiod_set_value(info->echo_pin, 1); + break; + } + + /* fire the timer */ + if (info->pps->params.mode & (PPS_ECHOASSERT | PPS_ECHOCLEAR)) { + info->echo_timer.expires = jiffies + info->echo_timeout; + add_timer(&info->echo_timer); + } +} + +/* Timer callback to reset the echo pin to the inactive state */ +static void pps_gpio_echo_timer_callback(struct timer_list *t) +{ + const struct pps_gpio_device_data *info; + + info = from_timer(info, t, echo_timer); + + gpiod_set_value(info->echo_pin, 0); +} + +static int pps_gpio_setup(struct device *dev) +{ + struct pps_gpio_device_data *data = dev_get_drvdata(dev); + int ret; + u32 value; + + data->gpio_pin = devm_gpiod_get(dev, NULL, GPIOD_IN); + if (IS_ERR(data->gpio_pin)) + return dev_err_probe(dev, PTR_ERR(data->gpio_pin), + "failed to request PPS GPIO\n"); + + data->assert_falling_edge = + device_property_read_bool(dev, "assert-falling-edge"); + + data->echo_pin = devm_gpiod_get_optional(dev, "echo", GPIOD_OUT_LOW); + if (IS_ERR(data->echo_pin)) + return dev_err_probe(dev, PTR_ERR(data->echo_pin), + "failed to request ECHO GPIO\n"); + + if (!data->echo_pin) + return 0; + + ret = device_property_read_u32(dev, "echo-active-ms", &value); + if (ret) { + dev_err(dev, "failed to get echo-active-ms from FW\n"); + return ret; + } + + /* sanity check on echo_active_ms */ + if (!value || value > 999) { + dev_err(dev, "echo-active-ms: %u - bad value from FW\n", value); + return -EINVAL; + } + + data->echo_active_ms = value; + + return 0; +} + +static unsigned long +get_irqf_trigger_flags(const struct pps_gpio_device_data *data) +{ + unsigned long flags = data->assert_falling_edge ? + IRQF_TRIGGER_FALLING : IRQF_TRIGGER_RISING; + + if (data->capture_clear) { + flags |= ((flags & IRQF_TRIGGER_RISING) ? + IRQF_TRIGGER_FALLING : IRQF_TRIGGER_RISING); + } + + return flags; +} + +static int pps_gpio_probe(struct platform_device *pdev) +{ + struct pps_gpio_device_data *data; + struct device *dev = &pdev->dev; + int ret; + int pps_default_params; + + /* allocate space for device info */ + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + dev_set_drvdata(dev, data); + + /* GPIO setup */ + ret = pps_gpio_setup(dev); + if (ret) + return ret; + + /* IRQ setup */ + ret = gpiod_to_irq(data->gpio_pin); + if (ret < 0) { + dev_err(dev, "failed to map GPIO to IRQ: %d\n", ret); + return -EINVAL; + } + data->irq = ret; + + /* initialize PPS specific parts of the bookkeeping data structure. */ + data->info.mode = PPS_CAPTUREASSERT | PPS_OFFSETASSERT | + PPS_ECHOASSERT | PPS_CANWAIT | PPS_TSFMT_TSPEC; + if (data->capture_clear) + data->info.mode |= PPS_CAPTURECLEAR | PPS_OFFSETCLEAR | + PPS_ECHOCLEAR; + data->info.owner = THIS_MODULE; + snprintf(data->info.name, PPS_MAX_NAME_LEN - 1, "%s.%d", + pdev->name, pdev->id); + if (data->echo_pin) { + data->info.echo = pps_gpio_echo; + data->echo_timeout = msecs_to_jiffies(data->echo_active_ms); + timer_setup(&data->echo_timer, pps_gpio_echo_timer_callback, 0); + } + + /* register PPS source */ + pps_default_params = PPS_CAPTUREASSERT | PPS_OFFSETASSERT; + if (data->capture_clear) + pps_default_params |= PPS_CAPTURECLEAR | PPS_OFFSETCLEAR; + data->pps = pps_register_source(&data->info, pps_default_params); + if (IS_ERR(data->pps)) { + dev_err(dev, "failed to register IRQ %d as PPS source\n", + data->irq); + return PTR_ERR(data->pps); + } + + /* register IRQ interrupt handler */ + ret = devm_request_irq(dev, data->irq, pps_gpio_irq_handler, + get_irqf_trigger_flags(data), data->info.name, data); + if (ret) { + pps_unregister_source(data->pps); + dev_err(dev, "failed to acquire IRQ %d\n", data->irq); + return -EINVAL; + } + + dev_info(data->pps->dev, "Registered IRQ %d as PPS source\n", + data->irq); + + return 0; +} + +static int pps_gpio_remove(struct platform_device *pdev) +{ + struct pps_gpio_device_data *data = platform_get_drvdata(pdev); + + pps_unregister_source(data->pps); + del_timer_sync(&data->echo_timer); + /* reset echo pin in any case */ + gpiod_set_value(data->echo_pin, 0); + dev_info(&pdev->dev, "removed IRQ %d as PPS source\n", data->irq); + return 0; +} + +static const struct of_device_id pps_gpio_dt_ids[] = { + { .compatible = "pps-gpio", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, pps_gpio_dt_ids); + +static struct platform_driver pps_gpio_driver = { + .probe = pps_gpio_probe, + .remove = pps_gpio_remove, + .driver = { + .name = PPS_GPIO_NAME, + .of_match_table = pps_gpio_dt_ids, + }, +}; + +module_platform_driver(pps_gpio_driver); +MODULE_AUTHOR("Ricardo Martins <rasm@fe.up.pt>"); +MODULE_AUTHOR("James Nuss <jamesnuss@nanometrics.ca>"); +MODULE_DESCRIPTION("Use GPIO pin as PPS source"); +MODULE_LICENSE("GPL"); +MODULE_VERSION("1.2.0"); diff --git a/drivers/pps/clients/pps-ktimer.c b/drivers/pps/clients/pps-ktimer.c new file mode 100644 index 000000000..d33106bd7 --- /dev/null +++ b/drivers/pps/clients/pps-ktimer.c @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * pps-ktimer.c -- kernel timer test client + * + * Copyright (C) 2005-2006 Rodolfo Giometti <giometti@linux.it> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/time.h> +#include <linux/timer.h> +#include <linux/pps_kernel.h> + +/* + * Global variables + */ + +static struct pps_device *pps; +static struct timer_list ktimer; + +/* + * The kernel timer + */ + +static void pps_ktimer_event(struct timer_list *unused) +{ + struct pps_event_time ts; + + /* First of all we get the time stamp... */ + pps_get_ts(&ts); + + pps_event(pps, &ts, PPS_CAPTUREASSERT, NULL); + + mod_timer(&ktimer, jiffies + HZ); +} + +/* + * The PPS info struct + */ + +static struct pps_source_info pps_ktimer_info = { + .name = "ktimer", + .path = "", + .mode = PPS_CAPTUREASSERT | PPS_OFFSETASSERT | + PPS_ECHOASSERT | + PPS_CANWAIT | PPS_TSFMT_TSPEC, + .owner = THIS_MODULE, +}; + +/* + * Module staff + */ + +static void __exit pps_ktimer_exit(void) +{ + dev_info(pps->dev, "ktimer PPS source unregistered\n"); + + del_timer_sync(&ktimer); + pps_unregister_source(pps); +} + +static int __init pps_ktimer_init(void) +{ + pps = pps_register_source(&pps_ktimer_info, + PPS_CAPTUREASSERT | PPS_OFFSETASSERT); + if (IS_ERR(pps)) { + pr_err("cannot register PPS source\n"); + return PTR_ERR(pps); + } + + timer_setup(&ktimer, pps_ktimer_event, 0); + mod_timer(&ktimer, jiffies + HZ); + + dev_info(pps->dev, "ktimer PPS source registered\n"); + + return 0; +} + +module_init(pps_ktimer_init); +module_exit(pps_ktimer_exit); + +MODULE_AUTHOR("Rodolfo Giometti <giometti@linux.it>"); +MODULE_DESCRIPTION("dummy PPS source by using a kernel timer (just for debug)"); +MODULE_LICENSE("GPL"); diff --git a/drivers/pps/clients/pps-ldisc.c b/drivers/pps/clients/pps-ldisc.c new file mode 100644 index 000000000..d73c4c2ed --- /dev/null +++ b/drivers/pps/clients/pps-ldisc.c @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * pps-ldisc.c -- PPS line discipline + * + * Copyright (C) 2008 Rodolfo Giometti <giometti@linux.it> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/serial_core.h> +#include <linux/tty.h> +#include <linux/pps_kernel.h> +#include <linux/bug.h> + +static void pps_tty_dcd_change(struct tty_struct *tty, unsigned int status) +{ + struct pps_device *pps; + struct pps_event_time ts; + + pps_get_ts(&ts); + + pps = pps_lookup_dev(tty); + /* + * This should never fail, but the ldisc locking is very + * convoluted, so don't crash just in case. + */ + if (WARN_ON_ONCE(pps == NULL)) + return; + + /* Now do the PPS event report */ + pps_event(pps, &ts, status ? PPS_CAPTUREASSERT : + PPS_CAPTURECLEAR, NULL); + + dev_dbg(pps->dev, "PPS %s at %lu\n", + status ? "assert" : "clear", jiffies); +} + +static int (*alias_n_tty_open)(struct tty_struct *tty); + +static int pps_tty_open(struct tty_struct *tty) +{ + struct pps_source_info info; + struct tty_driver *drv = tty->driver; + int index = tty->index + drv->name_base; + struct pps_device *pps; + int ret; + + info.owner = THIS_MODULE; + info.dev = NULL; + snprintf(info.name, PPS_MAX_NAME_LEN, "%s%d", drv->driver_name, index); + snprintf(info.path, PPS_MAX_NAME_LEN, "/dev/%s%d", drv->name, index); + info.mode = PPS_CAPTUREBOTH | \ + PPS_OFFSETASSERT | PPS_OFFSETCLEAR | \ + PPS_CANWAIT | PPS_TSFMT_TSPEC; + + pps = pps_register_source(&info, PPS_CAPTUREBOTH | \ + PPS_OFFSETASSERT | PPS_OFFSETCLEAR); + if (IS_ERR(pps)) { + pr_err("cannot register PPS source \"%s\"\n", info.path); + return PTR_ERR(pps); + } + pps->lookup_cookie = tty; + + /* Now open the base class N_TTY ldisc */ + ret = alias_n_tty_open(tty); + if (ret < 0) { + pr_err("cannot open tty ldisc \"%s\"\n", info.path); + goto err_unregister; + } + + dev_info(pps->dev, "source \"%s\" added\n", info.path); + + return 0; + +err_unregister: + pps_unregister_source(pps); + return ret; +} + +static void (*alias_n_tty_close)(struct tty_struct *tty); + +static void pps_tty_close(struct tty_struct *tty) +{ + struct pps_device *pps = pps_lookup_dev(tty); + + alias_n_tty_close(tty); + + if (WARN_ON(!pps)) + return; + + dev_info(pps->dev, "removed\n"); + pps_unregister_source(pps); +} + +static struct tty_ldisc_ops pps_ldisc_ops; + +/* + * Module stuff + */ + +static int __init pps_tty_init(void) +{ + int err; + + /* Inherit the N_TTY's ops */ + n_tty_inherit_ops(&pps_ldisc_ops); + + /* Save N_TTY's open()/close() methods */ + alias_n_tty_open = pps_ldisc_ops.open; + alias_n_tty_close = pps_ldisc_ops.close; + + /* Init PPS_TTY data */ + pps_ldisc_ops.owner = THIS_MODULE; + pps_ldisc_ops.num = N_PPS; + pps_ldisc_ops.name = "pps_tty"; + pps_ldisc_ops.dcd_change = pps_tty_dcd_change; + pps_ldisc_ops.open = pps_tty_open; + pps_ldisc_ops.close = pps_tty_close; + + err = tty_register_ldisc(&pps_ldisc_ops); + if (err) + pr_err("can't register PPS line discipline\n"); + else + pr_info("PPS line discipline registered\n"); + + return err; +} + +static void __exit pps_tty_cleanup(void) +{ + tty_unregister_ldisc(&pps_ldisc_ops); +} + +module_init(pps_tty_init); +module_exit(pps_tty_cleanup); + +MODULE_ALIAS_LDISC(N_PPS); +MODULE_AUTHOR("Rodolfo Giometti <giometti@linux.it>"); +MODULE_DESCRIPTION("PPS TTY device driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/pps/clients/pps_parport.c b/drivers/pps/clients/pps_parport.c new file mode 100644 index 000000000..42f93d4c6 --- /dev/null +++ b/drivers/pps/clients/pps_parport.c @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * pps_parport.c -- kernel parallel port PPS client + * + * Copyright (C) 2009 Alexander Gordeev <lasaine@lvk.cs.msu.su> + */ + + +/* + * TODO: + * implement echo over SEL pin + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/irqnr.h> +#include <linux/time.h> +#include <linux/slab.h> +#include <linux/parport.h> +#include <linux/pps_kernel.h> + +/* module parameters */ + +#define CLEAR_WAIT_MAX 100 +#define CLEAR_WAIT_MAX_ERRORS 5 + +static unsigned int clear_wait = 100; +MODULE_PARM_DESC(clear_wait, + "Maximum number of port reads when polling for signal clear," + " zero turns clear edge capture off entirely"); +module_param(clear_wait, uint, 0); + +static DEFINE_IDA(pps_client_index); + +/* internal per port structure */ +struct pps_client_pp { + struct pardevice *pardev; /* parport device */ + struct pps_device *pps; /* PPS device */ + unsigned int cw; /* port clear timeout */ + unsigned int cw_err; /* number of timeouts */ + int index; /* device number */ +}; + +static inline int signal_is_set(struct parport *port) +{ + return (port->ops->read_status(port) & PARPORT_STATUS_ACK) != 0; +} + +/* parport interrupt handler */ +static void parport_irq(void *handle) +{ + struct pps_event_time ts_assert, ts_clear; + struct pps_client_pp *dev = handle; + struct parport *port = dev->pardev->port; + unsigned int i; + unsigned long flags; + + /* first of all we get the time stamp... */ + pps_get_ts(&ts_assert); + + if (dev->cw == 0) + /* clear edge capture disabled */ + goto out_assert; + + /* try capture the clear edge */ + + /* We have to disable interrupts here. The idea is to prevent + * other interrupts on the same processor to introduce random + * lags while polling the port. Reading from IO port is known + * to take approximately 1us while other interrupt handlers can + * take much more potentially. + * + * Interrupts won't be disabled for a long time because the + * number of polls is limited by clear_wait parameter which is + * kept rather low. So it should never be an issue. + */ + local_irq_save(flags); + /* check the signal (no signal means the pulse is lost this time) */ + if (!signal_is_set(port)) { + local_irq_restore(flags); + dev_err(dev->pps->dev, "lost the signal\n"); + goto out_assert; + } + + /* poll the port until the signal is unset */ + for (i = dev->cw; i; i--) + if (!signal_is_set(port)) { + pps_get_ts(&ts_clear); + local_irq_restore(flags); + dev->cw_err = 0; + goto out_both; + } + local_irq_restore(flags); + + /* timeout */ + dev->cw_err++; + if (dev->cw_err >= CLEAR_WAIT_MAX_ERRORS) { + dev_err(dev->pps->dev, "disabled clear edge capture after %d" + " timeouts\n", dev->cw_err); + dev->cw = 0; + dev->cw_err = 0; + } + +out_assert: + /* fire assert event */ + pps_event(dev->pps, &ts_assert, + PPS_CAPTUREASSERT, NULL); + return; + +out_both: + /* fire assert event */ + pps_event(dev->pps, &ts_assert, + PPS_CAPTUREASSERT, NULL); + /* fire clear event */ + pps_event(dev->pps, &ts_clear, + PPS_CAPTURECLEAR, NULL); + return; +} + +static void parport_attach(struct parport *port) +{ + struct pardev_cb pps_client_cb; + int index; + struct pps_client_pp *device; + struct pps_source_info info = { + .name = KBUILD_MODNAME, + .path = "", + .mode = PPS_CAPTUREBOTH | \ + PPS_OFFSETASSERT | PPS_OFFSETCLEAR | \ + PPS_ECHOASSERT | PPS_ECHOCLEAR | \ + PPS_CANWAIT | PPS_TSFMT_TSPEC, + .owner = THIS_MODULE, + .dev = NULL + }; + + if (clear_wait > CLEAR_WAIT_MAX) { + pr_err("clear_wait value should be not greater then %d\n", + CLEAR_WAIT_MAX); + return; + } + + device = kzalloc(sizeof(struct pps_client_pp), GFP_KERNEL); + if (!device) { + pr_err("memory allocation failed, not attaching\n"); + return; + } + + index = ida_simple_get(&pps_client_index, 0, 0, GFP_KERNEL); + memset(&pps_client_cb, 0, sizeof(pps_client_cb)); + pps_client_cb.private = device; + pps_client_cb.irq_func = parport_irq; + pps_client_cb.flags = PARPORT_FLAG_EXCL; + device->pardev = parport_register_dev_model(port, + KBUILD_MODNAME, + &pps_client_cb, + index); + if (!device->pardev) { + pr_err("couldn't register with %s\n", port->name); + goto err_free; + } + + if (parport_claim_or_block(device->pardev) < 0) { + pr_err("couldn't claim %s\n", port->name); + goto err_unregister_dev; + } + + device->pps = pps_register_source(&info, + PPS_CAPTUREBOTH | PPS_OFFSETASSERT | PPS_OFFSETCLEAR); + if (IS_ERR(device->pps)) { + pr_err("couldn't register PPS source\n"); + goto err_release_dev; + } + + device->cw = clear_wait; + + port->ops->enable_irq(port); + device->index = index; + + pr_info("attached to %s\n", port->name); + + return; + +err_release_dev: + parport_release(device->pardev); +err_unregister_dev: + parport_unregister_device(device->pardev); +err_free: + ida_simple_remove(&pps_client_index, index); + kfree(device); +} + +static void parport_detach(struct parport *port) +{ + struct pardevice *pardev = port->cad; + struct pps_client_pp *device; + + /* FIXME: oooh, this is ugly! */ + if (!pardev || strcmp(pardev->name, KBUILD_MODNAME)) + /* not our port */ + return; + + device = pardev->private; + + port->ops->disable_irq(port); + pps_unregister_source(device->pps); + parport_release(pardev); + parport_unregister_device(pardev); + ida_simple_remove(&pps_client_index, device->index); + kfree(device); +} + +static struct parport_driver pps_parport_driver = { + .name = KBUILD_MODNAME, + .match_port = parport_attach, + .detach = parport_detach, + .devmodel = true, +}; +module_parport_driver(pps_parport_driver); + +MODULE_AUTHOR("Alexander Gordeev <lasaine@lvk.cs.msu.su>"); +MODULE_DESCRIPTION("parallel port PPS client"); +MODULE_LICENSE("GPL"); diff --git a/drivers/pps/generators/Kconfig b/drivers/pps/generators/Kconfig new file mode 100644 index 000000000..d615e640f --- /dev/null +++ b/drivers/pps/generators/Kconfig @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# PPS generators configuration +# + +comment "PPS generators support" + +config PPS_GENERATOR_PARPORT + tristate "Parallel port PPS signal generator" + depends on PARPORT && BROKEN + help + If you say yes here you get support for a PPS signal generator which + utilizes STROBE pin of a parallel port to send PPS signals. It uses + parport abstraction layer and hrtimers to precisely control the signal. diff --git a/drivers/pps/generators/Makefile b/drivers/pps/generators/Makefile new file mode 100644 index 000000000..2d56dd049 --- /dev/null +++ b/drivers/pps/generators/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Makefile for PPS generators. +# + +obj-$(CONFIG_PPS_GENERATOR_PARPORT) += pps_gen_parport.o + +ifeq ($(CONFIG_PPS_DEBUG),y) +EXTRA_CFLAGS += -DDEBUG +endif diff --git a/drivers/pps/generators/pps_gen_parport.c b/drivers/pps/generators/pps_gen_parport.c new file mode 100644 index 000000000..b3e084b75 --- /dev/null +++ b/drivers/pps/generators/pps_gen_parport.c @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * pps_gen_parport.c -- kernel parallel port PPS signal generator + * + * Copyright (C) 2009 Alexander Gordeev <lasaine@lvk.cs.msu.su> + */ + + +/* + * TODO: + * fix issues when realtime clock is adjusted in a leap + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/time.h> +#include <linux/hrtimer.h> +#include <linux/parport.h> + +#define SIGNAL 0 +#define NO_SIGNAL PARPORT_CONTROL_STROBE + +/* module parameters */ + +#define SEND_DELAY_MAX 100000 + +static unsigned int send_delay = 30000; +MODULE_PARM_DESC(delay, + "Delay between setting and dropping the signal (ns)"); +module_param_named(delay, send_delay, uint, 0); + + +#define SAFETY_INTERVAL 3000 /* set the hrtimer earlier for safety (ns) */ + +/* internal per port structure */ +struct pps_generator_pp { + struct pardevice *pardev; /* parport device */ + struct hrtimer timer; + long port_write_time; /* calibrated port write time (ns) */ +}; + +static struct pps_generator_pp device = { + .pardev = NULL, +}; + +static int attached; + +/* calibrated time between a hrtimer event and the reaction */ +static long hrtimer_error = SAFETY_INTERVAL; + +/* the kernel hrtimer event */ +static enum hrtimer_restart hrtimer_event(struct hrtimer *timer) +{ + struct timespec64 expire_time, ts1, ts2, ts3, dts; + struct pps_generator_pp *dev; + struct parport *port; + long lim, delta; + unsigned long flags; + + /* We have to disable interrupts here. The idea is to prevent + * other interrupts on the same processor to introduce random + * lags while polling the clock. ktime_get_real_ts64() takes <1us on + * most machines while other interrupt handlers can take much + * more potentially. + * + * NB: approx time with blocked interrupts = + * send_delay + 3 * SAFETY_INTERVAL + */ + local_irq_save(flags); + + /* first of all we get the time stamp... */ + ktime_get_real_ts64(&ts1); + expire_time = ktime_to_timespec64(hrtimer_get_softexpires(timer)); + dev = container_of(timer, struct pps_generator_pp, timer); + lim = NSEC_PER_SEC - send_delay - dev->port_write_time; + + /* check if we are late */ + if (expire_time.tv_sec != ts1.tv_sec || ts1.tv_nsec > lim) { + local_irq_restore(flags); + pr_err("we are late this time %lld.%09ld\n", + (s64)ts1.tv_sec, ts1.tv_nsec); + goto done; + } + + /* busy loop until the time is right for an assert edge */ + do { + ktime_get_real_ts64(&ts2); + } while (expire_time.tv_sec == ts2.tv_sec && ts2.tv_nsec < lim); + + /* set the signal */ + port = dev->pardev->port; + port->ops->write_control(port, SIGNAL); + + /* busy loop until the time is right for a clear edge */ + lim = NSEC_PER_SEC - dev->port_write_time; + do { + ktime_get_real_ts64(&ts2); + } while (expire_time.tv_sec == ts2.tv_sec && ts2.tv_nsec < lim); + + /* unset the signal */ + port->ops->write_control(port, NO_SIGNAL); + + ktime_get_real_ts64(&ts3); + + local_irq_restore(flags); + + /* update calibrated port write time */ + dts = timespec64_sub(ts3, ts2); + dev->port_write_time = + (dev->port_write_time + timespec64_to_ns(&dts)) >> 1; + +done: + /* update calibrated hrtimer error */ + dts = timespec64_sub(ts1, expire_time); + delta = timespec64_to_ns(&dts); + /* If the new error value is bigger then the old, use the new + * value, if not then slowly move towards the new value. This + * way it should be safe in bad conditions and efficient in + * good conditions. + */ + if (delta >= hrtimer_error) + hrtimer_error = delta; + else + hrtimer_error = (3 * hrtimer_error + delta) >> 2; + + /* update the hrtimer expire time */ + hrtimer_set_expires(timer, + ktime_set(expire_time.tv_sec + 1, + NSEC_PER_SEC - (send_delay + + dev->port_write_time + SAFETY_INTERVAL + + 2 * hrtimer_error))); + + return HRTIMER_RESTART; +} + +/* calibrate port write time */ +#define PORT_NTESTS_SHIFT 5 +static void calibrate_port(struct pps_generator_pp *dev) +{ + struct parport *port = dev->pardev->port; + int i; + long acc = 0; + + for (i = 0; i < (1 << PORT_NTESTS_SHIFT); i++) { + struct timespec64 a, b; + unsigned long irq_flags; + + local_irq_save(irq_flags); + ktime_get_real_ts64(&a); + port->ops->write_control(port, NO_SIGNAL); + ktime_get_real_ts64(&b); + local_irq_restore(irq_flags); + + b = timespec64_sub(b, a); + acc += timespec64_to_ns(&b); + } + + dev->port_write_time = acc >> PORT_NTESTS_SHIFT; + pr_info("port write takes %ldns\n", dev->port_write_time); +} + +static inline ktime_t next_intr_time(struct pps_generator_pp *dev) +{ + struct timespec64 ts; + + ktime_get_real_ts64(&ts); + + return ktime_set(ts.tv_sec + + ((ts.tv_nsec > 990 * NSEC_PER_MSEC) ? 1 : 0), + NSEC_PER_SEC - (send_delay + + dev->port_write_time + 3 * SAFETY_INTERVAL)); +} + +static void parport_attach(struct parport *port) +{ + struct pardev_cb pps_cb; + + if (send_delay > SEND_DELAY_MAX) { + pr_err("delay value should be not greater then %d\n", SEND_DELAY_MAX); + return; + } + + if (attached) { + /* we already have a port */ + return; + } + + memset(&pps_cb, 0, sizeof(pps_cb)); + pps_cb.private = &device; + pps_cb.flags = PARPORT_FLAG_EXCL; + device.pardev = parport_register_dev_model(port, KBUILD_MODNAME, + &pps_cb, 0); + if (!device.pardev) { + pr_err("couldn't register with %s\n", port->name); + return; + } + + if (parport_claim_or_block(device.pardev) < 0) { + pr_err("couldn't claim %s\n", port->name); + goto err_unregister_dev; + } + + pr_info("attached to %s\n", port->name); + attached = 1; + + calibrate_port(&device); + + hrtimer_init(&device.timer, CLOCK_REALTIME, HRTIMER_MODE_ABS); + device.timer.function = hrtimer_event; + hrtimer_start(&device.timer, next_intr_time(&device), HRTIMER_MODE_ABS); + + return; + +err_unregister_dev: + parport_unregister_device(device.pardev); +} + +static void parport_detach(struct parport *port) +{ + if (port->cad != device.pardev) + return; /* not our port */ + + hrtimer_cancel(&device.timer); + parport_release(device.pardev); + parport_unregister_device(device.pardev); +} + +static struct parport_driver pps_gen_parport_driver = { + .name = KBUILD_MODNAME, + .match_port = parport_attach, + .detach = parport_detach, + .devmodel = true, +}; +module_parport_driver(pps_gen_parport_driver); + +MODULE_AUTHOR("Alexander Gordeev <lasaine@lvk.cs.msu.su>"); +MODULE_DESCRIPTION("parallel port PPS signal generator"); +MODULE_LICENSE("GPL"); diff --git a/drivers/pps/kapi.c b/drivers/pps/kapi.c new file mode 100644 index 000000000..d9d566f70 --- /dev/null +++ b/drivers/pps/kapi.c @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * kernel API + * + * Copyright (C) 2005-2009 Rodolfo Giometti <giometti@linux.it> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/sched.h> +#include <linux/time.h> +#include <linux/timex.h> +#include <linux/spinlock.h> +#include <linux/fs.h> +#include <linux/pps_kernel.h> +#include <linux/slab.h> + +#include "kc.h" + +/* + * Local functions + */ + +static void pps_add_offset(struct pps_ktime *ts, struct pps_ktime *offset) +{ + ts->nsec += offset->nsec; + while (ts->nsec >= NSEC_PER_SEC) { + ts->nsec -= NSEC_PER_SEC; + ts->sec++; + } + while (ts->nsec < 0) { + ts->nsec += NSEC_PER_SEC; + ts->sec--; + } + ts->sec += offset->sec; +} + +static void pps_echo_client_default(struct pps_device *pps, int event, + void *data) +{ + dev_info(pps->dev, "echo %s %s\n", + event & PPS_CAPTUREASSERT ? "assert" : "", + event & PPS_CAPTURECLEAR ? "clear" : ""); +} + +/* + * Exported functions + */ + +/* pps_register_source - add a PPS source in the system + * @info: the PPS info struct + * @default_params: the default PPS parameters of the new source + * + * This function is used to add a new PPS source in the system. The new + * source is described by info's fields and it will have, as default PPS + * parameters, the ones specified into default_params. + * + * The function returns, in case of success, the PPS device. Otherwise + * ERR_PTR(errno). + */ + +struct pps_device *pps_register_source(struct pps_source_info *info, + int default_params) +{ + struct pps_device *pps; + int err; + + /* Sanity checks */ + if ((info->mode & default_params) != default_params) { + pr_err("%s: unsupported default parameters\n", + info->name); + err = -EINVAL; + goto pps_register_source_exit; + } + if ((info->mode & (PPS_TSFMT_TSPEC | PPS_TSFMT_NTPFP)) == 0) { + pr_err("%s: unspecified time format\n", + info->name); + err = -EINVAL; + goto pps_register_source_exit; + } + + /* Allocate memory for the new PPS source struct */ + pps = kzalloc(sizeof(struct pps_device), GFP_KERNEL); + if (pps == NULL) { + err = -ENOMEM; + goto pps_register_source_exit; + } + + /* These initializations must be done before calling idr_alloc() + * in order to avoid reces into pps_event(). + */ + pps->params.api_version = PPS_API_VERS; + pps->params.mode = default_params; + pps->info = *info; + + /* check for default echo function */ + if ((pps->info.mode & (PPS_ECHOASSERT | PPS_ECHOCLEAR)) && + pps->info.echo == NULL) + pps->info.echo = pps_echo_client_default; + + init_waitqueue_head(&pps->queue); + spin_lock_init(&pps->lock); + + /* Create the char device */ + err = pps_register_cdev(pps); + if (err < 0) { + pr_err("%s: unable to create char device\n", + info->name); + goto kfree_pps; + } + + dev_info(pps->dev, "new PPS source %s\n", info->name); + + return pps; + +kfree_pps: + kfree(pps); + +pps_register_source_exit: + pr_err("%s: unable to register source\n", info->name); + + return ERR_PTR(err); +} +EXPORT_SYMBOL(pps_register_source); + +/* pps_unregister_source - remove a PPS source from the system + * @pps: the PPS source + * + * This function is used to remove a previously registered PPS source from + * the system. + */ + +void pps_unregister_source(struct pps_device *pps) +{ + pps_kc_remove(pps); + pps_unregister_cdev(pps); + + /* don't have to kfree(pps) here because it will be done on + * device destruction */ +} +EXPORT_SYMBOL(pps_unregister_source); + +/* pps_event - register a PPS event into the system + * @pps: the PPS device + * @ts: the event timestamp + * @event: the event type + * @data: userdef pointer + * + * This function is used by each PPS client in order to register a new + * PPS event into the system (it's usually called inside an IRQ handler). + * + * If an echo function is associated with the PPS device it will be called + * as: + * pps->info.echo(pps, event, data); + */ +void pps_event(struct pps_device *pps, struct pps_event_time *ts, int event, + void *data) +{ + unsigned long flags; + int captured = 0; + struct pps_ktime ts_real = { .sec = 0, .nsec = 0, .flags = 0 }; + + /* check event type */ + BUG_ON((event & (PPS_CAPTUREASSERT | PPS_CAPTURECLEAR)) == 0); + + dev_dbg(pps->dev, "PPS event at %lld.%09ld\n", + (s64)ts->ts_real.tv_sec, ts->ts_real.tv_nsec); + + timespec_to_pps_ktime(&ts_real, ts->ts_real); + + spin_lock_irqsave(&pps->lock, flags); + + /* Must call the echo function? */ + if ((pps->params.mode & (PPS_ECHOASSERT | PPS_ECHOCLEAR))) + pps->info.echo(pps, event, data); + + /* Check the event */ + pps->current_mode = pps->params.mode; + if (event & pps->params.mode & PPS_CAPTUREASSERT) { + /* We have to add an offset? */ + if (pps->params.mode & PPS_OFFSETASSERT) + pps_add_offset(&ts_real, + &pps->params.assert_off_tu); + + /* Save the time stamp */ + pps->assert_tu = ts_real; + pps->assert_sequence++; + dev_dbg(pps->dev, "capture assert seq #%u\n", + pps->assert_sequence); + + captured = ~0; + } + if (event & pps->params.mode & PPS_CAPTURECLEAR) { + /* We have to add an offset? */ + if (pps->params.mode & PPS_OFFSETCLEAR) + pps_add_offset(&ts_real, + &pps->params.clear_off_tu); + + /* Save the time stamp */ + pps->clear_tu = ts_real; + pps->clear_sequence++; + dev_dbg(pps->dev, "capture clear seq #%u\n", + pps->clear_sequence); + + captured = ~0; + } + + pps_kc_event(pps, ts, event); + + /* Wake up if captured something */ + if (captured) { + pps->last_ev++; + wake_up_interruptible_all(&pps->queue); + + kill_fasync(&pps->async_queue, SIGIO, POLL_IN); + } + + spin_unlock_irqrestore(&pps->lock, flags); +} +EXPORT_SYMBOL(pps_event); diff --git a/drivers/pps/kc.c b/drivers/pps/kc.c new file mode 100644 index 000000000..50dc59af4 --- /dev/null +++ b/drivers/pps/kc.c @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PPS kernel consumer API + * + * Copyright (C) 2009-2010 Alexander Gordeev <lasaine@lvk.cs.msu.su> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/init.h> +#include <linux/spinlock.h> +#include <linux/pps_kernel.h> + +#include "kc.h" + +/* + * Global variables + */ + +/* state variables to bind kernel consumer */ +static DEFINE_SPINLOCK(pps_kc_hardpps_lock); +/* PPS API (RFC 2783): current source and mode for kernel consumer */ +static struct pps_device *pps_kc_hardpps_dev; /* unique pointer to device */ +static int pps_kc_hardpps_mode; /* mode bits for kernel consumer */ + +/* pps_kc_bind - control PPS kernel consumer binding + * @pps: the PPS source + * @bind_args: kernel consumer bind parameters + * + * This function is used to bind or unbind PPS kernel consumer according to + * supplied parameters. Should not be called in interrupt context. + */ +int pps_kc_bind(struct pps_device *pps, struct pps_bind_args *bind_args) +{ + /* Check if another consumer is already bound */ + spin_lock_irq(&pps_kc_hardpps_lock); + + if (bind_args->edge == 0) + if (pps_kc_hardpps_dev == pps) { + pps_kc_hardpps_mode = 0; + pps_kc_hardpps_dev = NULL; + spin_unlock_irq(&pps_kc_hardpps_lock); + dev_info(pps->dev, "unbound kernel" + " consumer\n"); + } else { + spin_unlock_irq(&pps_kc_hardpps_lock); + dev_err(pps->dev, "selected kernel consumer" + " is not bound\n"); + return -EINVAL; + } + else + if (pps_kc_hardpps_dev == NULL || + pps_kc_hardpps_dev == pps) { + pps_kc_hardpps_mode = bind_args->edge; + pps_kc_hardpps_dev = pps; + spin_unlock_irq(&pps_kc_hardpps_lock); + dev_info(pps->dev, "bound kernel consumer: " + "edge=0x%x\n", bind_args->edge); + } else { + spin_unlock_irq(&pps_kc_hardpps_lock); + dev_err(pps->dev, "another kernel consumer" + " is already bound\n"); + return -EINVAL; + } + + return 0; +} + +/* pps_kc_remove - unbind kernel consumer on PPS source removal + * @pps: the PPS source + * + * This function is used to disable kernel consumer on PPS source removal + * if this source was bound to PPS kernel consumer. Can be called on any + * source safely. Should not be called in interrupt context. + */ +void pps_kc_remove(struct pps_device *pps) +{ + spin_lock_irq(&pps_kc_hardpps_lock); + if (pps == pps_kc_hardpps_dev) { + pps_kc_hardpps_mode = 0; + pps_kc_hardpps_dev = NULL; + spin_unlock_irq(&pps_kc_hardpps_lock); + dev_info(pps->dev, "unbound kernel consumer" + " on device removal\n"); + } else + spin_unlock_irq(&pps_kc_hardpps_lock); +} + +/* pps_kc_event - call hardpps() on PPS event + * @pps: the PPS source + * @ts: PPS event timestamp + * @event: PPS event edge + * + * This function calls hardpps() when an event from bound PPS source occurs. + */ +void pps_kc_event(struct pps_device *pps, struct pps_event_time *ts, + int event) +{ + unsigned long flags; + + /* Pass some events to kernel consumer if activated */ + spin_lock_irqsave(&pps_kc_hardpps_lock, flags); + if (pps == pps_kc_hardpps_dev && event & pps_kc_hardpps_mode) + hardpps(&ts->ts_real, &ts->ts_raw); + spin_unlock_irqrestore(&pps_kc_hardpps_lock, flags); +} diff --git a/drivers/pps/kc.h b/drivers/pps/kc.h new file mode 100644 index 000000000..247305594 --- /dev/null +++ b/drivers/pps/kc.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * PPS kernel consumer API header + * + * Copyright (C) 2009-2010 Alexander Gordeev <lasaine@lvk.cs.msu.su> + */ + +#ifndef LINUX_PPS_KC_H +#define LINUX_PPS_KC_H + +#include <linux/errno.h> +#include <linux/pps_kernel.h> + +#ifdef CONFIG_NTP_PPS + +extern int pps_kc_bind(struct pps_device *pps, + struct pps_bind_args *bind_args); +extern void pps_kc_remove(struct pps_device *pps); +extern void pps_kc_event(struct pps_device *pps, + struct pps_event_time *ts, int event); + + +#else /* CONFIG_NTP_PPS */ + +static inline int pps_kc_bind(struct pps_device *pps, + struct pps_bind_args *bind_args) { return -EOPNOTSUPP; } +static inline void pps_kc_remove(struct pps_device *pps) {} +static inline void pps_kc_event(struct pps_device *pps, + struct pps_event_time *ts, int event) {} + +#endif /* CONFIG_NTP_PPS */ + +#endif /* LINUX_PPS_KC_H */ diff --git a/drivers/pps/pps.c b/drivers/pps/pps.c new file mode 100644 index 000000000..22a65ad4e --- /dev/null +++ b/drivers/pps/pps.c @@ -0,0 +1,489 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PPS core file + * + * Copyright (C) 2005-2009 Rodolfo Giometti <giometti@linux.it> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/sched.h> +#include <linux/uaccess.h> +#include <linux/idr.h> +#include <linux/mutex.h> +#include <linux/cdev.h> +#include <linux/poll.h> +#include <linux/pps_kernel.h> +#include <linux/slab.h> + +#include "kc.h" + +/* + * Local variables + */ + +static dev_t pps_devt; +static struct class *pps_class; + +static DEFINE_MUTEX(pps_idr_lock); +static DEFINE_IDR(pps_idr); + +/* + * Char device methods + */ + +static __poll_t pps_cdev_poll(struct file *file, poll_table *wait) +{ + struct pps_device *pps = file->private_data; + + poll_wait(file, &pps->queue, wait); + + return EPOLLIN | EPOLLRDNORM; +} + +static int pps_cdev_fasync(int fd, struct file *file, int on) +{ + struct pps_device *pps = file->private_data; + return fasync_helper(fd, file, on, &pps->async_queue); +} + +static int pps_cdev_pps_fetch(struct pps_device *pps, struct pps_fdata *fdata) +{ + unsigned int ev = pps->last_ev; + int err = 0; + + /* Manage the timeout */ + if (fdata->timeout.flags & PPS_TIME_INVALID) + err = wait_event_interruptible(pps->queue, + ev != pps->last_ev); + else { + unsigned long ticks; + + dev_dbg(pps->dev, "timeout %lld.%09d\n", + (long long) fdata->timeout.sec, + fdata->timeout.nsec); + ticks = fdata->timeout.sec * HZ; + ticks += fdata->timeout.nsec / (NSEC_PER_SEC / HZ); + + if (ticks != 0) { + err = wait_event_interruptible_timeout( + pps->queue, + ev != pps->last_ev, + ticks); + if (err == 0) + return -ETIMEDOUT; + } + } + + /* Check for pending signals */ + if (err == -ERESTARTSYS) { + dev_dbg(pps->dev, "pending signal caught\n"); + return -EINTR; + } + + return 0; +} + +static long pps_cdev_ioctl(struct file *file, + unsigned int cmd, unsigned long arg) +{ + struct pps_device *pps = file->private_data; + struct pps_kparams params; + void __user *uarg = (void __user *) arg; + int __user *iuarg = (int __user *) arg; + int err; + + switch (cmd) { + case PPS_GETPARAMS: + dev_dbg(pps->dev, "PPS_GETPARAMS\n"); + + spin_lock_irq(&pps->lock); + + /* Get the current parameters */ + params = pps->params; + + spin_unlock_irq(&pps->lock); + + err = copy_to_user(uarg, ¶ms, sizeof(struct pps_kparams)); + if (err) + return -EFAULT; + + break; + + case PPS_SETPARAMS: + dev_dbg(pps->dev, "PPS_SETPARAMS\n"); + + /* Check the capabilities */ + if (!capable(CAP_SYS_TIME)) + return -EPERM; + + err = copy_from_user(¶ms, uarg, sizeof(struct pps_kparams)); + if (err) + return -EFAULT; + if (!(params.mode & (PPS_CAPTUREASSERT | PPS_CAPTURECLEAR))) { + dev_dbg(pps->dev, "capture mode unspecified (%x)\n", + params.mode); + return -EINVAL; + } + + /* Check for supported capabilities */ + if ((params.mode & ~pps->info.mode) != 0) { + dev_dbg(pps->dev, "unsupported capabilities (%x)\n", + params.mode); + return -EINVAL; + } + + spin_lock_irq(&pps->lock); + + /* Save the new parameters */ + pps->params = params; + + /* Restore the read only parameters */ + if ((params.mode & (PPS_TSFMT_TSPEC | PPS_TSFMT_NTPFP)) == 0) { + /* section 3.3 of RFC 2783 interpreted */ + dev_dbg(pps->dev, "time format unspecified (%x)\n", + params.mode); + pps->params.mode |= PPS_TSFMT_TSPEC; + } + if (pps->info.mode & PPS_CANWAIT) + pps->params.mode |= PPS_CANWAIT; + pps->params.api_version = PPS_API_VERS; + + /* + * Clear unused fields of pps_kparams to avoid leaking + * uninitialized data of the PPS_SETPARAMS caller via + * PPS_GETPARAMS + */ + pps->params.assert_off_tu.flags = 0; + pps->params.clear_off_tu.flags = 0; + + spin_unlock_irq(&pps->lock); + + break; + + case PPS_GETCAP: + dev_dbg(pps->dev, "PPS_GETCAP\n"); + + err = put_user(pps->info.mode, iuarg); + if (err) + return -EFAULT; + + break; + + case PPS_FETCH: { + struct pps_fdata fdata; + + dev_dbg(pps->dev, "PPS_FETCH\n"); + + err = copy_from_user(&fdata, uarg, sizeof(struct pps_fdata)); + if (err) + return -EFAULT; + + err = pps_cdev_pps_fetch(pps, &fdata); + if (err) + return err; + + /* Return the fetched timestamp */ + spin_lock_irq(&pps->lock); + + fdata.info.assert_sequence = pps->assert_sequence; + fdata.info.clear_sequence = pps->clear_sequence; + fdata.info.assert_tu = pps->assert_tu; + fdata.info.clear_tu = pps->clear_tu; + fdata.info.current_mode = pps->current_mode; + + spin_unlock_irq(&pps->lock); + + err = copy_to_user(uarg, &fdata, sizeof(struct pps_fdata)); + if (err) + return -EFAULT; + + break; + } + case PPS_KC_BIND: { + struct pps_bind_args bind_args; + + dev_dbg(pps->dev, "PPS_KC_BIND\n"); + + /* Check the capabilities */ + if (!capable(CAP_SYS_TIME)) + return -EPERM; + + if (copy_from_user(&bind_args, uarg, + sizeof(struct pps_bind_args))) + return -EFAULT; + + /* Check for supported capabilities */ + if ((bind_args.edge & ~pps->info.mode) != 0) { + dev_err(pps->dev, "unsupported capabilities (%x)\n", + bind_args.edge); + return -EINVAL; + } + + /* Validate parameters roughly */ + if (bind_args.tsformat != PPS_TSFMT_TSPEC || + (bind_args.edge & ~PPS_CAPTUREBOTH) != 0 || + bind_args.consumer != PPS_KC_HARDPPS) { + dev_err(pps->dev, "invalid kernel consumer bind" + " parameters (%x)\n", bind_args.edge); + return -EINVAL; + } + + err = pps_kc_bind(pps, &bind_args); + if (err < 0) + return err; + + break; + } + default: + return -ENOTTY; + } + + return 0; +} + +#ifdef CONFIG_COMPAT +static long pps_cdev_compat_ioctl(struct file *file, + unsigned int cmd, unsigned long arg) +{ + struct pps_device *pps = file->private_data; + void __user *uarg = (void __user *) arg; + + cmd = _IOC(_IOC_DIR(cmd), _IOC_TYPE(cmd), _IOC_NR(cmd), sizeof(void *)); + + if (cmd == PPS_FETCH) { + struct pps_fdata_compat compat; + struct pps_fdata fdata; + int err; + + dev_dbg(pps->dev, "PPS_FETCH\n"); + + err = copy_from_user(&compat, uarg, sizeof(struct pps_fdata_compat)); + if (err) + return -EFAULT; + + memcpy(&fdata.timeout, &compat.timeout, + sizeof(struct pps_ktime_compat)); + + err = pps_cdev_pps_fetch(pps, &fdata); + if (err) + return err; + + /* Return the fetched timestamp */ + spin_lock_irq(&pps->lock); + + compat.info.assert_sequence = pps->assert_sequence; + compat.info.clear_sequence = pps->clear_sequence; + compat.info.current_mode = pps->current_mode; + + memcpy(&compat.info.assert_tu, &pps->assert_tu, + sizeof(struct pps_ktime_compat)); + memcpy(&compat.info.clear_tu, &pps->clear_tu, + sizeof(struct pps_ktime_compat)); + + spin_unlock_irq(&pps->lock); + + return copy_to_user(uarg, &compat, + sizeof(struct pps_fdata_compat)) ? -EFAULT : 0; + } + + return pps_cdev_ioctl(file, cmd, arg); +} +#else +#define pps_cdev_compat_ioctl NULL +#endif + +static int pps_cdev_open(struct inode *inode, struct file *file) +{ + struct pps_device *pps = container_of(inode->i_cdev, + struct pps_device, cdev); + file->private_data = pps; + kobject_get(&pps->dev->kobj); + return 0; +} + +static int pps_cdev_release(struct inode *inode, struct file *file) +{ + struct pps_device *pps = container_of(inode->i_cdev, + struct pps_device, cdev); + kobject_put(&pps->dev->kobj); + return 0; +} + +/* + * Char device stuff + */ + +static const struct file_operations pps_cdev_fops = { + .owner = THIS_MODULE, + .llseek = no_llseek, + .poll = pps_cdev_poll, + .fasync = pps_cdev_fasync, + .compat_ioctl = pps_cdev_compat_ioctl, + .unlocked_ioctl = pps_cdev_ioctl, + .open = pps_cdev_open, + .release = pps_cdev_release, +}; + +static void pps_device_destruct(struct device *dev) +{ + struct pps_device *pps = dev_get_drvdata(dev); + + cdev_del(&pps->cdev); + + /* Now we can release the ID for re-use */ + pr_debug("deallocating pps%d\n", pps->id); + mutex_lock(&pps_idr_lock); + idr_remove(&pps_idr, pps->id); + mutex_unlock(&pps_idr_lock); + + kfree(dev); + kfree(pps); +} + +int pps_register_cdev(struct pps_device *pps) +{ + int err; + dev_t devt; + + mutex_lock(&pps_idr_lock); + /* + * Get new ID for the new PPS source. After idr_alloc() calling + * the new source will be freely available into the kernel. + */ + err = idr_alloc(&pps_idr, pps, 0, PPS_MAX_SOURCES, GFP_KERNEL); + if (err < 0) { + if (err == -ENOSPC) { + pr_err("%s: too many PPS sources in the system\n", + pps->info.name); + err = -EBUSY; + } + goto out_unlock; + } + pps->id = err; + mutex_unlock(&pps_idr_lock); + + devt = MKDEV(MAJOR(pps_devt), pps->id); + + cdev_init(&pps->cdev, &pps_cdev_fops); + pps->cdev.owner = pps->info.owner; + + err = cdev_add(&pps->cdev, devt, 1); + if (err) { + pr_err("%s: failed to add char device %d:%d\n", + pps->info.name, MAJOR(pps_devt), pps->id); + goto free_idr; + } + pps->dev = device_create(pps_class, pps->info.dev, devt, pps, + "pps%d", pps->id); + if (IS_ERR(pps->dev)) { + err = PTR_ERR(pps->dev); + goto del_cdev; + } + + /* Override the release function with our own */ + pps->dev->release = pps_device_destruct; + + pr_debug("source %s got cdev (%d:%d)\n", pps->info.name, + MAJOR(pps_devt), pps->id); + + return 0; + +del_cdev: + cdev_del(&pps->cdev); + +free_idr: + mutex_lock(&pps_idr_lock); + idr_remove(&pps_idr, pps->id); +out_unlock: + mutex_unlock(&pps_idr_lock); + return err; +} + +void pps_unregister_cdev(struct pps_device *pps) +{ + pr_debug("unregistering pps%d\n", pps->id); + pps->lookup_cookie = NULL; + device_destroy(pps_class, pps->dev->devt); +} + +/* + * Look up a pps device by magic cookie. + * The cookie is usually a pointer to some enclosing device, but this + * code doesn't care; you should never be dereferencing it. + * + * This is a bit of a kludge that is currently used only by the PPS + * serial line discipline. It may need to be tweaked when a second user + * is found. + * + * There is no function interface for setting the lookup_cookie field. + * It's initialized to NULL when the pps device is created, and if a + * client wants to use it, just fill it in afterward. + * + * The cookie is automatically set to NULL in pps_unregister_source() + * so that it will not be used again, even if the pps device cannot + * be removed from the idr due to pending references holding the minor + * number in use. + */ +struct pps_device *pps_lookup_dev(void const *cookie) +{ + struct pps_device *pps; + unsigned id; + + rcu_read_lock(); + idr_for_each_entry(&pps_idr, pps, id) + if (cookie == pps->lookup_cookie) + break; + rcu_read_unlock(); + return pps; +} +EXPORT_SYMBOL(pps_lookup_dev); + +/* + * Module stuff + */ + +static void __exit pps_exit(void) +{ + class_destroy(pps_class); + unregister_chrdev_region(pps_devt, PPS_MAX_SOURCES); +} + +static int __init pps_init(void) +{ + int err; + + pps_class = class_create(THIS_MODULE, "pps"); + if (IS_ERR(pps_class)) { + pr_err("failed to allocate class\n"); + return PTR_ERR(pps_class); + } + pps_class->dev_groups = pps_groups; + + err = alloc_chrdev_region(&pps_devt, 0, PPS_MAX_SOURCES, "pps"); + if (err < 0) { + pr_err("failed to allocate char device region\n"); + goto remove_class; + } + + pr_info("LinuxPPS API ver. %d registered\n", PPS_API_VERS); + pr_info("Software ver. %s - Copyright 2005-2007 Rodolfo Giometti " + "<giometti@linux.it>\n", PPS_VERSION); + + return 0; + +remove_class: + class_destroy(pps_class); + + return err; +} + +subsys_initcall(pps_init); +module_exit(pps_exit); + +MODULE_AUTHOR("Rodolfo Giometti <giometti@linux.it>"); +MODULE_DESCRIPTION("LinuxPPS support (RFC 2783) - ver. " PPS_VERSION); +MODULE_LICENSE("GPL"); diff --git a/drivers/pps/sysfs.c b/drivers/pps/sysfs.c new file mode 100644 index 000000000..134bc33f6 --- /dev/null +++ b/drivers/pps/sysfs.c @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PPS sysfs support + * + * Copyright (C) 2007-2009 Rodolfo Giometti <giometti@linux.it> + */ + + +#include <linux/device.h> +#include <linux/module.h> +#include <linux/string.h> +#include <linux/pps_kernel.h> + +/* + * Attribute functions + */ + +static ssize_t assert_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct pps_device *pps = dev_get_drvdata(dev); + + if (!(pps->info.mode & PPS_CAPTUREASSERT)) + return 0; + + return sprintf(buf, "%lld.%09d#%d\n", + (long long) pps->assert_tu.sec, pps->assert_tu.nsec, + pps->assert_sequence); +} +static DEVICE_ATTR_RO(assert); + +static ssize_t clear_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct pps_device *pps = dev_get_drvdata(dev); + + if (!(pps->info.mode & PPS_CAPTURECLEAR)) + return 0; + + return sprintf(buf, "%lld.%09d#%d\n", + (long long) pps->clear_tu.sec, pps->clear_tu.nsec, + pps->clear_sequence); +} +static DEVICE_ATTR_RO(clear); + +static ssize_t mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct pps_device *pps = dev_get_drvdata(dev); + + return sprintf(buf, "%4x\n", pps->info.mode); +} +static DEVICE_ATTR_RO(mode); + +static ssize_t echo_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct pps_device *pps = dev_get_drvdata(dev); + + return sprintf(buf, "%d\n", !!pps->info.echo); +} +static DEVICE_ATTR_RO(echo); + +static ssize_t name_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct pps_device *pps = dev_get_drvdata(dev); + + return sprintf(buf, "%s\n", pps->info.name); +} +static DEVICE_ATTR_RO(name); + +static ssize_t path_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct pps_device *pps = dev_get_drvdata(dev); + + return sprintf(buf, "%s\n", pps->info.path); +} +static DEVICE_ATTR_RO(path); + +static struct attribute *pps_attrs[] = { + &dev_attr_assert.attr, + &dev_attr_clear.attr, + &dev_attr_mode.attr, + &dev_attr_echo.attr, + &dev_attr_name.attr, + &dev_attr_path.attr, + NULL, +}; + +static const struct attribute_group pps_group = { + .attrs = pps_attrs, +}; + +const struct attribute_group *pps_groups[] = { + &pps_group, + NULL, +}; |