root/drivers/gpu/drm/sun4i/sun4i_hdmi_ddc_clk.c
// 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_hdmi.h"

struct sun4i_ddc {
        struct clk_hw           hw;
        struct sun4i_hdmi       *hdmi;
        struct regmap_field     *reg;
        u8                      pre_div;
        u8                      m_offset;
};

static inline struct sun4i_ddc *hw_to_ddc(struct clk_hw *hw)
{
        return container_of(hw, struct sun4i_ddc, hw);
}

static unsigned long sun4i_ddc_calc_divider(unsigned long rate,
                                            unsigned long parent_rate,
                                            const u8 pre_div,
                                            const u8 m_offset,
                                            u8 *m, u8 *n)
{
        unsigned long best_rate = 0;
        u8 best_m = 0, best_n = 0, _m, _n;

        for (_m = 0; _m < 16; _m++) {
                for (_n = 0; _n < 8; _n++) {
                        unsigned long tmp_rate;

                        tmp_rate = (((parent_rate / pre_div) / 10) >> _n) /
                                (_m + m_offset);

                        if (tmp_rate > rate)
                                continue;

                        if (abs(rate - tmp_rate) < abs(rate - best_rate)) {
                                best_rate = tmp_rate;
                                best_m = _m;
                                best_n = _n;
                        }
                }
        }

        if (m && n) {
                *m = best_m;
                *n = best_n;
        }

        return best_rate;
}

static int sun4i_ddc_determine_rate(struct clk_hw *hw,
                                    struct clk_rate_request *req)
{
        struct sun4i_ddc *ddc = hw_to_ddc(hw);

        req->rate = sun4i_ddc_calc_divider(req->rate, req->best_parent_rate,
                                           ddc->pre_div, ddc->m_offset, NULL, NULL);

        return 0;
}

static unsigned long sun4i_ddc_recalc_rate(struct clk_hw *hw,
                                            unsigned long parent_rate)
{
        struct sun4i_ddc *ddc = hw_to_ddc(hw);
        unsigned int reg;
        u8 m, n;

        regmap_field_read(ddc->reg, &reg);
        m = (reg >> 3) & 0xf;
        n = reg & 0x7;

        return (((parent_rate / ddc->pre_div) / 10) >> n) /
               (m + ddc->m_offset);
}

static int sun4i_ddc_set_rate(struct clk_hw *hw, unsigned long rate,
                              unsigned long parent_rate)
{
        struct sun4i_ddc *ddc = hw_to_ddc(hw);
        u8 div_m, div_n;

        sun4i_ddc_calc_divider(rate, parent_rate, ddc->pre_div,
                               ddc->m_offset, &div_m, &div_n);

        regmap_field_write(ddc->reg,
                           SUN4I_HDMI_DDC_CLK_M(div_m) |
                           SUN4I_HDMI_DDC_CLK_N(div_n));

        return 0;
}

static const struct clk_ops sun4i_ddc_ops = {
        .recalc_rate    = sun4i_ddc_recalc_rate,
        .determine_rate = sun4i_ddc_determine_rate,
        .set_rate       = sun4i_ddc_set_rate,
};

int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *parent)
{
        struct clk_init_data init;
        struct sun4i_ddc *ddc;
        const char *parent_name;

        parent_name = __clk_get_name(parent);
        if (!parent_name)
                return -ENODEV;

        ddc = devm_kzalloc(hdmi->dev, sizeof(*ddc), GFP_KERNEL);
        if (!ddc)
                return -ENOMEM;

        ddc->reg = devm_regmap_field_alloc(hdmi->dev, hdmi->regmap,
                                           hdmi->variant->ddc_clk_reg);
        if (IS_ERR(ddc->reg))
                return PTR_ERR(ddc->reg);

        init.name = "hdmi-ddc";
        init.ops = &sun4i_ddc_ops;
        init.parent_names = &parent_name;
        init.num_parents = 1;

        ddc->hdmi = hdmi;
        ddc->hw.init = &init;
        ddc->pre_div = hdmi->variant->ddc_clk_pre_divider;
        ddc->m_offset = hdmi->variant->ddc_clk_m_offset;

        hdmi->ddc_clk = devm_clk_register(hdmi->dev, &ddc->hw);
        if (IS_ERR(hdmi->ddc_clk))
                return PTR_ERR(hdmi->ddc_clk);

        return 0;
}