[RFC PATCH 2/4] usb: typec: Character device for USB Power Delivery devices
From: Heikki Krogerus
Date: Tue Oct 26 2021 - 10:43:37 EST
Interim.
TODO/ideas:
- Figure out a proper magic value for the ioctl and check if
the ioctl range is OK.
- Register separate PD device for the cdev, and register it
only if the device (port, plug or partner) actually
supports USB PD (or come up with some other solution?).
- Introduce something like
struct pd_request {
struct pd_message request;
struct pd_message __user *response;
};
and use it instead of only single struct pd_messages everywhere.
- Add compat support.
- What do we do with Alerts and Attentions?
Signed-off-by: Heikki Krogerus <heikki.krogerus@xxxxxxxxxxxxxxx>
---
.../userspace-api/ioctl/ioctl-number.rst | 1 +
drivers/usb/typec/Makefile | 2 +-
drivers/usb/typec/class.c | 42 ++++
drivers/usb/typec/class.h | 4 +
drivers/usb/typec/pd-dev.c | 210 ++++++++++++++++++
drivers/usb/typec/pd-dev.h | 15 ++
include/linux/usb/pd_dev.h | 22 ++
include/linux/usb/typec.h | 8 +
include/uapi/linux/usb/pd_dev.h | 55 +++++
9 files changed, 358 insertions(+), 1 deletion(-)
create mode 100644 drivers/usb/typec/pd-dev.c
create mode 100644 drivers/usb/typec/pd-dev.h
create mode 100644 include/linux/usb/pd_dev.h
create mode 100644 include/uapi/linux/usb/pd_dev.h
diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
index 0ba463be6c588..fd443fd21f62a 100644
--- a/Documentation/userspace-api/ioctl/ioctl-number.rst
+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
@@ -175,6 +175,7 @@ Code Seq# Include File Comments
'P' 60-6F sound/sscape_ioctl.h conflict!
'P' 00-0F drivers/usb/class/usblp.c conflict!
'P' 01-09 drivers/misc/pci_endpoint_test.c conflict!
+'P' 70-7F uapi/linux/usb/pd_dev.h <mailto:linux-usb@xxxxxxxxxxxxxxx>
'Q' all linux/soundcard.h
'R' 00-1F linux/random.h conflict!
'R' 01 linux/rfkill.h conflict!
diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
index a0adb8947a301..be44528168013 100644
--- a/drivers/usb/typec/Makefile
+++ b/drivers/usb/typec/Makefile
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_TYPEC) += typec.o
-typec-y := class.o mux.o bus.o port-mapper.o
+typec-y := class.o mux.o bus.o port-mapper.o pd-dev.o
obj-$(CONFIG_TYPEC) += altmodes/
obj-$(CONFIG_TYPEC_TCPM) += tcpm/
obj-$(CONFIG_TYPEC_UCSI) += ucsi/
diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
index aeef453aa6585..19fcc5da175d7 100644
--- a/drivers/usb/typec/class.c
+++ b/drivers/usb/typec/class.c
@@ -15,6 +15,7 @@
#include "bus.h"
#include "class.h"
+#include "pd-dev.h"
static DEFINE_IDA(typec_index_ida);
@@ -665,6 +666,11 @@ static const struct attribute_group *typec_partner_groups[] = {
NULL
};
+char *typec_partner_devnode(struct device *dev, umode_t *mode, kuid_t *uid, kgid_t *gid)
+{
+ return kasprintf(GFP_KERNEL, "pd%u/partner", to_typec_port(dev->parent)->id);
+}
+
static void typec_partner_release(struct device *dev)
{
struct typec_partner *partner = to_typec_partner(dev);
@@ -676,6 +682,7 @@ static void typec_partner_release(struct device *dev)
const struct device_type typec_partner_dev_type = {
.name = "typec_partner",
.groups = typec_partner_groups,
+ .devnode = typec_partner_devnode,
.release = typec_partner_release,
};
@@ -807,6 +814,7 @@ struct typec_partner *typec_register_partner(struct typec_port *port,
ida_init(&partner->mode_ids);
partner->usb_pd = desc->usb_pd;
+ partner->pd_dev = desc->pd_dev;
partner->accessory = desc->accessory;
partner->num_altmodes = -1;
partner->pd_revision = desc->pd_revision;
@@ -826,6 +834,9 @@ struct typec_partner *typec_register_partner(struct typec_port *port,
partner->dev.type = &typec_partner_dev_type;
dev_set_name(&partner->dev, "%s-partner", dev_name(&port->dev));
+ if (partner->pd_dev)
+ partner->dev.devt = MKDEV(PD_DEV_MAJOR, port->id * 4 + 3);
+
ret = device_register(&partner->dev);
if (ret) {
dev_err(&port->dev, "failed to register partner (%d)\n", ret);
@@ -853,6 +864,13 @@ EXPORT_SYMBOL_GPL(typec_unregister_partner);
/* ------------------------------------------------------------------------- */
/* Type-C Cable Plugs */
+char *typec_plug_devnode(struct device *dev, umode_t *mode, kuid_t *uid, kgid_t *gid)
+{
+ return kasprintf(GFP_KERNEL, "pd%u/plug%d",
+ to_typec_port(dev->parent->parent)->id,
+ to_typec_plug(dev)->index);
+}
+
static void typec_plug_release(struct device *dev)
{
struct typec_plug *plug = to_typec_plug(dev);
@@ -891,6 +909,7 @@ static const struct attribute_group *typec_plug_groups[] = {
const struct device_type typec_plug_dev_type = {
.name = "typec_plug",
.groups = typec_plug_groups,
+ .devnode = typec_plug_devnode,
.release = typec_plug_release,
};
@@ -973,11 +992,16 @@ struct typec_plug *typec_register_plug(struct typec_cable *cable,
ida_init(&plug->mode_ids);
plug->num_altmodes = -1;
plug->index = desc->index;
+ plug->pd_dev = desc->pd_dev;
plug->dev.class = &typec_class;
plug->dev.parent = &cable->dev;
plug->dev.type = &typec_plug_dev_type;
dev_set_name(&plug->dev, "%s-%s", dev_name(cable->dev.parent), name);
+ if (plug->pd_dev)
+ plug->dev.devt = MKDEV(PD_DEV_MAJOR,
+ to_typec_port(cable->dev.parent)->id * 4 + 1 + plug->index);
+
ret = device_register(&plug->dev);
if (ret) {
dev_err(&cable->dev, "failed to register plug (%d)\n", ret);
@@ -1595,6 +1619,11 @@ static int typec_uevent(struct device *dev, struct kobj_uevent_env *env)
return ret;
}
+char *typec_devnode(struct device *dev, umode_t *mode, kuid_t *uid, kgid_t *gid)
+{
+ return kasprintf(GFP_KERNEL, "pd%u/port", to_typec_port(dev)->id);
+}
+
static void typec_release(struct device *dev)
{
struct typec_port *port = to_typec_port(dev);
@@ -1611,6 +1640,7 @@ const struct device_type typec_port_dev_type = {
.name = "typec_port",
.groups = typec_groups,
.uevent = typec_uevent,
+ .devnode = typec_devnode,
.release = typec_release,
};
@@ -2044,6 +2074,7 @@ struct typec_port *typec_register_port(struct device *parent,
port->id = id;
port->ops = cap->ops;
+ port->pd_dev = cap->pd_dev;
port->port_type = cap->type;
port->prefer_role = cap->prefer_role;
@@ -2055,6 +2086,9 @@ struct typec_port *typec_register_port(struct device *parent,
dev_set_name(&port->dev, "port%d", id);
dev_set_drvdata(&port->dev, cap->driver_data);
+ if (port->pd_dev)
+ port->dev.devt = MKDEV(PD_DEV_MAJOR, id * 4);
+
port->cap = kmemdup(cap, sizeof(*cap), GFP_KERNEL);
if (!port->cap) {
put_device(&port->dev);
@@ -2121,8 +2155,15 @@ static int __init typec_init(void)
if (ret)
goto err_unregister_mux_class;
+ ret = usbpd_dev_init();
+ if (ret)
+ goto err_unregister_class;
+
return 0;
+err_unregister_class:
+ class_unregister(&typec_class);
+
err_unregister_mux_class:
class_unregister(&typec_mux_class);
@@ -2135,6 +2176,7 @@ subsys_initcall(typec_init);
static void __exit typec_exit(void)
{
+ usbpd_dev_exit();
class_unregister(&typec_class);
ida_destroy(&typec_index_ida);
bus_unregister(&typec_bus);
diff --git a/drivers/usb/typec/class.h b/drivers/usb/typec/class.h
index aef03eb7e1523..87c072f2ad753 100644
--- a/drivers/usb/typec/class.h
+++ b/drivers/usb/typec/class.h
@@ -14,6 +14,7 @@ struct typec_plug {
enum typec_plug_index index;
struct ida mode_ids;
int num_altmodes;
+ const struct pd_dev *pd_dev;
};
struct typec_cable {
@@ -33,6 +34,7 @@ struct typec_partner {
int num_altmodes;
u16 pd_revision; /* 0300H = "3.0" */
enum usb_pd_svdm_ver svdm_version;
+ const struct pd_dev *pd_dev;
};
struct typec_port {
@@ -59,6 +61,8 @@ struct typec_port {
struct mutex port_list_lock; /* Port list lock */
void *pld;
+
+ const struct pd_dev *pd_dev;
};
#define to_typec_port(_dev_) container_of(_dev_, struct typec_port, dev)
diff --git a/drivers/usb/typec/pd-dev.c b/drivers/usb/typec/pd-dev.c
new file mode 100644
index 0000000000000..436853e046ce4
--- /dev/null
+++ b/drivers/usb/typec/pd-dev.c
@@ -0,0 +1,210 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * USB Power Delivery /dev entries
+ *
+ * Copyright (C) 2021 Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@xxxxxxxxxxxxxxx>
+ */
+
+#include <linux/cdev.h>
+#include <linux/fs.h>
+#include <linux/slab.h>
+#include <linux/usb/pd_dev.h>
+
+#include "class.h"
+
+#define PD_DEV_MAX (MINORMASK + 1)
+
+dev_t usbpd_devt;
+static struct cdev usb_pd_cdev;
+
+struct pddev {
+ struct device *dev;
+ struct typec_port *port;
+ const struct pd_dev *pd_dev;
+};
+
+static ssize_t usbpd_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
+{
+ /* FIXME TODO XXX */
+
+ /* Alert and Attention handling here (with poll) ? */
+
+ return 0;
+}
+
+static long usbpd_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+{
+ struct pddev *pd = file->private_data;
+ void __user *p = (void __user *)arg;
+ unsigned int pwr_role;
+ struct pd_message msg;
+ u32 configuration;
+ int ret = 0;
+
+ switch (cmd) {
+ case USBPDDEV_INFO:
+ if (copy_to_user(p, pd->pd_dev->info, sizeof(*pd->pd_dev->info)))
+ return -EFAULT;
+ break;
+ case USBPDDEV_CONFIGURE:
+ if (!pd->pd_dev->ops->configure)
+ return -ENOTTY;
+
+ if (copy_from_user(&configuration, p, sizeof(configuration)))
+ return -EFAULT;
+
+ ret = pd->pd_dev->ops->configure(pd->pd_dev, configuration);
+ if (ret)
+ return ret;
+ break;
+ case USBPDDEV_PWR_ROLE:
+ if (is_typec_plug(pd->dev))
+ return -ENOTTY;
+
+ if (is_typec_partner(pd->dev)) {
+ if (pd->port->pwr_role == TYPEC_SINK)
+ pwr_role = TYPEC_SOURCE;
+ else
+ pwr_role = TYPEC_SINK;
+ } else {
+ pwr_role = pd->port->pwr_role;
+ }
+
+ if (copy_to_user(p, &pwr_role, sizeof(unsigned int)))
+ return -EFAULT;
+ break;
+ case USBPDDEV_GET_MESSAGE:
+ if (!pd->pd_dev->ops->get_message)
+ return -ENOTTY;
+
+ if (copy_from_user(&msg, p, sizeof(msg)))
+ return -EFAULT;
+
+ ret = pd->pd_dev->ops->get_message(pd->pd_dev, &msg);
+ if (ret)
+ return ret;
+
+ if (copy_to_user(p, &msg, sizeof(msg)))
+ return -EFAULT;
+ break;
+ case USBPDDEV_SET_MESSAGE:
+ if (!pd->pd_dev->ops->set_message)
+ return -ENOTTY;
+
+ ret = pd->pd_dev->ops->set_message(pd->pd_dev, &msg);
+ if (ret)
+ return ret;
+
+ if (copy_to_user(p, &msg, sizeof(msg)))
+ return -EFAULT;
+ break;
+ case USBPDDEV_SUBMIT_MESSAGE:
+ if (!pd->pd_dev->ops->submit)
+ return -ENOTTY;
+
+ if (copy_from_user(&msg, p, sizeof(msg)))
+ return -EFAULT;
+
+ ret = pd->pd_dev->ops->submit(pd->pd_dev, &msg);
+ if (ret)
+ return ret;
+
+ if (copy_to_user(p, &msg, sizeof(msg)))
+ return -EFAULT;
+ break;
+ default:
+ return -ENOTTY;
+ }
+
+ return 0;
+}
+
+static int usbpd_open(struct inode *inode, struct file *file)
+{
+ struct device *dev;
+ struct pddev *pd;
+
+ dev = class_find_device_by_devt(&typec_class, inode->i_rdev);
+ if (!dev)
+ return -ENODEV;
+
+ pd = kzalloc(sizeof(*pd), GFP_KERNEL);
+ if (!pd) {
+ put_device(dev);
+ return -ENOMEM;
+ }
+
+ if (is_typec_partner(dev)) {
+ if (!to_typec_partner(dev)->usb_pd) {
+ put_device(dev);
+ kfree(pd);
+ return -ENODEV;
+ }
+ pd->port = to_typec_port(dev->parent);
+ pd->pd_dev = to_typec_partner(dev)->pd_dev;
+ } else if (is_typec_plug(dev)) {
+ pd->port = to_typec_port(dev->parent->parent);
+ pd->pd_dev = to_typec_plug(dev)->pd_dev;
+ } else {
+ pd->port = to_typec_port(dev);
+ pd->pd_dev = to_typec_port(dev)->pd_dev;
+ }
+
+ pd->dev = dev;
+ file->private_data = pd;
+
+ return 0;
+}
+
+static int usbpd_release(struct inode *inode, struct file *file)
+{
+ struct pddev *pd = file->private_data;
+
+ put_device(pd->dev);
+ kfree(pd);
+
+ return 0;
+}
+
+const struct file_operations usbpd_file_operations = {
+ .owner = THIS_MODULE,
+ .llseek = no_llseek,
+ .read = usbpd_read,
+ .unlocked_ioctl = usbpd_ioctl,
+ .compat_ioctl = compat_ptr_ioctl,
+ .open = usbpd_open,
+ .release = usbpd_release,
+};
+
+int __init usbpd_dev_init(void)
+{
+ int ret;
+
+ ret = alloc_chrdev_region(&usbpd_devt, 0, PD_DEV_MAX, "usb_pd");
+ if (ret)
+ return ret;
+
+ /*
+ * FIXME!
+ *
+ * Now the cdev is always created, even when the device does not support
+ * USB PD.
+ */
+
+ cdev_init(&usb_pd_cdev, &usbpd_file_operations);
+
+ ret = cdev_add(&usb_pd_cdev, usbpd_devt, PD_DEV_MAX);
+ if (ret) {
+ unregister_chrdev_region(usbpd_devt, PD_DEV_MAX);
+ return ret;
+ }
+
+ return 0;
+}
+
+void __exit usbpd_dev_exit(void)
+{
+ cdev_del(&usb_pd_cdev);
+ unregister_chrdev_region(usbpd_devt, PD_DEV_MAX);
+}
diff --git a/drivers/usb/typec/pd-dev.h b/drivers/usb/typec/pd-dev.h
new file mode 100644
index 0000000000000..2d817167c4042
--- /dev/null
+++ b/drivers/usb/typec/pd-dev.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __USB_TYPEC_PDDEV__
+#define __USB_TYPEC_PDDEV__
+
+#include <linux/kdev_t.h>
+
+#define PD_DEV_MAJOR MAJOR(usbpd_devt)
+
+extern dev_t usbpd_devt;
+
+int usbpd_dev_init(void);
+void usbpd_dev_exit(void);
+
+#endif /* __USB_TYPEC_PDDEV__ */
diff --git a/include/linux/usb/pd_dev.h b/include/linux/usb/pd_dev.h
new file mode 100644
index 0000000000000..d451928f94e78
--- /dev/null
+++ b/include/linux/usb/pd_dev.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __LINUX_USB_PDDEV_H
+#define __LINUX_USB_PDDEV_H
+
+#include <uapi/linux/usb/pd_dev.h>
+
+struct pd_dev;
+
+struct pd_ops {
+ int (*configure)(const struct pd_dev *dev, u32 flags);
+ int (*get_message)(const struct pd_dev *dev, struct pd_message *msg);
+ int (*set_message)(const struct pd_dev *dev, struct pd_message *msg);
+ int (*submit)(const struct pd_dev *dev, struct pd_message *msg);
+};
+
+struct pd_dev {
+ const struct pd_info *info;
+ const struct pd_ops *ops;
+};
+
+#endif /* __LINUX_USB_PDDEV_H */
diff --git a/include/linux/usb/typec.h b/include/linux/usb/typec.h
index e2e44bb1dad85..6df7b096f769c 100644
--- a/include/linux/usb/typec.h
+++ b/include/linux/usb/typec.h
@@ -20,6 +20,7 @@ struct typec_port;
struct typec_altmode_ops;
struct fwnode_handle;
+struct pd_dev;
struct device;
enum typec_port_type {
@@ -159,11 +160,13 @@ enum typec_plug_index {
* struct typec_plug_desc - USB Type-C Cable Plug Descriptor
* @index: SOP Prime for the plug connected to DFP and SOP Double Prime for the
* plug connected to UFP
+ * @pd_dev: USB Power Delivery Character Device
*
* Represents USB Type-C Cable Plug.
*/
struct typec_plug_desc {
enum typec_plug_index index;
+ const struct pd_dev *pd_dev;
};
/*
@@ -189,6 +192,7 @@ struct typec_cable_desc {
* @accessory: Audio, Debug or none.
* @identity: Discover Identity command data
* @pd_revision: USB Power Delivery Specification Revision if supported
+ * @pd_dev: USB Power Delivery Character Device
*
* Details about a partner that is attached to USB Type-C port. If @identity
* member exists when partner is registered, a directory named "identity" is
@@ -204,6 +208,7 @@ struct typec_partner_desc {
enum typec_accessory accessory;
struct usb_pd_identity *identity;
u16 pd_revision; /* 0300H = "3.0" */
+ const struct pd_dev *pd_dev;
};
/**
@@ -241,6 +246,7 @@ enum usb_pd_svdm_ver {
* @fwnode: Optional fwnode of the port
* @driver_data: Private pointer for driver specific info
* @ops: Port operations vector
+ * @pd_dev: USB Power Delivery Character Device
*
* Static capabilities of a single USB Type-C port.
*/
@@ -258,6 +264,8 @@ struct typec_capability {
void *driver_data;
const struct typec_operations *ops;
+
+ const struct pd_dev *pd_dev;
};
/* Specific to try_role(). Indicates the user want's to clear the preference. */
diff --git a/include/uapi/linux/usb/pd_dev.h b/include/uapi/linux/usb/pd_dev.h
new file mode 100644
index 0000000000000..44027bc6b6339
--- /dev/null
+++ b/include/uapi/linux/usb/pd_dev.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef _UAPI__LINUX_USB_PDDEV_H
+#define _UAPI__LINUX_USB_PDDEV_H
+
+#include <linux/types.h>
+#include <linux/usb/pd.h>
+
+/*
+ * struct pd_info - USB Power Delivery Device Information
+ * @specification_revision: USB Power Delivery Specification Revision
+ * @supported_ctrl_msgs: Supported Control Messages
+ * @supported_data_msgs: Supported Data Messages
+ * @supported_ext_msgs: Supported Extended Messages
+ *
+ * @specification_revision is in the same format as the Specification Revision
+ * Field in the Message Header. @supported_ctrl_msgs, @supported_data_msgs and
+ * @supported_ext_msgs list the messages, a bit for each, that can be used with
+ * USBPDDEV_SUBMIT_MESSAGE ioctl.
+ */
+struct pd_info {
+ __u8 specification_revision; /* XXX I don't know if this is useful? */
+ __u32 ctrl_msgs_supported;
+ __u32 data_msgs_supported;
+ __u32 ext_msgs_supported;
+} __attribute__ ((packed));
+
+/* Example configuration flags for ports. */
+#define USBPDDEV_CFPORT_ENTER_MODES BIT(0) /* Automatic alt mode entry. */
+
+/*
+ * For basic communication use USBPDDEV_SUBMIT_MESSAGE ioctl. GoodCRC is not
+ * supported. Response will also never be GoodCRC.
+ *
+ * To check cached objects (if they are cached) use USBPDDEV_GET_MESSAGE ioctl.
+ * Useful most likely with RDO and EUDO, but also with Identity etc.
+ * USBPDDEV_SET_MESSAGE is primarily meant to be used with ports. If supported,
+ * it can be used to assign the values for objects like EUDO that the port should
+ * use in future communication.
+ *
+ * The idea with USBPDDEV_CONFIGURE is that you could modify the behaviour of
+ * the underlying TCPM (or what ever interface you have) with some things. So
+ * for example, you could disable automatic alternate mode entry with it with
+ * that USBPDDEV_CFPORT_ENTER_MODES - It's just an example! - so basically, you
+ * could take over some things from TCPM with it.
+ */
+
+#define USBPDDEV_INFO _IOR('P', 0x70, struct pd_info)
+#define USBPDDEV_CONFIGURE _IOW('P', 0x71, __u32)
+#define USBPDDEV_PWR_ROLE _IOR('P', 0x72, int) /* The *current* role! */
+#define USBPDDEV_GET_MESSAGE _IOWR('P', 0x73, struct pd_message)
+#define USBPDDEV_SET_MESSAGE _IOW('P', 0x74, struct pd_message)
+#define USBPDDEV_SUBMIT_MESSAGE _IOWR('P', 0x75, struct pd_message)
+
+#endif /* _UAPI__LINUX_USB_PDDEV_H */
--
2.33.0