/*
 * AVR32 TLB operations
 *
 * Copyright (C) 2004-2006 Atmel Corporation
 *
 * 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/mm.h>

#include <asm/mmu_context.h>

#define _TLBEHI_I	0x100

void show_dtlb_entry(unsigned int index)
{
	unsigned int tlbehi, tlbehi_save, tlbelo, mmucr, mmucr_save;
	unsigned long flags;

	local_irq_save(flags);
	mmucr_save = sysreg_read(MMUCR);
	tlbehi_save = sysreg_read(TLBEHI);
	mmucr = mmucr_save & 0x13;
	mmucr |= index << 14;
	sysreg_write(MMUCR, mmucr);

	asm volatile("tlbr" : : : "memory");
	cpu_sync_pipeline();

	tlbehi = sysreg_read(TLBEHI);
	tlbelo = sysreg_read(TLBELO);

	printk("%2u: %c %c %02x   %05x %05x %o  %o  %c %c %c %c\n",
	       index,
	       (tlbehi & 0x200)?'1':'0',
	       (tlbelo & 0x100)?'1':'0',
	       (tlbehi & 0xff),
	       (tlbehi >> 12), (tlbelo >> 12),
	       (tlbelo >> 4) & 7, (tlbelo >> 2) & 3,
	       (tlbelo & 0x200)?'1':'0',
	       (tlbelo & 0x080)?'1':'0',
	       (tlbelo & 0x001)?'1':'0',
	       (tlbelo & 0x002)?'1':'0');

	sysreg_write(MMUCR, mmucr_save);
	sysreg_write(TLBEHI, tlbehi_save);
	cpu_sync_pipeline();
	local_irq_restore(flags);
}

void dump_dtlb(void)
{
	unsigned int i;

	printk("ID  V G ASID VPN   PFN   AP SZ C B W D\n");
	for (i = 0; i < 32; i++)
		show_dtlb_entry(i);
}

static unsigned long last_mmucr;

static inline void set_replacement_pointer(unsigned shift)
{
	unsigned long mmucr, mmucr_save;

	mmucr = mmucr_save = sysreg_read(MMUCR);

	/* Does this mapping already exist? */
	__asm__ __volatile__(
		"	tlbs\n"
		"	mfsr %0, %1"
		: "=r"(mmucr)
		: "i"(SYSREG_MMUCR));

	if (mmucr & SYSREG_BIT(MMUCR_N)) {
		/* Not found -- pick a not-recently-accessed entry */
		unsigned long rp;
		unsigned long tlbar = sysreg_read(TLBARLO);

		rp = 32 - fls(tlbar);
		if (rp == 32) {
			rp = 0;
			sysreg_write(TLBARLO, -1L);
		}

		mmucr &= 0x13;
		mmucr |= (rp << shift);

		sysreg_write(MMUCR, mmucr);
	}

	last_mmucr = mmucr;
}

static void update_dtlb(unsigned long address, pte_t pte, unsigned long asid)
{
	unsigned long vpn;

	vpn = (address & MMU_VPN_MASK) | _TLBEHI_VALID | asid;
	sysreg_write(TLBEHI, vpn);
	cpu_sync_pipeline();

	set_replacement_pointer(14);

	sysreg_write(TLBELO, pte_val(pte) & _PAGE_FLAGS_HARDWARE_MASK);

	/* Let's go */
	asm volatile("nop\n\ttlbw" : : : "memory");
	cpu_sync_pipeline();
}

void update_mmu_cache(struct vm_area_struct *vma,
		      unsigned long address, pte_t pte)
{
	unsigned long flags;

	/* ptrace may call this routine */
	if (vma && current->active_mm != vma->vm_mm)
		return;

	local_irq_save(flags);
	update_dtlb(address, pte, get_asid());
	local_irq_restore(flags);
}

void __flush_tlb_page(unsigned long asid, unsigned long page)
{
	unsigned long mmucr, tlbehi;

	page |= asid;
	sysreg_write(TLBEHI, page);
	cpu_sync_pipeline();
	asm volatile("tlbs");
	mmucr = sysreg_read(MMUCR);

	if (!(mmucr & SYSREG_BIT(MMUCR_N))) {
		unsigned long tlbarlo;
		unsigned long entry;

		/* Clear the "valid" bit */
		tlbehi = sysreg_read(TLBEHI);
		tlbehi &= ~_TLBEHI_VALID;
		sysreg_write(TLBEHI, tlbehi);
		cpu_sync_pipeline();

		/* mark the entry as "not accessed" */
		entry = (mmucr >> 14) & 0x3f;
		tlbarlo = sysreg_read(TLBARLO);
		tlbarlo |= (0x80000000 >> entry);
		sysreg_write(TLBARLO, tlbarlo);

		/* update the entry with valid bit clear */
		asm volatile("tlbw");
		cpu_sync_pipeline();
	}
}

void flush_tlb_page(struct vm_area_struct *vma, unsigned long page)
{
	if (vma->vm_mm && vma->vm_mm->context != NO_CONTEXT) {
		unsigned long flags, asid;
		unsigned long saved_asid = MMU_NO_ASID;

		asid = vma->vm_mm->context & MMU_CONTEXT_ASID_MASK;
		page &= PAGE_MASK;

		local_irq_save(flags);
		if (vma->vm_mm != current->mm) {
			saved_asid = get_asid();
			set_asid(asid);
		}

		__flush_tlb_page(asid, page);

		if (saved_asid != MMU_NO_ASID)
			set_asid(saved_asid);
		local_irq_restore(flags);
	}
}

void flush_tlb_range(struct vm_area_struct *vma, unsigned long start,
		     unsigned long end)
{
	struct mm_struct *mm = vma->vm_mm;

	if (mm->context != NO_CONTEXT) {
		unsigned long flags;
		int size;

		local_irq_save(flags);
		size = (end - start + (PAGE_SIZE - 1)) >> PAGE_SHIFT;
		if (size > (MMU_DTLB_ENTRIES / 4)) { /* Too many entries to flush */
			mm->context = NO_CONTEXT;
			if (mm == current->mm)
				activate_context(mm);
		} else {
			unsigned long asid = mm->context & MMU_CONTEXT_ASID_MASK;
			unsigned long saved_asid = MMU_NO_ASID;

			start &= PAGE_MASK;
			end += (PAGE_SIZE - 1);
			end &= PAGE_MASK;
			if (mm != current->mm) {
				saved_asid = get_asid();
				set_asid(asid);
			}

			while (start < end) {
				__flush_tlb_page(asid, start);
				start += PAGE_SIZE;
			}
			if (saved_asid != MMU_NO_ASID)
				set_asid(saved_asid);
		}
		local_irq_restore(flags);
	}
}

/*
 * TODO: If this is only called for addresses > TASK_SIZE, we can probably
 * skip the ASID stuff and just use the Global bit...
 */
void flush_tlb_kernel_range(unsigned long start, unsigned long end)
{
	unsigned long flags;
	int size;

	local_irq_save(flags);
	size = (end - start + (PAGE_SIZE - 1)) >> PAGE_SHIFT;
	if (size > (MMU_DTLB_ENTRIES / 4)) { /* Too many entries to flush */
		flush_tlb_all();
	} else {
		unsigned long asid = init_mm.context & MMU_CONTEXT_ASID_MASK;
		unsigned long saved_asid = get_asid();

		start &= PAGE_MASK;
		end += (PAGE_SIZE - 1);
		end &= PAGE_MASK;
		set_asid(asid);
		while (start < end) {
			__flush_tlb_page(asid, start);
			start += PAGE_SIZE;
		}
		set_asid(saved_asid);
	}
	local_irq_restore(flags);
}

void flush_tlb_mm(struct mm_struct *mm)
{
	/* Invalidate all TLB entries of this process by getting a new ASID */
	if (mm->context != NO_CONTEXT) {
		unsigned long flags;

		local_irq_save(flags);
		mm->context = NO_CONTEXT;
		if (mm == current->mm)
			activate_context(mm);
		local_irq_restore(flags);
	}
}

void flush_tlb_all(void)
{
	unsigned long flags;

	local_irq_save(flags);
	sysreg_write(MMUCR, sysreg_read(MMUCR) | SYSREG_BIT(MMUCR_I));
	local_irq_restore(flags);
}

#ifdef CONFIG_PROC_FS

#include <linux/seq_file.h>
#include <linux/proc_fs.h>
#include <linux/init.h>

static void *tlb_start(struct seq_file *tlb, loff_t *pos)
{
	static unsigned long tlb_index;

	if (*pos >= 32)
		return NULL;

	tlb_index = 0;
	return &tlb_index;
}

static void *tlb_next(struct seq_file *tlb, void *v, loff_t *pos)
{
	unsigned long *index = v;

	if (*index >= 31)
		return NULL;

	++*pos;
	++*index;
	return index;
}

static void tlb_stop(struct seq_file *tlb, void *v)
{

}

static int tlb_show(struct seq_file *tlb, void *v)
{
	unsigned int tlbehi, tlbehi_save, tlbelo, mmucr, mmucr_save;
	unsigned long flags;
	unsigned long *index = v;

	if (*index == 0)
		seq_puts(tlb, "ID  V G ASID VPN   PFN   AP SZ C B W D\n");

	BUG_ON(*index >= 32);

	local_irq_save(flags);
	mmucr_save = sysreg_read(MMUCR);
	tlbehi_save = sysreg_read(TLBEHI);
	mmucr = mmucr_save & 0x13;
	mmucr |= *index << 14;
	sysreg_write(MMUCR, mmucr);

	asm volatile("tlbr" : : : "memory");
	cpu_sync_pipeline();

	tlbehi = sysreg_read(TLBEHI);
	tlbelo = sysreg_read(TLBELO);

	sysreg_write(MMUCR, mmucr_save);
	sysreg_write(TLBEHI, tlbehi_save);
	cpu_sync_pipeline();
	local_irq_restore(flags);

	seq_printf(tlb, "%2lu: %c %c %02x   %05x %05x %o  %o  %c %c %c %c\n",
	       *index,
	       (tlbehi & 0x200)?'1':'0',
	       (tlbelo & 0x100)?'1':'0',
	       (tlbehi & 0xff),
	       (tlbehi >> 12), (tlbelo >> 12),
	       (tlbelo >> 4) & 7, (tlbelo >> 2) & 3,
	       (tlbelo & 0x200)?'1':'0',
	       (tlbelo & 0x080)?'1':'0',
	       (tlbelo & 0x001)?'1':'0',
	       (tlbelo & 0x002)?'1':'0');

	return 0;
}

static struct seq_operations tlb_ops = {
	.start		= tlb_start,
	.next		= tlb_next,
	.stop		= tlb_stop,
	.show		= tlb_show,
};

static int tlb_open(struct inode *inode, struct file *file)
{
	return seq_open(file, &tlb_ops);
}

static struct file_operations proc_tlb_operations = {
	.open		= tlb_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= seq_release,
};

static int __init proctlb_init(void)
{
	struct proc_dir_entry *entry;

	entry = create_proc_entry("tlb", 0, NULL);
	if (entry)
		entry->proc_fops = &proc_tlb_operations;
	return 0;
}
late_initcall(proctlb_init);
#endif /* CONFIG_PROC_FS */
