/*
  Copyright 2005 Jean-Baptiste Note

  This file is part of prism54usb.

  prism54usb is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  prism54usb 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 prism54usb; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

*/

/* This file contains definitions for some functions which are common to
 * PCI and PCI+net2280 for uart.
 */

#include <linux/pci.h> /* for PCI_COMMAND_XXX */
#include "islsm_log.h"
#include "islsm_uart.h"
#include "isl_sm.h"
#include "isl_38xx.h"

#if defined(ISL_PCI)

static inline uint32_t
islpci_read_pcireg(islpci_private *priv, uint32_t address)
{
	void __iomem           *device_base = priv->device_base;
	uint32_t                reg;
	unsigned long           flags;

	spin_lock_irqsave(&priv->pcireg_lock, flags);
	reg = readl(device_base + address);
	udelay(ISL38XX_WRITEIO_DELAY);
	spin_unlock_irqrestore(&priv->pcireg_lock, flags);
	return reg;
}

static inline uint32_t isl_read_pcireg(struct islsm *islsm, uint32_t address)
{
	islpci_private         *priv = ISLPCI_OF_ISLSM(islsm);
	return islpci_read_pcireg(priv, address);
}

static inline void
islpci_write_pcireg(islpci_private *priv, uint32_t value, uint32_t address)
{
	void __iomem           *device_base = priv->device_base;
	unsigned long           flags;

	spin_lock_irqsave(&priv->pcireg_lock, flags);
	writel(value, device_base + address);
	udelay(ISL38XX_WRITEIO_DELAY);
	spin_unlock_irqrestore(&priv->pcireg_lock, flags);
}

static inline void
isl_write_pcireg(struct islsm *islsm, uint32_t value, uint32_t address)
{
	islpci_private         *priv = ISLPCI_OF_ISLSM(islsm);
	islpci_write_pcireg(priv, value, address);
}
#elif defined(ISL_USBPCI)

#include "islusb_net2280.h"

static inline uint32_t isl_read_pcireg(struct islsm *islsm, uint32_t address)
{
	uint32_t                reg;
	struct p54u            *p54u = P54U_OF_ISLSM(islsm);
	(void) p54u_dev_readl(p54u, &reg, address);
	return reg;
}

static inline void
isl_write_pcireg(struct islsm *islsm, uint32_t value, uint32_t address)
{
	struct p54u            *p54u = P54U_OF_ISLSM(islsm);
	p54u_dev_writel(p54u, value, address);
	udelay(ISL38XX_WRITEIO_DELAY);
}

#endif

static void __attribute__ ((unused))
uart_prot_init(struct islsm *device)
{
	const uint32_t          uart_magic = 0x12345678;
	uint32_t                reg;

	/* Disable CTS/DR interrupts */
	reg = isl_read_pcireg(device, ISL38XX_INT_EN_REG);
	reg &= ~(ISL38XX_DEV_INT_PCIUART_CTS | ISL38XX_DEV_INT_PCIUART_DR);
	isl_write_pcireg(device, reg, ISL38XX_INT_EN_REG);
	udelay(ISL38XX_WRITEIO_DELAY);

	/* write magic to activate UART via PCI protocol */
	/* we don't send DR with this magic number */

	isl_write_pcireg(device, uart_magic, ISL38XX_GEN_PURP_COM_REG_1);

	/* give time to the device to raise CTS */
	mdelay(50);

	/* Check if the protocol is initialized */
	reg = isl_read_pcireg(device, ISL38XX_INT_IDENT_REG);
	reg &= ISL38XX_DEV_INT_PCIUART_CTS;

	if (reg) {
		/* ACK the interrupt */
		isl_write_pcireg(device, reg, ISL38XX_INT_ACK_REG);
		udelay(ISL38XX_WRITEIO_DELAY);
		islog(L_DEBUG, "UART PCI protocol initialized\n");

		/* Enable CTS/DR interrupts */
		reg = isl_read_pcireg(device, ISL38XX_INT_EN_REG);
		reg |=
		    (ISL38XX_DEV_INT_PCIUART_CTS | ISL38XX_DEV_INT_PCIUART_DR);
		isl_write_pcireg(device, reg, ISL38XX_INT_EN_REG);
		udelay(ISL38XX_WRITEIO_DELAY);

		/* And give CTS to the device */
		isl_write_pcireg(device,
				 ISL38XX_DEV_INT_PCIUART_CTS,
				 ISL38XX_DEV_INT_REG);
		udelay(ISL38XX_WRITEIO_DELAY);

		/* give ourselves CTS. This is needed for freemac, and
		 * should happen anyways for other protocols, albeit in
		 * IRQ handler. This will work okay if writing to the
		 * device does not happen too fast after init.
		 */

		//device->uart->cts = 1;
	} else {
		islog(L_DEBUG, "UART PCI protocol not operational\n");
	}
}

static void __attribute__ ((unused))
uart_prot_exit(struct islsm *device)
{
	uint32_t                reg;

	/* remove magic */
	isl_write_pcireg(device, 0, ISL38XX_GEN_PURP_COM_REG_1);
	udelay(ISL38XX_WRITEIO_DELAY);

	/* Disable CTS/DR interrupts */
	reg = isl_read_pcireg(device, ISL38XX_INT_EN_REG);
	reg &= ~(ISL38XX_DEV_INT_PCIUART_CTS | ISL38XX_DEV_INT_PCIUART_DR);
	isl_write_pcireg(device, reg, ISL38XX_INT_EN_REG);
	udelay(ISL38XX_WRITEIO_DELAY);
}

static void uart_cts(struct islsm *device)
{
	uart_instance_t        *uart = &device->uart;
	unsigned long           flags;
	uint32_t                data;

	if (!uart) {
		/* stray CTS signals can happen when we're opening then
		 * closing the device very fast. We should wait for this
		 * first CTS before returning from uart_prot_init */
		islog(L_IRQ, "stray CTS signal\n");
		return;
	}

	spin_lock_irqsave(&uart->lock, flags);
	if (uart_inq(uart, &uart->txq) > 0) {
		data = uart_qconsume(uart, &uart->txq);

		spin_unlock_irqrestore(&uart->lock, flags);

		isl_write_pcireg(device, data, ISL38XX_GEN_PURP_COM_REG_1);
		udelay(ISL38XX_WRITEIO_DELAY);

		/* Give DR */
		isl_write_pcireg(device,
				 ISL38XX_DEV_INT_PCIUART_DR,
				 ISL38XX_DEV_INT_REG);
		udelay(ISL38XX_WRITEIO_DELAY);

	} else {
		spin_unlock_irqrestore(&uart->lock, flags);
		/* Device gives us CTS without any data to transmit */

		/* yeah, right, make sure magic is removed -- we need
		 * this when the first CTS is incoming, in response to
		 * our own CTS. We should even wait for this event
		 * (second CTS) to happen in order to leave
		 * uart_prot_init */
		isl_write_pcireg(device, 0, ISL38XX_GEN_PURP_COM_REG_1);
		udelay(ISL38XX_WRITEIO_DELAY);
		uart->cts = 1;
	}
}

static void __attribute__ ((unused))
uart_dr(struct islsm *device)
{
	uart_instance_t        *uart = &device->uart;
	unsigned long           flags;
	uint32_t                reg;

	FN_ENTER;

	if (!uart) {
		islog(L_IRQ, "stray DR signal\n");
		return;
	}

	reg = isl_read_pcireg(device, ISL38XX_GEN_PURP_COM_REG_2);
	udelay(ISL38XX_WRITEIO_DELAY);

	spin_lock_irqsave(&uart->lock, flags);
	if (uart_inq(uart, &uart->rxq) < UARTPCI_RXQSIZE) {
		uint8_t                 data = (uint8_t) (reg & 0xff);
		uart_qproduce(uart, &uart->rxq, data);
	}
	spin_unlock_irqrestore(&uart->lock, flags);

	/* Give CTS */
	isl_write_pcireg(device, ISL38XX_DEV_INT_PCIUART_CTS,
			 ISL38XX_DEV_INT_REG);
	udelay(ISL38XX_WRITEIO_DELAY);

	/* Activate the bottom halve */
	wake_up_interruptible(&uart->wait);

	FN_EXIT0;
}

/* reboot the device into rom (address 0xe0000000)
 * or ram (address 0x00000000)
 */
static                  inline
    void isl_reboot(struct islsm *device, const int ram)
{
	u32                     reg;

	FN_ENTER;

	/* Disable irqs in case they were not */
	isl_write_pcireg(device, ISL38XX_INT_EN_REG, 0);

	/* Reset the device */
	/* clear the RAMBoot, the CLKRUN and the Reset bit */
	reg = isl_read_pcireg(device, ISL38XX_CTRL_STAT_REG);
	reg &= ~ISL38XX_CTRL_STAT_RESET;
	if (!ram)
		reg &= ~ISL38XX_CTRL_STAT_RAMBOOT;
	else
		reg |= ISL38XX_CTRL_STAT_RAMBOOT;
	reg &= ~ISL38XX_CTRL_STAT_CLKRUN;
	isl_write_pcireg(device, ISL38XX_CTRL_STAT_REG, reg);
	wmb();

	/* set the Reset bit without reading the register */
	isl_write_pcireg(device, ISL38XX_CTRL_STAT_REG,
			 reg | ISL38XX_CTRL_STAT_RESET);
	wmb();

	/* clear the Reset bit */
	isl_write_pcireg(device, ISL38XX_CTRL_STAT_REG, reg);
	wmb();

	/* wait for the device to reboot */
	mdelay(100);

	FN_EXIT0;
}

/* Memory window management function */
/* adjust the memory window of device for IO at address ptr.
   we return the offset in the memory window.
*/
static unsigned adjust_memory_window(struct islsm *device, uint32_t addr)
{
	uint32_t                cur_mem_base, mem_base;

	/* read from device the current memory window location */
	/* we could also cache the result */
	cur_mem_base = isl_read_pcireg(device, ISL38XX_DIR_MEM_BASE_REG);
	udelay(ISL38XX_WRITEIO_DELAY);

	/* check we're rounded to ISL38XX_PCI_MEM_SIZE */
	mem_base =
	    (addr / ISL38XX_MEMORY_WINDOW_SIZE) * ISL38XX_MEMORY_WINDOW_SIZE;

	if (mem_base != cur_mem_base) {
		islog(L_DEBUG, "Adjusting memory window to %x\n", mem_base);
		isl_write_pcireg(device, mem_base, ISL38XX_DIR_MEM_BASE_REG);
		udelay(ISL38XX_WRITEIO_DELAY);
	}

	return (addr - mem_base);
}

static uint32_t __attribute__ ((unused))
isl_read_devmem(struct islsm *device, uint32_t addr)
{
	unsigned                offset;
	uint32_t                reg;
	offset = adjust_memory_window(device, addr);
	reg = isl_read_pcireg(device, ISL38XX_DIRECT_MEM_WIN + offset);
	udelay(ISL38XX_WRITEIO_DELAY);
	return reg;
}

static void __attribute__ ((unused))
isl_write_devmem(struct islsm *device, uint32_t value, uint32_t addr)
{
	unsigned                offset;
	offset = adjust_memory_window(device, addr);
	isl_write_pcireg(device, value, ISL38XX_DIRECT_MEM_WIN + offset);
	udelay(ISL38XX_WRITEIO_DELAY);
}

/* Other common functions :
 * i2c, dma, etc.
 */
