| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * USB Role Switch Support | |
| * | |
| * Copyright (C) 2018 Intel Corporation | |
| * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com> | |
| * Hans de Goede <hdegoede@redhat.com> | |
| */ | |
| #include <linux/component.h> | |
| #include <linux/usb/role.h> | |
| #include <linux/property.h> | |
| #include <linux/device.h> | |
| #include <linux/lockdep.h> | |
| #include <linux/module.h> | |
| #include <linux/mutex.h> | |
| #include <linux/slab.h> | |
| static const struct class role_class = { | |
| .name = "usb_role", | |
| }; | |
| struct usb_role_switch { | |
| struct device dev; | |
| struct lock_class_key key; | |
| struct mutex lock; /* device lock*/ | |
| struct module *module; /* the module this device depends on */ | |
| enum usb_role role; | |
| bool registered; | |
| /* From descriptor */ | |
| struct device *usb2_port; | |
| struct device *usb3_port; | |
| struct device *udc; | |
| usb_role_switch_set_t set; | |
| usb_role_switch_get_t get; | |
| bool allow_userspace_control; | |
| }; | |
| #define to_role_switch(d) container_of(d, struct usb_role_switch, dev) | |
| static int connector_bind(struct device *dev, struct device *connector, void *data) | |
| { | |
| int ret; | |
| ret = sysfs_create_link(&dev->kobj, &connector->kobj, "connector"); | |
| if (ret) | |
| return ret; | |
| ret = sysfs_create_link(&connector->kobj, &dev->kobj, "usb-role-switch"); | |
| if (ret) | |
| sysfs_remove_link(&dev->kobj, "connector"); | |
| return ret; | |
| } | |
| static void connector_unbind(struct device *dev, struct device *connector, void *data) | |
| { | |
| sysfs_remove_link(&connector->kobj, "usb-role-switch"); | |
| sysfs_remove_link(&dev->kobj, "connector"); | |
| } | |
| static const struct component_ops connector_ops = { | |
| .bind = connector_bind, | |
| .unbind = connector_unbind, | |
| }; | |
| /** | |
| * usb_role_switch_set_role - Set USB role for a switch | |
| * @sw: USB role switch | |
| * @role: USB role to be switched to | |
| * | |
| * Set USB role @role for @sw. | |
| */ | |
| int usb_role_switch_set_role(struct usb_role_switch *sw, enum usb_role role) | |
| { | |
| int ret; | |
| if (IS_ERR_OR_NULL(sw)) | |
| return 0; | |
| if (!sw->registered) | |
| return -EOPNOTSUPP; | |
| mutex_lock(&sw->lock); | |
| ret = sw->set(sw, role); | |
| if (!ret) { | |
| sw->role = role; | |
| kobject_uevent(&sw->dev.kobj, KOBJ_CHANGE); | |
| } | |
| mutex_unlock(&sw->lock); | |
| return ret; | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_set_role); | |
| /** | |
| * usb_role_switch_get_role - Get the USB role for a switch | |
| * @sw: USB role switch | |
| * | |
| * Depending on the role-switch-driver this function returns either a cached | |
| * value of the last set role, or reads back the actual value from the hardware. | |
| */ | |
| enum usb_role usb_role_switch_get_role(struct usb_role_switch *sw) | |
| { | |
| enum usb_role role; | |
| if (IS_ERR_OR_NULL(sw) || !sw->registered) | |
| return USB_ROLE_NONE; | |
| mutex_lock(&sw->lock); | |
| if (sw->get) | |
| role = sw->get(sw); | |
| else | |
| role = sw->role; | |
| mutex_unlock(&sw->lock); | |
| return role; | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_get_role); | |
| static void *usb_role_switch_match(const struct fwnode_handle *fwnode, const char *id, | |
| void *data) | |
| { | |
| struct device *dev; | |
| if (id && !fwnode_property_present(fwnode, id)) | |
| return NULL; | |
| dev = class_find_device_by_fwnode(&role_class, fwnode); | |
| return dev ? to_role_switch(dev) : ERR_PTR(-EPROBE_DEFER); | |
| } | |
| static struct usb_role_switch * | |
| usb_role_switch_is_parent(struct fwnode_handle *fwnode) | |
| { | |
| struct fwnode_handle *parent; | |
| struct device *dev; | |
| if (!fwnode_device_is_compatible(fwnode, "usb-b-connector")) | |
| return NULL; | |
| parent = fwnode_get_parent(fwnode); | |
| if (!fwnode_property_present(parent, "usb-role-switch")) { | |
| fwnode_handle_put(parent); | |
| return NULL; | |
| } | |
| dev = class_find_device_by_fwnode(&role_class, parent); | |
| fwnode_handle_put(parent); | |
| return dev ? to_role_switch(dev) : ERR_PTR(-EPROBE_DEFER); | |
| } | |
| /** | |
| * usb_role_switch_get - Find USB role switch linked with the caller | |
| * @dev: The caller device | |
| * | |
| * Finds and returns role switch linked with @dev. The reference count for the | |
| * found switch is incremented. | |
| */ | |
| struct usb_role_switch *usb_role_switch_get(struct device *dev) | |
| { | |
| struct usb_role_switch *sw; | |
| sw = usb_role_switch_is_parent(dev_fwnode(dev)); | |
| if (!sw) | |
| sw = device_connection_find_match(dev, "usb-role-switch", NULL, | |
| usb_role_switch_match); | |
| if (!IS_ERR_OR_NULL(sw)) | |
| WARN_ON(!try_module_get(sw->module)); | |
| return sw; | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_get); | |
| /** | |
| * fwnode_usb_role_switch_get - Find USB role switch linked with the caller | |
| * @fwnode: The caller device node | |
| * | |
| * This is similar to the usb_role_switch_get() function above, but it searches | |
| * the switch using fwnode instead of device entry. | |
| */ | |
| struct usb_role_switch *fwnode_usb_role_switch_get(struct fwnode_handle *fwnode) | |
| { | |
| struct usb_role_switch *sw; | |
| sw = usb_role_switch_is_parent(fwnode); | |
| if (!sw) | |
| sw = fwnode_connection_find_match(fwnode, "usb-role-switch", | |
| NULL, usb_role_switch_match); | |
| if (!IS_ERR_OR_NULL(sw)) | |
| WARN_ON(!try_module_get(sw->module)); | |
| return sw; | |
| } | |
| EXPORT_SYMBOL_GPL(fwnode_usb_role_switch_get); | |
| /** | |
| * usb_role_switch_put - Release handle to a switch | |
| * @sw: USB Role Switch | |
| * | |
| * Decrement reference count for @sw. | |
| */ | |
| void usb_role_switch_put(struct usb_role_switch *sw) | |
| { | |
| if (!IS_ERR_OR_NULL(sw)) { | |
| module_put(sw->module); | |
| put_device(&sw->dev); | |
| } | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_put); | |
| /** | |
| * usb_role_switch_find_by_fwnode - Find USB role switch with its fwnode | |
| * @fwnode: fwnode of the USB Role Switch | |
| * | |
| * Finds and returns role switch with @fwnode. The reference count for the | |
| * found switch is incremented. | |
| */ | |
| struct usb_role_switch * | |
| usb_role_switch_find_by_fwnode(const struct fwnode_handle *fwnode) | |
| { | |
| struct device *dev; | |
| struct usb_role_switch *sw = NULL; | |
| if (!fwnode) | |
| return NULL; | |
| dev = class_find_device_by_fwnode(&role_class, fwnode); | |
| if (dev) { | |
| sw = to_role_switch(dev); | |
| WARN_ON(!try_module_get(sw->module)); | |
| } | |
| return sw; | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_find_by_fwnode); | |
| static umode_t | |
| usb_role_switch_is_visible(struct kobject *kobj, struct attribute *attr, int n) | |
| { | |
| struct device *dev = kobj_to_dev(kobj); | |
| struct usb_role_switch *sw = to_role_switch(dev); | |
| if (sw->allow_userspace_control) | |
| return attr->mode; | |
| return 0; | |
| } | |
| static const char * const usb_roles[] = { | |
| [USB_ROLE_NONE] = "none", | |
| [USB_ROLE_HOST] = "host", | |
| [USB_ROLE_DEVICE] = "device", | |
| }; | |
| const char *usb_role_string(enum usb_role role) | |
| { | |
| if (role < 0 || role >= ARRAY_SIZE(usb_roles)) | |
| return "unknown"; | |
| return usb_roles[role]; | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_string); | |
| static ssize_t | |
| role_show(struct device *dev, struct device_attribute *attr, char *buf) | |
| { | |
| struct usb_role_switch *sw = to_role_switch(dev); | |
| enum usb_role role = usb_role_switch_get_role(sw); | |
| return sprintf(buf, "%s\n", usb_roles[role]); | |
| } | |
| static ssize_t role_store(struct device *dev, struct device_attribute *attr, | |
| const char *buf, size_t size) | |
| { | |
| struct usb_role_switch *sw = to_role_switch(dev); | |
| int ret; | |
| ret = sysfs_match_string(usb_roles, buf); | |
| if (ret < 0) { | |
| bool res; | |
| /* Extra check if the user wants to disable the switch */ | |
| ret = kstrtobool(buf, &res); | |
| if (ret || res) | |
| return -EINVAL; | |
| } | |
| ret = usb_role_switch_set_role(sw, ret); | |
| if (ret) | |
| return ret; | |
| return size; | |
| } | |
| static DEVICE_ATTR_RW(role); | |
| static struct attribute *usb_role_switch_attrs[] = { | |
| &dev_attr_role.attr, | |
| NULL, | |
| }; | |
| static const struct attribute_group usb_role_switch_group = { | |
| .is_visible = usb_role_switch_is_visible, | |
| .attrs = usb_role_switch_attrs, | |
| }; | |
| static const struct attribute_group *usb_role_switch_groups[] = { | |
| &usb_role_switch_group, | |
| NULL, | |
| }; | |
| static int usb_role_switch_uevent(const struct device *dev, struct kobj_uevent_env *env) | |
| { | |
| int ret; | |
| ret = add_uevent_var(env, "USB_ROLE_SWITCH=%s", dev_name(dev)); | |
| if (ret) | |
| dev_err(dev, "failed to add uevent USB_ROLE_SWITCH\n"); | |
| return ret; | |
| } | |
| static void usb_role_switch_release(struct device *dev) | |
| { | |
| struct usb_role_switch *sw = to_role_switch(dev); | |
| mutex_destroy(&sw->lock); | |
| lockdep_unregister_key(&sw->key); | |
| kfree(sw); | |
| } | |
| static const struct device_type usb_role_dev_type = { | |
| .name = "usb_role_switch", | |
| .groups = usb_role_switch_groups, | |
| .uevent = usb_role_switch_uevent, | |
| .release = usb_role_switch_release, | |
| }; | |
| /** | |
| * usb_role_switch_register - Register USB Role Switch | |
| * @parent: Parent device for the switch | |
| * @desc: Description of the switch | |
| * | |
| * USB Role Switch is a device capable or choosing the role for USB connector. | |
| * On platforms where the USB controller is dual-role capable, the controller | |
| * driver will need to register the switch. On platforms where the USB host and | |
| * USB device controllers behind the connector are separate, there will be a | |
| * mux, and the driver for that mux will need to register the switch. | |
| * | |
| * Returns handle to a new role switch or ERR_PTR. The content of @desc is | |
| * copied. | |
| */ | |
| struct usb_role_switch * | |
| usb_role_switch_register(struct device *parent, | |
| const struct usb_role_switch_desc *desc) | |
| { | |
| struct usb_role_switch *sw; | |
| int ret; | |
| if (!desc || !desc->set) | |
| return ERR_PTR(-EINVAL); | |
| sw = kzalloc_obj(*sw); | |
| if (!sw) | |
| return ERR_PTR(-ENOMEM); | |
| lockdep_register_key(&sw->key); | |
| mutex_init_with_key(&sw->lock, &sw->key); | |
| sw->allow_userspace_control = desc->allow_userspace_control; | |
| sw->usb2_port = desc->usb2_port; | |
| sw->usb3_port = desc->usb3_port; | |
| sw->udc = desc->udc; | |
| sw->set = desc->set; | |
| sw->get = desc->get; | |
| sw->module = parent->driver->owner; | |
| sw->dev.parent = parent; | |
| sw->dev.fwnode = desc->fwnode; | |
| sw->dev.class = &role_class; | |
| sw->dev.type = &usb_role_dev_type; | |
| dev_set_drvdata(&sw->dev, desc->driver_data); | |
| dev_set_name(&sw->dev, "%s-role-switch", | |
| desc->name ? desc->name : dev_name(parent)); | |
| sw->registered = true; | |
| ret = device_register(&sw->dev); | |
| if (ret) { | |
| sw->registered = false; | |
| put_device(&sw->dev); | |
| return ERR_PTR(ret); | |
| } | |
| if (dev_fwnode(&sw->dev)) { | |
| ret = component_add(&sw->dev, &connector_ops); | |
| if (ret) | |
| dev_warn(&sw->dev, "failed to add component\n"); | |
| } | |
| /* TODO: Symlinks for the host port and the device controller. */ | |
| return sw; | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_register); | |
| /** | |
| * usb_role_switch_unregister - Unregsiter USB Role Switch | |
| * @sw: USB Role Switch | |
| * | |
| * Unregister switch that was registered with usb_role_switch_register(). | |
| */ | |
| void usb_role_switch_unregister(struct usb_role_switch *sw) | |
| { | |
| if (IS_ERR_OR_NULL(sw)) | |
| return; | |
| sw->registered = false; | |
| if (dev_fwnode(&sw->dev)) | |
| component_del(&sw->dev, &connector_ops); | |
| device_unregister(&sw->dev); | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_unregister); | |
| /** | |
| * usb_role_switch_set_drvdata - Assign private data pointer to a switch | |
| * @sw: USB Role Switch | |
| * @data: Private data pointer | |
| */ | |
| void usb_role_switch_set_drvdata(struct usb_role_switch *sw, void *data) | |
| { | |
| dev_set_drvdata(&sw->dev, data); | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_set_drvdata); | |
| /** | |
| * usb_role_switch_get_drvdata - Get the private data pointer of a switch | |
| * @sw: USB Role Switch | |
| */ | |
| void *usb_role_switch_get_drvdata(struct usb_role_switch *sw) | |
| { | |
| return dev_get_drvdata(&sw->dev); | |
| } | |
| EXPORT_SYMBOL_GPL(usb_role_switch_get_drvdata); | |
| static int __init usb_roles_init(void) | |
| { | |
| return class_register(&role_class); | |
| } | |
| subsys_initcall(usb_roles_init); | |
| static void __exit usb_roles_exit(void) | |
| { | |
| class_unregister(&role_class); | |
| } | |
| module_exit(usb_roles_exit); | |
| MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>"); | |
| MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); | |
| MODULE_LICENSE("GPL v2"); | |
| MODULE_DESCRIPTION("USB Role Class"); |