| // SPDX-License-Identifier: GPL-2.0-only |
| |
| #include <linux/clk-provider.h> |
| #include <linux/device/devres.h> |
| #include <linux/mfd/syscon.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/slab.h> |
| #include <soc/spacemit/ccu.h> |
| |
| #include "ccu_common.h" |
| |
| static DEFINE_IDA(auxiliary_ids); |
| static int spacemit_ccu_register(struct device *dev, |
| struct regmap *regmap, |
| struct regmap *lock_regmap, |
| const struct spacemit_ccu_data *data) |
| { |
| struct clk_hw_onecell_data *clk_data; |
| int i, ret; |
| |
| /* Nothing to do if the CCU does not implement any clocks */ |
| if (!data->hws) |
| return 0; |
| |
| clk_data = devm_kzalloc(dev, struct_size(clk_data, hws, data->num), |
| GFP_KERNEL); |
| if (!clk_data) |
| return -ENOMEM; |
| |
| clk_data->num = data->num; |
| |
| for (i = 0; i < data->num; i++) { |
| struct clk_hw *hw = data->hws[i]; |
| struct ccu_common *common; |
| const char *name; |
| |
| if (!hw) { |
| clk_data->hws[i] = ERR_PTR(-ENOENT); |
| continue; |
| } |
| |
| name = hw->init->name; |
| |
| common = hw_to_ccu_common(hw); |
| common->regmap = regmap; |
| common->lock_regmap = lock_regmap; |
| |
| ret = devm_clk_hw_register(dev, hw); |
| if (ret) { |
| dev_err(dev, "Cannot register clock %d - %s\n", |
| i, name); |
| return ret; |
| } |
| |
| clk_data->hws[i] = hw; |
| } |
| |
| ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, clk_data); |
| if (ret) |
| dev_err(dev, "failed to add clock hardware provider (%d)\n", ret); |
| |
| return ret; |
| } |
| |
| static void spacemit_cadev_release(struct device *dev) |
| { |
| struct auxiliary_device *adev = to_auxiliary_dev(dev); |
| |
| ida_free(&auxiliary_ids, adev->id); |
| kfree(to_spacemit_ccu_adev(adev)); |
| } |
| |
| static void spacemit_adev_unregister(void *data) |
| { |
| struct auxiliary_device *adev = data; |
| |
| auxiliary_device_delete(adev); |
| auxiliary_device_uninit(adev); |
| } |
| |
| static int spacemit_ccu_reset_register(struct device *dev, |
| struct regmap *regmap, |
| const char *reset_name) |
| { |
| struct spacemit_ccu_adev *cadev; |
| struct auxiliary_device *adev; |
| int ret; |
| |
| /* Nothing to do if the CCU does not implement a reset controller */ |
| if (!reset_name) |
| return 0; |
| |
| cadev = kzalloc_obj(*cadev); |
| if (!cadev) |
| return -ENOMEM; |
| |
| cadev->regmap = regmap; |
| |
| adev = &cadev->adev; |
| adev->name = reset_name; |
| adev->dev.parent = dev; |
| adev->dev.release = spacemit_cadev_release; |
| adev->dev.of_node = dev->of_node; |
| ret = ida_alloc(&auxiliary_ids, GFP_KERNEL); |
| if (ret < 0) |
| goto err_free_cadev; |
| adev->id = ret; |
| |
| ret = auxiliary_device_init(adev); |
| if (ret) |
| goto err_free_aux_id; |
| |
| ret = auxiliary_device_add(adev); |
| if (ret) { |
| auxiliary_device_uninit(adev); |
| return ret; |
| } |
| |
| return devm_add_action_or_reset(dev, spacemit_adev_unregister, adev); |
| |
| err_free_aux_id: |
| ida_free(&auxiliary_ids, adev->id); |
| err_free_cadev: |
| kfree(cadev); |
| |
| return ret; |
| } |
| |
| int spacemit_ccu_probe(struct platform_device *pdev, const char *compat) |
| { |
| struct regmap *base_regmap, *lock_regmap = NULL; |
| const struct spacemit_ccu_data *data; |
| struct device *dev = &pdev->dev; |
| int ret; |
| |
| base_regmap = device_node_to_regmap(dev->of_node); |
| if (IS_ERR(base_regmap)) |
| return dev_err_probe(dev, PTR_ERR(base_regmap), |
| "failed to get regmap\n"); |
| |
| /* |
| * The lock status of PLLs locate in MPMU region, while PLLs themselves |
| * are in APBS region. Reference to MPMU syscon is required to check PLL |
| * status. |
| */ |
| if (compat && of_device_is_compatible(dev->of_node, compat)) { |
| struct device_node *mpmu = of_parse_phandle(dev->of_node, |
| "spacemit,mpmu", 0); |
| if (!mpmu) |
| return dev_err_probe(dev, -ENODEV, |
| "Cannot parse MPMU region\n"); |
| |
| lock_regmap = device_node_to_regmap(mpmu); |
| of_node_put(mpmu); |
| |
| if (IS_ERR(lock_regmap)) |
| return dev_err_probe(dev, PTR_ERR(lock_regmap), |
| "failed to get lock regmap\n"); |
| } |
| |
| data = of_device_get_match_data(dev); |
| |
| ret = spacemit_ccu_register(dev, base_regmap, lock_regmap, data); |
| if (ret) |
| return dev_err_probe(dev, ret, "failed to register clocks\n"); |
| |
| ret = spacemit_ccu_reset_register(dev, base_regmap, data->reset_name); |
| if (ret) |
| return dev_err_probe(dev, ret, "failed to register resets\n"); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_NS_GPL(spacemit_ccu_probe, "CLK_SPACEMIT"); |
| |
| MODULE_DESCRIPTION("SpacemiT CCU common clock driver"); |
| MODULE_LICENSE("GPL"); |