Staging: comedi: add amplc_pci224 driver
authorIan Abbott <abbotti@mev.co.uk>
Wed, 18 Feb 2009 23:25:54 +0000 (15:25 -0800)
committerGreg Kroah-Hartman <gregkh@suse.de>
Fri, 3 Apr 2009 21:53:47 +0000 (14:53 -0700)
Driver for Amplicon PCI224 and PCI234 AO boards

From: Ian Abbott <abbotti@mev.co.uk>
Cc: David Schleef <ds@schleef.org>
Cc: Frank Mori Hess <fmhess@users.sourceforge.net>
Signed-off-by: Greg Kroah-Hartman <gregkh@suse.de>
drivers/staging/comedi/drivers/amplc_pci224.c [new file with mode: 0644]

diff --git a/drivers/staging/comedi/drivers/amplc_pci224.c b/drivers/staging/comedi/drivers/amplc_pci224.c
new file mode 100644 (file)
index 0000000..b821a0b
--- /dev/null
@@ -0,0 +1,1545 @@
+/*
+    comedi/drivers/amplc_pci224.c
+    Driver for Amplicon PCI224 and PCI234 AO boards.
+
+    Copyright (C) 2005 MEV Ltd. <http://www.mev.co.uk/>
+
+    COMEDI - Linux Control and Measurement Device Interface
+    Copyright (C) 1998,2000 David A. Schleef <ds@schleef.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+/*
+Driver: amplc_pci224
+Description: Amplicon PCI224, PCI234
+Author: Ian Abbott <abbotti@mev.co.uk>
+Devices: [Amplicon] PCI224 (amplc_pci224 or pci224),
+  PCI234 (amplc_pci224 or pci234)
+Updated: Wed, 22 Oct 2008 12:25:08 +0100
+Status: works, but see caveats
+
+Supports:
+
+  - ao_insn read/write
+  - ao_do_cmd mode with the following sources:
+
+    - start_src         TRIG_INT        TRIG_EXT
+    - scan_begin_src    TRIG_TIMER      TRIG_EXT
+    - convert_src       TRIG_NOW
+    - scan_end_src      TRIG_COUNT
+    - stop_src          TRIG_COUNT      TRIG_EXT        TRIG_NONE
+
+    The channel list must contain at least one channel with no repeated
+    channels.  The scan end count must equal the number of channels in
+    the channel list.
+
+    There is only one external trigger source so only one of start_src,
+    scan_begin_src or stop_src may use TRIG_EXT.
+
+Configuration options - PCI224:
+  [0] - PCI bus of device (optional).
+  [1] - PCI slot of device (optional).
+          If bus/slot is not specified, the first available PCI device
+          will be used.
+  [2] - Select available ranges according to jumper LK1.  All channels
+        are set to the same range:
+        0=Jumper position 1-2 (factory default), 4 software-selectable
+          internal voltage references, giving 4 bipolar and 4 unipolar
+          ranges:
+            [-10V,+10V], [-5V,+5V], [-2.5V,+2.5V], [-1.25V,+1.25V],
+            [0,+10V], [0,+5V], [0,+2.5V], [0,1.25V].
+        1=Jumper position 2-3, 1 external voltage reference, giving
+          1 bipolar and 1 unipolar range:
+            [-Vext,+Vext], [0,+Vext].
+
+Configuration options - PCI234:
+  [0] - PCI bus of device (optional).
+  [1] - PCI slot of device (optional).
+          If bus/slot is not specified, the first available PCI device
+          will be used.
+  [2] - Select internal or external voltage reference according to
+        jumper LK1.  This affects all channels:
+        0=Jumper position 1-2 (factory default), Vref=5V internal.
+        1=Jumper position 2-3, Vref=Vext external.
+  [3] - Select channel 0 range according to jumper LK2:
+        0=Jumper position 2-3 (factory default), range [-2*Vref,+2*Vref]
+          (10V bipolar when options[2]=0).
+        1=Jumper position 1-2, range [-Vref,+Vref]
+          (5V bipolar when options[2]=0).
+  [4] - Select channel 1 range according to jumper LK3: cf. options[3].
+  [5] - Select channel 2 range according to jumper LK4: cf. options[3].
+  [6] - Select channel 3 range according to jumper LK5: cf. options[3].
+
+Passing a zero for an option is the same as leaving it unspecified.
+
+Caveats:
+
+  1) All channels on the PCI224 share the same range.  Any change to the
+     range as a result of insn_write or a streaming command will affect
+     the output voltages of all channels, including those not specified
+     by the instruction or command.
+
+  2) For the analog output command,  the first scan may be triggered
+     falsely at the start of acquisition.  This occurs when the DAC scan
+     trigger source is switched from 'none' to 'timer' (scan_begin_src =
+     TRIG_TIMER) or 'external' (scan_begin_src == TRIG_EXT) at the start
+     of acquisition and the trigger source is at logic level 1 at the
+     time of the switch.  This is very likely for TRIG_TIMER.  For
+     TRIG_EXT, it depends on the state of the external line and whether
+     the CR_INVERT flag has been set.  The remaining scans are triggered
+     correctly.
+*/
+
+#include "../comedidev.h"
+
+#include "comedi_pci.h"
+
+#include "comedi_fc.h"
+#include "8253.h"
+
+#define DRIVER_NAME    "amplc_pci224"
+
+/*
+ * PCI IDs.
+ */
+/* #define PCI_VENDOR_ID_AMPLICON 0x14dc */
+#define PCI_DEVICE_ID_AMPLICON_PCI224 0x0007
+#define PCI_DEVICE_ID_AMPLICON_PCI234 0x0008
+#define PCI_DEVICE_ID_INVALID 0xffff
+
+/*
+ * PCI224/234 i/o space 1 (PCIBAR2) registers.
+ */
+#define PCI224_IO1_SIZE        0x20    /* Size of i/o space 1 (8-bit registers) */
+#define PCI224_Z2_CT0  0x14    /* 82C54 counter/timer 0 */
+#define PCI224_Z2_CT1  0x15    /* 82C54 counter/timer 1 */
+#define PCI224_Z2_CT2  0x16    /* 82C54 counter/timer 2 */
+#define PCI224_Z2_CTC  0x17    /* 82C54 counter/timer control word */
+#define PCI224_ZCLK_SCE        0x1A    /* Group Z Clock Configuration Register */
+#define PCI224_ZGAT_SCE        0x1D    /* Group Z Gate Configuration Register */
+#define PCI224_INT_SCE 0x1E    /* ISR Interrupt source mask register */
+                               /* /Interrupt status */
+
+/*
+ * PCI224/234 i/o space 2 (PCIBAR3) 16-bit registers.
+ */
+#define PCI224_IO2_SIZE        0x10    /* Size of i/o space 2 (16-bit registers). */
+#define PCI224_DACDATA 0x00    /* (w-o) DAC FIFO data. */
+#define PCI224_SOFTTRIG        0x00    /* (r-o) DAC software scan trigger. */
+#define PCI224_DACCON  0x02    /* (r/w) DAC status/configuration. */
+#define PCI224_FIFOSIZ 0x04    /* (w-o) FIFO size for wraparound mode. */
+#define PCI224_DACCEN  0x06    /* (w-o) DAC channel enable register. */
+
+/*
+ * DACCON values.
+ */
+/* (r/w) Scan trigger. */
+#define PCI224_DACCON_TRIG_MASK                (7 << 0)
+#define PCI224_DACCON_TRIG_NONE                (0 << 0)        /* none */
+#define PCI224_DACCON_TRIG_SW          (1 << 0)        /* software trig */
+#define PCI224_DACCON_TRIG_EXTP                (2 << 0)        /* ext +ve edge */
+#define PCI224_DACCON_TRIG_EXTN                (3 << 0)        /* ext -ve edge */
+#define PCI224_DACCON_TRIG_Z2CT0       (4 << 0)        /* Z2 CT0 out */
+#define PCI224_DACCON_TRIG_Z2CT1       (5 << 0)        /* Z2 CT1 out */
+#define PCI224_DACCON_TRIG_Z2CT2       (6 << 0)        /* Z2 CT2 out */
+/* (r/w) Polarity (PCI224 only, PCI234 always bipolar!). */
+#define PCI224_DACCON_POLAR_MASK       (1 << 3)
+#define PCI224_DACCON_POLAR_UNI                (0 << 3)        /* range [0,Vref] */
+#define PCI224_DACCON_POLAR_BI         (1 << 3)        /* range [-Vref,Vref] */
+/* (r/w) Internal Vref (PCI224 only, when LK1 in position 1-2). */
+#define PCI224_DACCON_VREF_MASK                (3 << 4)
+#define PCI224_DACCON_VREF_1_25                (0 << 4)        /* Vref = 1.25V */
+#define PCI224_DACCON_VREF_2_5         (1 << 4)        /* Vref = 2.5V */
+#define PCI224_DACCON_VREF_5           (2 << 4)        /* Vref = 5V */
+#define PCI224_DACCON_VREF_10          (3 << 4)        /* Vref = 10V */
+/* (r/w) Wraparound mode enable (to play back stored waveform). */
+#define PCI224_DACCON_FIFOWRAP         (1 << 7)
+/* (r/w) FIFO enable.  It MUST be set! */
+#define PCI224_DACCON_FIFOENAB         (1 << 8)
+/* (r/w) FIFO interrupt trigger level (most values are not very useful). */
+#define PCI224_DACCON_FIFOINTR_MASK    (7 << 9)
+#define PCI224_DACCON_FIFOINTR_EMPTY   (0 << 9)        /* when empty */
+#define PCI224_DACCON_FIFOINTR_NEMPTY  (1 << 9)        /* when not empty */
+#define PCI224_DACCON_FIFOINTR_NHALF   (2 << 9)        /* when not half full */
+#define PCI224_DACCON_FIFOINTR_HALF    (3 << 9)        /* when half full */
+#define PCI224_DACCON_FIFOINTR_NFULL   (4 << 9)        /* when not full */
+#define PCI224_DACCON_FIFOINTR_FULL    (5 << 9)        /* when full */
+/* (r-o) FIFO fill level. */
+#define PCI224_DACCON_FIFOFL_MASK      (7 << 12)
+#define PCI224_DACCON_FIFOFL_EMPTY     (1 << 12)       /* 0 */
+#define PCI224_DACCON_FIFOFL_ONETOHALF (0 << 12)       /* [1,2048] */
+#define PCI224_DACCON_FIFOFL_HALFTOFULL        (4 << 12)       /* [2049,4095] */
+#define PCI224_DACCON_FIFOFL_FULL      (6 << 12)       /* 4096 */
+/* (r-o) DAC busy flag. */
+#define PCI224_DACCON_BUSY             (1 << 15)
+/* (w-o) FIFO reset. */
+#define PCI224_DACCON_FIFORESET                (1 << 12)
+/* (w-o) Global reset (not sure what it does). */
+#define PCI224_DACCON_GLOBALRESET      (1 << 13)
+
+/*
+ * DAC FIFO size.
+ */
+#define PCI224_FIFO_SIZE       4096
+
+/*
+ * DAC FIFO guaranteed minimum room available, depending on reported fill level.
+ * The maximum room available depends on the reported fill level and how much
+ * has been written!
+ */
+#define PCI224_FIFO_ROOM_EMPTY         PCI224_FIFO_SIZE
+#define PCI224_FIFO_ROOM_ONETOHALF     (PCI224_FIFO_SIZE / 2)
+#define PCI224_FIFO_ROOM_HALFTOFULL    1
+#define PCI224_FIFO_ROOM_FULL          0
+
+/*
+ * Counter/timer clock input configuration sources.
+ */
+#define CLK_CLK                0       /* reserved (channel-specific clock) */
+#define CLK_10MHZ      1       /* internal 10 MHz clock */
+#define CLK_1MHZ       2       /* internal 1 MHz clock */
+#define CLK_100KHZ     3       /* internal 100 kHz clock */
+#define CLK_10KHZ      4       /* internal 10 kHz clock */
+#define CLK_1KHZ       5       /* internal 1 kHz clock */
+#define CLK_OUTNM1     6       /* output of channel-1 modulo total */
+#define CLK_EXT                7       /* external clock */
+/* Macro to construct clock input configuration register value. */
+#define CLK_CONFIG(chan, src)  ((((chan) & 3) << 3) | ((src) & 7))
+/* Timebases in ns. */
+#define TIMEBASE_10MHZ         100
+#define TIMEBASE_1MHZ          1000
+#define TIMEBASE_100KHZ                10000
+#define TIMEBASE_10KHZ         100000
+#define TIMEBASE_1KHZ          1000000
+
+/*
+ * Counter/timer gate input configuration sources.
+ */
+#define GAT_VCC                0       /* VCC (i.e. enabled) */
+#define GAT_GND                1       /* GND (i.e. disabled) */
+#define GAT_EXT                2       /* reserved (external gate input) */
+#define GAT_NOUTNM2    3       /* inverted output of channel-2 modulo total */
+/* Macro to construct gate input configuration register value. */
+#define GAT_CONFIG(chan, src)  ((((chan) & 3) << 3) | ((src) & 7))
+
+/*
+ * Summary of CLK_OUTNM1 and GAT_NOUTNM2 connections for PCI224 and PCI234:
+ *
+ *              Channel's       Channel's
+ *              clock input     gate input
+ * Channel      CLK_OUTNM1      GAT_NOUTNM2
+ * -------      ----------      -----------
+ * Z2-CT0       Z2-CT2-OUT      /Z2-CT1-OUT
+ * Z2-CT1       Z2-CT0-OUT      /Z2-CT2-OUT
+ * Z2-CT2       Z2-CT1-OUT      /Z2-CT0-OUT
+ */
+
+/*
+ * Interrupt enable/status bits
+ */
+#define PCI224_INTR_EXT                0x01    /* rising edge on external input */
+#define PCI224_INTR_DAC                0x04    /* DAC (FIFO) interrupt */
+#define PCI224_INTR_Z2CT1      0x20    /* rising edge on Z2-CT1 output */
+
+#define PCI224_INTR_EDGE_BITS  (PCI224_INTR_EXT | PCI224_INTR_Z2CT1)
+#define PCI224_INTR_LEVEL_BITS PCI224_INTR_DACFIFO
+
+/*
+ * Handy macros.
+ */
+
+/* Combine old and new bits. */
+#define COMBINE(old, new, mask)        (((old) & ~(mask)) | ((new) & (mask)))
+
+/* A generic null function pointer value.  */
+#define NULLFUNC       0
+
+/* Current CPU.  XXX should this be hard_smp_processor_id()? */
+#define THISCPU                smp_processor_id()
+
+/* State bits for use with atomic bit operations. */
+#define AO_CMD_STARTED 0
+
+/*
+ * Range tables.
+ */
+
+/* The software selectable internal ranges for PCI224 (option[2] == 0). */
+static const comedi_lrange range_pci224_internal = {
+       8,
+       {
+                       BIP_RANGE(10),
+                       BIP_RANGE(5),
+                       BIP_RANGE(2.5),
+                       BIP_RANGE(1.25),
+                       UNI_RANGE(10),
+                       UNI_RANGE(5),
+                       UNI_RANGE(2.5),
+                       UNI_RANGE(1.25),
+               }
+};
+
+static const unsigned short hwrange_pci224_internal[8] = {
+       PCI224_DACCON_POLAR_BI | PCI224_DACCON_VREF_10,
+       PCI224_DACCON_POLAR_BI | PCI224_DACCON_VREF_5,
+       PCI224_DACCON_POLAR_BI | PCI224_DACCON_VREF_2_5,
+       PCI224_DACCON_POLAR_BI | PCI224_DACCON_VREF_1_25,
+       PCI224_DACCON_POLAR_UNI | PCI224_DACCON_VREF_10,
+       PCI224_DACCON_POLAR_UNI | PCI224_DACCON_VREF_5,
+       PCI224_DACCON_POLAR_UNI | PCI224_DACCON_VREF_2_5,
+       PCI224_DACCON_POLAR_UNI | PCI224_DACCON_VREF_1_25,
+};
+
+/* The software selectable external ranges for PCI224 (option[2] == 1). */
+static const comedi_lrange range_pci224_external = {
+       2,
+       {
+                       RANGE_ext(-1, 1),       /* bipolar [-Vref,+Vref] */
+                       RANGE_ext(0, 1),        /* unipolar [0,+Vref] */
+               }
+};
+
+static const unsigned short hwrange_pci224_external[2] = {
+       PCI224_DACCON_POLAR_BI,
+       PCI224_DACCON_POLAR_UNI,
+};
+
+/* The hardware selectable Vref*2 external range for PCI234
+ * (option[2] == 1, option[3+n] == 0). */
+static const comedi_lrange range_pci234_ext2 = {
+       1,
+       {
+                       RANGE_ext(-2, 2),
+               }
+};
+
+/* The hardware selectable Vref external range for PCI234
+ * (option[2] == 1, option[3+n] == 1). */
+static const comedi_lrange range_pci234_ext = {
+       1,
+       {
+                       RANGE_ext(-1, 1),
+               }
+};
+
+/* This serves for all the PCI234 ranges. */
+static const unsigned short hwrange_pci234[1] = {
+       PCI224_DACCON_POLAR_BI, /* bipolar - hardware ignores it! */
+};
+
+/*
+ * Board descriptions.
+ */
+
+enum pci224_model { any_model, pci224_model, pci234_model };
+
+typedef struct pci224_board_struct {
+       const char *name;
+       unsigned short devid;
+       enum pci224_model model;
+       unsigned int ao_chans;
+       unsigned int ao_bits;
+} pci224_board;
+
+static const pci224_board pci224_boards[] = {
+       {
+             name:     "pci224",
+             devid: PCI_DEVICE_ID_AMPLICON_PCI224,
+             model:    pci224_model,
+             ao_chans:16,
+             ao_bits:  12,
+               },
+       {
+             name:     "pci234",
+             devid: PCI_DEVICE_ID_AMPLICON_PCI234,
+             model:    pci234_model,
+             ao_chans:4,
+             ao_bits:  16,
+               },
+       {
+             name:     DRIVER_NAME,
+             devid: PCI_DEVICE_ID_INVALID,
+             model:    any_model,      /* wildcard */
+               },
+};
+
+/*
+ * PCI driver table.
+ */
+
+static DEFINE_PCI_DEVICE_TABLE(pci224_pci_table) = {
+       {PCI_VENDOR_ID_AMPLICON, PCI_DEVICE_ID_AMPLICON_PCI224,
+               PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0},
+       {PCI_VENDOR_ID_AMPLICON, PCI_DEVICE_ID_AMPLICON_PCI234,
+               PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0},
+       {0}
+};
+
+MODULE_DEVICE_TABLE(pci, pci224_pci_table);
+
+/*
+ * Useful for shorthand access to the particular board structure
+ */
+#define thisboard ((pci224_board *)dev->board_ptr)
+
+/* 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 {
+       struct pci_dev *pci_dev;        /* PCI device */
+       const unsigned short *hwrange;
+       unsigned long iobase1;
+       unsigned long state;
+       spinlock_t ao_spinlock;
+       lsampl_t *ao_readback;
+       sampl_t *ao_scan_vals;
+       unsigned char *ao_scan_order;
+       int intr_cpuid;
+       short intr_running;
+       unsigned short daccon;
+       unsigned int cached_div1;
+       unsigned int cached_div2;
+       unsigned int ao_stop_count;
+       short ao_stop_continuous;
+       unsigned short ao_enab; /* max 16 channels so 'short' will do */
+       unsigned char intsce;
+} pci224_private;
+
+#define devpriv ((pci224_private *)dev->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 pci224_attach(comedi_device * dev, comedi_devconfig * it);
+static int pci224_detach(comedi_device * dev);
+static comedi_driver driver_amplc_pci224 = {
+      driver_name:DRIVER_NAME,
+      module:THIS_MODULE,
+      attach:pci224_attach,
+      detach:pci224_detach,
+      board_name:&pci224_boards[0].name,
+      offset:sizeof(pci224_board),
+      num_names:sizeof(pci224_boards) / sizeof(pci224_board),
+};
+
+COMEDI_PCI_INITCLEANUP(driver_amplc_pci224, pci224_pci_table);
+
+/*
+ * Called from the 'insn_write' function to perform a single write.
+ */
+static void
+pci224_ao_set_data(comedi_device * dev, int chan, int range, lsampl_t data)
+{
+       unsigned short mangled;
+
+       /* Store unmangled data for readback. */
+       devpriv->ao_readback[chan] = data;
+       /* Enable the channel. */
+       outw(1 << chan, dev->iobase + PCI224_DACCEN);
+       /* Set range and reset FIFO. */
+       devpriv->daccon = COMBINE(devpriv->daccon, devpriv->hwrange[range],
+               (PCI224_DACCON_POLAR_MASK | PCI224_DACCON_VREF_MASK));
+       outw(devpriv->daccon | PCI224_DACCON_FIFORESET,
+               dev->iobase + PCI224_DACCON);
+       /*
+        * Mangle the data.  The hardware expects:
+        * - bipolar: 16-bit 2's complement
+        * - unipolar: 16-bit unsigned
+        */
+       mangled = (unsigned short)data << (16 - thisboard->ao_bits);
+       if ((devpriv->daccon & PCI224_DACCON_POLAR_MASK) ==
+               PCI224_DACCON_POLAR_BI) {
+               mangled ^= 0x8000;
+       }
+       /* Write mangled data to the FIFO. */
+       outw(mangled, dev->iobase + PCI224_DACDATA);
+       /* Trigger the conversion. */
+       inw(dev->iobase + PCI224_SOFTTRIG);
+}
+
+/*
+ * 'insn_write' function for AO subdevice.
+ */
+static int
+pci224_ao_insn_write(comedi_device * dev, comedi_subdevice * s,
+       comedi_insn * insn, lsampl_t * data)
+{
+       int i;
+       int chan, range;
+
+       /* Unpack channel and range. */
+       chan = CR_CHAN(insn->chanspec);
+       range = CR_RANGE(insn->chanspec);
+
+       /* Writing a list of values to an AO channel is probably not
+        * very useful, but that's how the interface is defined. */
+       for (i = 0; i < insn->n; i++) {
+               pci224_ao_set_data(dev, chan, range, data[i]);
+       }
+       return i;
+}
+
+/*
+ * 'insn_read' function for AO subdevice.
+ *
+ * N.B. The value read will not be valid if the DAC channel has
+ * never been written successfully since the device was attached
+ * or since the channel has been used by an AO streaming write
+ * command.
+ */
+static int
+pci224_ao_insn_read(comedi_device * dev, comedi_subdevice * s,
+       comedi_insn * insn, lsampl_t * data)
+{
+       int i;
+       int chan;
+
+       chan = CR_CHAN(insn->chanspec);
+
+       for (i = 0; i < insn->n; i++) {
+               data[i] = devpriv->ao_readback[chan];
+       }
+
+       return i;
+}
+
+/*
+ * Just a wrapper for the inline function 'i8253_cascade_ns_to_timer'.
+ */
+static void
+pci224_cascade_ns_to_timer(int osc_base, unsigned int *d1, unsigned int *d2,
+       unsigned int *nanosec, int round_mode)
+{
+       i8253_cascade_ns_to_timer(osc_base, d1, d2, nanosec, round_mode);
+}
+
+/*
+ * Kills a command running on the AO subdevice.
+ */
+static void pci224_ao_stop(comedi_device * dev, comedi_subdevice * s)
+{
+       unsigned long flags;
+
+       if (!test_and_clear_bit(AO_CMD_STARTED, &devpriv->state)) {
+               return;
+       }
+
+       comedi_spin_lock_irqsave(&devpriv->ao_spinlock, flags);
+       /* Kill the interrupts. */
+       devpriv->intsce = 0;
+       outb(0, devpriv->iobase1 + PCI224_INT_SCE);
+       /*
+        * Interrupt routine may or may not be running.  We may or may not
+        * have been called from the interrupt routine (directly or
+        * indirectly via a comedi_events() callback routine).  It's highly
+        * unlikely that we've been called from some other interrupt routine
+        * but who knows what strange things coders get up to!
+        *
+        * If the interrupt routine is currently running, wait for it to
+        * finish, unless we appear to have been called via the interrupt
+        * routine.
+        */
+       while (devpriv->intr_running && devpriv->intr_cpuid != THISCPU) {
+               comedi_spin_unlock_irqrestore(&devpriv->ao_spinlock, flags);
+               comedi_spin_lock_irqsave(&devpriv->ao_spinlock, flags);
+       }
+       comedi_spin_unlock_irqrestore(&devpriv->ao_spinlock, flags);
+       /* Reconfigure DAC for insn_write usage. */
+       outw(0, dev->iobase + PCI224_DACCEN);   /* Disable channels. */
+       devpriv->daccon = COMBINE(devpriv->daccon,
+               PCI224_DACCON_TRIG_SW | PCI224_DACCON_FIFOINTR_EMPTY,
+               PCI224_DACCON_TRIG_MASK | PCI224_DACCON_FIFOINTR_MASK);
+       outw(devpriv->daccon | PCI224_DACCON_FIFORESET,
+               dev->iobase + PCI224_DACCON);
+}
+
+/*
+ * Handles start of acquisition for the AO subdevice.
+ */
+static void pci224_ao_start(comedi_device * dev, comedi_subdevice * s)
+{
+       comedi_cmd *cmd = &s->async->cmd;
+       unsigned long flags;
+
+       set_bit(AO_CMD_STARTED, &devpriv->state);
+       if (!devpriv->ao_stop_continuous && devpriv->ao_stop_count == 0) {
+               /* An empty acquisition! */
+               pci224_ao_stop(dev, s);
+               s->async->events |= COMEDI_CB_EOA;
+               comedi_event(dev, s);
+       } else {
+               /* Enable interrupts. */
+               comedi_spin_lock_irqsave(&devpriv->ao_spinlock, flags);
+               if (cmd->stop_src == TRIG_EXT) {
+                       devpriv->intsce = PCI224_INTR_EXT | PCI224_INTR_DAC;
+               } else {
+                       devpriv->intsce = PCI224_INTR_DAC;
+               }
+               outb(devpriv->intsce, devpriv->iobase1 + PCI224_INT_SCE);
+               comedi_spin_unlock_irqrestore(&devpriv->ao_spinlock, flags);
+       }
+}
+
+/*
+ * Handles interrupts from the DAC FIFO.
+ */
+static void pci224_ao_handle_fifo(comedi_device * dev, comedi_subdevice * s)
+{
+       comedi_cmd *cmd = &s->async->cmd;
+       unsigned int num_scans;
+       unsigned int room;
+       unsigned short dacstat;
+       unsigned int i, n;
+       unsigned int bytes_per_scan;
+
+       if (cmd->chanlist_len) {
+               bytes_per_scan = cmd->chanlist_len * sizeof(sampl_t);
+       } else {
+               /* Shouldn't get here! */
+               bytes_per_scan = sizeof(sampl_t);
+       }
+       /* Determine number of scans available in buffer. */
+       num_scans = comedi_buf_read_n_available(s->async) / bytes_per_scan;
+       if (!devpriv->ao_stop_continuous) {
+               /* Fixed number of scans. */
+               if (num_scans > devpriv->ao_stop_count) {
+                       num_scans = devpriv->ao_stop_count;
+               }
+       }
+
+       /* Determine how much room is in the FIFO (in samples). */
+       dacstat = inw(dev->iobase + PCI224_DACCON);
+       switch (dacstat & PCI224_DACCON_FIFOFL_MASK) {
+       case PCI224_DACCON_FIFOFL_EMPTY:
+               room = PCI224_FIFO_ROOM_EMPTY;
+               if (!devpriv->ao_stop_continuous
+                       && devpriv->ao_stop_count == 0) {
+                       /* FIFO empty at end of counted acquisition. */
+                       pci224_ao_stop(dev, s);
+                       s->async->events |= COMEDI_CB_EOA;
+                       comedi_event(dev, s);
+                       return;
+               }
+               break;
+       case PCI224_DACCON_FIFOFL_ONETOHALF:
+               room = PCI224_FIFO_ROOM_ONETOHALF;
+               break;
+       case PCI224_DACCON_FIFOFL_HALFTOFULL:
+               room = PCI224_FIFO_ROOM_HALFTOFULL;
+               break;
+       default:
+               room = PCI224_FIFO_ROOM_FULL;
+               break;
+       }
+       if (room >= PCI224_FIFO_ROOM_ONETOHALF) {
+               /* FIFO is less than half-full. */
+               if (num_scans == 0) {
+                       /* Nothing left to put in the FIFO. */
+                       pci224_ao_stop(dev, s);
+                       s->async->events |= COMEDI_CB_OVERFLOW;
+                       rt_printk(KERN_ERR "comedi%d: "
+                               "AO buffer underrun\n", dev->minor);
+               }
+       }
+       /* Determine how many new scans can be put in the FIFO. */
+       if (cmd->chanlist_len) {
+               room /= cmd->chanlist_len;
+       }
+       /* Determine how many scans to process. */
+       if (num_scans > room) {
+               num_scans = room;
+       }
+       /* Process scans. */
+       for (n = 0; n < num_scans; n++) {
+               cfc_read_array_from_buffer(s, &devpriv->ao_scan_vals[0],
+                       bytes_per_scan);
+               for (i = 0; i < cmd->chanlist_len; i++) {
+                       outw(devpriv->ao_scan_vals[devpriv->
+                                       ao_scan_order[i]],
+                               dev->iobase + PCI224_DACDATA);
+               }
+       }
+       if (!devpriv->ao_stop_continuous) {
+               devpriv->ao_stop_count -= num_scans;
+               if (devpriv->ao_stop_count == 0) {
+                       /*
+                        * Change FIFO interrupt trigger level to wait
+                        * until FIFO is empty.
+                        */
+                       devpriv->daccon = COMBINE(devpriv->daccon,
+                               PCI224_DACCON_FIFOINTR_EMPTY,
+                               PCI224_DACCON_FIFOINTR_MASK);
+                       outw(devpriv->daccon,
+                               dev->iobase + PCI224_DACCON);
+               }
+       }
+       if ((devpriv->daccon & PCI224_DACCON_TRIG_MASK) ==
+               PCI224_DACCON_TRIG_NONE) {
+               unsigned short trig;
+
+               /*
+                * This is the initial DAC FIFO interrupt at the
+                * start of the acquisition.  The DAC's scan trigger
+                * has been set to 'none' up until now.
+                *
+                * Now that data has been written to the FIFO, the
+                * DAC's scan trigger source can be set to the
+                * correct value.
+                *
+                * BUG: The first scan will be triggered immediately
+                * if the scan trigger source is at logic level 1.
+                */
+               if (cmd->scan_begin_src == TRIG_TIMER) {
+                       trig = PCI224_DACCON_TRIG_Z2CT0;
+               } else {
+                       /* cmd->scan_begin_src == TRIG_EXT */
+                       if (cmd->scan_begin_arg & CR_INVERT) {
+                               trig = PCI224_DACCON_TRIG_EXTN;
+                       } else {
+                               trig = PCI224_DACCON_TRIG_EXTP;
+                       }
+               }
+               devpriv->daccon = COMBINE(devpriv->daccon, trig,
+                       PCI224_DACCON_TRIG_MASK);
+               outw(devpriv->daccon, dev->iobase + PCI224_DACCON);
+       }
+       if (s->async->events) {
+               comedi_event(dev, s);
+       }
+}
+
+/*
+ * Internal trigger function to start acquisition on AO subdevice.
+ */
+static int
+pci224_ao_inttrig_start(comedi_device * dev, comedi_subdevice * s,
+       unsigned int trignum)
+{
+       if (trignum != 0)
+               return -EINVAL;
+
+       s->async->inttrig = NULLFUNC;
+       pci224_ao_start(dev, s);
+
+       return 1;
+}
+
+#define MAX_SCAN_PERIOD                0xFFFFFFFFU
+#define MIN_SCAN_PERIOD                2500
+#define CONVERT_PERIOD         625
+
+/*
+ * 'do_cmdtest' function for AO subdevice.
+ */
+static int
+pci224_ao_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_INT | TRIG_EXT;
+       if (!cmd->start_src || tmp != cmd->start_src)
+               err++;
+
+       tmp = cmd->scan_begin_src;
+       cmd->scan_begin_src &= TRIG_EXT | TRIG_TIMER;
+       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_EXT | 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++;
+
+       /* There's only one external trigger signal (which makes these
+        * tests easier).  Only one thing can use it. */
+       tmp = 0;
+       if (cmd->start_src & TRIG_EXT)
+               tmp++;
+       if (cmd->scan_begin_src & TRIG_EXT)
+               tmp++;
+       if (cmd->stop_src & TRIG_EXT)
+               tmp++;
+       if (tmp > 1)
+               err++;
+
+       if (err)
+               return 2;
+
+       /* Step 3: make sure arguments are trivially compatible. */
+
+       switch (cmd->start_src) {
+       case TRIG_INT:
+               if (cmd->start_arg != 0) {
+                       cmd->start_arg = 0;
+                       err++;
+               }
+               break;
+       case TRIG_EXT:
+               /* Force to external trigger 0. */
+               if ((cmd->start_arg & ~CR_FLAGS_MASK) != 0) {
+                       cmd->start_arg = COMBINE(cmd->start_arg, 0,
+                               ~CR_FLAGS_MASK);
+                       err++;
+               }
+               /* The only flag allowed is CR_EDGE, which is ignored. */
+               if ((cmd->start_arg & CR_FLAGS_MASK & ~CR_EDGE) != 0) {
+                       cmd->start_arg = COMBINE(cmd->start_arg, 0,
+                               CR_FLAGS_MASK & ~CR_EDGE);
+                       err++;
+               }
+               break;
+       }
+
+       switch (cmd->scan_begin_src) {
+       case TRIG_TIMER:
+               if (cmd->scan_begin_arg > MAX_SCAN_PERIOD) {
+                       cmd->scan_begin_arg = MAX_SCAN_PERIOD;
+                       err++;
+               }
+               tmp = cmd->chanlist_len * CONVERT_PERIOD;
+               if (tmp < MIN_SCAN_PERIOD) {
+                       tmp = MIN_SCAN_PERIOD;
+               }
+               if (cmd->scan_begin_arg < tmp) {
+                       cmd->scan_begin_arg = tmp;
+                       err++;
+               }
+               break;
+       case TRIG_EXT:
+               /* Force to external trigger 0. */
+               if ((cmd->scan_begin_arg & ~CR_FLAGS_MASK) != 0) {
+                       cmd->scan_begin_arg = COMBINE(cmd->scan_begin_arg, 0,
+                               ~CR_FLAGS_MASK);
+                       err++;
+               }
+               /* Only allow flags CR_EDGE and CR_INVERT.  Ignore CR_EDGE. */
+               if ((cmd->scan_begin_arg & CR_FLAGS_MASK &
+                               ~(CR_EDGE | CR_INVERT)) != 0) {
+                       cmd->scan_begin_arg = COMBINE(cmd->scan_begin_arg, 0,
+                               CR_FLAGS_MASK & ~(CR_EDGE | CR_INVERT));
+                       err++;
+               }
+               break;
+       }
+
+       /* cmd->convert_src == TRIG_NOW */
+       if (cmd->convert_arg != 0) {
+               cmd->convert_arg = 0;
+               err++;
+       }
+
+       /* cmd->scan_end_arg == 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_EXT:
+               /* Force to external trigger 0. */
+               if ((cmd->stop_arg & ~CR_FLAGS_MASK) != 0) {
+                       cmd->stop_arg = COMBINE(cmd->stop_arg, 0,
+                               ~CR_FLAGS_MASK);
+                       err++;
+               }
+               /* The only flag allowed is CR_EDGE, which is ignored. */
+               if ((cmd->stop_arg & CR_FLAGS_MASK & ~CR_EDGE) != 0) {
+                       cmd->stop_arg = COMBINE(cmd->stop_arg, 0,
+                               CR_FLAGS_MASK & ~CR_EDGE);
+               }
+               break;
+       case TRIG_NONE:
+               if (cmd->stop_arg != 0) {
+                       cmd->stop_arg = 0;
+                       err++;
+               }
+               break;
+       }
+
+       if (err)
+               return 3;
+
+       /* Step 4: fix up any arguments. */
+
+       if (cmd->scan_begin_src == TRIG_TIMER) {
+               unsigned int div1, div2, round;
+               int round_mode = cmd->flags & TRIG_ROUND_MASK;
+
+               tmp = cmd->scan_begin_arg;
+               /* Check whether to use a single timer. */
+               switch (round_mode) {
+               case TRIG_ROUND_NEAREST:
+               default:
+                       round = TIMEBASE_10MHZ / 2;
+                       break;
+               case TRIG_ROUND_DOWN:
+                       round = 0;
+                       break;
+               case TRIG_ROUND_UP:
+                       round = TIMEBASE_10MHZ - 1;
+                       break;
+               }
+               /* Be careful to avoid overflow! */
+               div2 = cmd->scan_begin_arg / TIMEBASE_10MHZ;
+               div2 += (round + cmd->scan_begin_arg % TIMEBASE_10MHZ) /
+                       TIMEBASE_10MHZ;
+               if (div2 <= 0x10000) {
+                       /* A single timer will suffice. */
+                       if (div2 < 2)
+                               div2 = 2;
+                       cmd->scan_begin_arg = div2 * TIMEBASE_10MHZ;
+                       if (cmd->scan_begin_arg < div2 ||
+                               cmd->scan_begin_arg < TIMEBASE_10MHZ) {
+                               /* Overflow! */
+                               cmd->scan_begin_arg = MAX_SCAN_PERIOD;
+                       }
+               } else {
+                       /* Use two timers. */
+                       div1 = devpriv->cached_div1;
+                       div2 = devpriv->cached_div2;
+                       pci224_cascade_ns_to_timer(TIMEBASE_10MHZ, &div1, &div2,
+                               &cmd->scan_begin_arg, round_mode);
+                       devpriv->cached_div1 = div1;
+                       devpriv->cached_div2 = div2;
+               }
+               if (tmp != cmd->scan_begin_arg) {
+                       err++;
+               }
+       }
+
+       if (err)
+               return 4;
+
+       /* Step 5: check channel list. */
+
+       if (cmd->chanlist && (cmd->chanlist_len > 0)) {
+               unsigned int range;
+               enum { range_err = 1, dupchan_err = 2, };
+               unsigned errors;
+               unsigned int n;
+               unsigned int ch;
+
+               /*
+                * Check all channels have the same range index.  Don't care
+                * about analogue reference, as we can't configure it.
+                *
+                * Check the list has no duplicate channels.
+                */
+               range = CR_RANGE(cmd->chanlist[0]);
+               errors = 0;
+               tmp = 0;
+               for (n = 0; n < cmd->chanlist_len; n++) {
+                       ch = CR_CHAN(cmd->chanlist[n]);
+                       if (tmp & (1U << ch)) {
+                               errors |= dupchan_err;
+                       }
+                       tmp |= (1U << ch);
+                       if (CR_RANGE(cmd->chanlist[n]) != range) {
+                               errors |= range_err;
+                       }
+               }
+               if (errors) {
+                       if (errors & dupchan_err) {
+                               DPRINTK("comedi%d: " DRIVER_NAME
+                                       ": ao_cmdtest: "
+                                       "entries in chanlist must contain no "
+                                       "duplicate channels\n", dev->minor);
+                       }
+                       if (errors & range_err) {
+                               DPRINTK("comedi%d: " DRIVER_NAME
+                                       ": ao_cmdtest: "
+                                       "entries in chanlist must all have "
+                                       "the same range index\n", dev->minor);
+                       }
+                       err++;
+               }
+       }
+
+       if (err)
+               return 5;
+
+       return 0;
+}
+
+/*
+ * 'do_cmd' function for AO subdevice.
+ */
+static int pci224_ao_cmd(comedi_device * dev, comedi_subdevice * s)
+{
+       comedi_cmd *cmd = &s->async->cmd;
+       int range;
+       unsigned int i, j;
+       unsigned int ch;
+       unsigned int rank;
+       unsigned long flags;
+
+       /* Cannot handle null/empty chanlist. */
+       if (cmd->chanlist == NULL || cmd->chanlist_len == 0) {
+               return -EINVAL;
+       }
+
+       /* Determine which channels are enabled and their load order.  */
+       devpriv->ao_enab = 0;
+
+       for (i = 0; i < cmd->chanlist_len; i++) {
+               ch = CR_CHAN(cmd->chanlist[i]);
+               devpriv->ao_enab |= 1U << ch;
+               rank = 0;
+               for (j = 0; j < cmd->chanlist_len; j++) {
+                       if (CR_CHAN(cmd->chanlist[j]) < ch) {
+                               rank++;
+                       }
+               }
+               devpriv->ao_scan_order[rank] = i;
+       }
+
+       /* Set enabled channels. */
+       outw(devpriv->ao_enab, dev->iobase + PCI224_DACCEN);
+
+       /* Determine range and polarity.  All channels the same.  */
+       range = CR_RANGE(cmd->chanlist[0]);
+
+       /*
+        * Set DAC range and polarity.
+        * Set DAC scan trigger source to 'none'.
+        * Set DAC FIFO interrupt trigger level to 'not half full'.
+        * Reset DAC FIFO.
+        *
+        * N.B. DAC FIFO interrupts are currently disabled.
+        */
+       devpriv->daccon = COMBINE(devpriv->daccon,
+               (devpriv->hwrange[range] | PCI224_DACCON_TRIG_NONE |
+                       PCI224_DACCON_FIFOINTR_NHALF),
+               (PCI224_DACCON_POLAR_MASK | PCI224_DACCON_VREF_MASK |
+                       PCI224_DACCON_TRIG_MASK | PCI224_DACCON_FIFOINTR_MASK));
+       outw(devpriv->daccon | PCI224_DACCON_FIFORESET,
+               dev->iobase + PCI224_DACCON);
+
+       if (cmd->scan_begin_src == TRIG_TIMER) {
+               unsigned int div1, div2, round;
+               unsigned int ns = cmd->scan_begin_arg;
+               int round_mode = cmd->flags & TRIG_ROUND_MASK;
+
+               /* Check whether to use a single timer. */
+               switch (round_mode) {
+               case TRIG_ROUND_NEAREST:
+               default:
+                       round = TIMEBASE_10MHZ / 2;
+                       break;
+               case TRIG_ROUND_DOWN:
+                       round = 0;
+                       break;
+               case TRIG_ROUND_UP:
+                       round = TIMEBASE_10MHZ - 1;
+                       break;
+               }
+               /* Be careful to avoid overflow! */
+               div2 = cmd->scan_begin_arg / TIMEBASE_10MHZ;
+               div2 += (round + cmd->scan_begin_arg % TIMEBASE_10MHZ) /
+                       TIMEBASE_10MHZ;
+               if (div2 <= 0x10000) {
+                       /* A single timer will suffice. */
+                       if (div2 < 2)
+                               div2 = 2;
+                       div2 &= 0xffff;
+                       div1 = 1;       /* Flag that single timer to be used. */
+               } else {
+                       /* Use two timers. */
+                       div1 = devpriv->cached_div1;
+                       div2 = devpriv->cached_div2;
+                       pci224_cascade_ns_to_timer(TIMEBASE_10MHZ, &div1, &div2,
+                               &ns, round_mode);
+               }
+
+               /*
+                * The output of timer Z2-0 will be used as the scan trigger
+                * source.
+                */
+               /* Make sure Z2-0 is gated on.  */
+               outb(GAT_CONFIG(0, GAT_VCC),
+                       devpriv->iobase1 + PCI224_ZGAT_SCE);
+               if (div1 == 1) {
+                       /* Not cascading.  Z2-0 needs 10 MHz clock. */
+                       outb(CLK_CONFIG(0, CLK_10MHZ),
+                               devpriv->iobase1 + PCI224_ZCLK_SCE);
+               } else {
+                       /* Cascading with Z2-2. */
+                       /* Make sure Z2-2 is gated on.  */
+                       outb(GAT_CONFIG(2, GAT_VCC),
+                               devpriv->iobase1 + PCI224_ZGAT_SCE);
+                       /* Z2-2 needs 10 MHz clock. */
+                       outb(CLK_CONFIG(2, CLK_10MHZ),
+                               devpriv->iobase1 + PCI224_ZCLK_SCE);
+                       /* Load Z2-2 mode (2) and counter (div1). */
+                       i8254_load(devpriv->iobase1 + PCI224_Z2_CT0, 0,
+                               2, div1, 2);
+                       /* Z2-0 is clocked from Z2-2's output. */
+                       outb(CLK_CONFIG(0, CLK_OUTNM1),
+                               devpriv->iobase1 + PCI224_ZCLK_SCE);
+               }
+               /* Load Z2-0 mode (2) and counter (div2). */
+               i8254_load(devpriv->iobase1 + PCI224_Z2_CT0, 0, 0, div2, 2);
+       }
+
+       /*
+        * Sort out end of acquisition.
+        */
+       switch (cmd->stop_src) {
+       case TRIG_COUNT:
+               /* Fixed number of scans.  */
+               devpriv->ao_stop_continuous = 0;
+               devpriv->ao_stop_count = cmd->stop_arg;
+               break;
+       default:
+               /* Continuous scans. */
+               devpriv->ao_stop_continuous = 1;
+               devpriv->ao_stop_count = 0;
+               break;
+       }
+
+       /*
+        * Sort out start of acquisition.
+        */
+       switch (cmd->start_src) {
+       case TRIG_INT:
+               comedi_spin_lock_irqsave(&devpriv->ao_spinlock, flags);
+               s->async->inttrig = &pci224_ao_inttrig_start;
+               comedi_spin_unlock_irqrestore(&devpriv->ao_spinlock, flags);
+               break;
+       case TRIG_EXT:
+               /* Enable external interrupt trigger to start acquisition. */
+               comedi_spin_lock_irqsave(&devpriv->ao_spinlock, flags);
+               devpriv->intsce |= PCI224_INTR_EXT;
+               outb(devpriv->intsce, devpriv->iobase1 + PCI224_INT_SCE);
+               comedi_spin_unlock_irqrestore(&devpriv->ao_spinlock, flags);
+               break;
+       }
+
+       return 0;
+}
+
+/*
+ * 'cancel' function for AO subdevice.
+ */
+static int pci224_ao_cancel(comedi_device * dev, comedi_subdevice * s)
+{
+       pci224_ao_stop(dev, s);
+       return 0;
+}
+
+/*
+ * 'munge' data for AO command.
+ */
+static void
+pci224_ao_munge(comedi_device * dev, comedi_subdevice * s, void *data,
+       unsigned int num_bytes, unsigned int chan_index)
+{
+       comedi_async *async = s->async;
+       sampl_t *array = data;
+       unsigned int length = num_bytes / sizeof(*array);
+       unsigned int offset;
+       unsigned int shift;
+       unsigned int i;
+
+       /* The hardware expects 16-bit numbers. */
+       shift = 16 - thisboard->ao_bits;
+       /* Channels will be all bipolar or all unipolar. */
+       if ((devpriv->hwrange[CR_RANGE(async->cmd.chanlist[0])] &
+                       PCI224_DACCON_POLAR_MASK) == PCI224_DACCON_POLAR_UNI) {
+               /* Unipolar */
+               offset = 0;
+       } else {
+               /* Bipolar */
+               offset = 32768;
+       }
+       /* Munge the data. */
+       for (i = 0; i < length; i++) {
+               array[i] = (array[i] << shift) - offset;
+       }
+}
+
+/*
+ * Interrupt handler.
+ */
+static irqreturn_t pci224_interrupt(int irq, void *d PT_REGS_ARG)
+{
+       comedi_device *dev = d;
+       comedi_subdevice *s = &dev->subdevices[0];
+       comedi_cmd *cmd;
+       unsigned char intstat, valid_intstat;
+       unsigned char curenab;
+       int retval = 0;
+       unsigned long flags;
+
+       intstat = inb(devpriv->iobase1 + PCI224_INT_SCE) & 0x3F;
+       if (intstat) {
+               retval = 1;
+               comedi_spin_lock_irqsave(&devpriv->ao_spinlock, flags);
+               valid_intstat = devpriv->intsce & intstat;
+               /* Temporarily disable interrupt sources. */
+               curenab = devpriv->intsce & ~intstat;
+               outb(curenab, devpriv->iobase1 + PCI224_INT_SCE);
+               devpriv->intr_running = 1;
+               devpriv->intr_cpuid = THISCPU;
+               comedi_spin_unlock_irqrestore(&devpriv->ao_spinlock, flags);
+               if (valid_intstat != 0) {
+                       cmd = &s->async->cmd;
+                       if (valid_intstat & PCI224_INTR_EXT) {
+                               devpriv->intsce &= ~PCI224_INTR_EXT;
+                               if (cmd->start_src == TRIG_EXT) {
+                                       pci224_ao_start(dev, s);
+                               } else if (cmd->stop_src == TRIG_EXT) {
+                                       pci224_ao_stop(dev, s);
+                               }
+                       }
+                       if (valid_intstat & PCI224_INTR_DAC) {
+                               pci224_ao_handle_fifo(dev, s);
+                       }
+               }
+               /* Reenable interrupt sources. */
+               comedi_spin_lock_irqsave(&devpriv->ao_spinlock, flags);
+               if (curenab != devpriv->intsce) {
+                       outb(devpriv->intsce,
+                               devpriv->iobase1 + PCI224_INT_SCE);
+               }
+               devpriv->intr_running = 0;
+               comedi_spin_unlock_irqrestore(&devpriv->ao_spinlock, flags);
+       }
+       return IRQ_RETVAL(retval);
+}
+
+/*
+ * This function looks for a PCI device matching the requested board name,
+ * bus and slot.
+ */
+static int
+pci224_find_pci(comedi_device * dev, int bus, int slot,
+       struct pci_dev **pci_dev_p)
+{
+       struct pci_dev *pci_dev = NULL;
+
+       *pci_dev_p = NULL;
+
+       /* Look for matching PCI device. */
+       for (pci_dev = pci_get_device(PCI_VENDOR_ID_AMPLICON, PCI_ANY_ID, NULL);
+               pci_dev != NULL;
+               pci_dev = pci_get_device(PCI_VENDOR_ID_AMPLICON, PCI_ANY_ID,
+                       pci_dev)) {
+               /* If bus/slot specified, check them. */
+               if (bus || slot) {
+                       if (bus != pci_dev->bus->number
+                               || slot != PCI_SLOT(pci_dev->devfn))
+                               continue;
+               }
+               if (thisboard->model == any_model) {
+                       /* Match any supported model. */
+                       int i;
+
+                       for (i = 0; i < ARRAY_SIZE(pci224_boards); i++) {
+                               if (pci_dev->device == pci224_boards[i].devid) {
+                                       /* Change board_ptr to matched board. */
+                                       dev->board_ptr = &pci224_boards[i];
+                                       break;
+                               }
+                       }
+                       if (i == ARRAY_SIZE(pci224_boards))
+                               continue;
+               } else {
+                       /* Match specific model name. */
+                       if (thisboard->devid != pci_dev->device)
+                               continue;
+               }
+
+               /* Found a match. */
+               *pci_dev_p = pci_dev;
+               return 0;
+       }
+       /* No match found. */
+       if (bus || slot) {
+               printk(KERN_ERR "comedi%d: error! "
+                       "no %s found at pci %02x:%02x!\n",
+                       dev->minor, thisboard->name, bus, slot);
+       } else {
+               printk(KERN_ERR "comedi%d: error! no %s found!\n",
+                       dev->minor, thisboard->name);
+       }
+       return -EIO;
+}
+
+/*
+ * 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 pci224_attach(comedi_device * dev, comedi_devconfig * it)
+{
+       comedi_subdevice *s;
+       struct pci_dev *pci_dev;
+       unsigned int irq;
+       int bus = 0, slot = 0;
+       unsigned n;
+       int ret;
+
+       printk(KERN_DEBUG "comedi%d: %s: attach\n", dev->minor, DRIVER_NAME);
+
+       bus = it->options[0];
+       slot = it->options[1];
+       if ((ret = alloc_private(dev, sizeof(pci224_private))) < 0) {
+               printk(KERN_ERR "comedi%d: error! out of memory!\n",
+                       dev->minor);
+               return ret;
+       }
+       if ((ret = pci224_find_pci(dev, bus, slot, &pci_dev)) < 0)
+               return ret;
+       devpriv->pci_dev = pci_dev;
+
+       if ((ret = comedi_pci_enable(pci_dev, DRIVER_NAME)) < 0) {
+               printk(KERN_ERR
+                       "comedi%d: error! cannot enable PCI device "
+                       "and request regions!\n", dev->minor);
+               return ret;
+       }
+       spin_lock_init(&devpriv->ao_spinlock);
+
+       devpriv->iobase1 = pci_resource_start(pci_dev, 2);
+       dev->iobase = pci_resource_start(pci_dev, 3);
+       irq = pci_dev->irq;
+
+       /* Allocate readback buffer for AO channels. */
+       devpriv->ao_readback = kmalloc(sizeof(devpriv->ao_readback[0]) *
+               thisboard->ao_chans, GFP_KERNEL);
+       if (!devpriv->ao_readback) {
+               return -ENOMEM;
+       }
+
+       /* Allocate buffer to hold values for AO channel scan. */
+       devpriv->ao_scan_vals = kmalloc(sizeof(devpriv->ao_scan_vals[0]) *
+               thisboard->ao_chans, GFP_KERNEL);
+       if (!devpriv->ao_scan_vals) {
+               return -ENOMEM;
+       }
+
+       /* Allocate buffer to hold AO channel scan order. */
+       devpriv->ao_scan_order = kmalloc(sizeof(devpriv->ao_scan_order[0]) *
+               thisboard->ao_chans, GFP_KERNEL);
+       if (!devpriv->ao_scan_order) {
+               return -ENOMEM;
+       }
+
+       /* Disable interrupt sources. */
+       devpriv->intsce = 0;
+       outb(0, devpriv->iobase1 + PCI224_INT_SCE);
+
+       /* Initialize the DAC hardware. */
+       outw(PCI224_DACCON_GLOBALRESET, dev->iobase + PCI224_DACCON);
+       outw(0, dev->iobase + PCI224_DACCEN);
+       outw(0, dev->iobase + PCI224_FIFOSIZ);
+       devpriv->daccon = (PCI224_DACCON_TRIG_SW | PCI224_DACCON_POLAR_BI |
+               PCI224_DACCON_FIFOENAB | PCI224_DACCON_FIFOINTR_EMPTY);
+       outw(devpriv->daccon | PCI224_DACCON_FIFORESET,
+               dev->iobase + PCI224_DACCON);
+
+       /* Allocate subdevices.  There is only one!  */
+       if ((ret = alloc_subdevices(dev, 1)) < 0) {
+               printk(KERN_ERR "comedi%d: error! out of memory!\n",
+                       dev->minor);
+               return ret;
+       }
+
+       s = dev->subdevices + 0;
+       /* Analog output subdevice. */
+       s->type = COMEDI_SUBD_AO;
+       s->subdev_flags = SDF_WRITABLE | SDF_GROUND | SDF_CMD_WRITE;
+       s->n_chan = thisboard->ao_chans;
+       s->maxdata = (1 << thisboard->ao_bits) - 1;
+       s->insn_write = &pci224_ao_insn_write;
+       s->insn_read = &pci224_ao_insn_read;
+       s->len_chanlist = s->n_chan;
+
+       dev->write_subdev = s;
+       s->do_cmd = &pci224_ao_cmd;
+       s->do_cmdtest = &pci224_ao_cmdtest;
+       s->cancel = &pci224_ao_cancel;
+       s->munge = &pci224_ao_munge;
+
+       /* Sort out channel range options. */
+       if (thisboard->model == pci234_model) {
+               /* PCI234 range options. */
+               const comedi_lrange **range_table_list;
+
+               s->range_table_list = range_table_list =
+                       kmalloc(sizeof(comedi_lrange *) * s->n_chan,
+                       GFP_KERNEL);
+               if (!s->range_table_list) {
+                       return -ENOMEM;
+               }
+               for (n = 2; n < 3 + s->n_chan; n++) {
+                       if (it->options[n] < 0 || it->options[n] > 1) {
+                               printk(KERN_WARNING "comedi%d: %s: warning! "
+                                       "bad options[%u]=%d\n",
+                                       dev->minor, DRIVER_NAME, n,
+                                       it->options[n]);
+                       }
+               }
+               for (n = 0; n < s->n_chan; n++) {
+                       if (n < COMEDI_NDEVCONFOPTS - 3 &&
+                               it->options[3 + n] == 1) {
+                               if (it->options[2] == 1) {
+                                       range_table_list[n] = &range_pci234_ext;
+                               } else {
+                                       range_table_list[n] = &range_bipolar5;
+                               }
+                       } else {
+                               if (it->options[2] == 1) {
+                                       range_table_list[n] =
+                                               &range_pci234_ext2;
+                               } else {
+                                       range_table_list[n] = &range_bipolar10;
+                               }
+                       }
+               }
+               devpriv->hwrange = hwrange_pci234;
+       } else {
+               /* PCI224 range options. */
+               if (it->options[2] == 1) {
+                       s->range_table = &range_pci224_external;
+                       devpriv->hwrange = hwrange_pci224_external;
+               } else {
+                       if (it->options[2] != 0) {
+                               printk(KERN_WARNING "comedi%d: %s: warning! "
+                                       "bad options[2]=%d\n",
+                                       dev->minor, DRIVER_NAME,
+                                       it->options[2]);
+                       }
+                       s->range_table = &range_pci224_internal;
+                       devpriv->hwrange = hwrange_pci224_internal;
+               }
+       }
+
+       dev->board_name = thisboard->name;
+
+       if (irq) {
+               ret = comedi_request_irq(irq, pci224_interrupt, IRQF_SHARED,
+                       DRIVER_NAME, dev);
+               if (ret < 0) {
+                       printk(KERN_ERR "comedi%d: error! "
+                               "unable to allocate irq %u\n", dev->minor, irq);
+                       return ret;
+               } else {
+                       dev->irq = irq;
+               }
+       }
+
+       printk(KERN_INFO "comedi%d: %s ", dev->minor, dev->board_name);
+       printk("(pci %s) ", pci_name(pci_dev));
+       if (irq) {
+               printk("(irq %u%s) ", irq, (dev->irq ? "" : " UNAVAILABLE"));
+       } else {
+               printk("(no irq) ");
+       }
+
+       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 pci224_detach(comedi_device * dev)
+{
+       printk(KERN_DEBUG "comedi%d: %s: detach\n", dev->minor, DRIVER_NAME);
+
+       if (dev->irq) {
+               comedi_free_irq(dev->irq, dev);
+       }
+       if (dev->subdevices) {
+               comedi_subdevice *s;
+
+               s = dev->subdevices + 0;
+               /* AO subdevice */
+               if (s->range_table_list) {
+                       kfree(s->range_table_list);
+               }
+       }
+       if (devpriv) {
+               if (devpriv->ao_readback) {
+                       kfree(devpriv->ao_readback);
+               }
+               if (devpriv->ao_scan_vals) {
+                       kfree(devpriv->ao_scan_vals);
+               }
+               if (devpriv->ao_scan_order) {
+                       kfree(devpriv->ao_scan_order);
+               }
+               if (devpriv->pci_dev) {
+                       if (dev->iobase) {
+                               comedi_pci_disable(devpriv->pci_dev);
+                       }
+                       pci_dev_put(devpriv->pci_dev);
+               }
+       }
+       if (dev->board_name) {
+               printk(KERN_INFO "comedi%d: %s removed\n",
+                       dev->minor, dev->board_name);
+       }
+
+       return 0;
+}