|  | // SPDX-License-Identifier: GPL-2.0 | 
|  | /* | 
|  | * Copyright (c) 2019 MediaTek Inc. | 
|  | * Author: Pierre Lee <pierre.lee@mediatek.com> | 
|  | */ | 
|  |  | 
|  | #include <linux/delay.h> | 
|  | #include <linux/iopoll.h> | 
|  | #include "clk-fhctl.h" | 
|  |  | 
|  | #define PERCENT_TO_DDSLMT(dds, percent_m10) \ | 
|  | ((((dds) * (percent_m10)) >> 5) / 100) | 
|  |  | 
|  |  | 
|  | #define POSDIV_LSB 24 | 
|  | #define POSDIV_MASK 0x7 | 
|  |  | 
|  |  | 
|  | static int mt_fh_ipi_init(void) | 
|  | { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  |  | 
|  | static int __mt_fh_hw_hopping(struct clk_mt_fhctl *fh, | 
|  | int pll_id, unsigned int new_dds, | 
|  | int postdiv) | 
|  | { | 
|  | unsigned int dds_mask, con_temp; | 
|  | unsigned int mon_dds = 0; | 
|  | int ret; | 
|  | struct clk_mt_fhctl_regs *fh_regs; | 
|  | struct clk_mt_fhctl_pll_data *pll_data; | 
|  |  | 
|  | fh_regs = fh->fh_regs; | 
|  | pll_data = fh->pll_data; | 
|  | dds_mask = fh->pll_data->dds_mask; | 
|  |  | 
|  | if (new_dds > dds_mask) | 
|  | return -EINVAL; | 
|  |  | 
|  | /* 1. sync ncpo to DDS of FHCTL */ | 
|  | writel((readl(fh_regs->reg_con_pcw) & pll_data->dds_mask) | | 
|  | FH_FHCTLX_PLL_TGL_ORG, fh_regs->reg_dds); | 
|  |  | 
|  | /* 2. enable DVFS and Hopping control */ | 
|  |  | 
|  | /* enable dvfs mode */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_SFSTRX_EN, 1); | 
|  | /* enable hopping */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FHCTLX_EN, 1); | 
|  |  | 
|  | /* for slope setting. */ | 
|  |  | 
|  | writel(pll_data->slope0_value, fh_regs->reg_slope0); | 
|  | /* SLOPE1 is for MEMPLL */ | 
|  | writel(pll_data->slope1_value, fh_regs->reg_slope1); | 
|  |  | 
|  | /* 3. switch to hopping control */ | 
|  | fh_set_field(fh_regs->reg_hp_en, (0x1U << pll_id), | 
|  | REG_HP_EN_FHCTL_CTR); | 
|  |  | 
|  | /* 4. set DFS DDS */ | 
|  | writel((new_dds) | (FH_FHCTLX_PLL_DVFS_TRI), fh_regs->reg_dvfs); | 
|  |  | 
|  | /* 4.1 ensure jump to target DDS */ | 
|  | /* Wait 1000 us until DDS stable */ | 
|  | ret = readl_poll_timeout_atomic(fh_regs->reg_mon, mon_dds, | 
|  | (mon_dds&pll_data->dds_mask) == new_dds, 10, 1000); | 
|  | if (ret) | 
|  | pr_info("ERROR %s: target_dds=0x%x, mon_dds=0x%x", | 
|  | __func__, new_dds, (mon_dds&pll_data->dds_mask)); | 
|  |  | 
|  | if (postdiv == -1) { | 
|  | /* Don't change DIV for fhctl UT */ | 
|  | con_temp = readl(fh_regs->reg_con_pcw) & (~dds_mask); | 
|  | con_temp = (con_temp | | 
|  | (readl(fh_regs->reg_mon) & dds_mask) | | 
|  | FH_XXPLL_CON_PCWCHG); | 
|  | } else { | 
|  | con_temp = readl(fh_regs->reg_con_pcw) & (~dds_mask); | 
|  | con_temp = con_temp & ~(POSDIV_MASK << POSDIV_LSB); | 
|  | con_temp = (con_temp | | 
|  | (readl(fh_regs->reg_mon) & dds_mask) | | 
|  | (postdiv & POSDIV_MASK) << POSDIV_LSB | | 
|  | FH_XXPLL_CON_PCWCHG); | 
|  | } | 
|  |  | 
|  | /* 5. write back to ncpo */ | 
|  | writel(con_temp, fh_regs->reg_con_pcw); | 
|  |  | 
|  | /* 6. switch to APMIXEDSYS control */ | 
|  | fh_set_field(fh_regs->reg_hp_en, BIT(pll_id), | 
|  | REG_HP_EN_APMIXEDSYS_CTR); | 
|  |  | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  |  | 
|  | static int clk_mt_fh_hw_pll_init(struct clk_mt_fhctl *fh) | 
|  | { | 
|  | struct clk_mt_fhctl_regs *fh_regs; | 
|  | struct clk_mt_fhctl_pll_data *pll_data; | 
|  | int pll_id; | 
|  | unsigned int mask; | 
|  |  | 
|  | pr_debug("mt_fh_pll_init() start "); | 
|  |  | 
|  | pll_id = fh->pll_data->pll_id; | 
|  | fh_regs = fh->fh_regs; | 
|  | pll_data = fh->pll_data; | 
|  |  | 
|  | mask = 1 << pll_id; | 
|  |  | 
|  | if (fh_regs == NULL) { | 
|  | pr_info("ERROR fh_reg (%d) is NULL", pll_id); | 
|  | return -EFAULT; | 
|  | } | 
|  |  | 
|  | if (pll_data == NULL) { | 
|  | pr_info("ERROR pll_data (%d) is NULL", pll_id); | 
|  | return -EFAULT; | 
|  | } | 
|  |  | 
|  | fh_set_field(fh_regs->reg_clk_con, mask, 1); | 
|  |  | 
|  | /* Release software-reset to reset */ | 
|  | fh_set_field(fh_regs->reg_rst_con, mask, 0); | 
|  | fh_set_field(fh_regs->reg_rst_con, mask, 1); | 
|  |  | 
|  | writel(0x00000000, fh_regs->reg_cfg);	/* No SSC/FH enabled */ | 
|  | writel(0x00000000, fh_regs->reg_updnlmt); /* clr all setting */ | 
|  | writel(0x00000000, fh_regs->reg_dds);	/* clr all settings */ | 
|  |  | 
|  | /* Check default enable SSC */ | 
|  | if (pll_data->pll_default_ssc_rate != 0) { | 
|  | // Default Enable SSC to 0~-N%; | 
|  | fh->hal_ops->pll_ssc_enable(fh, pll_data->pll_default_ssc_rate); | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int clk_mt_fh_hw_pll_unpause(struct clk_mt_fhctl *fh) | 
|  | { | 
|  | int pll_id; | 
|  | struct clk_mt_fhctl_regs *fh_regs; | 
|  | unsigned long flags = 0; | 
|  |  | 
|  |  | 
|  | pll_id = fh->pll_data->pll_id; | 
|  | fh_regs = fh->fh_regs; | 
|  |  | 
|  | if (fh->pll_data->pll_type != FH_PLL_TYPE_CPU) { | 
|  | pr_info("%s not support unpause.", fh->pll_data->pll_name); | 
|  | return -EFAULT; | 
|  | } | 
|  |  | 
|  | pr_debug("%s fh_pll_id:%d", __func__, pll_id); | 
|  |  | 
|  | spin_lock_irqsave(fh->lock, flags); | 
|  |  | 
|  | /* unpause  */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FHCTLX_CFG_PAUSE, 0); | 
|  |  | 
|  | spin_unlock_irqrestore(fh->lock, flags); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int clk_mt_fh_hw_pll_pause(struct clk_mt_fhctl *fh) | 
|  | { | 
|  | int pll_id; | 
|  | struct clk_mt_fhctl_regs *fh_regs; | 
|  | unsigned long flags = 0; | 
|  |  | 
|  | pll_id = fh->pll_data->pll_id; | 
|  | fh_regs = fh->fh_regs; | 
|  |  | 
|  | if (fh->pll_data->pll_type != FH_PLL_TYPE_CPU) { | 
|  | pr_info("%s not support pause.", fh->pll_data->pll_name); | 
|  | return -EFAULT; | 
|  | } | 
|  |  | 
|  | pr_debug("%s fh_pll_id:%d", __func__, pll_id); | 
|  |  | 
|  | spin_lock_irqsave(fh->lock, flags); | 
|  |  | 
|  | /* pause  */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FHCTLX_CFG_PAUSE, 1); | 
|  |  | 
|  | spin_unlock_irqrestore(fh->lock, flags); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int clk_mt_fh_hw_pll_ssc_disable(struct clk_mt_fhctl *fh) | 
|  | { | 
|  | int pll_id; | 
|  | unsigned long flags = 0; | 
|  | struct clk_mt_fhctl_regs *fh_regs; | 
|  | struct clk_mt_fhctl_pll_data *pll_data; | 
|  |  | 
|  |  | 
|  | pll_id = fh->pll_data->pll_id; | 
|  | fh_regs = fh->fh_regs; | 
|  | pll_data = fh->pll_data; | 
|  |  | 
|  | if (pll_data->pll_type == FH_PLL_TYPE_NOT_SUPPORT) { | 
|  | pr_info("%s not support SSC.", pll_data->pll_name); | 
|  | return -EPERM; | 
|  | } | 
|  |  | 
|  | pr_debug("fh_pll_id:%d", pll_id); | 
|  |  | 
|  | spin_lock_irqsave(fh->lock, flags); | 
|  |  | 
|  | /* Set the relative registers */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FRDDSX_EN, 0); | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FHCTLX_EN, 0); | 
|  |  | 
|  | /* Switch to APMIXEDSYS control */ | 
|  | fh_set_field(fh_regs->reg_hp_en, BIT(pll_id), | 
|  | REG_HP_EN_APMIXEDSYS_CTR); | 
|  |  | 
|  | spin_unlock_irqrestore(fh->lock, flags); | 
|  |  | 
|  | /* Wait for DDS to be stable */ | 
|  | udelay(30); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int clk_mt_fh_hw_pll_ssc_enable(struct clk_mt_fhctl *fh, int ssc_rate) | 
|  | { | 
|  | int pll_id; | 
|  | unsigned long flags = 0; | 
|  | unsigned int dds_mask; | 
|  | unsigned int updnlmt_val; | 
|  | struct clk_mt_fhctl_regs *fh_regs; | 
|  | struct clk_mt_fhctl_pll_data *pll_data; | 
|  |  | 
|  |  | 
|  | pll_id = fh->pll_data->pll_id; | 
|  | fh_regs = fh->fh_regs; | 
|  | pll_data = fh->pll_data; | 
|  | dds_mask = fh->pll_data->dds_mask; | 
|  |  | 
|  | if (pll_data->pll_type == FH_PLL_TYPE_NOT_SUPPORT) { | 
|  | pr_info("%s not support SSC.", pll_data->pll_name); | 
|  | return -EPERM; | 
|  | } | 
|  |  | 
|  | pr_debug("pll_id:%d ssc:0~-%d%%", pll_id, ssc_rate); | 
|  |  | 
|  | spin_lock_irqsave(fh->lock, flags); | 
|  |  | 
|  | /* Set the relative parameter registers (dt/df/upbnd/downbnd) */ | 
|  | fh_set_field(fh_regs->reg_cfg, MASK_FRDDSX_DYS, REG_CFG_DF_VAL); | 
|  | fh_set_field(fh_regs->reg_cfg, MASK_FRDDSX_DTS, REG_CFG_DT_VAL); | 
|  |  | 
|  | writel((readl(fh_regs->reg_con_pcw) & pll_data->dds_mask) | | 
|  | FH_FHCTLX_PLL_TGL_ORG, fh_regs->reg_dds); | 
|  |  | 
|  | /* Calculate UPDNLMT */ | 
|  | updnlmt_val = PERCENT_TO_DDSLMT((readl(fh_regs->reg_dds) & | 
|  | dds_mask), ssc_rate) << 16; | 
|  |  | 
|  | writel(updnlmt_val, fh_regs->reg_updnlmt); | 
|  |  | 
|  | /* Switch to FHCTL_CORE controller - Original design */ | 
|  | fh_set_field(fh_regs->reg_hp_en, (0x1U << pll_id), | 
|  | REG_HP_EN_FHCTL_CTR); | 
|  |  | 
|  | /* Enable SSC */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FRDDSX_EN, 1); | 
|  | /* Enable Hopping control */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FHCTLX_EN, 1); | 
|  |  | 
|  | /* Keep last ssc rate */ | 
|  | fh->pll_data->pll_default_ssc_rate = ssc_rate; | 
|  |  | 
|  | spin_unlock_irqrestore(fh->lock, flags); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int clk_mt_fh_hw_pll_hopping(struct clk_mt_fhctl *fh, | 
|  | unsigned int new_dds, | 
|  | int postdiv) | 
|  | { | 
|  | int pll_id; | 
|  | int ret; | 
|  | unsigned long flags = 0; | 
|  | unsigned int dds_mask; | 
|  | unsigned int updnlmt_val; | 
|  | unsigned int must_restore_ssc = 0; | 
|  | struct clk_mt_fhctl_regs *fh_regs; | 
|  | struct clk_mt_fhctl_pll_data *pll_data; | 
|  |  | 
|  |  | 
|  | pll_id = fh->pll_data->pll_id; | 
|  | fh_regs = fh->fh_regs; | 
|  | pll_data = fh->pll_data; | 
|  | dds_mask = pll_data->dds_mask; | 
|  |  | 
|  | if ((fh->pll_data->pll_type == FH_PLL_TYPE_NOT_SUPPORT) || | 
|  | (fh->pll_data->pll_type == FH_PLL_TYPE_CPU)) { | 
|  | pr_info("%s not support hopping in AP side", | 
|  | pll_data->pll_name); | 
|  | return -EPERM; | 
|  | } | 
|  |  | 
|  | pr_debug("fh_pll_id:%d", pll_id); | 
|  |  | 
|  | spin_lock_irqsave(fh->lock, flags); | 
|  |  | 
|  | /* Check SSC status. */ | 
|  | fh_get_field(fh_regs->reg_cfg, FH_FRDDSX_EN, must_restore_ssc); | 
|  | if (must_restore_ssc) { | 
|  | unsigned int pll_dds = 0; | 
|  | unsigned int mon_dds = 0; | 
|  |  | 
|  | /* only when SSC is enable, turn off ARMPLL hopping */ | 
|  | /* disable SSC mode */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FRDDSX_EN, 0); | 
|  | /* disable dvfs mode */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_SFSTRX_EN, 0); | 
|  | /* disable hp ctl */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FHCTLX_EN, 0); | 
|  |  | 
|  | pll_dds = (readl(fh_regs->reg_dds)) & pll_data->dds_mask; | 
|  |  | 
|  | /* Wait 1000 us until DDS stable */ | 
|  | ret = readl_poll_timeout_atomic(fh_regs->reg_mon, mon_dds, | 
|  | (mon_dds&pll_data->dds_mask) == pll_dds, 10, 1000); | 
|  | if (ret) | 
|  | pr_info("ERROR %s: target_dds=0x%x, mon_dds=0x%x", | 
|  | __func__, pll_dds, mon_dds&pll_data->dds_mask); | 
|  |  | 
|  | } | 
|  |  | 
|  | ret = __mt_fh_hw_hopping(fh, pll_id, new_dds, postdiv); | 
|  | if (ret) | 
|  | pr_info("__mt_fh_hw_hopping error:%d", ret); | 
|  |  | 
|  |  | 
|  | /* Enable SSC status, if need. */ | 
|  | if (must_restore_ssc) { | 
|  | /* disable SSC mode */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FRDDSX_EN, 0); | 
|  | /* disable dvfs mode */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_SFSTRX_EN, 0); | 
|  | /* disable hp ctl */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FHCTLX_EN, 0); | 
|  |  | 
|  | fh_set_field(fh_regs->reg_cfg, MASK_FRDDSX_DYS, REG_CFG_DF_VAL); | 
|  | fh_set_field(fh_regs->reg_cfg, MASK_FRDDSX_DTS, REG_CFG_DT_VAL); | 
|  |  | 
|  | writel((readl(fh_regs->reg_con_pcw) & pll_data->dds_mask) | | 
|  | FH_FHCTLX_PLL_TGL_ORG, fh_regs->reg_dds); | 
|  |  | 
|  | /* Calculate UPDNLMT */ | 
|  | updnlmt_val = PERCENT_TO_DDSLMT( | 
|  | (readl(fh_regs->reg_dds) & dds_mask), | 
|  | pll_data->pll_default_ssc_rate) << 16; | 
|  |  | 
|  | writel(updnlmt_val, fh_regs->reg_updnlmt); | 
|  |  | 
|  | /* Switch to FHCTL_CORE controller - Original design */ | 
|  | fh_set_field(fh_regs->reg_hp_en, | 
|  | (0x1U << pll_id), REG_HP_EN_FHCTL_CTR); | 
|  |  | 
|  | /* enable SSC mode */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FRDDSX_EN, 1); | 
|  | /* enable hopping ctl */ | 
|  | fh_set_field(fh_regs->reg_cfg, FH_FHCTLX_EN, 1); | 
|  | } | 
|  |  | 
|  | spin_unlock_irqrestore(fh->lock, flags); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | const struct fhctl_ipi_ops ipi_ops = { | 
|  | .ipi_init = mt_fh_ipi_init, | 
|  | }; | 
|  |  | 
|  | const struct clk_mt_fhctl_hal_ops mt_fhctl_hal_ops = { | 
|  | .pll_init = clk_mt_fh_hw_pll_init, | 
|  | .pll_unpause = clk_mt_fh_hw_pll_unpause, | 
|  | .pll_pause = clk_mt_fh_hw_pll_pause, | 
|  | .pll_ssc_disable = clk_mt_fh_hw_pll_ssc_disable, | 
|  | .pll_ssc_enable = clk_mt_fh_hw_pll_ssc_enable, | 
|  | .pll_hopping = clk_mt_fh_hw_pll_hopping, | 
|  | }; | 
|  |  |