Staging: comedi: add pcmmio and pcmuio drivers

Drivers for Winsystems PC-104 based multifunction IO board and 48
channel and 98 channel dio boards

From: Calin Culianu <calin@ajvar.org>
Cc: 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>

diff --git a/drivers/staging/comedi/drivers/pcmmio.c b/drivers/staging/comedi/drivers/pcmmio.c
new file mode 100644
index 0000000..5673b66
--- /dev/null
+++ b/drivers/staging/comedi/drivers/pcmmio.c
@@ -0,0 +1,1333 @@
+/*
+    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
+Description: A driver for the PCM-MIO multifunction board
+Devices: [Winsystems] PCM-MIO (pcmmio)
+Author: Calin Culianu <calin@ajvar.org>
+Updated: Wed, May 16 2007 16:21:10 -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 "../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 {
+	const char *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;
+	const comedi_lrange *ai_range_table, *ao_range_table;
+	comedi_insn_fn_t ai_rinsn, ao_rinsn, ao_winsn;
+} pcmmio_board;
+
+static const comedi_lrange ranges_ai =
+	{ 4, {RANGE(-5., 5.), RANGE(-10., 10.), RANGE(0., 5.), RANGE(0.,
+		10.)}
+};
+
+static const comedi_lrange 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 const 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:&ranges_ai,
+	      ao_range_table:&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 ((const 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:&pcmmio_boards[0].name,
+      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 PT_REGS_ARG);
+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);
+#ifdef notused
+static void lock_port(comedi_device * dev, int asic, int port);
+static void unlock_port(comedi_device * dev, int asic, int port);
+#endif
+
+/*
+ * 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 =
+		kcalloc(n_subdevs, sizeof(pcmmio_subdev_private), GFP_KERNEL);
+	if (!devpriv->sprivs) {
+		printk("cannot allocate subdevice private data structures\n");
+		return -ENOMEM;
+	}
+	/*
+	 * 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;
+	}
+
+	/* 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,
+				IRQF_SHARED, 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);
+}
+
+#ifdef notused
+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;
+}
+
+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);
+}
+#endif /* notused */
+
+static irqreturn_t interrupt_pcmmio(int irq, void *d PT_REGS_ARG)
+{
+	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);
+						}
+
+					}
+
+				}
+			}
+
+		}
+	}
+	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);
+	}
+
+	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);
+	}
+
+	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 */
+
+		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 */
+
+		adc_wait_ready(iobase + iooffset);
+
+		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);