PM / AVS: exynos-avs: Add Adaptive Voltage Scaling (AVS) device driver 50/111950/11
authorWook Song <wook16.song@samsung.com>
Tue, 24 Jan 2017 08:10:54 +0000 (17:10 +0900)
committerWook Song <wook16.song@samsung.com>
Mon, 17 Jul 2017 08:58:59 +0000 (17:58 +0900)
In Exynos SoC, Adaptive Supply Voltage (ASV) is a technique, which
provides sets of voltage values optimized to the SoC revision variations
for given operating frequencies of devices such as CPU, GPU, and BUS.
For each given operating frequency of the device, there is an ASV group
number, which indicates the SoC revision variaion. By using the ASV
group number, it is possible to identify which one is the most suitable
set of voltages. In general, the ASV group numbers are decided at the
manufactoring time and we can get these numbers by reading specific
memory addresses mapped to a special read-only region.

This patch adds a dt-based common device driver for ASV support in
Exynos SoC. Since the ASV technique can be also viewed as a Adaptive
Voltage Scaling (AVS) feature, we let this device driver be included in
drivers/power/avs directory. This common device driver supports basic
operations to read the ASV group number and replace the OPP table of the
device with the optimized one by using the ASV group number.

In the current version of the AVS common device driver, instead of
supporting all the resource types, CPU-related resource types are
supported. It will be extended to support other types of resources such
as GPU and BUS.

For the specific SoC support by using this common device driver, this
patch also adds a device driver for Exynos5433 SoC. In the case of
Exynos5433 SoC, there are four vesions of the OPP tables for each
resource. Since each OPP table contains 16 sets of voltages for 16 ASV
groups, 64 sets of operating points are required to only support the
Little cluster. Therefore, in the current implementation, this
Exynos5433-specific device driver only supports the optimized OPP table
for the ASV group number of 8 in version 2 of the OPP table (which is
the latest version). It will be also extended to supprot all of the 16
ASV groups in the OPP table version 2 soon.

Change-Id: Ie4d7e810a42c1cbcad7c6af9a77cce0964e104bb
Signed-off-by: Wook Song <wook16.song@samsung.com>
Signed-off-by: Chanwoo Choi <cw00.choi@samsung.com>
drivers/power/avs/Kconfig
drivers/power/avs/Makefile
drivers/power/avs/exynos-avs.c [new file with mode: 0644]

index a67eeac..aba14a0 100644 (file)
@@ -11,6 +11,16 @@ menuconfig POWER_AVS
 
          Say Y here to enable Adaptive Voltage Scaling class support.
 
+config EXYNOS_AVS
+        tristate "Exynos Adaptive Voltage Scaling support"
+       depends on ARCH_EXYNOS && ARM_DT_BL_CPUFREQ
+       depends on PM_OPP && OF && POWER_AVS
+        help
+          Say y here to enable support Adaptive Voltage Scaling (AVS) on Exynos
+         SoCs. By using this feature, the voltage which is optimized according
+         to the SoC variation can be used for each given operating frequency of
+         a device.
+
 config ROCKCHIP_IODOMAIN
         tristate "Rockchip IO domain support"
         depends on POWER_AVS && ARCH_ROCKCHIP && OF
index ba4c7bc..14cca9f 100644 (file)
@@ -1,2 +1,3 @@
+obj-$(CONFIG_EXYNOS_AVS)               += exynos-avs.o
 obj-$(CONFIG_POWER_AVS_OMAP)           += smartreflex.o
 obj-$(CONFIG_ROCKCHIP_IODOMAIN)                += rockchip-io-domain.o
diff --git a/drivers/power/avs/exynos-avs.c b/drivers/power/avs/exynos-avs.c
new file mode 100644 (file)
index 0000000..632ce79
--- /dev/null
@@ -0,0 +1,574 @@
+/*
+ * Samsung Exynos Adaptive Voltage Scaling
+ *
+ * Copyright (c) 2017 Samsung Electronics Co., Ltd.
+ * Wook Song <wook16.song@samsung.com>
+ * Chanwoo Choi <cw00.choi@samsung.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <linux/cpufreq.h>
+#include <linux/module.h>
+#include <linux/mfd/syscon.h>
+#include <linux/of_device.h>
+#include <linux/pm_opp.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/slab.h>
+
+#define EXYNOS_AVS_5433_BASE_ADDR_OFFSET_AVS_INFO              0x4000
+#define EXYNOS_AVS_5433_TBL_VER_OFFSET_FROM_AVS_INFO           0xc
+#define EXYNOS_AVS_5433_FUSED_FLAG_OFFSET_FROM_AVS_INFO                0xc
+#define EXYNOS_AVS_5433_REGMAP_INDEX_CHIPID                    0
+#define EXYNOS_AVS_5433_SUBGRP0_MIN_FREQ_CPU_LITTLE            1200000000
+#define EXYNOS_AVS_5433_SUBGRP1_MIN_FREQ_CPU_LITTLE            700000000
+#define EXYNOS_AVS_5433_SUBGRP0_MIN_FREQ_CPU_BIG               1700000000
+#define EXYNOS_AVS_5433_SUBGRP1_MIN_FREQ_CPU_BIG               1200000000
+#define EXYNOS_AVS_5433_SUPPORT_AVS_GROUP                      8
+#define EXYNOS_AVS_5433_SUPPORT_TABLE_VER_MAX                  8
+#define EXYNOS_AVS_5433_SUPPORT_TABLE_VER_MIN                  6
+
+enum exynos_avs_type {
+       TYPE_CPU_LITTLE = 0,
+       TYPE_CPU_BIG,
+};
+
+struct exynos_avs_desc {
+       int (*init_avs)(struct device *dev);
+       int (*cpu_notifier)(struct notifier_block *nb,
+                       unsigned long val, void *data);
+};
+
+struct exynos_avs_group {
+       unsigned long freq;
+       int group_num;
+       int is_support;
+};
+
+struct exynos_avs_drv {
+       enum exynos_avs_type type;
+       struct device_node *resource_np;
+       struct device *dev;
+       int table_ver;
+       int is_fused_avs_group;
+       struct regulator *regulator;
+       unsigned long u_volt_max;
+       unsigned long u_volt_min;
+       const struct exynos_avs_desc *desc;
+       struct regmap *chipid_regmap;
+       struct notifier_block cpu_nb;
+       struct exynos_avs_group *avs_group_table;
+};
+
+static inline struct device *get_first_cpu_device_of_cluster(unsigned int cpu)
+{
+       return get_cpu_device(cpumask_first(topology_core_cpumask(cpu)));
+}
+
+/*
+ * exynos_avs_free_opp_table() - free OPP table of the target device
+ * @to:                a pointer of the target device
+ *
+ * Helper function to free the OPP table of the target device. If any errors do
+ * not ocuur, the number of OPPs of the target device should be zero.
+ *
+ * Note: this function is only for operating-points v1.
+ */
+static void exynos_avs_free_opp_table(struct device *to)
+{
+       struct dev_pm_opp *opp;
+       unsigned long freq;
+       int i, nr_opp;
+
+       nr_opp = dev_pm_opp_get_opp_count(to);
+
+       for (i = 0, freq = 0; i < nr_opp; i++, freq++) {
+               rcu_read_lock();
+               opp = dev_pm_opp_find_freq_ceil(to, &freq);
+               rcu_read_unlock();
+               if (IS_ERR(opp)) {
+                       dev_err(to, "%s: Failed to find OPP for %lu khz\n",
+                                       __func__, freq);
+                       continue;
+               }
+               dev_pm_opp_remove(to, freq);
+       }
+}
+
+/*
+ * exynos_avs_update_opp_table() - update OPP table of the target device to that
+ * of the AVS device
+ * @dev:       a pointer of the corresponding AVS device
+ * @to:                a pointer of the target device
+ *
+ * Helper function to update OPP table of the target device to that of the AVS
+ * device. In the OPP table of the target device, only OPP which has the
+ * frequency value matching to the frequency value of a OPP in the OPP table of
+ * the AVS device is updated.
+ *
+ * Note: this function only works for operating-points v1.
+ */
+static int exynos_avs_update_opp_table(struct device *dev, struct device *to)
+{
+       struct exynos_avs_drv *drv = dev_get_drvdata(dev);
+       struct dev_pm_opp *opp;
+       struct exynos_avs_group *avs_group;
+       unsigned long freq, u_volt;
+       int nr_opp, i, ret;
+
+       nr_opp = dev_pm_opp_get_opp_count(dev);
+
+       for (i = 0; i < nr_opp; i++) {
+               avs_group = &drv->avs_group_table[i];
+               if (!avs_group->is_support)
+                       continue;
+
+               freq = avs_group->freq;
+
+               rcu_read_lock();
+               opp = dev_pm_opp_find_freq_exact(to, freq, true);
+               rcu_read_unlock();
+               if (IS_ERR(opp))
+                       continue;
+               dev_pm_opp_remove(to, freq);
+
+               rcu_read_lock();
+               opp = dev_pm_opp_find_freq_exact(dev, freq, true);
+               rcu_read_unlock();
+               if (IS_ERR(opp)) {
+                       ret = IS_ERR(opp);
+                       goto out_err;
+               }
+
+               u_volt = dev_pm_opp_get_voltage(opp);
+               if (!u_volt) {
+                       ret = -EINVAL;
+                       goto out_err;
+               }
+               ret = dev_pm_opp_add(to, freq, u_volt);
+               if (ret)
+                       goto out_err;
+       }
+
+       nr_opp = dev_pm_opp_get_opp_count(to);
+       drv->u_volt_max = 0;
+       drv->u_volt_min = ULONG_MAX;
+
+       for (i = 0, freq = 0; i < nr_opp; i++, freq++) {
+               rcu_read_lock();
+               opp = dev_pm_opp_find_freq_ceil(to, &freq);
+               if (IS_ERR(opp)) {
+                       ret = PTR_ERR(opp);
+                       rcu_read_unlock();
+                       goto out_err;
+               }
+
+               u_volt = dev_pm_opp_get_voltage(opp);
+               rcu_read_unlock();
+               if (u_volt > drv->u_volt_max)
+                       drv->u_volt_max = u_volt;
+               if (u_volt < drv->u_volt_min)
+                       drv->u_volt_min = u_volt;
+       }
+
+       return 0;
+
+out_err:
+       exynos_avs_free_opp_table(to);
+       of_init_opp_table(to);
+
+       return ret;
+}
+
+static int exynos_avs_5433_get_table_ver(struct device *dev)
+{
+       struct exynos_avs_drv *drv = dev_get_drvdata(dev);
+       unsigned int val, offset;
+       int ret;
+
+       offset = EXYNOS_AVS_5433_BASE_ADDR_OFFSET_AVS_INFO +
+               EXYNOS_AVS_5433_TBL_VER_OFFSET_FROM_AVS_INFO;
+       ret = regmap_read(drv->chipid_regmap, offset, &val);
+       if (ret < 0)
+               return ret;
+
+       val &= 0xF;
+
+       /*
+        * Note that, for Exynos 5433, there are four versions (that is, 0, 1,
+        * 2, and 3) of the AVS table, and it is determined by the value read
+        * from the specific register. The relations between the value and the
+        * version of AVS table are as follows:
+        *      when the value is 0, version 0 of the tableis used,
+        *      when the value is ranged from 1 to 3, version 1 is used,
+        *      when the value is 5, version 2 is used,
+        *      and when the value is ranged from 6 to 8, version 3 is used.
+        *
+        * Instead of supporting all the versions of the AVS table, this driver
+        * only supports the the case of version 3.
+        */
+       if ((val < EXYNOS_AVS_5433_SUPPORT_TABLE_VER_MIN) ||
+               (val > EXYNOS_AVS_5433_SUPPORT_TABLE_VER_MAX))
+               return -ENODEV;
+
+       return val;
+}
+
+static int exynos_avs_5433_is_fused_avs_group(struct device *dev)
+{
+       struct exynos_avs_drv *drv = dev_get_drvdata(dev);
+       unsigned int offset, val;
+       int ret;
+
+       offset = EXYNOS_AVS_5433_BASE_ADDR_OFFSET_AVS_INFO +
+               EXYNOS_AVS_5433_FUSED_FLAG_OFFSET_FROM_AVS_INFO;
+       ret = regmap_read(drv->chipid_regmap, offset, &val);
+       if (ret < 0)
+               return ret;
+
+       val = (val >> (BITS_PER_BYTE << 1)) & 0x1;
+
+       return val;
+}
+
+static int exynos_avs_5433_get_avs_group(struct device *dev, unsigned long freq)
+{
+       struct exynos_avs_drv *drv = dev_get_drvdata(dev);
+       unsigned int offset, val, val_pos;
+       int subgroup, ret;
+
+       /*
+        * Note that this driver does not support the case that it is required
+        * to read HPM and IDS values for getting AVS group values.
+        */
+       if (!drv->is_fused_avs_group)
+               return -ENODEV;
+
+       offset = EXYNOS_AVS_5433_BASE_ADDR_OFFSET_AVS_INFO;
+       ret = regmap_read(drv->chipid_regmap, offset, &val);
+       if (ret < 0)
+               return ret;
+
+       /*
+        * The AVS subgroup value can normally range from 0 to 2 and it
+        * is determined by the frequency value.
+        */
+       switch (drv->type) {
+       case TYPE_CPU_LITTLE:
+               if (freq >= EXYNOS_AVS_5433_SUBGRP0_MIN_FREQ_CPU_LITTLE)
+                       subgroup = 0;
+               else if (freq >= EXYNOS_AVS_5433_SUBGRP1_MIN_FREQ_CPU_LITTLE)
+                       subgroup = 1;
+               else
+                       subgroup = 2;
+
+               val_pos = 0;
+               break;
+       case TYPE_CPU_BIG:
+               if (freq >= EXYNOS_AVS_5433_SUBGRP0_MIN_FREQ_CPU_BIG)
+                       subgroup = 0;
+               else if (freq >= EXYNOS_AVS_5433_SUBGRP1_MIN_FREQ_CPU_BIG)
+                       subgroup = 1;
+               else
+                       subgroup = 2;
+
+               val_pos = (BITS_PER_BYTE << 1);
+               break;
+       default:
+               return -ENODEV;
+       }
+
+       val >>= (val_pos + (subgroup * (BITS_PER_BYTE >> 1)));
+
+       return val & 0xF;
+}
+
+/*
+ * exynos_avs_5433_verify_opp_cpu() - verify OPP table of the CPU device
+ *
+ * @dev:       a pointer of the corresponding AVS device
+ * @cpu:       a pointer of the target CPU device
+ *
+ * Helper function to verify OPP table of the target CPU device. This function
+ * checks that the regulator of the target CPU device can support all the
+ * voltage values of the OPP table of the device and, for the CPU frequency
+ * driver, updates the transition_latency value using the maximum and minimum
+ * supply voltage values of the target CPU's OPP table.
+ *
+ * Note: this function only works for operating-points v1.
+ */
+static int exynos_avs_5433_verify_opp_cpu(struct device *dev,
+               struct device *cpu)
+{
+       struct exynos_avs_drv *drv;
+       struct device *first_cpu;
+       struct device_node *np;
+       struct cpufreq_policy *policy;
+       struct dev_pm_opp *opp;
+       unsigned int transition_latency;
+       unsigned long freq, u_volt;
+       int i, nr_opp, ret;
+
+       drv = dev_get_drvdata(dev);
+       first_cpu = get_first_cpu_device_of_cluster(cpu->id);
+
+       policy = cpufreq_cpu_get(cpu->id);
+       if (!policy)
+               return -ENODEV;
+
+       np = of_node_get(first_cpu->of_node);
+       if (!np) {
+               ret = -ENODEV;
+               goto out_put_cpu;
+       }
+
+       if (of_property_read_u32(np, "clock-latency", &transition_latency))
+               transition_latency = CPUFREQ_ETERNAL;
+
+       nr_opp = dev_pm_opp_get_opp_count(first_cpu);
+       for (i = 0, freq = 0; i < nr_opp; i++, freq++) {
+               opp = dev_pm_opp_find_freq_ceil(first_cpu, &freq);
+               if (IS_ERR(opp)) {
+                       ret = PTR_ERR(opp);
+                       goto out_put;
+               }
+
+               u_volt = dev_pm_opp_get_voltage(opp);
+               if (!regulator_is_supported_voltage(drv->regulator,
+                                       u_volt, u_volt)) {
+                       ret = -EINVAL;
+                       goto out_put;
+               }
+       }
+
+       /*
+        * The value of transition_latency is basically determined by the value
+        * specified in the 'clock-latency' device tree property of the resource
+        * phandle. If ret is not 0, the transition_latency is increased by the
+        * value of ret. Otherwise, it is not required to do anything.
+        */
+       ret = regulator_set_voltage_time(drv->regulator,
+                       drv->u_volt_min, drv->u_volt_max);
+       if ((ret > 0) && (transition_latency != CPUFREQ_ETERNAL))
+               transition_latency += ret * 1000;
+
+       policy->cpuinfo.transition_latency = transition_latency;
+       ret = 0;
+
+out_put:
+       of_node_put(first_cpu->of_node);
+out_put_cpu:
+       cpufreq_cpu_put(policy);
+       return ret;
+
+}
+
+static int exynos_avs_5433_cpu_notifier(struct notifier_block *nb,
+               unsigned long val, void *data)
+{
+       struct exynos_avs_drv *drv;
+       struct device *cpu_dev;
+       struct device *first_cpu_dev;
+       unsigned int hcpu;
+       int each_cpu, num_hcpus, ret;
+
+       hcpu = (unsigned long) data;
+       drv = container_of(nb, struct exynos_avs_drv, cpu_nb);
+       first_cpu_dev = get_first_cpu_device_of_cluster(hcpu);
+       cpu_dev = get_cpu_device(hcpu);
+
+       if (drv->resource_np != first_cpu_dev->of_node)
+               return NOTIFY_DONE;
+
+       num_hcpus = 0;
+       for_each_cpu(each_cpu, topology_core_cpumask(hcpu)) {
+               if (cpu_online(each_cpu))
+                       num_hcpus++;
+       }
+
+       if (num_hcpus != 1)
+               return NOTIFY_DONE;
+
+       switch (val & ~CPU_TASKS_FROZEN) {
+       case CPU_ONLINE:
+               ret = exynos_avs_update_opp_table(drv->dev, first_cpu_dev);
+               if (ret)
+                       goto out_err;
+               ret = exynos_avs_5433_verify_opp_cpu(drv->dev, cpu_dev);
+               if (ret)
+                       goto out_err;
+               break;
+       case CPU_DOWN_PREPARE:
+               exynos_avs_free_opp_table(first_cpu_dev);
+               break;
+       }
+
+       return NOTIFY_OK;
+
+out_err:
+       exynos_avs_free_opp_table(first_cpu_dev);
+       of_init_opp_table(first_cpu_dev);
+       return NOTIFY_DONE;
+}
+
+static int exynos_avs_5433_init_avs(struct device *dev)
+{
+       struct exynos_avs_drv *drv = dev_get_drvdata(dev);
+       struct dev_pm_opp *opp;
+       struct device *cpu_dev;
+       unsigned long freq;
+       int nr_opp, i, ret;
+
+       drv->chipid_regmap = syscon_regmap_lookup_by_phandle(dev->of_node,
+                       "samsung,chipid-syscon");
+       if (IS_ERR(drv->chipid_regmap))
+               return PTR_ERR(drv->chipid_regmap);
+
+       drv->table_ver = exynos_avs_5433_get_table_ver(dev);
+       if (drv->table_ver < 0)
+               return drv->table_ver;
+
+       drv->is_fused_avs_group = exynos_avs_5433_is_fused_avs_group(dev);
+       if (drv->is_fused_avs_group < 0)
+               return drv->is_fused_avs_group;
+
+       drv->regulator = devm_regulator_get(dev, "vdd");
+       if (IS_ERR(drv->regulator))
+               return PTR_ERR(drv->regulator);
+
+       nr_opp = dev_pm_opp_get_opp_count(dev);
+       drv->avs_group_table = devm_kzalloc(dev,
+                       sizeof(*drv->avs_group_table) * nr_opp, GFP_KERNEL);
+       if (!drv->avs_group_table)
+               return -ENOMEM;
+
+       for (i = 0, freq = 0; i < nr_opp; i++, freq++) {
+               struct exynos_avs_group *avs_group;
+
+               opp = dev_pm_opp_find_freq_ceil(dev, &freq);
+               if (IS_ERR(opp))
+                       return PTR_ERR(opp);
+
+               avs_group = &drv->avs_group_table[i];
+               avs_group->is_support = false;
+               avs_group->freq = freq;
+               avs_group->group_num = exynos_avs_5433_get_avs_group(dev, freq);
+               if (avs_group->group_num == EXYNOS_AVS_5433_SUPPORT_AVS_GROUP)
+                       avs_group->is_support = true;
+       }
+
+       if ((drv->type != TYPE_CPU_LITTLE) && (drv->type != TYPE_CPU_BIG))
+               return -ENODEV;
+
+       for_each_online_cpu(i) {
+               cpu_dev = get_cpu_device(i);
+               if (cpu_dev->of_node == drv->resource_np) {
+                       ret = exynos_avs_update_opp_table(drv->dev, cpu_dev);
+                       if (ret)
+                               goto out_err;
+                       ret = exynos_avs_5433_verify_opp_cpu(drv->dev, cpu_dev);
+                       if (ret)
+                               goto out_err;
+               }
+       }
+
+       return 0;
+
+out_err:
+       exynos_avs_free_opp_table(cpu_dev);
+       of_init_opp_table(cpu_dev);
+
+       return ret;
+}
+
+static const struct exynos_avs_desc exynos_avs_5433_desc = {
+       .init_avs = exynos_avs_5433_init_avs,
+       .cpu_notifier = exynos_avs_5433_cpu_notifier,
+};
+
+static const struct of_device_id exynos_avs_of_match[] = {
+       {
+               .compatible = "samsung,exynos5433-avs",
+               .data = &exynos_avs_5433_desc,
+       },
+       { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, exynos_avs_of_match);
+
+static int exynos_avs_probe(struct platform_device *pdev)
+{
+       struct device *dev;
+       struct device_node *np;
+       struct exynos_avs_drv *drv;
+       const struct of_device_id *match_id;
+       int ret;
+
+       dev = &pdev->dev;
+       np = dev->of_node;
+
+       drv = devm_kzalloc(dev, sizeof(*drv), GFP_KERNEL);
+       if (!drv)
+               return -ENOMEM;
+
+       match_id = of_match_device(exynos_avs_of_match, dev);
+       drv->desc = match_id->data;
+       if (!drv->desc)
+               return -ENODEV;
+
+       drv->resource_np = of_parse_phandle(np, "resource", 0);
+       if (!drv->resource_np)
+               return -ENODEV;
+
+       drv->dev = dev;
+
+       if (of_property_read_bool(np, "samsung,avs-type-cpu-little"))
+               drv->type = TYPE_CPU_LITTLE;
+       else if (of_property_read_bool(np, "samsung,avs-type-cpu-big"))
+               drv->type = TYPE_CPU_BIG;
+       else
+               return -ENODEV;
+
+       ret = of_init_opp_table(dev);
+       if (ret)
+               return ret;
+
+       dev_set_drvdata(dev, drv);
+
+       ret = drv->desc->init_avs(dev);
+       if (ret)
+               return ret;
+
+       drv->cpu_nb.notifier_call = drv->desc->cpu_notifier;
+       drv->cpu_nb.next = NULL;
+       drv->cpu_nb.priority = INT_MIN;
+       ret = register_hotcpu_notifier(&drv->cpu_nb);
+       if (ret)
+               return ret;
+
+       dev_info(dev, "registered Exynos AVS driver\n");
+
+       return ret;
+}
+
+static struct platform_driver exynos_avs_platdrv = {
+       .probe = exynos_avs_probe,
+       .driver = {
+               .of_match_table = exynos_avs_of_match,
+               .name = "exynos-avs",
+       },
+};
+
+static int __init exynos_avs_init(void)
+{
+       return platform_driver_register(&exynos_avs_platdrv);
+}
+late_initcall(exynos_avs_init);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Wook Song <wook16.song@samsung.com>");
+MODULE_AUTHOR("Chanwoo Choi <cw00.choi@samsung.com>");
+MODULE_DESCRIPTION("Samsung Exynos AVS driver");