// SPDX-License-Identifier: GPL-2.0
/*
* USB Type-C Multiplexer/DeMultiplexer Switch support
*
* Copyright (C) 2018 Intel Corporation
* Author: Heikki Krogerus <[email protected]>
* Hans de Goede <[email protected]>
*/
#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(const 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(const 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 muxes the
* property is named "mode-switch" (@id). The value of the device
* property is ignored.
*/
if (id && !fwnode_property_present(fwnode, id))
return NULL;
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
*
* 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)
{
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",
NULL, 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);
const struct class typec_mux_class = {
.name = "typec_mux",
};