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