| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Synopsys AXS10X SDP I2S PLL clock driver |
| * |
| * Copyright (C) 2016 Synopsys |
| */ |
| |
| #include <linux/platform_device.h> |
| #include <linux/module.h> |
| #include <linux/clk-provider.h> |
| #include <linux/err.h> |
| #include <linux/device.h> |
| #include <linux/io.h> |
| #include <linux/of_address.h> |
| #include <linux/slab.h> |
| #include <linux/of.h> |
| |
| /* PLL registers addresses */ |
| #define PLL_IDIV_REG 0x0 |
| #define PLL_FBDIV_REG 0x4 |
| #define PLL_ODIV0_REG 0x8 |
| #define PLL_ODIV1_REG 0xC |
| |
| struct i2s_pll_cfg { |
| unsigned int rate; |
| unsigned int idiv; |
| unsigned int fbdiv; |
| unsigned int odiv0; |
| unsigned int odiv1; |
| }; |
| |
| static const struct i2s_pll_cfg i2s_pll_cfg_27m[] = { |
| /* 27 Mhz */ |
| { 1024000, 0x104, 0x451, 0x10E38, 0x2000 }, |
| { 1411200, 0x104, 0x596, 0x10D35, 0x2000 }, |
| { 1536000, 0x208, 0xA28, 0x10B2C, 0x2000 }, |
| { 2048000, 0x82, 0x451, 0x10E38, 0x2000 }, |
| { 2822400, 0x82, 0x596, 0x10D35, 0x2000 }, |
| { 3072000, 0x104, 0xA28, 0x10B2C, 0x2000 }, |
| { 2116800, 0x82, 0x3CF, 0x10C30, 0x2000 }, |
| { 2304000, 0x104, 0x79E, 0x10B2C, 0x2000 }, |
| { 0, 0, 0, 0, 0 }, |
| }; |
| |
| static const struct i2s_pll_cfg i2s_pll_cfg_28m[] = { |
| /* 28.224 Mhz */ |
| { 1024000, 0x82, 0x105, 0x107DF, 0x2000 }, |
| { 1411200, 0x28A, 0x1, 0x10001, 0x2000 }, |
| { 1536000, 0xA28, 0x187, 0x10042, 0x2000 }, |
| { 2048000, 0x41, 0x105, 0x107DF, 0x2000 }, |
| { 2822400, 0x145, 0x1, 0x10001, 0x2000 }, |
| { 3072000, 0x514, 0x187, 0x10042, 0x2000 }, |
| { 2116800, 0x514, 0x42, 0x10001, 0x2000 }, |
| { 2304000, 0x619, 0x82, 0x10001, 0x2000 }, |
| { 0, 0, 0, 0, 0 }, |
| }; |
| |
| struct i2s_pll_clk { |
| void __iomem *base; |
| struct clk_hw hw; |
| struct device *dev; |
| }; |
| |
| static inline void i2s_pll_write(struct i2s_pll_clk *clk, unsigned int reg, |
| unsigned int val) |
| { |
| writel_relaxed(val, clk->base + reg); |
| } |
| |
| static inline unsigned int i2s_pll_read(struct i2s_pll_clk *clk, |
| unsigned int reg) |
| { |
| return readl_relaxed(clk->base + reg); |
| } |
| |
| static inline struct i2s_pll_clk *to_i2s_pll_clk(struct clk_hw *hw) |
| { |
| return container_of(hw, struct i2s_pll_clk, hw); |
| } |
| |
| static inline unsigned int i2s_pll_get_value(unsigned int val) |
| { |
| return (val & 0x3F) + ((val >> 6) & 0x3F); |
| } |
| |
| static const struct i2s_pll_cfg *i2s_pll_get_cfg(unsigned long prate) |
| { |
| switch (prate) { |
| case 27000000: |
| return i2s_pll_cfg_27m; |
| case 28224000: |
| return i2s_pll_cfg_28m; |
| default: |
| return NULL; |
| } |
| } |
| |
| static unsigned long i2s_pll_recalc_rate(struct clk_hw *hw, |
| unsigned long parent_rate) |
| { |
| struct i2s_pll_clk *clk = to_i2s_pll_clk(hw); |
| unsigned int idiv, fbdiv, odiv; |
| |
| idiv = i2s_pll_get_value(i2s_pll_read(clk, PLL_IDIV_REG)); |
| fbdiv = i2s_pll_get_value(i2s_pll_read(clk, PLL_FBDIV_REG)); |
| odiv = i2s_pll_get_value(i2s_pll_read(clk, PLL_ODIV0_REG)); |
| |
| return ((parent_rate / idiv) * fbdiv) / odiv; |
| } |
| |
| static long i2s_pll_round_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long *prate) |
| { |
| struct i2s_pll_clk *clk = to_i2s_pll_clk(hw); |
| const struct i2s_pll_cfg *pll_cfg = i2s_pll_get_cfg(*prate); |
| int i; |
| |
| if (!pll_cfg) { |
| dev_err(clk->dev, "invalid parent rate=%ld\n", *prate); |
| return -EINVAL; |
| } |
| |
| for (i = 0; pll_cfg[i].rate != 0; i++) |
| if (pll_cfg[i].rate == rate) |
| return rate; |
| |
| return -EINVAL; |
| } |
| |
| static int i2s_pll_set_rate(struct clk_hw *hw, unsigned long rate, |
| unsigned long parent_rate) |
| { |
| struct i2s_pll_clk *clk = to_i2s_pll_clk(hw); |
| const struct i2s_pll_cfg *pll_cfg = i2s_pll_get_cfg(parent_rate); |
| int i; |
| |
| if (!pll_cfg) { |
| dev_err(clk->dev, "invalid parent rate=%ld\n", parent_rate); |
| return -EINVAL; |
| } |
| |
| for (i = 0; pll_cfg[i].rate != 0; i++) { |
| if (pll_cfg[i].rate == rate) { |
| i2s_pll_write(clk, PLL_IDIV_REG, pll_cfg[i].idiv); |
| i2s_pll_write(clk, PLL_FBDIV_REG, pll_cfg[i].fbdiv); |
| i2s_pll_write(clk, PLL_ODIV0_REG, pll_cfg[i].odiv0); |
| i2s_pll_write(clk, PLL_ODIV1_REG, pll_cfg[i].odiv1); |
| return 0; |
| } |
| } |
| |
| dev_err(clk->dev, "invalid rate=%ld, parent_rate=%ld\n", rate, |
| parent_rate); |
| return -EINVAL; |
| } |
| |
| static const struct clk_ops i2s_pll_ops = { |
| .recalc_rate = i2s_pll_recalc_rate, |
| .round_rate = i2s_pll_round_rate, |
| .set_rate = i2s_pll_set_rate, |
| }; |
| |
| static int i2s_pll_clk_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct device_node *node = dev->of_node; |
| const char *clk_name; |
| const char *parent_name; |
| struct clk *clk; |
| struct i2s_pll_clk *pll_clk; |
| struct clk_init_data init; |
| |
| pll_clk = devm_kzalloc(dev, sizeof(*pll_clk), GFP_KERNEL); |
| if (!pll_clk) |
| return -ENOMEM; |
| |
| pll_clk->base = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(pll_clk->base)) |
| return PTR_ERR(pll_clk->base); |
| |
| memset(&init, 0, sizeof(init)); |
| clk_name = node->name; |
| init.name = clk_name; |
| init.ops = &i2s_pll_ops; |
| parent_name = of_clk_get_parent_name(node, 0); |
| init.parent_names = &parent_name; |
| init.num_parents = 1; |
| pll_clk->hw.init = &init; |
| pll_clk->dev = dev; |
| |
| clk = devm_clk_register(dev, &pll_clk->hw); |
| if (IS_ERR(clk)) { |
| dev_err(dev, "failed to register %s clock (%ld)\n", |
| clk_name, PTR_ERR(clk)); |
| return PTR_ERR(clk); |
| } |
| |
| return of_clk_add_provider(node, of_clk_src_simple_get, clk); |
| } |
| |
| static void i2s_pll_clk_remove(struct platform_device *pdev) |
| { |
| of_clk_del_provider(pdev->dev.of_node); |
| } |
| |
| static const struct of_device_id i2s_pll_clk_id[] = { |
| { .compatible = "snps,axs10x-i2s-pll-clock", }, |
| { }, |
| }; |
| MODULE_DEVICE_TABLE(of, i2s_pll_clk_id); |
| |
| static struct platform_driver i2s_pll_clk_driver = { |
| .driver = { |
| .name = "axs10x-i2s-pll-clock", |
| .of_match_table = i2s_pll_clk_id, |
| }, |
| .probe = i2s_pll_clk_probe, |
| .remove_new = i2s_pll_clk_remove, |
| }; |
| module_platform_driver(i2s_pll_clk_driver); |
| |
| MODULE_AUTHOR("Jose Abreu <joabreu@synopsys.com>"); |
| MODULE_DESCRIPTION("Synopsys AXS10X SDP I2S PLL Clock Driver"); |
| MODULE_LICENSE("GPL v2"); |