linux/drivers/staging/comedi/drivers/das16m1.c
<<
>>
Prefs
   1/*
   2    comedi/drivers/das16m1.c
   3    CIO-DAS16/M1 driver
   4    Author: Frank Mori Hess, based on code from the das16
   5      driver.
   6    Copyright (C) 2001 Frank Mori Hess <fmhess@users.sourceforge.net>
   7
   8    COMEDI - Linux Control and Measurement Device Interface
   9    Copyright (C) 2000 David A. Schleef <ds@schleef.org>
  10
  11    This program is free software; you can redistribute it and/or modify
  12    it under the terms of the GNU General Public License as published by
  13    the Free Software Foundation; either version 2 of the License, or
  14    (at your option) any later version.
  15
  16    This program is distributed in the hope that it will be useful,
  17    but WITHOUT ANY WARRANTY; without even the implied warranty of
  18    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  19    GNU General Public License for more details.
  20*/
  21/*
  22Driver: das16m1
  23Description: CIO-DAS16/M1
  24Author: Frank Mori Hess <fmhess@users.sourceforge.net>
  25Devices: [Measurement Computing] CIO-DAS16/M1 (das16m1)
  26Status: works
  27
  28This driver supports a single board - the CIO-DAS16/M1.
  29As far as I know, there are no other boards that have
  30the same register layout.  Even the CIO-DAS16/M1/16 is
  31significantly different.
  32
  33I was _barely_ able to reach the full 1 MHz capability
  34of this board, using a hard real-time interrupt
  35(set the TRIG_RT flag in your struct comedi_cmd and use
  36rtlinux or RTAI).  The board can't do dma, so the bottleneck is
  37pulling the data across the ISA bus.  I timed the interrupt
  38handler, and it took my computer ~470 microseconds to pull 512
  39samples from the board.  So at 1 Mhz sampling rate,
  40expect your CPU to be spending almost all of its
  41time in the interrupt handler.
  42
  43This board has some unusual restrictions for its channel/gain list.  If the
  44list has 2 or more channels in it, then two conditions must be satisfied:
  45(1) - even/odd channels must appear at even/odd indices in the list
  46(2) - the list must have an even number of entries.
  47
  48Options:
  49        [0] - base io address
  50        [1] - irq (optional, but you probably want it)
  51
  52irq can be omitted, although the cmd interface will not work without it.
  53*/
  54
  55#include <linux/module.h>
  56#include <linux/interrupt.h>
  57#include "../comedidev.h"
  58
  59#include "8255.h"
  60#include "8253.h"
  61#include "comedi_fc.h"
  62
  63#define DAS16M1_SIZE2 8
  64
  65#define FIFO_SIZE 1024          /*  1024 sample fifo */
  66
  67/*
  68    CIO-DAS16_M1.pdf
  69
  70    "cio-das16/m1"
  71
  72  0     a/d bits 0-3, mux               start 12 bit
  73  1     a/d bits 4-11           unused
  74  2     status          control
  75  3     di 4 bit                do 4 bit
  76  4     unused                  clear interrupt
  77  5     interrupt, pacer
  78  6     channel/gain queue address
  79  7     channel/gain queue data
  80  89ab  8254
  81  cdef  8254
  82  400   8255
  83  404-407       8254
  84
  85*/
  86
  87#define DAS16M1_AI             0        /*  16-bit wide register */
  88#define   AI_CHAN(x)             ((x) & 0xf)
  89#define DAS16M1_CS             2
  90#define   EXT_TRIG_BIT           0x1
  91#define   OVRUN                  0x20
  92#define   IRQDATA                0x80
  93#define DAS16M1_DIO            3
  94#define DAS16M1_CLEAR_INTR     4
  95#define DAS16M1_INTR_CONTROL   5
  96#define   EXT_PACER              0x2
  97#define   INT_PACER              0x3
  98#define   PACER_MASK             0x3
  99#define   INTE                   0x80
 100#define DAS16M1_QUEUE_ADDR     6
 101#define DAS16M1_QUEUE_DATA     7
 102#define   Q_CHAN(x)              ((x) & 0x7)
 103#define   Q_RANGE(x)             (((x) & 0xf) << 4)
 104#define   UNIPOLAR               0x40
 105#define DAS16M1_8254_FIRST             0x8
 106#define DAS16M1_8254_FIRST_CNTRL       0xb
 107#define   TOTAL_CLEAR                    0x30
 108#define DAS16M1_8254_SECOND            0xc
 109#define DAS16M1_82C55                  0x400
 110#define DAS16M1_8254_THIRD             0x404
 111
 112static const struct comedi_lrange range_das16m1 = {
 113        9, {
 114                BIP_RANGE(5),
 115                BIP_RANGE(2.5),
 116                BIP_RANGE(1.25),
 117                BIP_RANGE(0.625),
 118                UNI_RANGE(10),
 119                UNI_RANGE(5),
 120                UNI_RANGE(2.5),
 121                UNI_RANGE(1.25),
 122                BIP_RANGE(10)
 123        }
 124};
 125
 126struct das16m1_private_struct {
 127        unsigned int control_state;
 128        unsigned int adc_count; /*  number of samples completed */
 129        /* initial value in lower half of hardware conversion counter,
 130         * needed to keep track of whether new count has been loaded into
 131         * counter yet (loaded by first sample conversion) */
 132        u16 initial_hw_count;
 133        unsigned short ai_buffer[FIFO_SIZE];
 134        unsigned int divisor1;  /*  divides master clock to obtain conversion speed */
 135        unsigned int divisor2;  /*  divides master clock to obtain conversion speed */
 136        unsigned long extra_iobase;
 137};
 138
 139static inline unsigned short munge_sample(unsigned short data)
 140{
 141        return (data >> 4) & 0xfff;
 142}
 143
 144static void munge_sample_array(unsigned short *array, unsigned int num_elements)
 145{
 146        unsigned int i;
 147
 148        for (i = 0; i < num_elements; i++)
 149                array[i] = munge_sample(array[i]);
 150}
 151
 152static int das16m1_ai_check_chanlist(struct comedi_device *dev,
 153                                     struct comedi_subdevice *s,
 154                                     struct comedi_cmd *cmd)
 155{
 156        int i;
 157
 158        if (cmd->chanlist_len == 1)
 159                return 0;
 160
 161        if ((cmd->chanlist_len % 2) != 0) {
 162                dev_dbg(dev->class_dev,
 163                        "chanlist must be of even length or length 1\n");
 164                return -EINVAL;
 165        }
 166
 167        for (i = 0; i < cmd->chanlist_len; i++) {
 168                unsigned int chan = CR_CHAN(cmd->chanlist[i]);
 169
 170                if ((i % 2) != (chan % 2)) {
 171                        dev_dbg(dev->class_dev,
 172                                 "even/odd channels must go have even/odd chanlist indices\n");
 173                        return -EINVAL;
 174                }
 175        }
 176
 177        return 0;
 178}
 179
 180static int das16m1_cmd_test(struct comedi_device *dev,
 181                            struct comedi_subdevice *s, struct comedi_cmd *cmd)
 182{
 183        struct das16m1_private_struct *devpriv = dev->private;
 184        int err = 0;
 185        unsigned int arg;
 186
 187        /* Step 1 : check if triggers are trivially valid */
 188
 189        err |= cfc_check_trigger_src(&cmd->start_src, TRIG_NOW | TRIG_EXT);
 190        err |= cfc_check_trigger_src(&cmd->scan_begin_src, TRIG_FOLLOW);
 191        err |= cfc_check_trigger_src(&cmd->convert_src, TRIG_TIMER | TRIG_EXT);
 192        err |= cfc_check_trigger_src(&cmd->scan_end_src, TRIG_COUNT);
 193        err |= cfc_check_trigger_src(&cmd->stop_src, TRIG_COUNT | TRIG_NONE);
 194
 195        if (err)
 196                return 1;
 197
 198        /* Step 2a : make sure trigger sources are unique */
 199
 200        err |= cfc_check_trigger_is_unique(cmd->start_src);
 201        err |= cfc_check_trigger_is_unique(cmd->convert_src);
 202        err |= cfc_check_trigger_is_unique(cmd->stop_src);
 203
 204        /* Step 2b : and mutually compatible */
 205
 206        if (err)
 207                return 2;
 208
 209        /* Step 3: check if arguments are trivially valid */
 210
 211        err |= cfc_check_trigger_arg_is(&cmd->start_arg, 0);
 212
 213        if (cmd->scan_begin_src == TRIG_FOLLOW) /* internal trigger */
 214                err |= cfc_check_trigger_arg_is(&cmd->scan_begin_arg, 0);
 215
 216        if (cmd->convert_src == TRIG_TIMER)
 217                err |= cfc_check_trigger_arg_min(&cmd->convert_arg, 1000);
 218
 219        err |= cfc_check_trigger_arg_is(&cmd->scan_end_arg, cmd->chanlist_len);
 220
 221        if (cmd->stop_src == TRIG_COUNT)
 222                err |= cfc_check_trigger_arg_min(&cmd->stop_arg, 1);
 223        else    /* TRIG_NONE */
 224                err |= cfc_check_trigger_arg_is(&cmd->stop_arg, 0);
 225
 226        if (err)
 227                return 3;
 228
 229        /* step 4: fix up arguments */
 230
 231        if (cmd->convert_src == TRIG_TIMER) {
 232                arg = cmd->convert_arg;
 233                i8253_cascade_ns_to_timer(I8254_OSC_BASE_10MHZ,
 234                                          &devpriv->divisor1,
 235                                          &devpriv->divisor2,
 236                                          &arg, cmd->flags);
 237                err |= cfc_check_trigger_arg_is(&cmd->convert_arg, arg);
 238        }
 239
 240        if (err)
 241                return 4;
 242
 243        /* Step 5: check channel list if it exists */
 244        if (cmd->chanlist && cmd->chanlist_len > 0)
 245                err |= das16m1_ai_check_chanlist(dev, s, cmd);
 246
 247        if (err)
 248                return 5;
 249
 250        return 0;
 251}
 252
 253static void das16m1_set_pacer(struct comedi_device *dev)
 254{
 255        struct das16m1_private_struct *devpriv = dev->private;
 256        unsigned long timer_base = dev->iobase + DAS16M1_8254_SECOND;
 257
 258        i8254_set_mode(timer_base, 0, 1, I8254_MODE2 | I8254_BINARY);
 259        i8254_set_mode(timer_base, 0, 2, I8254_MODE2 | I8254_BINARY);
 260
 261        i8254_write(timer_base, 0, 1, devpriv->divisor1);
 262        i8254_write(timer_base, 0, 2, devpriv->divisor2);
 263}
 264
 265static int das16m1_cmd_exec(struct comedi_device *dev,
 266                            struct comedi_subdevice *s)
 267{
 268        struct das16m1_private_struct *devpriv = dev->private;
 269        struct comedi_async *async = s->async;
 270        struct comedi_cmd *cmd = &async->cmd;
 271        unsigned long timer_base = dev->iobase + DAS16M1_8254_FIRST;
 272        unsigned int byte, i;
 273
 274        /* disable interrupts and internal pacer */
 275        devpriv->control_state &= ~INTE & ~PACER_MASK;
 276        outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL);
 277
 278        /*  set software count */
 279        devpriv->adc_count = 0;
 280        /* Initialize lower half of hardware counter, used to determine how
 281         * many samples are in fifo.  Value doesn't actually load into counter
 282         * until counter's next clock (the next a/d conversion) */
 283        i8254_set_mode(timer_base, 0, 1, I8254_MODE2 | I8254_BINARY);
 284        i8254_write(timer_base, 0, 1, 0);
 285        /* remember current reading of counter so we know when counter has
 286         * actually been loaded */
 287        devpriv->initial_hw_count = i8254_read(timer_base, 0, 1);
 288        /* setup channel/gain queue */
 289        for (i = 0; i < cmd->chanlist_len; i++) {
 290                outb(i, dev->iobase + DAS16M1_QUEUE_ADDR);
 291                byte =
 292                    Q_CHAN(CR_CHAN(cmd->chanlist[i])) |
 293                    Q_RANGE(CR_RANGE(cmd->chanlist[i]));
 294                outb(byte, dev->iobase + DAS16M1_QUEUE_DATA);
 295        }
 296
 297        /* enable interrupts and set internal pacer counter mode and counts */
 298        devpriv->control_state &= ~PACER_MASK;
 299        if (cmd->convert_src == TRIG_TIMER) {
 300                das16m1_set_pacer(dev);
 301                devpriv->control_state |= INT_PACER;
 302        } else {        /* TRIG_EXT */
 303                devpriv->control_state |= EXT_PACER;
 304        }
 305
 306        /*  set control & status register */
 307        byte = 0;
 308        /* if we are using external start trigger (also board dislikes having
 309         * both start and conversion triggers external simultaneously) */
 310        if (cmd->start_src == TRIG_EXT && cmd->convert_src != TRIG_EXT)
 311                byte |= EXT_TRIG_BIT;
 312
 313        outb(byte, dev->iobase + DAS16M1_CS);
 314        /* clear interrupt bit */
 315        outb(0, dev->iobase + DAS16M1_CLEAR_INTR);
 316
 317        devpriv->control_state |= INTE;
 318        outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL);
 319
 320        return 0;
 321}
 322
 323static int das16m1_cancel(struct comedi_device *dev, struct comedi_subdevice *s)
 324{
 325        struct das16m1_private_struct *devpriv = dev->private;
 326
 327        devpriv->control_state &= ~INTE & ~PACER_MASK;
 328        outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL);
 329
 330        return 0;
 331}
 332
 333static int das16m1_ai_eoc(struct comedi_device *dev,
 334                          struct comedi_subdevice *s,
 335                          struct comedi_insn *insn,
 336                          unsigned long context)
 337{
 338        unsigned int status;
 339
 340        status = inb(dev->iobase + DAS16M1_CS);
 341        if (status & IRQDATA)
 342                return 0;
 343        return -EBUSY;
 344}
 345
 346static int das16m1_ai_rinsn(struct comedi_device *dev,
 347                            struct comedi_subdevice *s,
 348                            struct comedi_insn *insn, unsigned int *data)
 349{
 350        struct das16m1_private_struct *devpriv = dev->private;
 351        int ret;
 352        int n;
 353        int byte;
 354
 355        /* disable interrupts and internal pacer */
 356        devpriv->control_state &= ~INTE & ~PACER_MASK;
 357        outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL);
 358
 359        /* setup channel/gain queue */
 360        outb(0, dev->iobase + DAS16M1_QUEUE_ADDR);
 361        byte =
 362            Q_CHAN(CR_CHAN(insn->chanspec)) | Q_RANGE(CR_RANGE(insn->chanspec));
 363        outb(byte, dev->iobase + DAS16M1_QUEUE_DATA);
 364
 365        for (n = 0; n < insn->n; n++) {
 366                /* clear IRQDATA bit */
 367                outb(0, dev->iobase + DAS16M1_CLEAR_INTR);
 368                /* trigger conversion */
 369                outb(0, dev->iobase);
 370
 371                ret = comedi_timeout(dev, s, insn, das16m1_ai_eoc, 0);
 372                if (ret)
 373                        return ret;
 374
 375                data[n] = munge_sample(inw(dev->iobase));
 376        }
 377
 378        return n;
 379}
 380
 381static int das16m1_di_rbits(struct comedi_device *dev,
 382                            struct comedi_subdevice *s,
 383                            struct comedi_insn *insn, unsigned int *data)
 384{
 385        unsigned int bits;
 386
 387        bits = inb(dev->iobase + DAS16M1_DIO) & 0xf;
 388        data[1] = bits;
 389        data[0] = 0;
 390
 391        return insn->n;
 392}
 393
 394static int das16m1_do_wbits(struct comedi_device *dev,
 395                            struct comedi_subdevice *s,
 396                            struct comedi_insn *insn,
 397                            unsigned int *data)
 398{
 399        if (comedi_dio_update_state(s, data))
 400                outb(s->state, dev->iobase + DAS16M1_DIO);
 401
 402        data[1] = s->state;
 403
 404        return insn->n;
 405}
 406
 407static void das16m1_handler(struct comedi_device *dev, unsigned int status)
 408{
 409        struct das16m1_private_struct *devpriv = dev->private;
 410        struct comedi_subdevice *s;
 411        struct comedi_async *async;
 412        struct comedi_cmd *cmd;
 413        u16 num_samples;
 414        u16 hw_counter;
 415
 416        s = dev->read_subdev;
 417        async = s->async;
 418        cmd = &async->cmd;
 419
 420        /*  figure out how many samples are in fifo */
 421        hw_counter = i8254_read(dev->iobase + DAS16M1_8254_FIRST, 0, 1);
 422        /* make sure hardware counter reading is not bogus due to initial value
 423         * not having been loaded yet */
 424        if (devpriv->adc_count == 0 && hw_counter == devpriv->initial_hw_count) {
 425                num_samples = 0;
 426        } else {
 427                /* The calculation of num_samples looks odd, but it uses the following facts.
 428                 * 16 bit hardware counter is initialized with value of zero (which really
 429                 * means 0x1000).  The counter decrements by one on each conversion
 430                 * (when the counter decrements from zero it goes to 0xffff).  num_samples
 431                 * is a 16 bit variable, so it will roll over in a similar fashion to the
 432                 * hardware counter.  Work it out, and this is what you get. */
 433                num_samples = -hw_counter - devpriv->adc_count;
 434        }
 435        /*  check if we only need some of the points */
 436        if (cmd->stop_src == TRIG_COUNT) {
 437                if (num_samples > cmd->stop_arg * cmd->chanlist_len)
 438                        num_samples = cmd->stop_arg * cmd->chanlist_len;
 439        }
 440        /*  make sure we dont try to get too many points if fifo has overrun */
 441        if (num_samples > FIFO_SIZE)
 442                num_samples = FIFO_SIZE;
 443        insw(dev->iobase, devpriv->ai_buffer, num_samples);
 444        munge_sample_array(devpriv->ai_buffer, num_samples);
 445        comedi_buf_write_samples(s, devpriv->ai_buffer, num_samples);
 446        devpriv->adc_count += num_samples;
 447
 448        if (cmd->stop_src == TRIG_COUNT) {
 449                if (devpriv->adc_count >= cmd->stop_arg * cmd->chanlist_len) {
 450                        /* end of acquisition */
 451                        async->events |= COMEDI_CB_EOA;
 452                }
 453        }
 454
 455        /* this probably won't catch overruns since the card doesn't generate
 456         * overrun interrupts, but we might as well try */
 457        if (status & OVRUN) {
 458                async->events |= COMEDI_CB_EOA | COMEDI_CB_ERROR;
 459                dev_err(dev->class_dev, "fifo overflow\n");
 460        }
 461
 462        comedi_handle_events(dev, s);
 463}
 464
 465static int das16m1_poll(struct comedi_device *dev, struct comedi_subdevice *s)
 466{
 467        unsigned long flags;
 468        unsigned int status;
 469
 470        /*  prevent race with interrupt handler */
 471        spin_lock_irqsave(&dev->spinlock, flags);
 472        status = inb(dev->iobase + DAS16M1_CS);
 473        das16m1_handler(dev, status);
 474        spin_unlock_irqrestore(&dev->spinlock, flags);
 475
 476        return comedi_buf_n_bytes_ready(s);
 477}
 478
 479static irqreturn_t das16m1_interrupt(int irq, void *d)
 480{
 481        int status;
 482        struct comedi_device *dev = d;
 483
 484        if (!dev->attached) {
 485                dev_err(dev->class_dev, "premature interrupt\n");
 486                return IRQ_HANDLED;
 487        }
 488        /*  prevent race with comedi_poll() */
 489        spin_lock(&dev->spinlock);
 490
 491        status = inb(dev->iobase + DAS16M1_CS);
 492
 493        if ((status & (IRQDATA | OVRUN)) == 0) {
 494                dev_err(dev->class_dev, "spurious interrupt\n");
 495                spin_unlock(&dev->spinlock);
 496                return IRQ_NONE;
 497        }
 498
 499        das16m1_handler(dev, status);
 500
 501        /* clear interrupt */
 502        outb(0, dev->iobase + DAS16M1_CLEAR_INTR);
 503
 504        spin_unlock(&dev->spinlock);
 505        return IRQ_HANDLED;
 506}
 507
 508static int das16m1_irq_bits(unsigned int irq)
 509{
 510        switch (irq) {
 511        case 10:
 512                return 0x0;
 513        case 11:
 514                return 0x1;
 515        case 12:
 516                return 0x2;
 517        case 15:
 518                return 0x3;
 519        case 2:
 520                return 0x4;
 521        case 3:
 522                return 0x5;
 523        case 5:
 524                return 0x6;
 525        case 7:
 526                return 0x7;
 527        default:
 528                return 0x0;
 529        }
 530}
 531
 532/*
 533 * Options list:
 534 *   0  I/O base
 535 *   1  IRQ
 536 */
 537static int das16m1_attach(struct comedi_device *dev,
 538                          struct comedi_devconfig *it)
 539{
 540        struct das16m1_private_struct *devpriv;
 541        struct comedi_subdevice *s;
 542        int ret;
 543
 544        devpriv = comedi_alloc_devpriv(dev, sizeof(*devpriv));
 545        if (!devpriv)
 546                return -ENOMEM;
 547
 548        ret = comedi_request_region(dev, it->options[0], 0x10);
 549        if (ret)
 550                return ret;
 551        /* Request an additional region for the 8255 */
 552        ret = __comedi_request_region(dev, dev->iobase + DAS16M1_82C55,
 553                                      DAS16M1_SIZE2);
 554        if (ret)
 555                return ret;
 556        devpriv->extra_iobase = dev->iobase + DAS16M1_82C55;
 557
 558        /* only irqs 2, 3, 4, 5, 6, 7, 10, 11, 12, 14, and 15 are valid */
 559        if ((1 << it->options[1]) & 0xdcfc) {
 560                ret = request_irq(it->options[1], das16m1_interrupt, 0,
 561                                  dev->board_name, dev);
 562                if (ret == 0)
 563                        dev->irq = it->options[1];
 564        }
 565
 566        ret = comedi_alloc_subdevices(dev, 4);
 567        if (ret)
 568                return ret;
 569
 570        s = &dev->subdevices[0];
 571        /* ai */
 572        s->type = COMEDI_SUBD_AI;
 573        s->subdev_flags = SDF_READABLE | SDF_DIFF;
 574        s->n_chan = 8;
 575        s->maxdata = (1 << 12) - 1;
 576        s->range_table = &range_das16m1;
 577        s->insn_read = das16m1_ai_rinsn;
 578        if (dev->irq) {
 579                dev->read_subdev = s;
 580                s->subdev_flags |= SDF_CMD_READ;
 581                s->len_chanlist = 256;
 582                s->do_cmdtest = das16m1_cmd_test;
 583                s->do_cmd = das16m1_cmd_exec;
 584                s->cancel = das16m1_cancel;
 585                s->poll = das16m1_poll;
 586        }
 587
 588        s = &dev->subdevices[1];
 589        /* di */
 590        s->type = COMEDI_SUBD_DI;
 591        s->subdev_flags = SDF_READABLE;
 592        s->n_chan = 4;
 593        s->maxdata = 1;
 594        s->range_table = &range_digital;
 595        s->insn_bits = das16m1_di_rbits;
 596
 597        s = &dev->subdevices[2];
 598        /* do */
 599        s->type = COMEDI_SUBD_DO;
 600        s->subdev_flags = SDF_WRITABLE;
 601        s->n_chan = 4;
 602        s->maxdata = 1;
 603        s->range_table = &range_digital;
 604        s->insn_bits = das16m1_do_wbits;
 605
 606        s = &dev->subdevices[3];
 607        /* 8255 */
 608        ret = subdev_8255_init(dev, s, NULL, DAS16M1_82C55);
 609        if (ret)
 610                return ret;
 611
 612        /*  disable upper half of hardware conversion counter so it doesn't mess with us */
 613        outb(TOTAL_CLEAR, dev->iobase + DAS16M1_8254_FIRST_CNTRL);
 614
 615        /*  initialize digital output lines */
 616        outb(0, dev->iobase + DAS16M1_DIO);
 617
 618        /* set the interrupt level */
 619        devpriv->control_state = das16m1_irq_bits(dev->irq) << 4;
 620        outb(devpriv->control_state, dev->iobase + DAS16M1_INTR_CONTROL);
 621
 622        return 0;
 623}
 624
 625static void das16m1_detach(struct comedi_device *dev)
 626{
 627        struct das16m1_private_struct *devpriv = dev->private;
 628
 629        if (devpriv && devpriv->extra_iobase)
 630                release_region(devpriv->extra_iobase, DAS16M1_SIZE2);
 631        comedi_legacy_detach(dev);
 632}
 633
 634static struct comedi_driver das16m1_driver = {
 635        .driver_name    = "das16m1",
 636        .module         = THIS_MODULE,
 637        .attach         = das16m1_attach,
 638        .detach         = das16m1_detach,
 639};
 640module_comedi_driver(das16m1_driver);
 641
 642MODULE_AUTHOR("Comedi http://www.comedi.org");
 643MODULE_DESCRIPTION("Comedi low-level driver");
 644MODULE_LICENSE("GPL");
 645