mirror of
https://github.com/torvalds/linux.git
synced 2024-11-22 12:11:40 +00:00
2e091a805f
The devm_ioremap() function never returns error pointers, it returns a
NULL pointer if there is an error.
Fixes: 4d5a7680f2
("perf: Add driver for Arm NI-700 interconnect PMU")
Signed-off-by: Dan Carpenter <dan.carpenter@linaro.org>
Reviewed-by: Robin Murphy <robin.murphy@arm.com>
Link: https://lore.kernel.org/r/04d6ccc3-6d31-4f0f-ab0f-7a88342cc09a@stanley.mountain
Signed-off-by: Will Deacon <will@kernel.org>
782 lines
19 KiB
C
782 lines
19 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
// Copyright (C) 2022-2024 Arm Limited
|
|
// NI-700 Network-on-Chip PMU driver
|
|
|
|
#include <linux/acpi.h>
|
|
#include <linux/bitfield.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/io.h>
|
|
#include <linux/io-64-nonatomic-lo-hi.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/of.h>
|
|
#include <linux/perf_event.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/slab.h>
|
|
|
|
/* Common registers */
|
|
#define NI_NODE_TYPE 0x000
|
|
#define NI_NODE_TYPE_NODE_ID GENMASK(31, 16)
|
|
#define NI_NODE_TYPE_NODE_TYPE GENMASK(15, 0)
|
|
|
|
#define NI_CHILD_NODE_INFO 0x004
|
|
#define NI_CHILD_PTR(n) (0x008 + (n) * 4)
|
|
|
|
#define NI700_PMUSELA 0x00c
|
|
|
|
/* Config node */
|
|
#define NI_PERIPHERAL_ID0 0xfe0
|
|
#define NI_PIDR0_PART_7_0 GENMASK(7, 0)
|
|
#define NI_PERIPHERAL_ID1 0xfe4
|
|
#define NI_PIDR1_PART_11_8 GENMASK(3, 0)
|
|
#define NI_PERIPHERAL_ID2 0xfe8
|
|
#define NI_PIDR2_VERSION GENMASK(7, 4)
|
|
|
|
/* PMU node */
|
|
#define NI_PMEVCNTR(n) (0x008 + (n) * 8)
|
|
#define NI_PMCCNTR_L 0x0f8
|
|
#define NI_PMCCNTR_U 0x0fc
|
|
#define NI_PMEVTYPER(n) (0x400 + (n) * 4)
|
|
#define NI_PMEVTYPER_NODE_TYPE GENMASK(12, 9)
|
|
#define NI_PMEVTYPER_NODE_ID GENMASK(8, 0)
|
|
#define NI_PMCNTENSET 0xc00
|
|
#define NI_PMCNTENCLR 0xc20
|
|
#define NI_PMINTENSET 0xc40
|
|
#define NI_PMINTENCLR 0xc60
|
|
#define NI_PMOVSCLR 0xc80
|
|
#define NI_PMOVSSET 0xcc0
|
|
#define NI_PMCFGR 0xe00
|
|
#define NI_PMCR 0xe04
|
|
#define NI_PMCR_RESET_CCNT BIT(2)
|
|
#define NI_PMCR_RESET_EVCNT BIT(1)
|
|
#define NI_PMCR_ENABLE BIT(0)
|
|
|
|
#define NI_NUM_COUNTERS 8
|
|
#define NI_CCNT_IDX 31
|
|
|
|
/* Event attributes */
|
|
#define NI_CONFIG_TYPE GENMASK_ULL(15, 0)
|
|
#define NI_CONFIG_NODEID GENMASK_ULL(31, 16)
|
|
#define NI_CONFIG_EVENTID GENMASK_ULL(47, 32)
|
|
|
|
#define NI_EVENT_TYPE(event) FIELD_GET(NI_CONFIG_TYPE, (event)->attr.config)
|
|
#define NI_EVENT_NODEID(event) FIELD_GET(NI_CONFIG_NODEID, (event)->attr.config)
|
|
#define NI_EVENT_EVENTID(event) FIELD_GET(NI_CONFIG_EVENTID, (event)->attr.config)
|
|
|
|
enum ni_part {
|
|
PART_NI_700 = 0x43b,
|
|
PART_NI_710AE = 0x43d,
|
|
};
|
|
|
|
enum ni_node_type {
|
|
NI_GLOBAL,
|
|
NI_VOLTAGE,
|
|
NI_POWER,
|
|
NI_CLOCK,
|
|
NI_ASNI,
|
|
NI_AMNI,
|
|
NI_PMU,
|
|
NI_HSNI,
|
|
NI_HMNI,
|
|
NI_PMNI,
|
|
};
|
|
|
|
struct arm_ni_node {
|
|
void __iomem *base;
|
|
enum ni_node_type type;
|
|
u16 id;
|
|
u32 num_components;
|
|
};
|
|
|
|
struct arm_ni_unit {
|
|
void __iomem *pmusela;
|
|
enum ni_node_type type;
|
|
u16 id;
|
|
bool ns;
|
|
union {
|
|
__le64 pmusel;
|
|
u8 event[8];
|
|
};
|
|
};
|
|
|
|
struct arm_ni_cd {
|
|
void __iomem *pmu_base;
|
|
u16 id;
|
|
int num_units;
|
|
int irq;
|
|
int cpu;
|
|
struct hlist_node cpuhp_node;
|
|
struct pmu pmu;
|
|
struct arm_ni_unit *units;
|
|
struct perf_event *evcnt[NI_NUM_COUNTERS];
|
|
struct perf_event *ccnt;
|
|
};
|
|
|
|
struct arm_ni {
|
|
struct device *dev;
|
|
void __iomem *base;
|
|
enum ni_part part;
|
|
int id;
|
|
int num_cds;
|
|
struct arm_ni_cd cds[] __counted_by(num_cds);
|
|
};
|
|
|
|
#define cd_to_ni(cd) container_of((cd), struct arm_ni, cds[(cd)->id])
|
|
#define pmu_to_cd(p) container_of((p), struct arm_ni_cd, pmu)
|
|
|
|
#define cd_for_each_unit(cd, u) \
|
|
for (struct arm_ni_unit *u = cd->units; u < cd->units + cd->num_units; u++)
|
|
|
|
static int arm_ni_hp_state;
|
|
|
|
struct arm_ni_event_attr {
|
|
struct device_attribute attr;
|
|
enum ni_node_type type;
|
|
};
|
|
|
|
#define NI_EVENT_ATTR(_name, _type) \
|
|
(&((struct arm_ni_event_attr[]) {{ \
|
|
.attr = __ATTR(_name, 0444, arm_ni_event_show, NULL), \
|
|
.type = _type, \
|
|
}})[0].attr.attr)
|
|
|
|
static ssize_t arm_ni_event_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct arm_ni_event_attr *eattr = container_of(attr, typeof(*eattr), attr);
|
|
|
|
if (eattr->type == NI_PMU)
|
|
return sysfs_emit(buf, "type=0x%x\n", eattr->type);
|
|
|
|
return sysfs_emit(buf, "type=0x%x,eventid=?,nodeid=?\n", eattr->type);
|
|
}
|
|
|
|
static umode_t arm_ni_event_attr_is_visible(struct kobject *kobj,
|
|
struct attribute *attr, int unused)
|
|
{
|
|
struct device *dev = kobj_to_dev(kobj);
|
|
struct arm_ni_cd *cd = pmu_to_cd(dev_get_drvdata(dev));
|
|
struct arm_ni_event_attr *eattr;
|
|
|
|
eattr = container_of(attr, typeof(*eattr), attr.attr);
|
|
|
|
cd_for_each_unit(cd, unit) {
|
|
if (unit->type == eattr->type && unit->ns)
|
|
return attr->mode;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct attribute *arm_ni_event_attrs[] = {
|
|
NI_EVENT_ATTR(asni, NI_ASNI),
|
|
NI_EVENT_ATTR(amni, NI_AMNI),
|
|
NI_EVENT_ATTR(cycles, NI_PMU),
|
|
NI_EVENT_ATTR(hsni, NI_HSNI),
|
|
NI_EVENT_ATTR(hmni, NI_HMNI),
|
|
NI_EVENT_ATTR(pmni, NI_PMNI),
|
|
NULL
|
|
};
|
|
|
|
static const struct attribute_group arm_ni_event_attrs_group = {
|
|
.name = "events",
|
|
.attrs = arm_ni_event_attrs,
|
|
.is_visible = arm_ni_event_attr_is_visible,
|
|
};
|
|
|
|
struct arm_ni_format_attr {
|
|
struct device_attribute attr;
|
|
u64 field;
|
|
};
|
|
|
|
#define NI_FORMAT_ATTR(_name, _fld) \
|
|
(&((struct arm_ni_format_attr[]) {{ \
|
|
.attr = __ATTR(_name, 0444, arm_ni_format_show, NULL), \
|
|
.field = _fld, \
|
|
}})[0].attr.attr)
|
|
|
|
static ssize_t arm_ni_format_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct arm_ni_format_attr *fmt = container_of(attr, typeof(*fmt), attr);
|
|
|
|
return sysfs_emit(buf, "config:%*pbl\n", 64, &fmt->field);
|
|
}
|
|
|
|
static struct attribute *arm_ni_format_attrs[] = {
|
|
NI_FORMAT_ATTR(type, NI_CONFIG_TYPE),
|
|
NI_FORMAT_ATTR(nodeid, NI_CONFIG_NODEID),
|
|
NI_FORMAT_ATTR(eventid, NI_CONFIG_EVENTID),
|
|
NULL
|
|
};
|
|
|
|
static const struct attribute_group arm_ni_format_attrs_group = {
|
|
.name = "format",
|
|
.attrs = arm_ni_format_attrs,
|
|
};
|
|
|
|
static ssize_t arm_ni_cpumask_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct arm_ni_cd *cd = pmu_to_cd(dev_get_drvdata(dev));
|
|
|
|
return cpumap_print_to_pagebuf(true, buf, cpumask_of(cd->cpu));
|
|
}
|
|
|
|
static struct device_attribute arm_ni_cpumask_attr =
|
|
__ATTR(cpumask, 0444, arm_ni_cpumask_show, NULL);
|
|
|
|
static ssize_t arm_ni_identifier_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct arm_ni *ni = cd_to_ni(pmu_to_cd(dev_get_drvdata(dev)));
|
|
u32 reg = readl_relaxed(ni->base + NI_PERIPHERAL_ID2);
|
|
int version = FIELD_GET(NI_PIDR2_VERSION, reg);
|
|
|
|
return sysfs_emit(buf, "%03x%02x\n", ni->part, version);
|
|
}
|
|
|
|
static struct device_attribute arm_ni_identifier_attr =
|
|
__ATTR(identifier, 0444, arm_ni_identifier_show, NULL);
|
|
|
|
static struct attribute *arm_ni_other_attrs[] = {
|
|
&arm_ni_cpumask_attr.attr,
|
|
&arm_ni_identifier_attr.attr,
|
|
NULL
|
|
};
|
|
|
|
static const struct attribute_group arm_ni_other_attr_group = {
|
|
.attrs = arm_ni_other_attrs,
|
|
NULL
|
|
};
|
|
|
|
static const struct attribute_group *arm_ni_attr_groups[] = {
|
|
&arm_ni_event_attrs_group,
|
|
&arm_ni_format_attrs_group,
|
|
&arm_ni_other_attr_group,
|
|
NULL
|
|
};
|
|
|
|
static void arm_ni_pmu_enable(struct pmu *pmu)
|
|
{
|
|
writel_relaxed(NI_PMCR_ENABLE, pmu_to_cd(pmu)->pmu_base + NI_PMCR);
|
|
}
|
|
|
|
static void arm_ni_pmu_disable(struct pmu *pmu)
|
|
{
|
|
writel_relaxed(0, pmu_to_cd(pmu)->pmu_base + NI_PMCR);
|
|
}
|
|
|
|
struct arm_ni_val {
|
|
unsigned int evcnt;
|
|
unsigned int ccnt;
|
|
};
|
|
|
|
static bool arm_ni_val_count_event(struct perf_event *evt, struct arm_ni_val *val)
|
|
{
|
|
if (is_software_event(evt))
|
|
return true;
|
|
|
|
if (NI_EVENT_TYPE(evt) == NI_PMU) {
|
|
val->ccnt++;
|
|
return val->ccnt <= 1;
|
|
}
|
|
|
|
val->evcnt++;
|
|
return val->evcnt <= NI_NUM_COUNTERS;
|
|
}
|
|
|
|
static int arm_ni_validate_group(struct perf_event *event)
|
|
{
|
|
struct perf_event *sibling, *leader = event->group_leader;
|
|
struct arm_ni_val val = { 0 };
|
|
|
|
if (leader == event)
|
|
return 0;
|
|
|
|
arm_ni_val_count_event(event, &val);
|
|
if (!arm_ni_val_count_event(leader, &val))
|
|
return -EINVAL;
|
|
|
|
for_each_sibling_event(sibling, leader) {
|
|
if (!arm_ni_val_count_event(sibling, &val))
|
|
return -EINVAL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int arm_ni_event_init(struct perf_event *event)
|
|
{
|
|
struct arm_ni_cd *cd = pmu_to_cd(event->pmu);
|
|
|
|
if (event->attr.type != event->pmu->type)
|
|
return -ENOENT;
|
|
|
|
if (is_sampling_event(event))
|
|
return -EINVAL;
|
|
|
|
event->cpu = cd->cpu;
|
|
if (NI_EVENT_TYPE(event) == NI_PMU)
|
|
return arm_ni_validate_group(event);
|
|
|
|
cd_for_each_unit(cd, unit) {
|
|
if (unit->type == NI_EVENT_TYPE(event) &&
|
|
unit->id == NI_EVENT_NODEID(event) && unit->ns) {
|
|
event->hw.config_base = (unsigned long)unit;
|
|
return arm_ni_validate_group(event);
|
|
}
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
static u64 arm_ni_read_ccnt(struct arm_ni_cd *cd)
|
|
{
|
|
u64 l, u_old, u_new;
|
|
int retries = 3; /* 1st time unlucky, 2nd improbable, 3rd just broken */
|
|
|
|
u_new = readl_relaxed(cd->pmu_base + NI_PMCCNTR_U);
|
|
do {
|
|
u_old = u_new;
|
|
l = readl_relaxed(cd->pmu_base + NI_PMCCNTR_L);
|
|
u_new = readl_relaxed(cd->pmu_base + NI_PMCCNTR_U);
|
|
} while (u_new != u_old && --retries);
|
|
WARN_ON(!retries);
|
|
|
|
return (u_new << 32) | l;
|
|
}
|
|
|
|
static void arm_ni_event_read(struct perf_event *event)
|
|
{
|
|
struct arm_ni_cd *cd = pmu_to_cd(event->pmu);
|
|
struct hw_perf_event *hw = &event->hw;
|
|
u64 count, prev;
|
|
bool ccnt = hw->idx == NI_CCNT_IDX;
|
|
|
|
do {
|
|
prev = local64_read(&hw->prev_count);
|
|
if (ccnt)
|
|
count = arm_ni_read_ccnt(cd);
|
|
else
|
|
count = readl_relaxed(cd->pmu_base + NI_PMEVCNTR(hw->idx));
|
|
} while (local64_cmpxchg(&hw->prev_count, prev, count) != prev);
|
|
|
|
count -= prev;
|
|
if (!ccnt)
|
|
count = (u32)count;
|
|
local64_add(count, &event->count);
|
|
}
|
|
|
|
static void arm_ni_event_start(struct perf_event *event, int flags)
|
|
{
|
|
struct arm_ni_cd *cd = pmu_to_cd(event->pmu);
|
|
|
|
writel_relaxed(1U << event->hw.idx, cd->pmu_base + NI_PMCNTENSET);
|
|
}
|
|
|
|
static void arm_ni_event_stop(struct perf_event *event, int flags)
|
|
{
|
|
struct arm_ni_cd *cd = pmu_to_cd(event->pmu);
|
|
|
|
writel_relaxed(1U << event->hw.idx, cd->pmu_base + NI_PMCNTENCLR);
|
|
if (flags & PERF_EF_UPDATE)
|
|
arm_ni_event_read(event);
|
|
}
|
|
|
|
static void arm_ni_init_ccnt(struct arm_ni_cd *cd)
|
|
{
|
|
local64_set(&cd->ccnt->hw.prev_count, S64_MIN);
|
|
lo_hi_writeq_relaxed(S64_MIN, cd->pmu_base + NI_PMCCNTR_L);
|
|
}
|
|
|
|
static void arm_ni_init_evcnt(struct arm_ni_cd *cd, int idx)
|
|
{
|
|
local64_set(&cd->evcnt[idx]->hw.prev_count, S32_MIN);
|
|
writel_relaxed(S32_MIN, cd->pmu_base + NI_PMEVCNTR(idx));
|
|
}
|
|
|
|
static int arm_ni_event_add(struct perf_event *event, int flags)
|
|
{
|
|
struct arm_ni_cd *cd = pmu_to_cd(event->pmu);
|
|
struct hw_perf_event *hw = &event->hw;
|
|
struct arm_ni_unit *unit;
|
|
enum ni_node_type type = NI_EVENT_TYPE(event);
|
|
u32 reg;
|
|
|
|
if (type == NI_PMU) {
|
|
if (cd->ccnt)
|
|
return -ENOSPC;
|
|
hw->idx = NI_CCNT_IDX;
|
|
cd->ccnt = event;
|
|
arm_ni_init_ccnt(cd);
|
|
} else {
|
|
hw->idx = 0;
|
|
while (cd->evcnt[hw->idx]) {
|
|
if (++hw->idx == NI_NUM_COUNTERS)
|
|
return -ENOSPC;
|
|
}
|
|
cd->evcnt[hw->idx] = event;
|
|
unit = (void *)hw->config_base;
|
|
unit->event[hw->idx] = NI_EVENT_EVENTID(event);
|
|
arm_ni_init_evcnt(cd, hw->idx);
|
|
lo_hi_writeq_relaxed(le64_to_cpu(unit->pmusel), unit->pmusela);
|
|
|
|
reg = FIELD_PREP(NI_PMEVTYPER_NODE_TYPE, type) |
|
|
FIELD_PREP(NI_PMEVTYPER_NODE_ID, NI_EVENT_NODEID(event));
|
|
writel_relaxed(reg, cd->pmu_base + NI_PMEVTYPER(hw->idx));
|
|
}
|
|
if (flags & PERF_EF_START)
|
|
arm_ni_event_start(event, 0);
|
|
return 0;
|
|
}
|
|
|
|
static void arm_ni_event_del(struct perf_event *event, int flags)
|
|
{
|
|
struct arm_ni_cd *cd = pmu_to_cd(event->pmu);
|
|
struct hw_perf_event *hw = &event->hw;
|
|
|
|
arm_ni_event_stop(event, PERF_EF_UPDATE);
|
|
|
|
if (hw->idx == NI_CCNT_IDX)
|
|
cd->ccnt = NULL;
|
|
else
|
|
cd->evcnt[hw->idx] = NULL;
|
|
}
|
|
|
|
static irqreturn_t arm_ni_handle_irq(int irq, void *dev_id)
|
|
{
|
|
struct arm_ni_cd *cd = dev_id;
|
|
irqreturn_t ret = IRQ_NONE;
|
|
u32 reg = readl_relaxed(cd->pmu_base + NI_PMOVSCLR);
|
|
|
|
if (reg & (1U << NI_CCNT_IDX)) {
|
|
ret = IRQ_HANDLED;
|
|
if (!(WARN_ON(!cd->ccnt))) {
|
|
arm_ni_event_read(cd->ccnt);
|
|
arm_ni_init_ccnt(cd);
|
|
}
|
|
}
|
|
for (int i = 0; i < NI_NUM_COUNTERS; i++) {
|
|
if (!(reg & (1U << i)))
|
|
continue;
|
|
ret = IRQ_HANDLED;
|
|
if (!(WARN_ON(!cd->evcnt[i]))) {
|
|
arm_ni_event_read(cd->evcnt[i]);
|
|
arm_ni_init_evcnt(cd, i);
|
|
}
|
|
}
|
|
writel_relaxed(reg, cd->pmu_base + NI_PMOVSCLR);
|
|
return ret;
|
|
}
|
|
|
|
static int arm_ni_init_cd(struct arm_ni *ni, struct arm_ni_node *node, u64 res_start)
|
|
{
|
|
struct arm_ni_cd *cd = ni->cds + node->id;
|
|
const char *name;
|
|
int err;
|
|
|
|
cd->id = node->id;
|
|
cd->num_units = node->num_components;
|
|
cd->units = devm_kcalloc(ni->dev, cd->num_units, sizeof(*(cd->units)), GFP_KERNEL);
|
|
if (!cd->units)
|
|
return -ENOMEM;
|
|
|
|
for (int i = 0; i < cd->num_units; i++) {
|
|
u32 reg = readl_relaxed(node->base + NI_CHILD_PTR(i));
|
|
void __iomem *unit_base = ni->base + reg;
|
|
struct arm_ni_unit *unit = cd->units + i;
|
|
|
|
reg = readl_relaxed(unit_base + NI_NODE_TYPE);
|
|
unit->type = FIELD_GET(NI_NODE_TYPE_NODE_TYPE, reg);
|
|
unit->id = FIELD_GET(NI_NODE_TYPE_NODE_ID, reg);
|
|
|
|
switch (unit->type) {
|
|
case NI_PMU:
|
|
reg = readl_relaxed(unit_base + NI_PMCFGR);
|
|
if (!reg) {
|
|
dev_info(ni->dev, "No access to PMU %d\n", cd->id);
|
|
devm_kfree(ni->dev, cd->units);
|
|
return 0;
|
|
}
|
|
unit->ns = true;
|
|
cd->pmu_base = unit_base;
|
|
break;
|
|
case NI_ASNI:
|
|
case NI_AMNI:
|
|
case NI_HSNI:
|
|
case NI_HMNI:
|
|
case NI_PMNI:
|
|
unit->pmusela = unit_base + NI700_PMUSELA;
|
|
writel_relaxed(1, unit->pmusela);
|
|
if (readl_relaxed(unit->pmusela) != 1)
|
|
dev_info(ni->dev, "No access to node 0x%04x%04x\n", unit->id, unit->type);
|
|
else
|
|
unit->ns = true;
|
|
break;
|
|
default:
|
|
/*
|
|
* e.g. FMU - thankfully bits 3:2 of FMU_ERR_FR0 are RES0 so
|
|
* can't alias any of the leaf node types we're looking for.
|
|
*/
|
|
dev_dbg(ni->dev, "Mystery node 0x%04x%04x\n", unit->id, unit->type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
res_start += cd->pmu_base - ni->base;
|
|
if (!devm_request_mem_region(ni->dev, res_start, SZ_4K, dev_name(ni->dev))) {
|
|
dev_err(ni->dev, "Failed to request PMU region 0x%llx\n", res_start);
|
|
return -EBUSY;
|
|
}
|
|
|
|
writel_relaxed(NI_PMCR_RESET_CCNT | NI_PMCR_RESET_EVCNT,
|
|
cd->pmu_base + NI_PMCR);
|
|
writel_relaxed(U32_MAX, cd->pmu_base + NI_PMCNTENCLR);
|
|
writel_relaxed(U32_MAX, cd->pmu_base + NI_PMOVSCLR);
|
|
writel_relaxed(U32_MAX, cd->pmu_base + NI_PMINTENSET);
|
|
|
|
cd->irq = platform_get_irq(to_platform_device(ni->dev), cd->id);
|
|
if (cd->irq < 0)
|
|
return cd->irq;
|
|
|
|
err = devm_request_irq(ni->dev, cd->irq, arm_ni_handle_irq,
|
|
IRQF_NOBALANCING | IRQF_NO_THREAD,
|
|
dev_name(ni->dev), cd);
|
|
if (err)
|
|
return err;
|
|
|
|
cd->cpu = cpumask_local_spread(0, dev_to_node(ni->dev));
|
|
cd->pmu = (struct pmu) {
|
|
.module = THIS_MODULE,
|
|
.parent = ni->dev,
|
|
.attr_groups = arm_ni_attr_groups,
|
|
.capabilities = PERF_PMU_CAP_NO_EXCLUDE,
|
|
.task_ctx_nr = perf_invalid_context,
|
|
.pmu_enable = arm_ni_pmu_enable,
|
|
.pmu_disable = arm_ni_pmu_disable,
|
|
.event_init = arm_ni_event_init,
|
|
.add = arm_ni_event_add,
|
|
.del = arm_ni_event_del,
|
|
.start = arm_ni_event_start,
|
|
.stop = arm_ni_event_stop,
|
|
.read = arm_ni_event_read,
|
|
};
|
|
|
|
name = devm_kasprintf(ni->dev, GFP_KERNEL, "arm_ni_%d_cd_%d", ni->id, cd->id);
|
|
if (!name)
|
|
return -ENOMEM;
|
|
|
|
err = cpuhp_state_add_instance_nocalls(arm_ni_hp_state, &cd->cpuhp_node);
|
|
if (err)
|
|
return err;
|
|
|
|
err = perf_pmu_register(&cd->pmu, name, -1);
|
|
if (err)
|
|
cpuhp_state_remove_instance_nocalls(arm_ni_hp_state, &cd->cpuhp_node);
|
|
|
|
return err;
|
|
}
|
|
|
|
static void arm_ni_probe_domain(void __iomem *base, struct arm_ni_node *node)
|
|
{
|
|
u32 reg = readl_relaxed(base + NI_NODE_TYPE);
|
|
|
|
node->base = base;
|
|
node->type = FIELD_GET(NI_NODE_TYPE_NODE_TYPE, reg);
|
|
node->id = FIELD_GET(NI_NODE_TYPE_NODE_ID, reg);
|
|
node->num_components = readl_relaxed(base + NI_CHILD_NODE_INFO);
|
|
}
|
|
|
|
static int arm_ni_probe(struct platform_device *pdev)
|
|
{
|
|
struct arm_ni_node cfg, vd, pd, cd;
|
|
struct arm_ni *ni;
|
|
struct resource *res;
|
|
void __iomem *base;
|
|
static atomic_t id;
|
|
int num_cds;
|
|
u32 reg, part;
|
|
|
|
/*
|
|
* We want to map the whole configuration space for ease of discovery,
|
|
* but the PMU pages are the only ones for which we can honestly claim
|
|
* exclusive ownership, so we'll request them explicitly once found.
|
|
*/
|
|
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
|
|
base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
|
|
if (!base)
|
|
return -ENOMEM;
|
|
|
|
arm_ni_probe_domain(base, &cfg);
|
|
if (cfg.type != NI_GLOBAL)
|
|
return -ENODEV;
|
|
|
|
reg = readl_relaxed(cfg.base + NI_PERIPHERAL_ID0);
|
|
part = FIELD_GET(NI_PIDR0_PART_7_0, reg);
|
|
reg = readl_relaxed(cfg.base + NI_PERIPHERAL_ID1);
|
|
part |= FIELD_GET(NI_PIDR1_PART_11_8, reg) << 8;
|
|
|
|
switch (part) {
|
|
case PART_NI_700:
|
|
case PART_NI_710AE:
|
|
break;
|
|
default:
|
|
dev_WARN(&pdev->dev, "Unknown part number: 0x%03x, this may go badly\n", part);
|
|
break;
|
|
}
|
|
|
|
num_cds = 0;
|
|
for (int v = 0; v < cfg.num_components; v++) {
|
|
reg = readl_relaxed(cfg.base + NI_CHILD_PTR(v));
|
|
arm_ni_probe_domain(base + reg, &vd);
|
|
for (int p = 0; p < vd.num_components; p++) {
|
|
reg = readl_relaxed(vd.base + NI_CHILD_PTR(p));
|
|
arm_ni_probe_domain(base + reg, &pd);
|
|
num_cds += pd.num_components;
|
|
}
|
|
}
|
|
|
|
ni = devm_kzalloc(&pdev->dev, struct_size(ni, cds, num_cds), GFP_KERNEL);
|
|
if (!ni)
|
|
return -ENOMEM;
|
|
|
|
ni->dev = &pdev->dev;
|
|
ni->base = base;
|
|
ni->num_cds = num_cds;
|
|
ni->part = part;
|
|
ni->id = atomic_fetch_inc(&id);
|
|
|
|
for (int v = 0; v < cfg.num_components; v++) {
|
|
reg = readl_relaxed(cfg.base + NI_CHILD_PTR(v));
|
|
arm_ni_probe_domain(base + reg, &vd);
|
|
for (int p = 0; p < vd.num_components; p++) {
|
|
reg = readl_relaxed(vd.base + NI_CHILD_PTR(p));
|
|
arm_ni_probe_domain(base + reg, &pd);
|
|
for (int c = 0; c < pd.num_components; c++) {
|
|
int ret;
|
|
|
|
reg = readl_relaxed(pd.base + NI_CHILD_PTR(c));
|
|
arm_ni_probe_domain(base + reg, &cd);
|
|
ret = arm_ni_init_cd(ni, &cd, res->start);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void arm_ni_remove(struct platform_device *pdev)
|
|
{
|
|
struct arm_ni *ni = platform_get_drvdata(pdev);
|
|
|
|
for (int i = 0; i < ni->num_cds; i++) {
|
|
struct arm_ni_cd *cd = ni->cds + i;
|
|
|
|
if (!cd->pmu_base)
|
|
continue;
|
|
|
|
writel_relaxed(0, cd->pmu_base + NI_PMCR);
|
|
writel_relaxed(U32_MAX, cd->pmu_base + NI_PMINTENCLR);
|
|
perf_pmu_unregister(&cd->pmu);
|
|
cpuhp_state_remove_instance_nocalls(arm_ni_hp_state, &cd->cpuhp_node);
|
|
}
|
|
}
|
|
|
|
#ifdef CONFIG_OF
|
|
static const struct of_device_id arm_ni_of_match[] = {
|
|
{ .compatible = "arm,ni-700" },
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(of, arm_ni_of_match);
|
|
#endif
|
|
|
|
#ifdef CONFIG_ACPI
|
|
static const struct acpi_device_id arm_ni_acpi_match[] = {
|
|
{ "ARMHCB70" },
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(acpi, arm_ni_acpi_match);
|
|
#endif
|
|
|
|
static struct platform_driver arm_ni_driver = {
|
|
.driver = {
|
|
.name = "arm-ni",
|
|
.of_match_table = of_match_ptr(arm_ni_of_match),
|
|
.acpi_match_table = ACPI_PTR(arm_ni_acpi_match),
|
|
},
|
|
.probe = arm_ni_probe,
|
|
.remove = arm_ni_remove,
|
|
};
|
|
|
|
static void arm_ni_pmu_migrate(struct arm_ni_cd *cd, unsigned int cpu)
|
|
{
|
|
perf_pmu_migrate_context(&cd->pmu, cd->cpu, cpu);
|
|
irq_set_affinity(cd->irq, cpumask_of(cpu));
|
|
cd->cpu = cpu;
|
|
}
|
|
|
|
static int arm_ni_pmu_online_cpu(unsigned int cpu, struct hlist_node *cpuhp_node)
|
|
{
|
|
struct arm_ni_cd *cd;
|
|
int node;
|
|
|
|
cd = hlist_entry_safe(cpuhp_node, struct arm_ni_cd, cpuhp_node);
|
|
node = dev_to_node(cd_to_ni(cd)->dev);
|
|
if (cpu_to_node(cd->cpu) != node && cpu_to_node(cpu) == node)
|
|
arm_ni_pmu_migrate(cd, cpu);
|
|
return 0;
|
|
}
|
|
|
|
static int arm_ni_pmu_offline_cpu(unsigned int cpu, struct hlist_node *cpuhp_node)
|
|
{
|
|
struct arm_ni_cd *cd;
|
|
unsigned int target;
|
|
int node;
|
|
|
|
cd = hlist_entry_safe(cpuhp_node, struct arm_ni_cd, cpuhp_node);
|
|
if (cpu != cd->cpu)
|
|
return 0;
|
|
|
|
node = dev_to_node(cd_to_ni(cd)->dev);
|
|
target = cpumask_any_and_but(cpumask_of_node(node), cpu_online_mask, cpu);
|
|
if (target >= nr_cpu_ids)
|
|
target = cpumask_any_but(cpu_online_mask, cpu);
|
|
|
|
if (target < nr_cpu_ids)
|
|
arm_ni_pmu_migrate(cd, target);
|
|
return 0;
|
|
}
|
|
|
|
static int __init arm_ni_init(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN,
|
|
"perf/arm/ni:online",
|
|
arm_ni_pmu_online_cpu,
|
|
arm_ni_pmu_offline_cpu);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
arm_ni_hp_state = ret;
|
|
|
|
ret = platform_driver_register(&arm_ni_driver);
|
|
if (ret)
|
|
cpuhp_remove_multi_state(arm_ni_hp_state);
|
|
return ret;
|
|
}
|
|
|
|
static void __exit arm_ni_exit(void)
|
|
{
|
|
platform_driver_unregister(&arm_ni_driver);
|
|
cpuhp_remove_multi_state(arm_ni_hp_state);
|
|
}
|
|
|
|
module_init(arm_ni_init);
|
|
module_exit(arm_ni_exit);
|
|
|
|
MODULE_AUTHOR("Robin Murphy <robin.murphy@arm.com>");
|
|
MODULE_DESCRIPTION("Arm NI-700 PMU driver");
|
|
MODULE_LICENSE("GPL v2");
|