Add the char device interface of mdio bus, like what i2c-dev or spidev do. They make it possible for user-space programs to access the bus directly.
Signed-off-by: Wei Li <liwei1...@163.com> --- drivers/net/phy/Kconfig | 10 ++ drivers/net/phy/Makefile | 1 + drivers/net/phy/mdio-dev.c | 376 +++++++++++++++++++++++++++++++++++++++++++++ include/linux/mdio-dev.h | 28 ++++ 4 files changed, 415 insertions(+) create mode 100644 drivers/net/phy/mdio-dev.c create mode 100644 include/linux/mdio-dev.h diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig index bdfbabb..cd688c5 100644 --- a/drivers/net/phy/Kconfig +++ b/drivers/net/phy/Kconfig @@ -158,6 +158,16 @@ config MDIO_XGENE This module provides a driver for the MDIO busses found in the APM X-Gene SoC's. +config MDIO_CHARDEV + tristate "MDIO device interface" + help + Say Y here to use mdio-* device files, usually found in the /dev + directory on your system. They make it possible to have user-space + programs use the MDIO bus. + + This support is also available as a module. If so, the module + will be called mdio-dev. + endif config PHYLINK diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile index 01acbcb..a0566f8 100644 --- a/drivers/net/phy/Makefile +++ b/drivers/net/phy/Makefile @@ -38,6 +38,7 @@ obj-$(CONFIG_MDIO_OCTEON) += mdio-octeon.o obj-$(CONFIG_MDIO_SUN4I) += mdio-sun4i.o obj-$(CONFIG_MDIO_THUNDER) += mdio-thunder.o obj-$(CONFIG_MDIO_XGENE) += mdio-xgene.o +obj-$(CONFIG_MDIO_CHARDEV) += mdio-dev.o obj-$(CONFIG_SFP) += sfp.o sfp-obj-$(CONFIG_SFP) += sfp-bus.o diff --git a/drivers/net/phy/mdio-dev.c b/drivers/net/phy/mdio-dev.c new file mode 100644 index 0000000..61487d2 --- /dev/null +++ b/drivers/net/phy/mdio-dev.c @@ -0,0 +1,376 @@ +/* + * mdio-dev.c - mdio-bus driver, char device interface + * + * Copyright (C) 2018 Wei Li <liwei1...@163.com> + * + * 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. + */ + +#include <linux/cdev.h> +#include <linux/device.h> +#include <linux/fs.h> +#include <linux/mdio-dev.h> +#include <linux/mdio.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/slab.h> +#include <linux/phy.h> + +/* + * An mdio_dev represents a mii_bus. It's coupled + * with a character special file which is accessed by user mode drivers. + * + * The list of mdio_dev structures is parallel to the mii_bus lists + * maintained by the driver model, and is updated using bus notifications. + */ +struct mdio_dev { + struct list_head list; + struct mii_bus *bus; + int nr; + struct device *dev; + struct cdev cdev; +}; + +static DEFINE_MUTEX(mdio_dev_lock); +static DEFINE_IDR(mdio_dev_idr); +static LIST_HEAD(mdio_dev_list); +static DEFINE_SPINLOCK(mdio_dev_list_lock); + +static struct mdio_dev *mdio_dev_get_by_bus(struct mii_bus *bus) +{ + struct mdio_dev *mdio_dev; + + if (!bus) + return NULL; + + spin_lock(&mdio_dev_list_lock); + list_for_each_entry(mdio_dev, &mdio_dev_list, list) { + if (mdio_dev->bus == bus) + goto found; + } + mdio_dev = NULL; +found: + spin_unlock(&mdio_dev_list_lock); + return mdio_dev; +} + +static int alloc_mdio_dev_id(struct mdio_dev *mdio_dev) +{ + int id; + + mutex_lock(&mdio_dev_lock); + id = idr_alloc(&mdio_dev_idr, mdio_dev, 0, MDIO_MINORS, GFP_KERNEL); + mutex_unlock(&mdio_dev_lock); + if (WARN(id < 0, "couldn't get idr")) + return id == -ENOSPC ? -EBUSY : id; + + mdio_dev->nr = id; + return 0; +} + +static void free_mdio_dev_id(struct mdio_dev *mdio_dev) +{ + mutex_lock(&mdio_dev_lock); + idr_remove(&mdio_dev_idr, mdio_dev->nr); + mutex_unlock(&mdio_dev_lock); + mdio_dev->nr = -1; +} + +static struct mii_bus *mdiodev_get_bus(int nr) +{ + struct mdio_dev *found; + + mutex_lock(&mdio_dev_lock); + found = idr_find(&mdio_dev_idr, nr); + if (!found) + goto exit; + + if (try_module_get(found->bus->owner)) + get_device(&found->bus->dev); + else + found = NULL; + +exit: + mutex_unlock(&mdio_dev_lock); + return found ? found->bus : NULL; +} + +static void mdiodev_put_bus(struct mii_bus *bus) +{ + if (!bus) + return; + + put_device(&bus->dev); + module_put(bus->owner); +} + +static struct mdio_dev *get_free_mdio_dev(struct mii_bus *bus) +{ + struct mdio_dev *mdio_dev; + + mdio_dev = kzalloc(sizeof(*mdio_dev), GFP_KERNEL); + if (!mdio_dev) + return ERR_PTR(-ENOMEM); + mdio_dev->bus = bus; + + if (alloc_mdio_dev_id(mdio_dev)) { + printk(KERN_ERR "mdio-dev: Out of device minors\n"); + kfree(mdio_dev); + return ERR_PTR(-ENODEV); + } + + spin_lock(&mdio_dev_list_lock); + list_add_tail(&mdio_dev->list, &mdio_dev_list); + spin_unlock(&mdio_dev_list_lock); + return mdio_dev; +} + +static void put_mdio_dev(struct mdio_dev *mdio_dev) +{ + spin_lock(&mdio_dev_list_lock); + list_del(&mdio_dev->list); + spin_unlock(&mdio_dev_list_lock); + free_mdio_dev_id(mdio_dev); + kfree(mdio_dev); +} + +static ssize_t name_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct mii_bus *bus = mdiodev_get_bus(MINOR(dev->devt)); + + if (!bus) + return -ENODEV; + return sprintf(buf, "%s\n", bus->name); +} +static DEVICE_ATTR_RO(name); + +static struct attribute *mdio_attrs[] = { + &dev_attr_name.attr, + NULL, +}; +ATTRIBUTE_GROUPS(mdio); + +/*-------------------------------------------------------------------------*/ + +static long mdiodev_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct mii_bus *bus = file->private_data; + struct mii_ioctl_data data; + int res; + + dev_dbg(&bus->dev, "ioctl, cmd=0x%02x, arg=0x%02lx\n", cmd, arg); + + switch (cmd) { + case SIOCSMIIREG: + if (copy_from_user(&data, + (struct mii_ioctl_data __user *)arg, sizeof(data))) + return -EFAULT; + + res = mdiobus_write(bus, data.phy_id, data.reg_num, data.val_in); + if (res < 0) + return -EIO; + return 0; + + case SIOCGMIIREG: + if (copy_from_user(&data, + (struct mii_ioctl_data __user *)arg, sizeof(data))) + return -EFAULT; + + res = mdiobus_read(bus, data.phy_id, data.reg_num); + if (res < 0) + return -EIO; + + data.val_out = res; + if (copy_to_user((struct mii_ioctl_data __user *)arg, + &data, sizeof(data))) + return -EFAULT; + return 0; + + default: + return -ENOTTY; + } + return 0; +} + +static int mdiodev_open(struct inode *inode, struct file *file) +{ + unsigned int minor = iminor(inode); + struct mii_bus *bus; + + bus = mdiodev_get_bus(minor); + if (!bus) + return -ENODEV; + + file->private_data = bus; + + return 0; +} + +static int mdiodev_release(struct inode *inode, struct file *file) +{ + struct mii_bus *bus = file->private_data; + + mdiodev_put_bus(bus); + file->private_data = NULL; + + return 0; +} + +static const struct file_operations mdiodev_fops = { + .owner = THIS_MODULE, + .llseek = no_llseek, + .unlocked_ioctl = mdiodev_ioctl, + .open = mdiodev_open, + .release = mdiodev_release, +}; + +/*-------------------------------------------------------------------------*/ + +static int mdio_major; +static struct class *mdio_dev_class; + +static int mdiodev_attach_bus(struct device *dev, void *dummy) +{ + struct mii_bus *bus; + struct mdio_dev *mdio_dev; + int res; + + if (dev->class != &mdio_bus_class) + return 0; + bus = to_mii_bus(dev); + + mdio_dev = get_free_mdio_dev(bus); + if (IS_ERR(mdio_dev)) + return PTR_ERR(mdio_dev); + + cdev_init(&mdio_dev->cdev, &mdiodev_fops); + mdio_dev->cdev.owner = THIS_MODULE; + res = cdev_add(&mdio_dev->cdev, MKDEV(mdio_major, mdio_dev->nr), 1); + if (res) + goto error_cdev; + + /* register this mdio device with the driver core */ + mdio_dev->dev = device_create(mdio_dev_class, &bus->dev, + MKDEV(mdio_major, mdio_dev->nr), NULL, + "mdio-%d", mdio_dev->nr); + if (IS_ERR(mdio_dev->dev)) { + res = PTR_ERR(mdio_dev->dev); + goto error; + } + + pr_debug("mdio-dev: bus [%s] registered as minor %d\n", + bus->name, mdio_dev->nr); + return 0; +error: + cdev_del(&mdio_dev->cdev); +error_cdev: + put_mdio_dev(mdio_dev); + return res; +} + +static int mdiodev_detach_bus(struct device *dev, void *dummy) +{ + struct mii_bus *bus; + struct mdio_dev *mdio_dev; + + if (dev->class != &mdio_bus_class) + return 0; + bus = to_mii_bus(dev); + + mdio_dev = mdio_dev_get_by_bus(bus); + if (!mdio_dev) /* attach_bus must have failed */ + return 0; + + cdev_del(&mdio_dev->cdev); + device_destroy(mdio_dev_class, MKDEV(mdio_major, mdio_dev->nr)); + put_mdio_dev(mdio_dev); + + pr_debug("mdio-dev: bus [%s] unregistered\n", bus->name); + return 0; +} + +static int mdiodev_notifier_call(struct notifier_block *nb, unsigned long action, + void *data) +{ + struct device *dev = data; + + switch (action) { + case BUS_NOTIFY_ADD_DEVICE: + return mdiodev_attach_bus(dev, NULL); + case BUS_NOTIFY_DEL_DEVICE: + return mdiodev_detach_bus(dev, NULL); + } + + return 0; +} + +static struct notifier_block mdiodev_notifier = { + .notifier_call = mdiodev_notifier_call, +}; + +/*-------------------------------------------------------------------------*/ + +static int __init mdio_dev_init(void) +{ + int res; + dev_t devid; + + printk(KERN_INFO "mdio /dev entries driver\n"); + + res = alloc_chrdev_region(&devid, 0, MDIO_MINORS, "mdio"); + if (res) + goto out; + + mdio_major = MAJOR(devid); + mdio_dev_class = class_create(THIS_MODULE, "mdio-dev"); + if (IS_ERR(mdio_dev_class)) { + res = PTR_ERR(mdio_dev_class); + goto out_unreg_chrdev; + } + mdio_dev_class->dev_groups = mdio_groups; + + /* Keep track of buses which will be added or removed later */ + res = mdiobus_register_notifier(&mdiodev_notifier); + if (res) + goto out_unreg_class; + + /* Bind to already existing buses right away */ + class_for_each_device(&mdio_bus_class, NULL, NULL, mdiodev_attach_bus); + + return 0; + +out_unreg_class: + class_destroy(mdio_dev_class); +out_unreg_chrdev: + unregister_chrdev_region(MKDEV(mdio_major, 0), MDIO_MINORS); +out: + printk(KERN_ERR "%s: Driver Initialisation failed\n", __FILE__); + return res; +} + +static void __exit mdio_dev_exit(void) +{ + mdiobus_unregister_notifier(&mdiodev_notifier); + class_for_each_device(&mdio_bus_class, NULL, NULL, mdiodev_detach_bus); + class_destroy(mdio_dev_class); + unregister_chrdev_region(MKDEV(mdio_major, 0), MDIO_MINORS); +} + +MODULE_AUTHOR("Wei Li <liwei1...@163.com>"); +MODULE_DESCRIPTION("MDIO /dev entries driver"); +MODULE_LICENSE("GPL"); + +module_init(mdio_dev_init); +module_exit(mdio_dev_exit); diff --git a/include/linux/mdio-dev.h b/include/linux/mdio-dev.h new file mode 100644 index 0000000..cc4ed36 --- /dev/null +++ b/include/linux/mdio-dev.h @@ -0,0 +1,28 @@ +/* + * mdio-dev.h - mdio-bus driver, char device interface + * + * Copyright (C) 2018 Wei Li <liwei1...@163.com> + * + * 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., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA. + */ +#ifndef _LINUX_MDIO_DEV_H +#define _LINUX_MDIO_DEV_H + +#include <uapi/linux/mii.h> + +#define MDIO_MINORS MINORMASK + +#endif /* _LINUX_MDIO_DEV_H */ -- 2.1.4