1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * comedi/drivers/pcl818.c |
4 | * |
5 | * Driver: pcl818 |
6 | * Description: Advantech PCL-818 cards, PCL-718 |
7 | * Author: Michal Dobes <dobes@tesnet.cz> |
8 | * Devices: [Advantech] PCL-818L (pcl818l), PCL-818H (pcl818h), |
9 | * PCL-818HD (pcl818hd), PCL-818HG (pcl818hg), PCL-818 (pcl818), |
10 | * PCL-718 (pcl718) |
11 | * Status: works |
12 | * |
13 | * All cards have 16 SE/8 DIFF ADCs, one or two DACs, 16 DI and 16 DO. |
14 | * Differences are only at maximal sample speed, range list and FIFO |
15 | * support. |
16 | * The driver support AI mode 0, 1, 3 other subdevices (AO, DI, DO) support |
17 | * only mode 0. If DMA/FIFO/INT are disabled then AI support only mode 0. |
18 | * PCL-818HD and PCL-818HG support 1kword FIFO. Driver support this FIFO |
19 | * but this code is untested. |
20 | * A word or two about DMA. Driver support DMA operations at two ways: |
21 | * 1) DMA uses two buffers and after one is filled then is generated |
22 | * INT and DMA restart with second buffer. With this mode I'm unable run |
23 | * more that 80Ksamples/secs without data dropouts on K6/233. |
24 | * 2) DMA uses one buffer and run in autoinit mode and the data are |
25 | * from DMA buffer moved on the fly with 2kHz interrupts from RTC. |
26 | * This mode is used if the interrupt 8 is available for allocation. |
27 | * If not, then first DMA mode is used. With this I can run at |
28 | * full speed one card (100ksamples/secs) or two cards with |
29 | * 60ksamples/secs each (more is problem on account of ISA limitations). |
30 | * To use this mode you must have compiled kernel with disabled |
31 | * "Enhanced Real Time Clock Support". |
32 | * Maybe you can have problems if you use xntpd or similar. |
33 | * If you've data dropouts with DMA mode 2 then: |
34 | * a) disable IDE DMA |
35 | * b) switch text mode console to fb. |
36 | * |
37 | * Options for PCL-818L: |
38 | * [0] - IO Base |
39 | * [1] - IRQ (0=disable, 2, 3, 4, 5, 6, 7) |
40 | * [2] - DMA (0=disable, 1, 3) |
41 | * [3] - 0, 10=10MHz clock for 8254 |
42 | * 1= 1MHz clock for 8254 |
43 | * [4] - 0, 5=A/D input -5V.. +5V |
44 | * 1, 10=A/D input -10V..+10V |
45 | * [5] - 0, 5=D/A output 0-5V (internal reference -5V) |
46 | * 1, 10=D/A output 0-10V (internal reference -10V) |
47 | * 2 =D/A output unknown (external reference) |
48 | * |
49 | * Options for PCL-818, PCL-818H: |
50 | * [0] - IO Base |
51 | * [1] - IRQ (0=disable, 2, 3, 4, 5, 6, 7) |
52 | * [2] - DMA (0=disable, 1, 3) |
53 | * [3] - 0, 10=10MHz clock for 8254 |
54 | * 1= 1MHz clock for 8254 |
55 | * [4] - 0, 5=D/A output 0-5V (internal reference -5V) |
56 | * 1, 10=D/A output 0-10V (internal reference -10V) |
57 | * 2 =D/A output unknown (external reference) |
58 | * |
59 | * Options for PCL-818HD, PCL-818HG: |
60 | * [0] - IO Base |
61 | * [1] - IRQ (0=disable, 2, 3, 4, 5, 6, 7) |
62 | * [2] - DMA/FIFO (-1=use FIFO, 0=disable both FIFO and DMA, |
63 | * 1=use DMA ch 1, 3=use DMA ch 3) |
64 | * [3] - 0, 10=10MHz clock for 8254 |
65 | * 1= 1MHz clock for 8254 |
66 | * [4] - 0, 5=D/A output 0-5V (internal reference -5V) |
67 | * 1, 10=D/A output 0-10V (internal reference -10V) |
68 | * 2 =D/A output unknown (external reference) |
69 | * |
70 | * Options for PCL-718: |
71 | * [0] - IO Base |
72 | * [1] - IRQ (0=disable, 2, 3, 4, 5, 6, 7) |
73 | * [2] - DMA (0=disable, 1, 3) |
74 | * [3] - 0, 10=10MHz clock for 8254 |
75 | * 1= 1MHz clock for 8254 |
76 | * [4] - 0=A/D Range is +/-10V |
77 | * 1= +/-5V |
78 | * 2= +/-2.5V |
79 | * 3= +/-1V |
80 | * 4= +/-0.5V |
81 | * 5= user defined bipolar |
82 | * 6= 0-10V |
83 | * 7= 0-5V |
84 | * 8= 0-2V |
85 | * 9= 0-1V |
86 | * 10= user defined unipolar |
87 | * [5] - 0, 5=D/A outputs 0-5V (internal reference -5V) |
88 | * 1, 10=D/A outputs 0-10V (internal reference -10V) |
89 | * 2=D/A outputs unknown (external reference) |
90 | * [6] - 0, 60=max 60kHz A/D sampling |
91 | * 1,100=max 100kHz A/D sampling (PCL-718 with Option 001 installed) |
92 | * |
93 | */ |
94 | |
95 | #include <linux/module.h> |
96 | #include <linux/gfp.h> |
97 | #include <linux/delay.h> |
98 | #include <linux/io.h> |
99 | #include <linux/interrupt.h> |
100 | #include <linux/comedi/comedidev.h> |
101 | #include <linux/comedi/comedi_8254.h> |
102 | #include <linux/comedi/comedi_isadma.h> |
103 | |
104 | /* |
105 | * Register I/O map |
106 | */ |
107 | #define PCL818_AI_LSB_REG 0x00 |
108 | #define PCL818_AI_MSB_REG 0x01 |
109 | #define PCL818_RANGE_REG 0x01 |
110 | #define PCL818_MUX_REG 0x02 |
111 | #define PCL818_MUX_SCAN(_first, _last) (((_last) << 4) | (_first)) |
112 | #define PCL818_DO_DI_LSB_REG 0x03 |
113 | #define PCL818_AO_LSB_REG(x) (0x04 + ((x) * 2)) |
114 | #define PCL818_AO_MSB_REG(x) (0x05 + ((x) * 2)) |
115 | #define PCL818_STATUS_REG 0x08 |
116 | #define PCL818_STATUS_NEXT_CHAN_MASK (0xf << 0) |
117 | #define PCL818_STATUS_INT BIT(4) |
118 | #define PCL818_STATUS_MUX BIT(5) |
119 | #define PCL818_STATUS_UNI BIT(6) |
120 | #define PCL818_STATUS_EOC BIT(7) |
121 | #define PCL818_CTRL_REG 0x09 |
122 | #define PCL818_CTRL_TRIG(x) (((x) & 0x3) << 0) |
123 | #define PCL818_CTRL_DISABLE_TRIG PCL818_CTRL_TRIG(0) |
124 | #define PCL818_CTRL_SOFT_TRIG PCL818_CTRL_TRIG(1) |
125 | #define PCL818_CTRL_EXT_TRIG PCL818_CTRL_TRIG(2) |
126 | #define PCL818_CTRL_PACER_TRIG PCL818_CTRL_TRIG(3) |
127 | #define PCL818_CTRL_DMAE BIT(2) |
128 | #define PCL818_CTRL_IRQ(x) ((x) << 4) |
129 | #define PCL818_CTRL_INTE BIT(7) |
130 | #define PCL818_CNTENABLE_REG 0x0a |
131 | #define PCL818_CNTENABLE_PACER_TRIG0 BIT(0) |
132 | #define PCL818_CNTENABLE_CNT0_INT_CLK BIT(1) /* 0=ext clk */ |
133 | #define PCL818_DO_DI_MSB_REG 0x0b |
134 | #define PCL818_TIMER_BASE 0x0c |
135 | |
136 | /* W: fifo enable/disable */ |
137 | #define PCL818_FI_ENABLE 6 |
138 | /* W: fifo interrupt clear */ |
139 | #define PCL818_FI_INTCLR 20 |
140 | /* W: fifo interrupt clear */ |
141 | #define PCL818_FI_FLUSH 25 |
142 | /* R: fifo status */ |
143 | #define PCL818_FI_STATUS 25 |
144 | /* R: one record from FIFO */ |
145 | #define PCL818_FI_DATALO 23 |
146 | #define PCL818_FI_DATAHI 24 |
147 | |
148 | #define MAGIC_DMA_WORD 0x5a5a |
149 | |
150 | static const struct comedi_lrange range_pcl818h_ai = { |
151 | 9, { |
152 | BIP_RANGE(5), |
153 | BIP_RANGE(2.5), |
154 | BIP_RANGE(1.25), |
155 | BIP_RANGE(0.625), |
156 | UNI_RANGE(10), |
157 | UNI_RANGE(5), |
158 | UNI_RANGE(2.5), |
159 | UNI_RANGE(1.25), |
160 | BIP_RANGE(10) |
161 | } |
162 | }; |
163 | |
164 | static const struct comedi_lrange range_pcl818hg_ai = { |
165 | 10, { |
166 | BIP_RANGE(5), |
167 | BIP_RANGE(0.5), |
168 | BIP_RANGE(0.05), |
169 | BIP_RANGE(0.005), |
170 | UNI_RANGE(10), |
171 | UNI_RANGE(1), |
172 | UNI_RANGE(0.1), |
173 | UNI_RANGE(0.01), |
174 | BIP_RANGE(10), |
175 | BIP_RANGE(1), |
176 | BIP_RANGE(0.1), |
177 | BIP_RANGE(0.01) |
178 | } |
179 | }; |
180 | |
181 | static const struct comedi_lrange range_pcl818l_l_ai = { |
182 | 4, { |
183 | BIP_RANGE(5), |
184 | BIP_RANGE(2.5), |
185 | BIP_RANGE(1.25), |
186 | BIP_RANGE(0.625) |
187 | } |
188 | }; |
189 | |
190 | static const struct comedi_lrange range_pcl818l_h_ai = { |
191 | 4, { |
192 | BIP_RANGE(10), |
193 | BIP_RANGE(5), |
194 | BIP_RANGE(2.5), |
195 | BIP_RANGE(1.25) |
196 | } |
197 | }; |
198 | |
199 | static const struct comedi_lrange range718_bipolar1 = { |
200 | 1, { |
201 | BIP_RANGE(1) |
202 | } |
203 | }; |
204 | |
205 | static const struct comedi_lrange range718_bipolar0_5 = { |
206 | 1, { |
207 | BIP_RANGE(0.5) |
208 | } |
209 | }; |
210 | |
211 | static const struct comedi_lrange range718_unipolar2 = { |
212 | 1, { |
213 | UNI_RANGE(2) |
214 | } |
215 | }; |
216 | |
217 | static const struct comedi_lrange range718_unipolar1 = { |
218 | 1, { |
219 | BIP_RANGE(1) |
220 | } |
221 | }; |
222 | |
223 | struct pcl818_board { |
224 | const char *name; |
225 | unsigned int ns_min; |
226 | int n_aochan; |
227 | const struct comedi_lrange *ai_range_type; |
228 | unsigned int has_dma:1; |
229 | unsigned int has_fifo:1; |
230 | unsigned int is_818:1; |
231 | }; |
232 | |
233 | static const struct pcl818_board boardtypes[] = { |
234 | { |
235 | .name = "pcl818l" , |
236 | .ns_min = 25000, |
237 | .n_aochan = 1, |
238 | .ai_range_type = &range_pcl818l_l_ai, |
239 | .has_dma = 1, |
240 | .is_818 = 1, |
241 | }, { |
242 | .name = "pcl818h" , |
243 | .ns_min = 10000, |
244 | .n_aochan = 1, |
245 | .ai_range_type = &range_pcl818h_ai, |
246 | .has_dma = 1, |
247 | .is_818 = 1, |
248 | }, { |
249 | .name = "pcl818hd" , |
250 | .ns_min = 10000, |
251 | .n_aochan = 1, |
252 | .ai_range_type = &range_pcl818h_ai, |
253 | .has_dma = 1, |
254 | .has_fifo = 1, |
255 | .is_818 = 1, |
256 | }, { |
257 | .name = "pcl818hg" , |
258 | .ns_min = 10000, |
259 | .n_aochan = 1, |
260 | .ai_range_type = &range_pcl818hg_ai, |
261 | .has_dma = 1, |
262 | .has_fifo = 1, |
263 | .is_818 = 1, |
264 | }, { |
265 | .name = "pcl818" , |
266 | .ns_min = 10000, |
267 | .n_aochan = 2, |
268 | .ai_range_type = &range_pcl818h_ai, |
269 | .has_dma = 1, |
270 | .is_818 = 1, |
271 | }, { |
272 | .name = "pcl718" , |
273 | .ns_min = 16000, |
274 | .n_aochan = 2, |
275 | .ai_range_type = &range_unipolar5, |
276 | .has_dma = 1, |
277 | }, { |
278 | .name = "pcm3718" , |
279 | .ns_min = 10000, |
280 | .ai_range_type = &range_pcl818h_ai, |
281 | .has_dma = 1, |
282 | .is_818 = 1, |
283 | }, |
284 | }; |
285 | |
286 | struct pcl818_private { |
287 | struct comedi_isadma *dma; |
288 | /* manimal allowed delay between samples (in us) for actual card */ |
289 | unsigned int ns_min; |
290 | /* MUX setting for actual AI operations */ |
291 | unsigned int act_chanlist[16]; |
292 | unsigned int act_chanlist_len; /* how long is actual MUX list */ |
293 | unsigned int act_chanlist_pos; /* actual position in MUX list */ |
294 | unsigned int usefifo:1; |
295 | unsigned int ai_cmd_running:1; |
296 | unsigned int ai_cmd_canceled:1; |
297 | }; |
298 | |
299 | static void pcl818_ai_setup_dma(struct comedi_device *dev, |
300 | struct comedi_subdevice *s, |
301 | unsigned int unread_samples) |
302 | { |
303 | struct pcl818_private *devpriv = dev->private; |
304 | struct comedi_isadma *dma = devpriv->dma; |
305 | struct comedi_isadma_desc *desc = &dma->desc[dma->cur_dma]; |
306 | unsigned int max_samples = comedi_bytes_to_samples(s, nbytes: desc->maxsize); |
307 | unsigned int nsamples; |
308 | |
309 | comedi_isadma_disable(dma_chan: dma->chan); |
310 | |
311 | /* |
312 | * Determine dma size based on the buffer maxsize plus the number of |
313 | * unread samples and the number of samples remaining in the command. |
314 | */ |
315 | nsamples = comedi_nsamples_left(s, nsamples: max_samples + unread_samples); |
316 | if (nsamples > unread_samples) { |
317 | nsamples -= unread_samples; |
318 | desc->size = comedi_samples_to_bytes(s, nsamples); |
319 | comedi_isadma_program(desc); |
320 | } |
321 | } |
322 | |
323 | static void pcl818_ai_set_chan_range(struct comedi_device *dev, |
324 | unsigned int chan, |
325 | unsigned int range) |
326 | { |
327 | outb(value: chan, port: dev->iobase + PCL818_MUX_REG); |
328 | outb(value: range, port: dev->iobase + PCL818_RANGE_REG); |
329 | } |
330 | |
331 | static void pcl818_ai_set_chan_scan(struct comedi_device *dev, |
332 | unsigned int first_chan, |
333 | unsigned int last_chan) |
334 | { |
335 | outb(PCL818_MUX_SCAN(first_chan, last_chan), |
336 | port: dev->iobase + PCL818_MUX_REG); |
337 | } |
338 | |
339 | static void pcl818_ai_setup_chanlist(struct comedi_device *dev, |
340 | unsigned int *chanlist, |
341 | unsigned int seglen) |
342 | { |
343 | struct pcl818_private *devpriv = dev->private; |
344 | unsigned int first_chan = CR_CHAN(chanlist[0]); |
345 | unsigned int last_chan; |
346 | unsigned int range; |
347 | int i; |
348 | |
349 | devpriv->act_chanlist_len = seglen; |
350 | devpriv->act_chanlist_pos = 0; |
351 | |
352 | /* store range list to card */ |
353 | for (i = 0; i < seglen; i++) { |
354 | last_chan = CR_CHAN(chanlist[i]); |
355 | range = CR_RANGE(chanlist[i]); |
356 | |
357 | devpriv->act_chanlist[i] = last_chan; |
358 | |
359 | pcl818_ai_set_chan_range(dev, chan: last_chan, range); |
360 | } |
361 | |
362 | udelay(1); |
363 | |
364 | pcl818_ai_set_chan_scan(dev, first_chan, last_chan); |
365 | } |
366 | |
367 | static void pcl818_ai_clear_eoc(struct comedi_device *dev) |
368 | { |
369 | /* writing any value clears the interrupt request */ |
370 | outb(value: 0, port: dev->iobase + PCL818_STATUS_REG); |
371 | } |
372 | |
373 | static void pcl818_ai_soft_trig(struct comedi_device *dev) |
374 | { |
375 | /* writing any value triggers a software conversion */ |
376 | outb(value: 0, port: dev->iobase + PCL818_AI_LSB_REG); |
377 | } |
378 | |
379 | static unsigned int pcl818_ai_get_fifo_sample(struct comedi_device *dev, |
380 | struct comedi_subdevice *s, |
381 | unsigned int *chan) |
382 | { |
383 | unsigned int val; |
384 | |
385 | val = inb(port: dev->iobase + PCL818_FI_DATALO); |
386 | val |= (inb(port: dev->iobase + PCL818_FI_DATAHI) << 8); |
387 | |
388 | if (chan) |
389 | *chan = val & 0xf; |
390 | |
391 | return (val >> 4) & s->maxdata; |
392 | } |
393 | |
394 | static unsigned int pcl818_ai_get_sample(struct comedi_device *dev, |
395 | struct comedi_subdevice *s, |
396 | unsigned int *chan) |
397 | { |
398 | unsigned int val; |
399 | |
400 | val = inb(port: dev->iobase + PCL818_AI_MSB_REG) << 8; |
401 | val |= inb(port: dev->iobase + PCL818_AI_LSB_REG); |
402 | |
403 | if (chan) |
404 | *chan = val & 0xf; |
405 | |
406 | return (val >> 4) & s->maxdata; |
407 | } |
408 | |
409 | static int pcl818_ai_eoc(struct comedi_device *dev, |
410 | struct comedi_subdevice *s, |
411 | struct comedi_insn *insn, |
412 | unsigned long context) |
413 | { |
414 | unsigned int status; |
415 | |
416 | status = inb(port: dev->iobase + PCL818_STATUS_REG); |
417 | if (status & PCL818_STATUS_INT) |
418 | return 0; |
419 | return -EBUSY; |
420 | } |
421 | |
422 | static bool pcl818_ai_write_sample(struct comedi_device *dev, |
423 | struct comedi_subdevice *s, |
424 | unsigned int chan, unsigned short val) |
425 | { |
426 | struct pcl818_private *devpriv = dev->private; |
427 | struct comedi_cmd *cmd = &s->async->cmd; |
428 | unsigned int expected_chan; |
429 | |
430 | expected_chan = devpriv->act_chanlist[devpriv->act_chanlist_pos]; |
431 | if (chan != expected_chan) { |
432 | dev_dbg(dev->class_dev, |
433 | "A/D mode1/3 %s - channel dropout %d!=%d !\n" , |
434 | (devpriv->dma) ? "DMA" : |
435 | (devpriv->usefifo) ? "FIFO" : "IRQ" , |
436 | chan, expected_chan); |
437 | s->async->events |= COMEDI_CB_ERROR; |
438 | return false; |
439 | } |
440 | |
441 | comedi_buf_write_samples(s, data: &val, nsamples: 1); |
442 | |
443 | devpriv->act_chanlist_pos++; |
444 | if (devpriv->act_chanlist_pos >= devpriv->act_chanlist_len) |
445 | devpriv->act_chanlist_pos = 0; |
446 | |
447 | if (cmd->stop_src == TRIG_COUNT && |
448 | s->async->scans_done >= cmd->stop_arg) { |
449 | s->async->events |= COMEDI_CB_EOA; |
450 | return false; |
451 | } |
452 | |
453 | return true; |
454 | } |
455 | |
456 | static void pcl818_handle_eoc(struct comedi_device *dev, |
457 | struct comedi_subdevice *s) |
458 | { |
459 | unsigned int chan; |
460 | unsigned int val; |
461 | |
462 | if (pcl818_ai_eoc(dev, s, NULL, context: 0)) { |
463 | dev_err(dev->class_dev, "A/D mode1/3 IRQ without DRDY!\n" ); |
464 | s->async->events |= COMEDI_CB_ERROR; |
465 | return; |
466 | } |
467 | |
468 | val = pcl818_ai_get_sample(dev, s, chan: &chan); |
469 | pcl818_ai_write_sample(dev, s, chan, val); |
470 | } |
471 | |
472 | static void pcl818_handle_dma(struct comedi_device *dev, |
473 | struct comedi_subdevice *s) |
474 | { |
475 | struct pcl818_private *devpriv = dev->private; |
476 | struct comedi_isadma *dma = devpriv->dma; |
477 | struct comedi_isadma_desc *desc = &dma->desc[dma->cur_dma]; |
478 | unsigned short *ptr = desc->virt_addr; |
479 | unsigned int nsamples = comedi_bytes_to_samples(s, nbytes: desc->size); |
480 | unsigned int chan; |
481 | unsigned int val; |
482 | int i; |
483 | |
484 | /* restart dma with the next buffer */ |
485 | dma->cur_dma = 1 - dma->cur_dma; |
486 | pcl818_ai_setup_dma(dev, s, unread_samples: nsamples); |
487 | |
488 | for (i = 0; i < nsamples; i++) { |
489 | val = ptr[i]; |
490 | chan = val & 0xf; |
491 | val = (val >> 4) & s->maxdata; |
492 | if (!pcl818_ai_write_sample(dev, s, chan, val)) |
493 | break; |
494 | } |
495 | } |
496 | |
497 | static void pcl818_handle_fifo(struct comedi_device *dev, |
498 | struct comedi_subdevice *s) |
499 | { |
500 | unsigned int status; |
501 | unsigned int chan; |
502 | unsigned int val; |
503 | int i, len; |
504 | |
505 | status = inb(port: dev->iobase + PCL818_FI_STATUS); |
506 | |
507 | if (status & 4) { |
508 | dev_err(dev->class_dev, "A/D mode1/3 FIFO overflow!\n" ); |
509 | s->async->events |= COMEDI_CB_ERROR; |
510 | return; |
511 | } |
512 | |
513 | if (status & 1) { |
514 | dev_err(dev->class_dev, |
515 | "A/D mode1/3 FIFO interrupt without data!\n" ); |
516 | s->async->events |= COMEDI_CB_ERROR; |
517 | return; |
518 | } |
519 | |
520 | if (status & 2) |
521 | len = 512; |
522 | else |
523 | len = 0; |
524 | |
525 | for (i = 0; i < len; i++) { |
526 | val = pcl818_ai_get_fifo_sample(dev, s, chan: &chan); |
527 | if (!pcl818_ai_write_sample(dev, s, chan, val)) |
528 | break; |
529 | } |
530 | } |
531 | |
532 | static irqreturn_t pcl818_interrupt(int irq, void *d) |
533 | { |
534 | struct comedi_device *dev = d; |
535 | struct pcl818_private *devpriv = dev->private; |
536 | struct comedi_subdevice *s = dev->read_subdev; |
537 | struct comedi_cmd *cmd = &s->async->cmd; |
538 | |
539 | if (!dev->attached || !devpriv->ai_cmd_running) { |
540 | pcl818_ai_clear_eoc(dev); |
541 | return IRQ_HANDLED; |
542 | } |
543 | |
544 | if (devpriv->ai_cmd_canceled) { |
545 | /* |
546 | * The cleanup from ai_cancel() has been delayed |
547 | * until now because the card doesn't seem to like |
548 | * being reprogrammed while a DMA transfer is in |
549 | * progress. |
550 | */ |
551 | s->async->scans_done = cmd->stop_arg; |
552 | s->cancel(dev, s); |
553 | return IRQ_HANDLED; |
554 | } |
555 | |
556 | if (devpriv->dma) |
557 | pcl818_handle_dma(dev, s); |
558 | else if (devpriv->usefifo) |
559 | pcl818_handle_fifo(dev, s); |
560 | else |
561 | pcl818_handle_eoc(dev, s); |
562 | |
563 | pcl818_ai_clear_eoc(dev); |
564 | |
565 | comedi_handle_events(dev, s); |
566 | return IRQ_HANDLED; |
567 | } |
568 | |
569 | static int check_channel_list(struct comedi_device *dev, |
570 | struct comedi_subdevice *s, |
571 | unsigned int *chanlist, unsigned int n_chan) |
572 | { |
573 | unsigned int chansegment[16]; |
574 | unsigned int i, nowmustbechan, seglen; |
575 | |
576 | /* correct channel and range number check itself comedi/range.c */ |
577 | if (n_chan < 1) { |
578 | dev_err(dev->class_dev, "range/channel list is empty!\n" ); |
579 | return 0; |
580 | } |
581 | |
582 | if (n_chan > 1) { |
583 | /* first channel is every time ok */ |
584 | chansegment[0] = chanlist[0]; |
585 | /* build part of chanlist */ |
586 | for (i = 1, seglen = 1; i < n_chan; i++, seglen++) { |
587 | /* we detect loop, this must by finish */ |
588 | |
589 | if (chanlist[0] == chanlist[i]) |
590 | break; |
591 | nowmustbechan = |
592 | (CR_CHAN(chansegment[i - 1]) + 1) % s->n_chan; |
593 | if (nowmustbechan != CR_CHAN(chanlist[i])) { |
594 | /* channel list isn't continuous :-( */ |
595 | dev_dbg(dev->class_dev, |
596 | "channel list must be continuous! chanlist[%i]=%d but must be %d or %d!\n" , |
597 | i, CR_CHAN(chanlist[i]), nowmustbechan, |
598 | CR_CHAN(chanlist[0])); |
599 | return 0; |
600 | } |
601 | /* well, this is next correct channel in list */ |
602 | chansegment[i] = chanlist[i]; |
603 | } |
604 | |
605 | /* check whole chanlist */ |
606 | for (i = 0; i < n_chan; i++) { |
607 | if (chanlist[i] != chansegment[i % seglen]) { |
608 | dev_dbg(dev->class_dev, |
609 | "bad channel or range number! chanlist[%i]=%d,%d,%d and not %d,%d,%d!\n" , |
610 | i, CR_CHAN(chansegment[i]), |
611 | CR_RANGE(chansegment[i]), |
612 | CR_AREF(chansegment[i]), |
613 | CR_CHAN(chanlist[i % seglen]), |
614 | CR_RANGE(chanlist[i % seglen]), |
615 | CR_AREF(chansegment[i % seglen])); |
616 | return 0; /* chan/gain list is strange */ |
617 | } |
618 | } |
619 | } else { |
620 | seglen = 1; |
621 | } |
622 | return seglen; |
623 | } |
624 | |
625 | static int check_single_ended(unsigned int port) |
626 | { |
627 | if (inb(port: port + PCL818_STATUS_REG) & PCL818_STATUS_MUX) |
628 | return 1; |
629 | return 0; |
630 | } |
631 | |
632 | static int ai_cmdtest(struct comedi_device *dev, struct comedi_subdevice *s, |
633 | struct comedi_cmd *cmd) |
634 | { |
635 | const struct pcl818_board *board = dev->board_ptr; |
636 | int err = 0; |
637 | |
638 | /* Step 1 : check if triggers are trivially valid */ |
639 | |
640 | err |= comedi_check_trigger_src(src: &cmd->start_src, TRIG_NOW); |
641 | err |= comedi_check_trigger_src(src: &cmd->scan_begin_src, TRIG_FOLLOW); |
642 | err |= comedi_check_trigger_src(src: &cmd->convert_src, |
643 | TRIG_TIMER | TRIG_EXT); |
644 | err |= comedi_check_trigger_src(src: &cmd->scan_end_src, TRIG_COUNT); |
645 | err |= comedi_check_trigger_src(src: &cmd->stop_src, TRIG_COUNT | TRIG_NONE); |
646 | |
647 | if (err) |
648 | return 1; |
649 | |
650 | /* Step 2a : make sure trigger sources are unique */ |
651 | |
652 | err |= comedi_check_trigger_is_unique(src: cmd->convert_src); |
653 | err |= comedi_check_trigger_is_unique(src: cmd->stop_src); |
654 | |
655 | /* Step 2b : and mutually compatible */ |
656 | |
657 | if (err) |
658 | return 2; |
659 | |
660 | /* Step 3: check if arguments are trivially valid */ |
661 | |
662 | err |= comedi_check_trigger_arg_is(arg: &cmd->start_arg, val: 0); |
663 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_begin_arg, val: 0); |
664 | |
665 | if (cmd->convert_src == TRIG_TIMER) { |
666 | err |= comedi_check_trigger_arg_min(arg: &cmd->convert_arg, |
667 | val: board->ns_min); |
668 | } else { /* TRIG_EXT */ |
669 | err |= comedi_check_trigger_arg_is(arg: &cmd->convert_arg, val: 0); |
670 | } |
671 | |
672 | err |= comedi_check_trigger_arg_is(arg: &cmd->scan_end_arg, |
673 | val: cmd->chanlist_len); |
674 | |
675 | if (cmd->stop_src == TRIG_COUNT) |
676 | err |= comedi_check_trigger_arg_min(arg: &cmd->stop_arg, val: 1); |
677 | else /* TRIG_NONE */ |
678 | err |= comedi_check_trigger_arg_is(arg: &cmd->stop_arg, val: 0); |
679 | |
680 | if (err) |
681 | return 3; |
682 | |
683 | /* step 4: fix up any arguments */ |
684 | |
685 | if (cmd->convert_src == TRIG_TIMER) { |
686 | unsigned int arg = cmd->convert_arg; |
687 | |
688 | comedi_8254_cascade_ns_to_timer(i8254: dev->pacer, nanosec: &arg, flags: cmd->flags); |
689 | err |= comedi_check_trigger_arg_is(arg: &cmd->convert_arg, val: arg); |
690 | } |
691 | |
692 | if (err) |
693 | return 4; |
694 | |
695 | /* step 5: complain about special chanlist considerations */ |
696 | |
697 | if (cmd->chanlist) { |
698 | if (!check_channel_list(dev, s, chanlist: cmd->chanlist, |
699 | n_chan: cmd->chanlist_len)) |
700 | return 5; /* incorrect channels list */ |
701 | } |
702 | |
703 | return 0; |
704 | } |
705 | |
706 | static int pcl818_ai_cmd(struct comedi_device *dev, |
707 | struct comedi_subdevice *s) |
708 | { |
709 | struct pcl818_private *devpriv = dev->private; |
710 | struct comedi_isadma *dma = devpriv->dma; |
711 | struct comedi_cmd *cmd = &s->async->cmd; |
712 | unsigned int ctrl = 0; |
713 | unsigned int seglen; |
714 | |
715 | if (devpriv->ai_cmd_running) |
716 | return -EBUSY; |
717 | |
718 | seglen = check_channel_list(dev, s, chanlist: cmd->chanlist, n_chan: cmd->chanlist_len); |
719 | if (seglen < 1) |
720 | return -EINVAL; |
721 | pcl818_ai_setup_chanlist(dev, chanlist: cmd->chanlist, seglen); |
722 | |
723 | devpriv->ai_cmd_running = 1; |
724 | devpriv->ai_cmd_canceled = 0; |
725 | devpriv->act_chanlist_pos = 0; |
726 | |
727 | if (cmd->convert_src == TRIG_TIMER) |
728 | ctrl |= PCL818_CTRL_PACER_TRIG; |
729 | else |
730 | ctrl |= PCL818_CTRL_EXT_TRIG; |
731 | |
732 | outb(value: 0, port: dev->iobase + PCL818_CNTENABLE_REG); |
733 | |
734 | if (dma) { |
735 | /* setup and enable dma for the first buffer */ |
736 | dma->cur_dma = 0; |
737 | pcl818_ai_setup_dma(dev, s, unread_samples: 0); |
738 | |
739 | ctrl |= PCL818_CTRL_INTE | PCL818_CTRL_IRQ(dev->irq) | |
740 | PCL818_CTRL_DMAE; |
741 | } else if (devpriv->usefifo) { |
742 | /* enable FIFO */ |
743 | outb(value: 1, port: dev->iobase + PCL818_FI_ENABLE); |
744 | } else { |
745 | ctrl |= PCL818_CTRL_INTE | PCL818_CTRL_IRQ(dev->irq); |
746 | } |
747 | outb(value: ctrl, port: dev->iobase + PCL818_CTRL_REG); |
748 | |
749 | if (cmd->convert_src == TRIG_TIMER) { |
750 | comedi_8254_update_divisors(i8254: dev->pacer); |
751 | comedi_8254_pacer_enable(i8254: dev->pacer, counter1: 1, counter2: 2, enable: true); |
752 | } |
753 | |
754 | return 0; |
755 | } |
756 | |
757 | static int pcl818_ai_cancel(struct comedi_device *dev, |
758 | struct comedi_subdevice *s) |
759 | { |
760 | struct pcl818_private *devpriv = dev->private; |
761 | struct comedi_isadma *dma = devpriv->dma; |
762 | struct comedi_cmd *cmd = &s->async->cmd; |
763 | |
764 | if (!devpriv->ai_cmd_running) |
765 | return 0; |
766 | |
767 | if (dma) { |
768 | if (cmd->stop_src == TRIG_NONE || |
769 | (cmd->stop_src == TRIG_COUNT && |
770 | s->async->scans_done < cmd->stop_arg)) { |
771 | if (!devpriv->ai_cmd_canceled) { |
772 | /* |
773 | * Wait for running dma transfer to end, |
774 | * do cleanup in interrupt. |
775 | */ |
776 | devpriv->ai_cmd_canceled = 1; |
777 | return 0; |
778 | } |
779 | } |
780 | comedi_isadma_disable(dma_chan: dma->chan); |
781 | } |
782 | |
783 | outb(PCL818_CTRL_DISABLE_TRIG, port: dev->iobase + PCL818_CTRL_REG); |
784 | comedi_8254_pacer_enable(i8254: dev->pacer, counter1: 1, counter2: 2, enable: false); |
785 | pcl818_ai_clear_eoc(dev); |
786 | |
787 | if (devpriv->usefifo) { /* FIFO shutdown */ |
788 | outb(value: 0, port: dev->iobase + PCL818_FI_INTCLR); |
789 | outb(value: 0, port: dev->iobase + PCL818_FI_FLUSH); |
790 | outb(value: 0, port: dev->iobase + PCL818_FI_ENABLE); |
791 | } |
792 | devpriv->ai_cmd_running = 0; |
793 | devpriv->ai_cmd_canceled = 0; |
794 | |
795 | return 0; |
796 | } |
797 | |
798 | static int pcl818_ai_insn_read(struct comedi_device *dev, |
799 | struct comedi_subdevice *s, |
800 | struct comedi_insn *insn, |
801 | unsigned int *data) |
802 | { |
803 | unsigned int chan = CR_CHAN(insn->chanspec); |
804 | unsigned int range = CR_RANGE(insn->chanspec); |
805 | int ret = 0; |
806 | int i; |
807 | |
808 | outb(PCL818_CTRL_SOFT_TRIG, port: dev->iobase + PCL818_CTRL_REG); |
809 | |
810 | pcl818_ai_set_chan_range(dev, chan, range); |
811 | pcl818_ai_set_chan_scan(dev, first_chan: chan, last_chan: chan); |
812 | |
813 | for (i = 0; i < insn->n; i++) { |
814 | pcl818_ai_clear_eoc(dev); |
815 | pcl818_ai_soft_trig(dev); |
816 | |
817 | ret = comedi_timeout(dev, s, insn, cb: pcl818_ai_eoc, context: 0); |
818 | if (ret) |
819 | break; |
820 | |
821 | data[i] = pcl818_ai_get_sample(dev, s, NULL); |
822 | } |
823 | pcl818_ai_clear_eoc(dev); |
824 | |
825 | return ret ? ret : insn->n; |
826 | } |
827 | |
828 | static int pcl818_ao_insn_write(struct comedi_device *dev, |
829 | struct comedi_subdevice *s, |
830 | struct comedi_insn *insn, |
831 | unsigned int *data) |
832 | { |
833 | unsigned int chan = CR_CHAN(insn->chanspec); |
834 | unsigned int val = s->readback[chan]; |
835 | int i; |
836 | |
837 | for (i = 0; i < insn->n; i++) { |
838 | val = data[i]; |
839 | outb(value: (val & 0x000f) << 4, |
840 | port: dev->iobase + PCL818_AO_LSB_REG(chan)); |
841 | outb(value: (val & 0x0ff0) >> 4, |
842 | port: dev->iobase + PCL818_AO_MSB_REG(chan)); |
843 | } |
844 | s->readback[chan] = val; |
845 | |
846 | return insn->n; |
847 | } |
848 | |
849 | static int pcl818_di_insn_bits(struct comedi_device *dev, |
850 | struct comedi_subdevice *s, |
851 | struct comedi_insn *insn, |
852 | unsigned int *data) |
853 | { |
854 | data[1] = inb(port: dev->iobase + PCL818_DO_DI_LSB_REG) | |
855 | (inb(port: dev->iobase + PCL818_DO_DI_MSB_REG) << 8); |
856 | |
857 | return insn->n; |
858 | } |
859 | |
860 | static int pcl818_do_insn_bits(struct comedi_device *dev, |
861 | struct comedi_subdevice *s, |
862 | struct comedi_insn *insn, |
863 | unsigned int *data) |
864 | { |
865 | if (comedi_dio_update_state(s, data)) { |
866 | outb(value: s->state & 0xff, port: dev->iobase + PCL818_DO_DI_LSB_REG); |
867 | outb(value: (s->state >> 8), port: dev->iobase + PCL818_DO_DI_MSB_REG); |
868 | } |
869 | |
870 | data[1] = s->state; |
871 | |
872 | return insn->n; |
873 | } |
874 | |
875 | static void pcl818_reset(struct comedi_device *dev) |
876 | { |
877 | const struct pcl818_board *board = dev->board_ptr; |
878 | unsigned int chan; |
879 | |
880 | /* flush and disable the FIFO */ |
881 | if (board->has_fifo) { |
882 | outb(value: 0, port: dev->iobase + PCL818_FI_INTCLR); |
883 | outb(value: 0, port: dev->iobase + PCL818_FI_FLUSH); |
884 | outb(value: 0, port: dev->iobase + PCL818_FI_ENABLE); |
885 | } |
886 | |
887 | /* disable analog input trigger */ |
888 | outb(PCL818_CTRL_DISABLE_TRIG, port: dev->iobase + PCL818_CTRL_REG); |
889 | pcl818_ai_clear_eoc(dev); |
890 | |
891 | pcl818_ai_set_chan_range(dev, chan: 0, range: 0); |
892 | |
893 | /* stop pacer */ |
894 | outb(value: 0, port: dev->iobase + PCL818_CNTENABLE_REG); |
895 | |
896 | /* set analog output channels to 0V */ |
897 | for (chan = 0; chan < board->n_aochan; chan++) { |
898 | outb(value: 0, port: dev->iobase + PCL818_AO_LSB_REG(chan)); |
899 | outb(value: 0, port: dev->iobase + PCL818_AO_MSB_REG(chan)); |
900 | } |
901 | |
902 | /* set all digital outputs low */ |
903 | outb(value: 0, port: dev->iobase + PCL818_DO_DI_MSB_REG); |
904 | outb(value: 0, port: dev->iobase + PCL818_DO_DI_LSB_REG); |
905 | } |
906 | |
907 | static void pcl818_set_ai_range_table(struct comedi_device *dev, |
908 | struct comedi_subdevice *s, |
909 | struct comedi_devconfig *it) |
910 | { |
911 | const struct pcl818_board *board = dev->board_ptr; |
912 | |
913 | /* default to the range table from the boardinfo */ |
914 | s->range_table = board->ai_range_type; |
915 | |
916 | /* now check the user config option based on the boardtype */ |
917 | if (board->is_818) { |
918 | if (it->options[4] == 1 || it->options[4] == 10) { |
919 | /* secondary range list jumper selectable */ |
920 | s->range_table = &range_pcl818l_h_ai; |
921 | } |
922 | } else { |
923 | switch (it->options[4]) { |
924 | case 0: |
925 | s->range_table = &range_bipolar10; |
926 | break; |
927 | case 1: |
928 | s->range_table = &range_bipolar5; |
929 | break; |
930 | case 2: |
931 | s->range_table = &range_bipolar2_5; |
932 | break; |
933 | case 3: |
934 | s->range_table = &range718_bipolar1; |
935 | break; |
936 | case 4: |
937 | s->range_table = &range718_bipolar0_5; |
938 | break; |
939 | case 6: |
940 | s->range_table = &range_unipolar10; |
941 | break; |
942 | case 7: |
943 | s->range_table = &range_unipolar5; |
944 | break; |
945 | case 8: |
946 | s->range_table = &range718_unipolar2; |
947 | break; |
948 | case 9: |
949 | s->range_table = &range718_unipolar1; |
950 | break; |
951 | default: |
952 | s->range_table = &range_unknown; |
953 | break; |
954 | } |
955 | } |
956 | } |
957 | |
958 | static void pcl818_alloc_dma(struct comedi_device *dev, unsigned int dma_chan) |
959 | { |
960 | struct pcl818_private *devpriv = dev->private; |
961 | |
962 | /* only DMA channels 3 and 1 are valid */ |
963 | if (!(dma_chan == 3 || dma_chan == 1)) |
964 | return; |
965 | |
966 | /* DMA uses two 16K buffers */ |
967 | devpriv->dma = comedi_isadma_alloc(dev, n_desc: 2, dma_chan1: dma_chan, dma_chan2: dma_chan, |
968 | PAGE_SIZE * 4, COMEDI_ISADMA_READ); |
969 | } |
970 | |
971 | static void pcl818_free_dma(struct comedi_device *dev) |
972 | { |
973 | struct pcl818_private *devpriv = dev->private; |
974 | |
975 | if (devpriv) |
976 | comedi_isadma_free(dma: devpriv->dma); |
977 | } |
978 | |
979 | static int pcl818_attach(struct comedi_device *dev, struct comedi_devconfig *it) |
980 | { |
981 | const struct pcl818_board *board = dev->board_ptr; |
982 | struct pcl818_private *devpriv; |
983 | struct comedi_subdevice *s; |
984 | unsigned int osc_base; |
985 | int ret; |
986 | |
987 | devpriv = comedi_alloc_devpriv(dev, size: sizeof(*devpriv)); |
988 | if (!devpriv) |
989 | return -ENOMEM; |
990 | |
991 | ret = comedi_request_region(dev, start: it->options[0], |
992 | len: board->has_fifo ? 0x20 : 0x10); |
993 | if (ret) |
994 | return ret; |
995 | |
996 | /* we can use IRQ 2-7 for async command support */ |
997 | if (it->options[1] >= 2 && it->options[1] <= 7) { |
998 | ret = request_irq(irq: it->options[1], handler: pcl818_interrupt, flags: 0, |
999 | name: dev->board_name, dev); |
1000 | if (ret == 0) |
1001 | dev->irq = it->options[1]; |
1002 | } |
1003 | |
1004 | /* should we use the FIFO? */ |
1005 | if (dev->irq && board->has_fifo && it->options[2] == -1) |
1006 | devpriv->usefifo = 1; |
1007 | |
1008 | /* we need an IRQ to do DMA on channel 3 or 1 */ |
1009 | if (dev->irq && board->has_dma) |
1010 | pcl818_alloc_dma(dev, dma_chan: it->options[2]); |
1011 | |
1012 | /* use 1MHz or 10MHz oscilator */ |
1013 | if ((it->options[3] == 0) || (it->options[3] == 10)) |
1014 | osc_base = I8254_OSC_BASE_10MHZ; |
1015 | else |
1016 | osc_base = I8254_OSC_BASE_1MHZ; |
1017 | |
1018 | dev->pacer = comedi_8254_io_alloc(iobase: dev->iobase + PCL818_TIMER_BASE, |
1019 | osc_base, I8254_IO8, regshift: 0); |
1020 | if (IS_ERR(ptr: dev->pacer)) |
1021 | return PTR_ERR(ptr: dev->pacer); |
1022 | |
1023 | /* max sampling speed */ |
1024 | devpriv->ns_min = board->ns_min; |
1025 | if (!board->is_818) { |
1026 | /* extended PCL718 to 100kHz DAC */ |
1027 | if ((it->options[6] == 1) || (it->options[6] == 100)) |
1028 | devpriv->ns_min = 10000; |
1029 | } |
1030 | |
1031 | ret = comedi_alloc_subdevices(dev, num_subdevices: 4); |
1032 | if (ret) |
1033 | return ret; |
1034 | |
1035 | s = &dev->subdevices[0]; |
1036 | s->type = COMEDI_SUBD_AI; |
1037 | s->subdev_flags = SDF_READABLE; |
1038 | if (check_single_ended(port: dev->iobase)) { |
1039 | s->n_chan = 16; |
1040 | s->subdev_flags |= SDF_COMMON | SDF_GROUND; |
1041 | } else { |
1042 | s->n_chan = 8; |
1043 | s->subdev_flags |= SDF_DIFF; |
1044 | } |
1045 | s->maxdata = 0x0fff; |
1046 | |
1047 | pcl818_set_ai_range_table(dev, s, it); |
1048 | |
1049 | s->insn_read = pcl818_ai_insn_read; |
1050 | if (dev->irq) { |
1051 | dev->read_subdev = s; |
1052 | s->subdev_flags |= SDF_CMD_READ; |
1053 | s->len_chanlist = s->n_chan; |
1054 | s->do_cmdtest = ai_cmdtest; |
1055 | s->do_cmd = pcl818_ai_cmd; |
1056 | s->cancel = pcl818_ai_cancel; |
1057 | } |
1058 | |
1059 | /* Analog Output subdevice */ |
1060 | s = &dev->subdevices[1]; |
1061 | if (board->n_aochan) { |
1062 | s->type = COMEDI_SUBD_AO; |
1063 | s->subdev_flags = SDF_WRITABLE | SDF_GROUND; |
1064 | s->n_chan = board->n_aochan; |
1065 | s->maxdata = 0x0fff; |
1066 | s->range_table = &range_unipolar5; |
1067 | if (board->is_818) { |
1068 | if ((it->options[4] == 1) || (it->options[4] == 10)) |
1069 | s->range_table = &range_unipolar10; |
1070 | if (it->options[4] == 2) |
1071 | s->range_table = &range_unknown; |
1072 | } else { |
1073 | if ((it->options[5] == 1) || (it->options[5] == 10)) |
1074 | s->range_table = &range_unipolar10; |
1075 | if (it->options[5] == 2) |
1076 | s->range_table = &range_unknown; |
1077 | } |
1078 | s->insn_write = pcl818_ao_insn_write; |
1079 | |
1080 | ret = comedi_alloc_subdev_readback(s); |
1081 | if (ret) |
1082 | return ret; |
1083 | } else { |
1084 | s->type = COMEDI_SUBD_UNUSED; |
1085 | } |
1086 | |
1087 | /* Digital Input subdevice */ |
1088 | s = &dev->subdevices[2]; |
1089 | s->type = COMEDI_SUBD_DI; |
1090 | s->subdev_flags = SDF_READABLE; |
1091 | s->n_chan = 16; |
1092 | s->maxdata = 1; |
1093 | s->range_table = &range_digital; |
1094 | s->insn_bits = pcl818_di_insn_bits; |
1095 | |
1096 | /* Digital Output subdevice */ |
1097 | s = &dev->subdevices[3]; |
1098 | s->type = COMEDI_SUBD_DO; |
1099 | s->subdev_flags = SDF_WRITABLE; |
1100 | s->n_chan = 16; |
1101 | s->maxdata = 1; |
1102 | s->range_table = &range_digital; |
1103 | s->insn_bits = pcl818_do_insn_bits; |
1104 | |
1105 | pcl818_reset(dev); |
1106 | |
1107 | return 0; |
1108 | } |
1109 | |
1110 | static void pcl818_detach(struct comedi_device *dev) |
1111 | { |
1112 | struct pcl818_private *devpriv = dev->private; |
1113 | |
1114 | if (devpriv) { |
1115 | pcl818_ai_cancel(dev, s: dev->read_subdev); |
1116 | pcl818_reset(dev); |
1117 | } |
1118 | pcl818_free_dma(dev); |
1119 | comedi_legacy_detach(dev); |
1120 | } |
1121 | |
1122 | static struct comedi_driver pcl818_driver = { |
1123 | .driver_name = "pcl818" , |
1124 | .module = THIS_MODULE, |
1125 | .attach = pcl818_attach, |
1126 | .detach = pcl818_detach, |
1127 | .board_name = &boardtypes[0].name, |
1128 | .num_names = ARRAY_SIZE(boardtypes), |
1129 | .offset = sizeof(struct pcl818_board), |
1130 | }; |
1131 | module_comedi_driver(pcl818_driver); |
1132 | |
1133 | MODULE_AUTHOR("Comedi https://www.comedi.org" ); |
1134 | MODULE_DESCRIPTION("Comedi low-level driver" ); |
1135 | MODULE_LICENSE("GPL" ); |
1136 | |