1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* |
3 | * pcl726.c |
4 | * Comedi driver for 6/12-Channel D/A Output and DIO cards |
5 | * |
6 | * COMEDI - Linux Control and Measurement Device Interface |
7 | * Copyright (C) 1998 David A. Schleef <ds@schleef.org> |
8 | */ |
9 | |
10 | /* |
11 | * Driver: pcl726 |
12 | * Description: Advantech PCL-726 & compatibles |
13 | * Author: David A. Schleef <ds@schleef.org> |
14 | * Status: untested |
15 | * Devices: [Advantech] PCL-726 (pcl726), PCL-727 (pcl727), PCL-728 (pcl728), |
16 | * [ADLink] ACL-6126 (acl6126), ACL-6128 (acl6128) |
17 | * |
18 | * Configuration Options: |
19 | * [0] - IO Base |
20 | * [1] - IRQ (ACL-6126 only) |
21 | * [2] - D/A output range for channel 0 |
22 | * [3] - D/A output range for channel 1 |
23 | * |
24 | * Boards with > 2 analog output channels: |
25 | * [4] - D/A output range for channel 2 |
26 | * [5] - D/A output range for channel 3 |
27 | * [6] - D/A output range for channel 4 |
28 | * [7] - D/A output range for channel 5 |
29 | * |
30 | * Boards with > 6 analog output channels: |
31 | * [8] - D/A output range for channel 6 |
32 | * [9] - D/A output range for channel 7 |
33 | * [10] - D/A output range for channel 8 |
34 | * [11] - D/A output range for channel 9 |
35 | * [12] - D/A output range for channel 10 |
36 | * [13] - D/A output range for channel 11 |
37 | * |
38 | * For PCL-726 the D/A output ranges are: |
39 | * 0: 0-5V, 1: 0-10V, 2: +/-5V, 3: +/-10V, 4: 4-20mA, 5: unknown |
40 | * |
41 | * For PCL-727: |
42 | * 0: 0-5V, 1: 0-10V, 2: +/-5V, 3: 4-20mA |
43 | * |
44 | * For PCL-728 and ACL-6128: |
45 | * 0: 0-5V, 1: 0-10V, 2: +/-5V, 3: +/-10V, 4: 4-20mA, 5: 0-20mA |
46 | * |
47 | * For ACL-6126: |
48 | * 0: 0-5V, 1: 0-10V, 2: +/-5V, 3: +/-10V, 4: 4-20mA |
49 | */ |
50 | |
51 | #include <linux/module.h> |
52 | #include <linux/interrupt.h> |
53 | #include <linux/comedi/comedidev.h> |
54 | |
55 | #define PCL726_AO_MSB_REG(x) (0x00 + ((x) * 2)) |
56 | #define PCL726_AO_LSB_REG(x) (0x01 + ((x) * 2)) |
57 | #define PCL726_DO_MSB_REG 0x0c |
58 | #define PCL726_DO_LSB_REG 0x0d |
59 | #define PCL726_DI_MSB_REG 0x0e |
60 | #define PCL726_DI_LSB_REG 0x0f |
61 | |
62 | #define PCL727_DI_MSB_REG 0x00 |
63 | #define PCL727_DI_LSB_REG 0x01 |
64 | #define PCL727_DO_MSB_REG 0x18 |
65 | #define PCL727_DO_LSB_REG 0x19 |
66 | |
67 | static const struct comedi_lrange *const rangelist_726[] = { |
68 | &range_unipolar5, |
69 | &range_unipolar10, |
70 | &range_bipolar5, |
71 | &range_bipolar10, |
72 | &range_4_20mA, |
73 | &range_unknown |
74 | }; |
75 | |
76 | static const struct comedi_lrange *const rangelist_727[] = { |
77 | &range_unipolar5, |
78 | &range_unipolar10, |
79 | &range_bipolar5, |
80 | &range_4_20mA |
81 | }; |
82 | |
83 | static const struct comedi_lrange *const rangelist_728[] = { |
84 | &range_unipolar5, |
85 | &range_unipolar10, |
86 | &range_bipolar5, |
87 | &range_bipolar10, |
88 | &range_4_20mA, |
89 | &range_0_20mA |
90 | }; |
91 | |
92 | struct pcl726_board { |
93 | const char *name; |
94 | unsigned long io_len; |
95 | unsigned int irq_mask; |
96 | const struct comedi_lrange *const *ao_ranges; |
97 | int ao_num_ranges; |
98 | int ao_nchan; |
99 | unsigned int have_dio:1; |
100 | unsigned int is_pcl727:1; |
101 | }; |
102 | |
103 | static const struct pcl726_board pcl726_boards[] = { |
104 | { |
105 | .name = "pcl726" , |
106 | .io_len = 0x10, |
107 | .ao_ranges = &rangelist_726[0], |
108 | .ao_num_ranges = ARRAY_SIZE(rangelist_726), |
109 | .ao_nchan = 6, |
110 | .have_dio = 1, |
111 | }, { |
112 | .name = "pcl727" , |
113 | .io_len = 0x20, |
114 | .ao_ranges = &rangelist_727[0], |
115 | .ao_num_ranges = ARRAY_SIZE(rangelist_727), |
116 | .ao_nchan = 12, |
117 | .have_dio = 1, |
118 | .is_pcl727 = 1, |
119 | }, { |
120 | .name = "pcl728" , |
121 | .io_len = 0x08, |
122 | .ao_num_ranges = ARRAY_SIZE(rangelist_728), |
123 | .ao_ranges = &rangelist_728[0], |
124 | .ao_nchan = 2, |
125 | }, { |
126 | .name = "acl6126" , |
127 | .io_len = 0x10, |
128 | .irq_mask = 0x96e8, |
129 | .ao_num_ranges = ARRAY_SIZE(rangelist_726), |
130 | .ao_ranges = &rangelist_726[0], |
131 | .ao_nchan = 6, |
132 | .have_dio = 1, |
133 | }, { |
134 | .name = "acl6128" , |
135 | .io_len = 0x08, |
136 | .ao_num_ranges = ARRAY_SIZE(rangelist_728), |
137 | .ao_ranges = &rangelist_728[0], |
138 | .ao_nchan = 2, |
139 | }, |
140 | }; |
141 | |
142 | struct pcl726_private { |
143 | const struct comedi_lrange *rangelist[12]; |
144 | unsigned int cmd_running:1; |
145 | }; |
146 | |
147 | static int pcl726_intr_insn_bits(struct comedi_device *dev, |
148 | struct comedi_subdevice *s, |
149 | struct comedi_insn *insn, |
150 | unsigned int *data) |
151 | { |
152 | data[1] = 0; |
153 | return insn->n; |
154 | } |
155 | |
156 | static int pcl726_intr_cmdtest(struct comedi_device *dev, |
157 | struct comedi_subdevice *s, |
158 | struct comedi_cmd *cmd) |
159 | { |
160 | int err = 0; |
161 | |
162 | /* Step 1 : check if triggers are trivially valid */ |
163 | |
164 | err |= comedi_check_trigger_src(src: &cmd->start_src, TRIG_NOW); |
165 | err |= comedi_check_trigger_src(src: &cmd->scan_begin_src, TRIG_EXT); |
166 | err |= comedi_check_trigger_src(src: &cmd->convert_src, TRIG_FOLLOW); |
167 | err |= comedi_check_trigger_src(src: &cmd->scan_end_src, TRIG_COUNT); |
168 | err |= comedi_check_trigger_src(src: &cmd->stop_src, TRIG_NONE); |
169 | |
170 | if (err) |
171 | return 1; |
172 | |
173 | /* Step 2a : make sure trigger sources are unique */ |
174 | /* Step 2b : and mutually compatible */ |
175 | |
176 | /* Step 3: check if arguments are trivially valid */ |
177 | |
178 | err |= comedi_check_trigger_arg_is(arg: &cmd->start_arg, val: 0); |
179 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_begin_arg, val: 0); |
180 | err |= comedi_check_trigger_arg_is(arg: &cmd->convert_arg, val: 0); |
181 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_end_arg, |
182 | val: cmd->chanlist_len); |
183 | err |= comedi_check_trigger_arg_is(arg: &cmd->stop_arg, val: 0); |
184 | |
185 | if (err) |
186 | return 3; |
187 | |
188 | /* Step 4: fix up any arguments */ |
189 | |
190 | /* Step 5: check channel list if it exists */ |
191 | |
192 | return 0; |
193 | } |
194 | |
195 | static int pcl726_intr_cmd(struct comedi_device *dev, |
196 | struct comedi_subdevice *s) |
197 | { |
198 | struct pcl726_private *devpriv = dev->private; |
199 | |
200 | devpriv->cmd_running = 1; |
201 | |
202 | return 0; |
203 | } |
204 | |
205 | static int pcl726_intr_cancel(struct comedi_device *dev, |
206 | struct comedi_subdevice *s) |
207 | { |
208 | struct pcl726_private *devpriv = dev->private; |
209 | |
210 | devpriv->cmd_running = 0; |
211 | |
212 | return 0; |
213 | } |
214 | |
215 | static irqreturn_t pcl726_interrupt(int irq, void *d) |
216 | { |
217 | struct comedi_device *dev = d; |
218 | struct comedi_subdevice *s = dev->read_subdev; |
219 | struct pcl726_private *devpriv = dev->private; |
220 | |
221 | if (devpriv->cmd_running) { |
222 | unsigned short val = 0; |
223 | |
224 | pcl726_intr_cancel(dev, s); |
225 | |
226 | comedi_buf_write_samples(s, data: &val, nsamples: 1); |
227 | comedi_handle_events(dev, s); |
228 | } |
229 | |
230 | return IRQ_HANDLED; |
231 | } |
232 | |
233 | static int pcl726_ao_insn_write(struct comedi_device *dev, |
234 | struct comedi_subdevice *s, |
235 | struct comedi_insn *insn, |
236 | unsigned int *data) |
237 | { |
238 | unsigned int chan = CR_CHAN(insn->chanspec); |
239 | unsigned int range = CR_RANGE(insn->chanspec); |
240 | int i; |
241 | |
242 | for (i = 0; i < insn->n; i++) { |
243 | unsigned int val = data[i]; |
244 | |
245 | s->readback[chan] = val; |
246 | |
247 | /* bipolar data to the DAC is two's complement */ |
248 | if (comedi_chan_range_is_bipolar(s, chan, range)) |
249 | val = comedi_offset_munge(s, val); |
250 | |
251 | /* order is important, MSB then LSB */ |
252 | outb(value: (val >> 8) & 0xff, port: dev->iobase + PCL726_AO_MSB_REG(chan)); |
253 | outb(value: val & 0xff, port: dev->iobase + PCL726_AO_LSB_REG(chan)); |
254 | } |
255 | |
256 | return insn->n; |
257 | } |
258 | |
259 | static int pcl726_di_insn_bits(struct comedi_device *dev, |
260 | struct comedi_subdevice *s, |
261 | struct comedi_insn *insn, |
262 | unsigned int *data) |
263 | { |
264 | const struct pcl726_board *board = dev->board_ptr; |
265 | unsigned int val; |
266 | |
267 | if (board->is_pcl727) { |
268 | val = inb(port: dev->iobase + PCL727_DI_LSB_REG); |
269 | val |= (inb(port: dev->iobase + PCL727_DI_MSB_REG) << 8); |
270 | } else { |
271 | val = inb(port: dev->iobase + PCL726_DI_LSB_REG); |
272 | val |= (inb(port: dev->iobase + PCL726_DI_MSB_REG) << 8); |
273 | } |
274 | |
275 | data[1] = val; |
276 | |
277 | return insn->n; |
278 | } |
279 | |
280 | static int pcl726_do_insn_bits(struct comedi_device *dev, |
281 | struct comedi_subdevice *s, |
282 | struct comedi_insn *insn, |
283 | unsigned int *data) |
284 | { |
285 | const struct pcl726_board *board = dev->board_ptr; |
286 | unsigned long io = dev->iobase; |
287 | unsigned int mask; |
288 | |
289 | mask = comedi_dio_update_state(s, data); |
290 | if (mask) { |
291 | if (board->is_pcl727) { |
292 | if (mask & 0x00ff) |
293 | outb(value: s->state & 0xff, port: io + PCL727_DO_LSB_REG); |
294 | if (mask & 0xff00) |
295 | outb(value: (s->state >> 8), port: io + PCL727_DO_MSB_REG); |
296 | } else { |
297 | if (mask & 0x00ff) |
298 | outb(value: s->state & 0xff, port: io + PCL726_DO_LSB_REG); |
299 | if (mask & 0xff00) |
300 | outb(value: (s->state >> 8), port: io + PCL726_DO_MSB_REG); |
301 | } |
302 | } |
303 | |
304 | data[1] = s->state; |
305 | |
306 | return insn->n; |
307 | } |
308 | |
309 | static int pcl726_attach(struct comedi_device *dev, |
310 | struct comedi_devconfig *it) |
311 | { |
312 | const struct pcl726_board *board = dev->board_ptr; |
313 | struct pcl726_private *devpriv; |
314 | struct comedi_subdevice *s; |
315 | int subdev; |
316 | int ret; |
317 | int i; |
318 | |
319 | ret = comedi_request_region(dev, start: it->options[0], len: board->io_len); |
320 | if (ret) |
321 | return ret; |
322 | |
323 | devpriv = comedi_alloc_devpriv(dev, size: sizeof(*devpriv)); |
324 | if (!devpriv) |
325 | return -ENOMEM; |
326 | |
327 | /* |
328 | * Hook up the external trigger source interrupt only if the |
329 | * user config option is valid and the board supports interrupts. |
330 | */ |
331 | if (it->options[1] && (board->irq_mask & (1 << it->options[1]))) { |
332 | ret = request_irq(irq: it->options[1], handler: pcl726_interrupt, flags: 0, |
333 | name: dev->board_name, dev); |
334 | if (ret == 0) { |
335 | /* External trigger source is from Pin-17 of CN3 */ |
336 | dev->irq = it->options[1]; |
337 | } |
338 | } |
339 | |
340 | /* setup the per-channel analog output range_table_list */ |
341 | for (i = 0; i < 12; i++) { |
342 | unsigned int opt = it->options[2 + i]; |
343 | |
344 | if (opt < board->ao_num_ranges && i < board->ao_nchan) |
345 | devpriv->rangelist[i] = board->ao_ranges[opt]; |
346 | else |
347 | devpriv->rangelist[i] = &range_unknown; |
348 | } |
349 | |
350 | subdev = board->have_dio ? 3 : 1; |
351 | if (dev->irq) |
352 | subdev++; |
353 | ret = comedi_alloc_subdevices(dev, num_subdevices: subdev); |
354 | if (ret) |
355 | return ret; |
356 | |
357 | subdev = 0; |
358 | |
359 | /* Analog Output subdevice */ |
360 | s = &dev->subdevices[subdev++]; |
361 | s->type = COMEDI_SUBD_AO; |
362 | s->subdev_flags = SDF_WRITABLE | SDF_GROUND; |
363 | s->n_chan = board->ao_nchan; |
364 | s->maxdata = 0x0fff; |
365 | s->range_table_list = devpriv->rangelist; |
366 | s->insn_write = pcl726_ao_insn_write; |
367 | |
368 | ret = comedi_alloc_subdev_readback(s); |
369 | if (ret) |
370 | return ret; |
371 | |
372 | if (board->have_dio) { |
373 | /* Digital Input subdevice */ |
374 | s = &dev->subdevices[subdev++]; |
375 | s->type = COMEDI_SUBD_DI; |
376 | s->subdev_flags = SDF_READABLE; |
377 | s->n_chan = 16; |
378 | s->maxdata = 1; |
379 | s->insn_bits = pcl726_di_insn_bits; |
380 | s->range_table = &range_digital; |
381 | |
382 | /* Digital Output subdevice */ |
383 | s = &dev->subdevices[subdev++]; |
384 | s->type = COMEDI_SUBD_DO; |
385 | s->subdev_flags = SDF_WRITABLE; |
386 | s->n_chan = 16; |
387 | s->maxdata = 1; |
388 | s->insn_bits = pcl726_do_insn_bits; |
389 | s->range_table = &range_digital; |
390 | } |
391 | |
392 | if (dev->irq) { |
393 | /* Digital Input subdevice - Interrupt support */ |
394 | s = &dev->subdevices[subdev++]; |
395 | dev->read_subdev = s; |
396 | s->type = COMEDI_SUBD_DI; |
397 | s->subdev_flags = SDF_READABLE | SDF_CMD_READ; |
398 | s->n_chan = 1; |
399 | s->maxdata = 1; |
400 | s->range_table = &range_digital; |
401 | s->insn_bits = pcl726_intr_insn_bits; |
402 | s->len_chanlist = 1; |
403 | s->do_cmdtest = pcl726_intr_cmdtest; |
404 | s->do_cmd = pcl726_intr_cmd; |
405 | s->cancel = pcl726_intr_cancel; |
406 | } |
407 | |
408 | return 0; |
409 | } |
410 | |
411 | static struct comedi_driver pcl726_driver = { |
412 | .driver_name = "pcl726" , |
413 | .module = THIS_MODULE, |
414 | .attach = pcl726_attach, |
415 | .detach = comedi_legacy_detach, |
416 | .board_name = &pcl726_boards[0].name, |
417 | .num_names = ARRAY_SIZE(pcl726_boards), |
418 | .offset = sizeof(struct pcl726_board), |
419 | }; |
420 | module_comedi_driver(pcl726_driver); |
421 | |
422 | MODULE_AUTHOR("Comedi https://www.comedi.org" ); |
423 | MODULE_DESCRIPTION("Comedi driver for Advantech PCL-726 & compatibles" ); |
424 | MODULE_LICENSE("GPL" ); |
425 | |