* Copyright (C) 2006 Felix Fietkau (nbd@openwrt.org)
  * Copyright (C) 2006 Broadcom Corporation.
  * Copyright (C) 2007 Michael Buesch <m@bues.ch>
+ * Copyright (C) 2013 Hauke Mehrtens <hauke@hauke-m.de>
  *
  * Distribute under GPL.
  */
 #include <linux/dma-mapping.h>
 #include <linux/ssb/ssb.h>
 #include <linux/slab.h>
+#include <linux/phy.h>
 
 #include <asm/uaccess.h>
 #include <asm/io.h>
        __b44_writephy(bp, phy_id, location, val);
 }
 
+static int b44_mdio_read_phylib(struct mii_bus *bus, int phy_id, int location)
+{
+       u32 val;
+       struct b44 *bp = bus->priv;
+       int rc = __b44_readphy(bp, phy_id, location, &val);
+       if (rc)
+               return 0xffffffff;
+       return val;
+}
+
+static int b44_mdio_write_phylib(struct mii_bus *bus, int phy_id, int location,
+                                u16 val)
+{
+       struct b44 *bp = bus->priv;
+       return __b44_writephy(bp, phy_id, location, val);
+}
+
 static int b44_phy_reset(struct b44 *bp)
 {
        u32 val;
 
        if (bp->flags & B44_FLAG_EXTERNAL_PHY) {
                bp->flags |= B44_FLAG_100_BASE_T;
-               bp->flags |= B44_FLAG_FULL_DUPLEX;
                if (!netif_carrier_ok(bp->dev)) {
                        u32 val = br32(bp, B44_TX_CTRL);
-                       val |= TX_CTRL_DUPLEX;
+                       if (bp->flags & B44_FLAG_FULL_DUPLEX)
+                               val |= TX_CTRL_DUPLEX;
+                       else
+                               val &= ~TX_CTRL_DUPLEX;
                        bw32(bp, B44_TX_CTRL, val);
                        netif_carrier_on(bp->dev);
                        b44_link_report(bp);
 {
        struct b44 *bp = netdev_priv(dev);
 
+       if (bp->flags & B44_FLAG_EXTERNAL_PHY) {
+               BUG_ON(!bp->phydev);
+               return phy_ethtool_gset(bp->phydev, cmd);
+       }
+
        cmd->supported = (SUPPORTED_Autoneg);
        cmd->supported |= (SUPPORTED_100baseT_Half |
                          SUPPORTED_100baseT_Full |
 static int b44_set_settings(struct net_device *dev, struct ethtool_cmd *cmd)
 {
        struct b44 *bp = netdev_priv(dev);
-       u32 speed = ethtool_cmd_speed(cmd);
+       u32 speed;
+       int ret;
+
+       if (bp->flags & B44_FLAG_EXTERNAL_PHY) {
+               BUG_ON(!bp->phydev);
+               spin_lock_irq(&bp->lock);
+               if (netif_running(dev))
+                       b44_setup_phy(bp);
+
+               ret = phy_ethtool_sset(bp->phydev, cmd);
+
+               spin_unlock_irq(&bp->lock);
+
+               return ret;
+       }
+
+       speed = ethtool_cmd_speed(cmd);
 
        /* We do not support gigabit. */
        if (cmd->autoneg == AUTONEG_ENABLE) {
 
 static int b44_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd)
 {
-       struct mii_ioctl_data *data = if_mii(ifr);
        struct b44 *bp = netdev_priv(dev);
        int err = -EINVAL;
 
                goto out;
 
        spin_lock_irq(&bp->lock);
-       err = generic_mii_ioctl(&bp->mii_if, data, cmd, NULL);
+       if (bp->flags & B44_FLAG_EXTERNAL_PHY) {
+               BUG_ON(!bp->phydev);
+               err = phy_mii_ioctl(bp->phydev, ifr, cmd);
+       } else {
+               err = generic_mii_ioctl(&bp->mii_if, if_mii(ifr), cmd, NULL);
+       }
        spin_unlock_irq(&bp->lock);
 out:
        return err;
 #endif
 };
 
+static void b44_adjust_link(struct net_device *dev)
+{
+       struct b44 *bp = netdev_priv(dev);
+       struct phy_device *phydev = bp->phydev;
+       bool status_changed = 0;
+
+       BUG_ON(!phydev);
+
+       if (bp->old_link != phydev->link) {
+               status_changed = 1;
+               bp->old_link = phydev->link;
+       }
+
+       /* reflect duplex change */
+       if (phydev->link) {
+               if ((phydev->duplex == DUPLEX_HALF) &&
+                   (bp->flags & B44_FLAG_FULL_DUPLEX)) {
+                       status_changed = 1;
+                       bp->flags &= ~B44_FLAG_FULL_DUPLEX;
+               } else if ((phydev->duplex == DUPLEX_FULL) &&
+                          !(bp->flags & B44_FLAG_FULL_DUPLEX)) {
+                       status_changed = 1;
+                       bp->flags |= B44_FLAG_FULL_DUPLEX;
+               }
+       }
+
+       if (status_changed) {
+               b44_check_phy(bp);
+               phy_print_status(phydev);
+       }
+}
+
+static int b44_register_phy_one(struct b44 *bp)
+{
+       struct mii_bus *mii_bus;
+       struct ssb_device *sdev = bp->sdev;
+       struct phy_device *phydev;
+       char bus_id[MII_BUS_ID_SIZE + 3];
+       int err;
+
+       mii_bus = mdiobus_alloc();
+       if (!mii_bus) {
+               dev_err(sdev->dev, "mdiobus_alloc() failed\n");
+               err = -ENOMEM;
+               goto err_out;
+       }
+
+       mii_bus->priv = bp;
+       mii_bus->read = b44_mdio_read_phylib;
+       mii_bus->write = b44_mdio_write_phylib;
+       mii_bus->name = "b44_eth_mii";
+       mii_bus->parent = sdev->dev;
+       mii_bus->phy_mask = ~(1 << bp->phy_addr);
+       snprintf(mii_bus->id, MII_BUS_ID_SIZE, "%x", instance);
+       mii_bus->irq = kmalloc(sizeof(int) * PHY_MAX_ADDR, GFP_KERNEL);
+       if (!mii_bus->irq) {
+               dev_err(sdev->dev, "mii_bus irq allocation failed\n");
+               err = -ENOMEM;
+               goto err_out_mdiobus;
+       }
+
+       memset(mii_bus->irq, PHY_POLL, sizeof(int) * PHY_MAX_ADDR);
+
+       bp->mii_bus = mii_bus;
+
+       err = mdiobus_register(mii_bus);
+       if (err) {
+               dev_err(sdev->dev, "failed to register MII bus\n");
+               goto err_out_mdiobus_irq;
+       }
+
+       snprintf(bus_id, sizeof(bus_id), PHY_ID_FMT, mii_bus->id, bp->phy_addr);
+
+       phydev = phy_connect(bp->dev, bus_id, &b44_adjust_link,
+                            PHY_INTERFACE_MODE_MII);
+       if (IS_ERR(phydev)) {
+               dev_err(sdev->dev, "could not attach PHY at %i\n",
+                       bp->phy_addr);
+               err = PTR_ERR(phydev);
+               goto err_out_mdiobus_unregister;
+       }
+
+       /* mask with MAC supported features */
+       phydev->supported &= (SUPPORTED_100baseT_Half |
+                             SUPPORTED_100baseT_Full |
+                             SUPPORTED_Autoneg |
+                             SUPPORTED_MII);
+       phydev->advertising = phydev->supported;
+
+       bp->phydev = phydev;
+       bp->old_link = 0;
+       bp->phy_addr = phydev->addr;
+
+       dev_info(sdev->dev, "attached PHY driver [%s] (mii_bus:phy_addr=%s)\n",
+                phydev->drv->name, dev_name(&phydev->dev));
+
+       return 0;
+
+err_out_mdiobus_unregister:
+       mdiobus_unregister(mii_bus);
+
+err_out_mdiobus_irq:
+       kfree(mii_bus->irq);
+
+err_out_mdiobus:
+       mdiobus_free(mii_bus);
+
+err_out:
+       return err;
+}
+
+static void b44_unregister_phy_one(struct b44 *bp)
+{
+       struct mii_bus *mii_bus = bp->mii_bus;
+
+       phy_disconnect(bp->phydev);
+       mdiobus_unregister(mii_bus);
+       kfree(mii_bus->irq);
+       mdiobus_free(mii_bus);
+}
+
 static int b44_init_one(struct ssb_device *sdev,
                        const struct ssb_device_id *ent)
 {
        if (b44_phy_reset(bp) < 0)
                bp->phy_addr = B44_PHY_ADDR_NO_LOCAL_PHY;
 
+       if (bp->flags & B44_FLAG_EXTERNAL_PHY) {
+               err = b44_register_phy_one(bp);
+               if (err) {
+                       dev_err(sdev->dev, "Cannot register PHY, aborting\n");
+                       goto err_out_unregister_netdev;
+               }
+       }
+
        netdev_info(dev, "%s %pM\n", DRV_DESCRIPTION, dev->dev_addr);
 
        return 0;
 
+err_out_unregister_netdev:
+       unregister_netdev(dev);
 err_out_powerdown:
        ssb_bus_may_powerdown(sdev->bus);
 
 static void b44_remove_one(struct ssb_device *sdev)
 {
        struct net_device *dev = ssb_get_drvdata(sdev);
+       struct b44 *bp = netdev_priv(dev);
 
        unregister_netdev(dev);
+       if (bp->flags & B44_FLAG_EXTERNAL_PHY)
+               b44_unregister_phy_one(bp);
        ssb_device_disable(sdev, 0);
        ssb_bus_may_powerdown(sdev->bus);
        free_netdev(dev);