/*
 * 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.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

#include <sys/errno.h>
#include <sys/types.h>
#include <sys/conf.h>
#include <sys/kmem.h>
#include <sys/ddi.h>
#include <sys/stat.h>
#include <sys/sunddi.h>
#include <sys/file.h>
#include <sys/open.h>
#include <sys/modctl.h>
#include <sys/ddi_impldefs.h>
#include <sys/sysmacros.h>

#include <sys/ioat.h>

static int ioat_open(dev_t *devp, int flag, int otyp, cred_t *cred);
static int ioat_close(dev_t devp, int flag, int otyp, cred_t *cred);
static int ioat_attach(dev_info_t *devi, ddi_attach_cmd_t cmd);
static int ioat_detach(dev_info_t *devi, ddi_detach_cmd_t cmd);
static int ioat_getinfo(dev_info_t *dip, ddi_info_cmd_t cmd, void *arg,
    void **result);

static 	struct cb_ops ioat_cb_ops = {
	ioat_open,		/* cb_open */
	ioat_close,		/* cb_close */
	nodev,			/* cb_strategy */
	nodev,			/* cb_print */
	nodev,			/* cb_dump */
	nodev,			/* cb_read */
	nodev,			/* cb_write */
	ioat_ioctl,		/* cb_ioctl */
	nodev,			/* cb_devmap */
	nodev,			/* cb_mmap */
	nodev,			/* cb_segmap */
	nochpoll,		/* cb_chpoll */
	ddi_prop_op,		/* cb_prop_op */
	NULL,			/* cb_stream */
	D_NEW | D_MP | D_64BIT | D_DEVMAP,	/* cb_flag */
	CB_REV
};

static struct dev_ops ioat_dev_ops = {
	DEVO_REV,		/* devo_rev */
	0,			/* devo_refcnt */
	ioat_getinfo,		/* devo_getinfo */
	nulldev,		/* devo_identify */
	nulldev,		/* devo_probe */
	ioat_attach,		/* devo_attach */
	ioat_detach,		/* devo_detach */
	nodev,			/* devo_reset */
	&ioat_cb_ops,		/* devo_cb_ops */
	NULL,			/* devo_bus_ops */
	NULL			/* power */
};

static struct modldrv ioat_modldrv = {
	&mod_driverops,		/* Type of module.  This one is a driver */
	"ioat driver v%I%",	/* Name of the module. */
	&ioat_dev_ops,		/* driver ops */
};

static struct modlinkage ioat_modlinkage = {
	MODREV_1,
	(void *) &ioat_modldrv,
	NULL
};


void *ioat_statep;

static int ioat_chip_init(ioat_state_t *state);
static void ioat_chip_fini(ioat_state_t *state);
static int ioat_drv_init(ioat_state_t *state);
static void ioat_drv_fini(ioat_state_t *state);
static uint_t ioat_isr(caddr_t parm);
static void ioat_intr_enable(ioat_state_t *state);
static void ioat_intr_disable(ioat_state_t *state);
void ioat_detach_finish(ioat_state_t *state);


ddi_device_acc_attr_t ioat_acc_attr = {
	DDI_DEVICE_ATTR_V0,		/* devacc_attr_version */
	DDI_NEVERSWAP_ACC,		/* devacc_attr_endian_flags */
	DDI_STORECACHING_OK_ACC,	/* devacc_attr_dataorder */
	DDI_DEFAULT_ACC			/* devacc_attr_access */
};

/* dcopy callback interface */
dcopy_device_cb_t ioat_cb = {
	DCOPY_DEVICECB_V0,
	0,		/* reserved */
	ioat_channel_alloc,
	ioat_channel_free,
	ioat_cmd_alloc,
	ioat_cmd_free,
	ioat_cmd_post,
	ioat_cmd_poll,
	ioat_unregister_complete
};

/*
 * _init()
 */
int
_init(void)
{
	int e;

	e = ddi_soft_state_init(&ioat_statep, sizeof (ioat_state_t), 1);
	if (e != 0) {
		return (e);
	}

	e = mod_install(&ioat_modlinkage);
	if (e != 0) {
		ddi_soft_state_fini(&ioat_statep);
		return (e);
	}

	return (0);
}

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

/*
 * _fini()
 */
int
_fini(void)
{
	int e;

	e = mod_remove(&ioat_modlinkage);
	if (e != 0) {
		return (e);
	}

	ddi_soft_state_fini(&ioat_statep);

	return (0);
}

/*
 * ioat_attach()
 */
static int
ioat_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
	ioat_state_t *state;
	int instance;
	int e;


	switch (cmd) {
	case DDI_ATTACH:
		break;

	case DDI_RESUME:
		instance = ddi_get_instance(dip);
		state = ddi_get_soft_state(ioat_statep, instance);
		if (state == NULL) {
			return (DDI_FAILURE);
		}
		e = ioat_channel_resume(state);
		if (e != DDI_SUCCESS) {
			return (DDI_FAILURE);
		}
		ioat_intr_enable(state);
		return (DDI_SUCCESS);

	default:
		return (DDI_FAILURE);
	}

	instance = ddi_get_instance(dip);
	e = ddi_soft_state_zalloc(ioat_statep, instance);
	if (e != DDI_SUCCESS) {
		return (DDI_FAILURE);
	}
	state = ddi_get_soft_state(ioat_statep, instance);
	if (state == NULL) {
		goto attachfail_get_soft_state;
	}

	state->is_dip = dip;
	state->is_instance = instance;

	/* setup the registers, save away some device info */
	e = ioat_chip_init(state);
	if (e != DDI_SUCCESS) {
		goto attachfail_chip_init;
	}

	/* initialize driver state, must be after chip init */
	e = ioat_drv_init(state);
	if (e != DDI_SUCCESS) {
		goto attachfail_drv_init;
	}

	/* create the minor node (for the ioctl) */
	e = ddi_create_minor_node(dip, "ioat", S_IFCHR, instance, DDI_PSEUDO,
	    0);
	if (e != DDI_SUCCESS) {
		goto attachfail_minor_node;
	}

	/* Enable device interrupts */
	ioat_intr_enable(state);

	/* Report that driver was loaded */
	ddi_report_dev(dip);

	/* register with dcopy */
	e = dcopy_device_register(state, &state->is_deviceinfo,
	    &state->is_device_handle);
	if (e != DCOPY_SUCCESS) {
		goto attachfail_register;
	}

	return (DDI_SUCCESS);

attachfail_register:
	ioat_intr_disable(state);
	ddi_remove_minor_node(dip, NULL);
attachfail_minor_node:
	ioat_drv_fini(state);
attachfail_drv_init:
	ioat_chip_fini(state);
attachfail_chip_init:
attachfail_get_soft_state:
	(void) ddi_soft_state_free(ioat_statep, instance);

	return (DDI_FAILURE);
}

/*
 * ioat_detach()
 */
static int
ioat_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
	ioat_state_t *state;
	int instance;
	int e;


	instance = ddi_get_instance(dip);
	state = ddi_get_soft_state(ioat_statep, instance);
	if (state == NULL) {
		return (DDI_FAILURE);
	}

	switch (cmd) {
	case DDI_DETACH:
		break;

	case DDI_SUSPEND:
		ioat_channel_suspend(state);
		return (DDI_SUCCESS);

	default:
		return (DDI_FAILURE);
	}

	/*
	 * try to unregister from dcopy.  Since this driver doesn't follow the
	 * traditional parent/child model, we may still be in use so we can't
	 * detach yet.
	 */
	e = dcopy_device_unregister(&state->is_device_handle);
	if (e != DCOPY_SUCCESS) {
		if (e == DCOPY_PENDING) {
			cmn_err(CE_NOTE, "device busy, performing asynchronous"
			    " detach\n");
		}
		return (DDI_FAILURE);
	}

	ioat_detach_finish(state);

	return (DDI_SUCCESS);
}

/*
 * ioat_getinfo()
 */
/*ARGSUSED*/
static int
ioat_getinfo(dev_info_t *dip, ddi_info_cmd_t cmd, void *arg, void **result)
{
	ioat_state_t *state;
	int instance;
	dev_t dev;
	int e;


	dev = (dev_t)arg;
	instance = getminor(dev);

	switch (cmd) {
	case DDI_INFO_DEVT2DEVINFO:
		state = ddi_get_soft_state(ioat_statep, instance);
		if (state == NULL) {
			return (DDI_FAILURE);
		}
		*result = (void *)state->is_dip;
		e = DDI_SUCCESS;
		break;

	case DDI_INFO_DEVT2INSTANCE:
		*result = (void *)(uintptr_t)instance;
		e = DDI_SUCCESS;
		break;

	default:
		e = DDI_FAILURE;
		break;
	}

	return (e);
}


/*
 * ioat_open()
 */
/*ARGSUSED*/
static int
ioat_open(dev_t *devp, int flag, int otyp, cred_t *cred)
{
	ioat_state_t *state;
	int instance;

	instance = getminor(*devp);
	state = ddi_get_soft_state(ioat_statep, instance);
	if (state == NULL) {
		return (ENXIO);
	}

	return (0);
}


/*
 * ioat_close()
 */
/*ARGSUSED*/
static int
ioat_close(dev_t devp, int flag, int otyp, cred_t *cred)
{
	return (0);
}


/*
 * ioat_chip_init()
 */
static int
ioat_chip_init(ioat_state_t *state)
{
	ddi_device_acc_attr_t attr;
	int e;


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

	e =  ddi_regs_map_setup(state->is_dip, 1, (caddr_t *)&state->is_genregs,
	    0, 0, &attr, &state->is_reg_handle);
	if (e != DDI_SUCCESS) {
		goto chipinitfail_regsmap;
	}

	/* save away ioat chip info */
	state->is_num_channels = (uint_t)ddi_get8(state->is_reg_handle,
	    &state->is_genregs[IOAT_CHANCNT]);

	/*
	 * If we get a bogus value, something is wrong with the H/W, fail to
	 * attach.
	 */
	if (state->is_num_channels == 0) {
		goto chipinitfail_numchan;
	}

	state->is_maxxfer = (uint_t)ddi_get8(state->is_reg_handle,
	    &state->is_genregs[IOAT_XFERCAP]);
	state->is_chanoff = (uintptr_t)ddi_get16(state->is_reg_handle,
	    (uint16_t *)&state->is_genregs[IOAT_PERPORT_OFF]);
	state->is_cbver = (uint_t)ddi_get8(state->is_reg_handle,
	    &state->is_genregs[IOAT_CBVER]);
	state->is_intrdelay = (uint_t)ddi_get16(state->is_reg_handle,
	    (uint16_t *)&state->is_genregs[IOAT_INTRDELAY]);
	state->is_status = (uint_t)ddi_get16(state->is_reg_handle,
	    (uint16_t *)&state->is_genregs[IOAT_CSSTATUS]);
	state->is_capabilities = (uint_t)ddi_get32(state->is_reg_handle,
	    (uint32_t *)&state->is_genregs[IOAT_DMACAPABILITY]);

	if (state->is_cbver & 0x10) {
		state->is_ver = IOAT_CBv1;
	} else if (state->is_cbver & 0x20) {
		state->is_ver = IOAT_CBv2;
	} else {
		goto chipinitfail_version;
	}

	return (DDI_SUCCESS);

chipinitfail_version:
chipinitfail_numchan:
	ddi_regs_map_free(&state->is_reg_handle);
chipinitfail_regsmap:
	return (DDI_FAILURE);
}


/*
 * ioat_chip_fini()
 */
static void
ioat_chip_fini(ioat_state_t *state)
{
	ddi_regs_map_free(&state->is_reg_handle);
}


/*
 * ioat_drv_init()
 */
static int
ioat_drv_init(ioat_state_t *state)
{
	ddi_acc_handle_t handle;
	int e;


	mutex_init(&state->is_mutex, NULL, MUTEX_DRIVER, NULL);

	state->is_deviceinfo.di_dip = state->is_dip;
	state->is_deviceinfo.di_num_dma = state->is_num_channels;
	state->is_deviceinfo.di_maxxfer = state->is_maxxfer;
	state->is_deviceinfo.di_capabilities = state->is_capabilities;
	state->is_deviceinfo.di_cb = &ioat_cb;

	e = pci_config_setup(state->is_dip, &handle);
	if (e != DDI_SUCCESS) {
		goto drvinitfail_config_setup;
	}

	/* read in Vendor ID */
	state->is_deviceinfo.di_id = (uint64_t)pci_config_get16(handle, 0);
	state->is_deviceinfo.di_id = state->is_deviceinfo.di_id << 16;

	/* read in Device ID */
	state->is_deviceinfo.di_id |= (uint64_t)pci_config_get16(handle, 2);
	state->is_deviceinfo.di_id = state->is_deviceinfo.di_id << 32;

	/* Add in chipset version */
	state->is_deviceinfo.di_id |= (uint64_t)state->is_cbver;
	pci_config_teardown(&handle);

	e = ddi_intr_hilevel(state->is_dip, 0);
	if (e != 0) {
		cmn_err(CE_WARN, "hilevel interrupt not supported\n");
		goto drvinitfail_hilevel;
	}

	/* we don't support MSIs for v2 yet */
	e = ddi_add_intr(state->is_dip, 0, NULL, NULL, ioat_isr,
	    (caddr_t)state);
	if (e != DDI_SUCCESS) {
		goto drvinitfail_add_intr;
	}

	e = ddi_get_iblock_cookie(state->is_dip, 0, &state->is_iblock_cookie);
	if (e != DDI_SUCCESS) {
		goto drvinitfail_iblock_cookie;
	}

	e = ioat_channel_init(state);
	if (e != DDI_SUCCESS) {
		goto drvinitfail_channel_init;
	}

	return (DDI_SUCCESS);

drvinitfail_channel_init:
drvinitfail_iblock_cookie:
	ddi_remove_intr(state->is_dip, 0, state->is_iblock_cookie);
drvinitfail_add_intr:
drvinitfail_hilevel:
drvinitfail_config_setup:
	mutex_destroy(&state->is_mutex);

	return (DDI_FAILURE);
}


/*
 * ioat_drv_fini()
 */
static void
ioat_drv_fini(ioat_state_t *state)
{
	ioat_channel_fini(state);
	ddi_remove_intr(state->is_dip, 0, state->is_iblock_cookie);
	mutex_destroy(&state->is_mutex);
}


/*
 * ioat_unregister_complete()
 */
void
ioat_unregister_complete(void *device_private, int status)
{
	ioat_state_t *state;


	state = device_private;

	if (status != DCOPY_SUCCESS) {
		cmn_err(CE_WARN, "asynchronous detach aborted\n");
		return;
	}

	cmn_err(CE_CONT, "detach completing\n");
	ioat_detach_finish(state);
}


/*
 * ioat_detach_finish()
 */
void
ioat_detach_finish(ioat_state_t *state)
{
	ioat_intr_disable(state);
	ddi_remove_minor_node(state->is_dip, NULL);
	ioat_drv_fini(state);
	ioat_chip_fini(state);
	(void) ddi_soft_state_free(ioat_statep, state->is_instance);
}


/*
 * ioat_intr_enable()
 */
static void
ioat_intr_enable(ioat_state_t *state)
{
	uint32_t intr_status;


	/* Clear any pending interrupts */
	intr_status = ddi_get32(state->is_reg_handle,
	    (uint32_t *)&state->is_genregs[IOAT_ATTNSTATUS]);
	if (intr_status != 0) {
		ddi_put32(state->is_reg_handle,
		    (uint32_t *)&state->is_genregs[IOAT_ATTNSTATUS],
		    intr_status);
	}

	/* Enable interrupts on the device */
	ddi_put8(state->is_reg_handle, &state->is_genregs[IOAT_INTRCTL],
	    IOAT_INTRCTL_MASTER_EN);
}


/*
 * ioat_intr_disable()
 */
static void
ioat_intr_disable(ioat_state_t *state)
{
	/*
	 * disable interrupts on the device. A read of the interrupt control
	 * register clears the enable bit.
	 */
	(void) ddi_get8(state->is_reg_handle,
	    &state->is_genregs[IOAT_INTRCTL]);
}


/*
 * ioat_isr()
 */
static uint_t
ioat_isr(caddr_t parm)
{
	uint32_t intr_status;
	ioat_state_t *state;
	uint8_t intrctrl;
	uint32_t chan;
	uint_t r;
	int i;

	state = (ioat_state_t *)parm;

	intrctrl = ddi_get8(state->is_reg_handle,
	    &state->is_genregs[IOAT_INTRCTL]);
	/* master interrupt enable should always be set */
	ASSERT(intrctrl & IOAT_INTRCTL_MASTER_EN);

	/* If the interrupt status bit isn't set, it's not ours */
	if (!(intrctrl & IOAT_INTRCTL_INTR_STAT)) {
		/* re-set master interrupt enable (since it clears on read) */
		ddi_put8(state->is_reg_handle,
		    &state->is_genregs[IOAT_INTRCTL], intrctrl);
		return (DDI_INTR_UNCLAIMED);
	}

	/* see which channels generated the interrupt */
	intr_status = ddi_get32(state->is_reg_handle,
	    (uint32_t *)&state->is_genregs[IOAT_ATTNSTATUS]);

	/* call the intr handler for the channels */
	r = DDI_INTR_UNCLAIMED;
	chan = 1;
	for (i = 0; i < state->is_num_channels; i++) {
		if (intr_status & chan) {
			ioat_channel_intr(&state->is_channel[i]);
			r = DDI_INTR_CLAIMED;
		}
		chan = chan << 1;
	}

	/*
	 * if interrupt status bit was set, there should have been an
	 * attention status bit set too.
	 */
	ASSERT(r == DDI_INTR_CLAIMED);

	/* re-set master interrupt enable (since it clears on read) */
	ddi_put8(state->is_reg_handle, &state->is_genregs[IOAT_INTRCTL],
	    intrctrl);

	return (r);
}