/* $NetBSD: $ */

/*-
  * Copyright (c) 2013 UPMC/LIP6
  * All rights reserved.
  * This software is distributed under the following condiions
  * compliant with the NetBSD foundation policy.
  *
  * 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.
  */

/* driver for the VHDL PS/2 keyboard controller */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: machdep.c,v 1.223 2008/07/02 17:28:56 ad Exp $");

#include <sys/param.h>
#include <sys/proc.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/conf.h>
#include <sys/kauth.h>
#include <sys/kthread.h>
#include <sys/condvar.h>
#include <sys/kmem.h>

#include <dev/pckbport/pckbportvar.h>

#include <uvm/uvm_extern.h> /* for kernel_map */

#include <machine/autoconf.h>
#include <machine/bus.h>

#undef KBC_DEBUG
#ifdef KBC_DEBUG
#define DPRINTF(m, x) {if (vcidebug_mask & (m)) printf x;}
#define KBCDEBUG_FUNC	0x1
#define KBCDEBUG_XFER	0x2
#define KBCDEBUG_XFER2	0x4
#define KBCDEBUG_INIT	0x8
static int vcidebug_mask = KBCDEBUG_XFER2 | KBCDEBUG_INIT;
#else
#define DPRINTF(m, x)
#endif

/* KBC device registers */
#define KBC_DATA(s)		(0x00 + s)
#define KBC_CTRLSTATUS		0x02

#define KBC_CTRLSTATUS_DATA(p)	(1 << p)
#define KBC_CTRLSTATUS_CMD(p)	(1 << (2 + p))
#define KBC_CTRLSTATUS_IRQ(p)	(1 << (8 + (p * 2)))
#define KBC_CTRLSTATUS_RST(p)	(1 << (9 + (p * 2)))

#define KBC_DIVIDER		0x03

#define KBC_DEVICE_REG_READ(m, reg) \
	bus_space_read_4((m)->m_tag, (m)->m_handle, (reg) * 4)
#define KBC_DEVICE_REG_WRITE(m, reg, val) \
	bus_space_write_4((m)->m_tag, (m)->m_handle, (reg) * 4, (val))

static int  vcikbc_match(device_t, cfdata_t, void *);
static void vcikbc_attach(device_t, device_t, void *);
static int  vcikbc_intr(void *);

struct vcikbc_softc {
	device_t sc_dev;
	struct vcikbc_m *sc_m;
	irq_t sc_ih;
	kmutex_t sc_mtx;
	lwp_t	   *sc_lwp;
	int sc_nports;
};

struct vcikbc_m {
	bus_space_tag_t m_tag;
	bus_space_handle_t m_handle;
	struct pckbport_tag *m_pt;
	struct vcikbc_softc *m_sc;
};

static int vcikbc_xt_translation(void *, pckbport_slot_t, int);
static int vcikbc_send_devcmd(void *, pckbport_slot_t, u_char);
static int vcikbc_poll_data1(void *, pckbport_slot_t);
static void vcikbc_slot_enable(void *, pckbport_slot_t, int);
static void vcikbc_intr_establish(void *, pckbport_slot_t);
static void vcikbc_set_poll(void *, pckbport_slot_t, int);

static struct pckbport_accessops const vcikbc_ops = {
        vcikbc_xt_translation,
	vcikbc_send_devcmd,
	vcikbc_poll_data1,
	vcikbc_slot_enable,
	vcikbc_intr_establish,
	vcikbc_set_poll
};

static struct vcikbc_m console_m = { 0 };
static bus_addr_t vcikbc_console_addr;

CFATTACH_DECL_NEW(vcikbc, sizeof(struct vcikbc_softc),
    vcikbc_match, vcikbc_attach, NULL, NULL);

int
vcikbc_match(device_t parent, cfdata_t match, void *aux)
{
	struct tsardevs_attach_args *tsd = aux;
	if (strcmp(tsd->tsd_devtype, "vci:ps2") != 0)
		return 0;
	return 1;
}

void
vcikbc_attach(device_t parent, device_t self, void *aux)
{
	struct vcikbc_softc *sc = device_private(self);
	struct vcikbc_m *m;
	struct tsardevs_attach_args *tsd = aux;
	const struct boot_fdt_prop *prop;
	char intstr[40];
	int status, i;
	int nports;
	int div_freq;
	uint32_t div_value;

	sc->sc_dev = self;
	aprint_normal(": PS/2 controller");

	if (tsd->tsd_reg[0].reg_addr == vcikbc_console_addr) {
		m = sc->sc_m = &console_m;
	} else {
		m = sc->sc_m = kmem_alloc(sizeof(struct vcikbc_m), KM_SLEEP);
		/* map the device */
		m->m_tag = tsd->tsd_tag;
		if (bus_space_map(m->m_tag,
		    tsd->tsd_reg[0].reg_addr, tsd->tsd_reg[0].reg_size, 0,
		    &m->m_handle)) {
				aprint_error_dev(self,
				    "couldn't map registers\n");
				return;
		}
	}
	m->m_sc = sc;

	if (fdt_get_clock_freq(tsd->tsd_node, &div_freq)) {
		char buf[8];
		humanize_number(buf, 8, div_freq, "Hz", 1000);
		aprint_verbose(", %s input clock", buf);
		div_value = div_freq / 1000000 * 300; /* 300us */
	} else {
		div_value = 8192; /* default value */
	}

	mutex_init(&sc->sc_mtx, MUTEX_DEFAULT, IPL_TTY);

	sc->sc_ih = intr_establish(tsd->tsd_irq, IPL_BIO,
	    device_xname(self), intstr, vcikbc_intr, sc, NULL);
	if (sc->sc_ih == NULL) {
		aprint_error_dev(self, "can't establish interrupt\n");
		return;
	} else {
		aprint_normal_dev(self, "interrupting at %s\n", intstr);
	}

	prop = fdt_find_prop(tsd->tsd_node->fdt_node_data, "ports");
	if (prop == NULL) {
		aprint_error_dev(self, "ports not set, assume 1 port\n");
		nports = 1;
	} else {
		nports = be32toh(prop->prop_value[0]);
	}

	sc->sc_nports = nports;
	KBC_DEVICE_REG_WRITE(m, KBC_DIVIDER, div_value);
	/* reset the controller */
	for (i = 0 ; i < nports; i++) {
		KBC_DEVICE_REG_WRITE(m, KBC_CTRLSTATUS, KBC_CTRLSTATUS_RST(i));
		status = KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS);
		while (status & KBC_CTRLSTATUS_RST(i)) {
			DPRINTF(KBCDEBUG_INIT,
			    ("vcikbc_attach port %d status 0x%x\n",
			    i, status));
			status = KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS);
		}
	}
	if (nports > 0) {
		DPRINTF(KBCDEBUG_INIT, ("vcikbc_attach status 0x%x\n", status));
	}
	m->m_pt = pckbport_attach(m, &vcikbc_ops);
	for (i = 0 ; i < nports; i++) {
		pckbport_attach_slot(sc->sc_dev, m->m_pt, i);
	}

	return;
}

static int
vcikbc_intr(void *arg)
{
        struct vcikbc_softc *sc = (struct vcikbc_softc *)arg;
        struct vcikbc_m *m = sc->sc_m;
	uint32_t data, status;
	int ret = 0;
	int i;

	mutex_enter(&sc->sc_mtx);
	status = KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS);
	for (i = 0; i < sc->sc_nports; i++) {
		while (status & KBC_CTRLSTATUS_DATA(i)) {
			ret = 1;
			data = KBC_DEVICE_REG_READ(m, KBC_DATA(i));
			DPRINTF(KBCDEBUG_XFER,
			    ("pckbc slot %d read 0x%x\n", i, data));
			pckbportintr(m->m_pt, i, data);
			status = KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS);
		}
	}
	mutex_exit(&sc->sc_mtx);
	return ret;
}

static int
vcikbc_send_devcmd(void *arg, pckbport_slot_t slot, u_char cmd)
{
	struct vcikbc_m *m = (struct vcikbc_m *)arg;
	DPRINTF(KBCDEBUG_XFER2, ("vcikbc_send_devcmd slot %d cmd 0x%x ",
	    slot, cmd));
	int timeout = 10000;
	timeout = 10000;
	while ((KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS) & KBC_CTRLSTATUS_CMD(slot)) == 0) {
		DELAY(10);
		if (--timeout == 0)
		{
			DPRINTF(KBCDEBUG_XFER2, (" timeout\n"));
			return 0;
		}
	}
	KBC_DEVICE_REG_WRITE(m, KBC_DATA(slot), cmd);
	DPRINTF(KBCDEBUG_XFER2, ("\n"));
	return 1;
}

static int
vcikbc_poll_data1(void *arg, pckbport_slot_t slot)
{
	struct vcikbc_m *m = (struct vcikbc_m *)arg;
	int timeout;
	uint32_t data;

	DPRINTF(KBCDEBUG_XFER2, ("vcikbc_poll_data1 slot %d", slot));
	timeout = 10000;
	while ((KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS) & KBC_CTRLSTATUS_DATA(slot)) == 0) {
		DELAY(10);
		if (--timeout == 0)
		{
			DPRINTF(KBCDEBUG_XFER2, (" timeout\n"));
			return -1;
		}
	}

	data = KBC_DEVICE_REG_READ(m, KBC_DATA(slot));
	DPRINTF(KBCDEBUG_XFER2, (" return 0x%x\n", data));
	return data;
}

static int
vcikbc_xt_translation(void *arg, pckbport_slot_t slot, int on)
{
	DPRINTF(KBCDEBUG_XFER2, ("vcikbc_xt_translation slot %d %s\n",
	    slot, on ? "on" : "off"));
	if (on)
		return 0; /* can't do xt translation */
	else
		return 1;
}

static void
vcikbc_slot_enable(void *arg, pckbport_slot_t slot, int on)
{
	/* nothing */
	DPRINTF(KBCDEBUG_XFER2, ("vcikbc_slot_enable slot %d\n", slot));
	return;
}

static void
vcikbc_intr_establish(void *arg, pckbport_slot_t slot)
{
	struct vcikbc_m *m = (struct vcikbc_m *)arg;
	DPRINTF(KBCDEBUG_XFER2, ("vcikbc_intr_establish slot %d\n", slot));
	KBC_DEVICE_REG_WRITE(m, KBC_CTRLSTATUS,
	    KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS) | KBC_CTRLSTATUS_IRQ(slot));
	DPRINTF(KBCDEBUG_XFER2, ("vcikbc_intr_establish slot %d: 0x%x\n", slot,
	    KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS)));
}

static void
vcikbc_set_poll(void *arg, pckbport_slot_t slot, int on)
{
	struct vcikbc_m *m = (struct vcikbc_m *)arg;
	if (on) {
		KBC_DEVICE_REG_WRITE(m, KBC_CTRLSTATUS,
		    KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS) & ~KBC_CTRLSTATUS_IRQ(slot));
	} else {
		KBC_DEVICE_REG_WRITE(m, KBC_CTRLSTATUS,
		    KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS) | KBC_CTRLSTATUS_IRQ(slot));
	}
	DPRINTF(KBCDEBUG_XFER2, ("vcikbc_set_poll slot %d %s 0x%x\n", slot,
	  on ? "on" : "off", KBC_DEVICE_REG_READ(m, KBC_CTRLSTATUS)));
}

void
vcikbc_cnattach(bus_space_tag_t iot, bus_addr_t addr, bus_size_t size,
    int slot)
{
	struct vcikbc_m *m = &console_m;

	m->m_tag = iot;
	if (kernel_map == NULL)
		return; /* not yet */
	if (vcikbc_console_addr != 0)
		return; /* already done */
	if (bus_space_map(iot, addr, size, 0, &m->m_handle)) {
		panic("can't map console kbd");
	}

	vcikbc_console_addr = addr;
	KBC_DEVICE_REG_WRITE(m, KBC_DIVIDER, 8192);
	pckbport_cnattach(m, &vcikbc_ops, slot);
}
