root/drivers/comedi/drivers/comedi_parport.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * comedi_parport.c
 * Comedi driver for standard parallel port
 *
 * For more information see:
 *      http://retired.beyondlogic.org/spp/parallel.htm
 *
 * COMEDI - Linux Control and Measurement Device Interface
 * Copyright (C) 1998,2001 David A. Schleef <ds@schleef.org>
 */

/*
 * Driver: comedi_parport
 * Description: Standard PC parallel port
 * Author: ds
 * Status: works in immediate mode
 * Devices: [standard] parallel port (comedi_parport)
 * Updated: Tue, 30 Apr 2002 21:11:45 -0700
 *
 * A cheap and easy way to get a few more digital I/O lines. Steal
 * additional parallel ports from old computers or your neighbors'
 * computers.
 *
 * Option list:
 *   0: I/O port base for the parallel port.
 *   1: IRQ (optional)
 *
 * Parallel Port Lines:
 *
 *       pin   subdev  chan  type  name
 *      -----  ------  ----  ----  --------------
 *        1      2       0    DO   strobe
 *        2      0       0    DIO  data 0
 *        3      0       1    DIO  data 1
 *        4      0       2    DIO  data 2
 *        5      0       3    DIO  data 3
 *        6      0       4    DIO  data 4
 *        7      0       5    DIO  data 5
 *        8      0       6    DIO  data 6
 *        9      0       7    DIO  data 7
 *       10      1       3    DI   ack
 *       11      1       4    DI   busy
 *       12      1       2    DI   paper out
 *       13      1       1    DI   select in
 *       14      2       1    DO   auto LF
 *       15      1       0    DI   error
 *       16      2       2    DO   init
 *       17      2       3    DO   select printer
 *      18-25                      ground
 *
 * When an IRQ is configured subdevice 3 pretends to be a digital
 * input subdevice, but it always returns 0 when read. However, if
 * you run a command with scan_begin_src=TRIG_EXT, it uses pin 10
 * as a external trigger, which can be used to wake up tasks.
 */

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

/*
 * Register map
 */
#define PARPORT_DATA_REG        0x00
#define PARPORT_STATUS_REG      0x01
#define PARPORT_CTRL_REG        0x02
#define PARPORT_CTRL_IRQ_ENA    BIT(4)
#define PARPORT_CTRL_BIDIR_ENA  BIT(5)

static int parport_data_reg_insn_bits(struct comedi_device *dev,
                                      struct comedi_subdevice *s,
                                      struct comedi_insn *insn,
                                      unsigned int *data)
{
        if (comedi_dio_update_state(s, data))
                outb(s->state, dev->iobase + PARPORT_DATA_REG);

        data[1] = inb(dev->iobase + PARPORT_DATA_REG);

        return insn->n;
}

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

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

        ctrl = inb(dev->iobase + PARPORT_CTRL_REG);
        if (s->io_bits)
                ctrl &= ~PARPORT_CTRL_BIDIR_ENA;
        else
                ctrl |= PARPORT_CTRL_BIDIR_ENA;
        outb(ctrl, dev->iobase + PARPORT_CTRL_REG);

        return insn->n;
}

static int parport_status_reg_insn_bits(struct comedi_device *dev,
                                        struct comedi_subdevice *s,
                                        struct comedi_insn *insn,
                                        unsigned int *data)
{
        data[1] = inb(dev->iobase + PARPORT_STATUS_REG) >> 3;

        return insn->n;
}

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

        if (comedi_dio_update_state(s, data)) {
                ctrl = inb(dev->iobase + PARPORT_CTRL_REG);
                ctrl &= (PARPORT_CTRL_IRQ_ENA | PARPORT_CTRL_BIDIR_ENA);
                ctrl |= s->state;
                outb(ctrl, dev->iobase + PARPORT_CTRL_REG);
        }

        data[1] = s->state;

        return insn->n;
}

static int parport_intr_insn_bits(struct comedi_device *dev,
                                  struct comedi_subdevice *s,
                                  struct comedi_insn *insn,
                                  unsigned int *data)
{
        data[1] = 0;
        return insn->n;
}

static int parport_intr_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_EXT);
        err |= comedi_check_trigger_src(&cmd->convert_src, TRIG_FOLLOW);
        err |= comedi_check_trigger_src(&cmd->scan_end_src, TRIG_COUNT);
        err |= comedi_check_trigger_src(&cmd->stop_src, TRIG_NONE);

        if (err)
                return 1;

        /* Step 2a : make sure trigger sources are unique */
        /* Step 2b : and mutually compatible */

        /* 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_is(&cmd->convert_arg, 0);
        err |= comedi_check_trigger_arg_is(&cmd->scan_end_arg,
                                           cmd->chanlist_len);
        err |= comedi_check_trigger_arg_is(&cmd->stop_arg, 0);

        if (err)
                return 3;

        /* Step 4: fix up any arguments */

        /* Step 5: check channel list if it exists */

        return 0;
}

static int parport_intr_cmd(struct comedi_device *dev,
                            struct comedi_subdevice *s)
{
        unsigned int ctrl;

        ctrl = inb(dev->iobase + PARPORT_CTRL_REG);
        ctrl |= PARPORT_CTRL_IRQ_ENA;
        outb(ctrl, dev->iobase + PARPORT_CTRL_REG);

        return 0;
}

static int parport_intr_cancel(struct comedi_device *dev,
                               struct comedi_subdevice *s)
{
        unsigned int ctrl;

        ctrl = inb(dev->iobase + PARPORT_CTRL_REG);
        ctrl &= ~PARPORT_CTRL_IRQ_ENA;
        outb(ctrl, dev->iobase + PARPORT_CTRL_REG);

        return 0;
}

static irqreturn_t parport_interrupt(int irq, void *d)
{
        struct comedi_device *dev = d;
        struct comedi_subdevice *s = dev->read_subdev;
        unsigned int ctrl;
        unsigned short val = 0;

        ctrl = inb(dev->iobase + PARPORT_CTRL_REG);
        if (!(ctrl & PARPORT_CTRL_IRQ_ENA))
                return IRQ_NONE;

        comedi_buf_write_samples(s, &val, 1);
        comedi_handle_events(dev, s);

        return IRQ_HANDLED;
}

static int parport_attach(struct comedi_device *dev,
                          struct comedi_devconfig *it)
{
        struct comedi_subdevice *s;
        int ret;

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

        if (it->options[1]) {
                ret = request_irq(it->options[1], parport_interrupt, 0,
                                  dev->board_name, dev);
                if (ret == 0)
                        dev->irq = it->options[1];
        }

        ret = comedi_alloc_subdevices(dev, dev->irq ? 4 : 3);
        if (ret)
                return ret;

        /* Digial I/O subdevice - Parallel port DATA register */
        s = &dev->subdevices[0];
        s->type         = COMEDI_SUBD_DIO;
        s->subdev_flags = SDF_READABLE | SDF_WRITABLE;
        s->n_chan       = 8;
        s->maxdata      = 1;
        s->range_table  = &range_digital;
        s->insn_bits    = parport_data_reg_insn_bits;
        s->insn_config  = parport_data_reg_insn_config;

        /* Digial Input subdevice - Parallel port STATUS register */
        s = &dev->subdevices[1];
        s->type         = COMEDI_SUBD_DI;
        s->subdev_flags = SDF_READABLE;
        s->n_chan       = 5;
        s->maxdata      = 1;
        s->range_table  = &range_digital;
        s->insn_bits    = parport_status_reg_insn_bits;

        /* Digial Output subdevice - Parallel port CONTROL register */
        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    = parport_ctrl_reg_insn_bits;

        if (dev->irq) {
                /* Digial Input subdevice - Interrupt support */
                s = &dev->subdevices[3];
                dev->read_subdev = s;
                s->type         = COMEDI_SUBD_DI;
                s->subdev_flags = SDF_READABLE | SDF_CMD_READ;
                s->n_chan       = 1;
                s->maxdata      = 1;
                s->range_table  = &range_digital;
                s->insn_bits    = parport_intr_insn_bits;
                s->len_chanlist = 1;
                s->do_cmdtest   = parport_intr_cmdtest;
                s->do_cmd       = parport_intr_cmd;
                s->cancel       = parport_intr_cancel;
        }

        outb(0, dev->iobase + PARPORT_DATA_REG);
        outb(0, dev->iobase + PARPORT_CTRL_REG);

        return 0;
}

static struct comedi_driver parport_driver = {
        .driver_name    = "comedi_parport",
        .module         = THIS_MODULE,
        .attach         = parport_attach,
        .detach         = comedi_legacy_detach,
};
module_comedi_driver(parport_driver);

MODULE_AUTHOR("Comedi https://www.comedi.org");
MODULE_DESCRIPTION("Comedi: Standard parallel port driver");
MODULE_LICENSE("GPL");