usb: typec: Support the WUSB3801 port controller
authorSamuel Holland <samuel@sholland.org>
Mon, 14 Feb 2022 05:01:17 +0000 (23:01 -0600)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Thu, 17 Feb 2022 15:23:18 +0000 (16:23 +0100)
WUSB3801 features a configurable port type, accessory detection, and
plug orientation detection. It provides a hardware "ID" pin output for
compatibility with USB 2.0 OTG PHYs. Add a typec class driver for it.

Reviewed-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
Signed-off-by: Samuel Holland <samuel@sholland.org>
Link: https://lore.kernel.org/r/20220214050118.61015-5-samuel@sholland.org
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/usb/typec/Kconfig
drivers/usb/typec/Makefile
drivers/usb/typec/wusb3801.c [new file with mode: 0644]

index bc918ca61da8413033c9a1858c9bc58509be9c5f..8f921213b17df324bda50a6bb2fc5afdf6331e72 100644 (file)
@@ -100,6 +100,16 @@ config TYPEC_QCOM_PMIC
          It will also enable the VBUS output to connected devices when a
          DFP connection is made.
 
+config TYPEC_WUSB3801
+       tristate "Willsemi WUSB3801 Type-C port controller driver"
+       depends on I2C
+       select REGMAP_I2C
+       help
+         Say Y or M here if your system has a WUSB3801 Type-C port controller.
+
+         If you choose to build this driver as a dynamically linked module, the
+         module will be called wusb3801.ko.
+
 source "drivers/usb/typec/mux/Kconfig"
 
 source "drivers/usb/typec/altmodes/Kconfig"
index 441dd6c4cbefd65b6a367e02bb8f23debe5bb540..43626acc0aafb1002ba2e16dac7f0398c9745f48 100644 (file)
@@ -10,4 +10,5 @@ obj-$(CONFIG_TYPEC_HD3SS3220) += hd3ss3220.o
 obj-$(CONFIG_TYPEC_QCOM_PMIC)  += qcom-pmic-typec.o
 obj-$(CONFIG_TYPEC_STUSB160X)  += stusb160x.o
 obj-$(CONFIG_TYPEC_RT1719)     += rt1719.o
+obj-$(CONFIG_TYPEC_WUSB3801)   += wusb3801.o
 obj-$(CONFIG_TYPEC)            += mux/
diff --git a/drivers/usb/typec/wusb3801.c b/drivers/usb/typec/wusb3801.c
new file mode 100644 (file)
index 0000000..e63509f
--- /dev/null
@@ -0,0 +1,437 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Willsemi WUSB3801 Type-C port controller driver
+ *
+ * Copyright (C) 2022 Samuel Holland <samuel@sholland.org>
+ */
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/usb/typec.h>
+
+#define WUSB3801_REG_DEVICE_ID         0x01
+#define WUSB3801_REG_CTRL0             0x02
+#define WUSB3801_REG_INT               0x03
+#define WUSB3801_REG_STAT              0x04
+#define WUSB3801_REG_CTRL1             0x05
+#define WUSB3801_REG_TEST00            0x06
+#define WUSB3801_REG_TEST01            0x07
+#define WUSB3801_REG_TEST02            0x08
+#define WUSB3801_REG_TEST03            0x09
+#define WUSB3801_REG_TEST04            0x0a
+#define WUSB3801_REG_TEST05            0x0b
+#define WUSB3801_REG_TEST06            0x0c
+#define WUSB3801_REG_TEST07            0x0d
+#define WUSB3801_REG_TEST08            0x0e
+#define WUSB3801_REG_TEST09            0x0f
+#define WUSB3801_REG_TEST0A            0x10
+#define WUSB3801_REG_TEST0B            0x11
+#define WUSB3801_REG_TEST0C            0x12
+#define WUSB3801_REG_TEST0D            0x13
+#define WUSB3801_REG_TEST0E            0x14
+#define WUSB3801_REG_TEST0F            0x15
+#define WUSB3801_REG_TEST10            0x16
+#define WUSB3801_REG_TEST11            0x17
+#define WUSB3801_REG_TEST12            0x18
+
+#define WUSB3801_DEVICE_ID_VERSION_ID  GENMASK(7, 3)
+#define WUSB3801_DEVICE_ID_VENDOR_ID   GENMASK(2, 0)
+
+#define WUSB3801_CTRL0_DIS_ACC_SUPPORT BIT(7)
+#define WUSB3801_CTRL0_TRY             GENMASK(6, 5)
+#define WUSB3801_CTRL0_TRY_NONE                (0x0 << 5)
+#define WUSB3801_CTRL0_TRY_SNK         (0x1 << 5)
+#define WUSB3801_CTRL0_TRY_SRC         (0x2 << 5)
+#define WUSB3801_CTRL0_CURRENT         GENMASK(4, 3) /* SRC */
+#define WUSB3801_CTRL0_CURRENT_DEFAULT (0x0 << 3)
+#define WUSB3801_CTRL0_CURRENT_1_5A    (0x1 << 3)
+#define WUSB3801_CTRL0_CURRENT_3_0A    (0x2 << 3)
+#define WUSB3801_CTRL0_ROLE            GENMASK(2, 1)
+#define WUSB3801_CTRL0_ROLE_SNK                (0x0 << 1)
+#define WUSB3801_CTRL0_ROLE_SRC                (0x1 << 1)
+#define WUSB3801_CTRL0_ROLE_DRP                (0x2 << 1)
+#define WUSB3801_CTRL0_INT_MASK                BIT(0)
+
+#define WUSB3801_INT_ATTACHED          BIT(0)
+#define WUSB3801_INT_DETACHED          BIT(1)
+
+#define WUSB3801_STAT_VBUS_DETECTED    BIT(7)
+#define WUSB3801_STAT_CURRENT          GENMASK(6, 5) /* SNK */
+#define WUSB3801_STAT_CURRENT_STANDBY  (0x0 << 5)
+#define WUSB3801_STAT_CURRENT_DEFAULT  (0x1 << 5)
+#define WUSB3801_STAT_CURRENT_1_5A     (0x2 << 5)
+#define WUSB3801_STAT_CURRENT_3_0A     (0x3 << 5)
+#define WUSB3801_STAT_PARTNER          GENMASK(4, 2)
+#define WUSB3801_STAT_PARTNER_STANDBY  (0x0 << 2)
+#define WUSB3801_STAT_PARTNER_SNK      (0x1 << 2)
+#define WUSB3801_STAT_PARTNER_SRC      (0x2 << 2)
+#define WUSB3801_STAT_PARTNER_AUDIO    (0x3 << 2)
+#define WUSB3801_STAT_PARTNER_DEBUG    (0x4 << 2)
+#define WUSB3801_STAT_ORIENTATION      GENMASK(1, 0)
+#define WUSB3801_STAT_ORIENTATION_NONE (0x0 << 0)
+#define WUSB3801_STAT_ORIENTATION_CC1  (0x1 << 0)
+#define WUSB3801_STAT_ORIENTATION_CC2  (0x2 << 0)
+#define WUSB3801_STAT_ORIENTATION_BOTH (0x3 << 0)
+
+#define WUSB3801_CTRL1_SM_RESET                BIT(0)
+
+#define WUSB3801_TEST01_VENDOR_SUB_ID  (BIT(8) | BIT(6))
+
+#define WUSB3801_TEST02_FORCE_ERR_RCY  BIT(8)
+
+#define WUSB3801_TEST0A_WAIT_VBUS      BIT(5)
+
+struct wusb3801 {
+       struct typec_capability cap;
+       struct device           *dev;
+       struct typec_partner    *partner;
+       struct typec_port       *port;
+       struct regmap           *regmap;
+       struct regulator        *vbus_supply;
+       unsigned int            partner_type;
+       enum typec_port_type    port_type;
+       enum typec_pwr_opmode   pwr_opmode;
+       bool                    vbus_on;
+};
+
+static enum typec_role wusb3801_get_default_role(struct wusb3801 *wusb3801)
+{
+       switch (wusb3801->port_type) {
+       case TYPEC_PORT_SRC:
+               return TYPEC_SOURCE;
+       case TYPEC_PORT_SNK:
+               return TYPEC_SINK;
+       case TYPEC_PORT_DRP:
+       default:
+               if (wusb3801->cap.prefer_role == TYPEC_SOURCE)
+                       return TYPEC_SOURCE;
+               return TYPEC_SINK;
+       }
+}
+
+static int wusb3801_map_port_type(enum typec_port_type type)
+{
+       switch (type) {
+       case TYPEC_PORT_SRC:
+               return WUSB3801_CTRL0_ROLE_SRC;
+       case TYPEC_PORT_SNK:
+               return WUSB3801_CTRL0_ROLE_SNK;
+       case TYPEC_PORT_DRP:
+       default:
+               return WUSB3801_CTRL0_ROLE_DRP;
+       }
+}
+
+static int wusb3801_map_pwr_opmode(enum typec_pwr_opmode mode)
+{
+       switch (mode) {
+       case TYPEC_PWR_MODE_USB:
+       default:
+               return WUSB3801_CTRL0_CURRENT_DEFAULT;
+       case TYPEC_PWR_MODE_1_5A:
+               return WUSB3801_CTRL0_CURRENT_1_5A;
+       case TYPEC_PWR_MODE_3_0A:
+               return WUSB3801_CTRL0_CURRENT_3_0A;
+       }
+}
+
+static unsigned int wusb3801_map_try_role(int role)
+{
+       switch (role) {
+       case TYPEC_NO_PREFERRED_ROLE:
+       default:
+               return WUSB3801_CTRL0_TRY_NONE;
+       case TYPEC_SINK:
+               return WUSB3801_CTRL0_TRY_SNK;
+       case TYPEC_SOURCE:
+               return WUSB3801_CTRL0_TRY_SRC;
+       }
+}
+
+static enum typec_orientation wusb3801_unmap_orientation(unsigned int status)
+{
+       switch (status & WUSB3801_STAT_ORIENTATION) {
+       case WUSB3801_STAT_ORIENTATION_NONE:
+       case WUSB3801_STAT_ORIENTATION_BOTH:
+       default:
+               return TYPEC_ORIENTATION_NONE;
+       case WUSB3801_STAT_ORIENTATION_CC1:
+               return TYPEC_ORIENTATION_NORMAL;
+       case WUSB3801_STAT_ORIENTATION_CC2:
+               return TYPEC_ORIENTATION_REVERSE;
+       }
+}
+
+static enum typec_pwr_opmode wusb3801_unmap_pwr_opmode(unsigned int status)
+{
+       switch (status & WUSB3801_STAT_CURRENT) {
+       case WUSB3801_STAT_CURRENT_STANDBY:
+       case WUSB3801_STAT_CURRENT_DEFAULT:
+       default:
+               return TYPEC_PWR_MODE_USB;
+       case WUSB3801_STAT_CURRENT_1_5A:
+               return TYPEC_PWR_MODE_1_5A;
+       case WUSB3801_STAT_CURRENT_3_0A:
+               return TYPEC_PWR_MODE_3_0A;
+       }
+}
+
+static int wusb3801_try_role(struct typec_port *port, int role)
+{
+       struct wusb3801 *wusb3801 = typec_get_drvdata(port);
+
+       return regmap_update_bits(wusb3801->regmap, WUSB3801_REG_CTRL0,
+                                 WUSB3801_CTRL0_TRY,
+                                 wusb3801_map_try_role(role));
+}
+
+static int wusb3801_port_type_set(struct typec_port *port,
+                                 enum typec_port_type type)
+{
+       struct wusb3801 *wusb3801 = typec_get_drvdata(port);
+       int ret;
+
+       ret = regmap_update_bits(wusb3801->regmap, WUSB3801_REG_CTRL0,
+                                WUSB3801_CTRL0_ROLE,
+                                wusb3801_map_port_type(type));
+       if (ret)
+               return ret;
+
+       wusb3801->port_type = type;
+
+       return 0;
+}
+
+static const struct typec_operations wusb3801_typec_ops = {
+       .try_role       = wusb3801_try_role,
+       .port_type_set  = wusb3801_port_type_set,
+};
+
+static int wusb3801_hw_init(struct wusb3801 *wusb3801)
+{
+       return regmap_write(wusb3801->regmap, WUSB3801_REG_CTRL0,
+                           wusb3801_map_try_role(wusb3801->cap.prefer_role) |
+                           wusb3801_map_pwr_opmode(wusb3801->pwr_opmode) |
+                           wusb3801_map_port_type(wusb3801->port_type));
+}
+
+static void wusb3801_hw_update(struct wusb3801 *wusb3801)
+{
+       struct typec_port *port = wusb3801->port;
+       struct device *dev = wusb3801->dev;
+       unsigned int partner_type, status;
+       int ret;
+
+       ret = regmap_read(wusb3801->regmap, WUSB3801_REG_STAT, &status);
+       if (ret) {
+               dev_warn(dev, "Failed to read port status: %d\n", ret);
+               status = 0;
+       }
+       dev_dbg(dev, "status = 0x%02x\n", status);
+
+       partner_type = status & WUSB3801_STAT_PARTNER;
+
+       if (partner_type == WUSB3801_STAT_PARTNER_SNK) {
+               if (!wusb3801->vbus_on) {
+                       ret = regulator_enable(wusb3801->vbus_supply);
+                       if (ret)
+                               dev_warn(dev, "Failed to enable VBUS: %d\n", ret);
+                       wusb3801->vbus_on = true;
+               }
+       } else {
+               if (wusb3801->vbus_on) {
+                       regulator_disable(wusb3801->vbus_supply);
+                       wusb3801->vbus_on = false;
+               }
+       }
+
+       if (partner_type != wusb3801->partner_type) {
+               struct typec_partner_desc desc = {};
+               enum typec_data_role data_role;
+               enum typec_role pwr_role = wusb3801_get_default_role(wusb3801);
+
+               switch (partner_type) {
+               case WUSB3801_STAT_PARTNER_STANDBY:
+                       break;
+               case WUSB3801_STAT_PARTNER_SNK:
+                       pwr_role = TYPEC_SOURCE;
+                       break;
+               case WUSB3801_STAT_PARTNER_SRC:
+                       pwr_role = TYPEC_SINK;
+                       break;
+               case WUSB3801_STAT_PARTNER_AUDIO:
+                       desc.accessory = TYPEC_ACCESSORY_AUDIO;
+                       break;
+               case WUSB3801_STAT_PARTNER_DEBUG:
+                       desc.accessory = TYPEC_ACCESSORY_DEBUG;
+                       break;
+               }
+
+               if (wusb3801->partner) {
+                       typec_unregister_partner(wusb3801->partner);
+                       wusb3801->partner = NULL;
+               }
+
+               if (partner_type != WUSB3801_STAT_PARTNER_STANDBY) {
+                       wusb3801->partner = typec_register_partner(port, &desc);
+                       if (IS_ERR(wusb3801->partner))
+                               dev_err(dev, "Failed to register partner: %ld\n",
+                                       PTR_ERR(wusb3801->partner));
+               }
+
+               data_role = pwr_role == TYPEC_SOURCE ? TYPEC_HOST : TYPEC_DEVICE;
+               typec_set_data_role(port, data_role);
+               typec_set_pwr_role(port, pwr_role);
+               typec_set_vconn_role(port, pwr_role);
+       }
+
+       typec_set_pwr_opmode(wusb3801->port,
+                            partner_type == WUSB3801_STAT_PARTNER_SRC
+                               ? wusb3801_unmap_pwr_opmode(status)
+                               : wusb3801->pwr_opmode);
+       typec_set_orientation(wusb3801->port,
+                             wusb3801_unmap_orientation(status));
+
+       wusb3801->partner_type = partner_type;
+}
+
+static irqreturn_t wusb3801_irq(int irq, void *data)
+{
+       struct wusb3801 *wusb3801 = data;
+       unsigned int dummy;
+
+       /*
+        * The interrupt register must be read in order to clear the IRQ,
+        * but all of the useful information is in the status register.
+        */
+       regmap_read(wusb3801->regmap, WUSB3801_REG_INT, &dummy);
+
+       wusb3801_hw_update(wusb3801);
+
+       return IRQ_HANDLED;
+}
+
+static const struct regmap_config config = {
+       .reg_bits       = 8,
+       .val_bits       = 8,
+       .max_register   = WUSB3801_REG_TEST12,
+};
+
+static int wusb3801_probe(struct i2c_client *client)
+{
+       struct device *dev = &client->dev;
+       struct fwnode_handle *connector;
+       struct wusb3801 *wusb3801;
+       const char *cap_str;
+       int ret;
+
+       wusb3801 = devm_kzalloc(dev, sizeof(*wusb3801), GFP_KERNEL);
+       if (!wusb3801)
+               return -ENOMEM;
+
+       i2c_set_clientdata(client, wusb3801);
+
+       wusb3801->dev = dev;
+
+       wusb3801->regmap = devm_regmap_init_i2c(client, &config);
+       if (IS_ERR(wusb3801->regmap))
+               return PTR_ERR(wusb3801->regmap);
+
+       wusb3801->vbus_supply = devm_regulator_get(dev, "vbus");
+       if (IS_ERR(wusb3801->vbus_supply))
+               return PTR_ERR(wusb3801->vbus_supply);
+
+       connector = device_get_named_child_node(dev, "connector");
+       if (!connector)
+               return -ENODEV;
+
+       ret = typec_get_fw_cap(&wusb3801->cap, connector);
+       if (ret)
+               goto err_put_connector;
+       wusb3801->port_type = wusb3801->cap.type;
+
+       ret = fwnode_property_read_string(connector, "typec-power-opmode", &cap_str);
+       if (ret)
+               goto err_put_connector;
+
+       ret = typec_find_pwr_opmode(cap_str);
+       if (ret < 0 || ret == TYPEC_PWR_MODE_PD)
+               goto err_put_connector;
+       wusb3801->pwr_opmode = ret;
+
+       /* Initialize the hardware with the devicetree settings. */
+       ret = wusb3801_hw_init(wusb3801);
+       if (ret)
+               return ret;
+
+       wusb3801->cap.revision          = USB_TYPEC_REV_1_2;
+       wusb3801->cap.accessory[0]      = TYPEC_ACCESSORY_AUDIO;
+       wusb3801->cap.accessory[1]      = TYPEC_ACCESSORY_DEBUG;
+       wusb3801->cap.orientation_aware = true;
+       wusb3801->cap.driver_data       = wusb3801;
+       wusb3801->cap.ops               = &wusb3801_typec_ops;
+
+       wusb3801->port = typec_register_port(dev, &wusb3801->cap);
+       if (IS_ERR(wusb3801->port)) {
+               ret = PTR_ERR(wusb3801->port);
+               goto err_put_connector;
+       }
+
+       /* Initialize the port attributes from the hardware state. */
+       wusb3801_hw_update(wusb3801);
+
+       ret = request_threaded_irq(client->irq, NULL, wusb3801_irq,
+                                  IRQF_ONESHOT, dev_name(dev), wusb3801);
+       if (ret)
+               goto err_unregister_port;
+
+       fwnode_handle_put(connector);
+
+       return 0;
+
+err_unregister_port:
+       typec_unregister_port(wusb3801->port);
+err_put_connector:
+       fwnode_handle_put(connector);
+
+       return ret;
+}
+
+static int wusb3801_remove(struct i2c_client *client)
+{
+       struct wusb3801 *wusb3801 = i2c_get_clientdata(client);
+
+       free_irq(client->irq, wusb3801);
+
+       if (wusb3801->partner)
+               typec_unregister_partner(wusb3801->partner);
+       typec_unregister_port(wusb3801->port);
+
+       if (wusb3801->vbus_on)
+               regulator_disable(wusb3801->vbus_supply);
+
+       return 0;
+}
+
+static const struct of_device_id wusb3801_of_match[] = {
+       { .compatible = "willsemi,wusb3801" },
+       {}
+};
+MODULE_DEVICE_TABLE(of, wusb3801_of_match);
+
+static struct i2c_driver wusb3801_driver = {
+       .probe_new      = wusb3801_probe,
+       .remove         = wusb3801_remove,
+       .driver         = {
+               .name           = "wusb3801",
+               .of_match_table = wusb3801_of_match,
+       },
+};
+
+module_i2c_driver(wusb3801_driver);
+
+MODULE_AUTHOR("Samuel Holland <samuel@sholland.org>");
+MODULE_DESCRIPTION("Willsemi WUSB3801 Type-C port controller driver");
+MODULE_LICENSE("GPL");