diff options
Diffstat (limited to 'drivers/devfreq/armbw-pm.c')
-rw-r--r-- | drivers/devfreq/armbw-pm.c | 466 |
1 files changed, 466 insertions, 0 deletions
diff --git a/drivers/devfreq/armbw-pm.c b/drivers/devfreq/armbw-pm.c new file mode 100644 index 000000000000..b5c05953eda3 --- /dev/null +++ b/drivers/devfreq/armbw-pm.c @@ -0,0 +1,466 @@ +/* + * Copyright (c) 2014, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that 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. + */ + +#define pr_fmt(fmt) "armbw-pm: " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/errno.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/of.h> +#include <linux/of_irq.h> +#include <linux/slab.h> +#include <linux/irq.h> +#include <linux/cpu_pm.h> +#include <linux/cpu.h> +#include "governor.h" +#include "governor_bw_hwmon.h" + + +#define DEFINE_CP15_READ(name, op1, n, m, op2) \ +static u32 read_##name(void) \ +{ \ + u32 val; \ + asm volatile ("mrc p15, " #op1 ", %0, c" #n ", c" #m ", " #op2 \ + : "=r" (val)); \ + return val; \ +} + +#define DEFINE_CP15_WRITE(name, op1, n, m, op2) \ +static void write_##name(u32 val) \ +{ \ + asm volatile ("mcr p15, " #op1 ", %0, c" #n ", c" #m", "#op2 \ + : : "r" (val)); \ +} + +#define DEFINE_CP15_RW(name, op1, n, m, op2) \ +DEFINE_CP15_READ(name, op1, n, m, op2) \ +DEFINE_CP15_WRITE(name, op1, n, m, op2) + +DEFINE_CP15_WRITE(pmselr, 0, 9, 12, 5) +DEFINE_CP15_WRITE(pmcntenset, 0, 9, 12, 1) +DEFINE_CP15_WRITE(pmcntenclr, 0, 9, 12, 2) +DEFINE_CP15_RW(pmovsr, 0, 9, 12, 3) +DEFINE_CP15_WRITE(pmxevtyper, 0, 9, 13, 1) +DEFINE_CP15_RW(pmxevcntr, 0, 9, 13, 2) +DEFINE_CP15_WRITE(pmintenset, 0, 9, 14, 1) +DEFINE_CP15_WRITE(pmintenclr, 0, 9, 14, 2) +DEFINE_CP15_WRITE(pmcr, 0, 9, 12, 0) + +struct bwmon_data { + int cpu; + u32 saved_evcntr; + unsigned long count; + u32 prev_rw_start_val; + u32 limit; +}; + +static DEFINE_SPINLOCK(bw_lock); +static struct bw_hwmon *globalhw; +static struct work_struct irqwork; +static int bw_irq; +static DEFINE_PER_CPU(struct bwmon_data, gov_data); +static int use_cnt; +static DEFINE_MUTEX(use_lock); +static struct workqueue_struct *bw_wq; +static u32 bytes_per_beat; + +#define RW_NUM 0x19 +#define RW_MON 0 + +static void mon_enable(void *info) +{ + /* Clear previous overflow state for given counter*/ + write_pmovsr(BIT(RW_MON)); + /* Enable event counter n */ + write_pmcntenset(BIT(RW_MON)); +} + +static void mon_disable(void *info) +{ + write_pmcntenclr(BIT(RW_MON)); +} + +static void mon_irq_enable(void *info) +{ + write_pmintenset(BIT(RW_MON)); +} + +static void mon_irq_disable(void *info) +{ + write_pmintenclr(BIT(RW_MON)); +} + +static void mon_set_counter(void *count) +{ + write_pmxevcntr(*(u32 *) count); +} + +static void mon_bw_init(void *evcntrval) +{ + u32 count; + + if (!evcntrval) + count = 0xFFFFFFFF; + else + count = *(u32 *) evcntrval; + + write_pmcr(BIT(0)); + write_pmselr(RW_MON); + write_pmxevtyper(RW_NUM); + write_pmxevcntr(count); +} + +static void percpu_bwirq_enable(void *info) +{ + enable_percpu_irq(bw_irq, IRQ_TYPE_EDGE_RISING); +} + +static void percpu_bwirq_disable(void *info) +{ + disable_percpu_irq(bw_irq); +} + +static irqreturn_t mon_intr_handler(int irq, void *dev_id) +{ + queue_work(bw_wq, &irqwork); + return IRQ_HANDLED; +} + +static void bwmon_work(struct work_struct *work) +{ + update_bw_hwmon(globalhw); +} + +static unsigned int beats_to_mbps(long long beats, unsigned int us) +{ + beats *= USEC_PER_SEC; + beats *= bytes_per_beat; + do_div(beats, us); + beats = DIV_ROUND_UP_ULL(beats, SZ_1M); + + return beats; +} + +static unsigned int mbps_to_beats(unsigned long mbps, unsigned int ms, + unsigned int tolerance_percent) +{ + mbps *= (100 + tolerance_percent) * ms; + mbps /= 100; + mbps = DIV_ROUND_UP(mbps, MSEC_PER_SEC); + mbps = mult_frac(mbps, SZ_1M, bytes_per_beat); + return mbps; +} + +static long mon_get_bw_count(u32 start_val) +{ + u32 overflow, count; + + count = read_pmxevcntr(); + overflow = read_pmovsr(); + if (overflow & BIT(RW_MON)) + return 0xFFFFFFFF - start_val + count; + else + return count - start_val; +} + +static void get_beat_count(void *arg) +{ + int cpu = smp_processor_id(); + struct bwmon_data *data = &per_cpu(gov_data, cpu); + + mon_disable(NULL); + data->count = mon_get_bw_count(data->prev_rw_start_val); +} + +static unsigned long measure_bw_and_set_irq(struct bw_hwmon *hw, + unsigned int tol, unsigned int us) +{ + unsigned long bw = 0; + unsigned long tempbw; + int cpu; + struct bwmon_data *data; + unsigned int sample_ms = hw->df->profile->polling_ms; + + spin_lock(&bw_lock); + on_each_cpu(get_beat_count, NULL, true); + for_each_possible_cpu(cpu) { + data = &per_cpu(gov_data, cpu); + + tempbw = beats_to_mbps(data->count, us); + data->limit = mbps_to_beats(tempbw, sample_ms, tol); + data->prev_rw_start_val = 0xFFFFFFFF - data->limit; + if (cpu_online(cpu)) + smp_call_function_single(cpu, mon_set_counter, + &(data->prev_rw_start_val), true); + bw += tempbw; + data->count = 0; + } + on_each_cpu(mon_enable, NULL, true); + spin_unlock(&bw_lock); + return bw; +} + +static void save_hotplugstate(void) +{ + int cpu = smp_processor_id(); + struct bwmon_data *data; + + data = &per_cpu(gov_data, cpu); + percpu_bwirq_disable(NULL); + mon_disable(NULL); + data->saved_evcntr = read_pmxevcntr(); + data->count = mon_get_bw_count(data->prev_rw_start_val); +} + +static void restore_hotplugstate(void) +{ + int cpu = smp_processor_id(); + u32 count; + struct bwmon_data *data; + + data = &per_cpu(gov_data, cpu); + percpu_bwirq_enable(NULL); + if (data->count != 0) + count = data->saved_evcntr; + else + count = data->prev_rw_start_val = 0xFFFFFFFF - data->limit; + mon_bw_init(&count); + mon_irq_enable(NULL); + mon_enable(NULL); +} + +static void save_pmstate(void) +{ + int cpu = smp_processor_id(); + struct bwmon_data *data; + + data = &per_cpu(gov_data, cpu); + mon_disable(NULL); + data->saved_evcntr = read_pmxevcntr(); +} + +static void restore_pmstate(void) +{ + int cpu = smp_processor_id(); + u32 count; + struct bwmon_data *data; + + data = &per_cpu(gov_data, cpu); + count = data->saved_evcntr; + mon_bw_init(&count); + mon_irq_enable(NULL); + mon_enable(NULL); +} + +static int pm_notif(struct notifier_block *nb, unsigned long action, + void *data) +{ + switch (action) { + case CPU_PM_ENTER: + save_pmstate(); + break; + case CPU_PM_ENTER_FAILED: + case CPU_PM_EXIT: + restore_pmstate(); + break; + } + + return NOTIFY_OK; +} + +static struct notifier_block bwmon_cpu_pm_nb = { + .notifier_call = pm_notif, +}; + +static int hotplug_notif(struct notifier_block *nb, unsigned long action, + void *data) +{ + switch (action) { + case CPU_DYING: + spin_lock(&bw_lock); + save_hotplugstate(); + spin_unlock(&bw_lock); + break; + case CPU_STARTING: + spin_lock(&bw_lock); + restore_hotplugstate(); + spin_unlock(&bw_lock); + break; + } + + return NOTIFY_OK; +} + +static struct notifier_block cpu_hotplug_nb = { + .notifier_call = hotplug_notif, +}; + +static int register_notifier(void) +{ + int ret = 0; + + mutex_lock(&use_lock); + if (use_cnt == 0) { + ret = cpu_pm_register_notifier(&bwmon_cpu_pm_nb); + if (ret) + goto out; + ret = register_cpu_notifier(&cpu_hotplug_nb); + if (ret) { + cpu_pm_unregister_notifier(&bwmon_cpu_pm_nb); + goto out; + } + } + use_cnt++; +out: + mutex_unlock(&use_lock); + return ret; +} + +static void unregister_notifier(void) +{ + mutex_lock(&use_lock); + if (use_cnt == 1) { + unregister_cpu_notifier(&cpu_hotplug_nb); + cpu_pm_unregister_notifier(&bwmon_cpu_pm_nb); + } else if (use_cnt == 0) { + pr_warn("Notifier ref count unbalanced\n"); + goto out; + } + use_cnt--; +out: + mutex_unlock(&use_lock); +} + +static void stop_bw_hwmon(struct bw_hwmon *hw) +{ + unregister_notifier(); + on_each_cpu(mon_disable, NULL, true); + on_each_cpu(mon_irq_disable, NULL, true); + on_each_cpu(percpu_bwirq_disable, NULL, true); + free_percpu_irq(bw_irq, &gov_data); +} + +static int start_bw_hwmon(struct bw_hwmon *hw, unsigned long mbps) +{ + u32 limit; + int cpu; + struct bwmon_data *data; + struct device *dev = hw->df->dev.parent; + int ret; + + ret = request_percpu_irq(bw_irq, mon_intr_handler, + "bw_hwmon", &gov_data); + if (ret) { + dev_err(dev, "Unable to register interrupt handler!\n"); + return ret; + } + + get_online_cpus(); + on_each_cpu(mon_bw_init, NULL, true); + on_each_cpu(mon_disable, NULL, true); + + ret = register_notifier(); + if (ret) { + pr_err("Unable to register notifier\n"); + return ret; + } + + limit = mbps_to_beats(mbps, hw->df->profile->polling_ms, 0); + limit /= num_online_cpus(); + + for_each_possible_cpu(cpu) { + data = &per_cpu(gov_data, cpu); + data->limit = limit; + data->prev_rw_start_val = 0xFFFFFFFF - data->limit; + } + + INIT_WORK(&irqwork, bwmon_work); + + on_each_cpu(percpu_bwirq_enable, NULL, true); + on_each_cpu(mon_irq_enable, NULL, true); + on_each_cpu(mon_enable, NULL, true); + put_online_cpus(); + return 0; +} + +static int armbw_pm_driver_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct bw_hwmon *bw; + int ret; + + bw = devm_kzalloc(dev, sizeof(*bw), GFP_KERNEL); + if (!bw) + return -ENOMEM; + bw->dev = dev; + + bw_irq = platform_get_irq(pdev, 0); + if (bw_irq < 0) { + pr_err("Unable to get IRQ number\n"); + return bw_irq; + } + + ret = of_property_read_u32(dev->of_node, "qcom,bytes-per-beat", + &bytes_per_beat); + + if (ret) { + pr_err("Unable to read bytes per beat\n"); + return ret; + } + + bw->start_hwmon = &start_bw_hwmon; + bw->stop_hwmon = &stop_bw_hwmon; + bw->meas_bw_and_set_irq = &measure_bw_and_set_irq; + globalhw = bw; + + ret = register_bw_hwmon(dev, bw); + if (ret) { + pr_err("CPUBW hwmon registration failed\n"); + return ret; + } + return 0; +} + +static struct of_device_id match_table[] = { + { .compatible = "qcom,armbw-pm" }, + {} +}; + +static struct platform_driver armbw_pm_driver = { + .probe = armbw_pm_driver_probe, + .driver = { + .name = "armbw-pm", + .of_match_table = match_table, + .owner = THIS_MODULE, + }, +}; + +static int __init armbw_pm_init(void) +{ + bw_wq = alloc_workqueue("armbw-pm-bwmon", WQ_HIGHPRI, 2); + return platform_driver_register(&armbw_pm_driver); +} +module_init(armbw_pm_init); + +static void __exit armbw_pm_exit(void) +{ + platform_driver_unregister(&armbw_pm_driver); + destroy_workqueue(bw_wq); +} +module_exit(armbw_pm_exit); |