diff options
Diffstat (limited to 'drivers/clk/clk-scmi.c')
-rw-r--r-- | drivers/clk/clk-scmi.c | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/drivers/clk/clk-scmi.c b/drivers/clk/clk-scmi.c new file mode 100644 index 0000000000..9170dba393 --- /dev/null +++ b/drivers/clk/clk-scmi.c @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * System Control and Power Interface (SCMI) Protocol based clock driver + * + * Copyright (C) 2018-2021 ARM Ltd. + */ + +#include <common.h> +#include <linux/clk.h> +#include <driver.h> +#include <linux/err.h> +#include <of.h> +#include <linux/scmi_protocol.h> +#include <linux/overflow.h> +#include <linux/math64.h> + +static const struct scmi_clk_proto_ops *scmi_proto_clk_ops; + +struct scmi_clk { + u32 id; + struct clk_hw hw; + const struct scmi_clock_info *info; + const struct scmi_protocol_handle *ph; +}; + +#define to_scmi_clk(clk) container_of(clk, struct scmi_clk, hw) + +static unsigned long scmi_clk_recalc_rate(struct clk_hw *hw, + unsigned long parent_rate) +{ + int ret; + u64 rate; + struct scmi_clk *clk = to_scmi_clk(hw); + + ret = scmi_proto_clk_ops->rate_get(clk->ph, clk->id, &rate); + if (ret) + return 0; + return rate; +} + +static long scmi_clk_round_rate(struct clk_hw *hw, unsigned long rate, + unsigned long *parent_rate) +{ + u64 fmin, fmax, ftmp; + struct scmi_clk *clk = to_scmi_clk(hw); + + /* + * We can't figure out what rate it will be, so just return the + * rate back to the caller. scmi_clk_recalc_rate() will be called + * after the rate is set and we'll know what rate the clock is + * running at then. + */ + if (clk->info->rate_discrete) + return rate; + + fmin = clk->info->range.min_rate; + fmax = clk->info->range.max_rate; + if (rate <= fmin) + return fmin; + else if (rate >= fmax) + return fmax; + + ftmp = rate - fmin; + ftmp += clk->info->range.step_size - 1; /* to round up */ + do_div(ftmp, clk->info->range.step_size); + + return ftmp * clk->info->range.step_size + fmin; +} + +static int scmi_clk_set_rate(struct clk_hw *hw, unsigned long rate, + unsigned long parent_rate) +{ + struct scmi_clk *clk = to_scmi_clk(hw); + + return scmi_proto_clk_ops->rate_set(clk->ph, clk->id, rate); +} + +static int scmi_clk_enable(struct clk_hw *hw) +{ + struct scmi_clk *clk = to_scmi_clk(hw); + + return scmi_proto_clk_ops->enable(clk->ph, clk->id); +} + +static void scmi_clk_disable(struct clk_hw *hw) +{ + struct scmi_clk *clk = to_scmi_clk(hw); + + scmi_proto_clk_ops->disable(clk->ph, clk->id); +} + +static const struct clk_ops scmi_clk_ops = { + .recalc_rate = scmi_clk_recalc_rate, + .round_rate = scmi_clk_round_rate, + .set_rate = scmi_clk_set_rate, + /* + * Unlike Linux, we can provide enable/disable callback as everything + * runs in atomic context. + */ + .enable = scmi_clk_enable, + .disable = scmi_clk_disable, +}; + +static int scmi_clk_ops_init(struct device_d *dev, struct scmi_clk *sclk) +{ + struct clk_init_data init = { + .flags = CLK_GET_RATE_NOCACHE, + .num_parents = 0, + .ops = &scmi_clk_ops, + .name = sclk->info->name, + }; + + sclk->hw.init = &init; + return clk_hw_register(dev, &sclk->hw); +} + +static int scmi_clocks_probe(struct scmi_device *sdev) +{ + int idx, count, err; + struct clk **clks; + struct clk_onecell_data *clk_data; + struct device_d *dev = &sdev->dev; + struct device_node *np = dev->device_node; + const struct scmi_handle *handle = sdev->handle; + struct scmi_protocol_handle *ph; + + if (!handle) + return -ENODEV; + + scmi_proto_clk_ops = + handle->protocol_get(sdev, SCMI_PROTOCOL_CLOCK, &ph); + if (IS_ERR(scmi_proto_clk_ops)) + return PTR_ERR(scmi_proto_clk_ops); + + count = scmi_proto_clk_ops->count_get(ph); + if (count < 0) { + dev_err(dev, "%pOFn: invalid clock output count\n", np); + return -EINVAL; + } + + clk_data = kzalloc(sizeof (*clk_data), GFP_KERNEL); + if (!clk_data) + return -ENOMEM; + + clk_data->clk_num = count; + clks = clk_data->clks = calloc(clk_data->clk_num, sizeof(struct clk *)); + + for (idx = 0; idx < count; idx++) { + struct scmi_clk *sclk; + + sclk = kzalloc(sizeof(*sclk), GFP_KERNEL); + if (!sclk) + return -ENOMEM; + + sclk->info = scmi_proto_clk_ops->info_get(ph, idx); + if (!sclk->info) { + dev_dbg(dev, "invalid clock info for idx %d\n", idx); + continue; + } + + sclk->id = idx; + sclk->ph = ph; + + err = scmi_clk_ops_init(dev, sclk); + if (err) { + dev_err(dev, "failed to register clock %d\n", idx); + kfree(sclk); + clks[idx] = NULL; + } else { + dev_dbg(dev, "Registered clock:%s\n", sclk->info->name); + clks[idx] = &sclk->hw.clk; + } + } + + return of_clk_add_provider(dev->device_node, of_clk_src_onecell_get, clk_data); +} + +static const struct scmi_device_id scmi_id_table[] = { + { SCMI_PROTOCOL_CLOCK, "clocks" }, + { }, +}; + +static struct scmi_driver scmi_clocks_driver = { + .name = "scmi-clocks", + .probe = scmi_clocks_probe, + .id_table = scmi_id_table, +}; +core_scmi_driver(scmi_clocks_driver); + +MODULE_AUTHOR("Sudeep Holla <sudeep.holla@arm.com>"); +MODULE_DESCRIPTION("ARM SCMI clock driver"); +MODULE_LICENSE("GPL v2"); |