| /* Copyright (c) 2017, 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) KBUILD_MODNAME ": " fmt |
| |
| #include <linux/kernel.h> |
| #include <linux/of_device.h> |
| #include <linux/platform_device.h> |
| #include <linux/mutex.h> |
| #include <linux/bitmap.h> |
| #include <linux/bitops.h> |
| #include <linux/slab.h> |
| #include <linux/io.h> |
| #include <linux/err.h> |
| #include <linux/hrtimer.h> |
| #include <linux/mfd/syscon.h> |
| #include <linux/regmap.h> |
| #include <linux/soc/qcom/llcc-qcom.h> |
| #include <linux/module.h> |
| #include "llcc_events.h" |
| #include "llcc_perfmon.h" |
| |
| #define LLCC_PERFMON_NAME "llcc_perfmon" |
| #define LLCC_PERFMON_COUNTER_MAX 16 |
| #define MAX_NUMBER_OF_PORTS 8 |
| #define NUM_CHANNELS 16 |
| #define MAX_STRING_SIZE 20 |
| #define DELIM_CHAR " " |
| |
| /** |
| * struct llcc_perfmon_counter_map - llcc perfmon counter map info |
| * @port_sel: Port selected for configured counter |
| * @event_sel: Event selected for configured counter |
| * @counter_dump: Cumulative counter dump |
| */ |
| struct llcc_perfmon_counter_map { |
| unsigned int port_sel; |
| unsigned int event_sel; |
| unsigned long long counter_dump; |
| }; |
| |
| struct llcc_perfmon_private; |
| /** |
| * struct event_port_ops - event port operation |
| * @event_config: Counter config support for port & event |
| * @event_enable: Counter enable support for port |
| * @event_filter_config: Port filter config support |
| */ |
| struct event_port_ops { |
| void (*event_config)(struct llcc_perfmon_private *, |
| unsigned int, unsigned int, bool); |
| void (*event_enable)(struct llcc_perfmon_private *, bool); |
| void (*event_filter_config)(struct llcc_perfmon_private *, |
| enum filter_type, unsigned long, bool); |
| }; |
| |
| /** |
| * struct llcc_perfmon_private - llcc perfmon private |
| * @llcc_map: llcc register address space map |
| * @broadcast_off: Offset of llcc broadcast address space |
| * @bank_off: Offset of llcc banks |
| * @num_banks: Number of banks supported |
| * @port_ops: struct event_port_ops |
| * @configured: Mapping of configured event counters |
| * @configured_counters: |
| * Count of configured counters. |
| * @enables_port: Port enabled for perfmon configuration |
| * @filtered_ports: Port filter enabled |
| * @port_configd: Number of perfmon port configuration supported |
| * @mutex: mutex to protect this structure |
| * @hrtimer: hrtimer instance for timer functionality |
| * @expires: timer expire time in nano seconds |
| */ |
| struct llcc_perfmon_private { |
| struct regmap *llcc_map; |
| unsigned int broadcast_off; |
| unsigned int bank_off[NUM_CHANNELS]; |
| unsigned int num_banks; |
| struct event_port_ops *port_ops[MAX_NUMBER_OF_PORTS]; |
| struct llcc_perfmon_counter_map configured[LLCC_PERFMON_COUNTER_MAX]; |
| unsigned int configured_counters; |
| unsigned int enables_port; |
| unsigned int filtered_ports; |
| unsigned int port_configd; |
| struct mutex mutex; |
| struct hrtimer hrtimer; |
| ktime_t expires; |
| }; |
| |
| static inline void llcc_bcast_write(struct llcc_perfmon_private *llcc_priv, |
| unsigned int offset, uint32_t val) |
| { |
| regmap_write(llcc_priv->llcc_map, llcc_priv->broadcast_off + offset, |
| val); |
| } |
| |
| static inline void llcc_bcast_read(struct llcc_perfmon_private *llcc_priv, |
| unsigned int offset, uint32_t *val) |
| { |
| regmap_read(llcc_priv->llcc_map, llcc_priv->broadcast_off + offset, |
| val); |
| } |
| |
| static void llcc_bcast_modify(struct llcc_perfmon_private *llcc_priv, |
| unsigned int offset, uint32_t val, uint32_t mask) |
| { |
| uint32_t readval; |
| |
| llcc_bcast_read(llcc_priv, offset, &readval); |
| readval &= ~mask; |
| readval |= val & mask; |
| llcc_bcast_write(llcc_priv, offset, readval); |
| } |
| |
| static void perfmon_counter_dump(struct llcc_perfmon_private *llcc_priv) |
| { |
| uint32_t val; |
| unsigned int i, j; |
| unsigned long long total; |
| |
| llcc_bcast_write(llcc_priv, PERFMON_DUMP, MONITOR_DUMP); |
| for (i = 0; i < llcc_priv->configured_counters - 1; i++) { |
| total = 0; |
| for (j = 0; j < llcc_priv->num_banks; j++) { |
| regmap_read(llcc_priv->llcc_map, llcc_priv->bank_off[j] |
| + LLCC_COUNTER_n_VALUE(i), &val); |
| total += val; |
| } |
| |
| llcc_priv->configured[i].counter_dump += total; |
| } |
| |
| total = 0; |
| for (j = 0; j < llcc_priv->num_banks; j++) { |
| regmap_read(llcc_priv->llcc_map, llcc_priv->bank_off[j] + |
| LLCC_COUNTER_n_VALUE(i), &val); |
| total += val; |
| } |
| |
| llcc_priv->configured[i].counter_dump += total; |
| } |
| |
| static ssize_t perfmon_counter_dump_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct llcc_perfmon_private *llcc_priv = dev_get_drvdata(dev); |
| uint32_t val; |
| unsigned int i, j; |
| unsigned long long total; |
| ssize_t cnt = 0, print; |
| |
| if (llcc_priv->configured_counters == 0) { |
| pr_err("counters not configured\n"); |
| return cnt; |
| } |
| |
| if (llcc_priv->expires.tv64) { |
| perfmon_counter_dump(llcc_priv); |
| for (i = 0; i < llcc_priv->configured_counters - 1; i++) { |
| print = snprintf(buf, MAX_STRING_SIZE, "Port %02d,", |
| llcc_priv->configured[i].port_sel); |
| buf += print; |
| cnt += print; |
| print = snprintf(buf, MAX_STRING_SIZE, "Event %02d,", |
| llcc_priv->configured[i].event_sel); |
| buf += print; |
| cnt += print; |
| |
| print = snprintf(buf, MAX_STRING_SIZE, "0x%016llx\n", |
| llcc_priv->configured[i].counter_dump); |
| buf += print; |
| cnt += print; |
| llcc_priv->configured[i].counter_dump = 0; |
| } |
| |
| print = snprintf(buf, MAX_STRING_SIZE, "CYCLE COUNT, ,"); |
| buf += print; |
| cnt += print; |
| print = snprintf(buf, MAX_STRING_SIZE, "0x%016llx\n", |
| llcc_priv->configured[i].counter_dump); |
| buf += print; |
| cnt += print; |
| llcc_priv->configured[i].counter_dump = 0; |
| hrtimer_forward_now(&llcc_priv->hrtimer, llcc_priv->expires); |
| } else { |
| llcc_bcast_write(llcc_priv, PERFMON_DUMP, MONITOR_DUMP); |
| |
| for (i = 0; i < llcc_priv->configured_counters - 1; i++) { |
| print = snprintf(buf, MAX_STRING_SIZE, "Port %02d,", |
| llcc_priv->configured[i].port_sel); |
| buf += print; |
| cnt += print; |
| print = snprintf(buf, MAX_STRING_SIZE, "Event %02d,", |
| llcc_priv->configured[i].event_sel); |
| buf += print; |
| cnt += print; |
| total = 0; |
| for (j = 0; j < llcc_priv->num_banks; j++) { |
| regmap_read(llcc_priv->llcc_map, |
| llcc_priv->bank_off[j] |
| + LLCC_COUNTER_n_VALUE(i), |
| &val); |
| print = snprintf(buf, MAX_STRING_SIZE, |
| "0x%08x,", val); |
| buf += print; |
| cnt += print; |
| total += val; |
| } |
| |
| print = snprintf(buf, MAX_STRING_SIZE, "0x%09llx\n", |
| total); |
| buf += print; |
| cnt += print; |
| } |
| |
| print = snprintf(buf, MAX_STRING_SIZE, "CYCLE COUNT, ,"); |
| buf += print; |
| cnt += print; |
| total = 0; |
| for (j = 0; j < llcc_priv->num_banks; j++) { |
| regmap_read(llcc_priv->llcc_map, |
| llcc_priv->bank_off[j] + |
| LLCC_COUNTER_n_VALUE(i), &val); |
| print = snprintf(buf, MAX_STRING_SIZE, "0x%08x,", val); |
| buf += print; |
| cnt += print; |
| total += val; |
| } |
| |
| print = snprintf(buf, MAX_STRING_SIZE, "0x%09llx\n", total); |
| buf += print; |
| cnt += print; |
| } |
| |
| return cnt; |
| } |
| |
| static ssize_t perfmon_configure_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| struct llcc_perfmon_private *llcc_priv = dev_get_drvdata(dev); |
| struct event_port_ops *port_ops; |
| unsigned int j; |
| unsigned long port_sel, event_sel; |
| uint32_t val; |
| char *token, *delim = DELIM_CHAR; |
| |
| mutex_lock(&llcc_priv->mutex); |
| if (llcc_priv->configured_counters) { |
| pr_err("Counters configured already, remove & try again\n"); |
| mutex_unlock(&llcc_priv->mutex); |
| return -EINVAL; |
| } |
| |
| llcc_priv->configured_counters = 0; |
| j = 0; |
| token = strsep((char **)&buf, delim); |
| |
| while (token != NULL) { |
| if (kstrtoul(token, 10, &port_sel)) |
| break; |
| |
| if (port_sel >= llcc_priv->port_configd) |
| break; |
| |
| token = strsep((char **)&buf, delim); |
| if (token == NULL) |
| break; |
| |
| if (kstrtoul(token, 10, &event_sel)) |
| break; |
| |
| token = strsep((char **)&buf, delim); |
| if (event_sel >= EVENT_NUM_MAX) { |
| pr_err("unsupported event num %ld\n", event_sel); |
| continue; |
| } |
| |
| llcc_priv->configured[j].port_sel = port_sel; |
| llcc_priv->configured[j].event_sel = event_sel; |
| port_ops = llcc_priv->port_ops[port_sel]; |
| pr_info("configured event %ld counter %d on port %ld\n", |
| event_sel, j, port_sel); |
| port_ops->event_config(llcc_priv, event_sel, j++, true); |
| if (!(llcc_priv->enables_port & (1 << port_sel))) |
| if (port_ops->event_enable) |
| port_ops->event_enable(llcc_priv, true); |
| |
| llcc_priv->enables_port |= (1 << port_sel); |
| |
| /* Last perfmon counter for cycle counter */ |
| if (llcc_priv->configured_counters++ == |
| (LLCC_PERFMON_COUNTER_MAX - 2)) |
| break; |
| } |
| |
| /* configure clock event */ |
| val = COUNT_CLOCK_EVENT | CLEAR_ON_ENABLE | CLEAR_ON_DUMP; |
| llcc_bcast_write(llcc_priv, PERFMON_COUNTER_n_CONFIG(j), val); |
| |
| llcc_priv->configured_counters++; |
| mutex_unlock(&llcc_priv->mutex); |
| return count; |
| } |
| |
| static ssize_t perfmon_remove_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| struct llcc_perfmon_private *llcc_priv = dev_get_drvdata(dev); |
| struct event_port_ops *port_ops; |
| unsigned int j, counter_remove = 0; |
| unsigned long port_sel, event_sel; |
| char *token, *delim = DELIM_CHAR; |
| |
| mutex_lock(&llcc_priv->mutex); |
| if (!llcc_priv->configured_counters) { |
| pr_err("Counters not configured\n"); |
| mutex_unlock(&llcc_priv->mutex); |
| return -EINVAL; |
| } |
| |
| j = 0; |
| token = strsep((char **)&buf, delim); |
| |
| while (token != NULL) { |
| if (kstrtoul(token, 10, &port_sel)) |
| break; |
| |
| if (port_sel >= llcc_priv->port_configd) |
| break; |
| |
| token = strsep((char **)&buf, delim); |
| if (token == NULL) |
| break; |
| |
| if (kstrtoul(token, 10, &event_sel)) |
| break; |
| |
| token = strsep((char **)&buf, delim); |
| if (event_sel >= EVENT_NUM_MAX) { |
| pr_err("unsupported event num %ld\n", event_sel); |
| continue; |
| } |
| |
| /* put dummy values */ |
| llcc_priv->configured[j].port_sel = MAX_NUMBER_OF_PORTS; |
| llcc_priv->configured[j].event_sel = 100; |
| port_ops = llcc_priv->port_ops[port_sel]; |
| pr_info("Removed event %ld counter %d from port %ld\n", |
| event_sel, j, port_sel); |
| |
| port_ops->event_config(llcc_priv, event_sel, j++, false); |
| if (llcc_priv->enables_port & (1 << port_sel)) |
| if (port_ops->event_enable) |
| port_ops->event_enable(llcc_priv, false); |
| |
| llcc_priv->enables_port &= ~(1 << port_sel); |
| |
| /* Last perfmon counter for cycle counter */ |
| if (counter_remove++ == (LLCC_PERFMON_COUNTER_MAX - 2)) |
| break; |
| } |
| |
| /* remove clock event */ |
| llcc_bcast_write(llcc_priv, PERFMON_COUNTER_n_CONFIG(j), 0); |
| |
| llcc_priv->configured_counters = 0; |
| mutex_unlock(&llcc_priv->mutex); |
| return count; |
| } |
| |
| static enum filter_type find_filter_type(char *filter) |
| { |
| enum filter_type ret = UNKNOWN; |
| |
| if (!strcmp(filter, "SCID")) |
| ret = SCID; |
| else if (!strcmp(filter, "MID")) |
| ret = MID; |
| else if (!strcmp(filter, "PROFILING_TAG")) |
| ret = PROFILING_TAG; |
| else if (!strcmp(filter, "WAY_ID")) |
| ret = WAY_ID; |
| |
| return ret; |
| } |
| |
| static ssize_t perfmon_filter_config_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct llcc_perfmon_private *llcc_priv = dev_get_drvdata(dev); |
| unsigned long port, val; |
| struct event_port_ops *port_ops; |
| char *token, *delim = DELIM_CHAR; |
| enum filter_type filter = UNKNOWN; |
| |
| mutex_lock(&llcc_priv->mutex); |
| |
| token = strsep((char **)&buf, delim); |
| if (token != NULL) |
| filter = find_filter_type(token); |
| |
| if (filter == UNKNOWN) { |
| pr_err("filter configuration failed, Unsupported filter\n"); |
| goto filter_config_free; |
| } |
| |
| token = strsep((char **)&buf, delim); |
| if (token == NULL) { |
| pr_err("filter configuration failed, Wrong input\n"); |
| goto filter_config_free; |
| } |
| |
| if (kstrtoul(token, 10, &val)) { |
| pr_err("filter configuration failed, Wrong input\n"); |
| goto filter_config_free; |
| } |
| |
| if ((filter == SCID) && (val >= SCID_MAX)) { |
| pr_err("filter configuration failed, SCID above MAX value\n"); |
| goto filter_config_free; |
| } |
| |
| |
| while (token != NULL) { |
| token = strsep((char **)&buf, delim); |
| if (token == NULL) |
| break; |
| |
| if (kstrtoul(token, 10, &port)) |
| break; |
| |
| llcc_priv->filtered_ports |= 1 << port; |
| port_ops = llcc_priv->port_ops[port]; |
| if (port_ops->event_filter_config) |
| port_ops->event_filter_config(llcc_priv, filter, val, |
| true); |
| } |
| |
| mutex_unlock(&llcc_priv->mutex); |
| return count; |
| |
| filter_config_free: |
| mutex_unlock(&llcc_priv->mutex); |
| return -EINVAL; |
| } |
| |
| static ssize_t perfmon_filter_remove_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, size_t count) |
| { |
| struct llcc_perfmon_private *llcc_priv = dev_get_drvdata(dev); |
| struct event_port_ops *port_ops; |
| unsigned long port, val; |
| char *token, *delim = DELIM_CHAR; |
| enum filter_type filter = UNKNOWN; |
| |
| mutex_lock(&llcc_priv->mutex); |
| token = strsep((char **)&buf, delim); |
| if (token != NULL) |
| filter = find_filter_type(token); |
| |
| if (filter == UNKNOWN) { |
| pr_err("filter configuration failed, Unsupported filter\n"); |
| goto filter_remove_free; |
| } |
| |
| token = strsep((char **)&buf, delim); |
| if (token == NULL) { |
| pr_err("filter configuration failed, Wrong input\n"); |
| goto filter_remove_free; |
| } |
| |
| if (kstrtoul(token, 10, &val)) { |
| pr_err("filter configuration failed, Wrong input\n"); |
| goto filter_remove_free; |
| } |
| |
| if ((filter == SCID) && (val >= SCID_MAX)) { |
| pr_err("filter configuration failed, SCID above MAX value\n"); |
| goto filter_remove_free; |
| } |
| |
| while (token != NULL) { |
| token = strsep((char **)&buf, delim); |
| if (token == NULL) |
| break; |
| |
| if (kstrtoul(token, 10, &port)) |
| break; |
| |
| llcc_priv->filtered_ports &= ~(1 << port); |
| port_ops = llcc_priv->port_ops[port]; |
| if (port_ops->event_filter_config) |
| port_ops->event_filter_config(llcc_priv, filter, val, |
| false); |
| } |
| |
| mutex_unlock(&llcc_priv->mutex); |
| return count; |
| |
| filter_remove_free: |
| mutex_unlock(&llcc_priv->mutex); |
| return count; |
| } |
| |
| static ssize_t perfmon_start_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct llcc_perfmon_private *llcc_priv = dev_get_drvdata(dev); |
| uint32_t val = 0, mask; |
| unsigned long start; |
| |
| if (kstrtoul(buf, 10, &start)) |
| return -EINVAL; |
| |
| mutex_lock(&llcc_priv->mutex); |
| if (start) { |
| if (!llcc_priv->configured_counters) |
| pr_err("start failed. perfmon not configured\n"); |
| |
| val = MANUAL_MODE | MONITOR_EN; |
| if (llcc_priv->expires.tv64) { |
| if (hrtimer_is_queued(&llcc_priv->hrtimer)) |
| hrtimer_forward_now(&llcc_priv->hrtimer, |
| llcc_priv->expires); |
| else |
| hrtimer_start(&llcc_priv->hrtimer, |
| llcc_priv->expires, |
| HRTIMER_MODE_REL_PINNED); |
| } |
| |
| } else { |
| if (llcc_priv->expires.tv64) |
| hrtimer_cancel(&llcc_priv->hrtimer); |
| |
| if (!llcc_priv->configured_counters) |
| pr_err("stop failed. perfmon not configured\n"); |
| } |
| |
| mask = PERFMON_MODE_MONITOR_MODE_MASK | PERFMON_MODE_MONITOR_EN_MASK; |
| llcc_bcast_modify(llcc_priv, PERFMON_MODE, val, mask); |
| |
| |
| mutex_unlock(&llcc_priv->mutex); |
| return count; |
| } |
| |
| static ssize_t perfmon_ns_periodic_dump_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct llcc_perfmon_private *llcc_priv = dev_get_drvdata(dev); |
| |
| if (kstrtos64(buf, 10, &llcc_priv->expires.tv64)) |
| return -EINVAL; |
| |
| mutex_lock(&llcc_priv->mutex); |
| if (!llcc_priv->expires.tv64) { |
| hrtimer_cancel(&llcc_priv->hrtimer); |
| mutex_unlock(&llcc_priv->mutex); |
| return count; |
| } |
| |
| if (hrtimer_is_queued(&llcc_priv->hrtimer)) |
| hrtimer_forward_now(&llcc_priv->hrtimer, llcc_priv->expires); |
| else |
| hrtimer_start(&llcc_priv->hrtimer, llcc_priv->expires, |
| HRTIMER_MODE_REL_PINNED); |
| |
| mutex_unlock(&llcc_priv->mutex); |
| return count; |
| } |
| |
| static ssize_t perfmon_scid_status_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct llcc_perfmon_private *llcc_priv = dev_get_drvdata(dev); |
| uint32_t val; |
| unsigned int i, offset; |
| ssize_t cnt = 0, print; |
| |
| for (i = 0; i < SCID_MAX; i++) { |
| offset = TRP_SCID_n_STATUS(i); |
| llcc_bcast_read(llcc_priv, offset, &val); |
| if (val & TRP_SCID_STATUS_ACTIVE_MASK) |
| print = snprintf(buf, MAX_STRING_SIZE, "SCID %02d %10s", |
| i, "ACTIVE"); |
| else |
| print = snprintf(buf, MAX_STRING_SIZE, "SCID %02d %10s", |
| i, "DEACTIVE"); |
| |
| buf += print; |
| cnt += print; |
| |
| val = (val & TRP_SCID_STATUS_CURRENT_CAP_MASK) |
| >> TRP_SCID_STATUS_CURRENT_CAP_SHIFT; |
| print = snprintf(buf, MAX_STRING_SIZE, ",0x%08x\n", val); |
| buf += print; |
| cnt += print; |
| } |
| |
| return cnt; |
| } |
| |
| static DEVICE_ATTR_RO(perfmon_counter_dump); |
| static DEVICE_ATTR_WO(perfmon_configure); |
| static DEVICE_ATTR_WO(perfmon_remove); |
| static DEVICE_ATTR_WO(perfmon_filter_config); |
| static DEVICE_ATTR_WO(perfmon_filter_remove); |
| static DEVICE_ATTR_WO(perfmon_start); |
| static DEVICE_ATTR_RO(perfmon_scid_status); |
| static DEVICE_ATTR_WO(perfmon_ns_periodic_dump); |
| |
| static struct attribute *llcc_perfmon_attrs[] = { |
| &dev_attr_perfmon_counter_dump.attr, |
| &dev_attr_perfmon_configure.attr, |
| &dev_attr_perfmon_remove.attr, |
| &dev_attr_perfmon_filter_config.attr, |
| &dev_attr_perfmon_filter_remove.attr, |
| &dev_attr_perfmon_start.attr, |
| &dev_attr_perfmon_scid_status.attr, |
| &dev_attr_perfmon_ns_periodic_dump.attr, |
| NULL, |
| }; |
| |
| static struct attribute_group llcc_perfmon_group = { |
| .attrs = llcc_perfmon_attrs, |
| }; |
| |
| static void perfmon_counter_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int port, unsigned int event_counter_num) |
| { |
| uint32_t val; |
| |
| val = (port & PERFMON_PORT_SELECT_MASK) | |
| ((event_counter_num << EVENT_SELECT_SHIFT) & |
| PERFMON_EVENT_SELECT_MASK) | CLEAR_ON_ENABLE | CLEAR_ON_DUMP; |
| llcc_bcast_write(llcc_priv, PERFMON_COUNTER_n_CONFIG(event_counter_num), |
| val); |
| } |
| |
| static void feac_event_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int event_type, unsigned int event_counter_num, |
| bool enable) |
| { |
| uint32_t val = 0, mask, counter_num = 0; |
| |
| mask = EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_FEAC)) |
| mask |= FILTER_SEL_MASK | FILTER_EN_MASK; |
| |
| if (enable) { |
| val = (event_type << EVENT_SEL_SHIFT) & EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_FEAC)) |
| val |= (FILTER_0 << FILTER_SEL_SHIFT) | FILTER_EN; |
| |
| counter_num = event_counter_num; |
| } |
| |
| llcc_bcast_modify(llcc_priv, FEAC_PROF_EVENT_n_CFG(event_counter_num), |
| val, mask); |
| perfmon_counter_config(llcc_priv, EVENT_PORT_FEAC, counter_num); |
| } |
| |
| static void feac_event_enable(struct llcc_perfmon_private *llcc_priv, |
| bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (enable) { |
| val = (BYTE_SCALING << BYTE_SCALING_SHIFT) | |
| (BEAT_SCALING << BEAT_SCALING_SHIFT) | PROF_EN; |
| |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_FEAC)) |
| val |= (FILTER_0 << FEAC_SCALING_FILTER_SEL_SHIFT) | |
| FEAC_SCALING_FILTER_EN; |
| } |
| |
| mask = PROF_CFG_BEAT_SCALING_MASK | PROF_CFG_BYTE_SCALING_MASK |
| | PROF_CFG_EN_MASK; |
| |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_FEAC)) |
| mask |= FEAC_SCALING_FILTER_SEL_MASK | |
| FEAC_SCALING_FILTER_EN_MASK; |
| |
| llcc_bcast_modify(llcc_priv, FEAC_PROF_CFG, val, mask); |
| } |
| |
| static void feac_event_filter_config(struct llcc_perfmon_private *llcc_priv, |
| enum filter_type filter, unsigned long match, bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (filter == SCID) { |
| if (enable) |
| val = (match << SCID_MATCH_SHIFT) |
| | SCID_MASK_MASK; |
| |
| mask = SCID_MATCH_MASK | SCID_MASK_MASK; |
| llcc_bcast_modify(llcc_priv, FEAC_PROF_FILTER_0_CFG6, val, |
| mask); |
| } else if (filter == MID) { |
| if (enable) |
| val = (match << MID_MATCH_SHIFT) |
| | MID_MASK_MASK; |
| |
| mask = MID_MATCH_MASK | MID_MASK_MASK; |
| llcc_bcast_modify(llcc_priv, FEAC_PROF_FILTER_0_CFG5, val, |
| mask); |
| } else { |
| pr_err("unknown filter/not supported\n"); |
| } |
| } |
| |
| static struct event_port_ops feac_port_ops = { |
| .event_config = feac_event_config, |
| .event_enable = feac_event_enable, |
| .event_filter_config = feac_event_filter_config, |
| }; |
| |
| static void ferc_event_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int event_type, unsigned int event_counter_num, |
| bool enable) |
| { |
| uint32_t val = 0, mask, counter_num = 0; |
| |
| mask = EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_FERC)) |
| mask |= FILTER_SEL_MASK | FILTER_EN_MASK; |
| |
| if (enable) { |
| val = event_type << EVENT_SEL_SHIFT; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_FERC)) |
| val |= (FILTER_0 << FILTER_SEL_SHIFT) | FILTER_EN; |
| |
| counter_num = event_counter_num; |
| } |
| |
| llcc_bcast_modify(llcc_priv, FERC_PROF_EVENT_n_CFG(event_counter_num), |
| val, mask); |
| perfmon_counter_config(llcc_priv, EVENT_PORT_FERC, counter_num); |
| } |
| |
| static void ferc_event_enable(struct llcc_perfmon_private *llcc_priv, |
| bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (enable) |
| val = (BYTE_SCALING << BYTE_SCALING_SHIFT) | |
| (BEAT_SCALING << BEAT_SCALING_SHIFT) | PROF_EN; |
| |
| mask = PROF_CFG_BEAT_SCALING_MASK | PROF_CFG_BYTE_SCALING_MASK |
| | PROF_CFG_EN_MASK; |
| llcc_bcast_modify(llcc_priv, FERC_PROF_CFG, val, mask); |
| } |
| |
| static void ferc_event_filter_config(struct llcc_perfmon_private *llcc_priv, |
| enum filter_type filter, unsigned long match, bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (filter != PROFILING_TAG) { |
| pr_err("unknown filter/not supported\n"); |
| return; |
| } |
| |
| if (enable) |
| val = (match << PROFTAG_MATCH_SHIFT) | |
| FILTER_0_MASK << PROFTAG_MASK_SHIFT; |
| |
| mask = PROFTAG_MATCH_MASK | PROFTAG_MASK_MASK; |
| llcc_bcast_modify(llcc_priv, FERC_PROF_FILTER_0_CFG0, val, mask); |
| } |
| |
| static struct event_port_ops ferc_port_ops = { |
| .event_config = ferc_event_config, |
| .event_enable = ferc_event_enable, |
| .event_filter_config = ferc_event_filter_config, |
| }; |
| |
| static void fewc_event_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int event_type, unsigned int event_counter_num, |
| bool enable) |
| { |
| uint32_t val = 0, mask, counter_num = 0; |
| |
| mask = EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_FEWC)) |
| mask |= FILTER_SEL_MASK | FILTER_EN_MASK; |
| |
| if (enable) { |
| val = (event_type << EVENT_SEL_SHIFT) & EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_FEWC)) |
| val |= (FILTER_0 << FILTER_SEL_SHIFT) | FILTER_EN; |
| |
| counter_num = event_counter_num; |
| } |
| |
| llcc_bcast_modify(llcc_priv, FEWC_PROF_EVENT_n_CFG(event_counter_num), |
| val, mask); |
| perfmon_counter_config(llcc_priv, EVENT_PORT_FEWC, counter_num); |
| } |
| |
| static void fewc_event_filter_config(struct llcc_perfmon_private *llcc_priv, |
| enum filter_type filter, unsigned long match, bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (filter != PROFILING_TAG) { |
| pr_err("unknown filter/not supported\n"); |
| return; |
| } |
| |
| if (enable) |
| val = (match << PROFTAG_MATCH_SHIFT) | |
| FILTER_0_MASK << PROFTAG_MASK_SHIFT; |
| |
| mask = PROFTAG_MATCH_MASK | PROFTAG_MASK_MASK; |
| llcc_bcast_modify(llcc_priv, FEWC_PROF_FILTER_0_CFG0, val, mask); |
| } |
| |
| static struct event_port_ops fewc_port_ops = { |
| .event_config = fewc_event_config, |
| .event_filter_config = fewc_event_filter_config, |
| }; |
| |
| static void beac_event_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int event_type, unsigned int event_counter_num, |
| bool enable) |
| { |
| uint32_t val = 0, mask, counter_num = 0; |
| |
| mask = EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_BEAC)) |
| mask |= FILTER_SEL_MASK | FILTER_EN_MASK; |
| |
| if (enable) { |
| val = (event_type << EVENT_SEL_SHIFT) & EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_BEAC)) |
| val |= (FILTER_0 << FILTER_SEL_SHIFT) | FILTER_EN; |
| |
| counter_num = event_counter_num; |
| } |
| |
| llcc_bcast_modify(llcc_priv, BEAC_PROF_EVENT_n_CFG(event_counter_num), |
| val, mask); |
| perfmon_counter_config(llcc_priv, EVENT_PORT_BEAC, counter_num); |
| } |
| |
| static void beac_event_enable(struct llcc_perfmon_private *llcc_priv, |
| bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (enable) |
| val = (BYTE_SCALING << BYTE_SCALING_SHIFT) | |
| (BEAT_SCALING << BEAT_SCALING_SHIFT) | PROF_EN; |
| |
| mask = PROF_CFG_BEAT_SCALING_MASK | PROF_CFG_BYTE_SCALING_MASK |
| | PROF_CFG_EN_MASK; |
| llcc_bcast_modify(llcc_priv, BEAC_PROF_CFG, val, mask); |
| } |
| |
| static void beac_event_filter_config(struct llcc_perfmon_private *llcc_priv, |
| enum filter_type filter, unsigned long match, bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (filter != PROFILING_TAG) { |
| pr_err("unknown filter/not supported\n"); |
| return; |
| } |
| |
| if (enable) |
| val = (match << BEAC_PROFTAG_MATCH_SHIFT) |
| | FILTER_0_MASK << BEAC_PROFTAG_MASK_SHIFT; |
| |
| mask = BEAC_PROFTAG_MASK_MASK | BEAC_PROFTAG_MATCH_MASK; |
| llcc_bcast_modify(llcc_priv, BEAC_PROF_FILTER_0_CFG5, val, mask); |
| |
| if (enable) |
| val = match << BEAC_MC_PROFTAG_SHIFT; |
| |
| mask = BEAC_MC_PROFTAG_MASK; |
| llcc_bcast_modify(llcc_priv, BEAC_PROF_CFG, val, mask); |
| } |
| |
| static struct event_port_ops beac_port_ops = { |
| .event_config = beac_event_config, |
| .event_enable = beac_event_enable, |
| .event_filter_config = beac_event_filter_config, |
| }; |
| |
| static void berc_event_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int event_type, unsigned int event_counter_num, |
| bool enable) |
| { |
| uint32_t val = 0, mask, counter_num = 0; |
| |
| mask = EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_BERC)) |
| mask |= FILTER_SEL_MASK | FILTER_EN_MASK; |
| |
| if (enable) { |
| val = (event_type << EVENT_SEL_SHIFT) & EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_BERC)) |
| val |= (FILTER_0 << FILTER_SEL_SHIFT) | FILTER_EN; |
| |
| counter_num = event_counter_num; |
| } |
| |
| llcc_bcast_modify(llcc_priv, BERC_PROF_EVENT_n_CFG(event_counter_num), |
| val, mask); |
| perfmon_counter_config(llcc_priv, EVENT_PORT_BERC, counter_num); |
| } |
| |
| static void berc_event_enable(struct llcc_perfmon_private *llcc_priv, |
| bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (enable) |
| val = (BYTE_SCALING << BYTE_SCALING_SHIFT) | |
| (BEAT_SCALING << BEAT_SCALING_SHIFT) | PROF_EN; |
| |
| mask = PROF_CFG_BEAT_SCALING_MASK | PROF_CFG_BYTE_SCALING_MASK |
| | PROF_CFG_EN_MASK; |
| llcc_bcast_modify(llcc_priv, BERC_PROF_CFG, val, mask); |
| } |
| |
| static void berc_event_filter_config(struct llcc_perfmon_private *llcc_priv, |
| enum filter_type filter, unsigned long match, bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (filter != PROFILING_TAG) { |
| pr_err("unknown filter/not supported\n"); |
| return; |
| } |
| |
| if (enable) |
| val = (match << PROFTAG_MATCH_SHIFT) | |
| FILTER_0_MASK << PROFTAG_MASK_SHIFT; |
| |
| mask = PROFTAG_MATCH_MASK | PROFTAG_MASK_MASK; |
| llcc_bcast_modify(llcc_priv, BERC_PROF_FILTER_0_CFG0, val, mask); |
| } |
| |
| static struct event_port_ops berc_port_ops = { |
| .event_config = berc_event_config, |
| .event_enable = berc_event_enable, |
| .event_filter_config = berc_event_filter_config, |
| }; |
| |
| static void trp_event_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int event_type, unsigned int event_counter_num, |
| bool enable) |
| { |
| uint32_t val = 0, mask, counter_num = 0; |
| |
| mask = EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_TRP)) |
| mask |= FILTER_SEL_MASK | FILTER_EN_MASK; |
| |
| if (enable) { |
| val = (event_type << EVENT_SEL_SHIFT) & EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_TRP)) |
| val |= (FILTER_0 << FILTER_SEL_SHIFT) | FILTER_EN; |
| |
| counter_num = event_counter_num; |
| } |
| |
| llcc_bcast_modify(llcc_priv, TRP_PROF_EVENT_n_CFG(event_counter_num), |
| val, mask); |
| perfmon_counter_config(llcc_priv, EVENT_PORT_TRP, counter_num); |
| } |
| |
| static void trp_event_filter_config(struct llcc_perfmon_private *llcc_priv, |
| enum filter_type filter, unsigned long match, bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (filter == SCID) { |
| if (enable) |
| val = (match << TRP_SCID_MATCH_SHIFT) |
| | TRP_SCID_MASK_MASK; |
| |
| mask = TRP_SCID_MATCH_MASK | TRP_SCID_MASK_MASK; |
| } else if (filter == WAY_ID) { |
| if (enable) |
| val = (match << TRP_WAY_ID_MATCH_SHIFT) |
| | TRP_WAY_ID_MASK_MASK; |
| |
| mask = TRP_WAY_ID_MATCH_MASK | |
| TRP_WAY_ID_MASK_MASK; |
| } else if (filter == PROFILING_TAG) { |
| if (enable) |
| val = (match << TRP_PROFTAG_MATCH_SHIFT) |
| | FILTER_0_MASK << TRP_PROFTAG_MASK_SHIFT; |
| |
| mask = TRP_PROFTAG_MATCH_MASK | TRP_PROFTAG_MASK_MASK; |
| } else { |
| pr_err("unknown filter/not supported\n"); |
| return; |
| } |
| |
| llcc_bcast_modify(llcc_priv, TRP_PROF_FILTER_0_CFG1, val, mask); |
| } |
| |
| static struct event_port_ops trp_port_ops = { |
| .event_config = trp_event_config, |
| .event_filter_config = trp_event_filter_config, |
| }; |
| |
| static void drp_event_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int event_type, unsigned int event_counter_num, |
| bool enable) |
| { |
| uint32_t val = 0, mask, counter_num = 0; |
| |
| mask = EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_DRP)) |
| mask |= FILTER_SEL_MASK | FILTER_EN_MASK; |
| |
| if (enable) { |
| val = (event_type << EVENT_SEL_SHIFT) & EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_DRP)) |
| val |= (FILTER_0 << FILTER_SEL_SHIFT) | FILTER_EN; |
| |
| counter_num = event_counter_num; |
| } |
| |
| llcc_bcast_modify(llcc_priv, DRP_PROF_EVENT_n_CFG(event_counter_num), |
| val, mask); |
| perfmon_counter_config(llcc_priv, EVENT_PORT_DRP, counter_num); |
| } |
| |
| static void drp_event_enable(struct llcc_perfmon_private *llcc_priv, |
| bool enable) |
| { |
| uint32_t val = 0, mask; |
| |
| if (enable) |
| val = (BEAT_SCALING << BEAT_SCALING_SHIFT) | PROF_EN; |
| |
| mask = PROF_CFG_BEAT_SCALING_MASK | PROF_CFG_EN_MASK; |
| llcc_bcast_modify(llcc_priv, DRP_PROF_CFG, val, mask); |
| } |
| |
| static struct event_port_ops drp_port_ops = { |
| .event_config = drp_event_config, |
| .event_enable = drp_event_enable, |
| }; |
| |
| static void pmgr_event_config(struct llcc_perfmon_private *llcc_priv, |
| unsigned int event_type, unsigned int event_counter_num, |
| bool enable) |
| { |
| uint32_t val = 0, mask, counter_num = 0; |
| |
| mask = EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_PMGR)) |
| mask |= FILTER_SEL_MASK | FILTER_EN_MASK; |
| |
| if (enable) { |
| val = (event_type << EVENT_SEL_SHIFT) & EVENT_SEL_MASK; |
| if (llcc_priv->filtered_ports & (1 << EVENT_PORT_PMGR)) |
| val |= (FILTER_0 << FILTER_SEL_SHIFT) | FILTER_EN; |
| |
| counter_num = event_counter_num; |
| } |
| |
| llcc_bcast_modify(llcc_priv, PMGR_PROF_EVENT_n_CFG(event_counter_num), |
| val, mask); |
| perfmon_counter_config(llcc_priv, EVENT_PORT_PMGR, counter_num); |
| } |
| |
| static struct event_port_ops pmgr_port_ops = { |
| .event_config = pmgr_event_config, |
| }; |
| |
| static void llcc_register_event_port(struct llcc_perfmon_private *llcc_priv, |
| struct event_port_ops *ops, unsigned int event_port_num) |
| { |
| if (llcc_priv->port_configd >= MAX_NUMBER_OF_PORTS) { |
| pr_err("Register port Failure!"); |
| return; |
| } |
| |
| llcc_priv->port_configd = llcc_priv->port_configd + 1; |
| llcc_priv->port_ops[event_port_num] = ops; |
| } |
| |
| static enum hrtimer_restart llcc_perfmon_timer_handler(struct hrtimer *hrtimer) |
| { |
| struct llcc_perfmon_private *llcc_priv = container_of(hrtimer, |
| struct llcc_perfmon_private, hrtimer); |
| |
| perfmon_counter_dump(llcc_priv); |
| hrtimer_forward_now(&llcc_priv->hrtimer, llcc_priv->expires); |
| return HRTIMER_RESTART; |
| } |
| |
| static int llcc_perfmon_probe(struct platform_device *pdev) |
| { |
| int result = 0; |
| struct llcc_perfmon_private *llcc_priv; |
| struct device *dev = &pdev->dev; |
| uint32_t val; |
| |
| llcc_priv = devm_kzalloc(&pdev->dev, sizeof(*llcc_priv), GFP_KERNEL); |
| if (llcc_priv == NULL) |
| return -ENOMEM; |
| |
| llcc_priv->llcc_map = syscon_node_to_regmap(dev->parent->of_node); |
| if (IS_ERR(llcc_priv->llcc_map)) |
| return PTR_ERR(llcc_priv->llcc_map); |
| |
| result = of_property_read_u32(pdev->dev.parent->of_node, |
| "qcom,llcc-broadcast-off", &llcc_priv->broadcast_off); |
| if (result) { |
| pr_err("Invalid qcom,broadcast-off entry\n"); |
| return result; |
| } |
| |
| llcc_bcast_read(llcc_priv, LLCC_COMMON_STATUS0, &val); |
| |
| llcc_priv->num_banks = (val & LB_CNT_MASK) >> LB_CNT_SHIFT; |
| result = of_property_read_variable_u32_array(pdev->dev.parent->of_node, |
| "qcom,llcc-banks-off", (u32 *)&llcc_priv->bank_off, |
| 1, llcc_priv->num_banks); |
| if (result < 0) { |
| pr_err("Invalid qcom,llcc-banks-off entry\n"); |
| return result; |
| } |
| |
| result = sysfs_create_group(&pdev->dev.kobj, &llcc_perfmon_group); |
| if (result) { |
| pr_err("Unable to create sysfs version group\n"); |
| return result; |
| } |
| |
| mutex_init(&llcc_priv->mutex); |
| platform_set_drvdata(pdev, llcc_priv); |
| llcc_register_event_port(llcc_priv, &feac_port_ops, EVENT_PORT_FEAC); |
| llcc_register_event_port(llcc_priv, &ferc_port_ops, EVENT_PORT_FERC); |
| llcc_register_event_port(llcc_priv, &fewc_port_ops, EVENT_PORT_FEWC); |
| llcc_register_event_port(llcc_priv, &beac_port_ops, EVENT_PORT_BEAC); |
| llcc_register_event_port(llcc_priv, &berc_port_ops, EVENT_PORT_BERC); |
| llcc_register_event_port(llcc_priv, &trp_port_ops, EVENT_PORT_TRP); |
| llcc_register_event_port(llcc_priv, &drp_port_ops, EVENT_PORT_DRP); |
| llcc_register_event_port(llcc_priv, &pmgr_port_ops, EVENT_PORT_PMGR); |
| hrtimer_init(&llcc_priv->hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); |
| llcc_priv->hrtimer.function = llcc_perfmon_timer_handler; |
| llcc_priv->expires.tv64 = 0; |
| return 0; |
| } |
| |
| static int llcc_perfmon_remove(struct platform_device *pdev) |
| { |
| struct llcc_perfmon_private *llcc_priv = platform_get_drvdata(pdev); |
| |
| while (hrtimer_active(&llcc_priv->hrtimer)) |
| hrtimer_cancel(&llcc_priv->hrtimer); |
| |
| mutex_destroy(&llcc_priv->mutex); |
| sysfs_remove_group(&pdev->dev.kobj, &llcc_perfmon_group); |
| platform_set_drvdata(pdev, NULL); |
| return 0; |
| } |
| |
| static const struct of_device_id of_match_llcc[] = { |
| { |
| .compatible = "qcom,llcc-perfmon", |
| }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, of_match_llcc); |
| |
| static struct platform_driver llcc_perfmon_driver = { |
| .probe = llcc_perfmon_probe, |
| .remove = llcc_perfmon_remove, |
| .driver = { |
| .name = LLCC_PERFMON_NAME, |
| .of_match_table = of_match_llcc, |
| } |
| }; |
| module_platform_driver(llcc_perfmon_driver); |
| |
| MODULE_DESCRIPTION("QCOM LLCC PMU MONITOR"); |
| MODULE_LICENSE("GPL v2"); |