/*
 * fm3_eth.c - Driver for Fujitsu FM3 Ethernet controller
 *
 * Copyright (C) 2012 Yoshinori Sato <ysato@users.sourceforge.jp>
 *
 * This program 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.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <common.h>
#include <malloc.h>
#include <net.h>
#include <netdev.h>
#include <miiphy.h>
#include <phy.h>
#include <asm/errno.h>
#include <asm/io.h>

#include "fm3_eth.h"

#define TIMEOUT_CNT 60000

static const int phy_addr[] = CONFIG_FM3_PHY_ADDR;

static void set_mac_address(struct eth_device *dev)
{
	unsigned long addrh, addrl;
	uchar *m = dev->enetaddr;

	addrl = m[0] | (m[1] << 8) | (m[2] << 16) | (m[3] << 24);
	addrh = 0x80000000 | m[4] | (m[5] << 8);
	writel(addrh, dev->iobase + GMAC_MAR0H);
	writel(addrl, dev->iobase + GMAC_MAR0L);

	printf("%s: MAC %pM\n", dev->name, m);
}

static int fm3_eth_phy_read(struct eth_device *dev,
				u8 phy, u8 reg, u16 *val)
{
	while (readl(dev->iobase + GMAC_GAR) & 0x0001);
	writel((phy << 11) | (reg << 6) | 0x0005, dev->iobase + GMAC_GAR);
	while (readl(dev->iobase + GMAC_GAR) & 0x0001);
	*val = readl(dev->iobase + GMAC_GDR);
	return 0;
}

static int fm3_eth_phy_write(struct eth_device *dev,
				u8 phy, u8 reg, u16  val)
{
	while (readl(dev->iobase + GMAC_GAR) & 0x0001);
	writel(val, dev->iobase + GMAC_GDR);
	writel((phy << 11) | (reg << 6) | 0x0007, dev->iobase + GMAC_GAR);
	while (readl(dev->iobase + GMAC_GAR) & 0x0001);

	return 0;
}

static int fm3_miiphy_read(const char *devname, u8 phy, u8 reg, u16 *val)
{
	struct eth_device *dev = eth_get_dev_by_name(devname);
	if (dev)
		return fm3_eth_phy_read(dev, phy, reg, val);
	return -1;
}
static int fm3_miiphy_write(const char *devname, u8 phy, u8 reg, u16 val)
{ 
	struct eth_device *dev = eth_get_dev_by_name(devname);
	if (dev)
		return fm3_eth_phy_write(dev, phy, reg, val);
	return -1;
}

static void phy_configure(struct phy_device *phy)
{
	fm3_eth_phy_write(phy->dev, phy->addr, MII_BMCR, BMCR_RESET);
	mdelay(1);
	fm3_eth_phy_write(phy->dev, phy->addr, MII_BMCR, 
			  BMCR_SPEED100 | BMCR_ANENABLE |
			  BMCR_FULLDPLX |BMCR_ANRESTART);

	debug("%s: phy initialized\n", phy->dev->name);

	return;
}

static int fm3_eth_send(struct eth_device *dev, void *packet, int len)
{
	struct fm3_desc *desc = (struct fm3_desc *)dev->priv;
	void *buf = NULL;
	int ret = 0;
	int timeout;

	if (!packet || len > 0x1fff) {
		printf("%s: %s: Invalid argument\n", dev->name, __func__);
		ret = -EINVAL;
		goto err;
	}
	desc->cur_tx->buf[1] = packet;
	desc->cur_tx->tbs[1] = len;
	desc->cur_tx->tdes0 |= 0xb0000000;
	writel(0, dev->iobase + DMAC_TPDR);
	writel(0x0001e7ff, dev->iobase + DMAC_SR);
	/* Wait until packet is transmitted */
	timeout = TIMEOUT_CNT;
	while ((desc->cur_tx->tdes0 & 0x80000000) && timeout--)
		udelay(1000);
	if (buf)
		free(buf);

	if (timeout < 0) {
		printf("%s: transmit timeout\n", dev->name);
		ret = -ETIMEDOUT;
	} else {
		debug("%s: %p-TDES=%08lx\n", dev->name, desc->cur_tx, desc->cur_tx->tdes0);
		if (desc->cur_tx->tdes0 & 0x00008000)
			ret = -EIO;
	}
err:
	desc->cur_tx = desc->cur_tx->buf[0];
	return ret;
}

static int fm3_eth_recv(struct eth_device *dev)
{
	struct fm3_desc *desc = (struct fm3_desc *)dev->priv;
	int len = 0;

	/* Check if descriptor own by host */
	if (!(desc->cur_rx->rdes0 & 0x80000000)) {
		/* Check for errors */
		if (!(desc->cur_rx->rdes0 & 0x8000)) {
			len = (desc->cur_rx->rdes0 >> 16) & 0x3fff;
			NetReceive(desc->cur_rx->buf[1], len);
		}
		desc->cur_rx->rdes0 |= 0x80000000;
		writel(0, dev->iobase + DMAC_RPDR);
		desc->cur_rx = desc->cur_rx->buf[0];
	}

	return len;
}

static int eth_dma_reset(struct eth_device *dev)
{
	writel(0x00000001, dev->iobase + DMAC_BMR);
	while(readl(dev->iobase + DMAC_BMR) & 0x00000001);
	while(readl(dev->iobase + DMAC_AHBSR) & 0x00000001);
	writel(readl(dev->iobase + GMAC_MCR) | 0x00008000,
	       dev->iobase + GMAC_MCR);
	writel(0x04025002, dev->iobase + DMAC_BMR);
	return 0;
}

static int eth_desc_init(struct eth_device *dev)
{
	struct fm3_desc *desc = (struct fm3_desc *)dev->priv;
	int i;
	unsigned char *buf;
	for (i = 0; i < NR_TX_DESC; i++) {
		desc->tx[i].tdes0 = 0x30000000;
		desc->tx[i].buf[0] = &desc->tx[(i + 1) % NR_TX_DESC];
	}
	desc->tx[NR_TX_DESC - 1].tdes0 |= 0x00200000;
	desc->cur_tx = &desc->tx[0];

	buf = malloc(NR_RX_DESC * PKTSIZE_ALIGN);
	if (buf == NULL)
		goto error;

	for (i = 0; i < NR_RX_DESC; i++) {
		desc->rx[i].rdes0 = 0x80000000;
		desc->rx[i].rbs[0] = 0;
		desc->rx[i].rbs[1] = PKTSIZE_ALIGN;
		desc->rx[i].buf[1] = buf + (PKTSIZE_ALIGN * i);
		desc->rx[i].buf[0] = &desc->rx[(i + 1) % NR_RX_DESC];
	}
	desc->rx[NR_RX_DESC - 1].rbs[0] |= 0x8000;
	desc->cur_rx = &desc->rx[0];
	writel((unsigned long)&desc->tx[0], dev->iobase + DMAC_TDLAR);
	writel((unsigned long)&desc->rx[0], dev->iobase + DMAC_RDLAR);
	return 0;
error:
	desc->rx[0].rdes0 = 0x00000000;
	return -1;
}


static struct phy_device *eth_phy_config(struct eth_device *dev)
{
	struct phy_device *phydev;
	phydev = phy_connect(miiphy_get_dev_by_name(dev->name),
			     phy_addr[dev->index], 
			     dev, PHY_INTERFACE_MODE_RMII);
	phy_configure(phydev);

	return phydev;
}

static int eth_config(struct eth_device *dev, bd_t *bd)
{
	u32 val;
	struct phy_device *phy;

	/* GMAC Initialize */
	writel((readl(dev->iobase + GMAC_GAR) & ~0x3c) | 0x04,
	       dev->iobase + GMAC_GAR);
	set_mac_address(dev);
	/* Configure phy */
	phy = eth_phy_config(dev);
	if (phy == NULL) {
		printf("%s: phy config timeout\n", dev->name);
		goto err_phy_cfg;
	}
	phy_startup(phy);

	val = readl(dev->iobase + GMAC_MCR);

	/* Set the transfer speed */
	if (phy->speed == 100) {
		printf("%s: 100Base/", dev->name);
		val |= 0x4000;
	} else if (phy->speed == 10) {
		printf("%s: 10Base/", dev->name);
		val &= ~0x4000;
	}

	/* Check if full duplex mode is supported by the phy */
	if (phy->duplex) {
		printf("Full\n");
		val |= 0x0800;
	} else {
		printf("Half\n");
		val &= ~0x0800;
	}
	writel(val, dev->iobase + GMAC_MCR);
	return 0;

err_phy_cfg:
	return -1;
}

static void fm3_eth_start(struct eth_device *dev)
{
	unsigned long omr;
	omr = readl(dev->iobase + DMAC_OMR);
	omr |= 0x2002;
	writel(omr, dev->iobase + DMAC_OMR);
	writel(readl(dev->iobase + GMAC_MCR) | 0x0c, dev->iobase + GMAC_MCR);
}

static int fm3_eth_init(struct eth_device *dev, bd_t *bd)
{
	int ret = 0;

	ret = eth_dma_reset(dev);
	if (ret)
		goto err;

	ret = eth_desc_init(dev);
	if (ret)
		goto err;

	ret = eth_config(dev, bd);
	if (ret)
		goto err_config;

	fm3_eth_start(dev);

	return ret;

err_config:
	free(((struct fm3_desc *)dev->priv)->rx[0].buf[1]);

err:
	return ret;
}

static void fm3_eth_halt(struct eth_device *dev)
{
	unsigned long omr;
	omr = readl(dev->iobase + DMAC_OMR);
	omr &= ~2;
	writel(omr, dev->iobase + DMAC_OMR);
}

int fm3_eth_initialize(bd_t *bd, int ch)
{
	struct eth_device *dev;
	struct fm3_desc *desc;

	dev = malloc(sizeof(*dev));
	if (!dev) {
		return -1;
	}
	memset(dev, 0, sizeof(*dev));

	desc = malloc(sizeof(*desc));
	if (!desc) {
		free(dev);
		return -1;
	}
	memset(desc, 0, sizeof(*desc));
	debug("%s-%d: desc=%p\n", ETHER_NAME, ch, desc);

	dev->iobase = FM3_MAC_BASE + (0x3000 * ch);

	dev->init = fm3_eth_init;
	dev->halt = fm3_eth_halt;
	dev->send = fm3_eth_send;
	dev->recv = fm3_eth_recv;
	dev->priv = desc;
	sprintf(dev->name, "%s-%d", ETHER_NAME, ch);

	eth_register(dev);

	if (!eth_getenv_enetaddr("ethaddr", dev->enetaddr))
		puts("Please set MAC address\n");
	dev->enetaddr[5] += ch;

	miiphy_register(dev->name, fm3_miiphy_read, fm3_miiphy_write);

	return 1;
}
