| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * UFS PHY driver for Samsung SoC |
| * |
| * Copyright (C) 2020 Samsung Electronics Co., Ltd. |
| * Author: Seungwon Jeon <essuuj@gmail.com> |
| * Author: Alim Akhtar <alim.akhtar@samsung.com> |
| * |
| */ |
| #include <linux/clk.h> |
| #include <linux/delay.h> |
| #include <linux/err.h> |
| #include <linux/of.h> |
| #include <linux/io.h> |
| #include <linux/iopoll.h> |
| #include <linux/mfd/syscon.h> |
| #include <linux/module.h> |
| #include <linux/phy/phy.h> |
| #include <linux/platform_device.h> |
| #include <linux/regmap.h> |
| |
| #include "phy-samsung-ufs.h" |
| |
| #define for_each_phy_lane(phy, i) \ |
| for (i = 0; i < (phy)->lane_cnt; i++) |
| #define for_each_phy_cfg(cfg) \ |
| for (; (cfg)->id; (cfg)++) |
| |
| #define PHY_DEF_LANE_CNT 1 |
| |
| static void samsung_ufs_phy_config(struct samsung_ufs_phy *phy, |
| const struct samsung_ufs_phy_cfg *cfg, |
| u8 lane) |
| { |
| enum {LANE_0, LANE_1}; /* lane index */ |
| |
| switch (lane) { |
| case LANE_0: |
| writel(cfg->val, (phy)->reg_pma + cfg->off_0); |
| break; |
| case LANE_1: |
| if (cfg->id == PHY_TRSV_BLK) |
| writel(cfg->val, (phy)->reg_pma + cfg->off_1); |
| break; |
| } |
| } |
| |
| static int samsung_ufs_phy_wait_for_lock_acq(struct phy *phy) |
| { |
| struct samsung_ufs_phy *ufs_phy = get_samsung_ufs_phy(phy); |
| const unsigned int timeout_us = 100000; |
| const unsigned int sleep_us = 10; |
| u32 val; |
| int err; |
| |
| err = readl_poll_timeout( |
| ufs_phy->reg_pma + PHY_APB_ADDR(PHY_PLL_LOCK_STATUS), |
| val, (val & PHY_PLL_LOCK_BIT), sleep_us, timeout_us); |
| if (err) { |
| dev_err(ufs_phy->dev, |
| "failed to get phy pll lock acquisition %d\n", err); |
| goto out; |
| } |
| |
| err = readl_poll_timeout( |
| ufs_phy->reg_pma + PHY_APB_ADDR(PHY_CDR_LOCK_STATUS), |
| val, (val & PHY_CDR_LOCK_BIT), sleep_us, timeout_us); |
| if (err) |
| dev_err(ufs_phy->dev, |
| "failed to get phy cdr lock acquisition %d\n", err); |
| out: |
| return err; |
| } |
| |
| static int samsung_ufs_phy_calibrate(struct phy *phy) |
| { |
| struct samsung_ufs_phy *ufs_phy = get_samsung_ufs_phy(phy); |
| struct samsung_ufs_phy_cfg **cfgs = ufs_phy->cfg; |
| const struct samsung_ufs_phy_cfg *cfg; |
| int err = 0; |
| int i; |
| |
| if (unlikely(ufs_phy->ufs_phy_state < CFG_PRE_INIT || |
| ufs_phy->ufs_phy_state >= CFG_TAG_MAX)) { |
| dev_err(ufs_phy->dev, "invalid phy config index %d\n", ufs_phy->ufs_phy_state); |
| return -EINVAL; |
| } |
| |
| cfg = cfgs[ufs_phy->ufs_phy_state]; |
| if (!cfg) |
| goto out; |
| |
| for_each_phy_cfg(cfg) { |
| for_each_phy_lane(ufs_phy, i) { |
| samsung_ufs_phy_config(ufs_phy, cfg, i); |
| } |
| } |
| |
| if (ufs_phy->ufs_phy_state == CFG_POST_PWR_HS) |
| err = samsung_ufs_phy_wait_for_lock_acq(phy); |
| |
| /** |
| * In Samsung ufshci, PHY need to be calibrated at different |
| * stages / state mainly before Linkstartup, after Linkstartup, |
| * before power mode change and after power mode change. |
| * Below state machine to make sure to calibrate PHY in each |
| * state. Here after configuring PHY in a given state, will |
| * change the state to next state so that next state phy |
| * calibration value can be programed |
| */ |
| out: |
| switch (ufs_phy->ufs_phy_state) { |
| case CFG_PRE_INIT: |
| ufs_phy->ufs_phy_state = CFG_POST_INIT; |
| break; |
| case CFG_POST_INIT: |
| ufs_phy->ufs_phy_state = CFG_PRE_PWR_HS; |
| break; |
| case CFG_PRE_PWR_HS: |
| ufs_phy->ufs_phy_state = CFG_POST_PWR_HS; |
| break; |
| case CFG_POST_PWR_HS: |
| /* Change back to INIT state */ |
| ufs_phy->ufs_phy_state = CFG_PRE_INIT; |
| break; |
| default: |
| dev_err(ufs_phy->dev, "wrong state for phy calibration\n"); |
| } |
| |
| return err; |
| } |
| |
| static int samsung_ufs_phy_symbol_clk_init(struct samsung_ufs_phy *phy) |
| { |
| int ret; |
| |
| phy->tx0_symbol_clk = devm_clk_get(phy->dev, "tx0_symbol_clk"); |
| if (IS_ERR(phy->tx0_symbol_clk)) { |
| dev_err(phy->dev, "failed to get tx0_symbol_clk clock\n"); |
| return PTR_ERR(phy->tx0_symbol_clk); |
| } |
| |
| phy->rx0_symbol_clk = devm_clk_get(phy->dev, "rx0_symbol_clk"); |
| if (IS_ERR(phy->rx0_symbol_clk)) { |
| dev_err(phy->dev, "failed to get rx0_symbol_clk clock\n"); |
| return PTR_ERR(phy->rx0_symbol_clk); |
| } |
| |
| phy->rx1_symbol_clk = devm_clk_get(phy->dev, "rx1_symbol_clk"); |
| if (IS_ERR(phy->rx1_symbol_clk)) { |
| dev_err(phy->dev, "failed to get rx1_symbol_clk clock\n"); |
| return PTR_ERR(phy->rx1_symbol_clk); |
| } |
| |
| ret = clk_prepare_enable(phy->tx0_symbol_clk); |
| if (ret) { |
| dev_err(phy->dev, "%s: tx0_symbol_clk enable failed %d\n", __func__, ret); |
| goto out; |
| } |
| |
| ret = clk_prepare_enable(phy->rx0_symbol_clk); |
| if (ret) { |
| dev_err(phy->dev, "%s: rx0_symbol_clk enable failed %d\n", __func__, ret); |
| goto out_disable_tx0_clk; |
| } |
| |
| ret = clk_prepare_enable(phy->rx1_symbol_clk); |
| if (ret) { |
| dev_err(phy->dev, "%s: rx1_symbol_clk enable failed %d\n", __func__, ret); |
| goto out_disable_rx0_clk; |
| } |
| |
| return 0; |
| |
| out_disable_rx0_clk: |
| clk_disable_unprepare(phy->rx0_symbol_clk); |
| out_disable_tx0_clk: |
| clk_disable_unprepare(phy->tx0_symbol_clk); |
| out: |
| return ret; |
| } |
| |
| static int samsung_ufs_phy_clks_init(struct samsung_ufs_phy *phy) |
| { |
| int ret; |
| |
| phy->ref_clk = devm_clk_get(phy->dev, "ref_clk"); |
| if (IS_ERR(phy->ref_clk)) |
| dev_err(phy->dev, "failed to get ref_clk clock\n"); |
| |
| ret = clk_prepare_enable(phy->ref_clk); |
| if (ret) { |
| dev_err(phy->dev, "%s: ref_clk enable failed %d\n", __func__, ret); |
| return ret; |
| } |
| |
| dev_dbg(phy->dev, "UFS MPHY ref_clk_rate = %ld\n", clk_get_rate(phy->ref_clk)); |
| |
| return 0; |
| } |
| |
| static int samsung_ufs_phy_init(struct phy *phy) |
| { |
| struct samsung_ufs_phy *ss_phy = get_samsung_ufs_phy(phy); |
| int ret; |
| |
| ss_phy->lane_cnt = phy->attrs.bus_width; |
| ss_phy->ufs_phy_state = CFG_PRE_INIT; |
| |
| if (ss_phy->drvdata->has_symbol_clk) { |
| ret = samsung_ufs_phy_symbol_clk_init(ss_phy); |
| if (ret) |
| dev_err(ss_phy->dev, "failed to set ufs phy symbol clocks\n"); |
| } |
| |
| ret = samsung_ufs_phy_clks_init(ss_phy); |
| if (ret) |
| dev_err(ss_phy->dev, "failed to set ufs phy clocks\n"); |
| |
| ret = samsung_ufs_phy_calibrate(phy); |
| if (ret) |
| dev_err(ss_phy->dev, "ufs phy calibration failed\n"); |
| |
| return ret; |
| } |
| |
| static int samsung_ufs_phy_power_on(struct phy *phy) |
| { |
| struct samsung_ufs_phy *ss_phy = get_samsung_ufs_phy(phy); |
| |
| samsung_ufs_phy_ctrl_isol(ss_phy, false); |
| return 0; |
| } |
| |
| static int samsung_ufs_phy_power_off(struct phy *phy) |
| { |
| struct samsung_ufs_phy *ss_phy = get_samsung_ufs_phy(phy); |
| |
| samsung_ufs_phy_ctrl_isol(ss_phy, true); |
| return 0; |
| } |
| |
| static int samsung_ufs_phy_set_mode(struct phy *generic_phy, |
| enum phy_mode mode, int submode) |
| { |
| struct samsung_ufs_phy *ss_phy = get_samsung_ufs_phy(generic_phy); |
| |
| ss_phy->mode = PHY_MODE_INVALID; |
| |
| if (mode > 0) |
| ss_phy->mode = mode; |
| |
| return 0; |
| } |
| |
| static int samsung_ufs_phy_exit(struct phy *phy) |
| { |
| struct samsung_ufs_phy *ss_phy = get_samsung_ufs_phy(phy); |
| |
| clk_disable_unprepare(ss_phy->ref_clk); |
| |
| if (ss_phy->drvdata->has_symbol_clk) { |
| clk_disable_unprepare(ss_phy->tx0_symbol_clk); |
| clk_disable_unprepare(ss_phy->rx0_symbol_clk); |
| clk_disable_unprepare(ss_phy->rx1_symbol_clk); |
| } |
| |
| return 0; |
| } |
| |
| static struct phy_ops samsung_ufs_phy_ops = { |
| .init = samsung_ufs_phy_init, |
| .exit = samsung_ufs_phy_exit, |
| .power_on = samsung_ufs_phy_power_on, |
| .power_off = samsung_ufs_phy_power_off, |
| .calibrate = samsung_ufs_phy_calibrate, |
| .set_mode = samsung_ufs_phy_set_mode, |
| .owner = THIS_MODULE, |
| }; |
| |
| static const struct of_device_id samsung_ufs_phy_match[]; |
| |
| static int samsung_ufs_phy_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| const struct of_device_id *match; |
| struct samsung_ufs_phy *phy; |
| struct phy *gen_phy; |
| struct phy_provider *phy_provider; |
| const struct samsung_ufs_phy_drvdata *drvdata; |
| int err = 0; |
| |
| match = of_match_node(samsung_ufs_phy_match, dev->of_node); |
| if (!match) { |
| err = -EINVAL; |
| dev_err(dev, "failed to get match_node\n"); |
| goto out; |
| } |
| |
| phy = devm_kzalloc(dev, sizeof(*phy), GFP_KERNEL); |
| if (!phy) { |
| err = -ENOMEM; |
| goto out; |
| } |
| |
| phy->reg_pma = devm_platform_ioremap_resource_byname(pdev, "phy-pma"); |
| if (IS_ERR(phy->reg_pma)) { |
| err = PTR_ERR(phy->reg_pma); |
| goto out; |
| } |
| |
| phy->reg_pmu = syscon_regmap_lookup_by_phandle( |
| dev->of_node, "samsung,pmu-syscon"); |
| if (IS_ERR(phy->reg_pmu)) { |
| err = PTR_ERR(phy->reg_pmu); |
| dev_err(dev, "failed syscon remap for pmu\n"); |
| goto out; |
| } |
| |
| gen_phy = devm_phy_create(dev, NULL, &samsung_ufs_phy_ops); |
| if (IS_ERR(gen_phy)) { |
| err = PTR_ERR(gen_phy); |
| dev_err(dev, "failed to create PHY for ufs-phy\n"); |
| goto out; |
| } |
| |
| drvdata = match->data; |
| phy->dev = dev; |
| phy->drvdata = drvdata; |
| phy->cfg = (struct samsung_ufs_phy_cfg **)drvdata->cfg; |
| phy->isol = &drvdata->isol; |
| phy->lane_cnt = PHY_DEF_LANE_CNT; |
| |
| phy_set_drvdata(gen_phy, phy); |
| |
| phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate); |
| if (IS_ERR(phy_provider)) { |
| err = PTR_ERR(phy_provider); |
| dev_err(dev, "failed to register phy-provider\n"); |
| goto out; |
| } |
| out: |
| return err; |
| } |
| |
| static const struct of_device_id samsung_ufs_phy_match[] = { |
| { |
| .compatible = "samsung,exynos7-ufs-phy", |
| .data = &exynos7_ufs_phy, |
| }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, samsung_ufs_phy_match); |
| |
| static struct platform_driver samsung_ufs_phy_driver = { |
| .probe = samsung_ufs_phy_probe, |
| .driver = { |
| .name = "samsung-ufs-phy", |
| .of_match_table = samsung_ufs_phy_match, |
| }, |
| }; |
| module_platform_driver(samsung_ufs_phy_driver); |
| MODULE_DESCRIPTION("Samsung SoC UFS PHY Driver"); |
| MODULE_AUTHOR("Seungwon Jeon <essuuj@gmail.com>"); |
| MODULE_AUTHOR("Alim Akhtar <alim.akhtar@samsung.com>"); |
| MODULE_LICENSE("GPL v2"); |