blob: 417ade3d2565d97b43ef4be7f8564bd0c96b6c7a [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2016 Free Electrons
* Copyright (C) 2016 NextThing Co
*
* Maxime Ripard <maxime.ripard@free-electrons.com>
*/
#include <linux/clk-provider.h>
#include <linux/regmap.h>
#include "sun4i_tcon.h"
#include "sun4i_dotclock.h"
struct sun4i_dclk {
struct clk_hw hw;
struct regmap *regmap;
struct sun4i_tcon *tcon;
};
static inline struct sun4i_dclk *hw_to_dclk(struct clk_hw *hw)
{
return container_of(hw, struct sun4i_dclk, hw);
}
static void sun4i_dclk_disable(struct clk_hw *hw)
{
struct sun4i_dclk *dclk = hw_to_dclk(hw);
regmap_update_bits(dclk->regmap, SUN4I_TCON0_DCLK_REG,
BIT(SUN4I_TCON0_DCLK_GATE_BIT), 0);
}
static int sun4i_dclk_enable(struct clk_hw *hw)
{
struct sun4i_dclk *dclk = hw_to_dclk(hw);
return regmap_update_bits(dclk->regmap, SUN4I_TCON0_DCLK_REG,
BIT(SUN4I_TCON0_DCLK_GATE_BIT),
BIT(SUN4I_TCON0_DCLK_GATE_BIT));
}
static int sun4i_dclk_is_enabled(struct clk_hw *hw)
{
struct sun4i_dclk *dclk = hw_to_dclk(hw);
u32 val;
regmap_read(dclk->regmap, SUN4I_TCON0_DCLK_REG, &val);
return val & BIT(SUN4I_TCON0_DCLK_GATE_BIT);
}
static unsigned long sun4i_dclk_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
struct sun4i_dclk *dclk = hw_to_dclk(hw);
u32 val;
regmap_read(dclk->regmap, SUN4I_TCON0_DCLK_REG, &val);
val >>= SUN4I_TCON0_DCLK_DIV_SHIFT;
val &= (1 << SUN4I_TCON0_DCLK_DIV_WIDTH) - 1;
if (!val)
val = 1;
return parent_rate / val;
}
static long sun4i_dclk_round_rate(struct clk_hw *hw, unsigned long rate,
unsigned long *parent_rate)
{
struct sun4i_dclk *dclk = hw_to_dclk(hw);
struct sun4i_tcon *tcon = dclk->tcon;
unsigned long best_parent = 0;
u8 best_div = 1;
int i;
for (i = tcon->dclk_min_div; i <= tcon->dclk_max_div; i++) {
u64 ideal = (u64)rate * i;
unsigned long rounded;
/*
* ideal has overflowed the max value that can be stored in an
* unsigned long, and every clk operation we might do on a
* truncated u64 value will give us incorrect results.
* Let's just stop there since bigger dividers will result in
* the same overflow issue.
*/
if (ideal > ULONG_MAX)
goto out;
rounded = clk_hw_round_rate(clk_hw_get_parent(hw),
ideal);
if (rounded == ideal) {
best_parent = rounded;
best_div = i;
goto out;
}
if (abs(rate - rounded / i) <
abs(rate - best_parent / best_div)) {
best_parent = rounded;
best_div = i;
}
}
out:
*parent_rate = best_parent;
return best_parent / best_div;
}
static int sun4i_dclk_set_rate(struct clk_hw *hw, unsigned long rate,
unsigned long parent_rate)
{
struct sun4i_dclk *dclk = hw_to_dclk(hw);
u8 div = parent_rate / rate;
return regmap_update_bits(dclk->regmap, SUN4I_TCON0_DCLK_REG,
GENMASK(6, 0), div);
}
static int sun4i_dclk_get_phase(struct clk_hw *hw)
{
struct sun4i_dclk *dclk = hw_to_dclk(hw);
u32 val;
regmap_read(dclk->regmap, SUN4I_TCON0_IO_POL_REG, &val);
val >>= 28;
val &= 3;
return val * 120;
}
static int sun4i_dclk_set_phase(struct clk_hw *hw, int degrees)
{
struct sun4i_dclk *dclk = hw_to_dclk(hw);
u32 val = degrees / 120;
val <<= 28;
regmap_update_bits(dclk->regmap, SUN4I_TCON0_IO_POL_REG,
GENMASK(29, 28),
val);
return 0;
}
static const struct clk_ops sun4i_dclk_ops = {
.disable = sun4i_dclk_disable,
.enable = sun4i_dclk_enable,
.is_enabled = sun4i_dclk_is_enabled,
.recalc_rate = sun4i_dclk_recalc_rate,
.round_rate = sun4i_dclk_round_rate,
.set_rate = sun4i_dclk_set_rate,
.get_phase = sun4i_dclk_get_phase,
.set_phase = sun4i_dclk_set_phase,
};
int sun4i_dclk_create(struct device *dev, struct sun4i_tcon *tcon)
{
const char *clk_name, *parent_name;
struct clk_init_data init;
struct sun4i_dclk *dclk;
int ret;
parent_name = __clk_get_name(tcon->sclk0);
ret = of_property_read_string_index(dev->of_node,
"clock-output-names", 0,
&clk_name);
if (ret)
return ret;
dclk = devm_kzalloc(dev, sizeof(*dclk), GFP_KERNEL);
if (!dclk)
return -ENOMEM;
dclk->tcon = tcon;
init.name = clk_name;
init.ops = &sun4i_dclk_ops;
init.parent_names = &parent_name;
init.num_parents = 1;
init.flags = CLK_SET_RATE_PARENT;
dclk->regmap = tcon->regs;
dclk->hw.init = &init;
tcon->dclk = clk_register(dev, &dclk->hw);
if (IS_ERR(tcon->dclk))
return PTR_ERR(tcon->dclk);
return 0;
}
EXPORT_SYMBOL(sun4i_dclk_create);
int sun4i_dclk_free(struct sun4i_tcon *tcon)
{
clk_unregister(tcon->dclk);
return 0;
}
EXPORT_SYMBOL(sun4i_dclk_free);