anolis: sw64: add powercap driver

ANBZ: #4688

This commit adds support for powercap driver on Sunway platform.

Signed-off-by: Jing Li <jingli@wxiat.com>
Reviewed-by: He Sheng <hesheng@wxiat.com>
Signed-off-by: Gu Zitao <guzitao@wxiat.com>
Reviewed-by: Min Li <gumi@linux.alibaba.com>
Link: https://gitee.com/anolis/cloud-kernel/pulls/5372
This commit is contained in:
Jing Li 2025-03-19 20:10:56 +08:00 committed by 小龙
parent b063444887
commit 32b84fc07a
3 changed files with 754 additions and 0 deletions

View File

@ -670,6 +670,14 @@ config ARCH_HIBERNATION_POSSIBLE
depends on SW64
def_bool y
config SW64_POWERCAP
bool "Sunway powercap driver"
select IPMI_SI
depends on SW64 && CPU_FREQ && ACPI && IPMI_HANDLER
help
This enables support for the sunway powercap driver
based on BMC and IPMI system interface.
config ARCH_SELECT_MEMORY_MODEL
def_bool ARCH_SPARSEMEM_ENABLE

View File

@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_PLATFORM_XUELANG) += legacy_xuelang.o
obj-$(CONFIG_PLATFORM_JUNZHANG) += legacy_junzhang.o
obj-$(CONFIG_SW64_POWERCAP) += powercap.o
obj-y += misc-platform.o

View File

@ -0,0 +1,745 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2025 WXIAT
*/
#define pr_fmt(fmt) "sunway-powercap: " fmt
#include <linux/acpi.h>
#include <linux/cpufreq.h>
#include <linux/cpumask.h>
#include <linux/ipmi.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/pm_qos.h>
#include <linux/timer.h>
#define SUNWAY_POWERCAP_NETFN 0x3A
#define SUNWAY_POWERCAP_ACPI_NOTIFY_VALUE 0x84
enum sunway_powercap_version {
SUNWAY_POWERCAP_V1 = 1,
SUNWAY_POWERCAP_VERSION_MAX,
};
enum sunway_powercap_mode {
SUNWAY_POWERCAP_MODE_POLL = 0,
SUNWAY_POWERCAP_MODE_INTERRUPT,
};
enum sunway_powercap_poll_interval {
SUNWAY_POWERCAP_POLL_INTERVAL0 = 50,
SUNWAY_POWERCAP_POLL_INTERVAL1 = 100,
SUNWAY_POWERCAP_POLL_INTERVAL2 = 200,
SUNWAY_POWERCAP_POLL_INTERVAL3 = 250,
};
enum sunway_powercap_cmd {
SUNWAY_POWERCAP_CMD_GET_CFG = 0x30,
SUNWAY_POWERCAP_CMD_GET_FREQ = 0x31,
SUNWAY_POWERCAP_CMD_ACK = 0x32,
};
enum sunway_powercap_state {
SUNWAY_POWERCAP_STATE_FREE = 0x0F,
SUNWAY_POWERCAP_STATE_LIMIT = 0xF0,
};
#pragma pack(1)
struct sunway_powercap_cfg {
u8 version;
u8 mode;
u8 poll_interval;
u8 reserved;
};
#define FREQ_FLAG_ENABLE (1 << 0)
#define FREQ_FLAG_FREE (1 << 1)
#define FREQ_FLAG_TERMINATE (1 << 2)
struct sunway_powercap_freq {
u32 target_freq;
u16 target_core;
u16 flags;
};
#define ACK_FLAG_VALID_SIZE (1 << 0)
#define ACK_FLAG_VALID_VERSION (1 << 1)
#define ACK_FLAG_VALID_MODE (1 << 2)
#define ACK_FLAG_VALID_INTERVAL (1 << 3)
#define ACK_FLAG_VALID_FREQ (1 << 4)
#define ACK_FLAG_VALID_NODE (1 << 5)
#define ACK_FLAG_VALID_CORE (1 << 6)
struct sunway_powercap_ack {
u8 cmd;
u8 flags;
u16 reserved;
};
/* Reset to default packing */
#pragma pack()
struct sunway_powercap_bmc_data {
struct device *bmc_device;
struct ipmi_addr address;
struct ipmi_user *user;
struct completion complete;
int interface;
struct kernel_ipmi_msg tx_message;
unsigned char tx_msg_data[IPMI_MAX_MSG_LENGTH];
long tx_msgid;
unsigned char rx_msg_data[IPMI_MAX_MSG_LENGTH];
unsigned short rx_msg_len;
unsigned char rx_result;
int rx_recv_type;
bool initialized;
};
struct sunway_powercap_driver_data {
struct device *dev;
unsigned char version;
unsigned char mode;
unsigned char poll_interval;
struct timer_list timer;
struct work_struct work;
struct ipmi_smi_watcher bmc_events;
struct ipmi_user_hndl ipmi_hndlrs;
struct sunway_powercap_bmc_data bmc_data;
};
struct sunway_powercap_cpu {
unsigned int state;
unsigned int node;
unsigned int core;
struct cpufreq_policy *policy;
struct freq_qos_request *qos_req;
};
static void sunway_powercap_register_bmc(int iface, struct device *dev);
static void sunway_powercap_bmc_gone(int iface);
static void sunway_powercap_msg_handler(struct ipmi_recv_msg *msg,
void *user_msg_data);
static struct sunway_powercap_driver_data driver_data = {
.bmc_events = {
.new_smi = sunway_powercap_register_bmc,
.smi_gone = sunway_powercap_bmc_gone,
},
.ipmi_hndlrs = {
.ipmi_recv_hndl = sunway_powercap_msg_handler,
},
};
static struct sunway_powercap_cpu powercap_cpu_data[NR_CPUS];
static unsigned char powercap_freq_ack[IPMI_MAX_MSG_LENGTH];
static int sunway_powercap_send_message(struct sunway_powercap_bmc_data *bmc_data)
{
int ret;
ret = ipmi_validate_addr(&bmc_data->address, sizeof(bmc_data->address));
if (ret) {
dev_err(bmc_data->bmc_device, "invalid ipmi addr (%d)\n", ret);
return ret;
}
bmc_data->tx_msgid++;
ret = ipmi_request_settime(bmc_data->user, &bmc_data->address,
bmc_data->tx_msgid, &bmc_data->tx_message,
bmc_data, 0, 0, 0);
if (ret) {
dev_err(bmc_data->bmc_device,
"unable to send message (%d)\n", ret);
return ret;
}
return 0;
}
static int sunway_powercap_send_cmd(struct sunway_powercap_bmc_data *bmc_data,
unsigned char cmd, const unsigned char *data, unsigned short data_len)
{
bmc_data->tx_message.cmd = cmd;
bmc_data->tx_message.data_len = data_len;
if (data_len)
memcpy(bmc_data->tx_msg_data, data, data_len);
return sunway_powercap_send_message(bmc_data);
}
static int sunway_powercap_query(struct sunway_powercap_bmc_data *bmc_data,
unsigned char cmd, const char *info)
{
int ret;
ret = sunway_powercap_send_cmd(bmc_data, cmd, NULL, 0);
if (ret) {
dev_err(bmc_data->bmc_device, "unable to query %s\n", info);
return ret;
}
wait_for_completion(&bmc_data->complete);
if (bmc_data->rx_result) {
dev_err(bmc_data->bmc_device, "rx error 0x%x when query %s\n",
bmc_data->rx_result, info);
return -EINVAL;
}
return 0;
}
static int sunway_powercap_ack_bmc(struct sunway_powercap_bmc_data *bmc_data,
const struct sunway_powercap_ack *ack, int num)
{
unsigned char cmd = SUNWAY_POWERCAP_CMD_ACK;
int ret;
ret = sunway_powercap_send_cmd(bmc_data, cmd,
(const char *)ack, sizeof(*ack) * num);
if (ret) {
dev_err(bmc_data->bmc_device, "unable to send ack\n");
return ret;
}
wait_for_completion(&bmc_data->complete);
return 0;
}
static inline unsigned int
powercap_target_node(const struct sunway_powercap_freq *freq)
{
return freq->target_core & 0x3F;
}
static inline unsigned int
powercap_target_core(const struct sunway_powercap_freq *freq)
{
return (freq->target_core >> 6) & 0x3FF;
}
static inline bool is_powercap_cpu_match(const struct sunway_powercap_cpu *data,
unsigned int node, unsigned int core)
{
if ((node != 0x3F) && (data->node != node))
return false;
if ((core != 0x3FF) && (data->core != core))
return false;
return true;
}
static int sunway_powercap_validate_freq(const struct sunway_powercap_freq *freq,
struct sunway_powercap_ack *ack)
{
unsigned int node = powercap_target_node(freq);
unsigned int core = powercap_target_core(freq);
int i;
/* Currently, core must be 0x3FF(all bits are 1) */
if (core != 0x3FF)
goto out_validate_freq;
for (i = 0; i < ARRAY_SIZE(powercap_cpu_data); i++) {
struct cpufreq_policy *policy = powercap_cpu_data[i].policy;
unsigned int target_freq = freq->target_freq;
if (!policy)
continue;
if (!is_powercap_cpu_match(&powercap_cpu_data[i], node, core))
continue;
/* Now we confirm that core and node are valid */
ack->flags |= ACK_FLAG_VALID_NODE;
ack->flags |= ACK_FLAG_VALID_CORE;
if (cpufreq_frequency_table_get_index(policy, target_freq) < 0) {
pr_err("invalid target freq %u\n", target_freq);
return -EINVAL;
}
ack->flags |= ACK_FLAG_VALID_FREQ;
return 0;
}
out_validate_freq:
pr_err("invalid core %u on node %u\n", core, node);
return -EINVAL;
}
static inline bool is_powercap_enabled(const struct sunway_powercap_freq *freq)
{
return !!(freq->flags & FREQ_FLAG_ENABLE);
}
static inline bool is_powercap_no_limit(const struct sunway_powercap_freq *freq)
{
return !!(freq->flags & FREQ_FLAG_FREE);
}
static inline bool
is_powercap_freq_data_terminate(const struct sunway_powercap_freq *freq)
{
return !!(freq->flags & FREQ_FLAG_TERMINATE);
}
static int sunway_powercap_handle_free_cpus(struct cpufreq_policy *policy,
struct freq_qos_request *req, const struct sunway_powercap_freq *freq)
{
int ret, related_cpu;
if (!is_powercap_enabled(freq) || is_powercap_no_limit(freq))
return 0;
ret = freq_qos_add_request(&policy->constraints,
req, FREQ_QOS_MAX, freq->target_freq);
if (ret < 0) {
pr_err("unable to add qos request on cpus %*pbl\n",
cpumask_pr_args(policy->related_cpus));
return ret;
}
for_each_cpu(related_cpu, policy->related_cpus)
powercap_cpu_data[related_cpu].state = SUNWAY_POWERCAP_STATE_LIMIT;
return 0;
}
static int sunway_powercap_handle_limit_cpus(struct cpufreq_policy *policy,
struct freq_qos_request *req, const struct sunway_powercap_freq *freq)
{
int ret, related_cpu;
if (is_powercap_enabled(freq) && !is_powercap_no_limit(freq)) {
ret = freq_qos_update_request(req, freq->target_freq);
if (ret < 0)
pr_err("unable to update qos request on cpus %*pbl\n",
cpumask_pr_args(policy->related_cpus));
return ret;
}
ret = freq_qos_remove_request(req);
if (ret < 0) {
pr_err("unable to remove qos request on cpus %*pbl\n",
cpumask_pr_args(policy->related_cpus));
return ret;
}
for_each_cpu(related_cpu, policy->related_cpus)
powercap_cpu_data[related_cpu].state = SUNWAY_POWERCAP_STATE_FREE;
return 0;
}
static int sunway_powercap_handle_one_freq(const struct sunway_powercap_freq *freq,
struct sunway_powercap_ack *ack)
{
int i;
unsigned int node = powercap_target_node(freq);
unsigned int core = powercap_target_core(freq);
unsigned int state;
struct freq_qos_request *req;
struct cpufreq_policy *policy;
cpumask_var_t done;
/* Ack freq */
ack->cmd = SUNWAY_POWERCAP_CMD_GET_FREQ;
/* Size must be valid here */
ack->flags |= ACK_FLAG_VALID_SIZE;
if (sunway_powercap_validate_freq(freq, ack))
return -EINVAL;
if (!alloc_cpumask_var(&done, GFP_KERNEL))
return -ENOMEM;
cpumask_clear(done);
for (i = 0; i < ARRAY_SIZE(powercap_cpu_data); i++) {
policy = powercap_cpu_data[i].policy;
if (!policy || policy_is_inactive(policy))
continue;
if (cpumask_test_cpu(i, done))
continue;
if (!is_powercap_cpu_match(&powercap_cpu_data[i], node, core))
continue;
state = powercap_cpu_data[i].state;
req = powercap_cpu_data[i].qos_req;
if (state == SUNWAY_POWERCAP_STATE_FREE)
sunway_powercap_handle_free_cpus(policy, req, freq);
else if (state == SUNWAY_POWERCAP_STATE_LIMIT)
sunway_powercap_handle_limit_cpus(policy, req, freq);
else
pr_err("cpu %d with invalid state 0x%x\n", i, state);
cpumask_or(done, done, policy->related_cpus);
}
free_cpumask_var(done);
return 0;
}
static int sunway_powercap_poll_once(struct sunway_powercap_bmc_data *bmc_data)
{
struct sunway_powercap_ack *ack;
struct sunway_powercap_freq *freq;
unsigned char cmd = SUNWAY_POWERCAP_CMD_GET_FREQ;
int ret, num, i;
query_freq:
/* Clean ACK data */
memset(powercap_freq_ack, 0, sizeof(powercap_freq_ack));
ret = sunway_powercap_query(bmc_data, cmd, "freq");
if (ret)
return ret;
ack = (struct sunway_powercap_ack *)&powercap_freq_ack[0];
freq = (struct sunway_powercap_freq *)&bmc_data->rx_msg_data[0];
/* Number of freq data */
num = bmc_data->rx_msg_len >> 3;
if (!num || (bmc_data->rx_msg_len & 0x7)) {
dev_err(bmc_data->bmc_device, "invalid freq size %d\n",
bmc_data->rx_msg_len);
/**
* The size must be multiple of 8 bytes, otherwise
* send only one ack with invalid size.
*/
ack->cmd = cmd;
ack->flags &= ~ACK_FLAG_VALID_SIZE;
sunway_powercap_ack_bmc(bmc_data, ack, 1);
return -EINVAL;
}
/* Handle freq data one by one */
for (i = 0; i < num; i++)
sunway_powercap_handle_one_freq(freq + i, ack + i);
sunway_powercap_ack_bmc(bmc_data, ack, num);
/* More freq Data needs to be queried */
if (!is_powercap_freq_data_terminate(&freq[num - 1]))
goto query_freq;
return 0;
}
static inline bool is_legal_poll_interval(u8 interval)
{
return (interval == SUNWAY_POWERCAP_POLL_INTERVAL0) ||
(interval == SUNWAY_POWERCAP_POLL_INTERVAL1) ||
(interval == SUNWAY_POWERCAP_POLL_INTERVAL2) ||
(interval == SUNWAY_POWERCAP_POLL_INTERVAL3);
}
static int sunway_powercap_validate_cfg(const struct sunway_powercap_cfg *cfg,
struct sunway_powercap_ack *ack)
{
bool valid = true;
if (!cfg->version || (cfg->version >= SUNWAY_POWERCAP_VERSION_MAX)) {
pr_err("invalid version %d\n", cfg->version);
valid = false;
} else
ack->flags |= ACK_FLAG_VALID_VERSION;
if (cfg->mode > SUNWAY_POWERCAP_MODE_INTERRUPT) {
pr_err("invalid mode %d\n", cfg->mode);
valid = false;
} else
ack->flags |= ACK_FLAG_VALID_MODE;
if ((cfg->mode == SUNWAY_POWERCAP_MODE_POLL) &&
!is_legal_poll_interval(cfg->poll_interval)) {
pr_err("invalid poll interval %dms\n", cfg->poll_interval);
valid = false;
} else
ack->flags |= ACK_FLAG_VALID_INTERVAL;
return valid ? 0 : -EINVAL;
}
static void sunway_powercap_add_timer(void)
{
unsigned long expire;
struct timer_list *timer = &driver_data.timer;
expire = jiffies + msecs_to_jiffies(driver_data.poll_interval);
timer->expires = round_jiffies_relative(expire);
add_timer(timer);
}
static void sunway_powercap_poll_func(struct timer_list *t)
{
struct work_struct *work = &driver_data.work;
schedule_work(work);
sunway_powercap_add_timer();
}
static void sunway_powercap_acpi_notify(acpi_handle device, u32 value, void *data)
{
struct device *dev = driver_data.dev;
struct work_struct *work = &driver_data.work;
if (value != SUNWAY_POWERCAP_ACPI_NOTIFY_VALUE) {
dev_err(dev, "unknown acpi notify value\n");
return;
}
schedule_work(work);
}
static int sunway_powercap_setup_cfg(const struct sunway_powercap_cfg *cfg)
{
bool is_poll_mode = (cfg->mode == SUNWAY_POWERCAP_MODE_POLL);
struct device *dev = driver_data.dev;
struct acpi_device *adev;
acpi_status status;
driver_data.version = cfg->version;
driver_data.mode = cfg->mode;
driver_data.poll_interval = cfg->poll_interval;
if (is_poll_mode) {
timer_setup(&driver_data.timer, sunway_powercap_poll_func, 0);
sunway_powercap_add_timer();
} else {
/* Must be interrupt mode */
adev = ACPI_COMPANION(dev);
if (WARN_ON(!adev))
return -EINVAL;
status = acpi_install_notify_handler(adev->handle,
ACPI_DEVICE_NOTIFY,
sunway_powercap_acpi_notify,
NULL);
if (ACPI_FAILURE(status)) {
dev_err(dev, "unable to register notifier %08x\n",
status);
return -EINVAL;
}
}
dev_info(dev, "found with version %d and %s mode\n",
driver_data.version,
is_poll_mode ? "polling" : "interrupt");
return 0;
}
static int sunway_powercap_init_cfg(struct sunway_powercap_bmc_data *bmc_data)
{
struct sunway_powercap_cfg cfg = { 0 };
struct sunway_powercap_ack ack = { 0 };
unsigned char cmd = SUNWAY_POWERCAP_CMD_GET_CFG;
int ret;
ret = sunway_powercap_query(bmc_data, cmd, "cfg");
if (ret)
return ret;
ack.cmd = cmd;
if (bmc_data->rx_msg_len != sizeof(cfg)) {
dev_err(bmc_data->bmc_device, "invalid cfg size %d\n",
bmc_data->rx_msg_len);
ret = -EINVAL;
}
if (!ret) {
ack.flags |= ACK_FLAG_VALID_SIZE;
memcpy(&cfg, bmc_data->rx_msg_data, sizeof(cfg));
ret = sunway_powercap_validate_cfg(&cfg, &ack);
if (!ret)
ret = sunway_powercap_setup_cfg(&cfg);
}
sunway_powercap_ack_bmc(bmc_data, &ack, 1);
return ret;
}
static void sunway_powercap_register_bmc(int iface, struct device *dev)
{
struct sunway_powercap_bmc_data *bmc_data = &driver_data.bmc_data;
int ret;
/* Multiple BMC for suwnay powercap are not supported */
if (bmc_data->initialized) {
dev_err(dev, "unable to register sunway-powercap repeatedly\n");
return;
}
bmc_data->address.addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE;
bmc_data->address.channel = IPMI_BMC_CHANNEL;
bmc_data->address.data[0] = 0;
bmc_data->interface = iface;
bmc_data->bmc_device = dev;
/* Create IPMI user */
ret = ipmi_create_user(bmc_data->interface, &driver_data.ipmi_hndlrs,
bmc_data, &bmc_data->user);
if (ret) {
dev_err(dev, "unable to register user with IPMI interface %d",
bmc_data->interface);
return;
}
/* Initialize message */
bmc_data->tx_msgid = 0;
bmc_data->tx_message.netfn = SUNWAY_POWERCAP_NETFN;
bmc_data->tx_message.data = bmc_data->tx_msg_data;
init_completion(&bmc_data->complete);
ret = sunway_powercap_init_cfg(bmc_data);
if (ret) {
dev_err(dev, "unable to initialize powercap configuration\n");
goto out_destroy_user;
}
bmc_data->initialized = true;
return;
out_destroy_user:
ipmi_destroy_user(bmc_data->user);
}
static void sunway_powercap_bmc_gone(int iface)
{
struct sunway_powercap_bmc_data *bmc_data = &driver_data.bmc_data;
if (WARN_ON(bmc_data->interface != iface))
return;
ipmi_destroy_user(bmc_data->user);
}
static void sunway_powercap_msg_handler(struct ipmi_recv_msg *msg,
void *user_msg_data)
{
struct sunway_powercap_bmc_data *bmc_data = user_msg_data;
if (msg->msgid != bmc_data->tx_msgid) {
dev_err(bmc_data->bmc_device,
"mismatch between rx msgid (0x%lx) and tx msgid (0x%lx)!\n",
msg->msgid,
bmc_data->tx_msgid);
ipmi_free_recv_msg(msg);
return;
}
bmc_data->rx_recv_type = msg->recv_type;
if (msg->msg.data_len > 0)
bmc_data->rx_result = msg->msg.data[0];
else
bmc_data->rx_result = IPMI_UNKNOWN_ERR_COMPLETION_CODE;
if (msg->msg.data_len > 1) {
bmc_data->rx_msg_len = msg->msg.data_len - 1;
memcpy(bmc_data->rx_msg_data, msg->msg.data + 1,
bmc_data->rx_msg_len);
} else
bmc_data->rx_msg_len = 0;
ipmi_free_recv_msg(msg);
complete(&bmc_data->complete);
}
static void do_powercap(struct work_struct *work)
{
sunway_powercap_poll_once(&driver_data.bmc_data);
}
static int sunway_powercap_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct cpufreq_policy *policy;
int cpu;
driver_data.dev = dev;
INIT_WORK(&driver_data.work, do_powercap);
for_each_possible_cpu(cpu) {
int related_cpu, rcid = cpu_physical_id(cpu);
struct freq_qos_request *req;
/* Initial state */
powercap_cpu_data[related_cpu].state = SUNWAY_POWERCAP_STATE_FREE;
powercap_cpu_data[related_cpu].core = rcid_to_core_id(rcid);
powercap_cpu_data[related_cpu].node = rcid_to_domain_id(rcid);
if (powercap_cpu_data[cpu].policy)
continue;
policy = cpufreq_cpu_get(cpu);
if (!policy)
continue;
req = devm_kzalloc(dev, sizeof(*req), GFP_KERNEL);
if (!req)
return -ENOMEM;
for_each_cpu(related_cpu, policy->related_cpus) {
powercap_cpu_data[related_cpu].policy = policy;
powercap_cpu_data[related_cpu].qos_req = req;
}
}
return ipmi_smi_watcher_register(&driver_data.bmc_events);
}
#ifdef CONFIG_ACPI
static const struct acpi_device_id sunway_powercap_acpi_match[] = {
{ "SUNW0203", 0 },
{},
};
#endif
static struct platform_driver sunway_powercap_driver = {
.probe = sunway_powercap_probe,
.driver = {
.name = "sunway-powercap",
.acpi_match_table = ACPI_PTR(sunway_powercap_acpi_match),
},
};
static int __init sunway_powercap_driver_init(void)
{
return platform_driver_register(&sunway_powercap_driver);
}
late_initcall(sunway_powercap_driver_init);