root/drivers/comedi/drivers/das800.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * comedi/drivers/das800.c
 * Driver for Keitley das800 series boards and compatibles
 * Copyright (C) 2000 Frank Mori Hess <fmhess@users.sourceforge.net>
 *
 * COMEDI - Linux Control and Measurement Device Interface
 * Copyright (C) 2000 David A. Schleef <ds@schleef.org>
 */
/*
 * Driver: das800
 * Description: Keithley Metrabyte DAS800 (& compatibles)
 * Author: Frank Mori Hess <fmhess@users.sourceforge.net>
 * Devices: [Keithley Metrabyte] DAS-800 (das-800), DAS-801 (das-801),
 * DAS-802 (das-802),
 * [Measurement Computing] CIO-DAS800 (cio-das800),
 * CIO-DAS801 (cio-das801), CIO-DAS802 (cio-das802),
 * CIO-DAS802/16 (cio-das802/16)
 * Status: works, cio-das802/16 untested - email me if you have tested it
 *
 * Configuration options:
 * [0] - I/O port base address
 * [1] - IRQ (optional, required for timed or externally triggered conversions)
 *
 * Notes:
 *      IRQ can be omitted, although the cmd interface will not work without it.
 *
 *      All entries in the channel/gain list must use the same gain and be
 *      consecutive channels counting upwards in channel number (these are
 *      hardware limitations.)
 *
 *      I've never tested the gain setting stuff since I only have a
 *      DAS-800 board with fixed gain.
 *
 *      The cio-das802/16 does not have a fifo-empty status bit!  Therefore
 *      only fifo-half-full transfers are possible with this card.
 *
 * cmd triggers supported:
 *      start_src:      TRIG_NOW | TRIG_EXT
 *      scan_begin_src: TRIG_FOLLOW
 *      scan_end_src:   TRIG_COUNT
 *      convert_src:    TRIG_TIMER | TRIG_EXT
 *      stop_src:       TRIG_NONE | TRIG_COUNT
 */

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

#define N_CHAN_AI             8 /*  number of analog input channels */

/* Registers for the das800 */

#define DAS800_LSB            0
#define   FIFO_EMPTY            0x1
#define   FIFO_OVF              0x2
#define DAS800_MSB            1
#define DAS800_CONTROL1       2
#define   CONTROL1_INTE         0x8
#define DAS800_CONV_CONTROL   2
#define   ITE                   0x1
#define   CASC                  0x2
#define   DTEN                  0x4
#define   IEOC                  0x8
#define   EACS                  0x10
#define   CONV_HCEN             0x80
#define DAS800_SCAN_LIMITS    2
#define DAS800_STATUS         2
#define   IRQ                   0x8
#define   BUSY                  0x80
#define DAS800_GAIN           3
#define   CIO_FFOV              0x8   /* cio-das802/16 fifo overflow */
#define   CIO_ENHF              0x90  /* cio-das802/16 fifo half full int ena */
#define   CONTROL1              0x80
#define   CONV_CONTROL          0xa0
#define   SCAN_LIMITS           0xc0
#define   ID                    0xe0
#define DAS800_8254           4
#define DAS800_STATUS2        7
#define   STATUS2_HCEN          0x80
#define   STATUS2_INTE          0X20
#define DAS800_ID             7

#define DAS802_16_HALF_FIFO_SZ  128

struct das800_board {
        const char *name;
        int ai_speed;
        const struct comedi_lrange *ai_range;
        int resolution;
};

static const struct comedi_lrange range_das801_ai = {
        9, {
                BIP_RANGE(5),
                BIP_RANGE(10),
                UNI_RANGE(10),
                BIP_RANGE(0.5),
                UNI_RANGE(1),
                BIP_RANGE(0.05),
                UNI_RANGE(0.1),
                BIP_RANGE(0.01),
                UNI_RANGE(0.02)
        }
};

static const struct comedi_lrange range_cio_das801_ai = {
        9, {
                BIP_RANGE(5),
                BIP_RANGE(10),
                UNI_RANGE(10),
                BIP_RANGE(0.5),
                UNI_RANGE(1),
                BIP_RANGE(0.05),
                UNI_RANGE(0.1),
                BIP_RANGE(0.005),
                UNI_RANGE(0.01)
        }
};

static const struct comedi_lrange range_das802_ai = {
        9, {
                BIP_RANGE(5),
                BIP_RANGE(10),
                UNI_RANGE(10),
                BIP_RANGE(2.5),
                UNI_RANGE(5),
                BIP_RANGE(1.25),
                UNI_RANGE(2.5),
                BIP_RANGE(0.625),
                UNI_RANGE(1.25)
        }
};

static const struct comedi_lrange range_das80216_ai = {
        8, {
                BIP_RANGE(10),
                UNI_RANGE(10),
                BIP_RANGE(5),
                UNI_RANGE(5),
                BIP_RANGE(2.5),
                UNI_RANGE(2.5),
                BIP_RANGE(1.25),
                UNI_RANGE(1.25)
        }
};

enum das800_boardinfo {
        BOARD_DAS800,
        BOARD_CIODAS800,
        BOARD_DAS801,
        BOARD_CIODAS801,
        BOARD_DAS802,
        BOARD_CIODAS802,
        BOARD_CIODAS80216,
};

static const struct das800_board das800_boards[] = {
        [BOARD_DAS800] = {
                .name           = "das-800",
                .ai_speed       = 25000,
                .ai_range       = &range_bipolar5,
                .resolution     = 12,
        },
        [BOARD_CIODAS800] = {
                .name           = "cio-das800",
                .ai_speed       = 20000,
                .ai_range       = &range_bipolar5,
                .resolution     = 12,
        },
        [BOARD_DAS801] = {
                .name           = "das-801",
                .ai_speed       = 25000,
                .ai_range       = &range_das801_ai,
                .resolution     = 12,
        },
        [BOARD_CIODAS801] = {
                .name           = "cio-das801",
                .ai_speed       = 20000,
                .ai_range       = &range_cio_das801_ai,
                .resolution     = 12,
        },
        [BOARD_DAS802] = {
                .name           = "das-802",
                .ai_speed       = 25000,
                .ai_range       = &range_das802_ai,
                .resolution     = 12,
        },
        [BOARD_CIODAS802] = {
                .name           = "cio-das802",
                .ai_speed       = 20000,
                .ai_range       = &range_das802_ai,
                .resolution     = 12,
        },
        [BOARD_CIODAS80216] = {
                .name           = "cio-das802/16",
                .ai_speed       = 10000,
                .ai_range       = &range_das80216_ai,
                .resolution     = 16,
        },
};

struct das800_private {
        unsigned int do_bits;   /* digital output bits */
};

static void das800_ind_write(struct comedi_device *dev,
                             unsigned int val, unsigned int reg)
{
        /*
         * Select dev->iobase + 2 to be desired register
         * then write to that register.
         */
        outb(reg, dev->iobase + DAS800_GAIN);
        outb(val, dev->iobase + 2);
}

static unsigned int das800_ind_read(struct comedi_device *dev, unsigned int reg)
{
        /*
         * Select dev->iobase + 7 to be desired register
         * then read from that register.
         */
        outb(reg, dev->iobase + DAS800_GAIN);
        return inb(dev->iobase + 7);
}

static void das800_enable(struct comedi_device *dev)
{
        const struct das800_board *board = dev->board_ptr;
        struct das800_private *devpriv = dev->private;
        unsigned long irq_flags;

        spin_lock_irqsave(&dev->spinlock, irq_flags);
        /*  enable fifo-half full interrupts for cio-das802/16 */
        if (board->resolution == 16)
                outb(CIO_ENHF, dev->iobase + DAS800_GAIN);
        /* enable hardware triggering */
        das800_ind_write(dev, CONV_HCEN, CONV_CONTROL);
        /* enable card's interrupt */
        das800_ind_write(dev, CONTROL1_INTE | devpriv->do_bits, CONTROL1);
        spin_unlock_irqrestore(&dev->spinlock, irq_flags);
}

static void das800_disable(struct comedi_device *dev)
{
        unsigned long irq_flags;

        spin_lock_irqsave(&dev->spinlock, irq_flags);
        /* disable hardware triggering of conversions */
        das800_ind_write(dev, 0x0, CONV_CONTROL);
        spin_unlock_irqrestore(&dev->spinlock, irq_flags);
}

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

static int das800_ai_check_chanlist(struct comedi_device *dev,
                                    struct comedi_subdevice *s,
                                    struct comedi_cmd *cmd)
{
        unsigned int chan0 = 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 != (chan0 + i) % s->n_chan) {
                        dev_dbg(dev->class_dev,
                                "chanlist must be consecutive, counting upwards\n");
                        return -EINVAL;
                }

                if (range != range0) {
                        dev_dbg(dev->class_dev,
                                "chanlist must all have the same gain\n");
                        return -EINVAL;
                }
        }

        return 0;
}

static int das800_ai_do_cmdtest(struct comedi_device *dev,
                                struct comedi_subdevice *s,
                                struct comedi_cmd *cmd)
{
        const struct das800_board *board = dev->board_ptr;
        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);
        err |= comedi_check_trigger_src(&cmd->convert_src,
                                        TRIG_TIMER | 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->convert_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);

        if (cmd->convert_src == TRIG_TIMER) {
                err |= comedi_check_trigger_arg_min(&cmd->convert_arg,
                                                    board->ai_speed);
        }

        err |= comedi_check_trigger_arg_min(&cmd->chanlist_len, 1);
        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->convert_src == TRIG_TIMER) {
                unsigned int arg = cmd->convert_arg;

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

        if (err)
                return 4;

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

        if (err)
                return 5;

        return 0;
}

static int das800_ai_do_cmd(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        const struct das800_board *board = dev->board_ptr;
        struct comedi_async *async = s->async;
        struct comedi_cmd *cmd = &async->cmd;
        unsigned int gain = CR_RANGE(cmd->chanlist[0]);
        unsigned int start_chan = CR_CHAN(cmd->chanlist[0]);
        unsigned int end_chan = (start_chan + cmd->chanlist_len - 1) % 8;
        unsigned int scan_chans = (end_chan << 3) | start_chan;
        int conv_bits;
        unsigned long irq_flags;

        das800_disable(dev);

        spin_lock_irqsave(&dev->spinlock, irq_flags);
        /* set scan limits */
        das800_ind_write(dev, scan_chans, SCAN_LIMITS);
        spin_unlock_irqrestore(&dev->spinlock, irq_flags);

        /* set gain */
        if (board->resolution == 12 && gain > 0)
                gain += 0x7;
        gain &= 0xf;
        outb(gain, dev->iobase + DAS800_GAIN);

        /* enable auto channel scan, send interrupts on end of conversion
         * and set clock source to internal or external
         */
        conv_bits = 0;
        conv_bits |= EACS | IEOC;
        if (cmd->start_src == TRIG_EXT)
                conv_bits |= DTEN;
        if (cmd->convert_src == TRIG_TIMER) {
                conv_bits |= CASC | ITE;
                comedi_8254_update_divisors(dev->pacer);
                comedi_8254_pacer_enable(dev->pacer, 1, 2, true);
        }

        spin_lock_irqsave(&dev->spinlock, irq_flags);
        das800_ind_write(dev, conv_bits, CONV_CONTROL);
        spin_unlock_irqrestore(&dev->spinlock, irq_flags);

        das800_enable(dev);
        return 0;
}

static unsigned int das800_ai_get_sample(struct comedi_device *dev)
{
        unsigned int lsb = inb(dev->iobase + DAS800_LSB);
        unsigned int msb = inb(dev->iobase + DAS800_MSB);

        return (msb << 8) | lsb;
}

static irqreturn_t das800_interrupt(int irq, void *d)
{
        struct comedi_device *dev = d;
        struct das800_private *devpriv = dev->private;
        struct comedi_subdevice *s = dev->read_subdev;
        struct comedi_async *async;
        struct comedi_cmd *cmd;
        unsigned long irq_flags;
        unsigned int status;
        unsigned short val;
        bool fifo_empty;
        bool fifo_overflow;
        int i;

        status = inb(dev->iobase + DAS800_STATUS);
        if (!(status & IRQ))
                return IRQ_NONE;
        if (!dev->attached)
                return IRQ_HANDLED;

        async = s->async;
        cmd = &async->cmd;

        spin_lock_irqsave(&dev->spinlock, irq_flags);
        status = das800_ind_read(dev, CONTROL1) & STATUS2_HCEN;
        /*
         * Don't release spinlock yet since we want to make sure
         * no one else disables hardware conversions.
         */

        /* if hardware conversions are not enabled, then quit */
        if (status == 0) {
                spin_unlock_irqrestore(&dev->spinlock, irq_flags);
                return IRQ_HANDLED;
        }

        for (i = 0; i < DAS802_16_HALF_FIFO_SZ; i++) {
                val = das800_ai_get_sample(dev);
                if (s->maxdata == 0x0fff) {
                        fifo_empty = !!(val & FIFO_EMPTY);
                        fifo_overflow = !!(val & FIFO_OVF);
                } else {
                        /* cio-das802/16 has no fifo empty status bit */
                        fifo_empty = false;
                        fifo_overflow = !!(inb(dev->iobase + DAS800_GAIN) &
                                                CIO_FFOV);
                }
                if (fifo_empty || fifo_overflow)
                        break;

                if (s->maxdata == 0x0fff)
                        val >>= 4;      /* 12-bit sample */

                val &= s->maxdata;
                comedi_buf_write_samples(s, &val, 1);

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

        if (fifo_overflow) {
                spin_unlock_irqrestore(&dev->spinlock, irq_flags);
                async->events |= COMEDI_CB_ERROR;
                comedi_handle_events(dev, s);
                return IRQ_HANDLED;
        }

        if (!(async->events & COMEDI_CB_CANCEL_MASK)) {
                /*
                 * Re-enable card's interrupt.
                 * We already have spinlock, so indirect addressing is safe
                 */
                das800_ind_write(dev, CONTROL1_INTE | devpriv->do_bits,
                                 CONTROL1);
                spin_unlock_irqrestore(&dev->spinlock, irq_flags);
        } else {
                /* otherwise, stop taking data */
                spin_unlock_irqrestore(&dev->spinlock, irq_flags);
                das800_disable(dev);
        }
        comedi_handle_events(dev, s);
        return IRQ_HANDLED;
}

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

        status = inb(dev->iobase + DAS800_STATUS);
        if ((status & BUSY) == 0)
                return 0;
        return -EBUSY;
}

static int das800_ai_insn_read(struct comedi_device *dev,
                               struct comedi_subdevice *s,
                               struct comedi_insn *insn,
                               unsigned int *data)
{
        struct das800_private *devpriv = dev->private;
        unsigned int chan = CR_CHAN(insn->chanspec);
        unsigned int range = CR_RANGE(insn->chanspec);
        unsigned long irq_flags;
        unsigned int val;
        int ret;
        int i;

        das800_disable(dev);

        /* set multiplexer */
        spin_lock_irqsave(&dev->spinlock, irq_flags);
        das800_ind_write(dev, chan | devpriv->do_bits, CONTROL1);
        spin_unlock_irqrestore(&dev->spinlock, irq_flags);

        /* set gain / range */
        if (s->maxdata == 0x0fff && range)
                range += 0x7;
        range &= 0xf;
        outb(range, dev->iobase + DAS800_GAIN);

        udelay(5);

        for (i = 0; i < insn->n; i++) {
                /* trigger conversion */
                outb_p(0, dev->iobase + DAS800_MSB);

                ret = comedi_timeout(dev, s, insn, das800_ai_eoc, 0);
                if (ret)
                        return ret;

                val = das800_ai_get_sample(dev);
                if (s->maxdata == 0x0fff)
                        val >>= 4;      /* 12-bit sample */
                data[i] = val & s->maxdata;
        }

        return insn->n;
}

static int das800_di_insn_bits(struct comedi_device *dev,
                               struct comedi_subdevice *s,
                               struct comedi_insn *insn,
                               unsigned int *data)
{
        data[1] = (inb(dev->iobase + DAS800_STATUS) >> 4) & 0x7;

        return insn->n;
}

static int das800_do_insn_bits(struct comedi_device *dev,
                               struct comedi_subdevice *s,
                               struct comedi_insn *insn,
                               unsigned int *data)
{
        struct das800_private *devpriv = dev->private;
        unsigned long irq_flags;

        if (comedi_dio_update_state(s, data)) {
                devpriv->do_bits = s->state << 4;

                spin_lock_irqsave(&dev->spinlock, irq_flags);
                das800_ind_write(dev, CONTROL1_INTE | devpriv->do_bits,
                                 CONTROL1);
                spin_unlock_irqrestore(&dev->spinlock, irq_flags);
        }

        data[1] = s->state;

        return insn->n;
}

static const struct das800_board *das800_probe(struct comedi_device *dev)
{
        const struct das800_board *board = dev->board_ptr;
        int index = board ? board - das800_boards : -EINVAL;
        int id_bits;
        unsigned long irq_flags;

        /*
         * The dev->board_ptr will be set by comedi_device_attach() if the
         * board name provided by the user matches a board->name in this
         * driver. If so, this function sanity checks the id_bits to verify
         * that the board is correct.
         *
         * If the dev->board_ptr is not set, the user is trying to attach
         * an unspecified board to this driver. In this case the id_bits
         * are used to 'probe' for the correct dev->board_ptr.
         */
        spin_lock_irqsave(&dev->spinlock, irq_flags);
        id_bits = das800_ind_read(dev, ID) & 0x3;
        spin_unlock_irqrestore(&dev->spinlock, irq_flags);

        switch (id_bits) {
        case 0x0:
                if (index == BOARD_DAS800 || index == BOARD_CIODAS800)
                        return board;
                index = BOARD_DAS800;
                break;
        case 0x2:
                if (index == BOARD_DAS801 || index == BOARD_CIODAS801)
                        return board;
                index = BOARD_DAS801;
                break;
        case 0x3:
                if (index == BOARD_DAS802 || index == BOARD_CIODAS802 ||
                    index == BOARD_CIODAS80216)
                        return board;
                index = BOARD_DAS802;
                break;
        default:
                dev_dbg(dev->class_dev, "Board model: 0x%x (unknown)\n",
                        id_bits);
                return NULL;
        }
        dev_dbg(dev->class_dev, "Board model (probed): %s series\n",
                das800_boards[index].name);

        return &das800_boards[index];
}

static int das800_attach(struct comedi_device *dev, struct comedi_devconfig *it)
{
        const struct das800_board *board;
        struct das800_private *devpriv;
        struct comedi_subdevice *s;
        unsigned int irq = it->options[1];
        unsigned long irq_flags;
        int ret;

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

        ret = comedi_request_region(dev, it->options[0], 0x8);
        if (ret)
                return ret;

        board = das800_probe(dev);
        if (!board)
                return -ENODEV;
        dev->board_ptr = board;
        dev->board_name = board->name;

        if (irq > 1 && irq <= 7) {
                ret = request_irq(irq, das800_interrupt, 0, "das800",
                                  dev);
                if (ret == 0)
                        dev->irq = irq;
        }

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

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

        /* Analog Input subdevice */
        s = &dev->subdevices[0];
        dev->read_subdev = s;
        s->type         = COMEDI_SUBD_AI;
        s->subdev_flags = SDF_READABLE | SDF_GROUND;
        s->n_chan       = 8;
        s->maxdata      = (1 << board->resolution) - 1;
        s->range_table  = board->ai_range;
        s->insn_read    = das800_ai_insn_read;
        if (dev->irq) {
                s->subdev_flags |= SDF_CMD_READ;
                s->len_chanlist = 8;
                s->do_cmdtest   = das800_ai_do_cmdtest;
                s->do_cmd       = das800_ai_do_cmd;
                s->cancel       = das800_cancel;
        }

        /* Digital Input subdevice */
        s = &dev->subdevices[1];
        s->type         = COMEDI_SUBD_DI;
        s->subdev_flags = SDF_READABLE;
        s->n_chan       = 3;
        s->maxdata      = 1;
        s->range_table  = &range_digital;
        s->insn_bits    = das800_di_insn_bits;

        /* Digital Output subdevice */
        s = &dev->subdevices[2];
        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    = das800_do_insn_bits;

        das800_disable(dev);

        /* initialize digital out channels */
        spin_lock_irqsave(&dev->spinlock, irq_flags);
        das800_ind_write(dev, CONTROL1_INTE | devpriv->do_bits, CONTROL1);
        spin_unlock_irqrestore(&dev->spinlock, irq_flags);

        return 0;
};

static struct comedi_driver driver_das800 = {
        .driver_name    = "das800",
        .module         = THIS_MODULE,
        .attach         = das800_attach,
        .detach         = comedi_legacy_detach,
        .num_names      = ARRAY_SIZE(das800_boards),
        .board_name     = &das800_boards[0].name,
        .offset         = sizeof(struct das800_board),
};
module_comedi_driver(driver_das800);

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