| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (c) 2019 Samsung Electronics Co., Ltd. |
| * http://www.samsung.com/ |
| * Author: Sylwester Nawrocki <s.nawrocki@samsung.com> |
| * |
| * Samsung Exynos SoC Adaptive Supply Voltage support |
| */ |
| |
| #include <linux/cpu.h> |
| #include <linux/device.h> |
| #include <linux/errno.h> |
| #include <linux/init.h> |
| #include <linux/mfd/syscon.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/of_device.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm_opp.h> |
| #include <linux/regmap.h> |
| #include <linux/soc/samsung/exynos-chipid.h> |
| |
| #include "exynos-asv.h" |
| #include "exynos5422-asv.h" |
| |
| #define MHZ 1000000U |
| |
| static int exynos_asv_update_cpu_opps(struct exynos_asv *asv, |
| struct device *cpu) |
| { |
| struct exynos_asv_subsys *subsys = NULL; |
| struct dev_pm_opp *opp; |
| unsigned int opp_freq; |
| int i; |
| |
| for (i = 0; i < ARRAY_SIZE(asv->subsys); i++) { |
| if (of_device_is_compatible(cpu->of_node, |
| asv->subsys[i].cpu_dt_compat)) { |
| subsys = &asv->subsys[i]; |
| break; |
| } |
| } |
| if (!subsys) |
| return -EINVAL; |
| |
| for (i = 0; i < subsys->table.num_rows; i++) { |
| unsigned int new_volt, volt; |
| int ret; |
| |
| opp_freq = exynos_asv_opp_get_frequency(subsys, i); |
| |
| opp = dev_pm_opp_find_freq_exact(cpu, opp_freq * MHZ, true); |
| if (IS_ERR(opp)) { |
| dev_info(asv->dev, "cpu%d opp%d, freq: %u missing\n", |
| cpu->id, i, opp_freq); |
| |
| continue; |
| } |
| |
| volt = dev_pm_opp_get_voltage(opp); |
| new_volt = asv->opp_get_voltage(subsys, i, volt); |
| dev_pm_opp_put(opp); |
| |
| if (new_volt == volt) |
| continue; |
| |
| ret = dev_pm_opp_adjust_voltage(cpu, opp_freq * MHZ, |
| new_volt, new_volt, new_volt); |
| if (ret < 0) |
| dev_err(asv->dev, |
| "Failed to adjust OPP %u Hz/%u uV for cpu%d\n", |
| opp_freq, new_volt, cpu->id); |
| else |
| dev_dbg(asv->dev, |
| "Adjusted OPP %u Hz/%u -> %u uV, cpu%d\n", |
| opp_freq, volt, new_volt, cpu->id); |
| } |
| |
| return 0; |
| } |
| |
| static int exynos_asv_update_opps(struct exynos_asv *asv) |
| { |
| struct opp_table *last_opp_table = NULL; |
| struct device *cpu; |
| int ret, cpuid; |
| |
| for_each_possible_cpu(cpuid) { |
| struct opp_table *opp_table; |
| |
| cpu = get_cpu_device(cpuid); |
| if (!cpu) |
| continue; |
| |
| opp_table = dev_pm_opp_get_opp_table(cpu); |
| if (IS_ERR_OR_NULL(opp_table)) |
| continue; |
| |
| if (!last_opp_table || opp_table != last_opp_table) { |
| last_opp_table = opp_table; |
| |
| ret = exynos_asv_update_cpu_opps(asv, cpu); |
| if (ret < 0) |
| dev_err(asv->dev, "Couldn't udate OPPs for cpu%d\n", |
| cpuid); |
| } |
| |
| dev_pm_opp_put_opp_table(opp_table); |
| } |
| |
| return 0; |
| } |
| |
| static int exynos_asv_probe(struct platform_device *pdev) |
| { |
| int (*probe_func)(struct exynos_asv *asv); |
| struct exynos_asv *asv; |
| struct device *cpu_dev; |
| u32 product_id = 0; |
| int ret, i; |
| |
| cpu_dev = get_cpu_device(0); |
| ret = dev_pm_opp_get_opp_count(cpu_dev); |
| if (ret < 0) |
| return -EPROBE_DEFER; |
| |
| asv = devm_kzalloc(&pdev->dev, sizeof(*asv), GFP_KERNEL); |
| if (!asv) |
| return -ENOMEM; |
| |
| asv->chipid_regmap = device_node_to_regmap(pdev->dev.of_node); |
| if (IS_ERR(asv->chipid_regmap)) { |
| dev_err(&pdev->dev, "Could not find syscon regmap\n"); |
| return PTR_ERR(asv->chipid_regmap); |
| } |
| |
| regmap_read(asv->chipid_regmap, EXYNOS_CHIPID_REG_PRO_ID, &product_id); |
| |
| switch (product_id & EXYNOS_MASK) { |
| case 0xE5422000: |
| probe_func = exynos5422_asv_init; |
| break; |
| default: |
| return -ENODEV; |
| } |
| |
| ret = of_property_read_u32(pdev->dev.of_node, "samsung,asv-bin", |
| &asv->of_bin); |
| if (ret < 0) |
| asv->of_bin = -EINVAL; |
| |
| asv->dev = &pdev->dev; |
| dev_set_drvdata(&pdev->dev, asv); |
| |
| for (i = 0; i < ARRAY_SIZE(asv->subsys); i++) |
| asv->subsys[i].asv = asv; |
| |
| ret = probe_func(asv); |
| if (ret < 0) |
| return ret; |
| |
| return exynos_asv_update_opps(asv); |
| } |
| |
| static const struct of_device_id exynos_asv_of_device_ids[] = { |
| { .compatible = "samsung,exynos4210-chipid" }, |
| {} |
| }; |
| |
| static struct platform_driver exynos_asv_driver = { |
| .driver = { |
| .name = "exynos-asv", |
| .of_match_table = exynos_asv_of_device_ids, |
| }, |
| .probe = exynos_asv_probe, |
| }; |
| module_platform_driver(exynos_asv_driver); |