| // SPDX-License-Identifier: GPL-2.0 | 
 | /* | 
 |  * Copyright (c) 2020 MediaTek Inc. | 
 |  */ | 
 |  | 
 | #include <linux/bitfield.h> | 
 | #include <linux/cpufreq.h> | 
 | #include <linux/energy_model.h> | 
 | #include <linux/init.h> | 
 | #include <linux/iopoll.h> | 
 | #include <linux/kernel.h> | 
 | #include <linux/module.h> | 
 | #include <linux/of_address.h> | 
 | #include <linux/of_platform.h> | 
 | #include <linux/slab.h> | 
 |  | 
 | #define LUT_MAX_ENTRIES			32U | 
 | #define LUT_FREQ			GENMASK(11, 0) | 
 | #define LUT_ROW_SIZE			0x4 | 
 | #define CPUFREQ_HW_STATUS		BIT(0) | 
 | #define SVS_HW_STATUS			BIT(1) | 
 | #define POLL_USEC			1000 | 
 | #define TIMEOUT_USEC			300000 | 
 |  | 
 | enum { | 
 | 	REG_FREQ_LUT_TABLE, | 
 | 	REG_FREQ_ENABLE, | 
 | 	REG_FREQ_PERF_STATE, | 
 | 	REG_FREQ_HW_STATE, | 
 | 	REG_EM_POWER_TBL, | 
 | 	REG_FREQ_LATENCY, | 
 |  | 
 | 	REG_ARRAY_SIZE, | 
 | }; | 
 |  | 
 | struct mtk_cpufreq_data { | 
 | 	struct cpufreq_frequency_table *table; | 
 | 	void __iomem *reg_bases[REG_ARRAY_SIZE]; | 
 | 	int nr_opp; | 
 | }; | 
 |  | 
 | static const u16 cpufreq_mtk_offsets[REG_ARRAY_SIZE] = { | 
 | 	[REG_FREQ_LUT_TABLE]	= 0x0, | 
 | 	[REG_FREQ_ENABLE]	= 0x84, | 
 | 	[REG_FREQ_PERF_STATE]	= 0x88, | 
 | 	[REG_FREQ_HW_STATE]	= 0x8c, | 
 | 	[REG_EM_POWER_TBL]	= 0x90, | 
 | 	[REG_FREQ_LATENCY]	= 0x110, | 
 | }; | 
 |  | 
 | static int __maybe_unused | 
 | mtk_cpufreq_get_cpu_power(unsigned long *mW, | 
 | 			  unsigned long *KHz, struct device *cpu_dev) | 
 | { | 
 | 	struct mtk_cpufreq_data *data; | 
 | 	struct cpufreq_policy *policy; | 
 | 	int i; | 
 |  | 
 | 	policy = cpufreq_cpu_get_raw(cpu_dev->id); | 
 | 	if (!policy) | 
 | 		return 0; | 
 |  | 
 | 	data = policy->driver_data; | 
 |  | 
 | 	for (i = 0; i < data->nr_opp; i++) { | 
 | 		if (data->table[i].frequency < *KHz) | 
 | 			break; | 
 | 	} | 
 | 	i--; | 
 |  | 
 | 	*KHz = data->table[i].frequency; | 
 | 	*mW = readl_relaxed(data->reg_bases[REG_EM_POWER_TBL] + | 
 | 			    i * LUT_ROW_SIZE) / 1000; | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int mtk_cpufreq_hw_target_index(struct cpufreq_policy *policy, | 
 | 				       unsigned int index) | 
 | { | 
 | 	struct mtk_cpufreq_data *data = policy->driver_data; | 
 |  | 
 | 	writel_relaxed(index, data->reg_bases[REG_FREQ_PERF_STATE]); | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static unsigned int mtk_cpufreq_hw_get(unsigned int cpu) | 
 | { | 
 | 	struct mtk_cpufreq_data *data; | 
 | 	struct cpufreq_policy *policy; | 
 | 	unsigned int index; | 
 |  | 
 | 	policy = cpufreq_cpu_get_raw(cpu); | 
 | 	if (!policy) | 
 | 		return 0; | 
 |  | 
 | 	data = policy->driver_data; | 
 |  | 
 | 	index = readl_relaxed(data->reg_bases[REG_FREQ_PERF_STATE]); | 
 | 	index = min(index, LUT_MAX_ENTRIES - 1); | 
 |  | 
 | 	return data->table[index].frequency; | 
 | } | 
 |  | 
 | static unsigned int mtk_cpufreq_hw_fast_switch(struct cpufreq_policy *policy, | 
 | 					       unsigned int target_freq) | 
 | { | 
 | 	struct mtk_cpufreq_data *data = policy->driver_data; | 
 | 	unsigned int index; | 
 |  | 
 | 	index = cpufreq_table_find_index_dl(policy, target_freq); | 
 |  | 
 | 	writel_relaxed(index, data->reg_bases[REG_FREQ_PERF_STATE]); | 
 |  | 
 | 	return policy->freq_table[index].frequency; | 
 | } | 
 |  | 
 | static int mtk_cpu_create_freq_table(struct platform_device *pdev, | 
 | 				     struct mtk_cpufreq_data *data) | 
 | { | 
 | 	struct device *dev = &pdev->dev; | 
 | 	u32 temp, i, freq, prev_freq = 0; | 
 | 	void __iomem *base_table; | 
 |  | 
 | 	data->table = devm_kcalloc(dev, LUT_MAX_ENTRIES + 1, | 
 | 				   sizeof(*data->table), GFP_KERNEL); | 
 | 	if (!data->table) | 
 | 		return -ENOMEM; | 
 |  | 
 | 	base_table = data->reg_bases[REG_FREQ_LUT_TABLE]; | 
 |  | 
 | 	for (i = 0; i < LUT_MAX_ENTRIES; i++) { | 
 | 		temp = readl_relaxed(base_table + (i * LUT_ROW_SIZE)); | 
 | 		freq = FIELD_GET(LUT_FREQ, temp) * 1000; | 
 |  | 
 | 		if (freq == prev_freq) | 
 | 			break; | 
 |  | 
 | 		data->table[i].frequency = freq; | 
 |  | 
 | 		dev_dbg(dev, "index=%d freq=%d\n", i, data->table[i].frequency); | 
 |  | 
 | 		prev_freq = freq; | 
 | 	} | 
 |  | 
 | 	data->table[i].frequency = CPUFREQ_TABLE_END; | 
 | 	data->nr_opp = i; | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int mtk_cpu_resources_init(struct platform_device *pdev, | 
 | 				  struct cpufreq_policy *policy, | 
 | 				  const u16 *offsets) | 
 | { | 
 | 	struct mtk_cpufreq_data *data; | 
 | 	struct device *dev = &pdev->dev; | 
 | 	void __iomem *base; | 
 | 	int ret, i; | 
 | 	int index; | 
 |  | 
 | 	data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); | 
 | 	if (!data) | 
 | 		return -ENOMEM; | 
 |  | 
 | 	index = of_perf_domain_get_sharing_cpumask(policy->cpu, "performance-domains", | 
 | 						   "#performance-domain-cells", | 
 | 						   policy->cpus); | 
 | 	if (index < 0) | 
 | 		return index; | 
 |  | 
 | 	base = devm_platform_ioremap_resource(pdev, index); | 
 | 	if (IS_ERR(base)) | 
 | 		return PTR_ERR(base); | 
 |  | 
 | 	for (i = REG_FREQ_LUT_TABLE; i < REG_ARRAY_SIZE; i++) | 
 | 		data->reg_bases[i] = base + offsets[i]; | 
 |  | 
 | 	ret = mtk_cpu_create_freq_table(pdev, data); | 
 | 	if (ret) { | 
 | 		dev_info(dev, "Domain-%d failed to create freq table\n", index); | 
 | 		return ret; | 
 | 	} | 
 |  | 
 | 	policy->freq_table = data->table; | 
 | 	policy->driver_data = data; | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int mtk_cpufreq_hw_cpu_init(struct cpufreq_policy *policy) | 
 | { | 
 | 	struct platform_device *pdev = cpufreq_get_driver_data(); | 
 | 	int sig, pwr_hw = CPUFREQ_HW_STATUS | SVS_HW_STATUS; | 
 | 	struct mtk_cpufreq_data *data; | 
 | 	unsigned int latency; | 
 | 	int ret; | 
 |  | 
 | 	/* Get the bases of cpufreq for domains */ | 
 | 	ret = mtk_cpu_resources_init(pdev, policy, platform_get_drvdata(pdev)); | 
 | 	if (ret) { | 
 | 		dev_info(&pdev->dev, "CPUFreq resource init failed\n"); | 
 | 		return ret; | 
 | 	} | 
 |  | 
 | 	data = policy->driver_data; | 
 |  | 
 | 	latency = readl_relaxed(data->reg_bases[REG_FREQ_LATENCY]) * 1000; | 
 | 	if (!latency) | 
 | 		latency = CPUFREQ_ETERNAL; | 
 |  | 
 | 	policy->cpuinfo.transition_latency = latency; | 
 | 	policy->fast_switch_possible = true; | 
 |  | 
 | 	/* HW should be in enabled state to proceed now */ | 
 | 	writel_relaxed(0x1, data->reg_bases[REG_FREQ_ENABLE]); | 
 | 	if (readl_poll_timeout(data->reg_bases[REG_FREQ_HW_STATE], sig, | 
 | 			       (sig & pwr_hw) == pwr_hw, POLL_USEC, | 
 | 			       TIMEOUT_USEC)) { | 
 | 		if (!(sig & CPUFREQ_HW_STATUS)) { | 
 | 			pr_info("cpufreq hardware of CPU%d is not enabled\n", | 
 | 				policy->cpu); | 
 | 			return -ENODEV; | 
 | 		} | 
 |  | 
 | 		pr_info("SVS of CPU%d is not enabled\n", policy->cpu); | 
 | 	} | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int mtk_cpufreq_hw_cpu_exit(struct cpufreq_policy *policy) | 
 | { | 
 | 	struct mtk_cpufreq_data *data = policy->driver_data; | 
 |  | 
 | 	/* HW should be in paused state now */ | 
 | 	writel_relaxed(0x0, data->reg_bases[REG_FREQ_ENABLE]); | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static void mtk_cpufreq_register_em(struct cpufreq_policy *policy) | 
 | { | 
 | 	struct em_data_callback em_cb = EM_DATA_CB(mtk_cpufreq_get_cpu_power); | 
 | 	struct mtk_cpufreq_data *data = policy->driver_data; | 
 |  | 
 | 	em_dev_register_perf_domain(get_cpu_device(policy->cpu), data->nr_opp, | 
 | 				    &em_cb, policy->cpus, true); | 
 | } | 
 |  | 
 | static struct cpufreq_driver cpufreq_mtk_hw_driver = { | 
 | 	.flags		= CPUFREQ_NEED_INITIAL_FREQ_CHECK | | 
 | 			  CPUFREQ_HAVE_GOVERNOR_PER_POLICY | | 
 | 			  CPUFREQ_IS_COOLING_DEV, | 
 | 	.verify		= cpufreq_generic_frequency_table_verify, | 
 | 	.target_index	= mtk_cpufreq_hw_target_index, | 
 | 	.get		= mtk_cpufreq_hw_get, | 
 | 	.init		= mtk_cpufreq_hw_cpu_init, | 
 | 	.exit		= mtk_cpufreq_hw_cpu_exit, | 
 | 	.register_em	= mtk_cpufreq_register_em, | 
 | 	.fast_switch	= mtk_cpufreq_hw_fast_switch, | 
 | 	.name		= "mtk-cpufreq-hw", | 
 | 	.attr		= cpufreq_generic_attr, | 
 | }; | 
 |  | 
 | static int mtk_cpufreq_hw_driver_probe(struct platform_device *pdev) | 
 | { | 
 | 	const void *data; | 
 | 	int ret; | 
 |  | 
 | 	data = of_device_get_match_data(&pdev->dev); | 
 | 	if (!data) | 
 | 		return -EINVAL; | 
 |  | 
 | 	platform_set_drvdata(pdev, (void *) data); | 
 | 	cpufreq_mtk_hw_driver.driver_data = pdev; | 
 |  | 
 | 	ret = cpufreq_register_driver(&cpufreq_mtk_hw_driver); | 
 | 	if (ret) | 
 | 		dev_err(&pdev->dev, "CPUFreq HW driver failed to register\n"); | 
 |  | 
 | 	return ret; | 
 | } | 
 |  | 
 | static int mtk_cpufreq_hw_driver_remove(struct platform_device *pdev) | 
 | { | 
 | 	return cpufreq_unregister_driver(&cpufreq_mtk_hw_driver); | 
 | } | 
 |  | 
 | static const struct of_device_id mtk_cpufreq_hw_match[] = { | 
 | 	{ .compatible = "mediatek,cpufreq-hw", .data = &cpufreq_mtk_offsets }, | 
 | 	{} | 
 | }; | 
 |  | 
 | static struct platform_driver mtk_cpufreq_hw_driver = { | 
 | 	.probe = mtk_cpufreq_hw_driver_probe, | 
 | 	.remove = mtk_cpufreq_hw_driver_remove, | 
 | 	.driver = { | 
 | 		.name = "mtk-cpufreq-hw", | 
 | 		.of_match_table = mtk_cpufreq_hw_match, | 
 | 	}, | 
 | }; | 
 | module_platform_driver(mtk_cpufreq_hw_driver); | 
 |  | 
 | MODULE_AUTHOR("Hector Yuan <hector.yuan@mediatek.com>"); | 
 | MODULE_DESCRIPTION("Mediatek cpufreq-hw driver"); | 
 | MODULE_LICENSE("GPL v2"); |