1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* |
3 | * pcl711.c |
4 | * Comedi driver for PC-LabCard PCL-711 and AdSys ACL-8112 and compatibles |
5 | * Copyright (C) 1998 David A. Schleef <ds@schleef.org> |
6 | * Janne Jalkanen <jalkanen@cs.hut.fi> |
7 | * Eric Bunn <ebu@cs.hut.fi> |
8 | * |
9 | * COMEDI - Linux Control and Measurement Device Interface |
10 | * Copyright (C) 1998 David A. Schleef <ds@schleef.org> |
11 | */ |
12 | |
13 | /* |
14 | * Driver: pcl711 |
15 | * Description: Advantech PCL-711 and 711b, ADLink ACL-8112 |
16 | * Devices: [Advantech] PCL-711 (pcl711), PCL-711B (pcl711b), |
17 | * [ADLink] ACL-8112HG (acl8112hg), ACL-8112DG (acl8112dg) |
18 | * Author: David A. Schleef <ds@schleef.org> |
19 | * Janne Jalkanen <jalkanen@cs.hut.fi> |
20 | * Eric Bunn <ebu@cs.hut.fi> |
21 | * Updated: |
22 | * Status: mostly complete |
23 | * |
24 | * Configuration Options: |
25 | * [0] - I/O port base |
26 | * [1] - IRQ, optional |
27 | */ |
28 | |
29 | #include <linux/module.h> |
30 | #include <linux/delay.h> |
31 | #include <linux/interrupt.h> |
32 | #include <linux/comedi/comedidev.h> |
33 | #include <linux/comedi/comedi_8254.h> |
34 | |
35 | /* |
36 | * I/O port register map |
37 | */ |
38 | #define PCL711_TIMER_BASE 0x00 |
39 | #define PCL711_AI_LSB_REG 0x04 |
40 | #define PCL711_AI_MSB_REG 0x05 |
41 | #define PCL711_AI_MSB_DRDY BIT(4) |
42 | #define PCL711_AO_LSB_REG(x) (0x04 + ((x) * 2)) |
43 | #define PCL711_AO_MSB_REG(x) (0x05 + ((x) * 2)) |
44 | #define PCL711_DI_LSB_REG 0x06 |
45 | #define PCL711_DI_MSB_REG 0x07 |
46 | #define PCL711_INT_STAT_REG 0x08 |
47 | #define PCL711_INT_STAT_CLR (0 << 0) /* any value will work */ |
48 | #define PCL711_AI_GAIN_REG 0x09 |
49 | #define PCL711_AI_GAIN(x) (((x) & 0xf) << 0) |
50 | #define PCL711_MUX_REG 0x0a |
51 | #define PCL711_MUX_CHAN(x) (((x) & 0xf) << 0) |
52 | #define PCL711_MUX_CS0 BIT(4) |
53 | #define PCL711_MUX_CS1 BIT(5) |
54 | #define PCL711_MUX_DIFF (PCL711_MUX_CS0 | PCL711_MUX_CS1) |
55 | #define PCL711_MODE_REG 0x0b |
56 | #define PCL711_MODE(x) (((x) & 0x7) << 0) |
57 | #define PCL711_MODE_DEFAULT PCL711_MODE(0) |
58 | #define PCL711_MODE_SOFTTRIG PCL711_MODE(1) |
59 | #define PCL711_MODE_EXT PCL711_MODE(2) |
60 | #define PCL711_MODE_EXT_IRQ PCL711_MODE(3) |
61 | #define PCL711_MODE_PACER PCL711_MODE(4) |
62 | #define PCL711_MODE_PACER_IRQ PCL711_MODE(6) |
63 | #define PCL711_MODE_IRQ(x) (((x) & 0x7) << 4) |
64 | #define PCL711_SOFTTRIG_REG 0x0c |
65 | #define PCL711_SOFTTRIG (0 << 0) /* any value will work */ |
66 | #define PCL711_DO_LSB_REG 0x0d |
67 | #define PCL711_DO_MSB_REG 0x0e |
68 | |
69 | static const struct comedi_lrange range_pcl711b_ai = { |
70 | 5, { |
71 | BIP_RANGE(5), |
72 | BIP_RANGE(2.5), |
73 | BIP_RANGE(1.25), |
74 | BIP_RANGE(0.625), |
75 | BIP_RANGE(0.3125) |
76 | } |
77 | }; |
78 | |
79 | static const struct comedi_lrange range_acl8112hg_ai = { |
80 | 12, { |
81 | BIP_RANGE(5), |
82 | BIP_RANGE(0.5), |
83 | BIP_RANGE(0.05), |
84 | BIP_RANGE(0.005), |
85 | UNI_RANGE(10), |
86 | UNI_RANGE(1), |
87 | UNI_RANGE(0.1), |
88 | UNI_RANGE(0.01), |
89 | BIP_RANGE(10), |
90 | BIP_RANGE(1), |
91 | BIP_RANGE(0.1), |
92 | BIP_RANGE(0.01) |
93 | } |
94 | }; |
95 | |
96 | static const struct comedi_lrange range_acl8112dg_ai = { |
97 | 9, { |
98 | BIP_RANGE(5), |
99 | BIP_RANGE(2.5), |
100 | BIP_RANGE(1.25), |
101 | BIP_RANGE(0.625), |
102 | UNI_RANGE(10), |
103 | UNI_RANGE(5), |
104 | UNI_RANGE(2.5), |
105 | UNI_RANGE(1.25), |
106 | BIP_RANGE(10) |
107 | } |
108 | }; |
109 | |
110 | struct pcl711_board { |
111 | const char *name; |
112 | int n_aichan; |
113 | int n_aochan; |
114 | int maxirq; |
115 | const struct comedi_lrange *ai_range_type; |
116 | }; |
117 | |
118 | static const struct pcl711_board boardtypes[] = { |
119 | { |
120 | .name = "pcl711" , |
121 | .n_aichan = 8, |
122 | .n_aochan = 1, |
123 | .ai_range_type = &range_bipolar5, |
124 | }, { |
125 | .name = "pcl711b" , |
126 | .n_aichan = 8, |
127 | .n_aochan = 1, |
128 | .maxirq = 7, |
129 | .ai_range_type = &range_pcl711b_ai, |
130 | }, { |
131 | .name = "acl8112hg" , |
132 | .n_aichan = 16, |
133 | .n_aochan = 2, |
134 | .maxirq = 15, |
135 | .ai_range_type = &range_acl8112hg_ai, |
136 | }, { |
137 | .name = "acl8112dg" , |
138 | .n_aichan = 16, |
139 | .n_aochan = 2, |
140 | .maxirq = 15, |
141 | .ai_range_type = &range_acl8112dg_ai, |
142 | }, |
143 | }; |
144 | |
145 | static void pcl711_ai_set_mode(struct comedi_device *dev, unsigned int mode) |
146 | { |
147 | /* |
148 | * The pcl711b board uses bits in the mode register to select the |
149 | * interrupt. The other boards supported by this driver all use |
150 | * jumpers on the board. |
151 | * |
152 | * Enables the interrupt when needed on the pcl711b board. These |
153 | * bits do nothing on the other boards. |
154 | */ |
155 | if (mode == PCL711_MODE_EXT_IRQ || mode == PCL711_MODE_PACER_IRQ) |
156 | mode |= PCL711_MODE_IRQ(dev->irq); |
157 | |
158 | outb(value: mode, port: dev->iobase + PCL711_MODE_REG); |
159 | } |
160 | |
161 | static unsigned int pcl711_ai_get_sample(struct comedi_device *dev, |
162 | struct comedi_subdevice *s) |
163 | { |
164 | unsigned int val; |
165 | |
166 | val = inb(port: dev->iobase + PCL711_AI_MSB_REG) << 8; |
167 | val |= inb(port: dev->iobase + PCL711_AI_LSB_REG); |
168 | |
169 | return val & s->maxdata; |
170 | } |
171 | |
172 | static int pcl711_ai_cancel(struct comedi_device *dev, |
173 | struct comedi_subdevice *s) |
174 | { |
175 | outb(PCL711_INT_STAT_CLR, port: dev->iobase + PCL711_INT_STAT_REG); |
176 | pcl711_ai_set_mode(dev, PCL711_MODE_SOFTTRIG); |
177 | return 0; |
178 | } |
179 | |
180 | static irqreturn_t pcl711_interrupt(int irq, void *d) |
181 | { |
182 | struct comedi_device *dev = d; |
183 | struct comedi_subdevice *s = dev->read_subdev; |
184 | struct comedi_cmd *cmd = &s->async->cmd; |
185 | unsigned short data; |
186 | |
187 | if (!dev->attached) { |
188 | dev_err(dev->class_dev, "spurious interrupt\n" ); |
189 | return IRQ_HANDLED; |
190 | } |
191 | |
192 | data = pcl711_ai_get_sample(dev, s); |
193 | |
194 | outb(PCL711_INT_STAT_CLR, port: dev->iobase + PCL711_INT_STAT_REG); |
195 | |
196 | comedi_buf_write_samples(s, data: &data, nsamples: 1); |
197 | |
198 | if (cmd->stop_src == TRIG_COUNT && |
199 | s->async->scans_done >= cmd->stop_arg) |
200 | s->async->events |= COMEDI_CB_EOA; |
201 | |
202 | comedi_handle_events(dev, s); |
203 | |
204 | return IRQ_HANDLED; |
205 | } |
206 | |
207 | static void pcl711_set_changain(struct comedi_device *dev, |
208 | struct comedi_subdevice *s, |
209 | unsigned int chanspec) |
210 | { |
211 | unsigned int chan = CR_CHAN(chanspec); |
212 | unsigned int range = CR_RANGE(chanspec); |
213 | unsigned int aref = CR_AREF(chanspec); |
214 | unsigned int mux = 0; |
215 | |
216 | outb(PCL711_AI_GAIN(range), port: dev->iobase + PCL711_AI_GAIN_REG); |
217 | |
218 | if (s->n_chan > 8) { |
219 | /* Select the correct MPC508A chip */ |
220 | if (aref == AREF_DIFF) { |
221 | chan &= 0x7; |
222 | mux |= PCL711_MUX_DIFF; |
223 | } else { |
224 | if (chan < 8) |
225 | mux |= PCL711_MUX_CS0; |
226 | else |
227 | mux |= PCL711_MUX_CS1; |
228 | } |
229 | } |
230 | outb(value: mux | PCL711_MUX_CHAN(chan), port: dev->iobase + PCL711_MUX_REG); |
231 | } |
232 | |
233 | static int pcl711_ai_eoc(struct comedi_device *dev, |
234 | struct comedi_subdevice *s, |
235 | struct comedi_insn *insn, |
236 | unsigned long context) |
237 | { |
238 | unsigned int status; |
239 | |
240 | status = inb(port: dev->iobase + PCL711_AI_MSB_REG); |
241 | if ((status & PCL711_AI_MSB_DRDY) == 0) |
242 | return 0; |
243 | return -EBUSY; |
244 | } |
245 | |
246 | static int pcl711_ai_insn_read(struct comedi_device *dev, |
247 | struct comedi_subdevice *s, |
248 | struct comedi_insn *insn, |
249 | unsigned int *data) |
250 | { |
251 | int ret; |
252 | int i; |
253 | |
254 | pcl711_set_changain(dev, s, chanspec: insn->chanspec); |
255 | |
256 | pcl711_ai_set_mode(dev, PCL711_MODE_SOFTTRIG); |
257 | |
258 | for (i = 0; i < insn->n; i++) { |
259 | outb(PCL711_SOFTTRIG, port: dev->iobase + PCL711_SOFTTRIG_REG); |
260 | |
261 | ret = comedi_timeout(dev, s, insn, cb: pcl711_ai_eoc, context: 0); |
262 | if (ret) |
263 | return ret; |
264 | |
265 | data[i] = pcl711_ai_get_sample(dev, s); |
266 | } |
267 | |
268 | return insn->n; |
269 | } |
270 | |
271 | static int pcl711_ai_cmdtest(struct comedi_device *dev, |
272 | struct comedi_subdevice *s, struct comedi_cmd *cmd) |
273 | { |
274 | int err = 0; |
275 | |
276 | /* Step 1 : check if triggers are trivially valid */ |
277 | |
278 | err |= comedi_check_trigger_src(src: &cmd->start_src, TRIG_NOW); |
279 | err |= comedi_check_trigger_src(src: &cmd->scan_begin_src, |
280 | TRIG_TIMER | TRIG_EXT); |
281 | err |= comedi_check_trigger_src(src: &cmd->convert_src, TRIG_NOW); |
282 | err |= comedi_check_trigger_src(src: &cmd->scan_end_src, TRIG_COUNT); |
283 | err |= comedi_check_trigger_src(src: &cmd->stop_src, TRIG_COUNT | TRIG_NONE); |
284 | |
285 | if (err) |
286 | return 1; |
287 | |
288 | /* Step 2a : make sure trigger sources are unique */ |
289 | |
290 | err |= comedi_check_trigger_is_unique(src: cmd->scan_begin_src); |
291 | err |= comedi_check_trigger_is_unique(src: cmd->stop_src); |
292 | |
293 | /* Step 2b : and mutually compatible */ |
294 | |
295 | if (err) |
296 | return 2; |
297 | |
298 | /* Step 3: check if arguments are trivially valid */ |
299 | |
300 | err |= comedi_check_trigger_arg_is(arg: &cmd->start_arg, val: 0); |
301 | |
302 | if (cmd->scan_begin_src == TRIG_EXT) { |
303 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_begin_arg, val: 0); |
304 | } else { |
305 | #define MAX_SPEED 1000 |
306 | err |= comedi_check_trigger_arg_min(arg: &cmd->scan_begin_arg, |
307 | MAX_SPEED); |
308 | } |
309 | |
310 | err |= comedi_check_trigger_arg_is(arg: &cmd->convert_arg, val: 0); |
311 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_end_arg, |
312 | val: cmd->chanlist_len); |
313 | |
314 | if (cmd->stop_src == TRIG_COUNT) |
315 | err |= comedi_check_trigger_arg_min(arg: &cmd->stop_arg, val: 1); |
316 | else /* TRIG_NONE */ |
317 | err |= comedi_check_trigger_arg_is(arg: &cmd->stop_arg, val: 0); |
318 | |
319 | if (err) |
320 | return 3; |
321 | |
322 | /* step 4 */ |
323 | |
324 | if (cmd->scan_begin_src == TRIG_TIMER) { |
325 | unsigned int arg = cmd->scan_begin_arg; |
326 | |
327 | comedi_8254_cascade_ns_to_timer(i8254: dev->pacer, nanosec: &arg, flags: cmd->flags); |
328 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_begin_arg, val: arg); |
329 | } |
330 | |
331 | if (err) |
332 | return 4; |
333 | |
334 | return 0; |
335 | } |
336 | |
337 | static int pcl711_ai_cmd(struct comedi_device *dev, struct comedi_subdevice *s) |
338 | { |
339 | struct comedi_cmd *cmd = &s->async->cmd; |
340 | |
341 | pcl711_set_changain(dev, s, chanspec: cmd->chanlist[0]); |
342 | |
343 | if (cmd->scan_begin_src == TRIG_TIMER) { |
344 | comedi_8254_update_divisors(i8254: dev->pacer); |
345 | comedi_8254_pacer_enable(i8254: dev->pacer, counter1: 1, counter2: 2, enable: true); |
346 | outb(PCL711_INT_STAT_CLR, port: dev->iobase + PCL711_INT_STAT_REG); |
347 | pcl711_ai_set_mode(dev, PCL711_MODE_PACER_IRQ); |
348 | } else { |
349 | pcl711_ai_set_mode(dev, PCL711_MODE_EXT_IRQ); |
350 | } |
351 | |
352 | return 0; |
353 | } |
354 | |
355 | static void pcl711_ao_write(struct comedi_device *dev, |
356 | unsigned int chan, unsigned int val) |
357 | { |
358 | outb(value: val & 0xff, port: dev->iobase + PCL711_AO_LSB_REG(chan)); |
359 | outb(value: (val >> 8) & 0xff, port: dev->iobase + PCL711_AO_MSB_REG(chan)); |
360 | } |
361 | |
362 | static int pcl711_ao_insn_write(struct comedi_device *dev, |
363 | struct comedi_subdevice *s, |
364 | struct comedi_insn *insn, |
365 | unsigned int *data) |
366 | { |
367 | unsigned int chan = CR_CHAN(insn->chanspec); |
368 | unsigned int val = s->readback[chan]; |
369 | int i; |
370 | |
371 | for (i = 0; i < insn->n; i++) { |
372 | val = data[i]; |
373 | pcl711_ao_write(dev, chan, val); |
374 | } |
375 | s->readback[chan] = val; |
376 | |
377 | return insn->n; |
378 | } |
379 | |
380 | static int pcl711_di_insn_bits(struct comedi_device *dev, |
381 | struct comedi_subdevice *s, |
382 | struct comedi_insn *insn, |
383 | unsigned int *data) |
384 | { |
385 | unsigned int val; |
386 | |
387 | val = inb(port: dev->iobase + PCL711_DI_LSB_REG); |
388 | val |= (inb(port: dev->iobase + PCL711_DI_MSB_REG) << 8); |
389 | |
390 | data[1] = val; |
391 | |
392 | return insn->n; |
393 | } |
394 | |
395 | static int pcl711_do_insn_bits(struct comedi_device *dev, |
396 | struct comedi_subdevice *s, |
397 | struct comedi_insn *insn, |
398 | unsigned int *data) |
399 | { |
400 | unsigned int mask; |
401 | |
402 | mask = comedi_dio_update_state(s, data); |
403 | if (mask) { |
404 | if (mask & 0x00ff) |
405 | outb(value: s->state & 0xff, port: dev->iobase + PCL711_DO_LSB_REG); |
406 | if (mask & 0xff00) |
407 | outb(value: (s->state >> 8), port: dev->iobase + PCL711_DO_MSB_REG); |
408 | } |
409 | |
410 | data[1] = s->state; |
411 | |
412 | return insn->n; |
413 | } |
414 | |
415 | static int pcl711_attach(struct comedi_device *dev, struct comedi_devconfig *it) |
416 | { |
417 | const struct pcl711_board *board = dev->board_ptr; |
418 | struct comedi_subdevice *s; |
419 | int ret; |
420 | |
421 | ret = comedi_request_region(dev, start: it->options[0], len: 0x10); |
422 | if (ret) |
423 | return ret; |
424 | |
425 | if (it->options[1] && it->options[1] <= board->maxirq) { |
426 | ret = request_irq(irq: it->options[1], handler: pcl711_interrupt, flags: 0, |
427 | name: dev->board_name, dev); |
428 | if (ret == 0) |
429 | dev->irq = it->options[1]; |
430 | } |
431 | |
432 | dev->pacer = comedi_8254_io_alloc(iobase: dev->iobase + PCL711_TIMER_BASE, |
433 | I8254_OSC_BASE_2MHZ, I8254_IO8, regshift: 0); |
434 | if (IS_ERR(ptr: dev->pacer)) |
435 | return PTR_ERR(ptr: dev->pacer); |
436 | |
437 | ret = comedi_alloc_subdevices(dev, num_subdevices: 4); |
438 | if (ret) |
439 | return ret; |
440 | |
441 | /* Analog Input subdevice */ |
442 | s = &dev->subdevices[0]; |
443 | s->type = COMEDI_SUBD_AI; |
444 | s->subdev_flags = SDF_READABLE | SDF_GROUND; |
445 | if (board->n_aichan > 8) |
446 | s->subdev_flags |= SDF_DIFF; |
447 | s->n_chan = board->n_aichan; |
448 | s->maxdata = 0xfff; |
449 | s->range_table = board->ai_range_type; |
450 | s->insn_read = pcl711_ai_insn_read; |
451 | if (dev->irq) { |
452 | dev->read_subdev = s; |
453 | s->subdev_flags |= SDF_CMD_READ; |
454 | s->len_chanlist = 1; |
455 | s->do_cmdtest = pcl711_ai_cmdtest; |
456 | s->do_cmd = pcl711_ai_cmd; |
457 | s->cancel = pcl711_ai_cancel; |
458 | } |
459 | |
460 | /* Analog Output subdevice */ |
461 | s = &dev->subdevices[1]; |
462 | s->type = COMEDI_SUBD_AO; |
463 | s->subdev_flags = SDF_WRITABLE; |
464 | s->n_chan = board->n_aochan; |
465 | s->maxdata = 0xfff; |
466 | s->range_table = &range_bipolar5; |
467 | s->insn_write = pcl711_ao_insn_write; |
468 | |
469 | ret = comedi_alloc_subdev_readback(s); |
470 | if (ret) |
471 | return ret; |
472 | |
473 | /* Digital Input subdevice */ |
474 | s = &dev->subdevices[2]; |
475 | s->type = COMEDI_SUBD_DI; |
476 | s->subdev_flags = SDF_READABLE; |
477 | s->n_chan = 16; |
478 | s->maxdata = 1; |
479 | s->range_table = &range_digital; |
480 | s->insn_bits = pcl711_di_insn_bits; |
481 | |
482 | /* Digital Output subdevice */ |
483 | s = &dev->subdevices[3]; |
484 | s->type = COMEDI_SUBD_DO; |
485 | s->subdev_flags = SDF_WRITABLE; |
486 | s->n_chan = 16; |
487 | s->maxdata = 1; |
488 | s->range_table = &range_digital; |
489 | s->insn_bits = pcl711_do_insn_bits; |
490 | |
491 | /* clear DAC */ |
492 | pcl711_ao_write(dev, chan: 0, val: 0x0); |
493 | pcl711_ao_write(dev, chan: 1, val: 0x0); |
494 | |
495 | return 0; |
496 | } |
497 | |
498 | static struct comedi_driver pcl711_driver = { |
499 | .driver_name = "pcl711" , |
500 | .module = THIS_MODULE, |
501 | .attach = pcl711_attach, |
502 | .detach = comedi_legacy_detach, |
503 | .board_name = &boardtypes[0].name, |
504 | .num_names = ARRAY_SIZE(boardtypes), |
505 | .offset = sizeof(struct pcl711_board), |
506 | }; |
507 | module_comedi_driver(pcl711_driver); |
508 | |
509 | MODULE_AUTHOR("Comedi https://www.comedi.org" ); |
510 | MODULE_DESCRIPTION("Comedi driver for PCL-711 compatible boards" ); |
511 | MODULE_LICENSE("GPL" ); |
512 | |