usb: typec: Support the WUSB3801 port controller
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>
This commit is contained in:
parent
2e7dfb0e9c
commit
d016cbe4d7
@ -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"
|
||||
|
@ -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/
|
||||
|
437
drivers/usb/typec/wusb3801.c
Normal file
437
drivers/usb/typec/wusb3801.c
Normal file
@ -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");
|
Loading…
Reference in New Issue
Block a user