/*
 * drivers/net/phy/fixed.c
 *
 * Driver for fixed PHYs, when transceiver is able to operate in one fixed mode.
 *
 * Author: Vitaly Bordug
 *
 * Copyright (c) 2006 MontaVista Software, Inc.
 *
 * 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.
 *
 */
#include <linux/kernel.h>
#include <linux/string.h>
#include <linux/errno.h>
#include <linux/unistd.h>
#include <linux/slab.h>
#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/skbuff.h>
#include <linux/spinlock.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/mii.h>
#include <linux/ethtool.h>
#include <linux/phy.h>
#include <linux/phy_fixed.h>

#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>

/* we need to track the allocated pointers in order to free them on exit */
static struct fixed_info *fixed_phy_ptrs[CONFIG_FIXED_MII_AMNT*MAX_PHY_AMNT];

/*-----------------------------------------------------------------------------
 *  If something weird is required to be done with link/speed,
 * network driver is able to assign a function to implement this.
 * May be useful for PHY's that need to be software-driven.
 *-----------------------------------------------------------------------------*/
int fixed_mdio_set_link_update(struct phy_device *phydev,
			       int (*link_update) (struct net_device *,
						   struct fixed_phy_status *))
{
	struct fixed_info *fixed;

	if (link_update == NULL)
		return -EINVAL;

	if (phydev) {
		if (phydev->bus) {
			fixed = phydev->bus->priv;
			fixed->link_update = link_update;
			return 0;
		}
	}
	return -EINVAL;
}

EXPORT_SYMBOL(fixed_mdio_set_link_update);

struct fixed_info *fixed_mdio_get_phydev (int phydev_ind)
{
	if (phydev_ind >= MAX_PHY_AMNT)
		return NULL;
	return fixed_phy_ptrs[phydev_ind];
}

EXPORT_SYMBOL(fixed_mdio_get_phydev);

/*-----------------------------------------------------------------------------
 *  This is used for updating internal mii regs from the status
 *-----------------------------------------------------------------------------*/
#if defined(CONFIG_FIXED_MII_100_FDX) || defined(CONFIG_FIXED_MII_10_FDX) || defined(CONFIG_FIXED_MII_1000_FDX)
static int fixed_mdio_update_regs(struct fixed_info *fixed)
{
	u16 *regs = fixed->regs;
	u16 bmsr = 0;
	u16 bmcr = 0;

	if (!regs) {
		printk(KERN_ERR "%s: regs not set up", __FUNCTION__);
		return -EINVAL;
	}

	if (fixed->phy_status.link)
		bmsr |= BMSR_LSTATUS;

	if (fixed->phy_status.duplex) {
		bmcr |= BMCR_FULLDPLX;

		switch (fixed->phy_status.speed) {
		case 100:
			bmsr |= BMSR_100FULL;
			bmcr |= BMCR_SPEED100;
			break;

		case 10:
			bmsr |= BMSR_10FULL;
			break;
		}
	} else {
		switch (fixed->phy_status.speed) {
		case 100:
			bmsr |= BMSR_100HALF;
			bmcr |= BMCR_SPEED100;
			break;

		case 10:
			bmsr |= BMSR_100HALF;
			break;
		}
	}

	regs[MII_BMCR] = bmcr;
	regs[MII_BMSR] = bmsr | 0x800;	/*we are always capable of 10 hdx */

	return 0;
}

static int fixed_mii_read(struct mii_bus *bus, int phy_id, int location)
{
	struct fixed_info *fixed = bus->priv;

	/* if user has registered link update callback, use it */
	if (fixed->phydev)
		if (fixed->phydev->attached_dev) {
			if (fixed->link_update) {
				fixed->link_update(fixed->phydev->attached_dev,
						   &fixed->phy_status);
				fixed_mdio_update_regs(fixed);
			}
		}

	if ((unsigned int)location >= fixed->regs_num)
		return -1;
	return fixed->regs[location];
}

static int fixed_mii_write(struct mii_bus *bus, int phy_id, int location,
			   u16 val)
{
	/* do nothing for now */
	return 0;
}

static int fixed_mii_reset(struct mii_bus *bus)
{
	/*nothing here - no way/need to reset it */
	return 0;
}
#endif

static int fixed_config_aneg(struct phy_device *phydev)
{
	/* :TODO:03/13/2006 09:45:37 PM::
	   The full autoneg funcionality can be emulated,
	   but no need to have anything here for now
	 */
	return 0;
}

/*-----------------------------------------------------------------------------
 * the manual bind will do the magic - with phy_id_mask == 0
 * match will never return true...
 *-----------------------------------------------------------------------------*/
static struct phy_driver fixed_mdio_driver = {
	.name = "Fixed PHY",
#ifdef CONFIG_FIXED_MII_1000_FDX
	.features = PHY_GBIT_FEATURES,
#else
	.features = PHY_BASIC_FEATURES,
#endif
	.config_aneg = fixed_config_aneg,
	.read_status = genphy_read_status,
	.driver = { .owner = THIS_MODULE, },
};

static void fixed_mdio_release(struct device *dev)
{
	struct phy_device *phydev = container_of(dev, struct phy_device, dev);
	struct mii_bus *bus = phydev->bus;
	struct fixed_info *fixed = bus->priv;

	kfree(phydev);
	kfree(bus->dev);
	kfree(bus);
	kfree(fixed->regs);
	kfree(fixed);
}

/*-----------------------------------------------------------------------------
 *  This func is used to create all the necessary stuff, bind
 * the fixed phy driver and register all it on the mdio_bus_type.
 * speed is either 10 or 100 or 1000, duplex is boolean.
 * number is used to create multiple fixed PHYs, so that several devices can
 * utilize them simultaneously.
 *
 * The device on mdio bus will look like [bus_id]:[phy_id],
 * bus_id = number
 * phy_id = speed+duplex.
 *-----------------------------------------------------------------------------*/
#if defined(CONFIG_FIXED_MII_100_FDX) || defined(CONFIG_FIXED_MII_10_FDX) || defined(CONFIG_FIXED_MII_1000_FDX)
struct fixed_info *fixed_mdio_register_device(
	int bus_id, int speed, int duplex, u8 phy_id)
{
	struct mii_bus *new_bus;
	struct fixed_info *fixed;
	struct phy_device *phydev;
	int err;

	struct device *dev = kzalloc(sizeof(struct device), GFP_KERNEL);

	if (dev == NULL)
		goto err_dev_alloc;

	new_bus = kzalloc(sizeof(struct mii_bus), GFP_KERNEL);

	if (new_bus == NULL)
		goto err_bus_alloc;

	fixed = kzalloc(sizeof(struct fixed_info), GFP_KERNEL);

	if (fixed == NULL)
		goto err_fixed_alloc;

	fixed->regs = kzalloc(MII_REGS_NUM * sizeof(int), GFP_KERNEL);
	if (NULL == fixed->regs)
		goto err_fixed_regs_alloc;

	fixed->regs_num = MII_REGS_NUM;
	fixed->phy_status.speed = speed;
	fixed->phy_status.duplex = duplex;
	fixed->phy_status.link = 1;

	new_bus->name = "Fixed MII Bus";
	new_bus->read = &fixed_mii_read;
	new_bus->write = &fixed_mii_write;
	new_bus->reset = &fixed_mii_reset;
	/*set up workspace */
	fixed_mdio_update_regs(fixed);
	new_bus->priv = fixed;

	new_bus->dev = dev;
	dev_set_drvdata(dev, new_bus);

	/* create phy_device and register it on the mdio bus */
	phydev = phy_device_create(new_bus, 0, 0);
	if (phydev == NULL)
		goto err_phy_dev_create;

	/*
	 * Put the phydev pointer into the fixed pack so that bus read/write
	 * code could be able to access for instance attached netdev. Well it
	 * doesn't have to do so, only in case of utilizing user-specified
	 * link-update...
	 */

	fixed->phydev = phydev;
	phydev->speed = speed;
	phydev->duplex = duplex;

	phydev->irq = PHY_IGNORE_INTERRUPT;
	phydev->dev.bus = &mdio_bus_type;

	snprintf(phydev->dev.bus_id, BUS_ID_SIZE,
		 PHY_ID_FMT, bus_id, phy_id);

	phydev->bus = new_bus;

	phydev->dev.driver = &fixed_mdio_driver.driver;
	phydev->dev.release = fixed_mdio_release;
	err = phydev->dev.driver->probe(&phydev->dev);
	if (err < 0) {
		printk(KERN_ERR "Phy %s: problems with fixed driver\n",
		       phydev->dev.bus_id);
		goto err_out;
	}
	err = device_register(&phydev->dev);
	if (err) {
		printk(KERN_ERR "Phy %s failed to register\n",
		       phydev->dev.bus_id);
		goto err_out;
	}
	//phydev->state = PHY_RUNNING; /* make phy go up quick, but in 10Mbit/HDX
	return fixed;

err_out:
	kfree(phydev);
err_phy_dev_create:
	kfree(fixed->regs);
err_fixed_regs_alloc:
	kfree(fixed);
err_fixed_alloc:
	kfree(new_bus);
err_bus_alloc:
	kfree(dev);
err_dev_alloc:

	return NULL;

}
#endif

MODULE_DESCRIPTION("Fixed PHY device & driver for PAL");
MODULE_AUTHOR("Vitaly Bordug");
MODULE_LICENSE("GPL");

static int __init fixed_init(void)
{
	int cnt = 0;
	int i;
/* register on the bus... Not expected to be matched
 * with anything there...
 *
 */
	phy_driver_register(&fixed_mdio_driver);

/* We will create several mdio devices here, and will bound the upper
 * driver to them.
 *
 * Then the external software can lookup the phy bus by searching
 * for 0:101, to be connected to the virtual 100M Fdx phy.
 *
 * In case several virtual PHYs required, the bus_id will be in form
 * [num]:[duplex]+[speed], which make it able even to define
 * driver-specific link control callback, if for instance PHY is
 * completely SW-driven.
 */
	for (i=1; i <= CONFIG_FIXED_MII_AMNT; i++) {
#ifdef CONFIG_FIXED_MII_1000_FDX
		fixed_phy_ptrs[cnt++] = fixed_mdio_register_device(0, 1000, 1, i);
#endif
#ifdef CONFIG_FIXED_MII_100_FDX
		fixed_phy_ptrs[cnt++] = fixed_mdio_register_device(1, 100, 1, i);
#endif
#ifdef CONFIG_FIXED_MII_10_FDX
		fixed_phy_ptrs[cnt++] = fixed_mdio_register_device(2, 10, 1, i);
#endif
	}

	return 0;
}

static void __exit fixed_exit(void)
{
	int i;

	phy_driver_unregister(&fixed_mdio_driver);
	for (i=0; i < MAX_PHY_AMNT; i++)
		if ( fixed_phy_ptrs[i] )
			device_unregister(&fixed_phy_ptrs[i]->phydev->dev);
}

module_init(fixed_init);
module_exit(fixed_exit);