/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */
/*
 * Copyright 2012 Garrett D'Amore <garrett@damore.org>.  All rights reserved.
 */

/*
 * PCI iommu initialization and configuration
 */

#include <sys/types.h>
#include <sys/kmem.h>
#include <sys/async.h>
#include <sys/sysmacros.h>
#include <sys/sunddi.h>
#include <sys/ddi_impldefs.h>
#include <sys/vmem.h>
#include <sys/machsystm.h>	/* lddphys() */
#include <sys/iommutsb.h>
#include <sys/pci/pci_obj.h>

/*LINTLIBRARY*/

static void iommu_tlb_flushall(iommu_t *iommu_p);
static void iommu_preserve_tsb(iommu_t *iommu_p);

void
iommu_create(pci_t *pci_p)
{
	dev_info_t *dip = pci_p->pci_dip;
	iommu_t *iommu_p;
	uintptr_t a;
	size_t cache_size;
	uint32_t tsb_entries;

	char map_name[32];
	extern uint64_t va_to_pa(void *);

	pci_dvma_range_prop_t	pci_dvma_range;

	/*
	 * Allocate iommu state structure and link it to the
	 * pci state structure.
	 */
	iommu_p = (iommu_t *)kmem_zalloc(sizeof (iommu_t), KM_SLEEP);
	pci_p->pci_iommu_p = iommu_p;
	iommu_p->iommu_pci_p = pci_p;
	iommu_p->iommu_inst = ddi_get_instance(dip);

	/*
	 * chip specific dvma_end, tsb_size & context support
	 */
	iommu_p->iommu_dvma_end = pci_iommu_dvma_end;
	a = pci_iommu_setup(iommu_p);

	/*
	 * Determine the virtual address of iommu registers.
	 */
	iommu_p->iommu_ctrl_reg =
	    (uint64_t *)(a + COMMON_IOMMU_CTRL_REG_OFFSET);
	iommu_p->iommu_tsb_base_addr_reg =
	    (uint64_t *)(a + COMMON_IOMMU_TSB_BASE_ADDR_REG_OFFSET);
	iommu_p->iommu_flush_page_reg =
	    (uint64_t *)(a + COMMON_IOMMU_FLUSH_PAGE_REG_OFFSET);

	/*
	 * Configure the rest of the iommu parameters according to:
	 * tsb_size and dvma_end
	 */
	iommu_p->iommu_tsb_vaddr = /* retrieve TSB VA reserved by system */
	    iommu_tsb_cookie_to_va(pci_p->pci_tsb_cookie);
	iommu_p->iommu_tsb_entries = tsb_entries =
	    IOMMU_TSBSIZE_TO_TSBENTRIES(iommu_p->iommu_tsb_size);
	iommu_p->iommu_tsb_paddr = va_to_pa((caddr_t)iommu_p->iommu_tsb_vaddr);
	iommu_p->iommu_dvma_cache_locks =
	    kmem_zalloc(pci_dvma_page_cache_entries, KM_SLEEP);

	iommu_p->iommu_dvma_base = iommu_p->iommu_dvma_end + 1
	    - (tsb_entries * IOMMU_PAGE_SIZE);
	iommu_p->dvma_base_pg = IOMMU_BTOP(iommu_p->iommu_dvma_base);
	iommu_p->iommu_dvma_reserve = tsb_entries >> 1;
	iommu_p->dvma_end_pg = IOMMU_BTOP(iommu_p->iommu_dvma_end);
	iommu_p->iommu_dma_bypass_base = COMMON_IOMMU_BYPASS_BASE;
	iommu_p->iommu_dma_bypass_end = pci_iommu_bypass_end_configure();

	/*
	 * export "virtual-dma" software property to support
	 * child devices needing to know DVMA range
	 */
	pci_dvma_range.dvma_base = (uint32_t)iommu_p->iommu_dvma_base;
	pci_dvma_range.dvma_len = (uint32_t)
	    iommu_p->iommu_dvma_end - iommu_p->iommu_dvma_base + 1;
	(void) ddi_prop_create(DDI_DEV_T_NONE, dip, DDI_PROP_CANSLEEP,
	    "virtual-dma", (caddr_t)&pci_dvma_range,
	    sizeof (pci_dvma_range));

	DEBUG2(DBG_ATTACH, dip, "iommu_create: ctrl=%p, tsb=%p\n",
	    iommu_p->iommu_ctrl_reg, iommu_p->iommu_tsb_base_addr_reg);
	DEBUG2(DBG_ATTACH, dip, "iommu_create: page_flush=%p, ctx_flush=%p\n",
	    iommu_p->iommu_flush_page_reg, iommu_p->iommu_flush_ctx_reg);
	DEBUG2(DBG_ATTACH, dip, "iommu_create: tsb vaddr=%p tsb_paddr=%p\n",
	    iommu_p->iommu_tsb_vaddr, iommu_p->iommu_tsb_paddr);
	DEBUG1(DBG_ATTACH, dip, "iommu_create: allocated size=%x\n",
	    iommu_tsb_cookie_to_size(pci_p->pci_tsb_cookie));
	DEBUG2(DBG_ATTACH, dip, "iommu_create: fast tsb tte addr: %x + %x\n",
	    iommu_p->iommu_tsb_vaddr,
	    pci_dvma_page_cache_entries * pci_dvma_page_cache_clustsz);
	DEBUG3(DBG_ATTACH, dip,
	    "iommu_create: tsb size=%x, tsb entries=%x, dvma base=%x\n",
	    iommu_p->iommu_tsb_size, iommu_p->iommu_tsb_entries,
	    iommu_p->iommu_dvma_base);
	DEBUG2(DBG_ATTACH, dip,
	    "iommu_create: dvma_cache_locks=%x cache_entries=%x\n",
	    iommu_p->iommu_dvma_cache_locks, pci_dvma_page_cache_entries);

	/*
	 * zero out the area to be used for iommu tsb
	 */
	bzero(iommu_p->iommu_tsb_vaddr, tsb_entries << 3);

	/*
	 * Create a virtual memory map for dvma address space.
	 * Reserve 'size' bytes of low dvma space for fast track cache.
	 */
	(void) snprintf(map_name, sizeof (map_name), "%s%d_dvma",
	    ddi_driver_name(dip), ddi_get_instance(dip));

	cache_size = IOMMU_PTOB(pci_dvma_page_cache_entries *
	    pci_dvma_page_cache_clustsz);
	iommu_p->iommu_dvma_fast_end = iommu_p->iommu_dvma_base +
	    cache_size - 1;
	iommu_p->iommu_dvma_map = vmem_create(map_name,
	    (void *)(iommu_p->iommu_dvma_fast_end + 1),
	    IOMMU_PTOB(tsb_entries) - cache_size, IOMMU_PAGE_SIZE,
	    NULL, NULL, NULL, IOMMU_PAGE_SIZE, VM_SLEEP);

	mutex_init(&iommu_p->dvma_debug_lock, NULL, MUTEX_DRIVER, NULL);

	/*
	 * On detach, the TSB Base Address Register gets set to zero,
	 * so if its zero here, there is no need to preserve TTEs.
	 */
	if (pci_preserve_iommu_tsb && *iommu_p->iommu_tsb_base_addr_reg)
		iommu_preserve_tsb(iommu_p);

	iommu_configure(iommu_p);
}

void
iommu_destroy(pci_t *pci_p)
{
#ifdef DEBUG
	dev_info_t *dip = pci_p->pci_dip;
#endif
	iommu_t *iommu_p = pci_p->pci_iommu_p;
	volatile uint64_t ctl_val = *iommu_p->iommu_ctrl_reg;

	DEBUG0(DBG_DETACH, dip, "iommu_destroy:\n");

	/*
	 * Disable the IOMMU by setting the TSB Base Address to zero
	 * and the TSB Table size to the smallest possible.
	 */
	ctl_val = ctl_val & ~(7 << COMMON_IOMMU_CTRL_TSB_SZ_SHIFT);

	*iommu_p->iommu_ctrl_reg = ctl_val;
	*iommu_p->iommu_tsb_base_addr_reg = 0;

	/*
	 * Return the boot time allocated tsb.
	 */
	iommu_tsb_free(pci_p->pci_tsb_cookie);

	/*
	 * Teardown any implementation-specific structures set up in
	 * pci_iommu_setup.
	 */
	pci_iommu_teardown(iommu_p);

	if (DVMA_DBG_ON(iommu_p))
		pci_dvma_debug_fini(iommu_p);
	mutex_destroy(&iommu_p->dvma_debug_lock);

	/*
	 * Free the dvma resource map.
	 */
	vmem_destroy(iommu_p->iommu_dvma_map);

	kmem_free(iommu_p->iommu_dvma_cache_locks,
	    pci_dvma_page_cache_entries);

	/*
	 * Free the iommu state structure.
	 */
	kmem_free(iommu_p, sizeof (iommu_t));
	pci_p->pci_iommu_p = NULL;
}

/*
 * re-program iommu on the fly while preserving on-going dma
 * transactions on the PCI bus.
 */
void
iommu_configure(iommu_t *iommu_p)
{
	pci_t *pci_p = iommu_p->iommu_pci_p;
	uint64_t cfgpa = pci_get_cfg_pabase(pci_p);
	dev_info_t *dip = iommu_p->iommu_pci_p->pci_dip;
	dev_info_t *cdip = NULL;
	volatile uint64_t ctl_val = (uint64_t)
	    ((iommu_p->iommu_tsb_size << COMMON_IOMMU_CTRL_TSB_SZ_SHIFT) |
	    (0 /* 8k page */ << COMMON_IOMMU_CTRL_TBW_SZ_SHIFT) |
	    COMMON_IOMMU_CTRL_ENABLE | COMMON_IOMMU_CTRL_DIAG_ENABLE |
	    (pci_lock_tlb ? COMMON_IOMMU_CTRL_LCK_ENABLE : 0));

	DEBUG2(DBG_ATTACH, dip, "iommu_configure: iommu_ctl=%08x.%08x\n",
	    HI32(ctl_val), LO32(ctl_val));
	if (!pci_preserve_iommu_tsb || !(*iommu_p->iommu_tsb_base_addr_reg)) {
		*iommu_p->iommu_ctrl_reg = COMMON_IOMMU_CTRL_DIAG_ENABLE;
		iommu_tlb_flushall(iommu_p);
		goto config;
	}
	cdip = ddi_get_child(dip);
	for (; cdip; cdip = ddi_get_next_sibling(cdip)) {
		uint32_t *reg_p;
		int reg_len;
		if (ddi_getlongprop(DDI_DEV_T_ANY, cdip, DDI_PROP_DONTPASS,
		    "reg", (caddr_t)&reg_p, &reg_len) != DDI_PROP_SUCCESS)
			continue;
		cfgpa += (*reg_p) & (PCI_CONF_ADDR_MASK ^ PCI_REG_REG_M);
		kmem_free(reg_p, reg_len);
		break;
	}

config:
	pci_iommu_config(iommu_p, ctl_val, cdip ? cfgpa : 0);
}

void
iommu_map_pages(iommu_t *iommu_p, ddi_dma_impl_t *mp,
		dvma_addr_t dvma_pg, size_t npages, size_t pfn_index)
{
	int i;
	dvma_addr_t pg_index = dvma_pg - iommu_p->dvma_base_pg;
	uint64_t *tte_addr = iommu_p->iommu_tsb_vaddr + pg_index;
	size_t pfn_last = pfn_index + npages;
	uint64_t tte = PCI_GET_MP_TTE(mp->dmai_tte);
#ifdef DEBUG
	dev_info_t *dip = iommu_p->iommu_pci_p->pci_dip;
#endif

	ASSERT(pfn_last <= mp->dmai_ndvmapages);

	DEBUG5(DBG_MAP_WIN, dip,
	    "iommu_map_pages:%x+%x=%x npages=0x%x pfn_index=0x%x\n",
	    (uint_t)iommu_p->dvma_base_pg, (uint_t)pg_index, dvma_pg,
	    (uint_t)npages, (uint_t)pfn_index);

	for (i = pfn_index; i < pfn_last; i++, pg_index++, tte_addr++) {
		iopfn_t pfn = PCI_GET_MP_PFN(mp, i);
		volatile uint64_t cur_tte = IOMMU_PTOB(pfn) | tte;

		DEBUG3(DBG_MAP_WIN, dip, "iommu_map_pages: mp=%p pg[%x]=%x\n",
		    mp, i, (uint_t)pfn);
		DEBUG3(DBG_MAP_WIN, dip,
		    "iommu_map_pages: pg_index=%x tte=%08x.%08x\n",
		    pg_index, HI32(cur_tte), LO32(cur_tte));
		ASSERT(TTE_IS_INVALID(*tte_addr));
		*tte_addr = cur_tte;
#ifdef DEBUG
		if (pfn == 0 && pci_warn_pp0)
			cmn_err(CE_WARN, "%s%d <%p> doing DMA to pp0\n",
			    ddi_driver_name(mp->dmai_rdip),
			    ddi_get_instance(mp->dmai_rdip), mp);
#endif
	}
	ASSERT(tte_addr == iommu_p->iommu_tsb_vaddr + pg_index);
#ifdef DEBUG
	if (HAS_REDZONE(mp)) {
		DEBUG1(DBG_MAP_WIN, dip, "iommu_map_pages: redzone pg=%x\n",
		    pg_index);
		ASSERT(TTE_IS_INVALID(iommu_p->iommu_tsb_vaddr[pg_index]));
	}
#endif
	if (DVMA_DBG_ON(iommu_p))
		pci_dvma_alloc_debug(iommu_p, (char *)mp->dmai_mapping,
		    mp->dmai_size, mp);
}

/*
 * iommu_map_window - map a dvma window into the iommu
 *
 * used by: pci_dma_win(), pci_dma_ctlops() - DDI_DMA_MOVWIN
 *
 * return value: none
 */
/*ARGSUSED*/
void
iommu_map_window(iommu_t *iommu_p, ddi_dma_impl_t *mp, window_t win_no)
{
	uint32_t obj_pg0_off = mp->dmai_roffset;
	uint32_t win_pg0_off = win_no ? 0 : obj_pg0_off;
	size_t win_size = mp->dmai_winsize;
	size_t pfn_index = win_size * win_no;			/* temp value */
	size_t obj_off = win_no ? pfn_index - obj_pg0_off : 0;	/* xferred sz */
	dvma_addr_t dvma_pg = IOMMU_BTOP(mp->dmai_mapping);
	size_t res_size = mp->dmai_object.dmao_size - obj_off + win_pg0_off;

	ASSERT(!(win_size & IOMMU_PAGE_OFFSET));
	if (win_no >= mp->dmai_nwin)
		return;
	if (res_size < win_size)		/* last window */
		win_size = res_size;		/* mp->dmai_winsize unchanged */

	mp->dmai_mapping = IOMMU_PTOB(dvma_pg) | win_pg0_off;
	mp->dmai_size = win_size - win_pg0_off;	/* cur win xferrable size */
	mp->dmai_offset = obj_off;		/* win offset into object */
	pfn_index = IOMMU_BTOP(pfn_index);	/* index into pfnlist */
	iommu_map_pages(iommu_p, mp, dvma_pg, IOMMU_BTOPR(win_size), pfn_index);
}

void
iommu_unmap_pages(iommu_t *iommu_p, dvma_addr_t dvma_pg, uint_t npages)
{
	dvma_addr_t pg_index = IOMMU_PAGE_INDEX(iommu_p, dvma_pg);

	for (; npages; npages--, dvma_pg++, pg_index++) {
		DEBUG1(DBG_UNMAP_WIN|DBG_CONT, 0, " %x", dvma_pg);
		IOMMU_UNLOAD_TTE(iommu_p, pg_index);

		if (!tm_mtlb_gc)
			IOMMU_PAGE_FLUSH(iommu_p, dvma_pg);
	}
}

void
iommu_remap_pages(iommu_t *iommu_p, ddi_dma_impl_t *mp, dvma_addr_t dvma_pg,
	size_t npages, size_t pfn_index)
{
	iommu_unmap_pages(iommu_p, dvma_pg, npages);
	iommu_map_pages(iommu_p, mp, dvma_pg, npages, pfn_index);
}

/*
 * iommu_unmap_window
 *
 * This routine is called to break down the iommu mappings to a dvma window.
 * Non partial mappings are viewed as single window mapping.
 *
 * used by: pci_dma_unbindhdl(), pci_dma_window(),
 *	and pci_dma_ctlops() - DDI_DMA_FREE, DDI_DMA_MOVWIN, DDI_DMA_NEXTWIN
 *
 * return value: none
 */
/*ARGSUSED*/
void
iommu_unmap_window(iommu_t *iommu_p, ddi_dma_impl_t *mp)
{
	dvma_addr_t dvma_pg = IOMMU_BTOP(mp->dmai_mapping);
	dvma_addr_t pg_index = IOMMU_PAGE_INDEX(iommu_p, dvma_pg);
	uint_t npages = IOMMU_BTOP(mp->dmai_winsize);
#ifdef DEBUG
	dev_info_t *dip = iommu_p->iommu_pci_p->pci_dip;
#endif
	/*
	 * Invalidate each page of the mapping in the tsb and flush
	 * it from the tlb.
	 */
	DEBUG2(DBG_UNMAP_WIN, dip, "mp=%p %x pfns:", mp, npages);
	if (mp->dmai_flags & DMAI_FLAGS_CONTEXT) {
		dvma_context_t ctx = MP2CTX(mp);
		for (; npages; npages--, pg_index++) {
			DEBUG1(DBG_UNMAP_WIN|DBG_CONT, dip, " %x", pg_index);
			IOMMU_UNLOAD_TTE(iommu_p, pg_index);
		}
		DEBUG1(DBG_UNMAP_WIN|DBG_CONT, dip, " (context %x)", ctx);
		*iommu_p->iommu_flush_ctx_reg = ctx;
	} else
		iommu_unmap_pages(iommu_p, dvma_pg, npages);

	DEBUG0(DBG_UNMAP_WIN|DBG_CONT, dip, "\n");

	if (DVMA_DBG_ON(iommu_p))
		pci_dvma_free_debug(iommu_p, (char *)mp->dmai_mapping,
		    mp->dmai_size, mp);
}

int
pci_alloc_tsb(pci_t *pci_p)
{
	uint16_t tsbc;

	if ((tsbc = iommu_tsb_alloc(pci_p->pci_id)) == IOMMU_TSB_COOKIE_NONE) {
		cmn_err(CE_WARN, "%s%d: Unable to allocate IOMMU TSB.",
		    ddi_driver_name(pci_p->pci_dip),
		    ddi_get_instance(pci_p->pci_dip));
		return (DDI_FAILURE);
	}
	pci_p->pci_tsb_cookie = tsbc;
	return (DDI_SUCCESS);
}

void
pci_free_tsb(pci_t *pci_p)
{
	iommu_tsb_free(pci_p->pci_tsb_cookie);
}

#if 0
/*
 * The following data structure is used to map a tsb size
 * to a tsb size configuration parameter in the iommu
 * control register.
 * This is a hardware table. It is here for reference only.
 */
static int pci_iommu_tsb_sizes[] = {
	0x2000,		/* 0 - 8 mb */
	0x4000,		/* 1 - 16 mb */
	0x8000,		/* 2 - 32 mb */
	0x10000,	/* 3 - 64 mb */
	0x20000,	/* 4 - 128 mb */
	0x40000,	/* 5 - 256 mb */
	0x80000,	/* 6 - 512 mb */
	0x100000	/* 7 - 1 gb */
};
#endif

uint_t
iommu_tsb_size_encode(uint_t tsb_bytes)
{
	uint_t i;

	for (i = 7; i && (tsb_bytes < (0x2000 << i)); i--)
		/* empty */;
	return (i);
}

/*
 * invalidate IOMMU TLB entries through diagnostic registers.
 */
static void
iommu_tlb_flushall(iommu_t *iommu_p)
{
	int i;
	uint64_t base = (uint64_t)(iommu_p->iommu_ctrl_reg) -
	    COMMON_IOMMU_CTRL_REG_OFFSET;
	volatile uint64_t *tlb_tag = (volatile uint64_t *)
	    (base + COMMON_IOMMU_TLB_TAG_DIAG_ACC_OFFSET);
	volatile uint64_t *tlb_data = (volatile uint64_t *)
	    (base + COMMON_IOMMU_TLB_DATA_DIAG_ACC_OFFSET);
	for (i = 0; i < IOMMU_TLB_ENTRIES; i++)
		tlb_tag[i] = tlb_data[i] = 0ull;
}

static void
iommu_preserve_tsb(iommu_t *iommu_p)
{
#ifdef DEBUG
	dev_info_t *dip = iommu_p->iommu_pci_p->pci_dip;
#endif
	uint_t i, obp_tsb_entries, obp_tsb_size, base_pg_index;
	uint64_t ctl = *iommu_p->iommu_ctrl_reg;
	uint64_t obp_tsb_pa = *iommu_p->iommu_tsb_base_addr_reg;
	uint64_t *base_tte_addr;

	DEBUG3(DBG_ATTACH, dip,
	    "iommu_tsb_base_addr_reg=0x%08x (0x%08x.0x%08x)\n",
	    iommu_p->iommu_tsb_base_addr_reg,
	    (uint32_t)(*iommu_p->iommu_tsb_base_addr_reg >> 32),
	    (uint32_t)(*iommu_p->iommu_tsb_base_addr_reg & 0xffffffff));

	obp_tsb_size = IOMMU_CTL_TO_TSBSIZE(ctl);
	obp_tsb_entries = IOMMU_TSBSIZE_TO_TSBENTRIES(obp_tsb_size);
	base_pg_index = iommu_p->dvma_end_pg - obp_tsb_entries + 1;
	base_tte_addr = iommu_p->iommu_tsb_vaddr +
	    (iommu_p->iommu_tsb_entries - obp_tsb_entries);

	/*
	 * old darwin prom does not set tsb size correctly, bail out.
	 */
	if ((obp_tsb_size == IOMMU_DARWIN_BOGUS_TSBSIZE) &&
	    (CHIP_TYPE(iommu_p->iommu_pci_p) == PCI_CHIP_SABRE))
		return;

	DEBUG3(DBG_ATTACH, dip, "iommu_preserve_tsb: kernel info\n"
	    "iommu_tsb_vaddr=%08x copy to base_tte_addr=%08x "
	    "base_pg_index=%x\n", iommu_p->iommu_tsb_vaddr,
	    base_tte_addr, base_pg_index);

	DEBUG3(DBG_ATTACH | DBG_CONT, dip, "iommu_preserve_tsb: obp info "
	    "obp_tsb_entries=0x%x obp_tsb_pa=%08x.%08x\n", obp_tsb_entries,
	    (uint32_t)(obp_tsb_pa >> 32), (uint32_t)obp_tsb_pa);

	for (i = 0; i < obp_tsb_entries; i++) {
		uint64_t tte = lddphys(obp_tsb_pa + i * 8);
		caddr_t va;

		if (TTE_IS_INVALID(tte)) {
			DEBUG0(DBG_ATTACH | DBG_CONT, dip, ".");
			continue;
		}

		base_tte_addr[i] = tte;
		DEBUG3(DBG_ATTACH | DBG_CONT, dip,
		    "\npreserve_tsb: (%x)=%08x.%08x\n", base_tte_addr + i,
		    (uint_t)(tte >> 32), (uint_t)(tte & 0xffffffff));

		/*
		 * permanantly reserve this page from dvma address space
		 * resource map
		 */

		va = (caddr_t)(IOMMU_PTOB(base_pg_index + i));
		(void) vmem_xalloc(iommu_p->iommu_dvma_map, IOMMU_PAGE_SIZE,
		    IOMMU_PAGE_SIZE, 0, 0, va, va + IOMMU_PAGE_SIZE,
		    VM_NOSLEEP | VM_BESTFIT | VM_PANIC);
	}
}