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