|  | // SPDX-License-Identifier: GPL-2.0+ | 
|  | /* | 
|  | * Author: zhanghongchen <zhanghongchen@loongson.cn> | 
|  | *         Yinbo Zhu <zhuyinbo@loongson.cn> | 
|  | * Copyright (C) 2022-2023 Loongson Technology Corporation Limited | 
|  | */ | 
|  |  | 
|  | #include <linux/interrupt.h> | 
|  | #include <linux/io.h> | 
|  | #include <linux/minmax.h> | 
|  | #include <linux/mod_devicetable.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/platform_device.h> | 
|  | #include <linux/property.h> | 
|  | #include <linux/thermal.h> | 
|  | #include <linux/units.h> | 
|  |  | 
|  | #include "thermal_hwmon.h" | 
|  |  | 
|  | #define LOONGSON2_MAX_SENSOR_SEL_NUM	3 | 
|  |  | 
|  | #define LOONGSON2_THSENS_CTRL_HI_REG	0x0 | 
|  | #define LOONGSON2_THSENS_CTRL_LOW_REG	0x8 | 
|  | #define LOONGSON2_THSENS_STATUS_REG	0x10 | 
|  | #define LOONGSON2_THSENS_OUT_REG	0x14 | 
|  |  | 
|  | #define LOONGSON2_THSENS_INT_LO		BIT(0) | 
|  | #define LOONGSON2_THSENS_INT_HIGH	BIT(1) | 
|  | #define LOONGSON2_THSENS_INT_EN		(LOONGSON2_THSENS_INT_LO | \ | 
|  | LOONGSON2_THSENS_INT_HIGH) | 
|  | #define LOONGSON2_THSENS_OUT_MASK	0xFF | 
|  |  | 
|  | /* | 
|  | * This flag is used to indicate the temperature reading | 
|  | * method of the Loongson-2K2000 | 
|  | */ | 
|  | #define LS2K2000_THSENS_OUT_FLAG	BIT(0) | 
|  |  | 
|  | struct loongson2_thermal_chip_data { | 
|  | unsigned int thermal_sensor_sel; | 
|  | unsigned int flags; | 
|  | }; | 
|  |  | 
|  | struct loongson2_thermal_data { | 
|  | void __iomem *ctrl_reg; | 
|  | void __iomem *temp_reg; | 
|  | const struct loongson2_thermal_chip_data *chip_data; | 
|  | }; | 
|  |  | 
|  | static void loongson2_set_ctrl_regs(struct loongson2_thermal_data *data, | 
|  | int ctrl_data, bool low, bool enable) | 
|  | { | 
|  | int reg_ctrl = 0; | 
|  | int reg_off  = data->chip_data->thermal_sensor_sel * 2; | 
|  | int ctrl_reg = low ? LOONGSON2_THSENS_CTRL_LOW_REG : LOONGSON2_THSENS_CTRL_HI_REG; | 
|  |  | 
|  | reg_ctrl = ctrl_data + HECTO; | 
|  | reg_ctrl |= enable ? 0x100 : 0; | 
|  | writew(reg_ctrl, data->ctrl_reg + ctrl_reg + reg_off); | 
|  | } | 
|  |  | 
|  | static int loongson2_thermal_set(struct loongson2_thermal_data *data, | 
|  | int low, int high, bool enable) | 
|  | { | 
|  | /* Set low temperature threshold */ | 
|  | loongson2_set_ctrl_regs(data, clamp(-40, low, high), true, enable); | 
|  |  | 
|  | /* Set high temperature threshold */ | 
|  | loongson2_set_ctrl_regs(data, clamp(125, low, high), false, enable); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int loongson2_2k1000_get_temp(struct thermal_zone_device *tz, int *temp) | 
|  | { | 
|  | int val; | 
|  | struct loongson2_thermal_data *data = thermal_zone_device_priv(tz); | 
|  |  | 
|  | val = readl(data->ctrl_reg + LOONGSON2_THSENS_OUT_REG); | 
|  | *temp = ((val & LOONGSON2_THSENS_OUT_MASK) - HECTO) * KILO; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int loongson2_2k2000_get_temp(struct thermal_zone_device *tz, int *temp) | 
|  | { | 
|  | int val; | 
|  | struct loongson2_thermal_data *data = thermal_zone_device_priv(tz); | 
|  |  | 
|  | val = readl(data->temp_reg); | 
|  | *temp = ((val & 0xffff) * 820 / 0x4000 - 311) * KILO; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static irqreturn_t loongson2_thermal_irq_thread(int irq, void *dev) | 
|  | { | 
|  | struct thermal_zone_device *tzd = dev; | 
|  | struct loongson2_thermal_data *data = thermal_zone_device_priv(tzd); | 
|  |  | 
|  | writeb(LOONGSON2_THSENS_INT_EN, data->ctrl_reg + LOONGSON2_THSENS_STATUS_REG); | 
|  |  | 
|  | thermal_zone_device_update(tzd, THERMAL_EVENT_UNSPECIFIED); | 
|  |  | 
|  | return IRQ_HANDLED; | 
|  | } | 
|  |  | 
|  | static int loongson2_thermal_set_trips(struct thermal_zone_device *tz, int low, int high) | 
|  | { | 
|  | struct loongson2_thermal_data *data = thermal_zone_device_priv(tz); | 
|  |  | 
|  | return loongson2_thermal_set(data, low/MILLI, high/MILLI, true); | 
|  | } | 
|  |  | 
|  | static struct thermal_zone_device_ops loongson2_of_thermal_ops = { | 
|  | .get_temp = loongson2_2k1000_get_temp, | 
|  | .set_trips = loongson2_thermal_set_trips, | 
|  | }; | 
|  |  | 
|  | static int loongson2_thermal_probe(struct platform_device *pdev) | 
|  | { | 
|  | struct device *dev = &pdev->dev; | 
|  | struct loongson2_thermal_data *data; | 
|  | struct thermal_zone_device *tzd; | 
|  | int ret, irq, i; | 
|  |  | 
|  | data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); | 
|  | if (!data) | 
|  | return -ENOMEM; | 
|  |  | 
|  | data->chip_data = device_get_match_data(dev); | 
|  |  | 
|  | data->ctrl_reg = devm_platform_ioremap_resource(pdev, 0); | 
|  | if (IS_ERR(data->ctrl_reg)) | 
|  | return PTR_ERR(data->ctrl_reg); | 
|  |  | 
|  | /* The temperature output register is separate for Loongson-2K2000 */ | 
|  | if (data->chip_data->flags & LS2K2000_THSENS_OUT_FLAG) { | 
|  | data->temp_reg = devm_platform_ioremap_resource(pdev, 1); | 
|  | if (IS_ERR(data->temp_reg)) | 
|  | return PTR_ERR(data->temp_reg); | 
|  |  | 
|  | loongson2_of_thermal_ops.get_temp = loongson2_2k2000_get_temp; | 
|  | } | 
|  |  | 
|  | irq = platform_get_irq(pdev, 0); | 
|  | if (irq < 0) | 
|  | return irq; | 
|  |  | 
|  | writeb(LOONGSON2_THSENS_INT_EN, data->ctrl_reg + LOONGSON2_THSENS_STATUS_REG); | 
|  |  | 
|  | loongson2_thermal_set(data, 0, 0, false); | 
|  |  | 
|  | for (i = 0; i <= LOONGSON2_MAX_SENSOR_SEL_NUM; i++) { | 
|  | tzd = devm_thermal_of_zone_register(dev, i, data, | 
|  | &loongson2_of_thermal_ops); | 
|  |  | 
|  | if (!IS_ERR(tzd)) | 
|  | break; | 
|  |  | 
|  | if (PTR_ERR(tzd) != -ENODEV) | 
|  | continue; | 
|  |  | 
|  | return dev_err_probe(dev, PTR_ERR(tzd), "failed to register"); | 
|  | } | 
|  |  | 
|  | ret = devm_request_threaded_irq(dev, irq, NULL, loongson2_thermal_irq_thread, | 
|  | IRQF_ONESHOT, "loongson2_thermal", tzd); | 
|  | if (ret < 0) | 
|  | return dev_err_probe(dev, ret, "failed to request alarm irq\n"); | 
|  |  | 
|  | devm_thermal_add_hwmon_sysfs(dev, tzd); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct loongson2_thermal_chip_data loongson2_thermal_ls2k1000_data = { | 
|  | .thermal_sensor_sel = 0, | 
|  | .flags = 0, | 
|  | }; | 
|  |  | 
|  | static const struct loongson2_thermal_chip_data loongson2_thermal_ls2k2000_data = { | 
|  | .thermal_sensor_sel = 0, | 
|  | .flags = LS2K2000_THSENS_OUT_FLAG, | 
|  | }; | 
|  |  | 
|  | static const struct of_device_id of_loongson2_thermal_match[] = { | 
|  | { | 
|  | .compatible = "loongson,ls2k1000-thermal", | 
|  | .data = &loongson2_thermal_ls2k1000_data, | 
|  | }, | 
|  | { | 
|  | .compatible = "loongson,ls2k2000-thermal", | 
|  | .data = &loongson2_thermal_ls2k2000_data, | 
|  | }, | 
|  | { /* end */ } | 
|  | }; | 
|  | MODULE_DEVICE_TABLE(of, of_loongson2_thermal_match); | 
|  |  | 
|  | static struct platform_driver loongson2_thermal_driver = { | 
|  | .driver = { | 
|  | .name		= "loongson2_thermal", | 
|  | .of_match_table = of_loongson2_thermal_match, | 
|  | }, | 
|  | .probe	= loongson2_thermal_probe, | 
|  | }; | 
|  | module_platform_driver(loongson2_thermal_driver); | 
|  |  | 
|  | MODULE_DESCRIPTION("Loongson2 thermal driver"); | 
|  | MODULE_AUTHOR("Loongson Technology Corporation Limited"); | 
|  | MODULE_LICENSE("GPL"); |