[PATCH v2 06/10] clk: ambarella: add CV75 CCU driver
Long Zhao via B4 Relay <[email protected]> Thu, 06 Aug 2026 17:34:14 +0800
| Newsgroups | org.kernel.vger.linux-serial,dev.linux.lists.soc,org.infradead.lists.linux-arm-kernel,org.kernel.feeds.b4-sent,org.kernel.vger.linux-clk,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-gpio,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <20260806-longzhao-upstream-cv75-v2-v2-6-6b09707c5fe9@ambarella.com> |
From: Long Zhao <[email protected]> Add a minimal Ambarella clock controller for CV75 covering the UART0 and core/ahb/apb clocks required for early console bring-up. Signed-off-by: Long Zhao <[email protected]> --- drivers/clk/Kconfig | 1 + drivers/clk/Makefile | 1 + drivers/clk/ambarella/Kconfig | 16 ++ drivers/clk/ambarella/Makefile | 6 + drivers/clk/ambarella/ccu-cv75.c | 296 ++++++++++++++++++++++++++++ drivers/clk/ambarella/ccu_common.c | 60 ++++++ drivers/clk/ambarella/ccu_common.h | 25 +++ drivers/clk/ambarella/ccu_mux_div.c | 223 +++++++++++++++++++++ drivers/clk/ambarella/ccu_mux_div.h | 35 ++++ drivers/clk/ambarella/ccu_pll.c | 374 ++++++++++++++++++++++++++++++++++++ drivers/clk/ambarella/ccu_pll.h | 70 +++++++ 11 files changed, 1107 insertions(+) diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig index 1717ce75a907..fbbf4963716c 100644 --- a/drivers/clk/Kconfig +++ b/drivers/clk/Kconfig @@ -506,6 +506,7 @@ config COMMON_CLK_RPMI the RISC-V platform management interface (RPMI) specification. source "drivers/clk/actions/Kconfig" +source "drivers/clk/ambarella/Kconfig" source "drivers/clk/analogbits/Kconfig" source "drivers/clk/aspeed/Kconfig" source "drivers/clk/bcm/Kconfig" diff --git a/drivers/clk/Makefile b/drivers/clk/Makefile index cc108a75a900..30d823ee606f 100644 --- a/drivers/clk/Makefile +++ b/drivers/clk/Makefile @@ -112,6 +112,7 @@ obj-$(CONFIG_COMMON_CLK_XGENE) += clk-xgene.o # please keep this section sorted lexicographically by directory path name obj-y += actions/ +obj-y += ambarella/ obj-y += analogbits/ obj-y += aspeed/ obj-$(CONFIG_COMMON_CLK_AT91) += at91/ diff --git a/drivers/clk/ambarella/Kconfig b/drivers/clk/ambarella/Kconfig new file mode 100644 index 000000000000..639993033f96 --- /dev/null +++ b/drivers/clk/ambarella/Kconfig @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only + +config CLK_AMBARELLA_CV75 + bool "Ambarella CV75 RCT clock controller" + depends on ARCH_AMBARELLA || COMPILE_TEST + select CLK_AMBARELLA_CCU + default ARCH_AMBARELLA + help + Say Y to enable support for the Reset and Clock Tree controller + found on Ambarella CV75 SoCs. The controller provides the core PLL, + bus clocks and UART clock required during early platform boot. + It is needed to use the serial console on CV75-based systems. + +config CLK_AMBARELLA_CCU + bool + select REGMAP_MMIO diff --git a/drivers/clk/ambarella/Makefile b/drivers/clk/ambarella/Makefile new file mode 100644 index 000000000000..36e96326ea97 --- /dev/null +++ b/drivers/clk/ambarella/Makefile @@ -0,0 +1,6 @@ +# +# Makefile for ambarella specific clk +# + +obj-$(CONFIG_CLK_AMBARELLA_CCU) += ccu_common.o ccu_mux_div.o ccu_pll.o +obj-$(CONFIG_CLK_AMBARELLA_CV75) += ccu-cv75.o diff --git a/drivers/clk/ambarella/ccu-cv75.c b/drivers/clk/ambarella/ccu-cv75.c new file mode 100644 index 000000000000..55132dfa7431 --- /dev/null +++ b/drivers/clk/ambarella/ccu-cv75.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2026 Ambarella, Inc. + * + * CV75 RCT clock controller for boot clocks. + */ + +#include <linux/clk-provider.h> +#include <linux/clk.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/slab.h> + +#include <dt-bindings/clock/ambarella,cv75-clock.h> + +#include "ccu_common.h" +#include "ccu_mux_div.h" +#include "ccu_pll.h" + +enum amb_cv75_clk_type { + AMB_CV75_CLK_FIXED_RATE, + AMB_CV75_CLK_FIXED_FACTOR, + AMB_CV75_CLK_PLL, + AMB_CV75_CLK_DIV, + AMB_CV75_CLK_MUX_DIV, +}; + +enum amb_cv75_clk_ref { + AMB_CV75_CLK_REF_OSC = -1, + AMB_CV75_CLK_REF_DUMMY = -2, +}; + +/* PLL version for CV75 (ambarella,clkpll-v1 in vendor DTS) */ +static const struct amb_pll_soc_data cv75_pll_soc_data = { + .pll_version = 1, + .fsout_mask = CTRL2_FSOUT_DIV2, + .fsout_val = CTRL2_FSOUT_DIV2, + .fsdiv_mask = CTRL2_FSDIV_DIV2, + .fsdiv_val = CTRL2_FSDIV_DIV2, + .vcodiv_mask = CTRL2_VCODIV_DIV2, + .vcodiv_val = CTRL2_VCODIV_DIV2, + .vco_max_mhz = 2600UL, + .vco_min_mhz = 850UL, + .vco_range = { 1800UL, 1400UL, 1100UL, 0UL }, +}; + +struct amb_cv75_clk_desc { + int id; + enum amb_cv75_clk_type type; + const char *name; + int parent; + + union { + struct { + unsigned long rate; + } fixed_rate; + struct { + unsigned long flags; + unsigned int mult; + unsigned int div; + } fixed_factor; + struct { + u32 reg_offset[REG_NUM]; + const struct amb_pll_soc_data *soc_data; + } pll; + struct { + u32 reg; + u32 shift; + u32 width; + u32 flags; + u32 fix_divider; + } div; + struct amb_mux_div_desc mux_div; + }; +}; + +static const struct amb_cv75_clk_desc cv75_clks[] = { + { + .id = AMB_CV75_CLK_REF_DUMMY, + .type = AMB_CV75_CLK_FIXED_RATE, + .name = "dummy", + .fixed_rate.rate = 0, + }, + { + .id = CV75_GCLK_CORE, + .type = AMB_CV75_CLK_PLL, + .name = "core", + .parent = AMB_CV75_CLK_REF_OSC, + .pll = { + .reg_offset = { + 0x000, 0x004, 0x100, + 0x104, 0x000, 0x000, + }, + .soc_data = &cv75_pll_soc_data, + }, + }, + { + .id = CV75_GCLK_AHB, + .type = AMB_CV75_CLK_FIXED_FACTOR, + .name = "ahb", + .parent = CV75_GCLK_CORE, + .fixed_factor = { + .flags = 0, + .mult = 1, + .div = 2, + }, + }, + { + .id = CV75_GCLK_APB, + .type = AMB_CV75_CLK_FIXED_FACTOR, + .name = "apb", + .parent = CV75_GCLK_CORE, + .fixed_factor = { + .flags = 0, + .mult = 1, + .div = 4, + }, + }, + { + .id = CV75_GCLK_UART0, + .type = AMB_CV75_CLK_MUX_DIV, + .name = "uart0", + .mux_div = { + .name = "uart0", + .parents = (const int[]) { + AMB_CV75_CLK_REF_OSC, CV75_GCLK_CORE, + AMB_CV75_CLK_REF_DUMMY, AMB_CV75_CLK_REF_DUMMY, + }, + .num_parents = 4, + .mux_reg = 0x1c8, + .mux_shift = 0, + .mux_mask = 0x3, + .div_reg = 0x038, + .div_shift = 0, + .div_width = 24, + .div_flags = CLK_DIVIDER_ONE_BASED, + .fix_divider = 1, + }, + }, +}; + +static struct clk_hw *amb_cv75_get_parent(struct amb_ccu *ccu, + struct clk_hw *osc, + struct clk_hw *dummy, + int parent) +{ + if (parent == AMB_CV75_CLK_REF_OSC) + return osc; + if (parent == AMB_CV75_CLK_REF_DUMMY) + return dummy; + + if (parent < 0 || parent >= ccu->data->num) + return ERR_PTR(-EINVAL); + + if (!ccu->data->hws[parent]) + return ERR_PTR(-EPROBE_DEFER); + + return ccu->data->hws[parent]; +} + +static struct clk_hw *amb_cv75_register_mux_div(struct device *dev, + struct amb_ccu *ccu, + const struct amb_cv75_clk_desc *desc, + struct clk_hw *osc, + struct clk_hw *dummy) +{ + const struct amb_mux_div_desc *md = &desc->mux_div; + struct clk_parent_data *pdata; + struct clk_hw *parent; + u8 i; + + pdata = devm_kcalloc(dev, md->num_parents, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + for (i = 0; i < md->num_parents; i++) { + if (md->parents[i] == AMB_CV75_CLK_REF_OSC) { + pdata[i].fw_name = "osc"; + continue; + } + + parent = amb_cv75_get_parent(ccu, osc, dummy, md->parents[i]); + if (IS_ERR(parent)) + return parent; + + pdata[i].hw = parent; + } + + return amb_mux_div_register(dev, ccu->map, md, pdata); +} + +static struct clk_hw *amb_cv75_register_clk(struct device *dev, + struct amb_ccu *ccu, + const struct amb_cv75_clk_desc *desc, + struct clk_hw *osc, + struct clk_hw *dummy) +{ + struct amb_pll_desc pll_desc; + struct clk_hw *parent; + + switch (desc->type) { + case AMB_CV75_CLK_FIXED_RATE: + return devm_clk_hw_register_fixed_rate(dev, desc->name, NULL, 0, + desc->fixed_rate.rate); + case AMB_CV75_CLK_FIXED_FACTOR: + parent = amb_cv75_get_parent(ccu, osc, dummy, desc->parent); + if (IS_ERR(parent)) + return parent; + + return devm_clk_hw_register_fixed_factor_parent_hw(dev, + desc->name, parent, + desc->fixed_factor.flags, + desc->fixed_factor.mult, + desc->fixed_factor.div); + case AMB_CV75_CLK_PLL: + parent = amb_cv75_get_parent(ccu, osc, dummy, desc->parent); + if (IS_ERR(parent)) + return parent; + + pll_desc.name = desc->name; + pll_desc.parent = parent; + memcpy((void *)pll_desc.reg_offset, desc->pll.reg_offset, + sizeof(pll_desc.reg_offset)); + pll_desc.soc_data = desc->pll.soc_data; + pll_desc.frac_mode = false; + + return amb_pll_register(dev, ccu->map, &pll_desc); + case AMB_CV75_CLK_DIV: + parent = amb_cv75_get_parent(ccu, osc, dummy, desc->parent); + if (IS_ERR(parent)) + return parent; + + return amb_div_register(dev, ccu->map, desc->name, parent, + desc->div.reg, desc->div.shift, + desc->div.width, desc->div.flags, + desc->div.fix_divider); + case AMB_CV75_CLK_MUX_DIV: + return amb_cv75_register_mux_div(dev, ccu, desc, osc, dummy); + default: + return ERR_PTR(-EINVAL); + } +} + +static int amb_cv75_rct_probe(struct platform_device *pdev) +{ + struct amb_ccu *ccu; + struct clk *osc_clk; + struct clk_hw *osc, *dummy = NULL, *hw; + int i; + + ccu = amb_ccu_init(pdev, CV75_CLK_NUM); + if (IS_ERR(ccu)) + return PTR_ERR(ccu); + + osc_clk = devm_clk_get(&pdev->dev, "osc"); + if (IS_ERR(osc_clk)) + return dev_err_probe(&pdev->dev, PTR_ERR(osc_clk), + "missing osc clock\n"); + osc = __clk_get_hw(osc_clk); + + for (i = 0; i < ARRAY_SIZE(cv75_clks); i++) { + hw = amb_cv75_register_clk(&pdev->dev, ccu, &cv75_clks[i], + osc, dummy); + if (IS_ERR(hw)) + return dev_err_probe(&pdev->dev, PTR_ERR(hw), + "failed to register %s\n", + cv75_clks[i].name); + + if (cv75_clks[i].id == AMB_CV75_CLK_REF_DUMMY) + dummy = hw; + else + ccu->data->hws[cv75_clks[i].id] = hw; + } + + return amb_ccu_register(ccu); +} + +static const struct of_device_id amb_cv75_rct_match[] = { + { .compatible = "ambarella,cv75-rct" }, + { } +}; +MODULE_DEVICE_TABLE(of, amb_cv75_rct_match); + +static struct platform_driver amb_cv75_rct_driver = { + .probe = amb_cv75_rct_probe, + .driver = { + .name = "ambarella-cv75-rct", + .of_match_table = amb_cv75_rct_match, + }, +}; +module_platform_driver(amb_cv75_rct_driver); + +MODULE_AUTHOR("Ambarella Inc."); +MODULE_DESCRIPTION("Ambarella CV75 RCT clock controller"); +MODULE_LICENSE("GPL"); diff --git a/drivers/clk/ambarella/ccu_common.c b/drivers/clk/ambarella/ccu_common.c new file mode 100644 index 000000000000..78587c5f2ce1 --- /dev/null +++ b/drivers/clk/ambarella/ccu_common.c @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2026 Ambarella, Inc. + */ + +#include <linux/clk-provider.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> + +#include "ccu_common.h" + +static const struct regmap_config amb_rct_regmap_config = { + .reg_bits = 32, + .val_bits = 32, + .reg_stride = 4, + .max_register = AMB_RCT_REG_SIZE - 4, +}; + +struct amb_ccu *amb_ccu_init(struct platform_device *pdev, + unsigned int num_clks) +{ + struct amb_ccu *ccu; + void __iomem *base; + + if (!num_clks) + return ERR_PTR(-EINVAL); + + ccu = devm_kzalloc(&pdev->dev, sizeof(*ccu), GFP_KERNEL); + if (!ccu) + return ERR_PTR(-ENOMEM); + + ccu->dev = &pdev->dev; + + ccu->data = devm_kzalloc(&pdev->dev, + struct_size(ccu->data, hws, num_clks), + GFP_KERNEL); + if (!ccu->data) + return ERR_PTR(-ENOMEM); + + ccu->data->num = num_clks; + + base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(base)) + return ERR_CAST(base); + + ccu->map = devm_regmap_init_mmio(&pdev->dev, base, + &amb_rct_regmap_config); + if (IS_ERR(ccu->map)) + return ERR_CAST(ccu->map); + + return ccu; +} + +int amb_ccu_register(struct amb_ccu *ccu) +{ + return devm_of_clk_add_hw_provider(ccu->dev, of_clk_hw_onecell_get, + ccu->data); +} diff --git a/drivers/clk/ambarella/ccu_common.h b/drivers/clk/ambarella/ccu_common.h new file mode 100644 index 000000000000..f2bf9d1f06d7 --- /dev/null +++ b/drivers/clk/ambarella/ccu_common.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (C) 2026 Ambarella, Inc. + */ + +#ifndef __CCU_COMMON_H +#define __CCU_COMMON_H + +#include <linux/clk-provider.h> +#include <linux/device.h> +#include <linux/regmap.h> + +#define AMB_RCT_REG_SIZE 0x1000 + +struct amb_ccu { + struct device *dev; + struct regmap *map; + struct clk_hw_onecell_data *data; +}; + +struct amb_ccu *amb_ccu_init(struct platform_device *pdev, + unsigned int num_clks); +int amb_ccu_register(struct amb_ccu *ccu); + +#endif diff --git a/drivers/clk/ambarella/ccu_mux_div.c b/drivers/clk/ambarella/ccu_mux_div.c new file mode 100644 index 000000000000..33430506f8ed --- /dev/null +++ b/drivers/clk/ambarella/ccu_mux_div.c @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2026 Ambarella, Inc. + * + * Regmap-backed mux + divider, derived from the vendor composite-clock driver. + */ + +#include <linux/clk-provider.h> +#include <linux/device.h> +#include <linux/regmap.h> + +#include "ccu_mux_div.h" + +struct amb_mux { + struct clk_hw hw; + struct regmap *map; + u32 offset; + u32 mask; + u32 shift; +}; + +struct amb_div { + struct clk_hw hw; + struct regmap *map; + u32 offset; + u32 shift; + u32 width; + u32 flags; + u32 fix_divider; +}; + +#define to_amb_mux(_hw) container_of(_hw, struct amb_mux, hw) +#define to_amb_div(_hw) container_of(_hw, struct amb_div, hw) + +static u8 amb_mux_get_parent(struct clk_hw *hw) +{ + struct amb_mux *mux = to_amb_mux(hw); + u32 val; + + regmap_read(mux->map, mux->offset, &val); + val >>= mux->shift; + val &= mux->mask; + + return val; +} + +static int amb_mux_set_parent(struct clk_hw *hw, u8 index) +{ + struct amb_mux *mux = to_amb_mux(hw); + + return regmap_update_bits(mux->map, mux->offset, + mux->mask << mux->shift, + index << mux->shift); +} + +static const struct clk_ops amb_mux_ops = { + .get_parent = amb_mux_get_parent, + .set_parent = amb_mux_set_parent, +}; + +static unsigned long amb_div_recalc_rate(struct clk_hw *hw, + unsigned long parent_rate) +{ + struct amb_div *div = to_amb_div(hw); + unsigned long rate; + u32 val; + + regmap_read(div->map, div->offset, &val); + + if (val & BIT(div->width)) + return 0; + + val >>= div->shift; + val &= clk_div_mask(div->width); + + rate = divider_recalc_rate(hw, parent_rate, val, NULL, + div->flags, div->width); + if (div->fix_divider) + do_div(rate, div->fix_divider); + + return rate; +} + +static int amb_div_determine_rate(struct clk_hw *hw, + struct clk_rate_request *req) +{ + struct amb_div *div = to_amb_div(hw); + struct clk_rate_request scaled = *req; + int ret; + + if (!req->rate && (div->flags & CLK_DIVIDER_ONE_BASED)) + return 0; + + if (div->fix_divider) { + scaled.rate = min(req->rate, + ULONG_MAX / div->fix_divider) * + div->fix_divider; + scaled.min_rate = min(req->min_rate, + ULONG_MAX / div->fix_divider) * + div->fix_divider; + scaled.max_rate = min(req->max_rate, + ULONG_MAX / div->fix_divider) * + div->fix_divider; + } + + ret = divider_determine_rate(hw, &scaled, NULL, div->width, + div->flags); + if (ret) + return ret; + + req->rate = scaled.rate; + req->best_parent_rate = scaled.best_parent_rate; + req->best_parent_hw = scaled.best_parent_hw; + if (div->fix_divider) + do_div(req->rate, div->fix_divider); + + return 0; +} + +static int amb_div_set_rate(struct clk_hw *hw, unsigned long rate, + unsigned long parent_rate) +{ + struct amb_div *div = to_amb_div(hw); + int val, mask; + + if (div->fix_divider) + rate *= div->fix_divider; + + if (!rate) { + val = BIT(div->width); + } else { + val = divider_get_val(rate, parent_rate, NULL, + div->width, div->flags); + if (val < 0) + return val; + } + + mask = (div->flags & CLK_DIVIDER_ONE_BASED) ? + clk_div_mask(div->width + 1) : clk_div_mask(div->width); + + regmap_update_bits(div->map, div->offset, mask << div->shift, + val << div->shift); + + if (!(div->flags & CLK_DIVIDER_ONE_BASED)) { + regmap_update_bits(div->map, div->offset, BIT(0), BIT(0)); + regmap_update_bits(div->map, div->offset, BIT(0), 0); + } + + return 0; +} + +static const struct clk_ops amb_div_ops = { + .recalc_rate = amb_div_recalc_rate, + .determine_rate = amb_div_determine_rate, + .set_rate = amb_div_set_rate, +}; + +struct clk_hw *amb_div_register(struct device *dev, struct regmap *map, + const char *name, const struct clk_hw *parent, + u32 div_reg, u32 div_shift, u32 div_width, + u32 div_flags, u32 fix_divider) +{ + struct amb_div *div; + struct clk_init_data init = {}; + int ret; + + div = devm_kzalloc(dev, sizeof(*div), GFP_KERNEL); + if (!div) + return ERR_PTR(-ENOMEM); + + div->map = map; + div->offset = div_reg; + div->shift = div_shift; + div->width = div_width; + div->flags = div_flags; + div->fix_divider = fix_divider; + + init.name = name; + init.ops = &amb_div_ops; + init.parent_hws = &parent; + init.num_parents = 1; + + div->hw.init = &init; + + ret = devm_clk_hw_register(dev, &div->hw); + if (ret) + return ERR_PTR(ret); + + return &div->hw; +} + +struct clk_hw *amb_mux_div_register(struct device *dev, struct regmap *map, + const struct amb_mux_div_desc *desc, + const struct clk_parent_data *parent_data) +{ + struct amb_mux *mux; + struct amb_div *div; + + mux = devm_kzalloc(dev, sizeof(*mux), GFP_KERNEL); + div = devm_kzalloc(dev, sizeof(*div), GFP_KERNEL); + if (!mux || !div) + return ERR_PTR(-ENOMEM); + + mux->map = map; + mux->offset = desc->mux_reg; + mux->shift = desc->mux_shift; + mux->mask = desc->mux_mask; + + div->map = map; + div->offset = desc->div_reg; + div->shift = desc->div_shift; + div->width = desc->div_width; + div->flags = desc->div_flags; + div->fix_divider = desc->fix_divider; + + return devm_clk_hw_register_composite_pdata(dev, desc->name, + parent_data, + desc->num_parents, + &mux->hw, &amb_mux_ops, + &div->hw, &amb_div_ops, + NULL, NULL, + CLK_SET_RATE_NO_REPARENT); +} diff --git a/drivers/clk/ambarella/ccu_mux_div.h b/drivers/clk/ambarella/ccu_mux_div.h new file mode 100644 index 000000000000..abaefff1201a --- /dev/null +++ b/drivers/clk/ambarella/ccu_mux_div.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (C) 2026 Ambarella, Inc. + */ + +#ifndef __CCU_MUX_DIV_H +#define __CCU_MUX_DIV_H + +#include <linux/clk-provider.h> +#include <linux/regmap.h> + +struct amb_mux_div_desc { + const char *name; + const int *parents; + u8 num_parents; + u32 mux_reg; + u32 mux_shift; + u32 mux_mask; + u32 div_reg; + u32 div_shift; + u32 div_width; + u32 div_flags; + u32 fix_divider; +}; + +struct clk_hw *amb_mux_div_register(struct device *dev, struct regmap *map, + const struct amb_mux_div_desc *desc, + const struct clk_parent_data *parent_data); + +struct clk_hw *amb_div_register(struct device *dev, struct regmap *map, + const char *name, const struct clk_hw *parent, + u32 div_reg, u32 div_shift, u32 div_width, + u32 div_flags, u32 fix_divider); + +#endif diff --git a/drivers/clk/ambarella/ccu_pll.c b/drivers/clk/ambarella/ccu_pll.c new file mode 100644 index 000000000000..1293836e222e --- /dev/null +++ b/drivers/clk/ambarella/ccu_pll.c @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2026 Ambarella, Inc. + */ + +#include <linux/clk-provider.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/math64.h> +#include <linux/rational.h> +#include <linux/regmap.h> +#include <linux/slab.h> + +#include "ccu_pll.h" + +#define AMB_PLL_MAX_SOUT 16UL +#define AMB_PLL_MAX_SDIV 16UL + +struct amb_pll { + struct clk_hw hw; + struct regmap *map; + u32 reg_offset[REG_NUM]; + const struct amb_pll_soc_data *soc_data; + u32 fix_divider; + bool frac_mode; +}; + +#define to_amb_pll(_hw) container_of(_hw, struct amb_pll, hw) + +static void amb_pll_write_enable(struct regmap *map, u32 offset, u32 val) +{ + regmap_write(map, offset, val); + regmap_write(map, offset, val | CTRL_WRITE_ENABLE); + regmap_write(map, offset, val); +} + +static unsigned long amb_pll_calc_vco(struct amb_pll *pll, + unsigned long parent_rate) +{ + const struct amb_pll_soc_data *soc_data = pll->soc_data; + u32 *reg = pll->reg_offset; + u32 pre_scaler = 1; + u32 ctrl_val, ctrl2_val = 0, frac_val; + u32 intp, sdiv, vcodiv, fsdiv; + u64 frac = 0, vco; + + if (reg[PRES_OFFSET]) { + regmap_read(pll->map, reg[PRES_OFFSET], &pre_scaler); + pre_scaler = (pre_scaler >> 4) + 1; + } + + regmap_read(pll->map, reg[CTRL_OFFSET], &ctrl_val); + intp = ((ctrl_val >> 24) & 0x7f) + 1; + sdiv = ((ctrl_val >> 12) & 0xf) + 1; + + if (soc_data->pll_version >= 2) { + vcodiv = (ctrl_val & soc_data->vcodiv_mask) == + soc_data->vcodiv_val ? 2 : 1; + fsdiv = (ctrl_val & soc_data->fsdiv_mask) == + soc_data->fsdiv_val ? 2 : 1; + } else { + regmap_read(pll->map, reg[CTRL2_OFFSET], &ctrl2_val); + vcodiv = (ctrl2_val & soc_data->vcodiv_mask) == + soc_data->vcodiv_val ? 2 : 1; + fsdiv = (ctrl2_val & soc_data->fsdiv_mask) == + soc_data->fsdiv_val ? 2 : 1; + } + + vco = (u64)parent_rate * vcodiv * fsdiv * intp * sdiv; + vco = div_u64(vco, pre_scaler); + + if (ctrl_val & CTRL_FRAC_MODE) { + regmap_read(pll->map, reg[FRAC_OFFSET], &frac_val); + frac = (u64)parent_rate * vcodiv * fsdiv * sdiv * frac_val; + frac = div_u64(frac, pre_scaler) >> 32; + } + + return vco + frac; +} + +static unsigned long amb_pll_recalc_rate(struct clk_hw *hw, + unsigned long parent_rate) +{ + struct amb_pll *pll = to_amb_pll(hw); + const struct amb_pll_soc_data *soc_data = pll->soc_data; + u32 *reg = pll->reg_offset; + u32 pre_scaler = 1, post_scaler = 1; + u32 ctrl_val, ctrl2_val = 0; + u32 vcodiv, fsout, sout; + u64 rate; + + regmap_read(pll->map, reg[CTRL_OFFSET], &ctrl_val); + if (ctrl_val & (CTRL_POWER_DOWN | CTRL_HALT_VCO | CTRL_FORCE_RESET)) + return 0; + + if (reg[PRES_OFFSET]) { + regmap_read(pll->map, reg[PRES_OFFSET], &pre_scaler); + pre_scaler = (pre_scaler >> 4) + 1; + } + + if (reg[POST_OFFSET]) { + regmap_read(pll->map, reg[POST_OFFSET], &post_scaler); + post_scaler = (post_scaler >> 4) + 1; + } + + if (ctrl_val & CTRL_BYPASS) + return parent_rate / pre_scaler / post_scaler; + + if (soc_data->pll_version >= 2) { + vcodiv = (ctrl_val & soc_data->vcodiv_mask) == + soc_data->vcodiv_val ? 2 : 1; + fsout = (ctrl_val & soc_data->fsout_mask) == + soc_data->fsout_val ? 2 : 1; + } else { + regmap_read(pll->map, reg[CTRL2_OFFSET], &ctrl2_val); + vcodiv = (ctrl2_val & soc_data->vcodiv_mask) == + soc_data->vcodiv_val ? 2 : 1; + fsout = (ctrl2_val & soc_data->fsout_mask) == + soc_data->fsout_val ? 2 : 1; + } + + sout = ((ctrl_val >> 16) & 0xf) + 1; + rate = amb_pll_calc_vco(pll, parent_rate); + + if (soc_data->pll_version >= 2) { + if (!(ctrl_val & CTRL_BYPASS_HSDIV)) + rate = div_u64(rate, vcodiv * fsout * sout); + } else { + if (!(ctrl2_val & CTRL2_BYPASS_HSDIV)) + rate = div_u64(rate, vcodiv * fsout * sout); + } + + rate = div_u64(rate, pll->fix_divider * post_scaler); + + return rate; +} + +static int amb_pll_determine_rate(struct clk_hw *hw, + struct clk_rate_request *req) +{ + struct amb_pll *pll = to_amb_pll(hw); + unsigned long half_refclk = req->best_parent_rate / 2; + + if (pll->frac_mode) + return 0; + + if (!half_refclk) + return 0; + + req->rate = roundup(req->rate, half_refclk); + + return 0; +} + +static int amb_pll_calc_params(struct amb_pll *pll, unsigned long rate, + unsigned long parent_rate, u32 ctrl2_val, + u32 *intp, u32 *sdiv, u32 *sout, + u32 *vcodiv, u32 *fsdiv, u32 *fsout) +{ + const struct amb_pll_soc_data *soc_data = pll->soc_data; + unsigned long max_numerator, max_denominator; + unsigned long intp_ul, sout_ul, rate_tmp; + u32 ctrl_val; + + *sdiv = 1; + + if (soc_data->pll_version >= 2) { + ctrl_val = 0; + *vcodiv = (ctrl_val & soc_data->vcodiv_mask) == + soc_data->vcodiv_val ? 2 : 1; + *fsdiv = (ctrl_val & soc_data->fsdiv_mask) == + soc_data->fsdiv_val ? 2 : 1; + *fsout = (ctrl_val & soc_data->fsout_mask) == + soc_data->fsout_val ? 2 : 1; + } else { + *vcodiv = (ctrl2_val & soc_data->vcodiv_mask) == + soc_data->vcodiv_val ? 2 : 1; + *fsdiv = (ctrl2_val & soc_data->fsdiv_mask) == + soc_data->fsdiv_val ? 2 : 1; + *fsout = (ctrl2_val & soc_data->fsout_mask) == + soc_data->fsout_val ? 2 : 1; + } + + if (rate < parent_rate) + return -EINVAL; + + max_numerator = soc_data->vco_max_mhz; + max_numerator = div_u64(max_numerator * 1000000ULL, parent_rate); + max_numerator = div_u64(max_numerator, *vcodiv * *fsdiv); + max_numerator = min(128UL, max_numerator); + if (!max_numerator) + return -EINVAL; + + max_denominator = AMB_PLL_MAX_SOUT; + rate_tmp = rate; + rational_best_approximation(rate_tmp, parent_rate, max_numerator, + max_denominator, &intp_ul, &sout_ul); + + while (parent_rate * *fsdiv * intp_ul * *sdiv / *fsout / sout_ul > + rate) { + unsigned long resolution = parent_rate / AMB_PLL_MAX_SOUT; + + if (rate_tmp <= resolution) + return -EINVAL; + + rate_tmp -= resolution; + rational_best_approximation(rate_tmp, parent_rate, + max_numerator, max_denominator, + &intp_ul, &sout_ul); + } + + while (parent_rate / 1000000 * *vcodiv * *fsdiv * intp_ul * *sdiv < + soc_data->vco_min_mhz) { + if (sout_ul > 8 || intp_ul > 64) + break; + + intp_ul *= 2; + sout_ul *= 2; + } + + if (intp_ul > max_numerator || sout_ul > max_denominator || + *sdiv > AMB_PLL_MAX_SDIV) + return -EINVAL; + + *intp = intp_ul; + *sout = sout_ul; + + return 0; +} + +static int amb_pll_set_rate(struct clk_hw *hw, unsigned long rate, + unsigned long parent_rate) +{ + struct amb_pll *pll = to_amb_pll(hw); + const struct amb_pll_soc_data *soc_data = pll->soc_data; + u32 *reg = pll->reg_offset; + u32 ctrl_val, ctrl2_val = 0, ctrl3_val, frac_val = 0; + u32 intp, sdiv, sout, vcodiv, fsdiv, fsout; + unsigned long old_rate, new_rate, rate_tmp; + int ret; + + if (!rate) { + regmap_read(pll->map, reg[CTRL_OFFSET], &ctrl_val); + ctrl_val |= CTRL_POWER_DOWN | CTRL_HALT_VCO; + amb_pll_write_enable(pll->map, reg[CTRL_OFFSET], ctrl_val); + return 0; + } + + rate *= pll->fix_divider; + + if (soc_data->ctrl2_val) + ctrl2_val = soc_data->ctrl2_val; + else + regmap_read(pll->map, reg[CTRL2_OFFSET], &ctrl2_val); + + ret = amb_pll_calc_params(pll, rate, parent_rate, ctrl2_val, + &intp, &sdiv, &sout, &vcodiv, &fsdiv, + &fsout); + if (ret) + return ret; + + if (soc_data->ctrl2_val) + regmap_write(pll->map, reg[CTRL2_OFFSET], soc_data->ctrl2_val); + + ctrl_val = ((intp - 1) & 0x7f) << 24; + ctrl_val |= ((sdiv - 1) & 0xf) << 12; + ctrl_val |= ((sout - 1) & 0xf) << 16; + if (soc_data->pll_version >= 2) { + ctrl_val |= vcodiv == 2 ? soc_data->vcodiv_val : 0; + ctrl_val |= fsdiv == 2 ? soc_data->fsdiv_val : 0; + ctrl_val |= fsout == 2 ? soc_data->fsout_val : 0; + } + + regmap_write(pll->map, reg[CTRL_OFFSET], ctrl_val); + regmap_write(pll->map, reg[FRAC_OFFSET], 0); + + old_rate = amb_pll_recalc_rate(hw, parent_rate) * pll->fix_divider; + rate_tmp = old_rate > rate ? 0 : rate - old_rate; + if (rate_tmp && pll->frac_mode) { + u64 dividend, divider; + + dividend = (u64)rate_tmp * sout * fsout; + dividend <<= 32; + divider = (u64)sdiv * fsdiv * parent_rate; + frac_val = DIV_ROUND_CLOSEST_ULL(dividend, divider); + regmap_write(pll->map, reg[FRAC_OFFSET], frac_val); + ctrl_val |= CTRL_FRAC_MODE; + } + + if (soc_data->pll_version >= 2) { + ctrl3_val = soc_data->ctrl3_val; + regmap_write(pll->map, reg[CTRL3_OFFSET], + ctrl3_val | CTRL3_VCO_CLAMP); + regmap_write(pll->map, reg[CTRL_OFFSET], + ctrl_val | CTRL_FORCE_RESET); + ndelay(100); + regmap_write(pll->map, reg[CTRL_OFFSET], + ctrl_val & ~CTRL_FORCE_RESET); + ndelay(100); + regmap_write(pll->map, reg[CTRL3_OFFSET], + ctrl3_val & ~CTRL3_VCO_CLAMP); + } else { + u32 fvco_mhz, range; + + fvco_mhz = amb_pll_calc_vco(pll, parent_rate) / 1000000UL; + for (range = 0; range < ARRAY_SIZE(soc_data->vco_range); + range++) { + if (fvco_mhz > soc_data->vco_range[range]) + break; + } + range = ARRAY_SIZE(soc_data->vco_range) - range - 1; + + regmap_read(pll->map, reg[CTRL3_OFFSET], &ctrl3_val); + ctrl3_val &= ~CTRL3_VCO_RANGE_MASK; + ctrl3_val |= range << 1; + regmap_write(pll->map, reg[CTRL3_OFFSET], ctrl3_val); + + if (frac_val) { + ctrl_val |= CTRL_FORCE_RESET; + amb_pll_write_enable(pll->map, reg[CTRL_OFFSET], + ctrl_val); + } + + ctrl_val &= ~CTRL_FORCE_RESET; + amb_pll_write_enable(pll->map, reg[CTRL_OFFSET], ctrl_val); + } + + new_rate = amb_pll_recalc_rate(hw, parent_rate); + rate_tmp = rate / pll->fix_divider; + if (max(new_rate, rate_tmp) - min(new_rate, rate_tmp) > 10) + pr_warn("%s: requested %lu, got %lu\n", clk_hw_get_name(hw), + rate_tmp, new_rate); + + return 0; +} + +static const struct clk_ops amb_pll_ops = { + .recalc_rate = amb_pll_recalc_rate, + .determine_rate = amb_pll_determine_rate, + .set_rate = amb_pll_set_rate, +}; + +struct clk_hw *amb_pll_register(struct device *dev, struct regmap *map, + const struct amb_pll_desc *desc) +{ + struct amb_pll *pll; + struct clk_init_data init = {}; + const struct clk_hw *parent = desc->parent; + int ret; + + pll = devm_kzalloc(dev, sizeof(*pll), GFP_KERNEL); + if (!pll) + return ERR_PTR(-ENOMEM); + + pll->map = map; + memcpy(pll->reg_offset, desc->reg_offset, sizeof(pll->reg_offset)); + pll->soc_data = desc->soc_data; + pll->fix_divider = 1; + pll->frac_mode = desc->frac_mode; + + init.name = desc->name; + init.ops = &amb_pll_ops; + init.flags = CLK_GET_RATE_NOCACHE | CLK_IS_CRITICAL; + init.parent_hws = &parent; + init.num_parents = 1; + + pll->hw.init = &init; + + ret = devm_clk_hw_register(dev, &pll->hw); + if (ret) + return ERR_PTR(ret); + + return &pll->hw; +} diff --git a/drivers/clk/ambarella/ccu_pll.h b/drivers/clk/ambarella/ccu_pll.h new file mode 100644 index 000000000000..b9e453e9debf --- /dev/null +++ b/drivers/clk/ambarella/ccu_pll.h @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (C) 2026 Ambarella, Inc. + */ + +#ifndef __CCU_PLL_H +#define __CCU_PLL_H + +#include <linux/bits.h> +#include <linux/clk-provider.h> +#include <linux/device.h> +#include <linux/regmap.h> +#include <linux/types.h> + +enum { + CTRL_OFFSET = 0, + FRAC_OFFSET, + CTRL2_OFFSET, + CTRL3_OFFSET, + PRES_OFFSET, + POST_OFFSET, + REG_NUM, +}; + +#define CTRL_BYPASS BIT(2) +#define CTRL_WRITE_ENABLE BIT(0) +#define CTRL_FRAC_MODE BIT(3) +#define CTRL_FORCE_RESET BIT(4) +#define CTRL_POWER_DOWN BIT(5) +#define CTRL_HALT_VCO BIT(6) +#define CTRL_VCODIV_DIV2 BIT(8) +#define CTRL_FSDIV_DIV2 BIT(9) +#define CTRL_FSOUT_DIV2 BIT(10) +#define CTRL_BYPASS_HSDIV BIT(11) + +#define CTRL2_VCODIV_DIV2 BIT(8) +#define CTRL2_FSDIV_DIV2 BIT(9) +#define CTRL2_FSOUT_DIV2 BIT(11) +#define CTRL2_BYPASS_HSDIV BIT(12) + +#define CTRL3_VCO_RANGE_MASK 0x6 +#define CTRL3_VCO_CLAMP 0x8 + +struct amb_pll_soc_data { + u32 pll_version; + u32 fsout_mask; + u32 fsout_val; + u32 fsdiv_mask; + u32 fsdiv_val; + u32 vcodiv_mask; + u32 vcodiv_val; + u32 vco_max_mhz; + u32 vco_min_mhz; + u32 vco_range[4]; + u32 ctrl2_val; + u32 ctrl3_val; +}; + +struct amb_pll_desc { + const char *name; + const struct clk_hw *parent; + const u32 reg_offset[REG_NUM]; + const struct amb_pll_soc_data *soc_data; + bool frac_mode; +}; + +struct clk_hw *amb_pll_register(struct device *dev, struct regmap *map, + const struct amb_pll_desc *desc); + +#endif -- 2.34.1