/*
 * 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 <config.h>
#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 struct {
	unsigned long tdes0;
	unsigned short tbs[2];
	void *buf[2];
} __attribute__((packed, aligned(4))) volatile tx_desc;

static struct {
	unsigned long rdes0;
	unsigned short rbs[2];
	void *buf[2];

} __attribute__((packed, aligned(4))) volatile rx_desc;

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(ETHER_NAME ": MAC %pM\n", 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;
}

#if defined(CONFIG_MII) || defined(CONFIG_CMD_MII)
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;
}
#endif

static void phy_configure(struct eth_device *dev)
{
	fm3_eth_phy_write(dev, CONFIG_FM3_PHY_ADDR, MII_BMCR, BMCR_RESET);
	mdelay(1);
	fm3_eth_phy_write(dev, CONFIG_FM3_PHY_ADDR, MII_BMCR, 
			  BMCR_SPEED100 | BMCR_ANENABLE | BMCR_FULLDPLX |BMCR_ANRESTART);

	printf(ETHER_NAME ": phy initialized\n");

	return;
}

static int fm3_eth_send(struct eth_device *dev, void *packet, int len)
{
	int ret = 0;
	int timeout;

	if (!packet || len > 0x1fff) {
		printf(ETHER_NAME ": %s: Invalid argument\n", __func__);
		ret = -EINVAL;
		goto err;
	}

	tx_desc.buf[0] = packet;
	tx_desc.tbs[0] = len;
	tx_desc.tbs[1] = 0;
	tx_desc.tdes0 = 0xbc200000;
	writel(readl(dev->iobase + DMAC_OMR) | 0x00002000, dev->iobase + DMAC_OMR);
	writel(0, dev->iobase + DMAC_TPDR);
	/* Wait until packet is transmitted */
	timeout = TIMEOUT_CNT;
	while ((tx_desc.tdes0 & 0x80000000) && timeout--)
		udelay(1000);

	if (timeout < 0) {
		printf(ETHER_NAME ": transmit timeout\n");
		ret = -ETIMEDOUT;
		goto err;
	}


err:
	return ret;
}

static int fm3_eth_recv(struct eth_device *dev)
{
	int len = 0;

	/* Check if descriptor own by host */
	if (rx_desc.rdes0 & 0x80000000) {
		/* Check for errors */
		if (!(rx_desc.rdes0 & 0x8000)) {
			len = (rx_desc.rdes0 >> 16) & 0x3fff;
			NetReceive(rx_desc.buf[0], len);
		}
		rx_desc.rdes0 = 0x80000000;
	}

	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)
{
	tx_desc.tdes0 = 0x30000000;
	tx_desc.tbs[0] = tx_desc.tbs[1] = 0;
	tx_desc.buf[0] = tx_desc.buf[1] = NULL;

	rx_desc.rdes0 = 0x80000000;
	rx_desc.rbs[0] = PKTSIZE_ALIGN;
	rx_desc.rbs[1] = 0;
	rx_desc.buf[0] = rx_desc.buf[1] = NULL;
	rx_desc.buf[0] = malloc(rx_desc.rbs[0]);
	if (rx_desc.buf[0] == NULL)
		goto error;
	writel((unsigned long)&tx_desc, dev->iobase + DMAC_TDLAR);
	writel((unsigned long)&rx_desc, dev->iobase + DMAC_RDLAR);
	return 0;
error:
	rx_desc.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),
			     CONFIG_FM3_PHY_ADDR, dev, PHY_INTERFACE_MODE_RMII);
	phy_configure(dev);

	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(ETHER_NAME " : phy config timeout\n");
		goto err_phy_cfg;
	}
	phy_startup(phy);

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

	/* Set the transfer speed */
	if (phy->speed == 100) {
		printf(ETHER_NAME ": 100Base/");
		val |= 0x4000;
	} else if (phy->speed == 10) {
		printf(ETHER_NAME ": 10Base/");
		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 |= 2;
	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(rx_desc.buf[0]);

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)
{
	struct eth_device *dev;

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

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

	dev->init = fm3_eth_init;
	dev->halt = fm3_eth_halt;
	dev->send = fm3_eth_send;
	dev->recv = fm3_eth_recv;
	strcpy(dev->name, ETHER_NAME);

	eth_register(dev);

	if (!eth_getenv_enetaddr("ethaddr", dev->enetaddr))
		puts("Please set MAC address\n");


#if defined(CONFIG_MII) || defined(CONFIG_CMD_MII)
	miiphy_register(dev->name, fm3_miiphy_read, fm3_miiphy_write);
#endif

	return 1;
}
