| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * PPS generators core file | |
| * | |
| * Copyright (C) 2024 Rodolfo Giometti <giometti@enneenne.com> | |
| */ | |
| #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/uaccess.h> | |
| #include <linux/idr.h> | |
| #include <linux/cdev.h> | |
| #include <linux/poll.h> | |
| #include <linux/fs.h> | |
| #include <linux/pps_gen_kernel.h> | |
| #include <linux/slab.h> | |
| /* | |
| * Local variables | |
| */ | |
| static dev_t pps_gen_devt; | |
| static const struct class pps_gen_class = { | |
| .name = "pps-gen", | |
| .dev_groups = pps_gen_groups | |
| }; | |
| static DEFINE_IDA(pps_gen_ida); | |
| /* | |
| * Char device methods | |
| */ | |
| static __poll_t pps_gen_cdev_poll(struct file *file, poll_table *wait) | |
| { | |
| struct pps_gen_device *pps_gen = file->private_data; | |
| poll_wait(file, &pps_gen->queue, wait); | |
| return EPOLLIN | EPOLLRDNORM; | |
| } | |
| static int pps_gen_cdev_fasync(int fd, struct file *file, int on) | |
| { | |
| struct pps_gen_device *pps_gen = file->private_data; | |
| return fasync_helper(fd, file, on, &pps_gen->async_queue); | |
| } | |
| static long pps_gen_cdev_ioctl(struct file *file, | |
| unsigned int cmd, unsigned long arg) | |
| { | |
| struct pps_gen_device *pps_gen = file->private_data; | |
| void __user *uarg = (void __user *) arg; | |
| unsigned int __user *uiuarg = (unsigned int __user *) arg; | |
| unsigned int status; | |
| int ret; | |
| switch (cmd) { | |
| case PPS_GEN_SETENABLE: | |
| dev_dbg(pps_gen->dev, "PPS_GEN_SETENABLE\n"); | |
| ret = get_user(status, uiuarg); | |
| if (ret) | |
| return -EFAULT; | |
| ret = pps_gen->info->enable(pps_gen, status); | |
| if (ret) | |
| return ret; | |
| pps_gen->enabled = status; | |
| break; | |
| case PPS_GEN_USESYSTEMCLOCK: | |
| dev_dbg(pps_gen->dev, "PPS_GEN_USESYSTEMCLOCK\n"); | |
| ret = put_user(pps_gen->info->use_system_clock, uiuarg); | |
| if (ret) | |
| return -EFAULT; | |
| break; | |
| case PPS_GEN_FETCHEVENT: { | |
| struct pps_gen_event info; | |
| unsigned int ev = pps_gen->last_ev; | |
| dev_dbg(pps_gen->dev, "PPS_GEN_FETCHEVENT\n"); | |
| ret = wait_event_interruptible(pps_gen->queue, | |
| ev != pps_gen->last_ev); | |
| if (ret == -ERESTARTSYS) { | |
| dev_dbg(pps_gen->dev, "pending signal caught\n"); | |
| return -EINTR; | |
| } | |
| spin_lock_irq(&pps_gen->lock); | |
| info.sequence = pps_gen->sequence; | |
| info.event = pps_gen->event; | |
| spin_unlock_irq(&pps_gen->lock); | |
| ret = copy_to_user(uarg, &info, sizeof(struct pps_gen_event)); | |
| if (ret) | |
| return -EFAULT; | |
| break; | |
| } | |
| default: | |
| return -ENOTTY; | |
| } | |
| return 0; | |
| } | |
| static int pps_gen_cdev_open(struct inode *inode, struct file *file) | |
| { | |
| struct pps_gen_device *pps_gen = container_of(inode->i_cdev, | |
| struct pps_gen_device, cdev); | |
| get_device(pps_gen->dev); | |
| file->private_data = pps_gen; | |
| return 0; | |
| } | |
| static int pps_gen_cdev_release(struct inode *inode, struct file *file) | |
| { | |
| struct pps_gen_device *pps_gen = file->private_data; | |
| put_device(pps_gen->dev); | |
| return 0; | |
| } | |
| /* | |
| * Char device stuff | |
| */ | |
| static const struct file_operations pps_gen_cdev_fops = { | |
| .owner = THIS_MODULE, | |
| .poll = pps_gen_cdev_poll, | |
| .fasync = pps_gen_cdev_fasync, | |
| .unlocked_ioctl = pps_gen_cdev_ioctl, | |
| .open = pps_gen_cdev_open, | |
| .release = pps_gen_cdev_release, | |
| }; | |
| static void pps_gen_device_destruct(struct device *dev) | |
| { | |
| struct pps_gen_device *pps_gen = dev_get_drvdata(dev); | |
| cdev_del(&pps_gen->cdev); | |
| pr_debug("deallocating pps-gen%d\n", pps_gen->id); | |
| ida_free(&pps_gen_ida, pps_gen->id); | |
| kfree(dev); | |
| kfree(pps_gen); | |
| } | |
| static int pps_gen_register_cdev(struct pps_gen_device *pps_gen) | |
| { | |
| int err; | |
| dev_t devt; | |
| err = ida_alloc_max(&pps_gen_ida, PPS_GEN_MAX_SOURCES - 1, GFP_KERNEL); | |
| if (err < 0) { | |
| if (err == -ENOSPC) { | |
| pr_err("too many PPS sources in the system\n"); | |
| err = -EBUSY; | |
| } | |
| return err; | |
| } | |
| pps_gen->id = err; | |
| devt = MKDEV(MAJOR(pps_gen_devt), pps_gen->id); | |
| cdev_init(&pps_gen->cdev, &pps_gen_cdev_fops); | |
| pps_gen->cdev.owner = pps_gen->info->owner; | |
| err = cdev_add(&pps_gen->cdev, devt, 1); | |
| if (err) { | |
| pr_err("failed to add char device %d:%d\n", | |
| MAJOR(pps_gen_devt), pps_gen->id); | |
| goto free_ida; | |
| } | |
| pps_gen->dev = device_create(&pps_gen_class, pps_gen->info->parent, devt, | |
| pps_gen, "pps-gen%d", pps_gen->id); | |
| if (IS_ERR(pps_gen->dev)) { | |
| err = PTR_ERR(pps_gen->dev); | |
| goto del_cdev; | |
| } | |
| pps_gen->dev->release = pps_gen_device_destruct; | |
| dev_set_drvdata(pps_gen->dev, pps_gen); | |
| pr_debug("generator got cdev (%d:%d)\n", | |
| MAJOR(pps_gen_devt), pps_gen->id); | |
| return 0; | |
| del_cdev: | |
| cdev_del(&pps_gen->cdev); | |
| free_ida: | |
| ida_free(&pps_gen_ida, pps_gen->id); | |
| return err; | |
| } | |
| static void pps_gen_unregister_cdev(struct pps_gen_device *pps_gen) | |
| { | |
| pr_debug("unregistering pps-gen%d\n", pps_gen->id); | |
| device_destroy(&pps_gen_class, pps_gen->dev->devt); | |
| } | |
| /* | |
| * Exported functions | |
| */ | |
| /** | |
| * pps_gen_register_source() - add a PPS generator in the system | |
| * @info: the PPS generator info struct | |
| * | |
| * This function is used to register a new PPS generator in the system. | |
| * When it returns successfully the new generator is up and running, and | |
| * it can be managed by the userspace. | |
| * | |
| * Return: the PPS generator device in case of success, and ERR_PTR(errno) | |
| * otherwise. | |
| */ | |
| struct pps_gen_device *pps_gen_register_source(const struct pps_gen_source_info *info) | |
| { | |
| struct pps_gen_device *pps_gen; | |
| int err; | |
| pps_gen = kzalloc_obj(struct pps_gen_device); | |
| if (pps_gen == NULL) { | |
| err = -ENOMEM; | |
| goto pps_gen_register_source_exit; | |
| } | |
| pps_gen->info = info; | |
| pps_gen->enabled = false; | |
| init_waitqueue_head(&pps_gen->queue); | |
| spin_lock_init(&pps_gen->lock); | |
| /* Create the char device */ | |
| err = pps_gen_register_cdev(pps_gen); | |
| if (err < 0) { | |
| pr_err(" unable to create char device\n"); | |
| goto kfree_pps_gen; | |
| } | |
| return pps_gen; | |
| kfree_pps_gen: | |
| kfree(pps_gen); | |
| pps_gen_register_source_exit: | |
| pr_err("unable to register generator\n"); | |
| return ERR_PTR(err); | |
| } | |
| EXPORT_SYMBOL(pps_gen_register_source); | |
| /** | |
| * pps_gen_unregister_source() - remove a PPS generator from the system | |
| * @pps_gen: the PPS generator device to be removed | |
| * | |
| * This function is used to deregister a PPS generator from the system. When | |
| * called, it disables the generator so no pulses are generated anymore. | |
| */ | |
| void pps_gen_unregister_source(struct pps_gen_device *pps_gen) | |
| { | |
| pps_gen_unregister_cdev(pps_gen); | |
| } | |
| EXPORT_SYMBOL(pps_gen_unregister_source); | |
| /* pps_gen_event - register a PPS generator event into the system | |
| * @pps: the PPS generator device | |
| * @event: the event type | |
| * @data: userdef pointer | |
| * | |
| * This function is used by each PPS generator in order to register a new | |
| * PPS event into the system (it's usually called inside an IRQ handler). | |
| */ | |
| void pps_gen_event(struct pps_gen_device *pps_gen, | |
| unsigned int event, void *data) | |
| { | |
| unsigned long flags; | |
| dev_dbg(pps_gen->dev, "PPS generator event %u\n", event); | |
| spin_lock_irqsave(&pps_gen->lock, flags); | |
| pps_gen->event = event; | |
| pps_gen->sequence++; | |
| pps_gen->last_ev++; | |
| wake_up_interruptible_all(&pps_gen->queue); | |
| kill_fasync(&pps_gen->async_queue, SIGIO, POLL_IN); | |
| spin_unlock_irqrestore(&pps_gen->lock, flags); | |
| } | |
| EXPORT_SYMBOL(pps_gen_event); | |
| /* | |
| * Module stuff | |
| */ | |
| static void __exit pps_gen_exit(void) | |
| { | |
| class_unregister(&pps_gen_class); | |
| unregister_chrdev_region(pps_gen_devt, PPS_GEN_MAX_SOURCES); | |
| } | |
| static int __init pps_gen_init(void) | |
| { | |
| int err; | |
| err = class_register(&pps_gen_class); | |
| if (err) { | |
| pr_err("failed to register class\n"); | |
| return err; | |
| } | |
| err = alloc_chrdev_region(&pps_gen_devt, 0, | |
| PPS_GEN_MAX_SOURCES, "pps-gen"); | |
| if (err < 0) { | |
| pr_err("failed to allocate char device region\n"); | |
| goto remove_class; | |
| } | |
| return 0; | |
| remove_class: | |
| class_unregister(&pps_gen_class); | |
| return err; | |
| } | |
| subsys_initcall(pps_gen_init); | |
| module_exit(pps_gen_exit); | |
| MODULE_AUTHOR("Rodolfo Giometti <giometti@enneenne.com>"); | |
| MODULE_DESCRIPTION("LinuxPPS generators support"); | |
| MODULE_LICENSE("GPL"); |