diff --git a/drivers/thermal/qcom/Kconfig b/drivers/thermal/qcom/Kconfig index ea9fbf657ff7..ea3ba4f7b967 100644 --- a/drivers/thermal/qcom/Kconfig +++ b/drivers/thermal/qcom/Kconfig @@ -140,3 +140,22 @@ config QTI_USERSPACE_CDEV socket, so that userspace cooling device can perform a mitigation action. +config QTI_THERMAL_LIMITS_DCVS + tristate "QTI LMH DCVS Driver" + depends on THERMAL && CPU_THERMAL + help + This enables the driver for Limits Management Hardware - DCVS block + for the application processors. The h/w block that is available for + each cluster can be used to perform quick thermal mitigations by + tracking temperatures of the CPUs and taking thermal action in the + hardware without s/w intervention. + +config QTI_SDPM_CLOCK_MONITOR + tristate "QTI SDPM Clock Monitor" + depends on COMMON_CLK && CPU_FREQ + help + This enables the QTI SDPM Clock Monitor. This driver can register + for different clock rate change notifications and write the clock + rate into the SDPM CSR register. This driver will receive the clock + list and the CSR details from devicetree. + diff --git a/drivers/thermal/qcom/Makefile b/drivers/thermal/qcom/Makefile index 51714d52a030..1f3ca7755eb0 100644 --- a/drivers/thermal/qcom/Makefile +++ b/drivers/thermal/qcom/Makefile @@ -21,3 +21,5 @@ obj-$(CONFIG_QTI_CPU_VOLTAGE_COOLING_DEVICE) += cpu_voltage_cooling.o obj-$(CONFIG_QTI_CPU_HOTPLUG_COOLING_DEVICE) += cpu_hotplug.o obj-$(CONFIG_QTI_DDR_COOLING_DEVICE) += ddr_cdev.o obj-$(CONFIG_QTI_USERSPACE_CDEV) += qti_userspace_cdev.o +obj-$(CONFIG_QTI_THERMAL_LIMITS_DCVS) += msm_lmh_dcvs.o +obj-${CONFIG_QTI_SDPM_CLOCK_MONITOR} += sdpm_clk.o diff --git a/drivers/thermal/qcom/msm_lmh_dcvs.c b/drivers/thermal/qcom/msm_lmh_dcvs.c new file mode 100644 index 000000000000..7a383c98f81c --- /dev/null +++ b/drivers/thermal/qcom/msm_lmh_dcvs.c @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2016-2021, The Linux Foundation. All rights reserved. + * Copyright (c) 2021-2022, Qualcomm Innovation Center, Inc. All rights reserved. + */ + +#define pr_fmt(fmt) "%s:%s " fmt, KBUILD_MODNAME, __func__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define LIMITS_DCVSH 0x10 +#define LIMITS_NODE_DCVS 0x44435653 + +#define LIMITS_SUB_FN_THERMAL 0x54484D4C +#define LIMITS_HI_THRESHOLD 0x48494748 +#define LIMITS_LOW_THRESHOLD 0x4C4F5700 +#define LIMITS_ARM_THRESHOLD 0x41524D00 + +#define LIMITS_CLUSTER_0 0x6370302D +#define LIMITS_CLUSTER_1 0x6370312D + +#define LIMITS_FREQ_CAP 0x46434150 + +#define LIMITS_TEMP_DEFAULT 75000 +#define LIMITS_TEMP_HIGH_THRESH_MAX 120000 +#define LIMITS_LOW_THRESHOLD_OFFSET 500 +#define LIMITS_POLLING_DELAY_MS 10 +#define LIMITS_CLUSTER_REQ_OFFSET 0x704 +#define LIMITS_CLUSTER_INT_CLR_OFFSET 0x8 +#define dcvsh_get_frequency(_val, _max) do { \ + _max = (_val) & 0x3FF; \ + _max *= 19200; \ +} while (0) +#define FREQ_KHZ_TO_HZ(_val) ((_val) * 1000) +#define FREQ_HZ_TO_KHZ(_val) ((_val) / 1000) + +enum lmh_hw_trips { + LIMITS_TRIP_ARM, + LIMITS_TRIP_HI, + LIMITS_TRIP_MAX, +}; + +struct __limits_cdev_data { + struct thermal_cooling_device *cdev; + u32 max_freq; +}; + +struct limits_dcvs_hw { + char sensor_name[THERMAL_NAME_LENGTH]; + uint32_t affinity; + int irq_num; + void *osm_hw_reg; + void *int_clr_reg; + cpumask_t core_map; + struct delayed_work freq_poll_work; + unsigned long max_freq[NR_CPUS]; + unsigned long hw_freq_limit; + struct device_attribute lmh_freq_attr; + struct list_head list; + bool is_irq_enabled; + struct mutex access_lock; + struct __limits_cdev_data *cdev_data; + uint32_t cdev_registered; + struct regulator *isens_reg[2]; +}; + +LIST_HEAD(lmh_dcvs_hw_list); +DEFINE_MUTEX(lmh_dcvs_list_access); + +static void limits_dcvs_get_freq_limits(struct limits_dcvs_hw *hw) +{ + unsigned long freq_ceil = UINT_MAX, freq_floor = 0; + struct device *cpu_dev = NULL; + uint32_t cpu, idx = 0; + + for_each_cpu(cpu, &hw->core_map) { + freq_ceil = UINT_MAX; + freq_floor = 0; + cpu_dev = get_cpu_device(cpu); + if (!cpu_dev) { + pr_err("Error in get CPU%d device\n", cpu); + idx++; + continue; + } + + dev_pm_opp_find_freq_floor(cpu_dev, &freq_ceil); + dev_pm_opp_find_freq_ceil(cpu_dev, &freq_floor); + + hw->max_freq[idx] = freq_ceil / 1000; + idx++; + } +} + +static unsigned long limits_mitigation_notify(struct limits_dcvs_hw *hw) +{ + uint32_t val = 0, max_cpu_ct = 0, max_cpu_limit = 0, idx = 0, cpu = 0; + struct device *cpu_dev = NULL; + unsigned long freq_val, max_limit = 0; + struct dev_pm_opp *opp_entry; + + val = readl_relaxed(hw->osm_hw_reg); + dcvsh_get_frequency(val, max_limit); + for_each_cpu(cpu, &hw->core_map) { + cpu_dev = get_cpu_device(cpu); + if (!cpu_dev) { + pr_err("Error in get CPU%d device\n", + cpumask_first(&hw->core_map)); + goto notify_exit; + } + + pr_debug("CPU:%d max value read:%lu\n", + cpumask_first(&hw->core_map), + max_limit); + freq_val = FREQ_KHZ_TO_HZ(max_limit); + opp_entry = dev_pm_opp_find_freq_floor(cpu_dev, &freq_val); + /* + * Hardware mitigation frequency can be lower than the lowest + * possible CPU frequency. In that case freq floor call will + * fail with -ERANGE and we need to match to the lowest + * frequency using freq_ceil. + */ + if (IS_ERR(opp_entry) && PTR_ERR(opp_entry) == -ERANGE) { + opp_entry = dev_pm_opp_find_freq_ceil(cpu_dev, + &freq_val); + if (IS_ERR(opp_entry)) + dev_err(cpu_dev, + "frequency:%lu. opp error:%ld\n", + freq_val, PTR_ERR(opp_entry)); + } + if (FREQ_HZ_TO_KHZ(freq_val) == hw->max_freq[idx]) { + max_cpu_ct++; + if (max_cpu_limit < hw->max_freq[idx]) + max_cpu_limit = hw->max_freq[idx]; + idx++; + continue; + } + max_limit = FREQ_HZ_TO_KHZ(freq_val); + break; + } + + if (max_cpu_ct == cpumask_weight(&hw->core_map)) + max_limit = max_cpu_limit; + + pr_debug("CPU:%d max limit:%lu\n", cpumask_first(&hw->core_map), + max_limit); + +notify_exit: + hw->hw_freq_limit = max_limit; + return max_limit; +} + +static void limits_dcvs_poll(struct work_struct *work) +{ + unsigned long max_limit = 0; + struct limits_dcvs_hw *hw = container_of(work, + struct limits_dcvs_hw, + freq_poll_work.work); + int cpu_ct = 0, cpu = 0, idx = 0; + + mutex_lock(&hw->access_lock); + if (hw->max_freq[0] == U32_MAX) + limits_dcvs_get_freq_limits(hw); + max_limit = limits_mitigation_notify(hw); + for_each_cpu(cpu, &hw->core_map) { + if (max_limit >= hw->max_freq[idx]) + cpu_ct++; + idx++; + } + if (cpu_ct >= cpumask_weight(&hw->core_map)) { + writel_relaxed(0xFF, hw->int_clr_reg); + hw->is_irq_enabled = true; + enable_irq(hw->irq_num); + } else { + mod_delayed_work(system_highpri_wq, &hw->freq_poll_work, + msecs_to_jiffies(LIMITS_POLLING_DELAY_MS)); + } + mutex_unlock(&hw->access_lock); +} + +static void lmh_dcvs_notify(struct limits_dcvs_hw *hw) +{ + if (hw->is_irq_enabled) { + hw->is_irq_enabled = false; + disable_irq_nosync(hw->irq_num); + limits_mitigation_notify(hw); + mod_delayed_work(system_highpri_wq, &hw->freq_poll_work, + msecs_to_jiffies(LIMITS_POLLING_DELAY_MS)); + } +} + +static irqreturn_t lmh_dcvs_handle_isr(int irq, void *data) +{ + struct limits_dcvs_hw *hw = data; + + mutex_lock(&hw->access_lock); + lmh_dcvs_notify(hw); + mutex_unlock(&hw->access_lock); + + return IRQ_HANDLED; +} + +static void limits_isens_qref_init(struct platform_device *pdev, + struct limits_dcvs_hw *hw, + int idx, char *reg_name, + char *reg_setting) +{ + int ret = 0; + uint32_t settings[3]; + + ret = of_property_read_u32_array(pdev->dev.of_node, + reg_setting, settings, 3); + if (ret) { + if (ret == -EINVAL) + return; + + pr_err("Regulator:isens_vref settings read error:%d\n", + ret); + return; + } + hw->isens_reg[idx] = devm_regulator_get(&pdev->dev, reg_name); + if (IS_ERR_OR_NULL(hw->isens_reg[idx])) { + pr_err("Regulator:isens_vref init error:%ld\n", + PTR_ERR(hw->isens_reg[idx])); + return; + } + ret = regulator_set_voltage(hw->isens_reg[idx], settings[0], + settings[1]); + if (ret) { + pr_err("Regulator:isens_vref set voltage error:%d\n", ret); + return; + } + ret = regulator_set_load(hw->isens_reg[idx], settings[2]); + if (ret) { + pr_err("Regulator:isens_vref set load error:%d\n", ret); + return; + } + if (regulator_enable(hw->isens_reg[idx])) { + pr_err("Failed to enable regulator:isens_vref\n"); + return; + } +} + +static void limits_isens_vref_ldo_init(struct platform_device *pdev, + struct limits_dcvs_hw *hw) +{ + limits_isens_qref_init(pdev, hw, 0, "isens_vref_1p8", + "isens-vref-1p8-settings"); + limits_isens_qref_init(pdev, hw, 1, "isens_vref_0p8", + "isens-vref-0p8-settings"); +} + +static ssize_t +lmh_freq_limit_show(struct device *dev, struct device_attribute *devattr, + char *buf) +{ + struct limits_dcvs_hw *hw = container_of(devattr, + struct limits_dcvs_hw, + lmh_freq_attr); + + return scnprintf(buf, PAGE_SIZE, "%lu\n", hw->hw_freq_limit); +} + +static int limits_dcvs_probe(struct platform_device *pdev) +{ + int ret; + int affinity = -1; + struct limits_dcvs_hw *hw; + struct device_node *dn = pdev->dev.of_node; + struct device_node *cpu_node, *lmh_node; + uint32_t request_reg, clear_reg; + int cpu, idx = 0; + cpumask_t mask = { CPU_BITS_NONE }; + const __be32 *addr; + + for_each_possible_cpu(cpu) { + cpu_node = of_cpu_device_node_get(cpu); + if (!cpu_node) + continue; + lmh_node = of_parse_phandle(cpu_node, "qcom,lmh-dcvs", 0); + if (lmh_node == dn) { + /*set the cpumask*/ + cpumask_set_cpu(cpu, &(mask)); + } + of_node_put(cpu_node); + of_node_put(lmh_node); + } + + hw = devm_kzalloc(&pdev->dev, sizeof(*hw), GFP_KERNEL); + if (!hw) + return -ENOMEM; + /* + * We just init regulator if none of the CPUs have + * reference to our LMH node + */ + if (cpumask_empty(&mask)) { + limits_isens_vref_ldo_init(pdev, hw); + mutex_lock(&lmh_dcvs_list_access); + INIT_LIST_HEAD(&hw->list); + list_add_tail(&hw->list, &lmh_dcvs_hw_list); + mutex_unlock(&lmh_dcvs_list_access); + return 0; + } + + hw->cdev_data = devm_kcalloc(&pdev->dev, cpumask_weight(&mask), + sizeof(*hw->cdev_data), + GFP_KERNEL); + if (!hw->cdev_data) + return -ENOMEM; + + cpumask_copy(&hw->core_map, &mask); + hw->cdev_registered = 0; + for_each_cpu(cpu, &hw->core_map) { + hw->cdev_data[idx].cdev = NULL; + hw->cdev_data[idx].max_freq = U32_MAX; + hw->max_freq[idx] = U32_MAX; + idx++; + } + ret = of_property_read_u32(dn, "qcom,affinity", &affinity); + if (ret) + return -ENODEV; + switch (affinity) { + case 0: + hw->affinity = LIMITS_CLUSTER_0; + break; + case 1: + hw->affinity = LIMITS_CLUSTER_1; + break; + default: + return -EINVAL; + } + + addr = of_get_address(dn, 0, NULL, NULL); + if (!addr) { + pr_err("Property llm-base-addr not found\n"); + return -EINVAL; + } + clear_reg = be32_to_cpu(addr[0]) + LIMITS_CLUSTER_INT_CLR_OFFSET; + addr = of_get_address(dn, 1, NULL, NULL); + if (!addr) { + pr_err("Property osm-base-addr not found\n"); + return -EINVAL; + } + request_reg = be32_to_cpu(addr[0]) + LIMITS_CLUSTER_REQ_OFFSET; + + hw->hw_freq_limit = U32_MAX; + snprintf(hw->sensor_name, sizeof(hw->sensor_name), "limits_sensor-%02d", + affinity); + + mutex_init(&hw->access_lock); + INIT_DEFERRABLE_WORK(&hw->freq_poll_work, limits_dcvs_poll); + hw->osm_hw_reg = devm_ioremap(&pdev->dev, request_reg, 0x4); + if (!hw->osm_hw_reg) { + pr_err("register remap failed\n"); + goto probe_exit; + } + hw->int_clr_reg = devm_ioremap(&pdev->dev, clear_reg, 0x4); + if (!hw->int_clr_reg) { + pr_err("interrupt clear reg remap failed\n"); + goto probe_exit; + } + + hw->irq_num = of_irq_get(pdev->dev.of_node, 0); + if (hw->irq_num < 0) { + pr_err("Error getting IRQ number. err:%d\n", hw->irq_num); + goto probe_exit; + } + hw->is_irq_enabled = true; + ret = devm_request_threaded_irq(&pdev->dev, hw->irq_num, NULL, + lmh_dcvs_handle_isr, IRQF_TRIGGER_HIGH | IRQF_ONESHOT + | IRQF_NO_SUSPEND | IRQF_SHARED, hw->sensor_name, hw); + if (ret) { + pr_err("Error registering for irq. err:%d\n", ret); + ret = 0; + goto probe_exit; + } + limits_isens_vref_ldo_init(pdev, hw); + sysfs_attr_init(&hw->lmh_freq_attr.attr); + hw->lmh_freq_attr.attr.name = "lmh_freq_limit"; + hw->lmh_freq_attr.show = lmh_freq_limit_show; + hw->lmh_freq_attr.attr.mode = 0444; + device_create_file(&pdev->dev, &hw->lmh_freq_attr); + +probe_exit: + mutex_lock(&lmh_dcvs_list_access); + INIT_LIST_HEAD(&hw->list); + list_add_tail(&hw->list, &lmh_dcvs_hw_list); + mutex_unlock(&lmh_dcvs_list_access); + + return ret; +} + +static const struct of_device_id limits_dcvs_match[] = { + { .compatible = "qcom,msm-hw-limits", }, + {}, +}; + +static struct platform_driver limits_dcvs_driver = { + .probe = limits_dcvs_probe, + .driver = { + .name = KBUILD_MODNAME, + .of_match_table = limits_dcvs_match, + }, +}; +builtin_platform_driver(limits_dcvs_driver); +MODULE_LICENSE("GPL"); diff --git a/drivers/thermal/qcom/sdpm_clk.c b/drivers/thermal/qcom/sdpm_clk.c new file mode 100644 index 000000000000..23b04177b21d --- /dev/null +++ b/drivers/thermal/qcom/sdpm_clk.c @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2020, The Linux Foundation. All rights reserved. + * Copyright (c) 2021-2022, Qualcomm Innovation Center, Inc. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define SDPM_DRIVER "sdpm-clk-notify" +#define CSR_MAX_VAL 7 +#define CSR_OFFSET 0xF00 +#define FREQ_HZ_TO_MHZ(f) ((f) / 1000000) + +struct sdpm_clk_instance; +struct sdpm_clk_data { + struct notifier_block clk_rate_nb; + struct clk *clk; + const char *clock_name; + struct notifier_block reg_nb; + struct regulator *reg; + uint8_t reg_enable; + uint32_t csr_id; + unsigned long last_freq; + struct mutex sdpm_mutex; + struct sdpm_clk_instance *sdpm_inst; +}; +struct sdpm_clk_instance { + struct device *dev; + void __iomem *regmap; + uint32_t clk_ct; + struct sdpm_clk_data *clk_data; +}; + +static void sdpm_csr_write(struct sdpm_clk_data *sdpm_data, + unsigned long clk_rate) +{ + struct sdpm_clk_instance *sdpm_inst = sdpm_data->sdpm_inst; + uint32_t val = sdpm_data->reg_enable ? clk_rate : 0; + + sdpm_data->last_freq = clk_rate; + + dev_dbg(sdpm_inst->dev, "clock:%s offset:0x%x frequency:%u\n", + sdpm_data->clock_name, + CSR_OFFSET + sdpm_data->csr_id * 4, val); + writel_relaxed(val, + sdpm_inst->regmap + CSR_OFFSET + sdpm_data->csr_id * 4); +} + +static int sdpm_reg_notifier(struct notifier_block *nb, unsigned long event, + void *data) +{ + struct sdpm_clk_data *sdpm_data = container_of(nb, + struct sdpm_clk_data, reg_nb); + + dev_dbg(sdpm_data->sdpm_inst->dev, "reg:%s event:%lu\n", + sdpm_data->clock_name, event); + switch (event) { + case REGULATOR_EVENT_ENABLE: + mutex_lock(&sdpm_data->sdpm_mutex); + sdpm_data->reg_enable = 1; + sdpm_csr_write(sdpm_data, sdpm_data->last_freq); + mutex_unlock(&sdpm_data->sdpm_mutex); + return NOTIFY_OK; + case REGULATOR_EVENT_DISABLE: + mutex_lock(&sdpm_data->sdpm_mutex); + sdpm_data->reg_enable = 0; + sdpm_csr_write(sdpm_data, sdpm_data->last_freq); + mutex_unlock(&sdpm_data->sdpm_mutex); + return NOTIFY_OK; + default: + return NOTIFY_OK; + } + + return NOTIFY_OK; +} + +static int sdpm_clock_notifier(struct notifier_block *nb, + unsigned long event, void *data) +{ + struct clk_notifier_data *ndata = data; + struct sdpm_clk_data *sdpm_data = container_of(nb, + struct sdpm_clk_data, clk_rate_nb); + + dev_dbg(sdpm_data->sdpm_inst->dev, "clock:%s event:%lu\n", + sdpm_data->clock_name, event); + switch (event) { + case PRE_RATE_CHANGE: + mutex_lock(&sdpm_data->sdpm_mutex); + if (ndata->new_rate > ndata->old_rate) + sdpm_csr_write(sdpm_data, + FREQ_HZ_TO_MHZ(ndata->new_rate)); + mutex_unlock(&sdpm_data->sdpm_mutex); + return NOTIFY_DONE; + case POST_RATE_CHANGE: + mutex_lock(&sdpm_data->sdpm_mutex); + if (ndata->new_rate < ndata->old_rate) + sdpm_csr_write(sdpm_data, + FREQ_HZ_TO_MHZ(ndata->new_rate)); + mutex_unlock(&sdpm_data->sdpm_mutex); + return NOTIFY_DONE; + case ABORT_RATE_CHANGE: + mutex_lock(&sdpm_data->sdpm_mutex); + if (ndata->new_rate > ndata->old_rate) + sdpm_csr_write(sdpm_data, + FREQ_HZ_TO_MHZ(ndata->old_rate)); + mutex_unlock(&sdpm_data->sdpm_mutex); + return NOTIFY_DONE; + default: + return NOTIFY_DONE; + } +} + +static int sdpm_clk_device_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + int ret = 0, idx = 0, clk_ct = 0, csr = 0, csr_ct = 0; + struct sdpm_clk_instance *sdpm_clk; + struct device_node *dev_node = dev->of_node; + struct resource *res; + + sdpm_clk = devm_kzalloc(dev, sizeof(*sdpm_clk), GFP_KERNEL); + if (!sdpm_clk) + return -ENOMEM; + sdpm_clk->dev = dev; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + dev_err(dev, "Couldn't get MEM resource\n"); + return -EINVAL; + } + dev_dbg(dev, "sdpm@0x%x size:%d\n", res->start, + resource_size(res)); + dev_set_drvdata(dev, sdpm_clk); + + sdpm_clk->regmap = devm_ioremap_resource(dev, res); + if (!sdpm_clk->regmap) { + dev_err(dev, "Couldn't get regmap\n"); + return -EINVAL; + } + + ret = of_property_count_strings(dev_node, "clock-names"); + if (ret < 0) { + dev_err(dev, "Couldn't get clock names. %d\n", ret); + return ret; + } + clk_ct = ret; + ret = of_property_count_u32_elems(dev_node, "csr-id"); + if (ret <= 0) { + dev_err(dev, "Couldn't get csr ID array. %d\n", ret); + return ret; + } + csr_ct = ret; + if (clk_ct != csr_ct) { + dev_err(dev, "Invalid csr:%d and clk:%d count.\n", csr_ct, + clk_ct); + return -EINVAL; + } + sdpm_clk->clk_ct = clk_ct; + sdpm_clk->clk_data = devm_kcalloc(dev, clk_ct, + sizeof(*sdpm_clk->clk_data), GFP_KERNEL); + if (!sdpm_clk->clk_data) + return -ENOMEM; + + for (idx = 0; idx < sdpm_clk->clk_ct; idx++) { + ret = of_property_read_string_index(dev_node, "clock-names", + idx, &sdpm_clk->clk_data[idx].clock_name); + if (ret < 0) { + dev_err(dev, "Couldn't get clk name index:%d. %d\n", + idx, ret); + return ret; + } + + sdpm_clk->clk_data[idx].clk = devm_clk_get(dev, + sdpm_clk->clk_data[idx].clock_name); + if (IS_ERR(sdpm_clk->clk_data[idx].clk)) + return PTR_ERR(sdpm_clk->clk_data[idx].clk); + + ret = of_property_read_u32_index(dev_node, "csr-id", idx, &csr); + if (ret < 0) { + dev_err(dev, "Couldn't get CSR for index:%d. %d\n", + idx, ret); + return ret; + } + if (ret > CSR_MAX_VAL) { + dev_err(dev, "Invalid CSR %d\n", csr); + return -EINVAL; + } + dev_dbg(dev, "SDPM clock:%s csr:%d initialized\n", + sdpm_clk->clk_data[idx].clock_name, csr); + sdpm_clk->clk_data[idx].csr_id = csr; + sdpm_clk->clk_data[idx].sdpm_inst = sdpm_clk; + sdpm_clk->clk_data[idx].clk_rate_nb.notifier_call = + sdpm_clock_notifier; + sdpm_clk->clk_data[idx].last_freq = FREQ_HZ_TO_MHZ( + clk_get_rate(sdpm_clk->clk_data[idx].clk)); + sdpm_clk->clk_data[idx].reg_enable = 1; + sdpm_clk->clk_data[idx].reg = NULL; + sdpm_csr_write(&sdpm_clk->clk_data[idx], + sdpm_clk->clk_data[idx].last_freq); + mutex_init(&sdpm_clk->clk_data[idx].sdpm_mutex); + clk_notifier_register(sdpm_clk->clk_data[idx].clk, + &sdpm_clk->clk_data[idx].clk_rate_nb); + sdpm_clk->clk_data[idx].reg = devm_regulator_get(dev, + sdpm_clk->clk_data[idx].clock_name); + if (IS_ERR(sdpm_clk->clk_data[idx].reg)) { + dev_err(dev, "regulator:%s get err:%d\n", + sdpm_clk->clk_data[idx].clock_name, + PTR_ERR(sdpm_clk->clk_data[idx].reg)); + if (PTR_ERR(sdpm_clk->clk_data[idx].reg) + == -EPROBE_DEFER) + return PTR_ERR(sdpm_clk->clk_data[idx].reg); + } else { + sdpm_clk->clk_data[idx].reg_nb.notifier_call = + sdpm_reg_notifier; + regulator_register_notifier( + sdpm_clk->clk_data[idx].reg, + &sdpm_clk->clk_data[idx].reg_nb); + } + } + + return 0; +} + +static int sdpm_clk_device_remove(struct platform_device *pdev) +{ + struct sdpm_clk_instance *sdpm_clk = + (struct sdpm_clk_instance *)dev_get_drvdata(&pdev->dev); + int idx = 0; + + for (idx = 0; idx < sdpm_clk->clk_ct; idx++) { + clk_notifier_unregister(sdpm_clk->clk_data[idx].clk, + &sdpm_clk->clk_data[idx].clk_rate_nb); + if (!sdpm_clk->clk_data[idx].reg) + continue; + regulator_unregister_notifier(sdpm_clk->clk_data[idx].reg, + &sdpm_clk->clk_data[idx].reg_nb); + } + + return 0; +} + +static const struct of_device_id sdpm_clk_device_match[] = { + {.compatible = "qcom,sdpm"}, + {} +}; + +static struct platform_driver sdpm_clk_device_driver = { + .probe = sdpm_clk_device_probe, + .remove = sdpm_clk_device_remove, + .driver = { + .name = SDPM_DRIVER, + .of_match_table = sdpm_clk_device_match, + }, +}; + +module_platform_driver(sdpm_clk_device_driver); +MODULE_LICENSE("GPL");