[PATCH 1/3] usb: USB Type-C Connector Class

From: Heikki Krogerus
Date: Tue Feb 09 2016 - 12:04:54 EST


The purpose of this class is to provide unified interface
for user space to get the status and basic information about
USB Type-C Connectors in the system, control data role
swapping, and when USB PD is available, also power role
swapping and Altenate Modes.

The class will export the following interfaces for every
USB Type-C Connector in the system to sysfs:

1. connected - Connection status of the connector
2. alternate_mode - The current Alternate Mode
3. alternate_modes - Lists all Alternate Modes the connector supports
4. partner_alt_modes - Lists partner's Alternate Modes when connected
5. partner_type - Can be USB, Charger, Alt Mode or Accessory
6. data_role - The current data role, host or device
7. data_roles - Data roles supported by the connector
8. power_role - Connector's current power role, source or sink
9. power_roles - Power roles supported by the connector
10. power_operation_mode - The current power level in use
11. usb_pd - yes if the connector supports USB PD.
12. audio_accessory - yes if the connector supports Audio Accessory
13. debug_accessory - yes if the connector supports Debug Accessory

The data_role, power_role and alternate_mode are also
writable and can be used for executing role swapping and
entering modes. When USB PD is not supported by the
connector or partner, power_role will reflect the value of
the data_role, and is not swappable independently.

Signed-off-by: Heikki Krogerus <heikki.krogerus@xxxxxxxxxxxxxxx>
---
drivers/usb/Kconfig | 2 +
drivers/usb/Makefile | 2 +
drivers/usb/type-c/Kconfig | 7 +
drivers/usb/type-c/Makefile | 1 +
drivers/usb/type-c/typec.c | 446 ++++++++++++++++++++++++++++++++++++++++++++
include/linux/usb/typec.h | 114 +++++++++++
6 files changed, 572 insertions(+)
create mode 100644 drivers/usb/type-c/Kconfig
create mode 100644 drivers/usb/type-c/Makefile
create mode 100644 drivers/usb/type-c/typec.c
create mode 100644 include/linux/usb/typec.h

diff --git a/drivers/usb/Kconfig b/drivers/usb/Kconfig
index 8ed451d..0c45547 100644
--- a/drivers/usb/Kconfig
+++ b/drivers/usb/Kconfig
@@ -151,6 +151,8 @@ source "drivers/usb/phy/Kconfig"

source "drivers/usb/gadget/Kconfig"

+source "drivers/usb/type-c/Kconfig"
+
config USB_LED_TRIG
bool "USB LED Triggers"
depends on LEDS_CLASS && USB_COMMON && LEDS_TRIGGERS
diff --git a/drivers/usb/Makefile b/drivers/usb/Makefile
index d5c57f1..4d712ee 100644
--- a/drivers/usb/Makefile
+++ b/drivers/usb/Makefile
@@ -61,3 +61,5 @@ obj-$(CONFIG_USB_GADGET) += gadget/
obj-$(CONFIG_USB_COMMON) += common/

obj-$(CONFIG_USBIP_CORE) += usbip/
+
+obj-$(CONFIG_TYPEC) += type-c/
diff --git a/drivers/usb/type-c/Kconfig b/drivers/usb/type-c/Kconfig
new file mode 100644
index 0000000..b229fb9
--- /dev/null
+++ b/drivers/usb/type-c/Kconfig
@@ -0,0 +1,7 @@
+
+menu "USB PD and Type-C drivers"
+
+config TYPEC
+ tristate
+
+endmenu
diff --git a/drivers/usb/type-c/Makefile b/drivers/usb/type-c/Makefile
new file mode 100644
index 0000000..1012a8b
--- /dev/null
+++ b/drivers/usb/type-c/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_TYPEC) += typec.o
diff --git a/drivers/usb/type-c/typec.c b/drivers/usb/type-c/typec.c
new file mode 100644
index 0000000..e425955
--- /dev/null
+++ b/drivers/usb/type-c/typec.c
@@ -0,0 +1,446 @@
+/*
+ * USB Type-C class
+ *
+ * Copyright (C) 2016, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@xxxxxxxxxxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#include <linux/device.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/usb/typec.h>
+
+#define to_typec_port(p) container_of(p, struct typec_port, dev)
+
+static DEFINE_IDA(typec_index_ida);
+
+/* -------------------------------- */
+
+int typec_connect(struct typec_port *port)
+{
+ kobject_uevent(&port->dev.kobj, KOBJ_CHANGE);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_connect);
+
+void typec_disconnect(struct typec_port *port)
+{
+ kobject_uevent(&port->dev.kobj, KOBJ_CHANGE);
+}
+EXPORT_SYMBOL_GPL(typec_disconnect);
+
+/* -------------------------------- */
+
+static ssize_t alternate_mode_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ struct typec_alt_mode alt_mode;
+ int ret;
+
+ if (!port->cap->set_alt_mode) {
+ dev_warn(dev, "entering Alternate Modes not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (!port->connected)
+ return -ENXIO;
+
+ if (sscanf(buf, "0x%hx,%u", &alt_mode.svid, &alt_mode.mid) != 2)
+ return -EINVAL;
+
+ mutex_lock(&port->lock);
+ ret = port->cap->set_alt_mode(port, &alt_mode);
+ mutex_unlock(&port->lock);
+ if (ret)
+ return ret;
+
+ return size;
+}
+
+static ssize_t alternate_mode_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ if (!port->cur_alt_mode)
+ return sprintf(buf, "none\n");
+
+ /* REVISIT: SIDs in human readable form? */
+ return sprintf(buf, "0x%hx,%u\n", port->cur_alt_mode->svid,
+ port->cur_alt_mode->mid);
+}
+static DEVICE_ATTR_RW(alternate_mode);
+
+static ssize_t alternate_modes_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+ struct typec_alt_mode *alt_mode;
+ int len = 0;
+
+ if (!port->cap->alt_modes)
+ return sprintf(buf, "none\n");
+
+ /* REVISIT: SIDs in human readable form? */
+ for (alt_mode = port->cap->alt_modes; alt_mode->svid; alt_mode++)
+ len += sprintf(buf + len, "0x%hx,%u\n", alt_mode->svid,
+ alt_mode->mid);
+
+ buf[len - 1] = '\0';
+ return len;
+}
+static DEVICE_ATTR_RO(alternate_modes);
+
+static ssize_t partner_alt_modes_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+ struct typec_alt_mode *alt_mode;
+ int len = 0;
+
+ if (!port->connected)
+ return -ENXIO;
+
+ if (!port->partner_alt_modes)
+ return sprintf(buf, "none\n");
+
+ /* REVISIT: SIDs in human readable form? */
+ for (alt_mode = port->partner_alt_modes; alt_mode->svid; alt_mode++)
+ len += sprintf(buf + len, "0x%hx,%u\n", alt_mode->svid,
+ alt_mode->mid);
+
+ buf[len - 1] = '\0';
+ return len;
+}
+static DEVICE_ATTR_RO(partner_alt_modes);
+
+static const char * const typec_partner_types[] = {
+ [TYPEC_PARTNER_NONE] = "unknown",
+ [TYPEC_PARTNER_USB] = "USB",
+ [TYPEC_PARTNER_CHARGER] = "Charger",
+ [TYPEC_PARTNER_ALTMODE] = "Alternate Mode",
+ [TYPEC_PARTNER_AUDIO] = "Audio Accessory",
+ [TYPEC_PARTNER_DEBUG] = "Debug Accessroy",
+};
+
+static ssize_t partner_type_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ if (!port->connected)
+ return -ENXIO;
+
+ return sprintf(buf, "%s\n", typec_partner_types[port->partner_type]);
+}
+static DEVICE_ATTR_RO(partner_type);
+
+static ssize_t data_role_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ enum typec_data_role role;
+ int ret;
+
+ if (port->cap->type != TYPEC_PORT_DRP) {
+ dev_dbg(dev, "data role swap only supported with DRP ports\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (!port->cap->dr_swap) {
+ dev_warn(dev, "data role swapping not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (!port->connected)
+ return -ENXIO;
+
+ if (!strncmp(buf, "host", 4))
+ role = TYPEC_HOST;
+ else if (!strncmp(buf, "device", 6))
+ role = TYPEC_DEVICE;
+ else
+ return -EINVAL;
+
+ if (port->data_role == role)
+ goto out;
+
+ mutex_lock(&port->lock);
+ ret = port->cap->dr_swap(port);
+ mutex_unlock(&port->lock);
+ if (ret)
+ return ret;
+out:
+ return size;
+}
+
+static ssize_t data_role_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ switch (port->cap->type) {
+ case TYPEC_PORT_DFP:
+ return sprintf(buf, "host\n");
+ case TYPEC_PORT_UFP:
+ return sprintf(buf, "device\n");
+ case TYPEC_PORT_DRP:
+ return sprintf(buf, "%s\n", port->data_role == TYPEC_HOST ?
+ "host" : "device");
+ default:
+ return sprintf(buf, "unknown\n");
+ };
+}
+static DEVICE_ATTR_RW(data_role);
+
+static ssize_t data_roles_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ if (port->cap->type == TYPEC_PORT_DRP)
+ return sprintf(buf, "host, device\n");
+
+ return data_role_show(dev, attr, buf);
+}
+static DEVICE_ATTR_RO(data_roles);
+
+static ssize_t power_role_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_port *port = to_typec_port(dev);
+ enum typec_pwr_role role;
+ int ret;
+
+ if (!port->cap->usb_pd) {
+ dev_dbg(dev, "power role swap only supported with USB PD\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (!port->cap->pr_swap) {
+ dev_warn(dev, "power role swapping not supported\n");
+ return -EOPNOTSUPP;
+ }
+
+ if (!port->connected)
+ return -ENXIO;
+
+ if (port->pwr_opmode != TYPEC_PWR_MODE_PD) {
+ dev_dbg(dev, "partner unable to swap power role\n");
+ return -EIO;
+ }
+
+ if (!strncmp(buf, "source", 6))
+ role = TYPEC_PWR_SOURCE;
+ else if (!strncmp(buf, "sink", 4))
+ role = TYPEC_PWR_SINK;
+ else
+ return -EINVAL;
+
+ if (port->pwr_role == role)
+ return size;
+
+ mutex_lock(&port->lock);
+ ret = port->cap->pr_swap(port);
+ mutex_unlock(&port->lock);
+ if (ret)
+ return ret;
+
+ return size;
+}
+
+static ssize_t power_role_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ switch (port->pwr_role) {
+ case TYPEC_PWR_SOURCE:
+ return sprintf(buf, "source\n");
+ case TYPEC_PWR_SINK:
+ return sprintf(buf, "sink\n");
+ default:
+ return sprintf(buf, "unknown\n");
+ };
+}
+static DEVICE_ATTR_RW(power_role);
+
+static ssize_t power_roles_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ if (port->cap->usb_pd || port->cap->type == TYPEC_PORT_DRP)
+ return sprintf(buf, "source, sink\n");
+
+ return power_role_show(dev, attr, buf);
+}
+static DEVICE_ATTR_RO(power_roles);
+
+static const char * const typec_pwr_opmodes[] = {
+ [TYPEC_PWR_MODE_USB] = "USB",
+ [TYPEC_PWR_MODE_BC1_2] = "BC1.2",
+ [TYPEC_PWR_MODE_1_5A] = "USB Type-C 1.5A",
+ [TYPEC_PWR_MODE_3_0A] = "USB Type-C 3.0A",
+ [TYPEC_PWR_MODE_PD] = "USB PD",
+};
+
+static ssize_t power_operation_mode_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ return sprintf(buf, "%s\n", typec_pwr_opmodes[port->pwr_opmode]);
+}
+static DEVICE_ATTR_RO(power_operation_mode);
+
+static ssize_t connected_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ return sprintf(buf, "%s\n", port->connected ? "yes" : "no");
+}
+static DEVICE_ATTR_RO(connected);
+
+static ssize_t usb_pd_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ return sprintf(buf, "%ssupported\n", port->cap->usb_pd ? "" : "not ");
+}
+static DEVICE_ATTR_RO(usb_pd);
+
+static ssize_t audio_accessory_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ return sprintf(buf, "%ssupported\n", port->cap->audio_accessory ?
+ "" : "not ");
+}
+static DEVICE_ATTR_RO(audio_accessory);
+
+static ssize_t debug_accessory_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ return sprintf(buf, "%ssupported\n", port->cap->debug_accessory ?
+ "" : "not ");
+}
+static DEVICE_ATTR_RO(debug_accessory);
+
+/* REVISIT: Consider creating the partner dependent sysfs files at runtime. */
+static struct attribute *typec_attrs[] = {
+ &dev_attr_alternate_mode.attr,
+ &dev_attr_alternate_modes.attr,
+ &dev_attr_partner_alt_modes.attr,
+ &dev_attr_partner_type.attr,
+ &dev_attr_data_role.attr,
+ &dev_attr_data_roles.attr,
+ &dev_attr_power_role.attr,
+ &dev_attr_power_roles.attr,
+ &dev_attr_power_operation_mode.attr,
+ &dev_attr_connected.attr,
+ &dev_attr_usb_pd.attr,
+ &dev_attr_audio_accessory.attr,
+ &dev_attr_debug_accessory.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(typec);
+
+static int typec_uevent(struct device *dev, struct kobj_uevent_env *env)
+{
+ int ret;
+
+ ret = add_uevent_var(env, "TYPEC_PORT=%s", dev_name(dev));
+ if (ret)
+ dev_err(dev, "failed to add uevent TYPEC_PORT\n");
+
+ return ret;
+}
+
+static void typec_release(struct device *dev)
+{
+ struct typec_port *port = to_typec_port(dev);
+
+ ida_simple_remove(&typec_index_ida, port->id);
+ kfree(port);
+}
+
+static struct class typec_class = {
+ .name = "type-c",
+ .dev_uevent = typec_uevent,
+ .dev_groups = typec_groups,
+ .dev_release = typec_release,
+};
+
+struct typec_port *typec_register_port(struct device *dev,
+ struct typec_capability *cap)
+{
+ struct typec_port *port;
+ int ret;
+ int id;
+
+ port = kzalloc(sizeof(*port), GFP_KERNEL);
+ if (!port)
+ return ERR_PTR(-ENOMEM);
+
+ id = ida_simple_get(&typec_index_ida, 0, 0, GFP_KERNEL);
+ if (id < 0) {
+ kfree(port);
+ return ERR_PTR(id);
+ }
+
+ port->id = id;
+ port->cap = cap;
+ port->dev.class = &typec_class;
+ port->dev.parent = dev;
+ dev_set_name(&port->dev, "usbc%d", id);
+ mutex_init(&port->lock);
+
+ ret = device_register(&port->dev);
+ if (ret) {
+ ida_simple_remove(&typec_index_ida, id);
+ put_device(&port->dev);
+ kfree(port);
+ return ERR_PTR(ret);
+ }
+
+ return port;
+}
+EXPORT_SYMBOL_GPL(typec_register_port);
+
+void typec_unregister_port(struct typec_port *port)
+{
+ device_unregister(&port->dev);
+}
+EXPORT_SYMBOL_GPL(typec_unregister_port);
+
+static int __init typec_init(void)
+{
+ return class_register(&typec_class);
+}
+subsys_initcall(typec_init);
+
+static void __exit typec_exit(void)
+{
+ return class_unregister(&typec_class);
+}
+module_exit(typec_exit);
+
+MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@xxxxxxxxxxxxxxx>");
+MODULE_LICENSE("GPL v2");
+MODULE_DESCRIPTION("USB Type-C Connector Class");
diff --git a/include/linux/usb/typec.h b/include/linux/usb/typec.h
new file mode 100644
index 0000000..d6e562c
--- /dev/null
+++ b/include/linux/usb/typec.h
@@ -0,0 +1,114 @@
+
+#ifndef __LINUX_USB_TYPEC_H
+#define __LINUX_USB_TYPEC_H
+
+#include <linux/types.h>
+
+enum typec_port_type {
+ TYPEC_PORT_DFP,
+ TYPEC_PORT_UFP,
+ TYPEC_PORT_DRP,
+};
+
+enum typec_data_role {
+ TYPEC_DEVICE,
+ TYPEC_HOST,
+};
+
+enum typec_pwr_role {
+ TYPEC_PWR_SINK,
+ TYPEC_PWR_SOURCE,
+};
+
+enum typec_pwr_opmode {
+ TYPEC_PWR_MODE_USB,
+ TYPEC_PWR_MODE_BC1_2,
+ TYPEC_PWR_MODE_1_5A,
+ TYPEC_PWR_MODE_3_0A,
+ TYPEC_PWR_MODE_PD,
+};
+
+enum typec_partner_type {
+ TYPEC_PARTNER_NONE,
+ TYPEC_PARTNER_USB,
+ TYPEC_PARTNER_CHARGER,
+ TYPEC_PARTNER_ALTMODE,
+ TYPEC_PARTNER_AUDIO,
+ TYPEC_PARTNER_DEBUG,
+};
+
+struct typec_alt_mode {
+ u16 svid;
+ u32 mid;
+};
+
+struct typec_port;
+
+/*
+ * struct typec_capability - USB Type-C Port Capabilities
+ * @type: DFP (Host-only), UFP (Device-only) or DRP (Dual Role)
+ * @usb_pd: USB Power Delivery support
+ * @alt_modes: Alternate Modes the connector supports (null terminated)
+ * @audio_accessory: Audio Accessory Adapter Mode support
+ * @debug_accessory: Debug Accessory Mode support
+ * @dr_swap: Data Role Swap support
+ * @pr_swap: Power Role Swap support
+ * @set_alt_mode: Enter given Alternate Mode
+ *
+ * Static capabilities of a single USB Type-C port.
+ */
+struct typec_capability {
+ enum typec_port_type type;
+ unsigned int usb_pd:1;
+ struct typec_alt_mode *alt_modes;
+ unsigned int audio_accessory:1;
+ unsigned int debug_accessory:1;
+
+ int (*dr_swap)(struct typec_port *);
+ int (*pr_swap)(struct typec_port *);
+ int (*set_alt_mode)(struct typec_port *,
+ struct typec_alt_mode *);
+};
+
+/*
+ * struct typec_port - USB Type-C Port
+ * @id: port index
+ * @dev: struct device instance
+ * @lock: Lock to protect concurrent access
+ * @data_role: Current USB role - Host or Device
+ * @pwr_role: Current Power role - Source or Sink
+ * @pwr_opmode: The power level in use at the moment
+ * @cur_alt_mode: The Alternate Mode currently in use
+ * @connected: Connection status
+ * @partner_type: Port type of the partner
+ * @partner_alt_modes: Alternate Modes the partner supports (null terminated)
+ * @cap: Port Capabilities
+ *
+ * Current status of a USB Type-C port and relevant partner details when
+ * connected.
+ */
+struct typec_port {
+ unsigned int id;
+ struct device dev;
+ struct mutex lock;
+
+ enum typec_data_role data_role;
+ enum typec_pwr_role pwr_role;
+ enum typec_pwr_opmode pwr_opmode;
+ struct typec_alt_mode *cur_alt_mode;
+
+ unsigned char connected;
+ enum typec_partner_type partner_type;
+ struct typec_alt_mode *partner_alt_modes;
+
+ const struct typec_capability *cap;
+};
+
+struct typec_port *typec_register_port(struct device *dev,
+ struct typec_capability *cap);
+void typec_unregister_port(struct typec_port *port);
+
+int typec_connect(struct typec_port *port);
+void typec_disconnect(struct typec_port *port);
+
+#endif /* __LINUX_USB_TYPEC_H */
--
2.7.0