[PATCH 08/10] clk: amlogic: Add A9 PLL clock controller driver

Jian Hu jian.hu at amlogic.com
Wed May 20 00:33:07 PDT 2026


On 5/15/2026 12:12 AM, Jerome Brunet wrote:
> [ EXTERNAL EMAIL ]
>
> On lun. 11 mai 2026 at 20:47, Jian Hu via B4 Relay <devnull+jian.hu.amlogic.com at kernel.org> wrote:
>
>> From: Jian Hu <jian.hu at amlogic.com>
>>
>> Add the PLL clock controller driver for the Amlogic A9 SoC family.
>>
>> Signed-off-by: Jian Hu <jian.hu at amlogic.com>
[...]
>> +
>> +/*
>> + * Compared with previous SoC PLLs, the A9 PLL input path has an inherent
>> + * 2-divider. The N pre-divider follows the same calculation rule as OD,
>> + * where the pre-divider ratio equals 2^N.
>> + *
>> + * A9 PLL is composed as follows:
>> + *
>> + *                      PLL
>> + *         +---------------------------------+
>> + *         |                                 |
>> + *         |             +--+                |
>> + *  in/2 >>---[ /2^N ]-->|  |      +-----+   |
>> + *         |             |  |------| DCO |----->> out
>> + *         |  +--------->|  |      +--v--+   |
>> + *         |  |          +--+         |      |
>> + *         |  |                       |      |
>> + *         |  +--[ *(M + (F/Fmax) ]<--+      |
>> + *         |                                 |
>> + *         +---------------------------------+
>> + *
>> + * out = in / 2  * (m + frac / frac_max) / 2^n
>> + */
>> +
>> +static struct clk_fixed_factor a9_gp0_in_div2_div = {
>> +     .mult = 1,
>> +     .div = 2,
>> +     .hw.init = &(struct clk_init_data){
>> +             .name = "gp0_in_div2_div",
>> +             .ops = &clk_fixed_factor_ops,
>> +             .parent_data = &(const struct clk_parent_data) {
>> +                     .fw_name = "in0",
>> +             },
>> +             .num_parents = 1,
>> +     },
>> +};
>> +
>> +static struct clk_regmap a9_gp0_in_div2 = {
>> +     .data = &(struct clk_regmap_gate_data) {
>> +             .offset = GP0PLL_CTRL0,
>> +             .bit_idx = 27,
>> +     },
>> +     .hw.init = &(struct clk_init_data) {
>> +             .name = "gp0_in_div2",
>> +             .ops = &clk_regmap_gate_ops,
>> +             .parent_hws = (const struct clk_hw *[]) {
>> +                     &a9_gp0_in_div2_div.hw
>> +             },
>> +             .num_parents = 1,
>> +     },
>> +};
> When document something, be sure it matches what you are doing
> afterward. It is confusing otherwise. Your comments above clearly miss
> this gate.
>
> A fixed 2 divider followed by a power of 2 divider ? Is it actually how
> the HW works or your modelisation power of 2 that's shifted by 1,
> mapping :
>   * 0 -> 2
>   * 1 -> 4
>   * etc ...
>
> ?


Sorry for missing the gate in PLL block diagram, above block diagram 
focuses on mathematical formulas.


A9 PLL is composed as follows in fact, M and frac have a 0.5 weight factor:

                        PLL
           +-----------------------------------------------------+
           |                                                     |
           |             +--+                                    |
    in   >>---[ /N ]-->  |  |                     +-----+       |
           |             |  |---------------------| DCO | |----->> out
           |  +--------->|  |                     +--v--+ |
           |  |          +--+                        |           |
           |  |                                      |           |
           |  +--[ *(M + (F/Fmax) ] * 0.5 + Enable<--+  |
           |                                                     |
           +-----------------------------------------------------+


         out = in  * (M + frac / frac_max) * 0.5 / N


  If we ignore frac and set N = 1, it simplifies to:

         out = in  * M  * 0.5

This can be rewritten as:

         out = (in / 2) * M

The 0.5 weight is hardware-controlled:

         For GP0/HIFI PLL: controlled by CTRL0 bit27 (gate)
         For MCLK PLL: enabled by default, no gate bit

To model this in the clock tree, we add:

         A fixed /2 divider after input clock to represent the 0.5 weight
         A gate clock to represent the enable control

The resulting structure is:
         input --> fixed div2 --> gate--> dco

I would appreciate your guidance If this is not appropriate.


>> +
>> +/* The output frequency range of the A9 PLL_DCO is 1.4 GHz to 2.8 GHz. */
>> +static const struct pll_mult_range a9_pll_mult_range = {
>> +     .min = 117,
>> +     .max = 233,
>> +};
> If PLL restriction is actually the DCO output rate, and only the reason
> to keep the pre-devider in the range above, I would definitely welcome a
> rework to express the constraints properly and split the pre-divider out.
>
>> +
>> +static const struct reg_sequence a9_gp0_pll_init_regs[] = {
>> +     { .reg = GP0PLL_CTRL0, .def = 0x00010000 },
>> +     { .reg = GP0PLL_CTRL1, .def = 0x11480000 },
>> +     { .reg = GP0PLL_CTRL2, .def = 0x1219b010 },
>> +     { .reg = GP0PLL_CTRL3, .def = 0x00008010 }
>> +};
>> +
>> +static struct clk_regmap a9_gp0_pll_dco = {
>> +     .data = &(struct meson_clk_pll_data) {
>> +             .en = {
>> +                     .reg_off = GP0PLL_CTRL0,
>> +                     .shift   = 28,
>> +                     .width   = 1,
>> +             },
>> +             .m = {
>> +                     .reg_off = GP0PLL_CTRL0,
>> +                     .shift   = 0,
>> +                     .width   = 9,
>> +             },
>> +             .n = {
>> +                     .reg_off = GP0PLL_CTRL0,
>> +                     .shift   = 12,
>> +                     .width   = 3,
>> +             },
>> +             .frac = {
>> +                     .reg_off = GP0PLL_CTRL1,
>> +                     .shift   = 0,
>> +                     .width   = 17,
>> +             },
>> +             .l = {
>> +                     .reg_off = GP0PLL_CTRL0,
>> +                     .shift   = 31,
>> +                     .width   = 1,
>> +             },
>> +             .rst = {
>> +                     .reg_off = GP0PLL_CTRL0,
>> +                     .shift   = 29,
>> +                     .width   = 1,
>> +             },
>> +             .l_detect = {
>> +                     .reg_off = GP0PLL_CTRL0,
>> +                     .shift   = 30,
>> +                     .width   = 1,
>> +             },
>> +             .range = &a9_pll_mult_range,
>> +             .init_regs = a9_gp0_pll_init_regs,
>> +             .init_count = ARRAY_SIZE(a9_gp0_pll_init_regs),
>> +             .flags = CLK_MESON_PLL_RST_ACTIVE_LOW |
>> +                      CLK_MESON_PLL_N_POWER_OF_TWO |
>> +                      CLK_MESON_PLL_L_DETECT_ACTIVE_HIGH,
>> +     },
>> +     .hw.init = &(struct clk_init_data) {
>> +             .name = "gp0_pll_dco",
>> +             .ops = &meson_clk_pll_ops,
>> +             .parent_hws = (const struct clk_hw *[]) {
>> +                     &a9_gp0_in_div2.hw
>> +             },
>> +             .num_parents = 1,
>> +     },
>> +};
>> +
>> +/* For gp0, hifi and mclk pll, the maximum value of od is 4. */
>> +static const struct clk_div_table a9_pll_od_table[] = {
>> +     { 0,  1 },
>> +     { 1,  2 },
>> +     { 2,  4 },
>> +     { 3,  8 },
>> +     { 4,  16 },
>> +     { /* sentinel */ }
>> +};
>> +
>> +static struct clk_regmap a9_gp0_pll = {
>> +     .data = &(struct clk_regmap_div_data) {
>> +             .offset = GP0PLL_CTRL0,
>> +             .shift = 20,
>> +             .width = 3,
>> +             .table = a9_pll_od_table,
>> +     },
>> +     .hw.init = &(struct clk_init_data) {
>> +             .name = "gp0_pll",
>> +             .ops = &clk_regmap_divider_ops,
>> +             .parent_hws = (const struct clk_hw *[]) {
>> +                     &a9_gp0_pll_dco.hw
>> +             },
>> +             .num_parents = 1,
>> +             .flags = CLK_SET_RATE_PARENT,
>> +     },
>> +};
>> +
>> +static struct clk_fixed_factor a9_hifi0_in_div2_div = {
>> +     .mult = 1,
>> +     .div = 2,
>> +     .hw.init = &(struct clk_init_data){
>> +             .name = "hifi0_in_div2_div",
>> +             .ops = &clk_fixed_factor_ops,
>> +             .parent_data = &(const struct clk_parent_data) {
>> +                     .fw_name = "in0",
>> +             },
>> +             .num_parents = 1,
>> +     },
>> +};
>> +
>> +static struct clk_regmap a9_hifi0_in_div2 = {
>> +     .data = &(struct clk_regmap_gate_data) {
>> +             .offset = HIFIPLL_CTRL0,
>> +             .bit_idx = 27,
>> +     },
>> +     .hw.init = &(struct clk_init_data) {
>> +             .name = "hifi0_in_div2",
>> +             .ops = &clk_regmap_gate_ops,
>> +             .parent_hws = (const struct clk_hw *[]) {
>> +                     &a9_hifi0_in_div2_div.hw
>> +             },
>> +             .num_parents = 1,
>> +     },
>> +};
>> +
>> +static const struct reg_sequence a9_hifi0_pll_init_regs[] = {
>> +     { .reg = HIFIPLL_CTRL0, .def = 0x00010000 },
>> +     { .reg = HIFIPLL_CTRL1, .def = 0x11480000 },
>> +     { .reg = HIFIPLL_CTRL2, .def = 0x1219b010 },
>> +     { .reg = HIFIPLL_CTRL3, .def = 0x00008010 }
>> +};
> It look like GP0 and HIFI PLL are exactly the same IP, you've even
> documented it as such. Yet all the code is duplicated. That's not OK.
>
> I understand that way we statically declared the clocks so far pushed
> you in that direction. That's something I'd like to fix properly
> someday.
>
> In the meantime, you could at least duplicate the memory at runtime to
> avoid copy/pasting the code. A minor change to clkc utils as suggested
> at the end of this message could help you do so.
>
> Same probably applies to mclks.


You're right, the GP0 and HIFI PLLs are indeed the same IP, differing 
only by frac_max:
     GP0: frac_max = 2^17
     HIFI: frac_max = 100000


Each clock requires its own clk_regmap and clk_hw structure, though the 
data in
clk_regmap can be shared between HIFI0 and HIFI1.


I have tried duplicating HIFI1's clock structure from HIFI0 at runtime.
Most members of clk_init_data (except parent_hws / parent_data) can be 
easily copied.


However, I have a question regarding dynamic parent assignment:
For example:
Clock B is created dynamically, and its parent is clock A (also created 
dynamically).
How should I properly assign this parent relationship?


Furthermore, how to handle more complex parent configurations dynamically?
For example:
Clock D has three parents: C, B, A (in an irregular order).


I would appreciate your guidance on how to handle these dynamic clock 
relationships properly.

>
>
[...]


Best regards,

Jian





More information about the linux-arm-kernel mailing list