@@ -13,6 +13,7 @@
#include <linux/fwnode_mdio.h>
#include <linux/kernel.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/netdevice.h>
#include <linux/of.h>
#include <linux/of_irq.h>
@@ -20,6 +21,8 @@
#include <linux/of_net.h>
#include <linux/phy.h>
#include <linux/phy_fixed.h>
+#include <linux/sched.h>
+#include <linux/string.h>
#define DEFAULT_GPIO_RESET_DELAY 10 /* in microseconds */
@@ -27,6 +30,52 @@ MODULE_AUTHOR("Grant Likely <grant.likely@secretlab.ca>");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("OpenFirmware MDIO bus (Ethernet PHY) accessors");
+#if IS_ENABLED(CONFIG_OF_DYNAMIC)
+/*
+ * OF changes can nest when probing one MDIO device enables another node on
+ * the same bus. Serialize independent changes while allowing that nesting.
+ */
+static DEFINE_MUTEX(of_mdio_reconfig_mutex);
+static struct task_struct *of_mdio_reconfig_owner;
+static unsigned int of_mdio_reconfig_depth;
+
+static void of_mdio_reconfig_lock(void)
+{
+ if (!mutex_trylock(&of_mdio_reconfig_mutex)) {
+ if (of_mdio_reconfig_owner == current) {
+ of_mdio_reconfig_depth++;
+ return;
+ }
+ mutex_lock(&of_mdio_reconfig_mutex);
+ }
+
+ WARN_ON_ONCE(of_mdio_reconfig_owner);
+ WARN_ON_ONCE(of_mdio_reconfig_depth);
+ of_mdio_reconfig_owner = current;
+ of_mdio_reconfig_depth = 1;
+}
+
+static void of_mdio_reconfig_unlock(void)
+{
+ WARN_ON_ONCE(of_mdio_reconfig_owner != current);
+ WARN_ON_ONCE(!of_mdio_reconfig_depth);
+
+ if (--of_mdio_reconfig_depth)
+ return;
+
+ of_mdio_reconfig_owner = NULL;
+ mutex_unlock(&of_mdio_reconfig_mutex);
+}
+#else
+static inline void of_mdio_reconfig_lock(void)
+{
+}
+
+static inline void of_mdio_reconfig_unlock(void)
+{
+}
+#endif
+
/* Extract the clause 22 phy ID from the compatible string of the form
* ethernet-phy-idAAAA.BBBB */
static int of_get_phy_id(struct device_node *device, u32 *phy_id)
@@ -44,7 +93,7 @@ int of_mdiobus_phy_device_register(struct mii_bus *mdio, struct phy_device *phy,
EXPORT_SYMBOL(of_mdiobus_phy_device_register);
static int of_mdiobus_register_phy(struct mii_bus *mdio,
- struct device_node *child, u32 addr)
+ struct device_node *child, u32 addr)
{
return fwnode_mdiobus_register_phy(mdio, of_fwnode_handle(child), addr);
}
@@ -80,10 +129,20 @@ static int of_mdiobus_register_device(struct mii_bus *mdio,
static int of_mdiobus_register_child(struct mii_bus *mdio,
struct device_node *child, u32 addr)
{
+ int rc;
+
+ if (of_node_test_and_set_flag(child, OF_POPULATED))
+ return 0;
+
if (of_mdiobus_child_is_phy(child))
- return of_mdiobus_register_phy(mdio, child, addr);
+ rc = of_mdiobus_register_phy(mdio, child, addr);
+ else
+ rc = of_mdiobus_register_device(mdio, child, addr);
- return of_mdiobus_register_device(mdio, child, addr);
+ if (rc)
+ of_node_clear_flag(child, OF_POPULATED);
+
+ return rc;
}
/* The following is a list of PHY compatible strings which appear in
@@ -255,13 +314,21 @@ int __of_mdiobus_register(struct mii_bus *mdio, struct device_node *np,
if (rc)
return rc;
+ of_mdio_reconfig_lock();
+ rc = mdiobus_device_change_begin(mdio, false);
+ if (rc) {
+ of_mdio_reconfig_unlock();
+ mdiobus_unregister(mdio);
+ return rc;
+ }
+
/* Loop over the child nodes and register a phy_device for each phy */
rc = __of_mdiobus_parse_phys(mdio, np, &scanphys);
if (rc)
- goto unregister;
+ goto out_change;
if (!scanphys)
- return 0;
+ goto out_change;
/* auto scan for PHYs with empty reg property */
for_each_available_child_of_node(np, child) {
@@ -272,19 +339,380 @@ int __of_mdiobus_register(struct mii_bus *mdio, struct device_node *np,
rc = of_mdiobus_scan_phy(mdio, child);
if (rc && rc != -ENODEV)
- goto put_unregister;
+ goto put_child;
+ rc = 0;
}
- return 0;
+out_change:
+ mdiobus_device_change_end(mdio, false);
+ of_mdio_reconfig_unlock();
+ if (!rc)
+ return 0;
-put_unregister:
- of_node_put(child);
-unregister:
mdiobus_unregister(mdio);
return rc;
+
+put_child:
+ of_node_put(child);
+ goto out_change;
}
EXPORT_SYMBOL(__of_mdiobus_register);
+#if IS_ENABLED(CONFIG_OF_DYNAMIC)
+static bool of_mdiobus_node_is_available(struct device_node *node)
+{
+ return !of_node_check_flag(node, OF_DETACHED) &&
+ of_device_is_available(node);
+}
+
+static int of_mdiobus_add_node(struct mii_bus *mdio,
+ struct device_node *node)
+{
+ struct device_node *child;
+ int addr, rc, ret = 0;
+
+ if (!of_mdiobus_node_is_available(node))
+ return 0;
+
+ if (of_node_name_eq(node, "ethernet-phy-package")) {
+ if (!of_property_present(node, "reg"))
+ return 0;
+
+ for_each_available_child_of_node(node, child) {
+ rc = of_mdiobus_add_node(mdio, child);
+ if (rc && rc != -ENODEV && !ret)
+ ret = rc;
+ }
+
+ return ret;
+ }
+
+ addr = of_mdio_parse_addr(&mdio->dev, node);
+ if (addr < 0)
+ rc = of_mdiobus_scan_phy(mdio, node);
+ else
+ rc = of_mdiobus_register_child(mdio, node, addr);
+
+ if (rc == -ENODEV && addr >= 0)
+ dev_err(&mdio->dev,
+ "MDIO device at address %d is missing.\n", addr);
+
+ return rc;
+}
+
+static bool of_mdiobus_node_is_busy(struct mii_bus *mdio,
+ struct device_node *node)
+{
+ struct mdio_device *mdiodev;
+ bool busy = false;
+
+ if (of_node_name_eq(node, "ethernet-phy-package")) {
+ for_each_child_of_node_scoped(node, child) {
+ if (of_mdiobus_node_is_busy(mdio, child))
+ return true;
+ }
+
+ return false;
+ }
+
+ if (!of_node_check_flag(node, OF_POPULATED))
+ return false;
+
+ mdiodev = of_mdio_find_device(node);
+ if (!mdiodev)
+ return true;
+ if (mdiodev->bus != mdio) {
+ put_device(&mdiodev->dev);
+ return false;
+ }
+
+ mutex_lock(&mdio->mdio_map_lock);
+ busy = mdio->mdio_map[mdiodev->addr] != mdiodev ||
+ (mdio->mdio_map_pending & BIT(mdiodev->addr));
+ if (mdiodev->flags & MDIO_DEVICE_FLAG_PHY) {
+ struct phy_device *phydev = to_phy_device(&mdiodev->dev);
+
+ if (phydev->attached) {
+ busy = true;
+ dev_warn(&mdiodev->dev,
+ "cannot remove an attached PHY; remove its consumer first\n");
+ }
+ }
+ mutex_unlock(&mdio->mdio_map_lock);
+ put_device(&mdiodev->dev);
+
+ return busy;
+}
+
+static struct device_node *
+of_mdiobus_get_removal_scope(struct device_node *node)
+{
+ struct device_node *parent;
+
+ if (of_node_name_eq(node, "ethernet-phy-package"))
+ return of_node_get(node);
+
+ parent = of_get_parent(node);
+ if (of_node_name_eq(parent, "ethernet-phy-package"))
+ return parent;
+
+ of_node_put(parent);
+ return of_node_get(node);
+}
+
+static int of_mdiobus_remove_node(struct mii_bus *mdio,
+ struct device_node *node)
+{
+ struct mdio_device *mdiodev;
+ struct device_node *child;
+ int ret;
+
+ if (of_node_name_eq(node, "ethernet-phy-package")) {
+ for_each_child_of_node(node, child) {
+ ret = of_mdiobus_remove_node(mdio, child);
+ if (ret) {
+ of_node_put(child);
+ return ret;
+ }
+ }
+ return 0;
+ }
+
+ if (!of_node_check_flag(node, OF_POPULATED))
+ return 0;
+
+ /* The OF node lookup also covers PHYs found by address scanning. */
+ mdiodev = of_mdio_find_device(node);
+ if (!mdiodev)
+ return -EBUSY;
+ if (mdiodev->bus != mdio) {
+ put_device(&mdiodev->dev);
+ return -ENODEV;
+ }
+
+ ret = mdiodev->device_remove_dynamic(mdiodev);
+ if (!ret)
+ mdiodev->device_free(mdiodev);
+ put_device(&mdiodev->dev);
+
+ return ret == -ENODEV ? 0 : ret;
+}
+
+static struct mii_bus *of_mdiobus_find_parent(struct device_node *node)
+{
+ struct device_node *parent, *bus_node;
+ struct mii_bus *mdio;
+
+ parent = of_get_parent(node);
+ if (!parent)
+ return NULL;
+
+ if (of_node_name_eq(parent, "ethernet-phy-package")) {
+ if (!of_device_is_available(parent) ||
+ !of_property_present(parent, "reg")) {
+ of_node_put(parent);
+ return NULL;
+ }
+
+ bus_node = of_get_parent(parent);
+ of_node_put(parent);
+ } else {
+ bus_node = parent;
+ }
+
+ mdio = of_mdio_find_bus(bus_node);
+ of_node_put(bus_node);
+
+ return mdio;
+}
+
+#if IS_ENABLED(CONFIG_OF_OVERLAY)
+/* Overlay entry notifier errors cannot stop removal after the tree changed. */
+static bool of_mdiobus_live_node_is_busy(struct device_node *node)
+{
+ struct device_node *scope;
+ struct mii_bus *mdio;
+ bool busy = true;
+
+ scope = of_mdiobus_get_removal_scope(node);
+ mdio = of_mdiobus_find_parent(scope);
+ if (!mdio)
+ goto out_put_scope;
+
+ if (!mdiobus_device_change_begin(mdio, true)) {
+ busy = of_mdiobus_node_is_busy(mdio, scope);
+ mdiobus_device_change_end(mdio, true);
+ }
+ put_device(&mdio->dev);
+
+out_put_scope:
+ of_node_put(scope);
+ return busy;
+}
+
+static struct device_node *
+of_mdiobus_overlay_target_child(struct device_node *target,
+ struct device_node *overlay_child)
+{
+ const char *name = kbasename(overlay_child->full_name);
+ struct device_node *child;
+
+ for_each_child_of_node(target, child) {
+ if (!of_node_cmp(kbasename(child->full_name), name))
+ return child;
+ }
+
+ return NULL;
+}
+
+static bool of_mdiobus_overlay_node_is_busy(struct device_node *overlay,
+ struct device_node *target,
+ bool added)
+{
+ struct device_node *overlay_child, *target_child;
+ bool busy, child_added;
+
+ if ((added || of_property_present(overlay, "status")) &&
+ of_mdiobus_live_node_is_busy(target))
+ return true;
+
+ for_each_child_of_node(overlay, overlay_child) {
+ target_child = of_mdiobus_overlay_target_child(target,
+ overlay_child);
+ if (!target_child)
+ continue;
+
+ child_added = of_node_check_flag(target_child, OF_OVERLAY);
+ busy = of_mdiobus_overlay_node_is_busy(overlay_child,
+ target_child, child_added);
+ of_node_put(target_child);
+ if (busy) {
+ of_node_put(overlay_child);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static int of_mdiobus_overlay_notify(struct notifier_block *nb,
+ unsigned long action, void *arg)
+{
+ struct of_overlay_notify_data *nd = arg;
+ bool busy;
+
+ if (action != OF_OVERLAY_PRE_REMOVE)
+ return NOTIFY_OK;
+
+ of_mdio_reconfig_lock();
+ busy = of_mdiobus_overlay_node_is_busy(nd->overlay, nd->target,
+ false);
+ of_mdio_reconfig_unlock();
+
+ return busy ? notifier_from_errno(-EBUSY) : NOTIFY_OK;
+}
+
+static struct notifier_block of_mdio_overlay_notifier = {
+ .notifier_call = of_mdiobus_overlay_notify,
+};
+#endif
+
+static int of_mdiobus_notify(struct notifier_block *nb, unsigned long action,
+ void *arg)
+{
+ struct of_reconfig_data *rd = arg;
+ struct device_node *scope;
+ struct mii_bus *mdio;
+ enum of_reconfig_change change;
+ bool removing;
+ int rc, ret = NOTIFY_OK;
+
+ of_mdio_reconfig_lock();
+ change = of_reconfig_get_state_change(action, rd);
+ switch (change) {
+ case OF_RECONFIG_CHANGE_ADD:
+ /* A newer change may have made this notification stale. */
+ if (!of_mdiobus_node_is_available(rd->dn))
+ goto out_unlock;
+ removing = false;
+ break;
+ case OF_RECONFIG_CHANGE_REMOVE:
+ /* A newer change may have made this notification stale. */
+ if (of_mdiobus_node_is_available(rd->dn))
+ goto out_unlock;
+ removing = true;
+ break;
+ default:
+ goto out_unlock;
+ }
+
+ mdio = of_mdiobus_find_parent(rd->dn);
+ if (!mdio)
+ goto out_unlock;
+
+ rc = mdiobus_device_change_begin(mdio, removing);
+ if (rc) {
+ if (!removing)
+ ret = notifier_from_errno(-EPROBE_DEFER);
+ goto out_put_mdio;
+ }
+
+ if (!removing) {
+ rc = of_mdiobus_add_node(mdio, rd->dn);
+ } else {
+ /* The node may already be detached from its parent hierarchy. */
+ scope = of_mdiobus_get_removal_scope(rd->dn);
+ if (of_mdiobus_node_is_busy(mdio, scope))
+ rc = -EBUSY;
+ else
+ rc = of_mdiobus_remove_node(mdio, rd->dn);
+ of_node_put(scope);
+ }
+
+ mdiobus_device_change_end(mdio, removing);
+ if (rc && (removing || rc != -ENODEV))
+ ret = notifier_from_errno(rc);
+
+out_put_mdio:
+ put_device(&mdio->dev);
+out_unlock:
+ of_mdio_reconfig_unlock();
+
+ return ret;
+}
+
+static struct notifier_block of_mdio_notifier = {
+ .notifier_call = of_mdiobus_notify,
+};
+
+static int __init of_mdio_init(void)
+{
+ int ret;
+
+ ret = of_reconfig_notifier_register(&of_mdio_notifier);
+ if (ret)
+ return ret;
+
+#if IS_ENABLED(CONFIG_OF_OVERLAY)
+ ret = of_overlay_notifier_register(&of_mdio_overlay_notifier);
+ if (ret)
+ of_reconfig_notifier_unregister(&of_mdio_notifier);
+#endif
+
+ return ret;
+}
+module_init(of_mdio_init);
+
+static void __exit of_mdio_exit(void)
+{
+#if IS_ENABLED(CONFIG_OF_OVERLAY)
+ of_overlay_notifier_unregister(&of_mdio_overlay_notifier);
+#endif
+ of_reconfig_notifier_unregister(&of_mdio_notifier);
+}
+module_exit(of_mdio_exit);
+#endif /* CONFIG_OF_DYNAMIC */
+
/**
* of_mdio_find_device - Given a device tree node, find the mdio_device
* @np: pointer to the mdio_device's device tree node