root/drivers/input/serio/pcips2.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * linux/drivers/input/serio/pcips2.c
 *
 *  Copyright (C) 2003 Russell King, All Rights Reserved.
 *
 *  I'm not sure if this is a generic PS/2 PCI interface or specific to
 *  the Mobility Electronics docking station.
 */
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/ioport.h>
#include <linux/input.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <linux/serio.h>
#include <linux/delay.h>
#include <asm/io.h>

#define PS2_CTRL                (0)
#define PS2_STATUS              (1)
#define PS2_DATA                (2)

#define PS2_CTRL_CLK            (1<<0)
#define PS2_CTRL_DAT            (1<<1)
#define PS2_CTRL_TXIRQ          (1<<2)
#define PS2_CTRL_ENABLE         (1<<3)
#define PS2_CTRL_RXIRQ          (1<<4)

#define PS2_STAT_CLK            (1<<0)
#define PS2_STAT_DAT            (1<<1)
#define PS2_STAT_PARITY         (1<<2)
#define PS2_STAT_RXFULL         (1<<5)
#define PS2_STAT_TXBUSY         (1<<6)
#define PS2_STAT_TXEMPTY        (1<<7)

struct pcips2_data {
        struct serio    *io;
        unsigned int    base;
        struct pci_dev  *dev;
};

static int pcips2_write(struct serio *io, unsigned char val)
{
        struct pcips2_data *ps2if = io->port_data;
        unsigned int stat;

        do {
                stat = inb(ps2if->base + PS2_STATUS);
                cpu_relax();
        } while (!(stat & PS2_STAT_TXEMPTY));

        outb(val, ps2if->base + PS2_DATA);

        return 0;
}

static irqreturn_t pcips2_interrupt(int irq, void *devid)
{
        struct pcips2_data *ps2if = devid;
        unsigned char status, scancode;
        int handled = 0;

        do {
                unsigned int flag;

                status = inb(ps2if->base + PS2_STATUS);
                if (!(status & PS2_STAT_RXFULL))
                        break;
                handled = 1;
                scancode = inb(ps2if->base + PS2_DATA);
                if (status == 0xff && scancode == 0xff)
                        break;

                flag = (status & PS2_STAT_PARITY) ? 0 : SERIO_PARITY;

                if (hweight8(scancode) & 1)
                        flag ^= SERIO_PARITY;

                serio_interrupt(ps2if->io, scancode, flag);
        } while (1);
        return IRQ_RETVAL(handled);
}

static void pcips2_flush_input(struct pcips2_data *ps2if)
{
        unsigned char status, scancode;

        do {
                status = inb(ps2if->base + PS2_STATUS);
                if (!(status & PS2_STAT_RXFULL))
                        break;
                scancode = inb(ps2if->base + PS2_DATA);
                if (status == 0xff && scancode == 0xff)
                        break;
        } while (1);
}

static int pcips2_open(struct serio *io)
{
        struct pcips2_data *ps2if = io->port_data;
        int ret, val = 0;

        outb(PS2_CTRL_ENABLE, ps2if->base);
        pcips2_flush_input(ps2if);

        ret = request_irq(ps2if->dev->irq, pcips2_interrupt, IRQF_SHARED,
                          "pcips2", ps2if);
        if (ret == 0)
                val = PS2_CTRL_ENABLE | PS2_CTRL_RXIRQ;

        outb(val, ps2if->base);

        return ret;
}

static void pcips2_close(struct serio *io)
{
        struct pcips2_data *ps2if = io->port_data;

        outb(0, ps2if->base);

        free_irq(ps2if->dev->irq, ps2if);
}

static int pcips2_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
        struct pcips2_data *ps2if;
        struct serio *serio;
        int ret;

        ret = pci_enable_device(dev);
        if (ret)
                goto out;

        ret = pci_request_regions(dev, "pcips2");
        if (ret)
                goto disable;

        ps2if = kzalloc_obj(*ps2if);
        serio = kzalloc_obj(*serio);
        if (!ps2if || !serio) {
                ret = -ENOMEM;
                goto release;
        }


        serio->id.type          = SERIO_8042;
        serio->write            = pcips2_write;
        serio->open             = pcips2_open;
        serio->close            = pcips2_close;
        strscpy(serio->name, pci_name(dev), sizeof(serio->name));
        strscpy(serio->phys, dev_name(&dev->dev), sizeof(serio->phys));
        serio->port_data        = ps2if;
        serio->dev.parent       = &dev->dev;
        ps2if->io               = serio;
        ps2if->dev              = dev;
        ps2if->base             = pci_resource_start(dev, 0);

        pci_set_drvdata(dev, ps2if);

        serio_register_port(ps2if->io);
        return 0;

 release:
        kfree(ps2if);
        kfree(serio);
        pci_release_regions(dev);
 disable:
        pci_disable_device(dev);
 out:
        return ret;
}

static void pcips2_remove(struct pci_dev *dev)
{
        struct pcips2_data *ps2if = pci_get_drvdata(dev);

        serio_unregister_port(ps2if->io);
        kfree(ps2if);
        pci_release_regions(dev);
        pci_disable_device(dev);
}

static const struct pci_device_id pcips2_ids[] = {
        {
                .vendor         = 0x14f2,       /* MOBILITY */
                .device         = 0x0123,       /* Keyboard */
                .subvendor      = PCI_ANY_ID,
                .subdevice      = PCI_ANY_ID,
                .class          = PCI_CLASS_INPUT_KEYBOARD << 8,
                .class_mask     = 0xffff00,
        },
        {
                .vendor         = 0x14f2,       /* MOBILITY */
                .device         = 0x0124,       /* Mouse */
                .subvendor      = PCI_ANY_ID,
                .subdevice      = PCI_ANY_ID,
                .class          = PCI_CLASS_INPUT_MOUSE << 8,
                .class_mask     = 0xffff00,
        },
        { 0, }
};
MODULE_DEVICE_TABLE(pci, pcips2_ids);

static struct pci_driver pcips2_driver = {
        .name                   = "pcips2",
        .id_table               = pcips2_ids,
        .probe                  = pcips2_probe,
        .remove                 = pcips2_remove,
};

module_pci_driver(pcips2_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Russell King <rmk@arm.linux.org.uk>");
MODULE_DESCRIPTION("PCI PS/2 keyboard/mouse driver");