| /* |
| * common function for clock framework source file |
| * |
| * Copyright (C) 2012 Marvell |
| * Chao Xie <xiechao.mail@gmail.com> |
| * Zhoujie Wu <zjwu@marvell.com> |
| * Lu Cao <lucao@marvell.com> |
| * |
| * This file is licensed under the terms of the GNU General Public |
| * License version 2. This program is licensed "as is" without any |
| * warranty of any kind, whether express or implied. |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/kernel.h> |
| #include <soc/asr/asrdcstat.h> |
| #include <linux/clk/dvfs-dvc.h> |
| #include <soc/asr/debugfs-asr.h> |
| #include <linux/gpio.h> |
| //#include <linux/miscled-rgb.h> |
| #include <linux/mfd/88pm80x.h> |
| #include "clk.h" |
| #include <dt-bindings/power/asr-power.h> |
| |
| struct vol_op_dcstat_info { |
| u64 time; /*ms*/ |
| u32 vol; /* mV */ |
| }; |
| |
| struct vol_dc_stat_info { |
| bool stat_start; |
| struct vol_op_dcstat_info ops_dcstat[MAX_PMIC_LEVEL]; |
| u32 vc_count[MAX_PMIC_LEVEL][MAX_PMIC_LEVEL]; /* [from][to] */ |
| u32 vc_total_count; |
| ktime_t breakdown_start; |
| ktime_t prev_ts; |
| u32 cur_lvl; |
| u64 total_time; /* ms */ |
| }; |
| |
| static DEFINE_SPINLOCK(vol_lock); |
| static struct vol_dc_stat_info vol_dcstat; |
| static int vol_ledstatus_start; |
| |
| static ktime_t vol_dcstat_time(void); |
| |
| /* |
| * Get platform specified dvc low power modes. Platform power file |
| * should define an array contains these modes with the same sequence |
| * in hwdvc_rails. |
| */ |
| unsigned int hwdvc_lpm[AP_COMP_MAX] = {POWER_MODE_MAX, POWER_MODE_CORE_POWERDOWN, |
| POWER_MODE_APPS_IDLE, POWER_MODE_SYS_SLEEP}; |
| |
| static u32 voltage_lvl[AP_COMP_MAX]; |
| /* Platform supported maximum lpms */ |
| u32 max_lpm = POWER_MODE_MAX; |
| |
| static u32 find_hwdvc_voltage_lvl(u32 lpm) |
| { |
| u32 hwlvl; |
| int i; |
| |
| for (i = 0; i < AP_COMP_MAX; i++) |
| if (hwdvc_lpm[i] == lpm) { |
| hwlvl = voltage_lvl[i]; |
| break; |
| } |
| |
| /* Incorrect lpm? Set AP_ACTIVE as default */ |
| if (i == AP_COMP_MAX) { |
| hwlvl = voltage_lvl[AP_ACTIVE]; |
| } |
| |
| return hwlvl; |
| } |
| |
| static void vol_ledstatus_show(u32 lpm) |
| { |
| u32 hwlvl = find_hwdvc_voltage_lvl(lpm); |
| |
| switch (hwlvl) { |
| #if 0 |
| case VL3: |
| /* VL3 : LED RG */ |
| led_rgb_output(LED_R, 1); |
| led_rgb_output(LED_G, 1); |
| led_rgb_output(LED_B, 0); |
| break; |
| case VL2: |
| /* VL2 : LED R */ |
| led_rgb_output(LED_R, 1); |
| led_rgb_output(LED_G, 0); |
| led_rgb_output(LED_B, 0); |
| break; |
| case VL1: |
| /* VL1 : LED G */ |
| led_rgb_output(LED_R, 0); |
| led_rgb_output(LED_G, 1); |
| led_rgb_output(LED_B, 0); |
| break; |
| case VL0: |
| /* VL0 : LED B */ |
| led_rgb_output(LED_R, 0); |
| led_rgb_output(LED_G, 0); |
| led_rgb_output(LED_B, 1); |
| break; |
| case -1: |
| /* lpm : LED -- */ |
| led_rgb_output(LED_R, 0); |
| led_rgb_output(LED_G, 0); |
| led_rgb_output(LED_B, 0); |
| break; |
| #endif |
| default: |
| break; |
| } |
| } |
| |
| void vol_ledstatus_event(u32 lpm) |
| { |
| spin_lock(&vol_lock); |
| if (!vol_ledstatus_start) |
| goto out; |
| vol_ledstatus_show(lpm); |
| out: |
| spin_unlock(&vol_lock); |
| } |
| |
| static ssize_t vol_led_read(struct file *filp, char __user *buffer, |
| size_t count, loff_t *ppos) |
| { |
| char *buf; |
| ssize_t ret, size = 2 * PAGE_SIZE - 1; |
| u32 len = 0; |
| |
| buf = (char *)__get_free_pages(GFP_NOIO, get_order(size)); |
| if (!buf) |
| return -ENOMEM; |
| |
| len += snprintf(buf + len, size - len, |
| "Help information :\n"); |
| len += snprintf(buf + len, size - len, |
| "echo 1 to start voltage led status:\n"); |
| len += snprintf(buf + len, size - len, |
| "echo 0 to stop voltage led status:\n"); |
| len += snprintf(buf + len, size - len, |
| "VLevel3: R, G\n"); |
| len += snprintf(buf + len, size - len, |
| "VLevel2: R\n"); |
| len += snprintf(buf + len, size - len, |
| "VLevel1: G\n"); |
| len += snprintf(buf + len, size - len, |
| "VLevel0: B\n"); |
| len += snprintf(buf + len, size - len, |
| " lpm: off\n"); |
| |
| ret = simple_read_from_buffer(buffer, count, ppos, buf, len); |
| free_pages((unsigned long)buf, get_order(size)); |
| return ret; |
| } |
| |
| static ssize_t vol_led_write(struct file *filp, const char __user *buffer, |
| size_t count, loff_t *ppos) |
| { |
| unsigned int start; |
| char buf[10] = { 0 }; |
| |
| if (copy_from_user(buf, buffer, count)) |
| return -EFAULT; |
| sscanf(buf, "%d", &start); |
| start = !!start; |
| |
| if (vol_ledstatus_start == start) { |
| pr_err("[WARNING]Voltage led status is already %s\n", |
| vol_dcstat.stat_start ? "started" : "stopped"); |
| return -EINVAL; |
| } |
| |
| spin_lock(&vol_lock); |
| vol_ledstatus_start = start; |
| #if 0 |
| if (vol_ledstatus_start) |
| vol_ledstatus_show(MAX_LPM_INDEX); |
| else { |
| led_rgb_output(LED_R, 0); |
| led_rgb_output(LED_G, 0); |
| led_rgb_output(LED_B, 0); |
| } |
| #endif |
| spin_unlock(&vol_lock); |
| |
| return count; |
| } |
| |
| static const struct file_operations vol_led_ops = { |
| .owner = THIS_MODULE, |
| .read = vol_led_read, |
| .write = vol_led_write, |
| }; |
| |
| static void vol_dcstat_update(u32 lpm) |
| { |
| ktime_t cur_ts; |
| u32 hwlvl; |
| u64 time_us; |
| |
| hwlvl = find_hwdvc_voltage_lvl(lpm); |
| if (vol_dcstat.cur_lvl == hwlvl) |
| return; |
| |
| /* update voltage change times */ |
| vol_dcstat.vc_count[vol_dcstat.cur_lvl][hwlvl]++; |
| |
| /* update voltage dc statistics */ |
| cur_ts = vol_dcstat_time(); |
| time_us = ktime_to_us(ktime_sub(cur_ts, vol_dcstat.prev_ts)); |
| vol_dcstat.ops_dcstat[vol_dcstat.cur_lvl].time += time_us; |
| vol_dcstat.prev_ts = cur_ts; |
| vol_dcstat.cur_lvl = hwlvl; |
| } |
| |
| void vol_dcstat_event(u32 lpm) |
| { |
| spin_lock(&vol_lock); |
| if (!vol_dcstat.stat_start) |
| goto out; |
| vol_dcstat_update(lpm); |
| out: |
| spin_unlock(&vol_lock); |
| } |
| |
| static ktime_t vol_dcstat_time(void) |
| { |
| ktime_t ktime_temp; |
| |
| /* flag for if timekeeping is suspended */ |
| if (unlikely(timekeeping_suspended)) |
| ktime_temp = ns_to_ktime(suspended_time); |
| else |
| ktime_temp = ktime_get(); |
| |
| return ktime_temp; |
| } |
| |
| static ssize_t vol_dc_read(struct file *filp, char __user *buffer, |
| size_t count, loff_t *ppos) |
| { |
| char *buf; |
| ssize_t ret, size = 2 * PAGE_SIZE - 1; |
| u32 i, j, dc_int = 0, dc_fra = 0, len = 0; |
| |
| buf = (char *)__get_free_pages(GFP_NOIO, get_order(size)); |
| if (!buf) |
| return -ENOMEM; |
| |
| if (!vol_dcstat.total_time) { |
| len += snprintf(buf + len, size - len, |
| "No stat information! "); |
| len += snprintf(buf + len, size - len, |
| "Help information :\n"); |
| len += snprintf(buf + len, size - len, |
| "1. echo 1 to start duty cycle stat:\n"); |
| len += snprintf(buf + len, size - len, |
| "2. echo 0 to stop duty cycle stat:\n"); |
| len += snprintf(buf + len, size - len, |
| "3. cat to check duty cycle info from start to stop:\n\n"); |
| goto out; |
| } |
| |
| if (vol_dcstat.stat_start) { |
| len += snprintf(buf + len, size - len, |
| "Please stop the vol duty cycle stats at first\n"); |
| goto out; |
| } |
| |
| len += snprintf(buf + len, size - len, |
| "Total time:%8llums (%6llus)\n", |
| div64_u64(vol_dcstat.total_time, (u64)(1000)), |
| div64_u64(vol_dcstat.total_time, (u64)(1000000))); |
| len += snprintf(buf + len, size - len, |
| "|Level|Vol(mV)|Time(ms)| %%|\n"); |
| for (i = VL0; i < MAX_PMIC_LEVEL; i++) { |
| dc_int = calculate_dc(vol_dcstat.ops_dcstat[i].time, |
| vol_dcstat.total_time, &dc_fra); |
| len += snprintf(buf + len, size - len, |
| "| VL_%2d|%7u|%8llu|%3u.%02u%%|\n", |
| i, vol_dcstat.ops_dcstat[i].vol, |
| div64_u64(vol_dcstat.ops_dcstat[i].time, |
| (u64)(1000)), |
| dc_int, dc_fra); |
| } |
| |
| /* show voltage-change times */ |
| len += snprintf(buf + len, size - len, |
| "\nTotal voltage-change times:%8u", |
| vol_dcstat.vc_total_count); |
| len += snprintf(buf + len, size - len, "\n|from\\to|"); |
| for (j = VL0; j < MAX_PMIC_LEVEL; j++) |
| len += snprintf(buf + len, size - len, " Level%d|", j); |
| for (i = VL0; i < MAX_PMIC_LEVEL; i++) { |
| len += snprintf(buf + len, size - len, "\n| Level%2d|", i); |
| for (j = VL0; j < MAX_PMIC_LEVEL; j++) |
| if (i == j) |
| len += snprintf(buf + len, size - len, |
| " --- |"); |
| else |
| /* [from][to] */ |
| len += snprintf(buf + len, size - len, |
| "%7u|", |
| vol_dcstat.vc_count[i][j]); |
| } |
| len += snprintf(buf + len, size - len, "\n"); |
| out: |
| ret = simple_read_from_buffer(buffer, count, ppos, buf, len); |
| free_pages((unsigned long)buf, get_order(size)); |
| return ret; |
| } |
| |
| static ssize_t vol_dc_write(struct file *filp, const char __user *buffer, |
| size_t count, loff_t *ppos) |
| { |
| unsigned int start, i, j; |
| char buf[10] = { 0 }; |
| ktime_t cur_ts; |
| u64 time_us; |
| struct device_node __maybe_unused *np = |
| of_find_node_by_name(NULL, "pxa1928_apmu_ver"); |
| |
| if (copy_from_user(buf, buffer, count)) |
| return -EFAULT; |
| sscanf(buf, "%d", &start); |
| start = !!start; |
| |
| if (vol_dcstat.stat_start == start) { |
| pr_err("[WARNING]Voltage duty-cycle statistics is already %s\n", |
| vol_dcstat.stat_start ? "started" : "stopped"); |
| return -EINVAL; |
| } |
| vol_dcstat.stat_start = start; |
| if (vol_dcstat.stat_start && np) |
| for (i = VL0; i < MAX_PMIC_LEVEL; i++) |
| pm8xx_dvc_getvolt(PM800_ID_BUCK1, i, |
| &vol_dcstat.ops_dcstat[i].vol); |
| |
| spin_lock(&vol_lock); |
| cur_ts = ktime_get(); |
| if (vol_dcstat.stat_start) { |
| for (i = VL0; i < MAX_PMIC_LEVEL; i++) { |
| vol_dcstat.ops_dcstat[i].time = 0; |
| for (j = VL0; j < MAX_PMIC_LEVEL; j++) |
| vol_dcstat.vc_count[i][j] = 0; |
| } |
| |
| vol_dcstat.prev_ts = cur_ts; |
| vol_dcstat.breakdown_start = cur_ts; |
| vol_dcstat.cur_lvl = voltage_lvl[AP_ACTIVE]; |
| vol_dcstat.total_time = -1UL; |
| vol_dcstat.vc_total_count = 0; |
| } else { |
| time_us = ktime_to_us(ktime_sub(cur_ts, vol_dcstat.prev_ts)); |
| vol_dcstat.ops_dcstat[vol_dcstat.cur_lvl].time += time_us; |
| vol_dcstat.total_time = ktime_to_us(ktime_sub(cur_ts, |
| vol_dcstat.breakdown_start)); |
| for (i = VL0; i < MAX_PMIC_LEVEL; i++) |
| for (j = VL0; j < MAX_PMIC_LEVEL; j++) |
| vol_dcstat.vc_total_count += |
| vol_dcstat.vc_count[i][j]; |
| } |
| spin_unlock(&vol_lock); |
| return count; |
| } |
| |
| static const struct file_operations vol_dc_ops = { |
| .owner = THIS_MODULE, |
| .read = vol_dc_read, |
| .write = vol_dc_write, |
| }; |
| |
| static int hwdvc_stat_notifier_handler(struct notifier_block *nb, |
| unsigned long rails, void *data) |
| { |
| struct hwdvc_notifier_data *vl; |
| |
| if (rails >= AP_COMP_MAX) |
| return NOTIFY_OK; |
| |
| spin_lock(&vol_lock); |
| vl = (struct hwdvc_notifier_data *)(data); |
| voltage_lvl[rails] = vl->newlv; |
| |
| if (!vol_dcstat.stat_start) |
| goto out_dc; |
| if (rails == AP_ACTIVE) |
| vol_dcstat_update(max_lpm); |
| out_dc: |
| if (!vol_ledstatus_start) |
| goto out_led; |
| if (rails == AP_ACTIVE) |
| vol_ledstatus_show(max_lpm); |
| out_led: |
| spin_unlock(&vol_lock); |
| return NOTIFY_OK; |
| } |
| |
| static struct notifier_block hwdvc_stat_ntf = { |
| .notifier_call = hwdvc_stat_notifier_handler, |
| }; |
| |
| static int __init hwdvc_stat_init(void) |
| { |
| struct dentry *dvfs_node, *volt_dc_stat, *volt_led; |
| u32 idx; |
| #ifdef CONFIG_ARM64 |
| int dvc_map[MAX_PMIC_LEVEL]; |
| int cnt; |
| #endif |
| struct device_node __maybe_unused *np = |
| of_find_node_by_name(NULL, "pxa1928_apmu_ver"); |
| #ifndef CONFIG_ARM64 |
| struct dvc_plat_info platinfo; |
| /* record voltage lvl when init */ |
| if (dvfs_get_dvcplatinfo(&platinfo)) |
| return -ENOENT; |
| |
| for (idx = VL0; idx < MAX_PMIC_LEVEL; idx++) |
| vol_dcstat.ops_dcstat[idx].vol = |
| platinfo.millivolts[idx]; |
| hwdvc_notifier_register(&hwdvc_stat_ntf); |
| #else |
| if (np) { |
| get_voltage_table_for_cp(dvc_map, &cnt); |
| for (idx = VL0; idx < MAX_PMIC_LEVEL; idx++) |
| vol_dcstat.ops_dcstat[idx].vol = |
| dvc_map[idx]; |
| hwdvc_notify_register(&hwdvc_stat_ntf); |
| } |
| #endif |
| |
| dvfs_node = debugfs_create_dir("vlstat", asr_debug_entry); |
| if (!dvfs_node) |
| return -ENOENT; |
| |
| volt_dc_stat = debugfs_create_file("vol_dc_stat", 0444, |
| dvfs_node, NULL, &vol_dc_ops); |
| if (!volt_dc_stat) |
| goto err_1; |
| |
| volt_led = debugfs_create_file("vol_led", 0644, |
| dvfs_node, NULL, &vol_led_ops); |
| if (!volt_led) |
| goto err_volt_led; |
| |
| return 0; |
| |
| err_volt_led: |
| debugfs_remove(volt_dc_stat); |
| err_1: |
| debugfs_remove(dvfs_node); |
| return -ENOENT; |
| } |
| arch_initcall(hwdvc_stat_init); |