summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/devicetree/bindings/usb/typec-switch-gpio.txt30
-rw-r--r--drivers/usb/typec/mux/Kconfig6
-rw-r--r--drivers/usb/typec/mux/Makefile1
-rw-r--r--drivers/usb/typec/mux/gpio-switch.c117
-rw-r--r--drivers/usb/typec/tcpm/tcpci.c38
-rw-r--r--drivers/usb/typec/tcpm/tcpm.c51
6 files changed, 238 insertions, 5 deletions
diff --git a/Documentation/devicetree/bindings/usb/typec-switch-gpio.txt b/Documentation/devicetree/bindings/usb/typec-switch-gpio.txt
new file mode 100644
index 000000000000..4ef76cfdf3d5
--- /dev/null
+++ b/Documentation/devicetree/bindings/usb/typec-switch-gpio.txt
@@ -0,0 +1,30 @@
+Typec orientation switch via a GPIO
+-----------------------------------
+
+Required properties:
+- compatible: should be set one of following:
+ - "nxp,ptn36043" for NXP Type-C SuperSpeed active switch.
+
+- gpios: the GPIO used to switch the super speed active channel,
+ GPIO_ACTIVE_HIGH: GPIO state high for cc1;
+ GPIO_ACTIVE_LOW: GPIO state low for cc1.
+- orientation-switch: must be present.
+
+Required sub-node:
+- port: specify the remote endpoint of typec switch consumer.
+
+Example:
+
+ptn36043 {
+ compatible = "nxp,ptn36043";
+ pinctrl-names = "default";
+ pinctrl-0 = <&pinctrl_ss_sel>;
+ gpios = <&gpio3 15 GPIO_ACTIVE_HIGH>;
+ orientation-switch;
+
+ port {
+ usb3_data_ss: endpoint {
+ remote-endpoint = <&typec_con_ss>;
+ };
+ };
+};
diff --git a/drivers/usb/typec/mux/Kconfig b/drivers/usb/typec/mux/Kconfig
index 01ed0d5e10e8..bc7d3c78e556 100644
--- a/drivers/usb/typec/mux/Kconfig
+++ b/drivers/usb/typec/mux/Kconfig
@@ -9,4 +9,10 @@ config TYPEC_MUX_PI3USB30532
Say Y or M if your system has a Pericom PI3USB30532 Type-C cross
switch / mux chip found on some devices with a Type-C port.
+config TYPEC_SWITCH_GPIO
+ tristate "Simple Super Speed Active Switch via GPIO"
+ help
+ Say Y or M if your system has a typec super speed channel
+ switch via a simple GPIO control.
+
endmenu
diff --git a/drivers/usb/typec/mux/Makefile b/drivers/usb/typec/mux/Makefile
index 1332e469b8a0..e29377c5ff9a 100644
--- a/drivers/usb/typec/mux/Makefile
+++ b/drivers/usb/typec/mux/Makefile
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_TYPEC_MUX_PI3USB30532) += pi3usb30532.o
+obj-$(CONFIG_TYPEC_SWITCH_GPIO) += gpio-switch.o
diff --git a/drivers/usb/typec/mux/gpio-switch.c b/drivers/usb/typec/mux/gpio-switch.c
new file mode 100644
index 000000000000..ef4805cdc575
--- /dev/null
+++ b/drivers/usb/typec/mux/gpio-switch.c
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0
+/**
+ * gpio-switch.c - typec switch via a simple GPIO control.
+ *
+ * Copyright 2019 NXP
+ * Author: Jun Li <jun.li@nxp.com>
+ *
+ */
+
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/kernel.h>
+#include <linux/platform_device.h>
+#include <linux/of.h>
+#include <linux/of_gpio.h>
+#include <linux/usb/typec_mux.h>
+
+struct gpio_typec_switch {
+ struct typec_switch *sw;
+ struct mutex lock;
+ struct gpio_desc *ss_sel;
+ struct gpio_desc *ss_reset;
+};
+
+static int switch_gpio_set(struct typec_switch *sw,
+ enum typec_orientation orientation)
+{
+ struct gpio_typec_switch *gpio_sw = typec_switch_get_drvdata(sw);
+
+ mutex_lock(&gpio_sw->lock);
+
+ switch (orientation) {
+ case TYPEC_ORIENTATION_NORMAL:
+ gpiod_set_value_cansleep(gpio_sw->ss_sel, 1);
+ break;
+ case TYPEC_ORIENTATION_REVERSE:
+ gpiod_set_value_cansleep(gpio_sw->ss_sel, 0);
+ break;
+ case TYPEC_ORIENTATION_NONE:
+ break;
+ }
+
+ mutex_unlock(&gpio_sw->lock);
+
+ return 0;
+}
+
+static int typec_switch_gpio_probe(struct platform_device *pdev)
+{
+ struct gpio_typec_switch *gpio_sw;
+ struct device *dev = &pdev->dev;
+ struct typec_switch_desc sw_desc;
+
+ gpio_sw = devm_kzalloc(dev, sizeof(*gpio_sw), GFP_KERNEL);
+ if (!gpio_sw)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, gpio_sw);
+
+ sw_desc.drvdata = gpio_sw;
+ sw_desc.fwnode = dev->fwnode;
+ sw_desc.set = switch_gpio_set;
+ mutex_init(&gpio_sw->lock);
+
+ /* Get the super speed mux reset GPIO, it's optional */
+ gpio_sw->ss_reset = devm_gpiod_get_optional(dev, "reset",
+ GPIOD_OUT_HIGH);
+ if (IS_ERR(gpio_sw->ss_reset))
+ return PTR_ERR(gpio_sw->ss_reset);
+
+ if (gpio_sw->ss_reset)
+ usleep_range(700, 1000);
+
+ /* Get the super speed active channel selection GPIO */
+ gpio_sw->ss_sel = devm_gpiod_get(dev, "switch", GPIOD_OUT_LOW);
+ if (IS_ERR(gpio_sw->ss_sel))
+ return PTR_ERR(gpio_sw->ss_sel);
+
+ gpio_sw->sw = typec_switch_register(dev, &sw_desc);
+ if (IS_ERR(gpio_sw->sw)) {
+ dev_err(dev, "Error registering typec switch: %ld\n", PTR_ERR(gpio_sw->sw));
+ return PTR_ERR(gpio_sw->sw);
+ }
+
+ return 0;
+}
+
+static int typec_switch_gpio_remove(struct platform_device *pdev)
+{
+ struct gpio_typec_switch *gpio_sw = platform_get_drvdata(pdev);
+
+ typec_switch_unregister(gpio_sw->sw);
+
+ return 0;
+}
+
+static const struct of_device_id of_typec_switch_gpio_match[] = {
+ { .compatible = "nxp,ptn36043" },
+ { .compatible = "nxp,cbtl04gp" },
+ { /* Sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, of_typec_switch_gpio_match);
+
+static struct platform_driver typec_switch_gpio_driver = {
+ .probe = typec_switch_gpio_probe,
+ .remove = typec_switch_gpio_remove,
+ .driver = {
+ .name = "typec-switch-gpio",
+ .of_match_table = of_typec_switch_gpio_match,
+ },
+};
+
+module_platform_driver(typec_switch_gpio_driver);
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("TypeC Super Speed Switch GPIO driver");
+MODULE_AUTHOR("Jun Li <jun.li@nxp.com>");
diff --git a/drivers/usb/typec/tcpm/tcpci.c b/drivers/usb/typec/tcpm/tcpci.c
index c1f7073a56de..4b2f380ca640 100644
--- a/drivers/usb/typec/tcpm/tcpci.c
+++ b/drivers/usb/typec/tcpm/tcpci.c
@@ -10,6 +10,7 @@
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
+#include <linux/irq.h>
#include <linux/property.h>
#include <linux/regmap.h>
#include <linux/usb/pd.h>
@@ -108,9 +109,6 @@ static int tcpci_start_toggling(struct tcpc_dev *tcpc,
struct tcpci *tcpci = tcpc_to_tcpci(tcpc);
unsigned int reg = TCPC_ROLE_CTRL_DRP;
- if (port_type != TYPEC_PORT_DRP)
- return -EOPNOTSUPP;
-
/* Handle vendor drp toggling */
if (tcpci->data->start_drp_toggling) {
ret = tcpci->data->start_drp_toggling(tcpci, tcpci->data, cc);
@@ -566,6 +564,7 @@ static int tcpci_probe(struct i2c_client *client,
if (IS_ERR(chip->tcpci))
return PTR_ERR(chip->tcpci);
+ irq_set_status_flags(client->irq, IRQ_DISABLE_UNLAZY);
err = devm_request_threaded_irq(&client->dev, client->irq, NULL,
_tcpci_irq,
IRQF_ONESHOT | IRQF_TRIGGER_LOW,
@@ -575,6 +574,8 @@ static int tcpci_probe(struct i2c_client *client,
return err;
}
+ device_set_wakeup_capable(chip->tcpci->dev, true);
+
return 0;
}
@@ -583,10 +584,40 @@ static int tcpci_remove(struct i2c_client *client)
struct tcpci_chip *chip = i2c_get_clientdata(client);
tcpci_unregister_port(chip->tcpci);
+ irq_clear_status_flags(client->irq, IRQ_DISABLE_UNLAZY);
+
+ return 0;
+}
+
+static int __maybe_unused tcpci_suspend(struct device *dev)
+{
+ struct i2c_client *i2c = to_i2c_client(dev);
+
+ if (device_may_wakeup(dev))
+ enable_irq_wake(i2c->irq);
+ else
+ disable_irq(i2c->irq);
return 0;
}
+
+static int __maybe_unused tcpci_resume(struct device *dev)
+{
+ struct i2c_client *i2c = to_i2c_client(dev);
+
+ if (device_may_wakeup(dev))
+ disable_irq_wake(i2c->irq);
+ else
+ enable_irq(i2c->irq);
+
+ return 0;
+}
+
+static const struct dev_pm_ops tcpci_pm_ops = {
+ SET_SYSTEM_SLEEP_PM_OPS(tcpci_suspend, tcpci_resume)
+};
+
static const struct i2c_device_id tcpci_id[] = {
{ "tcpci", 0 },
{ }
@@ -604,6 +635,7 @@ MODULE_DEVICE_TABLE(of, tcpci_of_match);
static struct i2c_driver tcpci_i2c_driver = {
.driver = {
.name = "tcpci",
+ .pm = &tcpci_pm_ops,
.of_match_table = of_match_ptr(tcpci_of_match),
},
.probe = tcpci_probe,
diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c
index 5f61d9977a15..ea68559ce60e 100644
--- a/drivers/usb/typec/tcpm/tcpm.c
+++ b/drivers/usb/typec/tcpm/tcpm.c
@@ -29,6 +29,8 @@
#include <linux/usb/tcpm.h>
#include <linux/usb/typec_altmode.h>
#include <linux/workqueue.h>
+#include <linux/extcon.h>
+#include <linux/extcon-provider.h>
#define FOREACH_STATE(S) \
S(INVALID_STATE), \
@@ -139,6 +141,12 @@ static const char * const tcpm_states[] = {
FOREACH_STATE(GENERATE_STRING)
};
+static const unsigned int tcpm_extcon_cable[] = {
+ EXTCON_USB_HOST,
+ EXTCON_USB,
+ EXTCON_NONE,
+};
+
enum vdm_states {
VDM_STATE_ERR_BUSY = -3,
VDM_STATE_ERR_SEND = -2,
@@ -193,6 +201,7 @@ struct pd_pps_data {
struct tcpm_port {
struct device *dev;
+ struct extcon_dev *edev;
struct mutex lock; /* tcpm state machine lock */
struct workqueue_struct *wq;
@@ -670,6 +679,20 @@ static int tcpm_mux_set(struct tcpm_port *port, int state,
ret = usb_role_switch_set_role(port->role_sw, usb_role);
if (ret)
return ret;
+ } else if (port->edev) {
+ if (usb_role == USB_ROLE_NONE) {
+ extcon_set_state_sync(port->edev, EXTCON_USB_HOST,
+ false);
+ extcon_set_state_sync(port->edev, EXTCON_USB, false);
+ } else if (usb_role == USB_ROLE_DEVICE) {
+ extcon_set_state_sync(port->edev, EXTCON_USB_HOST,
+ false);
+ extcon_set_state_sync(port->edev, EXTCON_USB, true);
+ } else {
+ extcon_set_state_sync(port->edev, EXTCON_USB, false);
+ extcon_set_state_sync(port->edev, EXTCON_USB_HOST,
+ true);
+ }
}
return typec_set_mode(port->typec_port, state);
@@ -2623,6 +2646,8 @@ static int tcpm_src_attach(struct tcpm_port *port)
if (port->attached)
return 0;
+ tcpm_set_cc(port, tcpm_rp_cc(port));
+
ret = tcpm_set_polarity(port, polarity);
if (ret < 0)
return ret;
@@ -2744,6 +2769,8 @@ static int tcpm_snk_attach(struct tcpm_port *port)
if (port->attached)
return 0;
+ tcpm_set_cc(port, TYPEC_CC_RD);
+
ret = tcpm_set_polarity(port, port->cc2 != TYPEC_CC_OPEN ?
TYPEC_POLARITY_CC2 : TYPEC_POLARITY_CC1);
if (ret < 0)
@@ -3146,7 +3173,11 @@ static void run_state_machine(struct tcpm_port *port)
ret = tcpm_snk_attach(port);
if (ret < 0)
tcpm_set_state(port, SNK_UNATTACHED, 0);
- else
+ else if (port->port_type == TYPEC_PORT_SRC &&
+ port->typec_caps.data == TYPEC_PORT_DRD) {
+ tcpm_typec_connect(port);
+ tcpm_log(port, "Keep at SNK_ATTACHED for USB data.");
+ } else
tcpm_set_state(port, SNK_STARTUP, 0);
break;
case SNK_STARTUP:
@@ -4744,7 +4775,7 @@ struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc)
mutex_init(&port->lock);
mutex_init(&port->swap_lock);
- port->wq = create_singlethread_workqueue(dev_name(dev));
+ port->wq = create_freezable_workqueue(dev_name(dev));
if (!port->wq)
return ERR_PTR(-ENOMEM);
INIT_DELAYED_WORK(&port->state_machine, tcpm_state_machine_work);
@@ -4820,6 +4851,19 @@ struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc)
}
}
+ port->edev = devm_extcon_dev_allocate(port->dev, tcpm_extcon_cable);
+ if (IS_ERR(port->edev)) {
+ dev_err(port->dev, "failed to allocate extcon dev.\n");
+ err = -ENOMEM;
+ goto out_role_sw_put;
+ }
+
+ err = devm_extcon_dev_register(port->dev, port->edev);
+ if (err) {
+ dev_err(port->dev, "failed to register extcon dev.\n");
+ goto out_role_sw_put;
+ }
+
mutex_lock(&port->lock);
tcpm_init(port);
mutex_unlock(&port->lock);
@@ -4840,6 +4884,9 @@ void tcpm_unregister_port(struct tcpm_port *port)
{
int i;
+ cancel_delayed_work_sync(&port->state_machine);
+ cancel_delayed_work_sync(&port->vdm_state_machine);
+
tcpm_reset_port(port);
for (i = 0; i < ARRAY_SIZE(port->port_altmode); i++)
typec_unregister_altmode(port->port_altmode[i]);