diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
commit | 2c3c1048746a4622d8c89a29670120dc8fab93c4 (patch) | |
tree | 848558de17fb3008cdf4d861b01ac7781903ce39 /drivers/usb/typec/mux.c | |
parent | Initial commit. (diff) | |
download | linux-2c3c1048746a4622d8c89a29670120dc8fab93c4.tar.xz linux-2c3c1048746a4622d8c89a29670120dc8fab93c4.zip |
Adding upstream version 6.1.76.upstream/6.1.76upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/usb/typec/mux.c')
-rw-r--r-- | drivers/usb/typec/mux.c | 518 |
1 files changed, 518 insertions, 0 deletions
diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c new file mode 100644 index 000000000..941735c73 --- /dev/null +++ b/drivers/usb/typec/mux.c @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * USB Type-C Multiplexer/DeMultiplexer Switch support + * + * Copyright (C) 2018 Intel Corporation + * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com> + * Hans de Goede <hdegoede@redhat.com> + */ + +#include <linux/device.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/property.h> +#include <linux/slab.h> + +#include "class.h" +#include "mux.h" + +#define TYPEC_MUX_MAX_DEVS 3 + +struct typec_switch { + struct typec_switch_dev *sw_devs[TYPEC_MUX_MAX_DEVS]; + unsigned int num_sw_devs; +}; + +static int switch_fwnode_match(struct device *dev, const void *fwnode) +{ + if (!is_typec_switch_dev(dev)) + return 0; + + return device_match_fwnode(dev, fwnode); +} + +static void *typec_switch_match(struct fwnode_handle *fwnode, const char *id, + void *data) +{ + struct device *dev; + + /* + * Device graph (OF graph) does not give any means to identify the + * device type or the device class of the remote port parent that @fwnode + * represents, so in order to identify the type or the class of @fwnode + * an additional device property is needed. With typec switches the + * property is named "orientation-switch" (@id). The value of the device + * property is ignored. + */ + if (id && !fwnode_property_present(fwnode, id)) + return NULL; + + /* + * At this point we are sure that @fwnode is a typec switch in all + * cases. If the switch hasn't yet been registered for some reason, the + * function "defers probe" for now. + */ + dev = class_find_device(&typec_mux_class, NULL, fwnode, + switch_fwnode_match); + + return dev ? to_typec_switch_dev(dev) : ERR_PTR(-EPROBE_DEFER); +} + +/** + * fwnode_typec_switch_get - Find USB Type-C orientation switch + * @fwnode: The caller device node + * + * Finds a switch linked with @dev. Returns a reference to the switch on + * success, NULL if no matching connection was found, or + * ERR_PTR(-EPROBE_DEFER) when a connection was found but the switch + * has not been enumerated yet. + */ +struct typec_switch *fwnode_typec_switch_get(struct fwnode_handle *fwnode) +{ + struct typec_switch_dev *sw_devs[TYPEC_MUX_MAX_DEVS]; + struct typec_switch *sw; + int count; + int err; + int i; + + sw = kzalloc(sizeof(*sw), GFP_KERNEL); + if (!sw) + return ERR_PTR(-ENOMEM); + + count = fwnode_connection_find_matches(fwnode, "orientation-switch", NULL, + typec_switch_match, + (void **)sw_devs, + ARRAY_SIZE(sw_devs)); + if (count <= 0) { + kfree(sw); + return NULL; + } + + for (i = 0; i < count; i++) { + if (IS_ERR(sw_devs[i])) { + err = PTR_ERR(sw_devs[i]); + goto put_sw_devs; + } + } + + for (i = 0; i < count; i++) { + WARN_ON(!try_module_get(sw_devs[i]->dev.parent->driver->owner)); + sw->sw_devs[i] = sw_devs[i]; + } + + sw->num_sw_devs = count; + + return sw; + +put_sw_devs: + for (i = 0; i < count; i++) { + if (!IS_ERR(sw_devs[i])) + put_device(&sw_devs[i]->dev); + } + + kfree(sw); + + return ERR_PTR(err); +} +EXPORT_SYMBOL_GPL(fwnode_typec_switch_get); + +/** + * typec_switch_put - Release USB Type-C orientation switch + * @sw: USB Type-C orientation switch + * + * Decrement reference count for @sw. + */ +void typec_switch_put(struct typec_switch *sw) +{ + struct typec_switch_dev *sw_dev; + unsigned int i; + + if (IS_ERR_OR_NULL(sw)) + return; + + for (i = 0; i < sw->num_sw_devs; i++) { + sw_dev = sw->sw_devs[i]; + + module_put(sw_dev->dev.parent->driver->owner); + put_device(&sw_dev->dev); + } + kfree(sw); +} +EXPORT_SYMBOL_GPL(typec_switch_put); + +static void typec_switch_release(struct device *dev) +{ + kfree(to_typec_switch_dev(dev)); +} + +const struct device_type typec_switch_dev_type = { + .name = "orientation_switch", + .release = typec_switch_release, +}; + +/** + * typec_switch_register - Register USB Type-C orientation switch + * @parent: Parent device + * @desc: Orientation switch description + * + * This function registers a switch that can be used for routing the correct + * data pairs depending on the cable plug orientation from the USB Type-C + * connector to the USB controllers. USB Type-C plugs can be inserted + * right-side-up or upside-down. + */ +struct typec_switch_dev * +typec_switch_register(struct device *parent, + const struct typec_switch_desc *desc) +{ + struct typec_switch_dev *sw_dev; + int ret; + + if (!desc || !desc->set) + return ERR_PTR(-EINVAL); + + sw_dev = kzalloc(sizeof(*sw_dev), GFP_KERNEL); + if (!sw_dev) + return ERR_PTR(-ENOMEM); + + sw_dev->set = desc->set; + + device_initialize(&sw_dev->dev); + sw_dev->dev.parent = parent; + sw_dev->dev.fwnode = desc->fwnode; + sw_dev->dev.class = &typec_mux_class; + sw_dev->dev.type = &typec_switch_dev_type; + sw_dev->dev.driver_data = desc->drvdata; + ret = dev_set_name(&sw_dev->dev, "%s-switch", desc->name ? desc->name : dev_name(parent)); + if (ret) { + put_device(&sw_dev->dev); + return ERR_PTR(ret); + } + + ret = device_add(&sw_dev->dev); + if (ret) { + dev_err(parent, "failed to register switch (%d)\n", ret); + put_device(&sw_dev->dev); + return ERR_PTR(ret); + } + + return sw_dev; +} +EXPORT_SYMBOL_GPL(typec_switch_register); + +int typec_switch_set(struct typec_switch *sw, + enum typec_orientation orientation) +{ + struct typec_switch_dev *sw_dev; + unsigned int i; + int ret; + + if (IS_ERR_OR_NULL(sw)) + return 0; + + for (i = 0; i < sw->num_sw_devs; i++) { + sw_dev = sw->sw_devs[i]; + + ret = sw_dev->set(sw_dev, orientation); + if (ret) + return ret; + } + + return 0; +} +EXPORT_SYMBOL_GPL(typec_switch_set); + +/** + * typec_switch_unregister - Unregister USB Type-C orientation switch + * @sw_dev: USB Type-C orientation switch + * + * Unregister switch that was registered with typec_switch_register(). + */ +void typec_switch_unregister(struct typec_switch_dev *sw_dev) +{ + if (!IS_ERR_OR_NULL(sw_dev)) + device_unregister(&sw_dev->dev); +} +EXPORT_SYMBOL_GPL(typec_switch_unregister); + +void typec_switch_set_drvdata(struct typec_switch_dev *sw_dev, void *data) +{ + dev_set_drvdata(&sw_dev->dev, data); +} +EXPORT_SYMBOL_GPL(typec_switch_set_drvdata); + +void *typec_switch_get_drvdata(struct typec_switch_dev *sw_dev) +{ + return dev_get_drvdata(&sw_dev->dev); +} +EXPORT_SYMBOL_GPL(typec_switch_get_drvdata); + +/* ------------------------------------------------------------------------- */ + +struct typec_mux { + struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS]; + unsigned int num_mux_devs; +}; + +static int mux_fwnode_match(struct device *dev, const void *fwnode) +{ + if (!is_typec_mux_dev(dev)) + return 0; + + return device_match_fwnode(dev, fwnode); +} + +static void *typec_mux_match(struct fwnode_handle *fwnode, const char *id, + void *data) +{ + const struct typec_altmode_desc *desc = data; + struct device *dev; + bool match; + int nval; + u16 *val; + int ret; + int i; + + /* + * Check has the identifier already been "consumed". If it + * has, no need to do any extra connection identification. + */ + match = !id; + if (match) + goto find_mux; + + if (!desc) { + /* + * Accessory Mode muxes & muxes which explicitly specify + * the required identifier can avoid SVID matching. + */ + match = fwnode_property_present(fwnode, "accessory") || + fwnode_property_present(fwnode, id); + if (match) + goto find_mux; + return NULL; + } + + /* Alternate Mode muxes */ + nval = fwnode_property_count_u16(fwnode, "svid"); + if (nval <= 0) + return NULL; + + val = kcalloc(nval, sizeof(*val), GFP_KERNEL); + if (!val) + return ERR_PTR(-ENOMEM); + + ret = fwnode_property_read_u16_array(fwnode, "svid", val, nval); + if (ret < 0) { + kfree(val); + return ERR_PTR(ret); + } + + for (i = 0; i < nval; i++) { + match = val[i] == desc->svid; + if (match) { + kfree(val); + goto find_mux; + } + } + kfree(val); + return NULL; + +find_mux: + dev = class_find_device(&typec_mux_class, NULL, fwnode, + mux_fwnode_match); + + return dev ? to_typec_mux_dev(dev) : ERR_PTR(-EPROBE_DEFER); +} + +/** + * fwnode_typec_mux_get - Find USB Type-C Multiplexer + * @fwnode: The caller device node + * @desc: Alt Mode description + * + * Finds a mux linked to the caller. This function is primarily meant for the + * Type-C drivers. Returns a reference to the mux on success, NULL if no + * matching connection was found, or ERR_PTR(-EPROBE_DEFER) when a connection + * was found but the mux has not been enumerated yet. + */ +struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode, + const struct typec_altmode_desc *desc) +{ + struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS]; + struct typec_mux *mux; + int count; + int err; + int i; + + mux = kzalloc(sizeof(*mux), GFP_KERNEL); + if (!mux) + return ERR_PTR(-ENOMEM); + + count = fwnode_connection_find_matches(fwnode, "mode-switch", + (void *)desc, typec_mux_match, + (void **)mux_devs, + ARRAY_SIZE(mux_devs)); + if (count <= 0) { + kfree(mux); + return NULL; + } + + for (i = 0; i < count; i++) { + if (IS_ERR(mux_devs[i])) { + err = PTR_ERR(mux_devs[i]); + goto put_mux_devs; + } + } + + for (i = 0; i < count; i++) { + WARN_ON(!try_module_get(mux_devs[i]->dev.parent->driver->owner)); + mux->mux_devs[i] = mux_devs[i]; + } + + mux->num_mux_devs = count; + + return mux; + +put_mux_devs: + for (i = 0; i < count; i++) { + if (!IS_ERR(mux_devs[i])) + put_device(&mux_devs[i]->dev); + } + + kfree(mux); + + return ERR_PTR(err); +} +EXPORT_SYMBOL_GPL(fwnode_typec_mux_get); + +/** + * typec_mux_put - Release handle to a Multiplexer + * @mux: USB Type-C Connector Multiplexer/DeMultiplexer + * + * Decrements reference count for @mux. + */ +void typec_mux_put(struct typec_mux *mux) +{ + struct typec_mux_dev *mux_dev; + unsigned int i; + + if (IS_ERR_OR_NULL(mux)) + return; + + for (i = 0; i < mux->num_mux_devs; i++) { + mux_dev = mux->mux_devs[i]; + module_put(mux_dev->dev.parent->driver->owner); + put_device(&mux_dev->dev); + } + kfree(mux); +} +EXPORT_SYMBOL_GPL(typec_mux_put); + +int typec_mux_set(struct typec_mux *mux, struct typec_mux_state *state) +{ + struct typec_mux_dev *mux_dev; + unsigned int i; + int ret; + + if (IS_ERR_OR_NULL(mux)) + return 0; + + for (i = 0; i < mux->num_mux_devs; i++) { + mux_dev = mux->mux_devs[i]; + + ret = mux_dev->set(mux_dev, state); + if (ret) + return ret; + } + + return 0; +} +EXPORT_SYMBOL_GPL(typec_mux_set); + +static void typec_mux_release(struct device *dev) +{ + kfree(to_typec_mux_dev(dev)); +} + +const struct device_type typec_mux_dev_type = { + .name = "mode_switch", + .release = typec_mux_release, +}; + +/** + * typec_mux_register - Register Multiplexer routing USB Type-C pins + * @parent: Parent device + * @desc: Multiplexer description + * + * USB Type-C connectors can be used for alternate modes of operation besides + * USB when Accessory/Alternate Modes are supported. With some of those modes, + * the pins on the connector need to be reconfigured. This function registers + * multiplexer switches routing the pins on the connector. + */ +struct typec_mux_dev * +typec_mux_register(struct device *parent, const struct typec_mux_desc *desc) +{ + struct typec_mux_dev *mux_dev; + int ret; + + if (!desc || !desc->set) + return ERR_PTR(-EINVAL); + + mux_dev = kzalloc(sizeof(*mux_dev), GFP_KERNEL); + if (!mux_dev) + return ERR_PTR(-ENOMEM); + + mux_dev->set = desc->set; + + device_initialize(&mux_dev->dev); + mux_dev->dev.parent = parent; + mux_dev->dev.fwnode = desc->fwnode; + mux_dev->dev.class = &typec_mux_class; + mux_dev->dev.type = &typec_mux_dev_type; + mux_dev->dev.driver_data = desc->drvdata; + ret = dev_set_name(&mux_dev->dev, "%s-mux", desc->name ? desc->name : dev_name(parent)); + if (ret) { + put_device(&mux_dev->dev); + return ERR_PTR(ret); + } + + ret = device_add(&mux_dev->dev); + if (ret) { + dev_err(parent, "failed to register mux (%d)\n", ret); + put_device(&mux_dev->dev); + return ERR_PTR(ret); + } + + return mux_dev; +} +EXPORT_SYMBOL_GPL(typec_mux_register); + +/** + * typec_mux_unregister - Unregister Multiplexer Switch + * @mux_dev: USB Type-C Connector Multiplexer/DeMultiplexer + * + * Unregister mux that was registered with typec_mux_register(). + */ +void typec_mux_unregister(struct typec_mux_dev *mux_dev) +{ + if (!IS_ERR_OR_NULL(mux_dev)) + device_unregister(&mux_dev->dev); +} +EXPORT_SYMBOL_GPL(typec_mux_unregister); + +void typec_mux_set_drvdata(struct typec_mux_dev *mux_dev, void *data) +{ + dev_set_drvdata(&mux_dev->dev, data); +} +EXPORT_SYMBOL_GPL(typec_mux_set_drvdata); + +void *typec_mux_get_drvdata(struct typec_mux_dev *mux_dev) +{ + return dev_get_drvdata(&mux_dev->dev); +} +EXPORT_SYMBOL_GPL(typec_mux_get_drvdata); + +struct class typec_mux_class = { + .name = "typec_mux", + .owner = THIS_MODULE, +}; |