root/usr/src/uts/sun4u/io/i2c/clients/pca9556.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */


#include <sys/stat.h>
#include <sys/file.h>
#include <sys/uio.h>
#include <sys/modctl.h>
#include <sys/open.h>
#include <sys/types.h>
#include <sys/kmem.h>
#include <sys/systm.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/conf.h>
#include <sys/mode.h>
#include <sys/note.h>
#include <sys/i2c/clients/i2c_gpio.h>
#include <sys/i2c/clients/pca9556_impl.h>

/*
 * The PCA9556 is a gpio chip with 8 I/O ports.  The ports may be controlled by
 * an 8 bit input  port register, 8 bit output port register, 8 bit polarity
 * inversion register and an 8 bit configuration register.
 *
 * The input port register is a read only port and writes to this register
 * will have no effect regardless of whether the pin is an input or output.
 *
 * The output port register reflects the outgoing logic levels of the pins
 * defined as outputs by the configuration register.  Bit values in this
 * register have no effect on pins defined as inputs.
 *
 * The polarity register enables polarity inversion of pins defined as inputs by
 * the configuration register.  A set bit inverts the corresponding port's
 * polarity.
 *
 * The configuration register configures the directions of the I/O pins.  If a
 * bit is set the corresponding port is enabled as an input and if cleared,
 * as an output.
 *
 * The commands supported in the ioctl routine are:
 * GPIO_GET_INPUT       -- Read bits in the input port register.
 * GPIO_GET_OUTPUT      -- Read bits in the output port register.
 * GPIO_SET_OUPUT       -- Modify bits in the output port register.
 * GPIO_GET_POLARITY    -- Read bits in the polarity register.
 * GPIO_SET_POLARITY    -- Modify bits in the polarity register.
 * GPIO_GET_CONFIG      -- Read bits in the configuration register.
 * GPIO_SET_CONFIG      -- Modify bits in the configuration register.
 *
 * A pointer to the i2c_gpio_t data structure is sent as the third argument
 * in the ioctl call.  The reg_mask member identifies the bits that the user
 * wants to read or modify and reg_val has the actual value of the
 * corresponding bits set in reg_mask.
 *
 * To read a whole register the user has to set all the  bits in reg_mask
 * and the values will be copied into reg_val.
 *
 * In addition the pca9555 device has been added to this driver.  It is similar
 * to the pca9556 except that it has 2 8 bit I/O ports.
 */

/*
 * cb ops
 */
static int pca9556_open(dev_t *, int, int, cred_t *);
static int pca9556_close(dev_t, int, int, cred_t *);
static int pca9556_ioctl(dev_t, int, intptr_t, int, cred_t *, int *);

/*
 * dev ops
 */
static int pca9556_s_attach(dev_info_t *dip, ddi_attach_cmd_t cmd);
static int pca9556_s_detach(dev_info_t *dip, ddi_detach_cmd_t cmd);
static int pca9556_info(dev_info_t *, ddi_info_cmd_t, void *, void **);

static struct cb_ops pca9556_cb_ops = {
        pca9556_open,                   /* open */
        pca9556_close,                  /* close */
        nodev,                          /* strategy */
        nodev,                          /* print */
        nodev,                          /* dump */
        nodev,                          /* read */
        nodev,                          /* write */
        pca9556_ioctl,                  /* ioctl */
        nodev,                          /* devmap */
        nodev,                          /* mmap */
        nodev,                          /* segmap */
        nochpoll,                       /* poll */
        ddi_prop_op,                    /* cb_prop_op */
        NULL,                           /* streamtab */
        D_NEW | D_MP | D_HOTPLUG,       /* Driver compatibility flag */
};

static struct dev_ops pca9556_dev_ops = {
        DEVO_REV,
        0,
        pca9556_info,
        nulldev,
        nulldev,
        pca9556_s_attach,
        pca9556_s_detach,
        nodev,
        &pca9556_cb_ops,
        NULL,
        NULL,
        ddi_quiesce_not_supported,      /* devo_quiesce */
};

static struct modldrv pca9556_modldrv = {
        &mod_driverops,         /* type of module - driver */
        "pca9556 device driver",
        &pca9556_dev_ops,
};

static struct modlinkage pca9556_modlinkage = {
        MODREV_1,
        &pca9556_modldrv,
        0
};

static void *pca9556_soft_statep;
int pca9556_debug;

int
_init(void)
{
        int    err;

        err = mod_install(&pca9556_modlinkage);
        if (err == 0) {
                (void) ddi_soft_state_init(&pca9556_soft_statep,
                    sizeof (pca9556_unit_t), PCA9556_MAX_SIZE);
        }
        return (err);
}

int
_fini(void)
{
        int    err;

        err = mod_remove(&pca9556_modlinkage);
        if (err == 0) {
                ddi_soft_state_fini(&pca9556_soft_statep);
        }
        return (err);
}

int
_info(struct modinfo *modinfop)
{
        return (mod_info(&pca9556_modlinkage, modinfop));
}

static int
pca9556_resume(dev_info_t *dip)
{
        int             instance = ddi_get_instance(dip);
        pca9556_unit_t  *pcap;
        int             err = DDI_SUCCESS;
        int             reg_offset, num_of_ports;
        int             i, j;
        uint8_t         reg, reg_num = 0;
        extern int      do_polled_io;
        int             saved_pio;

        pcap = (pca9556_unit_t *)
            ddi_get_soft_state(pca9556_soft_statep, instance);

        if (pcap == NULL)
                return (ENXIO);

        /*
         * Restore registers to status existing before cpr
         */
        pcap->pca9556_transfer->i2c_flags = I2C_WR;
        pcap->pca9556_transfer->i2c_wlen = 2;
        pcap->pca9556_transfer->i2c_rlen = 0;

        if (pcap->pca9555_device) {
                reg_offset = 2;
                num_of_ports = PCA9555_NUM_PORTS;
        } else {
                reg_offset = 1;
                num_of_ports = PCA9556_NUM_PORTS;
        }

        /*
         * Since the parent node that handles interrupts may have already
         * been suspended, perform the following i2c transfers in poll-mode.
         */
        saved_pio = do_polled_io;
        do_polled_io = 1;

        for (i = 0; i < num_of_ports; i++) {
                if (pcap->pca9555_device)
                        reg = PCA9555_OUTPUT_REG;
                else
                        reg = PCA9556_OUTPUT_REG;

                for (j = 0; j < PCA9556_NUM_REG; j++) {
                        pcap->pca9556_transfer->i2c_wbuf[0] = reg + i;
                        pcap->pca9556_transfer->i2c_wbuf[1] =
                            pcap->pca9556_cpr_state[reg_num++];

                        if (i2c_transfer(pcap->pca9556_hdl,
                            pcap->pca9556_transfer) != DDI_SUCCESS) {
                                err = EIO;

                                goto done;
                        }

                        reg = reg + reg_offset;
                }
        }

done:
        do_polled_io = saved_pio;
        if (err != DDI_SUCCESS) {
                cmn_err(CE_WARN, "%s Unable to restore registers",
                    pcap->pca9556_name);
        }

        /*
         * Clear busy flag so that transactions may continue
         */
        mutex_enter(&pcap->pca9556_mutex);
        pcap->pca9556_flags = pcap->pca9556_flags & ~PCA9556_BUSYFLAG;
        cv_broadcast(&pcap->pca9556_cv);
        mutex_exit(&pcap->pca9556_mutex);
        return (err);
}

static void
pca9556_detach(dev_info_t *dip)
{
        pca9556_unit_t *pcap;
        int             instance = ddi_get_instance(dip);

        pcap = ddi_get_soft_state(pca9556_soft_statep, instance);

        if ((pcap->pca9556_flags & PCA9556_REGFLAG) == PCA9556_REGFLAG) {
                i2c_client_unregister(pcap->pca9556_hdl);
        }
        if ((pcap->pca9556_flags & PCA9556_TBUFFLAG) == PCA9556_TBUFFLAG) {
                i2c_transfer_free(pcap->pca9556_hdl, pcap->pca9556_transfer);
        }
        if ((pcap->pca9556_flags & PCA9556_MINORFLAG) == PCA9556_MINORFLAG) {
                ddi_remove_minor_node(dip, NULL);
        }
        cv_destroy(&pcap->pca9556_cv);
        mutex_destroy(&pcap->pca9556_mutex);
        ddi_soft_state_free(pca9556_soft_statep, instance);

}

static int
pca9556_attach(dev_info_t *dip)
{
        pca9556_unit_t          *pcap;
        int                     instance = ddi_get_instance(dip);
        char                    name[MAXNAMELEN];
        char *device_name;
        minor_t                 minor;
        int                     i, num_ports;

        if (ddi_soft_state_zalloc(pca9556_soft_statep, instance) != 0) {
                cmn_err(CE_WARN, "%s%d failed to zalloc softstate",
                    ddi_get_name(dip), instance);
                return (DDI_FAILURE);
        }

        pcap = ddi_get_soft_state(pca9556_soft_statep, instance);

        if (pcap == NULL)
                return (DDI_FAILURE);

        mutex_init(&pcap->pca9556_mutex, NULL, MUTEX_DRIVER, NULL);
        cv_init(&pcap->pca9556_cv, NULL, CV_DRIVER, NULL);

        (void) snprintf(pcap->pca9556_name, sizeof (pcap->pca9556_name),
            "%s_%d", ddi_driver_name(dip), instance);

        device_name = ddi_get_name(dip);

        if (strcmp(device_name, "i2c-pca9555") == 0) {
                num_ports = PCA9555_NUM_PORTS;
                pcap->pca9555_device = B_TRUE;
        } else {
                num_ports = PCA9556_NUM_PORTS;
                pcap->pca9555_device = B_FALSE;
                minor = INST_TO_MINOR(instance);
        }

        for (i = 0; i < num_ports; i++) {
                if (!(pcap->pca9555_device)) {
                        (void) snprintf(pcap->pca9556_name,
                            sizeof (pcap->pca9556_name), "%s_%d",
                            ddi_driver_name(dip), instance);
                        (void) snprintf(name, sizeof (name), "%s",
                            pcap->pca9556_name);
                } else {
                        (void) sprintf(name, "port_%d", i);
                        minor = INST_TO_MINOR(instance) |
                            PORT_TO_MINOR(I2C_PORT(i));
                }

                if (ddi_create_minor_node(dip, name, S_IFCHR, minor,
                    PCA9556_NODE_TYPE, 0) == DDI_FAILURE) {
                        cmn_err(CE_WARN, "%s: failed to create node for %s",
                            pcap->pca9556_name, name);
                        pca9556_detach(dip);
                        return (DDI_FAILURE);
                }
        }
        pcap->pca9556_flags |= PCA9556_MINORFLAG;

        /*
         * Add a zero-length attribute to tell the world we support
         * kernel ioctls (for layered drivers)
         */
        (void) ddi_prop_create(DDI_DEV_T_NONE, dip, DDI_PROP_CANSLEEP,
            DDI_KERNEL_IOCTL, NULL, 0);


        /*
         * preallocate a single buffer for all reads and writes
         */
        if (i2c_transfer_alloc(pcap->pca9556_hdl, &pcap->pca9556_transfer,
            2, 2, I2C_SLEEP) != I2C_SUCCESS) {
                cmn_err(CE_WARN, "%s i2c_transfer_alloc failed",
                    pcap->pca9556_name);
                pca9556_detach(dip);
                return (DDI_FAILURE);
        }
        pcap->pca9556_flags |= PCA9556_TBUFFLAG;
        pcap->pca9556_transfer->i2c_version = I2C_XFER_REV;

        if (i2c_client_register(dip, &pcap->pca9556_hdl) != I2C_SUCCESS) {
                ddi_remove_minor_node(dip, NULL);
                cmn_err(CE_WARN, "%s i2c_client_register failed",
                    pcap->pca9556_name);
                pca9556_detach(dip);
                return (DDI_FAILURE);
        }
        pcap->pca9556_flags |= PCA9556_REGFLAG;

        /*
         * Store the dip for future dip.
         */
        pcap->pca9556_dip = dip;
        return (DDI_SUCCESS);
}


static int
pca9556_info(dev_info_t *dip, ddi_info_cmd_t cmd, void *arg, void **result)
{
        _NOTE(ARGUNUSED(dip))

        pca9556_unit_t  *pcap;
        int             instance = MINOR_TO_INST(getminor((dev_t)arg));

        switch (cmd) {
        case DDI_INFO_DEVT2DEVINFO:
                pcap = ddi_get_soft_state(pca9556_soft_statep, instance);
                if (pcap == NULL)
                        return (DDI_FAILURE);
                *result = (void *)pcap->pca9556_dip;
                return (DDI_SUCCESS);
        case DDI_INFO_DEVT2INSTANCE:
                *result = (void *)(uintptr_t)instance;
                return (DDI_SUCCESS);
        default:
                return (DDI_FAILURE);
        }
}

static int
pca9556_s_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        switch (cmd) {
        case DDI_ATTACH:
                return (pca9556_attach(dip));
        case DDI_RESUME:
                return (pca9556_resume(dip));
        default:
                return (DDI_FAILURE);
        }
}

static int
pca9556_suspend(dev_info_t *dip)
{
        pca9556_unit_t  *pcap;
        int             instance = ddi_get_instance(dip);
        int             err = DDI_SUCCESS;
        int             reg_offset, num_of_ports;
        int             i, j;
        uint8_t         reg, reg_num = 0;
        extern int      do_polled_io;
        int             saved_pio;

        pcap = ddi_get_soft_state(pca9556_soft_statep, instance);

        mutex_enter(&pcap->pca9556_mutex);
        while ((pcap->pca9556_flags & PCA9556_BUSYFLAG) == PCA9556_BUSYFLAG) {
                if (cv_wait_sig(&pcap->pca9556_cv,
                    &pcap->pca9556_mutex) <= 0) {
                        mutex_exit(&pcap->pca9556_mutex);
                        return (DDI_FAILURE);
                }
        }
        pcap->pca9556_flags |= PCA9556_BUSYFLAG;
        mutex_exit(&pcap->pca9556_mutex);

        /*
         * A pca9555 devices command registers are offset by 2 and it has 2
         * ports to save. A pca9556 devices command registers are offset by 1
         * while it only has one "port"
         */
        if (pcap->pca9555_device) {
                reg_offset = 2;
                num_of_ports = PCA9555_NUM_PORTS;
        } else {
                reg_offset = 1;
                num_of_ports = PCA9556_NUM_PORTS;
        }
        /*
         * Save the state of the registers
         */
        pcap->pca9556_transfer->i2c_flags = I2C_WR_RD;
        pcap->pca9556_transfer->i2c_wlen = 1;
        pcap->pca9556_transfer->i2c_rlen = 1;

        /*
         * Since the parent node that handles interrupts may have not been
         * resumed yet, perform the following i2c transfers in poll-mode.
         */
        saved_pio = do_polled_io;
        do_polled_io = 1;

        /*
         * The following for loop will run through once for a pca9556 device
         * and twice for a pca9555 device. i will represent the port number
         * for the pca9555.
         */
        for (i = 0; i < num_of_ports; i++) {
                /*
                 * We set the first Register here so it can be reset if we
                 * loop through (pca9555 device).
                 */
                if (pcap->pca9555_device)
                        reg = PCA9555_OUTPUT_REG;
                else
                        reg = PCA9556_OUTPUT_REG;

                /* We run through this loop 3 times. Once for each register */
                for (j = 0; j < PCA9556_NUM_REG; j++) {

                        /*
                         * We add the port number (0 for pca9556, 0 or 1 for
                         * a pca9555) to the register.
                         */
                        pcap->pca9556_transfer->i2c_wbuf[0] = reg + i;
                        if (i2c_transfer(pcap->pca9556_hdl,
                            pcap->pca9556_transfer) != DDI_SUCCESS) {
                                err = EIO;
                                goto done;
                        }

                        pcap->pca9556_cpr_state[reg_num++] =
                            pcap->pca9556_transfer->i2c_rbuf[0];
                        /*
                         * The register is then added to the offset and saved
                         * to go and read the next command register.
                         */
                        reg = reg + reg_offset;
                }
        }

done:
        do_polled_io = saved_pio;
        if (err != DDI_SUCCESS) {
                mutex_enter(&pcap->pca9556_mutex);
                pcap->pca9556_flags = pcap->pca9556_flags & ~PCA9556_BUSYFLAG;
                cv_broadcast(&pcap->pca9556_cv);
                mutex_exit(&pcap->pca9556_mutex);
                cmn_err(CE_WARN, "%s Suspend failed, unable to save registers",
                    pcap->pca9556_name);
                return (err);
        }
        return (DDI_SUCCESS);
}

static int
pca9556_s_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
        switch (cmd) {
        case DDI_DETACH:
                pca9556_detach(dip);
                return (DDI_SUCCESS);
        case DDI_SUSPEND:
                return (pca9556_suspend(dip));
        default:
                return (DDI_FAILURE);
        }
}

static int
pca9556_open(dev_t *devp, int flags, int otyp, cred_t *credp)
{
        int                     instance;
        pca9556_unit_t          *pcap;
        int                     err = EBUSY;

        /*
         * Make sure the open is for the right file type
         */
        if (otyp != OTYP_CHR)
                return (EINVAL);

        instance = MINOR_TO_INST(getminor(*devp));

        pcap = (pca9556_unit_t *)
            ddi_get_soft_state(pca9556_soft_statep, instance);
        if (pcap == NULL)
                return (ENXIO);

        /* must be privileged to access this device */
        if (drv_priv(credp) != 0)
                return (EPERM);

        /*
         * Enforce exclusive access if required
         */
        mutex_enter(&pcap->pca9556_mutex);
        if (flags & FEXCL) {
                if (pcap->pca9556_oflag == 0) {
                        pcap->pca9556_oflag = FEXCL;
                        err = DDI_SUCCESS;
                }
        } else if (pcap->pca9556_oflag != FEXCL) {
                pcap->pca9556_oflag = (uint16_t)FOPEN;
                err = DDI_SUCCESS;
        }
        mutex_exit(&pcap->pca9556_mutex);
        return (err);
}

static int
pca9556_close(dev_t dev, int flags, int otyp, cred_t *credp)
{
        int             instance;
        pca9556_unit_t  *pcap;

        _NOTE(ARGUNUSED(flags, credp))

        /*
         * Make sure the close is for the right file type
         */
        if (otyp != OTYP_CHR)
                return (EINVAL);

        instance = MINOR_TO_INST(getminor(dev));

        pcap = (pca9556_unit_t *)
            ddi_get_soft_state(pca9556_soft_statep, instance);
        if (pcap == NULL)
                return (ENXIO);

        mutex_enter(&pcap->pca9556_mutex);
        pcap->pca9556_oflag = 0;
        mutex_exit(&pcap->pca9556_mutex);
        return (0);
}

static int
pca9556_ioctl(dev_t dev, int cmd, intptr_t arg, int mode, cred_t *credp,
    int *rvalp)
{
        pca9556_unit_t          *pcap;
        int                     err = 0;
        int                     instance = MINOR_TO_INST(getminor(dev));
        int                     port;
        i2c_gpio_t              g_buf;
        uchar_t                 temp;
        boolean_t               write_io = B_FALSE;

        _NOTE(ARGUNUSED(credp, rvalp))

        pcap = (pca9556_unit_t *)
            ddi_get_soft_state(pca9556_soft_statep, instance);

        if (pcap->pca9555_device) {
                port =  MINOR_TO_PORT(getminor(dev));
        }
        if (pca9556_debug) {
                prom_printf("pca9556_ioctl: instance=%d\n", instance);
        }

        /*
         * We serialize here and  block any pending transacations.
         */
        mutex_enter(&pcap->pca9556_mutex);
        while ((pcap->pca9556_flags & PCA9556_BUSYFLAG) == PCA9556_BUSYFLAG) {
                if (cv_wait_sig(&pcap->pca9556_cv,
                    &pcap->pca9556_mutex) <= 0) {
                        mutex_exit(&pcap->pca9556_mutex);
                        return (EINTR);
                }
        }
        pcap->pca9556_flags |= PCA9556_BUSYFLAG;
        mutex_exit(&pcap->pca9556_mutex);
        if (ddi_copyin((caddr_t)arg, &g_buf,
            sizeof (i2c_gpio_t), mode) != DDI_SUCCESS) {

                err = EFAULT;

                goto cleanup;
        }
        pcap->pca9556_transfer->i2c_flags = I2C_WR_RD;
        pcap->pca9556_transfer->i2c_wlen = 1;
        pcap->pca9556_transfer->i2c_rlen = 1;

        /*
         * Evaluate which register is to be read or modified
         */

        switch (cmd) {
        case GPIO_GET_INPUT:
                if (pcap->pca9555_device)
                        pcap->pca9556_transfer->i2c_wbuf[0] =
                            PCA9555_INPUT_REG + port;
                else
                        pcap->pca9556_transfer->i2c_wbuf[0] =
                            PCA9556_INPUT_REG;
                break;

        case GPIO_SET_OUTPUT:
                write_io = B_TRUE;
                /*FALLTHROUGH*/

        case GPIO_GET_OUTPUT:
                if (pcap->pca9555_device)
                        pcap->pca9556_transfer->i2c_wbuf[0] =
                            PCA9555_OUTPUT_REG + port;
                else
                        pcap->pca9556_transfer->i2c_wbuf[0] =
                            PCA9556_OUTPUT_REG;
                break;

        case GPIO_SET_POLARITY:
                write_io = B_TRUE;
                /*FALLTHROUGH*/

        case GPIO_GET_POLARITY:
                if (pcap->pca9555_device)
                        pcap->pca9556_transfer->i2c_wbuf[0] =
                            PCA9555_POLARITY_REG + port;
                else
                        pcap->pca9556_transfer->i2c_wbuf[0] =
                            PCA9556_POLARITY_REG;
                break;

        case GPIO_SET_CONFIG:
                write_io = B_TRUE;
                /*FALLTHROUGH*/

        case GPIO_GET_CONFIG:
                if (pcap->pca9555_device)
                        pcap->pca9556_transfer->i2c_wbuf[0] =
                            PCA9555_CONFIG_REG + port;
                else
                        pcap->pca9556_transfer->i2c_wbuf[0] =
                            PCA9556_CONFIG_REG;
                break;
        }

        /*
         * Read the required register
         */
        if (i2c_transfer(pcap->pca9556_hdl, pcap->pca9556_transfer)
            != I2C_SUCCESS) {
                err = EIO;

                goto cleanup;
        }
        /*
         * Evaluate whether the register is to be read or modified
         */
        if (!write_io) {
                g_buf.reg_val = g_buf.reg_mask &
                    pcap->pca9556_transfer->i2c_rbuf[0];
                err = ddi_copyout(&g_buf, (caddr_t)arg,
                    sizeof (i2c_gpio_t), mode);
        } else {
                pcap->pca9556_transfer->i2c_flags = I2C_WR;
                pcap->pca9556_transfer->i2c_wlen = 2;
                pcap->pca9556_transfer->i2c_rlen = 0;

                /*
                 * Modify register without overwriting existing contents
                 */

                temp = pcap->pca9556_transfer->i2c_rbuf[0] & (~g_buf.reg_mask);
                pcap->pca9556_transfer->i2c_wbuf[1] = temp|
                    (g_buf.reg_val & g_buf.reg_mask);
                if (i2c_transfer(pcap->pca9556_hdl, pcap->pca9556_transfer)
                    != I2C_SUCCESS) {
                                err = EIO;
                }

        }
cleanup:
        mutex_enter(&pcap->pca9556_mutex);
        pcap->pca9556_flags  = pcap->pca9556_flags & ~PCA9556_BUSYFLAG;
        cv_signal(&pcap->pca9556_cv);
        mutex_exit(&pcap->pca9556_mutex);
        return (err);
        }