Re: [PATCH v15 05/11] clk: realtek: Add support for phase locked loops (PLLs)

From: Jerome Brunet

Date: Fri Oct 09 2026 - 14:53:47 EST


> Provide a full set of PLL operations for programmable PLLs and a read-only
> variant for fixed or hardware-managed PLLs.
>
> Signed-off-by: Cheng-Yu Lee <cylee12@xxxxxxxxxxx>
> Co-developed-by: Yu-Chun Lin <eleanor.lin@xxxxxxxxxxx>
> Signed-off-by: Yu-Chun Lin <eleanor.lin@xxxxxxxxxxx>
> Reviewed-by: Brian Masney <bmasney@xxxxxxxxxx>
>
> diff --git a/drivers/clk/realtek/Makefile b/drivers/clk/realtek/Makefile
> index 13000ed4ba11..71edc18121c6 100644
> --- a/drivers/clk/realtek/Makefile
> +++ b/drivers/clk/realtek/Makefile
> @@ -2,3 +2,5 @@
> obj-$(CONFIG_RTK_CLK_COMMON) += clk-rtk.o
>
> clk-rtk-y += clk-rtk-common.o
> +clk-rtk-y += clk-pll.o
> +clk-rtk-y += freq_table.o
> diff --git a/drivers/clk/realtek/clk-pll.c b/drivers/clk/realtek/clk-pll.c
> new file mode 100644
> index 000000000000..31670b8316cd
> --- /dev/null
> +++ b/drivers/clk/realtek/clk-pll.c
> @@ -0,0 +1,217 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (C) 2024-2026 Realtek Semiconductor Corporation
> + * Author: Cheng-Yu Lee <cylee12@xxxxxxxxxxx>
> + */
> +
> +#include <linux/export.h>
> +#include <linux/regmap.h>
> +#include <linux/spinlock.h>
> +#include "clk-pll.h"
> +
> +#define TIMEOUT 500
> +
> +static inline struct rtk_clk_regmap_pll *to_rtk_clk_regmap_pll(struct clk_hw *hw)
> +{
> + struct rtk_clk_regmap *clkr = to_rtk_clk_regmap(hw);
> +
> + return container_of(clkr, struct rtk_clk_regmap_pll, clkr);
> +}
> +
> +static int wait_freq_ready(struct rtk_clk_regmap_pll *clkp)
> +{
> + u32 pollval;
> +
> + /* reg == 0 means not configured.
> + * Register offset 0 is never a valid address on Realtek SoCs.

Fix the comment style please

> + */
> + if (!clkp->freq_ready_reg)
> + return 0;

Do you need to protect against your own driver providing bad offset ?

> +
> + return regmap_read_poll_timeout_atomic(clkp->clkr.regmap, clkp->freq_ready_reg, pollval,
> + (pollval & clkp->freq_ready_mask) == clkp->freq_ready_val, 1, TIMEOUT);
> +}
> +
> +static bool is_power_on(struct rtk_clk_regmap_pll *clkp)
> +{
> + u32 val;
> +
> + /* reg == 0 means not configured (assume always on).
> + * Register offset 0 is never a valid address on Realtek SoCs.
> + */

You've already said this. It would be more helpful to tell us that PLL that does not
provider a power reg is considered to be always powered, if that is the case

> + if (!clkp->power_reg)
> + return true;
> +
> + if (regmap_read(clkp->clkr.regmap, clkp->power_reg, &val))
> + return false;
> +
> + return (val & clkp->power_mask) == clkp->power_val_on;
> +}
> +
> +static void rtk_clk_regmap_pll_disable(struct clk_hw *hw)
> +{
> + struct rtk_clk_regmap_pll *clkp = to_rtk_clk_regmap_pll(hw);
> + unsigned long flags;
> +
> + if (!clkp->seq_power_off)
> + return;
> +
> + spin_lock_irqsave(&clkp->lock, flags);

What do you protect with this ?
regmap does provide some locking already and CCF too. Do you expect something
to poke the registers you are writing ?

> +
> + regmap_multi_reg_write(clkp->clkr.regmap, clkp->seq_power_off,
> + clkp->num_seq_power_off);
> +
> + spin_unlock_irqrestore(&clkp->lock, flags);
> +}
> +
> +static int rtk_clk_regmap_pll_is_enabled(struct clk_hw *hw)
> +{
> + struct rtk_clk_regmap_pll *clkp = to_rtk_clk_regmap_pll(hw);
> + unsigned long flags;
> + int ret;
> +
> + spin_lock_irqsave(&clkp->lock, flags);
> +
> + ret = is_power_on(clkp);
> +
> + spin_unlock_irqrestore(&clkp->lock, flags);
> +
> + return ret;
> +}
> +
> +static int rtk_clk_regmap_pll_determine_rate(struct clk_hw *hw,
> + struct clk_rate_request *req)
> +{
> + struct rtk_clk_regmap_pll *clkp = to_rtk_clk_regmap_pll(hw);
> + const struct rtk_freq_table *ftblv = NULL;
> +
> + ftblv = ftbl_find_by_rate(clkp->freq_tbl, req->rate);

Doesn't this completly disregard the parent rate ?

I understand that SoC vendor may expect a single frequency as input but it
it is pretty fragile to do so.

A PLL is usually some sort of (fractional) multiplier.
It is way better to provide the table of multiplier you support, along with their
sequence and properly doing the calculation here.

> +
> + if (ftblv && ftblv->rate >= req->min_rate) {
> + req->rate = ftblv->rate;
> + return 0;
> + }
> +
> + /* floor result is below min_rate; find the smallest rate >= min_rate */
> + ftblv = ftbl_find_ceil_by_rate(clkp->freq_tbl, req->min_rate);
> + if (!ftblv || ftblv->rate > req->max_rate)
> + return -EINVAL;

This is not how determine_rate() works. You just round to the closest rate the PLL
supports. You are free to round as you wish (bellow or closest)

Just test the different multipliers you support and return what is best compared to
the request. There is no reason to return EINVAL for this.

> +
> + req->rate = ftblv->rate;
> +
> + return 0;
> +}
> +
> +static unsigned long rtk_clk_regmap_pll_recalc_rate(struct clk_hw *hw,
> + unsigned long parent_rate)
> +{
> + struct rtk_clk_regmap_pll *clkp = to_rtk_clk_regmap_pll(hw);
> + const struct rtk_freq_table *fv;
> + unsigned long flags;
> + u32 freq_val;
> +
> + spin_lock_irqsave(&clkp->lock, flags);
> +
> + if (regmap_read(clkp->clkr.regmap, clkp->freq_reg, &freq_val)) {
> + spin_unlock_irqrestore(&clkp->lock, flags);
> + return 0;
> + }
> +
> + freq_val &= clkp->freq_mask;
> +
> + fv = ftbl_find_by_val_with_mask(clkp->freq_tbl, clkp->freq_mask,
> + freq_val);
> +
> + spin_unlock_irqrestore(&clkp->lock, flags);
> +
> + return fv ? fv->rate : 0;
> +}
> +
> +static int rtk_clk_regmap_pll_set_rate(struct clk_hw *hw, unsigned long rate,
> + unsigned long parent_rate)
> +{
> + struct rtk_clk_regmap_pll *clkp = to_rtk_clk_regmap_pll(hw);
> + const struct rtk_freq_table *fv;
> + unsigned long flags;
> + int ret;
> +
> + fv = ftbl_find_by_rate(clkp->freq_tbl, rate);
> + if (!fv || fv->rate != rate)
> + return -EINVAL;
> +
> + spin_lock_irqsave(&clkp->lock, flags);
> +
> + if (clkp->seq_pre_set_freq) {
> + ret = regmap_multi_reg_write(clkp->clkr.regmap, clkp->seq_pre_set_freq,
> + clkp->num_seq_pre_set_freq);
> + if (ret)
> + goto unlock;
> + }
> +
> + ret = regmap_update_bits(clkp->clkr.regmap, clkp->freq_reg,
> + clkp->freq_mask, fv->val);
> + if (ret)
> + goto unlock;
> +
> + if (clkp->seq_post_set_freq) {
> + ret = regmap_multi_reg_write(clkp->clkr.regmap, clkp->seq_post_set_freq,
> + clkp->num_seq_post_set_freq);
> + if (ret)
> + goto unlock;
> + }
> +
> + if (is_power_on(clkp)) {
> + ret = wait_freq_ready(clkp);
> + if (ret)
> + goto unlock;
> + }
> +
> +unlock:
> + spin_unlock_irqrestore(&clkp->lock, flags);
> +
> + return ret;
> +}
> +
> +static int rtk_clk_regmap_pll_enable(struct clk_hw *hw)
> +{
> + struct rtk_clk_regmap_pll *clkp = to_rtk_clk_regmap_pll(hw);
> + unsigned long flags;
> + int ret = 0;
> +
> + if (!clkp->seq_power_on)
> + return ret;
> +
> + spin_lock_irqsave(&clkp->lock, flags);
> +
> + if (is_power_on(clkp))
> + goto unlock;
> +
> + ret = regmap_multi_reg_write(clkp->clkr.regmap, clkp->seq_power_on,
> + clkp->num_seq_power_on);
> + if (ret)
> + goto unlock;
> +
> + ret = wait_freq_ready(clkp);
> + if (ret)
> + goto unlock;
> +
> +unlock:
> + spin_unlock_irqrestore(&clkp->lock, flags);
> +
> + return ret;
> +}
> +
> +const struct clk_ops rtk_clk_pll_ops = {
> + .enable = rtk_clk_regmap_pll_enable,
> + .disable = rtk_clk_regmap_pll_disable,
> + .is_enabled = rtk_clk_regmap_pll_is_enabled,
> + .recalc_rate = rtk_clk_regmap_pll_recalc_rate,
> + .determine_rate = rtk_clk_regmap_pll_determine_rate,
> + .set_rate = rtk_clk_regmap_pll_set_rate,
> +};
> +EXPORT_SYMBOL_NS_GPL(rtk_clk_pll_ops, "CLK_REALTEK");
> +
> +const struct clk_ops rtk_clk_pll_ro_ops = {
> + .recalc_rate = rtk_clk_regmap_pll_recalc_rate,

is_enabled ?

--
Jerome