| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * comedi_bond.c |
| * A Comedi driver to 'bond' or merge multiple drivers and devices as one. |
| * |
| * COMEDI - Linux Control and Measurement Device Interface |
| * Copyright (C) 2000 David A. Schleef <ds@schleef.org> |
| * Copyright (C) 2005 Calin A. Culianu <calin@ajvar.org> |
| */ |
| |
| /* |
| * Driver: comedi_bond |
| * Description: A driver to 'bond' (merge) multiple subdevices from multiple |
| * devices together as one. |
| * Devices: |
| * Author: ds |
| * Updated: Mon, 10 Oct 00:18:25 -0500 |
| * Status: works |
| * |
| * This driver allows you to 'bond' (merge) multiple comedi subdevices |
| * (coming from possibly difference boards and/or drivers) together. For |
| * example, if you had a board with 2 different DIO subdevices, and |
| * another with 1 DIO subdevice, you could 'bond' them with this driver |
| * so that they look like one big fat DIO subdevice. This makes writing |
| * applications slightly easier as you don't have to worry about managing |
| * different subdevices in the application -- you just worry about |
| * indexing one linear array of channel id's. |
| * |
| * Right now only DIO subdevices are supported as that's the personal itch |
| * I am scratching with this driver. If you want to add support for AI and AO |
| * subdevs, go right on ahead and do so! |
| * |
| * Commands aren't supported -- although it would be cool if they were. |
| * |
| * Configuration Options: |
| * List of comedi-minors to bond. All subdevices of the same type |
| * within each minor will be concatenated together in the order given here. |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/string.h> |
| #include <linux/slab.h> |
| #include "../comedi.h" |
| #include "../comedilib.h" |
| #include "../comedidev.h" |
| |
| struct bonded_device { |
| struct comedi_device *dev; |
| unsigned int minor; |
| unsigned int subdev; |
| unsigned int nchans; |
| }; |
| |
| struct comedi_bond_private { |
| char name[256]; |
| struct bonded_device **devs; |
| unsigned int ndevs; |
| unsigned int nchans; |
| }; |
| |
| static int bonding_dio_insn_bits(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| struct comedi_bond_private *devpriv = dev->private; |
| unsigned int n_left, n_done, base_chan; |
| unsigned int write_mask, data_bits; |
| struct bonded_device **devs; |
| |
| write_mask = data[0]; |
| data_bits = data[1]; |
| base_chan = CR_CHAN(insn->chanspec); |
| /* do a maximum of 32 channels, starting from base_chan. */ |
| n_left = devpriv->nchans - base_chan; |
| if (n_left > 32) |
| n_left = 32; |
| |
| n_done = 0; |
| devs = devpriv->devs; |
| do { |
| struct bonded_device *bdev = *devs++; |
| |
| if (base_chan < bdev->nchans) { |
| /* base channel falls within bonded device */ |
| unsigned int b_chans, b_mask, b_write_mask, b_data_bits; |
| int ret; |
| |
| /* |
| * Get num channels to do for bonded device and set |
| * up mask and data bits for bonded device. |
| */ |
| b_chans = bdev->nchans - base_chan; |
| if (b_chans > n_left) |
| b_chans = n_left; |
| b_mask = (b_chans < 32) ? ((1 << b_chans) - 1) |
| : 0xffffffff; |
| b_write_mask = (write_mask >> n_done) & b_mask; |
| b_data_bits = (data_bits >> n_done) & b_mask; |
| /* Read/Write the new digital lines. */ |
| ret = comedi_dio_bitfield2(bdev->dev, bdev->subdev, |
| b_write_mask, &b_data_bits, |
| base_chan); |
| if (ret < 0) |
| return ret; |
| /* Place read bits into data[1]. */ |
| data[1] &= ~(b_mask << n_done); |
| data[1] |= (b_data_bits & b_mask) << n_done; |
| /* |
| * Set up for following bonded device (if still have |
| * channels to read/write). |
| */ |
| base_chan = 0; |
| n_done += b_chans; |
| n_left -= b_chans; |
| } else { |
| /* Skip bonded devices before base channel. */ |
| base_chan -= bdev->nchans; |
| } |
| } while (n_left); |
| |
| return insn->n; |
| } |
| |
| static int bonding_dio_insn_config(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| struct comedi_bond_private *devpriv = dev->private; |
| unsigned int chan = CR_CHAN(insn->chanspec); |
| int ret; |
| struct bonded_device *bdev; |
| struct bonded_device **devs; |
| |
| /* |
| * Locate bonded subdevice and adjust channel. |
| */ |
| devs = devpriv->devs; |
| for (bdev = *devs++; chan >= bdev->nchans; bdev = *devs++) |
| chan -= bdev->nchans; |
| |
| /* |
| * The input or output configuration of each digital line is |
| * configured by a special insn_config instruction. chanspec |
| * contains the channel to be changed, and data[0] contains the |
| * configuration instruction INSN_CONFIG_DIO_OUTPUT, |
| * INSN_CONFIG_DIO_INPUT or INSN_CONFIG_DIO_QUERY. |
| * |
| * Note that INSN_CONFIG_DIO_OUTPUT == COMEDI_OUTPUT, |
| * and INSN_CONFIG_DIO_INPUT == COMEDI_INPUT. This is deliberate ;) |
| */ |
| switch (data[0]) { |
| case INSN_CONFIG_DIO_OUTPUT: |
| case INSN_CONFIG_DIO_INPUT: |
| ret = comedi_dio_config(bdev->dev, bdev->subdev, chan, data[0]); |
| break; |
| case INSN_CONFIG_DIO_QUERY: |
| ret = comedi_dio_get_config(bdev->dev, bdev->subdev, chan, |
| &data[1]); |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| if (ret >= 0) |
| ret = insn->n; |
| return ret; |
| } |
| |
| static int do_dev_config(struct comedi_device *dev, struct comedi_devconfig *it) |
| { |
| struct comedi_bond_private *devpriv = dev->private; |
| DECLARE_BITMAP(devs_opened, COMEDI_NUM_BOARD_MINORS); |
| int i; |
| |
| memset(&devs_opened, 0, sizeof(devs_opened)); |
| devpriv->name[0] = 0; |
| /* |
| * Loop through all comedi devices specified on the command-line, |
| * building our device list. |
| */ |
| for (i = 0; i < COMEDI_NDEVCONFOPTS && (!i || it->options[i]); ++i) { |
| char file[sizeof("/dev/comediXXXXXX")]; |
| int minor = it->options[i]; |
| struct comedi_device *d; |
| int sdev = -1, nchans; |
| struct bonded_device *bdev; |
| struct bonded_device **devs; |
| |
| if (minor < 0 || minor >= COMEDI_NUM_BOARD_MINORS) { |
| dev_err(dev->class_dev, |
| "Minor %d is invalid!\n", minor); |
| return -EINVAL; |
| } |
| if (minor == dev->minor) { |
| dev_err(dev->class_dev, |
| "Cannot bond this driver to itself!\n"); |
| return -EINVAL; |
| } |
| if (test_and_set_bit(minor, devs_opened)) { |
| dev_err(dev->class_dev, |
| "Minor %d specified more than once!\n", minor); |
| return -EINVAL; |
| } |
| |
| snprintf(file, sizeof(file), "/dev/comedi%d", minor); |
| file[sizeof(file) - 1] = 0; |
| |
| d = comedi_open(file); |
| |
| if (!d) { |
| dev_err(dev->class_dev, |
| "Minor %u could not be opened\n", minor); |
| return -ENODEV; |
| } |
| |
| /* Do DIO, as that's all we support now.. */ |
| while ((sdev = comedi_find_subdevice_by_type(d, COMEDI_SUBD_DIO, |
| sdev + 1)) > -1) { |
| nchans = comedi_get_n_channels(d, sdev); |
| if (nchans <= 0) { |
| dev_err(dev->class_dev, |
| "comedi_get_n_channels() returned %d on minor %u subdev %d!\n", |
| nchans, minor, sdev); |
| return -EINVAL; |
| } |
| bdev = kmalloc(sizeof(*bdev), GFP_KERNEL); |
| if (!bdev) |
| return -ENOMEM; |
| |
| bdev->dev = d; |
| bdev->minor = minor; |
| bdev->subdev = sdev; |
| bdev->nchans = nchans; |
| devpriv->nchans += nchans; |
| |
| /* |
| * Now put bdev pointer at end of devpriv->devs array |
| * list.. |
| */ |
| |
| /* ergh.. ugly.. we need to realloc :( */ |
| devs = krealloc(devpriv->devs, |
| (devpriv->ndevs + 1) * sizeof(*devs), |
| GFP_KERNEL); |
| if (!devs) { |
| dev_err(dev->class_dev, |
| "Could not allocate memory. Out of memory?\n"); |
| kfree(bdev); |
| return -ENOMEM; |
| } |
| devpriv->devs = devs; |
| devpriv->devs[devpriv->ndevs++] = bdev; |
| { |
| /* Append dev:subdev to devpriv->name */ |
| char buf[20]; |
| |
| snprintf(buf, sizeof(buf), "%u:%u ", |
| bdev->minor, bdev->subdev); |
| strlcat(devpriv->name, buf, |
| sizeof(devpriv->name)); |
| } |
| } |
| } |
| |
| if (!devpriv->nchans) { |
| dev_err(dev->class_dev, "No channels found!\n"); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int bonding_attach(struct comedi_device *dev, |
| struct comedi_devconfig *it) |
| { |
| struct comedi_bond_private *devpriv; |
| struct comedi_subdevice *s; |
| int ret; |
| |
| devpriv = comedi_alloc_devpriv(dev, sizeof(*devpriv)); |
| if (!devpriv) |
| return -ENOMEM; |
| |
| /* |
| * Setup our bonding from config params.. sets up our private struct.. |
| */ |
| ret = do_dev_config(dev, it); |
| if (ret) |
| return ret; |
| |
| dev->board_name = devpriv->name; |
| |
| ret = comedi_alloc_subdevices(dev, 1); |
| if (ret) |
| return ret; |
| |
| s = &dev->subdevices[0]; |
| s->type = COMEDI_SUBD_DIO; |
| s->subdev_flags = SDF_READABLE | SDF_WRITABLE; |
| s->n_chan = devpriv->nchans; |
| s->maxdata = 1; |
| s->range_table = &range_digital; |
| s->insn_bits = bonding_dio_insn_bits; |
| s->insn_config = bonding_dio_insn_config; |
| |
| dev_info(dev->class_dev, |
| "%s: %s attached, %u channels from %u devices\n", |
| dev->driver->driver_name, dev->board_name, |
| devpriv->nchans, devpriv->ndevs); |
| |
| return 0; |
| } |
| |
| static void bonding_detach(struct comedi_device *dev) |
| { |
| struct comedi_bond_private *devpriv = dev->private; |
| |
| if (devpriv && devpriv->devs) { |
| DECLARE_BITMAP(devs_closed, COMEDI_NUM_BOARD_MINORS); |
| |
| memset(&devs_closed, 0, sizeof(devs_closed)); |
| while (devpriv->ndevs--) { |
| struct bonded_device *bdev; |
| |
| bdev = devpriv->devs[devpriv->ndevs]; |
| if (!bdev) |
| continue; |
| if (!test_and_set_bit(bdev->minor, devs_closed)) |
| comedi_close(bdev->dev); |
| kfree(bdev); |
| } |
| kfree(devpriv->devs); |
| devpriv->devs = NULL; |
| } |
| } |
| |
| static struct comedi_driver bonding_driver = { |
| .driver_name = "comedi_bond", |
| .module = THIS_MODULE, |
| .attach = bonding_attach, |
| .detach = bonding_detach, |
| }; |
| module_comedi_driver(bonding_driver); |
| |
| MODULE_AUTHOR("Calin A. Culianu"); |
| MODULE_DESCRIPTION("comedi_bond: A driver for COMEDI to bond multiple COMEDI devices together as one."); |
| MODULE_LICENSE("GPL"); |