[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20250911014644.33fjkq26ixykciys@synopsys.com>
Date: Thu, 11 Sep 2025 01:46:46 +0000
From: Thinh Nguyen <Thinh.Nguyen@...opsys.com>
To: Sven Peter <sven@...nel.org>
CC: Greg Kroah-Hartman <gregkh@...uxfoundation.org>,
Rob Herring <robh@...nel.org>,
Krzysztof Kozlowski <krzk+dt@...nel.org>,
Conor Dooley <conor+dt@...nel.org>, Felipe Balbi <balbi@...nel.org>,
Janne Grunau <j@...nau.net>, Alyssa Rosenzweig <alyssa@...enzweig.io>,
Neal Gompa <neal@...pa.dev>, Vinod Koul <vkoul@...nel.org>,
Kishon Vijay Abraham I <kishon@...nel.org>,
Thinh Nguyen <Thinh.Nguyen@...opsys.com>,
Heikki Krogerus <heikki.krogerus@...ux.intel.com>,
Philipp Zabel <p.zabel@...gutronix.de>, Frank Li <Frank.Li@....com>,
Ran Wang <ran.wang_1@....com>, Peter Chen <peter.chen@....com>,
"linux-usb@...r.kernel.org" <linux-usb@...r.kernel.org>,
"devicetree@...r.kernel.org" <devicetree@...r.kernel.org>,
"linux-kernel@...r.kernel.org" <linux-kernel@...r.kernel.org>,
"asahi@...ts.linux.dev" <asahi@...ts.linux.dev>,
"linux-arm-kernel@...ts.infradead.org" <linux-arm-kernel@...ts.infradead.org>,
"linux-phy@...ts.infradead.org" <linux-phy@...ts.infradead.org>
Subject: Re: [PATCH v2 04/22] usb: dwc3: Add Apple Silicon DWC3 glue layer
driver
On Sat, Sep 06, 2025, Sven Peter wrote:
> As mad as it sounds, the dwc3 controller present on the Apple M1 must be
> reset and reinitialized whenever a device is unplugged from the root
> port or when the PHY mode is changed.
>
> This is required for at least the following reasons:
>
> - 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. When the USB PD controller detects a hotplug event it resets
> the eUSB2 repeater. Afterwards, no new device is recognized before
> the DWC3 core and PHY are reset as well because the eUSB2 repeater
> and the PHY/dwc3 block disagree about the current state.
>
> - It's possible to completely break the dwc3 controller by switching
> it to device mode and unplugging the cable at just the wrong time.
> If this happens dwc3 behaves as if no device is connected.
> CORESOFTRESET will also never clear after it has been set. The only
> workaround is to trigger a hard reset of the entire dwc3 core with
> its external reset line.
>
> - Whenever the PHY mode is changed (to e.g. transition to DisplayPort
> alternate mode or USB4) dwc3 has to be shutdown and reinitialized.
> Otherwise the Type-C port will not be usable until the entire SoC
> has been reset.
>
> Additionally, these controllers have a Apple-specific MMIO region after
> the common dwc3 region where some controls have to be updated. PHY
> bringup and shutdown also requires SUSPHY to be enabled for the ports
> to work correctly.
>
> In the future, this driver will also gain support for USB3-via-USB4
> tunneling which will require additional tweaks.
>
> Add a glue driver that takes of all of these constraints.
>
> Signed-off-by: Sven Peter <sven@...nel.org>
> ---
> MAINTAINERS | 1 +
> drivers/usb/dwc3/Kconfig | 11 ++
> drivers/usb/dwc3/Makefile | 1 +
> drivers/usb/dwc3/dwc3-apple.c | 425 ++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 438 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 0e085cb0762f765958d67be61ae0d3d773503431..e147e1b919d5737a34e684ec587872ce591c641a 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -2424,6 +2424,7 @@ F: drivers/pwm/pwm-apple.c
> F: drivers/soc/apple/*
> F: drivers/spi/spi-apple.c
> F: drivers/spmi/spmi-apple-controller.c
> +F: drivers/usb/dwc3/dwc3-apple.c
> F: drivers/video/backlight/apple_dwi_bl.c
> F: drivers/watchdog/apple_wdt.c
> F: include/dt-bindings/interrupt-controller/apple-aic.h
> diff --git a/drivers/usb/dwc3/Kconfig b/drivers/usb/dwc3/Kconfig
> index 310d182e10b50b253d7e5a51674806e6ec442a2a..8161cd8f5d0d82826262518a1aefa3096aae83a8 100644
> --- a/drivers/usb/dwc3/Kconfig
> +++ b/drivers/usb/dwc3/Kconfig
> @@ -189,4 +189,15 @@ config USB_DWC3_RTK
> or dual-role mode.
> Say 'Y' or 'M' if you have such device.
>
> +config USB_DWC3_APPLE
> + tristate "Apple Silicon DWC3 Platform Driver"
> + depends on OF && ARCH_APPLE
> + default USB_DWC3
> + select USB_ROLE_SWITCH
> + help
> + Support Apple Silicon SoCs with DesignWare Core USB3 IP.
> + The DesignWare Core USB3 IP has to be used in dual-role
> + mode on these machines.
> + Say 'Y' or 'M' if you have such device.
> +
> endif
> diff --git a/drivers/usb/dwc3/Makefile b/drivers/usb/dwc3/Makefile
> index 830e6c9e5fe073c1f662ce34b6a4a2da34c407a2..10b5e68cfd68d5ca9aa5a27b04f349f9bf58e65c 100644
> --- a/drivers/usb/dwc3/Makefile
> +++ b/drivers/usb/dwc3/Makefile
> @@ -43,6 +43,7 @@ endif
> ##
>
> obj-$(CONFIG_USB_DWC3_AM62) += dwc3-am62.o
> +obj-$(CONFIG_USB_DWC3_APPLE) += dwc3-apple.o
> obj-$(CONFIG_USB_DWC3_OMAP) += dwc3-omap.o
> obj-$(CONFIG_USB_DWC3_EXYNOS) += dwc3-exynos.o
> obj-$(CONFIG_USB_DWC3_PCI) += dwc3-pci.o
> diff --git a/drivers/usb/dwc3/dwc3-apple.c b/drivers/usb/dwc3/dwc3-apple.c
> new file mode 100644
> index 0000000000000000000000000000000000000000..27674f0c284104cbbe75f51cd55593a964c8c9d6
> --- /dev/null
> +++ b/drivers/usb/dwc3/dwc3-apple.c
> @@ -0,0 +1,425 @@
> +// 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://urldefense.com/v3/__https://www.ti.com__;!!A4F2R9G_pg!Y-C3WLMl9OBFefTLBP84UnZJKzMKYzc7ZrG_bwC4q1q3h0XeGx8NmqFr_-FDYmDtHq802yjIffmSdYRaKA$
> + */
> +
> +#include <linux/of.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/platform_device.h>
> +#include <linux/reset.h>
> +
> +#include "glue.h"
> +
> +enum dwc3_apple_mode {
> + DWC3_APPLE_OFF,
> + DWC3_APPLE_HOST,
> + DWC3_APPLE_DEVICE,
> +};
> +
> +/**
> + * 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
> + * @resets: Reset control
> + * @role_sw: USB role switch
> + * @lock: Mutex for synchronizing access
> + * @core_probe_done: True if dwc3_core_probe was already called after the first plug
> + * @mode: Current mode of the controller (off/host/device)
> + */
> +struct dwc3_apple {
> + struct dwc3 dwc;
> +
> + struct device *dev;
> + struct resource *mmio_resource;
> + void __iomem *apple_regs;
> +
> + struct reset_control *resets;
> + struct usb_role_switch *role_sw;
> +
> + struct mutex lock;
> +
> + bool core_probe_done;
> + enum dwc3_apple_mode mode;
> +};
> +
> +#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,
> + APPLE_DWC3_CIO_PENDING_HP_TIMER_VALUE);
> + dwc3_apple_mask(appledwc, APPLE_DWC3_CIO_LINK_TIMER, 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,
> + 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->core_probe_done);
> +
> + 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;
> +
> + ret = dwc3_core_probe(&probe_data);
> + if (ret)
> + return ret;
> +
> + appledwc->core_probe_done = true;
> + return 0;
> +}
> +
> +static int dwc3_apple_core_init(struct dwc3_apple *appledwc)
> +{
> + int ret;
> +
> + lockdep_assert_held(&appledwc->lock);
> +
> + if (appledwc->core_probe_done) {
> + ret = dwc3_core_init(&appledwc->dwc);
> + if (ret)
> + dev_err(appledwc->dev, "Failed to initialize DWC3 Core, err=%d\n", ret);
> + } else {
> + ret = dwc3_apple_core_probe(appledwc);
> + if (ret)
> + dev_err(appledwc->dev, "Failed to probe DWC3 Core, err=%d\n", ret);
> + }
> +
> + 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
> + */
> + 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_mode mode)
> +{
> + int ret, ret_reset;
> +
> + lockdep_assert_held(&appledwc->lock);
> +
> + ret = reset_control_deassert(appledwc->resets);
> + if (ret) {
> + dev_err(appledwc->dev, "Failed to deassert resets, 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.
> + */
> + dwc3_apple_setup_cio(appledwc);
> +
> + switch (mode) {
> + 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->mode = mode;
> + return 0;
> +
> +core_exit:
> + dwc3_core_exit(&appledwc->dwc);
> +reset_assert:
> + ret_reset = reset_control_assert(appledwc->resets);
> + if (ret_reset)
> + dev_warn(appledwc->dev, "Failed to assert resets, 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->mode) {
> + case DWC3_APPLE_OFF:
> + /* 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 */
> + dwc3_enable_susphy(&appledwc->dwc, true);
> + dwc3_core_exit(&appledwc->dwc);
> + appledwc->mode = DWC3_APPLE_OFF;
> +
> + ret = reset_control_assert(appledwc->resets);
> + if (ret) {
> + dev_err(appledwc->dev, "Failed to assert resets, 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);
> +
> + /*
> + * 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. When the USB PD controller
> + * detects a hotplug event it resets the eUSB2 repeater. Afterwards, no new device is
> + * recognized before the DWC3 core and PHY are reset as well because the eUSB2 repeater
> + * and the PHY/dwc3 block disagree about the current state.
> + * Additionally, the PHY is also incapable of switching between arbitrary modes when dwc3
> + * is kept online. It's also possible to get dwc3 into a state where no new device is
> + * recognized and even a soft reset is not enough to recover when unplugging a cable at the
> + * wrong time while in gadget mode. Only a hard reset triggered via the external reset line
> + * is able to recover from this state.
> + * We thus tear all of dwc3 down here and re-initialize it every time we get a plug change
> + * (or even mode change) event.
> + */
> + 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->mode) {
> + case DWC3_APPLE_HOST:
> + return USB_ROLE_HOST;
> + case DWC3_APPLE_DEVICE:
> + return USB_ROLE_DEVICE;
> + case DWC3_APPLE_OFF:
> + return USB_ROLE_NONE;
> + default:
> + /* Unreachable unless there's a bug in this driver */
> + WARN_ON_ONCE(1);
> + 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->resets = devm_reset_control_array_get_exclusive(dev);
> + if (IS_ERR(appledwc->resets))
> + return dev_err_probe(&pdev->dev, PTR_ERR(appledwc->resets),
> + "Failed to get resets\n");
> +
> + ret = reset_control_assert(appledwc->resets);
> + if (ret) {
> + dev_err(&pdev->dev, "Failed to assert resets, 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");
> +
> + /*
> + * Note that we only bring up dwc3 once the first device is attached because we need to know
> + * the role (e.g. host), mode (e.g. USB3) and lane orientation to bring up the PHY which is
> + * tightly coupled to dwc3.
> + */
> + appledwc->mode = DWC3_APPLE_OFF;
> + appledwc->core_probe_done = false;
> + 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);
> +
> + dwc3_apple_exit(appledwc);
> + if (appledwc->core_probe_done)
> + 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@...nel.org>");
> +MODULE_DESCRIPTION("DesignWare DWC3 Apple Silicon Glue Driver");
>
> --
> 2.34.1
>
>
I like these new changes! From the quick glance, this looks good to me.
Sorry for the delay, but I need get back to reviewing this in detail
next week.
BR,
Thinh
Powered by blists - more mailing lists