root/drivers/comedi/drivers/amplc_pci230.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * comedi/drivers/amplc_pci230.c
 * Driver for Amplicon PCI230 and PCI260 Multifunction I/O boards.
 *
 * Copyright (C) 2001 Allan Willcox <allanwillcox@ozemail.com.au>
 *
 * COMEDI - Linux Control and Measurement Device Interface
 * Copyright (C) 2000 David A. Schleef <ds@schleef.org>
 */

/*
 * Driver: amplc_pci230
 * Description: Amplicon PCI230, PCI260 Multifunction I/O boards
 * Author: Allan Willcox <allanwillcox@ozemail.com.au>,
 *   Steve D Sharples <steve.sharples@nottingham.ac.uk>,
 *   Ian Abbott <abbotti@mev.co.uk>
 * Updated: Mon, 01 Sep 2014 10:09:16 +0000
 * Devices: [Amplicon] PCI230 (amplc_pci230), PCI230+, PCI260, PCI260+
 * Status: works
 *
 * Configuration options:
 *   none
 *
 * Manual configuration of PCI cards is not supported; they are configured
 * automatically.
 *
 * The PCI230+ and PCI260+ have the same PCI device IDs as the PCI230 and
 * PCI260, but can be distinguished by the size of the PCI regions.  A
 * card will be configured as a "+" model if detected as such.
 *
 * Subdevices:
 *
 *                 PCI230(+)    PCI260(+)
 *                 ---------    ---------
 *   Subdevices       3            1
 *         0          AI           AI
 *         1          AO
 *         2          DIO
 *
 * AI Subdevice:
 *
 *   The AI subdevice has 16 single-ended channels or 8 differential
 *   channels.
 *
 *   The PCI230 and PCI260 cards have 12-bit resolution.  The PCI230+ and
 *   PCI260+ cards have 16-bit resolution.
 *
 *   For differential mode, use inputs 2N and 2N+1 for channel N (e.g. use
 *   inputs 14 and 15 for channel 7).  If the card is physically a PCI230
 *   or PCI260 then it actually uses a "pseudo-differential" mode where the
 *   inputs are sampled a few microseconds apart.  The PCI230+ and PCI260+
 *   use true differential sampling.  Another difference is that if the
 *   card is physically a PCI230 or PCI260, the inverting input is 2N,
 *   whereas for a PCI230+ or PCI260+ the inverting input is 2N+1.  So if a
 *   PCI230 is physically replaced by a PCI230+ (or a PCI260 with a
 *   PCI260+) and differential mode is used, the differential inputs need
 *   to be physically swapped on the connector.
 *
 *   The following input ranges are supported:
 *
 *     0 => [-10, +10] V
 *     1 => [-5, +5] V
 *     2 => [-2.5, +2.5] V
 *     3 => [-1.25, +1.25] V
 *     4 => [0, 10] V
 *     5 => [0, 5] V
 *     6 => [0, 2.5] V
 *
 * AI Commands:
 *
 *   +=========+==============+===========+============+==========+
 *   |start_src|scan_begin_src|convert_src|scan_end_src| stop_src |
 *   +=========+==============+===========+============+==========+
 *   |TRIG_NOW | TRIG_FOLLOW  |TRIG_TIMER | TRIG_COUNT |TRIG_NONE |
 *   |TRIG_INT |              |TRIG_EXT(3)|            |TRIG_COUNT|
 *   |         |              |TRIG_INT   |            |          |
 *   |         |--------------|-----------|            |          |
 *   |         | TRIG_TIMER(1)|TRIG_TIMER |            |          |
 *   |         | TRIG_EXT(2)  |           |            |          |
 *   |         | TRIG_INT     |           |            |          |
 *   +---------+--------------+-----------+------------+----------+
 *
 *   Note 1: If AI command and AO command are used simultaneously, only
 *           one may have scan_begin_src == TRIG_TIMER.
 *
 *   Note 2: For PCI230 and PCI230+, scan_begin_src == TRIG_EXT uses
 *           DIO channel 16 (pin 49) which will need to be configured as
 *           a digital input.  For PCI260+, the EXTTRIG/EXTCONVCLK input
 *           (pin 17) is used instead.  For PCI230, scan_begin_src ==
 *           TRIG_EXT is not supported.  The trigger is a rising edge
 *           on the input.
 *
 *   Note 3: For convert_src == TRIG_EXT, the EXTTRIG/EXTCONVCLK input
 *           (pin 25 on PCI230(+), pin 17 on PCI260(+)) is used.  The
 *           convert_arg value is interpreted as follows:
 *
 *             convert_arg == (CR_EDGE | 0) => rising edge
 *             convert_arg == (CR_EDGE | CR_INVERT | 0) => falling edge
 *             convert_arg == 0 => falling edge (backwards compatibility)
 *             convert_arg == 1 => rising edge (backwards compatibility)
 *
 *   All entries in the channel list must use the same analogue reference.
 *   If the analogue reference is not AREF_DIFF (not differential) each
 *   pair of channel numbers (0 and 1, 2 and 3, etc.) must use the same
 *   input range.  The input ranges used in the sequence must be all
 *   bipolar (ranges 0 to 3) or all unipolar (ranges 4 to 6).  The channel
 *   sequence must consist of 1 or more identical subsequences.  Within the
 *   subsequence, channels must be in ascending order with no repeated
 *   channels.  For example, the following sequences are valid: 0 1 2 3
 *   (single valid subsequence), 0 2 3 5 0 2 3 5 (repeated valid
 *   subsequence), 1 1 1 1 (repeated valid subsequence).  The following
 *   sequences are invalid: 0 3 2 1 (invalid subsequence), 0 2 3 5 0 2 3
 *   (incompletely repeated subsequence).  Some versions of the PCI230+ and
 *   PCI260+ have a bug that requires a subsequence longer than one entry
 *   long to include channel 0.
 *
 * AO Subdevice:
 *
 *   The AO subdevice has 2 channels with 12-bit resolution.
 *   The following output ranges are supported:
 *     0 => [0, 10] V
 *     1 => [-10, +10] V
 *
 * AO Commands:
 *
 *   +=========+==============+===========+============+==========+
 *   |start_src|scan_begin_src|convert_src|scan_end_src| stop_src |
 *   +=========+==============+===========+============+==========+
 *   |TRIG_INT | TRIG_TIMER(1)| TRIG_NOW  | TRIG_COUNT |TRIG_NONE |
 *   |         | TRIG_EXT(2)  |           |            |TRIG_COUNT|
 *   |         | TRIG_INT     |           |            |          |
 *   +---------+--------------+-----------+------------+----------+
 *
 *   Note 1: If AI command and AO command are used simultaneously, only
 *           one may have scan_begin_src == TRIG_TIMER.
 *
 *   Note 2: scan_begin_src == TRIG_EXT is only supported if the card is
 *           configured as a PCI230+ and is only supported on later
 *           versions of the card.  As a card configured as a PCI230+ is
 *           not guaranteed to support external triggering, please consider
 *           this support to be a bonus.  It uses the EXTTRIG/ EXTCONVCLK
 *           input (PCI230+ pin 25).  Triggering will be on the rising edge
 *           unless the CR_INVERT flag is set in scan_begin_arg.
 *
 *   The channels in the channel sequence must be in ascending order with
 *   no repeats.  All entries in the channel sequence must use the same
 *   output range.
 *
 * DIO Subdevice:
 *
 *   The DIO subdevice is a 8255 chip providing 24 DIO channels.  The DIO
 *   channels are configurable as inputs or outputs in four groups:
 *
 *     Port A  - channels  0 to  7
 *     Port B  - channels  8 to 15
 *     Port CL - channels 16 to 19
 *     Port CH - channels 20 to 23
 *
 *   Only mode 0 of the 8255 chip is supported.
 *
 *   Bit 0 of port C (DIO channel 16) is also used as an external scan
 *   trigger input for AI commands on PCI230 and PCI230+, so would need to
 *   be configured as an input to use it for that purpose.
 */

/*
 * Extra triggered scan functionality, interrupt bug-fix added by Steve
 * Sharples.  Support for PCI230+/260+, more triggered scan functionality,
 * and workarounds for (or detection of) various hardware problems added
 * by Ian Abbott.
 */

#include <linux/module.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/comedi/comedi_pci.h>
#include <linux/comedi/comedi_8255.h>
#include <linux/comedi/comedi_8254.h>

/*
 * PCI230 PCI configuration register information
 */
#define PCI_DEVICE_ID_PCI230 0x0000
#define PCI_DEVICE_ID_PCI260 0x0006

/*
 * PCI230 i/o space 1 registers.
 */
#define PCI230_PPI_X_BASE       0x00    /* User PPI (82C55) base */
#define PCI230_PPI_X_A          0x00    /* User PPI (82C55) port A */
#define PCI230_PPI_X_B          0x01    /* User PPI (82C55) port B */
#define PCI230_PPI_X_C          0x02    /* User PPI (82C55) port C */
#define PCI230_PPI_X_CMD        0x03    /* User PPI (82C55) control word */
#define PCI230_Z2_CT_BASE       0x14    /* 82C54 counter/timer base */
#define PCI230_ZCLK_SCE         0x1A    /* Group Z Clock Configuration */
#define PCI230_ZGAT_SCE         0x1D    /* Group Z Gate Configuration */
#define PCI230_INT_SCE          0x1E    /* Interrupt source mask (w) */
#define PCI230_INT_STAT         0x1E    /* Interrupt status (r) */

/*
 * PCI230 i/o space 2 registers.
 */
#define PCI230_DACCON           0x00    /* DAC control */
#define PCI230_DACOUT1          0x02    /* DAC channel 0 (w) */
#define PCI230_DACOUT2          0x04    /* DAC channel 1 (w) (not FIFO mode) */
#define PCI230_ADCDATA          0x08    /* ADC data (r) */
#define PCI230_ADCSWTRIG        0x08    /* ADC software trigger (w) */
#define PCI230_ADCCON           0x0A    /* ADC control */
#define PCI230_ADCEN            0x0C    /* ADC channel enable bits */
#define PCI230_ADCG             0x0E    /* ADC gain control bits */
/* PCI230+ i/o space 2 additional registers. */
#define PCI230P_ADCTRIG         0x10    /* ADC start acquisition trigger */
#define PCI230P_ADCTH           0x12    /* ADC analog trigger threshold */
#define PCI230P_ADCFFTH         0x14    /* ADC FIFO interrupt threshold */
#define PCI230P_ADCFFLEV        0x16    /* ADC FIFO level (r) */
#define PCI230P_ADCPTSC         0x18    /* ADC pre-trigger sample count (r) */
#define PCI230P_ADCHYST         0x1A    /* ADC analog trigger hysteresys */
#define PCI230P_EXTFUNC         0x1C    /* Extended functions */
#define PCI230P_HWVER           0x1E    /* Hardware version (r) */
/* PCI230+ hardware version 2 onwards. */
#define PCI230P2_DACDATA        0x02    /* DAC data (FIFO mode) (w) */
#define PCI230P2_DACSWTRIG      0x02    /* DAC soft trigger (FIFO mode) (r) */
#define PCI230P2_DACEN          0x06    /* DAC channel enable (FIFO mode) */

/*
 * DACCON read-write values.
 */
#define PCI230_DAC_OR(x)                (((x) & 0x1) << 0)
#define PCI230_DAC_OR_UNI               PCI230_DAC_OR(0) /* Output unipolar */
#define PCI230_DAC_OR_BIP               PCI230_DAC_OR(1) /* Output bipolar */
#define PCI230_DAC_OR_MASK              PCI230_DAC_OR(1)
/*
 * The following applies only if DAC FIFO support is enabled in the EXTFUNC
 * register (and only for PCI230+ hardware version 2 onwards).
 */
#define PCI230P2_DAC_FIFO_EN            BIT(8) /* FIFO enable */
/*
 * The following apply only if the DAC FIFO is enabled (and only for PCI230+
 * hardware version 2 onwards).
 */
#define PCI230P2_DAC_TRIG(x)            (((x) & 0x7) << 2)
#define PCI230P2_DAC_TRIG_NONE          PCI230P2_DAC_TRIG(0) /* none */
#define PCI230P2_DAC_TRIG_SW            PCI230P2_DAC_TRIG(1) /* soft trig */
#define PCI230P2_DAC_TRIG_EXTP          PCI230P2_DAC_TRIG(2) /* ext + edge */
#define PCI230P2_DAC_TRIG_EXTN          PCI230P2_DAC_TRIG(3) /* ext - edge */
#define PCI230P2_DAC_TRIG_Z2CT0         PCI230P2_DAC_TRIG(4) /* Z2 CT0 out */
#define PCI230P2_DAC_TRIG_Z2CT1         PCI230P2_DAC_TRIG(5) /* Z2 CT1 out */
#define PCI230P2_DAC_TRIG_Z2CT2         PCI230P2_DAC_TRIG(6) /* Z2 CT2 out */
#define PCI230P2_DAC_TRIG_MASK          PCI230P2_DAC_TRIG(7)
#define PCI230P2_DAC_FIFO_WRAP          BIT(7) /* FIFO wraparound mode */
#define PCI230P2_DAC_INT_FIFO(x)        (((x) & 7) << 9)
#define PCI230P2_DAC_INT_FIFO_EMPTY     PCI230P2_DAC_INT_FIFO(0) /* empty */
#define PCI230P2_DAC_INT_FIFO_NEMPTY    PCI230P2_DAC_INT_FIFO(1) /* !empty */
#define PCI230P2_DAC_INT_FIFO_NHALF     PCI230P2_DAC_INT_FIFO(2) /* !half */
#define PCI230P2_DAC_INT_FIFO_HALF      PCI230P2_DAC_INT_FIFO(3) /* half */
#define PCI230P2_DAC_INT_FIFO_NFULL     PCI230P2_DAC_INT_FIFO(4) /* !full */
#define PCI230P2_DAC_INT_FIFO_FULL      PCI230P2_DAC_INT_FIFO(5) /* full */
#define PCI230P2_DAC_INT_FIFO_MASK      PCI230P2_DAC_INT_FIFO(7)

/*
 * DACCON read-only values.
 */
#define PCI230_DAC_BUSY                 BIT(1) /* DAC busy. */
/*
 * The following apply only if the DAC FIFO is enabled (and only for PCI230+
 * hardware version 2 onwards).
 */
#define PCI230P2_DAC_FIFO_UNDERRUN_LATCHED      BIT(5) /* Underrun error */
#define PCI230P2_DAC_FIFO_EMPTY         BIT(13) /* FIFO empty */
#define PCI230P2_DAC_FIFO_FULL          BIT(14) /* FIFO full */
#define PCI230P2_DAC_FIFO_HALF          BIT(15) /* FIFO half full */

/*
 * DACCON write-only, transient values.
 */
/*
 * The following apply only if the DAC FIFO is enabled (and only for PCI230+
 * hardware version 2 onwards).
 */
#define PCI230P2_DAC_FIFO_UNDERRUN_CLEAR        BIT(5) /* Clear underrun */
#define PCI230P2_DAC_FIFO_RESET         BIT(12) /* FIFO reset */

/*
 * PCI230+ hardware version 2 DAC FIFO levels.
 */
#define PCI230P2_DAC_FIFOLEVEL_HALF     512
#define PCI230P2_DAC_FIFOLEVEL_FULL     1024
/* Free space in DAC FIFO. */
#define PCI230P2_DAC_FIFOROOM_EMPTY             PCI230P2_DAC_FIFOLEVEL_FULL
#define PCI230P2_DAC_FIFOROOM_ONETOHALF         \
        (PCI230P2_DAC_FIFOLEVEL_FULL - PCI230P2_DAC_FIFOLEVEL_HALF)
#define PCI230P2_DAC_FIFOROOM_HALFTOFULL        1
#define PCI230P2_DAC_FIFOROOM_FULL              0

/*
 * ADCCON read/write values.
 */
#define PCI230_ADC_TRIG(x)              (((x) & 0x7) << 0)
#define PCI230_ADC_TRIG_NONE            PCI230_ADC_TRIG(0) /* none */
#define PCI230_ADC_TRIG_SW              PCI230_ADC_TRIG(1) /* soft trig */
#define PCI230_ADC_TRIG_EXTP            PCI230_ADC_TRIG(2) /* ext + edge */
#define PCI230_ADC_TRIG_EXTN            PCI230_ADC_TRIG(3) /* ext - edge */
#define PCI230_ADC_TRIG_Z2CT0           PCI230_ADC_TRIG(4) /* Z2 CT0 out*/
#define PCI230_ADC_TRIG_Z2CT1           PCI230_ADC_TRIG(5) /* Z2 CT1 out */
#define PCI230_ADC_TRIG_Z2CT2           PCI230_ADC_TRIG(6) /* Z2 CT2 out */
#define PCI230_ADC_TRIG_MASK            PCI230_ADC_TRIG(7)
#define PCI230_ADC_IR(x)                (((x) & 0x1) << 3)
#define PCI230_ADC_IR_UNI               PCI230_ADC_IR(0) /* Input unipolar */
#define PCI230_ADC_IR_BIP               PCI230_ADC_IR(1) /* Input bipolar */
#define PCI230_ADC_IR_MASK              PCI230_ADC_IR(1)
#define PCI230_ADC_IM(x)                (((x) & 0x1) << 4)
#define PCI230_ADC_IM_SE                PCI230_ADC_IM(0) /* single ended */
#define PCI230_ADC_IM_DIF               PCI230_ADC_IM(1) /* differential */
#define PCI230_ADC_IM_MASK              PCI230_ADC_IM(1)
#define PCI230_ADC_FIFO_EN              BIT(8) /* FIFO enable */
#define PCI230_ADC_INT_FIFO(x)          (((x) & 0x7) << 9)
#define PCI230_ADC_INT_FIFO_EMPTY       PCI230_ADC_INT_FIFO(0) /* empty */
#define PCI230_ADC_INT_FIFO_NEMPTY      PCI230_ADC_INT_FIFO(1) /* !empty */
#define PCI230_ADC_INT_FIFO_NHALF       PCI230_ADC_INT_FIFO(2) /* !half */
#define PCI230_ADC_INT_FIFO_HALF        PCI230_ADC_INT_FIFO(3) /* half */
#define PCI230_ADC_INT_FIFO_NFULL       PCI230_ADC_INT_FIFO(4) /* !full */
#define PCI230_ADC_INT_FIFO_FULL        PCI230_ADC_INT_FIFO(5) /* full */
#define PCI230P_ADC_INT_FIFO_THRESH     PCI230_ADC_INT_FIFO(7) /* threshold */
#define PCI230_ADC_INT_FIFO_MASK        PCI230_ADC_INT_FIFO(7)

/*
 * ADCCON write-only, transient values.
 */
#define PCI230_ADC_FIFO_RESET           BIT(12) /* FIFO reset */
#define PCI230_ADC_GLOB_RESET           BIT(13) /* Global reset */

/*
 * ADCCON read-only values.
 */
#define PCI230_ADC_BUSY                 BIT(15) /* ADC busy */
#define PCI230_ADC_FIFO_EMPTY           BIT(12) /* FIFO empty */
#define PCI230_ADC_FIFO_FULL            BIT(13) /* FIFO full */
#define PCI230_ADC_FIFO_HALF            BIT(14) /* FIFO half full */
#define PCI230_ADC_FIFO_FULL_LATCHED    BIT(5)  /* FIFO overrun occurred */

/*
 * PCI230 ADC FIFO levels.
 */
#define PCI230_ADC_FIFOLEVEL_HALFFULL   2049    /* Value for FIFO half full */
#define PCI230_ADC_FIFOLEVEL_FULL       4096    /* FIFO size */

/*
 * PCI230+ EXTFUNC values.
 */
/* Route EXTTRIG pin to external gate inputs. */
#define PCI230P_EXTFUNC_GAT_EXTTRIG     BIT(0)
/* PCI230+ hardware version 2 values. */
/* Allow DAC FIFO to be enabled. */
#define PCI230P2_EXTFUNC_DACFIFO        BIT(1)

/*
 * 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 */

static unsigned int pci230_clk_config(unsigned int chan, unsigned int src)
{
        return ((chan & 3) << 3) | (src & 7);
}

/*
 * 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       /* external gate input (PPCn on PCI230) */
#define GAT_NOUTNM2     3       /* inverted output of channel-2 modulo total */

static unsigned int pci230_gat_config(unsigned int chan, unsigned int src)
{
        return ((chan & 3) << 3) | (src & 7);
}

/*
 * Summary of CLK_OUTNM1 and GAT_NOUTNM2 connections for PCI230 and PCI260:
 *
 *              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 enables/status register values.
 */
#define PCI230_INT_DISABLE              0
#define PCI230_INT_PPI_C0               BIT(0)
#define PCI230_INT_PPI_C3               BIT(1)
#define PCI230_INT_ADC                  BIT(2)
#define PCI230_INT_ZCLK_CT1             BIT(5)
/* For PCI230+ hardware version 2 when DAC FIFO enabled. */
#define PCI230P2_INT_DAC                BIT(4)

/*
 * (Potentially) shared resources and their owners
 */
enum {
        RES_Z2CT0 = BIT(0),     /* Z2-CT0 */
        RES_Z2CT1 = BIT(1),     /* Z2-CT1 */
        RES_Z2CT2 = BIT(2)      /* Z2-CT2 */
};

enum {
        OWNER_AICMD,            /* Owned by AI command */
        OWNER_AOCMD,            /* Owned by AO command */
        NUM_OWNERS              /* Number of owners */
};

/*
 * Handy macros.
 */

/* Combine old and new bits. */
#define COMBINE(old, new, mask) (((old) & ~(mask)) | ((new) & (mask)))

/* Current CPU.  XXX should this be hard_smp_processor_id()? */
#define THISCPU         smp_processor_id()

/*
 * Board descriptions for the two boards supported.
 */

struct pci230_board {
        const char *name;
        unsigned short id;
        unsigned char ai_bits;
        unsigned char ao_bits;
        unsigned char min_hwver; /* Minimum hardware version supported. */
        unsigned int have_dio:1;
};

static const struct pci230_board pci230_boards[] = {
        {
                .name           = "pci230+",
                .id             = PCI_DEVICE_ID_PCI230,
                .ai_bits        = 16,
                .ao_bits        = 12,
                .have_dio       = true,
                .min_hwver      = 1,
        },
        {
                .name           = "pci260+",
                .id             = PCI_DEVICE_ID_PCI260,
                .ai_bits        = 16,
                .min_hwver      = 1,
        },
        {
                .name           = "pci230",
                .id             = PCI_DEVICE_ID_PCI230,
                .ai_bits        = 12,
                .ao_bits        = 12,
                .have_dio       = true,
        },
        {
                .name           = "pci260",
                .id             = PCI_DEVICE_ID_PCI260,
                .ai_bits        = 12,
        },
};

struct pci230_private {
        spinlock_t isr_spinlock;        /* Interrupt spin lock */
        spinlock_t res_spinlock;        /* Shared resources spin lock */
        spinlock_t ai_stop_spinlock;    /* Spin lock for stopping AI command */
        spinlock_t ao_stop_spinlock;    /* Spin lock for stopping AO command */
        unsigned long daqio;            /* PCI230's DAQ I/O space */
        int intr_cpuid;                 /* ID of CPU running ISR */
        unsigned short hwver;           /* Hardware version (for '+' models) */
        unsigned short adccon;          /* ADCCON register value */
        unsigned short daccon;          /* DACCON register value */
        unsigned short adcfifothresh;   /* ADC FIFO threshold (PCI230+/260+) */
        unsigned short adcg;            /* ADCG register value */
        unsigned char ier;              /* Interrupt enable bits */
        unsigned char res_owned[NUM_OWNERS]; /* Owned resources */
        unsigned int intr_running:1;    /* Flag set in interrupt routine */
        unsigned int ai_bipolar:1;      /* Flag AI range is bipolar */
        unsigned int ao_bipolar:1;      /* Flag AO range is bipolar */
        unsigned int ai_cmd_started:1;  /* Flag AI command started */
        unsigned int ao_cmd_started:1;  /* Flag AO command started */
};

/* PCI230 clock source periods in ns */
static const unsigned int pci230_timebase[8] = {
        [CLK_10MHZ]     = I8254_OSC_BASE_10MHZ,
        [CLK_1MHZ]      = I8254_OSC_BASE_1MHZ,
        [CLK_100KHZ]    = I8254_OSC_BASE_100KHZ,
        [CLK_10KHZ]     = I8254_OSC_BASE_10KHZ,
        [CLK_1KHZ]      = I8254_OSC_BASE_1KHZ,
};

/* PCI230 analogue input range table */
static const struct comedi_lrange pci230_ai_range = {
        7, {
                BIP_RANGE(10),
                BIP_RANGE(5),
                BIP_RANGE(2.5),
                BIP_RANGE(1.25),
                UNI_RANGE(10),
                UNI_RANGE(5),
                UNI_RANGE(2.5)
        }
};

/* PCI230 analogue gain bits for each input range. */
static const unsigned char pci230_ai_gain[7] = { 0, 1, 2, 3, 1, 2, 3 };

/* PCI230 analogue output range table */
static const struct comedi_lrange pci230_ao_range = {
        2, {
                UNI_RANGE(10),
                BIP_RANGE(10)
        }
};

static unsigned short pci230_ai_read(struct comedi_device *dev)
{
        const struct pci230_board *board = dev->board_ptr;
        struct pci230_private *devpriv = dev->private;
        unsigned short data;

        /* Read sample. */
        data = inw(devpriv->daqio + PCI230_ADCDATA);
        /*
         * PCI230 is 12 bit - stored in upper bits of 16 bit register
         * (lower four bits reserved for expansion).  PCI230+ is 16 bit AI.
         *
         * If a bipolar range was specified, mangle it
         * (twos complement->straight binary).
         */
        if (devpriv->ai_bipolar)
                data ^= 0x8000;
        data >>= (16 - board->ai_bits);
        return data;
}

static unsigned short pci230_ao_mangle_datum(struct comedi_device *dev,
                                             unsigned short datum)
{
        const struct pci230_board *board = dev->board_ptr;
        struct pci230_private *devpriv = dev->private;

        /*
         * PCI230 is 12 bit - stored in upper bits of 16 bit register (lower
         * four bits reserved for expansion).  PCI230+ is also 12 bit AO.
         */
        datum <<= (16 - board->ao_bits);
        /*
         * If a bipolar range was specified, mangle it
         * (straight binary->twos complement).
         */
        if (devpriv->ao_bipolar)
                datum ^= 0x8000;
        return datum;
}

static void pci230_ao_write_nofifo(struct comedi_device *dev,
                                   unsigned short datum, unsigned int chan)
{
        struct pci230_private *devpriv = dev->private;

        /* Write mangled datum to appropriate DACOUT register. */
        outw(pci230_ao_mangle_datum(dev, datum),
             devpriv->daqio + ((chan == 0) ? PCI230_DACOUT1 : PCI230_DACOUT2));
}

static void pci230_ao_write_fifo(struct comedi_device *dev,
                                 unsigned short datum, unsigned int chan)
{
        struct pci230_private *devpriv = dev->private;

        /* Write mangled datum to appropriate DACDATA register. */
        outw(pci230_ao_mangle_datum(dev, datum),
             devpriv->daqio + PCI230P2_DACDATA);
}

static bool pci230_claim_shared(struct comedi_device *dev,
                                unsigned char res_mask, unsigned int owner)
{
        struct pci230_private *devpriv = dev->private;
        unsigned int o;
        unsigned long irqflags;

        spin_lock_irqsave(&devpriv->res_spinlock, irqflags);
        for (o = 0; o < NUM_OWNERS; o++) {
                if (o == owner)
                        continue;
                if (devpriv->res_owned[o] & res_mask) {
                        spin_unlock_irqrestore(&devpriv->res_spinlock,
                                               irqflags);
                        return false;
                }
        }
        devpriv->res_owned[owner] |= res_mask;
        spin_unlock_irqrestore(&devpriv->res_spinlock, irqflags);
        return true;
}

static void pci230_release_shared(struct comedi_device *dev,
                                  unsigned char res_mask, unsigned int owner)
{
        struct pci230_private *devpriv = dev->private;
        unsigned long irqflags;

        spin_lock_irqsave(&devpriv->res_spinlock, irqflags);
        devpriv->res_owned[owner] &= ~res_mask;
        spin_unlock_irqrestore(&devpriv->res_spinlock, irqflags);
}

static void pci230_release_all_resources(struct comedi_device *dev,
                                         unsigned int owner)
{
        pci230_release_shared(dev, (unsigned char)~0, owner);
}

static unsigned int pci230_divide_ns(u64 ns, unsigned int timebase,
                                     unsigned int flags)
{
        u64 div;
        unsigned int rem;

        div = ns;
        rem = do_div(div, timebase);
        switch (flags & CMDF_ROUND_MASK) {
        default:
        case CMDF_ROUND_NEAREST:
                div += DIV_ROUND_CLOSEST(rem, timebase);
                break;
        case CMDF_ROUND_DOWN:
                break;
        case CMDF_ROUND_UP:
                div += DIV_ROUND_UP(rem, timebase);
                break;
        }
        return div > UINT_MAX ? UINT_MAX : (unsigned int)div;
}

/*
 * Given desired period in ns, returns the required internal clock source
 * and gets the initial count.
 */
static unsigned int pci230_choose_clk_count(u64 ns, unsigned int *count,
                                            unsigned int flags)
{
        unsigned int clk_src, cnt;

        for (clk_src = CLK_10MHZ;; clk_src++) {
                cnt = pci230_divide_ns(ns, pci230_timebase[clk_src], flags);
                if (cnt <= 65536 || clk_src == CLK_1KHZ)
                        break;
        }
        *count = cnt;
        return clk_src;
}

static void pci230_ns_to_single_timer(unsigned int *ns, unsigned int flags)
{
        unsigned int count;
        unsigned int clk_src;

        clk_src = pci230_choose_clk_count(*ns, &count, flags);
        *ns = count * pci230_timebase[clk_src];
}

static void pci230_ct_setup_ns_mode(struct comedi_device *dev, unsigned int ct,
                                    unsigned int mode, u64 ns,
                                    unsigned int flags)
{
        unsigned int clk_src;
        unsigned int count;

        /* Set mode. */
        comedi_8254_set_mode(dev->pacer, ct, mode);
        /* Determine clock source and count. */
        clk_src = pci230_choose_clk_count(ns, &count, flags);
        /* Program clock source. */
        outb(pci230_clk_config(ct, clk_src), dev->iobase + PCI230_ZCLK_SCE);
        /* Set initial count. */
        if (count >= 65536)
                count = 0;

        comedi_8254_write(dev->pacer, ct, count);
}

static void pci230_cancel_ct(struct comedi_device *dev, unsigned int ct)
{
        /* Counter ct, 8254 mode 1, initial count not written. */
        comedi_8254_set_mode(dev->pacer, ct, I8254_MODE1);
}

static int pci230_ai_eoc(struct comedi_device *dev,
                         struct comedi_subdevice *s,
                         struct comedi_insn *insn,
                         unsigned long context)
{
        struct pci230_private *devpriv = dev->private;
        unsigned int status;

        status = inw(devpriv->daqio + PCI230_ADCCON);
        if ((status & PCI230_ADC_FIFO_EMPTY) == 0)
                return 0;
        return -EBUSY;
}

static int pci230_ai_insn_read(struct comedi_device *dev,
                               struct comedi_subdevice *s,
                               struct comedi_insn *insn, unsigned int *data)
{
        struct pci230_private *devpriv = dev->private;
        unsigned int n;
        unsigned int chan, range, aref;
        unsigned int gainshift;
        unsigned short adccon, adcen;
        int ret;

        /* Unpack channel and range. */
        chan = CR_CHAN(insn->chanspec);
        range = CR_RANGE(insn->chanspec);
        aref = CR_AREF(insn->chanspec);
        if (aref == AREF_DIFF) {
                /* Differential. */
                if (chan >= s->n_chan / 2) {
                        dev_dbg(dev->class_dev,
                                "%s: differential channel number out of range 0 to %u\n",
                                __func__, (s->n_chan / 2) - 1);
                        return -EINVAL;
                }
        }

        /*
         * Use Z2-CT2 as a conversion trigger instead of the built-in
         * software trigger, as otherwise triggering of differential channels
         * doesn't work properly for some versions of PCI230/260.  Also set
         * FIFO mode because the ADC busy bit only works for software triggers.
         */
        adccon = PCI230_ADC_TRIG_Z2CT2 | PCI230_ADC_FIFO_EN;
        /* Set Z2-CT2 output low to avoid any false triggers. */
        comedi_8254_set_mode(dev->pacer, 2, I8254_MODE0);
        devpriv->ai_bipolar = comedi_range_is_bipolar(s, range);
        if (aref == AREF_DIFF) {
                /* Differential. */
                gainshift = chan * 2;
                if (devpriv->hwver == 0) {
                        /*
                         * Original PCI230/260 expects both inputs of the
                         * differential channel to be enabled.
                         */
                        adcen = 3 << gainshift;
                } else {
                        /*
                         * PCI230+/260+ expects only one input of the
                         * differential channel to be enabled.
                         */
                        adcen = 1 << gainshift;
                }
                adccon |= PCI230_ADC_IM_DIF;
        } else {
                /* Single ended. */
                adcen = 1 << chan;
                gainshift = chan & ~1;
                adccon |= PCI230_ADC_IM_SE;
        }
        devpriv->adcg = (devpriv->adcg & ~(3 << gainshift)) |
                        (pci230_ai_gain[range] << gainshift);
        if (devpriv->ai_bipolar)
                adccon |= PCI230_ADC_IR_BIP;
        else
                adccon |= PCI230_ADC_IR_UNI;

        /*
         * Enable only this channel in the scan list - otherwise by default
         * we'll get one sample from each channel.
         */
        outw(adcen, devpriv->daqio + PCI230_ADCEN);

        /* Set gain for channel. */
        outw(devpriv->adcg, devpriv->daqio + PCI230_ADCG);

        /* Specify uni/bip, se/diff, conversion source, and reset FIFO. */
        devpriv->adccon = adccon;
        outw(adccon | PCI230_ADC_FIFO_RESET, devpriv->daqio + PCI230_ADCCON);

        /* Convert n samples */
        for (n = 0; n < insn->n; n++) {
                /*
                 * Trigger conversion by toggling Z2-CT2 output
                 * (finish with output high).
                 */
                comedi_8254_set_mode(dev->pacer, 2, I8254_MODE0);
                comedi_8254_set_mode(dev->pacer, 2, I8254_MODE1);

                /* wait for conversion to end */
                ret = comedi_timeout(dev, s, insn, pci230_ai_eoc, 0);
                if (ret)
                        return ret;

                /* read data */
                data[n] = pci230_ai_read(dev);
        }

        /* return the number of samples read/written */
        return n;
}

static int pci230_ao_insn_write(struct comedi_device *dev,
                                struct comedi_subdevice *s,
                                struct comedi_insn *insn,
                                unsigned int *data)
{
        struct pci230_private *devpriv = dev->private;
        unsigned int chan = CR_CHAN(insn->chanspec);
        unsigned int range = CR_RANGE(insn->chanspec);
        unsigned int val = s->readback[chan];
        int i;

        /*
         * Set range - see analogue output range table; 0 => unipolar 10V,
         * 1 => bipolar +/-10V range scale
         */
        devpriv->ao_bipolar = comedi_range_is_bipolar(s, range);
        outw(range, devpriv->daqio + PCI230_DACCON);

        for (i = 0; i < insn->n; i++) {
                val = data[i];
                pci230_ao_write_nofifo(dev, val, chan);
        }
        s->readback[chan] = val;

        return insn->n;
}

static int pci230_ao_check_chanlist(struct comedi_device *dev,
                                    struct comedi_subdevice *s,
                                    struct comedi_cmd *cmd)
{
        unsigned int prev_chan = CR_CHAN(cmd->chanlist[0]);
        unsigned int range0 = CR_RANGE(cmd->chanlist[0]);
        int i;

        for (i = 1; i < cmd->chanlist_len; i++) {
                unsigned int chan = CR_CHAN(cmd->chanlist[i]);
                unsigned int range = CR_RANGE(cmd->chanlist[i]);

                if (chan < prev_chan) {
                        dev_dbg(dev->class_dev,
                                "%s: channel numbers must increase\n",
                                __func__);
                        return -EINVAL;
                }

                if (range != range0) {
                        dev_dbg(dev->class_dev,
                                "%s: channels must have the same range\n",
                                __func__);
                        return -EINVAL;
                }

                prev_chan = chan;
        }

        return 0;
}

static int pci230_ao_cmdtest(struct comedi_device *dev,
                             struct comedi_subdevice *s, struct comedi_cmd *cmd)
{
        const struct pci230_board *board = dev->board_ptr;
        struct pci230_private *devpriv = dev->private;
        int err = 0;
        unsigned int tmp;

        /* Step 1 : check if triggers are trivially valid */

        err |= comedi_check_trigger_src(&cmd->start_src, TRIG_INT);

        tmp = TRIG_TIMER | TRIG_INT;
        if (board->min_hwver > 0 && devpriv->hwver >= 2) {
                /*
                 * For PCI230+ hardware version 2 onwards, allow external
                 * trigger from EXTTRIG/EXTCONVCLK input (PCI230+ pin 25).
                 *
                 * FIXME: The permitted scan_begin_src values shouldn't depend
                 * on devpriv->hwver (the detected card's actual hardware
                 * version).  They should only depend on board->min_hwver
                 * (the static capabilities of the configured card).  To fix
                 * it, a new card model, e.g. "pci230+2" would have to be
                 * defined with min_hwver set to 2.  It doesn't seem worth it
                 * for this alone.  At the moment, please consider
                 * scan_begin_src==TRIG_EXT support to be a bonus rather than a
                 * guarantee!
                 */
                tmp |= TRIG_EXT;
        }
        err |= comedi_check_trigger_src(&cmd->scan_begin_src, tmp);

        err |= comedi_check_trigger_src(&cmd->convert_src, TRIG_NOW);
        err |= comedi_check_trigger_src(&cmd->scan_end_src, TRIG_COUNT);
        err |= comedi_check_trigger_src(&cmd->stop_src, TRIG_COUNT | TRIG_NONE);

        if (err)
                return 1;

        /* Step 2a : make sure trigger sources are unique */

        err |= comedi_check_trigger_is_unique(cmd->scan_begin_src);
        err |= comedi_check_trigger_is_unique(cmd->stop_src);

        /* Step 2b : and mutually compatible */

        if (err)
                return 2;

        /* Step 3: check if arguments are trivially valid */

        err |= comedi_check_trigger_arg_is(&cmd->start_arg, 0);

#define MAX_SPEED_AO    8000    /* 8000 ns => 125 kHz */
/*
 * Comedi limit due to unsigned int cmd.  Driver limit =
 * 2^16 (16bit * counter) * 1000000ns (1kHz onboard clock) = 65.536s
 */
#define MIN_SPEED_AO    4294967295u     /* 4294967295ns = 4.29s */

        switch (cmd->scan_begin_src) {
        case TRIG_TIMER:
                err |= comedi_check_trigger_arg_min(&cmd->scan_begin_arg,
                                                    MAX_SPEED_AO);
                err |= comedi_check_trigger_arg_max(&cmd->scan_begin_arg,
                                                    MIN_SPEED_AO);
                break;
        case TRIG_EXT:
                /*
                 * External trigger - for PCI230+ hardware version 2 onwards.
                 */
                /* Trigger number must be 0. */
                if (cmd->scan_begin_arg & ~CR_FLAGS_MASK) {
                        cmd->scan_begin_arg = COMBINE(cmd->scan_begin_arg, 0,
                                                      ~CR_FLAGS_MASK);
                        err |= -EINVAL;
                }
                /*
                 * The only flags allowed are CR_EDGE and CR_INVERT.
                 * The CR_EDGE flag is ignored.
                 */
                if (cmd->scan_begin_arg & CR_FLAGS_MASK &
                    ~(CR_EDGE | CR_INVERT)) {
                        cmd->scan_begin_arg =
                            COMBINE(cmd->scan_begin_arg, 0,
                                    CR_FLAGS_MASK & ~(CR_EDGE | CR_INVERT));
                        err |= -EINVAL;
                }
                break;
        default:
                err |= comedi_check_trigger_arg_is(&cmd->scan_begin_arg, 0);
                break;
        }

        err |= comedi_check_trigger_arg_is(&cmd->scan_end_arg,
                                           cmd->chanlist_len);

        if (cmd->stop_src == TRIG_COUNT)
                err |= comedi_check_trigger_arg_min(&cmd->stop_arg, 1);
        else    /* TRIG_NONE */
                err |= comedi_check_trigger_arg_is(&cmd->stop_arg, 0);

        if (err)
                return 3;

        /* Step 4: fix up any arguments */

        if (cmd->scan_begin_src == TRIG_TIMER) {
                tmp = cmd->scan_begin_arg;
                pci230_ns_to_single_timer(&cmd->scan_begin_arg, cmd->flags);
                if (tmp != cmd->scan_begin_arg)
                        err++;
        }

        if (err)
                return 4;

        /* Step 5: check channel list if it exists */
        if (cmd->chanlist && cmd->chanlist_len > 0)
                err |= pci230_ao_check_chanlist(dev, s, cmd);

        if (err)
                return 5;

        return 0;
}

static void pci230_ao_stop(struct comedi_device *dev,
                           struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        unsigned long irqflags;
        unsigned char intsrc;
        bool started;
        struct comedi_cmd *cmd;

        spin_lock_irqsave(&devpriv->ao_stop_spinlock, irqflags);
        started = devpriv->ao_cmd_started;
        devpriv->ao_cmd_started = false;
        spin_unlock_irqrestore(&devpriv->ao_stop_spinlock, irqflags);
        if (!started)
                return;
        cmd = &s->async->cmd;
        if (cmd->scan_begin_src == TRIG_TIMER) {
                /* Stop scan rate generator. */
                pci230_cancel_ct(dev, 1);
        }
        /* Determine interrupt source. */
        if (devpriv->hwver < 2) {
                /* Not using DAC FIFO.  Using CT1 interrupt. */
                intsrc = PCI230_INT_ZCLK_CT1;
        } else {
                /* Using DAC FIFO interrupt. */
                intsrc = PCI230P2_INT_DAC;
        }
        /*
         * Disable interrupt and wait for interrupt routine to finish running
         * unless we are called from the interrupt routine.
         */
        spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
        devpriv->ier &= ~intsrc;
        while (devpriv->intr_running && devpriv->intr_cpuid != THISCPU) {
                spin_unlock_irqrestore(&devpriv->isr_spinlock, irqflags);
                spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
        }
        outb(devpriv->ier, dev->iobase + PCI230_INT_SCE);
        spin_unlock_irqrestore(&devpriv->isr_spinlock, irqflags);
        if (devpriv->hwver >= 2) {
                /*
                 * Using DAC FIFO.  Reset FIFO, clear underrun error,
                 * disable FIFO.
                 */
                devpriv->daccon &= PCI230_DAC_OR_MASK;
                outw(devpriv->daccon | PCI230P2_DAC_FIFO_RESET |
                     PCI230P2_DAC_FIFO_UNDERRUN_CLEAR,
                     devpriv->daqio + PCI230_DACCON);
        }
        /* Release resources. */
        pci230_release_all_resources(dev, OWNER_AOCMD);
}

static void pci230_handle_ao_nofifo(struct comedi_device *dev,
                                    struct comedi_subdevice *s)
{
        struct comedi_async *async = s->async;
        struct comedi_cmd *cmd = &async->cmd;
        unsigned short data;
        int i;

        if (cmd->stop_src == TRIG_COUNT && async->scans_done >= cmd->stop_arg)
                return;

        for (i = 0; i < cmd->chanlist_len; i++) {
                unsigned int chan = CR_CHAN(cmd->chanlist[i]);

                if (!comedi_buf_read_samples(s, &data, 1)) {
                        async->events |= COMEDI_CB_OVERFLOW;
                        return;
                }
                pci230_ao_write_nofifo(dev, data, chan);
                s->readback[chan] = data;
        }

        if (cmd->stop_src == TRIG_COUNT && async->scans_done >= cmd->stop_arg)
                async->events |= COMEDI_CB_EOA;
}

/*
 * Loads DAC FIFO (if using it) from buffer.
 * Returns false if AO finished due to completion or error, true if still going.
 */
static bool pci230_handle_ao_fifo(struct comedi_device *dev,
                                  struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        struct comedi_async *async = s->async;
        struct comedi_cmd *cmd = &async->cmd;
        unsigned int num_scans = comedi_nscans_left(s, 0);
        unsigned int room;
        unsigned short dacstat;
        unsigned int i, n;
        unsigned int events = 0;

        /* Get DAC FIFO status. */
        dacstat = inw(devpriv->daqio + PCI230_DACCON);

        if (cmd->stop_src == TRIG_COUNT && num_scans == 0)
                events |= COMEDI_CB_EOA;

        if (events == 0) {
                /* Check for FIFO underrun. */
                if (dacstat & PCI230P2_DAC_FIFO_UNDERRUN_LATCHED) {
                        dev_err(dev->class_dev, "AO FIFO underrun\n");
                        events |= COMEDI_CB_OVERFLOW | COMEDI_CB_ERROR;
                }
                /*
                 * Check for buffer underrun if FIFO less than half full
                 * (otherwise there will be loads of "DAC FIFO not half full"
                 * interrupts).
                 */
                if (num_scans == 0 &&
                    (dacstat & PCI230P2_DAC_FIFO_HALF) == 0) {
                        dev_err(dev->class_dev, "AO buffer underrun\n");
                        events |= COMEDI_CB_OVERFLOW | COMEDI_CB_ERROR;
                }
        }
        if (events == 0) {
                /* Determine how much room is in the FIFO (in samples). */
                if (dacstat & PCI230P2_DAC_FIFO_FULL)
                        room = PCI230P2_DAC_FIFOROOM_FULL;
                else if (dacstat & PCI230P2_DAC_FIFO_HALF)
                        room = PCI230P2_DAC_FIFOROOM_HALFTOFULL;
                else if (dacstat & PCI230P2_DAC_FIFO_EMPTY)
                        room = PCI230P2_DAC_FIFOROOM_EMPTY;
                else
                        room = PCI230P2_DAC_FIFOROOM_ONETOHALF;
                /* Convert room to number of scans that can be added. */
                room /= cmd->chanlist_len;
                /* Determine number of scans to process. */
                if (num_scans > room)
                        num_scans = room;
                /* Process scans. */
                for (n = 0; n < num_scans; n++) {
                        for (i = 0; i < cmd->chanlist_len; i++) {
                                unsigned int chan = CR_CHAN(cmd->chanlist[i]);
                                unsigned short datum;

                                comedi_buf_read_samples(s, &datum, 1);
                                pci230_ao_write_fifo(dev, datum, chan);
                                s->readback[chan] = datum;
                        }
                }

                if (cmd->stop_src == TRIG_COUNT &&
                    async->scans_done >= cmd->stop_arg) {
                        /*
                         * All data for the command has been written
                         * to FIFO.  Set FIFO interrupt trigger level
                         * to 'empty'.
                         */
                        devpriv->daccon &= ~PCI230P2_DAC_INT_FIFO_MASK;
                        devpriv->daccon |= PCI230P2_DAC_INT_FIFO_EMPTY;
                        outw(devpriv->daccon, devpriv->daqio + PCI230_DACCON);
                }
                /* Check if FIFO underrun occurred while writing to FIFO. */
                dacstat = inw(devpriv->daqio + PCI230_DACCON);
                if (dacstat & PCI230P2_DAC_FIFO_UNDERRUN_LATCHED) {
                        dev_err(dev->class_dev, "AO FIFO underrun\n");
                        events |= COMEDI_CB_OVERFLOW | COMEDI_CB_ERROR;
                }
        }
        async->events |= events;
        return !(async->events & COMEDI_CB_CANCEL_MASK);
}

static int pci230_ao_inttrig_scan_begin(struct comedi_device *dev,
                                        struct comedi_subdevice *s,
                                        unsigned int trig_num)
{
        struct pci230_private *devpriv = dev->private;
        unsigned long irqflags;

        if (trig_num)
                return -EINVAL;

        spin_lock_irqsave(&devpriv->ao_stop_spinlock, irqflags);
        if (!devpriv->ao_cmd_started) {
                spin_unlock_irqrestore(&devpriv->ao_stop_spinlock, irqflags);
                return 1;
        }
        /* Perform scan. */
        if (devpriv->hwver < 2) {
                /* Not using DAC FIFO. */
                spin_unlock_irqrestore(&devpriv->ao_stop_spinlock, irqflags);
                pci230_handle_ao_nofifo(dev, s);
                comedi_handle_events(dev, s);
        } else {
                /* Using DAC FIFO. */
                /* Read DACSWTRIG register to trigger conversion. */
                inw(devpriv->daqio + PCI230P2_DACSWTRIG);
                spin_unlock_irqrestore(&devpriv->ao_stop_spinlock, irqflags);
        }
        /* Delay.  Should driver be responsible for this? */
        /* XXX TODO: See if DAC busy bit can be used. */
        udelay(8);
        return 1;
}

static void pci230_ao_start(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        struct comedi_async *async = s->async;
        struct comedi_cmd *cmd = &async->cmd;
        unsigned long irqflags;

        devpriv->ao_cmd_started = true;

        if (devpriv->hwver >= 2) {
                /* Using DAC FIFO. */
                unsigned short scantrig;
                bool run;

                /* Preload FIFO data. */
                run = pci230_handle_ao_fifo(dev, s);
                comedi_handle_events(dev, s);
                if (!run) {
                        /* Stopped. */
                        return;
                }
                /* Set scan trigger source. */
                switch (cmd->scan_begin_src) {
                case TRIG_TIMER:
                        scantrig = PCI230P2_DAC_TRIG_Z2CT1;
                        break;
                case TRIG_EXT:
                        /* Trigger on EXTTRIG/EXTCONVCLK pin. */
                        if ((cmd->scan_begin_arg & CR_INVERT) == 0) {
                                /* +ve edge */
                                scantrig = PCI230P2_DAC_TRIG_EXTP;
                        } else {
                                /* -ve edge */
                                scantrig = PCI230P2_DAC_TRIG_EXTN;
                        }
                        break;
                case TRIG_INT:
                        scantrig = PCI230P2_DAC_TRIG_SW;
                        break;
                default:
                        /* Shouldn't get here. */
                        scantrig = PCI230P2_DAC_TRIG_NONE;
                        break;
                }
                devpriv->daccon =
                    (devpriv->daccon & ~PCI230P2_DAC_TRIG_MASK) | scantrig;
                outw(devpriv->daccon, devpriv->daqio + PCI230_DACCON);
        }
        switch (cmd->scan_begin_src) {
        case TRIG_TIMER:
                if (devpriv->hwver < 2) {
                        /* Not using DAC FIFO. */
                        /* Enable CT1 timer interrupt. */
                        spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
                        devpriv->ier |= PCI230_INT_ZCLK_CT1;
                        outb(devpriv->ier, dev->iobase + PCI230_INT_SCE);
                        spin_unlock_irqrestore(&devpriv->isr_spinlock,
                                               irqflags);
                }
                /* Set CT1 gate high to start counting. */
                outb(pci230_gat_config(1, GAT_VCC),
                     dev->iobase + PCI230_ZGAT_SCE);
                break;
        case TRIG_INT:
                async->inttrig = pci230_ao_inttrig_scan_begin;
                break;
        }
        if (devpriv->hwver >= 2) {
                /* Using DAC FIFO.  Enable DAC FIFO interrupt. */
                spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
                devpriv->ier |= PCI230P2_INT_DAC;
                outb(devpriv->ier, dev->iobase + PCI230_INT_SCE);
                spin_unlock_irqrestore(&devpriv->isr_spinlock, irqflags);
        }
}

static int pci230_ao_inttrig_start(struct comedi_device *dev,
                                   struct comedi_subdevice *s,
                                   unsigned int trig_num)
{
        struct comedi_cmd *cmd = &s->async->cmd;

        if (trig_num != cmd->start_src)
                return -EINVAL;

        s->async->inttrig = NULL;
        pci230_ao_start(dev, s);

        return 1;
}

static int pci230_ao_cmd(struct comedi_device *dev, struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        unsigned short daccon;
        unsigned int range;

        /* Get the command. */
        struct comedi_cmd *cmd = &s->async->cmd;

        if (cmd->scan_begin_src == TRIG_TIMER) {
                /* Claim Z2-CT1. */
                if (!pci230_claim_shared(dev, RES_Z2CT1, OWNER_AOCMD))
                        return -EBUSY;
        }

        /*
         * Set range - see analogue output range table; 0 => unipolar 10V,
         * 1 => bipolar +/-10V range scale
         */
        range = CR_RANGE(cmd->chanlist[0]);
        devpriv->ao_bipolar = comedi_range_is_bipolar(s, range);
        daccon = devpriv->ao_bipolar ? PCI230_DAC_OR_BIP : PCI230_DAC_OR_UNI;
        /* Use DAC FIFO for hardware version 2 onwards. */
        if (devpriv->hwver >= 2) {
                unsigned short dacen;
                unsigned int i;

                dacen = 0;
                for (i = 0; i < cmd->chanlist_len; i++)
                        dacen |= 1 << CR_CHAN(cmd->chanlist[i]);

                /* Set channel scan list. */
                outw(dacen, devpriv->daqio + PCI230P2_DACEN);
                /*
                 * Enable DAC FIFO.
                 * Set DAC scan source to 'none'.
                 * Set DAC FIFO interrupt trigger level to 'not half full'.
                 * Reset DAC FIFO and clear underrun.
                 *
                 * N.B. DAC FIFO interrupts are currently disabled.
                 */
                daccon |= PCI230P2_DAC_FIFO_EN | PCI230P2_DAC_FIFO_RESET |
                          PCI230P2_DAC_FIFO_UNDERRUN_CLEAR |
                          PCI230P2_DAC_TRIG_NONE | PCI230P2_DAC_INT_FIFO_NHALF;
        }

        /* Set DACCON. */
        outw(daccon, devpriv->daqio + PCI230_DACCON);
        /* Preserve most of DACCON apart from write-only, transient bits. */
        devpriv->daccon = daccon & ~(PCI230P2_DAC_FIFO_RESET |
                                     PCI230P2_DAC_FIFO_UNDERRUN_CLEAR);

        if (cmd->scan_begin_src == TRIG_TIMER) {
                /*
                 * Set the counter timer 1 to the specified scan frequency.
                 * cmd->scan_begin_arg is sampling period in ns.
                 * Gate it off for now.
                 */
                outb(pci230_gat_config(1, GAT_GND),
                     dev->iobase + PCI230_ZGAT_SCE);
                pci230_ct_setup_ns_mode(dev, 1, I8254_MODE3,
                                        cmd->scan_begin_arg,
                                        cmd->flags);
        }

        /* N.B. cmd->start_src == TRIG_INT */
        s->async->inttrig = pci230_ao_inttrig_start;

        return 0;
}

static int pci230_ao_cancel(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        pci230_ao_stop(dev, s);
        return 0;
}

static int pci230_ai_check_scan_period(struct comedi_cmd *cmd)
{
        unsigned int min_scan_period, chanlist_len;
        int err = 0;

        chanlist_len = cmd->chanlist_len;
        if (cmd->chanlist_len == 0)
                chanlist_len = 1;

        min_scan_period = chanlist_len * cmd->convert_arg;
        if (min_scan_period < chanlist_len ||
            min_scan_period < cmd->convert_arg) {
                /* Arithmetic overflow. */
                min_scan_period = UINT_MAX;
                err++;
        }
        if (cmd->scan_begin_arg < min_scan_period) {
                cmd->scan_begin_arg = min_scan_period;
                err++;
        }

        return !err;
}

static int pci230_ai_check_chanlist(struct comedi_device *dev,
                                    struct comedi_subdevice *s,
                                    struct comedi_cmd *cmd)
{
        struct pci230_private *devpriv = dev->private;
        unsigned int max_diff_chan = (s->n_chan / 2) - 1;
        unsigned int prev_chan = 0;
        unsigned int prev_range = 0;
        unsigned int prev_aref = 0;
        bool prev_bipolar = false;
        unsigned int subseq_len = 0;
        int i;

        for (i = 0; i < cmd->chanlist_len; i++) {
                unsigned int chanspec = cmd->chanlist[i];
                unsigned int chan = CR_CHAN(chanspec);
                unsigned int range = CR_RANGE(chanspec);
                unsigned int aref = CR_AREF(chanspec);
                bool bipolar = comedi_range_is_bipolar(s, range);

                if (aref == AREF_DIFF && chan >= max_diff_chan) {
                        dev_dbg(dev->class_dev,
                                "%s: differential channel number out of range 0 to %u\n",
                                __func__, max_diff_chan);
                        return -EINVAL;
                }

                if (i > 0) {
                        /*
                         * Channel numbers must strictly increase or
                         * subsequence must repeat exactly.
                         */
                        if (chan <= prev_chan && subseq_len == 0)
                                subseq_len = i;

                        if (subseq_len > 0 &&
                            cmd->chanlist[i % subseq_len] != chanspec) {
                                dev_dbg(dev->class_dev,
                                        "%s: channel numbers must increase or sequence must repeat exactly\n",
                                        __func__);
                                return -EINVAL;
                        }

                        if (aref != prev_aref) {
                                dev_dbg(dev->class_dev,
                                        "%s: channel sequence analogue references must be all the same (single-ended or differential)\n",
                                        __func__);
                                return -EINVAL;
                        }

                        if (bipolar != prev_bipolar) {
                                dev_dbg(dev->class_dev,
                                        "%s: channel sequence ranges must be all bipolar or all unipolar\n",
                                        __func__);
                                return -EINVAL;
                        }

                        if (aref != AREF_DIFF && range != prev_range &&
                            ((chan ^ prev_chan) & ~1) == 0) {
                                dev_dbg(dev->class_dev,
                                        "%s: single-ended channel pairs must have the same range\n",
                                        __func__);
                                return -EINVAL;
                        }
                }
                prev_chan = chan;
                prev_range = range;
                prev_aref = aref;
                prev_bipolar = bipolar;
        }

        if (subseq_len == 0)
                subseq_len = cmd->chanlist_len;

        if (cmd->chanlist_len % subseq_len) {
                dev_dbg(dev->class_dev,
                        "%s: sequence must repeat exactly\n", __func__);
                return -EINVAL;
        }

        /*
         * Buggy PCI230+ or PCI260+ requires channel 0 to be (first) in the
         * sequence if the sequence contains more than one channel. Hardware
         * versions 1 and 2 have the bug. There is no hardware version 3.
         *
         * Actually, there are two firmwares that report themselves as
         * hardware version 1 (the boards have different ADC chips with
         * slightly different timing requirements, which was supposed to
         * be invisible to software). The first one doesn't seem to have
         * the bug, but the second one does, and we can't tell them apart!
         */
        if (devpriv->hwver > 0 && devpriv->hwver < 4) {
                if (subseq_len > 1 && CR_CHAN(cmd->chanlist[0])) {
                        dev_info(dev->class_dev,
                                 "amplc_pci230: ai_cmdtest: Buggy PCI230+/260+ h/w version %u requires first channel of multi-channel sequence to be 0 (corrected in h/w version 4)\n",
                                 devpriv->hwver);
                        return -EINVAL;
                }
        }

        return 0;
}

static int pci230_ai_cmdtest(struct comedi_device *dev,
                             struct comedi_subdevice *s, struct comedi_cmd *cmd)
{
        const struct pci230_board *board = dev->board_ptr;
        struct pci230_private *devpriv = dev->private;
        int err = 0;
        unsigned int tmp;

        /* Step 1 : check if triggers are trivially valid */

        err |= comedi_check_trigger_src(&cmd->start_src, TRIG_NOW | TRIG_INT);

        tmp = TRIG_FOLLOW | TRIG_TIMER | TRIG_INT;
        if (board->have_dio || board->min_hwver > 0) {
                /*
                 * Unfortunately, we cannot trigger a scan off an external
                 * source on the PCI260 board, since it uses the PPIC0 (DIO)
                 * input, which isn't present on the PCI260.  For PCI260+
                 * we can use the EXTTRIG/EXTCONVCLK input on pin 17 instead.
                 */
                tmp |= TRIG_EXT;
        }
        err |= comedi_check_trigger_src(&cmd->scan_begin_src, tmp);
        err |= comedi_check_trigger_src(&cmd->convert_src,
                                        TRIG_TIMER | TRIG_INT | TRIG_EXT);
        err |= comedi_check_trigger_src(&cmd->scan_end_src, TRIG_COUNT);
        err |= comedi_check_trigger_src(&cmd->stop_src, TRIG_COUNT | TRIG_NONE);

        if (err)
                return 1;

        /* Step 2a : make sure trigger sources are unique */

        err |= comedi_check_trigger_is_unique(cmd->start_src);
        err |= comedi_check_trigger_is_unique(cmd->scan_begin_src);
        err |= comedi_check_trigger_is_unique(cmd->convert_src);
        err |= comedi_check_trigger_is_unique(cmd->stop_src);

        /* Step 2b : and mutually compatible */

        /*
         * If scan_begin_src is not TRIG_FOLLOW, then a monostable will be
         * set up to generate a fixed number of timed conversion pulses.
         */
        if (cmd->scan_begin_src != TRIG_FOLLOW &&
            cmd->convert_src != TRIG_TIMER)
                err |= -EINVAL;

        if (err)
                return 2;

        /* Step 3: check if arguments are trivially valid */

        err |= comedi_check_trigger_arg_is(&cmd->start_arg, 0);

#define MAX_SPEED_AI_SE         3200    /* PCI230 SE:   3200 ns => 312.5 kHz */
#define MAX_SPEED_AI_DIFF       8000    /* PCI230 DIFF: 8000 ns => 125 kHz */
#define MAX_SPEED_AI_PLUS       4000    /* PCI230+:     4000 ns => 250 kHz */
/*
 * Comedi limit due to unsigned int cmd.  Driver limit =
 * 2^16 (16bit * counter) * 1000000ns (1kHz onboard clock) = 65.536s
 */
#define MIN_SPEED_AI    4294967295u     /* 4294967295ns = 4.29s */

        if (cmd->convert_src == TRIG_TIMER) {
                unsigned int max_speed_ai;

                if (devpriv->hwver == 0) {
                        /*
                         * PCI230 or PCI260.  Max speed depends whether
                         * single-ended or pseudo-differential.
                         */
                        if (cmd->chanlist && cmd->chanlist_len > 0) {
                                /* Peek analogue reference of first channel. */
                                if (CR_AREF(cmd->chanlist[0]) == AREF_DIFF)
                                        max_speed_ai = MAX_SPEED_AI_DIFF;
                                else
                                        max_speed_ai = MAX_SPEED_AI_SE;

                        } else {
                                /* No channel list.  Assume single-ended. */
                                max_speed_ai = MAX_SPEED_AI_SE;
                        }
                } else {
                        /* PCI230+ or PCI260+. */
                        max_speed_ai = MAX_SPEED_AI_PLUS;
                }

                err |= comedi_check_trigger_arg_min(&cmd->convert_arg,
                                                    max_speed_ai);
                err |= comedi_check_trigger_arg_max(&cmd->convert_arg,
                                                    MIN_SPEED_AI);
        } else if (cmd->convert_src == TRIG_EXT) {
                /*
                 * external trigger
                 *
                 * convert_arg == (CR_EDGE | 0)
                 *                => trigger on +ve edge.
                 * convert_arg == (CR_EDGE | CR_INVERT | 0)
                 *                => trigger on -ve edge.
                 */
                if (cmd->convert_arg & CR_FLAGS_MASK) {
                        /* Trigger number must be 0. */
                        if (cmd->convert_arg & ~CR_FLAGS_MASK) {
                                cmd->convert_arg = COMBINE(cmd->convert_arg, 0,
                                                           ~CR_FLAGS_MASK);
                                err |= -EINVAL;
                        }
                        /*
                         * The only flags allowed are CR_INVERT and CR_EDGE.
                         * CR_EDGE is required.
                         */
                        if ((cmd->convert_arg & CR_FLAGS_MASK & ~CR_INVERT) !=
                            CR_EDGE) {
                                /* Set CR_EDGE, preserve CR_INVERT. */
                                cmd->convert_arg =
                                    COMBINE(cmd->start_arg, CR_EDGE | 0,
                                            CR_FLAGS_MASK & ~CR_INVERT);
                                err |= -EINVAL;
                        }
                } else {
                        /*
                         * Backwards compatibility with previous versions:
                         * convert_arg == 0 => trigger on -ve edge.
                         * convert_arg == 1 => trigger on +ve edge.
                         */
                        err |= comedi_check_trigger_arg_max(&cmd->convert_arg,
                                                            1);
                }
        } else {
                err |= comedi_check_trigger_arg_is(&cmd->convert_arg, 0);
        }

        err |= comedi_check_trigger_arg_is(&cmd->scan_end_arg,
                                           cmd->chanlist_len);

        if (cmd->stop_src == TRIG_COUNT)
                err |= comedi_check_trigger_arg_min(&cmd->stop_arg, 1);
        else    /* TRIG_NONE */
                err |= comedi_check_trigger_arg_is(&cmd->stop_arg, 0);

        if (cmd->scan_begin_src == TRIG_EXT) {
                /*
                 * external "trigger" to begin each scan:
                 * scan_begin_arg==0 => use PPC0 input -> gate of CT0 -> gate
                 * of CT2 (sample convert trigger is CT2)
                 */
                if (cmd->scan_begin_arg & ~CR_FLAGS_MASK) {
                        cmd->scan_begin_arg = COMBINE(cmd->scan_begin_arg, 0,
                                                      ~CR_FLAGS_MASK);
                        err |= -EINVAL;
                }
                /* The only flag allowed is CR_EDGE, which is ignored. */
                if (cmd->scan_begin_arg & CR_FLAGS_MASK & ~CR_EDGE) {
                        cmd->scan_begin_arg = COMBINE(cmd->scan_begin_arg, 0,
                                                      CR_FLAGS_MASK & ~CR_EDGE);
                        err |= -EINVAL;
                }
        } else if (cmd->scan_begin_src == TRIG_TIMER) {
                /* N.B. cmd->convert_arg is also TRIG_TIMER */
                if (!pci230_ai_check_scan_period(cmd))
                        err |= -EINVAL;

        } else {
                err |= comedi_check_trigger_arg_is(&cmd->scan_begin_arg, 0);
        }

        if (err)
                return 3;

        /* Step 4: fix up any arguments */

        if (cmd->convert_src == TRIG_TIMER) {
                tmp = cmd->convert_arg;
                pci230_ns_to_single_timer(&cmd->convert_arg, cmd->flags);
                if (tmp != cmd->convert_arg)
                        err++;
        }

        if (cmd->scan_begin_src == TRIG_TIMER) {
                /* N.B. cmd->convert_arg is also TRIG_TIMER */
                tmp = cmd->scan_begin_arg;
                pci230_ns_to_single_timer(&cmd->scan_begin_arg, cmd->flags);
                if (!pci230_ai_check_scan_period(cmd)) {
                        /* Was below minimum required.  Round up. */
                        pci230_ns_to_single_timer(&cmd->scan_begin_arg,
                                                  CMDF_ROUND_UP);
                        pci230_ai_check_scan_period(cmd);
                }
                if (tmp != cmd->scan_begin_arg)
                        err++;
        }

        if (err)
                return 4;

        /* Step 5: check channel list if it exists */
        if (cmd->chanlist && cmd->chanlist_len > 0)
                err |= pci230_ai_check_chanlist(dev, s, cmd);

        if (err)
                return 5;

        return 0;
}

static void pci230_ai_update_fifo_trigger_level(struct comedi_device *dev,
                                                struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        struct comedi_cmd *cmd = &s->async->cmd;
        unsigned int wake;
        unsigned short triglev;
        unsigned short adccon;

        if (cmd->flags & CMDF_WAKE_EOS)
                wake = cmd->scan_end_arg - s->async->cur_chan;
        else
                wake = comedi_nsamples_left(s, PCI230_ADC_FIFOLEVEL_HALFFULL);

        if (wake >= PCI230_ADC_FIFOLEVEL_HALFFULL) {
                triglev = PCI230_ADC_INT_FIFO_HALF;
        } else if (wake > 1 && devpriv->hwver > 0) {
                /* PCI230+/260+ programmable FIFO interrupt level. */
                if (devpriv->adcfifothresh != wake) {
                        devpriv->adcfifothresh = wake;
                        outw(wake, devpriv->daqio + PCI230P_ADCFFTH);
                }
                triglev = PCI230P_ADC_INT_FIFO_THRESH;
        } else {
                triglev = PCI230_ADC_INT_FIFO_NEMPTY;
        }
        adccon = (devpriv->adccon & ~PCI230_ADC_INT_FIFO_MASK) | triglev;
        if (adccon != devpriv->adccon) {
                devpriv->adccon = adccon;
                outw(adccon, devpriv->daqio + PCI230_ADCCON);
        }
}

static int pci230_ai_inttrig_convert(struct comedi_device *dev,
                                     struct comedi_subdevice *s,
                                     unsigned int trig_num)
{
        struct pci230_private *devpriv = dev->private;
        unsigned long irqflags;
        unsigned int delayus;

        if (trig_num)
                return -EINVAL;

        spin_lock_irqsave(&devpriv->ai_stop_spinlock, irqflags);
        if (!devpriv->ai_cmd_started) {
                spin_unlock_irqrestore(&devpriv->ai_stop_spinlock, irqflags);
                return 1;
        }
        /*
         * Trigger conversion by toggling Z2-CT2 output.
         * Finish with output high.
         */
        comedi_8254_set_mode(dev->pacer, 2, I8254_MODE0);
        comedi_8254_set_mode(dev->pacer, 2, I8254_MODE1);
        /*
         * Delay.  Should driver be responsible for this?  An
         * alternative would be to wait until conversion is complete,
         * but we can't tell when it's complete because the ADC busy
         * bit has a different meaning when FIFO enabled (and when
         * FIFO not enabled, it only works for software triggers).
         */
        if ((devpriv->adccon & PCI230_ADC_IM_MASK) == PCI230_ADC_IM_DIF &&
            devpriv->hwver == 0) {
                /* PCI230/260 in differential mode */
                delayus = 8;
        } else {
                /* single-ended or PCI230+/260+ */
                delayus = 4;
        }
        spin_unlock_irqrestore(&devpriv->ai_stop_spinlock, irqflags);
        udelay(delayus);
        return 1;
}

static int pci230_ai_inttrig_scan_begin(struct comedi_device *dev,
                                        struct comedi_subdevice *s,
                                        unsigned int trig_num)
{
        struct pci230_private *devpriv = dev->private;
        unsigned long irqflags;
        unsigned char zgat;

        if (trig_num)
                return -EINVAL;

        spin_lock_irqsave(&devpriv->ai_stop_spinlock, irqflags);
        if (devpriv->ai_cmd_started) {
                /* Trigger scan by waggling CT0 gate source. */
                zgat = pci230_gat_config(0, GAT_GND);
                outb(zgat, dev->iobase + PCI230_ZGAT_SCE);
                zgat = pci230_gat_config(0, GAT_VCC);
                outb(zgat, dev->iobase + PCI230_ZGAT_SCE);
        }
        spin_unlock_irqrestore(&devpriv->ai_stop_spinlock, irqflags);

        return 1;
}

static void pci230_ai_stop(struct comedi_device *dev,
                           struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        unsigned long irqflags;
        struct comedi_cmd *cmd;
        bool started;

        spin_lock_irqsave(&devpriv->ai_stop_spinlock, irqflags);
        started = devpriv->ai_cmd_started;
        devpriv->ai_cmd_started = false;
        spin_unlock_irqrestore(&devpriv->ai_stop_spinlock, irqflags);
        if (!started)
                return;
        cmd = &s->async->cmd;
        if (cmd->convert_src == TRIG_TIMER) {
                /* Stop conversion rate generator. */
                pci230_cancel_ct(dev, 2);
        }
        if (cmd->scan_begin_src != TRIG_FOLLOW) {
                /* Stop scan period monostable. */
                pci230_cancel_ct(dev, 0);
        }
        spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
        /*
         * Disable ADC interrupt and wait for interrupt routine to finish
         * running unless we are called from the interrupt routine.
         */
        devpriv->ier &= ~PCI230_INT_ADC;
        while (devpriv->intr_running && devpriv->intr_cpuid != THISCPU) {
                spin_unlock_irqrestore(&devpriv->isr_spinlock, irqflags);
                spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
        }
        outb(devpriv->ier, dev->iobase + PCI230_INT_SCE);
        spin_unlock_irqrestore(&devpriv->isr_spinlock, irqflags);
        /*
         * Reset FIFO, disable FIFO and set start conversion source to none.
         * Keep se/diff and bip/uni settings.
         */
        devpriv->adccon =
            (devpriv->adccon & (PCI230_ADC_IR_MASK | PCI230_ADC_IM_MASK)) |
            PCI230_ADC_TRIG_NONE;
        outw(devpriv->adccon | PCI230_ADC_FIFO_RESET,
             devpriv->daqio + PCI230_ADCCON);
        /* Release resources. */
        pci230_release_all_resources(dev, OWNER_AICMD);
}

static void pci230_ai_start(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        unsigned long irqflags;
        unsigned short conv;
        struct comedi_async *async = s->async;
        struct comedi_cmd *cmd = &async->cmd;

        devpriv->ai_cmd_started = true;

        /* Enable ADC FIFO trigger level interrupt. */
        spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
        devpriv->ier |= PCI230_INT_ADC;
        outb(devpriv->ier, dev->iobase + PCI230_INT_SCE);
        spin_unlock_irqrestore(&devpriv->isr_spinlock, irqflags);

        /*
         * Update conversion trigger source which is currently set
         * to CT2 output, which is currently stuck high.
         */
        switch (cmd->convert_src) {
        default:
                conv = PCI230_ADC_TRIG_NONE;
                break;
        case TRIG_TIMER:
                /* Using CT2 output. */
                conv = PCI230_ADC_TRIG_Z2CT2;
                break;
        case TRIG_EXT:
                if (cmd->convert_arg & CR_EDGE) {
                        if ((cmd->convert_arg & CR_INVERT) == 0) {
                                /* Trigger on +ve edge. */
                                conv = PCI230_ADC_TRIG_EXTP;
                        } else {
                                /* Trigger on -ve edge. */
                                conv = PCI230_ADC_TRIG_EXTN;
                        }
                } else {
                        /* Backwards compatibility. */
                        if (cmd->convert_arg) {
                                /* Trigger on +ve edge. */
                                conv = PCI230_ADC_TRIG_EXTP;
                        } else {
                                /* Trigger on -ve edge. */
                                conv = PCI230_ADC_TRIG_EXTN;
                        }
                }
                break;
        case TRIG_INT:
                /*
                 * Use CT2 output for software trigger due to problems
                 * in differential mode on PCI230/260.
                 */
                conv = PCI230_ADC_TRIG_Z2CT2;
                break;
        }
        devpriv->adccon = (devpriv->adccon & ~PCI230_ADC_TRIG_MASK) | conv;
        outw(devpriv->adccon, devpriv->daqio + PCI230_ADCCON);
        if (cmd->convert_src == TRIG_INT)
                async->inttrig = pci230_ai_inttrig_convert;

        /*
         * Update FIFO interrupt trigger level, which is currently
         * set to "full".
         */
        pci230_ai_update_fifo_trigger_level(dev, s);
        if (cmd->convert_src == TRIG_TIMER) {
                /* Update timer gates. */
                unsigned char zgat;

                if (cmd->scan_begin_src != TRIG_FOLLOW) {
                        /*
                         * Conversion timer CT2 needs to be gated by
                         * inverted output of monostable CT2.
                         */
                        zgat = pci230_gat_config(2, GAT_NOUTNM2);
                } else {
                        /*
                         * Conversion timer CT2 needs to be gated on
                         * continuously.
                         */
                        zgat = pci230_gat_config(2, GAT_VCC);
                }
                outb(zgat, dev->iobase + PCI230_ZGAT_SCE);
                if (cmd->scan_begin_src != TRIG_FOLLOW) {
                        /* Set monostable CT0 trigger source. */
                        switch (cmd->scan_begin_src) {
                        default:
                                zgat = pci230_gat_config(0, GAT_VCC);
                                break;
                        case TRIG_EXT:
                                /*
                                 * For CT0 on PCI230, the external trigger
                                 * (gate) signal comes from PPC0, which is
                                 * channel 16 of the DIO subdevice.  The
                                 * application needs to configure this as an
                                 * input in order to use it as an external scan
                                 * trigger.
                                 */
                                zgat = pci230_gat_config(0, GAT_EXT);
                                break;
                        case TRIG_TIMER:
                                /*
                                 * Monostable CT0 triggered by rising edge on
                                 * inverted output of CT1 (falling edge on CT1).
                                 */
                                zgat = pci230_gat_config(0, GAT_NOUTNM2);
                                break;
                        case TRIG_INT:
                                /*
                                 * Monostable CT0 is triggered by inttrig
                                 * function waggling the CT0 gate source.
                                 */
                                zgat = pci230_gat_config(0, GAT_VCC);
                                break;
                        }
                        outb(zgat, dev->iobase + PCI230_ZGAT_SCE);
                        switch (cmd->scan_begin_src) {
                        case TRIG_TIMER:
                                /*
                                 * Scan period timer CT1 needs to be
                                 * gated on to start counting.
                                 */
                                zgat = pci230_gat_config(1, GAT_VCC);
                                outb(zgat, dev->iobase + PCI230_ZGAT_SCE);
                                break;
                        case TRIG_INT:
                                async->inttrig = pci230_ai_inttrig_scan_begin;
                                break;
                        }
                }
        } else if (cmd->convert_src != TRIG_INT) {
                /* No longer need Z2-CT2. */
                pci230_release_shared(dev, RES_Z2CT2, OWNER_AICMD);
        }
}

static int pci230_ai_inttrig_start(struct comedi_device *dev,
                                   struct comedi_subdevice *s,
                                   unsigned int trig_num)
{
        struct comedi_cmd *cmd = &s->async->cmd;

        if (trig_num != cmd->start_arg)
                return -EINVAL;

        s->async->inttrig = NULL;
        pci230_ai_start(dev, s);

        return 1;
}

static void pci230_handle_ai(struct comedi_device *dev,
                             struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        struct comedi_async *async = s->async;
        struct comedi_cmd *cmd = &async->cmd;
        unsigned int status_fifo;
        unsigned int i;
        unsigned int nsamples;
        unsigned int fifoamount;
        unsigned short val;

        /* Determine number of samples to read. */
        nsamples = comedi_nsamples_left(s, PCI230_ADC_FIFOLEVEL_HALFFULL);
        if (nsamples == 0)
                return;

        fifoamount = 0;
        for (i = 0; i < nsamples; i++) {
                if (fifoamount == 0) {
                        /* Read FIFO state. */
                        status_fifo = inw(devpriv->daqio + PCI230_ADCCON);
                        if (status_fifo & PCI230_ADC_FIFO_FULL_LATCHED) {
                                /*
                                 * Report error otherwise FIFO overruns will go
                                 * unnoticed by the caller.
                                 */
                                dev_err(dev->class_dev, "AI FIFO overrun\n");
                                async->events |= COMEDI_CB_ERROR;
                                break;
                        } else if (status_fifo & PCI230_ADC_FIFO_EMPTY) {
                                /* FIFO empty. */
                                break;
                        } else if (status_fifo & PCI230_ADC_FIFO_HALF) {
                                /* FIFO half full. */
                                fifoamount = PCI230_ADC_FIFOLEVEL_HALFFULL;
                        } else if (devpriv->hwver > 0) {
                                /* Read PCI230+/260+ ADC FIFO level. */
                                fifoamount = inw(devpriv->daqio +
                                                 PCI230P_ADCFFLEV);
                                if (fifoamount == 0)
                                        break;  /* Shouldn't happen. */
                        } else {
                                /* FIFO not empty. */
                                fifoamount = 1;
                        }
                }

                val = pci230_ai_read(dev);
                if (!comedi_buf_write_samples(s, &val, 1))
                        break;

                fifoamount--;

                if (cmd->stop_src == TRIG_COUNT &&
                    async->scans_done >= cmd->stop_arg) {
                        async->events |= COMEDI_CB_EOA;
                        break;
                }
        }

        /* update FIFO interrupt trigger level if still running */
        if (!(async->events & COMEDI_CB_CANCEL_MASK))
                pci230_ai_update_fifo_trigger_level(dev, s);
}

static int pci230_ai_cmd(struct comedi_device *dev, struct comedi_subdevice *s)
{
        struct pci230_private *devpriv = dev->private;
        unsigned int i, chan, range, diff;
        unsigned int res_mask;
        unsigned short adccon, adcen;
        unsigned char zgat;

        /* Get the command. */
        struct comedi_async *async = s->async;
        struct comedi_cmd *cmd = &async->cmd;

        /*
         * Determine which shared resources are needed.
         */
        res_mask = 0;
        /*
         * Need Z2-CT2 to supply a conversion trigger source at a high
         * logic level, even if not doing timed conversions.
         */
        res_mask |= RES_Z2CT2;
        if (cmd->scan_begin_src != TRIG_FOLLOW) {
                /* Using Z2-CT0 monostable to gate Z2-CT2 conversion timer */
                res_mask |= RES_Z2CT0;
                if (cmd->scan_begin_src == TRIG_TIMER) {
                        /* Using Z2-CT1 for scan frequency */
                        res_mask |= RES_Z2CT1;
                }
        }
        /* Claim resources. */
        if (!pci230_claim_shared(dev, res_mask, OWNER_AICMD))
                return -EBUSY;

        /*
         * Steps:
         * - Set channel scan list.
         * - Set channel gains.
         * - Enable and reset FIFO, specify uni/bip, se/diff, and set
         *   start conversion source to point to something at a high logic
         *   level (we use the output of counter/timer 2 for this purpose.
         * - PAUSE to allow things to settle down.
         * - Reset the FIFO again because it needs resetting twice and there
         *   may have been a false conversion trigger on some versions of
         *   PCI230/260 due to the start conversion source being set to a
         *   high logic level.
         * - Enable ADC FIFO level interrupt.
         * - Set actual conversion trigger source and FIFO interrupt trigger
         *   level.
         * - If convert_src is TRIG_TIMER, set up the timers.
         */

        adccon = PCI230_ADC_FIFO_EN;
        adcen = 0;

        if (CR_AREF(cmd->chanlist[0]) == AREF_DIFF) {
                /* Differential - all channels must be differential. */
                diff = 1;
                adccon |= PCI230_ADC_IM_DIF;
        } else {
                /* Single ended - all channels must be single-ended. */
                diff = 0;
                adccon |= PCI230_ADC_IM_SE;
        }

        range = CR_RANGE(cmd->chanlist[0]);
        devpriv->ai_bipolar = comedi_range_is_bipolar(s, range);
        if (devpriv->ai_bipolar)
                adccon |= PCI230_ADC_IR_BIP;
        else
                adccon |= PCI230_ADC_IR_UNI;

        for (i = 0; i < cmd->chanlist_len; i++) {
                unsigned int gainshift;

                chan = CR_CHAN(cmd->chanlist[i]);
                range = CR_RANGE(cmd->chanlist[i]);
                if (diff) {
                        gainshift = 2 * chan;
                        if (devpriv->hwver == 0) {
                                /*
                                 * Original PCI230/260 expects both inputs of
                                 * the differential channel to be enabled.
                                 */
                                adcen |= 3 << gainshift;
                        } else {
                                /*
                                 * PCI230+/260+ expects only one input of the
                                 * differential channel to be enabled.
                                 */
                                adcen |= 1 << gainshift;
                        }
                } else {
                        gainshift = chan & ~1;
                        adcen |= 1 << chan;
                }
                devpriv->adcg = (devpriv->adcg & ~(3 << gainshift)) |
                                (pci230_ai_gain[range] << gainshift);
        }

        /* Set channel scan list. */
        outw(adcen, devpriv->daqio + PCI230_ADCEN);

        /* Set channel gains. */
        outw(devpriv->adcg, devpriv->daqio + PCI230_ADCG);

        /*
         * Set counter/timer 2 output high for use as the initial start
         * conversion source.
         */
        comedi_8254_set_mode(dev->pacer, 2, I8254_MODE1);

        /*
         * Temporarily use CT2 output as conversion trigger source and
         * temporarily set FIFO interrupt trigger level to 'full'.
         */
        adccon |= PCI230_ADC_INT_FIFO_FULL | PCI230_ADC_TRIG_Z2CT2;

        /*
         * Enable and reset FIFO, specify FIFO trigger level full, specify
         * uni/bip, se/diff, and temporarily set the start conversion source
         * to CT2 output.  Note that CT2 output is currently high, and this
         * will produce a false conversion trigger on some versions of the
         * PCI230/260, but that will be dealt with later.
         */
        devpriv->adccon = adccon;
        outw(adccon | PCI230_ADC_FIFO_RESET, devpriv->daqio + PCI230_ADCCON);

        /*
         * Delay -
         * Failure to include this will result in the first few channels'-worth
         * of data being corrupt, normally manifesting itself by large negative
         * voltages. It seems the board needs time to settle between the first
         * FIFO reset (above) and the second FIFO reset (below). Setting the
         * channel gains and scan list _before_ the first FIFO reset also
         * helps, though only slightly.
         */
        usleep_range(25, 100);

        /* Reset FIFO again. */
        outw(adccon | PCI230_ADC_FIFO_RESET, devpriv->daqio + PCI230_ADCCON);

        if (cmd->convert_src == TRIG_TIMER) {
                /*
                 * Set up CT2 as conversion timer, but gate it off for now.
                 * Note, counter/timer output 2 can be monitored on the
                 * connector: PCI230 pin 21, PCI260 pin 18.
                 */
                zgat = pci230_gat_config(2, GAT_GND);
                outb(zgat, dev->iobase + PCI230_ZGAT_SCE);
                /* Set counter/timer 2 to the specified conversion period. */
                pci230_ct_setup_ns_mode(dev, 2, I8254_MODE3, cmd->convert_arg,
                                        cmd->flags);
                if (cmd->scan_begin_src != TRIG_FOLLOW) {
                        /*
                         * Set up monostable on CT0 output for scan timing.  A
                         * rising edge on the trigger (gate) input of CT0 will
                         * trigger the monostable, causing its output to go low
                         * for the configured period.  The period depends on
                         * the conversion period and the number of conversions
                         * in the scan.
                         *
                         * Set the trigger high before setting up the
                         * monostable to stop it triggering.  The trigger
                         * source will be changed later.
                         */
                        zgat = pci230_gat_config(0, GAT_VCC);
                        outb(zgat, dev->iobase + PCI230_ZGAT_SCE);
                        pci230_ct_setup_ns_mode(dev, 0, I8254_MODE1,
                                                ((u64)cmd->convert_arg *
                                                 cmd->scan_end_arg),
                                                CMDF_ROUND_UP);
                        if (cmd->scan_begin_src == TRIG_TIMER) {
                                /*
                                 * Monostable on CT0 will be triggered by
                                 * output of CT1 at configured scan frequency.
                                 *
                                 * Set up CT1 but gate it off for now.
                                 */
                                zgat = pci230_gat_config(1, GAT_GND);
                                outb(zgat, dev->iobase + PCI230_ZGAT_SCE);
                                pci230_ct_setup_ns_mode(dev, 1, I8254_MODE3,
                                                        cmd->scan_begin_arg,
                                                        cmd->flags);
                        }
                }
        }

        if (cmd->start_src == TRIG_INT)
                s->async->inttrig = pci230_ai_inttrig_start;
        else    /* TRIG_NOW */
                pci230_ai_start(dev, s);

        return 0;
}

static int pci230_ai_cancel(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        pci230_ai_stop(dev, s);
        return 0;
}

/* Interrupt handler */
static irqreturn_t pci230_interrupt(int irq, void *d)
{
        unsigned char status_int, valid_status_int, temp_ier;
        struct comedi_device *dev = d;
        struct pci230_private *devpriv = dev->private;
        struct comedi_subdevice *s_ao = dev->write_subdev;
        struct comedi_subdevice *s_ai = dev->read_subdev;
        unsigned long irqflags;

        /* Read interrupt status/enable register. */
        status_int = inb(dev->iobase + PCI230_INT_STAT);

        if (status_int == PCI230_INT_DISABLE)
                return IRQ_NONE;

        spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
        valid_status_int = devpriv->ier & status_int;
        /*
         * Disable triggered interrupts.
         * (Only those interrupts that need re-enabling, are, later in the
         * handler).
         */
        temp_ier = devpriv->ier & ~status_int;
        outb(temp_ier, dev->iobase + PCI230_INT_SCE);
        devpriv->intr_running = true;
        devpriv->intr_cpuid = THISCPU;
        spin_unlock_irqrestore(&devpriv->isr_spinlock, irqflags);

        /*
         * Check the source of interrupt and handle it.
         * The PCI230 can cope with concurrent ADC, DAC, PPI C0 and C3
         * interrupts.  However, at present (Comedi-0.7.60) does not allow
         * concurrent execution of commands, instructions or a mixture of the
         * two.
         */

        if (valid_status_int & PCI230_INT_ZCLK_CT1)
                pci230_handle_ao_nofifo(dev, s_ao);

        if (valid_status_int & PCI230P2_INT_DAC)
                pci230_handle_ao_fifo(dev, s_ao);

        if (valid_status_int & PCI230_INT_ADC)
                pci230_handle_ai(dev, s_ai);

        /* Reenable interrupts. */
        spin_lock_irqsave(&devpriv->isr_spinlock, irqflags);
        if (devpriv->ier != temp_ier)
                outb(devpriv->ier, dev->iobase + PCI230_INT_SCE);
        devpriv->intr_running = false;
        spin_unlock_irqrestore(&devpriv->isr_spinlock, irqflags);

        if (s_ao)
                comedi_handle_events(dev, s_ao);
        comedi_handle_events(dev, s_ai);

        return IRQ_HANDLED;
}

/* Check if PCI device matches a specific board. */
static bool pci230_match_pci_board(const struct pci230_board *board,
                                   struct pci_dev *pci_dev)
{
        /* assume pci_dev->device != PCI_DEVICE_ID_INVALID */
        if (board->id != pci_dev->device)
                return false;
        if (board->min_hwver == 0)
                return true;
        /* Looking for a '+' model.  First check length of registers. */
        if (pci_resource_len(pci_dev, 3) < 32)
                return false;   /* Not a '+' model. */
        /*
         * TODO: temporarily enable PCI device and read the hardware version
         * register.  For now, assume it's okay.
         */
        return true;
}

/* Look for board matching PCI device. */
static const struct pci230_board *pci230_find_pci_board(struct pci_dev *pci_dev)
{
        unsigned int i;

        for (i = 0; i < ARRAY_SIZE(pci230_boards); i++)
                if (pci230_match_pci_board(&pci230_boards[i], pci_dev))
                        return &pci230_boards[i];
        return NULL;
}

static int pci230_auto_attach(struct comedi_device *dev,
                              unsigned long context_unused)
{
        struct pci_dev *pci_dev = comedi_to_pci_dev(dev);
        const struct pci230_board *board;
        struct pci230_private *devpriv;
        struct comedi_subdevice *s;
        int rc;

        dev_info(dev->class_dev, "amplc_pci230: attach pci %s\n",
                 pci_name(pci_dev));

        devpriv = comedi_alloc_devpriv(dev, sizeof(*devpriv));
        if (!devpriv)
                return -ENOMEM;

        spin_lock_init(&devpriv->isr_spinlock);
        spin_lock_init(&devpriv->res_spinlock);
        spin_lock_init(&devpriv->ai_stop_spinlock);
        spin_lock_init(&devpriv->ao_stop_spinlock);

        board = pci230_find_pci_board(pci_dev);
        if (!board) {
                dev_err(dev->class_dev,
                        "amplc_pci230: BUG! cannot determine board type!\n");
                return -EINVAL;
        }
        dev->board_ptr = board;
        dev->board_name = board->name;

        rc = comedi_pci_enable(dev);
        if (rc)
                return rc;

        /*
         * Read base addresses of the PCI230's two I/O regions from PCI
         * configuration register.
         */
        dev->iobase = pci_resource_start(pci_dev, 2);
        devpriv->daqio = pci_resource_start(pci_dev, 3);
        dev_dbg(dev->class_dev,
                "%s I/O region 1 0x%04lx I/O region 2 0x%04lx\n",
                dev->board_name, dev->iobase, devpriv->daqio);
        /* Read bits of DACCON register - only the output range. */
        devpriv->daccon = inw(devpriv->daqio + PCI230_DACCON) &
                          PCI230_DAC_OR_MASK;
        /*
         * Read hardware version register and set extended function register
         * if they exist.
         */
        if (pci_resource_len(pci_dev, 3) >= 32) {
                unsigned short extfunc = 0;

                devpriv->hwver = inw(devpriv->daqio + PCI230P_HWVER);
                if (devpriv->hwver < board->min_hwver) {
                        dev_err(dev->class_dev,
                                "%s - bad hardware version - got %u, need %u\n",
                                dev->board_name, devpriv->hwver,
                                board->min_hwver);
                        return -EIO;
                }
                if (devpriv->hwver > 0) {
                        if (!board->have_dio) {
                                /*
                                 * No DIO ports.  Route counters' external gates
                                 * to the EXTTRIG signal (PCI260+ pin 17).
                                 * (Otherwise, they would be routed to DIO
                                 * inputs PC0, PC1 and PC2 which don't exist
                                 * on PCI260[+].)
                                 */
                                extfunc |= PCI230P_EXTFUNC_GAT_EXTTRIG;
                        }
                        if (board->ao_bits && devpriv->hwver >= 2) {
                                /* Enable DAC FIFO functionality. */
                                extfunc |= PCI230P2_EXTFUNC_DACFIFO;
                        }
                }
                outw(extfunc, devpriv->daqio + PCI230P_EXTFUNC);
                if (extfunc & PCI230P2_EXTFUNC_DACFIFO) {
                        /*
                         * Temporarily enable DAC FIFO, reset it and disable
                         * FIFO wraparound.
                         */
                        outw(devpriv->daccon | PCI230P2_DAC_FIFO_EN |
                             PCI230P2_DAC_FIFO_RESET,
                             devpriv->daqio + PCI230_DACCON);
                        /* Clear DAC FIFO channel enable register. */
                        outw(0, devpriv->daqio + PCI230P2_DACEN);
                        /* Disable DAC FIFO. */
                        outw(devpriv->daccon, devpriv->daqio + PCI230_DACCON);
                }
        }
        /* Disable board's interrupts. */
        outb(0, dev->iobase + PCI230_INT_SCE);
        /* Set ADC to a reasonable state. */
        devpriv->adcg = 0;
        devpriv->adccon = PCI230_ADC_TRIG_NONE | PCI230_ADC_IM_SE |
                          PCI230_ADC_IR_BIP;
        outw(BIT(0), devpriv->daqio + PCI230_ADCEN);
        outw(devpriv->adcg, devpriv->daqio + PCI230_ADCG);
        outw(devpriv->adccon | PCI230_ADC_FIFO_RESET,
             devpriv->daqio + PCI230_ADCCON);

        if (pci_dev->irq) {
                rc = request_irq(pci_dev->irq, pci230_interrupt, IRQF_SHARED,
                                 dev->board_name, dev);
                if (rc == 0)
                        dev->irq = pci_dev->irq;
        }

        dev->pacer = comedi_8254_io_alloc(dev->iobase + PCI230_Z2_CT_BASE,
                                          0, I8254_IO8, 0);
        if (IS_ERR(dev->pacer))
                return PTR_ERR(dev->pacer);

        rc = comedi_alloc_subdevices(dev, 3);
        if (rc)
                return rc;

        s = &dev->subdevices[0];
        /* analog input subdevice */
        s->type = COMEDI_SUBD_AI;
        s->subdev_flags = SDF_READABLE | SDF_DIFF | SDF_GROUND;
        s->n_chan = 16;
        s->maxdata = (1 << board->ai_bits) - 1;
        s->range_table = &pci230_ai_range;
        s->insn_read = pci230_ai_insn_read;
        s->len_chanlist = 256;  /* but there are restrictions. */
        if (dev->irq) {
                dev->read_subdev = s;
                s->subdev_flags |= SDF_CMD_READ;
                s->do_cmd = pci230_ai_cmd;
                s->do_cmdtest = pci230_ai_cmdtest;
                s->cancel = pci230_ai_cancel;
        }

        s = &dev->subdevices[1];
        /* analog output subdevice */
        if (board->ao_bits) {
                s->type = COMEDI_SUBD_AO;
                s->subdev_flags = SDF_WRITABLE | SDF_GROUND;
                s->n_chan = 2;
                s->maxdata = (1 << board->ao_bits) - 1;
                s->range_table = &pci230_ao_range;
                s->insn_write = pci230_ao_insn_write;
                s->len_chanlist = 2;
                if (dev->irq) {
                        dev->write_subdev = s;
                        s->subdev_flags |= SDF_CMD_WRITE;
                        s->do_cmd = pci230_ao_cmd;
                        s->do_cmdtest = pci230_ao_cmdtest;
                        s->cancel = pci230_ao_cancel;
                }

                rc = comedi_alloc_subdev_readback(s);
                if (rc)
                        return rc;
        } else {
                s->type = COMEDI_SUBD_UNUSED;
        }

        s = &dev->subdevices[2];
        /* digital i/o subdevice */
        if (board->have_dio) {
                rc = subdev_8255_io_init(dev, s, PCI230_PPI_X_BASE);
                if (rc)
                        return rc;
        } else {
                s->type = COMEDI_SUBD_UNUSED;
        }

        return 0;
}

static struct comedi_driver amplc_pci230_driver = {
        .driver_name    = "amplc_pci230",
        .module         = THIS_MODULE,
        .auto_attach    = pci230_auto_attach,
        .detach         = comedi_pci_detach,
};

static int amplc_pci230_pci_probe(struct pci_dev *dev,
                                  const struct pci_device_id *id)
{
        return comedi_pci_auto_config(dev, &amplc_pci230_driver,
                                      id->driver_data);
}

static const struct pci_device_id amplc_pci230_pci_table[] = {
        { PCI_VDEVICE(AMPLICON, PCI_DEVICE_ID_PCI230) },
        { PCI_VDEVICE(AMPLICON, PCI_DEVICE_ID_PCI260) },
        { }
};
MODULE_DEVICE_TABLE(pci, amplc_pci230_pci_table);

static struct pci_driver amplc_pci230_pci_driver = {
        .name           = "amplc_pci230",
        .id_table       = amplc_pci230_pci_table,
        .probe          = amplc_pci230_pci_probe,
        .remove         = comedi_pci_auto_unconfig,
};
module_comedi_pci_driver(amplc_pci230_driver, amplc_pci230_pci_driver);

MODULE_AUTHOR("Comedi https://www.comedi.org");
MODULE_DESCRIPTION("Comedi driver for Amplicon PCI230(+) and PCI260(+)");
MODULE_LICENSE("GPL");