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