Commit | Line | Data |
---|---|---|
3c501880 PP |
1 | /* |
2 | comedi/drivers/dmm32at.c | |
3 | Diamond Systems mm32at code for a Comedi driver | |
4 | ||
5 | COMEDI - Linux Control and Measurement Device Interface | |
6 | Copyright (C) 2000 David A. Schleef <ds@schleef.org> | |
7 | ||
8 | This program is free software; you can redistribute it and/or modify | |
9 | it under the terms of the GNU General Public License as published by | |
10 | the Free Software Foundation; either version 2 of the License, or | |
11 | (at your option) any later version. | |
12 | ||
13 | This program is distributed in the hope that it will be useful, | |
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 | GNU General Public License for more details. | |
17 | ||
18 | You should have received a copy of the GNU General Public License | |
19 | along with this program; if not, write to the Free Software | |
20 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. | |
21 | ||
22 | */ | |
23 | /* | |
24 | Driver: dmm32at | |
25 | Description: Diamond Systems mm32at driver. | |
26 | Devices: | |
27 | Author: Perry J. Piplani <perry.j.piplani@nasa.gov> | |
28 | Updated: Fri Jun 4 09:13:24 CDT 2004 | |
29 | Status: experimental | |
30 | ||
31 | This driver is for the Diamond Systems MM-32-AT board | |
32 | http://www.diamondsystems.com/products/diamondmm32at It is being used | |
33 | on serveral projects inside NASA, without problems so far. For analog | |
34 | input commands, TRIG_EXT is not yet supported at all.. | |
35 | ||
36 | Configuration Options: | |
37 | comedi_config /dev/comedi0 dmm32at baseaddr,irq | |
38 | */ | |
39 | ||
25436dc9 | 40 | #include <linux/interrupt.h> |
3c501880 PP |
41 | #include "../comedidev.h" |
42 | #include <linux/ioport.h> | |
43 | ||
44 | /* Board register addresses */ | |
45 | ||
46 | #define DMM32AT_MEMSIZE 0x10 | |
47 | ||
48 | #define DMM32AT_CONV 0x00 | |
49 | #define DMM32AT_AILSB 0x00 | |
50 | #define DMM32AT_AUXDOUT 0x01 | |
51 | #define DMM32AT_AIMSB 0x01 | |
52 | #define DMM32AT_AILOW 0x02 | |
53 | #define DMM32AT_AIHIGH 0x03 | |
54 | ||
55 | #define DMM32AT_DACLSB 0x04 | |
56 | #define DMM32AT_DACSTAT 0x04 | |
57 | #define DMM32AT_DACMSB 0x05 | |
58 | ||
59 | #define DMM32AT_FIFOCNTRL 0x07 | |
60 | #define DMM32AT_FIFOSTAT 0x07 | |
61 | ||
62 | #define DMM32AT_CNTRL 0x08 | |
63 | #define DMM32AT_AISTAT 0x08 | |
64 | ||
65 | #define DMM32AT_INTCLOCK 0x09 | |
66 | ||
67 | #define DMM32AT_CNTRDIO 0x0a | |
68 | ||
69 | #define DMM32AT_AICONF 0x0b | |
70 | #define DMM32AT_AIRBACK 0x0b | |
71 | ||
72 | #define DMM32AT_CLK1 0x0d | |
73 | #define DMM32AT_CLK2 0x0e | |
74 | #define DMM32AT_CLKCT 0x0f | |
75 | ||
76 | #define DMM32AT_DIOA 0x0c | |
77 | #define DMM32AT_DIOB 0x0d | |
78 | #define DMM32AT_DIOC 0x0e | |
79 | #define DMM32AT_DIOCONF 0x0f | |
80 | ||
f7cbd7aa BP |
81 | #define dmm_inb(cdev, reg) inb((cdev->iobase)+reg) |
82 | #define dmm_outb(cdev, reg, valu) outb(valu, (cdev->iobase)+reg) | |
3c501880 PP |
83 | |
84 | /* Board register values. */ | |
85 | ||
86 | /* DMM32AT_DACSTAT 0x04 */ | |
87 | #define DMM32AT_DACBUSY 0x80 | |
88 | ||
89 | /* DMM32AT_FIFOCNTRL 0x07 */ | |
90 | #define DMM32AT_FIFORESET 0x02 | |
91 | #define DMM32AT_SCANENABLE 0x04 | |
92 | ||
93 | /* DMM32AT_CNTRL 0x08 */ | |
94 | #define DMM32AT_RESET 0x20 | |
95 | #define DMM32AT_INTRESET 0x08 | |
96 | #define DMM32AT_CLKACC 0x00 | |
97 | #define DMM32AT_DIOACC 0x01 | |
98 | ||
99 | /* DMM32AT_AISTAT 0x08 */ | |
100 | #define DMM32AT_STATUS 0x80 | |
101 | ||
102 | /* DMM32AT_INTCLOCK 0x09 */ | |
103 | #define DMM32AT_ADINT 0x80 | |
104 | #define DMM32AT_CLKSEL 0x03 | |
105 | ||
106 | /* DMM32AT_CNTRDIO 0x0a */ | |
107 | #define DMM32AT_FREQ12 0x80 | |
108 | ||
109 | /* DMM32AT_AICONF 0x0b */ | |
110 | #define DMM32AT_RANGE_U10 0x0c | |
111 | #define DMM32AT_RANGE_U5 0x0d | |
112 | #define DMM32AT_RANGE_B10 0x08 | |
113 | #define DMM32AT_RANGE_B5 0x00 | |
114 | #define DMM32AT_SCINT_20 0x00 | |
115 | #define DMM32AT_SCINT_15 0x10 | |
116 | #define DMM32AT_SCINT_10 0x20 | |
117 | #define DMM32AT_SCINT_5 0x30 | |
118 | ||
119 | /* DMM32AT_CLKCT 0x0f */ | |
120 | #define DMM32AT_CLKCT1 0x56 /* mode3 counter 1 - write low byte only */ | |
121 | #define DMM32AT_CLKCT2 0xb6 /* mode3 counter 2 - write high and low byte */ | |
122 | ||
123 | /* DMM32AT_DIOCONF 0x0f */ | |
124 | #define DMM32AT_DIENABLE 0x80 | |
125 | #define DMM32AT_DIRA 0x10 | |
126 | #define DMM32AT_DIRB 0x02 | |
127 | #define DMM32AT_DIRCL 0x01 | |
128 | #define DMM32AT_DIRCH 0x08 | |
129 | ||
130 | /* board AI ranges in comedi structure */ | |
9ced1de6 | 131 | static const struct comedi_lrange dmm32at_airanges = { |
3c501880 PP |
132 | 4, |
133 | { | |
0a85b6f0 MT |
134 | UNI_RANGE(10), |
135 | UNI_RANGE(5), | |
136 | BIP_RANGE(10), | |
137 | BIP_RANGE(5), | |
138 | } | |
3c501880 PP |
139 | }; |
140 | ||
141 | /* register values for above ranges */ | |
142 | static const unsigned char dmm32at_rangebits[] = { | |
143 | DMM32AT_RANGE_U10, | |
144 | DMM32AT_RANGE_U5, | |
145 | DMM32AT_RANGE_B10, | |
146 | DMM32AT_RANGE_B5, | |
147 | }; | |
148 | ||
149 | /* only one of these ranges is valid, as set by a jumper on the | |
150 | * board. The application should only use the range set by the jumper | |
151 | */ | |
9ced1de6 | 152 | static const struct comedi_lrange dmm32at_aoranges = { |
3c501880 PP |
153 | 4, |
154 | { | |
0a85b6f0 MT |
155 | UNI_RANGE(10), |
156 | UNI_RANGE(5), | |
157 | BIP_RANGE(10), | |
158 | BIP_RANGE(5), | |
159 | } | |
3c501880 PP |
160 | }; |
161 | ||
162 | /* | |
163 | * Board descriptions for two imaginary boards. Describing the | |
164 | * boards in this way is optional, and completely driver-dependent. | |
165 | * Some drivers use arrays such as this, other do not. | |
166 | */ | |
38baea3a | 167 | struct dmm32at_board { |
3c501880 PP |
168 | const char *name; |
169 | int ai_chans; | |
170 | int ai_bits; | |
9ced1de6 | 171 | const struct comedi_lrange *ai_ranges; |
3c501880 PP |
172 | int ao_chans; |
173 | int ao_bits; | |
9ced1de6 | 174 | const struct comedi_lrange *ao_ranges; |
3c501880 PP |
175 | int have_dio; |
176 | int dio_chans; | |
38baea3a BP |
177 | }; |
178 | static const struct dmm32at_board dmm32at_boards[] = { | |
3c501880 | 179 | { |
0a85b6f0 MT |
180 | .name = "dmm32at", |
181 | .ai_chans = 32, | |
182 | .ai_bits = 16, | |
183 | .ai_ranges = &dmm32at_airanges, | |
184 | .ao_chans = 4, | |
185 | .ao_bits = 12, | |
186 | .ao_ranges = &dmm32at_aoranges, | |
187 | .have_dio = 1, | |
188 | .dio_chans = 24, | |
189 | }, | |
3c501880 PP |
190 | }; |
191 | ||
3c501880 PP |
192 | /* this structure is for data unique to this hardware driver. If |
193 | * several hardware drivers keep similar information in this structure, | |
71b5f4f1 | 194 | * feel free to suggest moving the variable to the struct comedi_device struct. |
3c501880 | 195 | */ |
39d31e09 | 196 | struct dmm32at_private { |
3c501880 PP |
197 | |
198 | int data; | |
199 | int ai_inuse; | |
200 | unsigned int ai_scans_left; | |
201 | ||
202 | /* Used for AO readback */ | |
790c5541 | 203 | unsigned int ao_readback[4]; |
3c501880 PP |
204 | unsigned char dio_config; |
205 | ||
39d31e09 | 206 | }; |
3c501880 PP |
207 | |
208 | /* | |
209 | * most drivers define the following macro to make it easy to | |
210 | * access the private structure. | |
211 | */ | |
39d31e09 | 212 | #define devpriv ((struct dmm32at_private *)dev->private) |
3c501880 | 213 | |
3c501880 | 214 | /* prototypes for driver functions below */ |
0a85b6f0 MT |
215 | static int dmm32at_ai_rinsn(struct comedi_device *dev, |
216 | struct comedi_subdevice *s, | |
217 | struct comedi_insn *insn, unsigned int *data); | |
218 | static int dmm32at_ao_winsn(struct comedi_device *dev, | |
219 | struct comedi_subdevice *s, | |
220 | struct comedi_insn *insn, unsigned int *data); | |
221 | static int dmm32at_ao_rinsn(struct comedi_device *dev, | |
222 | struct comedi_subdevice *s, | |
223 | struct comedi_insn *insn, unsigned int *data); | |
224 | static int dmm32at_dio_insn_bits(struct comedi_device *dev, | |
225 | struct comedi_subdevice *s, | |
226 | struct comedi_insn *insn, unsigned int *data); | |
227 | static int dmm32at_dio_insn_config(struct comedi_device *dev, | |
228 | struct comedi_subdevice *s, | |
229 | struct comedi_insn *insn, | |
230 | unsigned int *data); | |
231 | static int dmm32at_ai_cmdtest(struct comedi_device *dev, | |
232 | struct comedi_subdevice *s, | |
233 | struct comedi_cmd *cmd); | |
234 | static int dmm32at_ai_cmd(struct comedi_device *dev, | |
235 | struct comedi_subdevice *s); | |
236 | static int dmm32at_ai_cancel(struct comedi_device *dev, | |
237 | struct comedi_subdevice *s); | |
3c501880 | 238 | static int dmm32at_ns_to_timer(unsigned int *ns, int round); |
70265d24 | 239 | static irqreturn_t dmm32at_isr(int irq, void *d); |
814900c9 | 240 | void dmm32at_setaitimer(struct comedi_device *dev, unsigned int nansec); |
3c501880 PP |
241 | |
242 | /* | |
243 | * Attach is called by the Comedi core to configure the driver | |
244 | * for a particular board. If you specified a board_name array | |
245 | * in the driver structure, dev->board_ptr contains that | |
246 | * address. | |
247 | */ | |
0a85b6f0 MT |
248 | static int dmm32at_attach(struct comedi_device *dev, |
249 | struct comedi_devconfig *it) | |
3c501880 | 250 | { |
df9d3349 | 251 | const struct dmm32at_board *board = comedi_board(dev); |
3c501880 | 252 | int ret; |
34c43922 | 253 | struct comedi_subdevice *s; |
3c501880 PP |
254 | unsigned char aihi, ailo, fifostat, aistat, intstat, airback; |
255 | unsigned long iobase; | |
256 | unsigned int irq; | |
257 | ||
258 | iobase = it->options[0]; | |
259 | irq = it->options[1]; | |
260 | ||
27bf0bc9 M |
261 | printk(KERN_INFO "comedi%d: dmm32at: attaching\n", dev->minor); |
262 | printk(KERN_DEBUG "dmm32at: probing at address 0x%04lx, irq %u\n", | |
263 | iobase, irq); | |
3c501880 PP |
264 | |
265 | /* register address space */ | |
df9d3349 | 266 | if (!request_region(iobase, DMM32AT_MEMSIZE, board->name)) { |
27bf0bc9 M |
267 | printk(KERN_ERR "comedi%d: dmm32at: I/O port conflict\n", |
268 | dev->minor); | |
3c501880 PP |
269 | return -EIO; |
270 | } | |
271 | dev->iobase = iobase; | |
272 | ||
273 | /* the following just makes sure the board is there and gets | |
274 | it to a known state */ | |
275 | ||
276 | /* reset the board */ | |
277 | dmm_outb(dev, DMM32AT_CNTRL, DMM32AT_RESET); | |
278 | ||
279 | /* allow a millisecond to reset */ | |
280 | udelay(1000); | |
281 | ||
282 | /* zero scan and fifo control */ | |
283 | dmm_outb(dev, DMM32AT_FIFOCNTRL, 0x0); | |
284 | ||
285 | /* zero interrupt and clock control */ | |
286 | dmm_outb(dev, DMM32AT_INTCLOCK, 0x0); | |
287 | ||
288 | /* write a test channel range, the high 3 bits should drop */ | |
289 | dmm_outb(dev, DMM32AT_AILOW, 0x80); | |
290 | dmm_outb(dev, DMM32AT_AIHIGH, 0xff); | |
291 | ||
292 | /* set the range at 10v unipolar */ | |
293 | dmm_outb(dev, DMM32AT_AICONF, DMM32AT_RANGE_U10); | |
294 | ||
295 | /* should take 10 us to settle, here's a hundred */ | |
296 | udelay(100); | |
297 | ||
298 | /* read back the values */ | |
299 | ailo = dmm_inb(dev, DMM32AT_AILOW); | |
300 | aihi = dmm_inb(dev, DMM32AT_AIHIGH); | |
301 | fifostat = dmm_inb(dev, DMM32AT_FIFOSTAT); | |
302 | aistat = dmm_inb(dev, DMM32AT_AISTAT); | |
303 | intstat = dmm_inb(dev, DMM32AT_INTCLOCK); | |
304 | airback = dmm_inb(dev, DMM32AT_AIRBACK); | |
305 | ||
27bf0bc9 | 306 | printk(KERN_DEBUG "dmm32at: lo=0x%02x hi=0x%02x fifostat=0x%02x\n", |
0a85b6f0 | 307 | ailo, aihi, fifostat); |
27bf0bc9 M |
308 | printk(KERN_DEBUG |
309 | "dmm32at: aistat=0x%02x intstat=0x%02x airback=0x%02x\n", | |
0a85b6f0 | 310 | aistat, intstat, airback); |
3c501880 PP |
311 | |
312 | if ((ailo != 0x00) || (aihi != 0x1f) || (fifostat != 0x80) || | |
0a85b6f0 | 313 | (aistat != 0x60 || (intstat != 0x00) || airback != 0x0c)) { |
27bf0bc9 | 314 | printk(KERN_ERR "dmmat32: board detection failed\n"); |
3c501880 PP |
315 | return -EIO; |
316 | } | |
317 | ||
318 | /* board is there, register interrupt */ | |
319 | if (irq) { | |
df9d3349 | 320 | ret = request_irq(irq, dmm32at_isr, 0, board->name, dev); |
3c501880 | 321 | if (ret < 0) { |
27bf0bc9 | 322 | printk(KERN_ERR "dmm32at: irq conflict\n"); |
3c501880 PP |
323 | return ret; |
324 | } | |
325 | dev->irq = irq; | |
326 | } | |
327 | ||
df9d3349 | 328 | dev->board_name = board->name; |
3c501880 PP |
329 | |
330 | /* | |
331 | * Allocate the private structure area. alloc_private() is a | |
332 | * convenient macro defined in comedidev.h. | |
333 | */ | |
39d31e09 | 334 | if (alloc_private(dev, sizeof(struct dmm32at_private)) < 0) |
3c501880 PP |
335 | return -ENOMEM; |
336 | ||
8b6c5694 HS |
337 | ret = comedi_alloc_subdevices(dev, 3); |
338 | if (ret) | |
339 | return ret; | |
3c501880 PP |
340 | |
341 | s = dev->subdevices + 0; | |
342 | dev->read_subdev = s; | |
343 | /* analog input subdevice */ | |
344 | s->type = COMEDI_SUBD_AI; | |
345 | /* we support single-ended (ground) and differential */ | |
346 | s->subdev_flags = SDF_READABLE | SDF_GROUND | SDF_DIFF | SDF_CMD_READ; | |
df9d3349 HS |
347 | s->n_chan = board->ai_chans; |
348 | s->maxdata = (1 << board->ai_bits) - 1; | |
349 | s->range_table = board->ai_ranges; | |
3c501880 PP |
350 | s->len_chanlist = 32; /* This is the maximum chanlist length that |
351 | the board can handle */ | |
352 | s->insn_read = dmm32at_ai_rinsn; | |
353 | s->do_cmd = dmm32at_ai_cmd; | |
354 | s->do_cmdtest = dmm32at_ai_cmdtest; | |
355 | s->cancel = dmm32at_ai_cancel; | |
356 | ||
357 | s = dev->subdevices + 1; | |
358 | /* analog output subdevice */ | |
359 | s->type = COMEDI_SUBD_AO; | |
360 | s->subdev_flags = SDF_WRITABLE; | |
df9d3349 HS |
361 | s->n_chan = board->ao_chans; |
362 | s->maxdata = (1 << board->ao_bits) - 1; | |
363 | s->range_table = board->ao_ranges; | |
3c501880 PP |
364 | s->insn_write = dmm32at_ao_winsn; |
365 | s->insn_read = dmm32at_ao_rinsn; | |
366 | ||
367 | s = dev->subdevices + 2; | |
368 | /* digital i/o subdevice */ | |
df9d3349 | 369 | if (board->have_dio) { |
3c501880 PP |
370 | |
371 | /* get access to the DIO regs */ | |
372 | dmm_outb(dev, DMM32AT_CNTRL, DMM32AT_DIOACC); | |
373 | /* set the DIO's to the defualt input setting */ | |
374 | devpriv->dio_config = DMM32AT_DIRA | DMM32AT_DIRB | | |
0a85b6f0 | 375 | DMM32AT_DIRCL | DMM32AT_DIRCH | DMM32AT_DIENABLE; |
3c501880 PP |
376 | dmm_outb(dev, DMM32AT_DIOCONF, devpriv->dio_config); |
377 | ||
378 | /* set up the subdevice */ | |
379 | s->type = COMEDI_SUBD_DIO; | |
380 | s->subdev_flags = SDF_READABLE | SDF_WRITABLE; | |
df9d3349 | 381 | s->n_chan = board->dio_chans; |
3c501880 PP |
382 | s->maxdata = 1; |
383 | s->state = 0; | |
384 | s->range_table = &range_digital; | |
385 | s->insn_bits = dmm32at_dio_insn_bits; | |
386 | s->insn_config = dmm32at_dio_insn_config; | |
387 | } else { | |
388 | s->type = COMEDI_SUBD_UNUSED; | |
389 | } | |
390 | ||
391 | /* success */ | |
27bf0bc9 | 392 | printk(KERN_INFO "comedi%d: dmm32at: attached\n", dev->minor); |
3c501880 PP |
393 | |
394 | return 1; | |
395 | ||
396 | } | |
397 | ||
484ecc95 | 398 | static void dmm32at_detach(struct comedi_device *dev) |
3c501880 | 399 | { |
3c501880 | 400 | if (dev->irq) |
5f74ea14 | 401 | free_irq(dev->irq, dev); |
3c501880 PP |
402 | if (dev->iobase) |
403 | release_region(dev->iobase, DMM32AT_MEMSIZE); | |
3c501880 PP |
404 | } |
405 | ||
406 | /* | |
407 | * "instructions" read/write data in "one-shot" or "software-triggered" | |
408 | * mode. | |
409 | */ | |
410 | ||
0a85b6f0 MT |
411 | static int dmm32at_ai_rinsn(struct comedi_device *dev, |
412 | struct comedi_subdevice *s, | |
413 | struct comedi_insn *insn, unsigned int *data) | |
3c501880 PP |
414 | { |
415 | int n, i; | |
416 | unsigned int d; | |
417 | unsigned char status; | |
418 | unsigned short msb, lsb; | |
419 | unsigned char chan; | |
420 | int range; | |
421 | ||
422 | /* get the channel and range number */ | |
423 | ||
424 | chan = CR_CHAN(insn->chanspec) & (s->n_chan - 1); | |
425 | range = CR_RANGE(insn->chanspec); | |
426 | ||
2696fb57 | 427 | /* printk("channel=0x%02x, range=%d\n",chan,range); */ |
3c501880 PP |
428 | |
429 | /* zero scan and fifo control and reset fifo */ | |
430 | dmm_outb(dev, DMM32AT_FIFOCNTRL, DMM32AT_FIFORESET); | |
431 | ||
432 | /* write the ai channel range regs */ | |
433 | dmm_outb(dev, DMM32AT_AILOW, chan); | |
434 | dmm_outb(dev, DMM32AT_AIHIGH, chan); | |
435 | /* set the range bits */ | |
436 | dmm_outb(dev, DMM32AT_AICONF, dmm32at_rangebits[range]); | |
437 | ||
438 | /* wait for circuit to settle */ | |
439 | for (i = 0; i < 40000; i++) { | |
440 | status = dmm_inb(dev, DMM32AT_AIRBACK); | |
441 | if ((status & DMM32AT_STATUS) == 0) | |
442 | break; | |
443 | } | |
444 | if (i == 40000) { | |
27bf0bc9 | 445 | printk(KERN_WARNING "dmm32at: timeout\n"); |
3c501880 PP |
446 | return -ETIMEDOUT; |
447 | } | |
448 | ||
449 | /* convert n samples */ | |
450 | for (n = 0; n < insn->n; n++) { | |
451 | /* trigger conversion */ | |
452 | dmm_outb(dev, DMM32AT_CONV, 0xff); | |
453 | /* wait for conversion to end */ | |
454 | for (i = 0; i < 40000; i++) { | |
455 | status = dmm_inb(dev, DMM32AT_AISTAT); | |
456 | if ((status & DMM32AT_STATUS) == 0) | |
457 | break; | |
458 | } | |
459 | if (i == 40000) { | |
27bf0bc9 | 460 | printk(KERN_WARNING "dmm32at: timeout\n"); |
3c501880 PP |
461 | return -ETIMEDOUT; |
462 | } | |
463 | ||
464 | /* read data */ | |
465 | lsb = dmm_inb(dev, DMM32AT_AILSB); | |
466 | msb = dmm_inb(dev, DMM32AT_AIMSB); | |
467 | ||
468 | /* invert sign bit to make range unsigned, this is an | |
25985edc | 469 | idiosyncrasy of the diamond board, it return |
3c501880 PP |
470 | conversions as a signed value, i.e. -32768 to |
471 | 32767, flipping the bit and interpreting it as | |
472 | signed gives you a range of 0 to 65535 which is | |
473 | used by comedi */ | |
474 | d = ((msb ^ 0x0080) << 8) + lsb; | |
475 | ||
476 | data[n] = d; | |
477 | } | |
478 | ||
479 | /* return the number of samples read/written */ | |
480 | return n; | |
481 | } | |
482 | ||
0a85b6f0 MT |
483 | static int dmm32at_ai_cmdtest(struct comedi_device *dev, |
484 | struct comedi_subdevice *s, | |
485 | struct comedi_cmd *cmd) | |
3c501880 PP |
486 | { |
487 | int err = 0; | |
488 | int tmp; | |
489 | int start_chan, gain, i; | |
490 | ||
2696fb57 | 491 | /* printk("dmmat32 in command test\n"); */ |
3c501880 PP |
492 | |
493 | /* cmdtest tests a particular command to see if it is valid. | |
494 | * Using the cmdtest ioctl, a user can create a valid cmd | |
495 | * and then have it executes by the cmd ioctl. | |
496 | * | |
497 | * cmdtest returns 1,2,3,4 or 0, depending on which tests | |
498 | * the command passes. */ | |
499 | ||
500 | /* step 1: make sure trigger sources are trivially valid */ | |
501 | ||
502 | tmp = cmd->start_src; | |
503 | cmd->start_src &= TRIG_NOW; | |
504 | if (!cmd->start_src || tmp != cmd->start_src) | |
505 | err++; | |
506 | ||
507 | tmp = cmd->scan_begin_src; | |
508 | cmd->scan_begin_src &= TRIG_TIMER /*| TRIG_EXT */ ; | |
509 | if (!cmd->scan_begin_src || tmp != cmd->scan_begin_src) | |
510 | err++; | |
511 | ||
512 | tmp = cmd->convert_src; | |
513 | cmd->convert_src &= TRIG_TIMER /*| TRIG_EXT */ ; | |
514 | if (!cmd->convert_src || tmp != cmd->convert_src) | |
515 | err++; | |
516 | ||
517 | tmp = cmd->scan_end_src; | |
518 | cmd->scan_end_src &= TRIG_COUNT; | |
519 | if (!cmd->scan_end_src || tmp != cmd->scan_end_src) | |
520 | err++; | |
521 | ||
522 | tmp = cmd->stop_src; | |
523 | cmd->stop_src &= TRIG_COUNT | TRIG_NONE; | |
524 | if (!cmd->stop_src || tmp != cmd->stop_src) | |
525 | err++; | |
526 | ||
527 | if (err) | |
528 | return 1; | |
529 | ||
27bf0bc9 M |
530 | /* step 2: make sure trigger sources are unique and mutually |
531 | * compatible */ | |
3c501880 | 532 | |
828684f9 | 533 | /* note that mutual compatibility is not an issue here */ |
3c501880 | 534 | if (cmd->scan_begin_src != TRIG_TIMER && |
0a85b6f0 | 535 | cmd->scan_begin_src != TRIG_EXT) |
3c501880 PP |
536 | err++; |
537 | if (cmd->convert_src != TRIG_TIMER && cmd->convert_src != TRIG_EXT) | |
538 | err++; | |
539 | if (cmd->stop_src != TRIG_COUNT && cmd->stop_src != TRIG_NONE) | |
540 | err++; | |
541 | ||
542 | if (err) | |
543 | return 2; | |
544 | ||
545 | /* step 3: make sure arguments are trivially compatible */ | |
546 | ||
547 | if (cmd->start_arg != 0) { | |
548 | cmd->start_arg = 0; | |
549 | err++; | |
550 | } | |
551 | #define MAX_SCAN_SPEED 1000000 /* in nanoseconds */ | |
552 | #define MIN_SCAN_SPEED 1000000000 /* in nanoseconds */ | |
553 | ||
554 | if (cmd->scan_begin_src == TRIG_TIMER) { | |
555 | if (cmd->scan_begin_arg < MAX_SCAN_SPEED) { | |
556 | cmd->scan_begin_arg = MAX_SCAN_SPEED; | |
557 | err++; | |
558 | } | |
559 | if (cmd->scan_begin_arg > MIN_SCAN_SPEED) { | |
560 | cmd->scan_begin_arg = MIN_SCAN_SPEED; | |
561 | err++; | |
562 | } | |
563 | } else { | |
564 | /* external trigger */ | |
565 | /* should be level/edge, hi/lo specification here */ | |
566 | /* should specify multiple external triggers */ | |
567 | if (cmd->scan_begin_arg > 9) { | |
568 | cmd->scan_begin_arg = 9; | |
569 | err++; | |
570 | } | |
571 | } | |
572 | if (cmd->convert_src == TRIG_TIMER) { | |
573 | if (cmd->convert_arg >= 17500) | |
574 | cmd->convert_arg = 20000; | |
575 | else if (cmd->convert_arg >= 12500) | |
576 | cmd->convert_arg = 15000; | |
577 | else if (cmd->convert_arg >= 7500) | |
578 | cmd->convert_arg = 10000; | |
579 | else | |
580 | cmd->convert_arg = 5000; | |
581 | ||
582 | } else { | |
583 | /* external trigger */ | |
584 | /* see above */ | |
585 | if (cmd->convert_arg > 9) { | |
586 | cmd->convert_arg = 9; | |
587 | err++; | |
588 | } | |
589 | } | |
590 | ||
591 | if (cmd->scan_end_arg != cmd->chanlist_len) { | |
592 | cmd->scan_end_arg = cmd->chanlist_len; | |
593 | err++; | |
594 | } | |
595 | if (cmd->stop_src == TRIG_COUNT) { | |
596 | if (cmd->stop_arg > 0xfffffff0) { | |
597 | cmd->stop_arg = 0xfffffff0; | |
598 | err++; | |
599 | } | |
600 | if (cmd->stop_arg == 0) { | |
601 | cmd->stop_arg = 1; | |
602 | err++; | |
603 | } | |
604 | } else { | |
605 | /* TRIG_NONE */ | |
606 | if (cmd->stop_arg != 0) { | |
607 | cmd->stop_arg = 0; | |
608 | err++; | |
609 | } | |
610 | } | |
611 | ||
612 | if (err) | |
613 | return 3; | |
614 | ||
615 | /* step 4: fix up any arguments */ | |
616 | ||
617 | if (cmd->scan_begin_src == TRIG_TIMER) { | |
618 | tmp = cmd->scan_begin_arg; | |
619 | dmm32at_ns_to_timer(&cmd->scan_begin_arg, | |
0a85b6f0 | 620 | cmd->flags & TRIG_ROUND_MASK); |
3c501880 PP |
621 | if (tmp != cmd->scan_begin_arg) |
622 | err++; | |
623 | } | |
624 | if (cmd->convert_src == TRIG_TIMER) { | |
625 | tmp = cmd->convert_arg; | |
626 | dmm32at_ns_to_timer(&cmd->convert_arg, | |
0a85b6f0 | 627 | cmd->flags & TRIG_ROUND_MASK); |
3c501880 PP |
628 | if (tmp != cmd->convert_arg) |
629 | err++; | |
630 | if (cmd->scan_begin_src == TRIG_TIMER && | |
0a85b6f0 MT |
631 | cmd->scan_begin_arg < |
632 | cmd->convert_arg * cmd->scan_end_arg) { | |
3c501880 | 633 | cmd->scan_begin_arg = |
0a85b6f0 | 634 | cmd->convert_arg * cmd->scan_end_arg; |
3c501880 PP |
635 | err++; |
636 | } | |
637 | } | |
638 | ||
639 | if (err) | |
640 | return 4; | |
641 | ||
642 | /* step 5 check the channel list, the channel list for this | |
643 | board must be consecutive and gains must be the same */ | |
644 | ||
645 | if (cmd->chanlist) { | |
646 | gain = CR_RANGE(cmd->chanlist[0]); | |
647 | start_chan = CR_CHAN(cmd->chanlist[0]); | |
648 | for (i = 1; i < cmd->chanlist_len; i++) { | |
649 | if (CR_CHAN(cmd->chanlist[i]) != | |
0a85b6f0 | 650 | (start_chan + i) % s->n_chan) { |
3c501880 | 651 | comedi_error(dev, |
0a85b6f0 | 652 | "entries in chanlist must be consecutive channels, counting upwards\n"); |
3c501880 PP |
653 | err++; |
654 | } | |
655 | if (CR_RANGE(cmd->chanlist[i]) != gain) { | |
656 | comedi_error(dev, | |
0a85b6f0 | 657 | "entries in chanlist must all have the same gain\n"); |
3c501880 PP |
658 | err++; |
659 | } | |
660 | } | |
661 | } | |
662 | ||
663 | if (err) | |
664 | return 5; | |
665 | ||
666 | return 0; | |
667 | } | |
668 | ||
da91b269 | 669 | static int dmm32at_ai_cmd(struct comedi_device *dev, struct comedi_subdevice *s) |
3c501880 | 670 | { |
ea6d0d4c | 671 | struct comedi_cmd *cmd = &s->async->cmd; |
3c501880 PP |
672 | int i, range; |
673 | unsigned char chanlo, chanhi, status; | |
674 | ||
675 | if (!cmd->chanlist) | |
676 | return -EINVAL; | |
677 | ||
678 | /* get the channel list and range */ | |
679 | chanlo = CR_CHAN(cmd->chanlist[0]) & (s->n_chan - 1); | |
680 | chanhi = chanlo + cmd->chanlist_len - 1; | |
681 | if (chanhi >= s->n_chan) | |
682 | return -EINVAL; | |
683 | range = CR_RANGE(cmd->chanlist[0]); | |
684 | ||
685 | /* reset fifo */ | |
686 | dmm_outb(dev, DMM32AT_FIFOCNTRL, DMM32AT_FIFORESET); | |
687 | ||
688 | /* set scan enable */ | |
689 | dmm_outb(dev, DMM32AT_FIFOCNTRL, DMM32AT_SCANENABLE); | |
690 | ||
691 | /* write the ai channel range regs */ | |
692 | dmm_outb(dev, DMM32AT_AILOW, chanlo); | |
693 | dmm_outb(dev, DMM32AT_AIHIGH, chanhi); | |
694 | ||
695 | /* set the range bits */ | |
696 | dmm_outb(dev, DMM32AT_AICONF, dmm32at_rangebits[range]); | |
697 | ||
698 | /* reset the interrupt just in case */ | |
699 | dmm_outb(dev, DMM32AT_CNTRL, DMM32AT_INTRESET); | |
700 | ||
701 | if (cmd->stop_src == TRIG_COUNT) | |
702 | devpriv->ai_scans_left = cmd->stop_arg; | |
703 | else { /* TRIG_NONE */ | |
27bf0bc9 M |
704 | devpriv->ai_scans_left = 0xffffffff; /* indicates TRIG_NONE to |
705 | * isr */ | |
3c501880 PP |
706 | } |
707 | ||
708 | /* wait for circuit to settle */ | |
709 | for (i = 0; i < 40000; i++) { | |
710 | status = dmm_inb(dev, DMM32AT_AIRBACK); | |
711 | if ((status & DMM32AT_STATUS) == 0) | |
712 | break; | |
713 | } | |
714 | if (i == 40000) { | |
27bf0bc9 | 715 | printk(KERN_WARNING "dmm32at: timeout\n"); |
3c501880 PP |
716 | return -ETIMEDOUT; |
717 | } | |
718 | ||
719 | if (devpriv->ai_scans_left > 1) { | |
720 | /* start the clock and enable the interrupts */ | |
721 | dmm32at_setaitimer(dev, cmd->scan_begin_arg); | |
722 | } else { | |
723 | /* start the interrups and initiate a single scan */ | |
724 | dmm_outb(dev, DMM32AT_INTCLOCK, DMM32AT_ADINT); | |
725 | dmm_outb(dev, DMM32AT_CONV, 0xff); | |
726 | } | |
727 | ||
27bf0bc9 | 728 | /* printk("dmmat32 in command\n"); */ |
3c501880 | 729 | |
27bf0bc9 M |
730 | /* for(i=0;i<cmd->chanlist_len;i++) */ |
731 | /* comedi_buf_put(s->async,i*100); */ | |
3c501880 | 732 | |
27bf0bc9 M |
733 | /* s->async->events |= COMEDI_CB_EOA; */ |
734 | /* comedi_event(dev, s); */ | |
3c501880 PP |
735 | |
736 | return 0; | |
737 | ||
738 | } | |
739 | ||
0a85b6f0 MT |
740 | static int dmm32at_ai_cancel(struct comedi_device *dev, |
741 | struct comedi_subdevice *s) | |
3c501880 PP |
742 | { |
743 | devpriv->ai_scans_left = 1; | |
744 | return 0; | |
745 | } | |
746 | ||
70265d24 | 747 | static irqreturn_t dmm32at_isr(int irq, void *d) |
3c501880 PP |
748 | { |
749 | unsigned char intstat; | |
750 | unsigned int samp; | |
751 | unsigned short msb, lsb; | |
752 | int i; | |
71b5f4f1 | 753 | struct comedi_device *dev = d; |
3c501880 PP |
754 | |
755 | if (!dev->attached) { | |
756 | comedi_error(dev, "spurious interrupt"); | |
757 | return IRQ_HANDLED; | |
758 | } | |
759 | ||
760 | intstat = dmm_inb(dev, DMM32AT_INTCLOCK); | |
761 | ||
762 | if (intstat & DMM32AT_ADINT) { | |
34c43922 | 763 | struct comedi_subdevice *s = dev->read_subdev; |
ea6d0d4c | 764 | struct comedi_cmd *cmd = &s->async->cmd; |
3c501880 PP |
765 | |
766 | for (i = 0; i < cmd->chanlist_len; i++) { | |
767 | /* read data */ | |
768 | lsb = dmm_inb(dev, DMM32AT_AILSB); | |
769 | msb = dmm_inb(dev, DMM32AT_AIMSB); | |
770 | ||
771 | /* invert sign bit to make range unsigned */ | |
772 | samp = ((msb ^ 0x0080) << 8) + lsb; | |
773 | comedi_buf_put(s->async, samp); | |
774 | } | |
775 | ||
776 | if (devpriv->ai_scans_left != 0xffffffff) { /* TRIG_COUNT */ | |
777 | devpriv->ai_scans_left--; | |
778 | if (devpriv->ai_scans_left == 0) { | |
779 | /* disable further interrupts and clocks */ | |
780 | dmm_outb(dev, DMM32AT_INTCLOCK, 0x0); | |
781 | /* set the buffer to be flushed with an EOF */ | |
782 | s->async->events |= COMEDI_CB_EOA; | |
783 | } | |
784 | ||
785 | } | |
786 | /* flush the buffer */ | |
787 | comedi_event(dev, s); | |
788 | } | |
789 | ||
790 | /* reset the interrupt */ | |
791 | dmm_outb(dev, DMM32AT_CNTRL, DMM32AT_INTRESET); | |
792 | return IRQ_HANDLED; | |
793 | } | |
794 | ||
795 | /* This function doesn't require a particular form, this is just | |
796 | * what happens to be used in some of the drivers. It should | |
797 | * convert ns nanoseconds to a counter value suitable for programming | |
798 | * the device. Also, it should adjust ns so that it cooresponds to | |
799 | * the actual time that the device will use. */ | |
800 | static int dmm32at_ns_to_timer(unsigned int *ns, int round) | |
801 | { | |
802 | /* trivial timer */ | |
803 | /* if your timing is done through two cascaded timers, the | |
804 | * i8253_cascade_ns_to_timer() function in 8253.h can be | |
805 | * very helpful. There are also i8254_load() and i8254_mm_load() | |
806 | * which can be used to load values into the ubiquitous 8254 counters | |
807 | */ | |
808 | ||
809 | return *ns; | |
810 | } | |
811 | ||
0a85b6f0 MT |
812 | static int dmm32at_ao_winsn(struct comedi_device *dev, |
813 | struct comedi_subdevice *s, | |
814 | struct comedi_insn *insn, unsigned int *data) | |
3c501880 PP |
815 | { |
816 | int i; | |
817 | int chan = CR_CHAN(insn->chanspec); | |
818 | unsigned char hi, lo, status; | |
819 | ||
820 | /* Writing a list of values to an AO channel is probably not | |
821 | * very useful, but that's how the interface is defined. */ | |
822 | for (i = 0; i < insn->n; i++) { | |
823 | ||
824 | devpriv->ao_readback[chan] = data[i]; | |
825 | ||
826 | /* get the low byte */ | |
827 | lo = data[i] & 0x00ff; | |
828 | /* high byte also contains channel number */ | |
829 | hi = (data[i] >> 8) + chan * (1 << 6); | |
2696fb57 | 830 | /* printk("writing 0x%02x 0x%02x\n",hi,lo); */ |
3c501880 PP |
831 | /* write the low and high values to the board */ |
832 | dmm_outb(dev, DMM32AT_DACLSB, lo); | |
833 | dmm_outb(dev, DMM32AT_DACMSB, hi); | |
834 | ||
835 | /* wait for circuit to settle */ | |
836 | for (i = 0; i < 40000; i++) { | |
837 | status = dmm_inb(dev, DMM32AT_DACSTAT); | |
838 | if ((status & DMM32AT_DACBUSY) == 0) | |
839 | break; | |
840 | } | |
841 | if (i == 40000) { | |
27bf0bc9 | 842 | printk(KERN_WARNING "dmm32at: timeout\n"); |
3c501880 PP |
843 | return -ETIMEDOUT; |
844 | } | |
845 | /* dummy read to update trigger the output */ | |
846 | status = dmm_inb(dev, DMM32AT_DACMSB); | |
847 | ||
848 | } | |
849 | ||
850 | /* return the number of samples read/written */ | |
851 | return i; | |
852 | } | |
853 | ||
854 | /* AO subdevices should have a read insn as well as a write insn. | |
855 | * Usually this means copying a value stored in devpriv. */ | |
0a85b6f0 MT |
856 | static int dmm32at_ao_rinsn(struct comedi_device *dev, |
857 | struct comedi_subdevice *s, | |
858 | struct comedi_insn *insn, unsigned int *data) | |
3c501880 PP |
859 | { |
860 | int i; | |
861 | int chan = CR_CHAN(insn->chanspec); | |
862 | ||
863 | for (i = 0; i < insn->n; i++) | |
864 | data[i] = devpriv->ao_readback[chan]; | |
865 | ||
866 | return i; | |
867 | } | |
868 | ||
869 | /* DIO devices are slightly special. Although it is possible to | |
870 | * implement the insn_read/insn_write interface, it is much more | |
871 | * useful to applications if you implement the insn_bits interface. | |
872 | * This allows packed reading/writing of the DIO channels. The | |
873 | * comedi core can convert between insn_bits and insn_read/write */ | |
0a85b6f0 MT |
874 | static int dmm32at_dio_insn_bits(struct comedi_device *dev, |
875 | struct comedi_subdevice *s, | |
876 | struct comedi_insn *insn, unsigned int *data) | |
3c501880 PP |
877 | { |
878 | unsigned char diobits; | |
879 | ||
3c501880 PP |
880 | /* The insn data is a mask in data[0] and the new data |
881 | * in data[1], each channel cooresponding to a bit. */ | |
882 | if (data[0]) { | |
883 | s->state &= ~data[0]; | |
884 | s->state |= data[0] & data[1]; | |
885 | /* Write out the new digital output lines */ | |
2696fb57 | 886 | /* outw(s->state,dev->iobase + DMM32AT_DIO); */ |
3c501880 PP |
887 | } |
888 | ||
889 | /* get access to the DIO regs */ | |
890 | dmm_outb(dev, DMM32AT_CNTRL, DMM32AT_DIOACC); | |
891 | ||
892 | /* if either part of dio is set for output */ | |
893 | if (((devpriv->dio_config & DMM32AT_DIRCL) == 0) || | |
0a85b6f0 | 894 | ((devpriv->dio_config & DMM32AT_DIRCH) == 0)) { |
3c501880 PP |
895 | diobits = (s->state & 0x00ff0000) >> 16; |
896 | dmm_outb(dev, DMM32AT_DIOC, diobits); | |
897 | } | |
898 | if ((devpriv->dio_config & DMM32AT_DIRB) == 0) { | |
899 | diobits = (s->state & 0x0000ff00) >> 8; | |
900 | dmm_outb(dev, DMM32AT_DIOB, diobits); | |
901 | } | |
902 | if ((devpriv->dio_config & DMM32AT_DIRA) == 0) { | |
903 | diobits = (s->state & 0x000000ff); | |
904 | dmm_outb(dev, DMM32AT_DIOA, diobits); | |
905 | } | |
906 | ||
907 | /* now read the state back in */ | |
908 | s->state = dmm_inb(dev, DMM32AT_DIOC); | |
909 | s->state <<= 8; | |
910 | s->state |= dmm_inb(dev, DMM32AT_DIOB); | |
911 | s->state <<= 8; | |
912 | s->state |= dmm_inb(dev, DMM32AT_DIOA); | |
913 | data[1] = s->state; | |
914 | ||
915 | /* on return, data[1] contains the value of the digital | |
916 | * input and output lines. */ | |
2696fb57 | 917 | /* data[1]=inw(dev->iobase + DMM32AT_DIO); */ |
3c501880 PP |
918 | /* or we could just return the software copy of the output values if |
919 | * it was a purely digital output subdevice */ | |
2696fb57 | 920 | /* data[1]=s->state; */ |
3c501880 | 921 | |
a2714e3e | 922 | return insn->n; |
3c501880 PP |
923 | } |
924 | ||
0a85b6f0 MT |
925 | static int dmm32at_dio_insn_config(struct comedi_device *dev, |
926 | struct comedi_subdevice *s, | |
927 | struct comedi_insn *insn, unsigned int *data) | |
3c501880 PP |
928 | { |
929 | unsigned char chanbit; | |
930 | int chan = CR_CHAN(insn->chanspec); | |
931 | ||
932 | if (insn->n != 1) | |
933 | return -EINVAL; | |
934 | ||
935 | if (chan < 8) | |
936 | chanbit = DMM32AT_DIRA; | |
937 | else if (chan < 16) | |
938 | chanbit = DMM32AT_DIRB; | |
939 | else if (chan < 20) | |
940 | chanbit = DMM32AT_DIRCL; | |
941 | else | |
942 | chanbit = DMM32AT_DIRCH; | |
943 | ||
944 | /* The input or output configuration of each digital line is | |
945 | * configured by a special insn_config instruction. chanspec | |
946 | * contains the channel to be changed, and data[0] contains the | |
947 | * value COMEDI_INPUT or COMEDI_OUTPUT. */ | |
948 | ||
949 | /* if output clear the bit, otherwise set it */ | |
20962c10 | 950 | if (data[0] == COMEDI_OUTPUT) |
3c501880 | 951 | devpriv->dio_config &= ~chanbit; |
20962c10 | 952 | else |
3c501880 | 953 | devpriv->dio_config |= chanbit; |
3c501880 PP |
954 | /* get access to the DIO regs */ |
955 | dmm_outb(dev, DMM32AT_CNTRL, DMM32AT_DIOACC); | |
956 | /* set the DIO's to the new configuration setting */ | |
957 | dmm_outb(dev, DMM32AT_DIOCONF, devpriv->dio_config); | |
958 | ||
959 | return 1; | |
960 | } | |
961 | ||
da91b269 | 962 | void dmm32at_setaitimer(struct comedi_device *dev, unsigned int nansec) |
3c501880 PP |
963 | { |
964 | unsigned char lo1, lo2, hi2; | |
965 | unsigned short both2; | |
966 | ||
967 | /* based on 10mhz clock */ | |
968 | lo1 = 200; | |
969 | both2 = nansec / 20000; | |
970 | hi2 = (both2 & 0xff00) >> 8; | |
971 | lo2 = both2 & 0x00ff; | |
972 | ||
973 | /* set the counter frequency to 10mhz */ | |
974 | dmm_outb(dev, DMM32AT_CNTRDIO, 0); | |
975 | ||
976 | /* get access to the clock regs */ | |
977 | dmm_outb(dev, DMM32AT_CNTRL, DMM32AT_CLKACC); | |
978 | ||
979 | /* write the counter 1 control word and low byte to counter */ | |
980 | dmm_outb(dev, DMM32AT_CLKCT, DMM32AT_CLKCT1); | |
981 | dmm_outb(dev, DMM32AT_CLK1, lo1); | |
982 | ||
983 | /* write the counter 2 control word and low byte then to counter */ | |
984 | dmm_outb(dev, DMM32AT_CLKCT, DMM32AT_CLKCT2); | |
985 | dmm_outb(dev, DMM32AT_CLK2, lo2); | |
986 | dmm_outb(dev, DMM32AT_CLK2, hi2); | |
987 | ||
988 | /* enable the ai conversion interrupt and the clock to start scans */ | |
989 | dmm_outb(dev, DMM32AT_INTCLOCK, DMM32AT_ADINT | DMM32AT_CLKSEL); | |
990 | ||
991 | } | |
992 | ||
17f49dd4 HS |
993 | static struct comedi_driver dmm32at_driver = { |
994 | .driver_name = "dmm32at", | |
995 | .module = THIS_MODULE, | |
996 | .attach = dmm32at_attach, | |
997 | .detach = dmm32at_detach, | |
998 | .board_name = &dmm32at_boards[0].name, | |
999 | .offset = sizeof(struct dmm32at_board), | |
1000 | .num_names = ARRAY_SIZE(dmm32at_boards), | |
1001 | }; | |
1002 | module_comedi_driver(dmm32at_driver); | |
90f703d3 AT |
1003 | |
1004 | MODULE_AUTHOR("Comedi http://www.comedi.org"); | |
1005 | MODULE_DESCRIPTION("Comedi low-level driver"); | |
1006 | MODULE_LICENSE("GPL"); |