root/drivers/comedi/drivers/ii_pci20kc.c
// SPDX-License-Identifier: GPL-2.0
/*
 * ii_pci20kc.c
 * Driver for Intelligent Instruments PCI-20001C carrier board and modules.
 *
 * Copyright (C) 2000 Markus Kempf <kempf@matsci.uni-sb.de>
 * with suggestions from David Schleef          16.06.2000
 */

/*
 * Driver: ii_pci20kc
 * Description: Intelligent Instruments PCI-20001C carrier board
 * Devices: [Intelligent Instrumentation] PCI-20001C (ii_pci20kc)
 * Author: Markus Kempf <kempf@matsci.uni-sb.de>
 * Status: works
 *
 * Supports the PCI-20001C-1a and PCI-20001C-2a carrier boards. The
 * -2a version has 32 on-board DIO channels. Three add-on modules
 * can be added to the carrier board for additional functionality.
 *
 * Supported add-on modules:
 *      PCI-20006M-1   1 channel, 16-bit analog output module
 *      PCI-20006M-2   2 channel, 16-bit analog output module
 *      PCI-20341M-1A  4 channel, 16-bit analog input module
 *
 * Options:
 *   0   Board base address
 *   1   IRQ (not-used)
 */

#include <linux/module.h>
#include <linux/io.h>
#include <linux/comedi/comedidev.h>

/*
 * Register I/O map
 */
#define II20K_SIZE                      0x400
#define II20K_MOD_OFFSET                0x100
#define II20K_ID_REG                    0x00
#define II20K_ID_MOD1_EMPTY             BIT(7)
#define II20K_ID_MOD2_EMPTY             BIT(6)
#define II20K_ID_MOD3_EMPTY             BIT(5)
#define II20K_ID_MASK                   0x1f
#define II20K_ID_PCI20001C_1A           0x1b    /* no on-board DIO */
#define II20K_ID_PCI20001C_2A           0x1d    /* on-board DIO */
#define II20K_MOD_STATUS_REG            0x40
#define II20K_MOD_STATUS_IRQ_MOD1       BIT(7)
#define II20K_MOD_STATUS_IRQ_MOD2       BIT(6)
#define II20K_MOD_STATUS_IRQ_MOD3       BIT(5)
#define II20K_DIO0_REG                  0x80
#define II20K_DIO1_REG                  0x81
#define II20K_DIR_ENA_REG               0x82
#define II20K_DIR_DIO3_OUT              BIT(7)
#define II20K_DIR_DIO2_OUT              BIT(6)
#define II20K_BUF_DISAB_DIO3            BIT(5)
#define II20K_BUF_DISAB_DIO2            BIT(4)
#define II20K_DIR_DIO1_OUT              BIT(3)
#define II20K_DIR_DIO0_OUT              BIT(2)
#define II20K_BUF_DISAB_DIO1            BIT(1)
#define II20K_BUF_DISAB_DIO0            BIT(0)
#define II20K_CTRL01_REG                0x83
#define II20K_CTRL01_SET                BIT(7)
#define II20K_CTRL01_DIO0_IN            BIT(4)
#define II20K_CTRL01_DIO1_IN            BIT(1)
#define II20K_DIO2_REG                  0xc0
#define II20K_DIO3_REG                  0xc1
#define II20K_CTRL23_REG                0xc3
#define II20K_CTRL23_SET                BIT(7)
#define II20K_CTRL23_DIO2_IN            BIT(4)
#define II20K_CTRL23_DIO3_IN            BIT(1)

#define II20K_ID_PCI20006M_1            0xe2    /* 1 AO channels */
#define II20K_ID_PCI20006M_2            0xe3    /* 2 AO channels */
#define II20K_AO_STRB_REG(x)            (0x0b + ((x) * 0x08))
#define II20K_AO_LSB_REG(x)             (0x0d + ((x) * 0x08))
#define II20K_AO_MSB_REG(x)             (0x0e + ((x) * 0x08))
#define II20K_AO_STRB_BOTH_REG          0x1b

#define II20K_ID_PCI20341M_1            0x77    /* 4 AI channels */
#define II20K_AI_STATUS_CMD_REG         0x01
#define II20K_AI_STATUS_CMD_BUSY        BIT(7)
#define II20K_AI_STATUS_CMD_HW_ENA      BIT(1)
#define II20K_AI_STATUS_CMD_EXT_START   BIT(0)
#define II20K_AI_LSB_REG                0x02
#define II20K_AI_MSB_REG                0x03
#define II20K_AI_PACER_RESET_REG        0x04
#define II20K_AI_16BIT_DATA_REG         0x06
#define II20K_AI_CONF_REG               0x10
#define II20K_AI_CONF_ENA               BIT(2)
#define II20K_AI_OPT_REG                0x11
#define II20K_AI_OPT_TRIG_ENA           BIT(5)
#define II20K_AI_OPT_TRIG_INV           BIT(4)
#define II20K_AI_OPT_TIMEBASE(x)        (((x) & 0x3) << 1)
#define II20K_AI_OPT_BURST_MODE         BIT(0)
#define II20K_AI_STATUS_REG             0x12
#define II20K_AI_STATUS_INT             BIT(7)
#define II20K_AI_STATUS_TRIG            BIT(6)
#define II20K_AI_STATUS_TRIG_ENA        BIT(5)
#define II20K_AI_STATUS_PACER_ERR       BIT(2)
#define II20K_AI_STATUS_DATA_ERR        BIT(1)
#define II20K_AI_STATUS_SET_TIME_ERR    BIT(0)
#define II20K_AI_LAST_CHAN_ADDR_REG     0x13
#define II20K_AI_CUR_ADDR_REG           0x14
#define II20K_AI_SET_TIME_REG           0x15
#define II20K_AI_DELAY_LSB_REG          0x16
#define II20K_AI_DELAY_MSB_REG          0x17
#define II20K_AI_CHAN_ADV_REG           0x18
#define II20K_AI_CHAN_RESET_REG         0x19
#define II20K_AI_START_TRIG_REG         0x1a
#define II20K_AI_COUNT_RESET_REG        0x1b
#define II20K_AI_CHANLIST_REG           0x80
#define II20K_AI_CHANLIST_ONBOARD_ONLY  BIT(5)
#define II20K_AI_CHANLIST_GAIN(x)       (((x) & 0x3) << 3)
#define II20K_AI_CHANLIST_MUX_ENA       BIT(2)
#define II20K_AI_CHANLIST_CHAN(x)       (((x) & 0x3) << 0)
#define II20K_AI_CHANLIST_LEN           0x80

/* the AO range is set by jumpers on the 20006M module */
static const struct comedi_lrange ii20k_ao_ranges = {
        3, {
                BIP_RANGE(5),   /* Chan 0 - W1/W3 in   Chan 1 - W2/W4 in  */
                UNI_RANGE(10),  /* Chan 0 - W1/W3 out  Chan 1 - W2/W4 in  */
                BIP_RANGE(10)   /* Chan 0 - W1/W3 in   Chan 1 - W2/W4 out */
        }
};

static const struct comedi_lrange ii20k_ai_ranges = {
        4, {
                BIP_RANGE(5),           /* gain 1 */
                BIP_RANGE(0.5),         /* gain 10 */
                BIP_RANGE(0.05),        /* gain 100 */
                BIP_RANGE(0.025)        /* gain 200 */
        },
};

static void __iomem *ii20k_module_iobase(struct comedi_device *dev,
                                         struct comedi_subdevice *s)
{
        return dev->mmio + (s->index + 1) * II20K_MOD_OFFSET;
}

static int ii20k_ao_insn_write(struct comedi_device *dev,
                               struct comedi_subdevice *s,
                               struct comedi_insn *insn,
                               unsigned int *data)
{
        void __iomem *iobase = ii20k_module_iobase(dev, s);
        unsigned int chan = CR_CHAN(insn->chanspec);
        int i;

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

                s->readback[chan] = val;

                /* munge the offset binary data to 2's complement */
                val = comedi_offset_munge(s, val);

                writeb(val & 0xff, iobase + II20K_AO_LSB_REG(chan));
                writeb((val >> 8) & 0xff, iobase + II20K_AO_MSB_REG(chan));
                writeb(0x00, iobase + II20K_AO_STRB_REG(chan));
        }

        return insn->n;
}

static int ii20k_ai_eoc(struct comedi_device *dev,
                        struct comedi_subdevice *s,
                        struct comedi_insn *insn,
                        unsigned long context)
{
        void __iomem *iobase = ii20k_module_iobase(dev, s);
        unsigned char status;

        status = readb(iobase + II20K_AI_STATUS_REG);
        if ((status & II20K_AI_STATUS_INT) == 0)
                return 0;
        return -EBUSY;
}

static void ii20k_ai_setup(struct comedi_device *dev,
                           struct comedi_subdevice *s,
                           unsigned int chanspec)
{
        void __iomem *iobase = ii20k_module_iobase(dev, s);
        unsigned int chan = CR_CHAN(chanspec);
        unsigned int range = CR_RANGE(chanspec);
        unsigned char val;

        /* initialize module */
        writeb(II20K_AI_CONF_ENA, iobase + II20K_AI_CONF_REG);

        /* software conversion */
        writeb(0, iobase + II20K_AI_STATUS_CMD_REG);

        /* set the time base for the settling time counter based on the gain */
        val = (range < 3) ? II20K_AI_OPT_TIMEBASE(0) : II20K_AI_OPT_TIMEBASE(2);
        writeb(val, iobase + II20K_AI_OPT_REG);

        /* set the settling time counter based on the gain */
        val = (range < 2) ? 0x58 : (range < 3) ? 0x93 : 0x99;
        writeb(val, iobase + II20K_AI_SET_TIME_REG);

        /* set number of input channels */
        writeb(1, iobase + II20K_AI_LAST_CHAN_ADDR_REG);

        /* set the channel list byte */
        val = II20K_AI_CHANLIST_ONBOARD_ONLY |
              II20K_AI_CHANLIST_MUX_ENA |
              II20K_AI_CHANLIST_GAIN(range) |
              II20K_AI_CHANLIST_CHAN(chan);
        writeb(val, iobase + II20K_AI_CHANLIST_REG);

        /* reset settling time counter and trigger delay counter */
        writeb(0, iobase + II20K_AI_COUNT_RESET_REG);

        /* reset channel scanner */
        writeb(0, iobase + II20K_AI_CHAN_RESET_REG);
}

static int ii20k_ai_insn_read(struct comedi_device *dev,
                              struct comedi_subdevice *s,
                              struct comedi_insn *insn,
                              unsigned int *data)
{
        void __iomem *iobase = ii20k_module_iobase(dev, s);
        int ret;
        int i;

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

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

                /* generate a software start convert signal */
                readb(iobase + II20K_AI_PACER_RESET_REG);

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

                val = readb(iobase + II20K_AI_LSB_REG);
                val |= (readb(iobase + II20K_AI_MSB_REG) << 8);

                /* munge the 2's complement data to offset binary */
                data[i] = comedi_offset_munge(s, val);
        }

        return insn->n;
}

static void ii20k_dio_config(struct comedi_device *dev,
                             struct comedi_subdevice *s)
{
        unsigned char ctrl01 = 0;
        unsigned char ctrl23 = 0;
        unsigned char dir_ena = 0;

        /* port 0 - channels 0-7 */
        if (s->io_bits & 0x000000ff) {
                /* output port */
                ctrl01 &= ~II20K_CTRL01_DIO0_IN;
                dir_ena &= ~II20K_BUF_DISAB_DIO0;
                dir_ena |= II20K_DIR_DIO0_OUT;
        } else {
                /* input port */
                ctrl01 |= II20K_CTRL01_DIO0_IN;
                dir_ena &= ~II20K_DIR_DIO0_OUT;
        }

        /* port 1 - channels 8-15 */
        if (s->io_bits & 0x0000ff00) {
                /* output port */
                ctrl01 &= ~II20K_CTRL01_DIO1_IN;
                dir_ena &= ~II20K_BUF_DISAB_DIO1;
                dir_ena |= II20K_DIR_DIO1_OUT;
        } else {
                /* input port */
                ctrl01 |= II20K_CTRL01_DIO1_IN;
                dir_ena &= ~II20K_DIR_DIO1_OUT;
        }

        /* port 2 - channels 16-23 */
        if (s->io_bits & 0x00ff0000) {
                /* output port */
                ctrl23 &= ~II20K_CTRL23_DIO2_IN;
                dir_ena &= ~II20K_BUF_DISAB_DIO2;
                dir_ena |= II20K_DIR_DIO2_OUT;
        } else {
                /* input port */
                ctrl23 |= II20K_CTRL23_DIO2_IN;
                dir_ena &= ~II20K_DIR_DIO2_OUT;
        }

        /* port 3 - channels 24-31 */
        if (s->io_bits & 0xff000000) {
                /* output port */
                ctrl23 &= ~II20K_CTRL23_DIO3_IN;
                dir_ena &= ~II20K_BUF_DISAB_DIO3;
                dir_ena |= II20K_DIR_DIO3_OUT;
        } else {
                /* input port */
                ctrl23 |= II20K_CTRL23_DIO3_IN;
                dir_ena &= ~II20K_DIR_DIO3_OUT;
        }

        ctrl23 |= II20K_CTRL01_SET;
        ctrl23 |= II20K_CTRL23_SET;

        /* order is important */
        writeb(ctrl01, dev->mmio + II20K_CTRL01_REG);
        writeb(ctrl23, dev->mmio + II20K_CTRL23_REG);
        writeb(dir_ena, dev->mmio + II20K_DIR_ENA_REG);
}

static int ii20k_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;
        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;

        ii20k_dio_config(dev, s);

        return insn->n;
}

static int ii20k_dio_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 & 0x000000ff)
                        writeb((s->state >> 0) & 0xff,
                               dev->mmio + II20K_DIO0_REG);
                if (mask & 0x0000ff00)
                        writeb((s->state >> 8) & 0xff,
                               dev->mmio + II20K_DIO1_REG);
                if (mask & 0x00ff0000)
                        writeb((s->state >> 16) & 0xff,
                               dev->mmio + II20K_DIO2_REG);
                if (mask & 0xff000000)
                        writeb((s->state >> 24) & 0xff,
                               dev->mmio + II20K_DIO3_REG);
        }

        data[1] = readb(dev->mmio + II20K_DIO0_REG);
        data[1] |= readb(dev->mmio + II20K_DIO1_REG) << 8;
        data[1] |= readb(dev->mmio + II20K_DIO2_REG) << 16;
        data[1] |= readb(dev->mmio + II20K_DIO3_REG) << 24;

        return insn->n;
}

static int ii20k_init_module(struct comedi_device *dev,
                             struct comedi_subdevice *s)
{
        void __iomem *iobase = ii20k_module_iobase(dev, s);
        unsigned char id;
        int ret;

        id = readb(iobase + II20K_ID_REG);
        switch (id) {
        case II20K_ID_PCI20006M_1:
        case II20K_ID_PCI20006M_2:
                /* Analog Output subdevice */
                s->type         = COMEDI_SUBD_AO;
                s->subdev_flags = SDF_WRITABLE;
                s->n_chan       = (id == II20K_ID_PCI20006M_2) ? 2 : 1;
                s->maxdata      = 0xffff;
                s->range_table  = &ii20k_ao_ranges;
                s->insn_write   = ii20k_ao_insn_write;

                ret = comedi_alloc_subdev_readback(s);
                if (ret)
                        return ret;
                break;
        case II20K_ID_PCI20341M_1:
                /* Analog Input subdevice */
                s->type         = COMEDI_SUBD_AI;
                s->subdev_flags = SDF_READABLE | SDF_DIFF;
                s->n_chan       = 4;
                s->maxdata      = 0xffff;
                s->range_table  = &ii20k_ai_ranges;
                s->insn_read    = ii20k_ai_insn_read;
                break;
        default:
                s->type = COMEDI_SUBD_UNUSED;
                break;
        }

        return 0;
}

static int ii20k_attach(struct comedi_device *dev,
                        struct comedi_devconfig *it)
{
        struct comedi_subdevice *s;
        unsigned int membase;
        unsigned char id;
        bool has_dio;
        int ret;

        membase = it->options[0];
        if (!membase || (membase & ~(0x100000 - II20K_SIZE))) {
                dev_warn(dev->class_dev,
                         "%s: invalid memory address specified\n",
                         dev->board_name);
                return -EINVAL;
        }

        if (!request_mem_region(membase, II20K_SIZE, dev->board_name)) {
                dev_warn(dev->class_dev, "%s: I/O mem conflict (%#x,%u)\n",
                         dev->board_name, membase, II20K_SIZE);
                return -EIO;
        }
        dev->iobase = membase;  /* actually, a memory address */

        dev->mmio = ioremap(membase, II20K_SIZE);
        if (!dev->mmio)
                return -ENOMEM;

        id = readb(dev->mmio + II20K_ID_REG);
        switch (id & II20K_ID_MASK) {
        case II20K_ID_PCI20001C_1A:
                has_dio = false;
                break;
        case II20K_ID_PCI20001C_2A:
                has_dio = true;
                break;
        default:
                return -ENODEV;
        }

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

        s = &dev->subdevices[0];
        if (id & II20K_ID_MOD1_EMPTY) {
                s->type = COMEDI_SUBD_UNUSED;
        } else {
                ret = ii20k_init_module(dev, s);
                if (ret)
                        return ret;
        }

        s = &dev->subdevices[1];
        if (id & II20K_ID_MOD2_EMPTY) {
                s->type = COMEDI_SUBD_UNUSED;
        } else {
                ret = ii20k_init_module(dev, s);
                if (ret)
                        return ret;
        }

        s = &dev->subdevices[2];
        if (id & II20K_ID_MOD3_EMPTY) {
                s->type = COMEDI_SUBD_UNUSED;
        } else {
                ret = ii20k_init_module(dev, s);
                if (ret)
                        return ret;
        }

        /* Digital I/O subdevice */
        s = &dev->subdevices[3];
        if (has_dio) {
                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    = ii20k_dio_insn_bits;
                s->insn_config  = ii20k_dio_insn_config;

                /* default all channels to input */
                ii20k_dio_config(dev, s);
        } else {
                s->type = COMEDI_SUBD_UNUSED;
        }

        return 0;
}

static void ii20k_detach(struct comedi_device *dev)
{
        if (dev->mmio)
                iounmap(dev->mmio);
        if (dev->iobase)        /* actually, a memory address */
                release_mem_region(dev->iobase, II20K_SIZE);
}

static struct comedi_driver ii20k_driver = {
        .driver_name    = "ii_pci20kc",
        .module         = THIS_MODULE,
        .attach         = ii20k_attach,
        .detach         = ii20k_detach,
};
module_comedi_driver(ii20k_driver);

MODULE_AUTHOR("Comedi https://www.comedi.org");
MODULE_DESCRIPTION("Comedi driver for Intelligent Instruments PCI-20001C");
MODULE_LICENSE("GPL");