Linux USB
 help / color / mirror / Atom feed
From: Igor Skalkin <igor.skalkin@oss.qualcomm.com>
To: "Michael S . Tsirkin" <mst@redhat.com>,
	Jason Wang <jasowangio@gmail.com>,
	Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: virtualization@lists.linux.dev, linux-usb@vger.kernel.org,
	Vasilii Ianikeev <vasilii.ianikeev@oss.qualcomm.com>,
	Aiswarya Cyriac <aiswarya.cyriac@oss.qualcomm.com>,
	Anton Yakovlev <anton.yakovlev@oss.qualcomm.com>,
	Trilok Soni <trilok.soni@oss.qualcomm.com>,
	Igor Skalkin <igor.skalkin@oss.qualcomm.com>
Subject: [PATCH 5/8] virtio-usb: add USB On-The-Go role-switching support
Date: Thu, 24 Sep 2026 18:09:04 +0200	[thread overview]
Message-ID: <20260924160907.145405-6-igor.skalkin@oss.qualcomm.com> (raw)
In-Reply-To: <20260924160907.145405-1-igor.skalkin@oss.qualcomm.com>

Add support for ports that can switch between the host and device
roles at runtime (USB OTG-style role switching): negotiate
VIRTIO_USB_F_SWITCH_ROLE support during probe, extend otg_get_role()
to also report each port's supported roles (is_otg), and register a
USB Role Switch class device so userspace can observe and control the
role and be notified of role changes via VIRTIO_USB_EVT_OTG_CHANGE_ROLE.

This is a proof-of-concept for internal demo purposes; the two
directions currently work quite differently and neither is fully
guest-initiated:

- Device-to-host: the guest writes the new role to its own USB Role
  Switch sysfs entry. The driver sends a switch command to the host
  device; the host device performs the switch and sends back a
  VIRTIO_USB_EVT_OTG_CHANGE_ROLE event, which is what actually updates
  the guest's role.
- Host-to-device: cannot be initiated from the guest at all. The host
  kernel driver switches role on its own, either from an OTG interrupt
  or a manual sysfs role write on the host side. The host virtio
  device notices the USB port disappearing, checks the role via
  sysfs, and if it changed, sends the guest a
  VIRTIO_USB_EVT_OTG_CHANGE_ROLE event; the guest driver just reacts
  to it.

Real USB OTG separates this into two signals: bus_req, asserted by a
B-device (peripheral) to request becoming host, and bus_drop,
asserted by the current host to grant permission for the role swap -
without bus_drop, the peripheral's request cannot succeed.
VIRTIO_USB_CMD_OTG_SWITCH_ROLE only implements the bus_req side;
there is no bus_drop equivalent, so the current host's permission is
implicitly always granted. Fine for a first version; can be
revisited if a real use case needs the host to refuse a switch.

Signed-off-by: Igor Skalkin <igor.skalkin@oss.qualcomm.com>
---
 drivers/usb/virtio_usb/controller.c |   94 ++++++--
 drivers/usb/virtio_usb/controller.h |   21 +
 drivers/usb/virtio_usb/device.c     |    8 
 drivers/usb/virtio_usb/host.c       |   45 +++-
 drivers/usb/virtio_usb/otg.c        |  401 ++++++++++++++++++++++++++++--------
 drivers/usb/virtio_usb/otg.h        |   16 +
 include/uapi/linux/virtio_usb.h     |    1 
 7 files changed, 474 insertions(+), 112 deletions(-)

diff --git a/drivers/usb/virtio_usb/controller.c b/drivers/usb/virtio_usb/controller.c
index 59af5cc..c018725 100644
--- a/drivers/usb/virtio_usb/controller.c
+++ b/drivers/usb/virtio_usb/controller.c
@@ -10,8 +10,8 @@
 #include <uapi/linux/virtio_ids.h>
 
 #include "controller.h"
-#include "host.h"
 #include "device.h"
+#include "host.h"
 #include "otg.h"
 #include "vq_common.h"
 
@@ -86,6 +86,14 @@ static int virtio_usb_validate(struct virtio_device *vdev)
 		return -EINVAL;
 	}
 
+	if ((!(virtio_has_feature(vdev, VIRTIO_USB_F_HOST)) ||
+	     !(virtio_has_feature(vdev, VIRTIO_USB_F_DEVICE))) &&
+	    virtio_has_feature(vdev, VIRTIO_USB_F_SWITCH_ROLE)) {
+		dev_err(&vdev->dev,
+			"OTG requires both (host and device) roles support\n");
+		return -EINVAL;
+	}
+
 	if (!virtio_usb_cmd_timeout_ms) {
 		dev_err(&vdev->dev, "msg_timeout_ms value cannot be zero\n");
 		return -EINVAL;
@@ -129,6 +137,12 @@ static int virtio_usb_probe(struct virtio_device *vdev)
 	if (virtio_has_feature(vdev, VIRTIO_USB_F_DEVICE))
 		vusb->device_role = 1;
 
+	if (virtio_has_feature(vdev, VIRTIO_USB_F_SWITCH_ROLE))
+		vusb->switch_role = 1;
+
+	dev_info(&vdev->dev, "%s nports %d (h_role %d dev_role %d)\n", __func__,
+		 vusb->nports, vusb->host_role, vusb->device_role);
+
 	/* Only allocate/negotiate the virtqueue triplets this instance
 	 * actually needs: HOST_* only exists when host_role is negotiated,
 	 * DEV_* only when device_role is negotiated, OTG_* only when both
@@ -219,20 +233,44 @@ static int virtio_usb_probe(struct virtio_device *vdev)
 		}
 	}
 
+	if (vusb->device_role) {
+		INIT_WORK(&vusb->vq_dev_event_work, virtio_usb_dc_event_work);
+		INIT_WORK(&vusb->vq_dev_data_rx_work, virtio_usb_dc_data_work);
+
+		/* Populate the shared device event queue before telling the
+		 * backend we are ready, so a DEV_CONNECTED event can never
+		 * race ahead of the driver having posted receive buffers.
+		 */
+		rc = virtio_usb_dc_event_populate(vusb);
+		if (rc) {
+			dev_err(&vdev->dev,
+				"%s virtio_usb_dc_event_populate() error(%d)\n",
+				__func__, rc);
+			goto on_error;
+		}
+	}
+
+	virtio_device_ready(vdev);
+
 	/* Resolve every port's role. With only one role negotiated, every
 	 * port unambiguously has that role. With both negotiated, query
-	 * each port's actual role individually via otg_get_role(), since
-	 * it is otherwise ambiguous (host|device).
+	 * each port's actual role - and, if switch_role is negotiated,
+	 * whether the port supports switching - individually via
+	 * otg_get_role(), since a port's own role is otherwise ambiguous
+	 * (host|device).
 	 */
 	for (i = 0; i < vusb->nports; i++) {
+		vusb->vports[i].vusb = vusb;
+		spin_lock_init(&vusb->vports[i].vhc_lock);
+
 		if (vusb->host_role && !vusb->device_role) {
 			vusb->vports[i].role = VIRTIO_USB_ROLE_HOST;
 		} else if (vusb->device_role && !vusb->host_role) {
 			vusb->vports[i].role = VIRTIO_USB_ROLE_DEVICE;
 		} else {
-			u32 status, role;
+			u32 status, role, supported_role;
 
-			status = otg_get_role(vusb, i, &role);
+			status = otg_get_role(vusb, i, &role, &supported_role);
 			if (status != VIRTIO_USB_S_OK) {
 				dev_err(&vdev->dev, "%s status(%d)\n", __func__,
 					status);
@@ -247,9 +285,25 @@ static int virtio_usb_probe(struct virtio_device *vdev)
 				goto on_error;
 			}
 			vusb->vports[i].role = role;
+			vusb->vports[i].is_otg =
+				supported_role &
+				(1UL << VIRTIO_USB_F_SWITCH_ROLE);
+
+			if (vusb->vports[i].is_otg && !vusb->switch_role) {
+				dev_err(&vdev->dev,
+					"%s port%d switch_role not supported\n",
+					__func__, i);
+				rc = -EIO;
+				goto on_error;
+			}
 		}
 	}
 
+	for (i = 0; i < vusb->nports; i++)
+		dev_info(&vdev->dev, "%s port[%d] VIRTIO_USB_F_%s is_otg %s\n",
+			 __func__, i, vusb->vports[i].role ? "DEVICE" : "HOST",
+			 vusb->vports[i].is_otg ? "TRUE" : "FALSE");
+
 	if (vusb->host_role) {
 		INIT_WORK(&vusb->vq_host_data_rx_work, virtio_usb_hc_rx_work);
 		INIT_WORK(&vusb->vq_host_evt_work, virtio_usb_hc_evt_work);
@@ -278,13 +332,9 @@ static int virtio_usb_probe(struct virtio_device *vdev)
 		}
 	}
 
-	if (vusb->device_role) {
-		INIT_WORK(&vusb->vq_dev_data_rx_work, virtio_usb_dc_data_work);
-		INIT_WORK(&vusb->vq_dev_event_work, virtio_usb_dc_event_work);
-
-		for (i = 0; i < vusb->nports; i++) {
-			if (vusb->vports[i].role != VIRTIO_USB_ROLE_DEVICE)
-				continue;
+	for (i = 0; i < vusb->nports; i++) {
+		if (vusb->vports[i].is_otg ||
+		    vusb->vports[i].role == VIRTIO_USB_ROLE_DEVICE) {
 			rc = virtio_usb_dc_init(vusb, i);
 			if (rc) {
 				dev_err(&vdev->dev,
@@ -293,17 +343,17 @@ static int virtio_usb_probe(struct virtio_device *vdev)
 				goto on_error;
 			}
 		}
-		rc = virtio_usb_dc_event_populate(vusb);
-		if (rc) {
-			dev_err(&vdev->dev,
-				"%s virtio_usb_dc_event_populate() error(%d)\n",
-				__func__, rc);
-			goto on_error;
-		}
 	}
 
-	virtio_device_ready(vdev);
+	if (vusb->host_role && vusb->device_role)
+		if (otg_sysfs_init(vusb))
+			goto on_error;
+
+	WRITE_ONCE(vusb->started, true);
+	if (vusb->device_role)
+		schedule_work(&vusb->vq_dev_event_work);
 
+	dev_info(&vdev->dev, "%s returns %d\n", __func__, rc);
 	return rc;
 
 on_error:
@@ -323,6 +373,9 @@ static void virtio_usb_remove(struct virtio_device *vdev)
 	struct virtio_usb *vusb = vdev->priv;
 	int i;
 
+	/* Stop queue-driven workers from scheduling new activity. */
+	WRITE_ONCE(vusb->started, false);
+
 	for (i = 0; i < vusb->nvqs; i++)
 		vusb->vqueues[i].stop(vusb, &vusb->vqueues[i]);
 
@@ -344,6 +397,7 @@ static void virtio_usb_remove(struct virtio_device *vdev)
 static const unsigned int virtio_usb_features[] = {
 	VIRTIO_USB_F_HOST,
 	VIRTIO_USB_F_DEVICE,
+	VIRTIO_USB_F_SWITCH_ROLE,
 };
 
 static const struct virtio_device_id id_table[] = {
diff --git a/drivers/usb/virtio_usb/controller.h b/drivers/usb/virtio_usb/controller.h
index 4d9e0e2..deee053 100644
--- a/drivers/usb/virtio_usb/controller.h
+++ b/drivers/usb/virtio_usb/controller.h
@@ -20,6 +20,7 @@ struct virtio_usb_hc_vp;
 struct virtio_usb_dc;
 /* Forward declaration - full definition in otg.h */
 struct virtio_usb_otg;
+struct virtio_usb_port_otg;
 
 #define VIRTIO_USB_VQ_COMMAND_IDX 0
 #define VIRTIO_USB_VQ_EVENT_IDX 1
@@ -31,14 +32,27 @@ struct virtio_usb_otg;
 
 /**
  * struct virtio_usb_port - Per-virtual-port state.
+ * @vusb: VirtIO usb device this port belongs to.
+ * @is_otg: True if this port supports dynamic role switching.
  * @role: Role of this port (VIRTIO_USB_ROLE_HOST or _DEVICE).
  * @vhc: Host controller - non-NULL when role is HOST.
  * @vudc: Device controller - non-NULL when role is DEVICE.
+ * @otg: Per-port OTG role-switching state - non-NULL when is_otg.
  */
 struct virtio_usb_port {
+	struct virtio_usb *vusb;
+	bool is_otg;
 	unsigned int role;
+	/*! Protects vhc against concurrent OTG-triggered init/deinit while
+	 * the host event-processing worker is reading it. Only needs to
+	 * guard the pointer itself: virtio_usb_hc_vp_init()/_deinit()'s
+	 * sleeping work (devm_kzalloc(), usb_add_hcd(), usb_remove_hcd())
+	 * happens outside this lock.
+	 */
+	spinlock_t vhc_lock;
 	struct virtio_usb_hc_vp *vhc;
 	struct virtio_usb_dc *vudc;
+	struct virtio_usb_port_otg *otg;
 };
 
 /**
@@ -63,6 +77,9 @@ struct virtio_usb_port {
  *               otg_get_role() below, since with both host_role and
  *               device_role negotiated a port's own role is otherwise
  *               ambiguous.
+ * @switch_role: flag indicating support for dynamically switching roles
+ * @started: True once probe() has finished bringing up every VP - guards
+ *           notify callbacks against running before setup is complete.
  * @vq_host_data_rx_work: Kernel work draining the host data queue, shared
  *                        across every host-role VP.
  * @vq_host_evt_work: Kernel work draining the host event queue, shared
@@ -76,13 +93,15 @@ struct virtio_usb {
 	struct virtio_device *vdev;
 	struct virtio_usb_queue *vqueues;
 	struct virtio_usb_port *vports;
-	unsigned int nports;
+	u32 nports;
 	u32 nvqs;
 	bool host_role;
 	bool device_role;
 	int host_vq_base;
 	int dev_vq_base;
 	int otg_vq_base;
+	bool switch_role;
+	bool started;
 	struct work_struct vq_host_data_rx_work;
 	struct work_struct vq_host_evt_work;
 	struct work_struct vq_dev_data_rx_work;
diff --git a/drivers/usb/virtio_usb/device.c b/drivers/usb/virtio_usb/device.c
index 798c265..5ae0fc8 100644
--- a/drivers/usb/virtio_usb/device.c
+++ b/drivers/usb/virtio_usb/device.c
@@ -1056,6 +1056,8 @@ static void virtio_usb_dc_event_process(struct virtio_usb_event *event)
 		break;
 	}
 	case VIRTIO_USB_EVT_DEV_DISCONNECTED: {
+		pr_info("%s port_id %d VIRTIO_USB_EVT_DEV_DISCONNECTED\n",
+			__func__, port_id);
 		if (vudc->driver && vudc->driver->disconnect)
 			vudc->driver->disconnect(&vudc->gadget);
 		if (vudc->registered)
@@ -1080,16 +1082,19 @@ static void virtio_usb_dc_event_process(struct virtio_usb_event *event)
 		break;
 	}
 	case VIRTIO_USB_EVT_DEV_RESET: {
+		pr_info("%s VIRTIO_USB_EVT_DEV_RESET\n", __func__);
 		if (driver)
 			usb_gadget_udc_reset(&vudc->gadget, driver);
 		break;
 	}
 	case VIRTIO_USB_EVT_DEV_SUSPEND: {
+		pr_info("%s VIRTIO_USB_EVT_DEV_SUSPEND\n", __func__);
 		if (driver && driver->suspend)
 			driver->suspend(&vudc->gadget);
 		break;
 	}
 	case VIRTIO_USB_EVT_DEV_RESUME: {
+		pr_info("%s VIRTIO_USB_EVT_DEV_RESUME\n", __func__);
 		if (driver && driver->resume)
 			driver->resume(&vudc->gadget);
 		break;
@@ -1308,6 +1313,9 @@ static void virtio_usb_dc_evt_notify_cb(struct virtqueue *vqueue)
 {
 	struct virtio_usb *vusb = vqueue->vdev->priv;
 
+	if (!READ_ONCE(vusb->started))
+		return;
+
 	schedule_work(&vusb->vq_dev_event_work);
 }
 
diff --git a/drivers/usb/virtio_usb/host.c b/drivers/usb/virtio_usb/host.c
index 9926e66..5dc47f3 100644
--- a/drivers/usb/virtio_usb/host.c
+++ b/drivers/usb/virtio_usb/host.c
@@ -1013,12 +1013,16 @@ int virtio_usb_hc_vp_init(struct virtio_usb *vusb, unsigned int vp_idx)
 		vhcd_vp->hcqs[i] = &vusb->vqueues[vusb->host_vq_base + i];
 
 	/* Install into the port before add_hcd so vhcd_vp->vusb is set */
+	spin_lock(&vusb->vports[vp_idx].vhc_lock);
 	vusb->vports[vp_idx].vhc = vhcd_vp;
+	spin_unlock(&vusb->vports[vp_idx].vhc_lock);
 
 	/* Add HCDs first so hs/ss are valid before any PORT_CONNECTED event */
 	rc = virtio_usb_add_hcd(vusb, vhcd_vp);
 	if (rc) {
+		spin_lock(&vusb->vports[vp_idx].vhc_lock);
 		vusb->vports[vp_idx].vhc = NULL;
+		spin_unlock(&vusb->vports[vp_idx].vhc_lock);
 		return rc;
 	}
 
@@ -1032,7 +1036,11 @@ int virtio_usb_hc_vp_init(struct virtio_usb *vusb, unsigned int vp_idx)
  */
 int virtio_usb_hc_vp_deinit(struct virtio_usb *vusb, unsigned int vp_idx)
 {
-	struct virtio_usb_hc_vp *vhcd_vp = vusb->vports[vp_idx].vhc;
+	struct virtio_usb_hc_vp *vhcd_vp;
+
+	spin_lock(&vusb->vports[vp_idx].vhc_lock);
+	vhcd_vp = vusb->vports[vp_idx].vhc;
+	spin_unlock(&vusb->vports[vp_idx].vhc_lock);
 
 	if (!vhcd_vp)
 		return 0;
@@ -1045,7 +1053,9 @@ int virtio_usb_hc_vp_deinit(struct virtio_usb *vusb, unsigned int vp_idx)
 	vhcd_vp->ss = NULL;
 	vhcd_vp->hs = NULL;
 
+	spin_lock(&vusb->vports[vp_idx].vhc_lock);
 	vusb->vports[vp_idx].vhc = NULL;
+	spin_unlock(&vusb->vports[vp_idx].vhc_lock);
 	return 0;
 }
 
@@ -1085,7 +1095,18 @@ static void virtio_usb_hc_evt_process_one(struct virtio_usb_event *uevent)
 		return;
 	}
 
+	/* vhc can be concurrently init/deinit'd by an OTG role switch
+	 * (virtio_usb_otg_event_process() runs on its own workqueue) -
+	 * vhc_lock protects only the pointer read itself; the vhcd_vp it
+	 * points to remains valid for as long as we hold a reference to
+	 * it here, since virtio_usb_hc_vp_deinit() only clears the
+	 * vports[vp_idx].vhc pointer under the same lock, it does not
+	 * free vhcd_vp itself (devm-managed, freed at device teardown).
+	 */
+	spin_lock(&vusb->vports[vp_idx].vhc_lock);
 	vhcd_vp = vusb->vports[vp_idx].vhc;
+	spin_unlock(&vusb->vports[vp_idx].vhc_lock);
+
 	if (!vhcd_vp) {
 		dev_err_ratelimited(
 			&vusb->vdev->dev,
@@ -1174,10 +1195,12 @@ static void virtio_usb_hc_evt_process_one(struct virtio_usb_event *uevent)
  * @work: kernel work item embedded in struct virtio_usb.
  *
  * The host event queue is shared across all host-role VPs and its VP
- * may not even exist yet at probe time (e.g. a dual-role instance
- * where every port currently reports device role), so events are
- * drained and processed here, in process context, rather than
- * directly inside the interrupt-context notify callback.
+ * may not even exist yet (or may be concurrently torn down by an OTG
+ * role switch), so events are drained and processed here, in process
+ * context, instead of directly inside the interrupt-context notify
+ * callback - this lets virtio_usb_hc_evt_process_one() safely take
+ * vhc_lock without needing an atomic-context-safe primitive on the
+ * writer side (virtio_usb_hc_vp_init()/_deinit() sleep).
  *
  * Context: Process context.
  */
@@ -1215,7 +1238,11 @@ static void virtio_usb_hc_dataq_stop_cb(struct virtio_usb *vusb,
 	cancel_work_sync(&vusb->vq_host_data_rx_work);
 
 	for (vp_idx = 0; vp_idx < vusb->nports; vp_idx++) {
-		struct virtio_usb_hc_vp *vhcd_vp = vusb->vports[vp_idx].vhc;
+		struct virtio_usb_hc_vp *vhcd_vp;
+
+		spin_lock(&vusb->vports[vp_idx].vhc_lock);
+		vhcd_vp = vusb->vports[vp_idx].vhc;
+		spin_unlock(&vusb->vports[vp_idx].vhc_lock);
 
 		if (!vhcd_vp)
 			continue;
@@ -1284,6 +1311,9 @@ static void virtio_usb_hc_evt_notify_cb(struct virtqueue *vqueue)
 {
 	struct virtio_usb *vusb = vqueue->vdev->priv;
 
+	if (!READ_ONCE(vusb->started))
+		return;
+
 	schedule_work(&vusb->vq_host_evt_work);
 }
 
@@ -1310,6 +1340,9 @@ static void virtio_usb_host_data_notify_cb(struct virtqueue *vqueue)
 {
 	struct virtio_usb *vusb = vqueue->vdev->priv;
 
+	if (!READ_ONCE(vusb->started))
+		return;
+
 	schedule_work(&vusb->vq_host_data_rx_work);
 }
 
diff --git a/include/uapi/linux/virtio_usb.h b/include/uapi/linux/virtio_usb.h
index 4cbfb3f..29cec5d 100644
--- a/include/uapi/linux/virtio_usb.h
+++ b/include/uapi/linux/virtio_usb.h
@@ -318,6 +318,7 @@ enum {
 struct virtio_usb_otg_cmd_role {
 	struct virtio_usb_cmd_status status;
 	__le32 role; /* VIRTIO_USB_ROLE_XXX */
+	__le32 supported_role; /* VIRTIO_USB_F_[HOST|DEVICE|SWITCH_ROLE] */
 };
 
 /*****************************************************************************
diff --git a/drivers/usb/virtio_usb/otg.c b/drivers/usb/virtio_usb/otg.c
index 557dfae..f0f390f 100644
--- a/drivers/usb/virtio_usb/otg.c
+++ b/drivers/usb/virtio_usb/otg.c
@@ -7,11 +7,238 @@
 
 #include <linux/mutex.h>
 #include "controller.h"
+#include "host.h"
 #include "otg.h"
 #include "vq_common.h"
 
+// for usb_role enum from include/linux/usb/role.h
+const char *role_switch_role_names[] = { "USB_ROLE_NONE", "USB_ROLE_HOST",
+					 "USB_ROLE_DEVICE" };
+
+static int vusb_set_role_cb(struct usb_role_switch *sw, enum usb_role role)
+{
+	struct virtio_usb_port *vport = usb_role_switch_get_drvdata(sw);
+
+	pr_info("%s set_role to %s\n", __func__, role_switch_role_names[role]);
+	mutex_lock(&vport->otg->lock);
+
+	if (role == USB_ROLE_HOST && vport->role == VIRTIO_USB_ROLE_DEVICE) {
+		pr_warn("%s we are in device role, switch to host\n", __func__);
+		schedule_work(&vport->otg->set_role_work);
+	}
+	mutex_unlock(&vport->otg->lock);
+	return 0;
+}
+
+static enum usb_role vusb_get_role_cb(struct usb_role_switch *sw)
+{
+	struct virtio_usb_port *vport = usb_role_switch_get_drvdata(sw);
+
+	switch (vport->role) {
+	case VIRTIO_USB_ROLE_HOST:
+		return USB_ROLE_HOST;
+	case VIRTIO_USB_ROLE_DEVICE:
+		return USB_ROLE_DEVICE;
+	default:
+		return USB_ROLE_NONE;
+	}
+}
+
+static void virtio_usb_otg_event_process(struct virtio_usb_event *event)
+{
+	struct virtio_usb *vusb = event->vusb;
+	struct virtio_usb_otg_event *evt = virtio_usb_event_buf(event);
+	int port_id = le32_to_cpu(evt->port);
+	struct virtio_usb_port *vport = &vusb->vports[port_id];
+	u32 status, role, supported_role, old_role;
+	int rc;
+
+	if (le32_to_cpu(evt->code) != VIRTIO_USB_EVT_OTG_CHANGE_ROLE) {
+		pr_err("%s wrong event code %d\n", __func__,
+		       le32_to_cpu(evt->code));
+		return;
+	}
+
+	old_role = vport->role;
+	pr_info("%s VIRTIO_USB_EVT_OTG_CHANGE_ROLE curr_role %s\n", __func__,
+		old_role == VIRTIO_USB_ROLE_DEVICE ? "VIRTIO_USB_ROLE_DEVICE" :
+						     "VIRTIO_USB_ROLE_HOST");
+
+	status = otg_get_role(vusb, port_id, &role, &supported_role);
+	if (status != VIRTIO_USB_S_OK) {
+		pr_err("%s otg_get_role status %d\n", __func__, status);
+		return;
+	}
+
+	/* A port's usb_hcd/root hub is only supposed to exist while that
+	 * port is actually in host role - it's immediately visible to the
+	 * rest of the kernel (lsusb, udev, ...) the moment usb_add_hcd()
+	 * runs, unlike the device-role side's vudc, which stays dormant
+	 * and kernel-invisible until a later BIND event. So, unlike vudc
+	 * (already unconditionally pre-allocated for every OTG-capable
+	 * port at probe time, regardless of its current role - see
+	 * virtio_usb_probe()), vhc must be created/destroyed dynamically,
+	 * exactly at the moment a port's role actually changes.
+	 */
+	if (role != old_role && role == VIRTIO_USB_ROLE_HOST) {
+		rc = virtio_usb_hc_vp_init(vusb, port_id);
+		if (rc) {
+			pr_err("%s virtio_usb_hc_vp_init() port=%d error(%d)\n",
+			       __func__, port_id, rc);
+			/* Leave vport->role at its old value: reporting a
+			 * host role with no working HCD behind it would be
+			 * worse than not switching at all.
+			 */
+			return;
+		}
+	} else if (role != old_role && old_role == VIRTIO_USB_ROLE_HOST) {
+		virtio_usb_hc_vp_deinit(vusb, port_id);
+	}
+
+	vport->role = role;
+	vport->is_otg = supported_role & (1UL << VIRTIO_USB_F_SWITCH_ROLE);
+	pr_info("%s The new role (%s) is set\n", __func__,
+		role == VIRTIO_USB_ROLE_DEVICE ? "VIRTIO_USB_ROLE_DEVICE" :
+						 "VIRTIO_USB_ROLE_HOST");
+}
+
+/**
+ * virtio_usb_otg_event_work() - OTG event queue receive worker.
+ * @work: Kernel work to handle event completion.
+ *
+ * Context: Process context.
+ */
+static void virtio_usb_otg_event_work(struct work_struct *work)
+{
+	struct virtio_usb_otg *otg =
+		container_of(work, struct virtio_usb_otg, event_process_work);
+	struct virtio_usb_queue *evtq = otg->oqs[VIRTIO_USB_VQ_EVENT_IDX];
+
+	virtio_usb_evt_work(evtq, virtio_usb_otg_event_process);
+}
+
+/**
+ * virtio_usb_otg_set_role_work() - OTG send command to device worker.
+ *
+ * Context: Process context.
+ */
+static void virtio_usb_otg_set_role_work(struct work_struct *work)
+{
+	struct virtio_usb_port_otg *vport_otg =
+		container_of(work, struct virtio_usb_port_otg, set_role_work);
+	struct virtio_usb_otg *otg = vport_otg->vusb->otg;
+	struct virtio_usb_otg_cmd_hdr *hdr;
+	struct virtio_usb_cmd *cmd;
+	int rc;
+
+	cmd = virtio_usb_cmd_alloc(
+		sizeof(*hdr), sizeof(struct virtio_usb_cmd_status), GFP_KERNEL);
+	if (!cmd)
+		return;
+
+	hdr = virtio_usb_cmd_request(cmd);
+	hdr->code = cpu_to_le32(VIRTIO_USB_CMD_OTG_SWITCH_ROLE);
+	hdr->port = cpu_to_le32(vport_otg->port_id);
+	cmd->msg.queue = otg->oqs[VIRTIO_USB_VQ_COMMAND_IDX];
+
+	mutex_lock(&otg->lock);
+	rc = virtio_usb_cmd_send_sync(vport_otg->vusb, NULL, NULL, cmd);
+	mutex_unlock(&otg->lock);
+
+	if (rc)
+		pr_err("%s virtio_usb_cmd_send_sync() error %d\n", __func__,
+		       rc);
+	else
+		pr_info("%s success\n", __func__);
+}
+
+/**
+ * virtio_usb_otg_event_populate() - Add events to the otg event queue.
+ * @vusb: VirtIO USB device.
+ *
+ * Context: Any context.
+ * Return: 0 on success -errno on failure.
+ */
+static int virtio_usb_otg_event_populate(struct virtio_usb *vusb)
+{
+	struct virtio_usb_queue *otg_evt_queue =
+		vusb->otg->oqs[VIRTIO_USB_VQ_EVENT_IDX];
+	struct virtio_usb_event *events;
+
+	events = virtio_usb_events_alloc(vusb, otg_evt_queue,
+					 sizeof(struct virtio_usb_otg_event));
+	if (!events)
+		return -ENOMEM;
+
+	return virtio_usb_events_populate(events);
+}
+
+static void otg_vports_deinit(struct virtio_usb *vusb)
+{
+	if (!vusb->vports)
+		return;
+
+	for (int i = 0; i < vusb->nports; ++i) {
+		struct virtio_usb_port *vport = &vusb->vports[i];
+		/* is_otg is set before otg_sysfs_init() runs (which is
+		 * where vport->otg actually gets allocated) - a probe
+		 * failure in between the two would otherwise leave
+		 * is_otg true but otg still NULL here.
+		 */
+		if (!vport->is_otg || !vport->otg)
+			continue;
+
+		cancel_work_sync(&vport->otg->set_role_work);
+		usb_role_switch_unregister(vport->otg->sw);
+		vport->otg->sw = NULL;
+		kfree(vport->otg->sysfs_name);
+		vport->otg->sysfs_name = NULL;
+	}
+}
+
+static int otg_vport_init(struct virtio_usb *vusb, int port_id)
+{
+	struct device *dev = &vusb->vdev->dev;
+	struct virtio_usb_port *vport = &vusb->vports[port_id];
+	struct virtio_usb_port_otg *vport_otg;
+	struct usb_role_switch_desc desc = { 0 };
+
+	vport_otg = devm_kzalloc(dev, sizeof(*vport_otg), GFP_KERNEL);
+	if (!vport_otg)
+		return -ENOMEM;
+
+	vport_otg->vusb = vusb;
+	vport_otg->port_id = port_id;
+	vport_otg->sysfs_name = kasprintf(GFP_KERNEL, "port%u", port_id);
+
+	mutex_init(&vport_otg->lock);
+	INIT_WORK(&vport_otg->set_role_work, virtio_usb_otg_set_role_work);
+
+	desc.name = vport_otg->sysfs_name;
+	desc.set = vusb_set_role_cb;
+	desc.get = vusb_get_role_cb;
+	desc.driver_data = vport;
+	desc.fwnode = NULL;
+	desc.allow_userspace_control = true;
+
+	vport_otg->sw = usb_role_switch_register(dev, &desc);
+	if (IS_ERR(vport_otg->sw)) {
+		int rc = PTR_ERR(vport_otg->sw);
+		dev_err(dev, "port%u: usb_role_switch_register() failed: %d\n",
+			port_id, rc);
+		vport_otg->sw = NULL;
+		kfree(vport_otg->sysfs_name);
+		vport_otg->sysfs_name = NULL;
+		return rc;
+	}
+
+	vport->otg = vport_otg;
+	return 0;
+}
+
 int otg_init(struct virtio_usb *vusb)
 {
+	int rc;
 	struct virtio_usb_otg *otg =
 		devm_kzalloc(&vusb->vdev->dev, sizeof(*otg), GFP_KERNEL);
 	unsigned int i;
@@ -24,12 +251,39 @@ int otg_init(struct virtio_usb *vusb)
 	for (i = 0; i < VIRTIO_USB_VQ_OTG_MAX; i++)
 		otg->oqs[i] = &vusb->vqueues[vusb->otg_vq_base + i];
 
+	INIT_WORK((struct work_struct *)&otg->event_process_work,
+		  virtio_usb_otg_event_work);
+
 	mutex_init(&otg->lock);
-	init_completion(&otg->completion);
+
+	rc = virtio_usb_otg_event_populate(vusb);
+	if (rc)
+		return rc;
 
 	return 0;
 }
 
+int otg_sysfs_init(struct virtio_usb *vusb)
+{
+	int i, rc;
+
+	for (i = 0; i < vusb->nports; i++)
+		if (vusb->vports[i].is_otg) {
+			/* Skip if already registered - probe may be retried
+			 * after a Virtio reset and the role switch device
+			 * persists across retries (devm allocation).
+			 * Calling usb_role_switch_register() again would
+			 * return -EEXIST from device_register().
+			 */
+			if (vusb->vports[i].otg)
+				continue;
+			rc = otg_vport_init(vusb, i);
+			if (rc)
+				return rc;
+		}
+	return 0;
+}
+
 void otg_deinit(struct virtio_usb *vusb)
 {
 	struct virtio_usb_otg *otg = vusb->otg;
@@ -37,55 +291,75 @@ void otg_deinit(struct virtio_usb *vusb)
 	if (!otg)
 		return;
 
-	/* Wake potential OTG command waiters before releasing OTG objects. */
-	complete_all(&otg->completion);
-
+	/*
+	 * event_process_work is already cancelled by
+	 * virtio_usb_otg_evtq_stop_cb(), called earlier in
+	 * virtio_usb_remove()'s vqueues[i].stop() loop.
+	 */
+	otg_vports_deinit(vusb);
 	vusb->otg = NULL;
 }
 
+/**
+ * virtio_usb_otg_evt_notify_cb() - OTG Event virtqueue notification callback
+ * @vqueue: Underlying event virtqueue.
+ *
+ * This callback function is called upon a vring interrupt request from the
+ * device.
+ *
+ * Context: Interrupt context.
+ */
+static void virtio_usb_otg_evt_notify_cb(struct virtqueue *vqueue)
+{
+	struct virtio_usb *vusb = vqueue->vdev->priv;
+
+	if (!READ_ONCE(vusb->started) || !vusb->otg)
+		return;
+
+	schedule_work(&vusb->otg->event_process_work);
+}
+
 /* Send an OTG command and get a response.
  *
  * The function is implemented as synchronous. Design pattern is
  * virtio_can.c/virtio_can_send_ctrl_msg()
  */
-u32 otg_get_role(struct virtio_usb *vusb, int port_id, u32 *role)
+u32 otg_get_role(struct virtio_usb *vusb, int port_id, u32 *role,
+		 u32 *supported_role)
 {
-	struct scatterlist sg_out, sg_in, *sgs[2] = { &sg_out, &sg_in };
-	struct virtqueue *vq =
-		vusb->otg->oqs[VIRTIO_USB_VQ_COMMAND_IDX]->vqueue;
-	unsigned int len;
+	struct virtio_usb_otg_cmd_hdr *hdr;
+	struct virtio_usb_otg_cmd_role *resp;
+	struct virtio_usb_cmd *cmd;
 	u32 status = VIRTIO_USB_S_ERR_INTERNAL;
+	int rc;
 
-	struct otg_get_role {
-		struct virtio_usb_otg_cmd_hdr cmd_hdr;
-		struct virtio_usb_otg_cmd_role cmd_role;
-	} *msg = kzalloc(sizeof(struct otg_get_role), GFP_KERNEL);
-
-	if (!msg)
+	cmd = virtio_usb_cmd_alloc(sizeof(*hdr), sizeof(*resp), GFP_KERNEL);
+	if (!cmd)
 		return status;
 
-	msg->cmd_hdr.code = cpu_to_le32(VIRTIO_USB_CMD_OTG_GET_ROLE);
-	msg->cmd_hdr.port = cpu_to_le32(port_id);
-	sg_init_one(&sg_out, &msg->cmd_hdr, sizeof(msg->cmd_hdr));
-	sg_init_one(&sg_in, &msg->cmd_role, sizeof(msg->cmd_role));
+	hdr = virtio_usb_cmd_request(cmd);
+	hdr->code = cpu_to_le32(VIRTIO_USB_CMD_OTG_GET_ROLE);
+	hdr->port = cpu_to_le32(port_id);
+	cmd->msg.queue = vusb->otg->oqs[VIRTIO_USB_VQ_COMMAND_IDX];
 
-	mutex_lock(&vusb->otg->lock);
+	virtio_usb_cmd_ref(cmd);
 
-	if (virtqueue_add_sgs(vq, sgs, 1u, 1u, msg, GFP_ATOMIC)) {
-		pr_err("%s virtqueue_add_sgs error\n", __func__);
-		goto exit;
-	}
+	mutex_lock(&vusb->otg->lock);
+	rc = virtio_usb_cmd_send_sync(vusb, NULL, NULL, cmd);
+	mutex_unlock(&vusb->otg->lock);
 
-	if (!virtqueue_kick(vq)) {
-		pr_err("%s virtqueue_kick error\n", __func__);
-		goto exit;
+	if (rc) {
+		pr_err("%s virtio_usb_cmd_send_sync() error %d\n", __func__,
+		       rc);
+		virtio_usb_cmd_unref(cmd);
+		return status;
 	}
 
-	while (!virtqueue_get_buf(vq, &len) && !virtqueue_is_broken(vq))
-		wait_for_completion(&vusb->otg->completion);
-
-	status = le32_to_cpu(msg->cmd_role.status.code);
-	*role = le32_to_cpu(msg->cmd_role.role);
+	resp = virtio_usb_cmd_response(cmd);
+	status = le32_to_cpu(resp->status.code);
+	*role = le32_to_cpu(resp->role);
+	*supported_role = le32_to_cpu(resp->supported_role);
+	virtio_usb_cmd_unref(cmd);
 
 	if (*role != VIRTIO_USB_ROLE_HOST && *role != VIRTIO_USB_ROLE_DEVICE)
 		pr_err("%s - wrong role (%d)\n", __func__, *role);
@@ -96,71 +370,32 @@ u32 otg_get_role(struct virtio_usb *vusb, int port_id, u32 *role)
 				"VIRTIO_USB_ROLE_DEVICE");
 	}
 
-exit:
-	kfree(msg);
-	mutex_unlock(&vusb->otg->lock);
 	return status;
 }
 
-static void virtio_usb_otg_cmd_notify_cb(struct virtqueue *vqueue)
-{
-	struct virtio_usb *vusb = vqueue->vdev->priv;
-
-	if (!vusb->otg)
-		return;
-
-	complete(&vusb->otg->completion);
-}
-
-static void virtio_usb_otg_cmdq_stop_cb(struct virtio_usb *vusb,
-					struct virtio_usb_queue *vq)
-{
-	unsigned long flags;
-
-	if (!vusb->otg || !vq->vqueue)
-		return;
-
-	/*
-	 * Wake sleepers in OTG synchronous command paths so they can
-	 * observe started=false and exit.
-	 */
-	complete_all(&vusb->otg->completion);
-
-	spin_lock_irqsave(&vq->lock, flags);
-	virtqueue_disable_cb(vq->vqueue);
-	spin_unlock_irqrestore(&vq->lock, flags);
-}
-
 static void virtio_usb_otg_evtq_stop_cb(struct virtio_usb *vusb,
 					struct virtio_usb_queue *vq)
 {
-	unsigned long flags;
-	u32 length;
-	void *buf;
-
-	if (!vq->vqueue)
-		return;
-
-	/* The OTG event queue is not populated yet at this stage (no
-	 * VIRTIO_USB_F_SWITCH_ROLE negotiation, no CHANGE_ROLE events),
-	 * so this only has to make sure del_vqs() finds the ring empty.
+	/*
+	 * Now that otg_get_role()/virtio_usb_otg_set_role_work() go
+	 * through the common, timeout-protected virtio_usb_cmd_send_sync()
+	 * instead of blocking indefinitely on virtqueue_is_broken(), it's
+	 * safe to cancel event_process_work directly here, same as every
+	 * other role's own evtq stop callback.
 	 */
-	spin_lock_irqsave(&vq->lock, flags);
-	virtqueue_disable_cb(vq->vqueue);
-	while ((buf = virtqueue_get_buf(vq->vqueue, &length)))
-		;
-	spin_unlock_irqrestore(&vq->lock, flags);
+	virtio_usb_evt_drain_stop_cb(
+		vq, vusb->otg ? &vusb->otg->event_process_work : NULL);
 }
 
 const struct virtio_usb_vq_desc otg_vqueues[VIRTIO_USB_VQ_OTG_MAX] = {
 	[VIRTIO_USB_VQ_COMMAND_IDX] = {
-		.callback = virtio_usb_otg_cmd_notify_cb,
+		.callback = virtio_usb_cmd_notify_cb,
 		.name = "virtusb-otg-cmd",
-		.process = NULL,
-		.stop = virtio_usb_otg_cmdq_stop_cb,
+		.process = virtio_usb_cmd_process_cb,
+		.stop = virtio_usb_cmdq_stop_cb,
 	},
 	[VIRTIO_USB_VQ_EVENT_IDX] = {
-		.callback = NULL,
+		.callback = virtio_usb_otg_evt_notify_cb,
 		.name = "virtusb-otg-evt",
 		.process = NULL,
 		.stop = virtio_usb_otg_evtq_stop_cb,
diff --git a/drivers/usb/virtio_usb/otg.h b/drivers/usb/virtio_usb/otg.h
index a34317c..85e9b9e 100644
--- a/drivers/usb/virtio_usb/otg.h
+++ b/drivers/usb/virtio_usb/otg.h
@@ -8,17 +8,29 @@
 #ifndef VIRTIO_USB_OTG_H
 #define VIRTIO_USB_OTG_H
 
+#include <linux/usb/role.h>
 #include "controller.h"
 
 extern int otg_init(struct virtio_usb *vusb);
+extern int otg_sysfs_init(struct virtio_usb *vusb);
 extern void otg_deinit(struct virtio_usb *vusb);
-extern u32 otg_get_role(struct virtio_usb *vusb, int port_id, u32 *role);
+extern u32 otg_get_role(struct virtio_usb *vusb, int port_id, u32 *role,
+			u32 *supported_role);
+
+struct virtio_usb_port_otg {
+	struct virtio_usb *vusb;
+	struct mutex lock;
+	struct usb_role_switch *sw;
+	char *sysfs_name;
+	struct work_struct set_role_work;
+	int port_id;
+};
 
 struct virtio_usb_otg {
 	struct virtio_usb *vusb;
 	struct mutex lock;
-	struct completion completion;
 	struct virtio_usb_queue *oqs[VIRTIO_USB_VQ_OTG_MAX];
+	struct work_struct event_process_work;
 };
 
 extern const struct virtio_usb_vq_desc otg_vqueues[VIRTIO_USB_VQ_OTG_MAX];

  parent reply	other threads:[~2026-09-24 16:09 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-24 16:08 [PATCH 0/8] virtio-usb: add dual-role virtio USB driver Igor Skalkin
2026-09-24 16:09 ` [PATCH 1/8] virtio-usb: add protocol header and skeleton dual-role driver Igor Skalkin
2026-09-25  5:14   ` Greg Kroah-Hartman
2026-09-28 14:19     ` Igor Skalkin
2026-09-25  5:21   ` Greg Kroah-Hartman
2026-09-28 14:14     ` Igor Skalkin
2026-09-24 16:09 ` [PATCH 2/8] virtio-usb: add host role (USB Host Controller) support Igor Skalkin
2026-09-25  5:18   ` Greg Kroah-Hartman
2026-09-28 14:01     ` Igor Skalkin
2026-09-24 16:09 ` [PATCH 3/8] virtio-usb: add device role (USB Device " Igor Skalkin
2026-09-24 16:09 ` [PATCH 4/8] virtio-usb: add OTG role query support Igor Skalkin
2026-09-24 16:09 ` Igor Skalkin [this message]
2026-09-24 16:09 ` [PATCH 6/8] virtio-usb: rework endpoint lifecycle to an async split-phase state machine Igor Skalkin
2026-09-24 16:09 ` [PATCH 7/8] virtio-usb: add SuperSpeed device-role support Igor Skalkin
2026-09-24 16:09 ` [PATCH 8/8] virtio-usb: support a guest UDC name prefix from the bind event Igor Skalkin
2026-09-25  5:17 ` [PATCH 0/8] virtio-usb: add dual-role virtio USB driver Greg Kroah-Hartman
2026-09-28 13:55   ` Igor Skalkin
2026-09-28 14:44     ` Greg Kroah-Hartman
2026-09-28 15:56       ` Igor Skalkin
2026-09-28 16:10         ` Greg Kroah-Hartman
2026-09-29  9:47     ` Michael S. Tsirkin
2026-09-29 16:01       ` Greg Kroah-Hartman
2026-09-29 19:02         ` Vasilii Ianikeev
2026-09-29 19:58         ` Vasilii Ianikeev

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260924160907.145405-6-igor.skalkin@oss.qualcomm.com \
    --to=igor.skalkin@oss.qualcomm.com \
    --cc=aiswarya.cyriac@oss.qualcomm.com \
    --cc=anton.yakovlev@oss.qualcomm.com \
    --cc=gregkh@linuxfoundation.org \
    --cc=jasowangio@gmail.com \
    --cc=linux-usb@vger.kernel.org \
    --cc=mst@redhat.com \
    --cc=trilok.soni@oss.qualcomm.com \
    --cc=vasilii.ianikeev@oss.qualcomm.com \
    --cc=virtualization@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox