diff options
Diffstat (limited to 'drivers/phy/samsung/phy-samsung-usb2.c')
-rw-r--r-- | drivers/phy/samsung/phy-samsung-usb2.c | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/drivers/phy/samsung/phy-samsung-usb2.c b/drivers/phy/samsung/phy-samsung-usb2.c new file mode 100644 index 000000000..ea8188669 --- /dev/null +++ b/drivers/phy/samsung/phy-samsung-usb2.c @@ -0,0 +1,264 @@ +/* + * Samsung SoC USB 1.1/2.0 PHY driver + * + * Copyright (C) 2013 Samsung Electronics Co., Ltd. + * Author: Kamil Debski <k.debski@samsung.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include <linux/clk.h> +#include <linux/mfd/syscon.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/phy/phy.h> +#include <linux/platform_device.h> +#include <linux/spinlock.h> +#include "phy-samsung-usb2.h" + +static int samsung_usb2_phy_power_on(struct phy *phy) +{ + struct samsung_usb2_phy_instance *inst = phy_get_drvdata(phy); + struct samsung_usb2_phy_driver *drv = inst->drv; + int ret; + + dev_dbg(drv->dev, "Request to power_on \"%s\" usb phy\n", + inst->cfg->label); + + if (drv->vbus) { + ret = regulator_enable(drv->vbus); + if (ret) + goto err_regulator; + } + + ret = clk_prepare_enable(drv->clk); + if (ret) + goto err_main_clk; + ret = clk_prepare_enable(drv->ref_clk); + if (ret) + goto err_instance_clk; + if (inst->cfg->power_on) { + spin_lock(&drv->lock); + ret = inst->cfg->power_on(inst); + spin_unlock(&drv->lock); + if (ret) + goto err_power_on; + } + + return 0; + +err_power_on: + clk_disable_unprepare(drv->ref_clk); +err_instance_clk: + clk_disable_unprepare(drv->clk); +err_main_clk: + if (drv->vbus) + regulator_disable(drv->vbus); +err_regulator: + return ret; +} + +static int samsung_usb2_phy_power_off(struct phy *phy) +{ + struct samsung_usb2_phy_instance *inst = phy_get_drvdata(phy); + struct samsung_usb2_phy_driver *drv = inst->drv; + int ret = 0; + + dev_dbg(drv->dev, "Request to power_off \"%s\" usb phy\n", + inst->cfg->label); + if (inst->cfg->power_off) { + spin_lock(&drv->lock); + ret = inst->cfg->power_off(inst); + spin_unlock(&drv->lock); + if (ret) + return ret; + } + clk_disable_unprepare(drv->ref_clk); + clk_disable_unprepare(drv->clk); + if (drv->vbus) + ret = regulator_disable(drv->vbus); + + return ret; +} + +static const struct phy_ops samsung_usb2_phy_ops = { + .power_on = samsung_usb2_phy_power_on, + .power_off = samsung_usb2_phy_power_off, + .owner = THIS_MODULE, +}; + +static struct phy *samsung_usb2_phy_xlate(struct device *dev, + struct of_phandle_args *args) +{ + struct samsung_usb2_phy_driver *drv; + + drv = dev_get_drvdata(dev); + if (!drv) + return ERR_PTR(-EINVAL); + + if (WARN_ON(args->args[0] >= drv->cfg->num_phys)) + return ERR_PTR(-ENODEV); + + return drv->instances[args->args[0]].phy; +} + +static const struct of_device_id samsung_usb2_phy_of_match[] = { +#ifdef CONFIG_PHY_EXYNOS4X12_USB2 + { + .compatible = "samsung,exynos3250-usb2-phy", + .data = &exynos3250_usb2_phy_config, + }, +#endif +#ifdef CONFIG_PHY_EXYNOS4210_USB2 + { + .compatible = "samsung,exynos4210-usb2-phy", + .data = &exynos4210_usb2_phy_config, + }, +#endif +#ifdef CONFIG_PHY_EXYNOS4X12_USB2 + { + .compatible = "samsung,exynos4x12-usb2-phy", + .data = &exynos4x12_usb2_phy_config, + }, +#endif +#ifdef CONFIG_PHY_EXYNOS5250_USB2 + { + .compatible = "samsung,exynos5250-usb2-phy", + .data = &exynos5250_usb2_phy_config, + }, +#endif +#ifdef CONFIG_PHY_S5PV210_USB2 + { + .compatible = "samsung,s5pv210-usb2-phy", + .data = &s5pv210_usb2_phy_config, + }, +#endif + { }, +}; +MODULE_DEVICE_TABLE(of, samsung_usb2_phy_of_match); + +static int samsung_usb2_phy_probe(struct platform_device *pdev) +{ + const struct samsung_usb2_phy_config *cfg; + struct device *dev = &pdev->dev; + struct phy_provider *phy_provider; + struct resource *mem; + struct samsung_usb2_phy_driver *drv; + int i, ret; + + if (!pdev->dev.of_node) { + dev_err(dev, "This driver is required to be instantiated from device tree\n"); + return -EINVAL; + } + + cfg = of_device_get_match_data(dev); + if (!cfg) + return -EINVAL; + + drv = devm_kzalloc(dev, sizeof(struct samsung_usb2_phy_driver) + + cfg->num_phys * sizeof(struct samsung_usb2_phy_instance), + GFP_KERNEL); + if (!drv) + return -ENOMEM; + + dev_set_drvdata(dev, drv); + spin_lock_init(&drv->lock); + + drv->cfg = cfg; + drv->dev = dev; + + mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); + drv->reg_phy = devm_ioremap_resource(dev, mem); + if (IS_ERR(drv->reg_phy)) { + dev_err(dev, "Failed to map register memory (phy)\n"); + return PTR_ERR(drv->reg_phy); + } + + drv->reg_pmu = syscon_regmap_lookup_by_phandle(pdev->dev.of_node, + "samsung,pmureg-phandle"); + if (IS_ERR(drv->reg_pmu)) { + dev_err(dev, "Failed to map PMU registers (via syscon)\n"); + return PTR_ERR(drv->reg_pmu); + } + + if (drv->cfg->has_mode_switch) { + drv->reg_sys = syscon_regmap_lookup_by_phandle( + pdev->dev.of_node, "samsung,sysreg-phandle"); + if (IS_ERR(drv->reg_sys)) { + dev_err(dev, "Failed to map system registers (via syscon)\n"); + return PTR_ERR(drv->reg_sys); + } + } + + drv->clk = devm_clk_get(dev, "phy"); + if (IS_ERR(drv->clk)) { + dev_err(dev, "Failed to get clock of phy controller\n"); + return PTR_ERR(drv->clk); + } + + drv->ref_clk = devm_clk_get(dev, "ref"); + if (IS_ERR(drv->ref_clk)) { + dev_err(dev, "Failed to get reference clock for the phy controller\n"); + return PTR_ERR(drv->ref_clk); + } + + drv->ref_rate = clk_get_rate(drv->ref_clk); + if (drv->cfg->rate_to_clk) { + ret = drv->cfg->rate_to_clk(drv->ref_rate, &drv->ref_reg_val); + if (ret) + return ret; + } + + drv->vbus = devm_regulator_get(dev, "vbus"); + if (IS_ERR(drv->vbus)) { + ret = PTR_ERR(drv->vbus); + if (ret == -EPROBE_DEFER) + return ret; + drv->vbus = NULL; + } + + for (i = 0; i < drv->cfg->num_phys; i++) { + char *label = drv->cfg->phys[i].label; + struct samsung_usb2_phy_instance *p = &drv->instances[i]; + + dev_dbg(dev, "Creating phy \"%s\"\n", label); + p->phy = devm_phy_create(dev, NULL, &samsung_usb2_phy_ops); + if (IS_ERR(p->phy)) { + dev_err(drv->dev, "Failed to create usb2_phy \"%s\"\n", + label); + return PTR_ERR(p->phy); + } + + p->cfg = &drv->cfg->phys[i]; + p->drv = drv; + phy_set_bus_width(p->phy, 8); + phy_set_drvdata(p->phy, p); + } + + phy_provider = devm_of_phy_provider_register(dev, + samsung_usb2_phy_xlate); + if (IS_ERR(phy_provider)) { + dev_err(drv->dev, "Failed to register phy provider\n"); + return PTR_ERR(phy_provider); + } + + return 0; +} + +static struct platform_driver samsung_usb2_phy_driver = { + .probe = samsung_usb2_phy_probe, + .driver = { + .of_match_table = samsung_usb2_phy_of_match, + .name = "samsung-usb2-phy", + } +}; + +module_platform_driver(samsung_usb2_phy_driver); +MODULE_DESCRIPTION("Samsung S5P/EXYNOS SoC USB PHY driver"); +MODULE_AUTHOR("Kamil Debski <k.debski@samsung.com>"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:samsung-usb2-phy"); |