/*	$OpenBSD: elan520.c,v 1.21 2014/12/10 12:27:56 mikeb Exp $	*/
/*	$NetBSD: elan520.c,v 1.4 2002/10/02 05:47:15 thorpej Exp $	*/

/*-
 * Copyright (c) 2002 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Jason R. Thorpe.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * Device driver for the AMD Elan SC520 System Controller.  This attaches
 * where the "pchb" driver might normally attach, and provides support for
 * extra features on the SC520, such as the watchdog timer and GPIO.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/gpio.h>
#include <sys/sysctl.h>
#include <sys/time.h>
#include <sys/timetc.h>

#include <machine/bus.h>

#include <machine/cpufunc.h>

#include <dev/pci/pcivar.h>
#include <dev/pci/pcidevs.h>

#include <dev/gpio/gpiovar.h>

#include <arch/i386/pci/elan520reg.h>

struct elansc_softc {
	struct device		sc_dev;
	bus_space_tag_t		sc_memt;
	bus_space_handle_t	sc_memh;

	/* GPIO interface */
	struct gpio_chipset_tag sc_gpio_gc;
	gpio_pin_t sc_gpio_pins[ELANSC_PIO_NPINS];

	/* GP timer */
	struct timecounter	sc_tc;
} *elansc;

int	elansc_match(struct device *, void *, void *);
void	elansc_attach(struct device *, struct device *, void *);
int	elansc_activate(struct device *, int);
void	elansc_update_cpuspeed(void);
void	elansc_setperf(int);
int	elansc_cpuspeed(int *);

void	elansc_wdogctl(struct elansc_softc *, int, uint16_t);
#define elansc_wdogctl_reset(sc)	elansc_wdogctl(sc, 1, 0)
#define elansc_wdogctl_write(sc, val)	elansc_wdogctl(sc, 0, val)
int	elansc_wdogctl_cb(void *, int);

int	elansc_gpio_pin_read(void *, int);
void	elansc_gpio_pin_write(void *, int, int);
void	elansc_gpio_pin_ctl(void *, int, int);

u_int	elansc_tc_read(struct timecounter *);

struct cfattach elansc_ca = {
	sizeof(struct elansc_softc), elansc_match, elansc_attach,
	NULL, elansc_activate
};

struct cfdriver elansc_cd = {
	NULL, "elansc", DV_DULL
};

int
elansc_match(struct device *parent, void *match, void *aux)
{
	struct pci_attach_args *pa = aux;

	if (PCI_VENDOR(pa->pa_id) == PCI_VENDOR_AMD &&
	    PCI_PRODUCT(pa->pa_id) == PCI_PRODUCT_AMD_ELANSC520)
		return (10);	/* beat pchb */

	return (0);
}

static const char *elansc_speeds[] = {
	"(reserved 00)",
	"100MHz",
	"133MHz",
	"(reserved 11)",
};

#define RSTBITS "\20\x07SCP\x06HRST\x05SRST\x04WDT\x03SD\x02PRGRST\x01PWRGOOD"

void
elansc_attach(struct device *parent, struct device *self, void *aux)
{
	struct elansc_softc *sc = (void *) self;
	struct pci_attach_args *pa = aux;
	struct gpiobus_attach_args gba;
	struct timecounter *tc;
	uint16_t rev, data;
	uint8_t ressta, cpuctl, tmr;
	int pin, reg, shift;

	sc->sc_memt = pa->pa_memt;
	if (bus_space_map(sc->sc_memt, MMCR_BASE_ADDR, NBPG, 0,
	    &sc->sc_memh) != 0) {
		printf(": unable to map registers\n");
		return;
	}

	rev = bus_space_read_2(sc->sc_memt, sc->sc_memh, MMCR_REVID);
	cpuctl = bus_space_read_1(sc->sc_memt, sc->sc_memh, MMCR_CPUCTL);
	ressta = bus_space_read_1(sc->sc_memt, sc->sc_memh, MMCR_RESSTA);

	printf(": product %d stepping %d.%d, CPU clock %s, reset %b\n",
	    (rev & REVID_PRODID) >> REVID_PRODID_SHIFT,
	    (rev & REVID_MAJSTEP) >> REVID_MAJSTEP_SHIFT,
	    (rev & REVID_MINSTEP),
	    elansc_speeds[cpuctl & CPUCTL_CPU_CLK_SPD_MASK],
	    ressta, RSTBITS);

	/*
	 * Determine cause of the last reset, and issue a warning if it
	 * was due to watchdog expiry.
	 */
	if (ressta & RESSTA_WDT_RST_DET)
		printf("%s: WARNING: LAST RESET DUE TO WATCHDOG EXPIRATION!\n",
		    sc->sc_dev.dv_xname);
	bus_space_write_1(sc->sc_memt, sc->sc_memh, MMCR_RESSTA, ressta);

	/* Set up the watchdog registers with some defaults. */
	elansc_wdogctl_write(sc, WDTMRCTL_WRST_ENB | WDTMRCTL_EXP_SEL30);

	/* ...and clear it. */
	elansc_wdogctl_reset(sc);

	wdog_register(elansc_wdogctl_cb, sc);
	elansc = sc;
	cpu_setperf = elansc_setperf;
	cpu_cpuspeed = elansc_cpuspeed;
	elansc_update_cpuspeed();

	/* Initialize GPIO pins array */
	for (pin = 0; pin < ELANSC_PIO_NPINS; pin++) {
		sc->sc_gpio_pins[pin].pin_num = pin;
		sc->sc_gpio_pins[pin].pin_caps = GPIO_PIN_INPUT |
		    GPIO_PIN_OUTPUT;

		/* Read initial state */
		reg = (pin < 16 ? MMCR_PIODIR15_0 : MMCR_PIODIR31_16);
		shift = pin % 16;
		data = bus_space_read_2(sc->sc_memt, sc->sc_memh, reg);
		if ((data & (1 << shift)) == 0)
			sc->sc_gpio_pins[pin].pin_flags = GPIO_PIN_INPUT;
		else
			sc->sc_gpio_pins[pin].pin_flags = GPIO_PIN_OUTPUT;
		if (elansc_gpio_pin_read(sc, pin) == 0)
			sc->sc_gpio_pins[pin].pin_state = GPIO_PIN_LOW;
		else
			sc->sc_gpio_pins[pin].pin_state = GPIO_PIN_HIGH;
	}

	/* Create controller tag */
	sc->sc_gpio_gc.gp_cookie = sc;
	sc->sc_gpio_gc.gp_pin_read = elansc_gpio_pin_read;
	sc->sc_gpio_gc.gp_pin_write = elansc_gpio_pin_write;
	sc->sc_gpio_gc.gp_pin_ctl = elansc_gpio_pin_ctl;

	gba.gba_name = "gpio";
	gba.gba_gc = &sc->sc_gpio_gc;
	gba.gba_pins = sc->sc_gpio_pins;
	gba.gba_npins = ELANSC_PIO_NPINS;

	/* Attach GPIO framework */
	config_found(&sc->sc_dev, &gba, gpiobus_print);

	/* Disable GP1/2, clear the current count, and set the period to max */
	bus_space_write_2(sc->sc_memt, sc->sc_memh, GPTMR1CTL,
		GPTMRCTL_ENB_WR | GPTMRCTL_CONT_CMP |
		GPTMRCTL_PSC_SEL | GPTMRCTL_RTG);
	bus_space_write_2(sc->sc_memt, sc->sc_memh, GPTMR1CNT, 0);
	bus_space_write_2(sc->sc_memt, sc->sc_memh, GPTMR1MAXCMPA, 0);

	bus_space_write_2(sc->sc_memt, sc->sc_memh, GPTMR2CTL,
		GPTMRCTL_ENB_WR | GPTMRCTL_CONT_CMP);
	bus_space_write_2(sc->sc_memt, sc->sc_memh, GPTMR2CNT, 0);
	bus_space_write_2(sc->sc_memt, sc->sc_memh, GPTMR2MAXCMPA, 0);

	tmr = bus_space_read_1(sc->sc_memt, sc->sc_memh, SWTMRCFG);

	/* Enable GP1/2 */
	bus_space_write_2(sc->sc_memt, sc->sc_memh, GPTMR1CTL,
		GPTMRCTL_ENB | GPTMRCTL_ENB_WR | GPTMRCTL_CONT_CMP |
		GPTMRCTL_PSC_SEL | GPTMRCTL_RTG);
	bus_space_write_2(sc->sc_memt, sc->sc_memh, GPTMR2CTL,
		GPTMRCTL_ENB | GPTMRCTL_ENB_WR | GPTMRCTL_CONT_CMP);

	/* Attach timer */
	tc = &sc->sc_tc;
	tc->tc_get_timecount = elansc_tc_read;
	tc->tc_poll_pps = NULL;
	tc->tc_counter_mask = ~0;
	tc->tc_frequency = (tmr & 1) ? (33000000 / 4) : (33333333 / 4);
	tc->tc_name = sc->sc_dev.dv_xname;
	tc->tc_quality = 1000;
	tc->tc_priv = sc;
	tc_init(tc);
}

int
elansc_activate(struct device *self, int act)
{
	switch (act) {
	case DVACT_POWERDOWN:
		wdog_shutdown(self);
		break;
	}

	return (0);
}

u_int
elansc_tc_read(struct timecounter *tc)
{
	struct elansc_softc *sc = tc->tc_priv;
	u_int32_t m1, m2, l;

	do {
		m1 = bus_space_read_2(sc->sc_memt, sc->sc_memh, GPTMR1CNT);
		l = bus_space_read_2(sc->sc_memt, sc->sc_memh, GPTMR2CNT);
		m2 = bus_space_read_2(sc->sc_memt, sc->sc_memh, GPTMR1CNT);
	} while (m1 != m2);

	return ((m1 << 16) | l);
}

void
elansc_wdogctl(struct elansc_softc *sc, int do_reset, uint16_t val)
{
	int s;
	uint8_t echo_mode;

	s = splhigh();

	/* Switch off GP bus echo mode. */
	echo_mode = bus_space_read_1(sc->sc_memt, sc->sc_memh, MMCR_GPECHO);
	bus_space_write_1(sc->sc_memt, sc->sc_memh, MMCR_GPECHO,
	    echo_mode & ~GPECHO_GP_ECHO_ENB);

	if (do_reset) {
		/* Reset the watchdog. */
		bus_space_write_2(sc->sc_memt, sc->sc_memh, MMCR_WDTMRCTL,
		    WDTMRCTL_RESET1);
		bus_space_write_2(sc->sc_memt, sc->sc_memh, MMCR_WDTMRCTL,
		    WDTMRCTL_RESET2);
	} else {
		/* Unlock the register. */
		bus_space_write_2(sc->sc_memt, sc->sc_memh, MMCR_WDTMRCTL,
		    WDTMRCTL_UNLOCK1);
		bus_space_write_2(sc->sc_memt, sc->sc_memh, MMCR_WDTMRCTL,
		    WDTMRCTL_UNLOCK2);

		/* Write the value. */
		bus_space_write_2(sc->sc_memt, sc->sc_memh, MMCR_WDTMRCTL,
		   val);
	}

	/* Switch GP bus echo mode back. */
	bus_space_write_1(sc->sc_memt, sc->sc_memh, MMCR_GPECHO, echo_mode);

	splx(s);
}

static const struct {
	int	period;		/* whole seconds */
	uint16_t exp;		/* exponent select */
} elansc_wdog_periods[] = {
	{ 1,	WDTMRCTL_EXP_SEL25 },
	{ 2,	WDTMRCTL_EXP_SEL26 },
	{ 4,	WDTMRCTL_EXP_SEL27 },
	{ 8,	WDTMRCTL_EXP_SEL28 },
	{ 16,	WDTMRCTL_EXP_SEL29 },
	{ 32,	WDTMRCTL_EXP_SEL30 },
};

int
elansc_wdogctl_cb(void *self, int period)
{
	struct elansc_softc *sc = self;
	int i;

	if (period == 0) {
		elansc_wdogctl_write(sc,
		    WDTMRCTL_WRST_ENB | WDTMRCTL_EXP_SEL30);
	} else {
		for (i = 0; i < (sizeof(elansc_wdog_periods) /
		    sizeof(elansc_wdog_periods[0])) - 1; i++)
			if (elansc_wdog_periods[i].period >= period)
				break;
		period = elansc_wdog_periods[i].period;
		elansc_wdogctl_write(sc, WDTMRCTL_ENB |
		    WDTMRCTL_WRST_ENB | elansc_wdog_periods[i].exp);
		elansc_wdogctl_reset(sc);
	}
	return (period);
}

void
elansc_update_cpuspeed(void)
{
	static const int elansc_mhz[] = { 0, 100, 133, 999 };
	uint8_t cpuctl;

	cpuctl = bus_space_read_1(elansc->sc_memt, elansc->sc_memh,
	    MMCR_CPUCTL);
	cpuspeed = elansc_mhz[cpuctl & CPUCTL_CPU_CLK_SPD_MASK];
}

void
elansc_setperf(int level)
{
	uint32_t eflags;
	uint8_t cpuctl, speed;

	level = (level > 50) ? 100 : 0;

	cpuctl = bus_space_read_1(elansc->sc_memt, elansc->sc_memh,
	    MMCR_CPUCTL);
	speed = (level == 100) ? 2 : 1;
	if ((cpuctl & CPUCTL_CPU_CLK_SPD_MASK) == speed)
		return;

	eflags = read_eflags();
	disable_intr();
	bus_space_write_1(elansc->sc_memt, elansc->sc_memh, MMCR_CPUCTL,
	    (cpuctl & ~CPUCTL_CPU_CLK_SPD_MASK) | speed);
	enable_intr();
	write_eflags(eflags);

	elansc_update_cpuspeed();
}

int
elansc_cpuspeed(int *freq)
{
	*freq = cpuspeed;
	return (0);
}

int
elansc_gpio_pin_read(void *arg, int pin)
{
	struct elansc_softc *sc = arg;
	int reg, shift;
	u_int16_t data;

	reg = (pin < 16 ? MMCR_PIODATA15_0 : MMCR_PIODATA31_16);
	shift = pin % 16;
	data = bus_space_read_2(sc->sc_memt, sc->sc_memh, reg);

	return ((data >> shift) & 0x1);
}

void
elansc_gpio_pin_write(void *arg, int pin, int value)
{
	struct elansc_softc *sc = arg;
	int reg, shift;
	u_int16_t data;

	reg = (pin < 16 ? MMCR_PIODATA15_0 : MMCR_PIODATA31_16);
	shift = pin % 16;
	data = bus_space_read_2(sc->sc_memt, sc->sc_memh, reg);
	if (value == 0)
		data &= ~(1 << shift);
	else if (value == 1)
		data |= (1 << shift);

	bus_space_write_2(sc->sc_memt, sc->sc_memh, reg, data);
}

void
elansc_gpio_pin_ctl(void *arg, int pin, int flags)
{
	struct elansc_softc *sc = arg;
	int reg, shift;
	u_int16_t data;

	reg = (pin < 16 ? MMCR_PIODIR15_0 : MMCR_PIODIR31_16);
	shift = pin % 16;
	data = bus_space_read_2(sc->sc_memt, sc->sc_memh, reg);
	if (flags & GPIO_PIN_INPUT)
		data &= ~(1 << shift);
	if (flags & GPIO_PIN_OUTPUT)
		data |= (1 << shift);

	bus_space_write_2(sc->sc_memt, sc->sc_memh, reg, data);
}
