diff options
author | Ionela Voinescu <ionela.voinescu@arm.com> | 2017-03-27 15:13:28 +0100 |
---|---|---|
committer | Ryan Harkin <ryan.harkin@linaro.org> | 2017-03-31 08:45:17 +0100 |
commit | c592512b09c039eb2e315b70e9f1667c8f9dcfa8 (patch) | |
tree | 5966f44e0736e23107cd7a593b41d0bc6759d582 | |
parent | be118de0829d88e70ded8919c2739a3f3112375d (diff) |
PM / devfreq: arm: add devfreq driver for ARM DSU cache portion controldsu_partial_powerdown_support_v1.0-20170331-001dsu_partial_powerdown_support_v1.0
The cache in the ARM DynamIQ Shared Unit (DSU) supports partial power down
by splitting the cache into an implementation-specific number of portions.
The DSU portion control DevFreq driver provides an energy-cost justified
demand-driven policy for controlling the number of portions enabled.
The driver also provides a specific DevFreq governor that will implement
the desired energy-cost-justification policy. This policy maps the number
of portions enabled to frequency (1 portion == 1Hz).
The driver does not control the clock frequency of the cache.
This version of the code is not suitable for use with the simple
ondemand governor.
Signed-off-by: Ionela Voinescu <ionela.voinescu@arm.com>
-rw-r--r-- | drivers/devfreq/Kconfig | 15 | ||||
-rw-r--r-- | drivers/devfreq/Makefile | 1 | ||||
-rw-r--r-- | drivers/devfreq/dsu-pctrl-devfreq.c | 849 |
3 files changed, 865 insertions, 0 deletions
diff --git a/drivers/devfreq/Kconfig b/drivers/devfreq/Kconfig index 64281bb2f650..f1b4a69c98a6 100644 --- a/drivers/devfreq/Kconfig +++ b/drivers/devfreq/Kconfig @@ -98,6 +98,21 @@ config ARM_TEGRA_DEVFREQ It reads ACTMON counters of memory controllers and adjusts the operating frequencies and voltages with OPP support. +config ARM64_DSU_PCTRL_DEVFREQ + tristate "ARM DSU Cache Portion Control DEVFREQ Driver" + depends on ARM64 + select PM_OPP + help + This adds the DevFreq driver for energy-cost-justified demand-driven + portion control of the ARM DSU cache. + This driver does not control frequency. + + It uses architecture specific instructions which might cause + fault when some ARM64 platform does not support it. + Make sure this driver works with your ARM64 platform. + + If unsure, say N. + source "drivers/devfreq/event/Kconfig" endif # PM_DEVFREQ diff --git a/drivers/devfreq/Makefile b/drivers/devfreq/Makefile index 5134f9ee983d..509844a5d83a 100644 --- a/drivers/devfreq/Makefile +++ b/drivers/devfreq/Makefile @@ -9,6 +9,7 @@ obj-$(CONFIG_DEVFREQ_GOV_USERSPACE) += governor_userspace.o obj-$(CONFIG_ARM_EXYNOS4_BUS_DEVFREQ) += exynos/ obj-$(CONFIG_ARM_EXYNOS5_BUS_DEVFREQ) += exynos/ obj-$(CONFIG_ARM_TEGRA_DEVFREQ) += tegra-devfreq.o +obj-$(CONFIG_ARM64_DSU_PCTRL_DEVFREQ) += dsu-pctrl-devfreq.o # DEVFREQ Event Drivers obj-$(CONFIG_PM_DEVFREQ_EVENT) += event/ diff --git a/drivers/devfreq/dsu-pctrl-devfreq.c b/drivers/devfreq/dsu-pctrl-devfreq.c new file mode 100644 index 000000000000..217bf360f41e --- /dev/null +++ b/drivers/devfreq/dsu-pctrl-devfreq.c @@ -0,0 +1,849 @@ +/* + * A devfreq driver for ARM DynamIQ Shared Unit (DSU) CPU memory power + * managemnt. + * + * Copyright (c) 2015-2017 ARM Ltd. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Sean McGoogan <Sean.McGoogan@arm.com> + * Lukasz Luba <lukasz.luba@arm.com> + * Ionela Voinescu <Ionela.Voinescu@arm.com> + * David Guillen Fandos, ARM Ltd. + * + * The cache in DSU supports partial power down by splitting the cache into an + * implementation-specific number of portions. This driver provides an + * energy-cost justified demand-driven policy for controlling the number of + * portions enabled. + * + * The policy maps the number of portions enabled to frequency (1 portion == + * 1Hz) and provides total/busy time statistics such that + * busy_time = (total_time * hit_count) / access_count. + * + * This also provides a specific DevFreq governor that will implement the + * desired energy-cost-justification policy as this version is not suitable + * for use with the simple_ondemand governor. + * DevFreq min/max/governor controls work as usual. + * + * + * There is no relation to actual frequency control of the cache device. + */ + +#include <linux/devfreq.h> +#include <linux/hrtimer.h> +#include <linux/ktime.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/pm_opp.h> +#include <linux/slab.h> +#include <linux/workqueue.h> + +#include "governor.h" + +#define CREATE_TRACE_POINTS +#include <trace/events/dsu_pctrl.h> + +#define DSU_PCTRL_PLATFORM_DEVICE_NAME "dsu_pctrl" +#define DSU_PCTRL_GOVERNOR_NAME "dsu_pc_governor" + +#define DSU_PCTRL_DEFAULT_POLLING_MS 10 +#define POLLING_DOWN_INTERVAL 10 +/* Required polling time in miliseconds */ +#define DSU_PCTRL_DEFAULT_REQUIRED_GOV_POLLING 10000 + +/* Default cache line size */ +#define DSU_PCTRL_DEFAULT_LINE_SIZE 64 + +/* Default size of the cache: 1MB */ +#define DSU_PCTRL_DEFAULT_SIZE_KB 1024 + +/* Default static energy leaked by the cache per MB */ +#define DSU_PCTRL_DEFAULT_CACHE_LEAKAGE 10000 + +/* + * Amount of energy used by the DRAM system per MB + * of transferred data (on average) expressed in uJ/MB + */ +#define DSU_PCTRL_DEFAULT_DRAM_ENERGY_PER_MB 130 + +#define SZ_1KB (1ULL << 10) +#define SZ_1MB (1ULL << 20) +#define ROUNDING_INTEGER_MUL SZ_1MB +#define PERCENT_MAX 100 + +/* + * Portion threshold to use for down-sizing (Td) + * between 0 and 100 + * 90 for 90% + */ +#define DOWNSIZE_PORTION_THRESHOLD 90 + +/* + * Portion threshold to use for up-sizing (Tu) + * between 0 and 100 + * 90 for 90% + */ +#define UPSIZE_PORTION_THRESHOLD 90 + +/* Bit-field positions for the CLUSTERPWRCTLR_EL1 register */ +#define PORTION_1 4 +#define PORTION_2 5 +#define PORTION_3 6 +#define PORTION_4 7 +/* Bit-masks for the CLUSTERPWRCTLR_EL1 register */ +#define PORTION_BITS ((1UL << PORTION_1) | \ + (1UL << PORTION_2) | \ + (1UL << PORTION_3) | \ + (1UL << PORTION_4)) +#define PORTION_MASK (~(PORTION_BITS)) + +#if defined(CONFIG_ARM64) +/* + * Read the system register 'sysreg' (using MRS), and then + * explicitly clear it, by writing zero to it (using MSR). + * Put the value read into 'result'. + */ +#define SYS_REG_READ_THEN_CLEAR(sysreg, result) \ +do { \ + __asm__ __volatile__( \ + "mrs %0, " #sysreg "\n\t" \ + "msr " #sysreg ", XZR" \ + : "=r" (result) /* only one output */ \ + /* no inputs */ \ + /* no clobbers */ \ + ); \ +} while (0) + +/* + * Read the system register 'sysreg' (using MRS). + * Put the value read into 'result'. + */ +#define SYS_REG_READ(sysreg, result) \ +do { \ + __asm__ __volatile__( \ + "mrs %0, " #sysreg \ + : "=r" (result) /* only one output */ \ + /* no inputs */ \ + /* no clobbers */ \ + ); \ +} while (0) + +/* + * Write 'value' to the system register 'sysreg' (using MSR). + */ +#define SYS_REG_WRITE(sysreg, value) \ +do { \ + __asm__ __volatile__( \ + "msr " #sysreg ", %0" \ + : /* no outputs */ \ + : "r" (value) /* only one input */ \ + /* no clobbers */ \ + ); \ +} while (0) + +#else + +#define SYS_REG_READ_THEN_CLEAR(sysreg, result) do {} while (0) +#define SYS_REG_READ(sysreg, result) do {} while (0) +#define SYS_REG_READ_THEN_CLEAR(sysreg, result) do {} while (0) +#define SYS_REG_WRITE(sysreg, result) do {} while (0) + +#endif + +enum arm_dsu_version { + ARM_DSU_R0 = 0, +}; + +struct dsu_pctrl_data { + u32 portion_min; + u32 portion_max; +}; + +struct dsu_pctrl { + int id; + struct dsu_pctrl_data *dsu_data; + struct devfreq *devfreq; + struct platform_device *pdev; + struct devfreq_dev_profile *devfreq_profile; + u32 portions; + u32 size; + u32 line_size; + u32 up_polling_ms; + u32 down_polling_ms; + u32 initial_freq; + u32 static_leakage_per_mb; + u32 dram_energy_per_mb; + + unsigned int *freq_table; + int freq_table_len; + + struct workqueue_struct *update_wq; + struct work_struct update_handle; + struct mutex lock; + struct hrtimer poll_timer; + + /* Leakage (static power) for a single portion (in uW) */ + unsigned long static_leakage; + unsigned long downsize_threshold; + unsigned long upsize_threshold; + unsigned long max_threshold; + unsigned long cur_num_portions; + + /* Contains state for the algorithm. It is clean during resume */ + struct { + unsigned long accesses_up; + unsigned long misses_up; + unsigned long accesses_down; + unsigned long misses_down; + unsigned long usec_up; + unsigned long usec_down; + unsigned int last_update; + } alg; +}; + +static atomic_t dsu_pctrl_device_id = ATOMIC_INIT(0); + +static const struct dsu_pctrl_data device_data[] = { + {.portion_min = 1, .portion_max = 4}, +}; + +static const struct of_device_id dsu_pctrl_devfreq_id[] = { + {.compatible = "arm,dsu_pctrl_r0", .data = + (void *)&device_data[ARM_DSU_R0]}, + {} +}; + +static int dsu_pctrl_set_active_portions(struct device *dev, + unsigned long portions) +{ + struct dsu_pctrl *dsu = dev_get_drvdata(dev); + struct dsu_pctrl_data *data = dsu->dsu_data; + unsigned long portion_active; + unsigned long portion_control; + + if (portions < data->portion_min || portions > data->portion_max) { + dev_warn(dev, "%s: Target of %lu-portions is " + "outside range of %u..%u\n", __func__, portions, + data->portion_min, data->portion_max); + + return -EINVAL; + } + + /* + * Set the number of portions in the DSU to portions + * + * portions Set of bit-fields to Enable + * --------- --------------------------- + * 4 PORTION_1|PORTION_2|PORTION_3|PORTION_4 + * 3 PORTION_1|PORTION_2|PORTION_3 + * 2 PORTION_1|PORTION_2 + * 1 PORTION_1 + * 0 <none> + */ + portion_active = ((1UL << portions) - 1) << PORTION_1; + + SYS_REG_READ(S3_0_c15_c3_5, portion_control); + + portion_control &= PORTION_MASK; + portion_control |= portion_active; + + SYS_REG_WRITE(S3_0_c15_c3_5, portion_control); + + /* Update current number of portions */ + dsu->cur_num_portions = portions; + + return 0; +} + +static int dsu_pctrl_devfreq_target(struct device *dev, + unsigned long *portions, u32 flags) +{ + /* Set requested portions */ + return dsu_pctrl_set_active_portions(dev, *portions); +} + +static int dsu_pctrl_devfreq_get_dev_status(struct device *dev, + struct devfreq_dev_status *stat) +{ + struct dsu_pctrl *dsu = dev_get_drvdata(dev); + unsigned int const usec = ktime_to_us(ktime_get()); + unsigned int delta; + unsigned long hits = 0; + unsigned long misses = 0; + unsigned long accesses; + + delta = usec - dsu->alg.last_update; + dsu->alg.last_update = usec; + + stat->current_frequency = dsu->cur_num_portions; + + SYS_REG_READ_THEN_CLEAR(S3_0_c15_c4_5, hits); + SYS_REG_READ_THEN_CLEAR(S3_0_c15_c4_6, misses); + + accesses = hits + misses; + + dsu->alg.accesses_up += accesses; + dsu->alg.accesses_down += accesses; + dsu->alg.misses_up += misses; + dsu->alg.misses_down += misses; + dsu->alg.usec_up += delta; + dsu->alg.usec_down += delta; + + if (!accesses) + accesses = 1; + + /* + * Although this is not yet suitable for use with the simple ondemand + * governor we'll fill these usage statistics. + */ + stat->total_time = delta; + stat->busy_time = stat->total_time * hits / accesses; + + trace_dsu_pctrl_dev_status(dsu->id, hits, misses, + dsu->cur_num_portions, stat->busy_time, + stat->total_time); + + return 0; +} + +static int dsu_pctrl_up_size_check(struct dsu_pctrl *dsu) +{ + struct dsu_pctrl_data *data = dsu->dsu_data; + unsigned long cache_miss_bw; + int ret = 0; + + /* + * If we are at the maximum number of active portions and we know + * there won't be a downsize attempt due to polling time constraints, + * we can skip calculations that won't lead to any action. + */ + if ((dsu->cur_num_portions >= data->portion_max) && + (dsu->alg.usec_down < dsu->down_polling_ms)) + goto cleanup; + + cache_miss_bw = dsu->line_size * dsu->alg.misses_up; + cache_miss_bw *= ROUNDING_INTEGER_MUL; + cache_miss_bw /= dsu->alg.usec_up; + + if (cache_miss_bw > dsu->upsize_threshold) + ret = 1; + +cleanup: + dsu->alg.usec_up = 0; + dsu->alg.misses_up = 0; + dsu->alg.accesses_up = 0; + + return ret; +} + +static int dsu_pctrl_down_size_check(struct dsu_pctrl *dsu) +{ + struct dsu_pctrl_data *data = dsu->dsu_data; + unsigned long cache_hit_bw; + int ret = 0; + + if (dsu->cur_num_portions <= data->portion_min) + goto cleanup; + + cache_hit_bw = dsu->alg.accesses_down - dsu->alg.misses_down; + cache_hit_bw *= dsu->line_size; + cache_hit_bw *= ROUNDING_INTEGER_MUL; + cache_hit_bw /= dsu->alg.usec_down; + + if (cache_hit_bw < (dsu->max_threshold * dsu->cur_num_portions - + dsu->downsize_threshold)) + ret = 1; + +cleanup: + dsu->alg.usec_down = 0; + dsu->alg.misses_down = 0; + dsu->alg.accesses_down = 0; + + return ret; +} + +static unsigned long dsu_pctrl_calc_next_portions(struct dsu_pctrl *dsu) +{ + unsigned long portions = dsu->cur_num_portions; + int up = 0, down = 0; + + up = dsu_pctrl_up_size_check(dsu); + if (up > 0) + return dsu->cur_num_portions + up; + + if (dsu->alg.usec_down >= dsu->down_polling_ms) { + down = dsu_pctrl_down_size_check(dsu); + if (down > 0) + portions = dsu->cur_num_portions - down; + } + + return portions; +} + +static int dsu_pctrl_governor_get_target_portions(struct devfreq *df, + unsigned long *portions) +{ + struct dsu_pctrl *dsu = dev_get_drvdata(df->dev.parent); + int err; + + err = devfreq_update_stats(df); + if (err) + return err; + + *portions = dsu_pctrl_calc_next_portions(dsu); + + return 0; +} + +static int dsu_pctrl_governor_event_handler(struct devfreq *devfreq, + unsigned int event, void *data) +{ + int ret = 0; + + switch (event) { + case DEVFREQ_GOV_START: + devfreq_monitor_start(devfreq); + break; + + case DEVFREQ_GOV_STOP: + devfreq_monitor_stop(devfreq); + break; + + case DEVFREQ_GOV_SUSPEND: + devfreq_monitor_suspend(devfreq); + break; + + case DEVFREQ_GOV_RESUME: + devfreq_monitor_resume(devfreq); + break; + + case DEVFREQ_GOV_INTERVAL: + devfreq_interval_update(devfreq, (unsigned int *)data); + break; + } + + return ret; +} + +static struct devfreq_governor dsu_pctrl_devfreq_governor = { + .name = DSU_PCTRL_GOVERNOR_NAME, + .get_target_freq = dsu_pctrl_governor_get_target_portions, + .event_handler = dsu_pctrl_governor_event_handler, +}; + +static void dsu_pctrl_handle_update(struct work_struct *work) +{ + struct dsu_pctrl *dsu = container_of(work, struct dsu_pctrl, + update_handle); + + mutex_lock(&dsu->devfreq->lock); + update_devfreq(dsu->devfreq); + mutex_unlock(&dsu->devfreq->lock); +} + +static enum hrtimer_restart dsu_pctrl_polling(struct hrtimer *hrtimer) +{ + struct dsu_pctrl *dsu = container_of(hrtimer, struct dsu_pctrl, + poll_timer); + + queue_work(dsu->update_wq, &dsu->update_handle); + + hrtimer_forward_now(&dsu->poll_timer, + ms_to_ktime(dsu->up_polling_ms)); + return HRTIMER_RESTART; +} + +static int dsu_pctrl_reinit_device(struct device *dev) +{ + struct dsu_pctrl *dsu = dev_get_drvdata(dev); + + /* Clean the algorithm statistics and start from scrach */ + memset(&dsu->alg, 0, sizeof(dsu->alg)); + dsu->alg.last_update = ktime_to_us(ktime_get()); + + return dsu_pctrl_set_active_portions(dev, dsu->initial_freq); +} + +static int dsu_pctrl_setup_devfreq_profile(struct platform_device *pdev) +{ + struct dsu_pctrl *dsu = platform_get_drvdata(pdev); + struct devfreq_dev_profile *df_profile; + + dsu->devfreq_profile = devm_kzalloc(&pdev->dev, + sizeof(struct devfreq_dev_profile), GFP_KERNEL); + if (IS_ERR(dsu->devfreq_profile)) { + dev_dbg(&pdev->dev, "no memory.\n"); + return PTR_ERR(dsu->devfreq_profile); + } + + df_profile = dsu->devfreq_profile; + + df_profile->target = dsu_pctrl_devfreq_target; + df_profile->get_dev_status = dsu_pctrl_devfreq_get_dev_status; + df_profile->freq_table = dsu->freq_table; + df_profile->max_state = dsu->freq_table_len; + df_profile->polling_ms = DSU_PCTRL_DEFAULT_REQUIRED_GOV_POLLING; + df_profile->initial_freq = dsu->initial_freq; + + return 0; +} + +static int dsu_pctrl_parse_dt(struct platform_device *pdev) +{ + const struct of_device_id *of_id; + struct device_node *node = pdev->dev.of_node; + struct dsu_pctrl *dsu = platform_get_drvdata(pdev); + int ret = 0; + + of_node_get(node); + + of_id = of_match_node(dsu_pctrl_devfreq_id, node); + dsu->dsu_data = (struct dsu_pctrl_data *)of_id->data; + + ret = of_property_read_u32(node, "static-leakage-per-mb", + &dsu->static_leakage_per_mb); + if (ret) + dsu->static_leakage_per_mb = DSU_PCTRL_DEFAULT_CACHE_LEAKAGE; + + ret = of_property_read_u32(node, "dram-energy-per-mb", + &dsu->dram_energy_per_mb); + if (ret) + dsu->dram_energy_per_mb = DSU_PCTRL_DEFAULT_DRAM_ENERGY_PER_MB; + + ret = of_property_read_u32(node, "size", &dsu->size); + if (ret) + dsu->size = DSU_PCTRL_DEFAULT_SIZE_KB; + + ret = of_property_read_u32(node, "line-size", &dsu->line_size); + if (ret) + dsu->line_size = DSU_PCTRL_DEFAULT_LINE_SIZE; + + ret = of_property_read_u32(node, "polling", &dsu->up_polling_ms); + if (ret) + dsu->up_polling_ms = DSU_PCTRL_DEFAULT_POLLING_MS; + dsu->down_polling_ms = POLLING_DOWN_INTERVAL * dsu->up_polling_ms; + + of_node_put(node); + + return 0; +} + +static int dsu_pctrl_create_configuration(struct platform_device *pdev) +{ + struct dsu_pctrl *dsu = platform_get_drvdata(pdev); + struct dsu_pctrl_data *data = dsu->dsu_data; + int i; + unsigned long portion; + + dsu->initial_freq = data->portion_max; + dsu->cur_num_portions = dsu->initial_freq; + + dsu->freq_table_len = data->portion_max - data->portion_min + 1; + dsu->freq_table = devm_kcalloc(&pdev->dev, dsu->freq_table_len, + sizeof(*dsu->freq_table), + GFP_KERNEL); + if (IS_ERR(dsu->freq_table)) + return -ENOMEM; + + portion = data->portion_min; + for (i = 0; portion <= data->portion_max; i++) + dsu->freq_table[i] = portion++; + + /* Leakage (static power) for a single portion (in uW) */ + dsu->static_leakage = dsu->static_leakage_per_mb * dsu->size; + dsu->static_leakage /= SZ_1KB; + dsu->static_leakage /= data->portion_max; + + /* Downsize and upsize thresholds are given in percentages */ + dsu->downsize_threshold = DOWNSIZE_PORTION_THRESHOLD; + dsu->downsize_threshold *= ROUNDING_INTEGER_MUL / PERCENT_MAX; + dsu->downsize_threshold *= dsu->static_leakage; + dsu->downsize_threshold /= dsu->dram_energy_per_mb; + + dsu->upsize_threshold = PERCENT_MAX - UPSIZE_PORTION_THRESHOLD; + dsu->upsize_threshold *= ROUNDING_INTEGER_MUL / PERCENT_MAX; + dsu->upsize_threshold *= dsu->static_leakage; + dsu->upsize_threshold /= dsu->dram_energy_per_mb; + + dsu->max_threshold = ROUNDING_INTEGER_MUL; + dsu->max_threshold *= dsu->static_leakage; + dsu->max_threshold /= dsu->dram_energy_per_mb; + + return 0; +} + +static void dsu_pctrl_remove_opps(struct platform_device *pdev) +{ + struct dev_pm_opp *opp; + int i, count; + unsigned long freq; + + count = dev_pm_opp_get_opp_count(&pdev->dev); + if (count <= 0) + return; + + rcu_read_lock(); + for (i = 0, freq = 0; i < count; i++, freq++) { + opp = dev_pm_opp_find_freq_ceil(&pdev->dev, &freq); + if (!IS_ERR(opp)) + dev_pm_opp_remove(&pdev->dev, freq); + } + rcu_read_unlock(); +} + +static int dsu_pctrl_enable_opps(struct platform_device *pdev) +{ + struct dsu_pctrl *dsu = platform_get_drvdata(pdev); + int i, ret; + int opp_count = 0; + + if (dsu->freq_table_len <= 0) + return -EINVAL; + + for (i = 0; i < dsu->freq_table_len; i++) { + ret = dev_pm_opp_add(&pdev->dev, dsu->freq_table[i], + dsu->freq_table[i]); + if (ret) + dev_warn(&pdev->dev, "cannot add a new OPP.\n"); + else + opp_count++; + } + + if (opp_count == 0) { + dev_err(&pdev->dev, "device has no OPP registered.\n"); + return -ENODEV; + } + + return 0; +} + +static int dsu_pctrl_setup(struct platform_device *pdev) +{ + struct dsu_pctrl *dsu = platform_get_drvdata(pdev); + struct dsu_pctrl_data *data = dsu->dsu_data; + int ret = 0; + + dsu->update_wq = create_workqueue("arm_dsu_pctrl_wq"); + if (IS_ERR(dsu->update_wq)) { + dev_err(&pdev->dev, "cannot create workqueue.\n"); + return PTR_ERR(dsu->update_wq); + } + INIT_WORK(&dsu->update_handle, dsu_pctrl_handle_update); + + mutex_init(&dsu->lock); + + ret = dsu_pctrl_create_configuration(pdev); + if (ret) { + dev_err(&pdev->dev, "cannot create frequency table.\n"); + return ret; + } + + ret = dsu_pctrl_enable_opps(pdev); + if (ret) { + dev_dbg(&pdev->dev, "device setup failed.\n"); + return ret; + } + + ret = dsu_pctrl_setup_devfreq_profile(pdev); + if (ret) { + dev_dbg(&pdev->dev, "device setup failed.\n"); + return ret; + } + + dsu->alg.last_update = ktime_to_us(ktime_get()); + + dsu->devfreq = devm_devfreq_add_device(&pdev->dev, + dsu->devfreq_profile, + DSU_PCTRL_GOVERNOR_NAME, NULL); + + if (IS_ERR(dsu->devfreq)) { + dev_err(&pdev->dev, "registering to devfreq failed.\n"); + return PTR_ERR(dsu->devfreq); + } + + mutex_lock(&dsu->devfreq->lock); + dsu->devfreq->min_freq = data->portion_min; + dsu->devfreq->max_freq = data->portion_max; + mutex_unlock(&dsu->devfreq->lock); + + return 0; +} + + +static int dsu_pctrl_init_device(struct platform_device *pdev) +{ + struct dsu_pctrl *dsu = platform_get_drvdata(pdev); + + dsu_pctrl_reinit_device(&pdev->dev); + + hrtimer_init(&dsu->poll_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + dsu->poll_timer.function = dsu_pctrl_polling; + hrtimer_start(&dsu->poll_timer, ms_to_ktime(dsu->up_polling_ms), + HRTIMER_MODE_REL); + + return 0; +} + +static int dsu_pctrl_suspend(struct device *dev) +{ + struct dsu_pctrl *dsu = dev_get_drvdata(dev); + struct dsu_pctrl_data *data = dsu->dsu_data; + int ret = 0; + + if (hrtimer_active(&dsu->poll_timer)) + hrtimer_cancel(&dsu->poll_timer); + + ret = devfreq_suspend_device(dsu->devfreq); + if (ret < 0) { + dev_err(dev, "failed to suspend devfreq device.\n"); + return ret; + } + + /* Set the number of active portions to minimum during suspend */ + ret = dsu_pctrl_set_active_portions(dev, data->portion_min); + if (ret < 0) { + dev_err(dev, "failed to set portions to minimum.\n"); + return ret; + } + + return ret; +} + +static int dsu_pctrl_resume(struct device *dev) +{ + + struct dsu_pctrl *dsu = dev_get_drvdata(dev); + int ret = 0; + + dsu_pctrl_reinit_device(dev); + + ret = devfreq_resume_device(dsu->devfreq); + if (ret < 0) + dev_err(dev, "failed to resume devfreq device.\n"); + + if (!hrtimer_active(&dsu->poll_timer)) + hrtimer_start(&dsu->poll_timer, ms_to_ktime(dsu->up_polling_ms), + HRTIMER_MODE_REL); + + return ret; +} + +static SIMPLE_DEV_PM_OPS(dsu_pctrl_pm, dsu_pctrl_suspend, dsu_pctrl_resume); + +static int dsu_pctrl_devfreq_probe(struct platform_device *pdev) +{ + struct dsu_pctrl *dsu; + int ret = 0; + + dev_info(&pdev->dev, "registering DSU portion control device.\n"); + + dsu = devm_kzalloc(&pdev->dev, sizeof(*dsu), GFP_KERNEL); + if (!dsu) + return -ENOMEM; + + platform_set_drvdata(pdev, dsu); + dsu->pdev = pdev; + dsu->id = atomic_inc_return(&dsu_pctrl_device_id); + + ret = dsu_pctrl_parse_dt(pdev); + if (ret) + goto failed; + + ret = dsu_pctrl_setup(pdev); + if (ret) + goto failed; + + ret = dsu_pctrl_init_device(pdev); + if (ret) + goto failed; + + return 0; +failed: + dev_err(&pdev->dev, "failed to register driver, err %d.\n", ret); + kfree(dsu); + return ret; +} + +static int dsu_pctrl_devfreq_remove(struct platform_device *pdev) +{ + struct dsu_pctrl *dsu = platform_get_drvdata(pdev); + int ret = 0; + + dev_info(&pdev->dev, "unregistering DSU portion control device.\n"); + + ret = dsu_pctrl_set_active_portions(&pdev->dev, dsu->initial_freq); + + /* Cancel hrtimer */ + if (hrtimer_active(&dsu->poll_timer)) + hrtimer_cancel(&dsu->poll_timer); + /* Wait for pending work */ + flush_workqueue(dsu->update_wq); + /* Destroy workqueue */ + destroy_workqueue(dsu->update_wq); + + devm_devfreq_remove_device(&pdev->dev, dsu->devfreq); + dsu_pctrl_remove_opps(pdev); + + return ret; +} + +MODULE_DEVICE_TABLE(of, dsu_pctrl_devfreq_id); + +static struct platform_driver dsu_pctrl_devfreq_driver = { + .probe = dsu_pctrl_devfreq_probe, + .remove = dsu_pctrl_devfreq_remove, + .driver = { + .name = DSU_PCTRL_PLATFORM_DEVICE_NAME, + .of_match_table = dsu_pctrl_devfreq_id, + .pm = &dsu_pctrl_pm, + .owner = THIS_MODULE, + }, +}; + +static int __init dsu_pctrl_devfreq_init(void) +{ + int ret = 0; + + ret = devfreq_add_governor(&dsu_pctrl_devfreq_governor); + if (ret) { + pr_err("%s: failed to add governor: %d.\n", __func__, ret); + return ret; + } + + ret = platform_driver_register(&dsu_pctrl_devfreq_driver); + if (ret) + devfreq_remove_governor(&dsu_pctrl_devfreq_governor); + + return ret; +} + +static void __exit dsu_pctrl_devfreq_exit(void) +{ + int ret; + + ret = devfreq_remove_governor(&dsu_pctrl_devfreq_governor); + if (ret) + pr_err("%s: failed to remove governor: %d.\n", __func__, ret); + + platform_driver_unregister(&dsu_pctrl_devfreq_driver); +} + +module_init(dsu_pctrl_devfreq_init) +module_exit(dsu_pctrl_devfreq_exit) + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ARM DSU PCTRL devfreq driver"); +MODULE_AUTHOR("ARM Ltd."); +MODULE_VERSION("1.0"); |