| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Intel Platform Monitory Technology Discovery driver |
| * |
| * Copyright (c) 2025, Intel Corporation. |
| * All Rights Reserved. |
| */ |
| |
| #include <linux/auxiliary_bus.h> |
| #include <linux/bitfield.h> |
| #include <linux/bits.h> |
| #include <linux/bug.h> |
| #include <linux/cleanup.h> |
| #include <linux/container_of.h> |
| #include <linux/device.h> |
| #include <linux/err.h> |
| #include <linux/io.h> |
| #include <linux/ioport.h> |
| #include <linux/kdev_t.h> |
| #include <linux/kobject.h> |
| #include <linux/list.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/overflow.h> |
| #include <linux/pci.h> |
| #include <linux/slab.h> |
| #include <linux/string_choices.h> |
| #include <linux/sysfs.h> |
| #include <linux/types.h> |
| #include <linux/uaccess.h> |
| |
| #include <linux/intel_pmt_features.h> |
| #include <linux/intel_vsec.h> |
| |
| #include "class.h" |
| |
| #define MAX_FEATURE_VERSION 0 |
| #define DT_TBIR GENMASK(2, 0) |
| #define FEAT_ATTR_SIZE(x) ((x) * sizeof(u32)) |
| #define PMT_GUID_SIZE(x) ((x) * sizeof(u32)) |
| #define PMT_ACCESS_TYPE_RSVD 0xF |
| #define SKIP_FEATURE 1 |
| |
| struct feature_discovery_table { |
| u32 access_type:4; |
| u32 version:8; |
| u32 size:16; |
| u32 reserved:4; |
| u32 id; |
| u32 offset; |
| u32 reserved2; |
| }; |
| |
| /* Common feature table header */ |
| struct feature_header { |
| u32 attr_size:8; |
| u32 num_guids:8; |
| u32 reserved:16; |
| }; |
| |
| /* Feature attribute fields */ |
| struct caps { |
| u32 caps; |
| }; |
| |
| struct command { |
| u32 max_stream_size:16; |
| u32 max_command_size:16; |
| }; |
| |
| struct watcher { |
| u32 reserved:21; |
| u32 period:11; |
| struct command command; |
| }; |
| |
| struct rmid { |
| u32 num_rmids:16; /* Number of Resource Monitoring IDs */ |
| u32 reserved:16; |
| struct watcher watcher; |
| }; |
| |
| struct feature_table { |
| struct feature_header header; |
| struct caps caps; |
| union { |
| struct command command; |
| struct watcher watcher; |
| struct rmid rmid; |
| }; |
| u32 *guids; |
| }; |
| |
| /* For backreference in struct feature */ |
| struct pmt_features_priv; |
| |
| struct feature { |
| struct feature_table table; |
| struct kobject kobj; |
| struct pmt_features_priv *priv; |
| struct list_head list; |
| const struct attribute_group *attr_group; |
| enum pmt_feature_id id; |
| }; |
| |
| struct pmt_features_priv { |
| struct device *parent; |
| struct device *dev; |
| int count; |
| u32 mask; |
| struct feature feature[]; |
| }; |
| |
| static LIST_HEAD(pmt_feature_list); |
| static DEFINE_MUTEX(feature_list_lock); |
| |
| #define to_pmt_feature(x) container_of(x, struct feature, kobj) |
| static void pmt_feature_release(struct kobject *kobj) |
| { |
| } |
| |
| static ssize_t caps_show(struct kobject *kobj, struct kobj_attribute *attr, |
| char *buf) |
| { |
| struct feature *feature = to_pmt_feature(kobj); |
| struct pmt_cap **pmt_caps; |
| u32 caps = feature->table.caps.caps; |
| ssize_t ret = 0; |
| |
| switch (feature->id) { |
| case FEATURE_PER_CORE_PERF_TELEM: |
| pmt_caps = pmt_caps_pcpt; |
| break; |
| case FEATURE_PER_CORE_ENV_TELEM: |
| pmt_caps = pmt_caps_pcet; |
| break; |
| case FEATURE_PER_RMID_PERF_TELEM: |
| pmt_caps = pmt_caps_rmid_perf; |
| break; |
| case FEATURE_ACCEL_TELEM: |
| pmt_caps = pmt_caps_accel; |
| break; |
| case FEATURE_UNCORE_TELEM: |
| pmt_caps = pmt_caps_uncore; |
| break; |
| case FEATURE_CRASH_LOG: |
| pmt_caps = pmt_caps_crashlog; |
| break; |
| case FEATURE_PETE_LOG: |
| pmt_caps = pmt_caps_pete; |
| break; |
| case FEATURE_TPMI_CTRL: |
| pmt_caps = pmt_caps_tpmi; |
| break; |
| case FEATURE_TRACING: |
| pmt_caps = pmt_caps_tracing; |
| break; |
| case FEATURE_PER_RMID_ENERGY_TELEM: |
| pmt_caps = pmt_caps_rmid_energy; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| while (*pmt_caps) { |
| struct pmt_cap *pmt_cap = *pmt_caps; |
| |
| while (pmt_cap->name) { |
| ret += sysfs_emit_at(buf, ret, "%-40s Available: %s\n", pmt_cap->name, |
| str_yes_no(pmt_cap->mask & caps)); |
| pmt_cap++; |
| } |
| pmt_caps++; |
| } |
| |
| return ret; |
| } |
| static struct kobj_attribute caps_attribute = __ATTR_RO(caps); |
| |
| static struct watcher *get_watcher(struct feature *feature) |
| { |
| switch (feature_layout[feature->id]) { |
| case LAYOUT_RMID: |
| return &feature->table.rmid.watcher; |
| case LAYOUT_WATCHER: |
| return &feature->table.watcher; |
| default: |
| return ERR_PTR(-EINVAL); |
| } |
| } |
| |
| static struct command *get_command(struct feature *feature) |
| { |
| switch (feature_layout[feature->id]) { |
| case LAYOUT_RMID: |
| return &feature->table.rmid.watcher.command; |
| case LAYOUT_WATCHER: |
| return &feature->table.watcher.command; |
| case LAYOUT_COMMAND: |
| return &feature->table.command; |
| default: |
| return ERR_PTR(-EINVAL); |
| } |
| } |
| |
| static ssize_t num_rmids_show(struct kobject *kobj, |
| struct kobj_attribute *attr, char *buf) |
| { |
| struct feature *feature = to_pmt_feature(kobj); |
| |
| return sysfs_emit(buf, "%u\n", feature->table.rmid.num_rmids); |
| } |
| static struct kobj_attribute num_rmids_attribute = __ATTR_RO(num_rmids); |
| |
| static ssize_t min_watcher_period_ms_show(struct kobject *kobj, |
| struct kobj_attribute *attr, char *buf) |
| { |
| struct feature *feature = to_pmt_feature(kobj); |
| struct watcher *watcher = get_watcher(feature); |
| |
| if (IS_ERR(watcher)) |
| return PTR_ERR(watcher); |
| |
| return sysfs_emit(buf, "%u\n", watcher->period); |
| } |
| static struct kobj_attribute min_watcher_period_ms_attribute = |
| __ATTR_RO(min_watcher_period_ms); |
| |
| static ssize_t max_stream_size_show(struct kobject *kobj, |
| struct kobj_attribute *attr, char *buf) |
| { |
| struct feature *feature = to_pmt_feature(kobj); |
| struct command *command = get_command(feature); |
| |
| if (IS_ERR(command)) |
| return PTR_ERR(command); |
| |
| return sysfs_emit(buf, "%u\n", command->max_stream_size); |
| } |
| static struct kobj_attribute max_stream_size_attribute = |
| __ATTR_RO(max_stream_size); |
| |
| static ssize_t max_command_size_show(struct kobject *kobj, |
| struct kobj_attribute *attr, char *buf) |
| { |
| struct feature *feature = to_pmt_feature(kobj); |
| struct command *command = get_command(feature); |
| |
| if (IS_ERR(command)) |
| return PTR_ERR(command); |
| |
| return sysfs_emit(buf, "%u\n", command->max_command_size); |
| } |
| static struct kobj_attribute max_command_size_attribute = |
| __ATTR_RO(max_command_size); |
| |
| static ssize_t guids_show(struct kobject *kobj, struct kobj_attribute *attr, |
| char *buf) |
| { |
| struct feature *feature = to_pmt_feature(kobj); |
| int i, count = 0; |
| |
| for (i = 0; i < feature->table.header.num_guids; i++) |
| count += sysfs_emit_at(buf, count, "0x%x\n", |
| feature->table.guids[i]); |
| |
| return count; |
| } |
| static struct kobj_attribute guids_attribute = __ATTR_RO(guids); |
| |
| static struct attribute *pmt_feature_rmid_attrs[] = { |
| &caps_attribute.attr, |
| &num_rmids_attribute.attr, |
| &min_watcher_period_ms_attribute.attr, |
| &max_stream_size_attribute.attr, |
| &max_command_size_attribute.attr, |
| &guids_attribute.attr, |
| NULL |
| }; |
| ATTRIBUTE_GROUPS(pmt_feature_rmid); |
| |
| static const struct kobj_type pmt_feature_rmid_ktype = { |
| .sysfs_ops = &kobj_sysfs_ops, |
| .release = pmt_feature_release, |
| .default_groups = pmt_feature_rmid_groups, |
| }; |
| |
| static struct attribute *pmt_feature_watcher_attrs[] = { |
| &caps_attribute.attr, |
| &min_watcher_period_ms_attribute.attr, |
| &max_stream_size_attribute.attr, |
| &max_command_size_attribute.attr, |
| &guids_attribute.attr, |
| NULL |
| }; |
| ATTRIBUTE_GROUPS(pmt_feature_watcher); |
| |
| static const struct kobj_type pmt_feature_watcher_ktype = { |
| .sysfs_ops = &kobj_sysfs_ops, |
| .release = pmt_feature_release, |
| .default_groups = pmt_feature_watcher_groups, |
| }; |
| |
| static struct attribute *pmt_feature_command_attrs[] = { |
| &caps_attribute.attr, |
| &max_stream_size_attribute.attr, |
| &max_command_size_attribute.attr, |
| &guids_attribute.attr, |
| NULL |
| }; |
| ATTRIBUTE_GROUPS(pmt_feature_command); |
| |
| static const struct kobj_type pmt_feature_command_ktype = { |
| .sysfs_ops = &kobj_sysfs_ops, |
| .release = pmt_feature_release, |
| .default_groups = pmt_feature_command_groups, |
| }; |
| |
| static struct attribute *pmt_feature_guids_attrs[] = { |
| &caps_attribute.attr, |
| &guids_attribute.attr, |
| NULL |
| }; |
| ATTRIBUTE_GROUPS(pmt_feature_guids); |
| |
| static const struct kobj_type pmt_feature_guids_ktype = { |
| .sysfs_ops = &kobj_sysfs_ops, |
| .release = pmt_feature_release, |
| .default_groups = pmt_feature_guids_groups, |
| }; |
| |
| static int |
| pmt_feature_get_disc_table(struct pmt_features_priv *priv, |
| struct resource *disc_res, |
| struct feature_discovery_table *disc_tbl) |
| { |
| void __iomem *disc_base; |
| |
| disc_base = devm_ioremap_resource(priv->dev, disc_res); |
| if (IS_ERR(disc_base)) |
| return PTR_ERR(disc_base); |
| |
| memcpy_fromio(disc_tbl, disc_base, sizeof(*disc_tbl)); |
| |
| devm_iounmap(priv->dev, disc_base); |
| |
| if (priv->mask & BIT(disc_tbl->id)) |
| return dev_err_probe(priv->dev, -EINVAL, "Duplicate feature: %s\n", |
| pmt_feature_names[disc_tbl->id]); |
| |
| /* |
| * Some devices may expose non-functioning entries that are |
| * reserved for future use. They have zero size. Do not fail |
| * probe for these. Just ignore them. |
| */ |
| if (disc_tbl->size == 0 || disc_tbl->access_type == PMT_ACCESS_TYPE_RSVD) |
| return SKIP_FEATURE; |
| |
| if (disc_tbl->version > MAX_FEATURE_VERSION) |
| return SKIP_FEATURE; |
| |
| if (!pmt_feature_id_is_valid(disc_tbl->id)) |
| return SKIP_FEATURE; |
| |
| priv->mask |= BIT(disc_tbl->id); |
| |
| return 0; |
| } |
| |
| static int |
| pmt_feature_get_feature_table(struct pmt_features_priv *priv, |
| struct feature *feature, |
| struct feature_discovery_table *disc_tbl, |
| struct resource *disc_res) |
| { |
| struct feature_table *feat_tbl = &feature->table; |
| struct feature_header *header; |
| struct resource res = {}; |
| resource_size_t res_size; |
| void __iomem *feat_base, *feat_offset; |
| void *tbl_offset; |
| size_t size; |
| u32 *guids; |
| u8 tbir; |
| |
| tbir = FIELD_GET(DT_TBIR, disc_tbl->offset); |
| |
| switch (disc_tbl->access_type) { |
| case ACCESS_LOCAL: |
| if (tbir) |
| return dev_err_probe(priv->dev, -EINVAL, |
| "Unsupported BAR index %u for access type %u\n", |
| tbir, disc_tbl->access_type); |
| |
| |
| /* |
| * For access_type LOCAL, the base address is as follows: |
| * base address = end of discovery region + base offset + 1 |
| */ |
| res = DEFINE_RES_MEM(disc_res->end + disc_tbl->offset + 1, |
| disc_tbl->size * sizeof(u32)); |
| break; |
| |
| default: |
| return dev_err_probe(priv->dev, -EINVAL, "Unrecognized access_type %u\n", |
| disc_tbl->access_type); |
| } |
| |
| feature->id = disc_tbl->id; |
| |
| /* Get the feature table */ |
| feat_base = devm_ioremap_resource(priv->dev, &res); |
| if (IS_ERR(feat_base)) |
| return PTR_ERR(feat_base); |
| |
| feat_offset = feat_base; |
| tbl_offset = feat_tbl; |
| |
| /* Get the header */ |
| header = &feat_tbl->header; |
| memcpy_fromio(header, feat_offset, sizeof(*header)); |
| |
| /* Validate fields fit within mapped resource */ |
| size = sizeof(*header) + FEAT_ATTR_SIZE(header->attr_size) + |
| PMT_GUID_SIZE(header->num_guids); |
| res_size = resource_size(&res); |
| if (WARN(size > res_size, "Bad table size %zu > %pa", size, &res_size)) |
| return -EINVAL; |
| |
| /* Get the feature attributes, including capability fields */ |
| tbl_offset += sizeof(*header); |
| feat_offset += sizeof(*header); |
| |
| memcpy_fromio(tbl_offset, feat_offset, FEAT_ATTR_SIZE(header->attr_size)); |
| |
| /* Finally, get the guids */ |
| guids = devm_kmalloc(priv->dev, PMT_GUID_SIZE(header->num_guids), GFP_KERNEL); |
| if (!guids) |
| return -ENOMEM; |
| |
| feat_offset += FEAT_ATTR_SIZE(header->attr_size); |
| |
| memcpy_fromio(guids, feat_offset, PMT_GUID_SIZE(header->num_guids)); |
| |
| feat_tbl->guids = guids; |
| |
| devm_iounmap(priv->dev, feat_base); |
| |
| return 0; |
| } |
| |
| static void pmt_features_add_feat(struct feature *feature) |
| { |
| guard(mutex)(&feature_list_lock); |
| list_add(&feature->list, &pmt_feature_list); |
| } |
| |
| static void pmt_features_remove_feat(struct feature *feature) |
| { |
| guard(mutex)(&feature_list_lock); |
| list_del(&feature->list); |
| } |
| |
| /* Get the discovery table and use it to get the feature table */ |
| static int pmt_features_discovery(struct pmt_features_priv *priv, |
| struct feature *feature, |
| struct intel_vsec_device *ivdev, |
| int idx) |
| { |
| struct feature_discovery_table disc_tbl = {}; /* Avoid false warning */ |
| struct resource *disc_res = &ivdev->resource[idx]; |
| const struct kobj_type *ktype; |
| int ret; |
| |
| ret = pmt_feature_get_disc_table(priv, disc_res, &disc_tbl); |
| if (ret) |
| return ret; |
| |
| ret = pmt_feature_get_feature_table(priv, feature, &disc_tbl, disc_res); |
| if (ret) |
| return ret; |
| |
| switch (feature_layout[feature->id]) { |
| case LAYOUT_RMID: |
| ktype = &pmt_feature_rmid_ktype; |
| feature->attr_group = &pmt_feature_rmid_group; |
| break; |
| case LAYOUT_WATCHER: |
| ktype = &pmt_feature_watcher_ktype; |
| feature->attr_group = &pmt_feature_watcher_group; |
| break; |
| case LAYOUT_COMMAND: |
| ktype = &pmt_feature_command_ktype; |
| feature->attr_group = &pmt_feature_command_group; |
| break; |
| case LAYOUT_CAPS_ONLY: |
| ktype = &pmt_feature_guids_ktype; |
| feature->attr_group = &pmt_feature_guids_group; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| ret = kobject_init_and_add(&feature->kobj, ktype, &priv->dev->kobj, |
| "%s", pmt_feature_names[feature->id]); |
| if (ret) |
| return ret; |
| |
| kobject_uevent(&feature->kobj, KOBJ_ADD); |
| pmt_features_add_feat(feature); |
| |
| return 0; |
| } |
| |
| static void pmt_features_remove(struct auxiliary_device *auxdev) |
| { |
| struct pmt_features_priv *priv = auxiliary_get_drvdata(auxdev); |
| int i; |
| |
| for (i = 0; i < priv->count; i++) { |
| struct feature *feature = &priv->feature[i]; |
| |
| pmt_features_remove_feat(feature); |
| sysfs_remove_group(&feature->kobj, feature->attr_group); |
| kobject_put(&feature->kobj); |
| } |
| |
| device_unregister(priv->dev); |
| } |
| |
| static int pmt_features_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *id) |
| { |
| struct intel_vsec_device *ivdev = auxdev_to_ivdev(auxdev); |
| struct pmt_features_priv *priv; |
| size_t size; |
| int ret, i; |
| |
| size = struct_size(priv, feature, ivdev->num_resources); |
| priv = devm_kzalloc(&auxdev->dev, size, GFP_KERNEL); |
| if (!priv) |
| return -ENOMEM; |
| |
| priv->parent = &ivdev->pcidev->dev; |
| auxiliary_set_drvdata(auxdev, priv); |
| |
| priv->dev = device_create(&intel_pmt_class, &auxdev->dev, MKDEV(0, 0), priv, |
| "%s-%s", "features", dev_name(priv->parent)); |
| if (IS_ERR(priv->dev)) |
| return dev_err_probe(priv->dev, PTR_ERR(priv->dev), |
| "Could not create %s-%s device node\n", |
| "features", dev_name(priv->dev)); |
| |
| /* Initialize each feature */ |
| for (i = 0; i < ivdev->num_resources; i++) { |
| struct feature *feature = &priv->feature[priv->count]; |
| |
| ret = pmt_features_discovery(priv, feature, ivdev, i); |
| if (ret == SKIP_FEATURE) |
| continue; |
| if (ret != 0) |
| goto abort_probe; |
| |
| feature->priv = priv; |
| priv->count++; |
| } |
| |
| return 0; |
| |
| abort_probe: |
| /* |
| * Only fully initialized features are tracked in priv->count, which is |
| * incremented only after a feature is completely set up (i.e., after |
| * discovery and sysfs registration). If feature initialization fails, |
| * the failing feature's state is local and does not require rollback. |
| * |
| * Therefore, on error, we can safely call the driver's remove() routine |
| * pmt_features_remove() to clean up only those features that were |
| * fully initialized and counted. All other resources are device-managed |
| * and will be cleaned up automatically during device_unregister(). |
| */ |
| pmt_features_remove(auxdev); |
| |
| return ret; |
| } |
| |
| static void pmt_get_features(struct intel_pmt_entry *entry, struct feature *f) |
| { |
| int num_guids = f->table.header.num_guids; |
| int i; |
| |
| for (i = 0; i < num_guids; i++) { |
| if (f->table.guids[i] != entry->guid) |
| continue; |
| |
| entry->feature_flags |= BIT(f->id); |
| |
| if (feature_layout[f->id] == LAYOUT_RMID) |
| entry->num_rmids = f->table.rmid.num_rmids; |
| else |
| entry->num_rmids = 0; /* entry is kzalloc but set anyway */ |
| } |
| } |
| |
| void intel_pmt_get_features(struct intel_pmt_entry *entry) |
| { |
| struct feature *feature; |
| |
| mutex_lock(&feature_list_lock); |
| list_for_each_entry(feature, &pmt_feature_list, list) { |
| if (feature->priv->parent != &entry->ep->pcidev->dev) |
| continue; |
| |
| pmt_get_features(entry, feature); |
| } |
| mutex_unlock(&feature_list_lock); |
| } |
| EXPORT_SYMBOL_NS_GPL(intel_pmt_get_features, "INTEL_PMT"); |
| |
| static const struct auxiliary_device_id pmt_features_id_table[] = { |
| { .name = "intel_vsec.discovery" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(auxiliary, pmt_features_id_table); |
| |
| static struct auxiliary_driver pmt_features_aux_driver = { |
| .id_table = pmt_features_id_table, |
| .remove = pmt_features_remove, |
| .probe = pmt_features_probe, |
| }; |
| module_auxiliary_driver(pmt_features_aux_driver); |
| |
| MODULE_AUTHOR("David E. Box <david.e.box@linux.intel.com>"); |
| MODULE_DESCRIPTION("Intel PMT Discovery driver"); |
| MODULE_LICENSE("GPL"); |
| MODULE_IMPORT_NS("INTEL_PMT"); |