amplc_pc236.c revision 727b286b44ea359d66f47d241cc2cdad36ed7bdc
1/*
2    comedi/drivers/amplc_pc236.c
3    Driver for Amplicon PC36AT and PCI236 DIO boards.
4
5    Copyright (C) 2002 MEV Ltd. <http://www.mev.co.uk/>
6
7    COMEDI - Linux Control and Measurement Device Interface
8    Copyright (C) 2000 David A. Schleef <ds@schleef.org>
9
10    This program is free software; you can redistribute it and/or modify
11    it under the terms of the GNU General Public License as published by
12    the Free Software Foundation; either version 2 of the License, or
13    (at your option) any later version.
14
15    This program is distributed in the hope that it will be useful,
16    but WITHOUT ANY WARRANTY; without even the implied warranty of
17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18    GNU General Public License for more details.
19
20    You should have received a copy of the GNU General Public License
21    along with this program; if not, write to the Free Software
22    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23
24*/
25/*
26Driver: amplc_pc236
27Description: Amplicon PC36AT, PCI236
28Author: Ian Abbott <abbotti@mev.co.uk>
29Devices: [Amplicon] PC36AT (pc36at), PCI236 (pci236 or amplc_pc236)
30Updated: Wed, 01 Apr 2009 15:41:25 +0100
31Status: works
32
33Configuration options - PC36AT:
34  [0] - I/O port base address
35  [1] - IRQ (optional)
36
37Configuration options - PCI236:
38  [0] - PCI bus of device (optional)
39  [1] - PCI slot of device (optional)
40  If bus/slot is not specified, the first available PCI device will be
41  used.
42
43The PC36AT ISA board and PCI236 PCI board have a single 8255 appearing
44as subdevice 0.
45
46Subdevice 1 pretends to be a digital input device, but it always returns
470 when read. However, if you run a command with scan_begin_src=TRIG_EXT,
48a rising edge on port C bit 3 acts as an external trigger, which can be
49used to wake up tasks.  This is like the comedi_parport device, but the
50only way to physically disable the interrupt on the PC36AT is to remove
51the IRQ jumper.  If no interrupt is connected, then subdevice 1 is
52unused.
53*/
54
55#include <linux/interrupt.h>
56
57#include "../comedidev.h"
58
59#include "comedi_pci.h"
60
61#include "8255.h"
62#include "plx9052.h"
63
64#define PC236_DRIVER_NAME	"amplc_pc236"
65
66/* PCI236 PCI configuration register information */
67#define PCI_VENDOR_ID_AMPLICON 0x14dc
68#define PCI_DEVICE_ID_AMPLICON_PCI236 0x0009
69#define PCI_DEVICE_ID_INVALID 0xffff
70
71/* PC36AT / PCI236 registers */
72
73#define PC236_IO_SIZE		4
74#define PC236_LCR_IO_SIZE	128
75
76/*
77 * INTCSR values for PCI236.
78 */
79/* Disable interrupt, also clear any interrupt there */
80#define PCI236_INTR_DISABLE (PLX9052_INTCSR_LI1ENAB_DISABLED \
81	| PLX9052_INTCSR_LI1POL_HIGH \
82	| PLX9052_INTCSR_LI2POL_HIGH \
83	| PLX9052_INTCSR_PCIENAB_DISABLED \
84	| PLX9052_INTCSR_LI1SEL_EDGE \
85	| PLX9052_INTCSR_LI1CLRINT_ASSERTED)
86/* Enable interrupt, also clear any interrupt there. */
87#define PCI236_INTR_ENABLE (PLX9052_INTCSR_LI1ENAB_ENABLED \
88	| PLX9052_INTCSR_LI1POL_HIGH \
89	| PLX9052_INTCSR_LI2POL_HIGH \
90	| PLX9052_INTCSR_PCIENAB_ENABLED \
91	| PLX9052_INTCSR_LI1SEL_EDGE \
92	| PLX9052_INTCSR_LI1CLRINT_ASSERTED)
93
94/*
95 * Board descriptions for Amplicon PC36AT and PCI236.
96 */
97
98enum pc236_bustype { isa_bustype, pci_bustype };
99enum pc236_model { pc36at_model, pci236_model, anypci_model };
100
101struct pc236_board {
102	const char *name;
103	const char *fancy_name;
104	unsigned short devid;
105	enum pc236_bustype bustype;
106	enum pc236_model model;
107};
108static const struct pc236_board pc236_boards[] = {
109	{
110	 .name = "pc36at",
111	 .fancy_name = "PC36AT",
112	 .bustype = isa_bustype,
113	 .model = pc36at_model,
114	 },
115#ifdef CONFIG_COMEDI_PCI
116	{
117	 .name = "pci236",
118	 .fancy_name = "PCI236",
119	 .devid = PCI_DEVICE_ID_AMPLICON_PCI236,
120	 .bustype = pci_bustype,
121	 .model = pci236_model,
122	 },
123#endif
124#ifdef CONFIG_COMEDI_PCI
125	{
126	 .name = PC236_DRIVER_NAME,
127	 .fancy_name = PC236_DRIVER_NAME,
128	 .devid = PCI_DEVICE_ID_INVALID,
129	 .bustype = pci_bustype,
130	 .model = anypci_model,	/* wildcard */
131	 },
132#endif
133};
134
135#ifdef CONFIG_COMEDI_PCI
136static DEFINE_PCI_DEVICE_TABLE(pc236_pci_table) = {
137	{
138	PCI_VENDOR_ID_AMPLICON, PCI_DEVICE_ID_AMPLICON_PCI236,
139		    PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0}, {
140	0}
141};
142
143MODULE_DEVICE_TABLE(pci, pc236_pci_table);
144#endif /* CONFIG_COMEDI_PCI */
145
146/*
147 * Useful for shorthand access to the particular board structure
148 */
149#define thisboard ((const struct pc236_board *)dev->board_ptr)
150
151/* this structure is for data unique to this hardware driver.  If
152   several hardware drivers keep similar information in this structure,
153   feel free to suggest moving the variable to the struct comedi_device struct.
154 */
155struct pc236_private {
156#ifdef CONFIG_COMEDI_PCI
157	/* PCI device */
158	struct pci_dev *pci_dev;
159	unsigned long lcr_iobase; /* PLX PCI9052 config registers in PCIBAR1 */
160#endif
161	int enable_irq;
162};
163
164#define devpriv ((struct pc236_private *)dev->private)
165
166/*
167 * The struct comedi_driver structure tells the Comedi core module
168 * which functions to call to configure/deconfigure (attach/detach)
169 * the board, and also about the kernel module that contains
170 * the device code.
171 */
172static int pc236_attach(struct comedi_device *dev, struct comedi_devconfig *it);
173static int pc236_detach(struct comedi_device *dev);
174static struct comedi_driver driver_amplc_pc236 = {
175	.driver_name = PC236_DRIVER_NAME,
176	.module = THIS_MODULE,
177	.attach = pc236_attach,
178	.detach = pc236_detach,
179	.board_name = &pc236_boards[0].name,
180	.offset = sizeof(struct pc236_board),
181	.num_names = ARRAY_SIZE(pc236_boards),
182};
183
184#ifdef CONFIG_COMEDI_PCI
185static int __devinit driver_amplc_pc236_pci_probe(struct pci_dev *dev,
186						  const struct pci_device_id
187						  *ent)
188{
189	return comedi_pci_auto_config(dev, driver_amplc_pc236.driver_name);
190}
191
192static void __devexit driver_amplc_pc236_pci_remove(struct pci_dev *dev)
193{
194	comedi_pci_auto_unconfig(dev);
195}
196
197static struct pci_driver driver_amplc_pc236_pci_driver = {
198	.id_table = pc236_pci_table,
199	.probe = &driver_amplc_pc236_pci_probe,
200	.remove = __devexit_p(&driver_amplc_pc236_pci_remove)
201};
202
203static int __init driver_amplc_pc236_init_module(void)
204{
205	int retval;
206
207	retval = comedi_driver_register(&driver_amplc_pc236);
208	if (retval < 0)
209		return retval;
210
211	driver_amplc_pc236_pci_driver.name =
212	    (char *)driver_amplc_pc236.driver_name;
213	return pci_register_driver(&driver_amplc_pc236_pci_driver);
214}
215
216static void __exit driver_amplc_pc236_cleanup_module(void)
217{
218	pci_unregister_driver(&driver_amplc_pc236_pci_driver);
219	comedi_driver_unregister(&driver_amplc_pc236);
220}
221
222module_init(driver_amplc_pc236_init_module);
223module_exit(driver_amplc_pc236_cleanup_module);
224#else
225static int __init driver_amplc_pc236_init_module(void)
226{
227	return comedi_driver_register(&driver_amplc_pc236);
228}
229
230static void __exit driver_amplc_pc236_cleanup_module(void)
231{
232	comedi_driver_unregister(&driver_amplc_pc236);
233}
234
235module_init(driver_amplc_pc236_init_module);
236module_exit(driver_amplc_pc236_cleanup_module);
237#endif
238
239static int pc236_request_region(unsigned minor, unsigned long from,
240				unsigned long extent);
241static void pc236_intr_disable(struct comedi_device *dev);
242static void pc236_intr_enable(struct comedi_device *dev);
243static int pc236_intr_check(struct comedi_device *dev);
244static int pc236_intr_insn(struct comedi_device *dev,
245			   struct comedi_subdevice *s, struct comedi_insn *insn,
246			   unsigned int *data);
247static int pc236_intr_cmdtest(struct comedi_device *dev,
248			      struct comedi_subdevice *s,
249			      struct comedi_cmd *cmd);
250static int pc236_intr_cmd(struct comedi_device *dev,
251			  struct comedi_subdevice *s);
252static int pc236_intr_cancel(struct comedi_device *dev,
253			     struct comedi_subdevice *s);
254static irqreturn_t pc236_interrupt(int irq, void *d);
255
256/*
257 * This function looks for a PCI device matching the requested board name,
258 * bus and slot.
259 */
260#ifdef CONFIG_COMEDI_PCI
261static int
262pc236_find_pci(struct comedi_device *dev, int bus, int slot,
263	       struct pci_dev **pci_dev_p)
264{
265	struct pci_dev *pci_dev = NULL;
266
267	*pci_dev_p = NULL;
268
269	/* Look for matching PCI device. */
270	for (pci_dev = pci_get_device(PCI_VENDOR_ID_AMPLICON, PCI_ANY_ID, NULL);
271	     pci_dev != NULL;
272	     pci_dev = pci_get_device(PCI_VENDOR_ID_AMPLICON,
273				      PCI_ANY_ID, pci_dev)) {
274		/* If bus/slot specified, check them. */
275		if (bus || slot) {
276			if (bus != pci_dev->bus->number
277			    || slot != PCI_SLOT(pci_dev->devfn))
278				continue;
279		}
280		if (thisboard->model == anypci_model) {
281			/* Match any supported model. */
282			int i;
283
284			for (i = 0; i < ARRAY_SIZE(pc236_boards); i++) {
285				if (pc236_boards[i].bustype != pci_bustype)
286					continue;
287				if (pci_dev->device == pc236_boards[i].devid) {
288					/* Change board_ptr to matched board. */
289					dev->board_ptr = &pc236_boards[i];
290					break;
291				}
292			}
293			if (i == ARRAY_SIZE(pc236_boards))
294				continue;
295		} else {
296			/* Match specific model name. */
297			if (pci_dev->device != thisboard->devid)
298				continue;
299		}
300
301		/* Found a match. */
302		*pci_dev_p = pci_dev;
303		return 0;
304	}
305	/* No match found. */
306	if (bus || slot) {
307		printk(KERN_ERR
308		       "comedi%d: error! no %s found at pci %02x:%02x!\n",
309		       dev->minor, thisboard->name, bus, slot);
310	} else {
311		printk(KERN_ERR "comedi%d: error! no %s found!\n",
312		       dev->minor, thisboard->name);
313	}
314	return -EIO;
315}
316#endif
317
318/*
319 * Attach is called by the Comedi core to configure the driver
320 * for a particular board.  If you specified a board_name array
321 * in the driver structure, dev->board_ptr contains that
322 * address.
323 */
324static int pc236_attach(struct comedi_device *dev, struct comedi_devconfig *it)
325{
326	struct comedi_subdevice *s;
327	unsigned long iobase = 0;
328	unsigned int irq = 0;
329#ifdef CONFIG_COMEDI_PCI
330	struct pci_dev *pci_dev = NULL;
331	int bus = 0, slot = 0;
332#endif
333	int share_irq = 0;
334	int ret;
335
336	printk(KERN_DEBUG "comedi%d: %s: attach\n", dev->minor,
337	       PC236_DRIVER_NAME);
338/*
339 * Allocate the private structure area.  alloc_private() is a
340 * convenient macro defined in comedidev.h.
341 */
342	ret = alloc_private(dev, sizeof(struct pc236_private));
343	if (ret < 0) {
344		printk(KERN_ERR "comedi%d: error! out of memory!\n",
345		       dev->minor);
346		return ret;
347	}
348	/* Process options. */
349	switch (thisboard->bustype) {
350	case isa_bustype:
351		iobase = it->options[0];
352		irq = it->options[1];
353		share_irq = 0;
354		break;
355#ifdef CONFIG_COMEDI_PCI
356	case pci_bustype:
357		bus = it->options[0];
358		slot = it->options[1];
359		share_irq = 1;
360
361		ret = pc236_find_pci(dev, bus, slot, &pci_dev);
362		if (ret < 0)
363			return ret;
364		devpriv->pci_dev = pci_dev;
365		break;
366#endif /* CONFIG_COMEDI_PCI */
367	default:
368		printk(KERN_ERR
369		       "comedi%d: %s: BUG! cannot determine board type!\n",
370		       dev->minor, PC236_DRIVER_NAME);
371		return -EINVAL;
372		break;
373	}
374
375/*
376 * Initialize dev->board_name.
377 */
378	dev->board_name = thisboard->name;
379
380	/* Enable device and reserve I/O spaces. */
381#ifdef CONFIG_COMEDI_PCI
382	if (pci_dev) {
383
384		ret = comedi_pci_enable(pci_dev, PC236_DRIVER_NAME);
385		if (ret < 0) {
386			printk(KERN_ERR
387			       "comedi%d: error! cannot enable PCI device and request regions!\n",
388			       dev->minor);
389			return ret;
390		}
391		devpriv->lcr_iobase = pci_resource_start(pci_dev, 1);
392		iobase = pci_resource_start(pci_dev, 2);
393		irq = pci_dev->irq;
394	} else
395#endif
396	{
397		ret = pc236_request_region(dev->minor, iobase, PC236_IO_SIZE);
398		if (ret < 0)
399			return ret;
400	}
401	dev->iobase = iobase;
402
403/*
404 * Allocate the subdevice structures.  alloc_subdevice() is a
405 * convenient macro defined in comedidev.h.
406 */
407	ret = alloc_subdevices(dev, 2);
408	if (ret < 0) {
409		printk(KERN_ERR "comedi%d: error! out of memory!\n",
410		       dev->minor);
411		return ret;
412	}
413
414	s = dev->subdevices + 0;
415	/* digital i/o subdevice (8255) */
416	ret = subdev_8255_init(dev, s, NULL, iobase);
417	if (ret < 0) {
418		printk(KERN_ERR "comedi%d: error! out of memory!\n",
419		       dev->minor);
420		return ret;
421	}
422	s = dev->subdevices + 1;
423	dev->read_subdev = s;
424	s->type = COMEDI_SUBD_UNUSED;
425	pc236_intr_disable(dev);
426	if (irq) {
427		unsigned long flags = share_irq ? IRQF_SHARED : 0;
428
429		if (request_irq(irq, pc236_interrupt, flags,
430				PC236_DRIVER_NAME, dev) >= 0) {
431			dev->irq = irq;
432			s->type = COMEDI_SUBD_DI;
433			s->subdev_flags = SDF_READABLE | SDF_CMD_READ;
434			s->n_chan = 1;
435			s->maxdata = 1;
436			s->range_table = &range_digital;
437			s->insn_bits = pc236_intr_insn;
438			s->do_cmdtest = pc236_intr_cmdtest;
439			s->do_cmd = pc236_intr_cmd;
440			s->cancel = pc236_intr_cancel;
441		}
442	}
443	printk(KERN_INFO "comedi%d: %s ", dev->minor, dev->board_name);
444	if (thisboard->bustype == isa_bustype) {
445		printk("(base %#lx) ", iobase);
446	} else {
447#ifdef CONFIG_COMEDI_PCI
448		printk("(pci %s) ", pci_name(pci_dev));
449#endif
450	}
451	if (irq)
452		printk("(irq %u%s) ", irq, (dev->irq ? "" : " UNAVAILABLE"));
453	else
454		printk("(no irq) ");
455
456	printk("attached\n");
457
458	return 1;
459}
460
461/*
462 * _detach is called to deconfigure a device.  It should deallocate
463 * resources.
464 * This function is also called when _attach() fails, so it should be
465 * careful not to release resources that were not necessarily
466 * allocated by _attach().  dev->private and dev->subdevices are
467 * deallocated automatically by the core.
468 */
469static int pc236_detach(struct comedi_device *dev)
470{
471	printk(KERN_DEBUG "comedi%d: %s: detach\n", dev->minor,
472	       PC236_DRIVER_NAME);
473	if (devpriv)
474		pc236_intr_disable(dev);
475
476	if (dev->irq)
477		free_irq(dev->irq, dev);
478	if (dev->subdevices)
479		subdev_8255_cleanup(dev, dev->subdevices + 0);
480	if (devpriv) {
481#ifdef CONFIG_COMEDI_PCI
482		if (devpriv->pci_dev) {
483			if (dev->iobase)
484				comedi_pci_disable(devpriv->pci_dev);
485			pci_dev_put(devpriv->pci_dev);
486		} else
487#endif
488		{
489			if (dev->iobase)
490				release_region(dev->iobase, PC236_IO_SIZE);
491		}
492	}
493	if (dev->board_name) {
494		printk(KERN_INFO "comedi%d: %s removed\n",
495		       dev->minor, dev->board_name);
496	}
497	return 0;
498}
499
500/*
501 * This function checks and requests an I/O region, reporting an error
502 * if there is a conflict.
503 */
504static int pc236_request_region(unsigned minor, unsigned long from,
505				unsigned long extent)
506{
507	if (!from || !request_region(from, extent, PC236_DRIVER_NAME)) {
508		printk(KERN_ERR "comedi%d: I/O port conflict (%#lx,%lu)!\n",
509		       minor, from, extent);
510		return -EIO;
511	}
512	return 0;
513}
514
515/*
516 * This function is called to mark the interrupt as disabled (no command
517 * configured on subdevice 1) and to physically disable the interrupt
518 * (not possible on the PC36AT, except by removing the IRQ jumper!).
519 */
520static void pc236_intr_disable(struct comedi_device *dev)
521{
522	unsigned long flags;
523
524	spin_lock_irqsave(&dev->spinlock, flags);
525	devpriv->enable_irq = 0;
526#ifdef CONFIG_COMEDI_PCI
527	if (devpriv->lcr_iobase)
528		outl(PCI236_INTR_DISABLE, devpriv->lcr_iobase + PLX9052_INTCSR);
529#endif
530	spin_unlock_irqrestore(&dev->spinlock, flags);
531}
532
533/*
534 * This function is called to mark the interrupt as enabled (a command
535 * configured on subdevice 1) and to physically enable the interrupt
536 * (not possible on the PC36AT, except by (re)connecting the IRQ jumper!).
537 */
538static void pc236_intr_enable(struct comedi_device *dev)
539{
540	unsigned long flags;
541
542	spin_lock_irqsave(&dev->spinlock, flags);
543	devpriv->enable_irq = 1;
544#ifdef CONFIG_COMEDI_PCI
545	if (devpriv->lcr_iobase)
546		outl(PCI236_INTR_ENABLE, devpriv->lcr_iobase + PLX9052_INTCSR);
547#endif
548	spin_unlock_irqrestore(&dev->spinlock, flags);
549}
550
551/*
552 * This function is called when an interrupt occurs to check whether
553 * the interrupt has been marked as enabled and was generated by the
554 * board.  If so, the function prepares the hardware for the next
555 * interrupt.
556 * Returns 0 if the interrupt should be ignored.
557 */
558static int pc236_intr_check(struct comedi_device *dev)
559{
560	int retval = 0;
561	unsigned long flags;
562
563	spin_lock_irqsave(&dev->spinlock, flags);
564	if (devpriv->enable_irq) {
565		retval = 1;
566#ifdef CONFIG_COMEDI_PCI
567		if (devpriv->lcr_iobase) {
568			if ((inl(devpriv->lcr_iobase + PLX9052_INTCSR)
569			     & PLX9052_INTCSR_LI1STAT_MASK)
570			    == PLX9052_INTCSR_LI1STAT_INACTIVE) {
571				retval = 0;
572			} else {
573				/* Clear interrupt and keep it enabled. */
574				outl(PCI236_INTR_ENABLE,
575				     devpriv->lcr_iobase + PLX9052_INTCSR);
576			}
577		}
578#endif
579	}
580	spin_unlock_irqrestore(&dev->spinlock, flags);
581
582	return retval;
583}
584
585/*
586 * Input from subdevice 1.
587 * Copied from the comedi_parport driver.
588 */
589static int pc236_intr_insn(struct comedi_device *dev,
590			   struct comedi_subdevice *s, struct comedi_insn *insn,
591			   unsigned int *data)
592{
593	data[1] = 0;
594	return 2;
595}
596
597/*
598 * Subdevice 1 command test.
599 * Copied from the comedi_parport driver.
600 */
601static int pc236_intr_cmdtest(struct comedi_device *dev,
602			      struct comedi_subdevice *s,
603			      struct comedi_cmd *cmd)
604{
605	int err = 0;
606	int tmp;
607
608	/* step 1 */
609
610	tmp = cmd->start_src;
611	cmd->start_src &= TRIG_NOW;
612	if (!cmd->start_src || tmp != cmd->start_src)
613		err++;
614
615	tmp = cmd->scan_begin_src;
616	cmd->scan_begin_src &= TRIG_EXT;
617	if (!cmd->scan_begin_src || tmp != cmd->scan_begin_src)
618		err++;
619
620	tmp = cmd->convert_src;
621	cmd->convert_src &= TRIG_FOLLOW;
622	if (!cmd->convert_src || tmp != cmd->convert_src)
623		err++;
624
625	tmp = cmd->scan_end_src;
626	cmd->scan_end_src &= TRIG_COUNT;
627	if (!cmd->scan_end_src || tmp != cmd->scan_end_src)
628		err++;
629
630	tmp = cmd->stop_src;
631	cmd->stop_src &= TRIG_NONE;
632	if (!cmd->stop_src || tmp != cmd->stop_src)
633		err++;
634
635	if (err)
636		return 1;
637
638	/* step 2: ignored */
639
640	if (err)
641		return 2;
642
643	/* step 3: */
644
645	if (cmd->start_arg != 0) {
646		cmd->start_arg = 0;
647		err++;
648	}
649	if (cmd->scan_begin_arg != 0) {
650		cmd->scan_begin_arg = 0;
651		err++;
652	}
653	if (cmd->convert_arg != 0) {
654		cmd->convert_arg = 0;
655		err++;
656	}
657	if (cmd->scan_end_arg != 1) {
658		cmd->scan_end_arg = 1;
659		err++;
660	}
661	if (cmd->stop_arg != 0) {
662		cmd->stop_arg = 0;
663		err++;
664	}
665
666	if (err)
667		return 3;
668
669	/* step 4: ignored */
670
671	if (err)
672		return 4;
673
674	return 0;
675}
676
677/*
678 * Subdevice 1 command.
679 */
680static int pc236_intr_cmd(struct comedi_device *dev, struct comedi_subdevice *s)
681{
682	pc236_intr_enable(dev);
683
684	return 0;
685}
686
687/*
688 * Subdevice 1 cancel command.
689 */
690static int pc236_intr_cancel(struct comedi_device *dev,
691			     struct comedi_subdevice *s)
692{
693	pc236_intr_disable(dev);
694
695	return 0;
696}
697
698/*
699 * Interrupt service routine.
700 * Based on the comedi_parport driver.
701 */
702static irqreturn_t pc236_interrupt(int irq, void *d)
703{
704	struct comedi_device *dev = d;
705	struct comedi_subdevice *s = dev->subdevices + 1;
706	int handled;
707
708	handled = pc236_intr_check(dev);
709	if (dev->attached && handled) {
710		comedi_buf_put(s->async, 0);
711		s->async->events |= COMEDI_CB_BLOCK | COMEDI_CB_EOS;
712		comedi_event(dev, s);
713	}
714	return IRQ_RETVAL(handled);
715}
716
717MODULE_AUTHOR("Comedi http://www.comedi.org");
718MODULE_DESCRIPTION("Comedi low-level driver");
719MODULE_LICENSE("GPL");
720