| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * CPUFreq driver for the Loongson-3 processors. |
| * |
| * All revisions of Loongson-3 processor support cpu_has_scalefreq feature. |
| * |
| * Author: Huacai Chen <chenhuacai@loongson.cn> |
| * Copyright (C) 2024 Loongson Technology Corporation Limited |
| */ |
| #include <linux/cpufreq.h> |
| #include <linux/delay.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/units.h> |
| |
| #include <asm/idle.h> |
| #include <asm/loongarch.h> |
| #include <asm/loongson.h> |
| |
| /* Message */ |
| union smc_message { |
| u32 value; |
| struct { |
| u32 id : 4; |
| u32 info : 4; |
| u32 val : 16; |
| u32 cmd : 6; |
| u32 extra : 1; |
| u32 complete : 1; |
| }; |
| }; |
| |
| /* Command return values */ |
| #define CMD_OK 0 /* No error */ |
| #define CMD_ERROR 1 /* Regular error */ |
| #define CMD_NOCMD 2 /* Command does not support */ |
| #define CMD_INVAL 3 /* Invalid Parameter */ |
| |
| /* Version commands */ |
| /* |
| * CMD_GET_VERSION - Get interface version |
| * Input: none |
| * Output: version |
| */ |
| #define CMD_GET_VERSION 0x1 |
| |
| /* Feature commands */ |
| /* |
| * CMD_GET_FEATURE - Get feature state |
| * Input: feature ID |
| * Output: feature flag |
| */ |
| #define CMD_GET_FEATURE 0x2 |
| |
| /* |
| * CMD_SET_FEATURE - Set feature state |
| * Input: feature ID, feature flag |
| * output: none |
| */ |
| #define CMD_SET_FEATURE 0x3 |
| |
| /* Feature IDs */ |
| #define FEATURE_SENSOR 0 |
| #define FEATURE_FAN 1 |
| #define FEATURE_DVFS 2 |
| |
| /* Sensor feature flags */ |
| #define FEATURE_SENSOR_ENABLE BIT(0) |
| #define FEATURE_SENSOR_SAMPLE BIT(1) |
| |
| /* Fan feature flags */ |
| #define FEATURE_FAN_ENABLE BIT(0) |
| #define FEATURE_FAN_AUTO BIT(1) |
| |
| /* DVFS feature flags */ |
| #define FEATURE_DVFS_ENABLE BIT(0) |
| #define FEATURE_DVFS_BOOST BIT(1) |
| #define FEATURE_DVFS_AUTO BIT(2) |
| #define FEATURE_DVFS_SINGLE_BOOST BIT(3) |
| |
| /* Sensor commands */ |
| /* |
| * CMD_GET_SENSOR_NUM - Get number of sensors |
| * Input: none |
| * Output: number |
| */ |
| #define CMD_GET_SENSOR_NUM 0x4 |
| |
| /* |
| * CMD_GET_SENSOR_STATUS - Get sensor status |
| * Input: sensor ID, type |
| * Output: sensor status |
| */ |
| #define CMD_GET_SENSOR_STATUS 0x5 |
| |
| /* Sensor types */ |
| #define SENSOR_INFO_TYPE 0 |
| #define SENSOR_INFO_TYPE_TEMP 1 |
| |
| /* Fan commands */ |
| /* |
| * CMD_GET_FAN_NUM - Get number of fans |
| * Input: none |
| * Output: number |
| */ |
| #define CMD_GET_FAN_NUM 0x6 |
| |
| /* |
| * CMD_GET_FAN_INFO - Get fan status |
| * Input: fan ID, type |
| * Output: fan info |
| */ |
| #define CMD_GET_FAN_INFO 0x7 |
| |
| /* |
| * CMD_SET_FAN_INFO - Set fan status |
| * Input: fan ID, type, value |
| * Output: none |
| */ |
| #define CMD_SET_FAN_INFO 0x8 |
| |
| /* Fan types */ |
| #define FAN_INFO_TYPE_LEVEL 0 |
| |
| /* DVFS commands */ |
| /* |
| * CMD_GET_FREQ_LEVEL_NUM - Get number of freq levels |
| * Input: CPU ID |
| * Output: number |
| */ |
| #define CMD_GET_FREQ_LEVEL_NUM 0x9 |
| |
| /* |
| * CMD_GET_FREQ_BOOST_LEVEL - Get the first boost level |
| * Input: CPU ID |
| * Output: number |
| */ |
| #define CMD_GET_FREQ_BOOST_LEVEL 0x10 |
| |
| /* |
| * CMD_GET_FREQ_LEVEL_INFO - Get freq level info |
| * Input: CPU ID, level ID |
| * Output: level info |
| */ |
| #define CMD_GET_FREQ_LEVEL_INFO 0x11 |
| |
| /* |
| * CMD_GET_FREQ_INFO - Get freq info |
| * Input: CPU ID, type |
| * Output: freq info |
| */ |
| #define CMD_GET_FREQ_INFO 0x12 |
| |
| /* |
| * CMD_SET_FREQ_INFO - Set freq info |
| * Input: CPU ID, type, value |
| * Output: none |
| */ |
| #define CMD_SET_FREQ_INFO 0x13 |
| |
| /* Freq types */ |
| #define FREQ_INFO_TYPE_FREQ 0 |
| #define FREQ_INFO_TYPE_LEVEL 1 |
| |
| #define FREQ_MAX_LEVEL 16 |
| |
| struct loongson3_freq_data { |
| unsigned int def_freq_level; |
| struct cpufreq_frequency_table table[]; |
| }; |
| |
| static struct mutex cpufreq_mutex[MAX_PACKAGES]; |
| static struct cpufreq_driver loongson3_cpufreq_driver; |
| static DEFINE_PER_CPU(struct loongson3_freq_data *, freq_data); |
| |
| static inline int do_service_request(u32 id, u32 info, u32 cmd, u32 val, u32 extra) |
| { |
| int retries; |
| unsigned int cpu = raw_smp_processor_id(); |
| unsigned int package = cpu_data[cpu].package; |
| union smc_message msg, last; |
| |
| mutex_lock(&cpufreq_mutex[package]); |
| |
| last.value = iocsr_read32(LOONGARCH_IOCSR_SMCMBX); |
| if (!last.complete) { |
| mutex_unlock(&cpufreq_mutex[package]); |
| return -EPERM; |
| } |
| |
| msg.id = id; |
| msg.info = info; |
| msg.cmd = cmd; |
| msg.val = val; |
| msg.extra = extra; |
| msg.complete = 0; |
| |
| iocsr_write32(msg.value, LOONGARCH_IOCSR_SMCMBX); |
| iocsr_write32(iocsr_read32(LOONGARCH_IOCSR_MISC_FUNC) | IOCSR_MISC_FUNC_SOFT_INT, |
| LOONGARCH_IOCSR_MISC_FUNC); |
| |
| for (retries = 0; retries < 10000; retries++) { |
| msg.value = iocsr_read32(LOONGARCH_IOCSR_SMCMBX); |
| if (msg.complete) |
| break; |
| |
| usleep_range(8, 12); |
| } |
| |
| if (!msg.complete || msg.cmd != CMD_OK) { |
| mutex_unlock(&cpufreq_mutex[package]); |
| return -EPERM; |
| } |
| |
| mutex_unlock(&cpufreq_mutex[package]); |
| |
| return msg.val; |
| } |
| |
| static unsigned int loongson3_cpufreq_get(unsigned int cpu) |
| { |
| int ret; |
| |
| ret = do_service_request(cpu, FREQ_INFO_TYPE_FREQ, CMD_GET_FREQ_INFO, 0, 0); |
| |
| return ret * KILO; |
| } |
| |
| static int loongson3_cpufreq_target(struct cpufreq_policy *policy, unsigned int index) |
| { |
| int ret; |
| |
| ret = do_service_request(cpu_data[policy->cpu].core, |
| FREQ_INFO_TYPE_LEVEL, CMD_SET_FREQ_INFO, index, 0); |
| |
| return (ret >= 0) ? 0 : ret; |
| } |
| |
| static int configure_freq_table(int cpu) |
| { |
| int i, ret, boost_level, max_level, freq_level; |
| struct platform_device *pdev = cpufreq_get_driver_data(); |
| struct loongson3_freq_data *data; |
| |
| if (per_cpu(freq_data, cpu)) |
| return 0; |
| |
| ret = do_service_request(cpu, 0, CMD_GET_FREQ_LEVEL_NUM, 0, 0); |
| if (ret < 0) |
| return ret; |
| max_level = ret; |
| |
| ret = do_service_request(cpu, 0, CMD_GET_FREQ_BOOST_LEVEL, 0, 0); |
| if (ret < 0) |
| return ret; |
| boost_level = ret; |
| |
| freq_level = min(max_level, FREQ_MAX_LEVEL); |
| data = devm_kzalloc(&pdev->dev, struct_size(data, table, freq_level + 1), GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| |
| data->def_freq_level = boost_level - 1; |
| |
| for (i = 0; i < freq_level; i++) { |
| ret = do_service_request(cpu, FREQ_INFO_TYPE_FREQ, CMD_GET_FREQ_LEVEL_INFO, i, 0); |
| if (ret < 0) { |
| devm_kfree(&pdev->dev, data); |
| return ret; |
| } |
| |
| data->table[i].frequency = ret * KILO; |
| data->table[i].flags = (i >= boost_level) ? CPUFREQ_BOOST_FREQ : 0; |
| } |
| |
| data->table[freq_level].flags = 0; |
| data->table[freq_level].frequency = CPUFREQ_TABLE_END; |
| |
| per_cpu(freq_data, cpu) = data; |
| |
| return 0; |
| } |
| |
| static int loongson3_cpufreq_cpu_init(struct cpufreq_policy *policy) |
| { |
| int i, ret, cpu = policy->cpu; |
| |
| ret = configure_freq_table(cpu); |
| if (ret < 0) |
| return ret; |
| |
| policy->cpuinfo.transition_latency = 10000; |
| policy->freq_table = per_cpu(freq_data, cpu)->table; |
| policy->suspend_freq = policy->freq_table[per_cpu(freq_data, cpu)->def_freq_level].frequency; |
| cpumask_copy(policy->cpus, topology_sibling_cpumask(cpu)); |
| |
| for_each_cpu(i, policy->cpus) { |
| if (i != cpu) |
| per_cpu(freq_data, i) = per_cpu(freq_data, cpu); |
| } |
| |
| if (policy_has_boost_freq(policy)) { |
| ret = cpufreq_enable_boost_support(); |
| if (ret < 0) { |
| pr_warn("cpufreq: Failed to enable boost: %d\n", ret); |
| return ret; |
| } |
| loongson3_cpufreq_driver.boost_enabled = true; |
| } |
| |
| return 0; |
| } |
| |
| static void loongson3_cpufreq_cpu_exit(struct cpufreq_policy *policy) |
| { |
| int cpu = policy->cpu; |
| |
| loongson3_cpufreq_target(policy, per_cpu(freq_data, cpu)->def_freq_level); |
| } |
| |
| static int loongson3_cpufreq_cpu_online(struct cpufreq_policy *policy) |
| { |
| return 0; |
| } |
| |
| static int loongson3_cpufreq_cpu_offline(struct cpufreq_policy *policy) |
| { |
| return 0; |
| } |
| |
| static struct cpufreq_driver loongson3_cpufreq_driver = { |
| .name = "loongson3", |
| .flags = CPUFREQ_CONST_LOOPS, |
| .init = loongson3_cpufreq_cpu_init, |
| .exit = loongson3_cpufreq_cpu_exit, |
| .online = loongson3_cpufreq_cpu_online, |
| .offline = loongson3_cpufreq_cpu_offline, |
| .get = loongson3_cpufreq_get, |
| .target_index = loongson3_cpufreq_target, |
| .attr = cpufreq_generic_attr, |
| .verify = cpufreq_generic_frequency_table_verify, |
| .suspend = cpufreq_generic_suspend, |
| }; |
| |
| static int loongson3_cpufreq_probe(struct platform_device *pdev) |
| { |
| int i, ret; |
| |
| for (i = 0; i < MAX_PACKAGES; i++) |
| devm_mutex_init(&pdev->dev, &cpufreq_mutex[i]); |
| |
| ret = do_service_request(0, 0, CMD_GET_VERSION, 0, 0); |
| if (ret <= 0) |
| return -EPERM; |
| |
| ret = do_service_request(FEATURE_DVFS, 0, CMD_SET_FEATURE, |
| FEATURE_DVFS_ENABLE | FEATURE_DVFS_BOOST, 0); |
| if (ret < 0) |
| return -EPERM; |
| |
| loongson3_cpufreq_driver.driver_data = pdev; |
| |
| ret = cpufreq_register_driver(&loongson3_cpufreq_driver); |
| if (ret) |
| return ret; |
| |
| pr_info("cpufreq: Loongson-3 CPU frequency driver.\n"); |
| |
| return 0; |
| } |
| |
| static void loongson3_cpufreq_remove(struct platform_device *pdev) |
| { |
| cpufreq_unregister_driver(&loongson3_cpufreq_driver); |
| } |
| |
| static struct platform_device_id cpufreq_id_table[] = { |
| { "loongson3_cpufreq", }, |
| { /* sentinel */ } |
| }; |
| MODULE_DEVICE_TABLE(platform, cpufreq_id_table); |
| |
| static struct platform_driver loongson3_platform_driver = { |
| .driver = { |
| .name = "loongson3_cpufreq", |
| }, |
| .id_table = cpufreq_id_table, |
| .probe = loongson3_cpufreq_probe, |
| .remove_new = loongson3_cpufreq_remove, |
| }; |
| module_platform_driver(loongson3_platform_driver); |
| |
| MODULE_AUTHOR("Huacai Chen <chenhuacai@loongson.cn>"); |
| MODULE_DESCRIPTION("CPUFreq driver for Loongson-3 processors"); |
| MODULE_LICENSE("GPL"); |