/*
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * Copyright (c) 2004, 2005
 *      Damien Bergamini <damien.bergamini@free.fr>. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice unmodified, this list of conditions, and the following
 *    disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

/*
 * Intel Wireless PRO/2200 mini-PCI adapter driver
 * ipw2200_hw.c is used t handle hardware operations and firmware operations.
 */
#include <sys/types.h>
#include <sys/byteorder.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/note.h>
#include <sys/stream.h>
#include <sys/strsun.h>

#include "ipw2200.h"
#include "ipw2200_impl.h"

/*
 * Hardware related operations
 */
#define	IPW2200_EEPROM_SHIFT_D		(2)
#define	IPW2200_EEPROM_SHIFT_Q		(4)

#define	IPW2200_EEPROM_C		(1 << 0)
#define	IPW2200_EEPROM_S		(1 << 1)
#define	IPW2200_EEPROM_D		(1 << IPW2200_EEPROM_SHIFT_D)
#define	IPW2200_EEPROM_Q		(1 << IPW2200_EEPROM_SHIFT_Q)

uint8_t
ipw2200_csr_get8(struct ipw2200_softc *sc, uint32_t off)
{
	return (ddi_get8(sc->sc_ioh, (uint8_t *)(sc->sc_regs + off)));
}

uint16_t
ipw2200_csr_get16(struct ipw2200_softc *sc, uint32_t off)
{
	return (ddi_get16(sc->sc_ioh,
	    (uint16_t *)((uintptr_t)sc->sc_regs + off)));
}

uint32_t
ipw2200_csr_get32(struct ipw2200_softc *sc, uint32_t off)
{
	return (ddi_get32(sc->sc_ioh,
	    (uint32_t *)((uintptr_t)sc->sc_regs + off)));
}

void
ipw2200_csr_getbuf32(struct ipw2200_softc *sc, uint32_t off,
	uint32_t *buf, size_t cnt)
{
	ddi_rep_get32(sc->sc_ioh, buf,
	    (uint32_t *)((uintptr_t)sc->sc_regs + off),
	    cnt, DDI_DEV_AUTOINCR);
}

void
ipw2200_csr_put8(struct ipw2200_softc *sc, uint32_t off,
	uint8_t val)
{
	ddi_put8(sc->sc_ioh, (uint8_t *)(sc->sc_regs + off), val);
}

void
ipw2200_csr_put16(struct ipw2200_softc *sc, uint32_t off,
	uint16_t val)
{
	ddi_put16(sc->sc_ioh,
	    (uint16_t *)((uintptr_t)sc->sc_regs + off), val);
}

void
ipw2200_csr_put32(struct ipw2200_softc *sc, uint32_t off,
	uint32_t val)
{
	ddi_put32(sc->sc_ioh,
	    (uint32_t *)((uintptr_t)sc->sc_regs + off), val);
}

uint8_t
ipw2200_imem_get8(struct ipw2200_softc *sc, uint32_t addr)
{
	ipw2200_csr_put32(sc, IPW2200_CSR_INDIRECT_ADDR, addr);
	return (ipw2200_csr_get8(sc, IPW2200_CSR_INDIRECT_DATA));
}

uint16_t
ipw2200_imem_get16(struct ipw2200_softc *sc,
	uint32_t addr)
{
	ipw2200_csr_put32(sc, IPW2200_CSR_INDIRECT_ADDR, addr);
	return (ipw2200_csr_get16(sc, IPW2200_CSR_INDIRECT_DATA));
}

uint32_t
ipw2200_imem_get32(struct ipw2200_softc *sc, uint32_t addr)
{
	ipw2200_csr_put32(sc, IPW2200_CSR_INDIRECT_ADDR, addr);
	return (ipw2200_csr_get32(sc, IPW2200_CSR_INDIRECT_DATA));
}

void
ipw2200_imem_put8(struct ipw2200_softc *sc, uint32_t addr, uint8_t val)
{
	ipw2200_csr_put32(sc, IPW2200_CSR_INDIRECT_ADDR, addr);
	ipw2200_csr_put8(sc, IPW2200_CSR_INDIRECT_DATA, val);
}

void
ipw2200_imem_put16(struct ipw2200_softc *sc, uint32_t addr,
	uint16_t val)
{
	ipw2200_csr_put32(sc, IPW2200_CSR_INDIRECT_ADDR, addr);
	ipw2200_csr_put16(sc, IPW2200_CSR_INDIRECT_DATA, val);
}

void
ipw2200_imem_put32(struct ipw2200_softc *sc, uint32_t addr,
	uint32_t val)
{
	ipw2200_csr_put32(sc, IPW2200_CSR_INDIRECT_ADDR, addr);
	ipw2200_csr_put32(sc, IPW2200_CSR_INDIRECT_DATA, val);
}

void
ipw2200_rom_control(struct ipw2200_softc *sc, uint32_t val)
{
	ipw2200_imem_put32(sc, IPW2200_IMEM_EEPROM_CTL, val);
	drv_usecwait(IPW2200_EEPROM_DELAY);
}

uint16_t
ipw2200_rom_get16(struct ipw2200_softc *sc, uint8_t addr)
{
	uint32_t	tmp;
	uint16_t	val;
	int		n;

	/*
	 * According to i2c bus protocol
	 */
	/* clock */
	ipw2200_rom_control(sc, 0);
	ipw2200_rom_control(sc, IPW2200_EEPROM_S);
	ipw2200_rom_control(sc, IPW2200_EEPROM_S | IPW2200_EEPROM_C);
	ipw2200_rom_control(sc, IPW2200_EEPROM_S);
	/* start bit */
	ipw2200_rom_control(sc, IPW2200_EEPROM_S | IPW2200_EEPROM_D);
	ipw2200_rom_control(sc, IPW2200_EEPROM_S | IPW2200_EEPROM_D |
	    IPW2200_EEPROM_C);
	/* read opcode */
	ipw2200_rom_control(sc, IPW2200_EEPROM_S | IPW2200_EEPROM_D);
	ipw2200_rom_control(sc, IPW2200_EEPROM_S | IPW2200_EEPROM_D |
	    IPW2200_EEPROM_C);
	ipw2200_rom_control(sc, IPW2200_EEPROM_S);
	ipw2200_rom_control(sc, IPW2200_EEPROM_S | IPW2200_EEPROM_C);
	/*
	 * address, totally 8 bits, defined by hardware, push from MSB to LSB
	 */
	for (n = 7; n >= 0; n--) {
		ipw2200_rom_control(sc, IPW2200_EEPROM_S |
		    (((addr >> n) & 1) << IPW2200_EEPROM_SHIFT_D));
		ipw2200_rom_control(sc, IPW2200_EEPROM_S |
		    (((addr >> n) & 1) << IPW2200_EEPROM_SHIFT_D) |
		    IPW2200_EEPROM_C);
	}

	ipw2200_rom_control(sc, IPW2200_EEPROM_S);

	/*
	 * data, totally 16 bits, defined by hardware, push from MSB to LSB
	 */
	val = 0;
	for (n = 15; n >= 0; n--) {
		ipw2200_rom_control(sc, IPW2200_EEPROM_S | IPW2200_EEPROM_C);
		ipw2200_rom_control(sc, IPW2200_EEPROM_S);
		tmp = ipw2200_imem_get32(sc, IPW2200_IMEM_EEPROM_CTL);
		val |= ((tmp & IPW2200_EEPROM_Q) >> IPW2200_EEPROM_SHIFT_Q)
		    << n;
	}

	ipw2200_rom_control(sc, 0);

	/* clear chip select and clock */
	ipw2200_rom_control(sc, IPW2200_EEPROM_S);
	ipw2200_rom_control(sc, 0);
	ipw2200_rom_control(sc, IPW2200_EEPROM_C);

	return (BE_16(val));
}

/*
 * Firmware related operations
 */
#define	IPW2200_FW_MAJOR_VERSION	(2)
#define	IPW2200_FW_MINOR_VERSION	(4)

#define	IPW2200_FW_MAJOR(x)((x) & 0xff)
#define	IPW2200_FW_MINOR(x)(((x) & 0xff) >> 8)

/*
 * These firwares were issued by Intel as binaries which need to be
 * loaded to hardware when card is initiated, or when fatal error
 * happened, or when the chip need be reset.
 */
static uint8_t ipw2200_boot_bin [] = {
#include "fw-ipw2200/ipw-2.4-boot.hex"
};
static uint8_t ipw2200_ucode_bin [] = {
#include "fw-ipw2200/ipw-2.4-bss_ucode.hex"
};
static uint8_t ipw2200_fw_bin [] = {
#include "fw-ipw2200/ipw-2.4-bss.hex"
};

#pragma pack(1)
struct header {
	uint32_t	version;
	uint32_t	mode;
};
#pragma pack()

int
ipw2200_cache_firmware(struct ipw2200_softc *sc)
{
	IPW2200_DBG(IPW2200_DBG_FW, (sc->sc_dip, CE_CONT,
	    "ipw2200_cache_firmware(): enter\n"));

	/* boot code */
	sc->sc_fw.boot_base = ipw2200_boot_bin + sizeof (struct header);
	sc->sc_fw.boot_size =
	    sizeof (ipw2200_boot_bin) - sizeof (struct header);
	/* ucode */
	sc->sc_fw.uc_base = ipw2200_ucode_bin + sizeof (struct header);
	sc->sc_fw.uc_size = sizeof (ipw2200_ucode_bin) - sizeof (struct header);
	/* firmware */
	sc->sc_fw.fw_base = ipw2200_fw_bin + sizeof (struct header);
	sc->sc_fw.fw_size = sizeof (ipw2200_fw_bin) - sizeof (struct header);

	sc->sc_flags |= IPW2200_FLAG_FW_CACHED;

	IPW2200_DBG(IPW2200_DBG_FW, (sc->sc_dip, CE_CONT,
	    "ipw2200_cache_firmware(): boot=%u,uc=%u,fw=%u\n",
	    sc->sc_fw.boot_size, sc->sc_fw.uc_size, sc->sc_fw.fw_size));
	IPW2200_DBG(IPW2200_DBG_FW, (sc->sc_dip, CE_CONT,
	    "ipw2200_cache_firmware(): exit\n"));

	return (DDI_SUCCESS);
}

/*
 * If user-land firmware loading is supported, this routine will
 * free kernel memory, when sc->sc_fw.bin_base & sc->sc_fw.bin_size
 * are not empty
 */
int
ipw2200_free_firmware(struct ipw2200_softc *sc)
{
	sc->sc_flags &= ~IPW2200_FLAG_FW_CACHED;

	return (DDI_SUCCESS);
}

/*
 * the following routines load code onto ipw2200 hardware
 */
int
ipw2200_load_uc(struct ipw2200_softc *sc, uint8_t *buf, size_t size)
{
	int		ntries, i;
	uint16_t	*w;

	ipw2200_csr_put32(sc, IPW2200_CSR_RST,
	    IPW2200_RST_STOP_MASTER | ipw2200_csr_get32(sc, IPW2200_CSR_RST));
	for (ntries = 0; ntries < 5; ntries++) {
		if (ipw2200_csr_get32(sc, IPW2200_CSR_RST) &
		    IPW2200_RST_MASTER_DISABLED)
			break;
		drv_usecwait(10);
	}
	if (ntries == 5) {
		IPW2200_WARN((sc->sc_dip, CE_CONT,
		    "ipw2200_load_uc(): timeout waiting for master"));
		return (DDI_FAILURE);
	}

	ipw2200_imem_put32(sc, 0x3000e0, 0x80000000);
	drv_usecwait(5000);
	ipw2200_csr_put32(sc, IPW2200_CSR_RST,
	    ~IPW2200_RST_PRINCETON_RESET &
	    ipw2200_csr_get32(sc, IPW2200_CSR_RST));
	drv_usecwait(5000);
	ipw2200_imem_put32(sc, 0x3000e0, 0);
	drv_usecwait(1000);
	ipw2200_imem_put32(sc, IPW2200_IMEM_EVENT_CTL, 1);
	drv_usecwait(1000);
	ipw2200_imem_put32(sc, IPW2200_IMEM_EVENT_CTL, 0);
	drv_usecwait(1000);
	ipw2200_imem_put8(sc, 0x200000, 0x00);
	ipw2200_imem_put8(sc, 0x200000, 0x40);
	drv_usecwait(1000);

	for (w = (uint16_t *)(uintptr_t)buf; size > 0; w++, size -= 2)
		ipw2200_imem_put16(sc, 0x200010, LE_16(*w));

	ipw2200_imem_put8(sc, 0x200000, 0x00);
	ipw2200_imem_put8(sc, 0x200000, 0x80);

	/*
	 * try many times to wait the upload is ready, 2000times
	 */
	for (ntries = 0; ntries < 2000; ntries++) {
		uint8_t val;

		val = ipw2200_imem_get8(sc, 0x200000);
		if (val & 1)
			break;
		drv_usecwait(1000); /* wait for a while */
	}
	if (ntries == 2000) {
		IPW2200_WARN((sc->sc_dip, CE_WARN,
		    "ipw2200_load_uc(): timeout waiting for ucode init.\n"));
		return (DDI_FAILURE);
	}

	for (i = 0; i < 7; i++)
		(void) ipw2200_imem_get32(sc, 0x200004);

	ipw2200_imem_put8(sc, 0x200000, 0x00);

	return (DDI_SUCCESS);
}

#define	MAX_DR_NUM	(64)
#define	MAX_DR_SIZE	(4096)

int
ipw2200_load_fw(struct ipw2200_softc *sc, uint8_t *buf, size_t size)
{
	struct dma_region	dr[MAX_DR_NUM]; /* maximal, 64 * 4KB = 256KB */
	uint8_t			*p, *end, *v;
	uint32_t		mlen;
	uint32_t		src, dst, ctl, len, sum, off;
	uint32_t		sentinel;
	int			ntries, err, cnt, i;
	clock_t			clk;

	ipw2200_imem_put32(sc, 0x3000a0, 0x27000);

	p   = buf;
	end = p + size;

	cnt = 0;
	err = ipw2200_dma_region_alloc(sc, &dr[cnt], MAX_DR_SIZE, DDI_DMA_READ,
	    DDI_DMA_STREAMING);
	if (err != DDI_SUCCESS)
		goto fail0;
	off = 0;
	src = dr[cnt].dr_pbase;

	ipw2200_csr_put32(sc, IPW2200_CSR_AUTOINC_ADDR, 0x27000);

	while (p < end) {
		dst = LE_32(*((uint32_t *)(uintptr_t)p)); p += 4;
		len = LE_32(*((uint32_t *)(uintptr_t)p)); p += 4;
		v = p;
		p += len;
		IPW2200_DBG(IPW2200_DBG_FW, (sc->sc_dip, CE_CONT,
		    "ipw2200_load_fw(): dst=0x%x,len=%u\n", dst, len));

		while (len > 0) {
			/*
			 * if no DMA region is available, allocate a new one
			 */
			if (off == dr[cnt].dr_size) {
				cnt++;
				if (cnt >= MAX_DR_NUM) {
					IPW2200_WARN((sc->sc_dip, CE_WARN,
					    "ipw2200_load_fw(): "
					    "maximum %d DRs is reached\n",
					    cnt));
					cnt--; /* only free alloced DMA */
					goto fail1;
				}
				err = ipw2200_dma_region_alloc(sc, &dr[cnt],
				    MAX_DR_SIZE, DDI_DMA_WRITE,
				    DDI_DMA_STREAMING);
				if (err != DDI_SUCCESS) {
					cnt--; /* only free alloced DMA */
					goto fail1;
				}
				off = 0;
				src = dr[cnt].dr_pbase;
			}
			mlen = min(IPW2200_CB_MAXDATALEN, len);
			mlen = min(mlen, dr[cnt].dr_size - off);

			(void) memcpy(dr[cnt].dr_base + off, v, mlen);
			(void) ddi_dma_sync(dr[cnt].dr_hnd, off, mlen,
			    DDI_DMA_SYNC_FORDEV);

			ctl = IPW2200_CB_DEFAULT_CTL | mlen;
			sum = ctl ^ src ^ dst;
			/*
			 * write a command
			 */
			ipw2200_csr_put32(sc, IPW2200_CSR_AUTOINC_DATA, ctl);
			ipw2200_csr_put32(sc, IPW2200_CSR_AUTOINC_DATA, src);
			ipw2200_csr_put32(sc, IPW2200_CSR_AUTOINC_DATA, dst);
			ipw2200_csr_put32(sc, IPW2200_CSR_AUTOINC_DATA, sum);

			off += mlen;
			src += mlen;
			dst += mlen;
			v   += mlen;
			len -= mlen;
		}
	}

	sentinel = ipw2200_csr_get32(sc, IPW2200_CSR_AUTOINC_ADDR);
	ipw2200_csr_put32(sc, IPW2200_CSR_AUTOINC_DATA, 0);

	IPW2200_DBG(IPW2200_DBG_FW, (sc->sc_dip, CE_CONT,
	    "ipw2200_load_fw(): sentinel=%x\n", sentinel));

	ipw2200_csr_put32(sc, IPW2200_CSR_RST,
	    ~(IPW2200_RST_MASTER_DISABLED | IPW2200_RST_STOP_MASTER)
	    & ipw2200_csr_get32(sc, IPW2200_CSR_RST));

	ipw2200_imem_put32(sc, 0x3000a4, 0x540100);
	for (ntries = 0; ntries < 400; ntries++) {
		uint32_t val;
		val = ipw2200_imem_get32(sc, 0x3000d0);
		if (val >= sentinel)
			break;
		drv_usecwait(100);
	}
	if (ntries == 400) {
		IPW2200_WARN((sc->sc_dip, CE_WARN,
		    "ipw2200_load_fw(): timeout processing command blocks\n"));
		goto fail1;
	}

	mutex_enter(&sc->sc_ilock);

	ipw2200_imem_put32(sc, 0x3000a4, 0x540c00);

	/*
	 * enable all interrupts
	 */
	ipw2200_csr_put32(sc, IPW2200_CSR_INTR_MASK, IPW2200_INTR_MASK_ALL);

	/*
	 * tell the adapter to initialize the firmware,
	 * just simply set it to 0
	 */
	ipw2200_csr_put32(sc, IPW2200_CSR_RST, 0);
	ipw2200_csr_put32(sc, IPW2200_CSR_CTL,
	    ipw2200_csr_get32(sc, IPW2200_CSR_CTL) |
	    IPW2200_CTL_ALLOW_STANDBY);

	/*
	 * wait for interrupt to notify fw initialization is done
	 */
	sc->sc_fw_ok = 0;
	while (!sc->sc_fw_ok) {
		/*
		 * There is an enhancement! we just change from 1s to 5s
		 */
		clk = ddi_get_lbolt() + drv_usectohz(5000000);	/* 5 second */
		if (cv_timedwait(&sc->sc_fw_cond, &sc->sc_ilock, clk) < 0)
			break;
	}
	mutex_exit(&sc->sc_ilock);

	if (!sc->sc_fw_ok) {
		IPW2200_WARN((sc->sc_dip, CE_WARN,
		    "ipw2200_load_fw(): firmware(%u) load failed!", size));
		goto fail1;
	}

	for (i = 0; i <= cnt; i++)
		ipw2200_dma_region_free(&dr[i]);

	return (DDI_SUCCESS);

fail1:
	IPW2200_WARN((sc->sc_dip, CE_WARN,
	    "ipw2200_load_fw(): DMA allocation failed, cnt=%d\n", cnt));
	for (i = 0; i <= cnt; i++)
		ipw2200_dma_region_free(&dr[i]);
fail0:
	return (DDI_FAILURE);
}