![Ian Abbott](/assets/img/avatar_default.png)
If the "write" file operation handler, `comedi_write()` is passed 0 for the amount to write, some error conditions are currently skipped and the function just returns 0. Change it to check those error conditions and return an error value if appropriate. The trickiest case is the check for when the previously set up asynchronous command has terminated with an error. In that case, `-EPIPE` is returned (as it is for a write of non-zero length) and the subdevice gets marked as non-busy. A zero-length write that returns 0 has no other effects, in particular, it does not cause the subdevice to be marked as non-busy. Signed-off-by: Ian Abbott <abbotti@mev.co.uk> Reviewed-by: H Hartley Sweeten <hsweeten@visionengravers.com> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
2929 lines
66 KiB
C
2929 lines
66 KiB
C
/*
|
|
* comedi/comedi_fops.c
|
|
* comedi kernel module
|
|
*
|
|
* COMEDI - Linux Control and Measurement Device Interface
|
|
* Copyright (C) 1997-2000 David A. Schleef <ds@schleef.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
|
|
#include "comedi_compat32.h"
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/fcntl.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/kmod.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/init.h>
|
|
#include <linux/device.h>
|
|
#include <linux/vmalloc.h>
|
|
#include <linux/fs.h>
|
|
#include "comedidev.h"
|
|
#include <linux/cdev.h>
|
|
#include <linux/stat.h>
|
|
|
|
#include <linux/io.h>
|
|
#include <linux/uaccess.h>
|
|
|
|
#include "comedi_internal.h"
|
|
|
|
/*
|
|
* comedi_subdevice "runflags"
|
|
* COMEDI_SRF_RT: DEPRECATED: command is running real-time
|
|
* COMEDI_SRF_ERROR: indicates an COMEDI_CB_ERROR event has occurred
|
|
* since the last command was started
|
|
* COMEDI_SRF_RUNNING: command is running
|
|
* COMEDI_SRF_FREE_SPRIV: free s->private on detach
|
|
*
|
|
* COMEDI_SRF_BUSY_MASK: runflags that indicate the subdevice is "busy"
|
|
*/
|
|
#define COMEDI_SRF_RT BIT(1)
|
|
#define COMEDI_SRF_ERROR BIT(2)
|
|
#define COMEDI_SRF_RUNNING BIT(27)
|
|
#define COMEDI_SRF_FREE_SPRIV BIT(31)
|
|
|
|
#define COMEDI_SRF_BUSY_MASK (COMEDI_SRF_ERROR | COMEDI_SRF_RUNNING)
|
|
|
|
/**
|
|
* struct comedi_file - Per-file private data for COMEDI device
|
|
* @dev: COMEDI device.
|
|
* @read_subdev: Current "read" subdevice.
|
|
* @write_subdev: Current "write" subdevice.
|
|
* @last_detach_count: Last known detach count.
|
|
* @last_attached: Last known attached/detached state.
|
|
*/
|
|
struct comedi_file {
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *read_subdev;
|
|
struct comedi_subdevice *write_subdev;
|
|
unsigned int last_detach_count;
|
|
bool last_attached:1;
|
|
};
|
|
|
|
#define COMEDI_NUM_MINORS 0x100
|
|
#define COMEDI_NUM_SUBDEVICE_MINORS \
|
|
(COMEDI_NUM_MINORS - COMEDI_NUM_BOARD_MINORS)
|
|
|
|
static int comedi_num_legacy_minors;
|
|
module_param(comedi_num_legacy_minors, int, S_IRUGO);
|
|
MODULE_PARM_DESC(comedi_num_legacy_minors,
|
|
"number of comedi minor devices to reserve for non-auto-configured devices (default 0)"
|
|
);
|
|
|
|
unsigned int comedi_default_buf_size_kb = CONFIG_COMEDI_DEFAULT_BUF_SIZE_KB;
|
|
module_param(comedi_default_buf_size_kb, uint, S_IRUGO | S_IWUSR);
|
|
MODULE_PARM_DESC(comedi_default_buf_size_kb,
|
|
"default asynchronous buffer size in KiB (default "
|
|
__MODULE_STRING(CONFIG_COMEDI_DEFAULT_BUF_SIZE_KB) ")");
|
|
|
|
unsigned int comedi_default_buf_maxsize_kb
|
|
= CONFIG_COMEDI_DEFAULT_BUF_MAXSIZE_KB;
|
|
module_param(comedi_default_buf_maxsize_kb, uint, S_IRUGO | S_IWUSR);
|
|
MODULE_PARM_DESC(comedi_default_buf_maxsize_kb,
|
|
"default maximum size of asynchronous buffer in KiB (default "
|
|
__MODULE_STRING(CONFIG_COMEDI_DEFAULT_BUF_MAXSIZE_KB) ")");
|
|
|
|
static DEFINE_MUTEX(comedi_board_minor_table_lock);
|
|
static struct comedi_device
|
|
*comedi_board_minor_table[COMEDI_NUM_BOARD_MINORS];
|
|
|
|
static DEFINE_MUTEX(comedi_subdevice_minor_table_lock);
|
|
/* Note: indexed by minor - COMEDI_NUM_BOARD_MINORS. */
|
|
static struct comedi_subdevice
|
|
*comedi_subdevice_minor_table[COMEDI_NUM_SUBDEVICE_MINORS];
|
|
|
|
static struct class *comedi_class;
|
|
static struct cdev comedi_cdev;
|
|
|
|
static void comedi_device_init(struct comedi_device *dev)
|
|
{
|
|
kref_init(&dev->refcount);
|
|
spin_lock_init(&dev->spinlock);
|
|
mutex_init(&dev->mutex);
|
|
init_rwsem(&dev->attach_lock);
|
|
dev->minor = -1;
|
|
}
|
|
|
|
static void comedi_dev_kref_release(struct kref *kref)
|
|
{
|
|
struct comedi_device *dev =
|
|
container_of(kref, struct comedi_device, refcount);
|
|
|
|
mutex_destroy(&dev->mutex);
|
|
put_device(dev->class_dev);
|
|
kfree(dev);
|
|
}
|
|
|
|
/**
|
|
* comedi_dev_put() - Release a use of a COMEDI device
|
|
* @dev: COMEDI device.
|
|
*
|
|
* Must be called when a user of a COMEDI device is finished with it.
|
|
* When the last user of the COMEDI device calls this function, the
|
|
* COMEDI device is destroyed.
|
|
*
|
|
* Return: 1 if the COMEDI device is destroyed by this call or @dev is
|
|
* NULL, otherwise return 0. Callers must not assume the COMEDI
|
|
* device is still valid if this function returns 0.
|
|
*/
|
|
int comedi_dev_put(struct comedi_device *dev)
|
|
{
|
|
if (dev)
|
|
return kref_put(&dev->refcount, comedi_dev_kref_release);
|
|
return 1;
|
|
}
|
|
EXPORT_SYMBOL_GPL(comedi_dev_put);
|
|
|
|
static struct comedi_device *comedi_dev_get(struct comedi_device *dev)
|
|
{
|
|
if (dev)
|
|
kref_get(&dev->refcount);
|
|
return dev;
|
|
}
|
|
|
|
static void comedi_device_cleanup(struct comedi_device *dev)
|
|
{
|
|
struct module *driver_module = NULL;
|
|
|
|
if (!dev)
|
|
return;
|
|
mutex_lock(&dev->mutex);
|
|
if (dev->attached)
|
|
driver_module = dev->driver->module;
|
|
comedi_device_detach(dev);
|
|
if (driver_module && dev->use_count)
|
|
module_put(driver_module);
|
|
mutex_unlock(&dev->mutex);
|
|
}
|
|
|
|
static bool comedi_clear_board_dev(struct comedi_device *dev)
|
|
{
|
|
unsigned int i = dev->minor;
|
|
bool cleared = false;
|
|
|
|
mutex_lock(&comedi_board_minor_table_lock);
|
|
if (dev == comedi_board_minor_table[i]) {
|
|
comedi_board_minor_table[i] = NULL;
|
|
cleared = true;
|
|
}
|
|
mutex_unlock(&comedi_board_minor_table_lock);
|
|
return cleared;
|
|
}
|
|
|
|
static struct comedi_device *comedi_clear_board_minor(unsigned minor)
|
|
{
|
|
struct comedi_device *dev;
|
|
|
|
mutex_lock(&comedi_board_minor_table_lock);
|
|
dev = comedi_board_minor_table[minor];
|
|
comedi_board_minor_table[minor] = NULL;
|
|
mutex_unlock(&comedi_board_minor_table_lock);
|
|
return dev;
|
|
}
|
|
|
|
static void comedi_free_board_dev(struct comedi_device *dev)
|
|
{
|
|
if (dev) {
|
|
comedi_device_cleanup(dev);
|
|
if (dev->class_dev) {
|
|
device_destroy(comedi_class,
|
|
MKDEV(COMEDI_MAJOR, dev->minor));
|
|
}
|
|
comedi_dev_put(dev);
|
|
}
|
|
}
|
|
|
|
static struct comedi_subdevice
|
|
*comedi_subdevice_from_minor(const struct comedi_device *dev, unsigned minor)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
unsigned int i = minor - COMEDI_NUM_BOARD_MINORS;
|
|
|
|
mutex_lock(&comedi_subdevice_minor_table_lock);
|
|
s = comedi_subdevice_minor_table[i];
|
|
if (s && s->device != dev)
|
|
s = NULL;
|
|
mutex_unlock(&comedi_subdevice_minor_table_lock);
|
|
return s;
|
|
}
|
|
|
|
static struct comedi_device *comedi_dev_get_from_board_minor(unsigned minor)
|
|
{
|
|
struct comedi_device *dev;
|
|
|
|
mutex_lock(&comedi_board_minor_table_lock);
|
|
dev = comedi_dev_get(comedi_board_minor_table[minor]);
|
|
mutex_unlock(&comedi_board_minor_table_lock);
|
|
return dev;
|
|
}
|
|
|
|
static struct comedi_device *comedi_dev_get_from_subdevice_minor(unsigned minor)
|
|
{
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int i = minor - COMEDI_NUM_BOARD_MINORS;
|
|
|
|
mutex_lock(&comedi_subdevice_minor_table_lock);
|
|
s = comedi_subdevice_minor_table[i];
|
|
dev = comedi_dev_get(s ? s->device : NULL);
|
|
mutex_unlock(&comedi_subdevice_minor_table_lock);
|
|
return dev;
|
|
}
|
|
|
|
/**
|
|
* comedi_dev_get_from_minor() - Get COMEDI device by minor device number
|
|
* @minor: Minor device number.
|
|
*
|
|
* Finds the COMEDI device associated with the minor device number, if any,
|
|
* and increments its reference count. The COMEDI device is prevented from
|
|
* being freed until a matching call is made to comedi_dev_put().
|
|
*
|
|
* Return: A pointer to the COMEDI device if it exists, with its usage
|
|
* reference incremented. Return NULL if no COMEDI device exists with the
|
|
* specified minor device number.
|
|
*/
|
|
struct comedi_device *comedi_dev_get_from_minor(unsigned minor)
|
|
{
|
|
if (minor < COMEDI_NUM_BOARD_MINORS)
|
|
return comedi_dev_get_from_board_minor(minor);
|
|
|
|
return comedi_dev_get_from_subdevice_minor(minor);
|
|
}
|
|
EXPORT_SYMBOL_GPL(comedi_dev_get_from_minor);
|
|
|
|
static struct comedi_subdevice *
|
|
comedi_read_subdevice(const struct comedi_device *dev, unsigned int minor)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
|
|
if (minor >= COMEDI_NUM_BOARD_MINORS) {
|
|
s = comedi_subdevice_from_minor(dev, minor);
|
|
if (!s || (s->subdev_flags & SDF_CMD_READ))
|
|
return s;
|
|
}
|
|
return dev->read_subdev;
|
|
}
|
|
|
|
static struct comedi_subdevice *
|
|
comedi_write_subdevice(const struct comedi_device *dev, unsigned int minor)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
|
|
if (minor >= COMEDI_NUM_BOARD_MINORS) {
|
|
s = comedi_subdevice_from_minor(dev, minor);
|
|
if (!s || (s->subdev_flags & SDF_CMD_WRITE))
|
|
return s;
|
|
}
|
|
return dev->write_subdev;
|
|
}
|
|
|
|
static void comedi_file_reset(struct file *file)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
struct comedi_subdevice *s, *read_s, *write_s;
|
|
unsigned int minor = iminor(file_inode(file));
|
|
|
|
read_s = dev->read_subdev;
|
|
write_s = dev->write_subdev;
|
|
if (minor >= COMEDI_NUM_BOARD_MINORS) {
|
|
s = comedi_subdevice_from_minor(dev, minor);
|
|
if (!s || s->subdev_flags & SDF_CMD_READ)
|
|
read_s = s;
|
|
if (!s || s->subdev_flags & SDF_CMD_WRITE)
|
|
write_s = s;
|
|
}
|
|
cfp->last_attached = dev->attached;
|
|
cfp->last_detach_count = dev->detach_count;
|
|
ACCESS_ONCE(cfp->read_subdev) = read_s;
|
|
ACCESS_ONCE(cfp->write_subdev) = write_s;
|
|
}
|
|
|
|
static void comedi_file_check(struct file *file)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
|
|
if (cfp->last_attached != dev->attached ||
|
|
cfp->last_detach_count != dev->detach_count)
|
|
comedi_file_reset(file);
|
|
}
|
|
|
|
static struct comedi_subdevice *comedi_file_read_subdevice(struct file *file)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
|
|
comedi_file_check(file);
|
|
return ACCESS_ONCE(cfp->read_subdev);
|
|
}
|
|
|
|
static struct comedi_subdevice *comedi_file_write_subdevice(struct file *file)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
|
|
comedi_file_check(file);
|
|
return ACCESS_ONCE(cfp->write_subdev);
|
|
}
|
|
|
|
static int resize_async_buffer(struct comedi_device *dev,
|
|
struct comedi_subdevice *s, unsigned new_size)
|
|
{
|
|
struct comedi_async *async = s->async;
|
|
int retval;
|
|
|
|
if (new_size > async->max_bufsize)
|
|
return -EPERM;
|
|
|
|
if (s->busy) {
|
|
dev_dbg(dev->class_dev,
|
|
"subdevice is busy, cannot resize buffer\n");
|
|
return -EBUSY;
|
|
}
|
|
if (comedi_buf_is_mmapped(s)) {
|
|
dev_dbg(dev->class_dev,
|
|
"subdevice is mmapped, cannot resize buffer\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
/* make sure buffer is an integral number of pages (we round up) */
|
|
new_size = (new_size + PAGE_SIZE - 1) & PAGE_MASK;
|
|
|
|
retval = comedi_buf_alloc(dev, s, new_size);
|
|
if (retval < 0)
|
|
return retval;
|
|
|
|
if (s->buf_change) {
|
|
retval = s->buf_change(dev, s);
|
|
if (retval < 0)
|
|
return retval;
|
|
}
|
|
|
|
dev_dbg(dev->class_dev, "subd %d buffer resized to %i bytes\n",
|
|
s->index, async->prealloc_bufsz);
|
|
return 0;
|
|
}
|
|
|
|
/* sysfs attribute files */
|
|
|
|
static ssize_t max_read_buffer_kb_show(struct device *csdev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
unsigned int minor = MINOR(csdev->devt);
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int size = 0;
|
|
|
|
dev = comedi_dev_get_from_minor(minor);
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
s = comedi_read_subdevice(dev, minor);
|
|
if (s && (s->subdev_flags & SDF_CMD_READ) && s->async)
|
|
size = s->async->max_bufsize / 1024;
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
comedi_dev_put(dev);
|
|
return snprintf(buf, PAGE_SIZE, "%u\n", size);
|
|
}
|
|
|
|
static ssize_t max_read_buffer_kb_store(struct device *csdev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
unsigned int minor = MINOR(csdev->devt);
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int size;
|
|
int err;
|
|
|
|
err = kstrtouint(buf, 10, &size);
|
|
if (err)
|
|
return err;
|
|
if (size > (UINT_MAX / 1024))
|
|
return -EINVAL;
|
|
size *= 1024;
|
|
|
|
dev = comedi_dev_get_from_minor(minor);
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
s = comedi_read_subdevice(dev, minor);
|
|
if (s && (s->subdev_flags & SDF_CMD_READ) && s->async)
|
|
s->async->max_bufsize = size;
|
|
else
|
|
err = -EINVAL;
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
comedi_dev_put(dev);
|
|
return err ? err : count;
|
|
}
|
|
static DEVICE_ATTR_RW(max_read_buffer_kb);
|
|
|
|
static ssize_t read_buffer_kb_show(struct device *csdev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
unsigned int minor = MINOR(csdev->devt);
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int size = 0;
|
|
|
|
dev = comedi_dev_get_from_minor(minor);
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
s = comedi_read_subdevice(dev, minor);
|
|
if (s && (s->subdev_flags & SDF_CMD_READ) && s->async)
|
|
size = s->async->prealloc_bufsz / 1024;
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
comedi_dev_put(dev);
|
|
return snprintf(buf, PAGE_SIZE, "%u\n", size);
|
|
}
|
|
|
|
static ssize_t read_buffer_kb_store(struct device *csdev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
unsigned int minor = MINOR(csdev->devt);
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int size;
|
|
int err;
|
|
|
|
err = kstrtouint(buf, 10, &size);
|
|
if (err)
|
|
return err;
|
|
if (size > (UINT_MAX / 1024))
|
|
return -EINVAL;
|
|
size *= 1024;
|
|
|
|
dev = comedi_dev_get_from_minor(minor);
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
s = comedi_read_subdevice(dev, minor);
|
|
if (s && (s->subdev_flags & SDF_CMD_READ) && s->async)
|
|
err = resize_async_buffer(dev, s, size);
|
|
else
|
|
err = -EINVAL;
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
comedi_dev_put(dev);
|
|
return err ? err : count;
|
|
}
|
|
static DEVICE_ATTR_RW(read_buffer_kb);
|
|
|
|
static ssize_t max_write_buffer_kb_show(struct device *csdev,
|
|
struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
unsigned int minor = MINOR(csdev->devt);
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int size = 0;
|
|
|
|
dev = comedi_dev_get_from_minor(minor);
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
s = comedi_write_subdevice(dev, minor);
|
|
if (s && (s->subdev_flags & SDF_CMD_WRITE) && s->async)
|
|
size = s->async->max_bufsize / 1024;
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
comedi_dev_put(dev);
|
|
return snprintf(buf, PAGE_SIZE, "%u\n", size);
|
|
}
|
|
|
|
static ssize_t max_write_buffer_kb_store(struct device *csdev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
unsigned int minor = MINOR(csdev->devt);
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int size;
|
|
int err;
|
|
|
|
err = kstrtouint(buf, 10, &size);
|
|
if (err)
|
|
return err;
|
|
if (size > (UINT_MAX / 1024))
|
|
return -EINVAL;
|
|
size *= 1024;
|
|
|
|
dev = comedi_dev_get_from_minor(minor);
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
s = comedi_write_subdevice(dev, minor);
|
|
if (s && (s->subdev_flags & SDF_CMD_WRITE) && s->async)
|
|
s->async->max_bufsize = size;
|
|
else
|
|
err = -EINVAL;
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
comedi_dev_put(dev);
|
|
return err ? err : count;
|
|
}
|
|
static DEVICE_ATTR_RW(max_write_buffer_kb);
|
|
|
|
static ssize_t write_buffer_kb_show(struct device *csdev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
unsigned int minor = MINOR(csdev->devt);
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int size = 0;
|
|
|
|
dev = comedi_dev_get_from_minor(minor);
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
s = comedi_write_subdevice(dev, minor);
|
|
if (s && (s->subdev_flags & SDF_CMD_WRITE) && s->async)
|
|
size = s->async->prealloc_bufsz / 1024;
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
comedi_dev_put(dev);
|
|
return snprintf(buf, PAGE_SIZE, "%u\n", size);
|
|
}
|
|
|
|
static ssize_t write_buffer_kb_store(struct device *csdev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
unsigned int minor = MINOR(csdev->devt);
|
|
struct comedi_device *dev;
|
|
struct comedi_subdevice *s;
|
|
unsigned int size;
|
|
int err;
|
|
|
|
err = kstrtouint(buf, 10, &size);
|
|
if (err)
|
|
return err;
|
|
if (size > (UINT_MAX / 1024))
|
|
return -EINVAL;
|
|
size *= 1024;
|
|
|
|
dev = comedi_dev_get_from_minor(minor);
|
|
if (!dev)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
s = comedi_write_subdevice(dev, minor);
|
|
if (s && (s->subdev_flags & SDF_CMD_WRITE) && s->async)
|
|
err = resize_async_buffer(dev, s, size);
|
|
else
|
|
err = -EINVAL;
|
|
mutex_unlock(&dev->mutex);
|
|
|
|
comedi_dev_put(dev);
|
|
return err ? err : count;
|
|
}
|
|
static DEVICE_ATTR_RW(write_buffer_kb);
|
|
|
|
static struct attribute *comedi_dev_attrs[] = {
|
|
&dev_attr_max_read_buffer_kb.attr,
|
|
&dev_attr_read_buffer_kb.attr,
|
|
&dev_attr_max_write_buffer_kb.attr,
|
|
&dev_attr_write_buffer_kb.attr,
|
|
NULL,
|
|
};
|
|
ATTRIBUTE_GROUPS(comedi_dev);
|
|
|
|
static void __comedi_clear_subdevice_runflags(struct comedi_subdevice *s,
|
|
unsigned bits)
|
|
{
|
|
s->runflags &= ~bits;
|
|
}
|
|
|
|
static void __comedi_set_subdevice_runflags(struct comedi_subdevice *s,
|
|
unsigned bits)
|
|
{
|
|
s->runflags |= bits;
|
|
}
|
|
|
|
static void comedi_update_subdevice_runflags(struct comedi_subdevice *s,
|
|
unsigned mask, unsigned bits)
|
|
{
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->spin_lock, flags);
|
|
__comedi_clear_subdevice_runflags(s, mask);
|
|
__comedi_set_subdevice_runflags(s, bits & mask);
|
|
spin_unlock_irqrestore(&s->spin_lock, flags);
|
|
}
|
|
|
|
static unsigned __comedi_get_subdevice_runflags(struct comedi_subdevice *s)
|
|
{
|
|
return s->runflags;
|
|
}
|
|
|
|
static unsigned comedi_get_subdevice_runflags(struct comedi_subdevice *s)
|
|
{
|
|
unsigned long flags;
|
|
unsigned runflags;
|
|
|
|
spin_lock_irqsave(&s->spin_lock, flags);
|
|
runflags = __comedi_get_subdevice_runflags(s);
|
|
spin_unlock_irqrestore(&s->spin_lock, flags);
|
|
return runflags;
|
|
}
|
|
|
|
static bool comedi_is_runflags_running(unsigned runflags)
|
|
{
|
|
return runflags & COMEDI_SRF_RUNNING;
|
|
}
|
|
|
|
static bool comedi_is_runflags_in_error(unsigned runflags)
|
|
{
|
|
return runflags & COMEDI_SRF_ERROR;
|
|
}
|
|
|
|
/**
|
|
* comedi_is_subdevice_running() - Check if async command running on subdevice
|
|
* @s: COMEDI subdevice.
|
|
*
|
|
* Return: %true if an asynchronous COMEDI command is active on the
|
|
* subdevice, else %false.
|
|
*/
|
|
bool comedi_is_subdevice_running(struct comedi_subdevice *s)
|
|
{
|
|
unsigned runflags = comedi_get_subdevice_runflags(s);
|
|
|
|
return comedi_is_runflags_running(runflags);
|
|
}
|
|
EXPORT_SYMBOL_GPL(comedi_is_subdevice_running);
|
|
|
|
static bool __comedi_is_subdevice_running(struct comedi_subdevice *s)
|
|
{
|
|
unsigned runflags = __comedi_get_subdevice_runflags(s);
|
|
|
|
return comedi_is_runflags_running(runflags);
|
|
}
|
|
|
|
static bool comedi_is_subdevice_idle(struct comedi_subdevice *s)
|
|
{
|
|
unsigned runflags = comedi_get_subdevice_runflags(s);
|
|
|
|
return !(runflags & COMEDI_SRF_BUSY_MASK);
|
|
}
|
|
|
|
bool comedi_can_auto_free_spriv(struct comedi_subdevice *s)
|
|
{
|
|
unsigned runflags = __comedi_get_subdevice_runflags(s);
|
|
|
|
return runflags & COMEDI_SRF_FREE_SPRIV;
|
|
}
|
|
|
|
/**
|
|
* comedi_set_spriv_auto_free() - Mark subdevice private data as freeable
|
|
* @s: COMEDI subdevice.
|
|
*
|
|
* Mark the subdevice as having a pointer to private data that can be
|
|
* automatically freed when the COMEDI device is detached from the low-level
|
|
* driver.
|
|
*/
|
|
void comedi_set_spriv_auto_free(struct comedi_subdevice *s)
|
|
{
|
|
__comedi_set_subdevice_runflags(s, COMEDI_SRF_FREE_SPRIV);
|
|
}
|
|
EXPORT_SYMBOL_GPL(comedi_set_spriv_auto_free);
|
|
|
|
/**
|
|
* comedi_alloc_spriv - Allocate memory for the subdevice private data
|
|
* @s: COMEDI subdevice.
|
|
* @size: Size of the memory to allocate.
|
|
*
|
|
* Allocate memory for the subdevice private data and point @s->private
|
|
* to it. The memory will be freed automatically when the COMEDI device
|
|
* is detached from the low-level driver.
|
|
*
|
|
* Return: A pointer to the allocated memory @s->private on success.
|
|
* Return NULL on failure.
|
|
*/
|
|
void *comedi_alloc_spriv(struct comedi_subdevice *s, size_t size)
|
|
{
|
|
s->private = kzalloc(size, GFP_KERNEL);
|
|
if (s->private)
|
|
comedi_set_spriv_auto_free(s);
|
|
return s->private;
|
|
}
|
|
EXPORT_SYMBOL_GPL(comedi_alloc_spriv);
|
|
|
|
/*
|
|
* This function restores a subdevice to an idle state.
|
|
*/
|
|
static void do_become_nonbusy(struct comedi_device *dev,
|
|
struct comedi_subdevice *s)
|
|
{
|
|
struct comedi_async *async = s->async;
|
|
|
|
comedi_update_subdevice_runflags(s, COMEDI_SRF_RUNNING, 0);
|
|
if (async) {
|
|
comedi_buf_reset(s);
|
|
async->inttrig = NULL;
|
|
kfree(async->cmd.chanlist);
|
|
async->cmd.chanlist = NULL;
|
|
s->busy = NULL;
|
|
wake_up_interruptible_all(&async->wait_head);
|
|
} else {
|
|
dev_err(dev->class_dev,
|
|
"BUG: (?) do_become_nonbusy called with async=NULL\n");
|
|
s->busy = NULL;
|
|
}
|
|
}
|
|
|
|
static int do_cancel(struct comedi_device *dev, struct comedi_subdevice *s)
|
|
{
|
|
int ret = 0;
|
|
|
|
if (comedi_is_subdevice_running(s) && s->cancel)
|
|
ret = s->cancel(dev, s);
|
|
|
|
do_become_nonbusy(dev, s);
|
|
|
|
return ret;
|
|
}
|
|
|
|
void comedi_device_cancel_all(struct comedi_device *dev)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
int i;
|
|
|
|
if (!dev->attached)
|
|
return;
|
|
|
|
for (i = 0; i < dev->n_subdevices; i++) {
|
|
s = &dev->subdevices[i];
|
|
if (s->async)
|
|
do_cancel(dev, s);
|
|
}
|
|
}
|
|
|
|
static int is_device_busy(struct comedi_device *dev)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
int i;
|
|
|
|
if (!dev->attached)
|
|
return 0;
|
|
|
|
for (i = 0; i < dev->n_subdevices; i++) {
|
|
s = &dev->subdevices[i];
|
|
if (s->busy)
|
|
return 1;
|
|
if (s->async && comedi_buf_is_mmapped(s))
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_DEVCONFIG ioctl
|
|
* attaches (and configures) or detaches a legacy device
|
|
*
|
|
* arg:
|
|
* pointer to comedi_devconfig structure (NULL if detaching)
|
|
*
|
|
* reads:
|
|
* comedi_devconfig structure (if attaching)
|
|
*
|
|
* writes:
|
|
* nothing
|
|
*/
|
|
static int do_devconfig_ioctl(struct comedi_device *dev,
|
|
struct comedi_devconfig __user *arg)
|
|
{
|
|
struct comedi_devconfig it;
|
|
|
|
if (!capable(CAP_SYS_ADMIN))
|
|
return -EPERM;
|
|
|
|
if (!arg) {
|
|
if (is_device_busy(dev))
|
|
return -EBUSY;
|
|
if (dev->attached) {
|
|
struct module *driver_module = dev->driver->module;
|
|
|
|
comedi_device_detach(dev);
|
|
module_put(driver_module);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (copy_from_user(&it, arg, sizeof(it)))
|
|
return -EFAULT;
|
|
|
|
it.board_name[COMEDI_NAMELEN - 1] = 0;
|
|
|
|
if (it.options[COMEDI_DEVCONF_AUX_DATA_LENGTH]) {
|
|
dev_warn(dev->class_dev,
|
|
"comedi_config --init_data is deprecated\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (dev->minor >= comedi_num_legacy_minors)
|
|
/* don't re-use dynamically allocated comedi devices */
|
|
return -EBUSY;
|
|
|
|
/* This increments the driver module count on success. */
|
|
return comedi_device_attach(dev, &it);
|
|
}
|
|
|
|
/*
|
|
* COMEDI_BUFCONFIG ioctl
|
|
* buffer configuration
|
|
*
|
|
* arg:
|
|
* pointer to comedi_bufconfig structure
|
|
*
|
|
* reads:
|
|
* comedi_bufconfig structure
|
|
*
|
|
* writes:
|
|
* modified comedi_bufconfig structure
|
|
*/
|
|
static int do_bufconfig_ioctl(struct comedi_device *dev,
|
|
struct comedi_bufconfig __user *arg)
|
|
{
|
|
struct comedi_bufconfig bc;
|
|
struct comedi_async *async;
|
|
struct comedi_subdevice *s;
|
|
int retval = 0;
|
|
|
|
if (copy_from_user(&bc, arg, sizeof(bc)))
|
|
return -EFAULT;
|
|
|
|
if (bc.subdevice >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
|
|
s = &dev->subdevices[bc.subdevice];
|
|
async = s->async;
|
|
|
|
if (!async) {
|
|
dev_dbg(dev->class_dev,
|
|
"subdevice does not have async capability\n");
|
|
bc.size = 0;
|
|
bc.maximum_size = 0;
|
|
goto copyback;
|
|
}
|
|
|
|
if (bc.maximum_size) {
|
|
if (!capable(CAP_SYS_ADMIN))
|
|
return -EPERM;
|
|
|
|
async->max_bufsize = bc.maximum_size;
|
|
}
|
|
|
|
if (bc.size) {
|
|
retval = resize_async_buffer(dev, s, bc.size);
|
|
if (retval < 0)
|
|
return retval;
|
|
}
|
|
|
|
bc.size = async->prealloc_bufsz;
|
|
bc.maximum_size = async->max_bufsize;
|
|
|
|
copyback:
|
|
if (copy_to_user(arg, &bc, sizeof(bc)))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_DEVINFO ioctl
|
|
* device info
|
|
*
|
|
* arg:
|
|
* pointer to comedi_devinfo structure
|
|
*
|
|
* reads:
|
|
* nothing
|
|
*
|
|
* writes:
|
|
* comedi_devinfo structure
|
|
*/
|
|
static int do_devinfo_ioctl(struct comedi_device *dev,
|
|
struct comedi_devinfo __user *arg,
|
|
struct file *file)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
struct comedi_devinfo devinfo;
|
|
|
|
memset(&devinfo, 0, sizeof(devinfo));
|
|
|
|
/* fill devinfo structure */
|
|
devinfo.version_code = COMEDI_VERSION_CODE;
|
|
devinfo.n_subdevs = dev->n_subdevices;
|
|
strlcpy(devinfo.driver_name, dev->driver->driver_name, COMEDI_NAMELEN);
|
|
strlcpy(devinfo.board_name, dev->board_name, COMEDI_NAMELEN);
|
|
|
|
s = comedi_file_read_subdevice(file);
|
|
if (s)
|
|
devinfo.read_subdevice = s->index;
|
|
else
|
|
devinfo.read_subdevice = -1;
|
|
|
|
s = comedi_file_write_subdevice(file);
|
|
if (s)
|
|
devinfo.write_subdevice = s->index;
|
|
else
|
|
devinfo.write_subdevice = -1;
|
|
|
|
if (copy_to_user(arg, &devinfo, sizeof(devinfo)))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_SUBDINFO ioctl
|
|
* subdevices info
|
|
*
|
|
* arg:
|
|
* pointer to array of comedi_subdinfo structures
|
|
*
|
|
* reads:
|
|
* nothing
|
|
*
|
|
* writes:
|
|
* array of comedi_subdinfo structures
|
|
*/
|
|
static int do_subdinfo_ioctl(struct comedi_device *dev,
|
|
struct comedi_subdinfo __user *arg, void *file)
|
|
{
|
|
int ret, i;
|
|
struct comedi_subdinfo *tmp, *us;
|
|
struct comedi_subdevice *s;
|
|
|
|
tmp = kcalloc(dev->n_subdevices, sizeof(*tmp), GFP_KERNEL);
|
|
if (!tmp)
|
|
return -ENOMEM;
|
|
|
|
/* fill subdinfo structs */
|
|
for (i = 0; i < dev->n_subdevices; i++) {
|
|
s = &dev->subdevices[i];
|
|
us = tmp + i;
|
|
|
|
us->type = s->type;
|
|
us->n_chan = s->n_chan;
|
|
us->subd_flags = s->subdev_flags;
|
|
if (comedi_is_subdevice_running(s))
|
|
us->subd_flags |= SDF_RUNNING;
|
|
#define TIMER_nanosec 5 /* backwards compatibility */
|
|
us->timer_type = TIMER_nanosec;
|
|
us->len_chanlist = s->len_chanlist;
|
|
us->maxdata = s->maxdata;
|
|
if (s->range_table) {
|
|
us->range_type =
|
|
(i << 24) | (0 << 16) | (s->range_table->length);
|
|
} else {
|
|
us->range_type = 0; /* XXX */
|
|
}
|
|
|
|
if (s->busy)
|
|
us->subd_flags |= SDF_BUSY;
|
|
if (s->busy == file)
|
|
us->subd_flags |= SDF_BUSY_OWNER;
|
|
if (s->lock)
|
|
us->subd_flags |= SDF_LOCKED;
|
|
if (s->lock == file)
|
|
us->subd_flags |= SDF_LOCK_OWNER;
|
|
if (!s->maxdata && s->maxdata_list)
|
|
us->subd_flags |= SDF_MAXDATA;
|
|
if (s->range_table_list)
|
|
us->subd_flags |= SDF_RANGETYPE;
|
|
if (s->do_cmd)
|
|
us->subd_flags |= SDF_CMD;
|
|
|
|
if (s->insn_bits != &insn_inval)
|
|
us->insn_bits_support = COMEDI_SUPPORTED;
|
|
else
|
|
us->insn_bits_support = COMEDI_UNSUPPORTED;
|
|
}
|
|
|
|
ret = copy_to_user(arg, tmp, dev->n_subdevices * sizeof(*tmp));
|
|
|
|
kfree(tmp);
|
|
|
|
return ret ? -EFAULT : 0;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_CHANINFO ioctl
|
|
* subdevice channel info
|
|
*
|
|
* arg:
|
|
* pointer to comedi_chaninfo structure
|
|
*
|
|
* reads:
|
|
* comedi_chaninfo structure
|
|
*
|
|
* writes:
|
|
* array of maxdata values to chaninfo->maxdata_list if requested
|
|
* array of range table lengths to chaninfo->range_table_list if requested
|
|
*/
|
|
static int do_chaninfo_ioctl(struct comedi_device *dev,
|
|
struct comedi_chaninfo __user *arg)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
struct comedi_chaninfo it;
|
|
|
|
if (copy_from_user(&it, arg, sizeof(it)))
|
|
return -EFAULT;
|
|
|
|
if (it.subdev >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
s = &dev->subdevices[it.subdev];
|
|
|
|
if (it.maxdata_list) {
|
|
if (s->maxdata || !s->maxdata_list)
|
|
return -EINVAL;
|
|
if (copy_to_user(it.maxdata_list, s->maxdata_list,
|
|
s->n_chan * sizeof(unsigned int)))
|
|
return -EFAULT;
|
|
}
|
|
|
|
if (it.flaglist)
|
|
return -EINVAL; /* flaglist not supported */
|
|
|
|
if (it.rangelist) {
|
|
int i;
|
|
|
|
if (!s->range_table_list)
|
|
return -EINVAL;
|
|
for (i = 0; i < s->n_chan; i++) {
|
|
int x;
|
|
|
|
x = (dev->minor << 28) | (it.subdev << 24) | (i << 16) |
|
|
(s->range_table_list[i]->length);
|
|
if (put_user(x, it.rangelist + i))
|
|
return -EFAULT;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_BUFINFO ioctl
|
|
* buffer information
|
|
*
|
|
* arg:
|
|
* pointer to comedi_bufinfo structure
|
|
*
|
|
* reads:
|
|
* comedi_bufinfo structure
|
|
*
|
|
* writes:
|
|
* modified comedi_bufinfo structure
|
|
*/
|
|
static int do_bufinfo_ioctl(struct comedi_device *dev,
|
|
struct comedi_bufinfo __user *arg, void *file)
|
|
{
|
|
struct comedi_bufinfo bi;
|
|
struct comedi_subdevice *s;
|
|
struct comedi_async *async;
|
|
|
|
if (copy_from_user(&bi, arg, sizeof(bi)))
|
|
return -EFAULT;
|
|
|
|
if (bi.subdevice >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
|
|
s = &dev->subdevices[bi.subdevice];
|
|
|
|
async = s->async;
|
|
|
|
if (!async) {
|
|
dev_dbg(dev->class_dev,
|
|
"subdevice does not have async capability\n");
|
|
bi.buf_write_ptr = 0;
|
|
bi.buf_read_ptr = 0;
|
|
bi.buf_write_count = 0;
|
|
bi.buf_read_count = 0;
|
|
bi.bytes_read = 0;
|
|
bi.bytes_written = 0;
|
|
goto copyback;
|
|
}
|
|
if (!s->busy) {
|
|
bi.bytes_read = 0;
|
|
bi.bytes_written = 0;
|
|
goto copyback_position;
|
|
}
|
|
if (s->busy != file)
|
|
return -EACCES;
|
|
|
|
if (bi.bytes_read && !(async->cmd.flags & CMDF_WRITE)) {
|
|
bi.bytes_read = comedi_buf_read_alloc(s, bi.bytes_read);
|
|
comedi_buf_read_free(s, bi.bytes_read);
|
|
|
|
if (comedi_is_subdevice_idle(s) &&
|
|
comedi_buf_read_n_available(s) == 0) {
|
|
do_become_nonbusy(dev, s);
|
|
}
|
|
}
|
|
|
|
if (bi.bytes_written && (async->cmd.flags & CMDF_WRITE)) {
|
|
bi.bytes_written =
|
|
comedi_buf_write_alloc(s, bi.bytes_written);
|
|
comedi_buf_write_free(s, bi.bytes_written);
|
|
}
|
|
|
|
copyback_position:
|
|
bi.buf_write_count = async->buf_write_count;
|
|
bi.buf_write_ptr = async->buf_write_ptr;
|
|
bi.buf_read_count = async->buf_read_count;
|
|
bi.buf_read_ptr = async->buf_read_ptr;
|
|
|
|
copyback:
|
|
if (copy_to_user(arg, &bi, sizeof(bi)))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int check_insn_config_length(struct comedi_insn *insn,
|
|
unsigned int *data)
|
|
{
|
|
if (insn->n < 1)
|
|
return -EINVAL;
|
|
|
|
switch (data[0]) {
|
|
case INSN_CONFIG_DIO_OUTPUT:
|
|
case INSN_CONFIG_DIO_INPUT:
|
|
case INSN_CONFIG_DISARM:
|
|
case INSN_CONFIG_RESET:
|
|
if (insn->n == 1)
|
|
return 0;
|
|
break;
|
|
case INSN_CONFIG_ARM:
|
|
case INSN_CONFIG_DIO_QUERY:
|
|
case INSN_CONFIG_BLOCK_SIZE:
|
|
case INSN_CONFIG_FILTER:
|
|
case INSN_CONFIG_SERIAL_CLOCK:
|
|
case INSN_CONFIG_BIDIRECTIONAL_DATA:
|
|
case INSN_CONFIG_ALT_SOURCE:
|
|
case INSN_CONFIG_SET_COUNTER_MODE:
|
|
case INSN_CONFIG_8254_READ_STATUS:
|
|
case INSN_CONFIG_SET_ROUTING:
|
|
case INSN_CONFIG_GET_ROUTING:
|
|
case INSN_CONFIG_GET_PWM_STATUS:
|
|
case INSN_CONFIG_PWM_SET_PERIOD:
|
|
case INSN_CONFIG_PWM_GET_PERIOD:
|
|
if (insn->n == 2)
|
|
return 0;
|
|
break;
|
|
case INSN_CONFIG_SET_GATE_SRC:
|
|
case INSN_CONFIG_GET_GATE_SRC:
|
|
case INSN_CONFIG_SET_CLOCK_SRC:
|
|
case INSN_CONFIG_GET_CLOCK_SRC:
|
|
case INSN_CONFIG_SET_OTHER_SRC:
|
|
case INSN_CONFIG_GET_COUNTER_STATUS:
|
|
case INSN_CONFIG_PWM_SET_H_BRIDGE:
|
|
case INSN_CONFIG_PWM_GET_H_BRIDGE:
|
|
case INSN_CONFIG_GET_HARDWARE_BUFFER_SIZE:
|
|
if (insn->n == 3)
|
|
return 0;
|
|
break;
|
|
case INSN_CONFIG_PWM_OUTPUT:
|
|
case INSN_CONFIG_ANALOG_TRIG:
|
|
if (insn->n == 5)
|
|
return 0;
|
|
break;
|
|
case INSN_CONFIG_DIGITAL_TRIG:
|
|
if (insn->n == 6)
|
|
return 0;
|
|
break;
|
|
/*
|
|
* by default we allow the insn since we don't have checks for
|
|
* all possible cases yet
|
|
*/
|
|
default:
|
|
pr_warn("No check for data length of config insn id %i is implemented\n",
|
|
data[0]);
|
|
pr_warn("Add a check to %s in %s\n", __func__, __FILE__);
|
|
pr_warn("Assuming n=%i is correct\n", insn->n);
|
|
return 0;
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int parse_insn(struct comedi_device *dev, struct comedi_insn *insn,
|
|
unsigned int *data, void *file)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
int ret = 0;
|
|
int i;
|
|
|
|
if (insn->insn & INSN_MASK_SPECIAL) {
|
|
/* a non-subdevice instruction */
|
|
|
|
switch (insn->insn) {
|
|
case INSN_GTOD:
|
|
{
|
|
struct timeval tv;
|
|
|
|
if (insn->n != 2) {
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
do_gettimeofday(&tv);
|
|
data[0] = tv.tv_sec;
|
|
data[1] = tv.tv_usec;
|
|
ret = 2;
|
|
|
|
break;
|
|
}
|
|
case INSN_WAIT:
|
|
if (insn->n != 1 || data[0] >= 100000) {
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
udelay(data[0] / 1000);
|
|
ret = 1;
|
|
break;
|
|
case INSN_INTTRIG:
|
|
if (insn->n != 1) {
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
if (insn->subdev >= dev->n_subdevices) {
|
|
dev_dbg(dev->class_dev,
|
|
"%d not usable subdevice\n",
|
|
insn->subdev);
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
s = &dev->subdevices[insn->subdev];
|
|
if (!s->async) {
|
|
dev_dbg(dev->class_dev, "no async\n");
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
if (!s->async->inttrig) {
|
|
dev_dbg(dev->class_dev, "no inttrig\n");
|
|
ret = -EAGAIN;
|
|
break;
|
|
}
|
|
ret = s->async->inttrig(dev, s, data[0]);
|
|
if (ret >= 0)
|
|
ret = 1;
|
|
break;
|
|
default:
|
|
dev_dbg(dev->class_dev, "invalid insn\n");
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
} else {
|
|
/* a subdevice instruction */
|
|
unsigned int maxdata;
|
|
|
|
if (insn->subdev >= dev->n_subdevices) {
|
|
dev_dbg(dev->class_dev, "subdevice %d out of range\n",
|
|
insn->subdev);
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
s = &dev->subdevices[insn->subdev];
|
|
|
|
if (s->type == COMEDI_SUBD_UNUSED) {
|
|
dev_dbg(dev->class_dev, "%d not usable subdevice\n",
|
|
insn->subdev);
|
|
ret = -EIO;
|
|
goto out;
|
|
}
|
|
|
|
/* are we locked? (ioctl lock) */
|
|
if (s->lock && s->lock != file) {
|
|
dev_dbg(dev->class_dev, "device locked\n");
|
|
ret = -EACCES;
|
|
goto out;
|
|
}
|
|
|
|
ret = comedi_check_chanlist(s, 1, &insn->chanspec);
|
|
if (ret < 0) {
|
|
ret = -EINVAL;
|
|
dev_dbg(dev->class_dev, "bad chanspec\n");
|
|
goto out;
|
|
}
|
|
|
|
if (s->busy) {
|
|
ret = -EBUSY;
|
|
goto out;
|
|
}
|
|
/* This looks arbitrary. It is. */
|
|
s->busy = parse_insn;
|
|
switch (insn->insn) {
|
|
case INSN_READ:
|
|
ret = s->insn_read(dev, s, insn, data);
|
|
if (ret == -ETIMEDOUT) {
|
|
dev_dbg(dev->class_dev,
|
|
"subdevice %d read instruction timed out\n",
|
|
s->index);
|
|
}
|
|
break;
|
|
case INSN_WRITE:
|
|
maxdata = s->maxdata_list
|
|
? s->maxdata_list[CR_CHAN(insn->chanspec)]
|
|
: s->maxdata;
|
|
for (i = 0; i < insn->n; ++i) {
|
|
if (data[i] > maxdata) {
|
|
ret = -EINVAL;
|
|
dev_dbg(dev->class_dev,
|
|
"bad data value(s)\n");
|
|
break;
|
|
}
|
|
}
|
|
if (ret == 0) {
|
|
ret = s->insn_write(dev, s, insn, data);
|
|
if (ret == -ETIMEDOUT) {
|
|
dev_dbg(dev->class_dev,
|
|
"subdevice %d write instruction timed out\n",
|
|
s->index);
|
|
}
|
|
}
|
|
break;
|
|
case INSN_BITS:
|
|
if (insn->n != 2) {
|
|
ret = -EINVAL;
|
|
} else {
|
|
/*
|
|
* Most drivers ignore the base channel in
|
|
* insn->chanspec. Fix this here if
|
|
* the subdevice has <= 32 channels.
|
|
*/
|
|
unsigned int orig_mask = data[0];
|
|
unsigned int shift = 0;
|
|
|
|
if (s->n_chan <= 32) {
|
|
shift = CR_CHAN(insn->chanspec);
|
|
if (shift > 0) {
|
|
insn->chanspec = 0;
|
|
data[0] <<= shift;
|
|
data[1] <<= shift;
|
|
}
|
|
}
|
|
ret = s->insn_bits(dev, s, insn, data);
|
|
data[0] = orig_mask;
|
|
if (shift > 0)
|
|
data[1] >>= shift;
|
|
}
|
|
break;
|
|
case INSN_CONFIG:
|
|
ret = check_insn_config_length(insn, data);
|
|
if (ret)
|
|
break;
|
|
ret = s->insn_config(dev, s, insn, data);
|
|
break;
|
|
default:
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
s->busy = NULL;
|
|
}
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_INSNLIST ioctl
|
|
* synchronous instruction list
|
|
*
|
|
* arg:
|
|
* pointer to comedi_insnlist structure
|
|
*
|
|
* reads:
|
|
* comedi_insnlist structure
|
|
* array of comedi_insn structures from insnlist->insns pointer
|
|
* data (for writes) from insns[].data pointers
|
|
*
|
|
* writes:
|
|
* data (for reads) to insns[].data pointers
|
|
*/
|
|
/* arbitrary limits */
|
|
#define MAX_SAMPLES 256
|
|
static int do_insnlist_ioctl(struct comedi_device *dev,
|
|
struct comedi_insnlist __user *arg, void *file)
|
|
{
|
|
struct comedi_insnlist insnlist;
|
|
struct comedi_insn *insns = NULL;
|
|
unsigned int *data = NULL;
|
|
int i = 0;
|
|
int ret = 0;
|
|
|
|
if (copy_from_user(&insnlist, arg, sizeof(insnlist)))
|
|
return -EFAULT;
|
|
|
|
data = kmalloc_array(MAX_SAMPLES, sizeof(unsigned int), GFP_KERNEL);
|
|
if (!data) {
|
|
ret = -ENOMEM;
|
|
goto error;
|
|
}
|
|
|
|
insns = kcalloc(insnlist.n_insns, sizeof(*insns), GFP_KERNEL);
|
|
if (!insns) {
|
|
ret = -ENOMEM;
|
|
goto error;
|
|
}
|
|
|
|
if (copy_from_user(insns, insnlist.insns,
|
|
sizeof(*insns) * insnlist.n_insns)) {
|
|
dev_dbg(dev->class_dev, "copy_from_user failed\n");
|
|
ret = -EFAULT;
|
|
goto error;
|
|
}
|
|
|
|
for (i = 0; i < insnlist.n_insns; i++) {
|
|
if (insns[i].n > MAX_SAMPLES) {
|
|
dev_dbg(dev->class_dev,
|
|
"number of samples too large\n");
|
|
ret = -EINVAL;
|
|
goto error;
|
|
}
|
|
if (insns[i].insn & INSN_MASK_WRITE) {
|
|
if (copy_from_user(data, insns[i].data,
|
|
insns[i].n * sizeof(unsigned int))) {
|
|
dev_dbg(dev->class_dev,
|
|
"copy_from_user failed\n");
|
|
ret = -EFAULT;
|
|
goto error;
|
|
}
|
|
}
|
|
ret = parse_insn(dev, insns + i, data, file);
|
|
if (ret < 0)
|
|
goto error;
|
|
if (insns[i].insn & INSN_MASK_READ) {
|
|
if (copy_to_user(insns[i].data, data,
|
|
insns[i].n * sizeof(unsigned int))) {
|
|
dev_dbg(dev->class_dev,
|
|
"copy_to_user failed\n");
|
|
ret = -EFAULT;
|
|
goto error;
|
|
}
|
|
}
|
|
if (need_resched())
|
|
schedule();
|
|
}
|
|
|
|
error:
|
|
kfree(insns);
|
|
kfree(data);
|
|
|
|
if (ret < 0)
|
|
return ret;
|
|
return i;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_INSN ioctl
|
|
* synchronous instruction
|
|
*
|
|
* arg:
|
|
* pointer to comedi_insn structure
|
|
*
|
|
* reads:
|
|
* comedi_insn structure
|
|
* data (for writes) from insn->data pointer
|
|
*
|
|
* writes:
|
|
* data (for reads) to insn->data pointer
|
|
*/
|
|
static int do_insn_ioctl(struct comedi_device *dev,
|
|
struct comedi_insn __user *arg, void *file)
|
|
{
|
|
struct comedi_insn insn;
|
|
unsigned int *data = NULL;
|
|
int ret = 0;
|
|
|
|
data = kmalloc_array(MAX_SAMPLES, sizeof(unsigned int), GFP_KERNEL);
|
|
if (!data) {
|
|
ret = -ENOMEM;
|
|
goto error;
|
|
}
|
|
|
|
if (copy_from_user(&insn, arg, sizeof(insn))) {
|
|
ret = -EFAULT;
|
|
goto error;
|
|
}
|
|
|
|
/* This is where the behavior of insn and insnlist deviate. */
|
|
if (insn.n > MAX_SAMPLES)
|
|
insn.n = MAX_SAMPLES;
|
|
if (insn.insn & INSN_MASK_WRITE) {
|
|
if (copy_from_user(data,
|
|
insn.data,
|
|
insn.n * sizeof(unsigned int))) {
|
|
ret = -EFAULT;
|
|
goto error;
|
|
}
|
|
}
|
|
ret = parse_insn(dev, &insn, data, file);
|
|
if (ret < 0)
|
|
goto error;
|
|
if (insn.insn & INSN_MASK_READ) {
|
|
if (copy_to_user(insn.data,
|
|
data,
|
|
insn.n * sizeof(unsigned int))) {
|
|
ret = -EFAULT;
|
|
goto error;
|
|
}
|
|
}
|
|
ret = insn.n;
|
|
|
|
error:
|
|
kfree(data);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int __comedi_get_user_cmd(struct comedi_device *dev,
|
|
struct comedi_cmd __user *arg,
|
|
struct comedi_cmd *cmd)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
|
|
if (copy_from_user(cmd, arg, sizeof(*cmd))) {
|
|
dev_dbg(dev->class_dev, "bad cmd address\n");
|
|
return -EFAULT;
|
|
}
|
|
|
|
if (cmd->subdev >= dev->n_subdevices) {
|
|
dev_dbg(dev->class_dev, "%d no such subdevice\n", cmd->subdev);
|
|
return -ENODEV;
|
|
}
|
|
|
|
s = &dev->subdevices[cmd->subdev];
|
|
|
|
if (s->type == COMEDI_SUBD_UNUSED) {
|
|
dev_dbg(dev->class_dev, "%d not valid subdevice\n",
|
|
cmd->subdev);
|
|
return -EIO;
|
|
}
|
|
|
|
if (!s->do_cmd || !s->do_cmdtest || !s->async) {
|
|
dev_dbg(dev->class_dev,
|
|
"subdevice %d does not support commands\n",
|
|
cmd->subdev);
|
|
return -EIO;
|
|
}
|
|
|
|
/* make sure channel/gain list isn't too long */
|
|
if (cmd->chanlist_len > s->len_chanlist) {
|
|
dev_dbg(dev->class_dev, "channel/gain list too long %d > %d\n",
|
|
cmd->chanlist_len, s->len_chanlist);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* Set the CMDF_WRITE flag to the correct state if the subdevice
|
|
* supports only "read" commands or only "write" commands.
|
|
*/
|
|
switch (s->subdev_flags & (SDF_CMD_READ | SDF_CMD_WRITE)) {
|
|
case SDF_CMD_READ:
|
|
cmd->flags &= ~CMDF_WRITE;
|
|
break;
|
|
case SDF_CMD_WRITE:
|
|
cmd->flags |= CMDF_WRITE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int __comedi_get_user_chanlist(struct comedi_device *dev,
|
|
struct comedi_subdevice *s,
|
|
unsigned int __user *user_chanlist,
|
|
struct comedi_cmd *cmd)
|
|
{
|
|
unsigned int *chanlist;
|
|
int ret;
|
|
|
|
cmd->chanlist = NULL;
|
|
chanlist = memdup_user(user_chanlist,
|
|
cmd->chanlist_len * sizeof(unsigned int));
|
|
if (IS_ERR(chanlist))
|
|
return PTR_ERR(chanlist);
|
|
|
|
/* make sure each element in channel/gain list is valid */
|
|
ret = comedi_check_chanlist(s, cmd->chanlist_len, chanlist);
|
|
if (ret < 0) {
|
|
kfree(chanlist);
|
|
return ret;
|
|
}
|
|
|
|
cmd->chanlist = chanlist;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_CMD ioctl
|
|
* asynchronous acquisition command set-up
|
|
*
|
|
* arg:
|
|
* pointer to comedi_cmd structure
|
|
*
|
|
* reads:
|
|
* comedi_cmd structure
|
|
* channel/range list from cmd->chanlist pointer
|
|
*
|
|
* writes:
|
|
* possibly modified comedi_cmd structure (when -EAGAIN returned)
|
|
*/
|
|
static int do_cmd_ioctl(struct comedi_device *dev,
|
|
struct comedi_cmd __user *arg, void *file)
|
|
{
|
|
struct comedi_cmd cmd;
|
|
struct comedi_subdevice *s;
|
|
struct comedi_async *async;
|
|
unsigned int __user *user_chanlist;
|
|
int ret;
|
|
|
|
/* get the user's cmd and do some simple validation */
|
|
ret = __comedi_get_user_cmd(dev, arg, &cmd);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* save user's chanlist pointer so it can be restored later */
|
|
user_chanlist = (unsigned int __user *)cmd.chanlist;
|
|
|
|
s = &dev->subdevices[cmd.subdev];
|
|
async = s->async;
|
|
|
|
/* are we locked? (ioctl lock) */
|
|
if (s->lock && s->lock != file) {
|
|
dev_dbg(dev->class_dev, "subdevice locked\n");
|
|
return -EACCES;
|
|
}
|
|
|
|
/* are we busy? */
|
|
if (s->busy) {
|
|
dev_dbg(dev->class_dev, "subdevice busy\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
/* make sure channel/gain list isn't too short */
|
|
if (cmd.chanlist_len < 1) {
|
|
dev_dbg(dev->class_dev, "channel/gain list too short %u < 1\n",
|
|
cmd.chanlist_len);
|
|
return -EINVAL;
|
|
}
|
|
|
|
async->cmd = cmd;
|
|
async->cmd.data = NULL;
|
|
|
|
/* load channel/gain list */
|
|
ret = __comedi_get_user_chanlist(dev, s, user_chanlist, &async->cmd);
|
|
if (ret)
|
|
goto cleanup;
|
|
|
|
ret = s->do_cmdtest(dev, s, &async->cmd);
|
|
|
|
if (async->cmd.flags & CMDF_BOGUS || ret) {
|
|
dev_dbg(dev->class_dev, "test returned %d\n", ret);
|
|
cmd = async->cmd;
|
|
/* restore chanlist pointer before copying back */
|
|
cmd.chanlist = (unsigned int __force *)user_chanlist;
|
|
cmd.data = NULL;
|
|
if (copy_to_user(arg, &cmd, sizeof(cmd))) {
|
|
dev_dbg(dev->class_dev, "fault writing cmd\n");
|
|
ret = -EFAULT;
|
|
goto cleanup;
|
|
}
|
|
ret = -EAGAIN;
|
|
goto cleanup;
|
|
}
|
|
|
|
if (!async->prealloc_bufsz) {
|
|
ret = -ENOMEM;
|
|
dev_dbg(dev->class_dev, "no buffer (?)\n");
|
|
goto cleanup;
|
|
}
|
|
|
|
comedi_buf_reset(s);
|
|
|
|
async->cb_mask = COMEDI_CB_BLOCK | COMEDI_CB_CANCEL_MASK;
|
|
if (async->cmd.flags & CMDF_WAKE_EOS)
|
|
async->cb_mask |= COMEDI_CB_EOS;
|
|
|
|
comedi_update_subdevice_runflags(s, COMEDI_SRF_BUSY_MASK,
|
|
COMEDI_SRF_RUNNING);
|
|
|
|
/*
|
|
* Set s->busy _after_ setting COMEDI_SRF_RUNNING flag to avoid
|
|
* race with comedi_read() or comedi_write().
|
|
*/
|
|
s->busy = file;
|
|
ret = s->do_cmd(dev, s);
|
|
if (ret == 0)
|
|
return 0;
|
|
|
|
cleanup:
|
|
do_become_nonbusy(dev, s);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_CMDTEST ioctl
|
|
* asynchronous acquisition command testing
|
|
*
|
|
* arg:
|
|
* pointer to comedi_cmd structure
|
|
*
|
|
* reads:
|
|
* comedi_cmd structure
|
|
* channel/range list from cmd->chanlist pointer
|
|
*
|
|
* writes:
|
|
* possibly modified comedi_cmd structure
|
|
*/
|
|
static int do_cmdtest_ioctl(struct comedi_device *dev,
|
|
struct comedi_cmd __user *arg, void *file)
|
|
{
|
|
struct comedi_cmd cmd;
|
|
struct comedi_subdevice *s;
|
|
unsigned int __user *user_chanlist;
|
|
int ret;
|
|
|
|
/* get the user's cmd and do some simple validation */
|
|
ret = __comedi_get_user_cmd(dev, arg, &cmd);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* save user's chanlist pointer so it can be restored later */
|
|
user_chanlist = (unsigned int __user *)cmd.chanlist;
|
|
|
|
s = &dev->subdevices[cmd.subdev];
|
|
|
|
/* user_chanlist can be NULL for COMEDI_CMDTEST ioctl */
|
|
if (user_chanlist) {
|
|
/* load channel/gain list */
|
|
ret = __comedi_get_user_chanlist(dev, s, user_chanlist, &cmd);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
ret = s->do_cmdtest(dev, s, &cmd);
|
|
|
|
kfree(cmd.chanlist); /* free kernel copy of user chanlist */
|
|
|
|
/* restore chanlist pointer before copying back */
|
|
cmd.chanlist = (unsigned int __force *)user_chanlist;
|
|
|
|
if (copy_to_user(arg, &cmd, sizeof(cmd))) {
|
|
dev_dbg(dev->class_dev, "bad cmd address\n");
|
|
ret = -EFAULT;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_LOCK ioctl
|
|
* lock subdevice
|
|
*
|
|
* arg:
|
|
* subdevice number
|
|
*
|
|
* reads:
|
|
* nothing
|
|
*
|
|
* writes:
|
|
* nothing
|
|
*/
|
|
static int do_lock_ioctl(struct comedi_device *dev, unsigned long arg,
|
|
void *file)
|
|
{
|
|
int ret = 0;
|
|
unsigned long flags;
|
|
struct comedi_subdevice *s;
|
|
|
|
if (arg >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
s = &dev->subdevices[arg];
|
|
|
|
spin_lock_irqsave(&s->spin_lock, flags);
|
|
if (s->busy || s->lock)
|
|
ret = -EBUSY;
|
|
else
|
|
s->lock = file;
|
|
spin_unlock_irqrestore(&s->spin_lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_UNLOCK ioctl
|
|
* unlock subdevice
|
|
*
|
|
* arg:
|
|
* subdevice number
|
|
*
|
|
* reads:
|
|
* nothing
|
|
*
|
|
* writes:
|
|
* nothing
|
|
*/
|
|
static int do_unlock_ioctl(struct comedi_device *dev, unsigned long arg,
|
|
void *file)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
|
|
if (arg >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
s = &dev->subdevices[arg];
|
|
|
|
if (s->busy)
|
|
return -EBUSY;
|
|
|
|
if (s->lock && s->lock != file)
|
|
return -EACCES;
|
|
|
|
if (s->lock == file)
|
|
s->lock = NULL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_CANCEL ioctl
|
|
* cancel asynchronous acquisition
|
|
*
|
|
* arg:
|
|
* subdevice number
|
|
*
|
|
* reads:
|
|
* nothing
|
|
*
|
|
* writes:
|
|
* nothing
|
|
*/
|
|
static int do_cancel_ioctl(struct comedi_device *dev, unsigned long arg,
|
|
void *file)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
|
|
if (arg >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
s = &dev->subdevices[arg];
|
|
if (!s->async)
|
|
return -EINVAL;
|
|
|
|
if (!s->busy)
|
|
return 0;
|
|
|
|
if (s->busy != file)
|
|
return -EBUSY;
|
|
|
|
return do_cancel(dev, s);
|
|
}
|
|
|
|
/*
|
|
* COMEDI_POLL ioctl
|
|
* instructs driver to synchronize buffers
|
|
*
|
|
* arg:
|
|
* subdevice number
|
|
*
|
|
* reads:
|
|
* nothing
|
|
*
|
|
* writes:
|
|
* nothing
|
|
*/
|
|
static int do_poll_ioctl(struct comedi_device *dev, unsigned long arg,
|
|
void *file)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
|
|
if (arg >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
s = &dev->subdevices[arg];
|
|
|
|
if (!s->busy)
|
|
return 0;
|
|
|
|
if (s->busy != file)
|
|
return -EBUSY;
|
|
|
|
if (s->poll)
|
|
return s->poll(dev, s);
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_SETRSUBD ioctl
|
|
* sets the current "read" subdevice on a per-file basis
|
|
*
|
|
* arg:
|
|
* subdevice number
|
|
*
|
|
* reads:
|
|
* nothing
|
|
*
|
|
* writes:
|
|
* nothing
|
|
*/
|
|
static int do_setrsubd_ioctl(struct comedi_device *dev, unsigned long arg,
|
|
struct file *file)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_subdevice *s_old, *s_new;
|
|
|
|
if (arg >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
|
|
s_new = &dev->subdevices[arg];
|
|
s_old = comedi_file_read_subdevice(file);
|
|
if (s_old == s_new)
|
|
return 0; /* no change */
|
|
|
|
if (!(s_new->subdev_flags & SDF_CMD_READ))
|
|
return -EINVAL;
|
|
|
|
/*
|
|
* Check the file isn't still busy handling a "read" command on the
|
|
* old subdevice (if any).
|
|
*/
|
|
if (s_old && s_old->busy == file && s_old->async &&
|
|
!(s_old->async->cmd.flags & CMDF_WRITE))
|
|
return -EBUSY;
|
|
|
|
ACCESS_ONCE(cfp->read_subdev) = s_new;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* COMEDI_SETWSUBD ioctl
|
|
* sets the current "write" subdevice on a per-file basis
|
|
*
|
|
* arg:
|
|
* subdevice number
|
|
*
|
|
* reads:
|
|
* nothing
|
|
*
|
|
* writes:
|
|
* nothing
|
|
*/
|
|
static int do_setwsubd_ioctl(struct comedi_device *dev, unsigned long arg,
|
|
struct file *file)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_subdevice *s_old, *s_new;
|
|
|
|
if (arg >= dev->n_subdevices)
|
|
return -EINVAL;
|
|
|
|
s_new = &dev->subdevices[arg];
|
|
s_old = comedi_file_write_subdevice(file);
|
|
if (s_old == s_new)
|
|
return 0; /* no change */
|
|
|
|
if (!(s_new->subdev_flags & SDF_CMD_WRITE))
|
|
return -EINVAL;
|
|
|
|
/*
|
|
* Check the file isn't still busy handling a "write" command on the
|
|
* old subdevice (if any).
|
|
*/
|
|
if (s_old && s_old->busy == file && s_old->async &&
|
|
(s_old->async->cmd.flags & CMDF_WRITE))
|
|
return -EBUSY;
|
|
|
|
ACCESS_ONCE(cfp->write_subdev) = s_new;
|
|
return 0;
|
|
}
|
|
|
|
static long comedi_unlocked_ioctl(struct file *file, unsigned int cmd,
|
|
unsigned long arg)
|
|
{
|
|
unsigned minor = iminor(file_inode(file));
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
int rc;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
|
|
/*
|
|
* Device config is special, because it must work on
|
|
* an unconfigured device.
|
|
*/
|
|
if (cmd == COMEDI_DEVCONFIG) {
|
|
if (minor >= COMEDI_NUM_BOARD_MINORS) {
|
|
/* Device config not appropriate on non-board minors. */
|
|
rc = -ENOTTY;
|
|
goto done;
|
|
}
|
|
rc = do_devconfig_ioctl(dev,
|
|
(struct comedi_devconfig __user *)arg);
|
|
if (rc == 0) {
|
|
if (arg == 0 &&
|
|
dev->minor >= comedi_num_legacy_minors) {
|
|
/*
|
|
* Successfully unconfigured a dynamically
|
|
* allocated device. Try and remove it.
|
|
*/
|
|
if (comedi_clear_board_dev(dev)) {
|
|
mutex_unlock(&dev->mutex);
|
|
comedi_free_board_dev(dev);
|
|
return rc;
|
|
}
|
|
}
|
|
}
|
|
goto done;
|
|
}
|
|
|
|
if (!dev->attached) {
|
|
dev_dbg(dev->class_dev, "no driver attached\n");
|
|
rc = -ENODEV;
|
|
goto done;
|
|
}
|
|
|
|
switch (cmd) {
|
|
case COMEDI_BUFCONFIG:
|
|
rc = do_bufconfig_ioctl(dev,
|
|
(struct comedi_bufconfig __user *)arg);
|
|
break;
|
|
case COMEDI_DEVINFO:
|
|
rc = do_devinfo_ioctl(dev, (struct comedi_devinfo __user *)arg,
|
|
file);
|
|
break;
|
|
case COMEDI_SUBDINFO:
|
|
rc = do_subdinfo_ioctl(dev,
|
|
(struct comedi_subdinfo __user *)arg,
|
|
file);
|
|
break;
|
|
case COMEDI_CHANINFO:
|
|
rc = do_chaninfo_ioctl(dev, (void __user *)arg);
|
|
break;
|
|
case COMEDI_RANGEINFO:
|
|
rc = do_rangeinfo_ioctl(dev, (void __user *)arg);
|
|
break;
|
|
case COMEDI_BUFINFO:
|
|
rc = do_bufinfo_ioctl(dev,
|
|
(struct comedi_bufinfo __user *)arg,
|
|
file);
|
|
break;
|
|
case COMEDI_LOCK:
|
|
rc = do_lock_ioctl(dev, arg, file);
|
|
break;
|
|
case COMEDI_UNLOCK:
|
|
rc = do_unlock_ioctl(dev, arg, file);
|
|
break;
|
|
case COMEDI_CANCEL:
|
|
rc = do_cancel_ioctl(dev, arg, file);
|
|
break;
|
|
case COMEDI_CMD:
|
|
rc = do_cmd_ioctl(dev, (struct comedi_cmd __user *)arg, file);
|
|
break;
|
|
case COMEDI_CMDTEST:
|
|
rc = do_cmdtest_ioctl(dev, (struct comedi_cmd __user *)arg,
|
|
file);
|
|
break;
|
|
case COMEDI_INSNLIST:
|
|
rc = do_insnlist_ioctl(dev,
|
|
(struct comedi_insnlist __user *)arg,
|
|
file);
|
|
break;
|
|
case COMEDI_INSN:
|
|
rc = do_insn_ioctl(dev, (struct comedi_insn __user *)arg,
|
|
file);
|
|
break;
|
|
case COMEDI_POLL:
|
|
rc = do_poll_ioctl(dev, arg, file);
|
|
break;
|
|
case COMEDI_SETRSUBD:
|
|
rc = do_setrsubd_ioctl(dev, arg, file);
|
|
break;
|
|
case COMEDI_SETWSUBD:
|
|
rc = do_setwsubd_ioctl(dev, arg, file);
|
|
break;
|
|
default:
|
|
rc = -ENOTTY;
|
|
break;
|
|
}
|
|
|
|
done:
|
|
mutex_unlock(&dev->mutex);
|
|
return rc;
|
|
}
|
|
|
|
static void comedi_vm_open(struct vm_area_struct *area)
|
|
{
|
|
struct comedi_buf_map *bm;
|
|
|
|
bm = area->vm_private_data;
|
|
comedi_buf_map_get(bm);
|
|
}
|
|
|
|
static void comedi_vm_close(struct vm_area_struct *area)
|
|
{
|
|
struct comedi_buf_map *bm;
|
|
|
|
bm = area->vm_private_data;
|
|
comedi_buf_map_put(bm);
|
|
}
|
|
|
|
static const struct vm_operations_struct comedi_vm_ops = {
|
|
.open = comedi_vm_open,
|
|
.close = comedi_vm_close,
|
|
};
|
|
|
|
static int comedi_mmap(struct file *file, struct vm_area_struct *vma)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
struct comedi_subdevice *s;
|
|
struct comedi_async *async;
|
|
struct comedi_buf_map *bm = NULL;
|
|
unsigned long start = vma->vm_start;
|
|
unsigned long size;
|
|
int n_pages;
|
|
int i;
|
|
int retval;
|
|
|
|
/*
|
|
* 'trylock' avoids circular dependency with current->mm->mmap_sem
|
|
* and down-reading &dev->attach_lock should normally succeed without
|
|
* contention unless the device is in the process of being attached
|
|
* or detached.
|
|
*/
|
|
if (!down_read_trylock(&dev->attach_lock))
|
|
return -EAGAIN;
|
|
|
|
if (!dev->attached) {
|
|
dev_dbg(dev->class_dev, "no driver attached\n");
|
|
retval = -ENODEV;
|
|
goto done;
|
|
}
|
|
|
|
if (vma->vm_flags & VM_WRITE)
|
|
s = comedi_file_write_subdevice(file);
|
|
else
|
|
s = comedi_file_read_subdevice(file);
|
|
if (!s) {
|
|
retval = -EINVAL;
|
|
goto done;
|
|
}
|
|
|
|
async = s->async;
|
|
if (!async) {
|
|
retval = -EINVAL;
|
|
goto done;
|
|
}
|
|
|
|
if (vma->vm_pgoff != 0) {
|
|
dev_dbg(dev->class_dev, "mmap() offset must be 0.\n");
|
|
retval = -EINVAL;
|
|
goto done;
|
|
}
|
|
|
|
size = vma->vm_end - vma->vm_start;
|
|
if (size > async->prealloc_bufsz) {
|
|
retval = -EFAULT;
|
|
goto done;
|
|
}
|
|
if (size & (~PAGE_MASK)) {
|
|
retval = -EFAULT;
|
|
goto done;
|
|
}
|
|
|
|
n_pages = size >> PAGE_SHIFT;
|
|
|
|
/* get reference to current buf map (if any) */
|
|
bm = comedi_buf_map_from_subdev_get(s);
|
|
if (!bm || n_pages > bm->n_pages) {
|
|
retval = -EINVAL;
|
|
goto done;
|
|
}
|
|
for (i = 0; i < n_pages; ++i) {
|
|
struct comedi_buf_page *buf = &bm->page_list[i];
|
|
|
|
if (remap_pfn_range(vma, start,
|
|
page_to_pfn(virt_to_page(buf->virt_addr)),
|
|
PAGE_SIZE, PAGE_SHARED)) {
|
|
retval = -EAGAIN;
|
|
goto done;
|
|
}
|
|
start += PAGE_SIZE;
|
|
}
|
|
|
|
vma->vm_ops = &comedi_vm_ops;
|
|
vma->vm_private_data = bm;
|
|
|
|
vma->vm_ops->open(vma);
|
|
|
|
retval = 0;
|
|
done:
|
|
up_read(&dev->attach_lock);
|
|
comedi_buf_map_put(bm); /* put reference to buf map - okay if NULL */
|
|
return retval;
|
|
}
|
|
|
|
static unsigned int comedi_poll(struct file *file, poll_table *wait)
|
|
{
|
|
unsigned int mask = 0;
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
struct comedi_subdevice *s, *s_read;
|
|
|
|
down_read(&dev->attach_lock);
|
|
|
|
if (!dev->attached) {
|
|
dev_dbg(dev->class_dev, "no driver attached\n");
|
|
goto done;
|
|
}
|
|
|
|
s = comedi_file_read_subdevice(file);
|
|
s_read = s;
|
|
if (s && s->async) {
|
|
poll_wait(file, &s->async->wait_head, wait);
|
|
if (s->busy != file || !comedi_is_subdevice_running(s) ||
|
|
(s->async->cmd.flags & CMDF_WRITE) ||
|
|
comedi_buf_read_n_available(s) > 0)
|
|
mask |= POLLIN | POLLRDNORM;
|
|
}
|
|
|
|
s = comedi_file_write_subdevice(file);
|
|
if (s && s->async) {
|
|
unsigned int bps = comedi_bytes_per_sample(s);
|
|
|
|
if (s != s_read)
|
|
poll_wait(file, &s->async->wait_head, wait);
|
|
if (s->busy != file || !comedi_is_subdevice_running(s) ||
|
|
!(s->async->cmd.flags & CMDF_WRITE) ||
|
|
comedi_buf_write_n_available(s) >= bps)
|
|
mask |= POLLOUT | POLLWRNORM;
|
|
}
|
|
|
|
done:
|
|
up_read(&dev->attach_lock);
|
|
return mask;
|
|
}
|
|
|
|
static ssize_t comedi_write(struct file *file, const char __user *buf,
|
|
size_t nbytes, loff_t *offset)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
struct comedi_async *async;
|
|
unsigned int n, m;
|
|
ssize_t count = 0;
|
|
int retval = 0;
|
|
DECLARE_WAITQUEUE(wait, current);
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
bool become_nonbusy = false;
|
|
bool attach_locked;
|
|
unsigned int old_detach_count;
|
|
|
|
/* Protect against device detachment during operation. */
|
|
down_read(&dev->attach_lock);
|
|
attach_locked = true;
|
|
old_detach_count = dev->detach_count;
|
|
|
|
if (!dev->attached) {
|
|
dev_dbg(dev->class_dev, "no driver attached\n");
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
|
|
s = comedi_file_write_subdevice(file);
|
|
if (!s || !s->async) {
|
|
retval = -EIO;
|
|
goto out;
|
|
}
|
|
|
|
async = s->async;
|
|
if (s->busy != file || !(async->cmd.flags & CMDF_WRITE)) {
|
|
retval = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
add_wait_queue(&async->wait_head, &wait);
|
|
while (count == 0 && !retval) {
|
|
unsigned runflags;
|
|
unsigned int wp, n1, n2;
|
|
|
|
set_current_state(TASK_INTERRUPTIBLE);
|
|
|
|
runflags = comedi_get_subdevice_runflags(s);
|
|
if (!comedi_is_runflags_running(runflags)) {
|
|
if (comedi_is_runflags_in_error(runflags))
|
|
retval = -EPIPE;
|
|
if (retval || nbytes)
|
|
become_nonbusy = true;
|
|
break;
|
|
}
|
|
if (nbytes == 0)
|
|
break;
|
|
|
|
/* Allocate all free buffer space. */
|
|
comedi_buf_write_alloc(s, async->prealloc_bufsz);
|
|
m = comedi_buf_write_n_allocated(s);
|
|
n = min_t(size_t, m, nbytes);
|
|
|
|
if (n == 0) {
|
|
if (file->f_flags & O_NONBLOCK) {
|
|
retval = -EAGAIN;
|
|
break;
|
|
}
|
|
schedule();
|
|
if (signal_pending(current)) {
|
|
retval = -ERESTARTSYS;
|
|
break;
|
|
}
|
|
if (s->busy != file ||
|
|
!(async->cmd.flags & CMDF_WRITE)) {
|
|
retval = -EINVAL;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
wp = async->buf_write_ptr;
|
|
n1 = min(n, async->prealloc_bufsz - wp);
|
|
n2 = n - n1;
|
|
m = copy_from_user(async->prealloc_buf + wp, buf, n1);
|
|
if (m)
|
|
m += n2;
|
|
else if (n2)
|
|
m = copy_from_user(async->prealloc_buf, buf + n1, n2);
|
|
if (m) {
|
|
n -= m;
|
|
retval = -EFAULT;
|
|
}
|
|
comedi_buf_write_free(s, n);
|
|
|
|
count += n;
|
|
nbytes -= n;
|
|
|
|
buf += n;
|
|
}
|
|
remove_wait_queue(&async->wait_head, &wait);
|
|
set_current_state(TASK_RUNNING);
|
|
if (become_nonbusy && count == 0) {
|
|
struct comedi_subdevice *new_s;
|
|
|
|
/*
|
|
* To avoid deadlock, cannot acquire dev->mutex
|
|
* while dev->attach_lock is held.
|
|
*/
|
|
up_read(&dev->attach_lock);
|
|
attach_locked = false;
|
|
mutex_lock(&dev->mutex);
|
|
/*
|
|
* Check device hasn't become detached behind our back.
|
|
* Checking dev->detach_count is unchanged ought to be
|
|
* sufficient (unless there have been 2**32 detaches in the
|
|
* meantime!), but check the subdevice pointer as well just in
|
|
* case.
|
|
*
|
|
* Also check the subdevice is still in a suitable state to
|
|
* become non-busy in case it changed behind our back.
|
|
*/
|
|
new_s = comedi_file_write_subdevice(file);
|
|
if (dev->attached && old_detach_count == dev->detach_count &&
|
|
s == new_s && new_s->async == async && s->busy == file &&
|
|
(async->cmd.flags & CMDF_WRITE) &&
|
|
!comedi_is_subdevice_running(s))
|
|
do_become_nonbusy(dev, s);
|
|
mutex_unlock(&dev->mutex);
|
|
}
|
|
out:
|
|
if (attach_locked)
|
|
up_read(&dev->attach_lock);
|
|
|
|
return count ? count : retval;
|
|
}
|
|
|
|
static ssize_t comedi_read(struct file *file, char __user *buf, size_t nbytes,
|
|
loff_t *offset)
|
|
{
|
|
struct comedi_subdevice *s;
|
|
struct comedi_async *async;
|
|
unsigned int n, m;
|
|
ssize_t count = 0;
|
|
int retval = 0;
|
|
DECLARE_WAITQUEUE(wait, current);
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
unsigned int old_detach_count;
|
|
bool become_nonbusy = false;
|
|
bool attach_locked;
|
|
|
|
/* Protect against device detachment during operation. */
|
|
down_read(&dev->attach_lock);
|
|
attach_locked = true;
|
|
old_detach_count = dev->detach_count;
|
|
|
|
if (!dev->attached) {
|
|
dev_dbg(dev->class_dev, "no driver attached\n");
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
|
|
s = comedi_file_read_subdevice(file);
|
|
if (!s || !s->async) {
|
|
retval = -EIO;
|
|
goto out;
|
|
}
|
|
|
|
async = s->async;
|
|
if (s->busy != file || (async->cmd.flags & CMDF_WRITE)) {
|
|
retval = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
add_wait_queue(&async->wait_head, &wait);
|
|
while (count == 0 && !retval) {
|
|
unsigned int rp, n1, n2;
|
|
|
|
set_current_state(TASK_INTERRUPTIBLE);
|
|
|
|
m = comedi_buf_read_n_available(s);
|
|
n = min_t(size_t, m, nbytes);
|
|
|
|
if (n == 0) {
|
|
unsigned runflags = comedi_get_subdevice_runflags(s);
|
|
|
|
if (!comedi_is_runflags_running(runflags)) {
|
|
if (comedi_is_runflags_in_error(runflags))
|
|
retval = -EPIPE;
|
|
if (retval || nbytes)
|
|
become_nonbusy = true;
|
|
break;
|
|
}
|
|
if (nbytes == 0)
|
|
break;
|
|
if (file->f_flags & O_NONBLOCK) {
|
|
retval = -EAGAIN;
|
|
break;
|
|
}
|
|
schedule();
|
|
if (signal_pending(current)) {
|
|
retval = -ERESTARTSYS;
|
|
break;
|
|
}
|
|
if (s->busy != file ||
|
|
(async->cmd.flags & CMDF_WRITE)) {
|
|
retval = -EINVAL;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
rp = async->buf_read_ptr;
|
|
n1 = min(n, async->prealloc_bufsz - rp);
|
|
n2 = n - n1;
|
|
m = copy_to_user(buf, async->prealloc_buf + rp, n1);
|
|
if (m)
|
|
m += n2;
|
|
else if (n2)
|
|
m = copy_to_user(buf + n1, async->prealloc_buf, n2);
|
|
if (m) {
|
|
n -= m;
|
|
retval = -EFAULT;
|
|
}
|
|
|
|
comedi_buf_read_alloc(s, n);
|
|
comedi_buf_read_free(s, n);
|
|
|
|
count += n;
|
|
nbytes -= n;
|
|
|
|
buf += n;
|
|
}
|
|
remove_wait_queue(&async->wait_head, &wait);
|
|
set_current_state(TASK_RUNNING);
|
|
if (become_nonbusy && count == 0) {
|
|
struct comedi_subdevice *new_s;
|
|
|
|
/*
|
|
* To avoid deadlock, cannot acquire dev->mutex
|
|
* while dev->attach_lock is held.
|
|
*/
|
|
up_read(&dev->attach_lock);
|
|
attach_locked = false;
|
|
mutex_lock(&dev->mutex);
|
|
/*
|
|
* Check device hasn't become detached behind our back.
|
|
* Checking dev->detach_count is unchanged ought to be
|
|
* sufficient (unless there have been 2**32 detaches in the
|
|
* meantime!), but check the subdevice pointer as well just in
|
|
* case.
|
|
*
|
|
* Also check the subdevice is still in a suitable state to
|
|
* become non-busy in case it changed behind our back.
|
|
*/
|
|
new_s = comedi_file_read_subdevice(file);
|
|
if (dev->attached && old_detach_count == dev->detach_count &&
|
|
s == new_s && new_s->async == async && s->busy == file &&
|
|
!(async->cmd.flags & CMDF_WRITE) &&
|
|
!comedi_is_subdevice_running(s) &&
|
|
comedi_buf_read_n_available(s) == 0)
|
|
do_become_nonbusy(dev, s);
|
|
mutex_unlock(&dev->mutex);
|
|
}
|
|
out:
|
|
if (attach_locked)
|
|
up_read(&dev->attach_lock);
|
|
|
|
return count ? count : retval;
|
|
}
|
|
|
|
static int comedi_open(struct inode *inode, struct file *file)
|
|
{
|
|
const unsigned minor = iminor(inode);
|
|
struct comedi_file *cfp;
|
|
struct comedi_device *dev = comedi_dev_get_from_minor(minor);
|
|
int rc;
|
|
|
|
if (!dev) {
|
|
pr_debug("invalid minor number\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
cfp = kzalloc(sizeof(*cfp), GFP_KERNEL);
|
|
if (!cfp)
|
|
return -ENOMEM;
|
|
|
|
cfp->dev = dev;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
if (!dev->attached && !capable(CAP_SYS_ADMIN)) {
|
|
dev_dbg(dev->class_dev, "not attached and not CAP_SYS_ADMIN\n");
|
|
rc = -ENODEV;
|
|
goto out;
|
|
}
|
|
if (dev->attached && dev->use_count == 0) {
|
|
if (!try_module_get(dev->driver->module)) {
|
|
rc = -ENXIO;
|
|
goto out;
|
|
}
|
|
if (dev->open) {
|
|
rc = dev->open(dev);
|
|
if (rc < 0) {
|
|
module_put(dev->driver->module);
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
dev->use_count++;
|
|
file->private_data = cfp;
|
|
comedi_file_reset(file);
|
|
rc = 0;
|
|
|
|
out:
|
|
mutex_unlock(&dev->mutex);
|
|
if (rc) {
|
|
comedi_dev_put(dev);
|
|
kfree(cfp);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int comedi_fasync(int fd, struct file *file, int on)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
|
|
return fasync_helper(fd, file, on, &dev->async_queue);
|
|
}
|
|
|
|
static int comedi_close(struct inode *inode, struct file *file)
|
|
{
|
|
struct comedi_file *cfp = file->private_data;
|
|
struct comedi_device *dev = cfp->dev;
|
|
struct comedi_subdevice *s = NULL;
|
|
int i;
|
|
|
|
mutex_lock(&dev->mutex);
|
|
|
|
if (dev->subdevices) {
|
|
for (i = 0; i < dev->n_subdevices; i++) {
|
|
s = &dev->subdevices[i];
|
|
|
|
if (s->busy == file)
|
|
do_cancel(dev, s);
|
|
if (s->lock == file)
|
|
s->lock = NULL;
|
|
}
|
|
}
|
|
if (dev->attached && dev->use_count == 1) {
|
|
if (dev->close)
|
|
dev->close(dev);
|
|
module_put(dev->driver->module);
|
|
}
|
|
|
|
dev->use_count--;
|
|
|
|
mutex_unlock(&dev->mutex);
|
|
comedi_dev_put(dev);
|
|
kfree(cfp);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct file_operations comedi_fops = {
|
|
.owner = THIS_MODULE,
|
|
.unlocked_ioctl = comedi_unlocked_ioctl,
|
|
.compat_ioctl = comedi_compat_ioctl,
|
|
.open = comedi_open,
|
|
.release = comedi_close,
|
|
.read = comedi_read,
|
|
.write = comedi_write,
|
|
.mmap = comedi_mmap,
|
|
.poll = comedi_poll,
|
|
.fasync = comedi_fasync,
|
|
.llseek = noop_llseek,
|
|
};
|
|
|
|
/**
|
|
* comedi_event() - Handle events for asynchronous COMEDI command
|
|
* @dev: COMEDI device.
|
|
* @s: COMEDI subdevice.
|
|
* Context: in_interrupt() (usually), @s->spin_lock spin-lock not held.
|
|
*
|
|
* If an asynchronous COMEDI command is active on the subdevice, process
|
|
* any %COMEDI_CB_... event flags that have been set, usually by an
|
|
* interrupt handler. These may change the run state of the asynchronous
|
|
* command, wake a task, and/or send a %SIGIO signal.
|
|
*/
|
|
void comedi_event(struct comedi_device *dev, struct comedi_subdevice *s)
|
|
{
|
|
struct comedi_async *async = s->async;
|
|
unsigned int events;
|
|
int si_code = 0;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->spin_lock, flags);
|
|
|
|
events = async->events;
|
|
async->events = 0;
|
|
if (!__comedi_is_subdevice_running(s)) {
|
|
spin_unlock_irqrestore(&s->spin_lock, flags);
|
|
return;
|
|
}
|
|
|
|
if (events & COMEDI_CB_CANCEL_MASK)
|
|
__comedi_clear_subdevice_runflags(s, COMEDI_SRF_RUNNING);
|
|
|
|
/*
|
|
* Remember if an error event has occurred, so an error can be
|
|
* returned the next time the user does a read() or write().
|
|
*/
|
|
if (events & COMEDI_CB_ERROR_MASK)
|
|
__comedi_set_subdevice_runflags(s, COMEDI_SRF_ERROR);
|
|
|
|
if (async->cb_mask & events) {
|
|
wake_up_interruptible(&async->wait_head);
|
|
si_code = async->cmd.flags & CMDF_WRITE ? POLL_OUT : POLL_IN;
|
|
}
|
|
|
|
spin_unlock_irqrestore(&s->spin_lock, flags);
|
|
|
|
if (si_code)
|
|
kill_fasync(&dev->async_queue, SIGIO, si_code);
|
|
}
|
|
EXPORT_SYMBOL_GPL(comedi_event);
|
|
|
|
/* Note: the ->mutex is pre-locked on successful return */
|
|
struct comedi_device *comedi_alloc_board_minor(struct device *hardware_device)
|
|
{
|
|
struct comedi_device *dev;
|
|
struct device *csdev;
|
|
unsigned i;
|
|
|
|
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
|
|
if (!dev)
|
|
return ERR_PTR(-ENOMEM);
|
|
comedi_device_init(dev);
|
|
comedi_set_hw_dev(dev, hardware_device);
|
|
mutex_lock(&dev->mutex);
|
|
mutex_lock(&comedi_board_minor_table_lock);
|
|
for (i = hardware_device ? comedi_num_legacy_minors : 0;
|
|
i < COMEDI_NUM_BOARD_MINORS; ++i) {
|
|
if (!comedi_board_minor_table[i]) {
|
|
comedi_board_minor_table[i] = dev;
|
|
break;
|
|
}
|
|
}
|
|
mutex_unlock(&comedi_board_minor_table_lock);
|
|
if (i == COMEDI_NUM_BOARD_MINORS) {
|
|
mutex_unlock(&dev->mutex);
|
|
comedi_device_cleanup(dev);
|
|
comedi_dev_put(dev);
|
|
dev_err(hardware_device,
|
|
"ran out of minor numbers for board device files\n");
|
|
return ERR_PTR(-EBUSY);
|
|
}
|
|
dev->minor = i;
|
|
csdev = device_create(comedi_class, hardware_device,
|
|
MKDEV(COMEDI_MAJOR, i), NULL, "comedi%i", i);
|
|
if (!IS_ERR(csdev))
|
|
dev->class_dev = get_device(csdev);
|
|
|
|
/* Note: dev->mutex needs to be unlocked by the caller. */
|
|
return dev;
|
|
}
|
|
|
|
void comedi_release_hardware_device(struct device *hardware_device)
|
|
{
|
|
int minor;
|
|
struct comedi_device *dev;
|
|
|
|
for (minor = comedi_num_legacy_minors; minor < COMEDI_NUM_BOARD_MINORS;
|
|
minor++) {
|
|
mutex_lock(&comedi_board_minor_table_lock);
|
|
dev = comedi_board_minor_table[minor];
|
|
if (dev && dev->hw_dev == hardware_device) {
|
|
comedi_board_minor_table[minor] = NULL;
|
|
mutex_unlock(&comedi_board_minor_table_lock);
|
|
comedi_free_board_dev(dev);
|
|
break;
|
|
}
|
|
mutex_unlock(&comedi_board_minor_table_lock);
|
|
}
|
|
}
|
|
|
|
int comedi_alloc_subdevice_minor(struct comedi_subdevice *s)
|
|
{
|
|
struct comedi_device *dev = s->device;
|
|
struct device *csdev;
|
|
unsigned i;
|
|
|
|
mutex_lock(&comedi_subdevice_minor_table_lock);
|
|
for (i = 0; i < COMEDI_NUM_SUBDEVICE_MINORS; ++i) {
|
|
if (!comedi_subdevice_minor_table[i]) {
|
|
comedi_subdevice_minor_table[i] = s;
|
|
break;
|
|
}
|
|
}
|
|
mutex_unlock(&comedi_subdevice_minor_table_lock);
|
|
if (i == COMEDI_NUM_SUBDEVICE_MINORS) {
|
|
dev_err(dev->class_dev,
|
|
"ran out of minor numbers for subdevice files\n");
|
|
return -EBUSY;
|
|
}
|
|
i += COMEDI_NUM_BOARD_MINORS;
|
|
s->minor = i;
|
|
csdev = device_create(comedi_class, dev->class_dev,
|
|
MKDEV(COMEDI_MAJOR, i), NULL, "comedi%i_subd%i",
|
|
dev->minor, s->index);
|
|
if (!IS_ERR(csdev))
|
|
s->class_dev = csdev;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void comedi_free_subdevice_minor(struct comedi_subdevice *s)
|
|
{
|
|
unsigned int i;
|
|
|
|
if (!s)
|
|
return;
|
|
if (s->minor < COMEDI_NUM_BOARD_MINORS ||
|
|
s->minor >= COMEDI_NUM_MINORS)
|
|
return;
|
|
|
|
i = s->minor - COMEDI_NUM_BOARD_MINORS;
|
|
mutex_lock(&comedi_subdevice_minor_table_lock);
|
|
if (s == comedi_subdevice_minor_table[i])
|
|
comedi_subdevice_minor_table[i] = NULL;
|
|
mutex_unlock(&comedi_subdevice_minor_table_lock);
|
|
if (s->class_dev) {
|
|
device_destroy(comedi_class, MKDEV(COMEDI_MAJOR, s->minor));
|
|
s->class_dev = NULL;
|
|
}
|
|
}
|
|
|
|
static void comedi_cleanup_board_minors(void)
|
|
{
|
|
struct comedi_device *dev;
|
|
unsigned i;
|
|
|
|
for (i = 0; i < COMEDI_NUM_BOARD_MINORS; i++) {
|
|
dev = comedi_clear_board_minor(i);
|
|
comedi_free_board_dev(dev);
|
|
}
|
|
}
|
|
|
|
static int __init comedi_init(void)
|
|
{
|
|
int i;
|
|
int retval;
|
|
|
|
pr_info("version " COMEDI_RELEASE " - http://www.comedi.org\n");
|
|
|
|
if (comedi_num_legacy_minors < 0 ||
|
|
comedi_num_legacy_minors > COMEDI_NUM_BOARD_MINORS) {
|
|
pr_err("invalid value for module parameter \"comedi_num_legacy_minors\". Valid values are 0 through %i.\n",
|
|
COMEDI_NUM_BOARD_MINORS);
|
|
return -EINVAL;
|
|
}
|
|
|
|
retval = register_chrdev_region(MKDEV(COMEDI_MAJOR, 0),
|
|
COMEDI_NUM_MINORS, "comedi");
|
|
if (retval)
|
|
return -EIO;
|
|
cdev_init(&comedi_cdev, &comedi_fops);
|
|
comedi_cdev.owner = THIS_MODULE;
|
|
|
|
retval = kobject_set_name(&comedi_cdev.kobj, "comedi");
|
|
if (retval) {
|
|
unregister_chrdev_region(MKDEV(COMEDI_MAJOR, 0),
|
|
COMEDI_NUM_MINORS);
|
|
return retval;
|
|
}
|
|
|
|
if (cdev_add(&comedi_cdev, MKDEV(COMEDI_MAJOR, 0), COMEDI_NUM_MINORS)) {
|
|
unregister_chrdev_region(MKDEV(COMEDI_MAJOR, 0),
|
|
COMEDI_NUM_MINORS);
|
|
return -EIO;
|
|
}
|
|
comedi_class = class_create(THIS_MODULE, "comedi");
|
|
if (IS_ERR(comedi_class)) {
|
|
pr_err("failed to create class\n");
|
|
cdev_del(&comedi_cdev);
|
|
unregister_chrdev_region(MKDEV(COMEDI_MAJOR, 0),
|
|
COMEDI_NUM_MINORS);
|
|
return PTR_ERR(comedi_class);
|
|
}
|
|
|
|
comedi_class->dev_groups = comedi_dev_groups;
|
|
|
|
/* XXX requires /proc interface */
|
|
comedi_proc_init();
|
|
|
|
/* create devices files for legacy/manual use */
|
|
for (i = 0; i < comedi_num_legacy_minors; i++) {
|
|
struct comedi_device *dev;
|
|
|
|
dev = comedi_alloc_board_minor(NULL);
|
|
if (IS_ERR(dev)) {
|
|
comedi_cleanup_board_minors();
|
|
cdev_del(&comedi_cdev);
|
|
unregister_chrdev_region(MKDEV(COMEDI_MAJOR, 0),
|
|
COMEDI_NUM_MINORS);
|
|
return PTR_ERR(dev);
|
|
}
|
|
/* comedi_alloc_board_minor() locked the mutex */
|
|
mutex_unlock(&dev->mutex);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
module_init(comedi_init);
|
|
|
|
static void __exit comedi_cleanup(void)
|
|
{
|
|
comedi_cleanup_board_minors();
|
|
class_destroy(comedi_class);
|
|
cdev_del(&comedi_cdev);
|
|
unregister_chrdev_region(MKDEV(COMEDI_MAJOR, 0), COMEDI_NUM_MINORS);
|
|
|
|
comedi_proc_cleanup();
|
|
}
|
|
module_exit(comedi_cleanup);
|
|
|
|
MODULE_AUTHOR("http://www.comedi.org");
|
|
MODULE_DESCRIPTION("Comedi core module");
|
|
MODULE_LICENSE("GPL");
|