| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2020 BAIKAL ELECTRONICS, JSC |
| * |
| * Authors: |
| * Serge Semin <Sergey.Semin@baikalelectronics.ru> |
| * Dmitry Dunaev <dmitry.dunaev@baikalelectronics.ru> |
| * |
| * Baikal-T1 CCU Dividers interface driver |
| */ |
| |
| #define pr_fmt(fmt) "bt1-ccu-div: " fmt |
| |
| #include <linux/kernel.h> |
| #include <linux/printk.h> |
| #include <linux/bits.h> |
| #include <linux/bitfield.h> |
| #include <linux/slab.h> |
| #include <linux/clk-provider.h> |
| #include <linux/of.h> |
| #include <linux/spinlock.h> |
| #include <linux/regmap.h> |
| #include <linux/delay.h> |
| #include <linux/time64.h> |
| #include <linux/debugfs.h> |
| |
| #include "ccu-div.h" |
| |
| #define CCU_DIV_CTL 0x00 |
| #define CCU_DIV_CTL_EN BIT(0) |
| #define CCU_DIV_CTL_RST BIT(1) |
| #define CCU_DIV_CTL_SET_CLKDIV BIT(2) |
| #define CCU_DIV_CTL_CLKDIV_FLD 4 |
| #define CCU_DIV_CTL_CLKDIV_MASK(_width) \ |
| GENMASK((_width) + CCU_DIV_CTL_CLKDIV_FLD - 1, CCU_DIV_CTL_CLKDIV_FLD) |
| #define CCU_DIV_CTL_LOCK_SHIFTED BIT(27) |
| #define CCU_DIV_CTL_LOCK_NORMAL BIT(31) |
| |
| #define CCU_DIV_RST_DELAY_US 1 |
| #define CCU_DIV_LOCK_CHECK_RETRIES 50 |
| |
| #define CCU_DIV_CLKDIV_MIN 0 |
| #define CCU_DIV_CLKDIV_MAX(_mask) \ |
| ((_mask) >> CCU_DIV_CTL_CLKDIV_FLD) |
| |
| /* |
| * Use the next two methods until there are generic field setter and |
| * getter available with non-constant mask support. |
| */ |
| static inline u32 ccu_div_get(u32 mask, u32 val) |
| { |
| return (val & mask) >> CCU_DIV_CTL_CLKDIV_FLD; |
| } |
| |
| static inline u32 ccu_div_prep(u32 mask, u32 val) |
| { |
| return (val << CCU_DIV_CTL_CLKDIV_FLD) & mask; |
| } |
| |
| static inline unsigned long ccu_div_lock_delay_ns(unsigned long ref_clk, |
| unsigned long div) |
| { |
| u64 ns = 4ULL * (div ?: 1) * NSEC_PER_SEC; |
| |
| do_div(ns, ref_clk); |
| |
| return ns; |
| } |
| |
| static inline unsigned long ccu_div_calc_freq(unsigned long ref_clk, |
| unsigned long div) |
| { |
| return ref_clk / (div ?: 1); |
| } |
| |
| static int ccu_div_var_update_clkdiv(struct ccu_div *div, |
| unsigned long parent_rate, |
| unsigned long divider) |
| { |
| unsigned long nd; |
| u32 val = 0; |
| u32 lock; |
| int count; |
| |
| nd = ccu_div_lock_delay_ns(parent_rate, divider); |
| |
| if (div->features & CCU_DIV_LOCK_SHIFTED) |
| lock = CCU_DIV_CTL_LOCK_SHIFTED; |
| else |
| lock = CCU_DIV_CTL_LOCK_NORMAL; |
| |
| regmap_update_bits(div->sys_regs, div->reg_ctl, |
| CCU_DIV_CTL_SET_CLKDIV, CCU_DIV_CTL_SET_CLKDIV); |
| |
| /* |
| * Until there is nsec-version of readl_poll_timeout() is available |
| * we have to implement the next polling loop. |
| */ |
| count = CCU_DIV_LOCK_CHECK_RETRIES; |
| do { |
| ndelay(nd); |
| regmap_read(div->sys_regs, div->reg_ctl, &val); |
| if (val & lock) |
| return 0; |
| } while (--count); |
| |
| return -ETIMEDOUT; |
| } |
| |
| static int ccu_div_var_enable(struct clk_hw *hw) |
| { |
| struct clk_hw *parent_hw = clk_hw_get_parent(hw); |
| struct ccu_div *div = to_ccu_div(hw); |
| unsigned long flags; |
| u32 val = 0; |
| int ret; |
| |
| if (!parent_hw) { |
| pr_err("Can't enable '%s' with no parent", clk_hw_get_name(hw)); |
| return -EINVAL; |
| } |
| |
| regmap_read(div->sys_regs, div->reg_ctl, &val); |
| if (val & CCU_DIV_CTL_EN) |
| return 0; |
| |
| spin_lock_irqsave(&div->lock, flags); |
| ret = ccu_div_var_update_clkdiv(div, clk_hw_get_rate(parent_hw), |
| ccu_div_get(div->mask, val)); |
| if (!ret) |
| regmap_update_bits(div->sys_regs, div->reg_ctl, |
| CCU_DIV_CTL_EN, CCU_DIV_CTL_EN); |
| spin_unlock_irqrestore(&div->lock, flags); |
| if (ret) |
| pr_err("Divider '%s' lock timed out\n", clk_hw_get_name(hw)); |
| |
| return ret; |
| } |
| |
| static int ccu_div_gate_enable(struct clk_hw *hw) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| unsigned long flags; |
| |
| spin_lock_irqsave(&div->lock, flags); |
| regmap_update_bits(div->sys_regs, div->reg_ctl, |
| CCU_DIV_CTL_EN, CCU_DIV_CTL_EN); |
| spin_unlock_irqrestore(&div->lock, flags); |
| |
| return 0; |
| } |
| |
| static void ccu_div_gate_disable(struct clk_hw *hw) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| unsigned long flags; |
| |
| spin_lock_irqsave(&div->lock, flags); |
| regmap_update_bits(div->sys_regs, div->reg_ctl, CCU_DIV_CTL_EN, 0); |
| spin_unlock_irqrestore(&div->lock, flags); |
| } |
| |
| static int ccu_div_gate_is_enabled(struct clk_hw *hw) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| u32 val = 0; |
| |
| regmap_read(div->sys_regs, div->reg_ctl, &val); |
| |
| return !!(val & CCU_DIV_CTL_EN); |
| } |
| |
| static unsigned long ccu_div_var_recalc_rate(struct clk_hw *hw, |
| unsigned long parent_rate) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| unsigned long divider; |
| u32 val = 0; |
| |
| regmap_read(div->sys_regs, div->reg_ctl, &val); |
| divider = ccu_div_get(div->mask, val); |
| |
| return ccu_div_calc_freq(parent_rate, divider); |
| } |
| |
| static inline unsigned long ccu_div_var_calc_divider(unsigned long rate, |
| unsigned long parent_rate, |
| unsigned int mask) |
| { |
| unsigned long divider; |
| |
| divider = parent_rate / rate; |
| return clamp_t(unsigned long, divider, CCU_DIV_CLKDIV_MIN, |
| CCU_DIV_CLKDIV_MAX(mask)); |
| } |
| |
| static long ccu_div_var_round_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long *parent_rate) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| unsigned long divider; |
| |
| divider = ccu_div_var_calc_divider(rate, *parent_rate, div->mask); |
| |
| return ccu_div_calc_freq(*parent_rate, divider); |
| } |
| |
| /* |
| * This method is used for the clock divider blocks, which support the |
| * on-the-fly rate change. So due to lacking the EN bit functionality |
| * they can't be gated before the rate adjustment. |
| */ |
| static int ccu_div_var_set_rate_slow(struct clk_hw *hw, unsigned long rate, |
| unsigned long parent_rate) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| unsigned long flags, divider; |
| u32 val; |
| int ret; |
| |
| divider = ccu_div_var_calc_divider(rate, parent_rate, div->mask); |
| if (divider == 1 && div->features & CCU_DIV_SKIP_ONE) { |
| divider = 0; |
| } else if (div->features & CCU_DIV_SKIP_ONE_TO_THREE) { |
| if (divider == 1 || divider == 2) |
| divider = 0; |
| else if (divider == 3) |
| divider = 4; |
| } |
| |
| val = ccu_div_prep(div->mask, divider); |
| |
| spin_lock_irqsave(&div->lock, flags); |
| regmap_update_bits(div->sys_regs, div->reg_ctl, div->mask, val); |
| ret = ccu_div_var_update_clkdiv(div, parent_rate, divider); |
| spin_unlock_irqrestore(&div->lock, flags); |
| if (ret) |
| pr_err("Divider '%s' lock timed out\n", clk_hw_get_name(hw)); |
| |
| return ret; |
| } |
| |
| /* |
| * This method is used for the clock divider blocks, which don't support |
| * the on-the-fly rate change. |
| */ |
| static int ccu_div_var_set_rate_fast(struct clk_hw *hw, unsigned long rate, |
| unsigned long parent_rate) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| unsigned long flags, divider; |
| u32 val; |
| |
| divider = ccu_div_var_calc_divider(rate, parent_rate, div->mask); |
| val = ccu_div_prep(div->mask, divider); |
| |
| /* |
| * Also disable the clock divider block if it was enabled by default |
| * or by the bootloader. |
| */ |
| spin_lock_irqsave(&div->lock, flags); |
| regmap_update_bits(div->sys_regs, div->reg_ctl, |
| div->mask | CCU_DIV_CTL_EN, val); |
| spin_unlock_irqrestore(&div->lock, flags); |
| |
| return 0; |
| } |
| |
| static unsigned long ccu_div_fixed_recalc_rate(struct clk_hw *hw, |
| unsigned long parent_rate) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| |
| return ccu_div_calc_freq(parent_rate, div->divider); |
| } |
| |
| static long ccu_div_fixed_round_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long *parent_rate) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| |
| return ccu_div_calc_freq(*parent_rate, div->divider); |
| } |
| |
| static int ccu_div_fixed_set_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long parent_rate) |
| { |
| return 0; |
| } |
| |
| int ccu_div_reset_domain(struct ccu_div *div) |
| { |
| unsigned long flags; |
| |
| if (!div || !(div->features & CCU_DIV_RESET_DOMAIN)) |
| return -EINVAL; |
| |
| spin_lock_irqsave(&div->lock, flags); |
| regmap_update_bits(div->sys_regs, div->reg_ctl, |
| CCU_DIV_CTL_RST, CCU_DIV_CTL_RST); |
| spin_unlock_irqrestore(&div->lock, flags); |
| |
| /* The next delay must be enough to cover all the resets. */ |
| udelay(CCU_DIV_RST_DELAY_US); |
| |
| return 0; |
| } |
| |
| #ifdef CONFIG_DEBUG_FS |
| |
| struct ccu_div_dbgfs_bit { |
| struct ccu_div *div; |
| const char *name; |
| u32 mask; |
| }; |
| |
| #define CCU_DIV_DBGFS_BIT_ATTR(_name, _mask) { \ |
| .name = _name, \ |
| .mask = _mask \ |
| } |
| |
| static const struct ccu_div_dbgfs_bit ccu_div_bits[] = { |
| CCU_DIV_DBGFS_BIT_ATTR("div_en", CCU_DIV_CTL_EN), |
| CCU_DIV_DBGFS_BIT_ATTR("div_rst", CCU_DIV_CTL_RST), |
| CCU_DIV_DBGFS_BIT_ATTR("div_bypass", CCU_DIV_CTL_SET_CLKDIV), |
| CCU_DIV_DBGFS_BIT_ATTR("div_lock", CCU_DIV_CTL_LOCK_NORMAL) |
| }; |
| |
| #define CCU_DIV_DBGFS_BIT_NUM ARRAY_SIZE(ccu_div_bits) |
| |
| /* |
| * It can be dangerous to change the Divider settings behind clock framework |
| * back, therefore we don't provide any kernel config based compile time option |
| * for this feature to enable. |
| */ |
| #undef CCU_DIV_ALLOW_WRITE_DEBUGFS |
| #ifdef CCU_DIV_ALLOW_WRITE_DEBUGFS |
| |
| static int ccu_div_dbgfs_bit_set(void *priv, u64 val) |
| { |
| const struct ccu_div_dbgfs_bit *bit = priv; |
| struct ccu_div *div = bit->div; |
| unsigned long flags; |
| |
| spin_lock_irqsave(&div->lock, flags); |
| regmap_update_bits(div->sys_regs, div->reg_ctl, |
| bit->mask, val ? bit->mask : 0); |
| spin_unlock_irqrestore(&div->lock, flags); |
| |
| return 0; |
| } |
| |
| static int ccu_div_dbgfs_var_clkdiv_set(void *priv, u64 val) |
| { |
| struct ccu_div *div = priv; |
| unsigned long flags; |
| u32 data; |
| |
| val = clamp_t(u64, val, CCU_DIV_CLKDIV_MIN, |
| CCU_DIV_CLKDIV_MAX(div->mask)); |
| data = ccu_div_prep(div->mask, val); |
| |
| spin_lock_irqsave(&div->lock, flags); |
| regmap_update_bits(div->sys_regs, div->reg_ctl, div->mask, data); |
| spin_unlock_irqrestore(&div->lock, flags); |
| |
| return 0; |
| } |
| |
| #define ccu_div_dbgfs_mode 0644 |
| |
| #else /* !CCU_DIV_ALLOW_WRITE_DEBUGFS */ |
| |
| #define ccu_div_dbgfs_bit_set NULL |
| #define ccu_div_dbgfs_var_clkdiv_set NULL |
| #define ccu_div_dbgfs_mode 0444 |
| |
| #endif /* !CCU_DIV_ALLOW_WRITE_DEBUGFS */ |
| |
| static int ccu_div_dbgfs_bit_get(void *priv, u64 *val) |
| { |
| const struct ccu_div_dbgfs_bit *bit = priv; |
| struct ccu_div *div = bit->div; |
| u32 data = 0; |
| |
| regmap_read(div->sys_regs, div->reg_ctl, &data); |
| *val = !!(data & bit->mask); |
| |
| return 0; |
| } |
| DEFINE_DEBUGFS_ATTRIBUTE(ccu_div_dbgfs_bit_fops, |
| ccu_div_dbgfs_bit_get, ccu_div_dbgfs_bit_set, "%llu\n"); |
| |
| static int ccu_div_dbgfs_var_clkdiv_get(void *priv, u64 *val) |
| { |
| struct ccu_div *div = priv; |
| u32 data = 0; |
| |
| regmap_read(div->sys_regs, div->reg_ctl, &data); |
| *val = ccu_div_get(div->mask, data); |
| |
| return 0; |
| } |
| DEFINE_DEBUGFS_ATTRIBUTE(ccu_div_dbgfs_var_clkdiv_fops, |
| ccu_div_dbgfs_var_clkdiv_get, ccu_div_dbgfs_var_clkdiv_set, "%llu\n"); |
| |
| static int ccu_div_dbgfs_fixed_clkdiv_get(void *priv, u64 *val) |
| { |
| struct ccu_div *div = priv; |
| |
| *val = div->divider; |
| |
| return 0; |
| } |
| DEFINE_DEBUGFS_ATTRIBUTE(ccu_div_dbgfs_fixed_clkdiv_fops, |
| ccu_div_dbgfs_fixed_clkdiv_get, NULL, "%llu\n"); |
| |
| static void ccu_div_var_debug_init(struct clk_hw *hw, struct dentry *dentry) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| struct ccu_div_dbgfs_bit *bits; |
| int didx, bidx, num = 2; |
| const char *name; |
| |
| num += !!(div->flags & CLK_SET_RATE_GATE) + |
| !!(div->features & CCU_DIV_RESET_DOMAIN); |
| |
| bits = kcalloc(num, sizeof(*bits), GFP_KERNEL); |
| if (!bits) |
| return; |
| |
| for (didx = 0, bidx = 0; bidx < CCU_DIV_DBGFS_BIT_NUM; ++bidx) { |
| name = ccu_div_bits[bidx].name; |
| if (!(div->flags & CLK_SET_RATE_GATE) && |
| !strcmp("div_en", name)) { |
| continue; |
| } |
| |
| if (!(div->features & CCU_DIV_RESET_DOMAIN) && |
| !strcmp("div_rst", name)) { |
| continue; |
| } |
| |
| bits[didx] = ccu_div_bits[bidx]; |
| bits[didx].div = div; |
| |
| if (div->features & CCU_DIV_LOCK_SHIFTED && |
| !strcmp("div_lock", name)) { |
| bits[didx].mask = CCU_DIV_CTL_LOCK_SHIFTED; |
| } |
| |
| debugfs_create_file_unsafe(bits[didx].name, ccu_div_dbgfs_mode, |
| dentry, &bits[didx], |
| &ccu_div_dbgfs_bit_fops); |
| ++didx; |
| } |
| |
| debugfs_create_file_unsafe("div_clkdiv", ccu_div_dbgfs_mode, dentry, |
| div, &ccu_div_dbgfs_var_clkdiv_fops); |
| } |
| |
| static void ccu_div_gate_debug_init(struct clk_hw *hw, struct dentry *dentry) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| struct ccu_div_dbgfs_bit *bit; |
| |
| bit = kmalloc(sizeof(*bit), GFP_KERNEL); |
| if (!bit) |
| return; |
| |
| *bit = ccu_div_bits[0]; |
| bit->div = div; |
| debugfs_create_file_unsafe(bit->name, ccu_div_dbgfs_mode, dentry, bit, |
| &ccu_div_dbgfs_bit_fops); |
| |
| debugfs_create_file_unsafe("div_clkdiv", 0400, dentry, div, |
| &ccu_div_dbgfs_fixed_clkdiv_fops); |
| } |
| |
| static void ccu_div_fixed_debug_init(struct clk_hw *hw, struct dentry *dentry) |
| { |
| struct ccu_div *div = to_ccu_div(hw); |
| |
| debugfs_create_file_unsafe("div_clkdiv", 0400, dentry, div, |
| &ccu_div_dbgfs_fixed_clkdiv_fops); |
| } |
| |
| #else /* !CONFIG_DEBUG_FS */ |
| |
| #define ccu_div_var_debug_init NULL |
| #define ccu_div_gate_debug_init NULL |
| #define ccu_div_fixed_debug_init NULL |
| |
| #endif /* !CONFIG_DEBUG_FS */ |
| |
| static const struct clk_ops ccu_div_var_gate_to_set_ops = { |
| .enable = ccu_div_var_enable, |
| .disable = ccu_div_gate_disable, |
| .is_enabled = ccu_div_gate_is_enabled, |
| .recalc_rate = ccu_div_var_recalc_rate, |
| .round_rate = ccu_div_var_round_rate, |
| .set_rate = ccu_div_var_set_rate_fast, |
| .debug_init = ccu_div_var_debug_init |
| }; |
| |
| static const struct clk_ops ccu_div_var_nogate_ops = { |
| .recalc_rate = ccu_div_var_recalc_rate, |
| .round_rate = ccu_div_var_round_rate, |
| .set_rate = ccu_div_var_set_rate_slow, |
| .debug_init = ccu_div_var_debug_init |
| }; |
| |
| static const struct clk_ops ccu_div_gate_ops = { |
| .enable = ccu_div_gate_enable, |
| .disable = ccu_div_gate_disable, |
| .is_enabled = ccu_div_gate_is_enabled, |
| .recalc_rate = ccu_div_fixed_recalc_rate, |
| .round_rate = ccu_div_fixed_round_rate, |
| .set_rate = ccu_div_fixed_set_rate, |
| .debug_init = ccu_div_gate_debug_init |
| }; |
| |
| static const struct clk_ops ccu_div_fixed_ops = { |
| .recalc_rate = ccu_div_fixed_recalc_rate, |
| .round_rate = ccu_div_fixed_round_rate, |
| .set_rate = ccu_div_fixed_set_rate, |
| .debug_init = ccu_div_fixed_debug_init |
| }; |
| |
| struct ccu_div *ccu_div_hw_register(const struct ccu_div_init_data *div_init) |
| { |
| struct clk_parent_data parent_data = { }; |
| struct clk_init_data hw_init = { }; |
| struct ccu_div *div; |
| int ret; |
| |
| if (!div_init) |
| return ERR_PTR(-EINVAL); |
| |
| div = kzalloc(sizeof(*div), GFP_KERNEL); |
| if (!div) |
| return ERR_PTR(-ENOMEM); |
| |
| /* |
| * Note since Baikal-T1 System Controller registers are MMIO-backed |
| * we won't check the regmap IO operations return status, because it |
| * must be zero anyway. |
| */ |
| div->hw.init = &hw_init; |
| div->id = div_init->id; |
| div->reg_ctl = div_init->base + CCU_DIV_CTL; |
| div->sys_regs = div_init->sys_regs; |
| div->flags = div_init->flags; |
| div->features = div_init->features; |
| spin_lock_init(&div->lock); |
| |
| hw_init.name = div_init->name; |
| hw_init.flags = div_init->flags; |
| |
| if (div_init->type == CCU_DIV_VAR) { |
| if (hw_init.flags & CLK_SET_RATE_GATE) |
| hw_init.ops = &ccu_div_var_gate_to_set_ops; |
| else |
| hw_init.ops = &ccu_div_var_nogate_ops; |
| div->mask = CCU_DIV_CTL_CLKDIV_MASK(div_init->width); |
| } else if (div_init->type == CCU_DIV_GATE) { |
| hw_init.ops = &ccu_div_gate_ops; |
| div->divider = div_init->divider; |
| } else if (div_init->type == CCU_DIV_FIXED) { |
| hw_init.ops = &ccu_div_fixed_ops; |
| div->divider = div_init->divider; |
| } else { |
| ret = -EINVAL; |
| goto err_free_div; |
| } |
| |
| if (!div_init->parent_name) { |
| ret = -EINVAL; |
| goto err_free_div; |
| } |
| parent_data.fw_name = div_init->parent_name; |
| hw_init.parent_data = &parent_data; |
| hw_init.num_parents = 1; |
| |
| ret = of_clk_hw_register(div_init->np, &div->hw); |
| if (ret) |
| goto err_free_div; |
| |
| return div; |
| |
| err_free_div: |
| kfree(div); |
| |
| return ERR_PTR(ret); |
| } |
| |
| void ccu_div_hw_unregister(struct ccu_div *div) |
| { |
| clk_hw_unregister(&div->hw); |
| |
| kfree(div); |
| } |