| 1 | |
| 2 | |
| 3 | * Driver for an SoC block (Numerically Controlled Oscillator)
|
| 4 | * found on t8103 (M1) and other Apple chips
|
| 5 | *
|
| 6 | * Copyright (C) The Asahi Linux Contributors
|
| 7 |
|
| 8 |
|
| 9 | #include <linux/bits.h>
|
| 10 | #include <linux/bitfield.h>
|
| 11 | #include <linux/clk-provider.h>
|
| 12 | #include <linux/io.h>
|
| 13 | #include <linux/kernel.h>
|
| 14 | #include <linux/math64.h>
|
| 15 | #include <linux/module.h>
|
| 16 | #include <linux/of.h>
|
| 17 | #include <linux/platform_device.h>
|
| 18 | #include <linux/spinlock.h>
|
| 19 |
|
| 20 | #define NCO_CHANNEL_STRIDE 0x4000
|
| 21 | #define NCO_CHANNEL_REGSIZE 20
|
| 22 |
|
| 23 | #define REG_CTRL 0
|
| 24 | #define CTRL_ENABLE BIT(31)
|
| 25 | #define REG_DIV 4
|
| 26 | #define DIV_FINE GENMASK(1, 0)
|
| 27 | #define DIV_COARSE GENMASK(12, 2)
|
| 28 | #define REG_INC1 8
|
| 29 | #define REG_INC2 12
|
| 30 | #define REG_ACCINIT 16
|
| 31 |
|
| 32 | |
| 33 | * Theory of operation (postulated)
|
| 34 | *
|
| 35 | * The REG_DIV register indirectly expresses a base integer divisor, roughly
|
| 36 | * corresponding to twice the desired ratio of input to output clock. This
|
| 37 | * base divisor is adjusted on a cycle-by-cycle basis based on the state of a
|
| 38 | * 32-bit phase accumulator to achieve a desired precise clock ratio over the
|
| 39 | * long term.
|
| 40 | *
|
| 41 | * Specifically an output clock cycle is produced after (REG_DIV divisor)/2
|
| 42 | * or (REG_DIV divisor + 1)/2 input cycles, the latter taking effect when top
|
| 43 | * bit of the 32-bit accumulator is set. The accumulator is incremented each
|
| 44 | * produced output cycle, by the value from either REG_INC1 or REG_INC2, which
|
| 45 | * of the two is selected depending again on the accumulator's current top bit.
|
| 46 | *
|
| 47 | * Because the NCO hardware implements counting of input clock cycles in part
|
| 48 | * in a Galois linear-feedback shift register, the higher bits of divisor
|
| 49 | * are programmed into REG_DIV by picking an appropriate LFSR state. See
|
| 50 | * applnco_compute_tables/applnco_div_translate for details on this.
|
| 51 |
|
| 52 |
|
| 53 | #define LFSR_POLY 0xa01
|
| 54 | #define LFSR_INIT 0x7ff
|
| 55 | #define LFSR_LEN 11
|
| 56 | #define LFSR_PERIOD ((1 << LFSR_LEN) - 1)
|
| 57 | #define LFSR_TBLSIZE (1 << LFSR_LEN)
|
| 58 |
|
| 59 |
|
| 60 | #define COARSE_DIV_OFFSET 2
|
| 61 |
|
| 62 | struct applnco_tables {
|
| 63 | u16 fwd[LFSR_TBLSIZE];
|
| 64 | u16 inv[LFSR_TBLSIZE];
|
| 65 | };
|
| 66 |
|
| 67 | struct applnco_channel {
|
| 68 | void __iomem *base;
|
| 69 | struct applnco_tables *tbl;
|
| 70 | struct clk_hw hw;
|
| 71 |
|
| 72 | spinlock_t lock;
|
| 73 | };
|
| 74 |
|
| 75 | #define to_applnco_channel(_hw) container_of(_hw, struct applnco_channel, hw)
|
| 76 |
|
| 77 | static void applnco_enable_nolock(struct clk_hw *hw)
|
| 78 | {
|
| 79 | struct applnco_channel *chan = to_applnco_channel(hw);
|
| 80 | u32 val;
|
| 81 |
|
| 82 | val = readl_relaxed(chan->base + REG_CTRL);
|
| 83 | writel_relaxed(val | CTRL_ENABLE, chan->base + REG_CTRL);
|
| 84 | }
|
| 85 |
|
| 86 | static void applnco_disable_nolock(struct clk_hw *hw)
|
| 87 | {
|
| 88 | struct applnco_channel *chan = to_applnco_channel(hw);
|
| 89 | u32 val;
|
| 90 |
|
| 91 | val = readl_relaxed(chan->base + REG_CTRL);
|
| 92 | writel_relaxed(val & ~CTRL_ENABLE, chan->base + REG_CTRL);
|
| 93 | }
|
| 94 |
|
| 95 | static int applnco_is_enabled(struct clk_hw *hw)
|
| 96 | {
|
| 97 | struct applnco_channel *chan = to_applnco_channel(hw);
|
| 98 |
|
| 99 | return (readl_relaxed(chan->base + REG_CTRL) & CTRL_ENABLE) != 0;
|
| 100 | }
|
| 101 |
|
| 102 | static void applnco_compute_tables(struct applnco_tables *tbl)
|
| 103 | {
|
| 104 | int i;
|
| 105 | u32 state = LFSR_INIT;
|
| 106 |
|
| 107 | |
| 108 | * Go through the states of a Galois LFSR and build
|
| 109 | * a coarse divisor translation table.
|
| 110 |
|
| 111 | for (i = LFSR_PERIOD; i > 0; i--) {
|
| 112 | if (state & 1)
|
| 113 | state = (state >> 1) ^ (LFSR_POLY >> 1);
|
| 114 | else
|
| 115 | state = (state >> 1);
|
| 116 | tbl->fwd[i] = state;
|
| 117 | tbl->inv[state] = i;
|
| 118 | }
|
| 119 |
|
| 120 |
|
| 121 | tbl->fwd[0] = 0;
|
| 122 | tbl->inv[0] = 0;
|
| 123 | }
|
| 124 |
|
| 125 | static bool applnco_div_out_of_range(unsigned int div)
|
| 126 | {
|
| 127 | unsigned int coarse = div / 4;
|
| 128 |
|
| 129 | return coarse < COARSE_DIV_OFFSET ||
|
| 130 | coarse >= COARSE_DIV_OFFSET + LFSR_TBLSIZE;
|
| 131 | }
|
| 132 |
|
| 133 | static u32 applnco_div_translate(struct applnco_tables *tbl, unsigned int div)
|
| 134 | {
|
| 135 | unsigned int coarse = div / 4;
|
| 136 |
|
| 137 | if (WARN_ON(applnco_div_out_of_range(div)))
|
| 138 | return 0;
|
| 139 |
|
| 140 | return FIELD_PREP(DIV_COARSE, tbl->fwd[coarse - COARSE_DIV_OFFSET]) |
|
| 141 | FIELD_PREP(DIV_FINE, div % 4);
|
| 142 | }
|
| 143 |
|
| 144 | static unsigned int applnco_div_translate_inv(struct applnco_tables *tbl, u32 regval)
|
| 145 | {
|
| 146 | unsigned int coarse, fine;
|
| 147 |
|
| 148 | coarse = tbl->inv[FIELD_GET(DIV_COARSE, regval)] + COARSE_DIV_OFFSET;
|
| 149 | fine = FIELD_GET(DIV_FINE, regval);
|
| 150 |
|
| 151 | return coarse * 4 + fine;
|
| 152 | }
|
| 153 |
|
| 154 | static int applnco_set_rate(struct clk_hw *hw, unsigned long rate,
|
| 155 | unsigned long parent_rate)
|
| 156 | {
|
| 157 | struct applnco_channel *chan = to_applnco_channel(hw);
|
| 158 | unsigned long flags;
|
| 159 | u32 div, inc1, inc2;
|
| 160 | bool was_enabled;
|
| 161 |
|
| 162 | div = 2 * parent_rate / rate;
|
| 163 | inc1 = 2 * parent_rate - div * rate;
|
| 164 | inc2 = inc1 - rate;
|
| 165 |
|
| 166 | if (applnco_div_out_of_range(div))
|
| 167 | return -EINVAL;
|
| 168 |
|
| 169 | div = applnco_div_translate(chan->tbl, div);
|
| 170 |
|
| 171 | spin_lock_irqsave(&chan->lock, flags);
|
| 172 | was_enabled = applnco_is_enabled(hw);
|
| 173 | applnco_disable_nolock(hw);
|
| 174 |
|
| 175 | writel_relaxed(div, chan->base + REG_DIV);
|
| 176 | writel_relaxed(inc1, chan->base + REG_INC1);
|
| 177 | writel_relaxed(inc2, chan->base + REG_INC2);
|
| 178 |
|
| 179 |
|
| 180 | writel_relaxed(1 << 31, chan->base + REG_ACCINIT);
|
| 181 |
|
| 182 | if (was_enabled)
|
| 183 | applnco_enable_nolock(hw);
|
| 184 | spin_unlock_irqrestore(&chan->lock, flags);
|
| 185 |
|
| 186 | return 0;
|
| 187 | }
|
| 188 |
|
| 189 | static unsigned long applnco_recalc_rate(struct clk_hw *hw,
|
| 190 | unsigned long parent_rate)
|
| 191 | {
|
| 192 | struct applnco_channel *chan = to_applnco_channel(hw);
|
| 193 | u32 div, inc1, inc2, incbase;
|
| 194 |
|
| 195 | div = applnco_div_translate_inv(chan->tbl,
|
| 196 | readl_relaxed(chan->base + REG_DIV));
|
| 197 |
|
| 198 | inc1 = readl_relaxed(chan->base + REG_INC1);
|
| 199 | inc2 = readl_relaxed(chan->base + REG_INC2);
|
| 200 |
|
| 201 | |
| 202 | * We don't support wraparound of accumulator
|
| 203 | * nor the edge case of both increments being zero
|
| 204 |
|
| 205 | if (inc1 >= (1 << 31) || inc2 < (1 << 31) || (inc1 == 0 && inc2 == 0))
|
| 206 | return 0;
|
| 207 |
|
| 208 |
|
| 209 | incbase = inc1 - inc2;
|
| 210 |
|
| 211 | return div64_u64(((u64) parent_rate) * 2 * incbase,
|
| 212 | ((u64) div) * incbase + inc1);
|
| 213 | }
|
| 214 |
|
| 215 | static int applnco_determine_rate(struct clk_hw *hw,
|
| 216 | struct clk_rate_request *req)
|
| 217 | {
|
| 218 | unsigned long lo = req->best_parent_rate / (COARSE_DIV_OFFSET + LFSR_TBLSIZE) + 1;
|
| 219 | unsigned long hi = req->best_parent_rate / COARSE_DIV_OFFSET;
|
| 220 |
|
| 221 | req->rate = clamp(req->rate, lo, hi);
|
| 222 |
|
| 223 | return 0;
|
| 224 | }
|
| 225 |
|
| 226 | static int applnco_enable(struct clk_hw *hw)
|
| 227 | {
|
| 228 | struct applnco_channel *chan = to_applnco_channel(hw);
|
| 229 | unsigned long flags;
|
| 230 |
|
| 231 | spin_lock_irqsave(&chan->lock, flags);
|
| 232 | applnco_enable_nolock(hw);
|
| 233 | spin_unlock_irqrestore(&chan->lock, flags);
|
| 234 |
|
| 235 | return 0;
|
| 236 | }
|
| 237 |
|
| 238 | static void applnco_disable(struct clk_hw *hw)
|
| 239 | {
|
| 240 | struct applnco_channel *chan = to_applnco_channel(hw);
|
| 241 | unsigned long flags;
|
| 242 |
|
| 243 | spin_lock_irqsave(&chan->lock, flags);
|
| 244 | applnco_disable_nolock(hw);
|
| 245 | spin_unlock_irqrestore(&chan->lock, flags);
|
| 246 | }
|
| 247 |
|
| 248 | static const struct clk_ops applnco_ops = {
|
| 249 | .set_rate = applnco_set_rate,
|
| 250 | .recalc_rate = applnco_recalc_rate,
|
| 251 | .determine_rate = applnco_determine_rate,
|
| 252 | .enable = applnco_enable,
|
| 253 | .disable = applnco_disable,
|
| 254 | .is_enabled = applnco_is_enabled,
|
| 255 | };
|
| 256 |
|
| 257 | static int applnco_probe(struct platform_device *pdev)
|
| 258 | {
|
| 259 | struct device_node *np = pdev->dev.of_node;
|
| 260 | struct clk_parent_data pdata = { .index = 0 };
|
| 261 | struct clk_init_data init;
|
| 262 | struct clk_hw_onecell_data *onecell_data;
|
| 263 | void __iomem *base;
|
| 264 | struct resource *res;
|
| 265 | struct applnco_tables *tbl;
|
| 266 | unsigned int nchannels;
|
| 267 | int ret, i;
|
| 268 |
|
| 269 | base = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
|
| 270 | if (IS_ERR(base))
|
| 271 | return PTR_ERR(base);
|
| 272 |
|
| 273 | if (resource_size(res) < NCO_CHANNEL_REGSIZE)
|
| 274 | return -EINVAL;
|
| 275 | nchannels = (resource_size(res) - NCO_CHANNEL_REGSIZE)
|
| 276 | / NCO_CHANNEL_STRIDE + 1;
|
| 277 |
|
| 278 | onecell_data = devm_kzalloc(&pdev->dev, struct_size(onecell_data, hws,
|
| 279 | nchannels), GFP_KERNEL);
|
| 280 | if (!onecell_data)
|
| 281 | return -ENOMEM;
|
| 282 | onecell_data->num = nchannels;
|
| 283 |
|
| 284 | tbl = devm_kzalloc(&pdev->dev, sizeof(*tbl), GFP_KERNEL);
|
| 285 | if (!tbl)
|
| 286 | return -ENOMEM;
|
| 287 | applnco_compute_tables(tbl);
|
| 288 |
|
| 289 | for (i = 0; i < nchannels; i++) {
|
| 290 | struct applnco_channel *chan;
|
| 291 |
|
| 292 | chan = devm_kzalloc(&pdev->dev, sizeof(*chan), GFP_KERNEL);
|
| 293 | if (!chan)
|
| 294 | return -ENOMEM;
|
| 295 | chan->base = base + NCO_CHANNEL_STRIDE * i;
|
| 296 | chan->tbl = tbl;
|
| 297 | spin_lock_init(&chan->lock);
|
| 298 |
|
| 299 | memset(&init, 0, sizeof(init));
|
| 300 | init.name = devm_kasprintf(&pdev->dev, GFP_KERNEL,
|
| 301 | "%s-%d", np->name, i);
|
| 302 | if (!init.name)
|
| 303 | return -ENOMEM;
|
| 304 |
|
| 305 | init.ops = &applnco_ops;
|
| 306 | init.parent_data = &pdata;
|
| 307 | init.num_parents = 1;
|
| 308 | init.flags = 0;
|
| 309 |
|
| 310 | chan->hw.init = &init;
|
| 311 | ret = devm_clk_hw_register(&pdev->dev, &chan->hw);
|
| 312 | if (ret)
|
| 313 | return ret;
|
| 314 |
|
| 315 | onecell_data->hws[i] = &chan->hw;
|
| 316 | }
|
| 317 |
|
| 318 | return devm_of_clk_add_hw_provider(&pdev->dev, of_clk_hw_onecell_get,
|
| 319 | onecell_data);
|
| 320 | }
|
| 321 |
|
| 322 | static const struct of_device_id applnco_ids[] = {
|
| 323 | { .compatible = "apple,t8103-nco" },
|
| 324 | { .compatible = "apple,nco" },
|
| 325 | { }
|
| 326 | };
|
| 327 | MODULE_DEVICE_TABLE(of, applnco_ids);
|
| 328 |
|
| 329 | static struct platform_driver applnco_driver = {
|
| 330 | .driver = {
|
| 331 | .name = "apple-nco",
|
| 332 | .of_match_table = applnco_ids,
|
| 333 | },
|
| 334 | .probe = applnco_probe,
|
| 335 | };
|
| 336 | module_platform_driver(applnco_driver);
|
| 337 |
|
| 338 | MODULE_AUTHOR("Martin Povišer <[email protected]>");
|
| 339 | MODULE_DESCRIPTION("Clock driver for NCO blocks on Apple SoCs");
|
| 340 | MODULE_LICENSE("GPL");
|