mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v4 0/3] usb: dwc3: support two connectors sharing one HS data bus
@ 2026-10-26 12:32 Qianhui Xu
  2026-10-26 12:32 ` [PATCH v4 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus Qianhui Xu
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Qianhui Xu @ 2026-10-26 12:32 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Thinh Nguyen, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-usb, devicetree, linux-arm-msm, linux-kernel

This is a formal submission, not an RFC: it supersedes the two-patch v2
series (<20261017081320.220525-1-xiaqinghuan@mail.ustc.edu.cn>) and the
shape-only v3 RFC (<20261019131848.344689-1-xiaqinghuan@mail.ustc.edu.cn>),
and it contains the driver that uses the binding it describes.  Sorry for the confusion caused by sending v3 as an
RFC threaded under v2; the intent was to settle the binding shape first,
but that is not how it should have been posted.  The series is complete
below, with the disposition of every review comment listed.

Two connectors on one DWC3 controller
=====================================

The board (Lenovo Legion Y700 Gen4, SM8750) wires the Super Speed pairs of
two USB-C receptacles to separate PHYs, while the High Speed D+/D- pair is
shared through a 2:1 mux controlled by a single GPIO:

	usb@a600000 {
		port-select-gpios = <&tlmm 29 GPIO_ACTIVE_HIGH>;

		ports {
			#address-cells = <1>;
			#size-cells = <0>;

			port@0 {
				reg = <0>;
				#address-cells = <1>;
				#size-cells = <0>;

				endpoint@0 {
					reg = <0>;
					remote-endpoint = <&usb_0_dwc3_hs>;
					usb-role-switch;
					mux-state = <0>;
				};

				endpoint@1 {
					reg = <1>;
					remote-endpoint = <&usb_1_dwc3_hs>;
					usb-role-switch;
					mux-state = <1>;
				};
			};

			port@1 {
				/* Super Speed of connector 0, separate PHY */
			};

			port@2 {
				/* Super Speed of connector 1, separate PHY */
			};
		};
	};

Both connectors are data-role dual and hand their role to the SoC through
pmic-glink, so the controller has to arbitrate between two independent role
requests and move the shared data path before it is reinitialised.

Series layout
=============

1/3 documents the two-connector HS data path: "port-select-gpios" plus two
    numbered endpoints of "port@0" carrying "mux-state" (which GPIO level
    connects that connector) and "usb-role-switch" (this connector has its
    own role-switch input).

2/3 adds usb_role_switch_at_endpoint(), so a role switch registered against
    an OF graph endpoint can be found by the consumer that owns the other
    end of that link.  Without it the endpoint registration in 1/3 would
    have no lookup path.

3/3 implements the arbitration in DWC3: one role switch per connector, the
    oldest pending request wins, the GPIO is flipped inside __dwc3_set_mode()
    after the previous host/gadget instance is torn down, and a connector
    only reports a role while it owns the data path.

Disposition of the review comments
==================================

sashiko, binding (v2 1/2):

 - "model the board mux with the standard mux subsystem": not adopted.
   drivers/usb/ has no mux_control consumer, mux only selects a channel and
   cannot express "oldest requester wins" role arbitration, and gpio-mux
   drives its idle level at probe, which would override the selection left
   by the bootloader.  The mux is therefore described where it is used, as
   a GPIO plus per-endpoint level.

 - "port@2 models the second connector as a controller bus": fixed.
   The second connector is now endpoint@1 of port@0, so "ports" keeps
   describing the controller's own buses.

 - "usb-role-switch does not belong on a graph endpoint": kept, but the
   missing lookup path is fixed by 2/3 instead of relying on a downstream
   only helper.

 - "conditionals do not require ports": fixed, "ports" and "port@0" are
   required when "port-select-gpios" is present, and the endpoint
   properties are rejected when it is absent.

 - "two ports may carry the same mux-state": fixed, the values are
   constrained to be complementary.

sashiko, driver (v2 2/2):

 - Critical, "the init error path leaves drd_work queued": fixed.
   Both switches are now registered before the initial dwc3_set_mode(),
   and the error path cancels the queued role work.

 - High, "dwc3_set_mode() is called after dropping the lock": fixed.
   The request order and the resulting mode request are taken in one
   critical section; dwc3_set_mode() only queues work, so holding the mux
   lock across it is safe.

 - High, "an in-place role swap overwrites the request timestamp": fixed.
   A request ticket is only taken on a NONE -> role transition, so a
   connector that is already using the bus cannot steal it.

 - High, "registering on the endpoint breaks the lookup": addressed by 2/3.

 - Medium, "fwnode leak on the error path": fixed, the endpoint reference
   is dropped on every exit path.

 - Medium, "error and desired_role are never read": fixed, desired_role is
   gone and the sticky mux error is reported to the connector on its next
   role request.

 - Medium, "add dwc3_pre_set_role()": not adopted in this series; the role
   is set from the same place as the single-connector switch, and adding a
   second entry point would duplicate that path.  If you prefer the helper,
   I will restructure the series that way.

Testing
=======

Mechanical, re-run on v4:

 - checkpatch --strict: 0 errors, 0 warnings, 0 checks on all three patches.
 - dt-doc-validate on snps,dwc3-common.yaml: clean.
 - W=1 compiles clean for drivers/usb/dwc3/ and drivers/usb/roles/.
 - Full arm64 Image.gz + dtbs build of a v7.3-rc7 tree with the series
   applied.

Board behaviour: this board's daily kernel carries this series, and it
registers both per-connector switches, visible as
a600000.usb-port0-role-switch and a600000.usb-port1-role-switch.  In the
earlier revisions the mux logic was exercised plug-by-plug: host mode on
either receptacle, moving the cable from one receptacle to the other hands
the controller over, and a charging-only connector next to an active one
does not disturb it.

The v4 changes are the review fixes listed above: registering the switches
before the initial mode request, taking the request order and the mode
request in one critical section, ticket handling on NONE -> role only, the
monotonic counter instead of jiffies, and dropping the unused field.  Those
were reviewed and built but not re-exercised plug-by-plug, because the
author's test setup talks to this board through the very USB link being
switched; I would rather say that than claim a test I did not run.  I can
re-test on request if a maintainer wants the swap sequence confirmed before
this goes in.

Qianhui Xu (3):
  dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus
  usb: roles: look up role switches attached to a graph endpoint
  usb: dwc3: support two connectors sharing one HS data bus


Qianhui Xu (3):
  dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS
    bus
  usb: roles: look up role switches attached to a graph endpoint
  usb: dwc3: support two connectors sharing one HS data bus

 .../bindings/usb/snps,dwc3-common.yaml        | 109 ++++++++-
 drivers/usb/dwc3/core.c                       |  29 ++-
 drivers/usb/dwc3/core.h                       |  39 ++++
 drivers/usb/dwc3/debugfs.c                    |   4 +
 drivers/usb/dwc3/drd.c                        | 219 +++++++++++++++++-
 drivers/usb/roles/class.c                     |  40 ++++
 6 files changed, 437 insertions(+), 3 deletions(-)

-- 
2.53.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v4 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus
  2026-10-26 12:32 [PATCH v4 0/3] usb: dwc3: support two connectors sharing one HS data bus Qianhui Xu
@ 2026-10-26 12:32 ` Qianhui Xu
  2026-11-02  8:13   ` Krzysztof Kozlowski
  2026-10-26 12:32 ` [PATCH v4 2/3] usb: roles: look up role switches attached to a graph endpoint Qianhui Xu
  2026-10-26 12:32 ` [PATCH v4 3/3] usb: dwc3: support two connectors sharing one HS data bus Qianhui Xu
  2 siblings, 1 reply; 5+ messages in thread
From: Qianhui Xu @ 2026-10-26 12:32 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Thinh Nguyen, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-usb, devicetree, linux-arm-msm, linux-kernel

Two connectors can share the High Speed D+/D- pair of a single DWC3
controller, with a GPIO selecting which one is connected.  Super Speed
lanes stay per connector.

Describe that in snps,dwc3-common.yaml:

  - port-select-gpios selects which connector drives the shared HS bus;
  - the shared HS bus is port@0 with two numbered endpoints, endpoint@0
    and endpoint@1, so that "ports" keeps describing the controller's own
    buses as the graph bindings expect;
  - each endpoint carries "mux-state", the port-select level of that
    connector, and "usb-role-switch" to declare that this connector has
    its own role switch input.

When port-select-gpios is absent the endpoint properties are rejected,
so existing single-connector descriptions are unaffected.  The two
mux-state values are constrained to be complementary.

Signed-off-by: Qianhui Xu <xiaqinghuan@mail.ustc.edu.cn>
---
 .../bindings/usb/snps,dwc3-common.yaml        | 109 +++++++++++++++++-
 1 file changed, 108 insertions(+), 1 deletion(-)

diff --git a/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml b/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml
index 6c0b8b6..6b06b00 100644
--- a/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml
+++ b/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml
@@ -13,6 +13,38 @@ description:
   Defines the properties of the DWC3 core as being embedded in either an
   vendor-specific implementation or as a standalone component.
 
+$defs:
+  hs-endpoint:
+    $ref: /schemas/graph.yaml#/$defs/endpoint-base
+    unevaluatedProperties: false
+
+    properties:
+      usb-role-switch:
+        type: boolean
+        description:
+          This connector endpoint has its own USB role switch. The switch is
+          registered against this endpoint and looked up with
+          usb_role_switch_at_endpoint().
+
+      mux-state:
+        $ref: /schemas/types.yaml#/definitions/uint32
+        enum: [0, 1]
+        description:
+          Logical level of the port-select GPIO which connects this
+          connector to the shared High Speed data bus.
+
+    dependentRequired:
+      usb-role-switch: [mux-state]
+      mux-state: [usb-role-switch]
+
+  hs-port:
+    $ref: /schemas/graph.yaml#/$defs/port-base
+    unevaluatedProperties: false
+
+    patternProperties:
+      '^endpoint@[01]$':
+        $ref: '#/$defs/hs-endpoint'
+
 allOf:
   - $ref: usb-drd.yaml#
   - if:
@@ -26,8 +58,83 @@ allOf:
       $ref: usb.yaml#
     else:
       $ref: usb-xhci.yaml#
+  - if:
+      required: [port-select-gpios]
+    then:
+      required: [dr_mode, ports]
+      properties:
+        dr_mode:
+          const: otg
+        ports:
+          required: [port@0]
+          properties:
+            port@0:
+              required: ['#address-cells', '#size-cells', endpoint@0, endpoint@1]
+              properties:
+                endpoint@0:
+                  required: [usb-role-switch, mux-state]
+                endpoint@1:
+                  required: [usb-role-switch, mux-state]
+    else:
+      properties:
+        ports:
+          properties:
+            port@0:
+              patternProperties:
+                '^endpoint@[01]$':
+                  properties:
+                    usb-role-switch: false
+                    mux-state: false
+  - if:
+      properties:
+        ports:
+          properties:
+            port@0:
+              properties:
+                endpoint@0:
+                  properties:
+                    mux-state:
+                      const: 0
+    then:
+      properties:
+        ports:
+          properties:
+            port@0:
+              properties:
+                endpoint@1:
+                  properties:
+                    mux-state:
+                      const: 1
+  - if:
+      properties:
+        ports:
+          properties:
+            port@0:
+              properties:
+                endpoint@1:
+                  properties:
+                    mux-state:
+                      const: 0
+    then:
+      properties:
+        ports:
+          properties:
+            port@0:
+              properties:
+                endpoint@0:
+                  properties:
+                    mux-state:
+                      const: 1
 
 properties:
+  port-select-gpios:
+    maxItems: 1
+    description:
+      Selects which of the two connectors drives the controller's shared High
+      Speed data bus. Each connector is described by a numbered endpoint of
+      "port@0" that carries its own role-switch input and the "mux-state"
+      level selecting it.
+
   extcon:
     maxItems: 1
     deprecated: true
@@ -411,7 +518,7 @@ properties:
 
     properties:
       port@0:
-        $ref: /schemas/graph.yaml#/properties/port
+        $ref: '#/$defs/hs-port'
         description: High Speed (HS) data bus.
 
       port@1:
-- 
2.53.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v4 2/3] usb: roles: look up role switches attached to a graph endpoint
  2026-10-26 12:32 [PATCH v4 0/3] usb: dwc3: support two connectors sharing one HS data bus Qianhui Xu
  2026-10-26 12:32 ` [PATCH v4 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus Qianhui Xu
@ 2026-10-26 12:32 ` Qianhui Xu
  2026-10-26 12:32 ` [PATCH v4 3/3] usb: dwc3: support two connectors sharing one HS data bus Qianhui Xu
  2 siblings, 0 replies; 5+ messages in thread
From: Qianhui Xu @ 2026-10-26 12:32 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Thinh Nguyen, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-usb, devicetree, linux-arm-msm, linux-kernel

usb_role_switch_get() and fwnode_usb_role_switch_get() look for a switch
on the caller's parent device node or through device_connection.  That
does not work when one controller exposes a role switch per connector
endpoint: the switch is registered against the remote graph endpoint and
the consumer (the connector) has no phandle to it.

Add usb_role_switch_at_endpoint(), which walks the caller's OF graph
endpoints and matches a switch registered on the remote endpoint, and try
it after the existing parent lookup.  There is no behaviour change for
existing users: if nothing is registered against an endpoint the lookup
falls through to the previous paths.

This is needed by DWC3 designs where two Type-C receptacles share one
High Speed data bus.

Signed-off-by: Qianhui Xu <xiaqinghuan@mail.ustc.edu.cn>
---
 drivers/usb/roles/class.c | 40 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/drivers/usb/roles/class.c b/drivers/usb/roles/class.c
index edec139..fc739fd 100644
--- a/drivers/usb/roles/class.c
+++ b/drivers/usb/roles/class.c
@@ -157,6 +157,42 @@ usb_role_switch_is_parent(struct fwnode_handle *fwnode)
 	return dev ? to_role_switch(dev) : ERR_PTR(-EPROBE_DEFER);
 }
 
+/*
+ * Look up a role switch registered against the remote endpoint of one of the
+ * caller's OF graph endpoints.  This is how a connector endpoint that shares a
+ * USB data bus with another connector (for example two Type-C receptacles on a
+ * single DWC3 controller, selected by a GPIO mux) finds its own role switch.
+ */
+static struct usb_role_switch *
+usb_role_switch_at_endpoint(struct fwnode_handle *fwnode)
+{
+	struct fwnode_handle *ep, *remote, *parent;
+	struct usb_role_switch *sw;
+	bool available;
+
+	fwnode_graph_for_each_endpoint(fwnode, ep) {
+		parent = fwnode_graph_get_remote_port_parent(ep);
+		if (!parent)
+			continue;
+		available = fwnode_device_is_available(parent);
+		fwnode_handle_put(parent);
+		if (!available)
+			continue;
+
+		remote = fwnode_graph_get_remote_endpoint(ep);
+		if (!remote)
+			continue;
+		sw = usb_role_switch_match(remote, "usb-role-switch", NULL);
+		fwnode_handle_put(remote);
+		if (!IS_ERR_OR_NULL(sw)) {
+			fwnode_handle_put(ep);
+			return sw;
+		}
+	}
+
+	return NULL;
+}
+
 /**
  * usb_role_switch_get - Find USB role switch linked with the caller
  * @dev: The caller device
@@ -169,6 +205,8 @@ struct usb_role_switch *usb_role_switch_get(struct device *dev)
 	struct usb_role_switch *sw;
 
 	sw = usb_role_switch_is_parent(dev_fwnode(dev));
+	if (!sw)
+		sw = usb_role_switch_at_endpoint(dev_fwnode(dev));
 	if (!sw)
 		sw = device_connection_find_match(dev, "usb-role-switch", NULL,
 						  usb_role_switch_match);
@@ -192,6 +230,8 @@ struct usb_role_switch *fwnode_usb_role_switch_get(struct fwnode_handle *fwnode)
 	struct usb_role_switch *sw;
 
 	sw = usb_role_switch_is_parent(fwnode);
+	if (!sw)
+		sw = usb_role_switch_at_endpoint(fwnode);
 	if (!sw)
 		sw = fwnode_connection_find_match(fwnode, "usb-role-switch",
 						  NULL, usb_role_switch_match);
-- 
2.53.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v4 3/3] usb: dwc3: support two connectors sharing one HS data bus
  2026-10-26 12:32 [PATCH v4 0/3] usb: dwc3: support two connectors sharing one HS data bus Qianhui Xu
  2026-10-26 12:32 ` [PATCH v4 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus Qianhui Xu
  2026-10-26 12:32 ` [PATCH v4 2/3] usb: roles: look up role switches attached to a graph endpoint Qianhui Xu
@ 2026-10-26 12:32 ` Qianhui Xu
  2 siblings, 0 replies; 5+ messages in thread
From: Qianhui Xu @ 2026-10-26 12:32 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Thinh Nguyen, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-usb, devicetree, linux-arm-msm, linux-kernel

On some boards a single DWC3 controller drives two receptacles: the Super
Speed pairs are wired to separate PHYs, while the High Speed data lines
are muxed to whichever connector is in use.  Such a controller has to
service two independent role-switch inputs, and the shared data path has
to be moved before the controller is reinitialised for the requested role.

Register one usb_role_switch per connector endpoint described by
"port-select-gpios" and the per-endpoint "mux-state" property.  Requests
from the two switches are serialised, the oldest pending request wins, and
the port-select GPIO is only changed once the previous host or gadget
instance has been torn down, which is where __dwc3_set_mode() runs.  A
connector only reports its own role while it owns the data path, so the
connector drivers see a consistent view of a shared controller.

The role change keeps the asynchronous semantics of the existing
single-connector role switch: __dwc3_set_mode() is the only place that
programs hardware, and it never aborts half-way, so a failed GPIO write
cannot leave a torn-down role behind.  The error is kept in the mux state
and reported back to the connector on its next role request.

While the two-connector mux owns role selection, the debugfs "mode" write
used to force a specific role is refused instead of racing with it.

Signed-off-by: Qianhui Xu <xiaqinghuan@mail.ustc.edu.cn>
---
Changes in v4:
 - register both role switches before the initial dwc3_set_mode(), and
   cancel the queued role work on the init error path (Critical)
 - take the request order and the resulting mode request in one critical
   section instead of dropping the mux lock around dwc3_set_mode() (High)
 - only take a request ticket on a NONE -> role transition, so an in-place
   role swap cannot steal the shared data path from the connector that
   asked first (High)
 - order requests with a monotonic counter instead of jiffies, removing
   the time_before() wraparound (High)
 - drop the unused desired_role field and report the sticky mux error to
   the connector instead of keeping it unread (Medium)

Changes in v3:
 - binding moved to "port-select-gpios" plus numbered endpoints carrying
   "mux-state" and "usb-role-switch"; the endpoint lookup helper is part of
   the series
---
 drivers/usb/dwc3/core.c    |  29 ++++-
 drivers/usb/dwc3/core.h    |  39 +++++++
 drivers/usb/dwc3/debugfs.c |   4 +
 drivers/usb/dwc3/drd.c     | 219 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 289 insertions(+), 2 deletions(-)

diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c
index fd5c2cd..6607e65 100644
--- a/drivers/usb/dwc3/core.c
+++ b/drivers/usb/dwc3/core.c
@@ -21,6 +21,7 @@
 #include <linux/io.h>
 #include <linux/list.h>
 #include <linux/delay.h>
+#include <linux/gpio/consumer.h>
 #include <linux/dma-mapping.h>
 #include <linux/of.h>
 #include <linux/of_graph.h>
@@ -165,7 +166,9 @@ EXPORT_SYMBOL_GPL(dwc3_set_prtcap);
 static void __dwc3_set_mode(struct work_struct *work)
 {
 	struct dwc3 *dwc = work_to_dwc(work);
+	struct dwc3_role_mux *mux = dwc->role_mux;
 	unsigned long flags;
+	int mux_port = 0;
 	int ret;
 	u32 reg;
 	u32 desired_dr_role;
@@ -174,6 +177,8 @@ static void __dwc3_set_mode(struct work_struct *work)
 	mutex_lock(&dwc->mutex);
 	spin_lock_irqsave(&dwc->lock, flags);
 	desired_dr_role = dwc->desired_dr_role;
+	if (mux)
+		mux_port = mux->desired_port;
 	spin_unlock_irqrestore(&dwc->lock, flags);
 
 	pm_runtime_get_sync(dwc->dev);
@@ -184,7 +189,8 @@ static void __dwc3_set_mode(struct work_struct *work)
 	if (!desired_dr_role)
 		goto out;
 
-	if (desired_dr_role == dwc->current_dr_role)
+	if (desired_dr_role == dwc->current_dr_role &&
+	    (!mux || mux_port == mux->active_port))
 		goto out;
 
 	if (desired_dr_role == DWC3_GCTL_PRTCAP_OTG && dwc->edev)
@@ -233,6 +239,27 @@ static void __dwc3_set_mode(struct work_struct *work)
 		dwc3_writel(dwc, DWC3_GCTL, reg);
 	}
 
+	/*
+	 * The previous host or gadget instance has been torn down above, so the
+	 * shared High Speed data path can now be moved to the requesting
+	 * connector.  A failure here must not abort the mode change half-way:
+	 * the controller would be left with a torn-down role and a stale
+	 * current_dr_role, which the next request would tear down a second time.
+	 */
+	if (mux && mux_port != mux->active_port) {
+		ret = gpiod_direction_output(mux->select, mux_port);
+		if (ret) {
+			WRITE_ONCE(mux->error, ret);
+			dev_err_ratelimited(dwc->dev,
+					    "failed to select USB port %d: %d\n",
+					    mux_port, ret);
+		} else {
+			spin_lock_irqsave(&dwc->lock, flags);
+			mux->active_port = mux_port;
+			spin_unlock_irqrestore(&dwc->lock, flags);
+		}
+	}
+
 	spin_lock_irqsave(&dwc->lock, flags);
 
 	dwc3_set_prtcap(dwc, desired_dr_role, false);
diff --git a/drivers/usb/dwc3/core.h b/drivers/usb/dwc3/core.h
index f3dabb3..1238376 100644
--- a/drivers/usb/dwc3/core.h
+++ b/drivers/usb/dwc3/core.h
@@ -1008,6 +1008,43 @@ struct dwc3_glue_ops {
 	void	(*pre_run_stop)(struct dwc3 *dwc, bool is_on);
 };
 
+/* Per-connector state of the optional shared-HS-data-bus mux. */
+struct dwc3_role_port {
+	struct dwc3		*dwc;
+	struct usb_role_switch	*sw;
+	struct fwnode_handle	*fwnode;
+	enum usb_role		role;
+	unsigned int		state;
+	/* request order, see dwc3_role_mux.seq */
+	u64			seq;
+};
+
+/**
+ * struct dwc3_role_mux - optional data-path mux between two connectors
+ * @select: GPIO selecting which connector drives the shared HS data lines
+ * @lock: serialises role requests coming from both connectors
+ * @ports: per-connector state, indexed by the endpoint's "mux-state"
+ * @desired_port: index of the connector that currently wins
+ * @active_port: index of the connector currently driving the data lines
+ * @seq: monotonic request counter used to order pending requests
+ * @error: sticky data-path switch error, reported to the role-switch users
+ *
+ * Two connectors can share the High Speed data lines of one controller while
+ * keeping their Super Speed lanes separate.  Each connector then needs its own
+ * role-switch input, and the shared data path has to be moved before the
+ * controller is reinitialised for the requested role.
+ */
+struct dwc3_role_mux {
+	struct gpio_desc	*select;
+	/* serialises role requests coming from both connectors */
+	struct mutex		lock;
+	struct dwc3_role_port	ports[2];
+	int			desired_port;
+	int			active_port;
+	u64			seq;
+	int			error;
+};
+
 /**
  * struct dwc3 - representation of our controller
  * @drd_work: workqueue used for role swapping
@@ -1064,6 +1101,7 @@ struct dwc3_glue_ops {
  *		- USBPHY_INTERFACE_MODE_UTMI
  *		- USBPHY_INTERFACE_MODE_UTMIW
  * @role_sw: usb_role_switch handle
+ * @role_mux: optional shared data-path mux plus per-connector role switches
  * @role_switch_default_mode: default operation mode of controller while
  *			usb role is USB_ROLE_NONE.
  * @usb_psy: pointer to power supply interface.
@@ -1265,6 +1303,7 @@ struct dwc3 {
 	struct notifier_block	edev_nb;
 	enum usb_phy_interface	hsphy_mode;
 	struct usb_role_switch	*role_sw;
+	struct dwc3_role_mux	*role_mux;
 	enum usb_dr_mode	role_switch_default_mode;
 
 	struct power_supply	*usb_psy;
diff --git a/drivers/usb/dwc3/debugfs.c b/drivers/usb/dwc3/debugfs.c
index 65d909e..0d42f10 100644
--- a/drivers/usb/dwc3/debugfs.c
+++ b/drivers/usb/dwc3/debugfs.c
@@ -438,6 +438,10 @@ static ssize_t dwc3_mode_write(struct file *file,
 	u32			mode = 0;
 	char			buf[32];
 
+	/* The connector providers own both the role and the data-path selection. */
+	if (dwc->role_mux)
+		return -EBUSY;
+
 	if (copy_from_user(&buf, ubuf, min_t(size_t, sizeof(buf) - 1, count)))
 		return -EFAULT;
 
diff --git a/drivers/usb/dwc3/drd.c b/drivers/usb/dwc3/drd.c
index f3e37d3..20821d6 100644
--- a/drivers/usb/dwc3/drd.c
+++ b/drivers/usb/dwc3/drd.c
@@ -8,6 +8,7 @@
  */
 
 #include <linux/extcon.h>
+#include <linux/gpio/consumer.h>
 #include <linux/of_platform.h>
 #include <linux/platform_device.h>
 #include <linux/property.h>
@@ -501,10 +502,219 @@ static enum usb_role dwc3_usb_role_switch_get(struct usb_role_switch *sw)
 	return role;
 }
 
+/*
+ * Index of the connector that should own the shared data path: the oldest
+ * pending request wins, and the last selection is kept when neither connector
+ * requests a role.  Requests are ordered by a monotonic counter rather than by
+ * a timestamp, so a wrapped counter cannot be mistaken for an old request.
+ */
+static int dwc3_role_mux_pick(struct dwc3_role_mux *mux)
+{
+	int sel = mux->desired_port;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(mux->ports); i++) {
+		if (mux->ports[i].role == USB_ROLE_NONE)
+			continue;
+		if (mux->ports[sel].role == USB_ROLE_NONE ||
+		    mux->ports[i].seq < mux->ports[sel].seq)
+			sel = i;
+	}
+
+	return sel;
+}
+
+static u32 dwc3_role_mux_mode(struct dwc3 *dwc, enum usb_role role)
+{
+	if (role == USB_ROLE_HOST)
+		return DWC3_GCTL_PRTCAP_HOST;
+	if (role == USB_ROLE_DEVICE)
+		return DWC3_GCTL_PRTCAP_DEVICE;
+	if (dwc->role_switch_default_mode == USB_DR_MODE_HOST)
+		return DWC3_GCTL_PRTCAP_HOST;
+
+	return DWC3_GCTL_PRTCAP_DEVICE;
+}
+
+static int dwc3_mux_role_set(struct usb_role_switch *sw, enum usb_role role)
+{
+	struct dwc3_role_port *port = usb_role_switch_get_drvdata(sw);
+	struct dwc3 *dwc = port->dwc;
+	struct dwc3_role_mux *mux = dwc->role_mux;
+	unsigned long flags;
+	enum usb_role selected_role;
+	int selected, ret;
+
+	if (role > USB_ROLE_DEVICE)
+		return -EINVAL;
+
+	/*
+	 * The request order and the mode request it implies are decided under one
+	 * lock, so a second connector cannot change desired_port between the
+	 * selection and the dwc3_set_mode() call that consumes it.
+	 */
+	mutex_lock(&mux->lock);
+	spin_lock_irqsave(&dwc->lock, flags);
+	/*
+	 * A role change between two non-NONE roles of the same connector must not
+	 * give that connector a new request ticket: it would otherwise steal the
+	 * data path from the connector that asked first.
+	 */
+	if (port->role == USB_ROLE_NONE && role != USB_ROLE_NONE)
+		port->seq = ++mux->seq;
+	port->role = role;
+	selected = dwc3_role_mux_pick(mux);
+	mux->desired_port = selected;
+	selected_role = mux->ports[selected].role;
+	/*
+	 * Same asynchronous semantics as the single-connector role switch: the
+	 * data path and the controller are reprogrammed from __dwc3_set_mode().
+	 * dwc3_set_mode() only queues that work, so the mux lock may be held.
+	 */
+	dwc3_set_mode(dwc, dwc3_role_mux_mode(dwc, selected_role));
+	ret = mux->error;
+	mux->error = 0;
+	spin_unlock_irqrestore(&dwc->lock, flags);
+	mutex_unlock(&mux->lock);
+
+	return ret;
+}
+
+static enum usb_role dwc3_mux_role_get(struct usb_role_switch *sw)
+{
+	struct dwc3_role_port *port = usb_role_switch_get_drvdata(sw);
+	struct dwc3 *dwc = port->dwc;
+	struct dwc3_role_mux *mux = dwc->role_mux;
+	unsigned long flags;
+	enum usb_role role = USB_ROLE_NONE;
+
+	/*
+	 * A connector only reports its own role while it owns the shared data
+	 * path, so the connector drivers never fight over the controller.
+	 */
+	spin_lock_irqsave(&dwc->lock, flags);
+	if (mux->active_port == port->state)
+		role = port->role;
+	spin_unlock_irqrestore(&dwc->lock, flags);
+
+	return role;
+}
+
+static void dwc3_role_mux_exit(struct dwc3 *dwc)
+{
+	struct dwc3_role_mux *mux = dwc->role_mux;
+	unsigned int i;
+
+	if (!mux)
+		return;
+
+	for (i = 0; i < ARRAY_SIZE(mux->ports); i++) {
+		if (mux->ports[i].sw)
+			usb_role_switch_unregister(mux->ports[i].sw);
+		mux->ports[i].sw = NULL;
+		fwnode_handle_put(mux->ports[i].fwnode);
+		mux->ports[i].fwnode = NULL;
+	}
+}
+
+/* Returns 1 when the optional two-connector mux has been set up. */
+static int dwc3_role_mux_init(struct dwc3 *dwc, u32 mode)
+{
+	struct usb_role_switch_desc desc = {};
+	struct dwc3_role_mux *mux;
+	struct fwnode_handle *ep;
+	struct gpio_desc *select;
+	char name[64];
+	u32 state;
+	int ret, i;
+
+	select = devm_gpiod_get_optional(dwc->dev, "port-select", GPIOD_ASIS);
+	if (IS_ERR(select))
+		return PTR_ERR(select);
+	if (!select)
+		return 0;
+
+	mux = devm_kzalloc(dwc->dev, sizeof(*mux), GFP_KERNEL);
+	if (!mux)
+		return -ENOMEM;
+
+	ret = gpiod_get_value_cansleep(select);
+	if (ret < 0)
+		return ret;
+
+	mux->select = select;
+	mux->desired_port = ret;
+	mux->active_port = -1;
+	mutex_init(&mux->lock);
+
+	fwnode_graph_for_each_endpoint(dev_fwnode(dwc->dev), ep) {
+		if (!fwnode_property_present(ep, "usb-role-switch"))
+			continue;
+
+		ret = fwnode_property_read_u32(ep, "mux-state", &state);
+		if (ret || state >= ARRAY_SIZE(mux->ports)) {
+			ret = -EINVAL;
+			goto err_put_ep;
+		}
+		if (mux->ports[state].fwnode) {
+			ret = -EINVAL;
+			goto err_put_ep;
+		}
+
+		mux->ports[state].fwnode = fwnode_handle_get(ep);
+		mux->ports[state].state = state;
+		mux->ports[state].dwc = dwc;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(mux->ports); i++) {
+		if (!mux->ports[i].fwnode) {
+			ret = -EINVAL;
+			goto err;
+		}
+	}
+
+	dwc->role_mux = mux;
+
+	for (i = 0; i < ARRAY_SIZE(mux->ports); i++) {
+		snprintf(name, sizeof(name), "%s-port%u", dev_name(dwc->dev), i);
+		desc.name = name;
+		desc.fwnode = mux->ports[i].fwnode;
+		desc.set = dwc3_mux_role_set;
+		desc.get = dwc3_mux_role_get;
+		desc.driver_data = &mux->ports[i];
+		mux->ports[i].sw = usb_role_switch_register(dwc->dev, &desc);
+		if (IS_ERR(mux->ports[i].sw)) {
+			ret = PTR_ERR(mux->ports[i].sw);
+			mux->ports[i].sw = NULL;
+			goto err;
+		}
+	}
+
+	/*
+	 * The controller is only started in its default role once both switches
+	 * are registered: before that a connector driver cannot request a role,
+	 * so nothing can race with this initial mode request.
+	 */
+	dwc3_set_mode(dwc, mode);
+
+	return 1;
+
+err:
+	dwc3_role_mux_exit(dwc);
+	dwc->role_mux = NULL;
+	/* A mode request queued before the failure must not run on freed state. */
+	cancel_work_sync(&dwc->drd_work);
+	return ret;
+err_put_ep:
+	fwnode_handle_put(ep);
+	goto err;
+}
+
 static int dwc3_setup_role_switch(struct dwc3 *dwc)
 {
 	struct usb_role_switch_desc dwc3_role_switch = {NULL};
 	u32 mode;
+	int ret;
 
 	dwc->role_switch_default_mode = usb_get_role_switch_default_mode(dwc->dev);
 	if (dwc->role_switch_default_mode == USB_DR_MODE_HOST) {
@@ -513,6 +723,11 @@ static int dwc3_setup_role_switch(struct dwc3 *dwc)
 		dwc->role_switch_default_mode = USB_DR_MODE_PERIPHERAL;
 		mode = DWC3_GCTL_PRTCAP_DEVICE;
 	}
+
+	ret = dwc3_role_mux_init(dwc, mode);
+	if (ret)
+		return ret < 0 ? ret : 0;
+
 	dwc3_set_mode(dwc, mode);
 
 	dwc3_role_switch.fwnode = dev_fwnode(dwc->dev);
@@ -526,7 +741,7 @@ static int dwc3_setup_role_switch(struct dwc3 *dwc)
 
 	if (dwc->dev->of_node) {
 		/* populate connector entry */
-		int ret = devm_of_platform_populate(dwc->dev);
+		ret = devm_of_platform_populate(dwc->dev);
 
 		if (ret) {
 			usb_role_switch_unregister(dwc->role_sw);
@@ -541,6 +756,7 @@ static int dwc3_setup_role_switch(struct dwc3 *dwc)
 #else
 #define ROLE_SWITCH 0
 #define dwc3_setup_role_switch(x) 0
+static inline void dwc3_role_mux_exit(struct dwc3 *dwc) {}
 #endif
 
 int dwc3_drd_init(struct dwc3 *dwc)
@@ -597,6 +813,7 @@ void dwc3_drd_exit(struct dwc3 *dwc)
 {
 	unsigned long flags;
 
+	dwc3_role_mux_exit(dwc);
 	if (dwc->role_sw)
 		usb_role_switch_unregister(dwc->role_sw);
 
-- 
2.53.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v4 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus
  2026-10-26 12:32 ` [PATCH v4 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus Qianhui Xu
@ 2026-11-02  8:13   ` Krzysztof Kozlowski
  0 siblings, 0 replies; 5+ messages in thread
From: Krzysztof Kozlowski @ 2026-11-02  8:13 UTC (permalink / raw)
  To: Qianhui Xu
  Cc: Greg Kroah-Hartman, Thinh Nguyen, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, linux-usb, devicetree,
	linux-arm-msm, linux-kernel

On Mon, Oct 26, 2026 at 08:32:09PM +0800, Qianhui Xu wrote:
> Two connectors can share the High Speed D+/D- pair of a single DWC3
> controller, with a GPIO selecting which one is connected.  Super Speed
> lanes stay per connector.

Unanswered Sashiko comments plus some confusion with duplicated posting
of v5 without the rest, dropping.

Best regards,
Krzysztof


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-11-02  8:13 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-26 12:32 [PATCH v4 0/3] usb: dwc3: support two connectors sharing one HS data bus Qianhui Xu
2026-10-26 12:32 ` [PATCH v4 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus Qianhui Xu
2026-11-02  8:13   ` Krzysztof Kozlowski
2026-10-26 12:32 ` [PATCH v4 2/3] usb: roles: look up role switches attached to a graph endpoint Qianhui Xu
2026-10-26 12:32 ` [PATCH v4 3/3] usb: dwc3: support two connectors sharing one HS data bus Qianhui Xu

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®