| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Copyright (c) 2026, Qualcomm Technologies, Inc. and/or its subsidiaries. | |
| */ | |
| #include <linux/clk-provider.h> | |
| #include <linux/clk/qcom.h> | |
| #include <linux/delay.h> | |
| #include <linux/err.h> | |
| #include <linux/export.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/regmap.h> | |
| #include <linux/regulator/consumer.h> | |
| #include <linux/slab.h> | |
| #define QCOM_CLK_REF_EN_MASK BIT(0) | |
| struct qcom_clk_ref_provider { | |
| size_t num_refs; | |
| struct qcom_clk_ref refs[] __counted_by(num_refs); | |
| }; | |
| static inline struct qcom_clk_ref *to_qcom_clk_ref(struct clk_hw *hw) | |
| { | |
| return container_of(hw, struct qcom_clk_ref, hw); | |
| } | |
| static const struct clk_parent_data qcom_clk_ref_parent_data = { | |
| .index = 0, | |
| }; | |
| static int qcom_clk_ref_prepare(struct clk_hw *hw) | |
| { | |
| struct qcom_clk_ref *rclk = to_qcom_clk_ref(hw); | |
| int ret; | |
| if (!rclk->desc.num_regulators) | |
| return 0; | |
| ret = regulator_bulk_enable(rclk->desc.num_regulators, rclk->regulators); | |
| if (ret) | |
| pr_err("Failed to enable regulators for %s: %d\n", | |
| clk_hw_get_name(hw), ret); | |
| return ret; | |
| } | |
| static void qcom_clk_ref_unprepare(struct clk_hw *hw) | |
| { | |
| struct qcom_clk_ref *rclk = to_qcom_clk_ref(hw); | |
| if (rclk->desc.num_regulators) | |
| regulator_bulk_disable(rclk->desc.num_regulators, rclk->regulators); | |
| } | |
| static int qcom_clk_ref_enable(struct clk_hw *hw) | |
| { | |
| struct qcom_clk_ref *rclk = to_qcom_clk_ref(hw); | |
| int ret; | |
| ret = regmap_set_bits(rclk->regmap, rclk->desc.offset, QCOM_CLK_REF_EN_MASK); | |
| if (ret) | |
| return ret; | |
| udelay(10); | |
| return 0; | |
| } | |
| static void qcom_clk_ref_disable(struct clk_hw *hw) | |
| { | |
| struct qcom_clk_ref *rclk = to_qcom_clk_ref(hw); | |
| regmap_clear_bits(rclk->regmap, rclk->desc.offset, QCOM_CLK_REF_EN_MASK); | |
| udelay(10); | |
| } | |
| static int qcom_clk_ref_is_enabled(struct clk_hw *hw) | |
| { | |
| struct qcom_clk_ref *rclk = to_qcom_clk_ref(hw); | |
| u32 val; | |
| int ret; | |
| ret = regmap_read(rclk->regmap, rclk->desc.offset, &val); | |
| if (ret) | |
| return 0; | |
| return !!(val & QCOM_CLK_REF_EN_MASK); | |
| } | |
| static const struct clk_ops qcom_clk_ref_ops = { | |
| .prepare = qcom_clk_ref_prepare, | |
| .unprepare = qcom_clk_ref_unprepare, | |
| .enable = qcom_clk_ref_enable, | |
| .disable = qcom_clk_ref_disable, | |
| .is_enabled = qcom_clk_ref_is_enabled, | |
| }; | |
| static int qcom_clk_ref_register(struct device *dev, struct regmap *regmap, | |
| struct qcom_clk_ref *clk_refs, | |
| const struct qcom_clk_ref_desc * const *descs, | |
| size_t num_clk_refs) | |
| { | |
| const struct qcom_clk_ref_desc *desc; | |
| struct clk_init_data init_data = {}; | |
| struct qcom_clk_ref *clk_ref; | |
| size_t clk_idx; | |
| unsigned int i; | |
| int ret; | |
| for (clk_idx = 0; clk_idx < num_clk_refs; clk_idx++) { | |
| clk_ref = &clk_refs[clk_idx]; | |
| desc = descs[clk_idx]; | |
| /* Skip unpopulated indices; the array is indexed by clock ID. */ | |
| if (!desc) | |
| continue; | |
| if (WARN_ON(!desc->name)) | |
| continue; | |
| clk_ref->regmap = regmap; | |
| clk_ref->desc = *desc; | |
| if (clk_ref->desc.num_regulators) { | |
| clk_ref->regulators = devm_kcalloc(dev, clk_ref->desc.num_regulators, | |
| sizeof(*clk_ref->regulators), | |
| GFP_KERNEL); | |
| if (!clk_ref->regulators) | |
| return -ENOMEM; | |
| for (i = 0; i < clk_ref->desc.num_regulators; i++) | |
| clk_ref->regulators[i].supply = | |
| clk_ref->desc.regulator_names[i]; | |
| ret = devm_regulator_bulk_get(dev, clk_ref->desc.num_regulators, | |
| clk_ref->regulators); | |
| if (ret) | |
| return dev_err_probe(dev, ret, | |
| "Failed to get regulators for %s\n", | |
| clk_ref->desc.name); | |
| } | |
| init_data.name = clk_ref->desc.name; | |
| init_data.parent_data = &qcom_clk_ref_parent_data; | |
| init_data.num_parents = 1; | |
| init_data.ops = &qcom_clk_ref_ops; | |
| clk_ref->hw.init = &init_data; | |
| ret = devm_clk_hw_register(dev, &clk_ref->hw); | |
| if (ret) | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static struct clk_hw *qcom_clk_ref_provider_get(struct of_phandle_args *clkspec, void *data) | |
| { | |
| struct qcom_clk_ref_provider *provider = data; | |
| unsigned int idx = clkspec->args[0]; | |
| if (idx >= provider->num_refs) | |
| return ERR_PTR(-EINVAL); | |
| if (!provider->refs[idx].regmap) | |
| return ERR_PTR(-ENOENT); | |
| return &provider->refs[idx].hw; | |
| } | |
| int qcom_clk_ref_probe(struct platform_device *pdev, | |
| const struct regmap_config *config, | |
| const struct qcom_clk_ref_desc * const *descs, | |
| size_t num_clk_refs) | |
| { | |
| struct qcom_clk_ref_provider *provider; | |
| struct device *dev = &pdev->dev; | |
| struct regmap *regmap; | |
| void __iomem *base; | |
| int ret; | |
| base = devm_platform_ioremap_resource(pdev, 0); | |
| if (IS_ERR(base)) | |
| return PTR_ERR(base); | |
| regmap = devm_regmap_init_mmio(dev, base, config); | |
| if (IS_ERR(regmap)) | |
| return PTR_ERR(regmap); | |
| provider = devm_kzalloc(dev, struct_size(provider, refs, num_clk_refs), | |
| GFP_KERNEL); | |
| if (!provider) | |
| return -ENOMEM; | |
| provider->num_refs = num_clk_refs; | |
| ret = qcom_clk_ref_register(dev, regmap, provider->refs, descs, | |
| provider->num_refs); | |
| if (ret) | |
| return ret; | |
| return devm_of_clk_add_hw_provider(dev, qcom_clk_ref_provider_get, provider); | |
| } | |
| EXPORT_SYMBOL_GPL(qcom_clk_ref_probe); |