| // 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(ðosu->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(ðosu->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)(ðosu->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(ðosu_priv->perfmons); | |
| perfmon = xa_load(ðosu_priv->perfmons, id); | |
| ethosu_perfmon_get(perfmon); | |
| xa_unlock(ðosu_priv->perfmons); | |
| return perfmon; | |
| } | |
| void ethosu_perfmon_open_file(struct ethosu_file_priv *ethosu_priv) | |
| { | |
| xa_init_flags(ðosu_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, ðosu->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(ðosu_priv->perfmons, id, perfmon) | |
| ethosu_perfmon_delete(ethosu_priv, perfmon); | |
| xa_destroy(ðosu_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(ðosu_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(ðosu_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, ðosu->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, ðosu->perfmon_state.lock) { | |
| if (ethosu->perfmon_state.active || ethosu->global_perfmon) { | |
| ethosu_perfmon_put(perfmon); | |
| return -EBUSY; | |
| } | |
| ethosu->global_perfmon = perfmon; | |
| } | |
| return 0; | |
| } |