mirror of
https://github.com/AsahiLinux/u-boot
synced 2024-12-21 02:33:07 +00:00
b4a72a9f5b
Add driver to support TI K3 generation SoC clocks. This driver registers the clocks provided via platform data, and adds support for controlling the clocks via DT handles. Signed-off-by: Tero Kristo <t-kristo@ti.com> Signed-off-by: Tero Kristo <kristo@kernel.org>
374 lines
8.7 KiB
C
374 lines
8.7 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/*
|
|
* Texas Instruments K3 clock driver
|
|
*
|
|
* Copyright (C) 2020 Texas Instruments Incorporated - http://www.ti.com/
|
|
* Tero Kristo <t-kristo@ti.com>
|
|
*/
|
|
|
|
#include <common.h>
|
|
#include <dm.h>
|
|
#include <errno.h>
|
|
#include <soc.h>
|
|
#include <clk-uclass.h>
|
|
#include "k3-clk.h"
|
|
|
|
#define PLL_MIN_FREQ 800000000
|
|
#define PLL_MAX_FREQ 3200000000UL
|
|
#define PLL_MAX_DIV 127
|
|
|
|
/**
|
|
* struct clk_map - mapping from dev/clk id tuples towards physical clocks
|
|
* @dev_id: device ID for the clock
|
|
* @clk_id: clock ID for the clock
|
|
* @clk: pointer to the registered clock entry for the mapping
|
|
*/
|
|
struct clk_map {
|
|
u16 dev_id;
|
|
u32 clk_id;
|
|
struct clk *clk;
|
|
};
|
|
|
|
/**
|
|
* struct ti_clk_data - clock controller information structure
|
|
* @map: mapping from dev/clk id tuples to physical clock entries
|
|
* @size: number of entries in the map
|
|
*/
|
|
struct ti_clk_data {
|
|
struct clk_map *map;
|
|
int size;
|
|
};
|
|
|
|
static ulong osc_freq;
|
|
|
|
static void clk_add_map(struct ti_clk_data *data, struct clk *clk,
|
|
u32 dev_id, u32 clk_id)
|
|
{
|
|
struct clk_map *map;
|
|
|
|
debug("%s: added clk=%p, data=%p, dev=%d, clk=%d\n", __func__,
|
|
clk, data, dev_id, clk_id);
|
|
if (!clk)
|
|
return;
|
|
|
|
map = data->map + data->size++;
|
|
|
|
map->dev_id = dev_id;
|
|
map->clk_id = clk_id;
|
|
map->clk = clk;
|
|
}
|
|
|
|
static const struct soc_attr ti_k3_soc_clk_data[] = {
|
|
#if IS_ENABLED(CONFIG_SOC_K3_J721E)
|
|
{
|
|
.family = "J721E",
|
|
.data = &j721e_clk_platdata,
|
|
},
|
|
{
|
|
.family = "J7200",
|
|
.data = &j7200_clk_platdata,
|
|
},
|
|
#endif
|
|
{ /* sentinel */ }
|
|
};
|
|
|
|
static int ti_clk_probe(struct udevice *dev)
|
|
{
|
|
struct ti_clk_data *data = dev_get_priv(dev);
|
|
struct clk *clk;
|
|
const char *name;
|
|
const struct clk_data *ti_clk_data;
|
|
int i, j;
|
|
const struct soc_attr *soc_match_data;
|
|
const struct ti_k3_clk_platdata *pdata;
|
|
|
|
debug("%s(dev=%p)\n", __func__, dev);
|
|
|
|
soc_match_data = soc_device_match(ti_k3_soc_clk_data);
|
|
if (!soc_match_data)
|
|
return -ENODEV;
|
|
|
|
pdata = (const struct ti_k3_clk_platdata *)soc_match_data->data;
|
|
|
|
data->map = kcalloc(pdata->soc_dev_clk_data_cnt, sizeof(*data->map),
|
|
GFP_KERNEL);
|
|
data->size = 0;
|
|
|
|
for (i = 0; i < pdata->clk_list_cnt; i++) {
|
|
ti_clk_data = &pdata->clk_list[i];
|
|
|
|
switch (ti_clk_data->type) {
|
|
case CLK_TYPE_FIXED_RATE:
|
|
name = ti_clk_data->clk.fixed_rate.name;
|
|
clk = clk_register_fixed_rate(NULL,
|
|
name,
|
|
ti_clk_data->clk.fixed_rate.rate);
|
|
break;
|
|
case CLK_TYPE_DIV:
|
|
name = ti_clk_data->clk.div.name;
|
|
clk = clk_register_divider(NULL, name,
|
|
ti_clk_data->clk.div.parent,
|
|
ti_clk_data->clk.div.flags,
|
|
map_physmem(ti_clk_data->clk.div.reg, 0, MAP_NOCACHE),
|
|
ti_clk_data->clk.div.shift,
|
|
ti_clk_data->clk.div.width,
|
|
0);
|
|
break;
|
|
case CLK_TYPE_MUX:
|
|
name = ti_clk_data->clk.mux.name;
|
|
clk = clk_register_mux(NULL, name,
|
|
ti_clk_data->clk.mux.parents,
|
|
ti_clk_data->clk.mux.num_parents,
|
|
ti_clk_data->clk.mux.flags,
|
|
map_physmem(ti_clk_data->clk.mux.reg, 0, MAP_NOCACHE),
|
|
ti_clk_data->clk.mux.shift,
|
|
ti_clk_data->clk.mux.width,
|
|
0);
|
|
break;
|
|
case CLK_TYPE_PLL:
|
|
name = ti_clk_data->clk.pll.name;
|
|
clk = clk_register_ti_pll(name,
|
|
ti_clk_data->clk.pll.parent,
|
|
map_physmem(ti_clk_data->clk.pll.reg, 0, MAP_NOCACHE));
|
|
|
|
if (!osc_freq)
|
|
osc_freq = clk_get_rate(clk_get_parent(clk));
|
|
break;
|
|
default:
|
|
name = NULL;
|
|
clk = NULL;
|
|
printf("WARNING: %s has encountered unknown clk type %d\n",
|
|
__func__, ti_clk_data->type);
|
|
}
|
|
|
|
if (clk && ti_clk_data->default_freq)
|
|
clk_set_rate(clk, ti_clk_data->default_freq);
|
|
|
|
if (clk && name) {
|
|
for (j = 0; j < pdata->soc_dev_clk_data_cnt; j++) {
|
|
if (!strcmp(name, pdata->soc_dev_clk_data[j].clk_name)) {
|
|
clk_add_map(data, clk, pdata->soc_dev_clk_data[j].dev_id,
|
|
pdata->soc_dev_clk_data[j].clk_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int _clk_cmp(u32 dev_id, u32 clk_id, const struct clk_map *map)
|
|
{
|
|
if (map->dev_id == dev_id && map->clk_id == clk_id)
|
|
return 0;
|
|
if (map->dev_id > dev_id ||
|
|
(map->dev_id == dev_id && map->clk_id > clk_id))
|
|
return -1;
|
|
return 1;
|
|
}
|
|
|
|
static int bsearch(u32 dev_id, u32 clk_id, struct clk_map *map, int num)
|
|
{
|
|
int result;
|
|
int idx;
|
|
|
|
for (idx = 0; idx < num; idx++) {
|
|
result = _clk_cmp(dev_id, clk_id, &map[idx]);
|
|
|
|
if (result == 0)
|
|
return idx;
|
|
}
|
|
|
|
return -ENOENT;
|
|
}
|
|
|
|
static int ti_clk_of_xlate(struct clk *clk,
|
|
struct ofnode_phandle_args *args)
|
|
{
|
|
struct ti_clk_data *data = dev_get_priv(clk->dev);
|
|
int idx;
|
|
|
|
debug("%s(clk=%p, args_count=%d [0]=%d [1]=%d)\n", __func__, clk,
|
|
args->args_count, args->args[0], args->args[1]);
|
|
|
|
if (args->args_count != 2) {
|
|
debug("Invalid args_count: %d\n", args->args_count);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!data->size)
|
|
return -EPROBE_DEFER;
|
|
|
|
idx = bsearch(args->args[0], args->args[1], data->map, data->size);
|
|
if (idx < 0)
|
|
return idx;
|
|
|
|
clk->id = idx;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ulong ti_clk_get_rate(struct clk *clk)
|
|
{
|
|
struct ti_clk_data *data = dev_get_priv(clk->dev);
|
|
struct clk *clkp = data->map[clk->id].clk;
|
|
|
|
return clk_get_rate(clkp);
|
|
}
|
|
|
|
static ulong ti_clk_set_rate(struct clk *clk, ulong rate)
|
|
{
|
|
struct ti_clk_data *data = dev_get_priv(clk->dev);
|
|
struct clk *clkp = data->map[clk->id].clk;
|
|
int div = 1;
|
|
ulong child_rate;
|
|
const struct clk_ops *ops;
|
|
ulong new_rate, rem;
|
|
ulong diff, new_diff;
|
|
|
|
/*
|
|
* We must propagate rate change to parent if current clock type
|
|
* does not allow setting it.
|
|
*/
|
|
while (clkp) {
|
|
ops = clkp->dev->driver->ops;
|
|
if (ops->set_rate)
|
|
break;
|
|
|
|
/*
|
|
* Store child rate so we can calculate the clock rate
|
|
* that must be passed to parent
|
|
*/
|
|
child_rate = clk_get_rate(clkp);
|
|
clkp = clk_get_parent(clkp);
|
|
if (clkp) {
|
|
debug("%s: propagating rate change to parent %s, rate=%u.\n",
|
|
__func__, clkp->dev->name, (u32)rate / div);
|
|
div *= clk_get_rate(clkp) / child_rate;
|
|
}
|
|
}
|
|
|
|
if (!clkp)
|
|
return -ENOSYS;
|
|
|
|
child_rate = clk_get_rate(clkp);
|
|
|
|
new_rate = clk_set_rate(clkp, rate / div);
|
|
|
|
diff = abs(new_rate - rate / div);
|
|
|
|
debug("%s: clk=%s, div=%d, rate=%u, new_rate=%u, diff=%u\n", __func__,
|
|
clkp->dev->name, div, (u32)rate, (u32)new_rate, (u32)diff);
|
|
|
|
/*
|
|
* If the new rate differs by 50% of the target,
|
|
* modify parent. This handles typical cases where we have a hsdiv
|
|
* following directly a PLL
|
|
*/
|
|
|
|
if (diff > rate / div / 2) {
|
|
ulong pll_tgt;
|
|
int pll_div = 0;
|
|
|
|
clk = clkp;
|
|
|
|
debug("%s: propagating rate change to parent, rate=%u.\n",
|
|
__func__, (u32)rate / div);
|
|
|
|
clkp = clk_get_parent(clkp);
|
|
|
|
if (rate > osc_freq) {
|
|
if (rate > PLL_MAX_FREQ / 2 && rate < PLL_MAX_FREQ) {
|
|
pll_tgt = rate;
|
|
pll_div = 1;
|
|
} else {
|
|
for (pll_div = 2; pll_div < PLL_MAX_DIV; pll_div++) {
|
|
pll_tgt = rate / div * pll_div;
|
|
if (pll_tgt >= PLL_MIN_FREQ && pll_tgt <= PLL_MAX_FREQ)
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
pll_tgt = osc_freq;
|
|
pll_div = rate / div / osc_freq;
|
|
}
|
|
|
|
debug("%s: pll_tgt=%u, rate=%u, div=%u\n", __func__,
|
|
(u32)pll_tgt, (u32)rate, pll_div);
|
|
|
|
clk_set_rate(clkp, pll_tgt);
|
|
|
|
return clk_set_rate(clk, rate / div) * div;
|
|
}
|
|
|
|
/*
|
|
* If the new rate differs by at least 5% of the target,
|
|
* we must check for rounding error in a divider, so try
|
|
* set rate with rate + (parent_freq % rate).
|
|
*/
|
|
|
|
if (diff > rate / div / 20) {
|
|
u64 parent_freq = clk_get_parent_rate(clkp);
|
|
|
|
rem = parent_freq % rate;
|
|
new_rate = clk_set_rate(clkp, (rate / div) + rem);
|
|
new_diff = abs(new_rate - rate / div);
|
|
|
|
if (new_diff > diff) {
|
|
new_rate = clk_set_rate(clkp, rate / div);
|
|
} else {
|
|
debug("%s: Using better rate %lu that gives diff %lu\n",
|
|
__func__, new_rate, new_diff);
|
|
}
|
|
}
|
|
|
|
return new_rate;
|
|
}
|
|
|
|
static int ti_clk_set_parent(struct clk *clk, struct clk *parent)
|
|
{
|
|
struct ti_clk_data *data = dev_get_priv(clk->dev);
|
|
struct clk *clkp = data->map[clk->id].clk;
|
|
struct clk *parentp = data->map[parent->id].clk;
|
|
|
|
return clk_set_parent(clkp, parentp);
|
|
}
|
|
|
|
static int ti_clk_enable(struct clk *clk)
|
|
{
|
|
struct ti_clk_data *data = dev_get_priv(clk->dev);
|
|
struct clk *clkp = data->map[clk->id].clk;
|
|
|
|
return clk_enable(clkp);
|
|
}
|
|
|
|
static int ti_clk_disable(struct clk *clk)
|
|
{
|
|
struct ti_clk_data *data = dev_get_priv(clk->dev);
|
|
struct clk *clkp = data->map[clk->id].clk;
|
|
|
|
return clk_disable(clkp);
|
|
}
|
|
|
|
static const struct udevice_id ti_clk_of_match[] = {
|
|
{ .compatible = "ti,k2g-sci-clk" },
|
|
{ /* sentinel */ },
|
|
};
|
|
|
|
static const struct clk_ops ti_clk_ops = {
|
|
.of_xlate = ti_clk_of_xlate,
|
|
.set_rate = ti_clk_set_rate,
|
|
.get_rate = ti_clk_get_rate,
|
|
.enable = ti_clk_enable,
|
|
.disable = ti_clk_disable,
|
|
.set_parent = ti_clk_set_parent,
|
|
};
|
|
|
|
U_BOOT_DRIVER(ti_clk) = {
|
|
.name = "ti-clk",
|
|
.id = UCLASS_CLK,
|
|
.of_match = ti_clk_of_match,
|
|
.probe = ti_clk_probe,
|
|
.priv_auto = sizeof(struct ti_clk_data),
|
|
.ops = &ti_clk_ops,
|
|
};
|