| // SPDX-License-Identifier: GPL-2.0-only OR MIT |
| /* |
| * Driver for an SoC block (Numerically Controlled Oscillator) |
| * found on t8103 (M1) and other Apple chips |
| * |
| * Copyright (C) The Asahi Linux Contributors |
| */ |
| |
| #include <linux/bits.h> |
| #include <linux/bitfield.h> |
| #include <linux/clk-provider.h> |
| #include <linux/io.h> |
| #include <linux/kernel.h> |
| #include <linux/math64.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/spinlock.h> |
| |
| #define NCO_CHANNEL_STRIDE 0x4000 |
| #define NCO_CHANNEL_REGSIZE 20 |
| |
| #define REG_CTRL 0 |
| #define CTRL_ENABLE BIT(31) |
| #define REG_DIV 4 |
| #define DIV_FINE GENMASK(1, 0) |
| #define DIV_COARSE GENMASK(12, 2) |
| #define REG_INC1 8 |
| #define REG_INC2 12 |
| #define REG_ACCINIT 16 |
| |
| /* |
| * Theory of operation (postulated) |
| * |
| * The REG_DIV register indirectly expresses a base integer divisor, roughly |
| * corresponding to twice the desired ratio of input to output clock. This |
| * base divisor is adjusted on a cycle-by-cycle basis based on the state of a |
| * 32-bit phase accumulator to achieve a desired precise clock ratio over the |
| * long term. |
| * |
| * Specifically an output clock cycle is produced after (REG_DIV divisor)/2 |
| * or (REG_DIV divisor + 1)/2 input cycles, the latter taking effect when top |
| * bit of the 32-bit accumulator is set. The accumulator is incremented each |
| * produced output cycle, by the value from either REG_INC1 or REG_INC2, which |
| * of the two is selected depending again on the accumulator's current top bit. |
| * |
| * Because the NCO hardware implements counting of input clock cycles in part |
| * in a Galois linear-feedback shift register, the higher bits of divisor |
| * are programmed into REG_DIV by picking an appropriate LFSR state. See |
| * applnco_compute_tables/applnco_div_translate for details on this. |
| */ |
| |
| #define LFSR_POLY 0xa01 |
| #define LFSR_INIT 0x7ff |
| #define LFSR_LEN 11 |
| #define LFSR_PERIOD ((1 << LFSR_LEN) - 1) |
| #define LFSR_TBLSIZE (1 << LFSR_LEN) |
| |
| /* The minimal attainable coarse divisor (first value in table) */ |
| #define COARSE_DIV_OFFSET 2 |
| |
| struct applnco_tables { |
| u16 fwd[LFSR_TBLSIZE]; |
| u16 inv[LFSR_TBLSIZE]; |
| }; |
| |
| struct applnco_channel { |
| void __iomem *base; |
| struct applnco_tables *tbl; |
| struct clk_hw hw; |
| |
| spinlock_t lock; |
| }; |
| |
| #define to_applnco_channel(_hw) container_of(_hw, struct applnco_channel, hw) |
| |
| static void applnco_enable_nolock(struct clk_hw *hw) |
| { |
| struct applnco_channel *chan = to_applnco_channel(hw); |
| u32 val; |
| |
| val = readl_relaxed(chan->base + REG_CTRL); |
| writel_relaxed(val | CTRL_ENABLE, chan->base + REG_CTRL); |
| } |
| |
| static void applnco_disable_nolock(struct clk_hw *hw) |
| { |
| struct applnco_channel *chan = to_applnco_channel(hw); |
| u32 val; |
| |
| val = readl_relaxed(chan->base + REG_CTRL); |
| writel_relaxed(val & ~CTRL_ENABLE, chan->base + REG_CTRL); |
| } |
| |
| static int applnco_is_enabled(struct clk_hw *hw) |
| { |
| struct applnco_channel *chan = to_applnco_channel(hw); |
| |
| return (readl_relaxed(chan->base + REG_CTRL) & CTRL_ENABLE) != 0; |
| } |
| |
| static void applnco_compute_tables(struct applnco_tables *tbl) |
| { |
| int i; |
| u32 state = LFSR_INIT; |
| |
| /* |
| * Go through the states of a Galois LFSR and build |
| * a coarse divisor translation table. |
| */ |
| for (i = LFSR_PERIOD; i > 0; i--) { |
| if (state & 1) |
| state = (state >> 1) ^ (LFSR_POLY >> 1); |
| else |
| state = (state >> 1); |
| tbl->fwd[i] = state; |
| tbl->inv[state] = i; |
| } |
| |
| /* Zero value is special-cased */ |
| tbl->fwd[0] = 0; |
| tbl->inv[0] = 0; |
| } |
| |
| static bool applnco_div_out_of_range(unsigned int div) |
| { |
| unsigned int coarse = div / 4; |
| |
| return coarse < COARSE_DIV_OFFSET || |
| coarse >= COARSE_DIV_OFFSET + LFSR_TBLSIZE; |
| } |
| |
| static u32 applnco_div_translate(struct applnco_tables *tbl, unsigned int div) |
| { |
| unsigned int coarse = div / 4; |
| |
| if (WARN_ON(applnco_div_out_of_range(div))) |
| return 0; |
| |
| return FIELD_PREP(DIV_COARSE, tbl->fwd[coarse - COARSE_DIV_OFFSET]) | |
| FIELD_PREP(DIV_FINE, div % 4); |
| } |
| |
| static unsigned int applnco_div_translate_inv(struct applnco_tables *tbl, u32 regval) |
| { |
| unsigned int coarse, fine; |
| |
| coarse = tbl->inv[FIELD_GET(DIV_COARSE, regval)] + COARSE_DIV_OFFSET; |
| fine = FIELD_GET(DIV_FINE, regval); |
| |
| return coarse * 4 + fine; |
| } |
| |
| static int applnco_set_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long parent_rate) |
| { |
| struct applnco_channel *chan = to_applnco_channel(hw); |
| unsigned long flags; |
| u32 div, inc1, inc2; |
| bool was_enabled; |
| |
| div = 2 * parent_rate / rate; |
| inc1 = 2 * parent_rate - div * rate; |
| inc2 = inc1 - rate; |
| |
| if (applnco_div_out_of_range(div)) |
| return -EINVAL; |
| |
| div = applnco_div_translate(chan->tbl, div); |
| |
| spin_lock_irqsave(&chan->lock, flags); |
| was_enabled = applnco_is_enabled(hw); |
| applnco_disable_nolock(hw); |
| |
| writel_relaxed(div, chan->base + REG_DIV); |
| writel_relaxed(inc1, chan->base + REG_INC1); |
| writel_relaxed(inc2, chan->base + REG_INC2); |
| |
| /* Presumably a neutral initial value for accumulator */ |
| writel_relaxed(1 << 31, chan->base + REG_ACCINIT); |
| |
| if (was_enabled) |
| applnco_enable_nolock(hw); |
| spin_unlock_irqrestore(&chan->lock, flags); |
| |
| return 0; |
| } |
| |
| static unsigned long applnco_recalc_rate(struct clk_hw *hw, |
| unsigned long parent_rate) |
| { |
| struct applnco_channel *chan = to_applnco_channel(hw); |
| u32 div, inc1, inc2, incbase; |
| |
| div = applnco_div_translate_inv(chan->tbl, |
| readl_relaxed(chan->base + REG_DIV)); |
| |
| inc1 = readl_relaxed(chan->base + REG_INC1); |
| inc2 = readl_relaxed(chan->base + REG_INC2); |
| |
| /* |
| * We don't support wraparound of accumulator |
| * nor the edge case of both increments being zero |
| */ |
| if (inc1 >= (1 << 31) || inc2 < (1 << 31) || (inc1 == 0 && inc2 == 0)) |
| return 0; |
| |
| /* Scale both sides of division by incbase to maintain precision */ |
| incbase = inc1 - inc2; |
| |
| return div64_u64(((u64) parent_rate) * 2 * incbase, |
| ((u64) div) * incbase + inc1); |
| } |
| |
| static long applnco_round_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long *parent_rate) |
| { |
| unsigned long lo = *parent_rate / (COARSE_DIV_OFFSET + LFSR_TBLSIZE) + 1; |
| unsigned long hi = *parent_rate / COARSE_DIV_OFFSET; |
| |
| return clamp(rate, lo, hi); |
| } |
| |
| static int applnco_enable(struct clk_hw *hw) |
| { |
| struct applnco_channel *chan = to_applnco_channel(hw); |
| unsigned long flags; |
| |
| spin_lock_irqsave(&chan->lock, flags); |
| applnco_enable_nolock(hw); |
| spin_unlock_irqrestore(&chan->lock, flags); |
| |
| return 0; |
| } |
| |
| static void applnco_disable(struct clk_hw *hw) |
| { |
| struct applnco_channel *chan = to_applnco_channel(hw); |
| unsigned long flags; |
| |
| spin_lock_irqsave(&chan->lock, flags); |
| applnco_disable_nolock(hw); |
| spin_unlock_irqrestore(&chan->lock, flags); |
| } |
| |
| static const struct clk_ops applnco_ops = { |
| .set_rate = applnco_set_rate, |
| .recalc_rate = applnco_recalc_rate, |
| .round_rate = applnco_round_rate, |
| .enable = applnco_enable, |
| .disable = applnco_disable, |
| .is_enabled = applnco_is_enabled, |
| }; |
| |
| static int applnco_probe(struct platform_device *pdev) |
| { |
| struct device_node *np = pdev->dev.of_node; |
| struct clk_parent_data pdata = { .index = 0 }; |
| struct clk_init_data init; |
| struct clk_hw_onecell_data *onecell_data; |
| void __iomem *base; |
| struct resource *res; |
| struct applnco_tables *tbl; |
| unsigned int nchannels; |
| int ret, i; |
| |
| base = devm_platform_get_and_ioremap_resource(pdev, 0, &res); |
| if (IS_ERR(base)) |
| return PTR_ERR(base); |
| |
| if (resource_size(res) < NCO_CHANNEL_REGSIZE) |
| return -EINVAL; |
| nchannels = (resource_size(res) - NCO_CHANNEL_REGSIZE) |
| / NCO_CHANNEL_STRIDE + 1; |
| |
| onecell_data = devm_kzalloc(&pdev->dev, struct_size(onecell_data, hws, |
| nchannels), GFP_KERNEL); |
| if (!onecell_data) |
| return -ENOMEM; |
| onecell_data->num = nchannels; |
| |
| tbl = devm_kzalloc(&pdev->dev, sizeof(*tbl), GFP_KERNEL); |
| if (!tbl) |
| return -ENOMEM; |
| applnco_compute_tables(tbl); |
| |
| for (i = 0; i < nchannels; i++) { |
| struct applnco_channel *chan; |
| |
| chan = devm_kzalloc(&pdev->dev, sizeof(*chan), GFP_KERNEL); |
| if (!chan) |
| return -ENOMEM; |
| chan->base = base + NCO_CHANNEL_STRIDE * i; |
| chan->tbl = tbl; |
| spin_lock_init(&chan->lock); |
| |
| memset(&init, 0, sizeof(init)); |
| init.name = devm_kasprintf(&pdev->dev, GFP_KERNEL, |
| "%s-%d", np->name, i); |
| init.ops = &applnco_ops; |
| init.parent_data = &pdata; |
| init.num_parents = 1; |
| init.flags = 0; |
| |
| chan->hw.init = &init; |
| ret = devm_clk_hw_register(&pdev->dev, &chan->hw); |
| if (ret) |
| return ret; |
| |
| onecell_data->hws[i] = &chan->hw; |
| } |
| |
| return devm_of_clk_add_hw_provider(&pdev->dev, of_clk_hw_onecell_get, |
| onecell_data); |
| } |
| |
| static const struct of_device_id applnco_ids[] = { |
| { .compatible = "apple,nco" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, applnco_ids); |
| |
| static struct platform_driver applnco_driver = { |
| .driver = { |
| .name = "apple-nco", |
| .of_match_table = applnco_ids, |
| }, |
| .probe = applnco_probe, |
| }; |
| module_platform_driver(applnco_driver); |
| |
| MODULE_AUTHOR("Martin PoviĊĦer <povik+lin@cutebit.org>"); |
| MODULE_DESCRIPTION("Clock driver for NCO blocks on Apple SoCs"); |
| MODULE_LICENSE("GPL"); |