wifi: free the USB OTG port when WiFi is off (internal-WiFi mux boards)

Some clones (XiFan R36Max2 family) wire the internal USB WiFi module to
the same OTG data lines as the physical USB port through a hardware mux
gpio: WiFi and the port are mutually exclusive, and users report that on
the stock OS disabling WiFi brings the port back.

Port ROCKNIX's 0026 USB2 PHY patch, which reads switch-gpios from the
otg-port node and exposes usb_switch_gpio / usb_switch_ext in sysfs
(boot default: internal WiFi). ROCKNIX itself never toggles the mux from
userspace, so the physical port stays dead there; wire it to our WiFi
toggle instead: wifictl enable routes the mux to the internal module,
wifictl disable flips it to the external port and powers its vbus. The
sysfs writes are no-ops on boards without the mux, and the patch is
inert unless the generated overlay (USBMUX hardware variant on the site)
adds switch-gpios to the PHY.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Douglas Teles
2026-07-01 20:32:04 -03:00
parent 94d26a87fd
commit 492a61eee2
2 changed files with 261 additions and 0 deletions
@@ -0,0 +1,238 @@
diff --git a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c
index 1bbff11..xxxxxxx 100644
--- a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c 2025-12-07 05:25:03.000000000 +0800
+++ b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c 2026-03-24 08:32:09.772362168 +0800
@@ -12,6 +12,7 @@
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/gpio/consumer.h>
+#include <linux/regulator/consumer.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/module.h>
@@ -189,6 +190,8 @@
* @port_id: flag for otg port or host port.
* @suspended: phy suspended flag.
* @vbus_attached: otg device vbus status.
+ * @switch_gpio: GPIO descriptor for USB signal routing switch.
+ * @ext_supply: regulator for external USB device power supply.
* @host_disconnect: usb host disconnect status.
* @bvalid_irq: IRQ number assigned for vbus valid rise detection.
* @id_irq: IRQ number assigned for ID pin detection.
@@ -209,6 +212,8 @@
unsigned int port_id;
bool suspended;
bool vbus_attached;
+ struct gpio_desc *switch_gpio;
+ struct regulator *ext_supply;
bool host_disconnect;
int bvalid_irq;
int id_irq;
@@ -640,6 +645,24 @@
.owner = THIS_MODULE,
};
+/*
+ * Check if we should be in USB host mode.
+ * Returns true if:
+ * - EXTCON_USB_HOST is set, OR
+ * - switch_gpio exists and is low (internal device mode)
+ */
+static bool rockchip_usb2phy_is_host_mode(struct rockchip_usb2phy *rphy,
+ struct rockchip_usb2phy_port *rport)
+{
+ if (rphy->edev && extcon_get_state(rphy->edev, EXTCON_USB_HOST) > 0)
+ return true;
+
+ if (rport->switch_gpio && gpiod_get_value_cansleep(rport->switch_gpio) == 0)
+ return true;
+
+ return false;
+}
+
static void rockchip_usb2phy_otg_sm_work(struct work_struct *work)
{
struct rockchip_usb2phy_port *rport =
@@ -666,7 +689,7 @@
rockchip_usb2phy_power_off(rport->phy);
fallthrough;
case OTG_STATE_B_IDLE:
- if (extcon_get_state(rphy->edev, EXTCON_USB_HOST) > 0) {
+ if (rockchip_usb2phy_is_host_mode(rphy, rport)) {
dev_dbg(&rport->phy->dev, "usb otg host connect\n");
rport->state = OTG_STATE_A_HOST;
rockchip_usb2phy_power_on(rport->phy);
@@ -740,7 +763,7 @@
sch_work = true;
break;
case OTG_STATE_A_HOST:
- if (extcon_get_state(rphy->edev, EXTCON_USB_HOST) == 0) {
+ if (!rockchip_usb2phy_is_host_mode(rphy, rport)) {
dev_dbg(&rport->phy->dev, "usb otg host disconnect\n");
rport->state = OTG_STATE_B_IDLE;
rockchip_usb2phy_power_off(rport->phy);
@@ -1042,6 +1065,115 @@
return IRQ_HANDLED;
}
+/*
+ * Helper to find OTG port
+ */
+static struct rockchip_usb2phy_port *
+rockchip_usb2phy_find_switch_port(struct rockchip_usb2phy *rphy)
+{
+ unsigned int i;
+
+ for (i = 0; i < rphy->phy_cfg->num_ports; i++) {
+ if (rphy->ports[i].port_id == USB2PHY_PORT_OTG)
+ return &rphy->ports[i];
+ }
+ return NULL;
+}
+
+static ssize_t usb_switch_gpio_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct rockchip_usb2phy *rphy = dev_get_drvdata(dev);
+ struct rockchip_usb2phy_port *rport;
+
+ rport = rockchip_usb2phy_find_switch_port(rphy);
+ if (!rport || !rport->switch_gpio)
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%d\n",
+ gpiod_get_value_cansleep(rport->switch_gpio));
+}
+
+static ssize_t usb_switch_gpio_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct rockchip_usb2phy *rphy = dev_get_drvdata(dev);
+ struct rockchip_usb2phy_port *rport;
+ unsigned int val;
+ int ret;
+
+ rport = rockchip_usb2phy_find_switch_port(rphy);
+ if (!rport || !rport->switch_gpio)
+ return -ENOTSUPP;
+
+ ret = kstrtouint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ gpiod_set_value_cansleep(rport->switch_gpio, val ? 1 : 0);
+ dev_info(dev, "switch_gpio set to %d\n", val ? 1 : 0);
+
+ return count;
+}
+static DEVICE_ATTR_RW(usb_switch_gpio);
+
+static ssize_t usb_switch_ext_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct rockchip_usb2phy *rphy = dev_get_drvdata(dev);
+ struct rockchip_usb2phy_port *rport;
+
+ rport = rockchip_usb2phy_find_switch_port(rphy);
+ if (!rport || !rport->ext_supply)
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%d\n", regulator_is_enabled(rport->ext_supply));
+}
+
+static ssize_t usb_switch_ext_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct rockchip_usb2phy *rphy = dev_get_drvdata(dev);
+ struct rockchip_usb2phy_port *rport;
+ unsigned int val;
+ int ret;
+
+ rport = rockchip_usb2phy_find_switch_port(rphy);
+ if (!rport || !rport->ext_supply)
+ return -ENOTSUPP;
+
+ ret = kstrtouint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val)
+ ret = regulator_enable(rport->ext_supply);
+ else
+ ret = regulator_disable(rport->ext_supply);
+
+ if (ret) {
+ dev_err(dev, "failed to %s ext-supply: %d\n",
+ val ? "enable" : "disable", ret);
+ return ret;
+ }
+ dev_info(dev, "ext-supply %s\n", val ? "enabled" : "disabled");
+
+ return count;
+}
+static DEVICE_ATTR_RW(usb_switch_ext);
+
+static struct attribute *rockchip_usb2phy_switch_attrs[] = {
+ &dev_attr_usb_switch_gpio.attr,
+ &dev_attr_usb_switch_ext.attr,
+ NULL,
+};
+
+static const struct attribute_group rockchip_usb2phy_switch_group = {
+ .attrs = rockchip_usb2phy_switch_attrs,
+};
+
static irqreturn_t rockchip_usb2phy_id_irq(int irq, void *data)
{
struct rockchip_usb2phy_port *rport = data;
@@ -1281,6 +1413,34 @@
mutex_init(&rport->mutex);
+ /*
+ * Get optional switch GPIO for USB signal routing.
+ * Some devices use a GPIO to multiplex USB signals between
+ * an external USB port and an internal device (like WiFi).
+ * Default to internal device (GPIO low) on init.
+ */
+ rport->switch_gpio = devm_fwnode_gpiod_get(rphy->dev,
+ of_fwnode_handle(child_np),
+ "switch", GPIOD_OUT_LOW,
+ "usb-switch");
+ if (IS_ERR(rport->switch_gpio)) {
+ if (PTR_ERR(rport->switch_gpio) != -ENOENT)
+ dev_warn(rphy->dev, "failed to get switch GPIO: %ld\n",
+ PTR_ERR(rport->switch_gpio));
+ rport->switch_gpio = NULL;
+ } else if (rport->switch_gpio) {
+ gpiod_set_value_cansleep(rport->switch_gpio, 0);
+ dev_info(rphy->dev, "USB switch GPIO initialized, default to internal device\n");
+ }
+
+ rport->ext_supply = devm_regulator_get_optional(&rport->phy->dev, "ext");
+ if (IS_ERR(rport->ext_supply)) {
+ rport->ext_supply = NULL;
+ } else if (rport->ext_supply) {
+ regulator_enable(rport->ext_supply);
+ dev_info(rphy->dev, "ext-supply regulator initialized, default enabled\n");
+ }
+
rport->mode = of_usb_get_dr_mode_by_phy(child_np, -1);
if (rport->mode == USB_DR_MODE_HOST ||
rport->mode == USB_DR_MODE_UNKNOWN) {
@@ -1480,6 +1640,13 @@
}
}
+ /* Create sysfs for USB switch control */
+ if (rockchip_usb2phy_find_switch_port(rphy)) {
+ ret = sysfs_create_group(&dev->kobj, &rockchip_usb2phy_switch_group);
+ if (ret)
+ dev_warn(dev, "failed to create usb_switch sysfs: %d\n", ret);
+ }
+
return PTR_ERR_OR_ZERO(provider);
put_child:
@@ -225,12 +225,35 @@ has_ap_mode() {
fi
}
# Alguns clones têm o módulo WiFi USB interno dividindo as linhas de dados
# da OTG com a porta USB física através de um mux de hardware (patch 0026
# expõe o gpio como sysfs usb_switch_gpio: 0 = WiFi interno, 1 = porta
# externa; usb_switch_ext liga o vbus da porta). Seguir o toggle de WiFi,
# igual ao SO stock: desabilitar o WiFi libera a porta física pra dongles
# e acessórios. No-op em placas sem o mux.
set_usb_wifi_mux() {
local val="$1" f
for f in /sys/devices/platform/*/*usb2phy*/usb_switch_gpio \
/sys/devices/platform/*usb2phy*/usb_switch_gpio; do
[ -e "${f}" ] && echo "${val}" > "${f}" 2>/dev/null
done
for f in /sys/devices/platform/*/*usb2phy*/usb_switch_ext \
/sys/devices/platform/*usb2phy*/usb_switch_ext; do
if [ -e "${f}" ]; then
# porta externa ativa (mux=1) precisa do vbus dela ligado
if [ "${val}" = "1" ]; then echo 1 > "${f}" 2>/dev/null; else echo 0 > "${f}" 2>/dev/null; fi
fi
done
}
case "${1}" in
enable)
rfkill unblock wifi
set_usb_wifi_mux 0
;;
disable)
rfkill block wifi
set_usb_wifi_mux 1
;;
connect)
if [ "${WIFI_TYPE}" = "1" ] && \