| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * GNSS receiver core | |
| * | |
| * Copyright (C) 2018 Johan Hovold <johan@kernel.org> | |
| */ | |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | |
| #include <linux/cdev.h> | |
| #include <linux/errno.h> | |
| #include <linux/fs.h> | |
| #include <linux/gnss.h> | |
| #include <linux/idr.h> | |
| #include <linux/init.h> | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/poll.h> | |
| #include <linux/slab.h> | |
| #include <linux/uaccess.h> | |
| #include <linux/wait.h> | |
| #define GNSS_FLAG_HAS_WRITE_RAW BIT(0) | |
| #define GNSS_MINORS 16 | |
| static DEFINE_IDA(gnss_minors); | |
| static dev_t gnss_first; | |
| /* FIFO size must be a power of two */ | |
| #define GNSS_READ_FIFO_SIZE 4096 | |
| #define GNSS_WRITE_BUF_SIZE 1024 | |
| #define to_gnss_device(d) container_of((d), struct gnss_device, dev) | |
| static int gnss_open(struct inode *inode, struct file *file) | |
| { | |
| struct gnss_device *gdev; | |
| int ret = 0; | |
| gdev = container_of(inode->i_cdev, struct gnss_device, cdev); | |
| get_device(&gdev->dev); | |
| stream_open(inode, file); | |
| file->private_data = gdev; | |
| down_write(&gdev->rwsem); | |
| if (gdev->disconnected) { | |
| ret = -ENODEV; | |
| goto unlock; | |
| } | |
| if (gdev->count++ == 0) { | |
| ret = gdev->ops->open(gdev); | |
| if (ret) | |
| gdev->count--; | |
| } | |
| unlock: | |
| up_write(&gdev->rwsem); | |
| if (ret) | |
| put_device(&gdev->dev); | |
| return ret; | |
| } | |
| static int gnss_release(struct inode *inode, struct file *file) | |
| { | |
| struct gnss_device *gdev = file->private_data; | |
| down_write(&gdev->rwsem); | |
| if (gdev->disconnected) | |
| goto unlock; | |
| if (--gdev->count == 0) { | |
| gdev->ops->close(gdev); | |
| kfifo_reset(&gdev->read_fifo); | |
| } | |
| unlock: | |
| up_write(&gdev->rwsem); | |
| put_device(&gdev->dev); | |
| return 0; | |
| } | |
| static ssize_t gnss_read(struct file *file, char __user *buf, | |
| size_t count, loff_t *pos) | |
| { | |
| struct gnss_device *gdev = file->private_data; | |
| unsigned int copied; | |
| int ret; | |
| mutex_lock(&gdev->read_mutex); | |
| while (kfifo_is_empty(&gdev->read_fifo)) { | |
| mutex_unlock(&gdev->read_mutex); | |
| if (gdev->disconnected) | |
| return 0; | |
| if (file->f_flags & O_NONBLOCK) | |
| return -EAGAIN; | |
| ret = wait_event_interruptible(gdev->read_queue, | |
| gdev->disconnected || | |
| !kfifo_is_empty(&gdev->read_fifo)); | |
| if (ret) | |
| return -ERESTARTSYS; | |
| mutex_lock(&gdev->read_mutex); | |
| } | |
| ret = kfifo_to_user(&gdev->read_fifo, buf, count, &copied); | |
| if (ret == 0) | |
| ret = copied; | |
| mutex_unlock(&gdev->read_mutex); | |
| return ret; | |
| } | |
| static ssize_t gnss_write(struct file *file, const char __user *buf, | |
| size_t count, loff_t *pos) | |
| { | |
| struct gnss_device *gdev = file->private_data; | |
| size_t written = 0; | |
| int ret; | |
| if (gdev->disconnected) | |
| return -EIO; | |
| if (!count) | |
| return 0; | |
| if (!(gdev->flags & GNSS_FLAG_HAS_WRITE_RAW)) | |
| return -EIO; | |
| /* Ignoring O_NONBLOCK, write_raw() is synchronous. */ | |
| ret = mutex_lock_interruptible(&gdev->write_mutex); | |
| if (ret) | |
| return -ERESTARTSYS; | |
| for (;;) { | |
| size_t n = count - written; | |
| if (n > GNSS_WRITE_BUF_SIZE) | |
| n = GNSS_WRITE_BUF_SIZE; | |
| if (copy_from_user(gdev->write_buf, buf, n)) { | |
| ret = -EFAULT; | |
| goto out_unlock; | |
| } | |
| /* | |
| * Assumes write_raw can always accept GNSS_WRITE_BUF_SIZE | |
| * bytes. | |
| * | |
| * FIXME: revisit | |
| */ | |
| down_read(&gdev->rwsem); | |
| if (!gdev->disconnected) | |
| ret = gdev->ops->write_raw(gdev, gdev->write_buf, n); | |
| else | |
| ret = -EIO; | |
| up_read(&gdev->rwsem); | |
| if (ret < 0) | |
| break; | |
| written += ret; | |
| buf += ret; | |
| if (written == count) | |
| break; | |
| } | |
| if (written) | |
| ret = written; | |
| out_unlock: | |
| mutex_unlock(&gdev->write_mutex); | |
| return ret; | |
| } | |
| static __poll_t gnss_poll(struct file *file, poll_table *wait) | |
| { | |
| struct gnss_device *gdev = file->private_data; | |
| __poll_t mask = 0; | |
| poll_wait(file, &gdev->read_queue, wait); | |
| if (!kfifo_is_empty(&gdev->read_fifo)) | |
| mask |= EPOLLIN | EPOLLRDNORM; | |
| if (gdev->disconnected) | |
| mask |= EPOLLHUP; | |
| return mask; | |
| } | |
| static const struct file_operations gnss_fops = { | |
| .owner = THIS_MODULE, | |
| .open = gnss_open, | |
| .release = gnss_release, | |
| .read = gnss_read, | |
| .write = gnss_write, | |
| .poll = gnss_poll, | |
| .llseek = no_llseek, | |
| }; | |
| static struct class *gnss_class; | |
| static void gnss_device_release(struct device *dev) | |
| { | |
| struct gnss_device *gdev = to_gnss_device(dev); | |
| kfree(gdev->write_buf); | |
| kfifo_free(&gdev->read_fifo); | |
| ida_simple_remove(&gnss_minors, gdev->id); | |
| kfree(gdev); | |
| } | |
| struct gnss_device *gnss_allocate_device(struct device *parent) | |
| { | |
| struct gnss_device *gdev; | |
| struct device *dev; | |
| int id; | |
| int ret; | |
| gdev = kzalloc(sizeof(*gdev), GFP_KERNEL); | |
| if (!gdev) | |
| return NULL; | |
| id = ida_simple_get(&gnss_minors, 0, GNSS_MINORS, GFP_KERNEL); | |
| if (id < 0) { | |
| kfree(gdev); | |
| return NULL; | |
| } | |
| gdev->id = id; | |
| dev = &gdev->dev; | |
| device_initialize(dev); | |
| dev->devt = gnss_first + id; | |
| dev->class = gnss_class; | |
| dev->parent = parent; | |
| dev->release = gnss_device_release; | |
| dev_set_drvdata(dev, gdev); | |
| dev_set_name(dev, "gnss%d", id); | |
| init_rwsem(&gdev->rwsem); | |
| mutex_init(&gdev->read_mutex); | |
| mutex_init(&gdev->write_mutex); | |
| init_waitqueue_head(&gdev->read_queue); | |
| ret = kfifo_alloc(&gdev->read_fifo, GNSS_READ_FIFO_SIZE, GFP_KERNEL); | |
| if (ret) | |
| goto err_put_device; | |
| gdev->write_buf = kzalloc(GNSS_WRITE_BUF_SIZE, GFP_KERNEL); | |
| if (!gdev->write_buf) | |
| goto err_put_device; | |
| cdev_init(&gdev->cdev, &gnss_fops); | |
| gdev->cdev.owner = THIS_MODULE; | |
| return gdev; | |
| err_put_device: | |
| put_device(dev); | |
| return NULL; | |
| } | |
| EXPORT_SYMBOL_GPL(gnss_allocate_device); | |
| void gnss_put_device(struct gnss_device *gdev) | |
| { | |
| put_device(&gdev->dev); | |
| } | |
| EXPORT_SYMBOL_GPL(gnss_put_device); | |
| int gnss_register_device(struct gnss_device *gdev) | |
| { | |
| int ret; | |
| /* Set a flag which can be accessed without holding the rwsem. */ | |
| if (gdev->ops->write_raw != NULL) | |
| gdev->flags |= GNSS_FLAG_HAS_WRITE_RAW; | |
| ret = cdev_device_add(&gdev->cdev, &gdev->dev); | |
| if (ret) { | |
| dev_err(&gdev->dev, "failed to add device: %d\n", ret); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| EXPORT_SYMBOL_GPL(gnss_register_device); | |
| void gnss_deregister_device(struct gnss_device *gdev) | |
| { | |
| down_write(&gdev->rwsem); | |
| gdev->disconnected = true; | |
| if (gdev->count) { | |
| wake_up_interruptible(&gdev->read_queue); | |
| gdev->ops->close(gdev); | |
| } | |
| up_write(&gdev->rwsem); | |
| cdev_device_del(&gdev->cdev, &gdev->dev); | |
| } | |
| EXPORT_SYMBOL_GPL(gnss_deregister_device); | |
| /* | |
| * Caller guarantees serialisation. | |
| * | |
| * Must not be called for a closed device. | |
| */ | |
| int gnss_insert_raw(struct gnss_device *gdev, const unsigned char *buf, | |
| size_t count) | |
| { | |
| int ret; | |
| ret = kfifo_in(&gdev->read_fifo, buf, count); | |
| wake_up_interruptible(&gdev->read_queue); | |
| return ret; | |
| } | |
| EXPORT_SYMBOL_GPL(gnss_insert_raw); | |
| static const char * const gnss_type_names[GNSS_TYPE_COUNT] = { | |
| [GNSS_TYPE_NMEA] = "NMEA", | |
| [GNSS_TYPE_SIRF] = "SiRF", | |
| [GNSS_TYPE_UBX] = "UBX", | |
| [GNSS_TYPE_MTK] = "MTK", | |
| }; | |
| static const char *gnss_type_name(struct gnss_device *gdev) | |
| { | |
| const char *name = NULL; | |
| if (gdev->type < GNSS_TYPE_COUNT) | |
| name = gnss_type_names[gdev->type]; | |
| if (!name) | |
| dev_WARN(&gdev->dev, "type name not defined\n"); | |
| return name; | |
| } | |
| static ssize_t type_show(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| struct gnss_device *gdev = to_gnss_device(dev); | |
| return sprintf(buf, "%s\n", gnss_type_name(gdev)); | |
| } | |
| static DEVICE_ATTR_RO(type); | |
| static struct attribute *gnss_attrs[] = { | |
| &dev_attr_type.attr, | |
| NULL, | |
| }; | |
| ATTRIBUTE_GROUPS(gnss); | |
| static int gnss_uevent(struct device *dev, struct kobj_uevent_env *env) | |
| { | |
| struct gnss_device *gdev = to_gnss_device(dev); | |
| int ret; | |
| ret = add_uevent_var(env, "GNSS_TYPE=%s", gnss_type_name(gdev)); | |
| if (ret) | |
| return ret; | |
| return 0; | |
| } | |
| static int __init gnss_module_init(void) | |
| { | |
| int ret; | |
| ret = alloc_chrdev_region(&gnss_first, 0, GNSS_MINORS, "gnss"); | |
| if (ret < 0) { | |
| pr_err("failed to allocate device numbers: %d\n", ret); | |
| return ret; | |
| } | |
| gnss_class = class_create(THIS_MODULE, "gnss"); | |
| if (IS_ERR(gnss_class)) { | |
| ret = PTR_ERR(gnss_class); | |
| pr_err("failed to create class: %d\n", ret); | |
| goto err_unregister_chrdev; | |
| } | |
| gnss_class->dev_groups = gnss_groups; | |
| gnss_class->dev_uevent = gnss_uevent; | |
| pr_info("GNSS driver registered with major %d\n", MAJOR(gnss_first)); | |
| return 0; | |
| err_unregister_chrdev: | |
| unregister_chrdev_region(gnss_first, GNSS_MINORS); | |
| return ret; | |
| } | |
| module_init(gnss_module_init); | |
| static void __exit gnss_module_exit(void) | |
| { | |
| class_destroy(gnss_class); | |
| unregister_chrdev_region(gnss_first, GNSS_MINORS); | |
| ida_destroy(&gnss_minors); | |
| } | |
| module_exit(gnss_module_exit); | |
| MODULE_AUTHOR("Johan Hovold <johan@kernel.org>"); | |
| MODULE_DESCRIPTION("GNSS receiver core"); | |
| MODULE_LICENSE("GPL v2"); |