root/drivers/comedi/drivers/pcl711.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * pcl711.c
 * Comedi driver for PC-LabCard PCL-711 and AdSys ACL-8112 and compatibles
 * Copyright (C) 1998 David A. Schleef <ds@schleef.org>
 *                    Janne Jalkanen <jalkanen@cs.hut.fi>
 *                    Eric Bunn <ebu@cs.hut.fi>
 *
 * COMEDI - Linux Control and Measurement Device Interface
 * Copyright (C) 1998 David A. Schleef <ds@schleef.org>
 */

/*
 * Driver: pcl711
 * Description: Advantech PCL-711 and 711b, ADLink ACL-8112
 * Devices: [Advantech] PCL-711 (pcl711), PCL-711B (pcl711b),
 *   [ADLink] ACL-8112HG (acl8112hg), ACL-8112DG (acl8112dg)
 * Author: David A. Schleef <ds@schleef.org>
 *         Janne Jalkanen <jalkanen@cs.hut.fi>
 *         Eric Bunn <ebu@cs.hut.fi>
 * Updated:
 * Status: mostly complete
 *
 * Configuration Options:
 *   [0] - I/O port base
 *   [1] - IRQ, optional
 */

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

/*
 * I/O port register map
 */
#define PCL711_TIMER_BASE       0x00
#define PCL711_AI_LSB_REG       0x04
#define PCL711_AI_MSB_REG       0x05
#define PCL711_AI_MSB_DRDY      BIT(4)
#define PCL711_AO_LSB_REG(x)    (0x04 + ((x) * 2))
#define PCL711_AO_MSB_REG(x)    (0x05 + ((x) * 2))
#define PCL711_DI_LSB_REG       0x06
#define PCL711_DI_MSB_REG       0x07
#define PCL711_INT_STAT_REG     0x08
#define PCL711_INT_STAT_CLR     (0 << 0)  /* any value will work */
#define PCL711_AI_GAIN_REG      0x09
#define PCL711_AI_GAIN(x)       (((x) & 0xf) << 0)
#define PCL711_MUX_REG          0x0a
#define PCL711_MUX_CHAN(x)      (((x) & 0xf) << 0)
#define PCL711_MUX_CS0          BIT(4)
#define PCL711_MUX_CS1          BIT(5)
#define PCL711_MUX_DIFF         (PCL711_MUX_CS0 | PCL711_MUX_CS1)
#define PCL711_MODE_REG         0x0b
#define PCL711_MODE(x)          (((x) & 0x7) << 0)
#define PCL711_MODE_DEFAULT     PCL711_MODE(0)
#define PCL711_MODE_SOFTTRIG    PCL711_MODE(1)
#define PCL711_MODE_EXT         PCL711_MODE(2)
#define PCL711_MODE_EXT_IRQ     PCL711_MODE(3)
#define PCL711_MODE_PACER       PCL711_MODE(4)
#define PCL711_MODE_PACER_IRQ   PCL711_MODE(6)
#define PCL711_MODE_IRQ(x)      (((x) & 0x7) << 4)
#define PCL711_SOFTTRIG_REG     0x0c
#define PCL711_SOFTTRIG         (0 << 0)  /* any value will work */
#define PCL711_DO_LSB_REG       0x0d
#define PCL711_DO_MSB_REG       0x0e

static const struct comedi_lrange range_pcl711b_ai = {
        5, {
                BIP_RANGE(5),
                BIP_RANGE(2.5),
                BIP_RANGE(1.25),
                BIP_RANGE(0.625),
                BIP_RANGE(0.3125)
        }
};

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

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

struct pcl711_board {
        const char *name;
        int n_aichan;
        int n_aochan;
        int maxirq;
        unsigned int min_io_start;
        const struct comedi_lrange *ai_range_type;
};

static const struct pcl711_board boardtypes[] = {
        {
                .name           = "pcl711",
                .n_aichan       = 8,
                .n_aochan       = 1,
                .ai_range_type  = &range_bipolar5,
        }, {
                .name           = "pcl711b",
                .n_aichan       = 8,
                .n_aochan       = 1,
                .maxirq         = 7,
                .ai_range_type  = &range_pcl711b_ai,
        }, {
                .name           = "acl8112hg",
                .n_aichan       = 16,
                .n_aochan       = 2,
                .maxirq         = 15,
                .min_io_start   = 0x200,
                .ai_range_type  = &range_acl8112hg_ai,
        }, {
                .name           = "acl8112dg",
                .n_aichan       = 16,
                .n_aochan       = 2,
                .maxirq         = 15,
                .min_io_start   = 0x200,
                .ai_range_type  = &range_acl8112dg_ai,
        },
};

static void pcl711_ai_set_mode(struct comedi_device *dev, unsigned int mode)
{
        /*
         * The pcl711b board uses bits in the mode register to select the
         * interrupt. The other boards supported by this driver all use
         * jumpers on the board.
         *
         * Enables the interrupt when needed on the pcl711b board. These
         * bits do nothing on the other boards.
         */
        if (mode == PCL711_MODE_EXT_IRQ || mode == PCL711_MODE_PACER_IRQ)
                mode |= PCL711_MODE_IRQ(dev->irq);

        outb(mode, dev->iobase + PCL711_MODE_REG);
}

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

        val = inb(dev->iobase + PCL711_AI_MSB_REG) << 8;
        val |= inb(dev->iobase + PCL711_AI_LSB_REG);

        return val & s->maxdata;
}

static int pcl711_ai_cancel(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        outb(PCL711_INT_STAT_CLR, dev->iobase + PCL711_INT_STAT_REG);
        pcl711_ai_set_mode(dev, PCL711_MODE_SOFTTRIG);
        return 0;
}

static irqreturn_t pcl711_interrupt(int irq, void *d)
{
        struct comedi_device *dev = d;
        struct comedi_subdevice *s = dev->read_subdev;
        struct comedi_cmd *cmd = &s->async->cmd;
        unsigned short data;

        if (!dev->attached) {
                dev_err(dev->class_dev, "spurious interrupt\n");
                return IRQ_HANDLED;
        }

        data = pcl711_ai_get_sample(dev, s);

        outb(PCL711_INT_STAT_CLR, dev->iobase + PCL711_INT_STAT_REG);

        comedi_buf_write_samples(s, &data, 1);

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

        comedi_handle_events(dev, s);

        return IRQ_HANDLED;
}

static void pcl711_set_changain(struct comedi_device *dev,
                                struct comedi_subdevice *s,
                                unsigned int chanspec)
{
        unsigned int chan = CR_CHAN(chanspec);
        unsigned int range = CR_RANGE(chanspec);
        unsigned int aref = CR_AREF(chanspec);
        unsigned int mux = 0;

        outb(PCL711_AI_GAIN(range), dev->iobase + PCL711_AI_GAIN_REG);

        if (s->n_chan > 8) {
                /* Select the correct MPC508A chip */
                if (aref == AREF_DIFF) {
                        chan &= 0x7;
                        mux |= PCL711_MUX_DIFF;
                } else {
                        if (chan < 8)
                                mux |= PCL711_MUX_CS0;
                        else
                                mux |= PCL711_MUX_CS1;
                }
        }
        outb(mux | PCL711_MUX_CHAN(chan), dev->iobase + PCL711_MUX_REG);
}

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

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

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

        pcl711_set_changain(dev, s, insn->chanspec);

        pcl711_ai_set_mode(dev, PCL711_MODE_SOFTTRIG);

        for (i = 0; i < insn->n; i++) {
                outb(PCL711_SOFTTRIG, dev->iobase + PCL711_SOFTTRIG_REG);

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

                data[i] = pcl711_ai_get_sample(dev, s);
        }

        return insn->n;
}

static int pcl711_ai_cmdtest(struct comedi_device *dev,
                             struct comedi_subdevice *s, struct comedi_cmd *cmd)
{
        int err = 0;

        /* 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_TIMER | TRIG_EXT);
        err |= comedi_check_trigger_src(&cmd->convert_src, TRIG_NOW);
        err |= comedi_check_trigger_src(&cmd->scan_end_src, TRIG_COUNT);
        err |= comedi_check_trigger_src(&cmd->stop_src, TRIG_COUNT | TRIG_NONE);

        if (err)
                return 1;

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

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

        /* Step 2b : and mutually compatible */

        if (err)
                return 2;

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

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

        if (cmd->scan_begin_src == TRIG_EXT) {
                err |= comedi_check_trigger_arg_is(&cmd->scan_begin_arg, 0);
        } else {
#define MAX_SPEED 1000
                err |= comedi_check_trigger_arg_min(&cmd->scan_begin_arg,
                                                    MAX_SPEED);
        }

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

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

        if (err)
                return 3;

        /* step 4 */

        if (cmd->scan_begin_src == TRIG_TIMER) {
                unsigned int arg = cmd->scan_begin_arg;

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

        if (err)
                return 4;

        return 0;
}

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

        pcl711_set_changain(dev, s, cmd->chanlist[0]);

        if (cmd->scan_begin_src == TRIG_TIMER) {
                comedi_8254_update_divisors(dev->pacer);
                comedi_8254_pacer_enable(dev->pacer, 1, 2, true);
                outb(PCL711_INT_STAT_CLR, dev->iobase + PCL711_INT_STAT_REG);
                pcl711_ai_set_mode(dev, PCL711_MODE_PACER_IRQ);
        } else {
                pcl711_ai_set_mode(dev, PCL711_MODE_EXT_IRQ);
        }

        return 0;
}

static void pcl711_ao_write(struct comedi_device *dev,
                            unsigned int chan, unsigned int val)
{
        outb(val & 0xff, dev->iobase + PCL711_AO_LSB_REG(chan));
        outb((val >> 8) & 0xff, dev->iobase + PCL711_AO_MSB_REG(chan));
}

static int pcl711_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 val = s->readback[chan];
        int i;

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

        return insn->n;
}

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

        val = inb(dev->iobase + PCL711_DI_LSB_REG);
        val |= (inb(dev->iobase + PCL711_DI_MSB_REG) << 8);

        data[1] = val;

        return insn->n;
}

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

        mask = comedi_dio_update_state(s, data);
        if (mask) {
                if (mask & 0x00ff)
                        outb(s->state & 0xff, dev->iobase + PCL711_DO_LSB_REG);
                if (mask & 0xff00)
                        outb((s->state >> 8), dev->iobase + PCL711_DO_MSB_REG);
        }

        data[1] = s->state;

        return insn->n;
}

static int pcl711_attach(struct comedi_device *dev, struct comedi_devconfig *it)
{
        const struct pcl711_board *board = dev->board_ptr;
        struct comedi_subdevice *s;
        int ret;

        ret = comedi_check_request_region(dev, it->options[0], 0x10,
                                          board->min_io_start, 0x3ff, 16);
        if (ret)
                return ret;

        if (it->options[1] && it->options[1] <= board->maxirq) {
                ret = request_irq(it->options[1], pcl711_interrupt, 0,
                                  dev->board_name, dev);
                if (ret == 0)
                        dev->irq = it->options[1];
        }

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

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

        /* Analog Input subdevice */
        s = &dev->subdevices[0];
        s->type         = COMEDI_SUBD_AI;
        s->subdev_flags = SDF_READABLE | SDF_GROUND;
        if (board->n_aichan > 8)
                s->subdev_flags |= SDF_DIFF;
        s->n_chan       = board->n_aichan;
        s->maxdata      = 0xfff;
        s->range_table  = board->ai_range_type;
        s->insn_read    = pcl711_ai_insn_read;
        if (dev->irq) {
                dev->read_subdev = s;
                s->subdev_flags |= SDF_CMD_READ;
                s->len_chanlist = 1;
                s->do_cmdtest   = pcl711_ai_cmdtest;
                s->do_cmd       = pcl711_ai_cmd;
                s->cancel       = pcl711_ai_cancel;
        }

        /* Analog Output subdevice */
        s = &dev->subdevices[1];
        s->type         = COMEDI_SUBD_AO;
        s->subdev_flags = SDF_WRITABLE;
        s->n_chan       = board->n_aochan;
        s->maxdata      = 0xfff;
        s->range_table  = &range_bipolar5;
        s->insn_write   = pcl711_ao_insn_write;

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

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

        /* Digital Output subdevice */
        s = &dev->subdevices[3];
        s->type         = COMEDI_SUBD_DO;
        s->subdev_flags = SDF_WRITABLE;
        s->n_chan       = 16;
        s->maxdata      = 1;
        s->range_table  = &range_digital;
        s->insn_bits    = pcl711_do_insn_bits;

        /* clear DAC */
        pcl711_ao_write(dev, 0, 0x0);
        pcl711_ao_write(dev, 1, 0x0);

        return 0;
}

static struct comedi_driver pcl711_driver = {
        .driver_name    = "pcl711",
        .module         = THIS_MODULE,
        .attach         = pcl711_attach,
        .detach         = comedi_legacy_detach,
        .board_name     = &boardtypes[0].name,
        .num_names      = ARRAY_SIZE(boardtypes),
        .offset         = sizeof(struct pcl711_board),
};
module_comedi_driver(pcl711_driver);

MODULE_AUTHOR("Comedi https://www.comedi.org");
MODULE_DESCRIPTION("Comedi driver for PCL-711 compatible boards");
MODULE_LICENSE("GPL");