root/drivers/comedi/drivers/addi_apci_3xxx.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * addi_apci_3xxx.c
 * Copyright (C) 2004,2005  ADDI-DATA GmbH for the source code of this module.
 * Project manager: S. Weber
 *
 *      ADDI-DATA GmbH
 *      Dieselstrasse 3
 *      D-77833 Ottersweier
 *      Tel: +19(0)7223/9493-0
 *      Fax: +49(0)7223/9493-92
 *      http://www.addi-data.com
 *      info@addi-data.com
 */

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

#define CONV_UNIT_NS            BIT(0)
#define CONV_UNIT_US            BIT(1)
#define CONV_UNIT_MS            BIT(2)

static const struct comedi_lrange apci3xxx_ai_range = {
        8, {
                BIP_RANGE(10),
                BIP_RANGE(5),
                BIP_RANGE(2),
                BIP_RANGE(1),
                UNI_RANGE(10),
                UNI_RANGE(5),
                UNI_RANGE(2),
                UNI_RANGE(1)
        }
};

static const struct comedi_lrange apci3xxx_ao_range = {
        2, {
                BIP_RANGE(10),
                UNI_RANGE(10)
        }
};

enum apci3xxx_boardid {
        BOARD_APCI3000_16,
        BOARD_APCI3000_8,
        BOARD_APCI3000_4,
        BOARD_APCI3006_16,
        BOARD_APCI3006_8,
        BOARD_APCI3006_4,
        BOARD_APCI3010_16,
        BOARD_APCI3010_8,
        BOARD_APCI3010_4,
        BOARD_APCI3016_16,
        BOARD_APCI3016_8,
        BOARD_APCI3016_4,
        BOARD_APCI3100_16_4,
        BOARD_APCI3100_8_4,
        BOARD_APCI3106_16_4,
        BOARD_APCI3106_8_4,
        BOARD_APCI3110_16_4,
        BOARD_APCI3110_8_4,
        BOARD_APCI3116_16_4,
        BOARD_APCI3116_8_4,
        BOARD_APCI3003,
        BOARD_APCI3002_16,
        BOARD_APCI3002_8,
        BOARD_APCI3002_4,
        BOARD_APCI3500,
};

struct apci3xxx_boardinfo {
        const char *name;
        int ai_subdev_flags;
        int ai_n_chan;
        unsigned int ai_maxdata;
        unsigned char ai_conv_units;
        unsigned int ai_min_acq_ns;
        unsigned int has_ao:1;
        unsigned int has_dig_in:1;
        unsigned int has_dig_out:1;
        unsigned int has_ttl_io:1;
};

static const struct apci3xxx_boardinfo apci3xxx_boardtypes[] = {
        [BOARD_APCI3000_16] = {
                .name                   = "apci3000-16",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3000_8] = {
                .name                   = "apci3000-8",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3000_4] = {
                .name                   = "apci3000-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 4,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3006_16] = {
                .name                   = "apci3006-16",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3006_8] = {
                .name                   = "apci3006-8",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3006_4] = {
                .name                   = "apci3006-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 4,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3010_16] = {
                .name                   = "apci3010-16",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3010_8] = {
                .name                   = "apci3010-8",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3010_4] = {
                .name                   = "apci3010-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 4,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3016_16] = {
                .name                   = "apci3016-16",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3016_8] = {
                .name                   = "apci3016-8",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3016_4] = {
                .name                   = "apci3016-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 4,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3100_16_4] = {
                .name                   = "apci3100-16-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ao                 = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3100_8_4] = {
                .name                   = "apci3100-8-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ao                 = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3106_16_4] = {
                .name                   = "apci3106-16-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ao                 = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3106_8_4] = {
                .name                   = "apci3106-8-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 10000,
                .has_ao                 = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3110_16_4] = {
                .name                   = "apci3110-16-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_ao                 = 1,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3110_8_4] = {
                .name                   = "apci3110-8-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0x0fff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_ao                 = 1,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3116_16_4] = {
                .name                   = "apci3116-16-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_ao                 = 1,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3116_8_4] = {
                .name                   = "apci3116-8-4",
                .ai_subdev_flags        = SDF_COMMON | SDF_GROUND | SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_ao                 = 1,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
                .has_ttl_io             = 1,
        },
        [BOARD_APCI3003] = {
                .name                   = "apci3003",
                .ai_subdev_flags        = SDF_DIFF,
                .ai_n_chan              = 4,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US |
                                          CONV_UNIT_NS,
                .ai_min_acq_ns          = 2500,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
        },
        [BOARD_APCI3002_16] = {
                .name                   = "apci3002-16",
                .ai_subdev_flags        = SDF_DIFF,
                .ai_n_chan              = 16,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
        },
        [BOARD_APCI3002_8] = {
                .name                   = "apci3002-8",
                .ai_subdev_flags        = SDF_DIFF,
                .ai_n_chan              = 8,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
        },
        [BOARD_APCI3002_4] = {
                .name                   = "apci3002-4",
                .ai_subdev_flags        = SDF_DIFF,
                .ai_n_chan              = 4,
                .ai_maxdata             = 0xffff,
                .ai_conv_units          = CONV_UNIT_MS | CONV_UNIT_US,
                .ai_min_acq_ns          = 5000,
                .has_dig_in             = 1,
                .has_dig_out            = 1,
        },
        [BOARD_APCI3500] = {
                .name                   = "apci3500",
                .has_ao                 = 1,
                .has_ttl_io             = 1,
        },
};

struct apci3xxx_private {
        unsigned int ai_timer;
        unsigned char ai_time_base;
};

static irqreturn_t apci3xxx_irq_handler(int irq, void *d)
{
        struct comedi_device *dev = d;
        struct comedi_subdevice *s = dev->read_subdev;
        unsigned int status;
        unsigned int val;

        /* Test if interrupt occur */
        status = readl(dev->mmio + 16);
        if ((status & 0x2) == 0x2) {
                /* Reset the interrupt */
                writel(status, dev->mmio + 16);

                val = readl(dev->mmio + 28);
                comedi_buf_write_samples(s, &val, 1);

                s->async->events |= COMEDI_CB_EOA;
                comedi_handle_events(dev, s);

                return IRQ_HANDLED;
        }
        return IRQ_NONE;
}

static int apci3xxx_ai_started(struct comedi_device *dev)
{
        if ((readl(dev->mmio + 8) & 0x80000) == 0x80000)
                return 1;

        return 0;
}

static int apci3xxx_ai_setup(struct comedi_device *dev, unsigned int chanspec)
{
        unsigned int chan = CR_CHAN(chanspec);
        unsigned int range = CR_RANGE(chanspec);
        unsigned int aref = CR_AREF(chanspec);
        unsigned int delay_mode;
        unsigned int val;

        if (apci3xxx_ai_started(dev))
                return -EBUSY;

        /* Clear the FIFO */
        writel(0x10000, dev->mmio + 12);

        /* Get and save the delay mode */
        delay_mode = readl(dev->mmio + 4);
        delay_mode &= 0xfffffef0;

        /* Channel configuration selection */
        writel(delay_mode, dev->mmio + 4);

        /* Make the configuration */
        val = (range & 3) | ((range >> 2) << 6) |
              ((aref == AREF_DIFF) << 7);
        writel(val, dev->mmio + 0);

        /* Channel selection */
        writel(delay_mode | 0x100, dev->mmio + 4);
        writel(chan, dev->mmio + 0);

        /* Restore delay mode */
        writel(delay_mode, dev->mmio + 4);

        /* Set the number of sequence to 1 */
        writel(1, dev->mmio + 48);

        return 0;
}

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

        status = readl(dev->mmio + 20);
        if (status & 0x1)
                return 0;
        return -EBUSY;
}

static int apci3xxx_ai_insn_read(struct comedi_device *dev,
                                 struct comedi_subdevice *s,
                                 struct comedi_insn *insn,
                                 unsigned int *data)
{
        int ret;
        int i;

        ret = apci3xxx_ai_setup(dev, insn->chanspec);
        if (ret)
                return ret;

        for (i = 0; i < insn->n; i++) {
                /* Start the conversion */
                writel(0x80000, dev->mmio + 8);

                /* Wait the EOS */
                ret = comedi_timeout(dev, s, insn, apci3xxx_ai_eoc, 0);
                if (ret)
                        return ret;

                /* Read the analog value */
                data[i] = readl(dev->mmio + 28);
        }

        return insn->n;
}

static int apci3xxx_ai_ns_to_timer(struct comedi_device *dev,
                                   unsigned int *ns, unsigned int flags)
{
        const struct apci3xxx_boardinfo *board = dev->board_ptr;
        struct apci3xxx_private *devpriv = dev->private;
        unsigned int base;
        unsigned int timer;
        int time_base;

        /* time_base: 0 = ns, 1 = us, 2 = ms */
        for (time_base = 0; time_base < 3; time_base++) {
                /* skip unsupported time bases */
                if (!(board->ai_conv_units & (1 << time_base)))
                        continue;

                switch (time_base) {
                case 0:
                        base = 1;
                        break;
                case 1:
                        base = 1000;
                        break;
                case 2:
                        base = 1000000;
                        break;
                }

                switch (flags & CMDF_ROUND_MASK) {
                case CMDF_ROUND_NEAREST:
                default:
                        timer = DIV_ROUND_CLOSEST(*ns, base);
                        break;
                case CMDF_ROUND_DOWN:
                        timer = *ns / base;
                        break;
                case CMDF_ROUND_UP:
                        timer = DIV_ROUND_UP(*ns, base);
                        break;
                }

                if (timer < 0x10000) {
                        devpriv->ai_time_base = time_base;
                        devpriv->ai_timer = timer;
                        *ns = timer * time_base;
                        return 0;
                }
        }
        return -EINVAL;
}

static int apci3xxx_ai_cmdtest(struct comedi_device *dev,
                               struct comedi_subdevice *s,
                               struct comedi_cmd *cmd)
{
        const struct apci3xxx_boardinfo *board = dev->board_ptr;
        int err = 0;
        unsigned int arg;

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

        err |= comedi_check_trigger_src(&cmd->start_src, TRIG_NOW);
        err |= comedi_check_trigger_src(&cmd->scan_begin_src, TRIG_FOLLOW);
        err |= comedi_check_trigger_src(&cmd->convert_src, TRIG_TIMER);
        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->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);
        err |= comedi_check_trigger_arg_is(&cmd->scan_begin_arg, 0);
        err |= comedi_check_trigger_arg_min(&cmd->convert_arg,
                                            board->ai_min_acq_ns);
        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 */

        arg = cmd->convert_arg;
        err |= apci3xxx_ai_ns_to_timer(dev, &arg, cmd->flags);
        err |= comedi_check_trigger_arg_is(&cmd->convert_arg, arg);

        if (err)
                return 4;

        return 0;
}

static int apci3xxx_ai_cmd(struct comedi_device *dev,
                           struct comedi_subdevice *s)
{
        struct apci3xxx_private *devpriv = dev->private;
        struct comedi_cmd *cmd = &s->async->cmd;
        int ret;

        ret = apci3xxx_ai_setup(dev, cmd->chanlist[0]);
        if (ret)
                return ret;

        /* Set the convert timing unit */
        writel(devpriv->ai_time_base, dev->mmio + 36);

        /* Set the convert timing */
        writel(devpriv->ai_timer, dev->mmio + 32);

        /* Start the conversion */
        writel(0x180000, dev->mmio + 8);

        return 0;
}

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

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

        status = readl(dev->mmio + 96);
        if (status & 0x100)
                return 0;
        return -EBUSY;
}

static int apci3xxx_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 range = CR_RANGE(insn->chanspec);
        int ret;
        int i;

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

                /* Set the range selection */
                writel(range, dev->mmio + 96);

                /* Write the analog value to the selected channel */
                writel((val << 8) | chan, dev->mmio + 100);

                /* Wait the end of transfer */
                ret = comedi_timeout(dev, s, insn, apci3xxx_ao_eoc, 0);
                if (ret)
                        return ret;

                s->readback[chan] = val;
        }

        return insn->n;
}

static int apci3xxx_di_insn_bits(struct comedi_device *dev,
                                 struct comedi_subdevice *s,
                                 struct comedi_insn *insn,
                                 unsigned int *data)
{
        data[1] = inl(dev->iobase + 32) & 0xf;

        return insn->n;
}

static int apci3xxx_do_insn_bits(struct comedi_device *dev,
                                 struct comedi_subdevice *s,
                                 struct comedi_insn *insn,
                                 unsigned int *data)
{
        s->state = inl(dev->iobase + 48) & 0xf;

        if (comedi_dio_update_state(s, data))
                outl(s->state, dev->iobase + 48);

        data[1] = s->state;

        return insn->n;
}

static int apci3xxx_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 = 0;
        int ret;

        /*
         * Port 0 (channels 0-7) are always inputs
         * Port 1 (channels 8-15) are always outputs
         * Port 2 (channels 16-23) are programmable i/o
         */
        if (data[0] != INSN_CONFIG_DIO_QUERY) {
                /* ignore all other instructions for ports 0 and 1 */
                if (chan < 16)
                        return -EINVAL;

                /* changing any channel in port 2 changes the entire port */
                mask = 0xff0000;
        }

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

        /* update port 2 configuration */
        outl((s->io_bits >> 24) & 0xff, dev->iobase + 224);

        return insn->n;
}

static int apci3xxx_dio_insn_bits(struct comedi_device *dev,
                                  struct comedi_subdevice *s,
                                  struct comedi_insn *insn,
                                  unsigned int *data)
{
        unsigned int mask;
        unsigned int val;

        mask = comedi_dio_update_state(s, data);
        if (mask) {
                if (mask & 0xff)
                        outl(s->state & 0xff, dev->iobase + 80);
                if (mask & 0xff0000)
                        outl((s->state >> 16) & 0xff, dev->iobase + 112);
        }

        val = inl(dev->iobase + 80);
        val |= (inl(dev->iobase + 64) << 8);
        if (s->io_bits & 0xff0000)
                val |= (inl(dev->iobase + 112) << 16);
        else
                val |= (inl(dev->iobase + 96) << 16);

        data[1] = val;

        return insn->n;
}

static int apci3xxx_reset(struct comedi_device *dev)
{
        unsigned int val;
        int i;

        /* Disable the interrupt */
        disable_irq(dev->irq);

        /* Clear the start command */
        writel(0, dev->mmio + 8);

        /* Reset the interrupt flags */
        val = readl(dev->mmio + 16);
        writel(val, dev->mmio + 16);

        /* clear the EOS */
        readl(dev->mmio + 20);

        /* Clear the FIFO */
        for (i = 0; i < 16; i++)
                val = readl(dev->mmio + 28);

        /* Enable the interrupt */
        enable_irq(dev->irq);

        return 0;
}

static int apci3xxx_auto_attach(struct comedi_device *dev,
                                unsigned long context)
{
        struct pci_dev *pcidev = comedi_to_pci_dev(dev);
        const struct apci3xxx_boardinfo *board = NULL;
        struct apci3xxx_private *devpriv;
        struct comedi_subdevice *s;
        int n_subdevices;
        int subdev;
        int ret;

        if (context < ARRAY_SIZE(apci3xxx_boardtypes))
                board = &apci3xxx_boardtypes[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;

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

        dev->iobase = pci_resource_start(pcidev, 2);
        dev->mmio = pci_ioremap_bar(pcidev, 3);
        if (!dev->mmio)
                return -ENOMEM;

        if (pcidev->irq > 0) {
                ret = request_irq(pcidev->irq, apci3xxx_irq_handler,
                                  IRQF_SHARED, dev->board_name, dev);
                if (ret == 0)
                        dev->irq = pcidev->irq;
        }

        n_subdevices = (board->ai_n_chan ? 0 : 1) + board->has_ao +
                       board->has_dig_in + board->has_dig_out +
                       board->has_ttl_io;
        ret = comedi_alloc_subdevices(dev, n_subdevices);
        if (ret)
                return ret;

        subdev = 0;

        /* Analog Input subdevice */
        if (board->ai_n_chan) {
                s = &dev->subdevices[subdev];
                s->type         = COMEDI_SUBD_AI;
                s->subdev_flags = SDF_READABLE | board->ai_subdev_flags;
                s->n_chan       = board->ai_n_chan;
                s->maxdata      = board->ai_maxdata;
                s->range_table  = &apci3xxx_ai_range;
                s->insn_read    = apci3xxx_ai_insn_read;
                if (dev->irq) {
                        /*
                         * FIXME: The hardware supports multiple scan modes
                         * but the original addi-data driver only supported
                         * reading a single channel with interrupts. Need a
                         * proper datasheet to fix this.
                         *
                         * The following scan modes are supported by the
                         * hardware:
                         *   1) Single software scan
                         *   2) Single hardware triggered scan
                         *   3) Continuous software scan
                         *   4) Continuous software scan with timer delay
                         *   5) Continuous hardware triggered scan
                         *   6) Continuous hardware triggered scan with timer
                         *      delay
                         *
                         * For now, limit the chanlist to a single channel.
                         */
                        dev->read_subdev = s;
                        s->subdev_flags |= SDF_CMD_READ;
                        s->len_chanlist = 1;
                        s->do_cmdtest   = apci3xxx_ai_cmdtest;
                        s->do_cmd       = apci3xxx_ai_cmd;
                        s->cancel       = apci3xxx_ai_cancel;
                }

                subdev++;
        }

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

                ret = comedi_alloc_subdev_readback(s);
                if (ret)
                        return ret;

                subdev++;
        }

        /* Digital Input subdevice */
        if (board->has_dig_in) {
                s = &dev->subdevices[subdev];
                s->type         = COMEDI_SUBD_DI;
                s->subdev_flags = SDF_READABLE;
                s->n_chan       = 4;
                s->maxdata      = 1;
                s->range_table  = &range_digital;
                s->insn_bits    = apci3xxx_di_insn_bits;

                subdev++;
        }

        /* Digital Output subdevice */
        if (board->has_dig_out) {
                s = &dev->subdevices[subdev];
                s->type         = COMEDI_SUBD_DO;
                s->subdev_flags = SDF_WRITABLE;
                s->n_chan       = 4;
                s->maxdata      = 1;
                s->range_table  = &range_digital;
                s->insn_bits    = apci3xxx_do_insn_bits;

                subdev++;
        }

        /* TTL Digital I/O subdevice */
        if (board->has_ttl_io) {
                s = &dev->subdevices[subdev];
                s->type         = COMEDI_SUBD_DIO;
                s->subdev_flags = SDF_READABLE | SDF_WRITABLE;
                s->n_chan       = 24;
                s->maxdata      = 1;
                s->io_bits      = 0xff; /* channels 0-7 are always outputs */
                s->range_table  = &range_digital;
                s->insn_config  = apci3xxx_dio_insn_config;
                s->insn_bits    = apci3xxx_dio_insn_bits;

                subdev++;
        }

        apci3xxx_reset(dev);
        return 0;
}

static void apci3xxx_detach(struct comedi_device *dev)
{
        if (dev->iobase)
                apci3xxx_reset(dev);
        comedi_pci_detach(dev);
}

static struct comedi_driver apci3xxx_driver = {
        .driver_name    = "addi_apci_3xxx",
        .module         = THIS_MODULE,
        .auto_attach    = apci3xxx_auto_attach,
        .detach         = apci3xxx_detach,
};

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

static const struct pci_device_id apci3xxx_pci_table[] = {
        { PCI_VDEVICE(ADDIDATA, 0x3010), .driver_data = BOARD_APCI3000_16 },
        { PCI_VDEVICE(ADDIDATA, 0x300f), .driver_data = BOARD_APCI3000_8 },
        { PCI_VDEVICE(ADDIDATA, 0x300e), .driver_data = BOARD_APCI3000_4 },
        { PCI_VDEVICE(ADDIDATA, 0x3013), .driver_data = BOARD_APCI3006_16 },
        { PCI_VDEVICE(ADDIDATA, 0x3014), .driver_data = BOARD_APCI3006_8 },
        { PCI_VDEVICE(ADDIDATA, 0x3015), .driver_data = BOARD_APCI3006_4 },
        { PCI_VDEVICE(ADDIDATA, 0x3016), .driver_data = BOARD_APCI3010_16 },
        { PCI_VDEVICE(ADDIDATA, 0x3017), .driver_data = BOARD_APCI3010_8 },
        { PCI_VDEVICE(ADDIDATA, 0x3018), .driver_data = BOARD_APCI3010_4 },
        { PCI_VDEVICE(ADDIDATA, 0x3019), .driver_data = BOARD_APCI3016_16 },
        { PCI_VDEVICE(ADDIDATA, 0x301a), .driver_data = BOARD_APCI3016_8 },
        { PCI_VDEVICE(ADDIDATA, 0x301b), .driver_data = BOARD_APCI3016_4 },
        { PCI_VDEVICE(ADDIDATA, 0x301c), .driver_data = BOARD_APCI3100_16_4 },
        { PCI_VDEVICE(ADDIDATA, 0x301d), .driver_data = BOARD_APCI3100_8_4 },
        { PCI_VDEVICE(ADDIDATA, 0x301e), .driver_data = BOARD_APCI3106_16_4 },
        { PCI_VDEVICE(ADDIDATA, 0x301f), .driver_data = BOARD_APCI3106_8_4 },
        { PCI_VDEVICE(ADDIDATA, 0x3020), .driver_data = BOARD_APCI3110_16_4 },
        { PCI_VDEVICE(ADDIDATA, 0x3021), .driver_data = BOARD_APCI3110_8_4 },
        { PCI_VDEVICE(ADDIDATA, 0x3022), .driver_data = BOARD_APCI3116_16_4 },
        { PCI_VDEVICE(ADDIDATA, 0x3023), .driver_data = BOARD_APCI3116_8_4 },
        { PCI_VDEVICE(ADDIDATA, 0x300B), .driver_data = BOARD_APCI3003 },
        { PCI_VDEVICE(ADDIDATA, 0x3002), .driver_data = BOARD_APCI3002_16 },
        { PCI_VDEVICE(ADDIDATA, 0x3003), .driver_data = BOARD_APCI3002_8 },
        { PCI_VDEVICE(ADDIDATA, 0x3004), .driver_data = BOARD_APCI3002_4 },
        { PCI_VDEVICE(ADDIDATA, 0x3024), .driver_data = BOARD_APCI3500 },
        { }
};
MODULE_DEVICE_TABLE(pci, apci3xxx_pci_table);

static struct pci_driver apci3xxx_pci_driver = {
        .name           = "addi_apci_3xxx",
        .id_table       = apci3xxx_pci_table,
        .probe          = apci3xxx_pci_probe,
        .remove         = comedi_pci_auto_unconfig,
};
module_comedi_pci_driver(apci3xxx_driver, apci3xxx_pci_driver);

MODULE_AUTHOR("Comedi https://www.comedi.org");
MODULE_DESCRIPTION("Comedi low-level driver");
MODULE_LICENSE("GPL");