| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Sophgo SG2042 PLL clock Driver |
| * |
| * Copyright (C) 2024 Sophgo Technology Inc. |
| * Copyright (C) 2024 Chen Wang <unicorn_wang@outlook.com> |
| */ |
| |
| #include <linux/array_size.h> |
| #include <linux/bitfield.h> |
| #include <linux/bits.h> |
| #include <linux/clk-provider.h> |
| #include <linux/io.h> |
| #include <linux/iopoll.h> |
| #include <linux/platform_device.h> |
| #include <asm/div64.h> |
| |
| #include <dt-bindings/clock/sophgo,sg2042-pll.h> |
| |
| #include "clk-sg2042.h" |
| |
| /* Registers defined in SYS_CTRL */ |
| #define R_PLL_BEGIN 0xC0 |
| #define R_PLL_STAT (0xC0 - R_PLL_BEGIN) |
| #define R_PLL_CLKEN_CONTROL (0xC4 - R_PLL_BEGIN) |
| #define R_MPLL_CONTROL (0xE8 - R_PLL_BEGIN) |
| #define R_FPLL_CONTROL (0xF4 - R_PLL_BEGIN) |
| #define R_DPLL0_CONTROL (0xF8 - R_PLL_BEGIN) |
| #define R_DPLL1_CONTROL (0xFC - R_PLL_BEGIN) |
| |
| /** |
| * struct sg2042_pll_clock - PLL clock |
| * @hw: clk_hw for initialization |
| * @id: used to map clk_onecell_data |
| * @base: used for readl/writel. |
| * **NOTE**: PLL registers are all in SYS_CTRL! |
| * @lock: spinlock to protect register access, modification |
| * of frequency can only be served one at the time. |
| * @offset_ctrl: offset of pll control registers |
| * @shift_status_lock: shift of XXX_LOCK in pll status register |
| * @shift_status_updating: shift of UPDATING_XXX in pll status register |
| * @shift_enable: shift of XXX_CLK_EN in pll enable register |
| */ |
| struct sg2042_pll_clock { |
| struct clk_hw hw; |
| |
| unsigned int id; |
| void __iomem *base; |
| /* protect register access */ |
| spinlock_t *lock; |
| |
| u32 offset_ctrl; |
| u8 shift_status_lock; |
| u8 shift_status_updating; |
| u8 shift_enable; |
| }; |
| |
| #define to_sg2042_pll_clk(_hw) container_of(_hw, struct sg2042_pll_clock, hw) |
| |
| #define KHZ 1000UL |
| #define MHZ (KHZ * KHZ) |
| |
| #define REFDIV_MIN 1 |
| #define REFDIV_MAX 63 |
| #define FBDIV_MIN 16 |
| #define FBDIV_MAX 320 |
| |
| #define PLL_FREF_SG2042 (25 * MHZ) |
| |
| #define PLL_FOUTPOSTDIV_MIN (16 * MHZ) |
| #define PLL_FOUTPOSTDIV_MAX (3200 * MHZ) |
| |
| #define PLL_FOUTVCO_MIN (800 * MHZ) |
| #define PLL_FOUTVCO_MAX (3200 * MHZ) |
| |
| struct sg2042_pll_ctrl { |
| unsigned long freq; |
| unsigned int fbdiv; |
| unsigned int postdiv1; |
| unsigned int postdiv2; |
| unsigned int refdiv; |
| }; |
| |
| #define PLLCTRL_FBDIV_MASK GENMASK(27, 16) |
| #define PLLCTRL_POSTDIV2_MASK GENMASK(14, 12) |
| #define PLLCTRL_POSTDIV1_MASK GENMASK(10, 8) |
| #define PLLCTRL_REFDIV_MASK GENMASK(5, 0) |
| |
| static inline u32 sg2042_pll_ctrl_encode(struct sg2042_pll_ctrl *ctrl) |
| { |
| return FIELD_PREP(PLLCTRL_FBDIV_MASK, ctrl->fbdiv) | |
| FIELD_PREP(PLLCTRL_POSTDIV2_MASK, ctrl->postdiv2) | |
| FIELD_PREP(PLLCTRL_POSTDIV1_MASK, ctrl->postdiv1) | |
| FIELD_PREP(PLLCTRL_REFDIV_MASK, ctrl->refdiv); |
| } |
| |
| static inline void sg2042_pll_ctrl_decode(unsigned int reg_value, |
| struct sg2042_pll_ctrl *ctrl) |
| { |
| ctrl->fbdiv = FIELD_GET(PLLCTRL_FBDIV_MASK, reg_value); |
| ctrl->refdiv = FIELD_GET(PLLCTRL_REFDIV_MASK, reg_value); |
| ctrl->postdiv1 = FIELD_GET(PLLCTRL_POSTDIV1_MASK, reg_value); |
| ctrl->postdiv2 = FIELD_GET(PLLCTRL_POSTDIV2_MASK, reg_value); |
| } |
| |
| static inline void sg2042_pll_enable(struct sg2042_pll_clock *pll, bool en) |
| { |
| u32 value; |
| |
| if (en) { |
| /* wait pll lock */ |
| if (readl_poll_timeout_atomic(pll->base + R_PLL_STAT, |
| value, |
| ((value >> pll->shift_status_lock) & 0x1), |
| 0, |
| 100000)) |
| pr_warn("%s not locked\n", pll->hw.init->name); |
| |
| /* wait pll updating */ |
| if (readl_poll_timeout_atomic(pll->base + R_PLL_STAT, |
| value, |
| !((value >> pll->shift_status_updating) & 0x1), |
| 0, |
| 100000)) |
| pr_warn("%s still updating\n", pll->hw.init->name); |
| |
| /* enable pll */ |
| value = readl(pll->base + R_PLL_CLKEN_CONTROL); |
| writel(value | (1 << pll->shift_enable), pll->base + R_PLL_CLKEN_CONTROL); |
| } else { |
| /* disable pll */ |
| value = readl(pll->base + R_PLL_CLKEN_CONTROL); |
| writel(value & (~(1 << pll->shift_enable)), pll->base + R_PLL_CLKEN_CONTROL); |
| } |
| } |
| |
| /** |
| * sg2042_pll_recalc_rate() - Calculate rate for plls |
| * @reg_value: current register value |
| * @parent_rate: parent frequency |
| * |
| * This function is used to calculate below "rate" in equation |
| * rate = (parent_rate/REFDIV) x FBDIV/POSTDIV1/POSTDIV2 |
| * = (parent_rate x FBDIV) / (REFDIV x POSTDIV1 x POSTDIV2) |
| * |
| * Return: The rate calculated. |
| */ |
| static unsigned long sg2042_pll_recalc_rate(unsigned int reg_value, |
| unsigned long parent_rate) |
| { |
| struct sg2042_pll_ctrl ctrl_table; |
| u64 numerator, denominator; |
| |
| sg2042_pll_ctrl_decode(reg_value, &ctrl_table); |
| |
| numerator = parent_rate * ctrl_table.fbdiv; |
| denominator = ctrl_table.refdiv * ctrl_table.postdiv1 * ctrl_table.postdiv2; |
| do_div(numerator, denominator); |
| return numerator; |
| } |
| |
| /** |
| * sg2042_pll_get_postdiv_1_2() - Based on input rate/prate/fbdiv/refdiv, |
| * look up the postdiv1_2 table to get the closest postdiiv combination. |
| * @rate: FOUTPOSTDIV |
| * @prate: parent rate, i.e. FREF |
| * @fbdiv: FBDIV |
| * @refdiv: REFDIV |
| * @postdiv1: POSTDIV1, output |
| * @postdiv2: POSTDIV2, output |
| * |
| * postdiv1_2 contains all the possible combination lists of POSTDIV1 and POSTDIV2 |
| * for example: |
| * postdiv1_2[0] = {2, 4, 8}, where div1 = 2, div2 = 4 , div1 * div2 = 8 |
| * |
| * See TRM: |
| * FOUTPOSTDIV = FREF * FBDIV / REFDIV / (POSTDIV1 * POSTDIV2) |
| * So we get following formula to get POSTDIV1 and POSTDIV2: |
| * POSTDIV = (prate/REFDIV) x FBDIV/rate |
| * above POSTDIV = POSTDIV1*POSTDIV2 |
| * |
| * Return: |
| * %0 - OK |
| * %-EINVAL - invalid argument, which means Failed to get the postdivs. |
| */ |
| static int sg2042_pll_get_postdiv_1_2(unsigned long rate, |
| unsigned long prate, |
| unsigned int fbdiv, |
| unsigned int refdiv, |
| unsigned int *postdiv1, |
| unsigned int *postdiv2) |
| { |
| int index; |
| u64 tmp0; |
| |
| /* POSTDIV_RESULT_INDEX point to 3rd element in the array postdiv1_2 */ |
| #define POSTDIV_RESULT_INDEX 2 |
| |
| static const int postdiv1_2[][3] = { |
| {2, 4, 8}, {3, 3, 9}, {2, 5, 10}, {2, 6, 12}, |
| {2, 7, 14}, {3, 5, 15}, {4, 4, 16}, {3, 6, 18}, |
| {4, 5, 20}, {3, 7, 21}, {4, 6, 24}, {5, 5, 25}, |
| {4, 7, 28}, {5, 6, 30}, {5, 7, 35}, {6, 6, 36}, |
| {6, 7, 42}, {7, 7, 49} |
| }; |
| |
| /* prate/REFDIV and result save to tmp0 */ |
| tmp0 = prate; |
| do_div(tmp0, refdiv); |
| |
| /* ((prate/REFDIV) x FBDIV) and result save to tmp0 */ |
| tmp0 *= fbdiv; |
| |
| /* ((prate/REFDIV) x FBDIV)/rate and result save to tmp0 */ |
| do_div(tmp0, rate); |
| |
| /* tmp0 is POSTDIV1*POSTDIV2, now we calculate div1 and div2 value */ |
| if (tmp0 <= 7) { |
| /* (div1 * div2) <= 7, no need to use array search */ |
| *postdiv1 = tmp0; |
| *postdiv2 = 1; |
| return 0; |
| } |
| |
| /* (div1 * div2) > 7, use array search */ |
| for (index = 0; index < ARRAY_SIZE(postdiv1_2); index++) { |
| if (tmp0 > postdiv1_2[index][POSTDIV_RESULT_INDEX]) { |
| continue; |
| } else { |
| /* found it */ |
| *postdiv1 = postdiv1_2[index][1]; |
| *postdiv2 = postdiv1_2[index][0]; |
| return 0; |
| } |
| } |
| pr_warn("%s can not find in postdiv array!\n", __func__); |
| return -EINVAL; |
| } |
| |
| /** |
| * sg2042_get_pll_ctl_setting() - Based on the given FOUTPISTDIV and the input |
| * FREF to calculate the REFDIV/FBDIV/PSTDIV1/POSTDIV2 combination for pllctrl |
| * register. |
| * @req_rate: expected output clock rate, i.e. FOUTPISTDIV |
| * @parent_rate: input parent clock rate, i.e. FREF |
| * @best: output to hold calculated combination of REFDIV/FBDIV/PSTDIV1/POSTDIV2 |
| * |
| * Return: |
| * %0 - OK |
| * %-EINVAL - invalid argument |
| */ |
| static int sg2042_get_pll_ctl_setting(struct sg2042_pll_ctrl *best, |
| unsigned long req_rate, |
| unsigned long parent_rate) |
| { |
| unsigned int fbdiv, refdiv, postdiv1, postdiv2; |
| unsigned long foutpostdiv; |
| u64 foutvco; |
| int ret; |
| u64 tmp; |
| |
| if (parent_rate != PLL_FREF_SG2042) { |
| pr_err("INVALID FREF: %ld\n", parent_rate); |
| return -EINVAL; |
| } |
| |
| if (req_rate < PLL_FOUTPOSTDIV_MIN || req_rate > PLL_FOUTPOSTDIV_MAX) { |
| pr_alert("INVALID FOUTPOSTDIV: %ld\n", req_rate); |
| return -EINVAL; |
| } |
| |
| memset(best, 0, sizeof(struct sg2042_pll_ctrl)); |
| |
| for (refdiv = REFDIV_MIN; refdiv < REFDIV_MAX + 1; refdiv++) { |
| /* required by hardware: FREF/REFDIV must > 10 */ |
| tmp = parent_rate; |
| do_div(tmp, refdiv); |
| if (tmp <= 10) |
| continue; |
| |
| for (fbdiv = FBDIV_MIN; fbdiv < FBDIV_MAX + 1; fbdiv++) { |
| /* |
| * FOUTVCO = FREF*FBDIV/REFDIV validation |
| * required by hardware, FOUTVCO must [800MHz, 3200MHz] |
| */ |
| foutvco = parent_rate * fbdiv; |
| do_div(foutvco, refdiv); |
| if (foutvco < PLL_FOUTVCO_MIN || foutvco > PLL_FOUTVCO_MAX) |
| continue; |
| |
| ret = sg2042_pll_get_postdiv_1_2(req_rate, parent_rate, |
| fbdiv, refdiv, |
| &postdiv1, &postdiv2); |
| if (ret) |
| continue; |
| |
| /* |
| * FOUTPOSTDIV = FREF*FBDIV/REFDIV/(POSTDIV1*POSTDIV2) |
| * = FOUTVCO/(POSTDIV1*POSTDIV2) |
| */ |
| tmp = foutvco; |
| do_div(tmp, (postdiv1 * postdiv2)); |
| foutpostdiv = (unsigned long)tmp; |
| /* Iterative to approach the expected value */ |
| if (abs_diff(foutpostdiv, req_rate) < abs_diff(best->freq, req_rate)) { |
| best->freq = foutpostdiv; |
| best->refdiv = refdiv; |
| best->fbdiv = fbdiv; |
| best->postdiv1 = postdiv1; |
| best->postdiv2 = postdiv2; |
| if (foutpostdiv == req_rate) |
| return 0; |
| } |
| continue; |
| } |
| } |
| |
| if (best->freq == 0) |
| return -EINVAL; |
| else |
| return 0; |
| } |
| |
| /** |
| * sg2042_clk_pll_recalc_rate() - recalc_rate callback for pll clks |
| * @hw: ccf use to hook get sg2042_pll_clock |
| * @parent_rate: parent rate |
| * |
| * The is function will be called through clk_get_rate |
| * and return current rate after decoding reg value |
| * |
| * Return: Current rate recalculated. |
| */ |
| static unsigned long sg2042_clk_pll_recalc_rate(struct clk_hw *hw, |
| unsigned long parent_rate) |
| { |
| struct sg2042_pll_clock *pll = to_sg2042_pll_clk(hw); |
| unsigned long rate; |
| u32 value; |
| |
| value = readl(pll->base + pll->offset_ctrl); |
| rate = sg2042_pll_recalc_rate(value, parent_rate); |
| |
| pr_debug("--> %s: pll_recalc_rate: val = %ld\n", |
| clk_hw_get_name(hw), rate); |
| return rate; |
| } |
| |
| static long sg2042_clk_pll_round_rate(struct clk_hw *hw, |
| unsigned long req_rate, |
| unsigned long *prate) |
| { |
| struct sg2042_pll_ctrl pctrl_table; |
| unsigned int value; |
| long proper_rate; |
| int ret; |
| |
| ret = sg2042_get_pll_ctl_setting(&pctrl_table, req_rate, *prate); |
| if (ret) { |
| proper_rate = 0; |
| goto out; |
| } |
| |
| value = sg2042_pll_ctrl_encode(&pctrl_table); |
| proper_rate = (long)sg2042_pll_recalc_rate(value, *prate); |
| |
| out: |
| pr_debug("--> %s: pll_round_rate: val = %ld\n", |
| clk_hw_get_name(hw), proper_rate); |
| return proper_rate; |
| } |
| |
| static int sg2042_clk_pll_determine_rate(struct clk_hw *hw, |
| struct clk_rate_request *req) |
| { |
| req->rate = sg2042_clk_pll_round_rate(hw, min(req->rate, req->max_rate), |
| &req->best_parent_rate); |
| pr_debug("--> %s: pll_determine_rate: val = %ld\n", |
| clk_hw_get_name(hw), req->rate); |
| return 0; |
| } |
| |
| static int sg2042_clk_pll_set_rate(struct clk_hw *hw, |
| unsigned long rate, |
| unsigned long parent_rate) |
| { |
| struct sg2042_pll_clock *pll = to_sg2042_pll_clk(hw); |
| struct sg2042_pll_ctrl pctrl_table; |
| unsigned long flags; |
| u32 value = 0; |
| int ret; |
| |
| spin_lock_irqsave(pll->lock, flags); |
| |
| sg2042_pll_enable(pll, 0); |
| |
| ret = sg2042_get_pll_ctl_setting(&pctrl_table, rate, parent_rate); |
| if (ret) { |
| pr_warn("%s: Can't find a proper pll setting\n", pll->hw.init->name); |
| goto out; |
| } |
| |
| value = sg2042_pll_ctrl_encode(&pctrl_table); |
| |
| /* write the value to top register */ |
| writel(value, pll->base + pll->offset_ctrl); |
| |
| out: |
| sg2042_pll_enable(pll, 1); |
| |
| spin_unlock_irqrestore(pll->lock, flags); |
| |
| pr_debug("--> %s: pll_set_rate: val = 0x%x\n", |
| clk_hw_get_name(hw), value); |
| return ret; |
| } |
| |
| static const struct clk_ops sg2042_clk_pll_ops = { |
| .recalc_rate = sg2042_clk_pll_recalc_rate, |
| .round_rate = sg2042_clk_pll_round_rate, |
| .determine_rate = sg2042_clk_pll_determine_rate, |
| .set_rate = sg2042_clk_pll_set_rate, |
| }; |
| |
| static const struct clk_ops sg2042_clk_pll_ro_ops = { |
| .recalc_rate = sg2042_clk_pll_recalc_rate, |
| .round_rate = sg2042_clk_pll_round_rate, |
| }; |
| |
| /* |
| * Clock initialization macro naming rules: |
| * FW: use CLK_HW_INIT_FW_NAME |
| * RO: means Read-Only |
| */ |
| #define SG2042_PLL_FW(_id, _name, _parent, _r_ctrl, _shift) \ |
| { \ |
| .id = _id, \ |
| .hw.init = CLK_HW_INIT_FW_NAME( \ |
| _name, \ |
| _parent, \ |
| &sg2042_clk_pll_ops, \ |
| CLK_GET_RATE_NOCACHE | CLK_GET_ACCURACY_NOCACHE),\ |
| .offset_ctrl = _r_ctrl, \ |
| .shift_status_lock = 8 + (_shift), \ |
| .shift_status_updating = _shift, \ |
| .shift_enable = _shift, \ |
| } |
| |
| #define SG2042_PLL_FW_RO(_id, _name, _parent, _r_ctrl, _shift) \ |
| { \ |
| .id = _id, \ |
| .hw.init = CLK_HW_INIT_FW_NAME( \ |
| _name, \ |
| _parent, \ |
| &sg2042_clk_pll_ro_ops, \ |
| CLK_GET_RATE_NOCACHE | CLK_GET_ACCURACY_NOCACHE),\ |
| .offset_ctrl = _r_ctrl, \ |
| .shift_status_lock = 8 + (_shift), \ |
| .shift_status_updating = _shift, \ |
| .shift_enable = _shift, \ |
| } |
| |
| static struct sg2042_pll_clock sg2042_pll_clks[] = { |
| SG2042_PLL_FW(MPLL_CLK, "mpll_clock", "cgi_main", R_MPLL_CONTROL, 0), |
| SG2042_PLL_FW_RO(FPLL_CLK, "fpll_clock", "cgi_main", R_FPLL_CONTROL, 3), |
| SG2042_PLL_FW_RO(DPLL0_CLK, "dpll0_clock", "cgi_dpll0", R_DPLL0_CONTROL, 4), |
| SG2042_PLL_FW_RO(DPLL1_CLK, "dpll1_clock", "cgi_dpll1", R_DPLL1_CONTROL, 5), |
| }; |
| |
| static DEFINE_SPINLOCK(sg2042_clk_lock); |
| |
| static int sg2042_clk_register_plls(struct device *dev, |
| struct sg2042_clk_data *clk_data, |
| struct sg2042_pll_clock pll_clks[], |
| int num_pll_clks) |
| { |
| struct sg2042_pll_clock *pll; |
| struct clk_hw *hw; |
| int i, ret = 0; |
| |
| for (i = 0; i < num_pll_clks; i++) { |
| pll = &pll_clks[i]; |
| /* assign these for ops usage during registration */ |
| pll->base = clk_data->iobase; |
| pll->lock = &sg2042_clk_lock; |
| |
| hw = &pll->hw; |
| ret = devm_clk_hw_register(dev, hw); |
| if (ret) { |
| pr_err("failed to register clock %s\n", pll->hw.init->name); |
| break; |
| } |
| |
| clk_data->onecell_data.hws[pll->id] = hw; |
| } |
| |
| return ret; |
| } |
| |
| static int sg2042_init_clkdata(struct platform_device *pdev, |
| int num_clks, |
| struct sg2042_clk_data **pp_clk_data) |
| { |
| struct sg2042_clk_data *clk_data; |
| |
| clk_data = devm_kzalloc(&pdev->dev, |
| struct_size(clk_data, onecell_data.hws, num_clks), |
| GFP_KERNEL); |
| if (!clk_data) |
| return -ENOMEM; |
| |
| clk_data->iobase = devm_platform_ioremap_resource(pdev, 0); |
| if (WARN_ON(IS_ERR(clk_data->iobase))) |
| return PTR_ERR(clk_data->iobase); |
| |
| clk_data->onecell_data.num = num_clks; |
| |
| *pp_clk_data = clk_data; |
| |
| return 0; |
| } |
| |
| static int sg2042_pll_probe(struct platform_device *pdev) |
| { |
| struct sg2042_clk_data *clk_data = NULL; |
| int num_clks; |
| int ret; |
| |
| num_clks = ARRAY_SIZE(sg2042_pll_clks); |
| |
| ret = sg2042_init_clkdata(pdev, num_clks, &clk_data); |
| if (ret) |
| goto error_out; |
| |
| ret = sg2042_clk_register_plls(&pdev->dev, clk_data, sg2042_pll_clks, |
| num_clks); |
| if (ret) |
| goto error_out; |
| |
| return devm_of_clk_add_hw_provider(&pdev->dev, |
| of_clk_hw_onecell_get, |
| &clk_data->onecell_data); |
| |
| error_out: |
| pr_err("%s failed error number %d\n", __func__, ret); |
| return ret; |
| } |
| |
| static const struct of_device_id sg2042_pll_match[] = { |
| { .compatible = "sophgo,sg2042-pll" }, |
| { /* sentinel */ } |
| }; |
| MODULE_DEVICE_TABLE(of, sg2042_pll_match); |
| |
| static struct platform_driver sg2042_pll_driver = { |
| .probe = sg2042_pll_probe, |
| .driver = { |
| .name = "clk-sophgo-sg2042-pll", |
| .of_match_table = sg2042_pll_match, |
| .suppress_bind_attrs = true, |
| }, |
| }; |
| module_platform_driver(sg2042_pll_driver); |
| |
| MODULE_AUTHOR("Chen Wang"); |
| MODULE_DESCRIPTION("Sophgo SG2042 pll clock driver"); |
| MODULE_LICENSE("GPL"); |