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];
next prev 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