From 492a61eee210d1bf60aa8c5c367c222572798bc1 Mon Sep 17 00:00:00 2001 From: Douglas Teles Date: Wed, 1 Jul 2026 20:32:04 -0300 Subject: [PATCH] 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 --- ...-inno-usb2-add-usb-switch-gpio-sysfs.patch | 238 ++++++++++++++++++ .../packages/archr/sources/scripts/wifictl | 23 ++ 2 files changed, 261 insertions(+) create mode 100644 projects/ArchR/devices/RK3326/patches/linux/0026-phy-rockchip-inno-usb2-add-usb-switch-gpio-sysfs.patch diff --git a/projects/ArchR/devices/RK3326/patches/linux/0026-phy-rockchip-inno-usb2-add-usb-switch-gpio-sysfs.patch b/projects/ArchR/devices/RK3326/patches/linux/0026-phy-rockchip-inno-usb2-add-usb-switch-gpio-sysfs.patch new file mode 100644 index 0000000000..bd3d8974c8 --- /dev/null +++ b/projects/ArchR/devices/RK3326/patches/linux/0026-phy-rockchip-inno-usb2-add-usb-switch-gpio-sysfs.patch @@ -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 + #include + #include ++#include + #include + #include + #include +@@ -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: diff --git a/projects/ArchR/packages/archr/sources/scripts/wifictl b/projects/ArchR/packages/archr/sources/scripts/wifictl index cc78c2c3f3..77e2b4f43f 100755 --- a/projects/ArchR/packages/archr/sources/scripts/wifictl +++ b/projects/ArchR/packages/archr/sources/scripts/wifictl @@ -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" ] && \