linux/drivers/accel/ethosu/ethosu_perfmon.c
Rob Herring (Arm) 17cdb54644 accel: ethosu: Add performance counter support
The Arm Ethos-U NPUs have a PMU with performance counters. The PMU h/w
supports up to 4 (U65) or 8 (U85) counters which can be programmed for
different events. There is also a dedicated cycle counter.

The ABI and implementation are copied from the V3D driver. The main
difference in the ABI is there is no query API for the event list. The
events differ between the U65 and U85, so the events lists are
maintained in userspace along with other differences between the U65 and
U85.

The cycle counter is always enabled when the PMU is enabled. When the
user requests N events, reading the counters will return the N events
plus the cycle counter.

Signed-off-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Tomeu Vizoso <tomeu@tomeuvizoso.net>
Reviewed-by: Maíra Canal <mcanal@igalia.com>
Link: https://patch.msgid.link/20260601173814.250071-1-tomeu@tomeuvizoso.net

---
v2:
 - Use XArray instead of idr
 - Rework locking to use per device spinlock to protect modifying active
   perfmon. Based on pending V3D changes:
   https://lore.kernel.org/all/20260508-v3d-perfmon-lifetime-v1-1-f5b5642c085f@igalia.com/
 - Add missing perfmon puts in ethosu_ioctl_perfmon_set_global() and
   ethosu_ioctl_perfmon_get_values() error paths.
 - Fix reading number of counters on U85.
 - Add defines NPU_REG_PMCCNTR_CFG

v3:
 - Add explicit padding to drm_ethosu_perfmon_destroy
 - Fix SPDX license expression
 - Fix comment typos
 - Convert perfmon lock from spinlock to mutex
 - Simplify switch_perfmon condition check
 - Remove unused ethosu_perfmon_init
 - Add lockdep_assert_held to ethosu_perfmon_stop_locked

v4:
 - Use drmm_mutex_init() for perfmon lock
 - Add lockdep_assert_held() to ethosu_perfmon_start()
 - Fix a few style issues reported by Maíra
2026-06-01 19:44:24 +02:00

302 lines
7.3 KiB
C

// SPDX-License-Identifier: GPL-2.0-only OR MIT
/* Copyright 2026 Arm, Ltd. */
/* Based on v3d_perfmon.c, Copyright (C) 2021 Raspberry Pi */
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/pm_runtime.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <drm/drm_file.h>
#include <drm/drm_ioctl.h>
#include <uapi/drm/ethosu_accel.h>
#include "ethosu_drv.h"
#include "ethosu_device.h"
void ethosu_perfmon_get(struct ethosu_perfmon *perfmon)
{
if (perfmon)
refcount_inc(&perfmon->refcnt);
}
void ethosu_perfmon_put(struct ethosu_perfmon *perfmon)
{
if (perfmon && refcount_dec_and_test(&perfmon->refcnt))
kfree(perfmon);
}
void ethosu_perfmon_start(struct ethosu_device *ethosu, struct ethosu_perfmon *perfmon)
{
unsigned int i;
u8 ncounters;
u32 mask;
lockdep_assert_held(&ethosu->perfmon_state.lock);
if (WARN_ON_ONCE(!perfmon || ethosu->perfmon_state.active))
return;
writel_relaxed(PMCR_CNT_EN, ethosu->pmu_regs + NPU_REG_PMCR);
writel_relaxed(PMU_EV_TYPE_CYCLES, ethosu->pmu_regs + NPU_REG_PMCCNTR_CFG);
mask = 0x80000000;
ncounters = perfmon->ncounters - 1;
if (ncounters)
mask |= GENMASK(ncounters - 1, 0);
for (i = 0; i < ncounters; i++)
writel_relaxed(perfmon->counters[i], ethosu->pmu_regs + NPU_REG_PMU_EVTYPER(i));
writel_relaxed(mask, ethosu->pmu_regs + NPU_REG_PMCNTENSET);
writel_relaxed(PMCR_CNT_EN | PMCR_EVENT_CNT_RST | PMCR_CYCLE_CNT_RST,
ethosu->pmu_regs + NPU_REG_PMCR);
ethosu->perfmon_state.active = perfmon;
}
void ethosu_perfmon_stop_locked(struct ethosu_device *ethosu, struct ethosu_perfmon *perfmon,
bool capture)
{
unsigned int i;
u8 ncounters;
u32 mask;
lockdep_assert_held(&ethosu->perfmon_state.lock);
if (!perfmon || perfmon != ethosu->perfmon_state.active)
return;
ncounters = perfmon->ncounters - 1;
if (!pm_runtime_get_if_active(ethosu->base.dev)) {
ethosu->perfmon_state.active = NULL;
return;
}
if (capture) {
for (i = 0; i < ncounters; i++)
perfmon->values[i] += readl_relaxed(ethosu->pmu_regs + NPU_REG_PMU_EVCNTR(i));
perfmon->values[ncounters] +=
readl_relaxed(ethosu->pmu_regs + NPU_REG_PMCCNTR_LO) |
(u64)readl_relaxed(ethosu->pmu_regs + NPU_REG_PMCCNTR_HI) << 32;
}
mask = 0x80000000;
if (ncounters)
mask |= GENMASK(ncounters - 1, 0);
writel_relaxed(mask, ethosu->pmu_regs + NPU_REG_PMCNTENCLR);
writel_relaxed(0, ethosu->pmu_regs + NPU_REG_PMCR);
ethosu->perfmon_state.active = NULL;
pm_runtime_put(ethosu->base.dev);
}
void ethosu_perfmon_stop(struct ethosu_device *ethosu, struct ethosu_perfmon *perfmon,
bool capture)
{
if (!perfmon)
return;
guard(mutex)(&ethosu->perfmon_state.lock);
ethosu_perfmon_stop_locked(ethosu, perfmon, capture);
}
struct ethosu_perfmon *ethosu_perfmon_find(struct ethosu_file_priv *ethosu_priv, int id)
{
struct ethosu_perfmon *perfmon;
xa_lock(&ethosu_priv->perfmons);
perfmon = xa_load(&ethosu_priv->perfmons, id);
ethosu_perfmon_get(perfmon);
xa_unlock(&ethosu_priv->perfmons);
return perfmon;
}
void ethosu_perfmon_open_file(struct ethosu_file_priv *ethosu_priv)
{
xa_init_flags(&ethosu_priv->perfmons, XA_FLAGS_ALLOC1);
}
static void ethosu_perfmon_delete(struct ethosu_file_priv *ethosu_priv,
struct ethosu_perfmon *perfmon)
{
struct ethosu_device *ethosu = ethosu_priv->edev;
/* If the active perfmon is being destroyed, stop it first */
scoped_guard(mutex, &ethosu->perfmon_state.lock) {
/* If the global perfmon is being destroyed, set it to NULL */
if (ethosu->global_perfmon == perfmon) {
ethosu->global_perfmon = NULL;
ethosu_perfmon_put(perfmon);
}
ethosu_perfmon_stop_locked(ethosu, perfmon, false);
}
ethosu_perfmon_put(perfmon);
}
void ethosu_perfmon_close_file(struct ethosu_file_priv *ethosu_priv)
{
struct ethosu_perfmon *perfmon;
unsigned long id;
xa_for_each(&ethosu_priv->perfmons, id, perfmon)
ethosu_perfmon_delete(ethosu_priv, perfmon);
xa_destroy(&ethosu_priv->perfmons);
}
int ethosu_ioctl_perfmon_create(struct drm_device *dev, void *data,
struct drm_file *file_priv)
{
struct ethosu_file_priv *ethosu_priv = file_priv->driver_priv;
struct drm_ethosu_perfmon_create *req = data;
struct ethosu_device *ethosu = to_ethosu_device(dev);
struct ethosu_perfmon *perfmon;
unsigned int i, event_max;
int ret;
u32 id;
/* Number of monitored counters cannot exceed HW limits. */
if (req->ncounters > ethosu->npu_info.pmu_counters)
return -EINVAL;
/* Make sure all counters are valid. */
event_max = ethosu_is_u65(ethosu) ? 433 : 671;
for (i = 0; i < req->ncounters; i++) {
if (req->counters[i] > event_max)
return -EINVAL;
}
/* Add 1 more counter for cycle counter */
req->ncounters++;
perfmon = kzalloc_flex(*perfmon, values, req->ncounters);
if (!perfmon)
return -ENOMEM;
for (i = 0; i < req->ncounters - 1; i++)
perfmon->counters[i] = req->counters[i];
perfmon->ncounters = req->ncounters;
refcount_set(&perfmon->refcnt, 1);
ret = xa_alloc(&ethosu_priv->perfmons, &id, perfmon, xa_limit_32b,
GFP_KERNEL);
if (ret < 0) {
kfree(perfmon);
return ret;
}
req->id = id;
return 0;
}
int ethosu_ioctl_perfmon_destroy(struct drm_device *dev, void *data,
struct drm_file *file_priv)
{
struct ethosu_file_priv *ethosu_priv = file_priv->driver_priv;
struct drm_ethosu_perfmon_destroy *req = data;
struct ethosu_perfmon *perfmon;
perfmon = xa_erase(&ethosu_priv->perfmons, req->id);
if (!perfmon)
return -EINVAL;
ethosu_perfmon_delete(ethosu_priv, perfmon);
return 0;
}
int ethosu_ioctl_perfmon_get_values(struct drm_device *dev, void *data,
struct drm_file *file_priv)
{
struct ethosu_device *ethosu = to_ethosu_device(dev);
struct ethosu_file_priv *ethosu_priv = file_priv->driver_priv;
struct drm_ethosu_perfmon_get_values *req = data;
struct ethosu_perfmon *perfmon;
int ret = 0;
if (req->pad != 0)
return -EINVAL;
perfmon = ethosu_perfmon_find(ethosu_priv, req->id);
if (!perfmon)
return -EINVAL;
ret = pm_runtime_resume_and_get(dev->dev);
if (ret) {
ethosu_perfmon_put(perfmon);
return ret;
}
ethosu_perfmon_stop(ethosu, perfmon, true);
pm_runtime_put_autosuspend(dev->dev);
if (copy_to_user(u64_to_user_ptr(req->values_ptr), perfmon->values,
perfmon->ncounters * sizeof(u64)))
ret = -EFAULT;
ethosu_perfmon_put(perfmon);
return ret;
}
int ethosu_ioctl_perfmon_set_global(struct drm_device *dev, void *data,
struct drm_file *file_priv)
{
struct ethosu_file_priv *ethosu_priv = file_priv->driver_priv;
struct drm_ethosu_perfmon_set_global *req = data;
struct ethosu_device *ethosu = to_ethosu_device(dev);
struct ethosu_perfmon *perfmon;
if (req->flags & ~DRM_ETHOSU_PERFMON_CLEAR_GLOBAL)
return -EINVAL;
perfmon = ethosu_perfmon_find(ethosu_priv, req->id);
if (!perfmon)
return -EINVAL;
/* If the request is to clear the global performance monitor */
if (req->flags & DRM_ETHOSU_PERFMON_CLEAR_GLOBAL) {
struct ethosu_perfmon *old;
scoped_guard(mutex, &ethosu->perfmon_state.lock) {
old = ethosu->global_perfmon;
if (!old) {
ethosu_perfmon_put(perfmon);
return -EINVAL;
}
ethosu->global_perfmon = NULL;
ethosu_perfmon_stop_locked(ethosu, old, true);
}
ethosu_perfmon_put(old);
ethosu_perfmon_put(perfmon);
return 0;
}
scoped_guard(mutex, &ethosu->perfmon_state.lock) {
if (ethosu->perfmon_state.active || ethosu->global_perfmon) {
ethosu_perfmon_put(perfmon);
return -EBUSY;
}
ethosu->global_perfmon = perfmon;
}
return 0;
}