/*
 * Copyright (c) 2013 Sughosh Ganu
 *
 * 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 this list of conditions
 *    and the following disclaimer.
 * 2. Redistributions in binary form must reproduce this list of conditions
 *    and the following disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``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 ANY
 * 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.
 */

#include <sys/cdefs.h>

__KERNEL_RCSID(0, "$NetBSD: omapl1x_timer.c,v 1.1 2013/10/02 16:48:26 matt Exp $");

#include "opt_timer.h"

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

#include <dev/clock_subr.h>

#include <machine/intr.h>

#include <arm/cpufunc.h>
#include <arm/pic/picvar.h>

#include <arm/omap/omap_tipb.h>
#include <arm/omap/omapl1x_reg.h>
#include <arm/omap/omapl1x_misc.h>

typedef struct timer_factors {
	uint32_t tf_counts_per_usec;
	uint32_t tf_period;
	uint32_t tf_enamode;
	uint32_t tf_ctr_reg;
	uint32_t tf_prd_reg;
	uint32_t tf_enamode_shift;
	uint32_t tf_intr_prd_en_shift;
	uint32_t tf_intr_prd_stat_shift;
} timer_factors_t;

typedef struct omapl1xtmr_softc {
	struct device		sc_dev;
	uint			sc_timerno;
	uint			sc_timer_freq;
	bus_space_tag_t		sc_iot;
	bus_space_handle_t	sc_ioh;
	bus_addr_t		sc_addr;
	size_t			sc_size;
	int			sc_intr;
	timer_factors_t		sc_tf;
	uint			sc_bot;
} omapl1xtmr_softc_t;

static struct omapl1x_wdt {
	bus_space_tag_t		wdt_iot;	/* Bus tag */
	bus_addr_t		wdt_addr;	/* Address */
	bus_space_handle_t	wdt_ioh;
	bus_size_t		wdt_size;
} wdt;

static int  omapl1xtimer_match(device_t, struct cfdata *, void *);
static void omapl1xtimer_attach(device_t, device_t, void *);

static int omapl1xtimer_clockintr(void *frame);
static int omapl1xtimer_statintr(void *frame);
static void omapl1x_microtime_init(void);
static inline uint32_t omapl1x_get_timecount(struct timecounter *tc);
static inline void omapl1xtimer_stop(struct omapl1xtmr_softc *sc);
static inline uint32_t omapl1xtimer_read(struct omapl1xtmr_softc *sc);
static void omapl1xtimer_prd_intr_dis(struct omapl1xtmr_softc *sc);
static void omapl1xtimer_prd_intr_enb(struct omapl1xtmr_softc *sc);
static void omapl1xtimer_prd_intr_clr(struct omapl1xtmr_softc *sc);
static void omapl1xtimer_start(struct omapl1xtmr_softc *sc);
static void timer_factors(struct omapl1xtmr_softc *sc, int ints_per_sec,
			  uint8_t enamode);
static void timer_init(struct omapl1xtmr_softc *sc, int schz, uint8_t enamode,
		       boolean_t intr);

static struct timecounter omapl1x_timecounter = {
	.tc_get_timecount = omapl1x_get_timecount,
	.tc_counter_mask = 0xffffffff,
	.tc_name = "gpt",
	.tc_quality = 100,
	.tc_priv = NULL
};

#ifdef OMAPL1X_TIMER_DEBUG
static void tfprint(uint, timer_factors_t *);
#endif

static uint32_t counts_per_usec;
static uint32_t counts_per_hz = ~0;
static struct omapl1xtmr_softc *clock_sc;
static struct omapl1xtmr_softc *stat_sc;
static struct omapl1xtmr_softc *ref_sc;

/* Timer modes */
#define TGCR_TIMMODE_64BIT		0x0
#define TGCR_TIMMODE_32BIT_UNCHANINED	0x1
#define TGCR_TIMMODE_64BIT_WDOG		0x2
#define TGCR_TIMMODE_32BIT_CHANINED	0x3
#define TGCR_TIMMODE_SHIFT		2

#define TGCR_RS_STOP		0x0
#define TGCR_RS_RUN		0x1
#define TGCR_RS_MASK		0x3
#define TGCR_RS_12_SHIFT	0
#define TGCR_RS_34_SHIFT	1

#define TCR_ENAMODE_DISABLE	0x0
#define TCR_ENAMODE_ONESHOT	0x1
#define TCR_ENAMODE_CONTINUOUS	0x2
#define TCR_ENAMODE_RELOAD	0x3
#define TCR_ENAMODE_MASK	0x3
#define TCR_ENAMODE_12_SHIFT	6
#define TCR_ENAMODE_34_SHIFT	22

#define INTR_PRD_12_EN_SHIFT	0
#define INTR_PRD_34_EN_SHIFT	16
#define INTR_PRD_12_STAT_SHIFT	1
#define INTR_PRD_34_STAT_SHIFT	17

/* Watchdog related macros */
#define WDTCR_WDTKEY1		0xA5C6
#define WDTCR_WDTKEY2		0xDA7E

#define WDTCR_WDKEY_SHIFT	16
#define WDTCR_WDEN_SHIFT	14
#define WDTCR_WDKEY_MASK	0xffff0000

CFATTACH_DECL_NEW(omapl1xtimer, sizeof(struct omapl1xtmr_softc),
    omapl1xtimer_match, omapl1xtimer_attach, NULL, NULL);

static void
omapl1xtimer_prd_intr_dis (struct omapl1xtmr_softc *sc)
{
	uint32_t val;
	timer_factors_t *tfp = &sc->sc_tf;

	val  = bus_space_read_4(sc->sc_iot, sc->sc_ioh, INTCTLSTAT);
	val &= ~(1 << tfp->tf_intr_prd_en_shift);
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, INTCTLSTAT, val);
}

static void
omapl1xtimer_prd_intr_enb (struct omapl1xtmr_softc *sc)
{
	uint32_t val;
	timer_factors_t *tfp = &sc->sc_tf;

	val  = bus_space_read_4(sc->sc_iot, sc->sc_ioh, INTCTLSTAT);
	val |= 1 << tfp->tf_intr_prd_en_shift;
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, INTCTLSTAT, val);
}

static void
omapl1xtimer_prd_intr_clr (struct omapl1xtmr_softc *sc)
{
	uint32_t val;
	timer_factors_t *tfp = &sc->sc_tf;

	val  = bus_space_read_4(sc->sc_iot, sc->sc_ioh, INTCTLSTAT);
	val |= 1 << tfp->tf_intr_prd_stat_shift;
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, INTCTLSTAT, val);
}

static inline uint32_t
omapl1xtimer_read (struct omapl1xtmr_softc *sc)
{
	timer_factors_t *tfp = &sc->sc_tf;

	return bus_space_read_4(sc->sc_iot, sc->sc_ioh, tfp->tf_ctr_reg);
}

static inline void
omapl1xtimer_stop (struct omapl1xtmr_softc *sc)
{
	uint32_t val;
	timer_factors_t *tfp = &sc->sc_tf;

	val  = bus_space_read_4(sc->sc_iot, sc->sc_ioh, TCR);
	val &= ~(TCR_ENAMODE_MASK << tfp->tf_enamode_shift);
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, TCR, val);
}

static void
omapl1xtimer_start (struct omapl1xtmr_softc *sc)
{
	uint32_t val, shift;
	timer_factors_t *tfp = &sc->sc_tf;

	/* get the timer to be used out of reset */
	shift = sc->sc_bot ? TGCR_RS_12_SHIFT : TGCR_RS_34_SHIFT;
	val = bus_space_read_4(sc->sc_iot, sc->sc_ioh, TGCR);

	val |= TGCR_RS_RUN << shift;
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, TGCR, val);

	/* set the desired timer period */
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, tfp->tf_prd_reg, 
			  tfp->tf_period);

	/* set the selected enamode to get the timer running */
	val = bus_space_read_4(sc->sc_iot, sc->sc_ioh, TCR);

	val |= tfp->tf_enamode << tfp->tf_enamode_shift;
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, TCR, val);
}

static inline uint32_t
omapl1x_get_timecount (struct timecounter *tc)
{
	return  omapl1xtimer_read(ref_sc);
}

int
omapl1xtimer_clockintr (void *frame)
{
	struct omapl1xtmr_softc *sc = clock_sc;

	omapl1xtimer_prd_intr_clr(sc);
	hardclock(frame);
	return 1;
}

int
omapl1xtimer_statintr (void *frame)
{
	struct omapl1xtmr_softc *sc = stat_sc;

	omapl1xtimer_prd_intr_clr(sc);
	statclock(frame);
	return 1;
}

static void
timer_init (struct omapl1xtmr_softc *sc, int schz, uint8_t enamode,
	   boolean_t intr)
{
	int val = 0;

	timer_factors(sc, schz, enamode);

	omapl1xtimer_stop(sc);
	omapl1xtimer_prd_intr_dis(sc);
	omapl1xtimer_prd_intr_clr(sc);

	/* Clear tcr, tgcr, timer counters and period registers */
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, TCR, 0);
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, TGCR, 0);
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, TIM12, 0);
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, TIM34, 0);
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, PRD12, 0);
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, PRD34, 0);

	if (intr)
		omapl1xtimer_prd_intr_enb(sc);

	/* Set timers to 32 bot unchained mode */
	val = TGCR_TIMMODE_32BIT_UNCHANINED << TGCR_TIMMODE_SHIFT;
	bus_space_write_4(sc->sc_iot, sc->sc_ioh, TGCR, val);

	omapl1xtimer_start(sc);
}

static void
omapl1x_microtime_init (void)
{
	if (ref_sc == NULL)
		panic("microtime reference timer was not configured.");
	timer_init(ref_sc, 0, TCR_ENAMODE_CONTINUOUS, FALSE);
}

void
setstatclockrate (int schz)
{
	if (stat_sc == NULL)
		panic("Statistics timer was not configured.");
	timer_init(stat_sc, schz, TCR_ENAMODE_CONTINUOUS, TRUE);
}

/*
 * clock_sc and stat_sc starts here
 * ref_sc is initialized already by tipbtimer_attach
 */
void
cpu_initclocks(void)
{
	if (clock_sc == NULL)
		panic("Clock timer was not configured.");
	if (stat_sc == NULL)
		panic("Statistics timer was not configured.");
	if (ref_sc == NULL)
		panic("Microtime reference timer was not configured.");

	/*
	 * We already have the timers running, but not generating interrupts.
	 * In addition, we've set stathz and profhz.
	 */
	printf("clock: hz=%d stathz=%d\n", hz, stathz);

	/*
	 * The "cookie" parameter must be zero to pass the interrupt frame
	 * through to hardclock() and statclock().
	 */
	intr_establish(clock_sc->sc_intr, IPL_CLOCK, IST_LEVEL_HIGH,
		       omapl1xtimer_clockintr, 0);

	intr_establish(stat_sc->sc_intr, IPL_HIGH, IST_LEVEL_HIGH,
		       omapl1xtimer_statintr, 0);

	timer_init(clock_sc, hz, TCR_ENAMODE_CONTINUOUS, TRUE);
	timer_init(stat_sc, stathz, TCR_ENAMODE_CONTINUOUS, TRUE);

	omapl1x_timecounter.tc_frequency = omapl1x_get_tc_freq();

	tc_init(&omapl1x_timecounter);
}

void
delay (u_int n)
{
	struct omapl1xtmr_softc *sc = ref_sc;
	uint32_t cur, last, delta, usecs;

	if (sc == NULL)
		panic("The timer must be initialized sooner.");

	/*
	 * This works by polling the timer and counting the
	 * number of microseconds that go by.
	 */
	last = omapl1xtimer_read(sc);

	delta = usecs = 0;

	while (n > usecs) {
		cur = omapl1xtimer_read(sc);

		/* Check to see if the timer has wrapped around. */
		if (last > cur)
			delta += (cur + (counts_per_hz - last));
		else
			delta += (cur - last);

		last = cur;

		if (delta >= counts_per_usec) {
			usecs += delta / counts_per_usec;
			delta %= counts_per_usec;
		}
	}
}

static void
timer_factors (struct omapl1xtmr_softc *sc, int ints_per_sec, uint8_t enamode)
{
	timer_factors_t *tfp = &sc->sc_tf;
	const uint32_t us_per_sec = 1000000;

	if (ints_per_sec == 0) {
		tfp->tf_period = ~0U;
		counts_per_usec = sc->sc_timer_freq / us_per_sec;
	} else {
		uint32_t count_freq;
		count_freq = sc->sc_timer_freq;
		count_freq /= ints_per_sec;
		tfp->tf_period = count_freq;
	}

	tfp->tf_counts_per_usec = sc->sc_timer_freq / us_per_sec;
	tfp->tf_enamode = enamode;

	if (sc->sc_bot) {
		tfp->tf_ctr_reg = TIM12;
		tfp->tf_prd_reg = PRD12;
		tfp->tf_enamode_shift = TCR_ENAMODE_12_SHIFT;
		tfp->tf_intr_prd_en_shift = INTR_PRD_12_EN_SHIFT;
		tfp->tf_intr_prd_stat_shift = INTR_PRD_12_STAT_SHIFT;
	} else {
		tfp->tf_ctr_reg = TIM34;
		tfp->tf_prd_reg = PRD34;
		tfp->tf_enamode_shift = TCR_ENAMODE_34_SHIFT;
		tfp->tf_intr_prd_en_shift = INTR_PRD_34_EN_SHIFT;
		tfp->tf_intr_prd_stat_shift = INTR_PRD_34_STAT_SHIFT;
	}

#ifdef OMAPL1X_TIMER_DEBUG
	tfprint(sc->sc_timerno, tfp);
	Debugger();
#endif
}

#ifdef OMAPL1X_TIMER_DEBUG
void
tfprint (uint n, timer_factors_t *tfp)
{
	printf("%s: timer# %d\n", __func__, n);
	printf("\ttf_counts_per_usec: %#x\n", tfp->tf_counts_per_usec);
	printf("\ttf_counter: %#x\n", tfp->period);
	printf("\ttf_enamode: %#x\n", tfp->tf_enamode);
}
#endif

static int
omapl1xtimer_match (device_t parent, struct cfdata *match, void *aux)
{
	return 1;
}

void
omapl1xtimer_attach (device_t parent, device_t self, void *aux)
{
	struct omapl1xtmr_softc *sc = device_private(self);
	struct tipb_attach_args *tipb = aux;

	sc->sc_timerno = self->dv_unit;
	sc->sc_iot = tipb->tipb_iot;
	sc->sc_intr = tipb->tipb_intr;
	sc->sc_addr = tipb->tipb_addr;
	sc->sc_bot = 1; /* use the bottom timer in all cases */
	sc->sc_size = OMAPL1X_TIMER_SIZE;

	if (bus_space_map(sc->sc_iot, sc->sc_addr, sc->sc_size, 0, &sc->sc_ioh))
		panic("%s: Cannot map registers", device_xname(self));

	aprint_normal("\n");
	aprint_naive("\n");

	switch (sc->sc_timerno) {
	case 0:
		/*
		 * timer #0 is the system clock
		 * it gets started later
		 */
		clock_sc = sc;
		sc->sc_timer_freq = OMAPL1X_TIMER0_FREQ;
		break;
	case 1:
		/*
		 * timer #2 is the stat clock
		 * it gets started later
		 */
		profhz = stathz = STATHZ;
		stat_sc = sc;
		sc->sc_timer_freq = OMAPL1X_TIMER2_FREQ;
		break;
	case 2:
		/*
		 * Timer #3 is used for microtime reference clock and for delay()
		 * autoloading, non-interrupting, just wraps around as an unsigned int.
		 * we start it now to make delay() available
		 */
		ref_sc = sc;
		sc->sc_timer_freq = OMAPL1X_TIMER3_FREQ;
		omapl1x_microtime_init();
		break;
	default:
		panic("bad omapl1x timer number %d\n", sc->sc_timerno);
		break;
	}

	wdt.wdt_iot = tipb->tipb_iot;
	wdt.wdt_addr = OMAPL1X_WDT_ADDR;
	wdt.wdt_size = OMAPL1X_WDT_SIZE;

	/* Map WDT registers. We want to use it for reseting the chip */
	if (bus_space_map(wdt.wdt_iot, wdt.wdt_addr,
			  wdt.wdt_size, 0, &wdt.wdt_ioh)) {
		aprint_error_dev(self, "can't map wdt mem space\n");
		return;
	}
}

void
omapl1x_reset (void)
{
	uint32_t val;

	printf("\n");
	delay(50000);

	val = bus_space_read_4(wdt.wdt_iot, wdt.wdt_ioh, TGCR);

	/*
	 * Get the timer out of reset  and put it in
	 * watchdog timer mode.
	 */
	val |= ((TGCR_RS_RUN << TGCR_RS_12_SHIFT) |
		(TGCR_RS_RUN << TGCR_RS_34_SHIFT));
	val |= (TGCR_TIMMODE_64BIT_WDOG << TGCR_TIMMODE_SHIFT);

	bus_space_write_4(wdt.wdt_iot, wdt.wdt_ioh, TGCR, val);

	/* Init the counter and period registers */
	bus_space_write_4(wdt.wdt_iot, wdt.wdt_ioh, TIM12, 0x0);
	bus_space_write_4(wdt.wdt_iot, wdt.wdt_ioh, TIM34, 0x0);
	bus_space_write_4(wdt.wdt_iot, wdt.wdt_ioh, PRD12, ~0);
	bus_space_write_4(wdt.wdt_iot, wdt.wdt_ioh, PRD34, ~0);

	val = bus_space_read_4(wdt.wdt_iot, wdt.wdt_ioh, WDTCR);

	/*
	 * Now enable the wdt and write the WDKEY1 to get the 
	 * wd in the Pre-active state.
	 */
	val |= (1 << WDTCR_WDEN_SHIFT);
	val |= (WDTCR_WDTKEY1 << WDTCR_WDKEY_SHIFT);

	bus_space_write_4(wdt.wdt_iot, wdt.wdt_ioh, WDTCR, val);

	/*
	 * Now write the WDKEY2 to get the wd in the Active 
	 * state.
	 */
	val = bus_space_read_4(wdt.wdt_iot, wdt.wdt_ioh, WDTCR);
	val &= ~WDTCR_WDKEY_MASK;
	val |= (WDTCR_WDTKEY2 << WDTCR_WDKEY_SHIFT);
	bus_space_write_4(wdt.wdt_iot, wdt.wdt_ioh, WDTCR, val);

	/* 
	 * Write an invalid value to the WDKEY to trigger
	 * the wd timeout right away.
	 */
	val = bus_space_read_4(wdt.wdt_iot, wdt.wdt_ioh, WDTCR);
	val &= ~WDTCR_WDKEY_MASK;
	bus_space_write_4(wdt.wdt_iot, wdt.wdt_ioh, WDTCR, val);

	while(1);
}