* [PATCH v8 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus
2026-11-06 11:42 [PATCH v8 0/3] usb: dwc3: two Type-C connectors sharing one HS data bus Qianhui Xu
@ 2026-11-06 11:42 ` Qianhui Xu
2026-11-06 11:42 ` [PATCH v8 2/3] usb: roles: look up role switches attached to a graph endpoint Qianhui Xu
2026-11-06 11:42 ` [PATCH v8 3/3] usb: dwc3: support two connectors sharing one HS data bus Qianhui Xu
2 siblings, 0 replies; 6+ messages in thread
From: Qianhui Xu @ 2026-11-06 11:42 UTC (permalink / raw)
To: gregkh, Thinh.Nguyen, robh, krzk+dt, conor+dt
Cc: linux-usb, devicetree, linux-arm-msm, linux-kernel
Two connectors can share the High Speed D+/D- pair of one DWC3 controller,
with a GPIO selecting which of them is connected, while their Super Speed
lanes stay separate.
Describe that in snps,dwc3-common.yaml:
- "port-select-gpios" selects which connector drives the shared bus;
- each connector is a port of the controller's "ports" node, "port@0"
and "port@2", with one endpoint per port, so that "ports" keeps
describing the controller's own buses as the graph bindings expect
(port@1 is the Super Speed lane of the PHY, described by
snps,dwc3.yaml);
- the endpoint of a connector carries "mux-state", the level of the
port-select GPIO that reaches it, and "usb-role-switch", declaring
that this connector has its own role-switch input.
The controller node must carry "usb-role-switch" as well, and dr_mode is
required to be "otg": dwc3_get_dr_mode() otherwise forces dr_mode back to
peripheral, dwc3_get_extcon() skips the role-switch branch and
dwc3_drd_init() never sets the mux up, so a device tree that passed the
schema would not describe a device tree the driver can act on.
The two connectors must claim opposite levels of the GPIO, which is
expressed as one choice between the two assignments, and endpoint names
use the pattern of graph.yaml so that an unnumbered or third endpoint is
rejected instead of escaping "unevaluatedProperties: false". When
port-select-gpios is absent the endpoint properties are rejected, so
existing single-connector descriptions are unaffected.
Signed-off-by: Qianhui Xu <xiaqinghuan@mail.ustc.edu.cn>
---
.../bindings/usb/snps,dwc3-common.yaml | 116 +++++++++++++++++-
1 file changed, 114 insertions(+), 2 deletions(-)
diff --git a/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml b/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml
index 6c0b8b6538..0be2a6e83e 100644
--- a/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml
+++ b/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml
@@ -13,6 +13,40 @@ 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. The two connectors of
+ one controller must use different levels; the driver rejects a
+ device tree where two connectors claim the same one.
+
+ dependentRequired:
+ usb-role-switch: [mux-state]
+ mux-state: [usb-role-switch]
+
+ hs-port:
+ $ref: /schemas/graph.yaml#/$defs/port-base
+ unevaluatedProperties: false
+
+ patternProperties:
+ '^endpoint(@[0-9a-f]+)?$':
+ $ref: '#/$defs/hs-endpoint'
+
allOf:
- $ref: usb-drd.yaml#
- if:
@@ -26,8 +60,87 @@ allOf:
$ref: usb.yaml#
else:
$ref: usb-xhci.yaml#
+ - if:
+ required: [port-select-gpios]
+ then:
+ # The mux path is only taken when the controller itself carries
+ # "usb-role-switch": dwc3_get_dr_mode() forces dr_mode back to
+ # peripheral, dwc3_get_extcon() skips the role-switch branch and
+ # dwc3_drd_init() never calls dwc3_role_mux_init() without it.
+ required: [dr_mode, ports, usb-role-switch]
+ properties:
+ dr_mode:
+ const: otg
+ ports:
+ # Each connector is one port with a single endpoint: the mux-state
+ # property on that endpoint says which level of the port-select GPIO
+ # reaches it, so two connectors are two ports, not two endpoints of
+ # one port. port@1 is the SuperSpeed lane of the PHY and is already
+ # described by snps,dwc3.yaml, so the second High Speed connector
+ # lands on port@2.
+ required: [port@0, port@2]
+ properties:
+ port@0:
+ $ref: '#/$defs/hs-port'
+ port@2:
+ $ref: '#/$defs/hs-port'
+
+ # The two connectors must claim opposite levels of the port-select
+ # GPIO; requiring both ports in every branch is what makes the two
+ # cases mutually exclusive for oneOf.
+ oneOf:
+ - required: [port@0, port@2]
+ properties:
+ port@0:
+ $ref: '#/$defs/hs-port'
+ properties:
+ endpoint:
+ properties:
+ mux-state:
+ const: 0
+ port@2:
+ $ref: '#/$defs/hs-port'
+ properties:
+ endpoint:
+ properties:
+ mux-state:
+ const: 1
+ - required: [port@0, port@2]
+ properties:
+ port@0:
+ $ref: '#/$defs/hs-port'
+ properties:
+ endpoint:
+ properties:
+ mux-state:
+ const: 1
+ port@2:
+ $ref: '#/$defs/hs-port'
+ properties:
+ endpoint:
+ properties:
+ mux-state:
+ const: 0
+ else:
+ properties:
+ ports:
+ properties:
+ port@0:
+ patternProperties:
+ '^endpoint(@[0-9a-f]+)?$':
+ properties:
+ usb-role-switch: false
+ mux-state: false
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 "port@0" or "port@2",
+ and the endpoint of that port carries the connector's own role-switch
+ input and the "mux-state" level selecting it.
+
extcon:
maxItems: 1
deprecated: true
@@ -411,7 +524,7 @@ properties:
properties:
port@0:
- $ref: /schemas/graph.yaml#/properties/port
+ $ref: '#/$defs/hs-port'
description: High Speed (HS) data bus.
port@1:
@@ -429,4 +542,3 @@ required:
additionalProperties: true
...
-
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH v8 3/3] usb: dwc3: support two connectors sharing one HS data bus
2026-11-06 11:42 [PATCH v8 0/3] usb: dwc3: two Type-C connectors sharing one HS data bus Qianhui Xu
2026-11-06 11:42 ` [PATCH v8 1/3] dt-bindings: usb: snps,dwc3: describe two connectors sharing one HS bus Qianhui Xu
2026-11-06 11:42 ` [PATCH v8 2/3] usb: roles: look up role switches attached to a graph endpoint Qianhui Xu
@ 2026-11-06 11:42 ` Qianhui Xu
2026-11-06 11:52 ` sashiko-bot
2 siblings, 1 reply; 6+ messages in thread
From: Qianhui Xu @ 2026-11-06 11:42 UTC (permalink / raw)
To: gregkh, Thinh.Nguyen, robh, krzk+dt, conor+dt
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 by the mux lock, 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 same-role request that moves the data path to
the other connector reinitialises the controller as well and starts it
from the same SUSPHY state as a role change.
The data-path selection and the mode request it implies are published
together under the controller lock, so the mode worker can never pair the
new connector with the role of the previous request. Publishing happens
after dwc3_pre_set_role(), which may sleep, so the worker cannot start
before the glue layer has been notified. A request that arrives while the
worker is already running is picked up at the end of that run, because
queue_work() does not re-queue a running work item.
The switches become usable only once both are registered and the initial
mode request has been issued; a request arriving before that is refused
instead of racing with the initial mode request. Teardown refuses further
requests before unregistering the switches, and only then drains the mode
worker, so a failed setup cannot leave a mode request queued behind the
drain or dereference a mux that is no longer published.
A connector reads back the role it requested, not the role currently on the
bus: hiding a pending request behind the active port would leave the
connector driver unable to release it, and the stale request would keep
winning the arbitration after unplug. The glue layer is notified with
dwc3_pre_set_role() before dwc3_set_mode_port(), outside spinlocks, as the
single-connector path does.
A failed port-select write is logged and counted per connector; it cannot
leave a torn-down role behind, because __dwc3_set_mode() is the only place
that programs hardware and it never aborts half-way.
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>
---
drivers/usb/dwc3/core.c | 93 +++++++++++-
drivers/usb/dwc3/core.h | 43 ++++++
drivers/usb/dwc3/debugfs.c | 4 +
drivers/usb/dwc3/drd.c | 287 ++++++++++++++++++++++++++++++++++++-
4 files changed, 424 insertions(+), 3 deletions(-)
diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c
index fd5c2cd36c..45220a036f 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,10 @@ 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;
+ bool pending, port_switch;
+ int mux_port = 0;
int ret;
u32 reg;
u32 desired_dr_role;
@@ -174,6 +178,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 +190,14 @@ static void __dwc3_set_mode(struct work_struct *work)
if (!desired_dr_role)
goto out;
- if (desired_dr_role == dwc->current_dr_role)
+ /*
+ * Moving the shared data path to the other connector is a mode switch even
+ * when the role does not change: the running host or gadget instance has to
+ * be torn down while its own data lines are still selected.
+ */
+ port_switch = mux && mux_port != mux->active_port;
+
+ if (desired_dr_role == dwc->current_dr_role && !port_switch)
goto out;
if (desired_dr_role == DWC3_GCTL_PRTCAP_OTG && dwc->edev)
@@ -233,8 +246,40 @@ 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 (port_switch) {
+ ret = gpiod_direction_output(mux->select, mux_port);
+ if (ret)
+ dev_err_ratelimited(dwc->dev,
+ "failed to select USB port %d: %d\n",
+ mux_port, ret);
+
+ spin_lock_irqsave(&dwc->lock, flags);
+ if (ret)
+ mux->ports[mux_port].errors++;
+ else
+ mux->active_port = mux_port;
+ spin_unlock_irqrestore(&dwc->lock, flags);
+ }
+
spin_lock_irqsave(&dwc->lock, flags);
+ /*
+ * dwc3_set_prtcap() only clears GUSB3PIPECTL.SUSPENDENABLE and
+ * GUSB2PHYCFG.SUSPHY when PRTCAP actually changes, but a same-role port
+ * switch reinitialises the controller too and must start from the same
+ * state: the instance created below would otherwise inherit the SUSPHY
+ * setting of the one torn down above.
+ */
+ if (port_switch)
+ dwc3_enable_susphy(dwc, false);
+
dwc3_set_prtcap(dwc, desired_dr_role, false);
spin_unlock_irqrestore(&dwc->lock, flags);
@@ -283,24 +328,68 @@ static void __dwc3_set_mode(struct work_struct *work)
}
out:
+ /*
+ * A request that arrived while this work was running did not re-queue it:
+ * queue_work() returns false for a work item that is already running. Pick
+ * such a request up here instead of waiting for the next connector event.
+ * The pair is compared with the snapshot taken above, so a data-path switch
+ * that failed is not retried in a loop.
+ */
+ if (mux) {
+ spin_lock_irqsave(&dwc->lock, flags);
+ pending = dwc->desired_dr_role != desired_dr_role ||
+ mux->desired_port != mux_port;
+ spin_unlock_irqrestore(&dwc->lock, flags);
+ if (pending)
+ queue_work(system_freezable_wq, &dwc->drd_work);
+ }
+
pm_runtime_put_autosuspend(dwc->dev);
mutex_unlock(&dwc->mutex);
}
-void dwc3_set_mode(struct dwc3 *dwc, u32 mode)
+static void dwc3_queue_mode(struct dwc3 *dwc, u32 mode, int port)
{
unsigned long flags;
if (dwc->dr_mode != USB_DR_MODE_OTG)
return;
+ /*
+ * The connector that owns the shared data path is published together with
+ * the mode request: __dwc3_set_mode() snapshots both under dwc->lock, and
+ * a torn pair would move the data path for the connector that asked last
+ * while bringing up the role requested by the one before it.
+ */
spin_lock_irqsave(&dwc->lock, flags);
dwc->desired_dr_role = mode;
+ if (dwc->role_mux && port >= 0)
+ dwc->role_mux->desired_port = port;
spin_unlock_irqrestore(&dwc->lock, flags);
queue_work(system_freezable_wq, &dwc->drd_work);
}
+void dwc3_set_mode(struct dwc3 *dwc, u32 mode)
+{
+ dwc3_queue_mode(dwc, mode, -1);
+}
+
+/**
+ * dwc3_set_mode_port - request a mode and the connector that owns the data path
+ * @dwc: pointer to our controller context structure
+ * @mode: mode to be requested
+ * @port: index of the connector that should drive the shared data path
+ *
+ * Same as dwc3_set_mode(), except that the data-path selection is published
+ * atomically with the mode request, so the mode worker never pairs the new
+ * connector with the previous role.
+ */
+void dwc3_set_mode_port(struct dwc3 *dwc, u32 mode, int port)
+{
+ dwc3_queue_mode(dwc, mode, port);
+}
+
u32 dwc3_core_fifo_space(struct dwc3_ep *dep, u8 type)
{
struct dwc3 *dwc = dep->dwc;
diff --git a/drivers/usb/dwc3/core.h b/drivers/usb/dwc3/core.h
index f3dabb37f8..1beef73212 100644
--- a/drivers/usb/dwc3/core.h
+++ b/drivers/usb/dwc3/core.h
@@ -1008,6 +1008,46 @@ 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;
+ /* data-path switch failures, for diagnostics only */
+ unsigned int errors;
+};
+
+/**
+ * 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
+ * @ready: both switches registered and the initial mode request issued
+ * @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
+ *
+ * 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;
+ /* false until both switches are registered and the mode was requested */
+ bool ready;
+ struct dwc3_role_port ports[2];
+ int desired_port;
+ int active_port;
+ u64 seq;
+};
+
/**
* struct dwc3 - representation of our controller
* @drd_work: workqueue used for role swapping
@@ -1064,6 +1104,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 +1306,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;
@@ -1614,6 +1656,7 @@ struct dwc3_gadget_ep_cmd_params {
/* prototypes */
void dwc3_set_prtcap(struct dwc3 *dwc, u32 mode, bool ignore_susphy);
void dwc3_set_mode(struct dwc3 *dwc, u32 mode);
+void dwc3_set_mode_port(struct dwc3 *dwc, u32 mode, int port);
u32 dwc3_core_fifo_space(struct dwc3_ep *dep, u8 type);
#define DWC3_IP_IS(_ip) \
diff --git a/drivers/usb/dwc3/debugfs.c b/drivers/usb/dwc3/debugfs.c
index 01ebd73198..82e7a334b1 100644
--- a/drivers/usb/dwc3/debugfs.c
+++ b/drivers/usb/dwc3/debugfs.c
@@ -434,6 +434,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 f3e37d3836..22dc8ad084 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,287 @@ 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;
+
+ if (role > USB_ROLE_DEVICE)
+ return -EINVAL;
+
+ mutex_lock(&mux->lock);
+
+ /*
+ * The switches are registered before the initial mode request is issued and
+ * before mux->ready is set, so a connector can find its switch while the
+ * mux is still being set up. Refusing the request keeps it from racing
+ * with the initial mode request and from queueing work that the error path
+ * would then have to undo.
+ */
+ if (!mux->ready) {
+ mutex_unlock(&mux->lock);
+ return -ENODEV;
+ }
+
+ 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);
+ selected_role = mux->ports[selected].role;
+ spin_unlock_irqrestore(&dwc->lock, flags);
+
+ /*
+ * Notify the glue layer before touching the controller, in the same order
+ * as the single-connector path. It may sleep (the Qualcomm glue calls
+ * pm_runtime_resume_and_get()), so it runs outside of dwc->lock -
+ * dwc3_set_mode() takes that lock itself and a recursive take deadlocks
+ * the controller on the very first role change.
+ *
+ * Pairing with the selection above is provided by mux->lock: a second
+ * connector blocks in mutex_lock() until this request has been issued.
+ *
+ * Same asynchronous semantics as the single-connector role switch: the
+ * data path and the controller are reprogrammed from __dwc3_set_mode().
+ */
+ dwc3_pre_set_role(dwc, selected_role);
+ /*
+ * The selected connector and the mode are published by one call, under
+ * dwc->lock, so __dwc3_set_mode() cannot observe the new connector together
+ * with the role of the previous request. Publishing happens after
+ * dwc3_pre_set_role() so the work cannot start before the glue layer has
+ * been notified.
+ */
+ dwc3_set_mode_port(dwc, dwc3_role_mux_mode(dwc, selected_role), selected);
+
+ mutex_unlock(&mux->lock);
+
+ /*
+ * A data-path switch failure is reported through the console and counted
+ * per connector; it is not returned here. This call has already updated
+ * the request state, so a stale error from another connector must not be
+ * reported as the result of this (successful) request.
+ */
+ return 0;
+}
+
+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;
+ unsigned long flags;
+ enum usb_role role;
+
+ /*
+ * Report the role this connector itself requested, whether or not it owns
+ * the shared data path right now. Hiding a pending request behind
+ * active_port makes connector drivers read back USB_ROLE_NONE, so they
+ * never send the set_role(USB_ROLE_NONE) that would release it; the stale
+ * request then keeps winning dwc3_role_mux_pick() after the connector has
+ * been unplugged and starves the other one.
+ */
+ spin_lock_irqsave(&dwc->lock, flags);
+ 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;
+
+ /*
+ * Refuse new requests first. dwc3_mux_role_set() takes this lock for its
+ * whole duration, so once it is held no request is in flight, and every
+ * later one returns before it can queue a mode request. The work itself
+ * is drained by the caller, which is the only way to keep a request from
+ * being queued after the drain.
+ */
+ mutex_lock(&mux->lock);
+ mux->ready = false;
+ mutex_unlock(&mux->lock);
+
+ 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);
+
+ /*
+ * Publish the mux before the first endpoint reference is taken:
+ * dwc3_role_mux_exit() is a no-op while dwc->role_mux is NULL, so every
+ * error path below would otherwise leak the fwnode references already
+ * acquired.
+ */
+ dwc->role_mux = mux;
+
+ 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;
+ }
+ }
+
+ /*
+ * Registering the switches makes them reachable from the connector
+ * drivers, so the initial mode request has to be issued inside the same
+ * critical section: a set() arriving in between would be silently
+ * overwritten by it while port->role keeps the requested value.
+ * dwc3_mux_role_set() serialises on the same lock.
+ */
+ mutex_lock(&mux->lock);
+
+ 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;
+ mutex_unlock(&mux->lock);
+ 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);
+ mux->ready = true;
+
+ mutex_unlock(&mux->lock);
+
+ return 1;
+
+err:
+ /*
+ * Unregister the switches before draining the work: dwc3_role_mux_exit()
+ * refuses every further request, and a request that was already past that
+ * check has queued its work by the time the lock is released, so the drain
+ * below cannot miss it. The reverse order would let a blocked request
+ * queue the work after the drain and run it on a half-torn-down mux.
+ * dwc->role_mux is left in place: it is devm-managed and freeing it here
+ * would only turn a late request into a NULL dereference.
+ */
+ dwc3_role_mux_exit(dwc);
+ 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 +791,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 +809,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 +824,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 +881,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);
^ permalink raw reply related [flat|nested] 6+ messages in thread