| 1 | // SPDX-License-Identifier: GPL-2.0+ |
| 2 | /* |
| 3 | * comedi_bond.c |
| 4 | * A Comedi driver to 'bond' or merge multiple drivers and devices as one. |
| 5 | * |
| 6 | * COMEDI - Linux Control and Measurement Device Interface |
| 7 | * Copyright (C) 2000 David A. Schleef <ds@schleef.org> |
| 8 | * Copyright (C) 2005 Calin A. Culianu <calin@ajvar.org> |
| 9 | */ |
| 10 | |
| 11 | /* |
| 12 | * Driver: comedi_bond |
| 13 | * Description: A driver to 'bond' (merge) multiple subdevices from multiple |
| 14 | * devices together as one. |
| 15 | * Devices: |
| 16 | * Author: ds |
| 17 | * Updated: Mon, 10 Oct 00:18:25 -0500 |
| 18 | * Status: works |
| 19 | * |
| 20 | * This driver allows you to 'bond' (merge) multiple comedi subdevices |
| 21 | * (coming from possibly difference boards and/or drivers) together. For |
| 22 | * example, if you had a board with 2 different DIO subdevices, and |
| 23 | * another with 1 DIO subdevice, you could 'bond' them with this driver |
| 24 | * so that they look like one big fat DIO subdevice. This makes writing |
| 25 | * applications slightly easier as you don't have to worry about managing |
| 26 | * different subdevices in the application -- you just worry about |
| 27 | * indexing one linear array of channel id's. |
| 28 | * |
| 29 | * Right now only DIO subdevices are supported as that's the personal itch |
| 30 | * I am scratching with this driver. If you want to add support for AI and AO |
| 31 | * subdevs, go right on ahead and do so! |
| 32 | * |
| 33 | * Commands aren't supported -- although it would be cool if they were. |
| 34 | * |
| 35 | * Configuration Options: |
| 36 | * List of comedi-minors to bond. All subdevices of the same type |
| 37 | * within each minor will be concatenated together in the order given here. |
| 38 | */ |
| 39 | |
| 40 | #include <linux/module.h> |
| 41 | #include <linux/string.h> |
| 42 | #include <linux/slab.h> |
| 43 | #include <linux/comedi.h> |
| 44 | #include <linux/comedi/comedilib.h> |
| 45 | #include <linux/comedi/comedidev.h> |
| 46 | |
| 47 | struct bonded_device { |
| 48 | struct comedi_device *dev; |
| 49 | unsigned int minor; |
| 50 | unsigned int subdev; |
| 51 | unsigned int nchans; |
| 52 | }; |
| 53 | |
| 54 | struct comedi_bond_private { |
| 55 | char name[256]; |
| 56 | struct bonded_device **devs; |
| 57 | unsigned int ndevs; |
| 58 | unsigned int nchans; |
| 59 | }; |
| 60 | |
| 61 | static int bonding_dio_insn_bits(struct comedi_device *dev, |
| 62 | struct comedi_subdevice *s, |
| 63 | struct comedi_insn *insn, unsigned int *data) |
| 64 | { |
| 65 | struct comedi_bond_private *devpriv = dev->private; |
| 66 | unsigned int n_left, n_done, base_chan; |
| 67 | unsigned int write_mask, data_bits; |
| 68 | struct bonded_device **devs; |
| 69 | |
| 70 | write_mask = data[0]; |
| 71 | data_bits = data[1]; |
| 72 | base_chan = CR_CHAN(insn->chanspec); |
| 73 | /* do a maximum of 32 channels, starting from base_chan. */ |
| 74 | n_left = devpriv->nchans - base_chan; |
| 75 | if (n_left > 32) |
| 76 | n_left = 32; |
| 77 | |
| 78 | n_done = 0; |
| 79 | devs = devpriv->devs; |
| 80 | do { |
| 81 | struct bonded_device *bdev = *devs++; |
| 82 | |
| 83 | if (base_chan < bdev->nchans) { |
| 84 | /* base channel falls within bonded device */ |
| 85 | unsigned int b_chans, b_mask, b_write_mask, b_data_bits; |
| 86 | int ret; |
| 87 | |
| 88 | /* |
| 89 | * Get num channels to do for bonded device and set |
| 90 | * up mask and data bits for bonded device. |
| 91 | */ |
| 92 | b_chans = bdev->nchans - base_chan; |
| 93 | if (b_chans > n_left) |
| 94 | b_chans = n_left; |
| 95 | b_mask = (b_chans < 32) ? ((1 << b_chans) - 1) |
| 96 | : 0xffffffff; |
| 97 | b_write_mask = (write_mask >> n_done) & b_mask; |
| 98 | b_data_bits = (data_bits >> n_done) & b_mask; |
| 99 | /* Read/Write the new digital lines. */ |
| 100 | ret = comedi_dio_bitfield2(dev: bdev->dev, subdev: bdev->subdev, |
| 101 | mask: b_write_mask, bits: &b_data_bits, |
| 102 | base_channel: base_chan); |
| 103 | if (ret < 0) |
| 104 | return ret; |
| 105 | /* Place read bits into data[1]. */ |
| 106 | data[1] &= ~(b_mask << n_done); |
| 107 | data[1] |= (b_data_bits & b_mask) << n_done; |
| 108 | /* |
| 109 | * Set up for following bonded device (if still have |
| 110 | * channels to read/write). |
| 111 | */ |
| 112 | base_chan = 0; |
| 113 | n_done += b_chans; |
| 114 | n_left -= b_chans; |
| 115 | } else { |
| 116 | /* Skip bonded devices before base channel. */ |
| 117 | base_chan -= bdev->nchans; |
| 118 | } |
| 119 | } while (n_left); |
| 120 | |
| 121 | return insn->n; |
| 122 | } |
| 123 | |
| 124 | static int bonding_dio_insn_config(struct comedi_device *dev, |
| 125 | struct comedi_subdevice *s, |
| 126 | struct comedi_insn *insn, unsigned int *data) |
| 127 | { |
| 128 | struct comedi_bond_private *devpriv = dev->private; |
| 129 | unsigned int chan = CR_CHAN(insn->chanspec); |
| 130 | int ret; |
| 131 | struct bonded_device *bdev; |
| 132 | struct bonded_device **devs; |
| 133 | |
| 134 | /* |
| 135 | * Locate bonded subdevice and adjust channel. |
| 136 | */ |
| 137 | devs = devpriv->devs; |
| 138 | for (bdev = *devs++; chan >= bdev->nchans; bdev = *devs++) |
| 139 | chan -= bdev->nchans; |
| 140 | |
| 141 | /* |
| 142 | * The input or output configuration of each digital line is |
| 143 | * configured by a special insn_config instruction. chanspec |
| 144 | * contains the channel to be changed, and data[0] contains the |
| 145 | * configuration instruction INSN_CONFIG_DIO_OUTPUT, |
| 146 | * INSN_CONFIG_DIO_INPUT or INSN_CONFIG_DIO_QUERY. |
| 147 | * |
| 148 | * Note that INSN_CONFIG_DIO_OUTPUT == COMEDI_OUTPUT, |
| 149 | * and INSN_CONFIG_DIO_INPUT == COMEDI_INPUT. This is deliberate ;) |
| 150 | */ |
| 151 | switch (data[0]) { |
| 152 | case INSN_CONFIG_DIO_OUTPUT: |
| 153 | case INSN_CONFIG_DIO_INPUT: |
| 154 | ret = comedi_dio_config(dev: bdev->dev, subdev: bdev->subdev, chan, io: data[0]); |
| 155 | break; |
| 156 | case INSN_CONFIG_DIO_QUERY: |
| 157 | ret = comedi_dio_get_config(dev: bdev->dev, subdev: bdev->subdev, chan, |
| 158 | io: &data[1]); |
| 159 | break; |
| 160 | default: |
| 161 | ret = -EINVAL; |
| 162 | break; |
| 163 | } |
| 164 | if (ret >= 0) |
| 165 | ret = insn->n; |
| 166 | return ret; |
| 167 | } |
| 168 | |
| 169 | static int do_dev_config(struct comedi_device *dev, struct comedi_devconfig *it) |
| 170 | { |
| 171 | struct comedi_bond_private *devpriv = dev->private; |
| 172 | DECLARE_BITMAP(devs_opened, COMEDI_NUM_BOARD_MINORS); |
| 173 | int i; |
| 174 | |
| 175 | memset(&devs_opened, 0, sizeof(devs_opened)); |
| 176 | devpriv->name[0] = 0; |
| 177 | /* |
| 178 | * Loop through all comedi devices specified on the command-line, |
| 179 | * building our device list. |
| 180 | */ |
| 181 | for (i = 0; i < COMEDI_NDEVCONFOPTS && (!i || it->options[i]); ++i) { |
| 182 | char file[sizeof("/dev/comediXXXXXX" )]; |
| 183 | int minor = it->options[i]; |
| 184 | struct comedi_device *d; |
| 185 | int sdev = -1, nchans; |
| 186 | struct bonded_device *bdev; |
| 187 | struct bonded_device **devs; |
| 188 | |
| 189 | if (minor < 0 || minor >= COMEDI_NUM_BOARD_MINORS) { |
| 190 | dev_err(dev->class_dev, |
| 191 | "Minor %d is invalid!\n" , minor); |
| 192 | return -EINVAL; |
| 193 | } |
| 194 | if (minor == dev->minor) { |
| 195 | dev_err(dev->class_dev, |
| 196 | "Cannot bond this driver to itself!\n" ); |
| 197 | return -EINVAL; |
| 198 | } |
| 199 | if (test_and_set_bit(nr: minor, addr: devs_opened)) { |
| 200 | dev_err(dev->class_dev, |
| 201 | "Minor %d specified more than once!\n" , minor); |
| 202 | return -EINVAL; |
| 203 | } |
| 204 | |
| 205 | snprintf(buf: file, size: sizeof(file), fmt: "/dev/comedi%d" , minor); |
| 206 | file[sizeof(file) - 1] = 0; |
| 207 | |
| 208 | d = comedi_open_from(path: file, from: dev->minor); |
| 209 | |
| 210 | if (!d) { |
| 211 | dev_err(dev->class_dev, |
| 212 | "Minor %u could not be opened\n" , minor); |
| 213 | return -ENODEV; |
| 214 | } |
| 215 | |
| 216 | /* Do DIO, as that's all we support now.. */ |
| 217 | while ((sdev = comedi_find_subdevice_by_type(dev: d, type: COMEDI_SUBD_DIO, |
| 218 | subd: sdev + 1)) > -1) { |
| 219 | nchans = comedi_get_n_channels(dev: d, subdevice: sdev); |
| 220 | if (nchans <= 0) { |
| 221 | dev_err(dev->class_dev, |
| 222 | "comedi_get_n_channels() returned %d on minor %u subdev %d!\n" , |
| 223 | nchans, minor, sdev); |
| 224 | return -EINVAL; |
| 225 | } |
| 226 | bdev = kmalloc(sizeof(*bdev), GFP_KERNEL); |
| 227 | if (!bdev) |
| 228 | return -ENOMEM; |
| 229 | |
| 230 | bdev->dev = d; |
| 231 | bdev->minor = minor; |
| 232 | bdev->subdev = sdev; |
| 233 | bdev->nchans = nchans; |
| 234 | devpriv->nchans += nchans; |
| 235 | |
| 236 | /* |
| 237 | * Now put bdev pointer at end of devpriv->devs array |
| 238 | * list.. |
| 239 | */ |
| 240 | |
| 241 | /* ergh.. ugly.. we need to realloc :( */ |
| 242 | devs = krealloc(devpriv->devs, |
| 243 | (devpriv->ndevs + 1) * sizeof(*devs), |
| 244 | GFP_KERNEL); |
| 245 | if (!devs) { |
| 246 | dev_err(dev->class_dev, |
| 247 | "Could not allocate memory. Out of memory?\n" ); |
| 248 | kfree(objp: bdev); |
| 249 | return -ENOMEM; |
| 250 | } |
| 251 | devpriv->devs = devs; |
| 252 | devpriv->devs[devpriv->ndevs++] = bdev; |
| 253 | { |
| 254 | /* Append dev:subdev to devpriv->name */ |
| 255 | char buf[20]; |
| 256 | |
| 257 | snprintf(buf, size: sizeof(buf), fmt: "%u:%u " , |
| 258 | bdev->minor, bdev->subdev); |
| 259 | strlcat(p: devpriv->name, q: buf, |
| 260 | avail: sizeof(devpriv->name)); |
| 261 | } |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | if (!devpriv->nchans) { |
| 266 | dev_err(dev->class_dev, "No channels found!\n" ); |
| 267 | return -EINVAL; |
| 268 | } |
| 269 | |
| 270 | return 0; |
| 271 | } |
| 272 | |
| 273 | static int bonding_attach(struct comedi_device *dev, |
| 274 | struct comedi_devconfig *it) |
| 275 | { |
| 276 | struct comedi_bond_private *devpriv; |
| 277 | struct comedi_subdevice *s; |
| 278 | int ret; |
| 279 | |
| 280 | devpriv = comedi_alloc_devpriv(dev, size: sizeof(*devpriv)); |
| 281 | if (!devpriv) |
| 282 | return -ENOMEM; |
| 283 | |
| 284 | /* |
| 285 | * Setup our bonding from config params.. sets up our private struct.. |
| 286 | */ |
| 287 | ret = do_dev_config(dev, it); |
| 288 | if (ret) |
| 289 | return ret; |
| 290 | |
| 291 | dev->board_name = devpriv->name; |
| 292 | |
| 293 | ret = comedi_alloc_subdevices(dev, num_subdevices: 1); |
| 294 | if (ret) |
| 295 | return ret; |
| 296 | |
| 297 | s = &dev->subdevices[0]; |
| 298 | s->type = COMEDI_SUBD_DIO; |
| 299 | s->subdev_flags = SDF_READABLE | SDF_WRITABLE; |
| 300 | s->n_chan = devpriv->nchans; |
| 301 | s->maxdata = 1; |
| 302 | s->range_table = &range_digital; |
| 303 | s->insn_bits = bonding_dio_insn_bits; |
| 304 | s->insn_config = bonding_dio_insn_config; |
| 305 | |
| 306 | dev_info(dev->class_dev, |
| 307 | "%s: %s attached, %u channels from %u devices\n" , |
| 308 | dev->driver->driver_name, dev->board_name, |
| 309 | devpriv->nchans, devpriv->ndevs); |
| 310 | |
| 311 | return 0; |
| 312 | } |
| 313 | |
| 314 | static void bonding_detach(struct comedi_device *dev) |
| 315 | { |
| 316 | struct comedi_bond_private *devpriv = dev->private; |
| 317 | |
| 318 | if (devpriv && devpriv->devs) { |
| 319 | DECLARE_BITMAP(devs_closed, COMEDI_NUM_BOARD_MINORS); |
| 320 | |
| 321 | memset(&devs_closed, 0, sizeof(devs_closed)); |
| 322 | while (devpriv->ndevs--) { |
| 323 | struct bonded_device *bdev; |
| 324 | |
| 325 | bdev = devpriv->devs[devpriv->ndevs]; |
| 326 | if (!bdev) |
| 327 | continue; |
| 328 | if (!test_and_set_bit(nr: bdev->minor, addr: devs_closed)) |
| 329 | comedi_close_from(dev: bdev->dev, from: dev->minor); |
| 330 | kfree(objp: bdev); |
| 331 | } |
| 332 | kfree(objp: devpriv->devs); |
| 333 | devpriv->devs = NULL; |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | static struct comedi_driver bonding_driver = { |
| 338 | .driver_name = "comedi_bond" , |
| 339 | .module = THIS_MODULE, |
| 340 | .attach = bonding_attach, |
| 341 | .detach = bonding_detach, |
| 342 | }; |
| 343 | module_comedi_driver(bonding_driver); |
| 344 | |
| 345 | MODULE_AUTHOR("Calin A. Culianu" ); |
| 346 | MODULE_DESCRIPTION("comedi_bond: A driver for COMEDI to bond multiple COMEDI devices together as one." ); |
| 347 | MODULE_LICENSE("GPL" ); |
| 348 | |