sun4i_hdmi_tmds_clk.c (5280B)
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}