| /* |
| comedi/drivers/s526.c |
| Sensoray s526 Comedi driver |
| |
| COMEDI - Linux Control and Measurement Device Interface |
| Copyright (C) 2000 David A. Schleef <ds@schleef.org> |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with this program; if not, write to the Free Software |
| Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
| |
| */ |
| /* |
| Driver: s526 |
| Description: Sensoray 526 driver |
| Devices: [Sensoray] 526 (s526) |
| Author: Richie |
| Everett Wang <everett.wang@everteq.com> |
| Updated: Thu, 14 Sep. 2006 |
| Status: experimental |
| |
| Encoder works |
| Analog input works |
| Analog output works |
| PWM output works |
| Commands are not supported yet. |
| |
| Configuration Options: |
| |
| comedi_config /dev/comedi0 s526 0x2C0,0x3 |
| |
| */ |
| |
| #include "../comedidev.h" |
| #include <linux/ioport.h> |
| #include <asm/byteorder.h> |
| |
| #define S526_SIZE 64 |
| |
| #define S526_START_AI_CONV 0 |
| #define S526_AI_READ 0 |
| |
| /* Ports */ |
| #define S526_IOSIZE 0x40 |
| #define S526_NUM_PORTS 27 |
| |
| /* registers */ |
| #define REG_TCR 0x00 |
| #define REG_WDC 0x02 |
| #define REG_DAC 0x04 |
| #define REG_ADC 0x06 |
| #define REG_ADD 0x08 |
| #define REG_DIO 0x0A |
| #define REG_IER 0x0C |
| #define REG_ISR 0x0E |
| #define REG_MSC 0x10 |
| #define REG_C0L 0x12 |
| #define REG_C0H 0x14 |
| #define REG_C0M 0x16 |
| #define REG_C0C 0x18 |
| #define REG_C1L 0x1A |
| #define REG_C1H 0x1C |
| #define REG_C1M 0x1E |
| #define REG_C1C 0x20 |
| #define REG_C2L 0x22 |
| #define REG_C2H 0x24 |
| #define REG_C2M 0x26 |
| #define REG_C2C 0x28 |
| #define REG_C3L 0x2A |
| #define REG_C3H 0x2C |
| #define REG_C3M 0x2E |
| #define REG_C3C 0x30 |
| #define REG_EED 0x32 |
| #define REG_EEC 0x34 |
| |
| struct counter_mode_register_t { |
| #if defined(__LITTLE_ENDIAN_BITFIELD) |
| unsigned short coutSource:1; |
| unsigned short coutPolarity:1; |
| unsigned short autoLoadResetRcap:3; |
| unsigned short hwCtEnableSource:2; |
| unsigned short ctEnableCtrl:2; |
| unsigned short clockSource:2; |
| unsigned short countDir:1; |
| unsigned short countDirCtrl:1; |
| unsigned short outputRegLatchCtrl:1; |
| unsigned short preloadRegSel:1; |
| unsigned short reserved:1; |
| #elif defined(__BIG_ENDIAN_BITFIELD) |
| unsigned short reserved:1; |
| unsigned short preloadRegSel:1; |
| unsigned short outputRegLatchCtrl:1; |
| unsigned short countDirCtrl:1; |
| unsigned short countDir:1; |
| unsigned short clockSource:2; |
| unsigned short ctEnableCtrl:2; |
| unsigned short hwCtEnableSource:2; |
| unsigned short autoLoadResetRcap:3; |
| unsigned short coutPolarity:1; |
| unsigned short coutSource:1; |
| #else |
| #error Unknown bit field order |
| #endif |
| }; |
| |
| union cmReg { |
| struct counter_mode_register_t reg; |
| unsigned short value; |
| }; |
| |
| #define MAX_GPCT_CONFIG_DATA 6 |
| |
| /* Different Application Classes for GPCT Subdevices */ |
| /* The list is not exhaustive and needs discussion! */ |
| enum S526_GPCT_APP_CLASS { |
| CountingAndTimeMeasurement, |
| SinglePulseGeneration, |
| PulseTrainGeneration, |
| PositionMeasurement, |
| Miscellaneous |
| }; |
| |
| /* Config struct for different GPCT subdevice Application Classes and |
| their options |
| */ |
| struct s526GPCTConfig { |
| enum S526_GPCT_APP_CLASS app; |
| int data[MAX_GPCT_CONFIG_DATA]; |
| }; |
| |
| struct s526_private { |
| unsigned int ao_readback[2]; |
| struct s526GPCTConfig s526_gpct_config[4]; |
| unsigned short s526_ai_config; |
| }; |
| |
| static int s526_gpct_rinsn(struct comedi_device *dev, |
| struct comedi_subdevice *s, struct comedi_insn *insn, |
| unsigned int *data) |
| { |
| unsigned int chan = CR_CHAN(insn->chanspec); |
| unsigned short datalow; |
| unsigned short datahigh; |
| int i; |
| |
| /* Read the low word first */ |
| for (i = 0; i < insn->n; i++) { |
| datalow = inw(dev->iobase + REG_C0L + chan * 8); |
| datahigh = inw(dev->iobase + REG_C0H + chan * 8); |
| data[i] = (int)(datahigh & 0x00FF); |
| data[i] = (data[i] << 16) | (datalow & 0xFFFF); |
| } |
| return i; |
| } |
| |
| static int s526_gpct_insn_config(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| struct s526_private *devpriv = dev->private; |
| unsigned int chan = CR_CHAN(insn->chanspec); |
| int i; |
| short value; |
| union cmReg cmReg; |
| |
| for (i = 0; i < MAX_GPCT_CONFIG_DATA; i++) |
| devpriv->s526_gpct_config[chan].data[i] = data[i]; |
| |
| /* Check what type of Counter the user requested, data[0] contains */ |
| /* the Application type */ |
| switch (data[0]) { |
| case INSN_CONFIG_GPCT_QUADRATURE_ENCODER: |
| /* |
| data[0]: Application Type |
| data[1]: Counter Mode Register Value |
| data[2]: Pre-load Register Value |
| data[3]: Conter Control Register |
| */ |
| devpriv->s526_gpct_config[chan].app = PositionMeasurement; |
| |
| #if 0 |
| /* Example of Counter Application */ |
| /* One-shot (software trigger) */ |
| cmReg.reg.coutSource = 0; /* out RCAP */ |
| cmReg.reg.coutPolarity = 1; /* Polarity inverted */ |
| cmReg.reg.autoLoadResetRcap = 0;/* Auto load disabled */ |
| cmReg.reg.hwCtEnableSource = 3; /* NOT RCAP */ |
| cmReg.reg.ctEnableCtrl = 2; /* Hardware */ |
| cmReg.reg.clockSource = 2; /* Internal */ |
| cmReg.reg.countDir = 1; /* Down */ |
| cmReg.reg.countDirCtrl = 1; /* Software */ |
| cmReg.reg.outputRegLatchCtrl = 0; /* latch on read */ |
| cmReg.reg.preloadRegSel = 0; /* PR0 */ |
| cmReg.reg.reserved = 0; |
| |
| outw(cmReg.value, dev->iobase + REG_C0M + chan * 8); |
| |
| outw(0x0001, dev->iobase + REG_C0H + chan * 8); |
| outw(0x3C68, dev->iobase + REG_C0L + chan * 8); |
| |
| /* Reset the counter */ |
| outw(0x8000, dev->iobase + REG_C0C + chan * 8); |
| /* Load the counter from PR0 */ |
| outw(0x4000, dev->iobase + REG_C0C + chan * 8); |
| |
| /* Reset RCAP (fires one-shot) */ |
| outw(0x0008, dev->iobase + REG_C0C + chan * 8); |
| |
| #endif |
| |
| #if 1 |
| /* Set Counter Mode Register */ |
| cmReg.value = data[1] & 0xFFFF; |
| outw(cmReg.value, dev->iobase + REG_C0M + chan * 8); |
| |
| /* Reset the counter if it is software preload */ |
| if (cmReg.reg.autoLoadResetRcap == 0) { |
| /* Reset the counter */ |
| outw(0x8000, dev->iobase + REG_C0C + chan * 8); |
| /* Load the counter from PR0 |
| * outw(0x4000, dev->iobase + REG_C0C + chan * 8); |
| */ |
| } |
| #else |
| /* 0 quadrature, 1 software control */ |
| cmReg.reg.countDirCtrl = 0; |
| |
| /* data[1] contains GPCT_X1, GPCT_X2 or GPCT_X4 */ |
| if (data[1] == GPCT_X2) |
| cmReg.reg.clockSource = 1; |
| else if (data[1] == GPCT_X4) |
| cmReg.reg.clockSource = 2; |
| else |
| cmReg.reg.clockSource = 0; |
| |
| /* When to take into account the indexpulse: */ |
| /*if (data[2] == GPCT_IndexPhaseLowLow) { |
| } else if (data[2] == GPCT_IndexPhaseLowHigh) { |
| } else if (data[2] == GPCT_IndexPhaseHighLow) { |
| } else if (data[2] == GPCT_IndexPhaseHighHigh) { |
| }*/ |
| /* Take into account the index pulse? */ |
| if (data[3] == GPCT_RESET_COUNTER_ON_INDEX) |
| /* Auto load with INDEX^ */ |
| cmReg.reg.autoLoadResetRcap = 4; |
| |
| /* Set Counter Mode Register */ |
| cmReg.value = (short)(data[1] & 0xFFFF); |
| outw(cmReg.value, dev->iobase + REG_C0M + chan * 8); |
| |
| /* Load the pre-load register high word */ |
| value = (short)((data[2] >> 16) & 0xFFFF); |
| outw(value, dev->iobase + REG_C0H + chan * 8); |
| |
| /* Load the pre-load register low word */ |
| value = (short)(data[2] & 0xFFFF); |
| outw(value, dev->iobase + REG_C0L + chan * 8); |
| |
| /* Write the Counter Control Register */ |
| if (data[3] != 0) { |
| value = (short)(data[3] & 0xFFFF); |
| outw(value, dev->iobase + REG_C0C + chan * 8); |
| } |
| /* Reset the counter if it is software preload */ |
| if (cmReg.reg.autoLoadResetRcap == 0) { |
| /* Reset the counter */ |
| outw(0x8000, dev->iobase + REG_C0C + chan * 8); |
| /* Load the counter from PR0 */ |
| outw(0x4000, dev->iobase + REG_C0C + chan * 8); |
| } |
| #endif |
| break; |
| |
| case INSN_CONFIG_GPCT_SINGLE_PULSE_GENERATOR: |
| /* |
| data[0]: Application Type |
| data[1]: Counter Mode Register Value |
| data[2]: Pre-load Register 0 Value |
| data[3]: Pre-load Register 1 Value |
| data[4]: Conter Control Register |
| */ |
| devpriv->s526_gpct_config[chan].app = SinglePulseGeneration; |
| |
| /* Set Counter Mode Register */ |
| cmReg.value = (short)(data[1] & 0xFFFF); |
| cmReg.reg.preloadRegSel = 0; /* PR0 */ |
| outw(cmReg.value, dev->iobase + REG_C0M + chan * 8); |
| |
| /* Load the pre-load register 0 high word */ |
| value = (short)((data[2] >> 16) & 0xFFFF); |
| outw(value, dev->iobase + REG_C0H + chan * 8); |
| |
| /* Load the pre-load register 0 low word */ |
| value = (short)(data[2] & 0xFFFF); |
| outw(value, dev->iobase + REG_C0L + chan * 8); |
| |
| /* Set Counter Mode Register */ |
| cmReg.value = (short)(data[1] & 0xFFFF); |
| cmReg.reg.preloadRegSel = 1; /* PR1 */ |
| outw(cmReg.value, dev->iobase + REG_C0M + chan * 8); |
| |
| /* Load the pre-load register 1 high word */ |
| value = (short)((data[3] >> 16) & 0xFFFF); |
| outw(value, dev->iobase + REG_C0H + chan * 8); |
| |
| /* Load the pre-load register 1 low word */ |
| value = (short)(data[3] & 0xFFFF); |
| outw(value, dev->iobase + REG_C0L + chan * 8); |
| |
| /* Write the Counter Control Register */ |
| if (data[4] != 0) { |
| value = (short)(data[4] & 0xFFFF); |
| outw(value, dev->iobase + REG_C0C + chan * 8); |
| } |
| break; |
| |
| case INSN_CONFIG_GPCT_PULSE_TRAIN_GENERATOR: |
| /* |
| data[0]: Application Type |
| data[1]: Counter Mode Register Value |
| data[2]: Pre-load Register 0 Value |
| data[3]: Pre-load Register 1 Value |
| data[4]: Conter Control Register |
| */ |
| devpriv->s526_gpct_config[chan].app = PulseTrainGeneration; |
| |
| /* Set Counter Mode Register */ |
| cmReg.value = (short)(data[1] & 0xFFFF); |
| cmReg.reg.preloadRegSel = 0; /* PR0 */ |
| outw(cmReg.value, dev->iobase + REG_C0M + chan * 8); |
| |
| /* Load the pre-load register 0 high word */ |
| value = (short)((data[2] >> 16) & 0xFFFF); |
| outw(value, dev->iobase + REG_C0H + chan * 8); |
| |
| /* Load the pre-load register 0 low word */ |
| value = (short)(data[2] & 0xFFFF); |
| outw(value, dev->iobase + REG_C0L + chan * 8); |
| |
| /* Set Counter Mode Register */ |
| cmReg.value = (short)(data[1] & 0xFFFF); |
| cmReg.reg.preloadRegSel = 1; /* PR1 */ |
| outw(cmReg.value, dev->iobase + REG_C0M + chan * 8); |
| |
| /* Load the pre-load register 1 high word */ |
| value = (short)((data[3] >> 16) & 0xFFFF); |
| outw(value, dev->iobase + REG_C0H + chan * 8); |
| |
| /* Load the pre-load register 1 low word */ |
| value = (short)(data[3] & 0xFFFF); |
| outw(value, dev->iobase + REG_C0L + chan * 8); |
| |
| /* Write the Counter Control Register */ |
| if (data[4] != 0) { |
| value = (short)(data[4] & 0xFFFF); |
| outw(value, dev->iobase + REG_C0C + chan * 8); |
| } |
| break; |
| |
| default: |
| return -EINVAL; |
| break; |
| } |
| |
| return insn->n; |
| } |
| |
| static int s526_gpct_winsn(struct comedi_device *dev, |
| struct comedi_subdevice *s, struct comedi_insn *insn, |
| unsigned int *data) |
| { |
| struct s526_private *devpriv = dev->private; |
| unsigned int chan = CR_CHAN(insn->chanspec); |
| short value; |
| union cmReg cmReg; |
| |
| cmReg.value = inw(dev->iobase + REG_C0M + chan * 8); |
| /* Check what Application of Counter this channel is configured for */ |
| switch (devpriv->s526_gpct_config[chan].app) { |
| case PositionMeasurement: |
| outw(0xFFFF & ((*data) >> 16), dev->iobase + REG_C0H + chan * 8); |
| outw(0xFFFF & (*data), dev->iobase + REG_C0L + chan * 8); |
| break; |
| |
| case SinglePulseGeneration: |
| outw(0xFFFF & ((*data) >> 16), dev->iobase + REG_C0H + chan * 8); |
| outw(0xFFFF & (*data), dev->iobase + REG_C0L + chan * 8); |
| break; |
| |
| case PulseTrainGeneration: |
| /* data[0] contains the PULSE_WIDTH |
| data[1] contains the PULSE_PERIOD |
| @pre PULSE_PERIOD > PULSE_WIDTH > 0 |
| The above periods must be expressed as a multiple of the |
| pulse frequency on the selected source |
| */ |
| if ((data[1] > data[0]) && (data[0] > 0)) { |
| (devpriv->s526_gpct_config[chan]).data[0] = data[0]; |
| (devpriv->s526_gpct_config[chan]).data[1] = data[1]; |
| } else { |
| return -EINVAL; |
| } |
| |
| value = (short)((*data >> 16) & 0xFFFF); |
| outw(value, dev->iobase + REG_C0H + chan * 8); |
| value = (short)(*data & 0xFFFF); |
| outw(value, dev->iobase + REG_C0L + chan * 8); |
| break; |
| default: /* Impossible */ |
| return -EINVAL; |
| break; |
| } |
| /* return the number of samples written */ |
| return insn->n; |
| } |
| |
| #define ISR_ADC_DONE 0x4 |
| static int s526_ai_insn_config(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| struct s526_private *devpriv = dev->private; |
| int result = -EINVAL; |
| |
| if (insn->n < 1) |
| return result; |
| |
| result = insn->n; |
| |
| /* data[0] : channels was set in relevant bits. |
| data[1] : delay |
| */ |
| /* COMMENT: abbotti 2008-07-24: I don't know why you'd want to |
| * enable channels here. The channel should be enabled in the |
| * INSN_READ handler. */ |
| |
| /* Enable ADC interrupt */ |
| outw(ISR_ADC_DONE, dev->iobase + REG_IER); |
| devpriv->s526_ai_config = (data[0] & 0x3FF) << 5; |
| if (data[1] > 0) |
| devpriv->s526_ai_config |= 0x8000; /* set the delay */ |
| |
| devpriv->s526_ai_config |= 0x0001; /* ADC start bit. */ |
| |
| return result; |
| } |
| |
| static int s526_ai_rinsn(struct comedi_device *dev, struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| struct s526_private *devpriv = dev->private; |
| unsigned int chan = CR_CHAN(insn->chanspec); |
| int n, i; |
| unsigned short value; |
| unsigned int d; |
| unsigned int status; |
| |
| /* Set configured delay, enable channel for this channel only, |
| * select "ADC read" channel, set "ADC start" bit. */ |
| value = (devpriv->s526_ai_config & 0x8000) | |
| ((1 << 5) << chan) | (chan << 1) | 0x0001; |
| |
| /* convert n samples */ |
| for (n = 0; n < insn->n; n++) { |
| /* trigger conversion */ |
| outw(value, dev->iobase + REG_ADC); |
| |
| #define TIMEOUT 100 |
| /* wait for conversion to end */ |
| for (i = 0; i < TIMEOUT; i++) { |
| status = inw(dev->iobase + REG_ISR); |
| if (status & ISR_ADC_DONE) { |
| outw(ISR_ADC_DONE, dev->iobase + REG_ISR); |
| break; |
| } |
| } |
| if (i == TIMEOUT) |
| return -ETIMEDOUT; |
| |
| /* read data */ |
| d = inw(dev->iobase + REG_ADD); |
| |
| /* munge data */ |
| data[n] = d ^ 0x8000; |
| } |
| |
| /* return the number of samples read/written */ |
| return n; |
| } |
| |
| static int s526_ao_winsn(struct comedi_device *dev, struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| struct s526_private *devpriv = dev->private; |
| unsigned int chan = CR_CHAN(insn->chanspec); |
| unsigned short val; |
| int i; |
| |
| val = chan << 1; |
| outw(val, dev->iobase + REG_DAC); |
| |
| for (i = 0; i < insn->n; i++) { |
| outw(data[i], dev->iobase + REG_ADD); |
| devpriv->ao_readback[chan] = data[i]; |
| /* starts the D/A conversion */ |
| outw(val + 1, dev->iobase + REG_DAC); |
| } |
| |
| return i; |
| } |
| |
| static int s526_ao_rinsn(struct comedi_device *dev, struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| struct s526_private *devpriv = dev->private; |
| unsigned int chan = CR_CHAN(insn->chanspec); |
| int i; |
| |
| for (i = 0; i < insn->n; i++) |
| data[i] = devpriv->ao_readback[chan]; |
| |
| return i; |
| } |
| |
| static int s526_dio_insn_bits(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| if (data[0]) { |
| s->state &= ~data[0]; |
| s->state |= data[0] & data[1]; |
| |
| outw(s->state, dev->iobase + REG_DIO); |
| } |
| |
| data[1] = inw(dev->iobase + REG_DIO) & 0xff; |
| |
| return insn->n; |
| } |
| |
| static int s526_dio_insn_config(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, unsigned int *data) |
| { |
| unsigned int chan = CR_CHAN(insn->chanspec); |
| int group, mask; |
| |
| group = chan >> 2; |
| mask = 0xF << (group << 2); |
| switch (data[0]) { |
| case INSN_CONFIG_DIO_OUTPUT: |
| /* bit 10/11 set the group 1/2's mode */ |
| s->state |= 1 << (group + 10); |
| s->io_bits |= mask; |
| break; |
| case INSN_CONFIG_DIO_INPUT: |
| s->state &= ~(1 << (group + 10)); /* 1 is output, 0 is input. */ |
| s->io_bits &= ~mask; |
| break; |
| case INSN_CONFIG_DIO_QUERY: |
| data[1] = (s->io_bits & mask) ? COMEDI_OUTPUT : COMEDI_INPUT; |
| return insn->n; |
| default: |
| return -EINVAL; |
| } |
| outw(s->state, dev->iobase + REG_DIO); |
| |
| return 1; |
| } |
| |
| static int s526_attach(struct comedi_device *dev, struct comedi_devconfig *it) |
| { |
| struct s526_private *devpriv; |
| struct comedi_subdevice *s; |
| int iobase; |
| int ret; |
| |
| dev->board_name = dev->driver->driver_name; |
| |
| iobase = it->options[0]; |
| if (!iobase || !request_region(iobase, S526_IOSIZE, dev->board_name)) { |
| comedi_error(dev, "I/O port conflict"); |
| return -EIO; |
| } |
| dev->iobase = iobase; |
| |
| ret = alloc_private(dev, sizeof(*devpriv)); |
| if (ret) |
| return ret; |
| devpriv = dev->private; |
| |
| ret = comedi_alloc_subdevices(dev, 4); |
| if (ret) |
| return ret; |
| |
| s = &dev->subdevices[0]; |
| /* GENERAL-PURPOSE COUNTER/TIME (GPCT) */ |
| s->type = COMEDI_SUBD_COUNTER; |
| s->subdev_flags = SDF_READABLE | SDF_WRITABLE | SDF_LSAMPL; |
| s->n_chan = 4; |
| s->maxdata = 0x00ffffff; /* 24 bit counter */ |
| s->insn_read = s526_gpct_rinsn; |
| s->insn_config = s526_gpct_insn_config; |
| s->insn_write = s526_gpct_winsn; |
| |
| s = &dev->subdevices[1]; |
| /* analog input subdevice */ |
| s->type = COMEDI_SUBD_AI; |
| s->subdev_flags = SDF_READABLE | SDF_DIFF; |
| /* channels 0 to 7 are the regular differential inputs */ |
| /* channel 8 is "reference 0" (+10V), channel 9 is "reference 1" (0V) */ |
| s->n_chan = 10; |
| s->maxdata = 0xffff; |
| s->range_table = &range_bipolar10; |
| s->len_chanlist = 16; |
| s->insn_read = s526_ai_rinsn; |
| s->insn_config = s526_ai_insn_config; |
| |
| s = &dev->subdevices[2]; |
| /* analog output subdevice */ |
| s->type = COMEDI_SUBD_AO; |
| s->subdev_flags = SDF_WRITABLE; |
| s->n_chan = 4; |
| s->maxdata = 0xffff; |
| s->range_table = &range_bipolar10; |
| s->insn_write = s526_ao_winsn; |
| s->insn_read = s526_ao_rinsn; |
| |
| s = &dev->subdevices[3]; |
| /* digital i/o subdevice */ |
| s->type = COMEDI_SUBD_DIO; |
| s->subdev_flags = SDF_READABLE | SDF_WRITABLE; |
| s->n_chan = 8; |
| s->maxdata = 1; |
| s->range_table = &range_digital; |
| s->insn_bits = s526_dio_insn_bits; |
| s->insn_config = s526_dio_insn_config; |
| |
| dev_info(dev->class_dev, "%s attached\n", dev->board_name); |
| |
| return 1; |
| } |
| |
| static void s526_detach(struct comedi_device *dev) |
| { |
| if (dev->iobase > 0) |
| release_region(dev->iobase, S526_IOSIZE); |
| } |
| |
| static struct comedi_driver s526_driver = { |
| .driver_name = "s526", |
| .module = THIS_MODULE, |
| .attach = s526_attach, |
| .detach = s526_detach, |
| }; |
| module_comedi_driver(s526_driver); |
| |
| MODULE_AUTHOR("Comedi http://www.comedi.org"); |
| MODULE_DESCRIPTION("Comedi low-level driver"); |
| MODULE_LICENSE("GPL"); |