// SPDX-License-Identifier: GPL-2.0
/*
 * RZV2H CPG Library. This library provides common functions to calculate
 * PLL parameters for the RZV2H SoC.
 *
 * Copyright (C) 2026 Renesas Electronics Corp.
 *
 */

#include <linux/clk/renesas.h>
#include <linux/export.h>
#include <linux/math.h>
#include <linux/types.h>
#include <linux/units.h>

/**
 * rzv2h_cpg_get_pll_pars - Finds the best combination of PLL parameters
 * for a given frequency.
 *
 * @limits: Pointer to the structure containing the limits for the PLL parameters
 * @pars: Pointer to the structure where the best calculated PLL parameters values
 * will be stored
 * @freq_millihz: Target output frequency in millihertz
 *
 * This function calculates the best set of PLL parameters (M, K, P, S) to achieve
 * the desired frequency.
 * There is no direct formula to calculate the PLL parameters, as it's an open
 * system of equations, therefore this function uses an iterative approach to
 * determine the best solution. The best solution is one that minimizes the error
 * (desired frequency - actual frequency).
 *
 * Return: true if a valid set of parameters values is found, false otherwise.
 */
bool rzv2h_cpg_get_pll_pars(const struct rzv2h_pll_limits *limits,
			    struct rzv2h_pll_pars *pars, u64 freq_millihz)
{
	unsigned long input_fref = limits->input_fref ?: (24 * MEGA);
	u64 fout_min_millihz = mul_u32_u32(limits->fout.min, MILLI);
	u64 fout_max_millihz = mul_u32_u32(limits->fout.max, MILLI);
	struct rzv2h_pll_pars p, best;

	if (freq_millihz > fout_max_millihz ||
	    freq_millihz < fout_min_millihz)
		return false;

	/* Initialize best error to maximum possible value */
	best.error_millihz = S64_MAX;

	for (p.p = limits->p.min; p.p <= limits->p.max; p.p++) {
		u32 fref = input_fref / p.p;
		u16 divider;

		for (divider = 1 << limits->s.min, p.s = limits->s.min;
			p.s <= limits->s.max; p.s++, divider <<= 1) {
			for (p.m = limits->m.min; p.m <= limits->m.max; p.m++) {
				u64 output_m, output_k_range;
				s64 pll_k, output_k;
				u64 fvco, output;

				/*
				 * The frequency generated by the PLL + divider
				 * is calculated as follows:
				 *
				 * With:
				 * Freq = Ffout = Ffvco / 2^(pll_s)
				 * Ffvco = (pll_m + (pll_k / 65536)) * Ffref
				 * Ffref = 24MHz / pll_p
				 *
				 * Freq can also be rewritten as:
				 * Freq = Ffvco / 2^(pll_s)
				 *      = ((pll_m + (pll_k / 65536)) * Ffref) / 2^(pll_s)
				 *      = (pll_m * Ffref) / 2^(pll_s) + ((pll_k / 65536) * Ffref) / 2^(pll_s)
				 *      = output_m + output_k
				 *
				 * Every parameter has been determined at this
				 * point, but pll_k.
				 *
				 * Considering that:
				 * limits->k.min <= pll_k <= limits->k.max
				 * Then:
				 * -0.5 <= (pll_k / 65536) < 0.5
				 * Therefore:
				 * -Ffref / (2 * 2^(pll_s)) <= output_k < Ffref / (2 * 2^(pll_s))
				 */

				/* Compute output M component (in mHz) */
				output_m = DIV_ROUND_CLOSEST_ULL(mul_u32_u32(p.m, fref) * MILLI,
								 divider);
				/* Compute range for output K (in mHz) */
				output_k_range = DIV_ROUND_CLOSEST_ULL(mul_u32_u32(fref, MILLI),
								       2 * divider);
				/*
				 * No point in continuing if we can't achieve
				 * the desired frequency
				 */
				if (freq_millihz <  (output_m - output_k_range) ||
				    freq_millihz >= (output_m + output_k_range)) {
					continue;
				}

				/*
				 * Compute the K component
				 *
				 * Since:
				 * Freq = output_m + output_k
				 * Then:
				 * output_k = Freq - output_m
				 *          = ((pll_k / 65536) * Ffref) / 2^(pll_s)
				 * Therefore:
				 * pll_k = (output_k * 65536 * 2^(pll_s)) / Ffref
				 */
				output_k = freq_millihz - output_m;
				pll_k = div_s64(output_k * 65536ULL * divider,
						fref);
				pll_k = DIV_S64_ROUND_CLOSEST(pll_k, MILLI);

				/* Validate K value within allowed limits */
				if (pll_k < limits->k.min ||
				    pll_k > limits->k.max)
					continue;

				p.k = pll_k;

				/* Compute (Ffvco * 65536) */
				fvco = mul_u32_u32(p.m * 65536 + p.k, fref);
				if (fvco < mul_u32_u32(limits->fvco.min, 65536) ||
				    fvco > mul_u32_u32(limits->fvco.max, 65536))
					continue;

				/* PLL_M component of (output * 65536 * PLL_P) */
				output = mul_u32_u32(p.m * 65536, input_fref);
				/* PLL_K component of (output * 65536 * PLL_P) */
				output += p.k * input_fref;
				/* Make it in mHz */
				output *= MILLI;
				output = DIV_U64_ROUND_CLOSEST(output, 65536 * p.p * divider);

				/* Check output frequency against limits */
				if (output < fout_min_millihz ||
				    output > fout_max_millihz)
					continue;

				p.error_millihz = freq_millihz - output;
				p.freq_millihz = output;

				/* If an exact match is found, return immediately */
				if (p.error_millihz == 0) {
					*pars = p;
					return true;
				}

				/* Update best match if error is smaller */
				if (abs(best.error_millihz) > abs(p.error_millihz))
					best = p;
			}
		}
	}

	/* If no valid parameters were found, return false */
	if (best.error_millihz == S64_MAX)
		return false;

	*pars = best;
	return true;
}
EXPORT_SYMBOL_NS_GPL(rzv2h_cpg_get_pll_pars, "RZV2H_CPG");

/*
 * rzv2h_cpg_get_pll_divs_pars - Finds the best combination of PLL parameters
 * and divider value for a given frequency.
 *
 * @limits: Pointer to the structure containing the limits for the PLL parameters
 * @pars: Pointer to the structure where the best calculated PLL parameters and
 * divider values will be stored
 * @table: Pointer to the array of valid divider values
 * @table_size: Size of the divider values array
 * @freq_millihz: Target output frequency in millihertz
 *
 * This function calculates the best set of PLL parameters (M, K, P, S) and divider
 * value to achieve the desired frequency. See rzv2h_cpg_get_pll_pars() for more
 * details on how the PLL parameters are calculated.
 *
 * freq_millihz is the desired frequency generated by the PLL followed by a
 * a gear.
 */
bool rzv2h_cpg_get_pll_divs_pars(const struct rzv2h_pll_limits *limits,
				 struct rzv2h_pll_div_pars *pars,
				 const u8 *table, u8 table_size, u64 freq_millihz)
{
	struct rzv2h_pll_div_pars p, best;

	best.div.error_millihz = S64_MAX;
	p.div.error_millihz = S64_MAX;
	for (unsigned int i = 0; i < table_size; i++) {
		if (!rzv2h_cpg_get_pll_pars(limits, &p.pll, freq_millihz * table[i]))
			continue;

		p.div.divider_value = table[i];
		p.div.freq_millihz = DIV_U64_ROUND_CLOSEST(p.pll.freq_millihz, table[i]);
		p.div.error_millihz = freq_millihz - p.div.freq_millihz;

		if (p.div.error_millihz == 0) {
			*pars = p;
			return true;
		}

		if (abs(best.div.error_millihz) > abs(p.div.error_millihz))
			best = p;
	}

	if (best.div.error_millihz == S64_MAX)
		return false;

	*pars = best;
	return true;
}
EXPORT_SYMBOL_NS_GPL(rzv2h_cpg_get_pll_divs_pars, "RZV2H_CPG");