root/usr/src/uts/sun4u/io/i2c/nexus/smbus.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.
 */


/*
 * This is the nexus driver for SMBUS devices.  It mostly does not use
 * the SMBUS protocol so that it fits better into the solaris i2c
 * framework.
 */

#include <sys/types.h>
#include <sys/conf.h>
#include <sys/file.h>
#include <sys/open.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/modctl.h>
#include <sys/stat.h>
#include <sys/kmem.h>
#include <sys/archsystm.h>
#include <sys/platform_module.h>

#include <sys/i2c/clients/i2c_client.h>
#include <sys/i2c/misc/i2c_svc.h>
#include <sys/i2c/misc/i2c_svc_impl.h>
#include <sys/i2c/nexus/smbus.h>

/*
 * static function declarations
 */
static uint_t smbus_intr_cmn(smbus_t *smbus, char *src);
static void smbus_intr_timeout(void *arg);
static void smbus_resume(dev_info_t *dip);
static void smbus_suspend(dev_info_t *dip);
static int smbus_bus_ctl(dev_info_t *dip, dev_info_t *rdip,
        ddi_ctl_enum_t op, void *arg, void *result);
static  int smbus_acquire(smbus_t *, dev_info_t *dip,
        i2c_transfer_t *tp);
static  void smbus_release(smbus_t *);
static int smbus_attach(dev_info_t *dip, ddi_attach_cmd_t cmd);
static int smbus_detach(dev_info_t *dip, ddi_detach_cmd_t cmd);
static void smbus_free_regs(smbus_t *smbus);
static int smbus_setup_regs(dev_info_t *dip, smbus_t *smbus);
static void smbus_reportdev(dev_info_t *dip, dev_info_t *rdip);
static void smbus_uninitchild(dev_info_t *cdip);
static int smbus_initchild(dev_info_t *cdip);
static int smbus_rd(smbus_t *smbus);
static int smbus_wr(smbus_t *smbus);
static void smbus_put(smbus_t *smbus, uint8_t reg, uint8_t data, uint8_t flags);
static uint8_t smbus_get(smbus_t *smbus, uint8_t reg);
static int smbus_dip_to_addr(dev_info_t *dip);
static uint_t smbus_intr(caddr_t arg);
static int smbus_switch(smbus_t *smbus);

static struct bus_ops smbus_busops = {
        BUSO_REV,
        nullbusmap,                     /* bus_map */
        NULL,                           /* bus_get_intrspec */
        NULL,                           /* bus_add_intrspec */
        NULL,                           /* bus_remove_intrspec */
        NULL,                           /* bus_map_fault */
        ddi_no_dma_map,                 /* bus_dma_map */
        ddi_no_dma_allochdl,            /* bus_dma_allochdl */
        ddi_no_dma_freehdl,             /* bus_dma_freehdl */
        ddi_no_dma_bindhdl,             /* bus_dma_bindhdl */
        ddi_no_dma_unbindhdl,           /* bus_unbindhdl */
        ddi_no_dma_flush,               /* bus_dma_flush */
        ddi_no_dma_win,                 /* bus_dma_win */
        ddi_no_dma_mctl,                /* bus_dma_ctl */
        smbus_bus_ctl,                  /* bus_ctl */
        ddi_bus_prop_op,                /* bus_prop_op */
        NULL,                           /* bus_get_eventcookie */
        NULL,                           /* bus_add_eventcall */
        NULL,                           /* bus_remove_eventcall */
        NULL,                           /* bus_post_event */
        0,                              /* bus_intr_ctl         */
        0,                              /* bus_config           */
        0,                              /* bus_unconfig         */
        0,                              /* bus_fm_init          */
        0,                              /* bus_fm_fini          */
        0,                              /* bus_fm_access_enter  */
        0,                              /* bus_fm_access_exit   */
        0,                              /* bus_power            */
        i_ddi_intr_ops                  /* bus_intr_op          */
};

struct cb_ops smbus_cb_ops = {
        nodev,                  /* open */
        nodev,                  /* close */
        nodev,                  /* strategy */
        nodev,                  /* print */
        nodev,                  /* dump */
        nodev,                  /* read */
        nodev,                  /* write */
        nodev,                  /* ioctl */
        nodev,                  /* devmap */
        nodev,                  /* mmap */
        nodev,                  /* segmap */
        nochpoll,               /* poll */
        ddi_prop_op,            /* cb_prop_op */
        0,                      /* streamtab  */
        D_MP | D_NEW            /* Driver compatibility flag */
};

static struct dev_ops smbus_ops = {
        DEVO_REV,
        0,
        ddi_no_info,
        nulldev,
        nulldev,
        smbus_attach,
        smbus_detach,
        nodev,
        &smbus_cb_ops,
        &smbus_busops,
        NULL,
        ddi_quiesce_not_supported,      /* devo_quiesce */
};

static struct modldrv modldrv = {
        &mod_driverops, /* Type of module. This one is a driver */
        "SMBUS nexus Driver",   /* Name of the module. */
        &smbus_ops,             /* driver ops */
};

static struct modlinkage modlinkage = {
        MODREV_1,
        &modldrv,
        NULL
};

/*
 * Globals
 */
static void     *smbus_state;

static int intr_timeout = INTR_TIMEOUT;

/*
 * The "interrupt-priorities" property is how a driver can specify a SPARC
 * PIL level to associate with each of its interrupt properties.  Most
 * self-identifying busses have a better mechanism for managing this, but I2C
 * doesn't.
 */
int smbus_pil = SMBUS_PIL;

i2c_nexus_reg_t smbus_regvec = {
        I2C_NEXUS_REV,
        smbus_transfer,
};

#ifdef DEBUG

static int smbus_print_lvl = 0;
static char msg_buff[1024];
static kmutex_t msg_buf_lock;

void
smbus_print(int flags, const char *fmt, ...)
{
        if (flags & smbus_print_lvl) {
                va_list ap;

                va_start(ap, fmt);

                if (smbus_print_lvl & PRT_PROM) {
                        prom_vprintf(fmt, ap);
                } else {

                        mutex_enter(&msg_buf_lock);
                        (void) vsprintf(msg_buff, fmt, ap);
                        if (smbus_print_lvl & PRT_BUFFONLY) {
                                cmn_err(CE_CONT, "?%s", msg_buff);
                        } else {
                                cmn_err(CE_CONT, "%s", msg_buff);
                        }
                        mutex_exit(&msg_buf_lock);
                }
                va_end(ap);
        }
}
#endif /* DEBUG */

int
_init(void)
{
        int status;

        status = ddi_soft_state_init(&smbus_state, sizeof (smbus_t),
            1);
        if (status != 0) {

                return (status);
        }

        if ((status = mod_install(&modlinkage)) != 0) {
                ddi_soft_state_fini(&smbus_state);
        } else {
#ifdef DEBUG
                mutex_init(&msg_buf_lock, NULL, MUTEX_DRIVER, NULL);
#endif
        }
        return (status);
}

int
_fini(void)
{
        int status;

        if ((status = mod_remove(&modlinkage)) == 0) {
                ddi_soft_state_fini(&smbus_state);
#ifdef DEBUG
                mutex_destroy(&msg_buf_lock);
#endif
        }

        return (status);
}

/*
 * The loadable-module _info(9E) entry point
 */
int
_info(struct modinfo *modinfop)
{
        return (mod_info(&modlinkage, modinfop));
}

static void
smbus_interrupts_on(smbus_t *smbus)
{
        int src_enable;

        src_enable = ddi_get32(smbus->smbus_confighandle,
            (uint32_t *)&smbus->smbus_configregaddr[SMBUS_SRC_ENA]);
        src_enable |= SMBUS_SMI;
        ddi_put32(smbus->smbus_confighandle,
            (uint32_t *)&smbus->smbus_configregaddr[SMBUS_SRC_ENA],
            src_enable);
        (void) ddi_get32(smbus->smbus_confighandle,
            (uint32_t *)&smbus->smbus_configregaddr[SMBUS_SRC_ENA]);
}

static void
smbus_interrupts_off(smbus_t *smbus)
{
        int src_enable;

        src_enable = ddi_get32(smbus->smbus_confighandle,
            (uint32_t *)&smbus->smbus_configregaddr[SMBUS_SRC_ENA]);
        src_enable &= ~SMBUS_SMI;
        ddi_put32(smbus->smbus_confighandle,
            (uint32_t *)&smbus->smbus_configregaddr[SMBUS_SRC_ENA],
            src_enable);
        (void) ddi_get32(smbus->smbus_confighandle,
            (uint32_t *)&smbus->smbus_configregaddr[SMBUS_SRC_ENA]);
}

static void
smbus_dodetach(dev_info_t *dip)
{
        smbus_t *smbus;
        int instance = ddi_get_instance(dip);

        smbus = ddi_get_soft_state(smbus_state, instance);

        if (smbus == NULL) {

                return;
        }

        cv_destroy(&smbus->smbus_cv);
        mutex_destroy(&smbus->smbus_mutex);

        if ((smbus->smbus_attachflags & INTERRUPT_PRI) != 0) {
                (void) ddi_prop_remove(DDI_DEV_T_NONE, dip,
                    "interrupt-priorities");
        }

        smbus_free_regs(smbus);

        if ((smbus->smbus_attachflags & NEXUS_REGISTER) != 0) {
                i2c_nexus_unregister(dip);
        }
        if ((smbus->smbus_attachflags & IMUTEX) != 0) {
                mutex_destroy(&smbus->smbus_imutex);
                cv_destroy(&smbus->smbus_icv);
        }

        if (smbus->smbus_timeout != 0) {
                (void) untimeout(smbus->smbus_timeout);
        }

        if ((smbus->smbus_attachflags & ADD_INTR) != 0) {
                ddi_remove_intr(dip, 0, smbus->smbus_icookie);
        }

        ddi_soft_state_free(smbus_state, instance);
}

static int
smbus_doattach(dev_info_t *dip)
{
        smbus_t *smbus;
        int instance = ddi_get_instance(dip);

        /*
         * Allocate soft state structure.
         */
        if (ddi_soft_state_zalloc(smbus_state, instance) != DDI_SUCCESS) {

                goto bad;
        }

        smbus = ddi_get_soft_state(smbus_state, instance);

        (void) snprintf(smbus->smbus_name, sizeof (smbus->smbus_name),
            "%s%d", ddi_node_name(dip), instance);

        smbus->smbus_dip = dip;

        mutex_init(&smbus->smbus_mutex, NULL, MUTEX_DRIVER, NULL);
        mutex_init(&smbus->smbus_imutex, NULL, MUTEX_DRIVER, NULL);
        cv_init(&smbus->smbus_cv, NULL, CV_DRIVER, NULL);
        cv_init(&smbus->smbus_intr_cv, NULL, CV_DRIVER, NULL);

        if (smbus_setup_regs(dip, smbus) != DDI_SUCCESS) {
                goto bad;
        }

        if (ddi_prop_exists(DDI_DEV_T_ANY, dip,  DDI_PROP_DONTPASS,
            "interrupts") == 1) {
                smbus->smbus_polling = 0;
                /*
                 * The "interrupt-priorities" property is how a driver can
                 * specify a SPARC PIL level to associate with each of its
                 * interrupt properties.  Most self-identifying busses have
                 * a better mechanism for managing this, but I2C doesn't.
                 */
                if (ddi_prop_exists(DDI_DEV_T_ANY, dip,
                    DDI_PROP_NOTPROM | DDI_PROP_DONTPASS,
                    "interrupt-priorities") != 1) {
                        (void) ddi_prop_create(DDI_DEV_T_NONE, dip,
                            DDI_PROP_CANSLEEP, "interrupt-priorities",
                            (caddr_t)&smbus_pil,
                            sizeof (smbus_pil));
                        smbus->smbus_attachflags |= INTERRUPT_PRI;
                }

                /*
                 * Clear status to clear any possible interrupt
                 */
                smbus_put(smbus, SMB_STS, 0xff, SMBUS_FLUSH);

                if (ddi_get_iblock_cookie(dip, 0, &smbus->smbus_icookie) !=
                    DDI_SUCCESS) {
                        goto bad;
                }

                if (ddi_add_intr(dip, 0, NULL, NULL, smbus_intr,
                    (caddr_t)smbus) != DDI_SUCCESS) {
                        cmn_err(CE_WARN, "%s failed to add interrupt",
                            smbus->smbus_name);
                        goto bad;
                }
                smbus->smbus_attachflags |= ADD_INTR;
        } else {
                smbus->smbus_polling = 1;
                /* Clear status */
                smbus_put(smbus, SMB_STS, 0xff, SMBUS_FLUSH);
        }

        /*
         * initialize a cv and mutex
         */
        cv_init(&smbus->smbus_icv, NULL, CV_DRIVER, NULL);
        mutex_init(&smbus->smbus_imutex, NULL, MUTEX_DRIVER,
            (void *)smbus->smbus_icookie);
        smbus->smbus_attachflags |= IMUTEX;

        /*
         * Register with the i2c framework
         */
        i2c_nexus_register(dip, &smbus_regvec);
        smbus->smbus_attachflags |= NEXUS_REGISTER;

        return (DDI_SUCCESS);

bad:
        smbus_dodetach(dip);

        return (DDI_FAILURE);
}

static int
smbus_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        switch (cmd) {
        case DDI_ATTACH:

                return (smbus_doattach(dip));
        case DDI_RESUME:
                smbus_resume(dip);

                return (DDI_SUCCESS);
        default:

                return (DDI_FAILURE);
        }
}

static int
smbus_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
        switch (cmd) {
        case DDI_DETACH:
                smbus_dodetach(dip);

                return (DDI_SUCCESS);
        case DDI_SUSPEND:
                smbus_suspend(dip);

                return (DDI_SUCCESS);
        default:

                return (DDI_FAILURE);
        }
}

static int
smbus_bus_ctl(dev_info_t *dip, dev_info_t *rdip, ddi_ctl_enum_t op,
    void *arg, void *result)
{
        switch (op) {
        case DDI_CTLOPS_INITCHILD:

                return (smbus_initchild((dev_info_t *)arg));
        case DDI_CTLOPS_UNINITCHILD:
                smbus_uninitchild((dev_info_t *)arg);

                return (DDI_SUCCESS);
        case DDI_CTLOPS_REPORTDEV:
                smbus_reportdev(dip, rdip);

                return (DDI_SUCCESS);
        case DDI_CTLOPS_DMAPMAPC:
        case DDI_CTLOPS_POKE:
        case DDI_CTLOPS_PEEK:
        case DDI_CTLOPS_IOMIN:
        case DDI_CTLOPS_REPORTINT:
        case DDI_CTLOPS_SIDDEV:
        case DDI_CTLOPS_SLAVEONLY:
        case DDI_CTLOPS_AFFINITY:
        case DDI_CTLOPS_PTOB:
        case DDI_CTLOPS_BTOP:
        case DDI_CTLOPS_BTOPR:
        case DDI_CTLOPS_DVMAPAGESIZE:

                return (DDI_FAILURE);
        default:

                return (ddi_ctlops(dip, rdip, op, arg, result));
        }
}

static int
smbus_initchild(dev_info_t *cdip)
{
        int32_t cell_size;
        int len;
        int32_t regs[2];
        int err;
        smbus_ppvt_t *ppvt;
        char name[30];

        SMBUS_PRINT((PRT_INIT, "smbus_initchild ENTER: %s\n",
            ddi_node_name(cdip)));

        len = sizeof (cell_size);
        err = ddi_getlongprop_buf(DDI_DEV_T_ANY, cdip,
            DDI_PROP_CANSLEEP, "#address-cells",
            (caddr_t)&cell_size, &len);
        if (err != DDI_PROP_SUCCESS || len != sizeof (cell_size)) {
                cmn_err(CE_WARN, "cannot find address-cells");

                return (DDI_FAILURE);
        }

        len = sizeof (regs);
        err = ddi_getlongprop_buf(DDI_DEV_T_ANY, cdip,
            DDI_PROP_DONTPASS | DDI_PROP_CANSLEEP,
            "reg", (caddr_t)regs, &len);

        if (err != DDI_PROP_SUCCESS) {
                cmn_err(CE_WARN, "cannot get reg property");

                return (DDI_FAILURE);
        }

        ppvt = kmem_zalloc(sizeof (smbus_ppvt_t), KM_SLEEP);
        ddi_set_parent_data(cdip, ppvt);

        /*
         * The reg property contains an unused first element (which is
         * the mux addr on xcal), and the second element is the i2c bus
         * address of the device.
         */
        ppvt->smbus_ppvt_addr = regs[1];
        (void) sprintf(name, "%x", regs[1]);

        ddi_set_name_addr(cdip, name);

        SMBUS_PRINT((PRT_INIT, "smbus_initchild SUCCESS: %s\n",
            ddi_node_name(cdip)));

        return (DDI_SUCCESS);
}

static void
smbus_uninitchild(dev_info_t *cdip)
{
        smbus_ppvt_t *ppvt;

        ppvt = ddi_get_parent_data(cdip);
        ddi_set_parent_data(cdip, NULL);

        ddi_set_name_addr(cdip, NULL);

        kmem_free(ppvt, sizeof (smbus_ppvt_t));

        SMBUS_PRINT((PRT_INIT, "smbus_uninitchild: %s\n", ddi_node_name(cdip)));
}

static void
smbus_reportdev(dev_info_t *dip, dev_info_t *rdip)
{
        smbus_ppvt_t *ppvt;

        ppvt = ddi_get_parent_data(rdip);

        cmn_err(CE_CONT, "?%s%d at %s%d: addr 0x%x",
            ddi_driver_name(rdip), ddi_get_instance(rdip),
            ddi_driver_name(dip), ddi_get_instance(dip),
            ppvt->smbus_ppvt_addr);
}

/*
 * smbus_setup_regs() is called to map in the registers
 * specific to the smbus.
 */
static int
smbus_setup_regs(dev_info_t *dip, smbus_t *smbus)
{
        ddi_device_acc_attr_t attr;
        int ret;

        attr.devacc_attr_version = DDI_DEVICE_ATTR_V0;
        attr.devacc_attr_endian_flags = DDI_STRUCTURE_LE_ACC;
        attr.devacc_attr_dataorder = DDI_STRICTORDER_ACC;

        ret = ddi_regs_map_setup(dip, 1, (caddr_t *)&smbus->smbus_regaddr,
            0, 0, &attr, &smbus->smbus_rhandle);

        if (ret == DDI_FAILURE) {
                cmn_err(CE_WARN, "%s unable to map regs", smbus->smbus_name);

        } else if (ret == DDI_REGS_ACC_CONFLICT) {
                cmn_err(CE_WARN,
                    "%s unable to map regs because of conflict",
                    smbus->smbus_name);
                ret = DDI_FAILURE;
        }

        if (ret == DDI_FAILURE) {

                return (ret);
        }

        ret = ddi_regs_map_setup(dip, 0, (caddr_t *)&smbus->smbus_configregaddr,
            0, 0, &attr, &smbus->smbus_confighandle);

        if (ret == DDI_FAILURE) {
                cmn_err(CE_WARN, "%s unable to map config regs",
                    smbus->smbus_name);

        } else if (ret == DDI_REGS_ACC_CONFLICT) {
                cmn_err(CE_WARN,
                    "%s unable to map config regs because of conflict",
                    smbus->smbus_name);
                ret = DDI_FAILURE;
        }

        return (ret);
}

/*
 * smbus_free_regs() frees any registers previously allocated.
 */
static void
smbus_free_regs(smbus_t *smbus)
{
        if (smbus->smbus_regaddr != NULL) {
                ddi_regs_map_free(&smbus->smbus_rhandle);
        }

        if (smbus->smbus_configregaddr != NULL) {
                ddi_regs_map_free(&smbus->smbus_confighandle);
        }
}

/*
 * smbus_dip_to_addr() takes a dip and returns an I2C address.
 */
static int
smbus_dip_to_addr(dev_info_t *cdip)
{
        smbus_ppvt_t *ppvt;

        ppvt = ddi_get_parent_data(cdip);

        return (ppvt->smbus_ppvt_addr);
}

/*
 * smbus_suspend() is called before the system suspends.  Existing
 * transfer in progress or waiting will complete, but new transfers are
 * effectively blocked by "acquiring" the bus.
 */
static void
smbus_suspend(dev_info_t *dip)
{
        smbus_t *smbus;
        int instance;

        instance = ddi_get_instance(dip);
        smbus = ddi_get_soft_state(smbus_state, instance);

        (void) smbus_acquire(smbus, NULL, NULL);
}

/*
 * smbus_resume() is called when the system resumes from CPR.  It releases
 * the hold that was placed on the i2c bus, which allows any real
 * transfers to continue.
 */
static void
smbus_resume(dev_info_t *dip)
{
        smbus_t *smbus;
        int instance;

        instance = ddi_get_instance(dip);
        smbus = ddi_get_soft_state(smbus_state, instance);

        smbus_release(smbus);
}

/*
 * smbus_acquire() is called by a thread wishing to "own" the SMbus.
 * It should not be held across multiple transfers.
 */
static int
smbus_acquire(smbus_t *smbus, dev_info_t *dip, i2c_transfer_t *tp)
{
        mutex_enter(&smbus->smbus_mutex);
        while (smbus->smbus_busy) {
                cv_wait(&smbus->smbus_cv, &smbus->smbus_mutex);
        }
        smbus->smbus_busy = 1;
        mutex_exit(&smbus->smbus_mutex);

        /*
         * On systems where OBP shares a smbus controller with the
         * OS, plat_shared_i2c_enter will serialize access to the
         * smbus controller.  Do not grab this lock during CPR
         * suspend as the CPR thread also acquires this muxex
         * through through prom_setprop which causes recursive
         * mutex enter.
         *
         * dip == NULL during CPR.
         */
        if ((&plat_shared_i2c_enter != NULL) && (dip != NULL)) {
                plat_shared_i2c_enter(smbus->smbus_dip);
        }

        smbus->smbus_cur_tran = tp;
        smbus->smbus_cur_dip = dip;

        return (SMBUS_SUCCESS);
}

/*
 * smbus_release() is called to release a hold made by smbus_acquire().
 */
static void
smbus_release(smbus_t *smbus)
{
        mutex_enter(&smbus->smbus_mutex);
        smbus->smbus_busy = 0;
        cv_signal(&smbus->smbus_cv);
        smbus->smbus_cur_tran = NULL;
        smbus->smbus_cur_dip = NULL;
        mutex_exit(&smbus->smbus_mutex);

        if ((&plat_shared_i2c_exit != NULL) && (smbus->smbus_cur_dip != NULL)) {
                plat_shared_i2c_exit(smbus->smbus_dip);
        }
}

static void
smbus_put(smbus_t *smbus, uint8_t reg, uint8_t data, uint8_t flags)
{
        ddi_acc_handle_t hp = smbus->smbus_rhandle;
        uint8_t *reg_addr = smbus->smbus_regaddr;
        uint8_t *config_addr = smbus->smbus_configregaddr;
        ddi_acc_handle_t config_handle = smbus->smbus_confighandle;

        ddi_put8(hp, &reg_addr[reg], data);

        SMBUS_PRINT((PRT_PUT, "smbus_put:  addr = %p data = %x\n",
            &reg_addr[reg], data));

        /*
         * if FLUSH flag is passed, read a config regs to make sure
         * data written is flushed.
         */
        if (flags & SMBUS_FLUSH) {
                (void) ddi_get8(config_handle, &config_addr[0]);
        }
}

static uint8_t
smbus_get(smbus_t *smbus, uint8_t reg)
{

        ddi_acc_handle_t hp = smbus->smbus_rhandle;
        uint8_t *regaddr = smbus->smbus_regaddr;
        uint8_t data;

        data = ddi_get8(hp, &regaddr[reg]);

        SMBUS_PRINT((PRT_GET, "smbus_get: data = %x\n", data));

        return (data);
}


/*
 * The southbridge smbus device appears to have a feature where
 * reads from the status register return 0 for a few microseconds
 * after clearing the status.
 *
 * "status_wait_idle" allows for this by retrying until
 * it gets the right answer or times out.  The loop count
 * and the delay are empirical. The routine uses up
 * 400 us if it fails.
 *
 * The fact that this routine waits for 10 us before the
 * first check is deliberate.
 */
static int
smbus_wait_idle(smbus_t *smbus)
{
        int retries = 40;
        int status;

        smbus_put(smbus, SMB_STS, 0xff, SMBUS_FLUSH);
        do {
                drv_usecwait(10);
                status = smbus_get(smbus, SMB_STS);
        } while (status != IDLE && --retries > 0);
        return (status);
}
/*
 * smbus_transfer is the function that is registered with
 * I2C services to be called for each i2c transaction.
 */
int
smbus_transfer(dev_info_t *dip, i2c_transfer_t *tp)
{
        smbus_t *smbus;
        uint8_t status;
        clock_t ctime;

        smbus = ddi_get_soft_state(smbus_state,
            ddi_get_instance(ddi_get_parent(dip)));

        if (smbus_acquire(smbus, dip, tp) == SMBUS_FAILURE) {
                tp->i2c_result = I2C_FAILURE;

                return (I2C_FAILURE);
        }

        tp->i2c_r_resid = tp->i2c_rlen;
        tp->i2c_w_resid = tp->i2c_wlen;
        tp->i2c_result = I2C_SUCCESS;
        smbus->smbus_retries = 0;
        smbus->smbus_bytes_to_read = 0;

        mutex_enter(&smbus->smbus_imutex);

        SMBUS_PRINT((PRT_TRANS, "smbus_transfer: rlen=%d wlen=%d flags=%d",
            tp->i2c_r_resid, tp->i2c_w_resid, tp->i2c_flags));

        /*
         * First clear the status bits, then read them back to determine
         * the current state.
         */
        status = smbus_wait_idle(smbus);

        if (status != IDLE) {
                /*
                 * Try to issue bus reset
                 * First reset the state machine.
                 */
                smbus_put(smbus, SMB_TYP, KILL, SMBUS_FLUSH);
                status = smbus_wait_idle(smbus);

                if (status != IDLE) {

                        smbus_put(smbus, SMB_TYP, T_OUT, SMBUS_FLUSH);
                        status = smbus_wait_idle(smbus);
                        if (status != IDLE) {
                                cmn_err(CE_WARN,
                                    "%s smbus not idle.  Unable to reset %x",
                                    smbus->smbus_name, status);
                                smbus->smbus_cur_tran->i2c_result = I2C_FAILURE;
                                mutex_exit(&smbus->smbus_imutex);
                                smbus_release(smbus);

                                return (I2C_FAILURE);
                        } else {
                                cmn_err(CE_WARN, "%s T_OUT reset required",
                                    smbus->smbus_name);
                        }
                }
        }

        if (smbus_switch(smbus) != SMBUS_COMPLETE) {
                if (smbus->smbus_polling) {
                        smbus->smbus_poll_complete = 0;
                        smbus->smbus_poll_retries = 0;
                        do {
                                drv_usecwait(SMBUS_POLL_INTERVAL);
                                (void) smbus_intr_cmn(smbus, SMBUS_POLL);
                        } while (!smbus->smbus_poll_complete);
                } else {
                        /*
                         * Start a timeout as there is a bug in southbridge
                         * smbus where sometimes a transaction never starts,
                         * and needs to be reinitiated.
                         */

                        smbus->smbus_timeout = timeout(smbus_intr_timeout,
                            smbus, drv_usectohz(intr_timeout));
                        SMBUS_PRINT((PRT_TRANS,
                            "starting timeout in smbus_transfer %p",
                            smbus->smbus_timeout));

                        ctime = ddi_get_lbolt();
                        ctime += drv_usectohz(SMBUS_TRANS_TIMEOUT);

                        smbus_interrupts_on(smbus);


                        cv_wait(&smbus->smbus_icv, &smbus->smbus_imutex);
                }
        }


        mutex_exit(&smbus->smbus_imutex);
        smbus_release(smbus);

        return (tp->i2c_result);
}

/*
 * This is called by smbus_intr_cmn() to figure out whether to call
 * smbus_wr or smbus_rd depending on the command and current state.
 */
static int
smbus_switch(smbus_t *smbus)
{
        int ret;
        i2c_transfer_t *tp = smbus->smbus_cur_tran;

        if (tp == NULL) {
                cmn_err(CE_WARN,
                    "%s smbus_cur_tran is NULL. Transaction failed",
                    smbus->smbus_name);

                return (SMBUS_FAILURE);
        }

        smbus->smbus_saved_w_resid = tp->i2c_w_resid;

        switch (tp->i2c_flags) {
        case I2C_WR:
                ret = smbus_wr(smbus);
                break;
        case I2C_RD:
                ret = smbus_rd(smbus);
                break;
        case I2C_WR_RD:
                /*
                 * We could do a bit more decoding here,
                 * to allow the transactions that would
                 * work as a single smbus command to
                 * be done as such.  It's not really
                 * worth the trouble.
                 */
                if (tp->i2c_w_resid > 0) {
                        ret = smbus_wr(smbus);
                } else {
                        ret = smbus_rd(smbus);
                }
                break;
        default:
                tp->i2c_result = I2C_FAILURE;
                ret = SMBUS_COMPLETE;
                break;
        }

        return (ret);
}

/*
 *
 */
static void
smbus_intr_timeout(void *arg)
{
        smbus_t *smbus = (smbus_t *)arg;

        mutex_enter(&smbus->smbus_imutex);
        /*
         * If timeout is already cleared, it means interrupt arrived
         * while timeout fired.  In this case, just return from here.
         */
        if (smbus->smbus_timeout == 0) {

                mutex_exit(&smbus->smbus_imutex);

                return;
        }

        (void) smbus_intr_cmn(smbus, SMBUS_TIMEOUT);
        mutex_exit(&smbus->smbus_imutex);
}

/*
 * smbus_intr() is the interrupt handler for smbus.
 */
static uint_t
smbus_intr(caddr_t arg)
{
        smbus_t *smbus = (smbus_t *)arg;
        uint32_t intr_status;
        uint_t result;

        /*
         * Check to see if intr is really from smbus
         */
        intr_status = ddi_get32(smbus->smbus_confighandle,
            (uint32_t *)&smbus->smbus_configregaddr[SMBUS_SRC_STATUS]);


        if ((intr_status & SMBUS_SMB_INTR_STATUS) == 0) {
                SMBUS_PRINT((PRT_INTR, "smbus_intr: intr not from smbus\n"));

                return (DDI_INTR_UNCLAIMED);
        }

        mutex_enter(&smbus->smbus_imutex);

        /*
         * If timeout is already cleared, it means it arrived before the intr.
         * In that case, just return from here.
         */
        if (smbus->smbus_timeout == 0) {

                mutex_exit(&smbus->smbus_imutex);

                return (DDI_INTR_CLAIMED);
        }

        result = smbus_intr_cmn(smbus, SMBUS_INTR);
        mutex_exit(&smbus->smbus_imutex);
        return (result);
}

/*
 * smbus_intr() is the interrupt handler for smbus.
 */
static uint_t
smbus_intr_cmn(smbus_t *smbus, char *src)
{
        i2c_transfer_t *tp;
        char error_str[128];
        uint8_t status;
        int ret = SMBUS_SUCCESS;
        timeout_id_t timer_id;

        ASSERT(mutex_owned(&smbus->smbus_imutex));
        error_str[0] = '\0';

        smbus_interrupts_off(smbus);

        tp = smbus->smbus_cur_tran;
        /*
         * This only happens when top half is interrupted or
         * times out, then the interrupt arrives.  Interrupt
         * was already disabled by top half, so just exit.
         */
        if (tp == NULL) {
                return (DDI_INTR_CLAIMED);
        }

        /*
         * This wait is required before reading the status, otherwise
         * a parity error can occur which causes a panic.  A bug with
         * southbridge SMBUS.
         */
        drv_usecwait(15);
        status = smbus_get(smbus, SMB_STS);
        if (smbus->smbus_polling) {
                /*
                 * If we are polling, then we expect not to
                 * get the right answer for a while,
                 * so we don't go on to that error stuff
                 * until we've polled the status for a
                 * few times. We check for errors here to save time,
                 * otherwise we would have to wait for the full
                 * poll timeout before dealing with them.
                 */
                if (status != (CMD_CMPL|IDLE) &&
                    (status & (FAILED|BUS_ERR|DRV_ERR)) == 0 &&
                    smbus->smbus_poll_retries++ < SMBUS_POLL_MAX_RETRIES) {
                                return (DDI_INTR_CLAIMED);
                }
                /*
                 * else either ...
                 * [] the command has completed, or;
                 * [] There has been an error, or;
                 * [] we timed out waiting for something useful
                 * to happen, so we go on to  to the error handling bit that
                 * follows, * which will reset the controller then restart the
                 * whole transaction.
                 *
                 * In all cases, clear "poll_retries" for the next command or
                 * retry
                 */
                smbus->smbus_poll_retries = 0;
        }

        /*
         * A bug in southbridge SMBUS sometimes requires a reset.  Status
         * should NOT be IDLE without any other bit set.  If it is, the
         * transaction should be restarted.
         */

        if (status == IDLE) {
                (void) sprintf(error_str, "%s bus is idle, ", error_str);
        }

        if ((status & CMD_CMPL) == 0) {
                (void) sprintf(error_str, "%s command failed to complete, ",
                    error_str);
        }
        if (status & BUS_ERR) {
                (void) sprintf(error_str, "%s bus error, ", error_str);
        }
        if (status & FAILED) {
                (void) sprintf(error_str, "%s failed transaction, ", error_str);
        }
        if (status & DRV_ERR) {
                (void) sprintf(error_str, "%s timeout or bus reset", error_str);
        }

        if (error_str[0] != '\0') {
                (void) sprintf(error_str, "%s %s ", error_str, src);
        }

        /*
         * Clear status to clear the interrupt.
         */
        smbus_put(smbus, SMB_STS, 0xff, SMBUS_FLUSH);
        if (error_str[0] != '\0') {
                smbus_put(smbus, SMB_TYP, KILL, SMBUS_FLUSH);
                if (smbus->smbus_retries++ < SMBUS_MAX_RETRIES) {
                        /*
                         * XXXX There was a panic here when the
                         * intr timeout was greater than the timeout
                         * for the entire transfer.
                         *
                         * Restore the value of w_resid before the
                         * last transaction.  r_resid doesn't need to
                         * be restored because it is only decremented
                         * after a successful read.  Need to do this
                         * here since smbus_switch() keys off of a
                         * resid to know whether to call smbus_rd() or
                         * smbus_wr().
                         */
                        tp->i2c_w_resid = smbus->smbus_saved_w_resid;
                        smbus->smbus_bytes_to_read = 0;

                        SMBUS_PRINT((PRT_INTR_ERR,
                            "retrying: %s %s w_resid=%d\n", error_str,
                            src, tp->i2c_w_resid));
                } else {
                        cmn_err(CE_WARN, "%s max retries exceeded: %s",
                            smbus->smbus_name, error_str);
                        /*
                         * bailing, but first will reset the bus.
                         */
                        smbus_put(smbus, SMB_TYP, KILL, SMBUS_FLUSH);
                        smbus_put(smbus, SMB_STS, 0xff, SMBUS_FLUSH);
                        smbus->smbus_cur_tran->i2c_result = I2C_FAILURE;

                        ret = SMBUS_FAILURE;
                }
        } else {
                smbus->smbus_retries = 0;
        }

        if (tp != NULL) {
                SMBUS_PRINT((PRT_INTR, "flags=%d  wresid=%d r_resid=%d %s\n",
                    tp->i2c_flags, tp->i2c_w_resid, tp->i2c_r_resid, src));
        }

        if (ret != SMBUS_FAILURE) {
                ret = smbus_switch(smbus);
        }

        if (smbus->smbus_polling) {
                if (ret == SMBUS_COMPLETE || ret == SMBUS_FAILURE) {
                        smbus->smbus_poll_complete = 1;
                }
        } else {
                /*
                 * Disable previous timeout.  In case it was about to fire this
                 * will let it exit without doing anything.
                 */
                timer_id = smbus->smbus_timeout;
                smbus->smbus_timeout = 0;
                mutex_exit(&smbus->smbus_imutex);
                (void) untimeout(timer_id);
                mutex_enter(&smbus->smbus_imutex);
                if (ret == SMBUS_COMPLETE || ret == SMBUS_FAILURE) {
                        cv_signal(&smbus->smbus_icv);
                } else {
                        smbus_interrupts_on(smbus);
                        smbus->smbus_timeout = timeout(smbus_intr_timeout,
                            smbus, drv_usectohz(intr_timeout));
                        SMBUS_PRINT((PRT_INTR, "smbus_intr starting timeout %p "
                            "%s", smbus->smbus_timeout, src));
                }
        }

        return (DDI_INTR_CLAIMED);
}

/*
 * smbus_wr handles writes to the smbus.  Unlike true I2C busses
 * such as provided by pcf8584, smbus attaches a start and stop bit for each
 * transaction, so this limits writes to the maximum number of bytes
 * in a single transaction, which is 33.
 *
 * If more than 33 bytes are contained in the transfer, a non-zero
 * residual has to be returned, and the calling driver has to restart
 * another transaction to complete writing out any remaining data.  The
 * reason for this is that most devices require a register/offset as the
 * first byte to be written for each SMBUS transaction.
 */
static int
smbus_wr(smbus_t *smbus)
{
        i2c_transfer_t *tp = smbus->smbus_cur_tran;
        uint8_t addr = smbus_dip_to_addr(smbus->smbus_cur_dip);
        int bytes_written = 0;
        uint8_t a;
        uint8_t b;

        if (tp->i2c_w_resid != tp->i2c_wlen) {
                return (SMBUS_COMPLETE);
        }

        SMBUS_PRINT((PRT_WR, "smbus_wr:  addr = %x resid = %d\n",
            addr, tp->i2c_w_resid));

        smbus_put(smbus, SMB_STS, 0xff, 0);

        /*
         * Address must be re-written for each command and it has to
         * be written before SMB_TYP.
         */
        smbus_put(smbus, DEV_ADDR, addr, 0);

        switch (tp->i2c_w_resid) {

        case 1:
                a = tp->i2c_wbuf[tp->i2c_wlen - tp->i2c_w_resid--];
                smbus_put(smbus, SMB_CMD, a, 0);
                smbus_put(smbus, SMB_TYP, SEND_BYTE, 0);
                SMBUS_PRINT((PRT_WR, "smbus_wr: send one byte:"
                    " %d\n", a));
                break;
        case 2:
                a = tp->i2c_wbuf[tp->i2c_wlen - tp->i2c_w_resid--];
                smbus_put(smbus, SMB_CMD, a, 0);

                b = tp->i2c_wbuf[tp->i2c_wlen - tp->i2c_w_resid--];
                smbus_put(smbus, DEV_DATA0, b, 0);
                smbus_put(smbus, SMB_TYP, WR_BYTE, 0);
                SMBUS_PRINT((PRT_WR, "smbus_wr: send two bytes:"
                    " %d %d\n", a, b));
                break;

        default:
                /*
                 * Write out as many bytes as possible in a single command.
                 * Note that BLK_DATA just creats a byte stream.  ie, the
                 * smbus protocol is not used or interpreted by this driver.
                 */
                smbus_put(smbus, SMB_TYP, WR_BLK, 0);
                a = tp->i2c_wbuf[tp->i2c_wlen - tp->i2c_w_resid--];

                smbus_put(smbus, SMB_CMD, a, 0);

                SMBUS_PRINT((PRT_WR, "smbus_wr: send multiple bytes: "));
                SMBUS_PRINT((PRT_WR, "%x ", a));

                while (tp->i2c_w_resid != 0) {
                        a = tp->i2c_wbuf[tp->i2c_wlen - tp->i2c_w_resid--];
                        smbus_put(smbus, BLK_DATA, a, 0);
                        SMBUS_PRINT((PRT_WR, "%x ", a));
                        /*
                         * Note that MAX_BLK_SEND defines how many bytes may
                         * be sent to the BLK_DATA register. The leading byte
                         * already sent to the SMB_CMD register doesn't count
                         * But ALL the BLK_DATA bytes count so pre-increment
                         * bytes_written before testing.
                         */
                        if (++bytes_written == MAX_BLK_SEND) {
                                break;
                        }
                }
                SMBUS_PRINT((PRT_WR, "\n"));
                smbus_put(smbus, DEV_DATA0, bytes_written, 0);
                break;
        }

        /*
         * writing anything to port reg starts transfer
         */
        smbus_put(smbus, STR_PORT, 0, SMBUS_FLUSH);

        return (SMBUS_PENDING);
}

/*
 * smbus_rd handles reads to the smbus.  Unlike a true I2C bus
 * such as provided by pcf8584, smbus attaches a start and stop bit
 * for each transaction, which limits reads to the maximum number of
 * bytes in a single SMBUS transaction.  (Block reads don't
 * seem to work on smbus, and the southbridge documentation is poor).
 *
 * It doesn't appear that reads spanning multiple I2C transactions
 * (ie each with a start-stop) affects the transfer when reading
 * multiple bytes from devices with internal counters.  The counter
 * is correctly maintained.
 *
 * RD_WORD and RD_BYTE write out the byte in the SMB_CMD register
 * before reading, so RCV_BYTE is used instead.
 *
 * Multi-byte reads iniatiate a SMBUS transaction for each byte to be
 * received.  Because register/offset information doesn't need to
 * be resent for each I2C transaction (as opposed to when writing data),
 * the driver can continue reading data in separate SMBUS transactions
 * until the requested buffer is filled.
 */
static int
smbus_rd(smbus_t *smbus)
{
        i2c_transfer_t *tp = smbus->smbus_cur_tran;
        uint8_t addr = smbus_dip_to_addr(smbus->smbus_cur_dip);

        if (smbus->smbus_bytes_to_read == 1) {
                tp->i2c_rbuf[tp->i2c_rlen - tp->i2c_r_resid] =
                    smbus_get(smbus, DEV_DATA0);
                SMBUS_PRINT((PRT_RD, "smbus_rd: data in = %d\n",
                    tp->i2c_rbuf[tp->i2c_rlen - tp->i2c_r_resid]));
                tp->i2c_r_resid--;
                smbus->smbus_bytes_to_read = 0;

                if (tp->i2c_r_resid == 0) {
                        return (SMBUS_COMPLETE);
                }
        }

        /*
         * Address must be re-written for each command.  It must
         * be written before SMB_TYP.
         */
        smbus_put(smbus, DEV_ADDR, addr | I2C_READ, 0);

        if (tp->i2c_r_resid == 0) {
                smbus->smbus_bytes_to_read = 0;

                return (SMBUS_COMPLETE);
        }

        smbus->smbus_bytes_to_read = 1;
        smbus_put(smbus, SMB_TYP, RCV_BYTE, 0);

        smbus_put(smbus, SMB_STS, 0xff, 0);

        SMBUS_PRINT((PRT_RD, "smbus_rd: starting a read addr = %x resid = %d "
            "bytes_to_read=%d\n", addr, tp->i2c_r_resid,
            smbus->smbus_bytes_to_read));

        smbus_put(smbus, STR_PORT, 0, SMBUS_FLUSH);

        return (SMBUS_PENDING);
}