1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* |
3 | * comedi/drivers/dt2814.c |
4 | * Hardware driver for Data Translation DT2814 |
5 | * |
6 | * COMEDI - Linux Control and Measurement Device Interface |
7 | * Copyright (C) 1998 David A. Schleef <ds@schleef.org> |
8 | */ |
9 | /* |
10 | * Driver: dt2814 |
11 | * Description: Data Translation DT2814 |
12 | * Author: ds |
13 | * Status: complete |
14 | * Devices: [Data Translation] DT2814 (dt2814) |
15 | * |
16 | * Configuration options: |
17 | * [0] - I/O port base address |
18 | * [1] - IRQ |
19 | * |
20 | * This card has 16 analog inputs multiplexed onto a 12 bit ADC. There |
21 | * is a minimally useful onboard clock. The base frequency for the |
22 | * clock is selected by jumpers, and the clock divider can be selected |
23 | * via programmed I/O. Unfortunately, the clock divider can only be |
24 | * a power of 10, from 1 to 10^7, of which only 3 or 4 are useful. In |
25 | * addition, the clock does not seem to be very accurate. |
26 | */ |
27 | |
28 | #include <linux/module.h> |
29 | #include <linux/interrupt.h> |
30 | #include <linux/comedi/comedidev.h> |
31 | #include <linux/delay.h> |
32 | |
33 | #define DT2814_CSR 0 |
34 | #define DT2814_DATA 1 |
35 | |
36 | /* |
37 | * flags |
38 | */ |
39 | |
40 | #define DT2814_FINISH 0x80 |
41 | #define DT2814_ERR 0x40 |
42 | #define DT2814_BUSY 0x20 |
43 | #define DT2814_ENB 0x10 |
44 | #define DT2814_CHANMASK 0x0f |
45 | |
46 | #define DT2814_TIMEOUT 10 |
47 | #define DT2814_MAX_SPEED 100000 /* Arbitrary 10 khz limit */ |
48 | |
49 | static int dt2814_ai_notbusy(struct comedi_device *dev, |
50 | struct comedi_subdevice *s, |
51 | struct comedi_insn *insn, |
52 | unsigned long context) |
53 | { |
54 | unsigned int status; |
55 | |
56 | status = inb(port: dev->iobase + DT2814_CSR); |
57 | if (context) |
58 | *(unsigned int *)context = status; |
59 | if (status & DT2814_BUSY) |
60 | return -EBUSY; |
61 | return 0; |
62 | } |
63 | |
64 | static int dt2814_ai_clear(struct comedi_device *dev) |
65 | { |
66 | unsigned int status = 0; |
67 | int ret; |
68 | |
69 | /* Wait until not busy and get status register value. */ |
70 | ret = comedi_timeout(dev, NULL, NULL, cb: dt2814_ai_notbusy, |
71 | context: (unsigned long)&status); |
72 | if (ret) |
73 | return ret; |
74 | |
75 | if (status & (DT2814_FINISH | DT2814_ERR)) { |
76 | /* |
77 | * There unread data, or the error flag is set. |
78 | * Read the data register twice to clear the condition. |
79 | */ |
80 | inb(port: dev->iobase + DT2814_DATA); |
81 | inb(port: dev->iobase + DT2814_DATA); |
82 | } |
83 | return 0; |
84 | } |
85 | |
86 | static int dt2814_ai_eoc(struct comedi_device *dev, |
87 | struct comedi_subdevice *s, |
88 | struct comedi_insn *insn, |
89 | unsigned long context) |
90 | { |
91 | unsigned int status; |
92 | |
93 | status = inb(port: dev->iobase + DT2814_CSR); |
94 | if (status & DT2814_FINISH) |
95 | return 0; |
96 | return -EBUSY; |
97 | } |
98 | |
99 | static int dt2814_ai_insn_read(struct comedi_device *dev, |
100 | struct comedi_subdevice *s, |
101 | struct comedi_insn *insn, unsigned int *data) |
102 | { |
103 | int n, hi, lo; |
104 | int chan; |
105 | int ret; |
106 | |
107 | dt2814_ai_clear(dev); /* clear stale data or error */ |
108 | for (n = 0; n < insn->n; n++) { |
109 | chan = CR_CHAN(insn->chanspec); |
110 | |
111 | outb(value: chan, port: dev->iobase + DT2814_CSR); |
112 | |
113 | ret = comedi_timeout(dev, s, insn, cb: dt2814_ai_eoc, context: 0); |
114 | if (ret) |
115 | return ret; |
116 | |
117 | hi = inb(port: dev->iobase + DT2814_DATA); |
118 | lo = inb(port: dev->iobase + DT2814_DATA); |
119 | |
120 | data[n] = (hi << 4) | (lo >> 4); |
121 | } |
122 | |
123 | return n; |
124 | } |
125 | |
126 | static int dt2814_ns_to_timer(unsigned int *ns, unsigned int flags) |
127 | { |
128 | int i; |
129 | unsigned int f; |
130 | |
131 | /* XXX ignores flags */ |
132 | |
133 | f = 10000; /* ns */ |
134 | for (i = 0; i < 8; i++) { |
135 | if ((2 * (*ns)) < (f * 11)) |
136 | break; |
137 | f *= 10; |
138 | } |
139 | |
140 | *ns = f; |
141 | |
142 | return i; |
143 | } |
144 | |
145 | static int dt2814_ai_cmdtest(struct comedi_device *dev, |
146 | struct comedi_subdevice *s, struct comedi_cmd *cmd) |
147 | { |
148 | int err = 0; |
149 | unsigned int arg; |
150 | |
151 | /* Step 1 : check if triggers are trivially valid */ |
152 | |
153 | err |= comedi_check_trigger_src(src: &cmd->start_src, TRIG_NOW); |
154 | err |= comedi_check_trigger_src(src: &cmd->scan_begin_src, TRIG_TIMER); |
155 | err |= comedi_check_trigger_src(src: &cmd->convert_src, TRIG_NOW); |
156 | err |= comedi_check_trigger_src(src: &cmd->scan_end_src, TRIG_COUNT); |
157 | err |= comedi_check_trigger_src(src: &cmd->stop_src, TRIG_COUNT | TRIG_NONE); |
158 | |
159 | if (err) |
160 | return 1; |
161 | |
162 | /* Step 2a : make sure trigger sources are unique */ |
163 | |
164 | err |= comedi_check_trigger_is_unique(src: cmd->stop_src); |
165 | |
166 | /* Step 2b : and mutually compatible */ |
167 | |
168 | if (err) |
169 | return 2; |
170 | |
171 | /* Step 3: check if arguments are trivially valid */ |
172 | |
173 | err |= comedi_check_trigger_arg_is(arg: &cmd->start_arg, val: 0); |
174 | |
175 | err |= comedi_check_trigger_arg_max(arg: &cmd->scan_begin_arg, val: 1000000000); |
176 | err |= comedi_check_trigger_arg_min(arg: &cmd->scan_begin_arg, |
177 | DT2814_MAX_SPEED); |
178 | |
179 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_end_arg, |
180 | val: cmd->chanlist_len); |
181 | |
182 | if (cmd->stop_src == TRIG_COUNT) |
183 | err |= comedi_check_trigger_arg_min(arg: &cmd->stop_arg, val: 2); |
184 | else /* TRIG_NONE */ |
185 | err |= comedi_check_trigger_arg_is(arg: &cmd->stop_arg, val: 0); |
186 | |
187 | if (err) |
188 | return 3; |
189 | |
190 | /* step 4: fix up any arguments */ |
191 | |
192 | arg = cmd->scan_begin_arg; |
193 | dt2814_ns_to_timer(ns: &arg, flags: cmd->flags); |
194 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_begin_arg, val: arg); |
195 | |
196 | if (err) |
197 | return 4; |
198 | |
199 | return 0; |
200 | } |
201 | |
202 | static int dt2814_ai_cmd(struct comedi_device *dev, struct comedi_subdevice *s) |
203 | { |
204 | struct comedi_cmd *cmd = &s->async->cmd; |
205 | int chan; |
206 | int trigvar; |
207 | |
208 | dt2814_ai_clear(dev); /* clear stale data or error */ |
209 | trigvar = dt2814_ns_to_timer(ns: &cmd->scan_begin_arg, flags: cmd->flags); |
210 | |
211 | chan = CR_CHAN(cmd->chanlist[0]); |
212 | |
213 | outb(value: chan | DT2814_ENB | (trigvar << 5), port: dev->iobase + DT2814_CSR); |
214 | |
215 | return 0; |
216 | } |
217 | |
218 | static int dt2814_ai_cancel(struct comedi_device *dev, |
219 | struct comedi_subdevice *s) |
220 | { |
221 | unsigned int status; |
222 | unsigned long flags; |
223 | |
224 | spin_lock_irqsave(&dev->spinlock, flags); |
225 | status = inb(port: dev->iobase + DT2814_CSR); |
226 | if (status & DT2814_ENB) { |
227 | /* |
228 | * Clear the timed trigger enable bit. |
229 | * |
230 | * Note: turning off timed mode triggers another |
231 | * sample. This will be mopped up by the calls to |
232 | * dt2814_ai_clear(). |
233 | */ |
234 | outb(value: status & DT2814_CHANMASK, port: dev->iobase + DT2814_CSR); |
235 | } |
236 | spin_unlock_irqrestore(lock: &dev->spinlock, flags); |
237 | return 0; |
238 | } |
239 | |
240 | static irqreturn_t dt2814_interrupt(int irq, void *d) |
241 | { |
242 | struct comedi_device *dev = d; |
243 | struct comedi_subdevice *s = dev->read_subdev; |
244 | struct comedi_async *async; |
245 | unsigned int lo, hi; |
246 | unsigned short data; |
247 | unsigned int status; |
248 | |
249 | if (!dev->attached) { |
250 | dev_err(dev->class_dev, "spurious interrupt\n" ); |
251 | return IRQ_HANDLED; |
252 | } |
253 | |
254 | async = s->async; |
255 | |
256 | spin_lock(lock: &dev->spinlock); |
257 | |
258 | status = inb(port: dev->iobase + DT2814_CSR); |
259 | if (!(status & DT2814_ENB)) { |
260 | /* Timed acquisition not enabled. Nothing to do. */ |
261 | spin_unlock(lock: &dev->spinlock); |
262 | return IRQ_HANDLED; |
263 | } |
264 | |
265 | if (!(status & (DT2814_FINISH | DT2814_ERR))) { |
266 | /* Spurious interrupt? */ |
267 | spin_unlock(lock: &dev->spinlock); |
268 | return IRQ_HANDLED; |
269 | } |
270 | |
271 | /* Read data or clear error. */ |
272 | hi = inb(port: dev->iobase + DT2814_DATA); |
273 | lo = inb(port: dev->iobase + DT2814_DATA); |
274 | |
275 | data = (hi << 4) | (lo >> 4); |
276 | |
277 | if (status & DT2814_ERR) { |
278 | async->events |= COMEDI_CB_ERROR; |
279 | } else { |
280 | comedi_buf_write_samples(s, data: &data, nsamples: 1); |
281 | if (async->cmd.stop_src == TRIG_COUNT && |
282 | async->scans_done >= async->cmd.stop_arg) { |
283 | async->events |= COMEDI_CB_EOA; |
284 | } |
285 | } |
286 | if (async->events & COMEDI_CB_CANCEL_MASK) { |
287 | /* |
288 | * Disable timed mode. |
289 | * |
290 | * Note: turning off timed mode triggers another |
291 | * sample. This will be mopped up by the calls to |
292 | * dt2814_ai_clear(). |
293 | */ |
294 | outb(value: status & DT2814_CHANMASK, port: dev->iobase + DT2814_CSR); |
295 | } |
296 | |
297 | spin_unlock(lock: &dev->spinlock); |
298 | |
299 | comedi_handle_events(dev, s); |
300 | return IRQ_HANDLED; |
301 | } |
302 | |
303 | static int dt2814_attach(struct comedi_device *dev, struct comedi_devconfig *it) |
304 | { |
305 | struct comedi_subdevice *s; |
306 | int ret; |
307 | |
308 | ret = comedi_request_region(dev, start: it->options[0], len: 0x2); |
309 | if (ret) |
310 | return ret; |
311 | |
312 | outb(value: 0, port: dev->iobase + DT2814_CSR); |
313 | if (dt2814_ai_clear(dev)) { |
314 | dev_err(dev->class_dev, "reset error (fatal)\n" ); |
315 | return -EIO; |
316 | } |
317 | |
318 | if (it->options[1]) { |
319 | ret = request_irq(irq: it->options[1], handler: dt2814_interrupt, flags: 0, |
320 | name: dev->board_name, dev); |
321 | if (ret == 0) |
322 | dev->irq = it->options[1]; |
323 | } |
324 | |
325 | ret = comedi_alloc_subdevices(dev, num_subdevices: 1); |
326 | if (ret) |
327 | return ret; |
328 | |
329 | s = &dev->subdevices[0]; |
330 | s->type = COMEDI_SUBD_AI; |
331 | s->subdev_flags = SDF_READABLE | SDF_GROUND; |
332 | s->n_chan = 16; /* XXX */ |
333 | s->insn_read = dt2814_ai_insn_read; |
334 | s->maxdata = 0xfff; |
335 | s->range_table = &range_unknown; /* XXX */ |
336 | if (dev->irq) { |
337 | dev->read_subdev = s; |
338 | s->subdev_flags |= SDF_CMD_READ; |
339 | s->len_chanlist = 1; |
340 | s->do_cmd = dt2814_ai_cmd; |
341 | s->do_cmdtest = dt2814_ai_cmdtest; |
342 | s->cancel = dt2814_ai_cancel; |
343 | } |
344 | |
345 | return 0; |
346 | } |
347 | |
348 | static void dt2814_detach(struct comedi_device *dev) |
349 | { |
350 | if (dev->irq) { |
351 | /* |
352 | * An extra conversion triggered on termination of an |
353 | * asynchronous command may still be in progress. Wait for |
354 | * it to finish and clear the data or error status. |
355 | */ |
356 | dt2814_ai_clear(dev); |
357 | } |
358 | comedi_legacy_detach(dev); |
359 | } |
360 | |
361 | static struct comedi_driver dt2814_driver = { |
362 | .driver_name = "dt2814" , |
363 | .module = THIS_MODULE, |
364 | .attach = dt2814_attach, |
365 | .detach = dt2814_detach, |
366 | }; |
367 | module_comedi_driver(dt2814_driver); |
368 | |
369 | MODULE_AUTHOR("Comedi https://www.comedi.org" ); |
370 | MODULE_DESCRIPTION("Comedi low-level driver" ); |
371 | MODULE_LICENSE("GPL" ); |
372 | |