/*	$OpenBSD: viasio.c,v 1.12 2009/03/29 21:53:52 sthen Exp $	*/
/*
 * Copyright (c) 2005 Alexander Yurchenko <grange@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * VIA VT1211 LPC Super I/O driver.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/kernel.h>
#include <sys/sensors.h>
#include <sys/timeout.h>

#include <machine/bus.h>

#include <dev/isa/isareg.h>
#include <dev/isa/isavar.h>

#include <dev/isa/viasioreg.h>

#ifdef VIASIO_DEBUG
#define DPRINTF(x) printf x
#else
#define DPRINTF(x)
#endif

/* autoconf flags */
#define VIASIO_CFFLAGS_HM_ENABLE	0x0001	/* enable HM if disabled */
#define VIASIO_CFFLAGS_WDG_ENABLE	0x0002	/* enable WDG if disabled */

struct viasio_softc {
	struct device		sc_dev;

	bus_space_tag_t		sc_iot;
	bus_space_handle_t	sc_ioh;

	/* Hardware monitor */
	bus_space_handle_t	sc_hm_ioh;
	int			sc_hm_clock;
	struct ksensor		sc_hm_sensors[VT1211_HM_NSENSORS];
	struct ksensordev	sc_sensordev;
	struct timeout		sc_hm_timo;

	/* Watchdog timer */
	bus_space_handle_t	sc_wdg_ioh;
};

int	viasio_probe(struct device *, void *, void *);
void	viasio_attach(struct device *, struct device *, void *);

void	viasio_hm_init(struct viasio_softc *);
void	viasio_hm_refresh(void *);

void	viasio_wdg_init(struct viasio_softc *);
int	viasio_wdg_cb(void *, int);

struct cfattach viasio_ca = {
	sizeof(struct viasio_softc),
	viasio_probe,
	viasio_attach
};

struct cfdriver viasio_cd = {
	NULL, "viasio", DV_DULL
};

static __inline void
viasio_conf_enable(bus_space_tag_t iot, bus_space_handle_t ioh)
{
	bus_space_write_1(iot, ioh, VT1211_INDEX, VT1211_CONF_EN_MAGIC);
	bus_space_write_1(iot, ioh, VT1211_INDEX, VT1211_CONF_EN_MAGIC);
}

static __inline void
viasio_conf_disable(bus_space_tag_t iot, bus_space_handle_t ioh)
{
	bus_space_write_1(iot, ioh, VT1211_INDEX, VT1211_CONF_DS_MAGIC);
}

static __inline u_int8_t
viasio_conf_read(bus_space_tag_t iot, bus_space_handle_t ioh, u_int8_t index)
{
	bus_space_write_1(iot, ioh, VT1211_INDEX, index);
	return (bus_space_read_1(iot, ioh, VT1211_DATA));
}

static __inline void
viasio_conf_write(bus_space_tag_t iot, bus_space_handle_t ioh, u_int8_t index,
    u_int8_t data)
{
	bus_space_write_1(iot, ioh, VT1211_INDEX, index);
	bus_space_write_1(iot, ioh, VT1211_DATA, data);
}

static __inline int64_t
viasio_raw2temp(int raw)
{
	int tblsize = sizeof(vt1211_hm_temptbl) / sizeof(vt1211_hm_temptbl[0]);
	int i;
	int raw1, raw2;
	int64_t temp = -1, temp1, temp2;

	if (raw < vt1211_hm_temptbl[0].raw ||
	    raw > vt1211_hm_temptbl[tblsize - 1].raw)
		return (-1);

	for (i = 0; i < tblsize - 1; i++) {
		raw1 = vt1211_hm_temptbl[i].raw;
		temp1 = vt1211_hm_temptbl[i].temp;
		raw2 = vt1211_hm_temptbl[i + 1].raw;
		temp2 = vt1211_hm_temptbl[i + 1].temp;

		if (raw >= raw1 && raw <= raw2) {
			/* linear interpolation */
			temp = temp1 + ((raw - raw1) * (temp2 - temp1)) /
			    (raw2 - raw1);
			break;
		}
	}

	return (temp);
}

int
viasio_probe(struct device *parent, void *match, void *aux)
{
	struct isa_attach_args *ia = aux;
	bus_space_tag_t iot;
	bus_space_handle_t ioh;
	u_int8_t reg;

	/* Match by device ID */
	iot = ia->ia_iot;
	if (bus_space_map(iot, ia->ipa_io[0].base, VT1211_IOSIZE, 0, &ioh))
		return (0);
	viasio_conf_enable(iot, ioh);
	reg = viasio_conf_read(iot, ioh, VT1211_ID);
	DPRINTF(("viasio_probe: id 0x%02x\n", reg));
	viasio_conf_disable(iot, ioh);
	bus_space_unmap(iot, ioh, VT1211_IOSIZE);
	if (reg == VT1211_ID_VT1211) {
		ia->ipa_nio = 1;
		ia->ipa_io[0].length = VT1211_IOSIZE;
		ia->ipa_nmem = 0;
		ia->ipa_nirq = 0;
		ia->ipa_ndrq = 0;
		return (1);
	}

	return (0);
}

void
viasio_attach(struct device *parent, struct device *self, void *aux)
{
	struct viasio_softc *sc = (void *)self;
	struct isa_attach_args *ia = aux;
	u_int8_t reg;

	/* Map ISA I/O space */
	sc->sc_iot = ia->ia_iot;
	if (bus_space_map(sc->sc_iot, ia->ipa_io[0].base,
	    VT1211_IOSIZE, 0, &sc->sc_ioh)) {
		printf(": can't map i/o space\n");
		return;
	}

	/* Enter configuration mode */
	viasio_conf_enable(sc->sc_iot, sc->sc_ioh);

	/* Read device revision */
	reg = viasio_conf_read(sc->sc_iot, sc->sc_ioh, VT1211_REV);
	printf(": VT1211 rev 0x%02x", reg);

	/* Initialize logical devices */
	viasio_hm_init(sc);
	viasio_wdg_init(sc);
	printf("\n");

	/* Escape from configuration mode */
	viasio_conf_disable(sc->sc_iot, sc->sc_ioh);
}

void
viasio_hm_init(struct viasio_softc *sc)
{
	u_int8_t reg0, reg1;
	u_int16_t iobase;
	int i;

	printf(", HM");

	/* Select HM logical device */
	viasio_conf_write(sc->sc_iot, sc->sc_ioh, VT1211_LDN, VT1211_LDN_HM);

	/*
	 * Check if logical device is activated by firmware.  If not
	 * try to activate it only if requested.
	 */
	reg0 = viasio_conf_read(sc->sc_iot, sc->sc_ioh, VT1211_HM_ACT);
	DPRINTF((": ACT 0x%02x", reg0));
	if ((reg0 & VT1211_HM_ACT_EN) == 0) {
		if ((sc->sc_dev.dv_cfdata->cf_flags &
		    VIASIO_CFFLAGS_HM_ENABLE) != 0) {
			reg0 |= VT1211_HM_ACT_EN;
			viasio_conf_write(sc->sc_iot, sc->sc_ioh,
			    VT1211_HM_ACT, reg0);
			reg0 = viasio_conf_read(sc->sc_iot, sc->sc_ioh,
			    VT1211_HM_ACT);
			DPRINTF((", new ACT 0x%02x", reg0));
			if ((reg0 & VT1211_HM_ACT_EN) == 0) {
				printf(" failed to activate");
				return;
			}
		} else {
			printf(" not activated");
			return;
		}
	}

	/* Read HM I/O space address */
	reg0 = viasio_conf_read(sc->sc_iot, sc->sc_ioh, VT1211_HM_ADDR_LSB);
	reg1 = viasio_conf_read(sc->sc_iot, sc->sc_ioh, VT1211_HM_ADDR_MSB);
	iobase = (reg1 << 8) | reg0;
	DPRINTF((", addr 0x%04x", iobase));

	/* Map HM I/O space */
	if (bus_space_map(sc->sc_iot, iobase, VT1211_HM_IOSIZE, 0,
	    &sc->sc_hm_ioh)) {
		printf(" can't map i/o space");
		return;
	}

	/*
	 * Check if hardware monitoring is enabled by firmware.  If not
	 * try to enable it only if requested.
	 */
	reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_CONF);
	DPRINTF((", CONF 0x%02x", reg0));
	if ((reg0 & VT1211_HM_CONF_START) == 0) {
		if ((sc->sc_dev.dv_cfdata->cf_flags &
		    VIASIO_CFFLAGS_HM_ENABLE) != 0) {
			reg0 |= VT1211_HM_CONF_START;
			bus_space_write_1(sc->sc_iot, sc->sc_hm_ioh,
			    VT1211_HM_CONF, reg0);
			reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh,
			    VT1211_HM_CONF);
			DPRINTF((", new CONF 0x%02x", reg0));
			if ((reg0 & VT1211_HM_CONF_START) == 0) {
				printf(" failed to enable monitoring");
				return;
			}
		} else {
			printf(" monitoring not enabled");
			return;
		}
	}

	/* Read PWM clock frequency */
	reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_PWMCS);
	sc->sc_hm_clock = vt1211_hm_clock[reg0 & 0x07];
	DPRINTF((", PWMCS 0x%02x, %dHz", reg0, sc->sc_hm_clock));

	/* Temperature reading 1 */
	sc->sc_hm_sensors[VT1211_HMS_TEMP1].type = SENSOR_TEMP;

	/* Universal channels (UCH) 1-5 */
	reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_UCHCONF);
	DPRINTF((", UCHCONF 0x%02x", reg0));
	for (i = 1; i <= 5; i++) {
		/* UCH can be configured either as thermal or voltage input */
		if (VT1211_HM_UCHCONF_ISTEMP(reg0, i)) {
			sc->sc_hm_sensors[VT1211_HMS_UCH1 + i - 1].type =
			    SENSOR_TEMP;
		} else {
			sc->sc_hm_sensors[VT1211_HMS_UCH1 + i - 1].type =
			    SENSOR_VOLTS_DC;
		}
		snprintf(sc->sc_hm_sensors[VT1211_HMS_UCH1 + i - 1].desc,
		    sizeof(sc->sc_hm_sensors[VT1211_HMS_UCH1 + i - 1].desc),
		    "UCH%d", i);
	}

	/* Internal +3.3V */
	sc->sc_hm_sensors[VT1211_HMS_33V].type = SENSOR_VOLTS_DC;
	strlcpy(sc->sc_hm_sensors[VT1211_HMS_33V].desc, "+3.3V",
	    sizeof(sc->sc_hm_sensors[VT1211_HMS_33V].desc));

	/* FAN reading 1, 2 */
	sc->sc_hm_sensors[VT1211_HMS_FAN1].type = SENSOR_FANRPM;
	sc->sc_hm_sensors[VT1211_HMS_FAN2].type = SENSOR_FANRPM;

	/* Start sensors */
	strlcpy(sc->sc_sensordev.xname, sc->sc_dev.dv_xname,
	    sizeof(sc->sc_sensordev.xname));
	for (i = 0; i < VT1211_HM_NSENSORS; i++)
		sensor_attach(&sc->sc_sensordev, &sc->sc_hm_sensors[i]);
	sensordev_install(&sc->sc_sensordev);
	timeout_set(&sc->sc_hm_timo, viasio_hm_refresh, sc);
	timeout_add_sec(&sc->sc_hm_timo, 1);
}

void
viasio_hm_refresh(void *arg)
{
	struct viasio_softc *sc = arg;
	u_int8_t reg0, reg1;
	int64_t val, rfact;
	int i;

	/* TEMP1 is a 10-bit thermal input */
	reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_TEMP1);
	reg1 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_TCONF1);
	reg1 = VT1211_HM_TCONF1_TEMP1(reg1);
	val = (reg0 << 2) | reg1;

	/* Convert to uK */
	/* XXX: conversion function is guessed */
	val = viasio_raw2temp(val);
	if (val == -1) {
		sc->sc_hm_sensors[VT1211_HMS_TEMP1].flags |= SENSOR_FINVALID;
	} else {
		sc->sc_hm_sensors[VT1211_HMS_TEMP1].flags &= ~SENSOR_FINVALID;
		sc->sc_hm_sensors[VT1211_HMS_TEMP1].value = val;
	}

	/* Universal channels 1-5 */
	for (i = 1; i <= 5; i++) {
		if (sc->sc_hm_sensors[VT1211_HMS_UCH1 + i - 1].type ==
		    SENSOR_TEMP) {
			/* UCH is a 10-bit thermal input */
			reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh,
			    VT1211_HM_UCH1 + i - 1);
			if (i == 1) {
				reg1 = bus_space_read_1(sc->sc_iot,
				    sc->sc_hm_ioh, VT1211_HM_VID4);
				reg1 = VT1211_HM_VID4_UCH1(reg1);
			} else {
				reg1 = bus_space_read_1(sc->sc_iot,
				    sc->sc_hm_ioh, VT1211_HM_ETR);
				reg1 = VT1211_HM_ETR_UCH(reg1, i);
			}
			val = (reg0 << 2) | reg1;

			/* Convert to uK */
			/* XXX: conversion function is guessed */
			val = viasio_raw2temp(val);
			if (val == -1) {
				sc->sc_hm_sensors[VT1211_HMS_UCH1 +
				    i - 1].flags |= SENSOR_FINVALID;
			} else {
				sc->sc_hm_sensors[VT1211_HMS_UCH1 +
				    i - 1].flags &= ~SENSOR_FINVALID;
				sc->sc_hm_sensors[VT1211_HMS_UCH1 +
				    i - 1].value = val;
			}
		} else {
			/* UCH is a voltage input */
			reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh,
			    VT1211_HM_UCH1 + i - 1);
			val = reg0;

			/* Convert to uV */
			/* XXX: conversion function is guessed */
			rfact = vt1211_hm_vrfact[i - 1];
			sc->sc_hm_sensors[VT1211_HMS_UCH1 + i - 1].value =
			    ((val * 100000000000ULL) / (rfact * 958));
		}
	}

	/* Read internal +3.3V */
	reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_33V);
	val = reg0;

	/* Convert to uV */
	/* XXX: conversion function is guessed */
	rfact = vt1211_hm_vrfact[5];
	sc->sc_hm_sensors[VT1211_HMS_33V].value = ((val * 100000000000ULL) /
	    (rfact * 958));

	/* Read FAN1 clock counter and divisor */
	reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_FAN1);
	reg1 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_FSCTL);
	reg1 = VT1211_HM_FSCTL_DIV1(reg1);
	val = reg0 << reg1;

	/* Convert to RPM */
	/* XXX: conversion function is guessed */
	if (val != 0) {		
		sc->sc_hm_sensors[VT1211_HMS_FAN1].value =
		    (sc->sc_hm_clock * 60 / 2) / val;
		sc->sc_hm_sensors[VT1211_HMS_FAN1].flags &= ~SENSOR_FINVALID;
	} else {
		sc->sc_hm_sensors[VT1211_HMS_FAN1].flags |= SENSOR_FINVALID;
	}

	/* Read FAN2 clock counter and divisor */
	reg0 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_FAN2);
	reg1 = bus_space_read_1(sc->sc_iot, sc->sc_hm_ioh, VT1211_HM_FSCTL);
	reg1 = VT1211_HM_FSCTL_DIV2(reg1);
	val = reg0 << reg1;

	/* Convert to RPM */
	/* XXX: conversion function is guessed */
	if (val != 0) {		
		sc->sc_hm_sensors[VT1211_HMS_FAN2].value =
		    (sc->sc_hm_clock * 60 / 2) / val;
		sc->sc_hm_sensors[VT1211_HMS_FAN2].flags &= ~SENSOR_FINVALID;
	} else {
		sc->sc_hm_sensors[VT1211_HMS_FAN2].flags |= SENSOR_FINVALID;
	}

	timeout_add_sec(&sc->sc_hm_timo, 1);
}

void
viasio_wdg_init(struct viasio_softc *sc)
{
	u_int8_t reg0, reg1;
	u_int16_t iobase;

	printf(", WDG");

	/* Select WDG logical device */
	viasio_conf_write(sc->sc_iot, sc->sc_ioh, VT1211_LDN, VT1211_LDN_WDG);

	/*
	 * Check if logical device is activated by firmware.  If not
	 * try to activate it only if requested.
	 */
	reg0 = viasio_conf_read(sc->sc_iot, sc->sc_ioh, VT1211_WDG_ACT);
	DPRINTF((": ACT 0x%02x", reg0));
	if ((reg0 & VT1211_WDG_ACT_EN) == 0) {
		if ((sc->sc_dev.dv_cfdata->cf_flags &
		    VIASIO_CFFLAGS_WDG_ENABLE) != 0) {
			reg0 |= VT1211_WDG_ACT_EN;
			viasio_conf_write(sc->sc_iot, sc->sc_ioh,
			    VT1211_WDG_ACT, reg0);
			reg0 = viasio_conf_read(sc->sc_iot, sc->sc_ioh,
			    VT1211_WDG_ACT);
			DPRINTF((", new ACT 0x%02x", reg0));
			if ((reg0 & VT1211_WDG_ACT_EN) == 0) {
				printf(" failed to activate");
				return;
			}
		} else {
			printf(" not activated");
			return;
		}
	}

	/* Read WDG I/O space address */
	reg0 = viasio_conf_read(sc->sc_iot, sc->sc_ioh, VT1211_WDG_ADDR_LSB);
	reg1 = viasio_conf_read(sc->sc_iot, sc->sc_ioh, VT1211_WDG_ADDR_MSB);
	iobase = (reg1 << 8) | reg0;
	DPRINTF((", addr 0x%04x", iobase));

	/* Map WDG I/O space */
	if (bus_space_map(sc->sc_iot, iobase, VT1211_WDG_IOSIZE, 0,
	    &sc->sc_wdg_ioh)) {
		printf(" can't map i/o space");
		return;
	}

	/* Register new watchdog */
	wdog_register(sc, viasio_wdg_cb);
}

int
viasio_wdg_cb(void *arg, int period)
{
	struct viasio_softc *sc = arg;
	int mins;

	mins = (period + 59) / 60;
	if (mins > 255)
		mins = 255;

	bus_space_write_1(sc->sc_iot, sc->sc_wdg_ioh, VT1211_WDG_TIMEOUT, mins);
	DPRINTF(("viasio_wdg_cb: %d mins\n", mins));

	return (mins * 60);
}
