From patchwork Fri Mar 2 14:56:25 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Amelie Delaunay X-Patchwork-Id: 10254879 Return-Path: Received: from mail.wl.linuxfoundation.org (pdx-wl-mail.web.codeaurora.org [172.30.200.125]) by pdx-korg-patchwork.web.codeaurora.org (Postfix) with ESMTP id DBDDF60211 for ; Fri, 2 Mar 2018 14:58:25 +0000 (UTC) Received: from mail.wl.linuxfoundation.org (localhost [127.0.0.1]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id C7DF7289B5 for ; Fri, 2 Mar 2018 14:58:25 +0000 (UTC) Received: by mail.wl.linuxfoundation.org (Postfix, from userid 486) id B86BA289C2; Fri, 2 Mar 2018 14:58:25 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on pdx-wl-mail.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-1.9 required=2.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID autolearn=unavailable version=3.3.1 Received: from bombadil.infradead.org (bombadil.infradead.org [198.137.202.133]) (using TLSv1.2 with cipher AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.wl.linuxfoundation.org (Postfix) with ESMTPS id A4133289B5 for ; Fri, 2 Mar 2018 14:58:24 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=lists.infradead.org; s=bombadil.20170209; h=Sender: Content-Transfer-Encoding:Content-Type:Cc:List-Subscribe:List-Help:List-Post: List-Archive:List-Unsubscribe:List-Id:MIME-Version:References:In-Reply-To: Message-ID:Date:Subject:To:From:Reply-To:Content-ID:Content-Description: Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID: List-Owner; bh=BEIn0ELne/sJijokEY3qQcZ8BufiEx4I38Lu2e4eevM=; b=UwQD6yyYq95TVA vffNnOS5wh/flakw/7uRPkYnnefrL6KRAaIj5279YNlZnDTiIvaXfCL1d9/W3GZ8PWJfE+GTE2Pnh cJMcf6FfQj2oCK/05XMY1w0tkUO5tGZnRdrC29Pdr5qb9b0Or7j1l7swiuj29hVmuq+RO6bj1Cf2D xWdPsogE3H/9eGn7p9i2rAKPIJKQVOQ7AZ/+Aq8XF1/GxBHmOHONa6ebzJDobp46fl2zkyfeBvoLN 7te7vB4qbFa2F3b/AT3sxVRQQuhFalatsfAUwwllDDFYDPOJa4LORlDe/CnAxXBWt61wc16hHaoKO MT37IVANQcDTWQqzJ5Yg==; Received: from localhost ([127.0.0.1] helo=bombadil.infradead.org) by bombadil.infradead.org with esmtp (Exim 4.89 #1 (Red Hat Linux)) id 1erm8T-000082-Nk; Fri, 02 Mar 2018 14:58:13 +0000 Received: from mx08-00178001.pphosted.com ([91.207.212.93] helo=mx07-00178001.pphosted.com) by bombadil.infradead.org with esmtps (Exim 4.89 #1 (Red Hat Linux)) id 1erm7F-0007lX-WA for linux-arm-kernel@lists.infradead.org; Fri, 02 Mar 2018 14:57:14 +0000 Received: from pps.filterd (m0046660.ppops.net [127.0.0.1]) by mx08-.pphosted.com (8.16.0.21/8.16.0.21) with SMTP id w22EsF6A010002; Fri, 2 Mar 2018 15:56:43 +0100 Received: from beta.dmz-eu.st.com (beta.dmz-eu.st.com [164.129.1.35]) by mx08-00178001.pphosted.com with ESMTP id 2gf30stj5r-1 (version=TLSv1 cipher=ECDHE-RSA-AES256-SHA bits=256 verify=NOT); Fri, 02 Mar 2018 15:56:43 +0100 Received: from zeta.dmz-eu.st.com (zeta.dmz-eu.st.com [164.129.230.9]) by beta.dmz-eu.st.com (STMicroelectronics) with ESMTP id EC3813A; Fri, 2 Mar 2018 14:56:41 +0000 (GMT) Received: from Webmail-eu.st.com (Safex1hubcas22.st.com [10.75.90.92]) by zeta.dmz-eu.st.com (STMicroelectronics) with ESMTP id BE4BDA7F1; Fri, 2 Mar 2018 14:56:41 +0000 (GMT) Received: from SAFEX1HUBCAS24.st.com (10.75.90.94) by Safex1hubcas22.st.com (10.75.90.92) with Microsoft SMTP Server (TLS) id 14.3.352.0; Fri, 2 Mar 2018 15:56:41 +0100 Received: from localhost (10.201.20.5) by webmail-ga.st.com (10.75.90.48) with Microsoft SMTP Server (TLS) id 14.3.361.1; Fri, 2 Mar 2018 15:56:41 +0100 From: Amelie Delaunay To: Rob Herring , Mark Rutland , Kishon Vijay Abraham I , Alexandre Torgue , Maxime Coquelin Subject: [PATCH v3 2/2] phy: stm32: add support for STM32 USB PHY Controller (USBPHYC) Date: Fri, 2 Mar 2018 15:56:25 +0100 Message-ID: <1520002585-13113-3-git-send-email-amelie.delaunay@st.com> X-Mailer: git-send-email 2.7.4 In-Reply-To: <1520002585-13113-1-git-send-email-amelie.delaunay@st.com> References: <1520002585-13113-1-git-send-email-amelie.delaunay@st.com> MIME-Version: 1.0 X-Originating-IP: [10.201.20.5] X-Proofpoint-Virus-Version: vendor=fsecure engine=2.50.10432:, , definitions=2018-03-02_08:, , signatures=0 X-CRM114-Version: 20100106-BlameMichelson ( TRE 0.8.0 (BSD) ) MR-646709E3 X-CRM114-CacheID: sfid-20180302_065659_118246_BF8C79DD X-CRM114-Status: GOOD ( 26.04 ) X-BeenThere: linux-arm-kernel@lists.infradead.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: devicetree@vger.kernel.org, Amelie Delaunay , linux-kernel@vger.kernel.org, linux-arm-kernel@lists.infradead.org Sender: "linux-arm-kernel" Errors-To: linux-arm-kernel-bounces+patchwork-linux-arm=patchwork.kernel.org@lists.infradead.org X-Virus-Scanned: ClamAV using ClamSMTP This patch adds phy transceiver driver for STM32 USB PHY Controller (USBPHYC) that provides dual port High-Speed phy for OTG (single port) and EHCI/OHCI host controller (two ports). One port of the phy is shared between the two USB controllers through a UTMI+ switch. Signed-off-by: Amelie Delaunay --- drivers/phy/st/Kconfig | 14 ++ drivers/phy/st/Makefile | 1 + drivers/phy/st/phy-stm32-usbphyc.c | 461 +++++++++++++++++++++++++++++++++++++ 3 files changed, 476 insertions(+) create mode 100644 drivers/phy/st/phy-stm32-usbphyc.c diff --git a/drivers/phy/st/Kconfig b/drivers/phy/st/Kconfig index 0814d3f..609719b 100644 --- a/drivers/phy/st/Kconfig +++ b/drivers/phy/st/Kconfig @@ -31,3 +31,17 @@ config PHY_STIH407_USB help Enable this support to enable the picoPHY device used by USB2 and USB3 controllers on STMicroelectronics STiH407 SoC families. + +config PHY_STM32_USBPHYC + tristate "STMicroelectronics STM32 USB HS PHY Controller driver" + depends on ARCH_STM32 || COMPILE_TEST + select GENERIC_PHY + help + Enable this to support the High-Speed USB transceivers that are part + of some STMicroelectronics STM32 SoCs. + + This driver controls the entire USB PHY block: the USB PHY controller + (USBPHYC) and the two 8-bit wide UTMI+ interfaces. First interface is + used by an HS USB Host controller, and the second one is shared + between an HS USB OTG controller and an HS USB Host controller, + selected by a USB switch. diff --git a/drivers/phy/st/Makefile b/drivers/phy/st/Makefile index e2adfe2..c0091ad 100644 --- a/drivers/phy/st/Makefile +++ b/drivers/phy/st/Makefile @@ -2,3 +2,4 @@ obj-$(CONFIG_PHY_MIPHY28LP) += phy-miphy28lp.o obj-$(CONFIG_PHY_ST_SPEAR1310_MIPHY) += phy-spear1310-miphy.o obj-$(CONFIG_PHY_ST_SPEAR1340_MIPHY) += phy-spear1340-miphy.o obj-$(CONFIG_PHY_STIH407_USB) += phy-stih407-usb.o +obj-$(CONFIG_PHY_STM32_USBPHYC) += phy-stm32-usbphyc.o diff --git a/drivers/phy/st/phy-stm32-usbphyc.c b/drivers/phy/st/phy-stm32-usbphyc.c new file mode 100644 index 0000000..1b2ac25 --- /dev/null +++ b/drivers/phy/st/phy-stm32-usbphyc.c @@ -0,0 +1,461 @@ +// SPDX-Licence-Identifier: GPL-2.0 +/* + * STMicroelectronics STM32 USB PHY Controller driver + * + * Copyright (C) 2018 STMicroelectronics + * Author(s): Amelie Delaunay . + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STM32_USBPHYC_PLL 0x0 +#define STM32_USBPHYC_MISC 0x8 +#define STM32_USBPHYC_VERSION 0x3F4 + +/* STM32_USBPHYC_PLL bit fields */ +#define PLLNDIV GENMASK(6, 0) +#define PLLFRACIN GENMASK(25, 10) +#define PLLEN BIT(26) +#define PLLSTRB BIT(27) +#define PLLSTRBYP BIT(28) +#define PLLFRACCTL BIT(29) +#define PLLDITHEN0 BIT(30) +#define PLLDITHEN1 BIT(31) + +/* STM32_USBPHYC_MISC bit fields */ +#define SWITHOST BIT(0) + +/* STM32_USBPHYC_VERSION bit fields */ +#define MINREV GENMASK(3, 0) +#define MAJREV GENMASK(7, 4) + +static const char * const supplies_names[] = { + "vdda1v1", /* 1V1 */ + "vdda1v8", /* 1V8 */ +}; + +#define NUM_SUPPLIES ARRAY_SIZE(supplies_names) + +#define PLL_LOCK_TIME_US 100 +#define PLL_PWR_DOWN_TIME_US 5 +#define PLL_FVCO_MHZ 2880 +#define PLL_INFF_MIN_RATE_HZ 19200000 +#define PLL_INFF_MAX_RATE_HZ 38400000 +#define HZ_PER_MHZ 1000000L + +struct pll_params { + u8 ndiv; + u16 frac; +}; + +struct stm32_usbphyc_phy { + struct phy *phy; + struct stm32_usbphyc *usbphyc; + struct regulator_bulk_data supplies[NUM_SUPPLIES]; + u32 index; + bool active; +}; + +struct stm32_usbphyc { + struct device *dev; + void __iomem *base; + struct clk *clk; + struct reset_control *rst; + struct stm32_usbphyc_phy **phys; + int nphys; + int switch_setup; + bool pll_enabled; +}; + +static inline void stm32_usbphyc_set_bits(void __iomem *reg, u32 bits) +{ + writel_relaxed(readl_relaxed(reg) | bits, reg); +} + +static inline void stm32_usbphyc_clr_bits(void __iomem *reg, u32 bits) +{ + writel_relaxed(readl_relaxed(reg) & ~bits, reg); +} + +void stm32_usbphyc_get_pll_params(u32 clk_rate, struct pll_params *pll_params) +{ + unsigned long long fvco, ndiv, frac; + + /* _ + * | FVCO = INFF*2*(NDIV + FRACT/2^16) when DITHER_DISABLE[1] = 1 + * | FVCO = 2880MHz + * < + * | NDIV = integer part of input bits to set the LDF + * |_FRACT = fractional part of input bits to set the LDF + * => PLLNDIV = integer part of (FVCO / (INFF*2)) + * => PLLFRACIN = fractional part of(FVCO / INFF*2) * 2^16 + * <=> PLLFRACIN = ((FVCO / (INFF*2)) - PLLNDIV) * 2^16 + */ + fvco = (unsigned long long)PLL_FVCO_MHZ * HZ_PER_MHZ; + + ndiv = fvco; + do_div(ndiv, (clk_rate * 2)); + pll_params->ndiv = (u8)ndiv; + + frac = fvco * (1 << 16); + do_div(frac, (clk_rate * 2)); + frac = frac - (ndiv * (1 << 16)); + pll_params->frac = (u16)frac; +} + +static int stm32_usbphyc_pll_init(struct stm32_usbphyc *usbphyc) +{ + struct pll_params pll_params; + u32 clk_rate = clk_get_rate(usbphyc->clk); + u32 ndiv, frac; + u32 usbphyc_pll; + + if ((clk_rate < PLL_INFF_MIN_RATE_HZ) || + (clk_rate > PLL_INFF_MAX_RATE_HZ)) { + dev_err(usbphyc->dev, "input clk freq (%dHz) out of range\n", + clk_rate); + return -EINVAL; + } + + stm32_usbphyc_get_pll_params(clk_rate, &pll_params); + ndiv = FIELD_PREP(PLLNDIV, pll_params.ndiv); + frac = FIELD_PREP(PLLFRACIN, pll_params.frac); + + usbphyc_pll = PLLDITHEN1 | PLLDITHEN0 | PLLSTRBYP | ndiv; + + if (pll_params.frac) + usbphyc_pll |= PLLFRACCTL | frac; + + writel_relaxed(usbphyc_pll, usbphyc->base + STM32_USBPHYC_PLL); + + dev_dbg(usbphyc->dev, "input clk freq=%dHz, ndiv=%lu, frac=%lu\n", + clk_rate, FIELD_GET(PLLNDIV, usbphyc_pll), + FIELD_GET(PLLFRACIN, usbphyc_pll)); + + return 0; +} + +static bool stm32_usbphyc_has_one_phy_active(struct stm32_usbphyc *usbphyc) +{ + int i; + + for (i = 0; i < usbphyc->nphys; i++) + if (usbphyc->phys[i]->active) + return true; + + return false; +} + +static int stm32_usbphyc_pll_enable(struct stm32_usbphyc *usbphyc) +{ + void __iomem *pll_reg = usbphyc->base + STM32_USBPHYC_PLL; + bool pllen = (readl_relaxed(pll_reg) & PLLEN); + int ret; + + /* Check if one phy port has already configured the pll */ + if (pllen && stm32_usbphyc_has_one_phy_active(usbphyc)) + return 0; + + if (pllen) { + stm32_usbphyc_clr_bits(pll_reg, PLLEN); + /* Wait for minimum width of powerdown pulse (ENABLE = Low) */ + udelay(PLL_PWR_DOWN_TIME_US); + } + + ret = stm32_usbphyc_pll_init(usbphyc); + if (ret) + return ret; + + stm32_usbphyc_set_bits(pll_reg, PLLEN); + + /* Wait for maximum lock time */ + udelay(PLL_LOCK_TIME_US); + + if (!(readl_relaxed(pll_reg) & PLLEN)) { + dev_err(usbphyc->dev, "PLLEN not set\n"); + return -EIO; + } + + return 0; +} + +static int stm32_usbphyc_pll_disable(struct stm32_usbphyc *usbphyc) +{ + void __iomem *pll_reg = usbphyc->base + STM32_USBPHYC_PLL; + + /* Check if other phy port active */ + if (stm32_usbphyc_has_one_phy_active(usbphyc)) + return 0; + + stm32_usbphyc_clr_bits(pll_reg, PLLEN); + /* Wait for minimum width of powerdown pulse (ENABLE = Low) */ + udelay(PLL_PWR_DOWN_TIME_US); + + if (readl_relaxed(pll_reg) & PLLEN) { + dev_err(usbphyc->dev, "PLL not reset\n"); + return -EIO; + } + + return 0; +} + +static int stm32_usbphyc_phy_init(struct phy *phy) +{ + struct stm32_usbphyc_phy *usbphyc_phy = phy_get_drvdata(phy); + struct stm32_usbphyc *usbphyc = usbphyc_phy->usbphyc; + int ret; + + ret = stm32_usbphyc_pll_enable(usbphyc); + if (ret) + return ret; + + usbphyc_phy->active = true; + + return 0; +} + +static int stm32_usbphyc_phy_exit(struct phy *phy) +{ + struct stm32_usbphyc_phy *usbphyc_phy = phy_get_drvdata(phy); + struct stm32_usbphyc *usbphyc = usbphyc_phy->usbphyc; + + usbphyc_phy->active = false; + + return stm32_usbphyc_pll_disable(usbphyc); +} + +static int stm32_usbphyc_phy_power_on(struct phy *phy) +{ + struct stm32_usbphyc_phy *usbphyc_phy = phy_get_drvdata(phy); + + return regulator_bulk_enable(NUM_SUPPLIES, usbphyc_phy->supplies); +} + +static int stm32_usbphyc_phy_power_off(struct phy *phy) +{ + struct stm32_usbphyc_phy *usbphyc_phy = phy_get_drvdata(phy); + + return regulator_bulk_disable(NUM_SUPPLIES, usbphyc_phy->supplies); +} + +static const struct phy_ops stm32_usbphyc_phy_ops = { + .init = stm32_usbphyc_phy_init, + .exit = stm32_usbphyc_phy_exit, + .power_on = stm32_usbphyc_phy_power_on, + .power_off = stm32_usbphyc_phy_power_off, + .owner = THIS_MODULE, +}; + +static void stm32_usbphyc_switch_setup(struct stm32_usbphyc *usbphyc, + u32 utmi_switch) +{ + if (!utmi_switch) + stm32_usbphyc_clr_bits(usbphyc->base + STM32_USBPHYC_MISC, + SWITHOST); + else + stm32_usbphyc_set_bits(usbphyc->base + STM32_USBPHYC_MISC, + SWITHOST); + usbphyc->switch_setup = utmi_switch; +} + +static struct phy *stm32_usbphyc_of_xlate(struct device *dev, + struct of_phandle_args *args) +{ + struct stm32_usbphyc *usbphyc = dev_get_drvdata(dev); + struct stm32_usbphyc_phy *usbphyc_phy = NULL; + struct device_node *phynode = args->np; + + int port = 0; + + for (port = 0; port < usbphyc->nphys; port++) { + if (phynode == usbphyc->phys[port]->phy->dev.of_node) { + usbphyc_phy = usbphyc->phys[port]; + break; + } + } + if (!usbphyc_phy) { + dev_err(dev, "failed to find phy\n"); + return ERR_PTR(-EINVAL); + } + + if (((usbphyc_phy->index == 0) && (args->args_count != 0)) || + ((usbphyc_phy->index == 1) && (args->args_count != 1))) { + dev_err(dev, "invalid number of cells for phy port%d\n", + usbphyc_phy->index); + return ERR_PTR(-EINVAL); + } + + /* Configure the UTMI switch for PHY port#2 */ + if (usbphyc_phy->index == 1) { + if (usbphyc->switch_setup < 0) { + stm32_usbphyc_switch_setup(usbphyc, args->args[0]); + } else { + if (args->args[0] != usbphyc->switch_setup) { + dev_err(dev, "phy port1 already used\n"); + return ERR_PTR(-EBUSY); + } + } + } + + return usbphyc_phy->phy; +} + +static int stm32_usbphyc_probe(struct platform_device *pdev) +{ + struct stm32_usbphyc *usbphyc; + struct device *dev = &pdev->dev; + struct device_node *child, *np = dev->of_node; + struct resource *res; + struct phy_provider *phy_provider; + u32 version; + int ret, port = 0; + + usbphyc = devm_kzalloc(dev, sizeof(*usbphyc), GFP_KERNEL); + if (!usbphyc) + return -ENOMEM; + usbphyc->dev = dev; + dev_set_drvdata(dev, usbphyc); + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + usbphyc->base = devm_ioremap_resource(dev, res); + if (IS_ERR(usbphyc->base)) + return PTR_ERR(usbphyc->base); + + usbphyc->clk = devm_clk_get(dev, 0); + if (IS_ERR(usbphyc->clk)) { + ret = PTR_ERR(usbphyc->clk); + dev_err(dev, "clk get failed: %d\n", ret); + return ret; + } + + ret = clk_prepare_enable(usbphyc->clk); + if (ret) { + dev_err(dev, "clk enable failed: %d\n", ret); + return ret; + } + + usbphyc->rst = devm_reset_control_get(dev, 0); + if (!IS_ERR(usbphyc->rst)) { + reset_control_assert(usbphyc->rst); + udelay(2); + reset_control_deassert(usbphyc->rst); + } + + usbphyc->switch_setup = -EINVAL; + usbphyc->nphys = of_get_child_count(np); + usbphyc->phys = devm_kcalloc(dev, usbphyc->nphys, + sizeof(*usbphyc->phys), GFP_KERNEL); + if (!usbphyc->phys) { + ret = -ENOMEM; + goto clk_disable; + } + + for_each_child_of_node(np, child) { + struct stm32_usbphyc_phy *usbphyc_phy; + struct phy *phy; + u32 index; + int i; + + phy = devm_phy_create(dev, child, &stm32_usbphyc_phy_ops); + if (IS_ERR(phy)) { + ret = PTR_ERR(phy); + if (ret != -EPROBE_DEFER) + dev_err(dev, + "failed to create phy%d: %d\n", i, ret); + goto put_child; + } + + usbphyc_phy = devm_kzalloc(dev, sizeof(*usbphyc_phy), + GFP_KERNEL); + if (!usbphyc_phy) { + ret = -ENOMEM; + goto put_child; + } + + for (i = 0; i < NUM_SUPPLIES; i++) + usbphyc_phy->supplies[i].supply = supplies_names[i]; + + ret = devm_regulator_bulk_get(&phy->dev, NUM_SUPPLIES, + usbphyc_phy->supplies); + if (ret) { + if (ret != -EPROBE_DEFER) + dev_err(&phy->dev, + "failed to get regulators: %d\n", ret); + goto put_child; + } + + ret = of_property_read_u32(child, "reg", &index); + if (ret || index > usbphyc->nphys) { + dev_err(&phy->dev, "invalid reg property: %d\n", ret); + goto put_child; + } + + usbphyc->phys[port] = usbphyc_phy; + phy_set_bus_width(phy, 8); + phy_set_drvdata(phy, usbphyc_phy); + + usbphyc->phys[port]->phy = phy; + usbphyc->phys[port]->usbphyc = usbphyc; + usbphyc->phys[port]->index = index; + usbphyc->phys[port]->active = false; + + port++; + } + + phy_provider = devm_of_phy_provider_register(dev, + stm32_usbphyc_of_xlate); + if (IS_ERR(phy_provider)) { + ret = PTR_ERR(phy_provider); + dev_err(dev, "failed to register phy provider: %d\n", ret); + goto clk_disable; + } + + version = readl_relaxed(usbphyc->base + STM32_USBPHYC_VERSION); + dev_info(dev, "registered rev:%lu.%lu\n", + FIELD_GET(MAJREV, version), FIELD_GET(MINREV, version)); + + return 0; + +put_child: + of_node_put(child); +clk_disable: + clk_disable_unprepare(usbphyc->clk); + + return ret; +} + +static int stm32_usbphyc_remove(struct platform_device *pdev) +{ + struct stm32_usbphyc *usbphyc = dev_get_drvdata(&pdev->dev); + + clk_disable_unprepare(usbphyc->clk); + + return 0; +} + +static const struct of_device_id stm32_usbphyc_of_match[] = { + { .compatible = "st,stm32mp1-usbphyc", }, + { }, +}; +MODULE_DEVICE_TABLE(of, stm32_usbphyc_of_match); + +static struct platform_driver stm32_usbphyc_driver = { + .probe = stm32_usbphyc_probe, + .remove = stm32_usbphyc_remove, + .driver = { + .of_match_table = stm32_usbphyc_of_match, + .name = "stm32-usbphyc", + } +}; +module_platform_driver(stm32_usbphyc_driver); + +MODULE_DESCRIPTION("STMicroelectronics STM32 USBPHYC driver"); +MODULE_AUTHOR("Amelie Delaunay "); +MODULE_LICENSE("GPL v2");