// SPDX-License-Identifier: GPL-2.0+
/*
 * Slow clock support for AT91 architectures.
 *
 * Copyright (C) 2020 Microchip Technology Inc. and its subsidiaries
 *
 * Author: Claudiu Beznea <claudiu.beznea@microchip.com>
 */

#include <clk-uclass.h>
#include <dm.h>
#include <dt-bindings/clk/at91.h>
#include <linux/clk-provider.h>

#include "pmc.h"

#define UBOOT_DM_CLK_AT91_SAM9X60_TD_SLCK	"at91-sam9x60-td-slck"
#define UBOOT_DM_CLK_AT91_SCKC			"at91-sckc"

#define AT91_OSC_SEL		BIT(24)
#define AT91_OSC_SEL_SHIFT	(24)

struct sam9x60_sckc {
	void __iomem *reg;
	const char **parent_names;
	unsigned int num_parents;
	struct clk clk;
};

#define to_sam9x60_sckc(c)	container_of(c, struct sam9x60_sckc, clk)

static int sam9x60_sckc_of_xlate(struct clk *clk,
				 struct ofnode_phandle_args *args)
{
	if (args->args_count != 1) {
		debug("AT91: SCKC: Invalid args_count: %d\n", args->args_count);
		return -EINVAL;
	}

	clk->id = AT91_TO_CLK_ID(PMC_TYPE_SLOW, args->args[0]);

	return 0;
}

static const struct clk_ops sam9x60_sckc_ops = {
	.of_xlate = sam9x60_sckc_of_xlate,
	.get_rate = clk_generic_get_rate,
};

static int sam9x60_td_slck_set_parent(struct clk *clk, struct clk *parent)
{
	struct sam9x60_sckc *sckc = to_sam9x60_sckc(clk);
	u32 i;

	for (i = 0; i < sckc->num_parents; i++) {
		if (!strcmp(parent->dev->name, sckc->parent_names[i]))
			break;
	}
	if (i == sckc->num_parents)
		return -EINVAL;

	pmc_update_bits(sckc->reg, 0, AT91_OSC_SEL, (i << AT91_OSC_SEL_SHIFT));

	return 0;
}

static const struct clk_ops sam9x60_td_slck_ops = {
	.get_rate = clk_generic_get_rate,
	.set_parent = sam9x60_td_slck_set_parent,
};

static struct clk *at91_sam9x60_clk_register_td_slck(struct sam9x60_sckc *sckc,
		const char *name, const char * const *parent_names,
		int num_parents)
{
	struct clk *clk;
	int ret = -ENOMEM, i;
	u32 val;

	if (!sckc || !name || !parent_names || num_parents != 2)
		return ERR_PTR(-EINVAL);

	sckc->parent_names = kzalloc(sizeof(*sckc->parent_names) * num_parents,
				     GFP_KERNEL);
	if (!sckc->parent_names)
		return ERR_PTR(ret);

	for (i = 0; i < num_parents; i++) {
		sckc->parent_names[i] = kmemdup(parent_names[i],
				strlen(parent_names[i]) + 1, GFP_KERNEL);
		if (!sckc->parent_names[i])
			goto free;
	}
	sckc->num_parents = num_parents;

	pmc_read(sckc->reg, 0, &val);
	val = (val & AT91_OSC_SEL) >> AT91_OSC_SEL_SHIFT;

	clk = &sckc->clk;
	ret = clk_register(clk, UBOOT_DM_CLK_AT91_SAM9X60_TD_SLCK, name,
			   parent_names[val]);
	if (ret) {
		i--;
		goto free;
	}

	return clk;

free:
	for (; i >= 0; i--)
		kfree(sckc->parent_names[i]);
	kfree(sckc->parent_names);

	return ERR_PTR(ret);
}

U_BOOT_DRIVER(at91_sam9x60_td_slck) = {
	.name = UBOOT_DM_CLK_AT91_SAM9X60_TD_SLCK,
	.id = UCLASS_CLK,
	.ops = &sam9x60_td_slck_ops,
	.flags = DM_FLAG_PRE_RELOC,
};

static int at91_sam9x60_sckc_probe(struct udevice *dev)
{
	struct sam9x60_sckc *sckc = dev_get_priv(dev);
	void __iomem *base = devfdt_get_addr_ptr(dev);
	const char *slow_rc_osc, *slow_osc;
	const char *parents[2];
	struct clk *clk, c;
	int ret;

	ret = clk_get_by_index(dev, 0, &c);
	if (ret)
		return ret;
	slow_rc_osc = clk_hw_get_name(&c);

	ret = clk_get_by_index(dev, 1, &c);
	if (ret)
		return ret;
	slow_osc = clk_hw_get_name(&c);

	clk = clk_register_fixed_factor(NULL, "md_slck", slow_rc_osc, 0, 1, 1);
	if (IS_ERR(clk))
		return PTR_ERR(clk);
	clk_dm(AT91_TO_CLK_ID(PMC_TYPE_SLOW, 0), clk);

	parents[0] = slow_rc_osc;
	parents[1] = slow_osc;
	sckc[1].reg = base;
	clk = at91_sam9x60_clk_register_td_slck(&sckc[1], "td_slck",
						parents, 2);
	if (IS_ERR(clk))
		return PTR_ERR(clk);
	clk_dm(AT91_TO_CLK_ID(PMC_TYPE_SLOW, 1), clk);

	return 0;
}

static const struct udevice_id sam9x60_sckc_ids[] = {
	{ .compatible = "microchip,sam9x60-sckc" },
	{ /* Sentinel. */ },
};

U_BOOT_DRIVER(at91_sckc) = {
	.name = UBOOT_DM_CLK_AT91_SCKC,
	.id = UCLASS_CLK,
	.of_match = sam9x60_sckc_ids,
	.priv_auto	= sizeof(struct sam9x60_sckc) * 2,
	.ops = &sam9x60_sckc_ops,
	.probe = at91_sam9x60_sckc_probe,
	.flags = DM_FLAG_PRE_RELOC,
};
