root/drivers/comedi/drivers/me4000.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * me4000.c
 * Source code for the Meilhaus ME-4000 board family.
 *
 * COMEDI - Linux Control and Measurement Device Interface
 * Copyright (C) 2000 David A. Schleef <ds@schleef.org>
 */

/*
 * Driver: me4000
 * Description: Meilhaus ME-4000 series boards
 * Devices: [Meilhaus] ME-4650 (me4000), ME-4670i, ME-4680, ME-4680i,
 *          ME-4680is
 * Author: gg (Guenter Gebhardt <g.gebhardt@meilhaus.com>)
 * Updated: Mon, 18 Mar 2002 15:34:01 -0800
 * Status: untested
 *
 * Supports:
 *      - Analog Input
 *      - Analog Output
 *      - Digital I/O
 *      - Counter
 *
 * Configuration Options: not applicable, uses PCI auto config
 *
 * The firmware required by these boards is available in the
 * comedi_nonfree_firmware tarball available from
 * https://www.comedi.org.
 */

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

#include "plx9052.h"

#define ME4000_FIRMWARE         "me4000_firmware.bin"

/*
 * ME4000 Register map and bit defines
 */
#define ME4000_AO_CHAN(x)                       ((x) * 0x18)

#define ME4000_AO_CTRL_REG(x)                   (0x00 + ME4000_AO_CHAN(x))
#define ME4000_AO_CTRL_MODE_0                   BIT(0)
#define ME4000_AO_CTRL_MODE_1                   BIT(1)
#define ME4000_AO_CTRL_STOP                     BIT(2)
#define ME4000_AO_CTRL_ENABLE_FIFO              BIT(3)
#define ME4000_AO_CTRL_ENABLE_EX_TRIG           BIT(4)
#define ME4000_AO_CTRL_EX_TRIG_EDGE             BIT(5)
#define ME4000_AO_CTRL_IMMEDIATE_STOP           BIT(7)
#define ME4000_AO_CTRL_ENABLE_DO                BIT(8)
#define ME4000_AO_CTRL_ENABLE_IRQ               BIT(9)
#define ME4000_AO_CTRL_RESET_IRQ                BIT(10)
#define ME4000_AO_STATUS_REG(x)                 (0x04 + ME4000_AO_CHAN(x))
#define ME4000_AO_STATUS_FSM                    BIT(0)
#define ME4000_AO_STATUS_FF                     BIT(1)
#define ME4000_AO_STATUS_HF                     BIT(2)
#define ME4000_AO_STATUS_EF                     BIT(3)
#define ME4000_AO_FIFO_REG(x)                   (0x08 + ME4000_AO_CHAN(x))
#define ME4000_AO_SINGLE_REG(x)                 (0x0c + ME4000_AO_CHAN(x))
#define ME4000_AO_TIMER_REG(x)                  (0x10 + ME4000_AO_CHAN(x))
#define ME4000_AI_CTRL_REG                      0x74
#define ME4000_AI_STATUS_REG                    0x74
#define ME4000_AI_CTRL_MODE_0                   BIT(0)
#define ME4000_AI_CTRL_MODE_1                   BIT(1)
#define ME4000_AI_CTRL_MODE_2                   BIT(2)
#define ME4000_AI_CTRL_SAMPLE_HOLD              BIT(3)
#define ME4000_AI_CTRL_IMMEDIATE_STOP           BIT(4)
#define ME4000_AI_CTRL_STOP                     BIT(5)
#define ME4000_AI_CTRL_CHANNEL_FIFO             BIT(6)
#define ME4000_AI_CTRL_DATA_FIFO                BIT(7)
#define ME4000_AI_CTRL_FULLSCALE                BIT(8)
#define ME4000_AI_CTRL_OFFSET                   BIT(9)
#define ME4000_AI_CTRL_EX_TRIG_ANALOG           BIT(10)
#define ME4000_AI_CTRL_EX_TRIG                  BIT(11)
#define ME4000_AI_CTRL_EX_TRIG_FALLING          BIT(12)
#define ME4000_AI_CTRL_EX_IRQ                   BIT(13)
#define ME4000_AI_CTRL_EX_IRQ_RESET             BIT(14)
#define ME4000_AI_CTRL_LE_IRQ                   BIT(15)
#define ME4000_AI_CTRL_LE_IRQ_RESET             BIT(16)
#define ME4000_AI_CTRL_HF_IRQ                   BIT(17)
#define ME4000_AI_CTRL_HF_IRQ_RESET             BIT(18)
#define ME4000_AI_CTRL_SC_IRQ                   BIT(19)
#define ME4000_AI_CTRL_SC_IRQ_RESET             BIT(20)
#define ME4000_AI_CTRL_SC_RELOAD                BIT(21)
#define ME4000_AI_STATUS_EF_CHANNEL             BIT(22)
#define ME4000_AI_STATUS_HF_CHANNEL             BIT(23)
#define ME4000_AI_STATUS_FF_CHANNEL             BIT(24)
#define ME4000_AI_STATUS_EF_DATA                BIT(25)
#define ME4000_AI_STATUS_HF_DATA                BIT(26)
#define ME4000_AI_STATUS_FF_DATA                BIT(27)
#define ME4000_AI_STATUS_LE                     BIT(28)
#define ME4000_AI_STATUS_FSM                    BIT(29)
#define ME4000_AI_CTRL_EX_TRIG_BOTH             BIT(31)
#define ME4000_AI_CHANNEL_LIST_REG              0x78
#define ME4000_AI_LIST_INPUT_DIFFERENTIAL       BIT(5)
#define ME4000_AI_LIST_RANGE(x)                 ((3 - ((x) & 3)) << 6)
#define ME4000_AI_LIST_LAST_ENTRY               BIT(8)
#define ME4000_AI_DATA_REG                      0x7c
#define ME4000_AI_CHAN_TIMER_REG                0x80
#define ME4000_AI_CHAN_PRE_TIMER_REG            0x84
#define ME4000_AI_SCAN_TIMER_LOW_REG            0x88
#define ME4000_AI_SCAN_TIMER_HIGH_REG           0x8c
#define ME4000_AI_SCAN_PRE_TIMER_LOW_REG        0x90
#define ME4000_AI_SCAN_PRE_TIMER_HIGH_REG       0x94
#define ME4000_AI_START_REG                     0x98
#define ME4000_IRQ_STATUS_REG                   0x9c
#define ME4000_IRQ_STATUS_EX                    BIT(0)
#define ME4000_IRQ_STATUS_LE                    BIT(1)
#define ME4000_IRQ_STATUS_AI_HF                 BIT(2)
#define ME4000_IRQ_STATUS_AO_0_HF               BIT(3)
#define ME4000_IRQ_STATUS_AO_1_HF               BIT(4)
#define ME4000_IRQ_STATUS_AO_2_HF               BIT(5)
#define ME4000_IRQ_STATUS_AO_3_HF               BIT(6)
#define ME4000_IRQ_STATUS_SC                    BIT(7)
#define ME4000_DIO_PORT_0_REG                   0xa0
#define ME4000_DIO_PORT_1_REG                   0xa4
#define ME4000_DIO_PORT_2_REG                   0xa8
#define ME4000_DIO_PORT_3_REG                   0xac
#define ME4000_DIO_DIR_REG                      0xb0
#define ME4000_AO_LOADSETREG_XX                 0xb4
#define ME4000_DIO_CTRL_REG                     0xb8
#define ME4000_DIO_CTRL_MODE_0                  BIT(0)
#define ME4000_DIO_CTRL_MODE_1                  BIT(1)
#define ME4000_DIO_CTRL_MODE_2                  BIT(2)
#define ME4000_DIO_CTRL_MODE_3                  BIT(3)
#define ME4000_DIO_CTRL_MODE_4                  BIT(4)
#define ME4000_DIO_CTRL_MODE_5                  BIT(5)
#define ME4000_DIO_CTRL_MODE_6                  BIT(6)
#define ME4000_DIO_CTRL_MODE_7                  BIT(7)
#define ME4000_DIO_CTRL_FUNCTION_0              BIT(8)
#define ME4000_DIO_CTRL_FUNCTION_1              BIT(9)
#define ME4000_DIO_CTRL_FIFO_HIGH_0             BIT(10)
#define ME4000_DIO_CTRL_FIFO_HIGH_1             BIT(11)
#define ME4000_DIO_CTRL_FIFO_HIGH_2             BIT(12)
#define ME4000_DIO_CTRL_FIFO_HIGH_3             BIT(13)
#define ME4000_AO_DEMUX_ADJUST_REG              0xbc
#define ME4000_AO_DEMUX_ADJUST_VALUE            0x4c
#define ME4000_AI_SAMPLE_COUNTER_REG            0xc0

#define ME4000_AI_FIFO_COUNT                    2048

#define ME4000_AI_MIN_TICKS                     66
#define ME4000_AI_MIN_SAMPLE_TIME               2000

#define ME4000_AI_CHANNEL_LIST_COUNT            1024

struct me4000_private {
        unsigned long plx_regbase;
        unsigned int ai_ctrl_mode;
        unsigned int ai_init_ticks;
        unsigned int ai_scan_ticks;
        unsigned int ai_chan_ticks;
};

enum me4000_boardid {
        BOARD_ME4650,
        BOARD_ME4660,
        BOARD_ME4660I,
        BOARD_ME4660S,
        BOARD_ME4660IS,
        BOARD_ME4670,
        BOARD_ME4670I,
        BOARD_ME4670S,
        BOARD_ME4670IS,
        BOARD_ME4680,
        BOARD_ME4680I,
        BOARD_ME4680S,
        BOARD_ME4680IS,
};

struct me4000_board {
        const char *name;
        int ai_nchan;
        unsigned int can_do_diff_ai:1;
        unsigned int can_do_sh_ai:1;    /* sample & hold (8 channels) */
        unsigned int ex_trig_analog:1;
        unsigned int has_ao:1;
        unsigned int has_ao_fifo:1;
        unsigned int has_counter:1;
};

static const struct me4000_board me4000_boards[] = {
        [BOARD_ME4650] = {
                .name           = "ME-4650",
                .ai_nchan       = 16,
        },
        [BOARD_ME4660] = {
                .name           = "ME-4660",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4660I] = {
                .name           = "ME-4660i",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4660S] = {
                .name           = "ME-4660s",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .can_do_sh_ai   = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4660IS] = {
                .name           = "ME-4660is",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .can_do_sh_ai   = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4670] = {
                .name           = "ME-4670",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .ex_trig_analog = 1,
                .has_ao         = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4670I] = {
                .name           = "ME-4670i",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .ex_trig_analog = 1,
                .has_ao         = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4670S] = {
                .name           = "ME-4670s",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .can_do_sh_ai   = 1,
                .ex_trig_analog = 1,
                .has_ao         = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4670IS] = {
                .name           = "ME-4670is",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .can_do_sh_ai   = 1,
                .ex_trig_analog = 1,
                .has_ao         = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4680] = {
                .name           = "ME-4680",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .ex_trig_analog = 1,
                .has_ao         = 1,
                .has_ao_fifo    = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4680I] = {
                .name           = "ME-4680i",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .ex_trig_analog = 1,
                .has_ao         = 1,
                .has_ao_fifo    = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4680S] = {
                .name           = "ME-4680s",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .can_do_sh_ai   = 1,
                .ex_trig_analog = 1,
                .has_ao         = 1,
                .has_ao_fifo    = 1,
                .has_counter    = 1,
        },
        [BOARD_ME4680IS] = {
                .name           = "ME-4680is",
                .ai_nchan       = 32,
                .can_do_diff_ai = 1,
                .can_do_sh_ai   = 1,
                .ex_trig_analog = 1,
                .has_ao         = 1,
                .has_ao_fifo    = 1,
                .has_counter    = 1,
        },
};

/*
 * NOTE: the ranges here are inverted compared to the values
 * written to the ME4000_AI_CHANNEL_LIST_REG,
 *
 * The ME4000_AI_LIST_RANGE() macro handles the inversion.
 */
static const struct comedi_lrange me4000_ai_range = {
        4, {
                UNI_RANGE(2.5),
                UNI_RANGE(10),
                BIP_RANGE(2.5),
                BIP_RANGE(10)
        }
};

static int me4000_xilinx_download(struct comedi_device *dev,
                                  const u8 *data, size_t size,
                                  unsigned long context)
{
        struct pci_dev *pcidev = comedi_to_pci_dev(dev);
        struct me4000_private *devpriv = dev->private;
        unsigned long xilinx_iobase = pci_resource_start(pcidev, 5);
        unsigned int file_length;
        unsigned int val;
        unsigned int i;

        /* Get data stream length from header. */
        if (size >= 4) {
                file_length = (((unsigned int)data[0] & 0xff) << 24) +
                              (((unsigned int)data[1] & 0xff) << 16) +
                              (((unsigned int)data[2] & 0xff) << 8) +
                              ((unsigned int)data[3] & 0xff);
        }
        if (size < 16 || file_length > size - 16) {
                dev_err(dev->class_dev, "Firmware length inconsistency\n");
                return -EINVAL;
        }

        if (!xilinx_iobase)
                return -ENODEV;

        /*
         * Set PLX local interrupt 2 polarity to high.
         * Interrupt is thrown by init pin of xilinx.
         */
        outl(PLX9052_INTCSR_LI2POL, devpriv->plx_regbase + PLX9052_INTCSR);

        /* Set /CS and /WRITE of the Xilinx */
        val = inl(devpriv->plx_regbase + PLX9052_CNTRL);
        val |= PLX9052_CNTRL_UIO2_DATA;
        outl(val, devpriv->plx_regbase + PLX9052_CNTRL);

        /* Init Xilinx with CS1 */
        inb(xilinx_iobase + 0xC8);

        /* Wait until /INIT pin is set */
        usleep_range(20, 1000);
        val = inl(devpriv->plx_regbase + PLX9052_INTCSR);
        if (!(val & PLX9052_INTCSR_LI2STAT)) {
                dev_err(dev->class_dev, "Can't init Xilinx\n");
                return -EIO;
        }

        /* Reset /CS and /WRITE of the Xilinx */
        val = inl(devpriv->plx_regbase + PLX9052_CNTRL);
        val &= ~PLX9052_CNTRL_UIO2_DATA;
        outl(val, devpriv->plx_regbase + PLX9052_CNTRL);

        /* Download Xilinx firmware */
        usleep_range(10, 1000);

        for (i = 0; i < file_length; i++) {
                outb(data[16 + i], xilinx_iobase);
                usleep_range(10, 1000);

                /* Check if BUSY flag is low */
                val = inl(devpriv->plx_regbase + PLX9052_CNTRL);
                if (val & PLX9052_CNTRL_UIO1_DATA) {
                        dev_err(dev->class_dev,
                                "Xilinx is still busy (i = %d)\n", i);
                        return -EIO;
                }
        }

        /* If done flag is high download was successful */
        val = inl(devpriv->plx_regbase + PLX9052_CNTRL);
        if (!(val & PLX9052_CNTRL_UIO0_DATA)) {
                dev_err(dev->class_dev, "DONE flag is not set\n");
                dev_err(dev->class_dev, "Download not successful\n");
                return -EIO;
        }

        /* Set /CS and /WRITE */
        val = inl(devpriv->plx_regbase + PLX9052_CNTRL);
        val |= PLX9052_CNTRL_UIO2_DATA;
        outl(val, devpriv->plx_regbase + PLX9052_CNTRL);

        return 0;
}

static void me4000_ai_reset(struct comedi_device *dev)
{
        unsigned int ctrl;

        /* Stop any running conversion */
        ctrl = inl(dev->iobase + ME4000_AI_CTRL_REG);
        ctrl |= ME4000_AI_CTRL_STOP | ME4000_AI_CTRL_IMMEDIATE_STOP;
        outl(ctrl, dev->iobase + ME4000_AI_CTRL_REG);

        /* Clear the control register */
        outl(0x0, dev->iobase + ME4000_AI_CTRL_REG);
}

static void me4000_reset(struct comedi_device *dev)
{
        struct me4000_private *devpriv = dev->private;
        unsigned int val;
        int chan;

        /* Disable interrupts on the PLX */
        outl(0, devpriv->plx_regbase + PLX9052_INTCSR);

        /* Software reset the PLX */
        val = inl(devpriv->plx_regbase + PLX9052_CNTRL);
        val |= PLX9052_CNTRL_PCI_RESET;
        outl(val, devpriv->plx_regbase + PLX9052_CNTRL);
        val &= ~PLX9052_CNTRL_PCI_RESET;
        outl(val, devpriv->plx_regbase + PLX9052_CNTRL);

        /* 0x8000 to the DACs means an output voltage of 0V */
        for (chan = 0; chan < 4; chan++)
                outl(0x8000, dev->iobase + ME4000_AO_SINGLE_REG(chan));

        me4000_ai_reset(dev);

        /* Set both stop bits in the analog output control register */
        val = ME4000_AO_CTRL_IMMEDIATE_STOP | ME4000_AO_CTRL_STOP;
        for (chan = 0; chan < 4; chan++)
                outl(val, dev->iobase + ME4000_AO_CTRL_REG(chan));

        /* Set the adustment register for AO demux */
        outl(ME4000_AO_DEMUX_ADJUST_VALUE,
             dev->iobase + ME4000_AO_DEMUX_ADJUST_REG);

        /*
         * Set digital I/O direction for port 0
         * to output on isolated versions
         */
        if (!(inl(dev->iobase + ME4000_DIO_DIR_REG) & 0x1))
                outl(0x1, dev->iobase + ME4000_DIO_CTRL_REG);
}

static unsigned int me4000_ai_get_sample(struct comedi_device *dev,
                                         struct comedi_subdevice *s)
{
        unsigned int val;

        /* read two's complement value and munge to offset binary */
        val = inl(dev->iobase + ME4000_AI_DATA_REG);
        return comedi_offset_munge(s, val);
}

static int me4000_ai_eoc(struct comedi_device *dev,
                         struct comedi_subdevice *s,
                         struct comedi_insn *insn,
                         unsigned long context)
{
        unsigned int status;

        status = inl(dev->iobase + ME4000_AI_STATUS_REG);
        if (status & ME4000_AI_STATUS_EF_DATA)
                return 0;
        return -EBUSY;
}

static int me4000_ai_insn_read(struct comedi_device *dev,
                               struct comedi_subdevice *s,
                               struct comedi_insn *insn,
                               unsigned int *data)
{
        unsigned int chan = CR_CHAN(insn->chanspec);
        unsigned int range = CR_RANGE(insn->chanspec);
        unsigned int aref = CR_AREF(insn->chanspec);
        unsigned int entry;
        int ret = 0;
        int i;

        entry = chan | ME4000_AI_LIST_RANGE(range);
        if (aref == AREF_DIFF) {
                if (!(s->subdev_flags & SDF_DIFF)) {
                        dev_err(dev->class_dev,
                                "Differential inputs are not available\n");
                        return -EINVAL;
                }

                if (!comedi_range_is_bipolar(s, range)) {
                        dev_err(dev->class_dev,
                                "Range must be bipolar when aref = diff\n");
                        return -EINVAL;
                }

                if (chan >= (s->n_chan / 2)) {
                        dev_err(dev->class_dev,
                                "Analog input is not available\n");
                        return -EINVAL;
                }
                entry |= ME4000_AI_LIST_INPUT_DIFFERENTIAL;
        }

        entry |= ME4000_AI_LIST_LAST_ENTRY;

        /* Enable channel list and data fifo for single acquisition mode */
        outl(ME4000_AI_CTRL_CHANNEL_FIFO | ME4000_AI_CTRL_DATA_FIFO,
             dev->iobase + ME4000_AI_CTRL_REG);

        /* Generate channel list entry */
        outl(entry, dev->iobase + ME4000_AI_CHANNEL_LIST_REG);

        /* Set the timer to maximum sample rate */
        outl(ME4000_AI_MIN_TICKS, dev->iobase + ME4000_AI_CHAN_TIMER_REG);
        outl(ME4000_AI_MIN_TICKS, dev->iobase + ME4000_AI_CHAN_PRE_TIMER_REG);

        for (i = 0; i < insn->n; i++) {
                unsigned int val;

                /* start conversion by dummy read */
                inl(dev->iobase + ME4000_AI_START_REG);

                ret = comedi_timeout(dev, s, insn, me4000_ai_eoc, 0);
                if (ret)
                        break;

                val = me4000_ai_get_sample(dev, s);
                data[i] = comedi_offset_munge(s, val);
        }

        me4000_ai_reset(dev);

        return ret ? ret : insn->n;
}

static int me4000_ai_cancel(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        me4000_ai_reset(dev);

        return 0;
}

static int me4000_ai_check_chanlist(struct comedi_device *dev,
                                    struct comedi_subdevice *s,
                                    struct comedi_cmd *cmd)
{
        unsigned int aref0 = CR_AREF(cmd->chanlist[0]);
        int i;

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

                if (aref != aref0) {
                        dev_dbg(dev->class_dev,
                                "Mode is not equal for all entries\n");
                        return -EINVAL;
                }

                if (aref == AREF_DIFF) {
                        if (!(s->subdev_flags & SDF_DIFF)) {
                                dev_err(dev->class_dev,
                                        "Differential inputs are not available\n");
                                return -EINVAL;
                        }

                        if (chan >= (s->n_chan / 2)) {
                                dev_dbg(dev->class_dev,
                                        "Channel number to high\n");
                                return -EINVAL;
                        }

                        if (!comedi_range_is_bipolar(s, range)) {
                                dev_dbg(dev->class_dev,
                                        "Bipolar is not selected in differential mode\n");
                                return -EINVAL;
                        }
                }
        }

        return 0;
}

static void me4000_ai_round_cmd_args(struct comedi_device *dev,
                                     struct comedi_subdevice *s,
                                     struct comedi_cmd *cmd)
{
        struct me4000_private *devpriv = dev->private;
        int rest;

        devpriv->ai_init_ticks = 0;
        devpriv->ai_scan_ticks = 0;
        devpriv->ai_chan_ticks = 0;

        if (cmd->start_arg) {
                devpriv->ai_init_ticks = (cmd->start_arg * 33) / 1000;
                rest = (cmd->start_arg * 33) % 1000;

                if ((cmd->flags & CMDF_ROUND_MASK) == CMDF_ROUND_NEAREST) {
                        if (rest > 33)
                                devpriv->ai_init_ticks++;
                } else if ((cmd->flags & CMDF_ROUND_MASK) == CMDF_ROUND_UP) {
                        if (rest)
                                devpriv->ai_init_ticks++;
                }
        }

        if (cmd->scan_begin_arg) {
                devpriv->ai_scan_ticks = (cmd->scan_begin_arg * 33) / 1000;
                rest = (cmd->scan_begin_arg * 33) % 1000;

                if ((cmd->flags & CMDF_ROUND_MASK) == CMDF_ROUND_NEAREST) {
                        if (rest > 33)
                                devpriv->ai_scan_ticks++;
                } else if ((cmd->flags & CMDF_ROUND_MASK) == CMDF_ROUND_UP) {
                        if (rest)
                                devpriv->ai_scan_ticks++;
                }
        }

        if (cmd->convert_arg) {
                devpriv->ai_chan_ticks = (cmd->convert_arg * 33) / 1000;
                rest = (cmd->convert_arg * 33) % 1000;

                if ((cmd->flags & CMDF_ROUND_MASK) == CMDF_ROUND_NEAREST) {
                        if (rest > 33)
                                devpriv->ai_chan_ticks++;
                } else if ((cmd->flags & CMDF_ROUND_MASK) == CMDF_ROUND_UP) {
                        if (rest)
                                devpriv->ai_chan_ticks++;
                }
        }
}

static void me4000_ai_write_chanlist(struct comedi_device *dev,
                                     struct comedi_subdevice *s,
                                     struct comedi_cmd *cmd)
{
        int i;

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

                entry = chan | ME4000_AI_LIST_RANGE(range);

                if (aref == AREF_DIFF)
                        entry |= ME4000_AI_LIST_INPUT_DIFFERENTIAL;

                if (i == (cmd->chanlist_len - 1))
                        entry |= ME4000_AI_LIST_LAST_ENTRY;

                outl(entry, dev->iobase + ME4000_AI_CHANNEL_LIST_REG);
        }
}

static int me4000_ai_do_cmd(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        struct me4000_private *devpriv = dev->private;
        struct comedi_cmd *cmd = &s->async->cmd;
        unsigned int ctrl;

        /* Write timer arguments */
        outl(devpriv->ai_init_ticks - 1,
             dev->iobase + ME4000_AI_SCAN_PRE_TIMER_LOW_REG);
        outl(0x0, dev->iobase + ME4000_AI_SCAN_PRE_TIMER_HIGH_REG);

        if (devpriv->ai_scan_ticks) {
                outl(devpriv->ai_scan_ticks - 1,
                     dev->iobase + ME4000_AI_SCAN_TIMER_LOW_REG);
                outl(0x0, dev->iobase + ME4000_AI_SCAN_TIMER_HIGH_REG);
        }

        outl(devpriv->ai_chan_ticks - 1,
             dev->iobase + ME4000_AI_CHAN_PRE_TIMER_REG);
        outl(devpriv->ai_chan_ticks - 1,
             dev->iobase + ME4000_AI_CHAN_TIMER_REG);

        /* Start sources */
        ctrl = devpriv->ai_ctrl_mode |
               ME4000_AI_CTRL_CHANNEL_FIFO |
               ME4000_AI_CTRL_DATA_FIFO;

        /* Stop triggers */
        if (cmd->stop_src == TRIG_COUNT) {
                outl(cmd->chanlist_len * cmd->stop_arg,
                     dev->iobase + ME4000_AI_SAMPLE_COUNTER_REG);
                ctrl |= ME4000_AI_CTRL_SC_IRQ;
        } else if (cmd->stop_src == TRIG_NONE &&
                   cmd->scan_end_src == TRIG_COUNT) {
                outl(cmd->scan_end_arg,
                     dev->iobase + ME4000_AI_SAMPLE_COUNTER_REG);
                ctrl |= ME4000_AI_CTRL_SC_IRQ;
        }
        ctrl |= ME4000_AI_CTRL_HF_IRQ;

        /* Write the setup to the control register */
        outl(ctrl, dev->iobase + ME4000_AI_CTRL_REG);

        /* Write the channel list */
        me4000_ai_write_chanlist(dev, s, cmd);

        /* Start acquistion by dummy read */
        inl(dev->iobase + ME4000_AI_START_REG);

        return 0;
}

static int me4000_ai_do_cmd_test(struct comedi_device *dev,
                                 struct comedi_subdevice *s,
                                 struct comedi_cmd *cmd)
{
        struct me4000_private *devpriv = dev->private;
        int err = 0;

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

        err |= comedi_check_trigger_src(&cmd->start_src, TRIG_NOW | TRIG_EXT);
        err |= comedi_check_trigger_src(&cmd->scan_begin_src,
                                        TRIG_FOLLOW | TRIG_TIMER | TRIG_EXT);
        err |= comedi_check_trigger_src(&cmd->convert_src,
                                        TRIG_TIMER | TRIG_EXT);
        err |= comedi_check_trigger_src(&cmd->scan_end_src,
                                        TRIG_NONE | TRIG_COUNT);
        err |= comedi_check_trigger_src(&cmd->stop_src, TRIG_NONE | TRIG_COUNT);

        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->scan_end_src);
        err |= comedi_check_trigger_is_unique(cmd->stop_src);

        /* Step 2b : and mutually compatible */

        if (cmd->start_src == TRIG_NOW &&
            cmd->scan_begin_src == TRIG_TIMER &&
            cmd->convert_src == TRIG_TIMER) {
                devpriv->ai_ctrl_mode = ME4000_AI_CTRL_MODE_0;
        } else if (cmd->start_src == TRIG_NOW &&
                   cmd->scan_begin_src == TRIG_FOLLOW &&
                   cmd->convert_src == TRIG_TIMER) {
                devpriv->ai_ctrl_mode = ME4000_AI_CTRL_MODE_0;
        } else if (cmd->start_src == TRIG_EXT &&
                   cmd->scan_begin_src == TRIG_TIMER &&
                   cmd->convert_src == TRIG_TIMER) {
                devpriv->ai_ctrl_mode = ME4000_AI_CTRL_MODE_1;
        } else if (cmd->start_src == TRIG_EXT &&
                   cmd->scan_begin_src == TRIG_FOLLOW &&
                   cmd->convert_src == TRIG_TIMER) {
                devpriv->ai_ctrl_mode = ME4000_AI_CTRL_MODE_1;
        } else if (cmd->start_src == TRIG_EXT &&
                   cmd->scan_begin_src == TRIG_EXT &&
                   cmd->convert_src == TRIG_TIMER) {
                devpriv->ai_ctrl_mode = ME4000_AI_CTRL_MODE_2;
        } else if (cmd->start_src == TRIG_EXT &&
                   cmd->scan_begin_src == TRIG_EXT &&
                   cmd->convert_src == TRIG_EXT) {
                devpriv->ai_ctrl_mode = ME4000_AI_CTRL_MODE_0 |
                                        ME4000_AI_CTRL_MODE_1;
        } else {
                err |= -EINVAL;
        }

        if (err)
                return 2;

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

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

        if (cmd->chanlist_len < 1) {
                cmd->chanlist_len = 1;
                err |= -EINVAL;
        }

        /* Round the timer arguments */
        me4000_ai_round_cmd_args(dev, s, cmd);

        if (devpriv->ai_init_ticks < 66) {
                cmd->start_arg = 2000;
                err |= -EINVAL;
        }
        if (devpriv->ai_scan_ticks && devpriv->ai_scan_ticks < 67) {
                cmd->scan_begin_arg = 2031;
                err |= -EINVAL;
        }
        if (devpriv->ai_chan_ticks < 66) {
                cmd->convert_arg = 2000;
                err |= -EINVAL;
        }

        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;

        /*
         * Stage 4. Check for argument conflicts.
         */
        if (cmd->start_src == TRIG_NOW &&
            cmd->scan_begin_src == TRIG_TIMER &&
            cmd->convert_src == TRIG_TIMER) {
                /* Check timer arguments */
                if (devpriv->ai_init_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid start arg\n");
                        cmd->start_arg = 2000;  /*  66 ticks at least */
                        err++;
                }
                if (devpriv->ai_chan_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid convert arg\n");
                        cmd->convert_arg = 2000;        /*  66 ticks at least */
                        err++;
                }
                if (devpriv->ai_scan_ticks <=
                    cmd->chanlist_len * devpriv->ai_chan_ticks) {
                        dev_err(dev->class_dev, "Invalid scan end arg\n");

                        /*  At least one tick more */
                        cmd->scan_end_arg = 2000 * cmd->chanlist_len + 31;
                        err++;
                }
        } else if (cmd->start_src == TRIG_NOW &&
                   cmd->scan_begin_src == TRIG_FOLLOW &&
                   cmd->convert_src == TRIG_TIMER) {
                /* Check timer arguments */
                if (devpriv->ai_init_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid start arg\n");
                        cmd->start_arg = 2000;  /*  66 ticks at least */
                        err++;
                }
                if (devpriv->ai_chan_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid convert arg\n");
                        cmd->convert_arg = 2000;        /*  66 ticks at least */
                        err++;
                }
        } else if (cmd->start_src == TRIG_EXT &&
                   cmd->scan_begin_src == TRIG_TIMER &&
                   cmd->convert_src == TRIG_TIMER) {
                /* Check timer arguments */
                if (devpriv->ai_init_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid start arg\n");
                        cmd->start_arg = 2000;  /*  66 ticks at least */
                        err++;
                }
                if (devpriv->ai_chan_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid convert arg\n");
                        cmd->convert_arg = 2000;        /*  66 ticks at least */
                        err++;
                }
                if (devpriv->ai_scan_ticks <=
                    cmd->chanlist_len * devpriv->ai_chan_ticks) {
                        dev_err(dev->class_dev, "Invalid scan end arg\n");

                        /*  At least one tick more */
                        cmd->scan_end_arg = 2000 * cmd->chanlist_len + 31;
                        err++;
                }
        } else if (cmd->start_src == TRIG_EXT &&
                   cmd->scan_begin_src == TRIG_FOLLOW &&
                   cmd->convert_src == TRIG_TIMER) {
                /* Check timer arguments */
                if (devpriv->ai_init_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid start arg\n");
                        cmd->start_arg = 2000;  /*  66 ticks at least */
                        err++;
                }
                if (devpriv->ai_chan_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid convert arg\n");
                        cmd->convert_arg = 2000;        /*  66 ticks at least */
                        err++;
                }
        } else if (cmd->start_src == TRIG_EXT &&
                   cmd->scan_begin_src == TRIG_EXT &&
                   cmd->convert_src == TRIG_TIMER) {
                /* Check timer arguments */
                if (devpriv->ai_init_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid start arg\n");
                        cmd->start_arg = 2000;  /*  66 ticks at least */
                        err++;
                }
                if (devpriv->ai_chan_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid convert arg\n");
                        cmd->convert_arg = 2000;        /*  66 ticks at least */
                        err++;
                }
        } else if (cmd->start_src == TRIG_EXT &&
                   cmd->scan_begin_src == TRIG_EXT &&
                   cmd->convert_src == TRIG_EXT) {
                /* Check timer arguments */
                if (devpriv->ai_init_ticks < ME4000_AI_MIN_TICKS) {
                        dev_err(dev->class_dev, "Invalid start arg\n");
                        cmd->start_arg = 2000;  /*  66 ticks at least */
                        err++;
                }
        }
        if (cmd->scan_end_src == TRIG_COUNT) {
                if (cmd->scan_end_arg == 0) {
                        dev_err(dev->class_dev, "Invalid scan end arg\n");
                        cmd->scan_end_arg = 1;
                        err++;
                }
        }

        if (err)
                return 4;

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

        if (err)
                return 5;

        return 0;
}

static irqreturn_t me4000_ai_isr(int irq, void *dev_id)
{
        unsigned int tmp;
        struct comedi_device *dev = dev_id;
        struct comedi_subdevice *s = dev->read_subdev;
        int i;
        int c = 0;
        unsigned short lval;

        if (!dev->attached)
                return IRQ_NONE;

        if (inl(dev->iobase + ME4000_IRQ_STATUS_REG) &
            ME4000_IRQ_STATUS_AI_HF) {
                /* Read status register to find out what happened */
                tmp = inl(dev->iobase + ME4000_AI_STATUS_REG);

                if (!(tmp & ME4000_AI_STATUS_FF_DATA) &&
                    !(tmp & ME4000_AI_STATUS_HF_DATA) &&
                    (tmp & ME4000_AI_STATUS_EF_DATA)) {
                        dev_err(dev->class_dev, "FIFO overflow\n");
                        s->async->events |= COMEDI_CB_ERROR;
                        c = ME4000_AI_FIFO_COUNT;
                } else if ((tmp & ME4000_AI_STATUS_FF_DATA) &&
                           !(tmp & ME4000_AI_STATUS_HF_DATA) &&
                           (tmp & ME4000_AI_STATUS_EF_DATA)) {
                        c = ME4000_AI_FIFO_COUNT / 2;
                } else {
                        dev_err(dev->class_dev, "Undefined FIFO state\n");
                        s->async->events |= COMEDI_CB_ERROR;
                        c = 0;
                }

                for (i = 0; i < c; i++) {
                        lval = me4000_ai_get_sample(dev, s);
                        if (!comedi_buf_write_samples(s, &lval, 1))
                                break;
                }

                /* Work is done, so reset the interrupt */
                tmp |= ME4000_AI_CTRL_HF_IRQ_RESET;
                outl(tmp, dev->iobase + ME4000_AI_CTRL_REG);
                tmp &= ~ME4000_AI_CTRL_HF_IRQ_RESET;
                outl(tmp, dev->iobase + ME4000_AI_CTRL_REG);
        }

        if (inl(dev->iobase + ME4000_IRQ_STATUS_REG) &
            ME4000_IRQ_STATUS_SC) {
                /* Acquisition is complete */
                s->async->events |= COMEDI_CB_EOA;

                /* Poll data until fifo empty */
                while (inl(dev->iobase + ME4000_AI_STATUS_REG) &
                       ME4000_AI_STATUS_EF_DATA) {
                        lval = me4000_ai_get_sample(dev, s);
                        if (!comedi_buf_write_samples(s, &lval, 1))
                                break;
                }

                /* Work is done, so reset the interrupt */
                tmp = inl(dev->iobase + ME4000_AI_CTRL_REG);
                tmp |= ME4000_AI_CTRL_SC_IRQ_RESET;
                outl(tmp, dev->iobase + ME4000_AI_CTRL_REG);
                tmp &= ~ME4000_AI_CTRL_SC_IRQ_RESET;
                outl(tmp, dev->iobase + ME4000_AI_CTRL_REG);
        }

        comedi_handle_events(dev, s);

        return IRQ_HANDLED;
}

static int me4000_ao_insn_write(struct comedi_device *dev,
                                struct comedi_subdevice *s,
                                struct comedi_insn *insn,
                                unsigned int *data)
{
        unsigned int chan = CR_CHAN(insn->chanspec);
        unsigned int tmp;

        /* Stop any running conversion */
        tmp = inl(dev->iobase + ME4000_AO_CTRL_REG(chan));
        tmp |= ME4000_AO_CTRL_IMMEDIATE_STOP;
        outl(tmp, dev->iobase + ME4000_AO_CTRL_REG(chan));

        /* Clear control register and set to single mode */
        outl(0x0, dev->iobase + ME4000_AO_CTRL_REG(chan));

        /* Write data value */
        outl(data[0], dev->iobase + ME4000_AO_SINGLE_REG(chan));

        /* Store in the mirror */
        s->readback[chan] = data[0];

        return 1;
}

static int me4000_dio_insn_bits(struct comedi_device *dev,
                                struct comedi_subdevice *s,
                                struct comedi_insn *insn,
                                unsigned int *data)
{
        if (comedi_dio_update_state(s, data)) {
                outl((s->state >> 0) & 0xFF,
                     dev->iobase + ME4000_DIO_PORT_0_REG);
                outl((s->state >> 8) & 0xFF,
                     dev->iobase + ME4000_DIO_PORT_1_REG);
                outl((s->state >> 16) & 0xFF,
                     dev->iobase + ME4000_DIO_PORT_2_REG);
                outl((s->state >> 24) & 0xFF,
                     dev->iobase + ME4000_DIO_PORT_3_REG);
        }

        data[1] = ((inl(dev->iobase + ME4000_DIO_PORT_0_REG) & 0xFF) << 0) |
                  ((inl(dev->iobase + ME4000_DIO_PORT_1_REG) & 0xFF) << 8) |
                  ((inl(dev->iobase + ME4000_DIO_PORT_2_REG) & 0xFF) << 16) |
                  ((inl(dev->iobase + ME4000_DIO_PORT_3_REG) & 0xFF) << 24);

        return insn->n;
}

static int me4000_dio_insn_config(struct comedi_device *dev,
                                  struct comedi_subdevice *s,
                                  struct comedi_insn *insn,
                                  unsigned int *data)
{
        unsigned int chan = CR_CHAN(insn->chanspec);
        unsigned int mask;
        unsigned int tmp;
        int ret;

        if (chan < 8)
                mask = 0x000000ff;
        else if (chan < 16)
                mask = 0x0000ff00;
        else if (chan < 24)
                mask = 0x00ff0000;
        else
                mask = 0xff000000;

        ret = comedi_dio_insn_config(dev, s, insn, data, mask);
        if (ret)
                return ret;

        tmp = inl(dev->iobase + ME4000_DIO_CTRL_REG);
        tmp &= ~(ME4000_DIO_CTRL_MODE_0 | ME4000_DIO_CTRL_MODE_1 |
                 ME4000_DIO_CTRL_MODE_2 | ME4000_DIO_CTRL_MODE_3 |
                 ME4000_DIO_CTRL_MODE_4 | ME4000_DIO_CTRL_MODE_5 |
                 ME4000_DIO_CTRL_MODE_6 | ME4000_DIO_CTRL_MODE_7);
        if (s->io_bits & 0x000000ff)
                tmp |= ME4000_DIO_CTRL_MODE_0;
        if (s->io_bits & 0x0000ff00)
                tmp |= ME4000_DIO_CTRL_MODE_2;
        if (s->io_bits & 0x00ff0000)
                tmp |= ME4000_DIO_CTRL_MODE_4;
        if (s->io_bits & 0xff000000)
                tmp |= ME4000_DIO_CTRL_MODE_6;

        /*
         * Check for optoisolated ME-4000 version.
         * If one the first port is a fixed output
         * port and the second is a fixed input port.
         */
        if (inl(dev->iobase + ME4000_DIO_DIR_REG)) {
                s->io_bits |= 0x000000ff;
                s->io_bits &= ~0x0000ff00;
                tmp |= ME4000_DIO_CTRL_MODE_0;
                tmp &= ~(ME4000_DIO_CTRL_MODE_2 | ME4000_DIO_CTRL_MODE_3);
        }

        outl(tmp, dev->iobase + ME4000_DIO_CTRL_REG);

        return insn->n;
}

static int me4000_auto_attach(struct comedi_device *dev,
                              unsigned long context)
{
        struct pci_dev *pcidev = comedi_to_pci_dev(dev);
        const struct me4000_board *board = NULL;
        struct me4000_private *devpriv;
        struct comedi_subdevice *s;
        int result;

        if (context < ARRAY_SIZE(me4000_boards))
                board = &me4000_boards[context];
        if (!board)
                return -ENODEV;
        dev->board_ptr = board;
        dev->board_name = board->name;

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

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

        devpriv->plx_regbase = pci_resource_start(pcidev, 1);
        dev->iobase = pci_resource_start(pcidev, 2);
        if (!devpriv->plx_regbase || !dev->iobase)
                return -ENODEV;

        result = comedi_load_firmware(dev, &pcidev->dev, ME4000_FIRMWARE,
                                      me4000_xilinx_download, 0);
        if (result < 0)
                return result;

        me4000_reset(dev);

        if (pcidev->irq > 0) {
                result = request_irq(pcidev->irq, me4000_ai_isr, IRQF_SHARED,
                                     dev->board_name, dev);
                if (result == 0) {
                        dev->irq = pcidev->irq;

                        /* Enable interrupts on the PLX */
                        outl(PLX9052_INTCSR_LI1ENAB | PLX9052_INTCSR_LI1POL |
                             PLX9052_INTCSR_PCIENAB,
                             devpriv->plx_regbase + PLX9052_INTCSR);
                }
        }

        result = comedi_alloc_subdevices(dev, 4);
        if (result)
                return result;

        /* Analog Input subdevice */
        s = &dev->subdevices[0];
        s->type         = COMEDI_SUBD_AI;
        s->subdev_flags = SDF_READABLE | SDF_COMMON | SDF_GROUND;
        if (board->can_do_diff_ai)
                s->subdev_flags |= SDF_DIFF;
        s->n_chan       = board->ai_nchan;
        s->maxdata      = 0xffff;
        s->len_chanlist = ME4000_AI_CHANNEL_LIST_COUNT;
        s->range_table  = &me4000_ai_range;
        s->insn_read    = me4000_ai_insn_read;

        if (dev->irq) {
                dev->read_subdev = s;
                s->subdev_flags |= SDF_CMD_READ;
                s->cancel       = me4000_ai_cancel;
                s->do_cmdtest   = me4000_ai_do_cmd_test;
                s->do_cmd       = me4000_ai_do_cmd;
        }

        /* Analog Output subdevice */
        s = &dev->subdevices[1];
        if (board->has_ao) {
                s->type         = COMEDI_SUBD_AO;
                s->subdev_flags = SDF_WRITABLE | SDF_COMMON | SDF_GROUND;
                s->n_chan       = 4;
                s->maxdata      = 0xffff;
                s->range_table  = &range_bipolar10;
                s->insn_write   = me4000_ao_insn_write;

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

        /* Digital I/O subdevice */
        s = &dev->subdevices[2];
        s->type         = COMEDI_SUBD_DIO;
        s->subdev_flags = SDF_READABLE | SDF_WRITABLE;
        s->n_chan       = 32;
        s->maxdata      = 1;
        s->range_table  = &range_digital;
        s->insn_bits    = me4000_dio_insn_bits;
        s->insn_config  = me4000_dio_insn_config;

        /*
         * Check for optoisolated ME-4000 version. If one the first
         * port is a fixed output port and the second is a fixed input port.
         */
        if (!inl(dev->iobase + ME4000_DIO_DIR_REG)) {
                s->io_bits |= 0xFF;
                outl(ME4000_DIO_CTRL_MODE_0,
                     dev->iobase + ME4000_DIO_DIR_REG);
        }

        /* Counter subdevice (8254) */
        s = &dev->subdevices[3];
        if (board->has_counter) {
                unsigned long timer_base = pci_resource_start(pcidev, 3);

                if (!timer_base)
                        return -ENODEV;

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

                comedi_8254_subdevice_init(s, dev->pacer);
        } else {
                s->type = COMEDI_SUBD_UNUSED;
        }

        return 0;
}

static void me4000_detach(struct comedi_device *dev)
{
        if (dev->irq) {
                struct me4000_private *devpriv = dev->private;

                /* Disable interrupts on the PLX */
                outl(0, devpriv->plx_regbase + PLX9052_INTCSR);
        }
        comedi_pci_detach(dev);
}

static struct comedi_driver me4000_driver = {
        .driver_name    = "me4000",
        .module         = THIS_MODULE,
        .auto_attach    = me4000_auto_attach,
        .detach         = me4000_detach,
};

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

static const struct pci_device_id me4000_pci_table[] = {
        { PCI_VDEVICE(MEILHAUS, 0x4650), .driver_data = BOARD_ME4650 },
        { PCI_VDEVICE(MEILHAUS, 0x4660), .driver_data = BOARD_ME4660 },
        { PCI_VDEVICE(MEILHAUS, 0x4661), .driver_data = BOARD_ME4660I },
        { PCI_VDEVICE(MEILHAUS, 0x4662), .driver_data = BOARD_ME4660S },
        { PCI_VDEVICE(MEILHAUS, 0x4663), .driver_data = BOARD_ME4660IS },
        { PCI_VDEVICE(MEILHAUS, 0x4670), .driver_data = BOARD_ME4670 },
        { PCI_VDEVICE(MEILHAUS, 0x4671), .driver_data = BOARD_ME4670I },
        { PCI_VDEVICE(MEILHAUS, 0x4672), .driver_data = BOARD_ME4670S },
        { PCI_VDEVICE(MEILHAUS, 0x4673), .driver_data = BOARD_ME4670IS },
        { PCI_VDEVICE(MEILHAUS, 0x4680), .driver_data = BOARD_ME4680 },
        { PCI_VDEVICE(MEILHAUS, 0x4681), .driver_data = BOARD_ME4680I },
        { PCI_VDEVICE(MEILHAUS, 0x4682), .driver_data = BOARD_ME4680S },
        { PCI_VDEVICE(MEILHAUS, 0x4683), .driver_data = BOARD_ME4680IS },
        { }
};
MODULE_DEVICE_TABLE(pci, me4000_pci_table);

static struct pci_driver me4000_pci_driver = {
        .name           = "me4000",
        .id_table       = me4000_pci_table,
        .probe          = me4000_pci_probe,
        .remove         = comedi_pci_auto_unconfig,
};
module_comedi_pci_driver(me4000_driver, me4000_pci_driver);

MODULE_AUTHOR("Comedi https://www.comedi.org");
MODULE_DESCRIPTION("Comedi driver for Meilhaus ME-4000 series boards");
MODULE_LICENSE("GPL");
MODULE_FIRMWARE(ME4000_FIRMWARE);