New driver from "Calin A. Culianu" <calin@ajvar.org>:
authorFrank Mori Hess <fmhess@speakeasy.net>
Wed, 31 Jan 2007 03:13:55 +0000 (03:13 +0000)
committerFrank Mori Hess <fmhess@speakeasy.net>
Wed, 31 Jan 2007 03:13:55 +0000 (03:13 +0000)
This driver adds support for the Winsystems PCM-MIO PC/104
based multifunction board.  This board has 16 AI, 8 AO, and 48 DIO
channels.  The driver is almost complete -- it is just missing
asynchronous mode for AI and AO.  But synchronous mode works fine.  There
is even IRQ support for the DIO lines (edge-triggered interrupts).

comedi/drivers/Makefile.am
comedi/drivers/pcmmio.c [new file with mode: 0644]

index 01a8a94b34be7a6e05cf07da70ba99e3c9122b4f..4f979be152e98ddc8ae8177c8e9667720c8096e6 100644 (file)
@@ -163,6 +163,7 @@ module_PROGRAMS = \
  pcl816.ko \
  pcl818.ko \
  pcmuio.ko \
+ pcmmio.ko \
  comedi_parport.ko \
  rtd520.ko \
  rti800.ko \
@@ -260,6 +261,7 @@ pcl816_ko_SOURCES = pcl816.c
 pcl818_ko_SOURCES = pcl818.c
 pcmda12_ko_SOURCES = pcmda12.c
 pcmuio_ko_SOURCES = pcmuio.c
+pcmmio_ko_SOURCES = pcmmio.c
 poc_ko_SOURCES = poc.c
 quatech_daqp_cs_ko_SOURCES = quatech_daqp_cs.c
 comedi_parport_ko_SOURCES = comedi_parport.c
diff --git a/comedi/drivers/pcmmio.c b/comedi/drivers/pcmmio.c
new file mode 100644 (file)
index 0000000..5902a54
--- /dev/null
@@ -0,0 +1,1250 @@
+/*
+    comedi/drivers/pcmmio.c
+    Driver for Winsystems PC-104 based multifunction IO board.
+
+    COMEDI - Linux Control and Measurement Device Interface
+    Copyright (C) 2007 Calin A. Culianu <calin@ajvar.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: pcmmio.o
+Description: A driver for the PCM-MIO multifunction board
+Devices: [Winsystems] PCM-MIO (pcmmio)
+Author: Calin Culianu <calin@ajvar.org>
+Updated: Tue, 30 Jan 2007 16:04:41 -0500
+Status: works
+
+A driver for the relatively new PCM-MIO multifunction board from
+Winsystems.  This board is a PC-104 based I/O board.  It contains
+four subdevices: 
+  subdevice 0 - 16 channels of 16-bit AI
+  subdevice 1 - 8 channels of 16-bit AO
+  subdevice 2 - first 24 channels of the 48 channel of DIO (with edge-triggered interrupt support)
+  subdevice 3 - last 24 channels of the 48 channel DIO (no interrupt support for this bank of channels)
+
+  Some notes:
+
+  Synchronous reads and writes are the only things implemented for AI and AO, 
+  even though the hardware itself can do streaming acquisition, etc.  Anyone
+  want to add asynchronous I/O for AI/AO as a feature?  Be my guest...
+
+  Asynchronous I/O for the DIO subdevices *is* implemented, however!  They are 
+  basically edge-triggered interrupts for any configuration of the first 
+  24 DIO-lines.
+
+  Also note that this interrupt support is untested.
+
+  A few words about edge-detection IRQ support (commands on DIO):
+
+  * To use edge-detection IRQ support for the DIO subdevice, pass the IRQ
+    of the board to the comedi_config command.  The board IRQ is not jumpered 
+    but rather configured through software, so any IRQ from 1-15 is OK.
+  
+  * Due to the genericity of the comedi API, you need to create a special 
+    comedi_command in order to use edge-triggered interrupts for DIO.
+  
+  * Use comedi_commands with TRIG_NOW.  Your callback will be called each 
+    time an edge is detected on the specified DIO line(s), and the data
+    values will be two sample_t's, which should be concatenated to form
+    one 32-bit unsigned int.  This value is the mask of channels that had
+    edges detected from your channel list.  Note that the bits positions
+    in the mask correspond to positions in your chanlist when you
+    specified the command and *not* channel id's!
+  
+ *  To set the polarity of the edge-detection interrupts pass a nonzero value 
+    for either CR_RANGE or CR_AREF for edge-up polarity, or a zero 
+    value for both CR_RANGE and CR_AREF if you want edge-down polarity.
+  
+
+Configuration Options:
+  [0] - I/O port base address
+  [1] - IRQ (optional -- for edge-detect interrupt support only, leave out if you don't need this feature)
+*/
+
+#include <linux/comedidev.h>
+#include <linux/pci.h> /* for PCI devices */
+
+#define MIN(a,b) ( ((a) < (b)) ? (a) : (b) )
+
+/* This stuff is all from pcmuio.c -- it refers to the DIO subdevices only */
+#define CHANS_PER_PORT   8
+#define PORTS_PER_ASIC   6
+#define INTR_PORTS_PER_ASIC   3
+#define MAX_CHANS_PER_SUBDEV 24 /* number of channels per comedi subdevice */
+#define PORTS_PER_SUBDEV (MAX_CHANS_PER_SUBDEV/CHANS_PER_PORT)
+#define CHANS_PER_ASIC (CHANS_PER_PORT*PORTS_PER_ASIC)
+#define INTR_CHANS_PER_ASIC 24
+#define INTR_PORTS_PER_SUBDEV (INTR_CHANS_PER_ASIC/CHANS_PER_PORT)
+#define MAX_DIO_CHANS   (PORTS_PER_ASIC*1*CHANS_PER_PORT)
+#define MAX_ASICS       (MAX_DIO_CHANS/CHANS_PER_ASIC)
+#define SDEV_NO ((int)(s - dev->subdevices))
+#define CALC_N_DIO_SUBDEVS(nchans) ((nchans)/MAX_CHANS_PER_SUBDEV + (!!((nchans)%MAX_CHANS_PER_SUBDEV)) /*+ (nchans > INTR_CHANS_PER_ASIC ? 2 : 1)*/)
+/* IO Memory sizes */
+#define ASIC_IOSIZE (0x0B)
+#define PCMMIO48_IOSIZE ASIC_IOSIZE
+
+/* Some offsets - these are all in the 16byte IO memory offset from
+   the base address.  Note that there is a paging scheme to swap out
+   offsets 0x8-0xA using the PAGELOCK register.  See the table below.
+
+  Register(s)       Pages        R/W?        Description
+  --------------------------------------------------------------
+  REG_PORTx         All          R/W         Read/Write/Configure IO
+  REG_INT_PENDING   All          ReadOnly    Quickly see which INT_IDx has int.
+  REG_PAGELOCK      All          WriteOnly   Select a page 
+  REG_POLx          Pg. 1 only   WriteOnly   Select edge-detection polarity
+  REG_ENABx         Pg. 2 only   WriteOnly   Enable/Disable edge-detect. int.
+  REG_INT_IDx       Pg. 3 only   R/W         See which ports/bits have ints.
+ */
+#define REG_PORT0 0x0
+#define REG_PORT1 0x1
+#define REG_PORT2 0x2
+#define REG_PORT3 0x3
+#define REG_PORT4 0x4
+#define REG_PORT5 0x5
+#define REG_INT_PENDING 0x6
+#define REG_PAGELOCK 0x7 /* page selector register, upper 2 bits select a page
+                            and bits 0-5 are used to 'lock down' a particular
+                            port above to make it readonly.  */
+#define REG_POL0 0x8 
+#define REG_POL1 0x9
+#define REG_POL2 0xA
+#define REG_ENAB0 0x8 
+#define REG_ENAB1 0x9
+#define REG_ENAB2 0xA
+#define REG_INT_ID0 0x8
+#define REG_INT_ID1 0x9
+#define REG_INT_ID2 0xA
+
+#define NUM_PAGED_REGS 3
+#define NUM_PAGES 4
+#define FIRST_PAGED_REG 0x8
+#define REG_PAGE_BITOFFSET 6
+#define REG_LOCK_BITOFFSET 0
+#define REG_PAGE_MASK (~((0x1<<REG_PAGE_BITOFFSET)-1))
+#define REG_LOCK_MASK ~(REG_PAGE_MASK)
+#define PAGE_POL 1
+#define PAGE_ENAB 2
+#define PAGE_INT_ID 3
+
+typedef int (*comedi_insn_fn_t)(comedi_device *,comedi_subdevice *,comedi_insn *,lsampl_t *);
+
+static int ai_rinsn(comedi_device *, comedi_subdevice *, comedi_insn *, lsampl_t *);
+static int ao_rinsn(comedi_device *, comedi_subdevice *, comedi_insn *, lsampl_t *);
+static int ao_winsn(comedi_device *, comedi_subdevice *, comedi_insn *, lsampl_t *);
+
+/*
+ * Board descriptions for two imaginary boards.  Describing the
+ * boards in this way is optional, and completely driver-dependent.
+ * Some drivers use arrays such as this, other do not.
+ */
+typedef struct pcmmio_board_struct
+{
+       char * const name;
+       const int dio_num_asics;
+       const int dio_num_ports;
+    const int total_iosize;
+    const int ai_bits;
+    const int ao_bits;
+    const int n_ai_chans;
+    const int n_ao_chans;
+    comedi_lrange *ai_range_table, *ao_range_table;
+    comedi_insn_fn_t ai_rinsn, ao_rinsn, ao_winsn;
+} pcmmio_board;
+
+static struct {
+  int length;
+  comedi_krange ranges[4];
+} ranges_ai = { 4, { RANGE(-5.,5.), RANGE(-10.,10.), RANGE(0.,5.), RANGE(0.,10.) } };
+
+static struct {
+  int length;
+  comedi_krange ranges[6];
+} ranges_ao = { 6, { RANGE(0.,5.), RANGE(0.,10.) , RANGE(-5.,5.), RANGE(-10.,10.), RANGE(-2.5, 2.5), RANGE(-2.5, 7.5) } };
+
+static pcmmio_board pcmmio_boards[] = 
+{
+       {
+               name:              "pcmmio",
+               dio_num_asics:     1,
+               dio_num_ports:     6,
+        total_iosize:      32, 
+        ai_bits:           16,
+        ao_bits:           16,
+        n_ai_chans:        16,
+        n_ao_chans:        8,
+        ai_range_table:    (comedi_lrange *)&ranges_ai,
+        ao_range_table:    (comedi_lrange *)&ranges_ao,
+        ai_rinsn:          ai_rinsn,
+        ao_rinsn:          ao_rinsn,
+        ao_winsn:          ao_winsn
+       },
+};
+
+/*
+ * Useful for shorthand access to the particular board structure
+ */
+#define thisboard ((pcmmio_board *)dev->board_ptr)
+
+/* this structure is for data unique to this subdevice.  */
+typedef struct 
+{
+  
+  union {
+    /* for DIO: mapping of halfwords (bytes) in port/chanarray to iobase */
+    unsigned long iobases[PORTS_PER_SUBDEV];
+    
+    /* for AI/AO */
+    unsigned long iobase;
+  };
+  union {
+    struct {
+      
+      /* The below is only used for intr subdevices */
+      struct {
+        int asic; /* if non-negative, this subdev has an interrupt asic */
+        int first_chan; /* if nonnegative, the first channel id for 
+                           interrupts. */
+        int num_asic_chans; /* the number of asic channels in this subdev
+                               that have interrutps */
+        int asic_chan; /* if nonnegative, the first channel id with
+                          respect to the asic that has interrupts */
+        int enabled_mask; /* subdev-relative channel mask for channels
+                             we are interested in */
+        int active;
+        int stop_count;
+        int continuous;
+        spinlock_t spinlock;
+      } intr;
+    } dio;
+    struct {
+      lsampl_t shadow_samples[8]; /* the last lsampl_t data written */
+    } ao;
+  };
+} pcmmio_subdev_private;
+
+/* this structure is for data unique to this hardware driver.  If
+   several hardware drivers keep similar information in this structure,
+   feel free to suggest moving the variable to the comedi_device struct.  */
+typedef struct
+{  
+  /* stuff for DIO */
+  struct 
+  {
+    unsigned char pagelock; /* current page and lock*/
+    unsigned char pol [NUM_PAGED_REGS]; /* shadow of POLx registers */
+    unsigned char enab[NUM_PAGED_REGS]; /* shadow of ENABx registers */
+    int num;
+    unsigned long iobase;
+    unsigned int irq;
+    spinlock_t spinlock;
+  } asics[MAX_ASICS];  
+  pcmmio_subdev_private *sprivs;
+} pcmmio_private;
+
+/*
+ * most drivers define the following macro to make it easy to
+ * access the private structure.
+ */
+#define devpriv ((pcmmio_private *)dev->private)
+#define subpriv ((pcmmio_subdev_private *)s->private)
+/*
+ * The comedi_driver structure tells the Comedi core module
+ * which functions to call to configure/deconfigure (attach/detach)
+ * the board, and also about the kernel module that contains
+ * the device code.
+ */
+static int pcmmio_attach(comedi_device *dev, comedi_devconfig *it);
+static int pcmmio_detach(comedi_device *dev);
+
+static comedi_driver driver = 
+{
+       driver_name:    "pcmmio",
+       module:         THIS_MODULE,
+       attach:         pcmmio_attach,
+       detach:         pcmmio_detach,
+/* It is not necessary to implement the following members if you are
+ * writing a driver for a ISA PnP or PCI card */
+       /* Most drivers will support multiple types of boards by
+        * having an array of board structures.  These were defined
+        * in pcmmio_boards[] above.  Note that the element 'name'
+        * was first in the structure -- Comedi uses this fact to
+        * extract the name of the board without knowing any details
+        * about the structure except for its length.
+        * When a device is attached (by comedi_config), the name
+        * of the device is given to Comedi, and Comedi tries to
+        * match it by going through the list of board names.  If
+        * there is a match, the address of the pointer is put
+        * into dev->board_ptr and driver->attach() is called.
+        *
+        * Note that these are not necessary if you can determine
+        * the type of board in software.  ISA PnP, PCI, and PCMCIA
+        * devices are such boards.
+        */
+       board_name:     (const char **)pcmmio_boards,
+       offset:         sizeof(pcmmio_board),
+       num_names:      sizeof(pcmmio_boards) / sizeof(pcmmio_board),
+};
+
+static int pcmmio_dio_insn_bits(comedi_device *dev,comedi_subdevice *s,
+                                comedi_insn *insn,lsampl_t *data);
+static int pcmmio_dio_insn_config(comedi_device *dev,comedi_subdevice *s,
+                                  comedi_insn *insn,lsampl_t *data);
+
+static irqreturn_t interrupt_pcmmio(int irq, void *d, struct pt_regs *regs);
+static void pcmmio_stop_intr(comedi_device *, comedi_subdevice *);
+static int  pcmmio_cancel(comedi_device *dev, comedi_subdevice *s);
+static int pcmmio_cmd(comedi_device *dev, comedi_subdevice *s);
+static int pcmmio_cmdtest(comedi_device *dev, comedi_subdevice *s, comedi_cmd *cmd);
+
+/* some helper functions to deal with specifics of this device's registers */
+static void init_asics(comedi_device *dev); /* sets up/clears ASIC chips to defaults */
+static void switch_page(comedi_device *dev, int asic, int page);
+static void lock_port(comedi_device *dev, int asic, int port);
+static void unlock_port(comedi_device *dev, int asic, int port);
+
+/*
+ * Attach is called by the Comedi core to configure the driver
+ * for a particular board.  If you specified a board_name array
+ * in the driver structure, dev->board_ptr contains that
+ * address.
+ */
+static int pcmmio_attach(comedi_device *dev, comedi_devconfig *it)
+{
+       comedi_subdevice *s;
+    int sdev_no, chans_left, n_dio_subdevs, n_subdevs, port, asic, thisasic_chanct = 0;
+       unsigned long iobase;
+       unsigned int irq[MAX_ASICS];
+
+    iobase = it->options[0];
+    irq[0] = it->options[1];
+
+    printk("comedi%d: %s: io: %lx ", dev->minor, driver.driver_name, iobase);
+       
+    dev->iobase = iobase;
+    
+    if ( !iobase || !request_region(iobase, 
+                                    thisboard->total_iosize, 
+                                    driver.driver_name) ) {
+      printk("I/O port conflict\n");
+      return -EIO;
+    }
+        
+/*
+ * Initialize dev->board_name.  Note that we can use the "thisboard"
+ * macro now, since we just initialized it in the last line.
+ */
+    dev->board_name = thisboard->name;
+
+/*
+ * Allocate the private structure area.  alloc_private() is a
+ * convenient macro defined in comedidev.h.
+ */
+    if (alloc_private(dev,sizeof(pcmmio_private)) < 0) {
+      printk("cannot allocate private data structure\n");
+      return -ENOMEM;
+    }
+
+    for (asic = 0; asic < MAX_ASICS; ++asic) {
+      devpriv->asics[asic].num = asic;
+      devpriv->asics[asic].iobase = dev->iobase + 16 + asic*ASIC_IOSIZE;
+      devpriv->asics[asic].irq = 0; /* this gets actually set at the end of 
+                                       this function when we 
+                                       comedi_request_irqs */
+      spin_lock_init(&devpriv->asics[asic].spinlock);
+    }
+    
+    chans_left = CHANS_PER_ASIC * thisboard->dio_num_asics;    
+    n_dio_subdevs = CALC_N_DIO_SUBDEVS(chans_left);
+    n_subdevs = n_dio_subdevs + 2;
+    devpriv->sprivs = (pcmmio_subdev_private *)kmalloc(sizeof(pcmmio_subdev_private) * n_subdevs, GFP_KERNEL);
+    if (!devpriv->sprivs) {
+        printk("cannot allocate subdevice private data structures\n");
+        return -ENOMEM;
+    }
+    memset(devpriv->sprivs, 0, sizeof(pcmmio_subdev_private) * n_subdevs);
+      /*
+       * Allocate the subdevice structures.  alloc_subdevice() is a
+       * convenient macro defined in comedidev.h.
+       *
+       * Allocate 1 AI + 1 AO + 2 DIO subdevs (24 lines per DIO)
+       */
+    if ( alloc_subdevices(dev, n_subdevs ) < 0 ) {
+        printk("cannot allocate subdevice data structures\n");
+        return -ENOMEM;
+    }
+
+    memset(dev->subdevices, 0, sizeof(*dev->subdevices) * n_subdevs);
+    
+    /* First, AI */
+    sdev_no = 0;
+    s = dev->subdevices + sdev_no;
+    s->private = devpriv->sprivs + sdev_no;
+    s->maxdata = (1<<thisboard->ai_bits)-1;
+    s->range_table = thisboard->ai_range_table;
+    s->subdev_flags = SDF_READABLE|SDF_GROUND|SDF_DIFF;
+    s->type = COMEDI_SUBD_AI;
+    s->n_chan = thisboard->n_ai_chans;
+    s->len_chanlist = s->n_chan;
+    s->insn_read = thisboard->ai_rinsn;
+    subpriv->iobase = dev->iobase+0;
+    /* initialize the resource enable register by clearing it */
+    outb(0, subpriv->iobase + 3);
+    outb(0, subpriv->iobase+4 + 3);
+
+    /* Next, AO */
+    ++sdev_no;
+    s = dev->subdevices + sdev_no;
+    s->private = devpriv->sprivs + sdev_no;
+    s->maxdata = (1<<thisboard->ao_bits)-1;
+    s->range_table = thisboard->ao_range_table;
+    s->subdev_flags = SDF_READABLE;
+    s->type = COMEDI_SUBD_AO;
+    s->n_chan = thisboard->n_ao_chans;
+    s->len_chanlist = s->n_chan;
+    s->insn_read = thisboard->ao_rinsn;
+    s->insn_write = thisboard->ao_winsn;
+    subpriv->iobase = dev->iobase+8;
+    /* initialize the resource enable register by clearing it */
+    outb(0, subpriv->iobase + 3);
+    outb(0, subpriv->iobase+4 + 3);
+
+    ++sdev_no;    
+    port = 0;
+    asic = 0;    
+    for (; sdev_no < (int)dev->n_subdevices; ++sdev_no)
+    {      
+      int byte_no;
+
+      s = dev->subdevices + sdev_no;
+      s->private = devpriv->sprivs + sdev_no;
+      s->maxdata = 1;
+      s->range_table = &range_digital;      
+      s->subdev_flags = SDF_READABLE|SDF_WRITABLE;
+      s->type = COMEDI_SUBD_DIO;
+      s->insn_bits = pcmmio_dio_insn_bits;
+      s->insn_config = pcmmio_dio_insn_config;
+      s->n_chan = MIN(chans_left, MAX_CHANS_PER_SUBDEV);
+      subpriv->dio.intr.asic = -1;
+      subpriv->dio.intr.first_chan = -1;
+      subpriv->dio.intr.asic_chan = -1;
+      subpriv->dio.intr.num_asic_chans = -1;
+      subpriv->dio.intr.active = 0;
+      s->len_chanlist = 1; 
+
+      /* save the ioport address for each 'port' of 8 channels in the 
+         subdevice */         
+      for (byte_no = 0; byte_no < PORTS_PER_SUBDEV; ++byte_no, ++port) {
+          if (port >= PORTS_PER_ASIC) {
+            port = 0;
+            ++asic;
+            thisasic_chanct = 0;
+          }
+          subpriv->iobases[byte_no] = devpriv->asics[asic].iobase + port;
+
+          if (thisasic_chanct < CHANS_PER_PORT*INTR_PORTS_PER_ASIC
+              && subpriv->dio.intr.asic < 0) {
+            /* this is an interrupt subdevice, so setup the struct */
+            subpriv->dio.intr.asic = asic;
+            subpriv->dio.intr.active = 0;
+            subpriv->dio.intr.stop_count = 0;
+            subpriv->dio.intr.first_chan = byte_no * 8;
+            subpriv->dio.intr.asic_chan = thisasic_chanct;
+            subpriv->dio.intr.num_asic_chans = s->n_chan - subpriv->dio.intr.first_chan;
+            s->cancel = pcmmio_cancel;
+            s->do_cmd = pcmmio_cmd;
+            s->do_cmdtest = pcmmio_cmdtest;
+            s->len_chanlist = subpriv->dio.intr.num_asic_chans; 
+          }
+          thisasic_chanct += CHANS_PER_PORT;
+      }
+      spin_lock_init(&subpriv->dio.intr.spinlock);
+
+      chans_left -= s->n_chan;
+
+      if (!chans_left) {
+        asic = 0; /* reset the asic to our first asic, to do intr subdevs */
+        port = 0;
+      }
+      
+    }
+
+    init_asics(dev); /* clear out all the registers, basically */
+
+    for (asic = 0; irq[0] && asic < MAX_ASICS; ++asic) {
+      if (irq[asic] && comedi_request_irq(irq[asic], interrupt_pcmmio, SA_SHIRQ, thisboard->name, dev)) {
+        int i;
+        /* unroll the allocated irqs.. */
+        for (i = asic-1; i >= 0; --i) {
+          comedi_free_irq(irq[i], dev);          
+          devpriv->asics[i].irq = irq[i] = 0;
+        }
+        irq[asic] = 0;
+      }
+      devpriv->asics[asic].irq = irq[asic];
+    }
+    
+    dev->irq = irq[0]; /* grr.. wish comedi dev struct supported multiple 
+                          irqs.. */
+
+    if (irq[0]) {
+      printk("irq: %u ", irq[0]);
+      if (irq[1] && thisboard->dio_num_asics == 2) 
+        printk("second ASIC irq: %u ", irq[1]);
+    } else {
+      printk("(IRQ mode disabled) ");
+    }
+
+    printk("attached\n");
+    
+    return 1;
+}
+
+
+/*
+ * _detach is called to deconfigure a device.  It should deallocate
+ * resources.  
+ * This function is also called when _attach() fails, so it should be
+ * careful not to release resources that were not necessarily
+ * allocated by _attach().  dev->private and dev->subdevices are
+ * deallocated automatically by the core.
+ */
+static int pcmmio_detach(comedi_device *dev)
+{
+    int i;
+
+    printk("comedi%d: %s: remove\n", dev->minor, driver.driver_name);
+    if (dev->iobase)
+      release_region(dev->iobase, thisboard->total_iosize);
+
+    for (i = 0; i < MAX_ASICS; ++i) {
+      if (devpriv && devpriv->asics[i].irq) 
+        comedi_free_irq(devpriv->asics[i].irq, dev);
+    }
+    
+    if (devpriv && devpriv->sprivs)
+      kfree(devpriv->sprivs);
+
+    return 0;
+}
+
+
+/* DIO devices are slightly special.  Although it is possible to
+ * implement the insn_read/insn_write interface, it is much more
+ * useful to applications if you implement the insn_bits interface.
+ * This allows packed reading/writing of the DIO channels.  The
+ * comedi core can convert between insn_bits and insn_read/write */
+static int pcmmio_dio_insn_bits(comedi_device *dev, comedi_subdevice *s,
+                                comedi_insn *insn, lsampl_t *data)
+{
+    int byte_no;
+    if (insn->n != 2) return -EINVAL;
+
+     /* NOTE:
+        reading a 0 means this channel was high 
+        writine a 0 sets the channel high
+        reading a 1 means this channel was low 
+        writing a 1 means set this channel low
+
+        Therefore everything is always inverted. */
+
+       /* The insn data is a mask in data[0] and the new data
+        * in data[1], each channel cooresponding to a bit. */
+
+#ifdef DAMMIT_ITS_BROKEN
+        /* DEBUG */
+    printk("write mask: %08x  data: %08x\n", data[0], data[1]);
+#endif
+
+    s->state = 0;
+
+    for (byte_no = 0; byte_no < s->n_chan/CHANS_PER_PORT; ++byte_no) {
+           /* address of 8-bit port */
+        unsigned long ioaddr = subpriv->iobases[byte_no], 
+           /* bit offset of port in 32-bit doubleword */
+            offset = byte_no * 8; 
+                      /* this 8-bit port's data */
+        unsigned char byte = 0,   
+                      /* The write mask for this port (if any) */
+                      write_mask_byte = (data[0] >> offset) & 0xff, 
+                      /* The data byte for this port */
+                      data_byte = (data[1] >> offset) & 0xff;
+
+        byte = inb(ioaddr); /* read all 8-bits for this port */
+
+#ifdef DAMMIT_ITS_BROKEN
+        /* DEBUG */
+        printk("byte %d wmb %02x db %02x offset %02d io %04x, data_in %02x ", byte_no, (unsigned)write_mask_byte, (unsigned)data_byte, offset, ioaddr, (unsigned)byte);
+#endif
+
+        if ( write_mask_byte ) { 
+          /* this byte has some write_bits -- so set the output lines */
+          byte &= ~write_mask_byte; /* clear bits for write mask */
+          byte |= ~data_byte & write_mask_byte; /* set to inverted data_byte */
+          /* Write out the new digital output state */
+          outb(byte, ioaddr);
+        }
+
+#ifdef DAMMIT_ITS_BROKEN
+        /* DEBUG */
+        printk("data_out_byte %02x\n", (unsigned)byte);
+#endif
+        /* save the digital input lines for this byte.. */
+        s->state |= ((unsigned int)byte) << offset; 
+    }
+
+    /* now return the DIO lines to data[1] - note they came inverted! */
+    data[1] = ~s->state; 
+
+#ifdef DAMMIT_ITS_BROKEN
+    /* DEBUG */
+    printk("s->state %08x data_out %08x\n", s->state, data[1]);
+#endif
+    
+    return 2;
+}
+
+/* The input or output configuration of each digital line is
+ * configured by a special insn_config instruction.  chanspec
+ * contains the channel to be changed, and data[0] contains the
+ * value COMEDI_INPUT or COMEDI_OUTPUT. */
+static int pcmmio_dio_insn_config(comedi_device *dev, comedi_subdevice *s,
+                                  comedi_insn *insn, lsampl_t *data)
+{
+    int chan = CR_CHAN(insn->chanspec), byte_no = chan/8, bit_no = chan % 8;
+    unsigned long ioaddr;
+    unsigned char byte;
+
+    /* Compute ioaddr for this channel */
+    ioaddr = subpriv->iobases[byte_no];
+    
+     /* NOTE:
+        writing a 0 an IO channel's bit sets the channel to INPUT 
+        and pulls the line high as well
+        
+        writing a 1 to an IO channel's  bit pulls the line low
+
+        All channels are implicitly always in OUTPUT mode -- but when 
+        they are high they can be considered to be in INPUT mode..
+
+        Thus, we only force channels low if the config request was INPUT, 
+        otherwise we do nothing to the hardware.    */
+
+       switch(data[0])
+       {
+       case INSN_CONFIG_DIO_OUTPUT:
+          /* save to io_bits -- don't actually do anything since 
+             all input channels are also output channels... */
+          s->io_bits |= 1<<chan;
+          break;
+       case INSN_CONFIG_DIO_INPUT:
+          /* write a 0 to the actual register representing the channel
+             to set it to 'input'.  0 means "float high". */
+          byte = inb(ioaddr);
+          byte &= ~(1<<bit_no); /**< set input channel to '0' */
+
+          /* write out byte -- this is the only time we actually affect the 
+             hardware as all channels are implicitly output -- but input 
+             channels are set to float-high */
+          outb(byte, ioaddr);
+          
+          /* save to io_bits */
+          s->io_bits &= ~(1<<chan);
+          break;
+
+       case INSN_CONFIG_DIO_QUERY:
+          /* retreive from shadow register */
+          data[1] = (s->io_bits & (1 << chan)) ? COMEDI_OUTPUT : COMEDI_INPUT;
+          return insn->n;
+          break;
+
+       default:
+          return -EINVAL;
+          break;
+       }    
+
+       return insn->n;    
+}
+
+static void init_asics(comedi_device *dev) /* sets up an 
+                                              ASIC chip to defaults */
+{
+  int asic;
+  
+  for (asic = 0; asic < thisboard->dio_num_asics; ++asic)
+  {
+    int port, page;
+    unsigned long baseaddr = devpriv->asics[asic].iobase;
+
+    switch_page(dev, asic,  0); /* switch back to page 0 */
+
+    /* first, clear all the DIO port bits */
+    for (port = 0; port < PORTS_PER_ASIC; ++port) 
+      outb(0, baseaddr + REG_PORT0 + port);
+
+    /* Next, clear all the paged registers for each page */
+    for (page = 1; page < NUM_PAGES; ++page)
+    {
+      int reg;
+      /* now clear all the paged registers*/
+      switch_page(dev, asic, page);
+      for (reg = FIRST_PAGED_REG; reg < FIRST_PAGED_REG+NUM_PAGED_REGS; ++reg)
+        outb(0, baseaddr + reg);
+    }
+
+    /* DEBUG  set rising edge interrupts on port0 of both asics*/
+    /*switch_page(dev, asic, PAGE_POL);
+      outb(0xff, baseaddr + REG_POL0);
+      switch_page(dev, asic, PAGE_ENAB);
+      outb(0xff, baseaddr + REG_ENAB0);*/
+    /* END DEBUG */
+
+    switch_page(dev, asic,  0); /* switch back to default page 0 */
+    
+  }
+}
+
+
+static void switch_page(comedi_device *dev, int asic, int page)
+{
+  if (asic < 0 || asic >= thisboard->dio_num_asics) return; /* paranoia */
+  if (page < 0 || page >= NUM_PAGES) return; /* more paranoia */
+
+  devpriv->asics[asic].pagelock &= ~REG_PAGE_MASK;
+  devpriv->asics[asic].pagelock |= page<<REG_PAGE_BITOFFSET;
+
+  /* now write out the shadow register */
+  outb(devpriv->asics[asic].pagelock, 
+       devpriv->asics[asic].iobase + REG_PAGELOCK);
+}
+
+static void lock_port(comedi_device *dev, int asic, int port)
+{
+  if (asic < 0 || asic >= thisboard->dio_num_asics) return; /* paranoia */
+  if (port < 0 || port >= PORTS_PER_ASIC) return; /* more paranoia */
+
+  devpriv->asics[asic].pagelock |= 0x1<<port;  
+  /* now write out the shadow register */
+  outb(devpriv->asics[asic].pagelock, devpriv->asics[asic].iobase + REG_PAGELOCK);
+  return;
+  (void)lock_port(dev, asic, port); /* not reached, suppress compiler warnings*/
+}
+
+static void unlock_port(comedi_device *dev, int asic, int port)
+{
+  if (asic < 0 || asic >= thisboard->dio_num_asics) return; /* paranoia */
+  if (port < 0 || port >= PORTS_PER_ASIC) return; /* more paranoia */
+  devpriv->asics[asic].pagelock &= ~(0x1<<port) | REG_LOCK_MASK;  
+  /* now write out the shadow register */
+  outb(devpriv->asics[asic].pagelock, devpriv->asics[asic].iobase + REG_PAGELOCK);
+  (void)unlock_port(dev, asic, port); /* not reached, suppress compiler warnings*/
+}
+
+static irqreturn_t interrupt_pcmmio(int irq, void *d, struct pt_regs *regs)
+{
+  int asic, got1 = 0;
+  comedi_device *dev = (comedi_device *)d;
+  
+  for (asic = 0; asic < MAX_ASICS; ++asic) {
+    if (irq == devpriv->asics[asic].irq) {
+      unsigned long flags;    
+      unsigned triggered = 0;
+      unsigned long iobase = devpriv->asics[asic].iobase;
+      /* it is an interrupt for ASIC #asic */
+      unsigned char int_pend;
+
+      comedi_spin_lock_irqsave(&devpriv->asics[asic].spinlock, flags);
+
+      int_pend = inb(iobase + REG_INT_PENDING) & 0x07;
+
+      if (int_pend) {
+        int port;
+        for (port = 0; port < INTR_PORTS_PER_ASIC; ++port) {
+          if (int_pend & (0x1<<port)) {
+            unsigned char io_lines_with_edges = 0;
+            switch_page(dev, asic, PAGE_INT_ID);
+            io_lines_with_edges = inb(iobase + REG_INT_ID0 + port);
+           
+            if (io_lines_with_edges) 
+              /* clear pending interrupt */
+              outb(0, iobase + REG_INT_ID0 + port);
+
+            triggered |= io_lines_with_edges << port*8;
+          }
+        }
+
+        ++got1;
+      }
+
+      comedi_spin_unlock_irqrestore(&devpriv->asics[asic].spinlock, flags);
+
+      if (triggered) {        
+        comedi_subdevice *s;
+        /* TODO here: dispatch io lines to subdevs with commands.. */
+        printk("PCMMIO DEBUG: got edge detect interrupt %d asic %d which_chans: %06x\n", irq, asic, triggered);
+        for (s = dev->subdevices+2; s < dev->subdevices + dev->n_subdevices; ++s) {
+          if (subpriv->dio.intr.asic == asic) { /* this is an interrupt subdev, and it matches this asic! */
+            unsigned long flags;
+            unsigned oldevents;
+            
+            comedi_spin_lock_irqsave(&subpriv->dio.intr.spinlock, flags);
+            
+            oldevents = s->async->events;
+
+            if (subpriv->dio.intr.active) {
+              unsigned mytrig = ((triggered >> subpriv->dio.intr.asic_chan) & ((0x1<<subpriv->dio.intr.num_asic_chans)-1)) << subpriv->dio.intr.first_chan;
+              if (mytrig & subpriv->dio.intr.enabled_mask) {
+                lsampl_t val = 0;
+                unsigned int n, ch, len;
+                
+                len = s->async->cmd.chanlist_len;
+                for (n = 0; n < len; n++) {
+                  ch = CR_CHAN(s->async->cmd.chanlist[n]);
+                  if (mytrig & (1U << ch)) {
+                    val |= (1U << n);
+                  }
+                }
+                /* Write the scan to the buffer. */
+                if (comedi_buf_put(s->async, ((sampl_t *)&val)[0])
+                    && comedi_buf_put(s->async, ((sampl_t *)&val)[1]) ) {
+                  s->async->events |= (COMEDI_CB_BLOCK | COMEDI_CB_EOS);
+                } else {
+                  /* Overflow! Stop acquisition!! */
+                  /* TODO: STOP_ACQUISITION_CALL_HERE!! */
+                  pcmmio_stop_intr(dev, s); 
+                }
+                
+                /* Check for end of acquisition. */
+                if (!subpriv->dio.intr.continuous) {
+                  /* stop_src == TRIG_COUNT */
+                  if (subpriv->dio.intr.stop_count > 0) {
+                    subpriv->dio.intr.stop_count--;
+                    if (subpriv->dio.intr.stop_count == 0) {
+                      s->async->events |= COMEDI_CB_EOA;
+                      /* TODO: STOP_ACQUISITION_CALL_HERE!! */
+                      pcmmio_stop_intr(dev, s); 
+                    }
+                  }
+                }
+              }
+            }
+
+            comedi_spin_unlock_irqrestore(&subpriv->dio.intr.spinlock, flags);
+            
+            if (oldevents != s->async->events) {
+              comedi_event(dev, s, s->async->events);
+            }
+
+          }
+          
+        }
+      }
+
+    }
+  }
+  if (!got1) return IRQ_NONE; /* interrupt from other source */
+  return IRQ_HANDLED;
+}
+
+
+static void pcmmio_stop_intr(comedi_device *dev, comedi_subdevice *s)
+{
+  int nports, firstport, asic, port;
+  
+  if ( (asic = subpriv->dio.intr.asic) < 0 ) return; /* not an interrupt subdev */
+  
+  subpriv->dio.intr.enabled_mask = 0;
+  subpriv->dio.intr.active = 0;
+  s->async->inttrig = 0;
+  nports = subpriv->dio.intr.num_asic_chans / CHANS_PER_PORT;
+  firstport = subpriv->dio.intr.asic_chan / CHANS_PER_PORT;
+  switch_page(dev, asic, PAGE_ENAB);
+  for (port = firstport; port < firstport+nports; ++port) {
+    /* disable all intrs for this subdev.. */
+    outb(0, devpriv->asics[asic].iobase + REG_ENAB0 + port);
+  }
+}
+
+static int pcmmio_start_intr(comedi_device *dev, comedi_subdevice *s)
+{
+  if (!subpriv->dio.intr.continuous && subpriv->dio.intr.stop_count == 0) {
+    /* An empty acquisition! */
+    s->async->events |= COMEDI_CB_EOA;
+    subpriv->dio.intr.active = 0;
+    return 1;
+  } else {
+    unsigned bits = 0, pol_bits = 0, n;
+    int nports, firstport, asic, port;
+    comedi_cmd *cmd = &s->async->cmd;  
+    
+    if ( (asic = subpriv->dio.intr.asic) < 0 ) return 1; /* not an interrupt 
+                                                        subdev */
+    subpriv->dio.intr.enabled_mask = 0;
+    subpriv->dio.intr.active = 1;
+    nports = subpriv->dio.intr.num_asic_chans / CHANS_PER_PORT;
+    firstport = subpriv->dio.intr.asic_chan / CHANS_PER_PORT;
+    if (cmd->chanlist) {
+      for (n = 0; n < cmd->chanlist_len; n++) {
+        bits |= (1U << CR_CHAN(cmd->chanlist[n]));
+        pol_bits |= 
+          (CR_AREF(cmd->chanlist[n]) || CR_RANGE(cmd->chanlist[n]) ? 1U : 0U)
+            << CR_CHAN(cmd->chanlist[n]);
+      }
+    }
+    bits &=  ((0x1<<subpriv->dio.intr.num_asic_chans)-1) << subpriv->dio.intr.first_chan;
+    subpriv->dio.intr.enabled_mask = bits;
+
+    { /* the below code configures the board to use a specific IRQ from 0-15. */
+      unsigned char b;
+      /* set resource enable register to enable IRQ operation */
+      outb(1<<4, dev->iobase+3);
+      /* set bits 0-3 of b to the irq number from 0-15 */
+      b = dev->irq & ((1<<4)-1); 
+      outb(b, dev->iobase+2);
+      /* done, we told the board what irq to use */
+    }
+
+    switch_page(dev, asic, PAGE_ENAB);
+    for (port = firstport; port < firstport+nports; ++port) {
+      unsigned enab = bits >> (subpriv->dio.intr.first_chan + (port-firstport)*8) & 0xff, 
+               pol = pol_bits >> (subpriv->dio.intr.first_chan + (port-firstport)*8) & 0xff ;
+      /* set enab intrs for this subdev.. */
+      outb(enab, devpriv->asics[asic].iobase + REG_ENAB0 + port);
+      switch_page(dev, asic, PAGE_POL);
+      outb(pol, devpriv->asics[asic].iobase + REG_ENAB0 + port);
+    }
+  }
+  return 0;
+}
+
+static int  pcmmio_cancel(comedi_device *dev, comedi_subdevice *s)
+{
+  unsigned long flags;
+
+  comedi_spin_lock_irqsave(&subpriv->dio.intr.spinlock, flags);
+  if (subpriv->dio.intr.active)  pcmmio_stop_intr(dev, s);
+  comedi_spin_unlock_irqrestore(&subpriv->dio.intr.spinlock, flags);
+
+  return 0;  
+}
+
+/*
+ * Internal trigger function to start acquisition for an 'INTERRUPT' subdevice.
+ */
+static int
+pcmmio_inttrig_start_intr(comedi_device *dev, comedi_subdevice *s, unsigned int trignum)
+{
+       unsigned long flags;
+       int event = 0;
+
+       if (trignum != 0) return -EINVAL;
+
+       comedi_spin_lock_irqsave(&subpriv->dio.intr.spinlock, flags);
+       s->async->inttrig = 0;
+       if (subpriv->dio.intr.active) {
+               event = pcmmio_start_intr(dev, s);
+       }
+       comedi_spin_unlock_irqrestore(&subpriv->dio.intr.spinlock, flags);
+
+       if (event) {
+               comedi_event(dev, s, s->async->events);
+       }
+
+       return 1;
+}
+
+
+/*
+ * 'do_cmd' function for an 'INTERRUPT' subdevice.
+ */
+static int
+pcmmio_cmd(comedi_device *dev, comedi_subdevice *s)
+{
+       comedi_cmd *cmd = &s->async->cmd;
+       unsigned long flags;
+       int event = 0;
+
+       comedi_spin_lock_irqsave(&subpriv->dio.intr.spinlock, flags);
+       subpriv->dio.intr.active = 1;
+
+       /* Set up end of acquisition. */
+       switch (cmd->stop_src) {
+       case TRIG_COUNT:
+               subpriv->dio.intr.continuous = 0;
+               subpriv->dio.intr.stop_count = cmd->stop_arg;
+               break;
+       default:
+               /* TRIG_NONE */
+               subpriv->dio.intr.continuous = 1;
+               subpriv->dio.intr.stop_count = 0;
+               break;
+       }
+
+       /* Set up start of acquisition. */
+       switch (cmd->start_src) {
+       case TRIG_INT:
+               s->async->inttrig = pcmmio_inttrig_start_intr;
+               break;
+       default:
+               /* TRIG_NOW */
+               event = pcmmio_start_intr(dev, s);
+               break;
+       }
+       comedi_spin_unlock_irqrestore(&subpriv->dio.intr.spinlock, flags);
+
+       if (event) {
+               comedi_event(dev, s, s->async->events);
+       }
+
+       return 0;
+}
+
+/*
+ * 'do_cmdtest' function for an 'INTERRUPT' subdevice.
+ */
+static int
+pcmmio_cmdtest(comedi_device *dev, comedi_subdevice *s,
+               comedi_cmd *cmd)
+{
+       int err = 0;
+       unsigned int tmp;
+
+       /* step 1: make sure trigger sources are trivially valid */
+
+       tmp = cmd->start_src;
+       cmd->start_src &= (TRIG_NOW | TRIG_INT);
+       if (!cmd->start_src || tmp != cmd->start_src) err++;
+
+       tmp = cmd->scan_begin_src;
+       cmd->scan_begin_src &= TRIG_EXT;
+       if (!cmd->scan_begin_src || tmp != cmd->scan_begin_src) err++;
+
+       tmp = cmd->convert_src;
+       cmd->convert_src &= TRIG_NOW;
+       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 */
+
+       /* these tests are true if more than one _src bit is set */
+       if ((cmd->start_src & (cmd->start_src - 1)) != 0) err++;
+       if ((cmd->scan_begin_src & (cmd->scan_begin_src - 1)) != 0) err++;
+       if ((cmd->convert_src & (cmd->convert_src - 1)) != 0) err++;
+       if ((cmd->scan_end_src & (cmd->scan_end_src - 1)) != 0) err++;
+       if ((cmd->stop_src & (cmd->stop_src - 1)) != 0) err++;
+
+       if (err) return 2;
+
+       /* step 3: make sure arguments are trivially compatible */
+
+       /* cmd->start_src == TRIG_NOW || cmd->start_src == TRIG_INT */
+       if (cmd->start_arg != 0) {
+               cmd->start_arg = 0;
+               err++;
+       }
+
+       /* cmd->scan_begin_src == TRIG_EXT */
+       if (cmd->scan_begin_arg != 0) {
+               cmd->scan_begin_arg = 0;
+               err++;
+       }
+
+       /* cmd->convert_src == TRIG_NOW */
+       if (cmd->convert_arg != 0) {
+               cmd->convert_arg = 0;
+               err++;
+       }
+
+       /* cmd->scan_end_src == TRIG_COUNT */
+       if (cmd->scan_end_arg != cmd->chanlist_len) {
+               cmd->scan_end_arg = cmd->chanlist_len;
+               err++;
+       }
+
+       switch (cmd->stop_src) {
+       case TRIG_COUNT:
+               /* any count allowed */
+               break;
+       case TRIG_NONE:
+               if (cmd->stop_arg != 0) {
+                  cmd->stop_arg = 0;
+                  err++;
+               }
+               break;
+       default:
+               break;
+       }
+
+       if (err) return 3;
+
+       /* step 4: fix up any arguments */
+
+       /* if (err) return 4; */
+
+       return 0;
+}
+
+static int adc_wait_ready(unsigned long iobase)
+{
+  unsigned long retry = 100000;
+  while (retry--)
+    if (inb(iobase + 3) & 0x80) return 0;
+  return 1;
+}
+
+/* All this is for AI and AO */
+static int ai_rinsn(comedi_device *dev, comedi_subdevice *s, comedi_insn *insn, lsampl_t *data)
+{
+  int n;
+  unsigned long iobase = subpriv->iobase;
+  
+  /* 
+  1. write the CMD byte (to BASE+2)
+  2. read junk lo byte (BASE+0)
+  3. read junk hi byte (BASE+1)
+  4. (mux settled so) write CMD byte again (BASE+2)
+  5. read valid lo byte(BASE+0)
+  6. read valid hi byte(BASE+1)
+
+  Additionally note that the BASE += 4 if the channel >= 8 
+  */
+  
+
+  /* convert n samples */
+  for(n = 0; n < insn->n; n++) {
+    unsigned chan = CR_CHAN(insn->chanspec), range = CR_RANGE(insn->chanspec), aref = CR_AREF(insn->chanspec);
+    unsigned char command_byte = 0;
+    unsigned iooffset = 0;
+    sampl_t sample, adc_adjust = 0;    
+    
+    if (chan > 7) chan -= 8, iooffset = 4; /* use the second dword for channels > 7 */
+    
+    if (aref != AREF_DIFF) {
+      aref = AREF_GROUND;
+      command_byte |= 1<<7; /* set bit 7 to indicate single-ended */      
+    }
+    if (range < 2) adc_adjust = 0x8000; /* bipolar ranges (-5,5 .. -10,10 need to be adjusted -- that is.. they need to wrap around by adding 0x8000 */
+
+    if (chan % 2) {
+      command_byte |= 1<<6; /* odd-numbered channels have bit 6 set */
+    }
+    
+    /* select the channel, bits 4-5 == chan/2 */
+    command_byte |= ((chan/2) & 0x3)<<4;
+
+    
+    /* set the range, bits 2-3 */
+    command_byte |= (range & 0x3) << 2;
+
+    /* need to do this twice to make sure mux settled */
+    outb(command_byte, iobase + iooffset + 2);  /* chan/range/aref select */
+    (void)inb(iobase+iooffset+0); /* discard junk lo byte */
+    (void)inb(iobase+iooffset+1); /* discard junk hi byte */
+    
+    adc_wait_ready(iobase + iooffset); /* wait for the adc to say it finised the conversion */
+    
+    outb(command_byte, iobase + iooffset + 2);  /* select the chan/range/aref AGAIN */
+    sample = inb(iobase+iooffset+0); /* read data lo byte */
+    sample |= inb(iobase+iooffset+1)<<8; /* read data hi byte */
+    sample += adc_adjust; /* adjustment .. munge data */
+    data[n] = sample;
+  }
+  /* return the number of samples read/written */
+  return n;
+}
+
+static int ao_rinsn(comedi_device *dev, comedi_subdevice *s, comedi_insn *insn, lsampl_t *data)
+{
+  int n;
+  for(n = 0; n < insn->n; n++) {
+    unsigned chan = CR_CHAN(insn->chanspec);
+    if (chan < s->n_chan)
+      data[n] = subpriv->ao.shadow_samples[chan];
+  }
+  return n;
+}
+
+static int wait_dac_ready(unsigned long iobase)
+{
+  unsigned long retry = 100000L;
+
+  /* This may seem like an absurd way to handle waiting and violates the
+     "no busy waiting" policy. The fact is that the hardware is 
+     normally so fast that we usually only need one time through the loop 
+     anyway. The longer timeout is for rare occasions and for detecting 
+     non-existant hardware.  */
+  
+  while(retry--)
+  {
+    if (inb(iobase+3) & 0x80)
+        return 0;
+      
+  }
+  return 1;
+}
+
+static int ao_winsn(comedi_device *dev, comedi_subdevice *s, comedi_insn *insn, lsampl_t *data)
+{
+  int n;
+  unsigned iobase = subpriv->iobase, iooffset = 0;
+  
+  for(n = 0; n < insn->n; n++) {
+    unsigned chan = CR_CHAN(insn->chanspec), range = CR_RANGE(insn->chanspec);
+    if (chan < s->n_chan) {
+      unsigned char command_byte = 0, range_byte = range&((1<<4)-1);
+      if (chan >= 4) chan -=4, iooffset += 4;
+      /* set the range.. */
+      outb(range_byte, iobase+iooffset+0); 
+      outb(0, iobase+iooffset+1);
+
+      /* tell it to begin */
+      command_byte = (chan << 1) | 0x60;
+      outb(command_byte, iobase+iooffset+2);
+      
+      wait_dac_ready(iobase+iooffset);
+
+      outb(data[n] & 0xff, iobase+iooffset+0); /* low order byte */
+      outb((data[n]>>8) & 0xff, iobase+iooffset+1); /* high order byte */
+      command_byte = 0x70 | (chan<<1); /* set bit 4 of command byte to indicate data is loaded and trigger conversion */
+      /* trigger converion */
+      outb(command_byte, iobase+iooffset+2); 
+
+      wait_dac_ready(iobase+iooffset);
+            
+      subpriv->ao.shadow_samples[chan] = data[n]; /* save to shadow register for ao_rinsn */
+    }
+  }
+  return n;
+}
+
+
+/*
+ * A convenient macro that defines init_module() and cleanup_module(),
+ * as necessary.
+ */
+COMEDI_INITCLEANUP(driver);
+
+
+