芯片手册中的clk框架
CCM(Clock Control Module)框架图

外部时钟的输入源有24MHz,32.768KHz以及四个EXT CLK。这7个输入源都可以直接连接到CCM,但是PLL只能以24MHz,32.768KHz作为输入。从PLL和分频器出来的时钟也可以作为CCM的输入。每一个Slice在经过MUX模块后,由分频器产生我们需要的时钟频率,然后再输出给Gate模块,以便控制时钟的开关。
CLK ROOT相关寄存器

每一个Slice都有自己的index、偏移地址和时钟源配置寄存器CCM_TARGET_ROOTn。这里的偏移地址是相对于CCM的基地址而言的。
PLL输出
PLL的输出即CCM的输入,下图是所有的PLL信息。PLL的参数设置由CCM_ANALOG系列寄存器,例如PLL配置寄存器(外部时钟输入选择(24MHz/PAD_CLK),分频系数P、M、K、S设置寄存器。PAD_CLK可以选择CLKIN1 或 CLKIN2。


PLL的计算公式如下:
F
o
u
t
=
(
m
+
k
65536
)
∗
F
i
n
p
∗
2
s
F_{out} = \frac{(m+\frac{k}{65536})*F_{in}}{p*2^s}
Fout=p∗2s(m+65536k)∗Fin
在频率范围内的clock基本上都能用此公式计算出来。ARM PLL, GPU PLL, VPU PLL, DRAM PLL, Audio PLL1/2, and Video PLL1都是基本PLL。
时钟分频器
同步时钟分频器(PRE_PODF和POST_PODF)可用于对源时钟频率执行整数除法。在除法因子变化期间,分频器保证其输出上有一个干净的时钟信号。分频器执行**1/(N+1)**除法。故障可能导致同步分频器进入无法恢复的状态,因此必须向同步分频器提供干净的时钟信号。
时钟门控器
CCM可以根据系统低功耗模式对片上外围设备进行自动时钟关闭。每个逻辑域可以分别声明其在每个时钟上的依赖级别。如果检测到不依赖任何域的时钟,它将被关闭以节省电力。时钟门控单元用于控制时钟输出。时钟门控器在LPCG中使用活动时钟门控,这意味着需要活动时钟根。时钟生成模块仅对时钟源执行多路复用、门控和除分。因此,当相应的时钟源停止时,生成模块的时钟根将停止。必须为LPCG主动门控的时钟根设置ENABLE位。
8对1多路时钟复位器
8对1多路复用器是一种组合多路复用器,可以随时切换时钟源。多路复用器输出不能保证干净的时钟信号。在更改多路复用器选择之前,必须对多路复用器的输出时钟路径进行时钟门控。
时钟切片
每个切片单源都连接着8个来自PLL/PLL分频器的输入时钟,切片分为五大类,分别为Core, Bus, Peripheral (IP), 和DRAM。

下图显示了时钟切片可以包含的CCM时钟组件,以及相关的寄存器控件。并非所有时钟切片类型都将包含下图中提供的所有组件。请参考以下部分,以确定特定时钟切片类型中包含的组件。下面显示的切片由一个后分隔器和一个带有2个输入源的时钟开关多路复用器组成。每个输入源内部都有一个预分频器、一个时钟门和一个时钟多路复用器。

IP切片和上图有些不同,为了满足更小的外设时钟要求,支持两个分频器设置,最大分频数为512。

CCGR(CCM Clock Gating Register )接口(翻译)
当进入或离开低功耗模式时,CCM可以控制CCM_ANALOG中的PLL。在开机重置(POR)后进入低功耗模式之前,软件必须在CCM_ANALOG中设置PLL覆盖。
一个逻辑领域有四个级别的低功耗模式:
- No need
- RUN
- STOP
- WAIT
CCM仅在域状态在STOP之间切换时采取行动(深度睡眠模式与STOP相同)。有4个域可以分配。RDC可以将任何CPU平台分配给任何域。如果域为空,则该域被视为STOP。
每个域都可以向CCM声明其依赖性。不允许使用任何时钟,而不在自己的域中声明。域通过写入依赖级别来声明其对时钟的依赖性。针对低功耗模式下行为的设置如下:

每个域只能更改分配给控制访问的位。任何无关紧要的写给它的东西都会被忽略。例如,域0只能写入位[1:0]。除位[1:0]外,写入的任何位都将被忽略。其他域可以读取所有其他域设置。域0的默认值为2,关机后将进入STOP模式。当其他域设置的默认值为0时,将不需要它。
设置时钟源时,设置不会立即生效。该设置将首先进入影子寄存器。如果PLL关闭或新设置进入影子寄存器以声明对PLL的依赖性,PLL将立即打开。当PLL准备就绪时,影子寄存器中的设置将更新为新设置。在此期间,将设置和清除待处理位。然后,CCM将发送PLL控制信号作为影子寄存器,并根据设置寄存器通知GPCPLL状态。在其他情况下,设置将立即从影子寄存器中更新。时钟源相互依赖。

子CLK分频公式
F r e q = ( c l o c k s o u r c e f r e q ) ( p r e _ d i v + 1 ) ∗ ( p o s t _ d i v + 1 ) Freq = \frac{(clock source freq)}{(pre\_div+1)*(post\_div+1)} Freq=(pre_div+1)∗(post_div+1)(clocksourcefreq)
驱动中clk的一些概念
(1)fixed rate clock
这一类clock具有固定的频率,不能开关、不能调整频率、不能选择parent、不需要提供任何的clk_ops回调函数,是最简单的一类clock。可以直接通过DTS配置的方式支持,clock framework core能直接从DTS中解出clock信息,并自动注册到kernel,不需要任何driver支持。例如24MHz晶振和32.768K的晶振。


(2)fixed factor clock
这一类clock具有固定的factor(即multiplier和divider),clock的频率是由parent clock的频率,乘以mul,除以div,多用于一些具有固定分频系数的clock。由于parent clock的频率可以改变,因而fix factor clock也可该改变频率,因此也会提供.recalc_rate/.set_rate/.round_rate等回调。以第一行的clk为例,这里的"sys_pll1_50m"就是我们想要的fixed factor clock。“sys_pll1_50m"的clk的父时钟节点为"sys_pll2_out”(1000MHz),倍频系数为1,分频系数为20。

(3)mux clock
这一类clock可以选择多个parent,因为会实现.get_parent/.set_parent/.recalc_rate回调。该接口可注册mux控制比较规则的clock(类似divider clock):

(4)divider clock
这一类clock可以设置分频值(因而会提供.recalc_rate/.set_rate/.round_rate回调)。

(5)gate clock
这一类clock只可开关(会提供.enable/.disable回调),可使用下面接口注册:

(6)composite clock
顾名思义,就是mux、divider、gate等clock的组合,可通过下面接口注册。

i.MX平台的各类CLK API
上面简单列举了Linux驱动中的CLK类型,现在我们从API的角度深入分析这些类型的驱动。clk驱动是基于数据手册中的CCM_BASE(0x3038_0000)和CCM_ANALOG(0x30360000)寄存器组。
正式开始之前
驱动中的clk_hw_onecell_data结构存储clk的数量以及clk_hw结构,每一个clk都有自己的clk_hw结构。也就是说驱动中会使用hws[clk_index]的形式存储每一个clk信息。
struct clk_hw_onecell_data {
unsigned int num;
struct clk_hw *hws[];
};
struct clk_hw {
struct clk_core *core;
struct clk *clk;
const struct clk_init_data *init;
};
clk_core代表clock framework的核心驱动对象。
clk结构存储具体的clk信息,例如父节点、可选择的父节点列表和数量、寄存器中的位移和位宽、它们的子节点、enable和status寄存器地址,时钟速度、clk标志位等等信息。
struct clk {
struct list_head node;
struct clk *parent;
struct clk **parent_table; /* list of parents to */
unsigned short parent_num; /* choose between */
unsigned char src_shift; /* source clock field in the */
unsigned char src_width; /* configuration register */
struct sh_clk_ops *ops;
struct list_head children;
struct list_head sibling; /* node for children */
int usecount;
unsigned long rate;
unsigned long flags;
void __iomem *enable_reg;
void __iomem *status_reg;
unsigned int enable_bit;
void __iomem *mapped_reg;
unsigned int div_mask;
unsigned long arch_flags;
void *priv;
struct clk_mapping *mapping;
struct cpufreq_frequency_table *freq_table;
unsigned int nr_freqs;
};
clk_init_data中是clock框架中共享的初始化数据。
(1)fixed rate clock
这里以24MHz的晶振为例,hws是一个数组
hws[IMX8MP_CLK_24M] = imx_obtain_fixed_clk_hw(np, "osc_24m");
imx_obtain_fixed_clk_hw解析24MHz晶振的设备树节点,然后使用__clk_get_hw添加进clk framework(core->hw)。IMX8MP_CLK_24M这里的索引值和dts保持一致。
(2)fixed factor clock
imx_clk_hw_fixed_factor主要用于产生固定的PLL分频。例如基于sys_pll1分出的固定频率组。
hws[IMX8MP_SYS_PLL1_400M] = imx_clk_hw_fixed_factor("sys_pll1_400m", "sys_pll1_out", 1, 2);
hws[IMX8MP_SYS_PLL1_800M] = imx_clk_hw_fixed_factor("sys_pll1_800m", "sys_pll1_out", 1, 1);
最终会调用到__clk_hw_register_fixed_factor。

__clk_hw_register_fixed_factor函数的主要思想如下:
-
将倍频系数和分频系数存入fix结构;
-
设置init数据段中的clk_fixed_factor_ops(产生固定频率的函数集)和name(“sys_pll1_400m”),这里的clk_init_data在clk provider和clk framework之间是共享的。
-
将这个clk_hw硬件时钟对象注册进clk core。
由于IMX8MP_SYS_PLL1_400M=55,最后上面那行clk代码的含义为:第55个hws结构中的clk名为"sys_pll1_400m",父节点"sys_pll1_out",分频系数2,倍频系数1。它所支持的ops为clk_fixed_factor_ops。

clk_fixed_factor_ops代码如下:
static unsigned long clk_factor_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
struct clk_fixed_factor *fix = to_clk_fixed_factor(hw);
unsigned long long int rate;
rate = (unsigned long long int)parent_rate * fix->mult;
do_div(rate, fix->div);
return (unsigned long)rate;
}
static long clk_factor_round_rate(struct clk_hw *hw, unsigned long rate,
unsigned long *prate)
{
struct clk_fixed_factor *fix = to_clk_fixed_factor(hw);
if (clk_hw_get_flags(hw) & CLK_SET_RATE_PARENT) {
unsigned long best_parent;
best_parent = (rate / fix->mult) * fix->div;
*prate = clk_hw_round_rate(clk_hw_get_parent(hw), best_parent);
}
return (*prate / fix->div) * fix->mult;
}
static int clk_factor_set_rate(struct clk_hw *hw, unsigned long rate,
unsigned long parent_rate)
{
return 0;
}
const struct clk_ops clk_fixed_factor_ops = {
.round_rate = clk_factor_round_rate,
.set_rate = clk_factor_set_rate,
.recalc_rate = clk_factor_recalc_rate,
};
由于是固定频率的时钟,因此set_rate函数直接返回成功即可。
//待分析调用过程
(3)mux clock
hws[IMX8MP_VIDEO_PLL1_REF_SEL] = imx_clk_hw_mux("video_pll1_ref_sel", anatop_base + 0x28, 0, 2, pll_ref_sels, ARRAY_SIZE(pll_ref_sels));
static const char * const pll_ref_sels[] = { "osc_24m", "dummy", "dummy", "dummy", };
imx_clk_hw_mux将"video_pll1_ref_sel"的偏移地址mux->reg,寄存器宽度2-1(mux->shift)以及可选的父节点source列表。__clk_hw_register_mux的调用流程和上一个类似,将相关ops结果体注册进clk core框架。注意整个注册过程必须锁定。

mux相关的ops代码如下,
static u8 clk_mux_get_parent(struct clk_hw *hw)
{
struct clk_mux *mux = to_clk_mux(hw);
u32 val;
val = clk_mux_readl(mux) >> mux->shift;
val &= mux->mask;
//查表
return clk_mux_val_to_index(hw, mux->table, mux->flags, val);
}
static int clk_mux_set_parent(struct clk_hw *hw, u8 index)
{
struct clk_mux *mux = to_clk_mux(hw);
u32 val = clk_mux_index_to_val(mux->table, mux->flags, index);
unsigned long flags = 0;
u32 reg;
if (mux->lock)
spin_lock_irqsave(mux->lock, flags);
else
__acquire(mux->lock);
if (mux->flags & CLK_MUX_HIWORD_MASK) {
reg = mux->mask << (mux->shift + 16);
} else {
reg = clk_mux_readl(mux);
reg &= ~(mux->mask << mux->shift);
}
val = val << mux->shift;
reg |= val;
clk_mux_writel(mux, reg);
if (mux->lock)
spin_unlock_irqrestore(mux->lock, flags);
else
__release(mux->lock);
return 0;
}
static int clk_mux_determine_rate(struct clk_hw *hw,
struct clk_rate_request *req)
{
struct clk_mux *mux = to_clk_mux(hw);
return clk_mux_determine_rate_flags(hw, req, mux->flags);
}
const struct clk_ops clk_mux_ops = {
.get_parent = clk_mux_get_parent,
.set_parent = clk_mux_set_parent,
.determine_rate = clk_mux_determine_rate,
};
(4)divider clock
hws[IMX8MP_CLK_IPG_ROOT] = imx_clk_hw_divider2("ipg_root", "ahb_root", ccm_base + 0x9080, 0, 1);
实际调用__clk_hw_register_divider,核心过程和之前的mux时钟类似,也是设置divider clock相关的ops,供驱动调用。这里第三个参数是寄存器地址。

clk_divider_set_rate是如何拿到需要写入的寄存器地址呢?在__clk_hw_register_divider中会将传入的ccm_base+0x9080,shift和width依次存入clk_divider中的reg、shift和width,然后注册进了clk core系统。在驱动里调用set_rate函数时,core会根据传入的hw拿到clk_divider结构,因此也就找到了需要写入的寄存器信息。
static int clk_divider_set_rate(struct clk_hw *hw, unsigned long rate,
unsigned long parent_rate)
{
struct clk_divider *divider = to_clk_divider(hw);
int value;
unsigned long flags = 0;
u32 val;
value = divider_get_val(rate, parent_rate, divider->table,
divider->width, divider->flags);
if (value < 0)
return value;
if (divider->lock)
spin_lock_irqsave(divider->lock, flags);
else
__acquire(divider->lock);
if (divider->flags & CLK_DIVIDER_HIWORD_MASK) {
val = clk_div_mask(divider->width) << (divider->shift + 16);
} else {
val = clk_div_readl(divider);
val &= ~(clk_div_mask(divider->width) << divider->shift);
}
val |= (u32)value << divider->shift;
clk_div_writel(divider, val);
if (divider->lock)
spin_unlock_irqrestore(divider->lock, flags);
else
__release(divider->lock);
return 0;
}
const struct clk_ops clk_divider_ops = {
.recalc_rate = clk_divider_recalc_rate,
.round_rate = clk_divider_round_rate,
.set_rate = clk_divider_set_rate,
};
(5)gate clock
这里写入clk系统的的寄存器是CCM_ANALOG_VIDEO_PLL1_GEN_CTRL。
hws[IMX8MP_VIDEO_PLL1_OUT] = imx_clk_hw_gate("video_pll1_out", "video_pll1_bypass", anatop_base + 0x28, 13);
其中控制gate的位是13位。

软件部分的逻辑和其他clock类似,通过将寄存器信息写入clk_gate,然后将其注册进clk core框架中。

(6)composite clock
这里API不再是通用API,而是位于drivers/clk/imx/clk-composite-8m.c里的imx8m_clk_hw_composite_flags,下面的API差异是传入的flag不同引起的。从而区别不同场景下的clk composite要求。
static const char * const imx8mp_media_apb_sels[] = {"osc_24m", "sys_pll2_125m", "sys_pll1_800m",
"sys_pll3_out", "sys_pll1_40m", "audio_pll2_out",
"clk_ext1", "sys_pll1_133m", };
hws[IMX8MP_CLK_MEDIA_APB] = imx8m_clk_hw_composite_bus("media_apb", imx8mp_media_apb_sels, ccm_base + 0x8a80);
hws[IMX8MP_CLK_PWM1] = imx8m_clk_hw_composite("pwm1", imx8mp_pwm1_sels, ccm_base + 0xb380);
对于 media apb clk,偏移量为8a80,


对于CORE的composite clk,divider_ops使用的是通用API;对于总线类型的composite clk,divider_ops使用的是自定义的API。


imx8m_clk_composite_mux_ops支持选择时钟的父节点、获取时钟的父节点。
imx8m_clk_composite_divider_ops支持计算预分频和分频参数、设置分频参数。
示例分析
1.驱动API加载过程
前面的章节已经说过,gate,mux,divider等api的使用将CLK SPEC操作相关的信息(名字,寄存器位移等等)写入了clk core框架。imx8mp vendor层的clk core驱动中的主要分为三步:
- of_clk_add_hw_provider,将此soc dts节点,clk解析回调函数of_clk_hw_onecell_get和回调数据注册进clk core。其中clk_hw_data包含了所有的hw clk信息。这一步最重要。
- imx_clk_init_on优先初始化dts中定义的"init-on-array"时钟节点,例如NAND、SD、HSIO时钟。
- imx_register_uart_clocks注册串口时钟。
clk_hw_data->num = IMX8MP_CLK_END;
hws = clk_hw_data->hws;
of_clk_add_hw_provider(np, of_clk_hw_onecell_get, clk_hw_data);
imx_clk_init_on(np, hws);
imx_register_uart_clocks(4);
PLL驱动
drivers/clk/imx/clk-pll14xx.c,此驱动设置了基础PLL的值。pll1443x是基础PLL,pll1416x是整型PLL。pll1443的pmks表如下:
static const struct imx_pll14xx_rate_table imx_pll1443x_tbl[] = {
PLL_1443X_RATE(1039500000U, 173, 2, 1, 16384),
PLL_1443X_RATE(650000000U, 325, 3, 2, 0),
PLL_1443X_RATE(594000000U, 198, 2, 2, 0),
PLL_1443X_RATE(519750000U, 173, 2, 2, 16384),
PLL_1443X_RATE(393216000U, 262, 2, 3, 9437),
PLL_1443X_RATE(361267200U, 361, 3, 3, 17511),
};
在clk信息写入时,已经将pll 表传入了clk core。这时imx_dev_clk_hw_pll14xx函数会用于初始化pll1443相关的信息,最重要的就是将clk_pll1443x_ops注册进框架里,尤其是PLL的prepare,set_rate等函数。
hws[IMX8MM_VIDEO_PLL1] = imx_clk_hw_pll14xx("video_pll1", "video_pll1_ref_sel", base + 0x28, &imx_1443x_pll);
static const struct clk_ops clk_pll1443x_ops = {
.prepare = clk_pll14xx_prepare,
.unprepare = clk_pll14xx_unprepare,
.is_prepared = clk_pll14xx_is_prepared,
.recalc_rate = clk_pll1443x_recalc_rate,
.round_rate = clk_pll14xx_round_rate,
.set_rate = clk_pll1443x_set_rate,
};
2.LVDS 中CLK驱动API调用过程
驱动使用demo
下面是驱动中使用的常用API,我们会对其调用过程进行分析。
struct clk *clk_root;
clk_root = devm_clk_get(dev, "ldb");
clk_prepare(clk_root);
clk_enable(clk_root);
clk_set_rate(clk_root, rate_xxx);
devm_clk_get
对于devm_clk_get函数来说,通过clk_get(dev, id)拿到ldb对于的clk数组。of_parse_clkspec函数会根据传入的clock names链表中查找匹配,找到到clock-names = "ldb"对应的索引值,然后在一个列表中查找由phandle指向的节点并返回给clkspec
clk_get-->of_clk_get_hw
struct clk_hw *of_clk_get_hw(struct device_node *np, int index,
const char *con_id)
{
int ret;
struct clk_hw *hw;
struct of_phandle_args clkspec;
ret = of_parse_clkspec(np, index, con_id, &clkspec);
if (ret)
return ERR_PTR(ret);
hw = of_clk_get_hw_from_clkspec(&clkspec);
of_node_put(clkspec.np);
return hw;
}
of_clk_get_hw_from_clkspec再根据clkspec查找到clk对于的clk_hw结构。由于在clk_imx8mp.c驱动的probe函数中提前讲hw clk provider注册进了clk框架,也就是我们在第三章提到了各类注册clk的数据。
of_clk_add_hw_provider(np, of_clk_hw_onecell_get, clk_hw_data);
对于我们这里ldb设备树中定义的clock index值(314),of_clk_get_hw_from_clkspec去hw clk provider中寻找clkspec所对应的索引数据。也就是我们在clk驱动中写入的ldb root相关的clk_gate信息。
hws[IMX8MP_CLK_MEDIA_LDB_ROOT] = imx_clk_hw_gate2_shared2("media_ldb_root_clk", "media_ldb", ccm_base + 0x45d0, 0, &share_count_media);
clk_prepare_enable
clk_prepare_enable是.prepare和.enable函数的组合函数。先让clk framework做好,然后使能clk framework中的时钟资源。
ret = clk_prepare(clk);
if (ret)
return ret;
ret = clk_enable(clk);
clk_prepare的定义如下,clk_prepare可以睡眠,这使它与clk_enable不同。 在一个简单的例子中,如果操作可能会休眠,clk_prepare可以代替clk_enable来解除一个clk的门。 其中一个例子是一个通过I2c访问的clk。 在复杂的情况下,一个clk ungate操作可能需要一个快速和一个慢速部分。
正是这个原因,clk_prepare和clk_enable不是相互排斥的。 事实上,clk_prepare必须在clk_enable之前调用。clk_core_prepare_lock对与时钟的操作进行了加锁,然后调用 core->ops->prepare(core->hw)。clk_prepare会从parent开始逐级调用.prepare函数,例如在clk_pll14xx_prepare中会等待PLL锁定。
int clk_prepare(struct clk *clk)
{
if (!clk)
return 0;
return clk_core_prepare_lock(clk->core);
}
clk_enable
clk_enable的调用过程如下,最终会打开寄存器的gate enable位。
clk_enable(clk);
clk->ops->enable(clk->hw);
[resolves to...]
clk_gate_enable(hw);
[resolves struct clk gate with to_clk_gate(hw)]
clk_gate_set_bit(gate);
clk_set_rate
clk_set_rate核心函数如下。
clk_set_rate(clk, rate);
clk->ops->set_rate(clk, rate);
clk->rate = clk->ops->recalc(clk);
propagate_rate(clk);
以IMX8MP_CLK_MEDIA_LDB举例,我们在驱动中需要将其设置为74.25*7MHz。在clk驱动中我们将其注册成了一个composite类型的clk,这意味着我们可以选择input source,设置分频,gate控制。
hws[IMX8MP_CLK_MEDIA_LDB] = imx8m_clk_hw_composite("media_ldb", imx8mp_media_ldb_sels, ccm_base + 0xbf00);
因此在这里clk->ops->set_rate指imx8m_clk_composite_divider_set_rate,clk->ops->recalc(clk)指imx8m_clk_composite_divider_recalc_rate。
imx8m_clk_composite_divider_set_rate
imx8m_clk_composite_compute_dividers会根据所需的频率和parent source的频率计算预分频参数和分频参数,然后将两个参数设置进寄存器。
static int imx8m_clk_composite_divider_set_rate(struct clk_hw *hw,
unsigned long rate,
unsigned long parent_rate)
{
struct clk_divider *divider = to_clk_divider(hw);
unsigned long flags;
int prediv_value;
int div_value;
int ret;
u32 val;
ret = imx8m_clk_composite_compute_dividers(rate, parent_rate,
&prediv_value, &div_value);
if (ret)
return -EINVAL;
spin_lock_irqsave(divider->lock, flags);
val = readl(divider->reg);
val &= ~((clk_div_mask(divider->width) << divider->shift) |
(clk_div_mask(PCG_DIV_WIDTH) << PCG_DIV_SHIFT));
val |= (u32)(prediv_value - 1) << divider->shift;
val |= (u32)(div_value - 1) << PCG_DIV_SHIFT;
writel(val, divider->reg);
spin_unlock_irqrestore(divider->lock, flags);
return ret;
}
imx8m_clk_composite_divider_recalc_rate
imx8m_clk_composite_divider_recalc_rate验证刚才的分频是否正确。
static unsigned long imx8m_clk_composite_divider_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
struct clk_divider *divider = to_clk_divider(hw);
unsigned long prediv_rate;
unsigned int prediv_value;
unsigned int div_value;
prediv_value = readl(divider->reg) >> divider->shift;
prediv_value &= clk_div_mask(divider->width);
prediv_rate = divider_recalc_rate(hw, parent_rate, prediv_value,
NULL, divider->flags,
divider->width);
div_value = readl(divider->reg) >> PCG_DIV_SHIFT;
div_value &= clk_div_mask(PCG_DIV_WIDTH);
return divider_recalc_rate(hw, prediv_rate, div_value, NULL,
divider->flags, PCG_DIV_WIDTH);
}