linux/drivers/gpu/drm/sun4i/sun4i_hdmi_tmds_clk.c
<<
>>
Prefs
   1// SPDX-License-Identifier: GPL-2.0-or-later
   2/*
   3 * Copyright (C) 2016 Free Electrons
   4 * Copyright (C) 2016 NextThing Co
   5 *
   6 * Maxime Ripard <maxime.ripard@free-electrons.com>
   7 */
   8
   9#include <linux/clk-provider.h>
  10#include <linux/io.h>
  11
  12#include "sun4i_hdmi.h"
  13
  14struct sun4i_tmds {
  15        struct clk_hw           hw;
  16        struct sun4i_hdmi       *hdmi;
  17
  18        u8                      div_offset;
  19};
  20
  21static inline struct sun4i_tmds *hw_to_tmds(struct clk_hw *hw)
  22{
  23        return container_of(hw, struct sun4i_tmds, hw);
  24}
  25
  26
  27static unsigned long sun4i_tmds_calc_divider(unsigned long rate,
  28                                             unsigned long parent_rate,
  29                                             u8 div_offset,
  30                                             u8 *div,
  31                                             bool *half)
  32{
  33        unsigned long best_rate = 0;
  34        u8 best_m = 0, m;
  35        bool is_double = false;
  36
  37        for (m = div_offset ?: 1; m < (16 + div_offset); m++) {
  38                u8 d;
  39
  40                for (d = 1; d < 3; d++) {
  41                        unsigned long tmp_rate;
  42
  43                        tmp_rate = parent_rate / m / d;
  44
  45                        if (tmp_rate > rate)
  46                                continue;
  47
  48                        if (!best_rate ||
  49                            (rate - tmp_rate) < (rate - best_rate)) {
  50                                best_rate = tmp_rate;
  51                                best_m = m;
  52                                is_double = (d == 2) ? true : false;
  53                        }
  54                }
  55        }
  56
  57        if (div && half) {
  58                *div = best_m;
  59                *half = is_double;
  60        }
  61
  62        return best_rate;
  63}
  64
  65
  66static int sun4i_tmds_determine_rate(struct clk_hw *hw,
  67                                     struct clk_rate_request *req)
  68{
  69        struct sun4i_tmds *tmds = hw_to_tmds(hw);
  70        struct clk_hw *parent = NULL;
  71        unsigned long best_parent = 0;
  72        unsigned long rate = req->rate;
  73        int best_div = 1, best_half = 1;
  74        int i, j, p;
  75
  76        /*
  77         * We only consider PLL3, since the TCON is very likely to be
  78         * clocked from it, and to have the same rate than our HDMI
  79         * clock, so we should not need to do anything.
  80         */
  81
  82        for (p = 0; p < clk_hw_get_num_parents(hw); p++) {
  83                parent = clk_hw_get_parent_by_index(hw, p);
  84                if (!parent)
  85                        continue;
  86
  87                for (i = 1; i < 3; i++) {
  88                        for (j = tmds->div_offset ?: 1;
  89                             j < (16 + tmds->div_offset); j++) {
  90                                unsigned long ideal = rate * i * j;
  91                                unsigned long rounded;
  92
  93                                rounded = clk_hw_round_rate(parent, ideal);
  94
  95                                if (rounded == ideal) {
  96                                        best_parent = rounded;
  97                                        best_half = i;
  98                                        best_div = j;
  99                                        goto out;
 100                                }
 101
 102                                if (!best_parent ||
 103                                    abs(rate - rounded / i / j) <
 104                                    abs(rate - best_parent / best_half /
 105                                        best_div)) {
 106                                        best_parent = rounded;
 107                                        best_half = i;
 108                                        best_div = j;
 109                                }
 110                        }
 111                }
 112        }
 113
 114        if (!parent)
 115                return -EINVAL;
 116
 117out:
 118        req->rate = best_parent / best_half / best_div;
 119        req->best_parent_rate = best_parent;
 120        req->best_parent_hw = parent;
 121
 122        return 0;
 123}
 124
 125static unsigned long sun4i_tmds_recalc_rate(struct clk_hw *hw,
 126                                            unsigned long parent_rate)
 127{
 128        struct sun4i_tmds *tmds = hw_to_tmds(hw);
 129        u32 reg;
 130
 131        reg = readl(tmds->hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
 132        if (reg & SUN4I_HDMI_PAD_CTRL1_HALVE_CLK)
 133                parent_rate /= 2;
 134
 135        reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
 136        reg = ((reg >> 4) & 0xf) + tmds->div_offset;
 137        if (!reg)
 138                reg = 1;
 139
 140        return parent_rate / reg;
 141}
 142
 143static int sun4i_tmds_set_rate(struct clk_hw *hw, unsigned long rate,
 144                               unsigned long parent_rate)
 145{
 146        struct sun4i_tmds *tmds = hw_to_tmds(hw);
 147        bool half;
 148        u32 reg;
 149        u8 div;
 150
 151        sun4i_tmds_calc_divider(rate, parent_rate, tmds->div_offset,
 152                                &div, &half);
 153
 154        reg = readl(tmds->hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
 155        reg &= ~SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
 156        if (half)
 157                reg |= SUN4I_HDMI_PAD_CTRL1_HALVE_CLK;
 158        writel(reg, tmds->hdmi->base + SUN4I_HDMI_PAD_CTRL1_REG);
 159
 160        reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
 161        reg &= ~SUN4I_HDMI_PLL_CTRL_DIV_MASK;
 162        writel(reg | SUN4I_HDMI_PLL_CTRL_DIV(div - tmds->div_offset),
 163               tmds->hdmi->base + SUN4I_HDMI_PLL_CTRL_REG);
 164
 165        return 0;
 166}
 167
 168static u8 sun4i_tmds_get_parent(struct clk_hw *hw)
 169{
 170        struct sun4i_tmds *tmds = hw_to_tmds(hw);
 171        u32 reg;
 172
 173        reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_DBG0_REG);
 174        return ((reg & SUN4I_HDMI_PLL_DBG0_TMDS_PARENT_MASK) >>
 175                SUN4I_HDMI_PLL_DBG0_TMDS_PARENT_SHIFT);
 176}
 177
 178static int sun4i_tmds_set_parent(struct clk_hw *hw, u8 index)
 179{
 180        struct sun4i_tmds *tmds = hw_to_tmds(hw);
 181        u32 reg;
 182
 183        if (index > 1)
 184                return -EINVAL;
 185
 186        reg = readl(tmds->hdmi->base + SUN4I_HDMI_PLL_DBG0_REG);
 187        reg &= ~SUN4I_HDMI_PLL_DBG0_TMDS_PARENT_MASK;
 188        writel(reg | SUN4I_HDMI_PLL_DBG0_TMDS_PARENT(index),
 189               tmds->hdmi->base + SUN4I_HDMI_PLL_DBG0_REG);
 190
 191        return 0;
 192}
 193
 194static const struct clk_ops sun4i_tmds_ops = {
 195        .determine_rate = sun4i_tmds_determine_rate,
 196        .recalc_rate    = sun4i_tmds_recalc_rate,
 197        .set_rate       = sun4i_tmds_set_rate,
 198
 199        .get_parent     = sun4i_tmds_get_parent,
 200        .set_parent     = sun4i_tmds_set_parent,
 201};
 202
 203int sun4i_tmds_create(struct sun4i_hdmi *hdmi)
 204{
 205        struct clk_init_data init;
 206        struct sun4i_tmds *tmds;
 207        const char *parents[2];
 208
 209        parents[0] = __clk_get_name(hdmi->pll0_clk);
 210        if (!parents[0])
 211                return -ENODEV;
 212
 213        parents[1] = __clk_get_name(hdmi->pll1_clk);
 214        if (!parents[1])
 215                return -ENODEV;
 216
 217        tmds = devm_kzalloc(hdmi->dev, sizeof(*tmds), GFP_KERNEL);
 218        if (!tmds)
 219                return -ENOMEM;
 220
 221        init.name = "hdmi-tmds";
 222        init.ops = &sun4i_tmds_ops;
 223        init.parent_names = parents;
 224        init.num_parents = 2;
 225        init.flags = CLK_SET_RATE_PARENT;
 226
 227        tmds->hdmi = hdmi;
 228        tmds->hw.init = &init;
 229        tmds->div_offset = hdmi->variant->tmds_clk_div_offset;
 230
 231        hdmi->tmds_clk = devm_clk_register(hdmi->dev, &tmds->hw);
 232        if (IS_ERR(hdmi->tmds_clk))
 233                return PTR_ERR(hdmi->tmds_clk);
 234
 235        return 0;
 236}
 237