/**
 ** Proll (PROM replacement)
 ** iommu.c: Functions for DVMA management.
 ** Copyright 1999 Pete Zaitcev
 ** This code is licensed under GNU General Public License.
 **/
#include "config.h"
#include "libopenbios/bindings.h"
#include "libopenbios/ofmem.h"
#include "drivers/drivers.h"
#include "iommu.h"
#include "arch/sparc32/ofmem_sparc32.h"

#ifdef CONFIG_DEBUG_IOMMU
#define DPRINTF(fmt, args...)                   \
    do { printk(fmt , ##args); } while (0)
#else
#define DPRINTF(fmt, args...)
#endif

/*
 * IOMMU parameters
 */
struct iommu {
    struct iommu_regs *regs;
    unsigned int *page_table;
    unsigned long plow;     /* Base bus address */
};

static struct iommu ciommu;

static void
iommu_invalidate(struct iommu_regs *iregs)
{
    iregs->tlbflush = 0;
}

/*
 * XXX This is a problematic interface. We alloc _memory_ which is uncached.
 * So if we ever reuse allocations somebody is going to get uncached pages.
 * Returned address is always aligned by page.
 * BTW, we were not going to give away anonymous storage, were we not?
 */
void *
dvma_alloc(int size, unsigned int *pphys)
{
    void *va;
    unsigned int pa, ba;
    unsigned int npages;
    unsigned int mva, mpa;
    unsigned int i;
    unsigned int *iopte;
    struct iommu *t = &ciommu;
    int ret;

    npages = (size + (PAGE_SIZE-1)) / PAGE_SIZE;
    ret = ofmem_posix_memalign(&va, npages * PAGE_SIZE, PAGE_SIZE);
    if (ret != 0)
        return NULL;

    ba = (unsigned int)mem_alloc(&cdvmem, npages * PAGE_SIZE, PAGE_SIZE);
    if (ba == 0)
        return NULL;

    pa = (unsigned int)va2pa((unsigned long)va);

    /*
     * Change page attributes in MMU to uncached.
     */
    mva = (unsigned int) va;
    mpa = (unsigned int) pa;
    ofmem_arch_map_pages(mpa, mva, npages * PAGE_SIZE, ofmem_arch_io_translation_mode(mpa));

    /*
     * Map into IOMMU page table.
     */
    mpa = (unsigned int) pa;
    iopte = &t->page_table[(ba - t->plow) / PAGE_SIZE];
    for (i = 0; i < npages; i++) {
        *iopte++ = MKIOPTE(mpa);
        mpa += PAGE_SIZE;
    }

    *pphys = ba;

    return va;
}

/*
 * Initialize IOMMU
 * This looks like initialization of CPU MMU but
 * the routine is higher in food chain.
 */
static struct iommu_regs *
iommu_init(struct iommu *t, uint64_t base)
{
    unsigned int *ptab;
    int ptsize;
#ifdef CONFIG_DEBUG_IOMMU
    unsigned int impl, vers;
#endif
    unsigned int tmp;
    struct iommu_regs *regs;
    int ret;
    unsigned long vasize;

    regs = (struct iommu_regs *)ofmem_map_io(base, IOMMU_REGS);
    if (regs == NULL) {
        DPRINTF("Cannot map IOMMU\n");
        for (;;) { }
    }
    t->regs = regs;
#ifdef CONFIG_DEBUG_IOMMU
    impl = (regs->control & IOMMU_CTRL_IMPL) >> 28;
    vers = (regs->control & IOMMU_CTRL_VERS) >> 24;
#endif

    tmp = regs->control;
    tmp &= ~(IOMMU_CTRL_RNGE);

    tmp |= (IOMMU_RNGE_32MB | IOMMU_CTRL_ENAB);
    t->plow = 0xfe000000;		/* End - 32 MB */
    /* Size of VA region that we manage */
    vasize = 0x2000000; /* 32 MB */

    regs->control = tmp;
    iommu_invalidate(regs);

    /* Allocate IOMMU page table */
    /* Tremendous alignment causes great waste... */
    ptsize = (vasize / PAGE_SIZE) * sizeof(int);
    ret = ofmem_posix_memalign((void *)&ptab, ptsize, ptsize);
    if (ret != 0) {
        DPRINTF("Cannot allocate IOMMU table [0x%x]\n", ptsize);
        for (;;) { }
    }
    t->page_table = ptab;

    /* flush_cache_all(); */
    /** flush_tlb_all(); **/
    tmp = (unsigned int)va2pa((unsigned long)ptab);
    regs->base = tmp >> 4;
    iommu_invalidate(regs);

    DPRINTF("IOMMU: impl %d vers %d page table at 0x%p (pa 0x%x) of size %d bytes\n",
            impl, vers, t->page_table, tmp, ptsize);

    mem_init(&cdvmem, (char*)t->plow, (char *)0xfffff000);
    return regs;
}

/* ( addr.lo addr.hi size -- virt ) */

static void
ob_iommu_map_in(void)
{
    phys_addr_t phys;
    ucell size, virt;

    size = POP();
    phys = POP();
    phys = (phys << 32) + POP();

    virt = ofmem_map_io(phys, size);

    PUSH(virt);
}

/* ( virt size ) */

static void
ob_iommu_map_out(void)
{
    ucell size = POP();
    ucell virt = POP();

    ofmem_release_io(virt, size);
}

void
ob_init_iommu(uint64_t base)
{
    struct iommu_regs *regs;

    regs = iommu_init(&ciommu, base);

    push_str("/iommu");
    fword("find-device");
    PUSH((unsigned long)regs);
    fword("encode-int");
    push_str("address");
    fword("property");

    PUSH(base >> 32);
    fword("encode-int");
    PUSH(base & 0xffffffff);
    fword("encode-int");
    fword("encode+");
    PUSH(IOMMU_REGS);
    fword("encode-int");
    fword("encode+");
    push_str("reg");
    fword("property");

    bind_func("map-in", ob_iommu_map_in);
    bind_func("map-out", ob_iommu_map_out);
}