root/drivers/platform/x86/intel/tpmi_power_domains.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Mapping of TPMI power domains CPU mapping
 *
 * Copyright (c) 2024, Intel Corporation.
 */

#include <linux/bitfield.h>
#include <linux/cleanup.h>
#include <linux/cpuhotplug.h>
#include <linux/cpumask.h>
#include <linux/errno.h>
#include <linux/export.h>
#include <linux/hashtable.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/overflow.h>
#include <linux/slab.h>
#include <linux/topology.h>
#include <linux/types.h>

#include <asm/cpu_device_id.h>
#include <asm/intel-family.h>
#include <asm/msr.h>

#include "tpmi_power_domains.h"

#define MSR_PM_LOGICAL_ID       0x54

/*
 * Struct of MSR 0x54
 * [15:11] PM_DOMAIN_ID
 * [10:3] MODULE_ID (aka IDI_AGENT_ID)
 * [2:0] LP_ID
 * For Atom:
 *   [2] Always 0
 *   [1:0] core ID within module
 * For Core
 *   [2:1] Always 0
 *   [0] thread ID
 */

#define LP_ID_MASK              GENMASK_ULL(2, 0)
#define MODULE_ID_MASK          GENMASK_ULL(10, 3)
#define PM_DOMAIN_ID_MASK       GENMASK_ULL(15, 11)

/**
 * struct tpmi_cpu_info - Mapping information for a CPU
 * @hnode: Used to add mapping information to hash list
 * @linux_cpu:  Linux CPU number
 * @pkg_id: Package ID of this CPU
 * @punit_thread_id: Punit thread id of this CPU
 * @punit_core_id: Punit core id
 * @punit_domain_id: Power domain id from Punit
 *
 * Structure to store mapping information for a Linux CPU
 * to a Punit core, thread and power domain.
 */
struct tpmi_cpu_info {
        struct hlist_node hnode;
        int linux_cpu;
        u8 pkg_id;
        u8 punit_thread_id;
        u8 punit_core_id;
        u8 punit_domain_id;
};

static DEFINE_PER_CPU(struct tpmi_cpu_info, tpmi_cpu_info);

/* The dynamically assigned cpu hotplug state to free later */
static enum cpuhp_state tpmi_hp_state __read_mostly;

#define MAX_POWER_DOMAINS       8

static cpumask_t *tpmi_power_domain_mask;

static u16 *domain_die_map;

/* Lock to protect tpmi_power_domain_mask and tpmi_cpu_hash */
static DEFINE_MUTEX(tpmi_lock);

static const struct x86_cpu_id tpmi_cpu_ids[] = {
        X86_MATCH_VFM(INTEL_GRANITERAPIDS_X,    NULL),
        X86_MATCH_VFM(INTEL_ATOM_CRESTMONT_X,   NULL),
        X86_MATCH_VFM(INTEL_ATOM_CRESTMONT,     NULL),
        X86_MATCH_VFM(INTEL_ATOM_DARKMONT_X,    NULL),
        X86_MATCH_VFM(INTEL_GRANITERAPIDS_D,    NULL),
        X86_MATCH_VFM(INTEL_DIAMONDRAPIDS_X,    NULL),
        {}
};
MODULE_DEVICE_TABLE(x86cpu, tpmi_cpu_ids);

static DECLARE_HASHTABLE(tpmi_cpu_hash, 8);

static bool tpmi_domain_is_valid(struct tpmi_cpu_info *info)
{
        return info->pkg_id < topology_max_packages() &&
                info->punit_domain_id < MAX_POWER_DOMAINS;
}

int tpmi_get_linux_cpu_number(int package_id, int domain_id, int punit_core_id)
{
        struct tpmi_cpu_info *info;
        int ret = -EINVAL;

        guard(mutex)(&tpmi_lock);
        hash_for_each_possible(tpmi_cpu_hash, info, hnode, punit_core_id) {
                if (info->punit_domain_id == domain_id && info->pkg_id == package_id) {
                        ret = info->linux_cpu;
                        break;
                }
        }

        return ret;
}
EXPORT_SYMBOL_NS_GPL(tpmi_get_linux_cpu_number, "INTEL_TPMI_POWER_DOMAIN");

int tpmi_get_punit_core_number(int cpu_no)
{
        if (cpu_no >= num_possible_cpus())
                return -EINVAL;

        return per_cpu(tpmi_cpu_info, cpu_no).punit_core_id;
}
EXPORT_SYMBOL_NS_GPL(tpmi_get_punit_core_number, "INTEL_TPMI_POWER_DOMAIN");

int tpmi_get_power_domain_id(int cpu_no)
{
        if (cpu_no >= num_possible_cpus())
                return -EINVAL;

        return per_cpu(tpmi_cpu_info, cpu_no).punit_domain_id;
}
EXPORT_SYMBOL_NS_GPL(tpmi_get_power_domain_id, "INTEL_TPMI_POWER_DOMAIN");

cpumask_t *tpmi_get_power_domain_mask(int cpu_no)
{
        struct tpmi_cpu_info *info;
        cpumask_t *mask;
        int index;

        if (cpu_no >= num_possible_cpus())
                return NULL;

        info = &per_cpu(tpmi_cpu_info, cpu_no);
        if (!tpmi_domain_is_valid(info))
                return NULL;

        index = info->pkg_id * MAX_POWER_DOMAINS + info->punit_domain_id;
        guard(mutex)(&tpmi_lock);
        mask = &tpmi_power_domain_mask[index];

        return mask;
}
EXPORT_SYMBOL_NS_GPL(tpmi_get_power_domain_mask, "INTEL_TPMI_POWER_DOMAIN");

int tpmi_get_linux_die_id(int pkg_id, int domain_id)
{
        if (pkg_id >= topology_max_packages() || domain_id >= MAX_POWER_DOMAINS)
                return -EINVAL;

        return domain_die_map[pkg_id * MAX_POWER_DOMAINS + domain_id];
}
EXPORT_SYMBOL_NS_GPL(tpmi_get_linux_die_id, "INTEL_TPMI_POWER_DOMAIN");

static int tpmi_get_logical_id(unsigned int cpu, struct tpmi_cpu_info *info)
{
        u64 data;
        int ret;

        ret = rdmsrq_safe(MSR_PM_LOGICAL_ID, &data);
        if (ret)
                return ret;

        info->punit_domain_id = FIELD_GET(PM_DOMAIN_ID_MASK, data);
        if (info->punit_domain_id >= MAX_POWER_DOMAINS)
                return -EINVAL;

        info->punit_thread_id = FIELD_GET(LP_ID_MASK, data);
        info->punit_core_id = FIELD_GET(MODULE_ID_MASK, data);
        info->pkg_id = topology_logical_package_id(cpu);
        info->linux_cpu = cpu;

        return 0;
}

static int tpmi_cpu_online(unsigned int cpu)
{
        struct tpmi_cpu_info *info = &per_cpu(tpmi_cpu_info, cpu);
        int ret, index;

        /* Don't fail CPU online for some bad mapping of CPUs */
        ret = tpmi_get_logical_id(cpu, info);
        if (ret)
                return 0;

        index = info->pkg_id * MAX_POWER_DOMAINS + info->punit_domain_id;

        guard(mutex)(&tpmi_lock);
        cpumask_set_cpu(cpu, &tpmi_power_domain_mask[index]);
        hash_add(tpmi_cpu_hash, &info->hnode, info->punit_core_id);

        domain_die_map[info->pkg_id * MAX_POWER_DOMAINS + info->punit_domain_id] =
                        topology_die_id(cpu);

        return 0;
}

static int __init tpmi_init(void)
{
        const struct x86_cpu_id *id;
        u64 data;
        int ret;

        id = x86_match_cpu(tpmi_cpu_ids);
        if (!id)
                return -ENODEV;

        /* Check for MSR 0x54 presence */
        ret = rdmsrq_safe(MSR_PM_LOGICAL_ID, &data);
        if (ret)
                return ret;

        tpmi_power_domain_mask = kzalloc_objs(*tpmi_power_domain_mask,
                                              size_mul(topology_max_packages(), MAX_POWER_DOMAINS));
        if (!tpmi_power_domain_mask)
                return -ENOMEM;

        domain_die_map = kcalloc(size_mul(topology_max_packages(), MAX_POWER_DOMAINS),
                                 sizeof(*domain_die_map), GFP_KERNEL);
        if (!domain_die_map) {
                ret = -ENOMEM;
                goto free_domain_mask;
        }

        ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
                                "platform/x86/tpmi_power_domains:online",
                                tpmi_cpu_online, NULL);
        if (ret < 0)
                goto free_domain_map;

        tpmi_hp_state = ret;

        return 0;

free_domain_map:
        kfree(domain_die_map);

free_domain_mask:
        kfree(tpmi_power_domain_mask);

        return ret;
}
module_init(tpmi_init)

static void __exit tpmi_exit(void)
{
        cpuhp_remove_state(tpmi_hp_state);
        kfree(tpmi_power_domain_mask);
        kfree(domain_die_map);
}
module_exit(tpmi_exit)

MODULE_DESCRIPTION("TPMI Power Domains Mapping");
MODULE_LICENSE("GPL");