/*	$OpenBSD: sch311x.c,v 1.12 2009/08/22 03:51:07 mk Exp $	*/
/*
 * Copyright (c) 2008 Mark Kettenis <kettenis@openbsd.org>
 * Copyright (c) 2009 Michael Knudsen <mk@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.
 */

/*
 * SMSC SCH3112, SCH3114, and SCH3116 LPC Super I/O driver.
 */

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

#include <machine/bus.h>

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

/* Device identifiers */
#define SCHSIO_ID_SCH3112	0x7c
#define SCHSIO_ID_SCH3114	0x7d
#define SCHSIO_ID_SCH3116	0x7f

#define SCHSIO_IOSIZE 0x02
#define SCHSIO_PORT_CONFIG	0x00

/* These are used in configuration mode */
#define SCHSIO_PORT_INDEX	0x00
#define SCHSIO_PORT_DATA	0x01

#define SCHSIO_CONFIG_ENTER	0x55
#define SCHSIO_CONFIG_LEAVE	0xaa

/* Register definitions */
#define SCHSIO_IDX_LDEVSEL	0x07 /* Logical device select */
#define SCHSIO_IDX_DEVICE	0x20 /* Device ID */
#define SCHSIO_IDX_REV		0x21 /* Device revision */

#define SCHSIO_IDX_BASE_HI	0x60 /* Configuration base address */
#define SCHSIO_IDX_BASE_LO	0x61

/* Logical devices */
#define SCHSIO_LDEV_RUNTIME	0x0a /* holds wdog and sensors */
#define SCHSIO_LDEV_RUNTIME_SZ	0x100

/* Hardware monitor */
#define SCHSIO_HWM_INTERVAL	5 /* seconds */

/* Register access */
#define SCHSIO_HWM_INDEX	0x70
#define SCHSIO_HWM_DATA		0x71

/* Sensor definitions */
/* Voltage */
#define SCHSIO_HWM_VOLT1	0x20
#define SCHSIO_HWM_VOLT2	0x21
#define SCHSIO_HWM_VOLT3	0x22
#define SCHSIO_HWM_VOLT4	0x23
#define SCHSIO_HWM_VOLT5	0x24
#define SCHSIO_HWM_VOLT6	0x99
#define SCHSIO_HWM_VOLT7	0x9a

/* Temperature */
#define SCHSIO_HWM_TEMP1	0x26
#define SCHSIO_HWM_TEMP2	0x25
#define SCHSIO_HWM_TEMP3	0x27

/* Fan speed */
#define SCHSIO_HWM_TACH1_L	0x28
#define SCHSIO_HWM_TACH1_U	0x29
#define SCHSIO_HWM_TACH2_L	0x2a
#define SCHSIO_HWM_TACH2_U	0x2b
#define SCHSIO_HWM_TACH3_L	0x2c
#define SCHSIO_HWM_TACH3_U	0x2d

/* 11111 = 90kHz * 10^9 */
#define SCHSIO_FAN_RPM(x)	(1000000000 / ((x) * 11111) * 60)

#define SCHSIO_CONV_VOLT1	66400
#define SCHSIO_CONV_VOLT2	20000
#define SCHSIO_CONV_VOLT3	43800
#define SCHSIO_CONV_VOLT4	66400
#define SCHSIO_CONV_VOLT5	160000
#define SCHSIO_CONV_VOLT6	43800
#define SCHSIO_CONV_VOLT7	43800
#define SCHSIO_VOLT_MUV(x, k)	(1000000 * (x) / 2560000 * (k))

#define SCHSIO_TEMP_MUK(x)	(((x) + 273) * 1000000)

#define SCHSIO_SENSORS		13

#define SCHSIO_SENSOR_FAN1	0
#define SCHSIO_SENSOR_FAN2	1
#define SCHSIO_SENSOR_FAN3	2

#define SCHSIO_SENSOR_VOLT1	3
#define SCHSIO_SENSOR_VOLT2	4
#define SCHSIO_SENSOR_VOLT3	5
#define SCHSIO_SENSOR_VOLT4	6
#define SCHSIO_SENSOR_VOLT5	7
#define SCHSIO_SENSOR_VOLT6	8
#define SCHSIO_SENSOR_VOLT7	9

#define SCHSIO_SENSOR_TEMP1	10
#define SCHSIO_SENSOR_TEMP2	11
#define SCHSIO_SENSOR_TEMP3	12


/* Watchdog */

/* Register access */
#define SCHSIO_WDT_GPIO		0x47
#define SCHSIO_WDT_TIMEOUT	0x65
#define SCHSIO_WDT_VAL		0x66
#define SCHSIO_WDT_CFG		0x67
#define SCHSIO_WDT_CTRL		0x68

/* Bits */
#define SCHSIO_WDT_GPIO_MASK	0x0f
#define SCHSIO_WDT_GPIO_OUT	0x0e

#define SCHSIO_WDT_TO_SECONDS		(1 << 7)

#define SCHSIO_WDT_CTRL_TRIGGERED	(1 << 0)
#define SCHSIO_WDT_CFG_KBDEN		(1 << 1)
#define SCHSIO_WDT_CFG_MSEN		(1 << 2)

/* autoconf(9) flags etc. */
#define SCHSIO_CFFLAGS_WDTEN	(1 << 0)

#define DEVNAME(x) ((x)->sc_dev.dv_xname)

struct schsio_softc {
	struct device		sc_dev;

	bus_space_tag_t		sc_iot;
	bus_space_handle_t	sc_ioh;

	bus_space_handle_t	sc_ioh_rr;

	struct ksensordev	sc_sensordev;
	struct ksensor		sc_sensor[SCHSIO_SENSORS];
};

int	schsio_probe(struct device *, void *, void *);
void	schsio_attach(struct device *, struct device *, void *);

static __inline void schsio_config_enable(bus_space_tag_t iot,
    bus_space_handle_t ioh);
static __inline void schsio_config_disable(bus_space_tag_t iot,
    bus_space_handle_t ioh);

u_int8_t schsio_config_read(bus_space_tag_t iot, bus_space_handle_t ioh,
    u_int8_t reg);
void schsio_config_write(bus_space_tag_t iot, bus_space_handle_t ioh,
    u_int8_t reg, u_int8_t val);

/* HWM prototypes */
void schsio_hwm_init(struct schsio_softc *sc);
void schsio_hwm_update(void *arg);
u_int8_t schsio_hwm_read(struct schsio_softc *sc, u_int8_t reg);

/* Watchdog prototypes */

void schsio_wdt_init(struct schsio_softc *sc);
int schsio_wdt_cb(void *arg, int period);

struct cfattach schsio_ca = {
	sizeof(struct schsio_softc),
	schsio_probe,
	schsio_attach
};

struct cfdriver schsio_cd = {
	NULL, "schsio", DV_DULL
};

static __inline void
schsio_config_enable(bus_space_tag_t iot, bus_space_handle_t ioh)
{
	bus_space_write_1(iot, ioh, SCHSIO_PORT_CONFIG, SCHSIO_CONFIG_ENTER);
}

static __inline void
schsio_config_disable(bus_space_tag_t iot, bus_space_handle_t ioh)
{
	bus_space_write_1(iot, ioh, SCHSIO_PORT_CONFIG, SCHSIO_CONFIG_LEAVE);
}

u_int8_t
schsio_config_read(bus_space_tag_t iot, bus_space_handle_t ioh,
    u_int8_t reg)
{
	bus_space_write_1(iot, ioh, SCHSIO_PORT_INDEX, reg);
	return (bus_space_read_1(iot, ioh, SCHSIO_PORT_DATA));
}

void
schsio_config_write(bus_space_tag_t iot, bus_space_handle_t ioh,
    u_int8_t reg, u_int8_t val)
{
	bus_space_write_1(iot, ioh, SCHSIO_PORT_INDEX, reg);
	bus_space_write_1(iot, ioh, SCHSIO_PORT_DATA, val);
}

int
schsio_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, SCHSIO_IOSIZE, 0, &ioh))
		return (0);

	schsio_config_enable(iot, ioh);
	reg = schsio_config_read(iot, ioh, SCHSIO_IDX_DEVICE);
	schsio_config_disable(iot, ioh);

	bus_space_unmap(iot, ia->ipa_io[0].base, SCHSIO_IOSIZE);

	switch (reg) {
	case SCHSIO_ID_SCH3112:
	case SCHSIO_ID_SCH3114:
	case SCHSIO_ID_SCH3116:
		ia->ipa_nio = 1;
		ia->ipa_io[0].length = SCHSIO_IOSIZE;
		ia->ipa_nmem = 0;
		ia->ipa_nirq = 0;
		ia->ipa_ndrq = 0;
		ia->ia_aux = (void *)(u_long) reg;

		return (1);
		break;
	}

	return (0);
}

void
schsio_attach(struct device *parent, struct device *self, void *aux)
{
	struct schsio_softc *sc = (void *)self;
	struct isa_attach_args *ia = aux;
	u_int16_t iobase;
	u_int8_t reg0, reg1;

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

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

	/* Check device ID */
	reg0 = (u_int8_t)(u_long) ia->ia_aux;
	switch (reg0) {
	case SCHSIO_ID_SCH3112:
		printf(": SCH3112");
		break;
	case SCHSIO_ID_SCH3114:
		printf(": SCH3114");
		break;
	case SCHSIO_ID_SCH3116:
		printf(": SCH3116");
		break;
	}

	/* Read device revision */
	reg0 = schsio_config_read(sc->sc_iot, sc->sc_ioh, SCHSIO_IDX_REV);
	printf(" rev 0x%02x", reg0);

	/* Select runtime registers logical device */
	schsio_config_write(sc->sc_iot, sc->sc_ioh, SCHSIO_IDX_LDEVSEL,
	    SCHSIO_LDEV_RUNTIME);

	reg0 = schsio_config_read(sc->sc_iot, sc->sc_ioh,
	    SCHSIO_IDX_BASE_HI);
	reg1 = schsio_config_read(sc->sc_iot, sc->sc_ioh,
	    SCHSIO_IDX_BASE_LO);
	iobase = (reg0 << 8) | reg1;

	if (bus_space_map(sc->sc_iot, iobase, SCHSIO_LDEV_RUNTIME_SZ,
	    0, &sc->sc_ioh_rr)) {
		printf(": can't map i/o space\n");
		return;
	}

	schsio_wdt_init(sc);
	schsio_hwm_init(sc);

	printf("\n");

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

void
schsio_hwm_init(struct schsio_softc *sc)
{
	int i;

	/* Set up sensors */
	for (i = SCHSIO_SENSOR_FAN1; i < SCHSIO_SENSOR_FAN3 + 1; i++)
		sc->sc_sensor[i].type = SENSOR_FANRPM;

	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_VOLT1].desc, "+2.5V",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_VOLT1].desc));
	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_VOLT2].desc, "+1.5V (Vccp)",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_VOLT2].desc));
	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_VOLT3].desc, "+3.3V (VCC)",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_VOLT3].desc));
	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_VOLT4].desc, "+5V",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_VOLT4].desc));
	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_VOLT5].desc, "+12V",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_VOLT5].desc));
	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_VOLT6].desc, "+3.3V (VTR)",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_VOLT6].desc));
	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_VOLT7].desc, "+3V (Vbat)",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_VOLT7].desc));
	for (i = SCHSIO_SENSOR_VOLT1; i < SCHSIO_SENSOR_VOLT7 + 1; i++)
		sc->sc_sensor[i].type = SENSOR_VOLTS_DC;

	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_TEMP1].desc, "Internal",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_TEMP1].desc));
	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_TEMP2].desc, "Remote",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_TEMP2].desc));
	strlcpy(sc->sc_sensor[SCHSIO_SENSOR_TEMP3].desc, "Remote",
	    sizeof(sc->sc_sensor[SCHSIO_SENSOR_TEMP3].desc));
	for (i = SCHSIO_SENSOR_TEMP1; i < SCHSIO_SENSOR_TEMP3 + 1; i++)
		sc->sc_sensor[i].type = SENSOR_TEMP;

	strlcpy(sc->sc_sensordev.xname, DEVNAME(sc),
	    sizeof(sc->sc_sensordev.xname));

	for (i = 0; i < SCHSIO_SENSORS; i++)
		sensor_attach(&sc->sc_sensordev, &sc->sc_sensor[i]);

	if (sensor_task_register(sc, schsio_hwm_update,
	    SCHSIO_HWM_INTERVAL) == NULL) {
		printf(": unable to register update task");
		return;
	}
	sensordev_install(&sc->sc_sensordev);
}

void
schsio_hwm_update(void *arg)
{
	struct schsio_softc *sc;
	u_int16_t tach;
	int8_t temp;
	u_int8_t volt;
	u_int8_t reg0, reg1;

	sc = (struct schsio_softc *)arg;

	reg0 = schsio_hwm_read(sc, SCHSIO_HWM_TACH1_L);
	reg1 = schsio_hwm_read(sc, SCHSIO_HWM_TACH1_U);
	tach = (reg1 << 8) | reg0;
	sc->sc_sensor[SCHSIO_SENSOR_FAN1].value = SCHSIO_FAN_RPM(tach);
	sc->sc_sensor[SCHSIO_SENSOR_FAN1].flags =
	    (tach == 0xffff) ? SENSOR_FINVALID : 0;

	reg0 = schsio_hwm_read(sc, SCHSIO_HWM_TACH2_L);
	reg1 = schsio_hwm_read(sc, SCHSIO_HWM_TACH2_U);
	tach = (reg1 << 8) | reg0;
	sc->sc_sensor[SCHSIO_SENSOR_FAN2].value = SCHSIO_FAN_RPM(tach);
	sc->sc_sensor[SCHSIO_SENSOR_FAN2].flags =
	    (tach == 0xffff) ? SENSOR_FINVALID : 0;

	reg0 = schsio_hwm_read(sc, SCHSIO_HWM_TACH3_L);
	reg1 = schsio_hwm_read(sc, SCHSIO_HWM_TACH3_U);
	tach = (reg1 << 8) | reg0;
	sc->sc_sensor[SCHSIO_SENSOR_FAN3].value = SCHSIO_FAN_RPM(tach);
	sc->sc_sensor[SCHSIO_SENSOR_FAN3].flags =
	    (tach == 0xffff) ? SENSOR_FINVALID : 0;

	volt = schsio_hwm_read(sc, SCHSIO_HWM_VOLT1);
	sc->sc_sensor[SCHSIO_SENSOR_VOLT1].value =
	    SCHSIO_VOLT_MUV(volt, SCHSIO_CONV_VOLT1);

	volt = schsio_hwm_read(sc, SCHSIO_HWM_VOLT2);
	sc->sc_sensor[SCHSIO_SENSOR_VOLT2].value =
	    SCHSIO_VOLT_MUV(volt, SCHSIO_CONV_VOLT2);

	volt = schsio_hwm_read(sc, SCHSIO_HWM_VOLT3);
	sc->sc_sensor[SCHSIO_SENSOR_VOLT3].value =
	    SCHSIO_VOLT_MUV(volt, SCHSIO_CONV_VOLT3);

	volt = schsio_hwm_read(sc, SCHSIO_HWM_VOLT4);
	sc->sc_sensor[SCHSIO_SENSOR_VOLT4].value =
	    SCHSIO_VOLT_MUV(volt, SCHSIO_CONV_VOLT4);

	volt = schsio_hwm_read(sc, SCHSIO_HWM_VOLT5);
	sc->sc_sensor[SCHSIO_SENSOR_VOLT5].value =
	    SCHSIO_VOLT_MUV(volt, SCHSIO_CONV_VOLT5);

	volt = schsio_hwm_read(sc, SCHSIO_HWM_VOLT6);
	sc->sc_sensor[SCHSIO_SENSOR_VOLT6].value =
	    SCHSIO_VOLT_MUV(volt, SCHSIO_CONV_VOLT6);

	volt = schsio_hwm_read(sc, SCHSIO_HWM_VOLT7);
	sc->sc_sensor[SCHSIO_SENSOR_VOLT7].value =
	    SCHSIO_VOLT_MUV(volt, SCHSIO_CONV_VOLT7);

	temp = schsio_hwm_read(sc, SCHSIO_HWM_TEMP1);
	sc->sc_sensor[SCHSIO_SENSOR_TEMP1].value = SCHSIO_TEMP_MUK(temp);
	sc->sc_sensor[SCHSIO_SENSOR_TEMP1].flags =
	    ((uint8_t)temp == 0x80) ? SENSOR_FINVALID : 0;

	temp = schsio_hwm_read(sc, SCHSIO_HWM_TEMP2);
	sc->sc_sensor[SCHSIO_SENSOR_TEMP2].value = SCHSIO_TEMP_MUK(temp);
	sc->sc_sensor[SCHSIO_SENSOR_TEMP2].flags =
	    ((uint8_t)temp == 0x80) ? SENSOR_FINVALID : 0;

	temp = schsio_hwm_read(sc, SCHSIO_HWM_TEMP3);
	sc->sc_sensor[SCHSIO_SENSOR_TEMP3].value = SCHSIO_TEMP_MUK(temp);
	sc->sc_sensor[SCHSIO_SENSOR_TEMP3].flags =
	    ((uint8_t)temp == 0x80) ? SENSOR_FINVALID : 0;

}

u_int8_t
schsio_hwm_read(struct schsio_softc *sc, u_int8_t reg)
{
	bus_space_write_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_HWM_INDEX, reg);
	return (bus_space_read_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_HWM_DATA));
}

void
schsio_wdt_init(struct schsio_softc *sc)
{
	u_int8_t reg;

	reg = bus_space_read_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_WDT_GPIO);
	if ((reg & SCHSIO_WDT_GPIO_MASK) != SCHSIO_WDT_GPIO_OUT) {
		if (sc->sc_dev.dv_cfdata->cf_flags & SCHSIO_CFFLAGS_WDTEN) {
			reg &= ~0x0f;
			reg |= SCHSIO_WDT_GPIO_OUT;
			bus_space_write_1(sc->sc_iot, sc->sc_ioh_rr,
			    SCHSIO_WDT_GPIO, reg);
		}
		else {
			printf(", watchdog disabled");
			return;
		}
	}

	/* First of all, make sure the wdt is disabled */
	bus_space_write_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_WDT_VAL, 0);

	/* Clear triggered status */
	reg = bus_space_read_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_WDT_CTRL);
	if (reg & SCHSIO_WDT_CTRL_TRIGGERED) {
		printf(", warning: watchdog triggered");
		reg &= ~SCHSIO_WDT_CTRL_TRIGGERED;
		bus_space_write_1(sc->sc_iot, sc->sc_ioh_rr,
		    SCHSIO_WDT_CTRL, reg);
	}

	/* Disable wdt reset by mouse and kbd */
	reg = bus_space_read_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_WDT_CFG);
	reg &= ~(SCHSIO_WDT_CFG_MSEN | SCHSIO_WDT_CFG_MSEN);
	bus_space_write_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_WDT_CFG, reg);

	wdog_register(sc, schsio_wdt_cb);
}

int
schsio_wdt_cb(void *arg, int period)
{
	struct schsio_softc *sc;
	uint8_t val, minute, reg;

	sc = (struct schsio_softc *)arg;

	if (period > 255) {
		val = period / 60;
		minute = 1;
	} else {
		val = period;
		minute = 0;
	}

	/* Set unit */
	reg = bus_space_read_1(sc->sc_iot, sc->sc_ioh_rr,
	    SCHSIO_WDT_TIMEOUT);
	if (!minute)
		reg |= SCHSIO_WDT_TO_SECONDS;
	else
		reg &= ~SCHSIO_WDT_TO_SECONDS;

	bus_space_write_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_WDT_TIMEOUT,
	    reg);

	/* Set value */
	bus_space_write_1(sc->sc_iot, sc->sc_ioh_rr, SCHSIO_WDT_VAL, val);

	if (!minute)
		return val;
	else
		return val * 60;
}

