/*
 * linux/arch/arm/mach-omap2/mmu.c
 *
 * Support for non-MPU OMAP2 MMUs.
 *
 * Copyright (C) 2002-2007 Nokia Corporation
 *
 * Written by Toshihiro Kobayashi <toshihiro.kobayashi@nokia.com>
 *        and Paul Mundt <paul.mundt@nokia.com>
 *
 * TWL support: Hiroshi DOYU <Hiroshi.DOYU@nokia.com>
 *
 * 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.
 */
#include <linux/types.h>
#include <linux/init.h>
#include <linux/rwsem.h>
#include <linux/device.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/err.h>
#include "mmu.h"
#include <asm/arch/mmu.h>
#include <asm/tlbflush.h>
#include <asm/io.h>
#include <asm/sizes.h>

static void *dspvect_page;
#define DSP_INIT_PAGE	0xfff000

static inline void
omap2_mmu_read_tlb(struct omap_mmu *mmu, struct cam_ram_regset *cr)
{
	cr->cam = omap_mmu_read_reg(mmu, OMAP_MMU_READ_CAM);
	cr->ram = omap_mmu_read_reg(mmu, OMAP_MMU_READ_RAM);
}

static inline void
omap2_mmu_load_tlb(struct omap_mmu *mmu, struct cam_ram_regset *cr)
{
	/* Set the CAM and RAM entries */
	omap_mmu_write_reg(mmu, cr->cam | OMAP_MMU_CAM_V, OMAP_MMU_CAM);
	omap_mmu_write_reg(mmu, cr->ram, OMAP_MMU_RAM);
}

static void exmap_setup_iomap_page(struct omap_mmu *mmu, unsigned long phys,
				   unsigned long dsp_io_adr, int index)
{
	unsigned long dspadr;
	void *virt;
	struct omap_mmu_tlb_entry tlb_ent;

	dspadr = (IOMAP_VAL << 18) + (dsp_io_adr << 1);
	virt = omap_mmu_to_virt(mmu, dspadr);
	exmap_set_armmmu(mmu, (unsigned long)virt, phys, PAGE_SIZE);
	INIT_EXMAP_TBL_ENTRY_4KB_PRESERVED(mmu->exmap_tbl + index, NULL, virt);
	INIT_TLB_ENTRY_4KB_ES32_PRESERVED(&tlb_ent, dspadr, phys);
	omap_mmu_load_pte_entry(mmu, &tlb_ent);
}

static void exmap_clear_iomap_page(struct omap_mmu *mmu,
				   unsigned long dsp_io_adr)
{
	unsigned long dspadr;
	void *virt;

	dspadr = (IOMAP_VAL << 18) + (dsp_io_adr << 1);
	virt = omap_mmu_to_virt(mmu, dspadr);
	exmap_clear_armmmu(mmu, (unsigned long)virt, PAGE_SIZE);
	/* DSP MMU is shutting down. not handled here. */
}

#define OMAP24XX_MAILBOX_BASE	(L4_24XX_BASE + 0x94000)
#define OMAP2420_GPT5_BASE	(L4_24XX_BASE + 0x7c000)
#define OMAP2420_GPT6_BASE	(L4_24XX_BASE + 0x7e000)
#define OMAP2420_GPT7_BASE	(L4_24XX_BASE + 0x80000)
#define OMAP2420_GPT8_BASE	(L4_24XX_BASE + 0x82000)
#define OMAP24XX_EAC_BASE	(L4_24XX_BASE + 0x90000)
#define OMAP24XX_STI_BASE	(L4_24XX_BASE + 0x68000)
#define OMAP24XX_STI_CH_BASE	(L4_24XX_BASE + 0x0c000000)

static int exmap_setup_preserved_entries(struct omap_mmu *mmu)
{
	int i, n = 0;

	exmap_setup_preserved_mem_page(mmu, dspvect_page, DSP_INIT_PAGE, n++);

	/* REVISIT: This will need to be revisited for 3430 */
	exmap_setup_iomap_page(mmu, OMAP2_PRCM_BASE, 0x7000, n++);
	exmap_setup_iomap_page(mmu, OMAP24XX_MAILBOX_BASE, 0x11000, n++);

	if (cpu_is_omap2420()) {
		exmap_setup_iomap_page(mmu, OMAP2420_GPT5_BASE, 0xe000, n++);
		exmap_setup_iomap_page(mmu, OMAP2420_GPT6_BASE, 0xe800, n++);
		exmap_setup_iomap_page(mmu, OMAP2420_GPT7_BASE, 0xf000, n++);
		exmap_setup_iomap_page(mmu, OMAP2420_GPT8_BASE, 0xf800, n++);
		exmap_setup_iomap_page(mmu, OMAP24XX_EAC_BASE,  0x10000, n++);
		exmap_setup_iomap_page(mmu, OMAP24XX_STI_BASE, 0xc800, n++);
		for (i = 0; i < 5; i++)
			exmap_setup_preserved_mem_page(mmu,
				__va(OMAP24XX_STI_CH_BASE + i*SZ_4K),
				0xfb0000 + i*SZ_4K, n++);
	}

	return n;
}

static void exmap_clear_preserved_entries(struct omap_mmu *mmu)
{
	int i;

	exmap_clear_iomap_page(mmu, 0x7000);	/* PRCM registers */
	exmap_clear_iomap_page(mmu, 0x11000);	/* MAILBOX registers */

	if (cpu_is_omap2420()) {
		exmap_clear_iomap_page(mmu, 0xe000);	/* GPT5 */
		exmap_clear_iomap_page(mmu, 0xe800);	/* GPT6 */
		exmap_clear_iomap_page(mmu, 0xf000);	/* GPT7 */
		exmap_clear_iomap_page(mmu, 0xf800);	/* GPT8 */
		exmap_clear_iomap_page(mmu, 0x10000);	/* EAC */
		exmap_clear_iomap_page(mmu, 0xc800);	/* STI */
		for (i = 0; i < 5; i++)			/* STI CH */
			exmap_clear_mem_page(mmu, 0xfb0000 + i*SZ_4K);
	}

	exmap_clear_mem_page(mmu, DSP_INIT_PAGE);
}

#define MMU_IRQ_MASK \
	(OMAP_MMU_IRQ_MULTIHITFAULT | \
	 OMAP_MMU_IRQ_TABLEWALKFAULT | \
	 OMAP_MMU_IRQ_EMUMISS | \
	 OMAP_MMU_IRQ_TRANSLATIONFAULT)

static int omap2_mmu_startup(struct omap_mmu *mmu)
{
	u32 rev = omap_mmu_read_reg(mmu, OMAP_MMU_REVISION);

	pr_info("MMU: OMAP %s MMU initialized (HW v%d.%d)\n", mmu->name,
		(rev >> 4) & 0xf, rev & 0xf);

	dspvect_page = (void *)__get_dma_pages(GFP_KERNEL, 0);
	if (dspvect_page == NULL) {
		dev_err(mmu->dev, "MMU %s: failed to allocate memory "
			"for vector table\n", mmu->name);
		return -ENOMEM;
	}

	mmu->nr_exmap_preserved = exmap_setup_preserved_entries(mmu);

	omap_mmu_write_reg(mmu, MMU_IRQ_MASK, OMAP_MMU_IRQENABLE);

	return 0;
}

static void omap2_mmu_shutdown(struct omap_mmu *mmu)
{
	exmap_clear_preserved_entries(mmu);

	if (dspvect_page != NULL) {
		unsigned long virt;

		down_read(&mmu->exmap_sem);

		virt = (unsigned long)omap_mmu_to_virt(mmu, DSP_INIT_PAGE);
		flush_tlb_kernel_range(virt, virt + PAGE_SIZE);
		free_page((unsigned long)dspvect_page);
		dspvect_page = NULL;

		up_read(&mmu->exmap_sem);
	}
}

static ssize_t omap2_mmu_show(struct omap_mmu *mmu, char *buf,
			      struct omap_mmu_tlb_lock *tlb_lock)
{
	int i, len;

	len = sprintf(buf, "P: preserved, V: valid\n"
			   "B: big endian, L:little endian, "
			   "M: mixed page attribute\n"
			   "ety P V size   cam_va     ram_pa E ES M\n");
			 /* 00: P V  4KB 0x300000 0x10171800 B 16 M */

	for (i = 0; i < mmu->nr_tlb_entries; i++) {
		struct omap_mmu_tlb_entry ent;
		struct cam_ram_regset cr;
		struct omap_mmu_tlb_lock entry_lock;
		char *pgsz_str, *elsz_str;

		/* read a TLB entry */
		entry_lock.base   = tlb_lock->base;
		entry_lock.victim = i;
		omap_mmu_read_tlb(mmu, &entry_lock, &cr);

		ent.pgsz   = cr.cam & OMAP_MMU_CAM_PAGESIZE_MASK;
		ent.prsvd  = cr.cam & OMAP_MMU_CAM_P;
		ent.valid  = cr.cam & OMAP_MMU_CAM_V;
		ent.va     = cr.cam & OMAP_MMU_CAM_VATAG_MASK;
		ent.endian = cr.ram & OMAP_MMU_RAM_ENDIANNESS;
		ent.elsz   = cr.ram & OMAP_MMU_RAM_ELEMENTSIZE_MASK;
		ent.pa     = cr.ram & OMAP_MMU_RAM_PADDR_MASK;
		ent.mixed  = cr.ram & OMAP_MMU_RAM_MIXED;

		pgsz_str = (ent.pgsz == OMAP_MMU_CAM_PAGESIZE_16MB) ? "64MB":
			   (ent.pgsz == OMAP_MMU_CAM_PAGESIZE_1MB)  ? " 1MB":
			   (ent.pgsz == OMAP_MMU_CAM_PAGESIZE_64KB) ? "64KB":
			   (ent.pgsz == OMAP_MMU_CAM_PAGESIZE_4KB)  ? " 4KB":
								     " ???";
		elsz_str = (ent.elsz == OMAP_MMU_RAM_ELEMENTSIZE_8)  ? " 8":
			   (ent.elsz == OMAP_MMU_RAM_ELEMENTSIZE_16) ? "16":
			   (ent.elsz == OMAP_MMU_RAM_ELEMENTSIZE_32) ? "32":
								      "??";

		if (i == tlb_lock->base)
			len += sprintf(buf + len, "lock base = %d\n",
				       tlb_lock->base);
		if (i == tlb_lock->victim)
			len += sprintf(buf + len, "victim    = %d\n",
				       tlb_lock->victim);

		len += sprintf(buf + len,
			       /* 00: P V  4KB 0x300000 0x10171800 B 16 M */
			       "%02d: %c %c %s 0x%06lx 0x%08lx %c %s %c\n",
			       i,
			       ent.prsvd ? 'P' : ' ',
			       ent.valid ? 'V' : ' ',
			       pgsz_str, ent.va, ent.pa,
			       ent.endian ? 'B' : 'L',
			       elsz_str,
			       ent.mixed ? 'M' : ' ');
	}

	return len;
}

#define get_cam_va_mask(pgsz) \
	(((pgsz) == OMAP_MMU_CAM_PAGESIZE_16MB) ? 0xff000000 : \
	 ((pgsz) == OMAP_MMU_CAM_PAGESIZE_1MB)  ? 0xfff00000 : \
	 ((pgsz) == OMAP_MMU_CAM_PAGESIZE_64KB) ? 0xffff0000 : \
	 ((pgsz) == OMAP_MMU_CAM_PAGESIZE_4KB)  ? 0xfffff000 : 0)

static inline unsigned long omap2_mmu_cam_va(struct cam_ram_regset *cr)
{
	unsigned int page_size = cr->cam & OMAP_MMU_CAM_PAGESIZE_MASK;
	unsigned int mask = get_cam_va_mask(cr->cam & page_size);

	return cr->cam & mask;
}

static struct cam_ram_regset *
omap2_mmu_cam_ram_alloc(struct omap_mmu *mmu, struct omap_mmu_tlb_entry *entry)
{
	struct cam_ram_regset *cr;

	if (entry->va & ~(get_cam_va_mask(entry->pgsz))) {
		dev_err(mmu->dev, "MMU %s: mapping vadr (0x%06lx) is not on"
			" an aligned boundary\n", mmu->name, entry->va);
		return ERR_PTR(-EINVAL);
	}

	cr = kmalloc(sizeof(struct cam_ram_regset), GFP_KERNEL);

	cr->cam = (entry->va & OMAP_MMU_CAM_VATAG_MASK) |
		  entry->prsvd | entry->pgsz;
	cr->ram = entry->pa | entry->endian | entry->elsz;

	return cr;
}

static inline int omap2_mmu_cam_ram_valid(struct cam_ram_regset *cr)
{
	return cr->cam & OMAP_MMU_CAM_V;
}

static void omap2_mmu_interrupt(struct omap_mmu *mmu)
{
	unsigned long status, va;

	status = MMU_IRQ_MASK & omap_mmu_read_reg(mmu, OMAP_MMU_IRQSTATUS);
	va = omap_mmu_read_reg(mmu, OMAP_MMU_FAULT_AD);

	pr_info("%s\n", (status & OMAP_MMU_IRQ_MULTIHITFAULT)?
		"multi hit":"");
	pr_info("%s\n", (status & OMAP_MMU_IRQ_TABLEWALKFAULT)?
		"table walk fault":"");
	pr_info("%s\n", (status & OMAP_MMU_IRQ_EMUMISS)?
		"EMU miss":"");
	pr_info("%s\n", (status & OMAP_MMU_IRQ_TRANSLATIONFAULT)?
		"translation fault":"");
	pr_info("%s\n", (status & OMAP_MMU_IRQ_TLBMISS)?
		"TLB miss":"");
	pr_info("fault address = %#08lx\n", va);

	omap_mmu_disable(mmu);
	omap_mmu_write_reg(mmu, status, OMAP_MMU_IRQSTATUS);

	mmu->fault_address = va;
	schedule_work(&mmu->irq_work);
}

static pgprot_t omap2_mmu_pte_get_attr(struct omap_mmu_tlb_entry *entry)
{
	u32 attr;

	attr = entry->mixed << 5;
	attr |= entry->endian;
	attr |= entry->elsz >> 3;
	attr <<= ((entry->pgsz & OMAP_MMU_CAM_PAGESIZE_4KB) ? 0:6);

	return attr;
}

struct omap_mmu_ops omap2_mmu_ops = {
	.startup	= omap2_mmu_startup,
	.shutdown	= omap2_mmu_shutdown,
	.read_tlb	= omap2_mmu_read_tlb,
	.load_tlb	= omap2_mmu_load_tlb,
	.show		= omap2_mmu_show,
	.cam_va		= omap2_mmu_cam_va,
	.cam_ram_alloc	= omap2_mmu_cam_ram_alloc,
	.cam_ram_valid	= omap2_mmu_cam_ram_valid,
	.interrupt	= omap2_mmu_interrupt,
	.pte_get_attr	= omap2_mmu_pte_get_attr,
};
EXPORT_SYMBOL_GPL(omap2_mmu_ops);

MODULE_LICENSE("GPL");
