| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Apple Silicon DWC3 Glue driver |
| * Copyright (C) The Asahi Linux Contributors |
| * |
| * Based on: |
| * - dwc3-qcom.c Copyright (c) 2018, The Linux Foundation. All rights reserved. |
| * - dwc3-of-simple.c Copyright (c) 2015 Texas Instruments Incorporated - https://www.ti.com |
| */ |
| |
| #include <linux/of.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/platform_device.h> |
| #include <linux/reset.h> |
| |
| #include "glue.h" |
| |
| /* |
| * This platform requires a very specific sequence of operations to bring up dwc3 and its USB3 PHY: |
| * |
| * 1) The PHY itself has to be brought up; for this we need to know the mode (USB3, |
| * USB3+DisplayPort, USB4, etc) and the lane orientation. This happens through typec_mux_set. |
| * 2) DWC3 has to be brought up but we must not touch the gadget area or start xhci yet. |
| * 3) The PHY bring-up has to be finalized and dwc3's PIPE interface has to be switched to the |
| * USB3 PHY, this is done inside phy_set_mode. |
| * 4) We can now initialize xhci or gadget mode. |
| * |
| * We can switch 1 and 2 but 3 has to happen after (1 and 2) and 4 has to happen after 3. |
| * |
| * And then to bring this all down again: |
| * |
| * 1) DWC3 has to exit host or gadget mode and must no longer touch those registers |
| * 2) The PHY has to switch dwc3's PIPE interface back to the dummy backend |
| * 3) The PHY itself can be shut down, this happens from typec_mux_set |
| * |
| * We also can't transition the PHY from one mode to another while dwc3 is up and running (this is |
| * slightly wrong, some transitions are possible, others aren't but because we have no documentation |
| * for this I'd rather play it safe). |
| * |
| * After both the PHY and dwc3 are initialized we will only ever see a single "new device connected" |
| * event. If we just keep them running only the first device plugged in will ever work. XHCI's port |
| * status register actually does show the correct state but no interrupt ever comes in. In gadget |
| * mode we don't even get a USBDisconnected event and everything looks like there's still something |
| * connected on the other end. |
| * This can be partially explained because the USB2 D+/D- lines are connected through a stateful |
| * eUSB2 repeater which in turn is controlled by a variant of the TI TPS6598x USB PD chip which |
| * resets the repeater out-of-band everytime the CC lines are (dis)connected. This then requires a |
| * PHY reset to make sure the PHY and the eUSB2 repeater state are synchronized again. |
| * |
| * And to make this all extra fun: If we get the order of some of this wrong either the port is just |
| * broken until a phy+dwc3 reset, or it's broken until a full SoC reset (likely because we can't |
| * reset some parts of the PHY), or some watchdog kicks in after a few seconds and forces a full SoC |
| * reset (mostly seen this with USB4/Thunderbolt but there's clearly some watchdog that hates |
| * invalid states). |
| * |
| * Hence there's really no good way to keep dwc3 fully up and running after we disconnect a cable |
| * because then we can't shut down the PHY anymore. And if we kept the PHY running in whatever mode |
| * it was until the next cable is connected we'd need to tear it all down and bring it back up again |
| * anyway to detect and use the next device. |
| * |
| * Instead, we just shut down everything when a cable is disconnected and transition to |
| * DWC3_APPLE_NO_CABLE. |
| * During initial probe we don't have any information about the connected cable and can't bring up |
| * the PHY properly and thus also can't fully bring up dwc3. Instead, we just keep everything off |
| * and defer the first dwc3 probe until we get the first cable connected event. Until then we stay |
| * in DWC3_APPLE_PROBE_PENDING. |
| * Once a cable is connected we then keep track of the controller mode here by transitioning to |
| * DWC3_APPLE_HOST or DWC3_APPLE_DEVICE. |
| */ |
| enum dwc3_apple_state { |
| DWC3_APPLE_PROBE_PENDING, /* Before first cable connection, dwc3_core_probe not called */ |
| DWC3_APPLE_NO_CABLE, /* No cable connected, dwc3 suspended after dwc3_core_exit */ |
| DWC3_APPLE_HOST, /* Cable connected, dwc3 in host mode */ |
| DWC3_APPLE_DEVICE, /* Cable connected, dwc3 in device mode */ |
| }; |
| |
| /** |
| * struct dwc3_apple - Apple-specific DWC3 USB controller |
| * @dwc: Core DWC3 structure |
| * @dev: Pointer to the device structure |
| * @mmio_resource: Resource to be passed to dwc3_core_probe |
| * @apple_regs: Apple-specific DWC3 registers |
| * @reset: Reset control |
| * @role_sw: USB role switch |
| * @lock: Mutex for synchronizing access |
| * @state: Current state of the controller, see documentation for the enum for details |
| */ |
| struct dwc3_apple { |
| struct dwc3 dwc; |
| |
| struct device *dev; |
| struct resource *mmio_resource; |
| void __iomem *apple_regs; |
| |
| struct reset_control *reset; |
| struct usb_role_switch *role_sw; |
| |
| struct mutex lock; |
| |
| enum dwc3_apple_state state; |
| }; |
| |
| #define to_dwc3_apple(d) container_of((d), struct dwc3_apple, dwc) |
| |
| /* |
| * Apple Silicon dwc3 vendor-specific registers |
| * |
| * These registers were identified by tracing XNU's memory access patterns and correlating them with |
| * debug output over serial to determine their names. We don't exactly know what these do but |
| * without these USB3 devices sometimes don't work. |
| */ |
| #define APPLE_DWC3_REGS_START 0xcd00 |
| #define APPLE_DWC3_REGS_END 0xcdff |
| |
| #define APPLE_DWC3_CIO_LFPS_OFFSET 0xcd38 |
| #define APPLE_DWC3_CIO_LFPS_OFFSET_VALUE 0xf800f80 |
| |
| #define APPLE_DWC3_CIO_BW_NGT_OFFSET 0xcd3c |
| #define APPLE_DWC3_CIO_BW_NGT_OFFSET_VALUE 0xfc00fc0 |
| |
| #define APPLE_DWC3_CIO_LINK_TIMER 0xcd40 |
| #define APPLE_DWC3_CIO_PENDING_HP_TIMER GENMASK(23, 16) |
| #define APPLE_DWC3_CIO_PENDING_HP_TIMER_VALUE 0x14 |
| #define APPLE_DWC3_CIO_PM_LC_TIMER GENMASK(15, 8) |
| #define APPLE_DWC3_CIO_PM_LC_TIMER_VALUE 0xa |
| #define APPLE_DWC3_CIO_PM_ENTRY_TIMER GENMASK(7, 0) |
| #define APPLE_DWC3_CIO_PM_ENTRY_TIMER_VALUE 0x10 |
| |
| static inline void dwc3_apple_writel(struct dwc3_apple *appledwc, u32 offset, u32 value) |
| { |
| writel(value, appledwc->apple_regs + offset - APPLE_DWC3_REGS_START); |
| } |
| |
| static inline u32 dwc3_apple_readl(struct dwc3_apple *appledwc, u32 offset) |
| { |
| return readl(appledwc->apple_regs + offset - APPLE_DWC3_REGS_START); |
| } |
| |
| static inline void dwc3_apple_mask(struct dwc3_apple *appledwc, u32 offset, u32 mask, u32 value) |
| { |
| u32 reg; |
| |
| reg = dwc3_apple_readl(appledwc, offset); |
| reg &= ~mask; |
| reg |= value; |
| dwc3_apple_writel(appledwc, offset, reg); |
| } |
| |
| static void dwc3_apple_setup_cio(struct dwc3_apple *appledwc) |
| { |
| dwc3_apple_writel(appledwc, APPLE_DWC3_CIO_LFPS_OFFSET, APPLE_DWC3_CIO_LFPS_OFFSET_VALUE); |
| dwc3_apple_writel(appledwc, APPLE_DWC3_CIO_BW_NGT_OFFSET, |
| APPLE_DWC3_CIO_BW_NGT_OFFSET_VALUE); |
| dwc3_apple_mask(appledwc, APPLE_DWC3_CIO_LINK_TIMER, APPLE_DWC3_CIO_PENDING_HP_TIMER, |
| FIELD_PREP(APPLE_DWC3_CIO_PENDING_HP_TIMER, |
| APPLE_DWC3_CIO_PENDING_HP_TIMER_VALUE)); |
| dwc3_apple_mask(appledwc, APPLE_DWC3_CIO_LINK_TIMER, APPLE_DWC3_CIO_PM_LC_TIMER, |
| FIELD_PREP(APPLE_DWC3_CIO_PM_LC_TIMER, APPLE_DWC3_CIO_PM_LC_TIMER_VALUE)); |
| dwc3_apple_mask(appledwc, APPLE_DWC3_CIO_LINK_TIMER, APPLE_DWC3_CIO_PM_ENTRY_TIMER, |
| FIELD_PREP(APPLE_DWC3_CIO_PM_ENTRY_TIMER, |
| APPLE_DWC3_CIO_PM_ENTRY_TIMER_VALUE)); |
| } |
| |
| static void dwc3_apple_set_ptrcap(struct dwc3_apple *appledwc, u32 mode) |
| { |
| guard(spinlock_irqsave)(&appledwc->dwc.lock); |
| dwc3_set_prtcap(&appledwc->dwc, mode, false); |
| } |
| |
| static int dwc3_apple_core_probe(struct dwc3_apple *appledwc) |
| { |
| struct dwc3_probe_data probe_data = {}; |
| int ret; |
| |
| lockdep_assert_held(&appledwc->lock); |
| WARN_ON_ONCE(appledwc->state != DWC3_APPLE_PROBE_PENDING); |
| |
| appledwc->dwc.dev = appledwc->dev; |
| probe_data.dwc = &appledwc->dwc; |
| probe_data.res = appledwc->mmio_resource; |
| probe_data.ignore_clocks_and_resets = true; |
| probe_data.skip_core_init_mode = true; |
| probe_data.properties = DWC3_DEFAULT_PROPERTIES; |
| |
| ret = dwc3_core_probe(&probe_data); |
| if (ret) |
| return ret; |
| |
| appledwc->state = DWC3_APPLE_NO_CABLE; |
| return 0; |
| } |
| |
| static int dwc3_apple_core_init(struct dwc3_apple *appledwc) |
| { |
| int ret; |
| |
| lockdep_assert_held(&appledwc->lock); |
| |
| switch (appledwc->state) { |
| case DWC3_APPLE_PROBE_PENDING: |
| ret = dwc3_apple_core_probe(appledwc); |
| if (ret) |
| dev_err(appledwc->dev, "Failed to probe DWC3 Core, err=%d\n", ret); |
| break; |
| case DWC3_APPLE_NO_CABLE: |
| ret = dwc3_core_init(&appledwc->dwc); |
| if (ret) |
| dev_err(appledwc->dev, "Failed to initialize DWC3 Core, err=%d\n", ret); |
| break; |
| default: |
| /* Unreachable unless there's a bug in this driver */ |
| WARN_ON_ONCE(1); |
| ret = -EINVAL; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| static void dwc3_apple_phy_set_mode(struct dwc3_apple *appledwc, enum phy_mode mode) |
| { |
| lockdep_assert_held(&appledwc->lock); |
| |
| /* |
| * This platform requires SUSPHY to be enabled here already in order to properly configure |
| * the PHY and switch dwc3's PIPE interface to USB3 PHY. |
| */ |
| dwc3_enable_susphy(&appledwc->dwc, true); |
| phy_set_mode(appledwc->dwc.usb2_generic_phy[0], mode); |
| phy_set_mode(appledwc->dwc.usb3_generic_phy[0], mode); |
| } |
| |
| static int dwc3_apple_init(struct dwc3_apple *appledwc, enum dwc3_apple_state state) |
| { |
| int ret, ret_reset; |
| |
| lockdep_assert_held(&appledwc->lock); |
| |
| ret = reset_control_deassert(appledwc->reset); |
| if (ret) { |
| dev_err(appledwc->dev, "Failed to deassert reset, err=%d\n", ret); |
| return ret; |
| } |
| |
| ret = dwc3_apple_core_init(appledwc); |
| if (ret) |
| goto reset_assert; |
| |
| /* |
| * Now that the core is initialized and already went through dwc3_core_soft_reset we can |
| * configure some unknown Apple-specific settings and then bring up xhci or gadget mode. |
| */ |
| dwc3_apple_setup_cio(appledwc); |
| |
| switch (state) { |
| case DWC3_APPLE_HOST: |
| appledwc->dwc.dr_mode = USB_DR_MODE_HOST; |
| dwc3_apple_set_ptrcap(appledwc, DWC3_GCTL_PRTCAP_HOST); |
| dwc3_apple_phy_set_mode(appledwc, PHY_MODE_USB_HOST); |
| ret = dwc3_host_init(&appledwc->dwc); |
| if (ret) { |
| dev_err(appledwc->dev, "Failed to initialize host, ret=%d\n", ret); |
| goto core_exit; |
| } |
| |
| break; |
| case DWC3_APPLE_DEVICE: |
| appledwc->dwc.dr_mode = USB_DR_MODE_PERIPHERAL; |
| dwc3_apple_set_ptrcap(appledwc, DWC3_GCTL_PRTCAP_DEVICE); |
| dwc3_apple_phy_set_mode(appledwc, PHY_MODE_USB_DEVICE); |
| ret = dwc3_gadget_init(&appledwc->dwc); |
| if (ret) { |
| dev_err(appledwc->dev, "Failed to initialize gadget, ret=%d\n", ret); |
| goto core_exit; |
| } |
| break; |
| default: |
| /* Unreachable unless there's a bug in this driver */ |
| WARN_ON_ONCE(1); |
| ret = -EINVAL; |
| goto core_exit; |
| } |
| |
| appledwc->state = state; |
| return 0; |
| |
| core_exit: |
| dwc3_core_exit(&appledwc->dwc); |
| reset_assert: |
| ret_reset = reset_control_assert(appledwc->reset); |
| if (ret_reset) |
| dev_warn(appledwc->dev, "Failed to assert reset, err=%d\n", ret_reset); |
| |
| return ret; |
| } |
| |
| static int dwc3_apple_exit(struct dwc3_apple *appledwc) |
| { |
| int ret = 0; |
| |
| lockdep_assert_held(&appledwc->lock); |
| |
| switch (appledwc->state) { |
| case DWC3_APPLE_PROBE_PENDING: |
| case DWC3_APPLE_NO_CABLE: |
| /* Nothing to do if we're already off */ |
| return 0; |
| case DWC3_APPLE_DEVICE: |
| dwc3_gadget_exit(&appledwc->dwc); |
| break; |
| case DWC3_APPLE_HOST: |
| dwc3_host_exit(&appledwc->dwc); |
| break; |
| } |
| |
| /* |
| * This platform requires SUSPHY to be enabled in order to properly power down the PHY |
| * and switch dwc3's PIPE interface back to a dummy PHY (i.e. no USB3 support and USB2 via |
| * a different PHY connected through ULPI). |
| */ |
| dwc3_enable_susphy(&appledwc->dwc, true); |
| dwc3_core_exit(&appledwc->dwc); |
| appledwc->state = DWC3_APPLE_NO_CABLE; |
| |
| ret = reset_control_assert(appledwc->reset); |
| if (ret) { |
| dev_err(appledwc->dev, "Failed to assert reset, err=%d\n", ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int dwc3_usb_role_switch_set(struct usb_role_switch *sw, enum usb_role role) |
| { |
| struct dwc3_apple *appledwc = usb_role_switch_get_drvdata(sw); |
| int ret; |
| |
| guard(mutex)(&appledwc->lock); |
| |
| /* |
| * We need to tear all of dwc3 down and re-initialize it every time a cable is |
| * connected or disconnected or when the mode changes. See the documentation for enum |
| * dwc3_apple_state for details. |
| */ |
| ret = dwc3_apple_exit(appledwc); |
| if (ret) |
| return ret; |
| |
| switch (role) { |
| case USB_ROLE_NONE: |
| /* Nothing to do if no cable is connected */ |
| return 0; |
| case USB_ROLE_HOST: |
| return dwc3_apple_init(appledwc, DWC3_APPLE_HOST); |
| case USB_ROLE_DEVICE: |
| return dwc3_apple_init(appledwc, DWC3_APPLE_DEVICE); |
| default: |
| dev_err(appledwc->dev, "Invalid target role: %d\n", role); |
| return -EINVAL; |
| } |
| } |
| |
| static enum usb_role dwc3_usb_role_switch_get(struct usb_role_switch *sw) |
| { |
| struct dwc3_apple *appledwc = usb_role_switch_get_drvdata(sw); |
| |
| guard(mutex)(&appledwc->lock); |
| |
| switch (appledwc->state) { |
| case DWC3_APPLE_HOST: |
| return USB_ROLE_HOST; |
| case DWC3_APPLE_DEVICE: |
| return USB_ROLE_DEVICE; |
| case DWC3_APPLE_NO_CABLE: |
| case DWC3_APPLE_PROBE_PENDING: |
| return USB_ROLE_NONE; |
| default: |
| /* Unreachable unless there's a bug in this driver */ |
| dev_err(appledwc->dev, "Invalid internal state: %d\n", appledwc->state); |
| return USB_ROLE_NONE; |
| } |
| } |
| |
| static int dwc3_apple_setup_role_switch(struct dwc3_apple *appledwc) |
| { |
| struct usb_role_switch_desc dwc3_role_switch = { NULL }; |
| |
| dwc3_role_switch.fwnode = dev_fwnode(appledwc->dev); |
| dwc3_role_switch.set = dwc3_usb_role_switch_set; |
| dwc3_role_switch.get = dwc3_usb_role_switch_get; |
| dwc3_role_switch.driver_data = appledwc; |
| appledwc->role_sw = usb_role_switch_register(appledwc->dev, &dwc3_role_switch); |
| if (IS_ERR(appledwc->role_sw)) |
| return PTR_ERR(appledwc->role_sw); |
| |
| return 0; |
| } |
| |
| static int dwc3_apple_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct dwc3_apple *appledwc; |
| int ret; |
| |
| appledwc = devm_kzalloc(&pdev->dev, sizeof(*appledwc), GFP_KERNEL); |
| if (!appledwc) |
| return -ENOMEM; |
| |
| appledwc->dev = &pdev->dev; |
| mutex_init(&appledwc->lock); |
| |
| appledwc->reset = devm_reset_control_get_exclusive(dev, NULL); |
| if (IS_ERR(appledwc->reset)) |
| return dev_err_probe(&pdev->dev, PTR_ERR(appledwc->reset), |
| "Failed to get reset control\n"); |
| |
| ret = reset_control_assert(appledwc->reset); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to assert reset, err=%d\n", ret); |
| return ret; |
| } |
| |
| appledwc->mmio_resource = platform_get_resource_byname(pdev, IORESOURCE_MEM, "dwc3-core"); |
| if (!appledwc->mmio_resource) { |
| dev_err(dev, "Failed to get DWC3 MMIO\n"); |
| return -EINVAL; |
| } |
| |
| appledwc->apple_regs = devm_platform_ioremap_resource_byname(pdev, "dwc3-apple"); |
| if (IS_ERR(appledwc->apple_regs)) |
| return dev_err_probe(dev, PTR_ERR(appledwc->apple_regs), |
| "Failed to map Apple-specific MMIO\n"); |
| |
| /* |
| * On this platform, DWC3 can only be brought up after parts of the PHY have been |
| * initialized with knowledge of the target mode and cable orientation from typec_set_mux. |
| * Since this has not happened here we cannot setup DWC3 yet and instead defer this until |
| * the first cable is connected. See the documentation for enum dwc3_apple_state for |
| * details. |
| */ |
| appledwc->state = DWC3_APPLE_PROBE_PENDING; |
| ret = dwc3_apple_setup_role_switch(appledwc); |
| if (ret) |
| return dev_err_probe(&pdev->dev, ret, "Failed to setup role switch\n"); |
| |
| return 0; |
| } |
| |
| static void dwc3_apple_remove(struct platform_device *pdev) |
| { |
| struct dwc3 *dwc = platform_get_drvdata(pdev); |
| struct dwc3_apple *appledwc = to_dwc3_apple(dwc); |
| |
| guard(mutex)(&appledwc->lock); |
| |
| usb_role_switch_unregister(appledwc->role_sw); |
| |
| /* |
| * If we're still in DWC3_APPLE_PROBE_PENDING we never got any cable connected event and |
| * dwc3_core_probe was never called and there's hence no need to call dwc3_core_remove. |
| * dwc3_apple_exit can be called unconditionally because it checks the state itself. |
| */ |
| dwc3_apple_exit(appledwc); |
| if (appledwc->state != DWC3_APPLE_PROBE_PENDING) |
| dwc3_core_remove(&appledwc->dwc); |
| } |
| |
| static const struct of_device_id dwc3_apple_of_match[] = { |
| { .compatible = "apple,t8103-dwc3" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(of, dwc3_apple_of_match); |
| |
| static struct platform_driver dwc3_apple_driver = { |
| .probe = dwc3_apple_probe, |
| .remove = dwc3_apple_remove, |
| .driver = { |
| .name = "dwc3-apple", |
| .of_match_table = dwc3_apple_of_match, |
| }, |
| }; |
| |
| module_platform_driver(dwc3_apple_driver); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Sven Peter <sven@kernel.org>"); |
| MODULE_DESCRIPTION("DesignWare DWC3 Apple Silicon Glue Driver"); |