diff options
Diffstat (limited to 'drivers/gpu/drm/sun4i/sun4i_dotclock.c')
-rw-r--r-- | drivers/gpu/drm/sun4i/sun4i_dotclock.c | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/drivers/gpu/drm/sun4i/sun4i_dotclock.c b/drivers/gpu/drm/sun4i/sun4i_dotclock.c new file mode 100644 index 000000000..417ade3d2 --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun4i_dotclock.c @@ -0,0 +1,206 @@ +// 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); |