/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (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 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

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

/*
 * Autovectored Interrupt Configuration and Deconfiguration
 */

#include <sys/param.h>
#include <sys/cmn_err.h>
#include <sys/trap.h>
#include <sys/t_lock.h>
#include <sys/avintr.h>
#include <sys/kmem.h>
#include <sys/machlock.h>
#include <sys/systm.h>
#include <sys/machsystm.h>
#include <sys/sunddi.h>
#include <sys/x_call.h>
#include <sys/cpuvar.h>
#include <sys/atomic.h>
#include <sys/smp_impldefs.h>
#include <sys/sdt.h>
#include <sys/stack.h>
#include <sys/ddi_impldefs.h>

typedef struct av_softinfo {
	cpuset_t	av_pending;	/* pending bitmasks */
} av_softinfo_t;

static void insert_av(void *intr_id, struct av_head *vectp, avfunc f,
	caddr_t arg1, caddr_t arg2, uint64_t *ticksp, int pri_level,
	dev_info_t *dip);
static void remove_av(void *intr_id, struct av_head *vectp, avfunc f,
	int pri_level, int vect);

/*
 * Arrange for a driver to be called when a particular
 * auto-vectored interrupt occurs.
 * NOTE: if a device can generate interrupts on more than
 * one level, or if a driver services devices that interrupt
 * on more than one level, then the driver should install
 * itself on each of those levels.
 */
static char badsoft[] =
	"add_avintr: bad soft interrupt level %d for driver '%s'\n";
static char multilevel[] =
	"!IRQ%d is being shared by drivers with different interrupt levels.\n"
	"This may result in reduced system performance.";
static char multilevel2[] =
	"Cannot register interrupt for '%s' device at IPL %d because it\n"
	"conflicts with another device using the same vector %d with an IPL\n"
	"of %d. Reconfigure the conflicting devices to use different vectors.";

#define	MAX_VECT	256
struct autovec *nmivect = NULL;
struct av_head autovect[MAX_VECT];
struct av_head softvect[LOCK_LEVEL + 1];
kmutex_t av_lock;
ddi_softint_hdl_impl_t softlevel1_hdl =
	{0, NULL, NULL, NULL, 0, NULL, NULL, NULL};


/*
 * clear/check softint pending flag corresponding for
 * the current CPU
 */
void
av_clear_softint_pending(av_softinfo_t *infop)
{
	CPUSET_ATOMIC_DEL(infop->av_pending, CPU->cpu_seqid);
}

boolean_t
av_check_softint_pending(av_softinfo_t *infop, boolean_t check_all)
{
	if (check_all)
		return (!CPUSET_ISNULL(infop->av_pending));
	else
		return (CPU_IN_SET(infop->av_pending, CPU->cpu_seqid) != 0);
}

/*
 * It first sets our av softint pending bit for the current CPU,
 * then it sets the CPU softint pending bit for pri.
 */
void
av_set_softint_pending(int pri, av_softinfo_t *infop)
{
	CPUSET_ATOMIC_ADD(infop->av_pending, CPU->cpu_seqid);

	atomic_or_32((uint32_t *)&CPU->cpu_softinfo.st_pending, 1 << pri);
}

/*
 * register nmi interrupt routine. The first arg is used only to order
 * various nmi interrupt service routines in the chain. Higher lvls will
 * be called first
 */
int
add_nmintr(int lvl, avfunc nmintr, char *name, caddr_t arg)
{
	struct autovec  *mem;
	struct autovec *p, *prev = NULL;

	if (nmintr == NULL) {
		printf("Attempt to add null vect for %s on nmi\n", name);
		return (0);

	}

	mem = kmem_zalloc(sizeof (struct autovec), KM_SLEEP);
	mem->av_vector = nmintr;
	mem->av_intarg1 = arg;
	mem->av_intarg2 = NULL;
	mem->av_intr_id = NULL;
	mem->av_prilevel = lvl;
	mem->av_dip = NULL;
	mem->av_link = NULL;

	mutex_enter(&av_lock);

	if (!nmivect) {
		nmivect = mem;
		mutex_exit(&av_lock);
		return (1);
	}
	/* find where it goes in list */
	for (p = nmivect; p != NULL; p = p->av_link) {
		if (p->av_vector == nmintr && p->av_intarg1 == arg) {
			/*
			 * already in list
			 * So? Somebody added the same interrupt twice.
			 */
			cmn_err(CE_WARN, "Driver already registered '%s'",
			    name);
			kmem_free(mem, sizeof (struct autovec));
			mutex_exit(&av_lock);
			return (0);
		}
		if (p->av_prilevel < lvl) {
			if (p == nmivect) {   /* it's at head of list */
				mem->av_link = p;
				nmivect = mem;
			} else {
				mem->av_link = p;
				prev->av_link = mem;
			}
			mutex_exit(&av_lock);
			return (1);
		}
		prev = p;

	}
	/* didn't find it, add it to the end */
	prev->av_link = mem;
	mutex_exit(&av_lock);
	return (1);

}

/*
 * register a hardware interrupt handler.
 */
int
add_avintr(void *intr_id, int lvl, avfunc xxintr, char *name, int vect,
    caddr_t arg1, caddr_t arg2, uint64_t *ticksp, dev_info_t *dip)
{
	struct av_head *vecp = (struct av_head *)0;
	avfunc f;
	int s, vectindex;			/* save old spl value */
	ushort_t hi_pri;

	if ((f = xxintr) == NULL) {
		printf("Attempt to add null vect for %s on vector %d\n",
			name, vect);
		return (0);

	}
	vectindex = vect % MAX_VECT;

	vecp = &autovect[vectindex];

	/*
	 * "hi_pri == 0" implies all entries on list are "unused",
	 * which means that it's OK to just insert this one.
	 */
	hi_pri = vecp->avh_hi_pri;
	if (vecp->avh_link && (hi_pri != 0)) {
		if (((hi_pri > LOCK_LEVEL) && (lvl < LOCK_LEVEL)) ||
		    ((hi_pri < LOCK_LEVEL) && (lvl > LOCK_LEVEL))) {
			cmn_err(CE_WARN, multilevel2, name, lvl, vect,
				hi_pri);
			return (0);
		}
		if ((vecp->avh_lo_pri != lvl) || (hi_pri != lvl))
			cmn_err(CE_NOTE, multilevel, vect);
	}

	insert_av(intr_id, vecp, f, arg1, arg2, ticksp, lvl, dip);
	s = splhi();
	/*
	 * do what ever machine specific things are necessary
	 * to set priority level (e.g. set picmasks)
	 */
	mutex_enter(&av_lock);
	(*addspl)(vect, lvl, vecp->avh_lo_pri, vecp->avh_hi_pri);
	mutex_exit(&av_lock);
	splx(s);
	return (1);

}

void
update_avsoftintr_args(void *intr_id, int lvl, caddr_t arg2)
{
	struct autovec *p;
	struct autovec *target = NULL;
	struct av_head *vectp = (struct av_head *)&softvect[lvl];

	for (p = vectp->avh_link; p && p->av_vector; p = p->av_link) {
		if (p->av_intr_id == intr_id) {
			target = p;
			break;
		}
	}

	if (target == NULL)
		return;
	target->av_intarg2 = arg2;
}

/*
 * Register a software interrupt handler
 */
int
add_avsoftintr(void *intr_id, int lvl, avfunc xxintr, char *name,
    caddr_t arg1, caddr_t arg2)
{
	int slvl;
	ddi_softint_hdl_impl_t	*hdlp = (ddi_softint_hdl_impl_t *)intr_id;

	if ((slvl = slvltovect(lvl)) != -1)
		return (add_avintr(intr_id, lvl, xxintr,
		    name, slvl, arg1, arg2, NULL, NULL));

	if (intr_id == NULL) {
		printf("Attempt to add null intr_id for %s on level %d\n",
		    name, lvl);
		return (0);
	}

	if (xxintr == NULL) {
		printf("Attempt to add null handler for %s on level %d\n",
		    name, lvl);
		return (0);
	}

	if (lvl <= 0 || lvl > LOCK_LEVEL) {
		printf(badsoft, lvl, name);
		return (0);
	}

	if (hdlp->ih_pending == NULL) {
		hdlp->ih_pending =
			kmem_zalloc(sizeof (av_softinfo_t), KM_SLEEP);
	}

	insert_av(intr_id, &softvect[lvl], xxintr, arg1, arg2, NULL, lvl, NULL);

	return (1);
}

/* insert an interrupt vector into chain */
static void
insert_av(void *intr_id, struct av_head *vectp, avfunc f, caddr_t arg1,
    caddr_t arg2, uint64_t *ticksp, int pri_level, dev_info_t *dip)
{
	/*
	 * Protect rewrites of the list
	 */
	struct autovec *p, *mem;

	mem = kmem_zalloc(sizeof (struct autovec), KM_SLEEP);
	mem->av_vector = f;
	mem->av_intarg1 = arg1;
	mem->av_intarg2 = arg2;
	mem->av_ticksp = ticksp;
	mem->av_intr_id = intr_id;
	mem->av_prilevel = pri_level;
	mem->av_dip = dip;
	mem->av_link = NULL;

	mutex_enter(&av_lock);

	if (vectp->avh_link == NULL) {	/* Nothing on list - put it at head */
		vectp->avh_link = mem;
		vectp->avh_hi_pri = vectp->avh_lo_pri = (ushort_t)pri_level;

		mutex_exit(&av_lock);
		return;
	}

	/* find where it goes in list */
	for (p = vectp->avh_link; p != NULL; p = p->av_link) {
		if (p->av_vector == NULL) {	/* freed struct available */
			kmem_free(mem, sizeof (struct autovec));
			p->av_intarg1 = arg1;
			p->av_intarg2 = arg2;
			p->av_ticksp = ticksp;
			p->av_intr_id = intr_id;
			p->av_prilevel = pri_level;
			p->av_dip = dip;
			if (pri_level > (int)vectp->avh_hi_pri) {
				vectp->avh_hi_pri = (ushort_t)pri_level;
			}
			if (pri_level < (int)vectp->avh_lo_pri) {
				vectp->avh_lo_pri = (ushort_t)pri_level;
			}
			p->av_vector = f;
			mutex_exit(&av_lock);
			return;
		}
	}
	/* insert new intpt at beginning of chain */
	mem->av_link = vectp->avh_link;
	vectp->avh_link = mem;
	if (pri_level > (int)vectp->avh_hi_pri) {
		vectp->avh_hi_pri = (ushort_t)pri_level;
	}
	if (pri_level < (int)vectp->avh_lo_pri) {
		vectp->avh_lo_pri = (ushort_t)pri_level;
	}
	mutex_exit(&av_lock);
}

static int
av_rem_softintr(void *intr_id, int lvl, avfunc xxintr, boolean_t rem_softinfo)
{
	struct av_head *vecp = (struct av_head *)0;
	int slvl;
	ddi_softint_hdl_impl_t	*hdlp = (ddi_softint_hdl_impl_t *)intr_id;
	av_softinfo_t *infop = (av_softinfo_t *)hdlp->ih_pending;

	if (xxintr == NULL)
		return (0);

	if ((slvl = slvltovect(lvl)) != -1) {
		rem_avintr(intr_id, lvl, xxintr, slvl);
		return (1);
	}

	if (lvl <= 0 && lvl >= LOCK_LEVEL) {
		return (0);
	}
	vecp = &softvect[lvl];
	remove_av(intr_id, vecp, xxintr, lvl, 0);

	if (rem_softinfo) {
		kmem_free(infop, sizeof (av_softinfo_t));
		hdlp->ih_pending = NULL;
	}

	return (1);
}

int
av_softint_movepri(void *intr_id, int old_lvl)
{
	int ret;
	ddi_softint_hdl_impl_t	*hdlp = (ddi_softint_hdl_impl_t *)intr_id;

	ret = add_avsoftintr(intr_id, hdlp->ih_pri, hdlp->ih_cb_func,
	    DEVI(hdlp->ih_dip)->devi_name, hdlp->ih_cb_arg1, hdlp->ih_cb_arg2);

	if (ret) {
		(void) av_rem_softintr(intr_id, old_lvl, hdlp->ih_cb_func,
		    B_FALSE);
	}

	return (ret);
}

/*
 * Remove a driver from the autovector list.
 */
int
rem_avsoftintr(void *intr_id, int lvl, avfunc xxintr)
{
	return (av_rem_softintr(intr_id, lvl, xxintr, B_TRUE));
}

void
rem_avintr(void *intr_id, int lvl, avfunc xxintr, int vect)
{
	struct av_head *vecp = (struct av_head *)0;
	avfunc f;
	int s, vectindex;			/* save old spl value */

	if ((f = xxintr) == NULL)
		return;

	vectindex = vect % MAX_VECT;
	vecp = &autovect[vectindex];
	remove_av(intr_id, vecp, f, lvl, vect);
	s = splhi();
	mutex_enter(&av_lock);
	(*delspl)(vect, lvl, vecp->avh_lo_pri, vecp->avh_hi_pri);
	mutex_exit(&av_lock);
	splx(s);
}


/*
 * After having made a change to an autovector list, wait until we have
 * seen each cpu not executing an interrupt at that level--so we know our
 * change has taken effect completely (no old state in registers, etc).
 */
void
wait_till_seen(int ipl)
{
	int cpu_in_chain, cix;
	struct cpu *cpup;
	cpuset_t cpus_to_check;

	CPUSET_ALL(cpus_to_check);
	do {
		cpu_in_chain = 0;
		for (cix = 0; cix < NCPU; cix++) {
			cpup = cpu[cix];
			if (cpup != NULL && CPU_IN_SET(cpus_to_check, cix)) {
				if (intr_active(cpup, ipl)) {
					cpu_in_chain = 1;
				} else {
					CPUSET_DEL(cpus_to_check, cix);
				}
			}
		}
	} while (cpu_in_chain);
}

/* remove an interrupt vector from the chain */
static void
remove_av(void *intr_id, struct av_head *vectp, avfunc f, int pri_level,
	int vect)
{
	struct autovec *endp, *p, *target;
	int	lo_pri, hi_pri;
	int	ipl;
	/*
	 * Protect rewrites of the list
	 */
	target = NULL;

	mutex_enter(&av_lock);
	ipl = pri_level;
	lo_pri = MAXIPL;
	hi_pri = 0;
	for (endp = p = vectp->avh_link; p && p->av_vector; p = p->av_link) {
		endp = p;
		if ((p->av_vector == f) && (p->av_intr_id == intr_id)) {
			/* found the handler */
			target = p;
			continue;
		}
		if (p->av_prilevel > hi_pri)
			hi_pri = p->av_prilevel;
		if (p->av_prilevel < lo_pri)
			lo_pri = p->av_prilevel;
	}
	if (ipl < hi_pri)
		ipl = hi_pri;
	if (target == NULL) {	/* not found */
		printf("Couldn't remove function %p at %d, %d\n",
			(void *)f, vect, pri_level);
		mutex_exit(&av_lock);
		return;
	}

	target->av_vector = NULL;
	target->av_ticksp = NULL;
	wait_till_seen(ipl);
	if (endp != target) {	/* vector to be removed is not last in chain */
		target->av_vector = endp->av_vector;
		target->av_intarg1 = endp->av_intarg1;
		target->av_intarg2 = endp->av_intarg2;
		target->av_ticksp = endp->av_ticksp;
		target->av_intr_id = endp->av_intr_id;
		target->av_prilevel = endp->av_prilevel;
		target->av_dip = endp->av_dip;
		/*
		 * We have a hole here where the routine corresponding to
		 * endp may not get called. Do a wait_till_seen to take care
		 * of this.
		 */
		wait_till_seen(ipl);
		endp->av_vector = NULL;
		endp->av_ticksp = NULL;
	}

	if (lo_pri > hi_pri) {	/* the chain is now empty */
		/* Leave the unused entries here for probable future use */
		vectp->avh_lo_pri = MAXIPL;
		vectp->avh_hi_pri = 0;
	} else {
		if ((int)vectp->avh_lo_pri < lo_pri)
			vectp->avh_lo_pri = (ushort_t)lo_pri;
		if ((int)vectp->avh_hi_pri > hi_pri)
			vectp->avh_hi_pri = (ushort_t)hi_pri;
	}
	mutex_exit(&av_lock);
	wait_till_seen(ipl);
}

/*
 * Trigger a soft interrupt.
 */
void
siron(void)
{
	(*setsoftint)(1, softlevel1_hdl.ih_pending);
}

/*
 * Walk the autovector table for this vector, invoking each
 * interrupt handler as we go.
 */

extern uint64_t intr_get_time(void);

void
av_dispatch_autovect(uint_t vec)
{
	struct autovec *av;

	ASSERT_STACK_ALIGNED();

	while ((av = autovect[vec].avh_link) != NULL) {
		uint_t numcalled = 0;
		uint_t claimed = 0;

		for (; av; av = av->av_link) {
			uint_t r;
			uint_t (*intr)() = av->av_vector;
			caddr_t arg1 = av->av_intarg1;
			caddr_t arg2 = av->av_intarg2;
			dev_info_t *dip = av->av_dip;

			numcalled++;
			if (intr == NULL)
				break;

			DTRACE_PROBE4(interrupt__start, dev_info_t *, dip,
			    void *, intr, caddr_t, arg1, caddr_t, arg2);
			r = (*intr)(arg1, arg2);
			DTRACE_PROBE4(interrupt__complete, dev_info_t *, dip,
			    void *, intr, caddr_t, arg1, uint_t, r);
			claimed |= r;
			if (av->av_ticksp && av->av_prilevel <= LOCK_LEVEL)
				atomic_add_64(av->av_ticksp, intr_get_time());
		}

		/*
		 * If there's only one interrupt handler in the chain,
		 * or if no-one claimed the interrupt at all give up now.
		 */
		if (numcalled == 1 || claimed == 0)
			break;
	}
}

/*
 * Call every soft interrupt handler we can find at this level once.
 */
void
av_dispatch_softvect(uint_t pil)
{
	struct autovec *av;
	ddi_softint_hdl_impl_t	*hdlp;
	uint_t (*intr)();
	caddr_t arg1;
	caddr_t arg2;

	ASSERT_STACK_ALIGNED();
	ASSERT(pil >= 0 && pil <= PIL_MAX);

	for (av = softvect[pil].avh_link; av; av = av->av_link) {
		if ((intr = av->av_vector) == NULL)
			break;
		arg1 = av->av_intarg1;
		arg2 = av->av_intarg2;

		hdlp = (ddi_softint_hdl_impl_t *)av->av_intr_id;
		ASSERT(hdlp);

		/*
		 * Each cpu has its own pending bit in hdlp->ih_pending,
		 * here av_check/clear_softint_pending is just checking
		 * and clearing the pending bit for the current cpu, who
		 * has just triggered a softint.
		 */
		if (av_check_softint_pending(hdlp->ih_pending, B_FALSE)) {
			av_clear_softint_pending(hdlp->ih_pending);
			(void) (*intr)(arg1, arg2);
		}
	}
}

struct regs;

/*
 * Call every NMI handler we know of once.
 */
void
av_dispatch_nmivect(struct regs *rp)
{
	struct autovec *av;

	ASSERT_STACK_ALIGNED();

	for (av = nmivect; av; av = av->av_link)
		(void) (av->av_vector)(av->av_intarg1, rp);
}