/*
 * arch/arm/mach-cxd4115/hwtimer.c
 *
 * Hardware timer driver for CXD4115
 *
 * Copyright 2007,2008,2009 Sony Corporation
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 */

#include <linux/version.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/bio.h>
#include <asm/irq.h>
#include <asm/arch/regs-timer.h>
#include <asm/arch-cxd4115/hwtimer.h>
#include <linux/amba/bus.h>

static struct list_head hwtimer_list;
#define HWTIMER_NAME "hwtimer"

#define HWTIMER_CH CONFIG_HARDWARE_TIMER_CH
#define HWTIMER_BASE   (CXD4115_TIMER_BASE(HWTIMER_CH))
#define HWTIMER_REG(x) (IO_ADDRESSP(HWTIMER_BASE) + (x))

#define HWTIMER_TMCTL_INT_OFF  (TMST | TMCS_FREERUN | TMCK_DIV4)
#define HWTIMER_TMCTL_INT_ON   (TMST | TMINT | TMCS_FREERUN | TMCK_DIV4)
#define HWTIMER_TMCTL_OFF      (TMCS_FREERUN | TMCK_DIV4)
#define HWTIMER_TMCR_RESET     (TMCLR | TMINTCLR)
#define HWTIMER_TMCR_RESET_INT (TMINTCLR)

#ifndef CONFIG_PREEMPT_RT
static DEFINE_SPINLOCK(hwtimer_lock);
#else
static DEFINE_RAW_SPINLOCK(hwtimer_lock);
#endif /* CONFIG_PREEMPT_RT */

unsigned long hwtimer_read_clock(void)
{
	return readl(HWTIMER_REG(CXD4105_TIMERREAD));
}

static void hwtimer_on(unsigned long expire)
{
	writel(expire, HWTIMER_REG(CXD4105_TIMERCMP));
	writel(HWTIMER_TMCTL_INT_ON, HWTIMER_REG(CXD4105_TIMERCTL));
}

static void hwtimer_clear(void)
{
	writel(HWTIMER_TMCR_RESET_INT, HWTIMER_REG(CXD4105_TIMERCLR));
	writel(HWTIMER_TMCTL_INT_OFF, HWTIMER_REG(CXD4105_TIMERCTL));
}

static irqreturn_t hwtimer_intr(int irq, void *dev_id)
{
	static unsigned long flag;
	
	hwtimer_clear();
	spin_lock_irqsave(&hwtimer_lock, flag);
	while (!list_empty(&hwtimer_list)) {
		struct timer_list *timer = list_entry(hwtimer_list.next, struct timer_list, entry);
		
		if (time_after_eq(hwtimer_read_clock(), timer->expires)) {
			list_del_init(&timer->entry);
			timer->function(timer->data);
		}
		else {
			unsigned long expire = timer->expires;
			
			hwtimer_on(expire);
			break;
		}
	}
	spin_unlock_irqrestore(&hwtimer_lock, flag);
	return IRQ_HANDLED;
}

static void hwtimer_dev_init(void)
{
	writel(HWTIMER_TMCR_RESET, HWTIMER_REG(CXD4105_TIMERCLR));
	writel(HWTIMER_TMCTL_INT_OFF, HWTIMER_REG(CXD4105_TIMERCTL));
}

static void hwtimer_dev_exit(void)
{
	writel(HWTIMER_TMCTL_OFF, HWTIMER_REG(CXD4105_TIMERCTL));
}

static int hwtimer_interrupt_register(void)
{
	if (request_irq(IRQ_TIMER(HWTIMER_CH), hwtimer_intr,
			IRQF_DISABLED | IRQF_TIMER, HWTIMER_NAME, HWTIMER_NAME)){
		return -EBUSY;
	}
	return 0;
}

static void hwtimer_interrupt_unregister(void)
{
	free_irq(IRQ_TIMER(HWTIMER_CH), HWTIMER_NAME);
}

static int hwtimer_suspend(struct amba_device *dev, pm_message_t state)
{
	disable_irq(IRQ_TIMER(HWTIMER_CH));
	hwtimer_dev_exit();

	return 0;
}

static int hwtimer_resume(struct amba_device *dev)
{
	hwtimer_dev_init();
	enable_irq(IRQ_TIMER(HWTIMER_CH));

	return 0;
}

static struct amba_id hwtimer_ids[] = 
{
	{
		.id   = HWTIMER_BASE,
		.mask = 0xFFFFFFFF,
	},
	{ 0, 0 },
};

#define HWTIMER_SIZE 0x20
static struct amba_device amba_hwtimer_dev = {
	.dev = {
		.bus_id = HWTIMER_NAME,
	},
	.res = {
		 .start = HWTIMER_BASE,
		 .end   = HWTIMER_BASE+HWTIMER_SIZE-1,
		 .flags = IORESOURCE_MEM,
	 },
	.irq = { IRQ_TIMER(HWTIMER_CH), },
	.periphid = HWTIMER_BASE,
};

static struct amba_driver amba_hwtimer_driver =
{
	.drv = 
	{
		.name = HWTIMER_NAME,
	},
	.id_table = hwtimer_ids, /* unique value */
	.suspend	= hwtimer_suspend,
	.resume		= hwtimer_resume,
};

static int __init hwtimer_init(void)
{
	int err;

	err = hwtimer_interrupt_register();
	if (err)
		goto out;

	hwtimer_dev_init();
	INIT_LIST_HEAD(&hwtimer_list);

	err = amba_device_register(&amba_hwtimer_dev, &iomem_resource);
	if (err) {
		hwtimer_interrupt_unregister();
		goto out;
	}

	err = amba_driver_register(&amba_hwtimer_driver);
	if (err) {
		hwtimer_interrupt_unregister();
		amba_device_unregister(&amba_hwtimer_dev);		
	}
out:
	return err;
	
}

static void __exit hwtimer_exit(void)
{
	amba_driver_unregister(&amba_hwtimer_driver);
	amba_device_unregister(&amba_hwtimer_dev);
	hwtimer_interrupt_unregister();
	hwtimer_dev_exit();
}

void hwtimer_init_timer(struct timer_list *timer, void (*func)(unsigned long), unsigned long data)
{
	timer->function = func;
	timer->data = data;
	INIT_LIST_HEAD(&timer->entry);
}

#define HWTIMER_OFFSET 10 /* 10usec is the performance limits offset */
void hwtimer_mod_timer(struct timer_list *timer, unsigned long expire)
{
	struct timer_list *t;
	static unsigned long flag;

	BUG_ON(!timer->function);
	spin_lock_irqsave(&hwtimer_lock, flag);
	if (time_after_eq(hwtimer_read_clock()+HWTIMER_OFFSET, expire)) {
		timer->function(timer->data);
		goto out;
	}

	timer->expires = expire;
	if (!list_empty(&timer->entry)) {
		list_del_init(&timer->entry);
	}

	list_for_each_entry(t, &hwtimer_list, entry) {
		if (time_after(t->expires, timer->expires))
			break;
	}
	list_add_tail(&timer->entry, &t->entry);

	t = list_entry(hwtimer_list.next, struct timer_list, entry);		

	if (t->expires == expire) {
		hwtimer_on(expire);
	}
out:
	spin_unlock_irqrestore(&hwtimer_lock, flag);
}

void hwtimer_add_timer(struct timer_list *timer, unsigned long expire)
{
	hwtimer_mod_timer(timer, expire);
}

void hwtimer_del_timer(struct timer_list *timer)
{
	static unsigned long flag;

	spin_lock_irqsave(&hwtimer_lock, flag);
	if (!list_empty(&timer->entry)) {
		list_del_init(&timer->entry);
	}
	spin_unlock_irqrestore(&hwtimer_lock, flag);
}

static void hwtimer_process_timeout(unsigned long __data)
{
	struct task_struct *task = (struct task_struct *)__data;
	wake_up_process(task);
}

#define MAX_TIMEOUT 0x7fffffff
fastcall signed long __sched hwtimer_schedule_timeout(unsigned long timeout)
{
	struct timer_list timer;
	unsigned long expire;

	if (timeout > MAX_TIMEOUT)
		return timeout;

	expire = timeout + hwtimer_read_clock();
	
	hwtimer_init_timer(&timer, hwtimer_process_timeout, (unsigned long)current);
	hwtimer_mod_timer(&timer, expire);
	schedule();
	timeout = expire - hwtimer_read_clock();
	hwtimer_del_timer(&timer);

	return (signed long)timeout < HWTIMER_OFFSET ? 0 : timeout;
}

fastcall signed long __sched hwtimer_usleep(unsigned long timeout)
{
	set_current_state(TASK_UNINTERRUPTIBLE);
	return hwtimer_schedule_timeout(timeout);
}

EXPORT_SYMBOL(hwtimer_read_clock);
EXPORT_SYMBOL(hwtimer_init_timer);
EXPORT_SYMBOL(hwtimer_mod_timer);
EXPORT_SYMBOL(hwtimer_add_timer);
EXPORT_SYMBOL(hwtimer_del_timer);
EXPORT_SYMBOL(hwtimer_schedule_timeout);
EXPORT_SYMBOL(hwtimer_usleep);

module_init(hwtimer_init);
module_exit(hwtimer_exit);
