/*
 *  linux/arch/arm/mach-dmw/sec.c
 *
 *  Copyright (C) 2010 DSP Group
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * 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 St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/err.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/platform_device.h>
#include <linux/spinlock.h>
#include <asm/fiq.h>
#include <asm/io.h>
#include <asm/bitops.h>
#include <mach/irqs.h>
#include <mach/platform.h>
#include <mach/sec.h>

#define CPU2COM_MEM	0x000 /* R/W */
#define COM2CPU_MEM	0x100 /* RO  */

#define CPU2COM_STAT1	0x600 /* RO  */
#define CPU2COM_STAT2	0x604 /* RO  */
#define CPU2COM_OVR1	0x610 /* RO  */
#define CPU2COM_OVR2	0x614 /* RO  */
#define COM2CPU_STAT1	0x640 /* RO/CBW */
#define COM2CPU_STAT2	0x644 /* RO/CBW */
#define COM2CPU_MASK1	0x648 /* R/W */
#define COM2CPU_MASK2	0x64C /* R/W */
#define COM2CPU_OVR1	0x650 /* R/W */
#define COM2CPU_OVR2	0x654 /* R/W */
#define COM2CPU_CAUS1	0x658 /* RO  */
#define COM2CPU_CAUS2	0x65C /* RO  */
#define COM_CAUSE	0x660 /* RO  */

#define SEC_SEM_COUNT	64

#define SEC_BUSY	0x1

struct sec_msg {
	int id;
	int flags;
	sec_callback cb;
	void *context;
};

struct sec_sem {
	struct sec_msg *enter;
	struct sec_msg *number;
};

struct sec_dev {
	void __iomem *regs;
	struct clk   *clk;
	int irq;
	int sw_irq;
	unsigned long sw_pending[2];

	spinlock_t lock;

	struct sec_msg messages[SEC_SEM_COUNT];
};

static struct sec_dev *sec = NULL;

extern unsigned char dw_sec_fiqasm_start, dw_sec_fiqasm_end;
static char dw_sec_fiqstack[PAGE_SIZE]; /* One whole memory page */

static int
dw_sec_fiqsetup(void *data, int relinquish)
{
	printk(KERN_ERR "%s(): not implemented!\n", __func__);

	return -1;
}

static struct fiq_handler dw_sec_fiqhandler = {
	.name = "SEC_FIQ",
	.fiq_op = dw_sec_fiqsetup,
};

static unsigned long
sec_read_css_mem(int id)
{
	return readl(sec->regs + COM2CPU_MEM + id*4);
}

static unsigned long
sec_read_app_mem(int id)
{
	return readl(sec->regs + CPU2COM_MEM + id*4);
}

static void
sec_write_mem(int id, unsigned long data)
{
	writel(data, sec->regs + CPU2COM_MEM + id*4);
}

static void
sec_int_clear(struct sec_msg *msg)
{
	writel(1 << (msg->id%32), sec->regs + COM2CPU_STAT1 + (msg->id>>5)*4);
}

static void
sec_int_enable(struct sec_msg *msg)
{
	unsigned long tmp, flags;

	local_save_flags(flags);
	local_fiq_disable();

	tmp = readl(sec->regs + COM2CPU_MASK1 + (msg->id>>5)*4);
	tmp |=  1 << (msg->id%32);
	writel(tmp, sec->regs + COM2CPU_MASK1 + (msg->id>>5)*4);

	local_irq_restore(flags);
}

static void
sec_int_disable(struct sec_msg *msg)
{
	unsigned long tmp, flags;

	local_save_flags(flags);
	local_fiq_disable();

	tmp = readl(sec->regs + COM2CPU_MASK1 + (msg->id>>5)*4);
	tmp &= ~(1 << (msg->id%32));
	writel(tmp, sec->regs + COM2CPU_MASK1 + (msg->id>>5)*4);

	local_irq_restore(flags);
}

static void
sec_overwrite(struct sec_msg *msg, int enable)
{
	unsigned long tmp;

	tmp = readl(sec->regs + COM2CPU_OVR1 + (msg->id>>5)*4);
	if (enable)
		tmp |=   1 << (msg->id%32);
	else
		tmp &= ~(1 << (msg->id%32));
	writel(tmp, sec->regs + COM2CPU_OVR1 + (msg->id>>5)*4);
}

static void
sec_fiq_handler(void)
{
	int i, trigger_sw = 0;
	unsigned long stat[2];

	stat[0] = readl_relaxed(sec->regs + COM2CPU_CAUS1);
	stat[1] = readl(sec->regs + COM2CPU_CAUS2);

	i = find_first_bit(stat, 64);
	while (i < 64) {
		struct sec_msg *msg = &sec->messages[i];

		if (msg->flags & SEC_FIQ) {
			if (msg->flags & SEC_DONT_CLEAR) {
				sec_int_disable(msg);
				msg->cb(i, sec_read_css_mem(i), msg->context);
			} else {
				msg->cb(i, sec_read_css_mem(i), msg->context);
				sec_int_clear(msg);
			}
		} else {
			sec_int_disable(msg);
			set_bit(i, sec->sw_pending);
			trigger_sw = 1;
		}

		i = find_next_bit(stat, 64, i+1);
	}

	if (trigger_sw)
		dmw_trigger_sw_irq(sec->sw_irq);
}

static irqreturn_t
sec_sw_interrupt_handler(int irq, void *data)
{
	struct sec_dev *sec = (struct sec_dev *)data;
	int i;

	i = find_first_bit(sec->sw_pending, 64);
	while (i < 64) {
		struct sec_msg *msg = &sec->messages[i];

		clear_bit(i, sec->sw_pending);

		msg->cb(i, sec_read_css_mem(i), msg->context);

		if (!(msg->flags & SEC_DONT_CLEAR)) {
			sec_int_clear(msg);
			sec_int_enable(msg);
		}

		i = find_next_bit(sec->sw_pending, 64, i+1);
	}

	return IRQ_HANDLED;
}

int
sec_msg_trigger(struct sec_msg *msg, unsigned long data)
{
	if (!msg)
		return -EINVAL;

	sec_write_mem(msg->id, data);

	return 0;
}
EXPORT_SYMBOL(sec_msg_trigger);

void
sec_msg_clear(struct sec_msg *msg)
{
	if (!msg)
		return;

	sec_int_clear(msg);
	sec_int_enable(msg);
}
EXPORT_SYMBOL(sec_msg_clear);

struct sec_msg *
sec_msg_register(sec_callback cb, int id, int sec_flags, void *context)
{
	struct sec_msg *msg = NULL;
	int ret = -EBUSY;
	unsigned long flags;

	if ((id < 0) || (id >= SEC_SEM_COUNT))
		return ERR_PTR(-EINVAL);

	spin_lock_irqsave(&sec->lock, flags);

	if (!(sec->messages[id].flags & SEC_BUSY)) {
		msg = &sec->messages[id];
		msg->cb = cb;
		msg->context = context;
		msg->flags = sec_flags;
		msg->flags |= SEC_BUSY;
		sec_overwrite(msg, (sec_flags & SEC_OVERWRITE));
		if (cb)
			sec_int_enable(msg);
	}

	spin_unlock_irqrestore(&sec->lock, flags);

	if (msg)
		return msg;
	return ERR_PTR(ret);
}
EXPORT_SYMBOL(sec_msg_register);

void
sec_msg_deregister(struct sec_msg *msg)
{
	unsigned long flags;

	if (!msg)
		return;

	spin_lock_irqsave(&sec->lock, flags);

	if (msg->cb)
		sec_int_disable(msg);
	clear_bit(msg->id, sec->sw_pending);
	msg->flags = 0;
	msg->cb = NULL;

	spin_unlock_irqrestore(&sec->lock, flags);
}
EXPORT_SYMBOL(sec_msg_deregister);


static int __init
sec_probe(struct platform_device *pdev)
{
	int i, ret, irq, sw_irq;
	struct pt_regs regs;
	unsigned int fiqasm_len;
	struct resource *res;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res)
		return -EINVAL;

	irq = platform_get_irq(pdev, 0);
	if (irq < 0)
		return irq;

	sec = kzalloc(sizeof(struct sec_dev), GFP_KERNEL);
	if (!sec)
		return -ENOMEM;

	for (i = 0; i < SEC_SEM_COUNT; i++) {
		sec->messages[i].id = i;
		sec->messages[i].flags = 0;
		sec->messages[i].cb = NULL;
	}

	spin_lock_init(&sec->lock);

	sec->irq = irq;

	if (!request_mem_region(res->start, res->end - res->start + 1,
	    pdev->name)) {
		dev_err(&pdev->dev, "failed to request memory resource\n");
		ret = -EBUSY;
		goto out_kfree;
	}

	sec->regs = ioremap_nocache(DMW_SEC_BASE, 0x1000);
	if (!sec->regs) {
		dev_err(&pdev->dev, "failed to map registers\n");
		ret = -ENOMEM;
		goto out_mem;
	}

	ret = claim_fiq(&dw_sec_fiqhandler);
	if (ret) {
		dev_err(&pdev->dev, "cannot claim FIQ\n");
		ret = -EIO;
		goto out_unmap;
	}

	sec->clk = clk_get(&pdev->dev, NULL);
	if (IS_ERR(sec->clk)) {
		dev_err(&pdev->dev, "cannot get clock\n");
		ret = PTR_ERR(sec->clk);
		goto out_fiq;
	}
	clk_enable(sec->clk);

	fiqasm_len = &dw_sec_fiqasm_end - &dw_sec_fiqasm_start;
	set_fiq_handler(&dw_sec_fiqasm_start, fiqasm_len);

	regs.ARM_sp = (u32)dw_sec_fiqstack + sizeof(dw_sec_fiqstack);
	regs.ARM_sp &= ~0x7; /* 8-bit align */
	regs.ARM_r9 = (u32)sec_fiq_handler;
	set_fiq_regs(&regs);

	dmw_set_irq_fiq(sec->irq, 1); /* Tie to FIQ */

	/* register sw interrupt for SEC handlers */
	sw_irq = platform_get_irq(pdev, 1);
	if (sw_irq < 0) {
		ret = sw_irq;
		goto out_fiq;
	}
	sec->sw_irq = sw_irq;
	ret = request_irq(sw_irq, sec_sw_interrupt_handler, 0, "SEC_SW", sec);
	if (ret) {
		dev_err(&pdev->dev, "cannot register software interrupt\n");
		goto out_fiq;
	}

	enable_fiq(sec->irq);
	enable_irq_wake(sec->irq);
	enable_irq_wake(sec->sw_irq);

	return 0;

out_fiq:
	release_fiq(&dw_sec_fiqhandler);
out_unmap:
	iounmap(sec->regs);
out_mem:
	release_mem_region(res->start, res->end - res->start + 1);
out_kfree:
	kfree(sec);

	return ret;
}

static struct platform_driver sec_driver = {
	.driver = {
		.name = "sec",
		.owner = THIS_MODULE,
	},
};

static int __init
sec_init(void)
{
	return platform_driver_probe(&sec_driver, sec_probe);
}

arch_initcall(sec_init);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("DMW96 Semaphore Exchange Center driver");
MODULE_AUTHOR("Andreas Weissel <andreas.weissel@dspg.com>");
