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(path: file); |
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(size: 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(objp: devpriv->devs, |
243 | new_size: (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(dev: bdev->dev); |
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 | |