/*	$OpenBSD: berkwdt.c,v 1.6 2010/04/08 00:23:53 tedu Exp $ */

/*
 * Copyright (c) 2009 Wim Van Sebroeck <wim@iguana.be>
 *
 * 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.
 */

/*
 * Berkshire PCI-PC Watchdog Card Driver
 * http://www.pcwatchdog.com/
 */

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

#include <machine/bus.h>

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

struct berkwdt_softc {
	struct device	sc_dev;

	/* device access through bus space */
	bus_space_tag_t		sc_iot;
	bus_space_handle_t	sc_ioh;

	/* the timeout period */
	int		sc_period;
};

int berkwdt_match(struct device *, void *, void *);
void berkwdt_attach(struct device *, struct device *, void *);

void berkwdt_start(struct berkwdt_softc *sc);
void berkwdt_stop(struct berkwdt_softc *sc);
void berkwdt_reload(struct berkwdt_softc *sc);
int berkwdt_send_command(struct berkwdt_softc *sc, u_int8_t cmd, int *val);

int berkwdt_set_timeout(void *, int);

struct cfattach berkwdt_ca = {
	sizeof(struct berkwdt_softc), berkwdt_match, berkwdt_attach
};

struct cfdriver berkwdt_cd = {
	NULL, "berkwdt", DV_DULL
};

const struct pci_matchid berkwdt_devices[] = {
	{ PCI_VENDOR_PIJNENBURG, PCI_PRODUCT_PIJNENBURG_PCWD_PCI }
};

/* PCWD-PCI I/O Port definitions */
#define PCWD_PCI_RELOAD		0x00	/* Re-trigger */
#define PCWD_PCI_CS1		0x01	/* Control Status 1 */
#define PCWD_PCI_CS2		0x02	/* Control Status 2 */
#define PCWD_PCI_WDT_DIS	0x03	/* Watchdog Disable */
#define PCWD_PCI_LSB		0x04	/* Command / Response */
#define PCWD_PCI_MSB		0x05	/* Command/Response LSB */
#define PCWD_PCI_CMD		0x06	/* Command/Response MSB */

/* Port 1 : Control Status #1 */
#define WD_PCI_WTRP		0x01	/* Watchdog Trip status */
#define WD_PCI_TTRP		0x04	/* Temperature Trip status */
#define WD_PCI_R2DS		0x40	/* Relay 2 Disable Temp-trip reset */

/* Port 2 : Control Status #2 */
#define WD_PCI_WDIS		0x10	/* Watchdog Disable */
#define WD_PCI_WRSP		0x40	/* Watchdog wrote response */

/*
 * According to documentation max. time to process a command for the pci
 * watchdog card is 100 ms, so we give it 150 ms to do its job.
 */
#define PCI_CMD_TIMEOUT		150

/* Watchdog's internal commands */
#define CMD_WRITE_WD_TIMEOUT	0x19

int
berkwdt_send_command(struct berkwdt_softc *sc, u_int8_t cmd, int *val)
{
	u_int8_t msb;
	u_int8_t lsb;
	u_int8_t got_response;
	int count;

	msb = *val / 256;
	lsb = *val % 256;

	/* Send command with data (data first!) */
	bus_space_write_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_LSB, lsb);
	bus_space_write_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_MSB, msb);
	bus_space_write_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_CMD, cmd);

	got_response = bus_space_read_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_CS2);
	got_response &= WD_PCI_WRSP;
	for (count = 0; count < PCI_CMD_TIMEOUT && !got_response; count++) {
		delay(1000);
		got_response = bus_space_read_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_CS2);
		got_response &= WD_PCI_WRSP;
	}

	if (got_response) {
		/* read back response */
		lsb = bus_space_read_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_LSB);
		msb = bus_space_read_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_MSB);
		*val = (msb << 8) + lsb;

		/* clear WRSP bit */
		bus_space_read_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_CMD);
		return 1;
	}

	return 0;
}

void
berkwdt_start(struct berkwdt_softc *sc)
{
	u_int8_t reg;

	bus_space_write_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_WDT_DIS, 0x00);
	delay(1000);

	reg = bus_space_read_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_CS2);
	if (reg & WD_PCI_WDIS) {
		printf("%s: unable to enable\n", sc->sc_dev.dv_xname);
	}
}

void
berkwdt_stop(struct berkwdt_softc *sc)
{
	u_int8_t reg;

	bus_space_write_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_WDT_DIS, 0xa5);
	delay(1000);
	bus_space_write_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_WDT_DIS, 0xa5);
	delay(1000);

	reg = bus_space_read_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_CS2);
	if (!(reg & WD_PCI_WDIS)) {
		printf("%s: unable to disable\n", sc->sc_dev.dv_xname);
	}
}

void
berkwdt_reload(struct berkwdt_softc *sc)
{
	bus_space_write_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_RELOAD, 0x42);
}

int
berkwdt_match(struct device *parent, void *match, void *aux)
{
	return (pci_matchbyid((struct pci_attach_args *)aux, berkwdt_devices,
	    sizeof(berkwdt_devices) / sizeof(berkwdt_devices[0])));
}

void
berkwdt_attach(struct device *parent, struct device *self, void *aux)
{
	struct berkwdt_softc *sc = (struct berkwdt_softc *)self;
	struct pci_attach_args *const pa = (struct pci_attach_args *)aux;
	bus_size_t iosize;
	u_int8_t reg;

	/* retrieve the I/O region (BAR 0) */
	if (pci_mapreg_map(pa, 0x10, PCI_MAPREG_TYPE_IO, 0,
	    &sc->sc_iot, &sc->sc_ioh, NULL, &iosize, 0) != 0) {
		printf(": couldn't find PCI I/O region\n");
		return;
	}

	/* Check for reboot by the card */
	reg = bus_space_read_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_CS1);
	if (reg & WD_PCI_WTRP) {
		printf(", warning: watchdog triggered");

		if (reg & WD_PCI_TTRP)
			printf(", overheat detected");

		/* clear trip status & LED and keep mode of relay 2 */
		reg &= WD_PCI_R2DS;
		reg |= WD_PCI_WTRP;
		bus_space_write_1(sc->sc_iot, sc->sc_ioh, PCWD_PCI_CS1, reg);
	}

	printf("\n");

	/* ensure that the watchdog is disabled */
	berkwdt_stop(sc);
	sc->sc_period = 0;

	/* register with the watchdog framework */
	wdog_register(sc, berkwdt_set_timeout);
}

int
berkwdt_set_timeout(void *self, int timeout)
{
	struct berkwdt_softc *sc = self;
	int new_timeout = timeout;

	if (timeout == 0) {
		/* Disable watchdog */
		berkwdt_stop(sc);
	} else {
		if (sc->sc_period != timeout) {
			/* Set new timeout */
			berkwdt_send_command(sc, CMD_WRITE_WD_TIMEOUT,
			    &new_timeout);
		}
		if (sc->sc_period == 0) {
			/* Enable watchdog */
			berkwdt_start(sc);
			berkwdt_reload(sc);
		} else {
			/* Reset timer */
			berkwdt_reload(sc);
		}
	}
	sc->sc_period = timeout;

	return (timeout);
}

