mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2025-08-05 16:54:27 +00:00

NI-700 has a distinct PMU interrupt output for each Clock Domain, however some integrations may still combine these together externally. The initial driver didn't attempt to support this, in anticipation of a more general solution for IRQ sharing between system PMU instances, but that's still a way off, so let's make this intermediate step for now to at least allow sharing IRQs within an individual NI instance. Now that CPU affinity and migration are cleaned up, it's fairly straightforward to adopt similar logic to arm-cmn, to identify CDs with a common interrupt and loop over them directly in the handler. Signed-off-by: Shouping Wang <allen.wang@hj-micro.com> [ rm: Rework for affinity handling, cosmetics, new commit message ] Signed-off-by: Robin Murphy <robin.murphy@arm.com> Link: https://lore.kernel.org/r/f62db639d3b54c959ec477db7b8ccecbef1ca310.1752256072.git.robin.murphy@arm.com Signed-off-by: Will Deacon <will@kernel.org>
809 lines
20 KiB
C
809 lines
20 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;
|
|
s8 irq_friend;
|
|
int num_units;
|
|
int irq;
|
|
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 cpu;
|
|
int num_cds;
|
|
struct hlist_node cpuhp_node;
|
|
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 ni_for_each_cd(n, c) \
|
|
for (struct arm_ni_cd *c = n->cds; c < n->cds + n->num_cds; c++) if (c->pmu_base)
|
|
|
|
#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 *ni = cd_to_ni(pmu_to_cd(dev_get_drvdata(dev)));
|
|
|
|
return cpumap_print_to_pagebuf(true, buf, cpumask_of(ni->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,
|
|
};
|
|
|
|
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_to_ni(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;
|
|
|
|
for (;;) {
|
|
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);
|
|
if (!cd->irq_friend)
|
|
return ret;
|
|
cd += cd->irq_friend;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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);
|
|
|
|
cd->irq = platform_get_irq(to_platform_device(ni->dev), cd->id);
|
|
if (cd->irq < 0)
|
|
return cd->irq;
|
|
|
|
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;
|
|
|
|
return perf_pmu_register(&cd->pmu, name, -1);
|
|
}
|
|
|
|
static void arm_ni_remove(struct platform_device *pdev)
|
|
{
|
|
struct arm_ni *ni = platform_get_drvdata(pdev);
|
|
|
|
ni_for_each_cd(ni, cd) {
|
|
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, &ni->cpuhp_node);
|
|
}
|
|
|
|
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_init_irqs(struct arm_ni *ni)
|
|
{
|
|
int err;
|
|
|
|
ni_for_each_cd(ni, cd) {
|
|
for (struct arm_ni_cd *prev = cd; prev-- > ni->cds; ) {
|
|
if (prev->irq == cd->irq) {
|
|
prev->irq_friend = cd - prev;
|
|
goto set_inten;
|
|
}
|
|
}
|
|
err = devm_request_irq(ni->dev, cd->irq, arm_ni_handle_irq,
|
|
IRQF_NOBALANCING | IRQF_NO_THREAD | IRQF_NO_AUTOEN,
|
|
dev_name(ni->dev), cd);
|
|
if (err)
|
|
return err;
|
|
|
|
irq_set_affinity(cd->irq, cpumask_of(ni->cpu));
|
|
set_inten:
|
|
writel_relaxed(U32_MAX, cd->pmu_base + NI_PMINTENSET);
|
|
}
|
|
|
|
ni_for_each_cd(ni, cd)
|
|
if (!cd->irq_friend)
|
|
enable_irq(cd->irq);
|
|
return 0;
|
|
}
|
|
|
|
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 ret, 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);
|
|
ni->cpu = cpumask_local_spread(0, dev_to_node(ni->dev));
|
|
platform_set_drvdata(pdev, ni);
|
|
|
|
ret = cpuhp_state_add_instance_nocalls(arm_ni_hp_state, &ni->cpuhp_node);
|
|
if (ret)
|
|
return ret;
|
|
|
|
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++) {
|
|
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) {
|
|
ni->cds[cd.id].pmu_base = NULL;
|
|
arm_ni_remove(pdev);
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ret = arm_ni_init_irqs(ni);
|
|
if (ret)
|
|
arm_ni_remove(pdev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
#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),
|
|
.suppress_bind_attrs = true,
|
|
},
|
|
.probe = arm_ni_probe,
|
|
.remove = arm_ni_remove,
|
|
};
|
|
|
|
static void arm_ni_pmu_migrate(struct arm_ni *ni, unsigned int cpu)
|
|
{
|
|
ni_for_each_cd(ni, cd) {
|
|
perf_pmu_migrate_context(&cd->pmu, ni->cpu, cpu);
|
|
irq_set_affinity(cd->irq, cpumask_of(cpu));
|
|
}
|
|
ni->cpu = cpu;
|
|
}
|
|
|
|
static int arm_ni_pmu_online_cpu(unsigned int cpu, struct hlist_node *cpuhp_node)
|
|
{
|
|
struct arm_ni *ni;
|
|
int node;
|
|
|
|
ni = hlist_entry_safe(cpuhp_node, struct arm_ni, cpuhp_node);
|
|
node = dev_to_node(ni->dev);
|
|
if (cpu_to_node(ni->cpu) != node && cpu_to_node(cpu) == node)
|
|
arm_ni_pmu_migrate(ni, cpu);
|
|
return 0;
|
|
}
|
|
|
|
static int arm_ni_pmu_offline_cpu(unsigned int cpu, struct hlist_node *cpuhp_node)
|
|
{
|
|
struct arm_ni *ni;
|
|
unsigned int target;
|
|
int node;
|
|
|
|
ni = hlist_entry_safe(cpuhp_node, struct arm_ni, cpuhp_node);
|
|
if (cpu != ni->cpu)
|
|
return 0;
|
|
|
|
node = dev_to_node(ni->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(ni, 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");
|