From 85f2e8adaba6aaf613b22476ed2db2a70fa3d8e1 Mon Sep 17 00:00:00 2001 From: Frank Mori Hess Date: Wed, 18 Feb 2009 15:29:44 -0800 Subject: Staging: comedi: add das16m1 driver From: Frank Mori Hess Driver for Measurement Computing CIO-DAS16/M1 From: Frank Mori Hess Cc: David Schleef Cc: Ian Abbott Signed-off-by: Greg Kroah-Hartman --- drivers/staging/comedi/drivers/das16m1.c | 765 +++++++++++++++++++++++++++++++ 1 file changed, 765 insertions(+) create mode 100644 drivers/staging/comedi/drivers/das16m1.c --- /dev/null +++ b/drivers/staging/comedi/drivers/das16m1.c @@ -0,0 +1,765 @@ +/* + comedi/drivers/das16m1.c + CIO-DAS16/M1 driver + Author: Frank Mori Hess, based on code from the das16 + driver. + Copyright (C) 2001 Frank Mori Hess + + COMEDI - Linux Control and Measurement Device Interface + Copyright (C) 2000 David A. Schleef + + 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: das16m1 +Description: CIO-DAS16/M1 +Author: Frank Mori Hess +Devices: [Measurement Computing] CIO-DAS16/M1 (cio-das16/m1) +Status: works + +This driver supports a single board - the CIO-DAS16/M1. +As far as I know, there are no other boards that have +the same register layout. Even the CIO-DAS16/M1/16 is +significantly different. + +I was _barely_ able to reach the full 1 MHz capability +of this board, using a hard real-time interrupt +(set the TRIG_RT flag in your comedi_cmd and use +rtlinux or RTAI). The board can't do dma, so the bottleneck is +pulling the data across the ISA bus. I timed the interrupt +handler, and it took my computer ~470 microseconds to pull 512 +samples from the board. So at 1 Mhz sampling rate, +expect your CPU to be spending almost all of its +time in the interrupt handler. + +This board has some unusual restrictions for its channel/gain list. If the +list has 2 or more channels in it, then two conditions must be satisfied: +(1) - even/odd channels must appear at even/odd indices in the list +(2) - the list must have an even number of entries. + +Options: + [0] - base io address + [1] - irq (optional, but you probably want it) + +irq can be omitted, although the cmd interface will not work without it. +*/ + +#include +#include "../comedidev.h" + +#include "8255.h" +#include "8253.h" +#include "comedi_fc.h" + +#define DAS16M1_SIZE 16 +#define DAS16M1_SIZE2 8 + +#define DAS16M1_XTAL 100 //10 MHz master clock + +#define FIFO_SIZE 1024 // 1024 sample fifo + +/* + CIO-DAS16_M1.pdf + + "cio-das16/m1" + + 0 a/d bits 0-3, mux start 12 bit + 1 a/d bits 4-11 unused + 2 status control + 3 di 4 bit do 4 bit + 4 unused clear interrupt + 5 interrupt, pacer + 6 channel/gain queue address + 7 channel/gain queue data + 89ab 8254 + cdef 8254 + 400 8255 + 404-407 8254 + +*/ + +#define DAS16M1_AI 0 // 16-bit wide register +#define AI_CHAN(x) ((x) & 0xf) +#define DAS16M1_CS 2 +#define EXT_TRIG_BIT 0x1 +#define OVRUN 0x20 +#define IRQDATA 0x80 +#define DAS16M1_DIO 3 +#define DAS16M1_CLEAR_INTR 4 +#define DAS16M1_INTR_CONTROL 5 +#define EXT_PACER 0x2 +#define INT_PACER 0x3 +#define PACER_MASK 0x3 +#define INTE 0x80 +#define DAS16M1_QUEUE_ADDR 6 +#define DAS16M1_QUEUE_DATA 7 +#define Q_CHAN(x) ((x) & 0x7) +#define Q_RANGE(x) (((x) & 0xf) << 4) +#define UNIPOLAR 0x40 +#define DAS16M1_8254_FIRST 0x8 +#define DAS16M1_8254_FIRST_CNTRL 0xb +#define TOTAL_CLEAR 0x30 +#define DAS16M1_8254_SECOND 0xc +#define DAS16M1_82C55 0x400 +#define DAS16M1_8254_THIRD 0x404 + +static const comedi_lrange range_das16m1 = { 9, + { + BIP_RANGE(5), + BIP_RANGE(2.5), + BIP_RANGE(1.25), + BIP_RANGE(0.625), + UNI_RANGE(10), + UNI_RANGE(5), + UNI_RANGE(2.5), + UNI_RANGE(1.25), + BIP_RANGE(10), + } +}; + +static int das16m1_do_wbits(comedi_device * dev, comedi_subdevice * s, + comedi_insn * insn, lsampl_t * data); +static int das16m1_di_rbits(comedi_device * dev, comedi_subdevice * s, + comedi_insn * insn, lsampl_t * data); +static int das16m1_ai_rinsn(comedi_device * dev, comedi_subdevice * s, + comedi_insn * insn, lsampl_t * data); + +static int das16m1_cmd_test(comedi_device * dev, comedi_subdevice * s, + comedi_cmd * cmd); +static int das16m1_cmd_exec(comedi_device * dev, comedi_subdevice * s); +static int das16m1_cancel(comedi_device * dev, comedi_subdevice * s); + +static int das16m1_poll(comedi_device * dev, comedi_subdevice * s); +static irqreturn_t das16m1_interrupt(int irq, void *d PT_REGS_ARG); +static void das16m1_handler(comedi_device * dev, unsigned int status); + +static unsigned int das16m1_set_pacer(comedi_device * dev, unsigned int ns, + int round_flag); + +static int das16m1_irq_bits(unsigned int irq); + +typedef struct das16m1_board_struct { + const char *name; + unsigned int ai_speed; +} das16m1_board; + +static const das16m1_board das16m1_boards[] = { + { + name: "cio-das16/m1", // CIO-DAS16_M1.pdf + ai_speed:1000, // 1MHz max speed + }, +}; + +#define das16m1_num_boards ((sizeof(das16m1_boards)) / (sizeof(das16m1_boards[0]))) + +static int das16m1_attach(comedi_device * dev, comedi_devconfig * it); +static int das16m1_detach(comedi_device * dev); +static comedi_driver driver_das16m1 = { + driver_name:"das16m1", + module:THIS_MODULE, + attach:das16m1_attach, + detach:das16m1_detach, + board_name:&das16m1_boards[0].name, + num_names:das16m1_num_boards, + offset:sizeof(das16m1_boards[0]), +}; + +struct das16m1_private_struct { + unsigned int control_state; + volatile unsigned int adc_count; // number of samples completed + /* initial value in lower half of hardware conversion counter, + * needed to keep track of whether new count has been loaded into + * counter yet (loaded by first sample conversion) */ + u16 initial_hw_count; + sampl_t ai_buffer[FIFO_SIZE]; + unsigned int do_bits; // saves status of digital output bits + unsigned int divisor1; // divides master clock to obtain conversion speed + unsigned int divisor2; // divides master clock to obtain conversion speed +}; +#define devpriv ((struct das16m1_private_struct *)(dev->private)) +#define thisboard ((const das16m1_board *)(dev->board_ptr)) + +COMEDI_INITCLEANUP(driver_das16m1); + +static inline sampl_t munge_sample(sampl_t data) +{ + return (data >> 4) & 0xfff; +} + +static int das16m1_cmd_test(comedi_device * dev, comedi_subdevice * s, + comedi_cmd * cmd) +{ + unsigned int err = 0, tmp, i; + + /* make sure triggers are valid */ + tmp = cmd->start_src; + cmd->start_src &= TRIG_NOW | TRIG_EXT; + if (!cmd->start_src || tmp != cmd->start_src) + err++; + + tmp = cmd->scan_begin_src; + cmd->scan_begin_src &= TRIG_FOLLOW; + if (!cmd->scan_begin_src || tmp != cmd->scan_begin_src) + err++; + + tmp = cmd->convert_src; + cmd->convert_src &= TRIG_TIMER | TRIG_EXT; + if (!cmd->convert_src || tmp != cmd->convert_src) + err++; + + tmp = cmd->scan_end_src; + cmd->scan_end_src &= TRIG_COUNT; + if (!cmd->scan_end_src || tmp != cmd->scan_end_src) + err++; + + tmp = cmd->stop_src; + cmd->stop_src &= TRIG_COUNT | TRIG_NONE; + if (!cmd->stop_src || tmp != cmd->stop_src) + err++; + + if (err) + return 1; + + /* step 2: make sure trigger sources are unique and mutually compatible */ + if (cmd->stop_src != TRIG_COUNT && cmd->stop_src != TRIG_NONE) + err++; + if (cmd->start_src != TRIG_NOW && cmd->start_src != TRIG_EXT) + err++; + if (cmd->convert_src != TRIG_TIMER && cmd->convert_src != TRIG_EXT) + err++; + + if (err) + return 2; + + /* step 3: make sure arguments are trivially compatible */ + if (cmd->start_arg != 0) { + cmd->start_arg = 0; + err++; + } + + if (cmd->scan_begin_src == TRIG_FOLLOW) { + /* internal trigger */ + if (cmd->scan_begin_arg != 0) { + cmd->scan_begin_arg = 0; + err++; + } + } + + if (cmd->convert_src == TRIG_TIMER) { + if (cmd->convert_arg < thisboard->ai_speed) { + cmd->convert_arg = thisboard->ai_speed; + err++; + } + } + + if (cmd->scan_end_arg != cmd->chanlist_len) { + cmd->scan_end_arg = cmd->chanlist_len; + err++; + } + + if (cmd->stop_src == TRIG_COUNT) { + /* any count is allowed */ + } else { + /* TRIG_NONE */ + if (cmd->stop_arg != 0) { + cmd->stop_arg = 0; + err++; + } + } + + if (err) + return 3; + + /* step 4: fix up arguments */ + + if (cmd->convert_src == TRIG_TIMER) { + tmp = cmd->convert_arg; + /* calculate counter values that give desired timing */ + i8253_cascade_ns_to_timer_2div(DAS16M1_XTAL, + &(devpriv->divisor1), &(devpriv->divisor2), + &(cmd->convert_arg), cmd->flags & TRIG_ROUND_MASK); + if (tmp != cmd->convert_arg) + err++; + } + + if (err) + return 4; + + // check chanlist against board's peculiarities + if (cmd->chanlist && cmd->chanlist_len > 1) { + for (i = 0; i < cmd->chanlist_len; i++) { + // even/odd channels must go into even/odd queue addresses + if ((i % 2) != (CR_CHAN(cmd->chanlist[i]) % 2)) { + comedi_error(dev, "bad chanlist:\n" + " even/odd channels must go have even/odd chanlist indices"); + err++; + } + } + if ((cmd->chanlist_len % 2) != 0) { + comedi_error(dev, + "chanlist must be of even length or length 1"); + err++; + } + } + + if (err) + return 5; + + return 0; +} + +static int das16m1_cmd_exec(comedi_device * dev, comedi_subdevice * s) +{ + comedi_async *async = s->async; + comedi_cmd *cmd = &async->cmd; + unsigned int byte, i; + + if (dev->irq == 0) { + comedi_error(dev, "irq required to execute comedi_cmd"); + return -1; + } + + /* disable interrupts and internal pacer */ + devpriv->control_state &= ~INTE & ~PACER_MASK; + outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL); + + // set software count + devpriv->adc_count = 0; + /* Initialize lower half of hardware counter, used to determine how + * many samples are in fifo. Value doesn't actually load into counter + * until counter's next clock (the next a/d conversion) */ + i8254_load(dev->iobase + DAS16M1_8254_FIRST, 0, 1, 0, 2); + /* remember current reading of counter so we know when counter has + * actually been loaded */ + devpriv->initial_hw_count = + i8254_read(dev->iobase + DAS16M1_8254_FIRST, 0, 1); + /* setup channel/gain queue */ + for (i = 0; i < cmd->chanlist_len; i++) { + outb(i, dev->iobase + DAS16M1_QUEUE_ADDR); + byte = Q_CHAN(CR_CHAN(cmd-> + chanlist[i])) | Q_RANGE(CR_RANGE(cmd-> + chanlist[i])); + outb(byte, dev->iobase + DAS16M1_QUEUE_DATA); + } + + /* set counter mode and counts */ + cmd->convert_arg = + das16m1_set_pacer(dev, cmd->convert_arg, + cmd->flags & TRIG_ROUND_MASK); + + // set control & status register + byte = 0; + /* if we are using external start trigger (also board dislikes having + * both start and conversion triggers external simultaneously) */ + if (cmd->start_src == TRIG_EXT && cmd->convert_src != TRIG_EXT) { + byte |= EXT_TRIG_BIT; + } + outb(byte, dev->iobase + DAS16M1_CS); + /* clear interrupt bit */ + outb(0, dev->iobase + DAS16M1_CLEAR_INTR); + + /* enable interrupts and internal pacer */ + devpriv->control_state &= ~PACER_MASK; + if (cmd->convert_src == TRIG_TIMER) { + devpriv->control_state |= INT_PACER; + } else { + devpriv->control_state |= EXT_PACER; + } + devpriv->control_state |= INTE; + outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL); + + return 0; +} + +static int das16m1_cancel(comedi_device * dev, comedi_subdevice * s) +{ + devpriv->control_state &= ~INTE & ~PACER_MASK; + outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL); + + return 0; +} + +static int das16m1_ai_rinsn(comedi_device * dev, comedi_subdevice * s, + comedi_insn * insn, lsampl_t * data) +{ + int i, n; + int byte; + const int timeout = 1000; + + /* disable interrupts and internal pacer */ + devpriv->control_state &= ~INTE & ~PACER_MASK; + outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL); + + /* setup channel/gain queue */ + outb(0, dev->iobase + DAS16M1_QUEUE_ADDR); + byte = Q_CHAN(CR_CHAN(insn->chanspec)) | Q_RANGE(CR_RANGE(insn-> + chanspec)); + outb(byte, dev->iobase + DAS16M1_QUEUE_DATA); + + for (n = 0; n < insn->n; n++) { + /* clear IRQDATA bit */ + outb(0, dev->iobase + DAS16M1_CLEAR_INTR); + /* trigger conversion */ + outb(0, dev->iobase); + + for (i = 0; i < timeout; i++) { + if (inb(dev->iobase + DAS16M1_CS) & IRQDATA) + break; + } + if (i == timeout) { + comedi_error(dev, "timeout"); + return -ETIME; + } + data[n] = munge_sample(inw(dev->iobase)); + } + + return n; +} + +static int das16m1_di_rbits(comedi_device * dev, comedi_subdevice * s, + comedi_insn * insn, lsampl_t * data) +{ + lsampl_t bits; + + bits = inb(dev->iobase + DAS16M1_DIO) & 0xf; + data[1] = bits; + data[0] = 0; + + return 2; +} + +static int das16m1_do_wbits(comedi_device * dev, comedi_subdevice * s, + comedi_insn * insn, lsampl_t * data) +{ + lsampl_t wbits; + + // only set bits that have been masked + data[0] &= 0xf; + wbits = devpriv->do_bits; + // zero bits that have been masked + wbits &= ~data[0]; + // set masked bits + wbits |= data[0] & data[1]; + devpriv->do_bits = wbits; + data[1] = wbits; + + outb(devpriv->do_bits, dev->iobase + DAS16M1_DIO); + + return 2; +} + +static int das16m1_poll(comedi_device * dev, comedi_subdevice * s) +{ + unsigned long flags; + unsigned int status; + + // prevent race with interrupt handler + comedi_spin_lock_irqsave(&dev->spinlock, flags); + status = inb(dev->iobase + DAS16M1_CS); + das16m1_handler(dev, status); + comedi_spin_unlock_irqrestore(&dev->spinlock, flags); + + return s->async->buf_write_count - s->async->buf_read_count; +} + +static irqreturn_t das16m1_interrupt(int irq, void *d PT_REGS_ARG) +{ + int status; + comedi_device *dev = d; + + if (dev->attached == 0) { + comedi_error(dev, "premature interrupt"); + return IRQ_HANDLED; + } + // prevent race with comedi_poll() + spin_lock(&dev->spinlock); + + status = inb(dev->iobase + DAS16M1_CS); + + if ((status & (IRQDATA | OVRUN)) == 0) { + comedi_error(dev, "spurious interrupt"); + spin_unlock(&dev->spinlock); + return IRQ_NONE; + } + + das16m1_handler(dev, status); + + /* clear interrupt */ + outb(0, dev->iobase + DAS16M1_CLEAR_INTR); + + spin_unlock(&dev->spinlock); + return IRQ_HANDLED; +} + +static void munge_sample_array(sampl_t * array, unsigned int num_elements) +{ + unsigned int i; + + for (i = 0; i < num_elements; i++) { + array[i] = munge_sample(array[i]); + } +} + +static void das16m1_handler(comedi_device * dev, unsigned int status) +{ + comedi_subdevice *s; + comedi_async *async; + comedi_cmd *cmd; + u16 num_samples; + u16 hw_counter; + + s = dev->read_subdev; + async = s->async; + async->events = 0; + cmd = &async->cmd; + + // figure out how many samples are in fifo + hw_counter = i8254_read(dev->iobase + DAS16M1_8254_FIRST, 0, 1); + /* make sure hardware counter reading is not bogus due to initial value + * not having been loaded yet */ + if (devpriv->adc_count == 0 && hw_counter == devpriv->initial_hw_count) { + num_samples = 0; + } else { + /* The calculation of num_samples looks odd, but it uses the following facts. + * 16 bit hardware counter is initialized with value of zero (which really + * means 0x1000). The counter decrements by one on each conversion + * (when the counter decrements from zero it goes to 0xffff). num_samples + * is a 16 bit variable, so it will roll over in a similar fashion to the + * hardware counter. Work it out, and this is what you get. */ + num_samples = -hw_counter - devpriv->adc_count; + } + // check if we only need some of the points + if (cmd->stop_src == TRIG_COUNT) { + if (num_samples > cmd->stop_arg * cmd->chanlist_len) + num_samples = cmd->stop_arg * cmd->chanlist_len; + } + // make sure we dont try to get too many points if fifo has overrun + if (num_samples > FIFO_SIZE) + num_samples = FIFO_SIZE; + insw(dev->iobase, devpriv->ai_buffer, num_samples); + munge_sample_array(devpriv->ai_buffer, num_samples); + cfc_write_array_to_buffer(s, devpriv->ai_buffer, + num_samples * sizeof(sampl_t)); + devpriv->adc_count += num_samples; + + if (cmd->stop_src == TRIG_COUNT) { + if (devpriv->adc_count >= cmd->stop_arg * cmd->chanlist_len) { /* end of acquisition */ + das16m1_cancel(dev, s); + async->events |= COMEDI_CB_EOA; + } + } + + /* this probably won't catch overruns since the card doesn't generate + * overrun interrupts, but we might as well try */ + if (status & OVRUN) { + das16m1_cancel(dev, s); + async->events |= COMEDI_CB_EOA | COMEDI_CB_ERROR; + comedi_error(dev, "fifo overflow"); + } + + comedi_event(dev, s); + +} + +/* This function takes a time in nanoseconds and sets the * + * 2 pacer clocks to the closest frequency possible. It also * + * returns the actual sampling period. */ +static unsigned int das16m1_set_pacer(comedi_device * dev, unsigned int ns, + int rounding_flags) +{ + i8253_cascade_ns_to_timer_2div(DAS16M1_XTAL, &(devpriv->divisor1), + &(devpriv->divisor2), &ns, rounding_flags & TRIG_ROUND_MASK); + + /* Write the values of ctr1 and ctr2 into counters 1 and 2 */ + i8254_load(dev->iobase + DAS16M1_8254_SECOND, 0, 1, devpriv->divisor1, + 2); + i8254_load(dev->iobase + DAS16M1_8254_SECOND, 0, 2, devpriv->divisor2, + 2); + + return ns; +} + +static int das16m1_irq_bits(unsigned int irq) +{ + int ret; + + switch (irq) { + case 10: + ret = 0x0; + break; + case 11: + ret = 0x1; + break; + case 12: + ret = 0x2; + break; + case 15: + ret = 0x3; + break; + case 2: + ret = 0x4; + break; + case 3: + ret = 0x5; + break; + case 5: + ret = 0x6; + break; + case 7: + ret = 0x7; + break; + default: + return -1; + break; + } + return (ret << 4); +} + +/* + * Options list: + * 0 I/O base + * 1 IRQ + */ + +static int das16m1_attach(comedi_device * dev, comedi_devconfig * it) +{ + comedi_subdevice *s; + int ret; + unsigned int irq; + unsigned long iobase; + + iobase = it->options[0]; + + printk("comedi%d: das16m1:", dev->minor); + + if ((ret = alloc_private(dev, + sizeof(struct das16m1_private_struct))) < 0) + return ret; + + dev->board_name = thisboard->name; + + printk(" io 0x%lx-0x%lx 0x%lx-0x%lx", + iobase, iobase + DAS16M1_SIZE, + iobase + DAS16M1_82C55, iobase + DAS16M1_82C55 + DAS16M1_SIZE2); + if (!request_region(iobase, DAS16M1_SIZE, driver_das16m1.driver_name)) { + printk(" I/O port conflict\n"); + return -EIO; + } + if (!request_region(iobase + DAS16M1_82C55, DAS16M1_SIZE2, + driver_das16m1.driver_name)) { + release_region(iobase, DAS16M1_SIZE); + printk(" I/O port conflict\n"); + return -EIO; + } + dev->iobase = iobase; + + /* now for the irq */ + irq = it->options[1]; + // make sure it is valid + if (das16m1_irq_bits(irq) >= 0) { + ret = comedi_request_irq(irq, das16m1_interrupt, 0, + driver_das16m1.driver_name, dev); + if (ret < 0) { + printk(", irq unavailable\n"); + return ret; + } + dev->irq = irq; + printk(", irq %u\n", irq); + } else if (irq == 0) { + printk(", no irq\n"); + } else { + printk(", invalid irq\n" + " valid irqs are 2, 3, 5, 7, 10, 11, 12, or 15\n"); + return -EINVAL; + } + + if ((ret = alloc_subdevices(dev, 4)) < 0) + return ret; + + s = dev->subdevices + 0; + dev->read_subdev = s; + /* ai */ + s->type = COMEDI_SUBD_AI; + s->subdev_flags = SDF_READABLE | SDF_CMD_READ; + s->n_chan = 8; + s->subdev_flags = SDF_DIFF; + s->len_chanlist = 256; + s->maxdata = (1 << 12) - 1; + s->range_table = &range_das16m1; + s->insn_read = das16m1_ai_rinsn; + s->do_cmdtest = das16m1_cmd_test; + s->do_cmd = das16m1_cmd_exec; + s->cancel = das16m1_cancel; + s->poll = das16m1_poll; + + s = dev->subdevices + 1; + /* di */ + s->type = COMEDI_SUBD_DI; + s->subdev_flags = SDF_READABLE; + s->n_chan = 4; + s->maxdata = 1; + s->range_table = &range_digital; + s->insn_bits = das16m1_di_rbits; + + s = dev->subdevices + 2; + /* do */ + s->type = COMEDI_SUBD_DO; + s->subdev_flags = SDF_WRITABLE | SDF_READABLE; + s->n_chan = 4; + s->maxdata = 1; + s->range_table = &range_digital; + s->insn_bits = das16m1_do_wbits; + + s = dev->subdevices + 3; + /* 8255 */ + subdev_8255_init(dev, s, NULL, dev->iobase + DAS16M1_82C55); + + // disable upper half of hardware conversion counter so it doesn't mess with us + outb(TOTAL_CLEAR, dev->iobase + DAS16M1_8254_FIRST_CNTRL); + + // initialize digital output lines + outb(devpriv->do_bits, dev->iobase + DAS16M1_DIO); + + /* set the interrupt level */ + if (dev->irq) + devpriv->control_state = das16m1_irq_bits(dev->irq); + else + devpriv->control_state = 0; + outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL); + + return 0; +} + +static int das16m1_detach(comedi_device * dev) +{ + printk("comedi%d: das16m1: remove\n", dev->minor); + +// das16m1_reset(dev); + + if (dev->subdevices) + subdev_8255_cleanup(dev, dev->subdevices + 3); + + if (dev->irq) + comedi_free_irq(dev->irq, dev); + + if (dev->iobase) { + release_region(dev->iobase, DAS16M1_SIZE); + release_region(dev->iobase + DAS16M1_82C55, DAS16M1_SIZE2); + } + + return 0; +}