diff options
Diffstat (limited to 'drivers/thermal')
101 files changed, 32964 insertions, 0 deletions
diff --git a/drivers/thermal/Kconfig b/drivers/thermal/Kconfig new file mode 100644 index 000000000..0e69edc77 --- /dev/null +++ b/drivers/thermal/Kconfig @@ -0,0 +1,492 @@ +# +# Generic thermal sysfs drivers configuration +# + +menuconfig THERMAL + tristate "Generic Thermal sysfs driver" + help + Generic Thermal Sysfs driver offers a generic mechanism for + thermal management. Usually it's made up of one or more thermal + zone and cooling device. + Each thermal zone contains its own temperature, trip points, + cooling devices. + All platforms with ACPI thermal support can use this driver. + If you want this support, you should say Y or M here. + +if THERMAL + +config THERMAL_STATISTICS + bool "Thermal state transition statistics" + help + Export thermal state transition statistics information through sysfs. + + If in doubt, say N. + +config THERMAL_EMERGENCY_POWEROFF_DELAY_MS + int "Emergency poweroff delay in milli-seconds" + depends on THERMAL + default 0 + help + Thermal subsystem will issue a graceful shutdown when + critical temperatures are reached using orderly_poweroff(). In + case of failure of an orderly_poweroff(), the thermal emergency + poweroff kicks in after a delay has elapsed and shuts down the system. + This config is number of milliseconds to delay before emergency + poweroff kicks in. Similarly to the critical trip point, + the delay should be carefully profiled so as to give adequate + time for orderly_poweroff() to finish on regular execution. + If set to 0 emergency poweroff will not be supported. + + In doubt, leave as 0. + +config THERMAL_HWMON + bool + prompt "Expose thermal sensors as hwmon device" + depends on HWMON=y || HWMON=THERMAL + default y + help + In case a sensor is registered with the thermal + framework, this option will also register it + as a hwmon. The sensor will then have the common + hwmon sysfs interface. + + Say 'Y' here if you want all thermal sensors to + have hwmon sysfs interface too. + +config THERMAL_OF + bool + prompt "APIs to parse thermal data out of device tree" + depends on OF + default y + help + This options provides helpers to add the support to + read and parse thermal data definitions out of the + device tree blob. + + Say 'Y' here if you need to build thermal infrastructure + based on device tree. + +config THERMAL_WRITABLE_TRIPS + bool "Enable writable trip points" + help + This option allows the system integrator to choose whether + trip temperatures can be changed from userspace. The + writable trips need to be specified when setting up the + thermal zone but the choice here takes precedence. + + Say 'Y' here if you would like to allow userspace tools to + change trip temperatures. + +choice + prompt "Default Thermal governor" + default THERMAL_DEFAULT_GOV_STEP_WISE + help + This option sets which thermal governor shall be loaded at + startup. If in doubt, select 'step_wise'. + +config THERMAL_DEFAULT_GOV_STEP_WISE + bool "step_wise" + select THERMAL_GOV_STEP_WISE + help + Use the step_wise governor as default. This throttles the + devices one step at a time. + +config THERMAL_DEFAULT_GOV_FAIR_SHARE + bool "fair_share" + select THERMAL_GOV_FAIR_SHARE + help + Use the fair_share governor as default. This throttles the + devices based on their 'contribution' to a zone. The + contribution should be provided through platform data. + +config THERMAL_DEFAULT_GOV_USER_SPACE + bool "user_space" + select THERMAL_GOV_USER_SPACE + help + Select this if you want to let the user space manage the + platform thermals. + +config THERMAL_DEFAULT_GOV_POWER_ALLOCATOR + bool "power_allocator" + select THERMAL_GOV_POWER_ALLOCATOR + help + Select this if you want to control temperature based on + system and device power allocation. This governor can only + operate on cooling devices that implement the power API. + +endchoice + +config THERMAL_GOV_FAIR_SHARE + bool "Fair-share thermal governor" + help + Enable this to manage platform thermals using fair-share governor. + +config THERMAL_GOV_STEP_WISE + bool "Step_wise thermal governor" + help + Enable this to manage platform thermals using a simple linear + governor. + +config THERMAL_GOV_BANG_BANG + bool "Bang Bang thermal governor" + default n + help + Enable this to manage platform thermals using bang bang governor. + + Say 'Y' here if you want to use two point temperature regulation + used for fans without throttling. Some fan drivers depend on this + governor to be enabled (e.g. acerhdf). + +config THERMAL_GOV_USER_SPACE + bool "User_space thermal governor" + help + Enable this to let the user space manage the platform thermals. + +config THERMAL_GOV_POWER_ALLOCATOR + bool "Power allocator thermal governor" + help + Enable this to manage platform thermals by dynamically + allocating and limiting power to devices. + +config CPU_THERMAL + bool "generic cpu cooling support" + depends on CPU_FREQ + depends on THERMAL_OF + help + This implements the generic cpu cooling mechanism through frequency + reduction. An ACPI version of this already exists + (drivers/acpi/processor_thermal.c). + This will be useful for platforms using the generic thermal interface + and not the ACPI interface. + + If you want this support, you should say Y here. + +config CLOCK_THERMAL + bool "Generic clock cooling support" + depends on COMMON_CLK + depends on PM_OPP + help + This entry implements the generic clock cooling mechanism through + frequency clipping. Typically used to cool off co-processors. The + device that is configured to use this cooling mechanism will be + controlled to reduce clock frequency whenever temperature is high. + +config DEVFREQ_THERMAL + bool "Generic device cooling support" + depends on PM_DEVFREQ + depends on PM_OPP + help + This implements the generic devfreq cooling mechanism through + frequency reduction for devices using devfreq. + + This will throttle the device by limiting the maximum allowed DVFS + frequency corresponding to the cooling level. + + In order to use the power extensions of the cooling device, + devfreq should use the simple_ondemand governor. + + If you want this support, you should say Y here. + +config THERMAL_EMULATION + bool "Thermal emulation mode support" + help + Enable this option to make a emul_temp sysfs node in thermal zone + directory to support temperature emulation. With emulation sysfs node, + user can manually input temperature and test the different trip + threshold behaviour for simulation purpose. + + WARNING: Be careful while enabling this option on production systems, + because userland can easily disable the thermal policy by simply + flooding this sysfs node with low temperature values. + +config HISI_THERMAL + tristate "Hisilicon thermal driver" + depends on ARCH_HISI || COMPILE_TEST + depends on HAS_IOMEM + depends on OF + default y + help + Enable this to plug hisilicon's thermal sensor driver into the Linux + thermal framework. cpufreq is used as the cooling device to throttle + CPUs when the passive trip is crossed. + +config IMX_THERMAL + tristate "Temperature sensor driver for Freescale i.MX SoCs" + depends on (ARCH_MXC && CPU_THERMAL) || COMPILE_TEST + depends on NVMEM || !NVMEM + depends on MFD_SYSCON + depends on OF + help + Support for Temperature Monitor (TEMPMON) found on Freescale i.MX SoCs. + It supports one critical trip point and one passive trip point. The + cpufreq is used as the cooling device to throttle CPUs when the + passive trip is crossed. + +config MAX77620_THERMAL + tristate "Temperature sensor driver for Maxim MAX77620 PMIC" + depends on MFD_MAX77620 + depends on OF + help + Support for die junction temperature warning alarm for Maxim + Semiconductor PMIC MAX77620 device. Device generates two alarm + interrupts when PMIC die temperature cross the threshold of + 120 degC and 140 degC. + +config QORIQ_THERMAL + tristate "QorIQ Thermal Monitoring Unit" + depends on THERMAL_OF + depends on HAS_IOMEM + help + Support for Thermal Monitoring Unit (TMU) found on QorIQ platforms. + It supports one critical trip point and one passive trip point. The + cpufreq is used as the cooling device to throttle CPUs when the + passive trip is crossed. + +config SPEAR_THERMAL + tristate "SPEAr thermal sensor driver" + depends on PLAT_SPEAR || COMPILE_TEST + depends on HAS_IOMEM + depends on OF + help + Enable this to plug the SPEAr thermal sensor driver into the Linux + thermal framework. + +config ROCKCHIP_THERMAL + tristate "Rockchip thermal driver" + depends on ARCH_ROCKCHIP || COMPILE_TEST + depends on RESET_CONTROLLER + depends on HAS_IOMEM + help + Rockchip thermal driver provides support for Temperature sensor + ADC (TS-ADC) found on Rockchip SoCs. It supports one critical + trip point. Cpufreq is used as the cooling device and will throttle + CPUs when the Temperature crosses the passive trip point. + +config RCAR_THERMAL + tristate "Renesas R-Car thermal driver" + depends on ARCH_RENESAS || COMPILE_TEST + depends on HAS_IOMEM + help + Enable this to plug the R-Car thermal sensor driver into the Linux + thermal framework. + +config RCAR_GEN3_THERMAL + tristate "Renesas R-Car Gen3 thermal driver" + depends on ARCH_RENESAS || COMPILE_TEST + depends on HAS_IOMEM + depends on OF + help + Enable this to plug the R-Car Gen3 thermal sensor driver into the Linux + thermal framework. + +config KIRKWOOD_THERMAL + tristate "Temperature sensor on Marvell Kirkwood SoCs" + depends on MACH_KIRKWOOD || COMPILE_TEST + depends on HAS_IOMEM + depends on OF + help + Support for the Kirkwood thermal sensor driver into the Linux thermal + framework. Only kirkwood 88F6282 and 88F6283 have this sensor. + +config DOVE_THERMAL + tristate "Temperature sensor on Marvell Dove SoCs" + depends on ARCH_DOVE || MACH_DOVE || COMPILE_TEST + depends on HAS_IOMEM + depends on OF + help + Support for the Dove thermal sensor driver in the Linux thermal + framework. + +config DB8500_THERMAL + tristate "DB8500 thermal management" + depends on MFD_DB8500_PRCMU + default y + help + Adds DB8500 thermal management implementation according to the thermal + management framework. A thermal zone with several trip points will be + created. Cooling devices can be bound to the trip points to cool this + thermal zone if trip points reached. + +config ARMADA_THERMAL + tristate "Marvell EBU Armada SoCs thermal management" + depends on ARCH_MVEBU || COMPILE_TEST + depends on HAS_IOMEM + depends on OF + help + Enable this option if you want to have support for thermal management + controller present in Marvell EBU Armada SoCs (370,375,XP,38x,7K,8K). + +config DA9062_THERMAL + tristate "DA9062/DA9061 Dialog Semiconductor thermal driver" + depends on MFD_DA9062 || COMPILE_TEST + depends on OF + help + Enable this for the Dialog Semiconductor thermal sensor driver. + This will report PMIC junction over-temperature for one thermal trip + zone. + Compatible with the DA9062 and DA9061 PMICs. + +config INTEL_POWERCLAMP + tristate "Intel PowerClamp idle injection driver" + depends on THERMAL + depends on X86 + depends on CPU_SUP_INTEL + help + Enable this to enable Intel PowerClamp idle injection driver. This + enforce idle time which results in more package C-state residency. The + user interface is exposed via generic thermal framework. + +config X86_PKG_TEMP_THERMAL + tristate "X86 package temperature thermal driver" + depends on X86_THERMAL_VECTOR + select THERMAL_GOV_USER_SPACE + select THERMAL_WRITABLE_TRIPS + default m + help + Enable this to register CPU digital sensor for package temperature as + thermal zone. Each package will have its own thermal zone. There are + two trip points which can be set by user to get notifications via thermal + notification methods. + +config INTEL_SOC_DTS_IOSF_CORE + tristate + depends on X86 && PCI + select IOSF_MBI + help + This is becoming a common feature for Intel SoCs to expose the additional + digital temperature sensors (DTSs) using side band interface (IOSF). This + implements the common set of helper functions to register, get temperature + and get/set thresholds on DTSs. + +config INTEL_SOC_DTS_THERMAL + tristate "Intel SoCs DTS thermal driver" + depends on X86 && PCI && ACPI + select INTEL_SOC_DTS_IOSF_CORE + select THERMAL_WRITABLE_TRIPS + help + Enable this to register Intel SoCs (e.g. Bay Trail) platform digital + temperature sensor (DTS). These SoCs have two additional DTSs in + addition to DTSs on CPU cores. Each DTS will be registered as a + thermal zone. There are two trip points. One of the trip point can + be set by user mode programs to get notifications via Linux thermal + notification methods.The other trip is a critical trip point, which + was set by the driver based on the TJ MAX temperature. + +config INTEL_QUARK_DTS_THERMAL + tristate "Intel Quark DTS thermal driver" + depends on X86_INTEL_QUARK + help + Enable this to register Intel Quark SoC (e.g. X1000) platform digital + temperature sensor (DTS). For X1000 SoC, it has one on-die DTS. + The DTS will be registered as a thermal zone. There are two trip points: + hot & critical. The critical trip point default value is set by + underlying BIOS/Firmware. + +menu "ACPI INT340X thermal drivers" +source drivers/thermal/int340x_thermal/Kconfig +endmenu + +config INTEL_BXT_PMIC_THERMAL + tristate "Intel Broxton PMIC thermal driver" + depends on X86 && INTEL_SOC_PMIC_BXTWC && REGMAP + help + Select this driver for Intel Broxton PMIC with ADC channels monitoring + system temperature measurements and alerts. + This driver is used for monitoring the ADC channels of PMIC and handles + the alert trip point interrupts and notifies the thermal framework with + the trip point and temperature details of the zone. + +config INTEL_PCH_THERMAL + tristate "Intel PCH Thermal Reporting Driver" + depends on X86 && PCI + help + Enable this to support thermal reporting on certain intel PCHs. + Thermal reporting device will provide temperature reading, + programmable trip points and other information. + +config MTK_THERMAL + tristate "Temperature sensor driver for mediatek SoCs" + depends on ARCH_MEDIATEK || COMPILE_TEST + depends on HAS_IOMEM + depends on NVMEM || NVMEM=n + depends on RESET_CONTROLLER + default y + help + Enable this option if you want to have support for thermal management + controller present in Mediatek SoCs + +menu "Broadcom thermal drivers" +depends on ARCH_BCM || ARCH_BRCMSTB || ARCH_BCM2835 || COMPILE_TEST +source "drivers/thermal/broadcom/Kconfig" +endmenu + +menu "Texas Instruments thermal drivers" +depends on ARCH_HAS_BANDGAP || COMPILE_TEST +depends on HAS_IOMEM +source "drivers/thermal/ti-soc-thermal/Kconfig" +endmenu + +menu "Samsung thermal drivers" +depends on ARCH_EXYNOS || COMPILE_TEST +source "drivers/thermal/samsung/Kconfig" +endmenu + +menu "STMicroelectronics thermal drivers" +depends on ARCH_STI && OF +source "drivers/thermal/st/Kconfig" +endmenu + +config TANGO_THERMAL + tristate "Tango thermal management" + depends on ARCH_TANGO || COMPILE_TEST + help + Enable the Tango thermal driver, which supports the primitive + temperature sensor embedded in Tango chips since the SMP8758. + This sensor only generates a 1-bit signal to indicate whether + the die temperature exceeds a programmable threshold. + +source "drivers/thermal/tegra/Kconfig" + +config QCOM_SPMI_TEMP_ALARM + tristate "Qualcomm SPMI PMIC Temperature Alarm" + depends on OF && SPMI && IIO + select REGMAP_SPMI + help + This enables a thermal sysfs driver for Qualcomm plug-and-play (QPNP) + PMIC devices. It shows up in sysfs as a thermal sensor with multiple + trip points. The temperature reported by the thermal sensor reflects the + real time die temperature if an ADC is present or an estimate of the + temperature based upon the over temperature stage value. + +config GENERIC_ADC_THERMAL + tristate "Generic ADC based thermal sensor" + depends on IIO + help + This enabled a thermal sysfs driver for the temperature sensor + which is connected to the General Purpose ADC. The ADC channel + is read via IIO framework and the channel information is provided + to this driver. This driver reports the temperature by reading ADC + channel and converts it to temperature based on lookup table. + +menu "Qualcomm thermal drivers" +depends on (ARCH_QCOM && OF) || COMPILE_TEST +source "drivers/thermal/qcom/Kconfig" +endmenu + +config ZX2967_THERMAL + tristate "Thermal sensors on zx2967 SoC" + depends on ARCH_ZX || COMPILE_TEST + help + Enable the zx2967 thermal sensors driver, which supports + the primitive temperature sensor embedded in zx2967 SoCs. + This sensor generates the real time die temperature. + +config UNIPHIER_THERMAL + tristate "Socionext UniPhier thermal driver" + depends on ARCH_UNIPHIER || COMPILE_TEST + depends on THERMAL_OF && MFD_SYSCON + help + Enable this to plug in UniPhier on-chip PVT thermal driver into the + thermal framework. The driver supports CPU thermal zone temperature + reporting and a couple of trip points. +endif diff --git a/drivers/thermal/Makefile b/drivers/thermal/Makefile new file mode 100644 index 000000000..610344eb3 --- /dev/null +++ b/drivers/thermal/Makefile @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for sensor chip drivers. +# + +obj-$(CONFIG_THERMAL) += thermal_sys.o +thermal_sys-y += thermal_core.o thermal_sysfs.o \ + thermal_helpers.o + +# interface to/from other layers providing sensors +thermal_sys-$(CONFIG_THERMAL_HWMON) += thermal_hwmon.o +thermal_sys-$(CONFIG_THERMAL_OF) += of-thermal.o + +# governors +thermal_sys-$(CONFIG_THERMAL_GOV_FAIR_SHARE) += fair_share.o +thermal_sys-$(CONFIG_THERMAL_GOV_BANG_BANG) += gov_bang_bang.o +thermal_sys-$(CONFIG_THERMAL_GOV_STEP_WISE) += step_wise.o +thermal_sys-$(CONFIG_THERMAL_GOV_USER_SPACE) += user_space.o +thermal_sys-$(CONFIG_THERMAL_GOV_POWER_ALLOCATOR) += power_allocator.o + +# cpufreq cooling +thermal_sys-$(CONFIG_CPU_THERMAL) += cpu_cooling.o + +# clock cooling +thermal_sys-$(CONFIG_CLOCK_THERMAL) += clock_cooling.o + +# devfreq cooling +thermal_sys-$(CONFIG_DEVFREQ_THERMAL) += devfreq_cooling.o + +# platform thermal drivers +obj-y += broadcom/ +obj-$(CONFIG_QCOM_SPMI_TEMP_ALARM) += qcom-spmi-temp-alarm.o +obj-$(CONFIG_SPEAR_THERMAL) += spear_thermal.o +obj-$(CONFIG_ROCKCHIP_THERMAL) += rockchip_thermal.o +obj-$(CONFIG_RCAR_THERMAL) += rcar_thermal.o +obj-$(CONFIG_RCAR_GEN3_THERMAL) += rcar_gen3_thermal.o +obj-$(CONFIG_KIRKWOOD_THERMAL) += kirkwood_thermal.o +obj-y += samsung/ +obj-$(CONFIG_DOVE_THERMAL) += dove_thermal.o +obj-$(CONFIG_DB8500_THERMAL) += db8500_thermal.o +obj-$(CONFIG_ARMADA_THERMAL) += armada_thermal.o +obj-$(CONFIG_TANGO_THERMAL) += tango_thermal.o +obj-$(CONFIG_IMX_THERMAL) += imx_thermal.o +obj-$(CONFIG_MAX77620_THERMAL) += max77620_thermal.o +obj-$(CONFIG_QORIQ_THERMAL) += qoriq_thermal.o +obj-$(CONFIG_DA9062_THERMAL) += da9062-thermal.o +obj-$(CONFIG_INTEL_POWERCLAMP) += intel_powerclamp.o +obj-$(CONFIG_X86_PKG_TEMP_THERMAL) += x86_pkg_temp_thermal.o +obj-$(CONFIG_INTEL_SOC_DTS_IOSF_CORE) += intel_soc_dts_iosf.o +obj-$(CONFIG_INTEL_SOC_DTS_THERMAL) += intel_soc_dts_thermal.o +obj-$(CONFIG_INTEL_QUARK_DTS_THERMAL) += intel_quark_dts_thermal.o +obj-$(CONFIG_TI_SOC_THERMAL) += ti-soc-thermal/ +obj-$(CONFIG_INT340X_THERMAL) += int340x_thermal/ +obj-$(CONFIG_INTEL_BXT_PMIC_THERMAL) += intel_bxt_pmic_thermal.o +obj-$(CONFIG_INTEL_PCH_THERMAL) += intel_pch_thermal.o +obj-$(CONFIG_ST_THERMAL) += st/ +obj-$(CONFIG_QCOM_TSENS) += qcom/ +obj-y += tegra/ +obj-$(CONFIG_HISI_THERMAL) += hisi_thermal.o +obj-$(CONFIG_MTK_THERMAL) += mtk_thermal.o +obj-$(CONFIG_GENERIC_ADC_THERMAL) += thermal-generic-adc.o +obj-$(CONFIG_ZX2967_THERMAL) += zx2967_thermal.o +obj-$(CONFIG_UNIPHIER_THERMAL) += uniphier_thermal.o diff --git a/drivers/thermal/armada_thermal.c b/drivers/thermal/armada_thermal.c new file mode 100644 index 000000000..1c9830b2c --- /dev/null +++ b/drivers/thermal/armada_thermal.c @@ -0,0 +1,723 @@ +/* + * Marvell EBU Armada SoCs thermal sensor driver + * + * Copyright (C) 2013 Marvell + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#include <linux/device.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/of.h> +#include <linux/module.h> +#include <linux/delay.h> +#include <linux/platform_device.h> +#include <linux/of_device.h> +#include <linux/thermal.h> +#include <linux/iopoll.h> +#include <linux/mfd/syscon.h> +#include <linux/regmap.h> + +/* Thermal Manager Control and Status Register */ +#define PMU_TDC0_SW_RST_MASK (0x1 << 1) +#define PMU_TM_DISABLE_OFFS 0 +#define PMU_TM_DISABLE_MASK (0x1 << PMU_TM_DISABLE_OFFS) +#define PMU_TDC0_REF_CAL_CNT_OFFS 11 +#define PMU_TDC0_REF_CAL_CNT_MASK (0x1ff << PMU_TDC0_REF_CAL_CNT_OFFS) +#define PMU_TDC0_OTF_CAL_MASK (0x1 << 30) +#define PMU_TDC0_START_CAL_MASK (0x1 << 25) + +#define A375_UNIT_CONTROL_SHIFT 27 +#define A375_UNIT_CONTROL_MASK 0x7 +#define A375_READOUT_INVERT BIT(15) +#define A375_HW_RESETn BIT(8) + +/* Errata fields */ +#define CONTROL0_TSEN_TC_TRIM_MASK 0x7 +#define CONTROL0_TSEN_TC_TRIM_VAL 0x3 + +#define CONTROL0_TSEN_START BIT(0) +#define CONTROL0_TSEN_RESET BIT(1) +#define CONTROL0_TSEN_ENABLE BIT(2) +#define CONTROL0_TSEN_AVG_BYPASS BIT(6) +#define CONTROL0_TSEN_CHAN_SHIFT 13 +#define CONTROL0_TSEN_CHAN_MASK 0xF +#define CONTROL0_TSEN_OSR_SHIFT 24 +#define CONTROL0_TSEN_OSR_MAX 0x3 +#define CONTROL0_TSEN_MODE_SHIFT 30 +#define CONTROL0_TSEN_MODE_EXTERNAL 0x2 +#define CONTROL0_TSEN_MODE_MASK 0x3 + +#define CONTROL1_TSEN_AVG_SHIFT 0 +#define CONTROL1_TSEN_AVG_MASK 0x7 +#define CONTROL1_EXT_TSEN_SW_RESET BIT(7) +#define CONTROL1_EXT_TSEN_HW_RESETn BIT(8) + +#define STATUS_POLL_PERIOD_US 1000 +#define STATUS_POLL_TIMEOUT_US 100000 + +struct armada_thermal_data; + +/* Marvell EBU Thermal Sensor Dev Structure */ +struct armada_thermal_priv { + struct device *dev; + struct regmap *syscon; + char zone_name[THERMAL_NAME_LENGTH]; + /* serialize temperature reads/updates */ + struct mutex update_lock; + struct armada_thermal_data *data; + int current_channel; +}; + +struct armada_thermal_data { + /* Initialize the thermal IC */ + void (*init)(struct platform_device *pdev, + struct armada_thermal_priv *priv); + + /* Formula coeficients: temp = (b - m * reg) / div */ + s64 coef_b; + s64 coef_m; + u32 coef_div; + bool inverted; + bool signed_sample; + + /* Register shift and mask to access the sensor temperature */ + unsigned int temp_shift; + unsigned int temp_mask; + u32 is_valid_bit; + + /* Syscon access */ + unsigned int syscon_control0_off; + unsigned int syscon_control1_off; + unsigned int syscon_status_off; + + /* One sensor is in the thermal IC, the others are in the CPUs if any */ + unsigned int cpu_nr; +}; + +struct armada_drvdata { + enum drvtype { + LEGACY, + SYSCON + } type; + union { + struct armada_thermal_priv *priv; + struct thermal_zone_device *tz; + } data; +}; + +/* + * struct armada_thermal_sensor - hold the information of one thermal sensor + * @thermal: pointer to the local private structure + * @tzd: pointer to the thermal zone device + * @id: identifier of the thermal sensor + */ +struct armada_thermal_sensor { + struct armada_thermal_priv *priv; + int id; +}; + +static void armadaxp_init(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + struct armada_thermal_data *data = priv->data; + u32 reg; + + regmap_read(priv->syscon, data->syscon_control1_off, ®); + reg |= PMU_TDC0_OTF_CAL_MASK; + + /* Reference calibration value */ + reg &= ~PMU_TDC0_REF_CAL_CNT_MASK; + reg |= (0xf1 << PMU_TDC0_REF_CAL_CNT_OFFS); + + /* Reset the sensor */ + reg |= PMU_TDC0_SW_RST_MASK; + + regmap_write(priv->syscon, data->syscon_control1_off, reg); + + /* Enable the sensor */ + regmap_read(priv->syscon, data->syscon_status_off, ®); + reg &= ~PMU_TM_DISABLE_MASK; + regmap_write(priv->syscon, data->syscon_status_off, reg); +} + +static void armada370_init(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + struct armada_thermal_data *data = priv->data; + u32 reg; + + regmap_read(priv->syscon, data->syscon_control1_off, ®); + reg |= PMU_TDC0_OTF_CAL_MASK; + + /* Reference calibration value */ + reg &= ~PMU_TDC0_REF_CAL_CNT_MASK; + reg |= (0xf1 << PMU_TDC0_REF_CAL_CNT_OFFS); + + /* Reset the sensor */ + reg &= ~PMU_TDC0_START_CAL_MASK; + + regmap_write(priv->syscon, data->syscon_control1_off, reg); + + msleep(10); +} + +static void armada375_init(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + struct armada_thermal_data *data = priv->data; + u32 reg; + + regmap_read(priv->syscon, data->syscon_control1_off, ®); + reg &= ~(A375_UNIT_CONTROL_MASK << A375_UNIT_CONTROL_SHIFT); + reg &= ~A375_READOUT_INVERT; + reg &= ~A375_HW_RESETn; + regmap_write(priv->syscon, data->syscon_control1_off, reg); + + msleep(20); + + reg |= A375_HW_RESETn; + regmap_write(priv->syscon, data->syscon_control1_off, reg); + + msleep(50); +} + +static int armada_wait_sensor_validity(struct armada_thermal_priv *priv) +{ + u32 reg; + + return regmap_read_poll_timeout(priv->syscon, + priv->data->syscon_status_off, reg, + reg & priv->data->is_valid_bit, + STATUS_POLL_PERIOD_US, + STATUS_POLL_TIMEOUT_US); +} + +static void armada380_init(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + struct armada_thermal_data *data = priv->data; + u32 reg; + + /* Disable the HW/SW reset */ + regmap_read(priv->syscon, data->syscon_control1_off, ®); + reg |= CONTROL1_EXT_TSEN_HW_RESETn; + reg &= ~CONTROL1_EXT_TSEN_SW_RESET; + regmap_write(priv->syscon, data->syscon_control1_off, reg); + + /* Set Tsen Tc Trim to correct default value (errata #132698) */ + regmap_read(priv->syscon, data->syscon_control0_off, ®); + reg &= ~CONTROL0_TSEN_TC_TRIM_MASK; + reg |= CONTROL0_TSEN_TC_TRIM_VAL; + regmap_write(priv->syscon, data->syscon_control0_off, reg); +} + +static void armada_ap806_init(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + struct armada_thermal_data *data = priv->data; + u32 reg; + + regmap_read(priv->syscon, data->syscon_control0_off, ®); + reg &= ~CONTROL0_TSEN_RESET; + reg |= CONTROL0_TSEN_START | CONTROL0_TSEN_ENABLE; + + /* Sample every ~2ms */ + reg |= CONTROL0_TSEN_OSR_MAX << CONTROL0_TSEN_OSR_SHIFT; + + /* Enable average (2 samples by default) */ + reg &= ~CONTROL0_TSEN_AVG_BYPASS; + + regmap_write(priv->syscon, data->syscon_control0_off, reg); +} + +static void armada_cp110_init(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + struct armada_thermal_data *data = priv->data; + u32 reg; + + armada380_init(pdev, priv); + + /* Sample every ~2ms */ + regmap_read(priv->syscon, data->syscon_control0_off, ®); + reg |= CONTROL0_TSEN_OSR_MAX << CONTROL0_TSEN_OSR_SHIFT; + regmap_write(priv->syscon, data->syscon_control0_off, reg); + + /* Average the output value over 2^1 = 2 samples */ + regmap_read(priv->syscon, data->syscon_control1_off, ®); + reg &= ~CONTROL1_TSEN_AVG_MASK << CONTROL1_TSEN_AVG_SHIFT; + reg |= 1 << CONTROL1_TSEN_AVG_SHIFT; + regmap_write(priv->syscon, data->syscon_control1_off, reg); +} + +static bool armada_is_valid(struct armada_thermal_priv *priv) +{ + u32 reg; + + if (!priv->data->is_valid_bit) + return true; + + regmap_read(priv->syscon, priv->data->syscon_status_off, ®); + + return reg & priv->data->is_valid_bit; +} + +/* There is currently no board with more than one sensor per channel */ +static int armada_select_channel(struct armada_thermal_priv *priv, int channel) +{ + struct armada_thermal_data *data = priv->data; + u32 ctrl0; + + if (channel < 0 || channel > priv->data->cpu_nr) + return -EINVAL; + + if (priv->current_channel == channel) + return 0; + + /* Stop the measurements */ + regmap_read(priv->syscon, data->syscon_control0_off, &ctrl0); + ctrl0 &= ~CONTROL0_TSEN_START; + regmap_write(priv->syscon, data->syscon_control0_off, ctrl0); + + /* Reset the mode, internal sensor will be automatically selected */ + ctrl0 &= ~(CONTROL0_TSEN_MODE_MASK << CONTROL0_TSEN_MODE_SHIFT); + + /* Other channels are external and should be selected accordingly */ + if (channel) { + /* Change the mode to external */ + ctrl0 |= CONTROL0_TSEN_MODE_EXTERNAL << + CONTROL0_TSEN_MODE_SHIFT; + /* Select the sensor */ + ctrl0 &= ~(CONTROL0_TSEN_CHAN_MASK << CONTROL0_TSEN_CHAN_SHIFT); + ctrl0 |= (channel - 1) << CONTROL0_TSEN_CHAN_SHIFT; + } + + /* Actually set the mode/channel */ + regmap_write(priv->syscon, data->syscon_control0_off, ctrl0); + priv->current_channel = channel; + + /* Re-start the measurements */ + ctrl0 |= CONTROL0_TSEN_START; + regmap_write(priv->syscon, data->syscon_control0_off, ctrl0); + + /* + * The IP has a latency of ~15ms, so after updating the selected source, + * we must absolutely wait for the sensor validity bit to ensure we read + * actual data. + */ + if (armada_wait_sensor_validity(priv)) { + dev_err(priv->dev, + "Temperature sensor reading not valid\n"); + return -EIO; + } + + return 0; +} + +static int armada_read_sensor(struct armada_thermal_priv *priv, int *temp) +{ + u32 reg, div; + s64 sample, b, m; + + regmap_read(priv->syscon, priv->data->syscon_status_off, ®); + reg = (reg >> priv->data->temp_shift) & priv->data->temp_mask; + if (priv->data->signed_sample) + /* The most significant bit is the sign bit */ + sample = sign_extend32(reg, fls(priv->data->temp_mask) - 1); + else + sample = reg; + + /* Get formula coeficients */ + b = priv->data->coef_b; + m = priv->data->coef_m; + div = priv->data->coef_div; + + if (priv->data->inverted) + *temp = div_s64((m * sample) - b, div); + else + *temp = div_s64(b - (m * sample), div); + + return 0; +} + +static int armada_get_temp_legacy(struct thermal_zone_device *thermal, + int *temp) +{ + struct armada_thermal_priv *priv = thermal->devdata; + int ret; + + /* Valid check */ + if (!armada_is_valid(priv)) { + dev_err(priv->dev, + "Temperature sensor reading not valid\n"); + return -EIO; + } + + /* Do the actual reading */ + ret = armada_read_sensor(priv, temp); + + return ret; +} + +static struct thermal_zone_device_ops legacy_ops = { + .get_temp = armada_get_temp_legacy, +}; + +static int armada_get_temp(void *_sensor, int *temp) +{ + struct armada_thermal_sensor *sensor = _sensor; + struct armada_thermal_priv *priv = sensor->priv; + int ret; + + mutex_lock(&priv->update_lock); + + /* Select the desired channel */ + ret = armada_select_channel(priv, sensor->id); + if (ret) + goto unlock_mutex; + + /* Do the actual reading */ + ret = armada_read_sensor(priv, temp); + +unlock_mutex: + mutex_unlock(&priv->update_lock); + + return ret; +} + +static struct thermal_zone_of_device_ops of_ops = { + .get_temp = armada_get_temp, +}; + +static const struct armada_thermal_data armadaxp_data = { + .init = armadaxp_init, + .temp_shift = 10, + .temp_mask = 0x1ff, + .coef_b = 3153000000ULL, + .coef_m = 10000000ULL, + .coef_div = 13825, + .syscon_status_off = 0xb0, + .syscon_control1_off = 0xd0, +}; + +static const struct armada_thermal_data armada370_data = { + .init = armada370_init, + .is_valid_bit = BIT(9), + .temp_shift = 10, + .temp_mask = 0x1ff, + .coef_b = 3153000000ULL, + .coef_m = 10000000ULL, + .coef_div = 13825, + .syscon_status_off = 0x0, + .syscon_control1_off = 0x4, +}; + +static const struct armada_thermal_data armada375_data = { + .init = armada375_init, + .is_valid_bit = BIT(10), + .temp_shift = 0, + .temp_mask = 0x1ff, + .coef_b = 3171900000ULL, + .coef_m = 10000000ULL, + .coef_div = 13616, + .syscon_status_off = 0x78, + .syscon_control0_off = 0x7c, + .syscon_control1_off = 0x80, +}; + +static const struct armada_thermal_data armada380_data = { + .init = armada380_init, + .is_valid_bit = BIT(10), + .temp_shift = 0, + .temp_mask = 0x3ff, + .coef_b = 1172499100ULL, + .coef_m = 2000096ULL, + .coef_div = 4201, + .inverted = true, + .syscon_control0_off = 0x70, + .syscon_control1_off = 0x74, + .syscon_status_off = 0x78, +}; + +static const struct armada_thermal_data armada_ap806_data = { + .init = armada_ap806_init, + .is_valid_bit = BIT(16), + .temp_shift = 0, + .temp_mask = 0x3ff, + .coef_b = -150000LL, + .coef_m = 423ULL, + .coef_div = 1, + .inverted = true, + .signed_sample = true, + .syscon_control0_off = 0x84, + .syscon_control1_off = 0x88, + .syscon_status_off = 0x8C, + .cpu_nr = 4, +}; + +static const struct armada_thermal_data armada_cp110_data = { + .init = armada_cp110_init, + .is_valid_bit = BIT(10), + .temp_shift = 0, + .temp_mask = 0x3ff, + .coef_b = 1172499100ULL, + .coef_m = 2000096ULL, + .coef_div = 4201, + .inverted = true, + .syscon_control0_off = 0x70, + .syscon_control1_off = 0x74, + .syscon_status_off = 0x78, +}; + +static const struct of_device_id armada_thermal_id_table[] = { + { + .compatible = "marvell,armadaxp-thermal", + .data = &armadaxp_data, + }, + { + .compatible = "marvell,armada370-thermal", + .data = &armada370_data, + }, + { + .compatible = "marvell,armada375-thermal", + .data = &armada375_data, + }, + { + .compatible = "marvell,armada380-thermal", + .data = &armada380_data, + }, + { + .compatible = "marvell,armada-ap806-thermal", + .data = &armada_ap806_data, + }, + { + .compatible = "marvell,armada-cp110-thermal", + .data = &armada_cp110_data, + }, + { + /* sentinel */ + }, +}; +MODULE_DEVICE_TABLE(of, armada_thermal_id_table); + +static const struct regmap_config armada_thermal_regmap_config = { + .reg_bits = 32, + .reg_stride = 4, + .val_bits = 32, + .fast_io = true, +}; + +static int armada_thermal_probe_legacy(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + struct armada_thermal_data *data = priv->data; + struct resource *res; + void __iomem *base; + + /* First memory region points towards the status register */ + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -EIO; + + /* + * Edit the resource start address and length to map over all the + * registers, instead of pointing at them one by one. + */ + res->start -= data->syscon_status_off; + res->end = res->start + max(data->syscon_status_off, + max(data->syscon_control0_off, + data->syscon_control1_off)) + + sizeof(unsigned int) - 1; + + base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(base)) + return PTR_ERR(base); + + priv->syscon = devm_regmap_init_mmio(&pdev->dev, base, + &armada_thermal_regmap_config); + if (IS_ERR(priv->syscon)) + return PTR_ERR(priv->syscon); + + return 0; +} + +static int armada_thermal_probe_syscon(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + priv->syscon = syscon_node_to_regmap(pdev->dev.parent->of_node); + if (IS_ERR(priv->syscon)) + return PTR_ERR(priv->syscon); + + return 0; +} + +static void armada_set_sane_name(struct platform_device *pdev, + struct armada_thermal_priv *priv) +{ + const char *name = dev_name(&pdev->dev); + char *insane_char; + + if (strlen(name) > THERMAL_NAME_LENGTH) { + /* + * When inside a system controller, the device name has the + * form: f06f8000.system-controller:ap-thermal so stripping + * after the ':' should give us a shorter but meaningful name. + */ + name = strrchr(name, ':'); + if (!name) + name = "armada_thermal"; + else + name++; + } + + /* Save the name locally */ + strncpy(priv->zone_name, name, THERMAL_NAME_LENGTH - 1); + priv->zone_name[THERMAL_NAME_LENGTH - 1] = '\0'; + + /* Then check there are no '-' or hwmon core will complain */ + do { + insane_char = strpbrk(priv->zone_name, "-"); + if (insane_char) + *insane_char = '_'; + } while (insane_char); +} + +static int armada_thermal_probe(struct platform_device *pdev) +{ + struct thermal_zone_device *tz; + struct armada_thermal_sensor *sensor; + struct armada_drvdata *drvdata; + const struct of_device_id *match; + struct armada_thermal_priv *priv; + int sensor_id; + int ret; + + match = of_match_device(armada_thermal_id_table, &pdev->dev); + if (!match) + return -ENODEV; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + drvdata = devm_kzalloc(&pdev->dev, sizeof(*drvdata), GFP_KERNEL); + if (!drvdata) + return -ENOMEM; + + priv->dev = &pdev->dev; + priv->data = (struct armada_thermal_data *)match->data; + + mutex_init(&priv->update_lock); + + /* + * Legacy DT bindings only described "control1" register (also referred + * as "control MSB" on old documentation). Then, bindings moved to cover + * "control0/control LSB" and "control1/control MSB" registers within + * the same resource, which was then of size 8 instead of 4. + * + * The logic of defining sporadic registers is broken. For instance, it + * blocked the addition of the overheat interrupt feature that needed + * another resource somewhere else in the same memory area. One solution + * is to define an overall system controller and put the thermal node + * into it, which requires the use of regmaps across all the driver. + */ + if (IS_ERR(syscon_node_to_regmap(pdev->dev.parent->of_node))) { + /* Ensure device name is correct for the thermal core */ + armada_set_sane_name(pdev, priv); + + ret = armada_thermal_probe_legacy(pdev, priv); + if (ret) + return ret; + + priv->data->init(pdev, priv); + + /* Wait the sensors to be valid */ + armada_wait_sensor_validity(priv); + + tz = thermal_zone_device_register(priv->zone_name, 0, 0, priv, + &legacy_ops, NULL, 0, 0); + if (IS_ERR(tz)) { + dev_err(&pdev->dev, + "Failed to register thermal zone device\n"); + return PTR_ERR(tz); + } + + drvdata->type = LEGACY; + drvdata->data.tz = tz; + platform_set_drvdata(pdev, drvdata); + + return 0; + } + + ret = armada_thermal_probe_syscon(pdev, priv); + if (ret) + return ret; + + priv->current_channel = -1; + priv->data->init(pdev, priv); + drvdata->type = SYSCON; + drvdata->data.priv = priv; + platform_set_drvdata(pdev, drvdata); + + /* + * There is one channel for the IC and one per CPU (if any), each + * channel has one sensor. + */ + for (sensor_id = 0; sensor_id <= priv->data->cpu_nr; sensor_id++) { + sensor = devm_kzalloc(&pdev->dev, + sizeof(struct armada_thermal_sensor), + GFP_KERNEL); + if (!sensor) + return -ENOMEM; + + /* Register the sensor */ + sensor->priv = priv; + sensor->id = sensor_id; + tz = devm_thermal_zone_of_sensor_register(&pdev->dev, + sensor->id, sensor, + &of_ops); + if (IS_ERR(tz)) { + dev_info(&pdev->dev, "Thermal sensor %d unavailable\n", + sensor_id); + devm_kfree(&pdev->dev, sensor); + continue; + } + } + + return 0; +} + +static int armada_thermal_exit(struct platform_device *pdev) +{ + struct armada_drvdata *drvdata = platform_get_drvdata(pdev); + + if (drvdata->type == LEGACY) + thermal_zone_device_unregister(drvdata->data.tz); + + return 0; +} + +static struct platform_driver armada_thermal_driver = { + .probe = armada_thermal_probe, + .remove = armada_thermal_exit, + .driver = { + .name = "armada_thermal", + .of_match_table = armada_thermal_id_table, + }, +}; + +module_platform_driver(armada_thermal_driver); + +MODULE_AUTHOR("Ezequiel Garcia <ezequiel.garcia@free-electrons.com>"); +MODULE_DESCRIPTION("Marvell EBU Armada SoCs thermal driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/broadcom/Kconfig b/drivers/thermal/broadcom/Kconfig new file mode 100644 index 000000000..c106a15bf --- /dev/null +++ b/drivers/thermal/broadcom/Kconfig @@ -0,0 +1,24 @@ +config BCM2835_THERMAL + tristate "Thermal sensors on bcm2835 SoC" + depends on ARCH_BCM2835 || COMPILE_TEST + depends on HAS_IOMEM + depends on THERMAL_OF + help + Support for thermal sensors on Broadcom bcm2835 SoCs. + +config BRCMSTB_THERMAL + tristate "Broadcom STB AVS TMON thermal driver" + depends on ARCH_BRCMSTB || COMPILE_TEST + help + Enable this driver if you have a Broadcom STB SoC and would like + thermal framework support. + +config BCM_NS_THERMAL + tristate "Northstar thermal driver" + depends on ARCH_BCM_IPROC || COMPILE_TEST + default y if ARCH_BCM_IPROC + help + Support for the Northstar and Northstar Plus family of SoCs (e.g. + BCM4708, BCM4709, BCM5301x, BCM95852X, etc). It contains DMU (Device + Management Unit) block with a thermal sensor that allows checking CPU + temperature. diff --git a/drivers/thermal/broadcom/Makefile b/drivers/thermal/broadcom/Makefile new file mode 100644 index 000000000..fae10ecaf --- /dev/null +++ b/drivers/thermal/broadcom/Makefile @@ -0,0 +1,3 @@ +obj-$(CONFIG_BCM2835_THERMAL) += bcm2835_thermal.o +obj-$(CONFIG_BRCMSTB_THERMAL) += brcmstb_thermal.o +obj-$(CONFIG_BCM_NS_THERMAL) += ns-thermal.o diff --git a/drivers/thermal/broadcom/bcm2835_thermal.c b/drivers/thermal/broadcom/bcm2835_thermal.c new file mode 100644 index 000000000..8646fb742 --- /dev/null +++ b/drivers/thermal/broadcom/bcm2835_thermal.c @@ -0,0 +1,323 @@ +/* + * Driver for Broadcom BCM2835 SoC temperature sensor + * + * Copyright (C) 2016 Martin Sperl + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/clk.h> +#include <linux/debugfs.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/thermal.h> + +#include "../thermal_hwmon.h" + +#define BCM2835_TS_TSENSCTL 0x00 +#define BCM2835_TS_TSENSSTAT 0x04 + +#define BCM2835_TS_TSENSCTL_PRWDW BIT(0) +#define BCM2835_TS_TSENSCTL_RSTB BIT(1) + +/* + * bandgap reference voltage in 6 mV increments + * 000b = 1178 mV, 001b = 1184 mV, ... 111b = 1220 mV + */ +#define BCM2835_TS_TSENSCTL_CTRL_BITS 3 +#define BCM2835_TS_TSENSCTL_CTRL_SHIFT 2 +#define BCM2835_TS_TSENSCTL_CTRL_MASK \ + GENMASK(BCM2835_TS_TSENSCTL_CTRL_BITS + \ + BCM2835_TS_TSENSCTL_CTRL_SHIFT - 1, \ + BCM2835_TS_TSENSCTL_CTRL_SHIFT) +#define BCM2835_TS_TSENSCTL_CTRL_DEFAULT 1 +#define BCM2835_TS_TSENSCTL_EN_INT BIT(5) +#define BCM2835_TS_TSENSCTL_DIRECT BIT(6) +#define BCM2835_TS_TSENSCTL_CLR_INT BIT(7) +#define BCM2835_TS_TSENSCTL_THOLD_SHIFT 8 +#define BCM2835_TS_TSENSCTL_THOLD_BITS 10 +#define BCM2835_TS_TSENSCTL_THOLD_MASK \ + GENMASK(BCM2835_TS_TSENSCTL_THOLD_BITS + \ + BCM2835_TS_TSENSCTL_THOLD_SHIFT - 1, \ + BCM2835_TS_TSENSCTL_THOLD_SHIFT) +/* + * time how long the block to be asserted in reset + * which based on a clock counter (TSENS clock assumed) + */ +#define BCM2835_TS_TSENSCTL_RSTDELAY_SHIFT 18 +#define BCM2835_TS_TSENSCTL_RSTDELAY_BITS 8 +#define BCM2835_TS_TSENSCTL_REGULEN BIT(26) + +#define BCM2835_TS_TSENSSTAT_DATA_BITS 10 +#define BCM2835_TS_TSENSSTAT_DATA_SHIFT 0 +#define BCM2835_TS_TSENSSTAT_DATA_MASK \ + GENMASK(BCM2835_TS_TSENSSTAT_DATA_BITS + \ + BCM2835_TS_TSENSSTAT_DATA_SHIFT - 1, \ + BCM2835_TS_TSENSSTAT_DATA_SHIFT) +#define BCM2835_TS_TSENSSTAT_VALID BIT(10) +#define BCM2835_TS_TSENSSTAT_INTERRUPT BIT(11) + +struct bcm2835_thermal_data { + struct thermal_zone_device *tz; + void __iomem *regs; + struct clk *clk; + struct dentry *debugfsdir; +}; + +static int bcm2835_thermal_adc2temp(u32 adc, int offset, int slope) +{ + return offset + slope * adc; +} + +static int bcm2835_thermal_temp2adc(int temp, int offset, int slope) +{ + temp -= offset; + temp /= slope; + + if (temp < 0) + temp = 0; + if (temp >= BIT(BCM2835_TS_TSENSSTAT_DATA_BITS)) + temp = BIT(BCM2835_TS_TSENSSTAT_DATA_BITS) - 1; + + return temp; +} + +static int bcm2835_thermal_get_temp(void *d, int *temp) +{ + struct bcm2835_thermal_data *data = d; + u32 val = readl(data->regs + BCM2835_TS_TSENSSTAT); + + if (!(val & BCM2835_TS_TSENSSTAT_VALID)) + return -EIO; + + val &= BCM2835_TS_TSENSSTAT_DATA_MASK; + + *temp = bcm2835_thermal_adc2temp( + val, + thermal_zone_get_offset(data->tz), + thermal_zone_get_slope(data->tz)); + + return 0; +} + +static const struct debugfs_reg32 bcm2835_thermal_regs[] = { + { + .name = "ctl", + .offset = 0 + }, + { + .name = "stat", + .offset = 4 + } +}; + +static void bcm2835_thermal_debugfs(struct platform_device *pdev) +{ + struct bcm2835_thermal_data *data = platform_get_drvdata(pdev); + struct debugfs_regset32 *regset; + + data->debugfsdir = debugfs_create_dir("bcm2835_thermal", NULL); + if (!data->debugfsdir) + return; + + regset = devm_kzalloc(&pdev->dev, sizeof(*regset), GFP_KERNEL); + if (!regset) + return; + + regset->regs = bcm2835_thermal_regs; + regset->nregs = ARRAY_SIZE(bcm2835_thermal_regs); + regset->base = data->regs; + + debugfs_create_regset32("regset", 0444, data->debugfsdir, regset); +} + +static const struct thermal_zone_of_device_ops bcm2835_thermal_ops = { + .get_temp = bcm2835_thermal_get_temp, +}; + +/* + * Note: as per Raspberry Foundation FAQ + * (https://www.raspberrypi.org/help/faqs/#performanceOperatingTemperature) + * the recommended temperature range for the SoC -40C to +85C + * so the trip limit is set to 80C. + * this applies to all the BCM283X SoC + */ + +static const struct of_device_id bcm2835_thermal_of_match_table[] = { + { + .compatible = "brcm,bcm2835-thermal", + }, + { + .compatible = "brcm,bcm2836-thermal", + }, + { + .compatible = "brcm,bcm2837-thermal", + }, + {}, +}; +MODULE_DEVICE_TABLE(of, bcm2835_thermal_of_match_table); + +static int bcm2835_thermal_probe(struct platform_device *pdev) +{ + const struct of_device_id *match; + struct thermal_zone_device *tz; + struct bcm2835_thermal_data *data; + struct resource *res; + int err = 0; + u32 val; + unsigned long rate; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + match = of_match_device(bcm2835_thermal_of_match_table, + &pdev->dev); + if (!match) + return -EINVAL; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + data->regs = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(data->regs)) { + err = PTR_ERR(data->regs); + dev_err(&pdev->dev, "Could not get registers: %d\n", err); + return err; + } + + data->clk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(data->clk)) { + err = PTR_ERR(data->clk); + if (err != -EPROBE_DEFER) + dev_err(&pdev->dev, "Could not get clk: %d\n", err); + return err; + } + + err = clk_prepare_enable(data->clk); + if (err) + return err; + + rate = clk_get_rate(data->clk); + if ((rate < 1920000) || (rate > 5000000)) + dev_warn(&pdev->dev, + "Clock %pCn running at %lu Hz is outside of the recommended range: 1.92 to 5MHz\n", + data->clk, rate); + + /* register of thermal sensor and get info from DT */ + tz = thermal_zone_of_sensor_register(&pdev->dev, 0, data, + &bcm2835_thermal_ops); + if (IS_ERR(tz)) { + err = PTR_ERR(tz); + dev_err(&pdev->dev, + "Failed to register the thermal device: %d\n", + err); + goto err_clk; + } + + /* + * right now the FW does set up the HW-block, so we are not + * touching the configuration registers. + * But if the HW is not enabled, then set it up + * using "sane" values used by the firmware right now. + */ + val = readl(data->regs + BCM2835_TS_TSENSCTL); + if (!(val & BCM2835_TS_TSENSCTL_RSTB)) { + int trip_temp, offset, slope; + + slope = thermal_zone_get_slope(tz); + offset = thermal_zone_get_offset(tz); + /* + * For now we deal only with critical, otherwise + * would need to iterate + */ + err = tz->ops->get_trip_temp(tz, 0, &trip_temp); + if (err < 0) { + dev_err(&pdev->dev, + "Not able to read trip_temp: %d\n", + err); + goto err_tz; + } + + /* set bandgap reference voltage and enable voltage regulator */ + val = (BCM2835_TS_TSENSCTL_CTRL_DEFAULT << + BCM2835_TS_TSENSCTL_CTRL_SHIFT) | + BCM2835_TS_TSENSCTL_REGULEN; + + /* use the recommended reset duration */ + val |= (0xFE << BCM2835_TS_TSENSCTL_RSTDELAY_SHIFT); + + /* trip_adc value from info */ + val |= bcm2835_thermal_temp2adc(trip_temp, + offset, + slope) + << BCM2835_TS_TSENSCTL_THOLD_SHIFT; + + /* write the value back to the register as 2 steps */ + writel(val, data->regs + BCM2835_TS_TSENSCTL); + val |= BCM2835_TS_TSENSCTL_RSTB; + writel(val, data->regs + BCM2835_TS_TSENSCTL); + } + + data->tz = tz; + + platform_set_drvdata(pdev, data); + + /* + * Thermal_zone doesn't enable hwmon as default, + * enable it here + */ + tz->tzp->no_hwmon = false; + err = thermal_add_hwmon_sysfs(tz); + if (err) + goto err_tz; + + bcm2835_thermal_debugfs(pdev); + + return 0; +err_tz: + thermal_zone_of_sensor_unregister(&pdev->dev, tz); +err_clk: + clk_disable_unprepare(data->clk); + + return err; +} + +static int bcm2835_thermal_remove(struct platform_device *pdev) +{ + struct bcm2835_thermal_data *data = platform_get_drvdata(pdev); + struct thermal_zone_device *tz = data->tz; + + debugfs_remove_recursive(data->debugfsdir); + thermal_zone_of_sensor_unregister(&pdev->dev, tz); + clk_disable_unprepare(data->clk); + + return 0; +} + +static struct platform_driver bcm2835_thermal_driver = { + .probe = bcm2835_thermal_probe, + .remove = bcm2835_thermal_remove, + .driver = { + .name = "bcm2835_thermal", + .of_match_table = bcm2835_thermal_of_match_table, + }, +}; +module_platform_driver(bcm2835_thermal_driver); + +MODULE_AUTHOR("Martin Sperl"); +MODULE_DESCRIPTION("Thermal driver for bcm2835 chip"); +MODULE_LICENSE("GPL"); diff --git a/drivers/thermal/broadcom/brcmstb_thermal.c b/drivers/thermal/broadcom/brcmstb_thermal.c new file mode 100644 index 000000000..8d16a41ea --- /dev/null +++ b/drivers/thermal/broadcom/brcmstb_thermal.c @@ -0,0 +1,374 @@ +/* + * Broadcom STB AVS TMON thermal sensor driver + * + * Copyright (c) 2015-2017 Broadcom + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#define DRV_NAME "brcmstb_thermal" + +#define pr_fmt(fmt) DRV_NAME ": " fmt + +#include <linux/bitops.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/irqreturn.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/of_device.h> +#include <linux/thermal.h> + +#define AVS_TMON_STATUS 0x00 + #define AVS_TMON_STATUS_valid_msk BIT(11) + #define AVS_TMON_STATUS_data_msk GENMASK(10, 1) + #define AVS_TMON_STATUS_data_shift 1 + +#define AVS_TMON_EN_OVERTEMP_RESET 0x04 + #define AVS_TMON_EN_OVERTEMP_RESET_msk BIT(0) + +#define AVS_TMON_RESET_THRESH 0x08 + #define AVS_TMON_RESET_THRESH_msk GENMASK(10, 1) + #define AVS_TMON_RESET_THRESH_shift 1 + +#define AVS_TMON_INT_IDLE_TIME 0x10 + +#define AVS_TMON_EN_TEMP_INT_SRCS 0x14 + #define AVS_TMON_EN_TEMP_INT_SRCS_high BIT(1) + #define AVS_TMON_EN_TEMP_INT_SRCS_low BIT(0) + +#define AVS_TMON_INT_THRESH 0x18 + #define AVS_TMON_INT_THRESH_high_msk GENMASK(26, 17) + #define AVS_TMON_INT_THRESH_high_shift 17 + #define AVS_TMON_INT_THRESH_low_msk GENMASK(10, 1) + #define AVS_TMON_INT_THRESH_low_shift 1 + +#define AVS_TMON_TEMP_INT_CODE 0x1c +#define AVS_TMON_TP_TEST_ENABLE 0x20 + +/* Default coefficients */ +#define AVS_TMON_TEMP_SLOPE 487 +#define AVS_TMON_TEMP_OFFSET 410040 + +/* HW related temperature constants */ +#define AVS_TMON_TEMP_MAX 0x3ff +#define AVS_TMON_TEMP_MIN -88161 +#define AVS_TMON_TEMP_MASK AVS_TMON_TEMP_MAX + +enum avs_tmon_trip_type { + TMON_TRIP_TYPE_LOW = 0, + TMON_TRIP_TYPE_HIGH, + TMON_TRIP_TYPE_RESET, + TMON_TRIP_TYPE_MAX, +}; + +struct avs_tmon_trip { + /* HW bit to enable the trip */ + u32 enable_offs; + u32 enable_mask; + + /* HW field to read the trip temperature */ + u32 reg_offs; + u32 reg_msk; + int reg_shift; +}; + +static struct avs_tmon_trip avs_tmon_trips[] = { + /* Trips when temperature is below threshold */ + [TMON_TRIP_TYPE_LOW] = { + .enable_offs = AVS_TMON_EN_TEMP_INT_SRCS, + .enable_mask = AVS_TMON_EN_TEMP_INT_SRCS_low, + .reg_offs = AVS_TMON_INT_THRESH, + .reg_msk = AVS_TMON_INT_THRESH_low_msk, + .reg_shift = AVS_TMON_INT_THRESH_low_shift, + }, + /* Trips when temperature is above threshold */ + [TMON_TRIP_TYPE_HIGH] = { + .enable_offs = AVS_TMON_EN_TEMP_INT_SRCS, + .enable_mask = AVS_TMON_EN_TEMP_INT_SRCS_high, + .reg_offs = AVS_TMON_INT_THRESH, + .reg_msk = AVS_TMON_INT_THRESH_high_msk, + .reg_shift = AVS_TMON_INT_THRESH_high_shift, + }, + /* Automatically resets chip when above threshold */ + [TMON_TRIP_TYPE_RESET] = { + .enable_offs = AVS_TMON_EN_OVERTEMP_RESET, + .enable_mask = AVS_TMON_EN_OVERTEMP_RESET_msk, + .reg_offs = AVS_TMON_RESET_THRESH, + .reg_msk = AVS_TMON_RESET_THRESH_msk, + .reg_shift = AVS_TMON_RESET_THRESH_shift, + }, +}; + +struct brcmstb_thermal_priv { + void __iomem *tmon_base; + struct device *dev; + struct thermal_zone_device *thermal; +}; + +/* Convert a HW code to a temperature reading (millidegree celsius) */ +static inline int avs_tmon_code_to_temp(struct thermal_zone_device *tz, + u32 code) +{ + return (AVS_TMON_TEMP_OFFSET - + (int)((code & AVS_TMON_TEMP_MAX) * AVS_TMON_TEMP_SLOPE)); +} + +/* + * Convert a temperature value (millidegree celsius) to a HW code + * + * @temp: temperature to convert + * @low: if true, round toward the low side + */ +static inline u32 avs_tmon_temp_to_code(struct thermal_zone_device *tz, + int temp, bool low) +{ + if (temp < AVS_TMON_TEMP_MIN) + return AVS_TMON_TEMP_MAX; /* Maximum code value */ + + if (temp >= AVS_TMON_TEMP_OFFSET) + return 0; /* Minimum code value */ + + if (low) + return (u32)(DIV_ROUND_UP(AVS_TMON_TEMP_OFFSET - temp, + AVS_TMON_TEMP_SLOPE)); + else + return (u32)((AVS_TMON_TEMP_OFFSET - temp) / + AVS_TMON_TEMP_SLOPE); +} + +static int brcmstb_get_temp(void *data, int *temp) +{ + struct brcmstb_thermal_priv *priv = data; + u32 val; + long t; + + val = __raw_readl(priv->tmon_base + AVS_TMON_STATUS); + + if (!(val & AVS_TMON_STATUS_valid_msk)) { + dev_err(priv->dev, "reading not valid\n"); + return -EIO; + } + + val = (val & AVS_TMON_STATUS_data_msk) >> AVS_TMON_STATUS_data_shift; + + t = avs_tmon_code_to_temp(priv->thermal, val); + if (t < 0) + *temp = 0; + else + *temp = t; + + return 0; +} + +static void avs_tmon_trip_enable(struct brcmstb_thermal_priv *priv, + enum avs_tmon_trip_type type, int en) +{ + struct avs_tmon_trip *trip = &avs_tmon_trips[type]; + u32 val = __raw_readl(priv->tmon_base + trip->enable_offs); + + dev_dbg(priv->dev, "%sable trip, type %d\n", en ? "en" : "dis", type); + + if (en) + val |= trip->enable_mask; + else + val &= ~trip->enable_mask; + + __raw_writel(val, priv->tmon_base + trip->enable_offs); +} + +static int avs_tmon_get_trip_temp(struct brcmstb_thermal_priv *priv, + enum avs_tmon_trip_type type) +{ + struct avs_tmon_trip *trip = &avs_tmon_trips[type]; + u32 val = __raw_readl(priv->tmon_base + trip->reg_offs); + + val &= trip->reg_msk; + val >>= trip->reg_shift; + + return avs_tmon_code_to_temp(priv->thermal, val); +} + +static void avs_tmon_set_trip_temp(struct brcmstb_thermal_priv *priv, + enum avs_tmon_trip_type type, + int temp) +{ + struct avs_tmon_trip *trip = &avs_tmon_trips[type]; + u32 val, orig; + + dev_dbg(priv->dev, "set temp %d to %d\n", type, temp); + + /* round toward low temp for the low interrupt */ + val = avs_tmon_temp_to_code(priv->thermal, temp, + type == TMON_TRIP_TYPE_LOW); + + val <<= trip->reg_shift; + val &= trip->reg_msk; + + orig = __raw_readl(priv->tmon_base + trip->reg_offs); + orig &= ~trip->reg_msk; + orig |= val; + __raw_writel(orig, priv->tmon_base + trip->reg_offs); +} + +static int avs_tmon_get_intr_temp(struct brcmstb_thermal_priv *priv) +{ + u32 val; + + val = __raw_readl(priv->tmon_base + AVS_TMON_TEMP_INT_CODE); + return avs_tmon_code_to_temp(priv->thermal, val); +} + +static irqreturn_t brcmstb_tmon_irq_thread(int irq, void *data) +{ + struct brcmstb_thermal_priv *priv = data; + int low, high, intr; + + low = avs_tmon_get_trip_temp(priv, TMON_TRIP_TYPE_LOW); + high = avs_tmon_get_trip_temp(priv, TMON_TRIP_TYPE_HIGH); + intr = avs_tmon_get_intr_temp(priv); + + dev_dbg(priv->dev, "low/intr/high: %d/%d/%d\n", + low, intr, high); + + /* Disable high-temp until next threshold shift */ + if (intr >= high) + avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_HIGH, 0); + /* Disable low-temp until next threshold shift */ + if (intr <= low) + avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_LOW, 0); + + /* + * Notify using the interrupt temperature, in case the temperature + * changes before it can next be read out + */ + thermal_zone_device_update(priv->thermal, intr); + + return IRQ_HANDLED; +} + +static int brcmstb_set_trips(void *data, int low, int high) +{ + struct brcmstb_thermal_priv *priv = data; + + dev_dbg(priv->dev, "set trips %d <--> %d\n", low, high); + + /* + * Disable low-temp if "low" is too small. As per thermal framework + * API, we use -INT_MAX rather than INT_MIN. + */ + if (low <= -INT_MAX) { + avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_LOW, 0); + } else { + avs_tmon_set_trip_temp(priv, TMON_TRIP_TYPE_LOW, low); + avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_LOW, 1); + } + + /* Disable high-temp if "high" is too big. */ + if (high == INT_MAX) { + avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_HIGH, 0); + } else { + avs_tmon_set_trip_temp(priv, TMON_TRIP_TYPE_HIGH, high); + avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_HIGH, 1); + } + + return 0; +} + +static struct thermal_zone_of_device_ops of_ops = { + .get_temp = brcmstb_get_temp, + .set_trips = brcmstb_set_trips, +}; + +static const struct of_device_id brcmstb_thermal_id_table[] = { + { .compatible = "brcm,avs-tmon" }, + {}, +}; +MODULE_DEVICE_TABLE(of, brcmstb_thermal_id_table); + +static int brcmstb_thermal_probe(struct platform_device *pdev) +{ + struct thermal_zone_device *thermal; + struct brcmstb_thermal_priv *priv; + struct resource *res; + int irq, ret; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + priv->tmon_base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(priv->tmon_base)) + return PTR_ERR(priv->tmon_base); + + priv->dev = &pdev->dev; + platform_set_drvdata(pdev, priv); + + thermal = thermal_zone_of_sensor_register(&pdev->dev, 0, priv, &of_ops); + if (IS_ERR(thermal)) { + ret = PTR_ERR(thermal); + dev_err(&pdev->dev, "could not register sensor: %d\n", ret); + return ret; + } + + priv->thermal = thermal; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) { + dev_err(&pdev->dev, "could not get IRQ\n"); + ret = irq; + goto err; + } + ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, + brcmstb_tmon_irq_thread, IRQF_ONESHOT, + DRV_NAME, priv); + if (ret < 0) { + dev_err(&pdev->dev, "could not request IRQ: %d\n", ret); + goto err; + } + + dev_info(&pdev->dev, "registered AVS TMON of-sensor driver\n"); + + return 0; + +err: + thermal_zone_of_sensor_unregister(&pdev->dev, thermal); + return ret; +} + +static int brcmstb_thermal_exit(struct platform_device *pdev) +{ + struct brcmstb_thermal_priv *priv = platform_get_drvdata(pdev); + struct thermal_zone_device *thermal = priv->thermal; + + if (thermal) + thermal_zone_of_sensor_unregister(&pdev->dev, priv->thermal); + + return 0; +} + +static struct platform_driver brcmstb_thermal_driver = { + .probe = brcmstb_thermal_probe, + .remove = brcmstb_thermal_exit, + .driver = { + .name = DRV_NAME, + .of_match_table = brcmstb_thermal_id_table, + }, +}; +module_platform_driver(brcmstb_thermal_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Brian Norris"); +MODULE_DESCRIPTION("Broadcom STB AVS TMON thermal driver"); diff --git a/drivers/thermal/broadcom/ns-thermal.c b/drivers/thermal/broadcom/ns-thermal.c new file mode 100644 index 000000000..322e741a2 --- /dev/null +++ b/drivers/thermal/broadcom/ns-thermal.c @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2017 RafaÅ‚ MiÅ‚ecki <rafal@milecki.pl> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include <linux/module.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/thermal.h> + +#define PVTMON_CONTROL0 0x00 +#define PVTMON_CONTROL0_SEL_MASK 0x0000000e +#define PVTMON_CONTROL0_SEL_TEMP_MONITOR 0x00000000 +#define PVTMON_CONTROL0_SEL_TEST_MODE 0x0000000e +#define PVTMON_STATUS 0x08 + +struct ns_thermal { + struct thermal_zone_device *tz; + void __iomem *pvtmon; +}; + +static int ns_thermal_get_temp(void *data, int *temp) +{ + struct ns_thermal *ns_thermal = data; + int offset = thermal_zone_get_offset(ns_thermal->tz); + int slope = thermal_zone_get_slope(ns_thermal->tz); + u32 val; + + val = readl(ns_thermal->pvtmon + PVTMON_CONTROL0); + if ((val & PVTMON_CONTROL0_SEL_MASK) != PVTMON_CONTROL0_SEL_TEMP_MONITOR) { + /* Clear current mode selection */ + val &= ~PVTMON_CONTROL0_SEL_MASK; + + /* Set temp monitor mode (it's the default actually) */ + val |= PVTMON_CONTROL0_SEL_TEMP_MONITOR; + + writel(val, ns_thermal->pvtmon + PVTMON_CONTROL0); + } + + val = readl(ns_thermal->pvtmon + PVTMON_STATUS); + *temp = slope * val + offset; + + return 0; +} + +static const struct thermal_zone_of_device_ops ns_thermal_ops = { + .get_temp = ns_thermal_get_temp, +}; + +static int ns_thermal_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct ns_thermal *ns_thermal; + + ns_thermal = devm_kzalloc(dev, sizeof(*ns_thermal), GFP_KERNEL); + if (!ns_thermal) + return -ENOMEM; + + ns_thermal->pvtmon = of_iomap(dev_of_node(dev), 0); + if (WARN_ON(!ns_thermal->pvtmon)) + return -ENOENT; + + ns_thermal->tz = devm_thermal_zone_of_sensor_register(dev, 0, + ns_thermal, + &ns_thermal_ops); + if (IS_ERR(ns_thermal->tz)) { + iounmap(ns_thermal->pvtmon); + return PTR_ERR(ns_thermal->tz); + } + + platform_set_drvdata(pdev, ns_thermal); + + return 0; +} + +static int ns_thermal_remove(struct platform_device *pdev) +{ + struct ns_thermal *ns_thermal = platform_get_drvdata(pdev); + + iounmap(ns_thermal->pvtmon); + + return 0; +} + +static const struct of_device_id ns_thermal_of_match[] = { + { .compatible = "brcm,ns-thermal", }, + {}, +}; +MODULE_DEVICE_TABLE(of, ns_thermal_of_match); + +static struct platform_driver ns_thermal_driver = { + .probe = ns_thermal_probe, + .remove = ns_thermal_remove, + .driver = { + .name = "ns-thermal", + .of_match_table = ns_thermal_of_match, + }, +}; +module_platform_driver(ns_thermal_driver); + +MODULE_AUTHOR("RafaÅ‚ MiÅ‚ecki <rafal@milecki.pl>"); +MODULE_DESCRIPTION("Northstar thermal driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/clock_cooling.c b/drivers/thermal/clock_cooling.c new file mode 100644 index 000000000..56711c255 --- /dev/null +++ b/drivers/thermal/clock_cooling.c @@ -0,0 +1,452 @@ +/* + * drivers/thermal/clock_cooling.c + * + * Copyright (C) 2014 Eduardo Valentin <edubezval@gmail.com> + * + * Copyright (C) 2013 Texas Instruments Inc. + * Contact: Eduardo Valentin <eduardo.valentin@ti.com> + * + * Highly based on cpu_cooling.c. + * Copyright (C) 2012 Samsung Electronics Co., Ltd(http://www.samsung.com) + * Copyright (C) 2012 Amit Daniel <amit.kachhap@linaro.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + */ +#include <linux/clk.h> +#include <linux/cpufreq.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/idr.h> +#include <linux/mutex.h> +#include <linux/pm_opp.h> +#include <linux/slab.h> +#include <linux/thermal.h> +#include <linux/clock_cooling.h> + +/** + * struct clock_cooling_device - data for cooling device with clock + * @id: unique integer value corresponding to each clock_cooling_device + * registered. + * @dev: struct device pointer to the device being used to cool off using + * clock frequencies. + * @cdev: thermal_cooling_device pointer to keep track of the + * registered cooling device. + * @clk_rate_change_nb: reference to notifier block used to receive clock + * rate changes. + * @freq_table: frequency table used to keep track of available frequencies. + * @clock_state: integer value representing the current state of clock + * cooling devices. + * @clock_val: integer value representing the absolute value of the clipped + * frequency. + * @clk: struct clk reference used to enforce clock limits. + * @lock: mutex lock to protect this struct. + * + * This structure is required for keeping information of each + * clock_cooling_device registered. In order to prevent corruption of this a + * mutex @lock is used. + */ +struct clock_cooling_device { + int id; + struct device *dev; + struct thermal_cooling_device *cdev; + struct notifier_block clk_rate_change_nb; + struct cpufreq_frequency_table *freq_table; + unsigned long clock_state; + unsigned long clock_val; + struct clk *clk; + struct mutex lock; /* lock to protect the content of this struct */ +}; +#define to_clock_cooling_device(x) \ + container_of(x, struct clock_cooling_device, clk_rate_change_nb) +static DEFINE_IDA(clock_ida); + +/* Below code defines functions to be used for clock as cooling device */ + +enum clock_cooling_property { + GET_LEVEL, + GET_FREQ, + GET_MAXL, +}; + +/** + * clock_cooling_get_property - fetch a property of interest for a give cpu. + * @ccdev: clock cooling device reference + * @input: query parameter + * @output: query return + * @property: type of query (frequency, level, max level) + * + * This is the common function to + * 1. get maximum clock cooling states + * 2. translate frequency to cooling state + * 3. translate cooling state to frequency + * Note that the code may be not in good shape + * but it is written in this way in order to: + * a) reduce duplicate code as most of the code can be shared. + * b) make sure the logic is consistent when translating between + * cooling states and frequencies. + * + * Return: 0 on success, -EINVAL when invalid parameters are passed. + */ +static int clock_cooling_get_property(struct clock_cooling_device *ccdev, + unsigned long input, + unsigned long *output, + enum clock_cooling_property property) +{ + int i; + unsigned long max_level = 0, level = 0; + unsigned int freq = CPUFREQ_ENTRY_INVALID; + int descend = -1; + struct cpufreq_frequency_table *pos, *table = ccdev->freq_table; + + if (!output) + return -EINVAL; + + if (!table) + return -EINVAL; + + cpufreq_for_each_valid_entry(pos, table) { + /* ignore duplicate entry */ + if (freq == pos->frequency) + continue; + + /* get the frequency order */ + if (freq != CPUFREQ_ENTRY_INVALID && descend == -1) + descend = freq > pos->frequency; + + freq = pos->frequency; + max_level++; + } + + /* No valid cpu frequency entry */ + if (max_level == 0) + return -EINVAL; + + /* max_level is an index, not a counter */ + max_level--; + + /* get max level */ + if (property == GET_MAXL) { + *output = max_level; + return 0; + } + + if (property == GET_FREQ) + level = descend ? input : (max_level - input); + + i = 0; + cpufreq_for_each_valid_entry(pos, table) { + /* ignore duplicate entry */ + if (freq == pos->frequency) + continue; + + /* now we have a valid frequency entry */ + freq = pos->frequency; + + if (property == GET_LEVEL && (unsigned int)input == freq) { + /* get level by frequency */ + *output = descend ? i : (max_level - i); + return 0; + } + if (property == GET_FREQ && level == i) { + /* get frequency by level */ + *output = freq; + return 0; + } + i++; + } + + return -EINVAL; +} + +/** + * clock_cooling_get_level - return the cooling level of given clock cooling. + * @cdev: reference of a thermal cooling device of used as clock cooling device + * @freq: the frequency of interest + * + * This function will match the cooling level corresponding to the + * requested @freq and return it. + * + * Return: The matched cooling level on success or THERMAL_CSTATE_INVALID + * otherwise. + */ +unsigned long clock_cooling_get_level(struct thermal_cooling_device *cdev, + unsigned long freq) +{ + struct clock_cooling_device *ccdev = cdev->devdata; + unsigned long val; + + if (clock_cooling_get_property(ccdev, (unsigned long)freq, &val, + GET_LEVEL)) + return THERMAL_CSTATE_INVALID; + + return val; +} +EXPORT_SYMBOL_GPL(clock_cooling_get_level); + +/** + * clock_cooling_get_frequency - get the absolute value of frequency from level. + * @ccdev: clock cooling device reference + * @level: cooling level + * + * This function matches cooling level with frequency. Based on a cooling level + * of frequency, equals cooling state of cpu cooling device, it will return + * the corresponding frequency. + * e.g level=0 --> 1st MAX FREQ, level=1 ---> 2nd MAX FREQ, .... etc + * + * Return: 0 on error, the corresponding frequency otherwise. + */ +static unsigned long +clock_cooling_get_frequency(struct clock_cooling_device *ccdev, + unsigned long level) +{ + int ret = 0; + unsigned long freq; + + ret = clock_cooling_get_property(ccdev, level, &freq, GET_FREQ); + if (ret) + return 0; + + return freq; +} + +/** + * clock_cooling_apply - function to apply frequency clipping. + * @ccdev: clock_cooling_device pointer containing frequency clipping data. + * @cooling_state: value of the cooling state. + * + * Function used to make sure the clock layer is aware of current thermal + * limits. The limits are applied by updating the clock rate in case it is + * higher than the corresponding frequency based on the requested cooling_state. + * + * Return: 0 on success, an error code otherwise (-EINVAL in case wrong + * cooling state). + */ +static int clock_cooling_apply(struct clock_cooling_device *ccdev, + unsigned long cooling_state) +{ + unsigned long clip_freq, cur_freq; + int ret = 0; + + /* Here we write the clipping */ + /* Check if the old cooling action is same as new cooling action */ + if (ccdev->clock_state == cooling_state) + return 0; + + clip_freq = clock_cooling_get_frequency(ccdev, cooling_state); + if (!clip_freq) + return -EINVAL; + + cur_freq = clk_get_rate(ccdev->clk); + + mutex_lock(&ccdev->lock); + ccdev->clock_state = cooling_state; + ccdev->clock_val = clip_freq; + /* enforce clock level */ + if (cur_freq > clip_freq) + ret = clk_set_rate(ccdev->clk, clip_freq); + mutex_unlock(&ccdev->lock); + + return ret; +} + +/** + * clock_cooling_clock_notifier - notifier callback on clock rate changes. + * @nb: struct notifier_block * with callback info. + * @event: value showing clock event for which this function invoked. + * @data: callback-specific data + * + * Callback to hijack the notification on clock transition. + * Every time there is a clock change, we intercept all pre change events + * and block the transition in case the new rate infringes thermal limits. + * + * Return: NOTIFY_DONE (success) or NOTIFY_BAD (new_rate > thermal limit). + */ +static int clock_cooling_clock_notifier(struct notifier_block *nb, + unsigned long event, void *data) +{ + struct clk_notifier_data *ndata = data; + struct clock_cooling_device *ccdev = to_clock_cooling_device(nb); + + switch (event) { + case PRE_RATE_CHANGE: + /* + * checks on current state + * TODO: current method is not best we can find as it + * allows possibly voltage transitions, in case DVFS + * layer is also hijacking clock pre notifications. + */ + if (ndata->new_rate > ccdev->clock_val) + return NOTIFY_BAD; + /* fall through */ + case POST_RATE_CHANGE: + case ABORT_RATE_CHANGE: + default: + return NOTIFY_DONE; + } +} + +/* clock cooling device thermal callback functions are defined below */ + +/** + * clock_cooling_get_max_state - callback function to get the max cooling state. + * @cdev: thermal cooling device pointer. + * @state: fill this variable with the max cooling state. + * + * Callback for the thermal cooling device to return the clock + * max cooling state. + * + * Return: 0 on success, an error code otherwise. + */ +static int clock_cooling_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct clock_cooling_device *ccdev = cdev->devdata; + unsigned long count = 0; + int ret; + + ret = clock_cooling_get_property(ccdev, 0, &count, GET_MAXL); + if (!ret) + *state = count; + + return ret; +} + +/** + * clock_cooling_get_cur_state - function to get the current cooling state. + * @cdev: thermal cooling device pointer. + * @state: fill this variable with the current cooling state. + * + * Callback for the thermal cooling device to return the clock + * current cooling state. + * + * Return: 0 (success) + */ +static int clock_cooling_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct clock_cooling_device *ccdev = cdev->devdata; + + *state = ccdev->clock_state; + + return 0; +} + +/** + * clock_cooling_set_cur_state - function to set the current cooling state. + * @cdev: thermal cooling device pointer. + * @state: set this variable to the current cooling state. + * + * Callback for the thermal cooling device to change the clock cooling + * current cooling state. + * + * Return: 0 on success, an error code otherwise. + */ +static int clock_cooling_set_cur_state(struct thermal_cooling_device *cdev, + unsigned long state) +{ + struct clock_cooling_device *clock_device = cdev->devdata; + + return clock_cooling_apply(clock_device, state); +} + +/* Bind clock callbacks to thermal cooling device ops */ +static struct thermal_cooling_device_ops const clock_cooling_ops = { + .get_max_state = clock_cooling_get_max_state, + .get_cur_state = clock_cooling_get_cur_state, + .set_cur_state = clock_cooling_set_cur_state, +}; + +/** + * clock_cooling_register - function to create clock cooling device. + * @dev: struct device pointer to the device used as clock cooling device. + * @clock_name: string containing the clock used as cooling mechanism. + * + * This interface function registers the clock cooling device with the name + * "thermal-clock-%x". The cooling device is based on clock frequencies. + * The struct device is assumed to be capable of DVFS transitions. + * The OPP layer is used to fetch and fill the available frequencies for + * the referred device. The ordered frequency table is used to control + * the clock cooling device cooling states and to limit clock transitions + * based on the cooling state requested by the thermal framework. + * + * Return: a valid struct thermal_cooling_device pointer on success, + * on failure, it returns a corresponding ERR_PTR(). + */ +struct thermal_cooling_device * +clock_cooling_register(struct device *dev, const char *clock_name) +{ + struct thermal_cooling_device *cdev; + struct clock_cooling_device *ccdev = NULL; + char dev_name[THERMAL_NAME_LENGTH]; + int ret = 0; + + ccdev = devm_kzalloc(dev, sizeof(*ccdev), GFP_KERNEL); + if (!ccdev) + return ERR_PTR(-ENOMEM); + + mutex_init(&ccdev->lock); + ccdev->dev = dev; + ccdev->clk = devm_clk_get(dev, clock_name); + if (IS_ERR(ccdev->clk)) + return ERR_CAST(ccdev->clk); + + ret = ida_simple_get(&clock_ida, 0, 0, GFP_KERNEL); + if (ret < 0) + return ERR_PTR(ret); + ccdev->id = ret; + + snprintf(dev_name, sizeof(dev_name), "thermal-clock-%d", ccdev->id); + + cdev = thermal_cooling_device_register(dev_name, ccdev, + &clock_cooling_ops); + if (IS_ERR(cdev)) { + ida_simple_remove(&clock_ida, ccdev->id); + return ERR_PTR(-EINVAL); + } + ccdev->cdev = cdev; + ccdev->clk_rate_change_nb.notifier_call = clock_cooling_clock_notifier; + + /* Assuming someone has already filled the opp table for this device */ + ret = dev_pm_opp_init_cpufreq_table(dev, &ccdev->freq_table); + if (ret) { + ida_simple_remove(&clock_ida, ccdev->id); + return ERR_PTR(ret); + } + ccdev->clock_state = 0; + ccdev->clock_val = clock_cooling_get_frequency(ccdev, 0); + + clk_notifier_register(ccdev->clk, &ccdev->clk_rate_change_nb); + + return cdev; +} +EXPORT_SYMBOL_GPL(clock_cooling_register); + +/** + * clock_cooling_unregister - function to remove clock cooling device. + * @cdev: thermal cooling device pointer. + * + * This interface function unregisters the "thermal-clock-%x" cooling device. + */ +void clock_cooling_unregister(struct thermal_cooling_device *cdev) +{ + struct clock_cooling_device *ccdev; + + if (!cdev) + return; + + ccdev = cdev->devdata; + + clk_notifier_unregister(ccdev->clk, &ccdev->clk_rate_change_nb); + dev_pm_opp_free_cpufreq_table(ccdev->dev, &ccdev->freq_table); + + thermal_cooling_device_unregister(ccdev->cdev); + ida_simple_remove(&clock_ida, ccdev->id); +} +EXPORT_SYMBOL_GPL(clock_cooling_unregister); diff --git a/drivers/thermal/cpu_cooling.c b/drivers/thermal/cpu_cooling.c new file mode 100644 index 000000000..8592ef694 --- /dev/null +++ b/drivers/thermal/cpu_cooling.c @@ -0,0 +1,820 @@ +/* + * linux/drivers/thermal/cpu_cooling.c + * + * Copyright (C) 2012 Samsung Electronics Co., Ltd(http://www.samsung.com) + * Copyright (C) 2012 Amit Daniel <amit.kachhap@linaro.org> + * + * Copyright (C) 2014 Viresh Kumar <viresh.kumar@linaro.org> + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ +#include <linux/module.h> +#include <linux/thermal.h> +#include <linux/cpufreq.h> +#include <linux/err.h> +#include <linux/idr.h> +#include <linux/pm_opp.h> +#include <linux/slab.h> +#include <linux/cpu.h> +#include <linux/cpu_cooling.h> + +#include <trace/events/thermal.h> + +/* + * Cooling state <-> CPUFreq frequency + * + * Cooling states are translated to frequencies throughout this driver and this + * is the relation between them. + * + * Highest cooling state corresponds to lowest possible frequency. + * + * i.e. + * level 0 --> 1st Max Freq + * level 1 --> 2nd Max Freq + * ... + */ + +/** + * struct freq_table - frequency table along with power entries + * @frequency: frequency in KHz + * @power: power in mW + * + * This structure is built when the cooling device registers and helps + * in translating frequency to power and vice versa. + */ +struct freq_table { + u32 frequency; + u32 power; +}; + +/** + * struct time_in_idle - Idle time stats + * @time: previous reading of the absolute time that this cpu was idle + * @timestamp: wall time of the last invocation of get_cpu_idle_time_us() + */ +struct time_in_idle { + u64 time; + u64 timestamp; +}; + +/** + * struct cpufreq_cooling_device - data for cooling device with cpufreq + * @id: unique integer value corresponding to each cpufreq_cooling_device + * registered. + * @last_load: load measured by the latest call to cpufreq_get_requested_power() + * @cpufreq_state: integer value representing the current state of cpufreq + * cooling devices. + * @clipped_freq: integer value representing the absolute value of the clipped + * frequency. + * @max_level: maximum cooling level. One less than total number of valid + * cpufreq frequencies. + * @freq_table: Freq table in descending order of frequencies + * @cdev: thermal_cooling_device pointer to keep track of the + * registered cooling device. + * @policy: cpufreq policy. + * @node: list_head to link all cpufreq_cooling_device together. + * @idle_time: idle time stats + * + * This structure is required for keeping information of each registered + * cpufreq_cooling_device. + */ +struct cpufreq_cooling_device { + int id; + u32 last_load; + unsigned int cpufreq_state; + unsigned int clipped_freq; + unsigned int max_level; + struct freq_table *freq_table; /* In descending order */ + struct thermal_cooling_device *cdev; + struct cpufreq_policy *policy; + struct list_head node; + struct time_in_idle *idle_time; +}; + +static DEFINE_IDA(cpufreq_ida); +static DEFINE_MUTEX(cooling_list_lock); +static LIST_HEAD(cpufreq_cdev_list); + +/* Below code defines functions to be used for cpufreq as cooling device */ + +/** + * get_level: Find the level for a particular frequency + * @cpufreq_cdev: cpufreq_cdev for which the property is required + * @freq: Frequency + * + * Return: level corresponding to the frequency. + */ +static unsigned long get_level(struct cpufreq_cooling_device *cpufreq_cdev, + unsigned int freq) +{ + struct freq_table *freq_table = cpufreq_cdev->freq_table; + unsigned long level; + + for (level = 1; level <= cpufreq_cdev->max_level; level++) + if (freq > freq_table[level].frequency) + break; + + return level - 1; +} + +/** + * cpufreq_thermal_notifier - notifier callback for cpufreq policy change. + * @nb: struct notifier_block * with callback info. + * @event: value showing cpufreq event for which this function invoked. + * @data: callback-specific data + * + * Callback to hijack the notification on cpufreq policy transition. + * Every time there is a change in policy, we will intercept and + * update the cpufreq policy with thermal constraints. + * + * Return: 0 (success) + */ +static int cpufreq_thermal_notifier(struct notifier_block *nb, + unsigned long event, void *data) +{ + struct cpufreq_policy *policy = data; + unsigned long clipped_freq; + struct cpufreq_cooling_device *cpufreq_cdev; + + if (event != CPUFREQ_ADJUST) + return NOTIFY_DONE; + + mutex_lock(&cooling_list_lock); + list_for_each_entry(cpufreq_cdev, &cpufreq_cdev_list, node) { + /* + * A new copy of the policy is sent to the notifier and can't + * compare that directly. + */ + if (policy->cpu != cpufreq_cdev->policy->cpu) + continue; + + /* + * policy->max is the maximum allowed frequency defined by user + * and clipped_freq is the maximum that thermal constraints + * allow. + * + * If clipped_freq is lower than policy->max, then we need to + * readjust policy->max. + * + * But, if clipped_freq is greater than policy->max, we don't + * need to do anything. + */ + clipped_freq = cpufreq_cdev->clipped_freq; + + if (policy->max > clipped_freq) + cpufreq_verify_within_limits(policy, 0, clipped_freq); + break; + } + mutex_unlock(&cooling_list_lock); + + return NOTIFY_OK; +} + +/** + * update_freq_table() - Update the freq table with power numbers + * @cpufreq_cdev: the cpufreq cooling device in which to update the table + * @capacitance: dynamic power coefficient for these cpus + * + * Update the freq table with power numbers. This table will be used in + * cpu_power_to_freq() and cpu_freq_to_power() to convert between power and + * frequency efficiently. Power is stored in mW, frequency in KHz. The + * resulting table is in descending order. + * + * Return: 0 on success, -EINVAL if there are no OPPs for any CPUs, + * or -ENOMEM if we run out of memory. + */ +static int update_freq_table(struct cpufreq_cooling_device *cpufreq_cdev, + u32 capacitance) +{ + struct freq_table *freq_table = cpufreq_cdev->freq_table; + struct dev_pm_opp *opp; + struct device *dev = NULL; + int num_opps = 0, cpu = cpufreq_cdev->policy->cpu, i; + + dev = get_cpu_device(cpu); + if (unlikely(!dev)) { + dev_warn(&cpufreq_cdev->cdev->device, + "No cpu device for cpu %d\n", cpu); + return -ENODEV; + } + + num_opps = dev_pm_opp_get_opp_count(dev); + if (num_opps < 0) + return num_opps; + + /* + * The cpufreq table is also built from the OPP table and so the count + * should match. + */ + if (num_opps != cpufreq_cdev->max_level + 1) { + dev_warn(dev, "Number of OPPs not matching with max_levels\n"); + return -EINVAL; + } + + for (i = 0; i <= cpufreq_cdev->max_level; i++) { + unsigned long freq = freq_table[i].frequency * 1000; + u32 freq_mhz = freq_table[i].frequency / 1000; + u64 power; + u32 voltage_mv; + + /* + * Find ceil frequency as 'freq' may be slightly lower than OPP + * freq due to truncation while converting to kHz. + */ + opp = dev_pm_opp_find_freq_ceil(dev, &freq); + if (IS_ERR(opp)) { + dev_err(dev, "failed to get opp for %lu frequency\n", + freq); + return -EINVAL; + } + + voltage_mv = dev_pm_opp_get_voltage(opp) / 1000; + dev_pm_opp_put(opp); + + /* + * Do the multiplication with MHz and millivolt so as + * to not overflow. + */ + power = (u64)capacitance * freq_mhz * voltage_mv * voltage_mv; + do_div(power, 1000000000); + + /* power is stored in mW */ + freq_table[i].power = power; + } + + return 0; +} + +static u32 cpu_freq_to_power(struct cpufreq_cooling_device *cpufreq_cdev, + u32 freq) +{ + int i; + struct freq_table *freq_table = cpufreq_cdev->freq_table; + + for (i = 1; i <= cpufreq_cdev->max_level; i++) + if (freq > freq_table[i].frequency) + break; + + return freq_table[i - 1].power; +} + +static u32 cpu_power_to_freq(struct cpufreq_cooling_device *cpufreq_cdev, + u32 power) +{ + int i; + struct freq_table *freq_table = cpufreq_cdev->freq_table; + + for (i = 0; i < cpufreq_cdev->max_level; i++) + if (power >= freq_table[i].power) + break; + + return freq_table[i].frequency; +} + +/** + * get_load() - get load for a cpu since last updated + * @cpufreq_cdev: &struct cpufreq_cooling_device for this cpu + * @cpu: cpu number + * @cpu_idx: index of the cpu in time_in_idle* + * + * Return: The average load of cpu @cpu in percentage since this + * function was last called. + */ +static u32 get_load(struct cpufreq_cooling_device *cpufreq_cdev, int cpu, + int cpu_idx) +{ + u32 load; + u64 now, now_idle, delta_time, delta_idle; + struct time_in_idle *idle_time = &cpufreq_cdev->idle_time[cpu_idx]; + + now_idle = get_cpu_idle_time(cpu, &now, 0); + delta_idle = now_idle - idle_time->time; + delta_time = now - idle_time->timestamp; + + if (delta_time <= delta_idle) + load = 0; + else + load = div64_u64(100 * (delta_time - delta_idle), delta_time); + + idle_time->time = now_idle; + idle_time->timestamp = now; + + return load; +} + +/** + * get_dynamic_power() - calculate the dynamic power + * @cpufreq_cdev: &cpufreq_cooling_device for this cdev + * @freq: current frequency + * + * Return: the dynamic power consumed by the cpus described by + * @cpufreq_cdev. + */ +static u32 get_dynamic_power(struct cpufreq_cooling_device *cpufreq_cdev, + unsigned long freq) +{ + u32 raw_cpu_power; + + raw_cpu_power = cpu_freq_to_power(cpufreq_cdev, freq); + return (raw_cpu_power * cpufreq_cdev->last_load) / 100; +} + +/* cpufreq cooling device callback functions are defined below */ + +/** + * cpufreq_get_max_state - callback function to get the max cooling state. + * @cdev: thermal cooling device pointer. + * @state: fill this variable with the max cooling state. + * + * Callback for the thermal cooling device to return the cpufreq + * max cooling state. + * + * Return: 0 on success, an error code otherwise. + */ +static int cpufreq_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct cpufreq_cooling_device *cpufreq_cdev = cdev->devdata; + + *state = cpufreq_cdev->max_level; + return 0; +} + +/** + * cpufreq_get_cur_state - callback function to get the current cooling state. + * @cdev: thermal cooling device pointer. + * @state: fill this variable with the current cooling state. + * + * Callback for the thermal cooling device to return the cpufreq + * current cooling state. + * + * Return: 0 on success, an error code otherwise. + */ +static int cpufreq_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct cpufreq_cooling_device *cpufreq_cdev = cdev->devdata; + + *state = cpufreq_cdev->cpufreq_state; + + return 0; +} + +/** + * cpufreq_set_cur_state - callback function to set the current cooling state. + * @cdev: thermal cooling device pointer. + * @state: set this variable to the current cooling state. + * + * Callback for the thermal cooling device to change the cpufreq + * current cooling state. + * + * Return: 0 on success, an error code otherwise. + */ +static int cpufreq_set_cur_state(struct thermal_cooling_device *cdev, + unsigned long state) +{ + struct cpufreq_cooling_device *cpufreq_cdev = cdev->devdata; + unsigned int clip_freq; + + /* Request state should be less than max_level */ + if (WARN_ON(state > cpufreq_cdev->max_level)) + return -EINVAL; + + /* Check if the old cooling action is same as new cooling action */ + if (cpufreq_cdev->cpufreq_state == state) + return 0; + + clip_freq = cpufreq_cdev->freq_table[state].frequency; + cpufreq_cdev->cpufreq_state = state; + cpufreq_cdev->clipped_freq = clip_freq; + + cpufreq_update_policy(cpufreq_cdev->policy->cpu); + + return 0; +} + +/** + * cpufreq_get_requested_power() - get the current power + * @cdev: &thermal_cooling_device pointer + * @tz: a valid thermal zone device pointer + * @power: pointer in which to store the resulting power + * + * Calculate the current power consumption of the cpus in milliwatts + * and store it in @power. This function should actually calculate + * the requested power, but it's hard to get the frequency that + * cpufreq would have assigned if there were no thermal limits. + * Instead, we calculate the current power on the assumption that the + * immediate future will look like the immediate past. + * + * We use the current frequency and the average load since this + * function was last called. In reality, there could have been + * multiple opps since this function was last called and that affects + * the load calculation. While it's not perfectly accurate, this + * simplification is good enough and works. REVISIT this, as more + * complex code may be needed if experiments show that it's not + * accurate enough. + * + * Return: 0 on success, -E* if getting the static power failed. + */ +static int cpufreq_get_requested_power(struct thermal_cooling_device *cdev, + struct thermal_zone_device *tz, + u32 *power) +{ + unsigned long freq; + int i = 0, cpu; + u32 total_load = 0; + struct cpufreq_cooling_device *cpufreq_cdev = cdev->devdata; + struct cpufreq_policy *policy = cpufreq_cdev->policy; + u32 *load_cpu = NULL; + + freq = cpufreq_quick_get(policy->cpu); + + if (trace_thermal_power_cpu_get_power_enabled()) { + u32 ncpus = cpumask_weight(policy->related_cpus); + + load_cpu = kcalloc(ncpus, sizeof(*load_cpu), GFP_KERNEL); + } + + for_each_cpu(cpu, policy->related_cpus) { + u32 load; + + if (cpu_online(cpu)) + load = get_load(cpufreq_cdev, cpu, i); + else + load = 0; + + total_load += load; + if (load_cpu) + load_cpu[i] = load; + + i++; + } + + cpufreq_cdev->last_load = total_load; + + *power = get_dynamic_power(cpufreq_cdev, freq); + + if (load_cpu) { + trace_thermal_power_cpu_get_power(policy->related_cpus, freq, + load_cpu, i, *power); + + kfree(load_cpu); + } + + return 0; +} + +/** + * cpufreq_state2power() - convert a cpu cdev state to power consumed + * @cdev: &thermal_cooling_device pointer + * @tz: a valid thermal zone device pointer + * @state: cooling device state to be converted + * @power: pointer in which to store the resulting power + * + * Convert cooling device state @state into power consumption in + * milliwatts assuming 100% load. Store the calculated power in + * @power. + * + * Return: 0 on success, -EINVAL if the cooling device state could not + * be converted into a frequency or other -E* if there was an error + * when calculating the static power. + */ +static int cpufreq_state2power(struct thermal_cooling_device *cdev, + struct thermal_zone_device *tz, + unsigned long state, u32 *power) +{ + unsigned int freq, num_cpus; + struct cpufreq_cooling_device *cpufreq_cdev = cdev->devdata; + + /* Request state should be less than max_level */ + if (WARN_ON(state > cpufreq_cdev->max_level)) + return -EINVAL; + + num_cpus = cpumask_weight(cpufreq_cdev->policy->cpus); + + freq = cpufreq_cdev->freq_table[state].frequency; + *power = cpu_freq_to_power(cpufreq_cdev, freq) * num_cpus; + + return 0; +} + +/** + * cpufreq_power2state() - convert power to a cooling device state + * @cdev: &thermal_cooling_device pointer + * @tz: a valid thermal zone device pointer + * @power: power in milliwatts to be converted + * @state: pointer in which to store the resulting state + * + * Calculate a cooling device state for the cpus described by @cdev + * that would allow them to consume at most @power mW and store it in + * @state. Note that this calculation depends on external factors + * such as the cpu load or the current static power. Calling this + * function with the same power as input can yield different cooling + * device states depending on those external factors. + * + * Return: 0 on success, -ENODEV if no cpus are online or -EINVAL if + * the calculated frequency could not be converted to a valid state. + * The latter should not happen unless the frequencies available to + * cpufreq have changed since the initialization of the cpu cooling + * device. + */ +static int cpufreq_power2state(struct thermal_cooling_device *cdev, + struct thermal_zone_device *tz, u32 power, + unsigned long *state) +{ + unsigned int cur_freq, target_freq; + u32 last_load, normalised_power; + struct cpufreq_cooling_device *cpufreq_cdev = cdev->devdata; + struct cpufreq_policy *policy = cpufreq_cdev->policy; + + cur_freq = cpufreq_quick_get(policy->cpu); + power = power > 0 ? power : 0; + last_load = cpufreq_cdev->last_load ?: 1; + normalised_power = (power * 100) / last_load; + target_freq = cpu_power_to_freq(cpufreq_cdev, normalised_power); + + *state = get_level(cpufreq_cdev, target_freq); + trace_thermal_power_cpu_limit(policy->related_cpus, target_freq, *state, + power); + return 0; +} + +/* Bind cpufreq callbacks to thermal cooling device ops */ + +static struct thermal_cooling_device_ops cpufreq_cooling_ops = { + .get_max_state = cpufreq_get_max_state, + .get_cur_state = cpufreq_get_cur_state, + .set_cur_state = cpufreq_set_cur_state, +}; + +static struct thermal_cooling_device_ops cpufreq_power_cooling_ops = { + .get_max_state = cpufreq_get_max_state, + .get_cur_state = cpufreq_get_cur_state, + .set_cur_state = cpufreq_set_cur_state, + .get_requested_power = cpufreq_get_requested_power, + .state2power = cpufreq_state2power, + .power2state = cpufreq_power2state, +}; + +/* Notifier for cpufreq policy change */ +static struct notifier_block thermal_cpufreq_notifier_block = { + .notifier_call = cpufreq_thermal_notifier, +}; + +static unsigned int find_next_max(struct cpufreq_frequency_table *table, + unsigned int prev_max) +{ + struct cpufreq_frequency_table *pos; + unsigned int max = 0; + + cpufreq_for_each_valid_entry(pos, table) { + if (pos->frequency > max && pos->frequency < prev_max) + max = pos->frequency; + } + + return max; +} + +/** + * __cpufreq_cooling_register - helper function to create cpufreq cooling device + * @np: a valid struct device_node to the cooling device device tree node + * @policy: cpufreq policy + * Normally this should be same as cpufreq policy->related_cpus. + * @capacitance: dynamic power coefficient for these cpus + * + * This interface function registers the cpufreq cooling device with the name + * "thermal-cpufreq-%x". This api can support multiple instances of cpufreq + * cooling devices. It also gives the opportunity to link the cooling device + * with a device tree node, in order to bind it via the thermal DT code. + * + * Return: a valid struct thermal_cooling_device pointer on success, + * on failure, it returns a corresponding ERR_PTR(). + */ +static struct thermal_cooling_device * +__cpufreq_cooling_register(struct device_node *np, + struct cpufreq_policy *policy, u32 capacitance) +{ + struct thermal_cooling_device *cdev; + struct cpufreq_cooling_device *cpufreq_cdev; + char dev_name[THERMAL_NAME_LENGTH]; + unsigned int freq, i, num_cpus; + int ret; + struct thermal_cooling_device_ops *cooling_ops; + bool first; + + if (IS_ERR_OR_NULL(policy)) { + pr_err("%s: cpufreq policy isn't valid: %p\n", __func__, policy); + return ERR_PTR(-EINVAL); + } + + i = cpufreq_table_count_valid_entries(policy); + if (!i) { + pr_debug("%s: CPUFreq table not found or has no valid entries\n", + __func__); + return ERR_PTR(-ENODEV); + } + + cpufreq_cdev = kzalloc(sizeof(*cpufreq_cdev), GFP_KERNEL); + if (!cpufreq_cdev) + return ERR_PTR(-ENOMEM); + + cpufreq_cdev->policy = policy; + num_cpus = cpumask_weight(policy->related_cpus); + cpufreq_cdev->idle_time = kcalloc(num_cpus, + sizeof(*cpufreq_cdev->idle_time), + GFP_KERNEL); + if (!cpufreq_cdev->idle_time) { + cdev = ERR_PTR(-ENOMEM); + goto free_cdev; + } + + /* max_level is an index, not a counter */ + cpufreq_cdev->max_level = i - 1; + + cpufreq_cdev->freq_table = kmalloc_array(i, + sizeof(*cpufreq_cdev->freq_table), + GFP_KERNEL); + if (!cpufreq_cdev->freq_table) { + cdev = ERR_PTR(-ENOMEM); + goto free_idle_time; + } + + ret = ida_simple_get(&cpufreq_ida, 0, 0, GFP_KERNEL); + if (ret < 0) { + cdev = ERR_PTR(ret); + goto free_table; + } + cpufreq_cdev->id = ret; + + snprintf(dev_name, sizeof(dev_name), "thermal-cpufreq-%d", + cpufreq_cdev->id); + + /* Fill freq-table in descending order of frequencies */ + for (i = 0, freq = -1; i <= cpufreq_cdev->max_level; i++) { + freq = find_next_max(policy->freq_table, freq); + cpufreq_cdev->freq_table[i].frequency = freq; + + /* Warn for duplicate entries */ + if (!freq) + pr_warn("%s: table has duplicate entries\n", __func__); + else + pr_debug("%s: freq:%u KHz\n", __func__, freq); + } + + if (capacitance) { + ret = update_freq_table(cpufreq_cdev, capacitance); + if (ret) { + cdev = ERR_PTR(ret); + goto remove_ida; + } + + cooling_ops = &cpufreq_power_cooling_ops; + } else { + cooling_ops = &cpufreq_cooling_ops; + } + + cdev = thermal_of_cooling_device_register(np, dev_name, cpufreq_cdev, + cooling_ops); + if (IS_ERR(cdev)) + goto remove_ida; + + cpufreq_cdev->clipped_freq = cpufreq_cdev->freq_table[0].frequency; + cpufreq_cdev->cdev = cdev; + + mutex_lock(&cooling_list_lock); + /* Register the notifier for first cpufreq cooling device */ + first = list_empty(&cpufreq_cdev_list); + list_add(&cpufreq_cdev->node, &cpufreq_cdev_list); + mutex_unlock(&cooling_list_lock); + + if (first) + cpufreq_register_notifier(&thermal_cpufreq_notifier_block, + CPUFREQ_POLICY_NOTIFIER); + + return cdev; + +remove_ida: + ida_simple_remove(&cpufreq_ida, cpufreq_cdev->id); +free_table: + kfree(cpufreq_cdev->freq_table); +free_idle_time: + kfree(cpufreq_cdev->idle_time); +free_cdev: + kfree(cpufreq_cdev); + return cdev; +} + +/** + * cpufreq_cooling_register - function to create cpufreq cooling device. + * @policy: cpufreq policy + * + * This interface function registers the cpufreq cooling device with the name + * "thermal-cpufreq-%x". This api can support multiple instances of cpufreq + * cooling devices. + * + * Return: a valid struct thermal_cooling_device pointer on success, + * on failure, it returns a corresponding ERR_PTR(). + */ +struct thermal_cooling_device * +cpufreq_cooling_register(struct cpufreq_policy *policy) +{ + return __cpufreq_cooling_register(NULL, policy, 0); +} +EXPORT_SYMBOL_GPL(cpufreq_cooling_register); + +/** + * of_cpufreq_cooling_register - function to create cpufreq cooling device. + * @policy: cpufreq policy + * + * This interface function registers the cpufreq cooling device with the name + * "thermal-cpufreq-%x". This api can support multiple instances of cpufreq + * cooling devices. Using this API, the cpufreq cooling device will be + * linked to the device tree node provided. + * + * Using this function, the cooling device will implement the power + * extensions by using a simple cpu power model. The cpus must have + * registered their OPPs using the OPP library. + * + * It also takes into account, if property present in policy CPU node, the + * static power consumed by the cpu. + * + * Return: a valid struct thermal_cooling_device pointer on success, + * and NULL on failure. + */ +struct thermal_cooling_device * +of_cpufreq_cooling_register(struct cpufreq_policy *policy) +{ + struct device_node *np = of_get_cpu_node(policy->cpu, NULL); + struct thermal_cooling_device *cdev = NULL; + u32 capacitance = 0; + + if (!np) { + pr_err("cpu_cooling: OF node not available for cpu%d\n", + policy->cpu); + return NULL; + } + + if (of_find_property(np, "#cooling-cells", NULL)) { + of_property_read_u32(np, "dynamic-power-coefficient", + &capacitance); + + cdev = __cpufreq_cooling_register(np, policy, capacitance); + if (IS_ERR(cdev)) { + pr_err("cpu_cooling: cpu%d is not running as cooling device: %ld\n", + policy->cpu, PTR_ERR(cdev)); + cdev = NULL; + } + } + + of_node_put(np); + return cdev; +} +EXPORT_SYMBOL_GPL(of_cpufreq_cooling_register); + +/** + * cpufreq_cooling_unregister - function to remove cpufreq cooling device. + * @cdev: thermal cooling device pointer. + * + * This interface function unregisters the "thermal-cpufreq-%x" cooling device. + */ +void cpufreq_cooling_unregister(struct thermal_cooling_device *cdev) +{ + struct cpufreq_cooling_device *cpufreq_cdev; + bool last; + + if (!cdev) + return; + + cpufreq_cdev = cdev->devdata; + + mutex_lock(&cooling_list_lock); + list_del(&cpufreq_cdev->node); + /* Unregister the notifier for the last cpufreq cooling device */ + last = list_empty(&cpufreq_cdev_list); + mutex_unlock(&cooling_list_lock); + + if (last) + cpufreq_unregister_notifier(&thermal_cpufreq_notifier_block, + CPUFREQ_POLICY_NOTIFIER); + + thermal_cooling_device_unregister(cpufreq_cdev->cdev); + ida_simple_remove(&cpufreq_ida, cpufreq_cdev->id); + kfree(cpufreq_cdev->idle_time); + kfree(cpufreq_cdev->freq_table); + kfree(cpufreq_cdev); +} +EXPORT_SYMBOL_GPL(cpufreq_cooling_unregister); diff --git a/drivers/thermal/da9062-thermal.c b/drivers/thermal/da9062-thermal.c new file mode 100644 index 000000000..01b0cb994 --- /dev/null +++ b/drivers/thermal/da9062-thermal.c @@ -0,0 +1,315 @@ +/* + * Thermal device driver for DA9062 and DA9061 + * Copyright (C) 2017 Dialog Semiconductor + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +/* When over-temperature is reached, an interrupt from the device will be + * triggered. Following this event the interrupt will be disabled and + * periodic transmission of uevents (HOT trip point) should define the + * first level of temperature supervision. It is expected that any final + * implementation of the thermal driver will include a .notify() function + * to implement these uevents to userspace. + * + * These uevents are intended to indicate non-invasive temperature control + * of the system, where the necessary measures for cooling are the + * responsibility of the host software. Once the temperature falls again, + * the IRQ is re-enabled so the start of a new over-temperature event can + * be detected without constant software monitoring. + */ + +#include <linux/errno.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/thermal.h> +#include <linux/workqueue.h> + +#include <linux/mfd/da9062/core.h> +#include <linux/mfd/da9062/registers.h> + +/* Minimum, maximum and default polling millisecond periods are provided + * here as an example. It is expected that any final implementation to also + * include a modification of these settings to match the required + * application. + */ +#define DA9062_DEFAULT_POLLING_MS_PERIOD 3000 +#define DA9062_MAX_POLLING_MS_PERIOD 10000 +#define DA9062_MIN_POLLING_MS_PERIOD 1000 + +#define DA9062_MILLI_CELSIUS(t) ((t) * 1000) + +struct da9062_thermal_config { + const char *name; +}; + +struct da9062_thermal { + struct da9062 *hw; + struct delayed_work work; + struct thermal_zone_device *zone; + enum thermal_device_mode mode; + struct mutex lock; /* protection for da9062_thermal temperature */ + int temperature; + int irq; + const struct da9062_thermal_config *config; + struct device *dev; +}; + +static void da9062_thermal_poll_on(struct work_struct *work) +{ + struct da9062_thermal *thermal = container_of(work, + struct da9062_thermal, + work.work); + unsigned long delay; + unsigned int val; + int ret; + + /* clear E_TEMP */ + ret = regmap_write(thermal->hw->regmap, + DA9062AA_EVENT_B, + DA9062AA_E_TEMP_MASK); + if (ret < 0) { + dev_err(thermal->dev, + "Cannot clear the TJUNC temperature status\n"); + goto err_enable_irq; + } + + /* Now read E_TEMP again: it is acting like a status bit. + * If over-temperature, then this status will be true. + * If not over-temperature, this status will be false. + */ + ret = regmap_read(thermal->hw->regmap, + DA9062AA_EVENT_B, + &val); + if (ret < 0) { + dev_err(thermal->dev, + "Cannot check the TJUNC temperature status\n"); + goto err_enable_irq; + } + + if (val & DA9062AA_E_TEMP_MASK) { + mutex_lock(&thermal->lock); + thermal->temperature = DA9062_MILLI_CELSIUS(125); + mutex_unlock(&thermal->lock); + thermal_zone_device_update(thermal->zone, + THERMAL_EVENT_UNSPECIFIED); + + delay = msecs_to_jiffies(thermal->zone->passive_delay); + queue_delayed_work(system_freezable_wq, &thermal->work, delay); + return; + } + + mutex_lock(&thermal->lock); + thermal->temperature = DA9062_MILLI_CELSIUS(0); + mutex_unlock(&thermal->lock); + thermal_zone_device_update(thermal->zone, + THERMAL_EVENT_UNSPECIFIED); + +err_enable_irq: + enable_irq(thermal->irq); +} + +static irqreturn_t da9062_thermal_irq_handler(int irq, void *data) +{ + struct da9062_thermal *thermal = data; + + disable_irq_nosync(thermal->irq); + queue_delayed_work(system_freezable_wq, &thermal->work, 0); + + return IRQ_HANDLED; +} + +static int da9062_thermal_get_mode(struct thermal_zone_device *z, + enum thermal_device_mode *mode) +{ + struct da9062_thermal *thermal = z->devdata; + *mode = thermal->mode; + return 0; +} + +static int da9062_thermal_get_trip_type(struct thermal_zone_device *z, + int trip, + enum thermal_trip_type *type) +{ + struct da9062_thermal *thermal = z->devdata; + + switch (trip) { + case 0: + *type = THERMAL_TRIP_HOT; + break; + default: + dev_err(thermal->dev, + "Driver does not support more than 1 trip-wire\n"); + return -EINVAL; + } + + return 0; +} + +static int da9062_thermal_get_trip_temp(struct thermal_zone_device *z, + int trip, + int *temp) +{ + struct da9062_thermal *thermal = z->devdata; + + switch (trip) { + case 0: + *temp = DA9062_MILLI_CELSIUS(125); + break; + default: + dev_err(thermal->dev, + "Driver does not support more than 1 trip-wire\n"); + return -EINVAL; + } + + return 0; +} + +static int da9062_thermal_get_temp(struct thermal_zone_device *z, + int *temp) +{ + struct da9062_thermal *thermal = z->devdata; + + mutex_lock(&thermal->lock); + *temp = thermal->temperature; + mutex_unlock(&thermal->lock); + + return 0; +} + +static struct thermal_zone_device_ops da9062_thermal_ops = { + .get_temp = da9062_thermal_get_temp, + .get_mode = da9062_thermal_get_mode, + .get_trip_type = da9062_thermal_get_trip_type, + .get_trip_temp = da9062_thermal_get_trip_temp, +}; + +static const struct da9062_thermal_config da9062_config = { + .name = "da9062-thermal", +}; + +static const struct of_device_id da9062_compatible_reg_id_table[] = { + { .compatible = "dlg,da9062-thermal", .data = &da9062_config }, + { }, +}; + +MODULE_DEVICE_TABLE(of, da9062_compatible_reg_id_table); + +static int da9062_thermal_probe(struct platform_device *pdev) +{ + struct da9062 *chip = dev_get_drvdata(pdev->dev.parent); + struct da9062_thermal *thermal; + unsigned int pp_tmp = DA9062_DEFAULT_POLLING_MS_PERIOD; + const struct of_device_id *match; + int ret = 0; + + match = of_match_node(da9062_compatible_reg_id_table, + pdev->dev.of_node); + if (!match) + return -ENXIO; + + if (pdev->dev.of_node) { + if (!of_property_read_u32(pdev->dev.of_node, + "polling-delay-passive", + &pp_tmp)) { + if (pp_tmp < DA9062_MIN_POLLING_MS_PERIOD || + pp_tmp > DA9062_MAX_POLLING_MS_PERIOD) { + dev_warn(&pdev->dev, + "Out-of-range polling period %d ms\n", + pp_tmp); + pp_tmp = DA9062_DEFAULT_POLLING_MS_PERIOD; + } + } + } + + thermal = devm_kzalloc(&pdev->dev, sizeof(struct da9062_thermal), + GFP_KERNEL); + if (!thermal) { + ret = -ENOMEM; + goto err; + } + + thermal->config = match->data; + thermal->hw = chip; + thermal->mode = THERMAL_DEVICE_ENABLED; + thermal->dev = &pdev->dev; + + INIT_DELAYED_WORK(&thermal->work, da9062_thermal_poll_on); + mutex_init(&thermal->lock); + + thermal->zone = thermal_zone_device_register(thermal->config->name, + 1, 0, thermal, + &da9062_thermal_ops, NULL, pp_tmp, + 0); + if (IS_ERR(thermal->zone)) { + dev_err(&pdev->dev, "Cannot register thermal zone device\n"); + ret = PTR_ERR(thermal->zone); + goto err; + } + + dev_dbg(&pdev->dev, + "TJUNC temperature polling period set at %d ms\n", + thermal->zone->passive_delay); + + ret = platform_get_irq_byname(pdev, "THERMAL"); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to get platform IRQ.\n"); + goto err_zone; + } + thermal->irq = ret; + + ret = request_threaded_irq(thermal->irq, NULL, + da9062_thermal_irq_handler, + IRQF_TRIGGER_LOW | IRQF_ONESHOT, + "THERMAL", thermal); + if (ret) { + dev_err(&pdev->dev, + "Failed to request thermal device IRQ.\n"); + goto err_zone; + } + + platform_set_drvdata(pdev, thermal); + return 0; + +err_zone: + thermal_zone_device_unregister(thermal->zone); +err: + return ret; +} + +static int da9062_thermal_remove(struct platform_device *pdev) +{ + struct da9062_thermal *thermal = platform_get_drvdata(pdev); + + free_irq(thermal->irq, thermal); + cancel_delayed_work_sync(&thermal->work); + thermal_zone_device_unregister(thermal->zone); + return 0; +} + +static struct platform_driver da9062_thermal_driver = { + .probe = da9062_thermal_probe, + .remove = da9062_thermal_remove, + .driver = { + .name = "da9062-thermal", + .of_match_table = da9062_compatible_reg_id_table, + }, +}; + +module_platform_driver(da9062_thermal_driver); + +MODULE_AUTHOR("Steve Twiss"); +MODULE_DESCRIPTION("Thermal TJUNC device driver for Dialog DA9062 and DA9061"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:da9062-thermal"); diff --git a/drivers/thermal/db8500_thermal.c b/drivers/thermal/db8500_thermal.c new file mode 100644 index 000000000..f491faf16 --- /dev/null +++ b/drivers/thermal/db8500_thermal.c @@ -0,0 +1,533 @@ +/* + * db8500_thermal.c - DB8500 Thermal Management Implementation + * + * Copyright (C) 2012 ST-Ericsson + * Copyright (C) 2012 Linaro Ltd. + * + * Author: Hongbo Zhang <hongbo.zhang@linaro.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/cpu_cooling.h> +#include <linux/interrupt.h> +#include <linux/mfd/dbx500-prcmu.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_data/db8500_thermal.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/thermal.h> + +#define PRCMU_DEFAULT_MEASURE_TIME 0xFFF +#define PRCMU_DEFAULT_LOW_TEMP 0 + +struct db8500_thermal_zone { + struct thermal_zone_device *therm_dev; + struct mutex th_lock; + struct work_struct therm_work; + struct db8500_thsens_platform_data *trip_tab; + enum thermal_device_mode mode; + enum thermal_trend trend; + unsigned long cur_temp_pseudo; + unsigned int cur_index; +}; + +/* Local function to check if thermal zone matches cooling devices */ +static int db8500_thermal_match_cdev(struct thermal_cooling_device *cdev, + struct db8500_trip_point *trip_point) +{ + int i; + + if (!strlen(cdev->type)) + return -EINVAL; + + for (i = 0; i < COOLING_DEV_MAX; i++) { + if (!strcmp(trip_point->cdev_name[i], cdev->type)) + return 0; + } + + return -ENODEV; +} + +/* Callback to bind cooling device to thermal zone */ +static int db8500_cdev_bind(struct thermal_zone_device *thermal, + struct thermal_cooling_device *cdev) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + struct db8500_thsens_platform_data *ptrips = pzone->trip_tab; + unsigned long max_state, upper, lower; + int i, ret = -EINVAL; + + cdev->ops->get_max_state(cdev, &max_state); + + for (i = 0; i < ptrips->num_trips; i++) { + if (db8500_thermal_match_cdev(cdev, &ptrips->trip_points[i])) + continue; + + upper = lower = i > max_state ? max_state : i; + + ret = thermal_zone_bind_cooling_device(thermal, i, cdev, + upper, lower, THERMAL_WEIGHT_DEFAULT); + + dev_info(&cdev->device, "%s bind to %d: %d-%s\n", cdev->type, + i, ret, ret ? "fail" : "succeed"); + } + + return ret; +} + +/* Callback to unbind cooling device from thermal zone */ +static int db8500_cdev_unbind(struct thermal_zone_device *thermal, + struct thermal_cooling_device *cdev) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + struct db8500_thsens_platform_data *ptrips = pzone->trip_tab; + int i, ret = -EINVAL; + + for (i = 0; i < ptrips->num_trips; i++) { + if (db8500_thermal_match_cdev(cdev, &ptrips->trip_points[i])) + continue; + + ret = thermal_zone_unbind_cooling_device(thermal, i, cdev); + + dev_info(&cdev->device, "%s unbind from %d: %s\n", cdev->type, + i, ret ? "fail" : "succeed"); + } + + return ret; +} + +/* Callback to get current temperature */ +static int db8500_sys_get_temp(struct thermal_zone_device *thermal, int *temp) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + + /* + * TODO: There is no PRCMU interface to get temperature data currently, + * so a pseudo temperature is returned , it works for thermal framework + * and this will be fixed when the PRCMU interface is available. + */ + *temp = pzone->cur_temp_pseudo; + + return 0; +} + +/* Callback to get temperature changing trend */ +static int db8500_sys_get_trend(struct thermal_zone_device *thermal, + int trip, enum thermal_trend *trend) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + + *trend = pzone->trend; + + return 0; +} + +/* Callback to get thermal zone mode */ +static int db8500_sys_get_mode(struct thermal_zone_device *thermal, + enum thermal_device_mode *mode) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + + mutex_lock(&pzone->th_lock); + *mode = pzone->mode; + mutex_unlock(&pzone->th_lock); + + return 0; +} + +/* Callback to set thermal zone mode */ +static int db8500_sys_set_mode(struct thermal_zone_device *thermal, + enum thermal_device_mode mode) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + + mutex_lock(&pzone->th_lock); + + pzone->mode = mode; + if (mode == THERMAL_DEVICE_ENABLED) + schedule_work(&pzone->therm_work); + + mutex_unlock(&pzone->th_lock); + + return 0; +} + +/* Callback to get trip point type */ +static int db8500_sys_get_trip_type(struct thermal_zone_device *thermal, + int trip, enum thermal_trip_type *type) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + struct db8500_thsens_platform_data *ptrips = pzone->trip_tab; + + if (trip >= ptrips->num_trips) + return -EINVAL; + + *type = ptrips->trip_points[trip].type; + + return 0; +} + +/* Callback to get trip point temperature */ +static int db8500_sys_get_trip_temp(struct thermal_zone_device *thermal, + int trip, int *temp) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + struct db8500_thsens_platform_data *ptrips = pzone->trip_tab; + + if (trip >= ptrips->num_trips) + return -EINVAL; + + *temp = ptrips->trip_points[trip].temp; + + return 0; +} + +/* Callback to get critical trip point temperature */ +static int db8500_sys_get_crit_temp(struct thermal_zone_device *thermal, + int *temp) +{ + struct db8500_thermal_zone *pzone = thermal->devdata; + struct db8500_thsens_platform_data *ptrips = pzone->trip_tab; + int i; + + for (i = ptrips->num_trips - 1; i > 0; i--) { + if (ptrips->trip_points[i].type == THERMAL_TRIP_CRITICAL) { + *temp = ptrips->trip_points[i].temp; + return 0; + } + } + + return -EINVAL; +} + +static struct thermal_zone_device_ops thdev_ops = { + .bind = db8500_cdev_bind, + .unbind = db8500_cdev_unbind, + .get_temp = db8500_sys_get_temp, + .get_trend = db8500_sys_get_trend, + .get_mode = db8500_sys_get_mode, + .set_mode = db8500_sys_set_mode, + .get_trip_type = db8500_sys_get_trip_type, + .get_trip_temp = db8500_sys_get_trip_temp, + .get_crit_temp = db8500_sys_get_crit_temp, +}; + +static void db8500_thermal_update_config(struct db8500_thermal_zone *pzone, + unsigned int idx, enum thermal_trend trend, + unsigned long next_low, unsigned long next_high) +{ + prcmu_stop_temp_sense(); + + pzone->cur_index = idx; + pzone->cur_temp_pseudo = (next_low + next_high)/2; + pzone->trend = trend; + + prcmu_config_hotmon((u8)(next_low/1000), (u8)(next_high/1000)); + prcmu_start_temp_sense(PRCMU_DEFAULT_MEASURE_TIME); +} + +static irqreturn_t prcmu_low_irq_handler(int irq, void *irq_data) +{ + struct db8500_thermal_zone *pzone = irq_data; + struct db8500_thsens_platform_data *ptrips = pzone->trip_tab; + unsigned int idx = pzone->cur_index; + unsigned long next_low, next_high; + + if (unlikely(idx == 0)) + /* Meaningless for thermal management, ignoring it */ + return IRQ_HANDLED; + + if (idx == 1) { + next_high = ptrips->trip_points[0].temp; + next_low = PRCMU_DEFAULT_LOW_TEMP; + } else { + next_high = ptrips->trip_points[idx-1].temp; + next_low = ptrips->trip_points[idx-2].temp; + } + idx -= 1; + + db8500_thermal_update_config(pzone, idx, THERMAL_TREND_DROPPING, + next_low, next_high); + + dev_dbg(&pzone->therm_dev->device, + "PRCMU set max %ld, min %ld\n", next_high, next_low); + + schedule_work(&pzone->therm_work); + + return IRQ_HANDLED; +} + +static irqreturn_t prcmu_high_irq_handler(int irq, void *irq_data) +{ + struct db8500_thermal_zone *pzone = irq_data; + struct db8500_thsens_platform_data *ptrips = pzone->trip_tab; + unsigned int idx = pzone->cur_index; + unsigned long next_low, next_high; + + if (idx < ptrips->num_trips - 1) { + next_high = ptrips->trip_points[idx+1].temp; + next_low = ptrips->trip_points[idx].temp; + idx += 1; + + db8500_thermal_update_config(pzone, idx, THERMAL_TREND_RAISING, + next_low, next_high); + + dev_dbg(&pzone->therm_dev->device, + "PRCMU set max %ld, min %ld\n", next_high, next_low); + } else if (idx == ptrips->num_trips - 1) + pzone->cur_temp_pseudo = ptrips->trip_points[idx].temp + 1; + + schedule_work(&pzone->therm_work); + + return IRQ_HANDLED; +} + +static void db8500_thermal_work(struct work_struct *work) +{ + enum thermal_device_mode cur_mode; + struct db8500_thermal_zone *pzone; + + pzone = container_of(work, struct db8500_thermal_zone, therm_work); + + mutex_lock(&pzone->th_lock); + cur_mode = pzone->mode; + mutex_unlock(&pzone->th_lock); + + if (cur_mode == THERMAL_DEVICE_DISABLED) + return; + + thermal_zone_device_update(pzone->therm_dev, THERMAL_EVENT_UNSPECIFIED); + dev_dbg(&pzone->therm_dev->device, "thermal work finished.\n"); +} + +#ifdef CONFIG_OF +static struct db8500_thsens_platform_data* + db8500_thermal_parse_dt(struct platform_device *pdev) +{ + struct db8500_thsens_platform_data *ptrips; + struct device_node *np = pdev->dev.of_node; + char prop_name[32]; + const char *tmp_str; + u32 tmp_data; + int i, j; + + ptrips = devm_kzalloc(&pdev->dev, sizeof(*ptrips), GFP_KERNEL); + if (!ptrips) + return NULL; + + if (of_property_read_u32(np, "num-trips", &tmp_data)) + goto err_parse_dt; + + if (tmp_data > THERMAL_MAX_TRIPS) + goto err_parse_dt; + + ptrips->num_trips = tmp_data; + + for (i = 0; i < ptrips->num_trips; i++) { + sprintf(prop_name, "trip%d-temp", i); + if (of_property_read_u32(np, prop_name, &tmp_data)) + goto err_parse_dt; + + ptrips->trip_points[i].temp = tmp_data; + + sprintf(prop_name, "trip%d-type", i); + if (of_property_read_string(np, prop_name, &tmp_str)) + goto err_parse_dt; + + if (!strcmp(tmp_str, "active")) + ptrips->trip_points[i].type = THERMAL_TRIP_ACTIVE; + else if (!strcmp(tmp_str, "passive")) + ptrips->trip_points[i].type = THERMAL_TRIP_PASSIVE; + else if (!strcmp(tmp_str, "hot")) + ptrips->trip_points[i].type = THERMAL_TRIP_HOT; + else if (!strcmp(tmp_str, "critical")) + ptrips->trip_points[i].type = THERMAL_TRIP_CRITICAL; + else + goto err_parse_dt; + + sprintf(prop_name, "trip%d-cdev-num", i); + if (of_property_read_u32(np, prop_name, &tmp_data)) + goto err_parse_dt; + + if (tmp_data > COOLING_DEV_MAX) + goto err_parse_dt; + + for (j = 0; j < tmp_data; j++) { + sprintf(prop_name, "trip%d-cdev-name%d", i, j); + if (of_property_read_string(np, prop_name, &tmp_str)) + goto err_parse_dt; + + if (strlen(tmp_str) >= THERMAL_NAME_LENGTH) + goto err_parse_dt; + + strcpy(ptrips->trip_points[i].cdev_name[j], tmp_str); + } + } + return ptrips; + +err_parse_dt: + dev_err(&pdev->dev, "Parsing device tree data error.\n"); + return NULL; +} +#else +static inline struct db8500_thsens_platform_data* + db8500_thermal_parse_dt(struct platform_device *pdev) +{ + return NULL; +} +#endif + +static int db8500_thermal_probe(struct platform_device *pdev) +{ + struct db8500_thermal_zone *pzone = NULL; + struct db8500_thsens_platform_data *ptrips = NULL; + struct device_node *np = pdev->dev.of_node; + int low_irq, high_irq, ret = 0; + unsigned long dft_low, dft_high; + + if (np) + ptrips = db8500_thermal_parse_dt(pdev); + else + ptrips = dev_get_platdata(&pdev->dev); + + if (!ptrips) + return -EINVAL; + + pzone = devm_kzalloc(&pdev->dev, sizeof(*pzone), GFP_KERNEL); + if (!pzone) + return -ENOMEM; + + mutex_init(&pzone->th_lock); + mutex_lock(&pzone->th_lock); + + pzone->mode = THERMAL_DEVICE_DISABLED; + pzone->trip_tab = ptrips; + + INIT_WORK(&pzone->therm_work, db8500_thermal_work); + + low_irq = platform_get_irq_byname(pdev, "IRQ_HOTMON_LOW"); + if (low_irq < 0) { + dev_err(&pdev->dev, "Get IRQ_HOTMON_LOW failed.\n"); + ret = low_irq; + goto out_unlock; + } + + ret = devm_request_threaded_irq(&pdev->dev, low_irq, NULL, + prcmu_low_irq_handler, IRQF_NO_SUSPEND | IRQF_ONESHOT, + "dbx500_temp_low", pzone); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to allocate temp low irq.\n"); + goto out_unlock; + } + + high_irq = platform_get_irq_byname(pdev, "IRQ_HOTMON_HIGH"); + if (high_irq < 0) { + dev_err(&pdev->dev, "Get IRQ_HOTMON_HIGH failed.\n"); + ret = high_irq; + goto out_unlock; + } + + ret = devm_request_threaded_irq(&pdev->dev, high_irq, NULL, + prcmu_high_irq_handler, IRQF_NO_SUSPEND | IRQF_ONESHOT, + "dbx500_temp_high", pzone); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to allocate temp high irq.\n"); + goto out_unlock; + } + + pzone->therm_dev = thermal_zone_device_register("db8500_thermal_zone", + ptrips->num_trips, 0, pzone, &thdev_ops, NULL, 0, 0); + + if (IS_ERR(pzone->therm_dev)) { + dev_err(&pdev->dev, "Register thermal zone device failed.\n"); + ret = PTR_ERR(pzone->therm_dev); + goto out_unlock; + } + dev_info(&pdev->dev, "Thermal zone device registered.\n"); + + dft_low = PRCMU_DEFAULT_LOW_TEMP; + dft_high = ptrips->trip_points[0].temp; + + db8500_thermal_update_config(pzone, 0, THERMAL_TREND_STABLE, + dft_low, dft_high); + + platform_set_drvdata(pdev, pzone); + pzone->mode = THERMAL_DEVICE_ENABLED; + +out_unlock: + mutex_unlock(&pzone->th_lock); + + return ret; +} + +static int db8500_thermal_remove(struct platform_device *pdev) +{ + struct db8500_thermal_zone *pzone = platform_get_drvdata(pdev); + + thermal_zone_device_unregister(pzone->therm_dev); + cancel_work_sync(&pzone->therm_work); + mutex_destroy(&pzone->th_lock); + + return 0; +} + +static int db8500_thermal_suspend(struct platform_device *pdev, + pm_message_t state) +{ + struct db8500_thermal_zone *pzone = platform_get_drvdata(pdev); + + flush_work(&pzone->therm_work); + prcmu_stop_temp_sense(); + + return 0; +} + +static int db8500_thermal_resume(struct platform_device *pdev) +{ + struct db8500_thermal_zone *pzone = platform_get_drvdata(pdev); + struct db8500_thsens_platform_data *ptrips = pzone->trip_tab; + unsigned long dft_low, dft_high; + + dft_low = PRCMU_DEFAULT_LOW_TEMP; + dft_high = ptrips->trip_points[0].temp; + + db8500_thermal_update_config(pzone, 0, THERMAL_TREND_STABLE, + dft_low, dft_high); + + return 0; +} + +#ifdef CONFIG_OF +static const struct of_device_id db8500_thermal_match[] = { + { .compatible = "stericsson,db8500-thermal" }, + {}, +}; +MODULE_DEVICE_TABLE(of, db8500_thermal_match); +#endif + +static struct platform_driver db8500_thermal_driver = { + .driver = { + .name = "db8500-thermal", + .of_match_table = of_match_ptr(db8500_thermal_match), + }, + .probe = db8500_thermal_probe, + .suspend = db8500_thermal_suspend, + .resume = db8500_thermal_resume, + .remove = db8500_thermal_remove, +}; + +module_platform_driver(db8500_thermal_driver); + +MODULE_AUTHOR("Hongbo Zhang <hongbo.zhang@stericsson.com>"); +MODULE_DESCRIPTION("DB8500 thermal driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/thermal/devfreq_cooling.c b/drivers/thermal/devfreq_cooling.c new file mode 100644 index 000000000..ef5925688 --- /dev/null +++ b/drivers/thermal/devfreq_cooling.c @@ -0,0 +1,608 @@ +/* + * devfreq_cooling: Thermal cooling device implementation for devices using + * devfreq + * + * Copyright (C) 2014-2015 ARM Limited + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * TODO: + * - If OPPs are added or removed after devfreq cooling has + * registered, the devfreq cooling won't react to it. + */ + +#include <linux/devfreq.h> +#include <linux/devfreq_cooling.h> +#include <linux/export.h> +#include <linux/idr.h> +#include <linux/slab.h> +#include <linux/pm_opp.h> +#include <linux/thermal.h> + +#include <trace/events/thermal.h> + +#define SCALE_ERROR_MITIGATION 100 + +static DEFINE_IDA(devfreq_ida); + +/** + * struct devfreq_cooling_device - Devfreq cooling device + * @id: unique integer value corresponding to each + * devfreq_cooling_device registered. + * @cdev: Pointer to associated thermal cooling device. + * @devfreq: Pointer to associated devfreq device. + * @cooling_state: Current cooling state. + * @power_table: Pointer to table with maximum power draw for each + * cooling state. State is the index into the table, and + * the power is in mW. + * @freq_table: Pointer to a table with the frequencies sorted in descending + * order. You can index the table by cooling device state + * @freq_table_size: Size of the @freq_table and @power_table + * @power_ops: Pointer to devfreq_cooling_power, used to generate the + * @power_table. + * @res_util: Resource utilization scaling factor for the power. + * It is multiplied by 100 to minimize the error. It is used + * for estimation of the power budget instead of using + * 'utilization' (which is 'busy_time / 'total_time'). + * The 'res_util' range is from 100 to (power_table[state] * 100) + * for the corresponding 'state'. + */ +struct devfreq_cooling_device { + int id; + struct thermal_cooling_device *cdev; + struct devfreq *devfreq; + unsigned long cooling_state; + u32 *power_table; + u32 *freq_table; + size_t freq_table_size; + struct devfreq_cooling_power *power_ops; + u32 res_util; + int capped_state; +}; + +/** + * partition_enable_opps() - disable all opps above a given state + * @dfc: Pointer to devfreq we are operating on + * @cdev_state: cooling device state we're setting + * + * Go through the OPPs of the device, enabling all OPPs until + * @cdev_state and disabling those frequencies above it. + */ +static int partition_enable_opps(struct devfreq_cooling_device *dfc, + unsigned long cdev_state) +{ + int i; + struct device *dev = dfc->devfreq->dev.parent; + + for (i = 0; i < dfc->freq_table_size; i++) { + struct dev_pm_opp *opp; + int ret = 0; + unsigned int freq = dfc->freq_table[i]; + bool want_enable = i >= cdev_state ? true : false; + + opp = dev_pm_opp_find_freq_exact(dev, freq, !want_enable); + + if (PTR_ERR(opp) == -ERANGE) + continue; + else if (IS_ERR(opp)) + return PTR_ERR(opp); + + dev_pm_opp_put(opp); + + if (want_enable) + ret = dev_pm_opp_enable(dev, freq); + else + ret = dev_pm_opp_disable(dev, freq); + + if (ret) + return ret; + } + + return 0; +} + +static int devfreq_cooling_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct devfreq_cooling_device *dfc = cdev->devdata; + + *state = dfc->freq_table_size - 1; + + return 0; +} + +static int devfreq_cooling_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct devfreq_cooling_device *dfc = cdev->devdata; + + *state = dfc->cooling_state; + + return 0; +} + +static int devfreq_cooling_set_cur_state(struct thermal_cooling_device *cdev, + unsigned long state) +{ + struct devfreq_cooling_device *dfc = cdev->devdata; + struct devfreq *df = dfc->devfreq; + struct device *dev = df->dev.parent; + int ret; + + if (state == dfc->cooling_state) + return 0; + + dev_dbg(dev, "Setting cooling state %lu\n", state); + + if (state >= dfc->freq_table_size) + return -EINVAL; + + ret = partition_enable_opps(dfc, state); + if (ret) + return ret; + + dfc->cooling_state = state; + + return 0; +} + +/** + * freq_get_state() - get the cooling state corresponding to a frequency + * @dfc: Pointer to devfreq cooling device + * @freq: frequency in Hz + * + * Return: the cooling state associated with the @freq, or + * THERMAL_CSTATE_INVALID if it wasn't found. + */ +static unsigned long +freq_get_state(struct devfreq_cooling_device *dfc, unsigned long freq) +{ + int i; + + for (i = 0; i < dfc->freq_table_size; i++) { + if (dfc->freq_table[i] == freq) + return i; + } + + return THERMAL_CSTATE_INVALID; +} + +static unsigned long get_voltage(struct devfreq *df, unsigned long freq) +{ + struct device *dev = df->dev.parent; + unsigned long voltage; + struct dev_pm_opp *opp; + + opp = dev_pm_opp_find_freq_exact(dev, freq, true); + if (PTR_ERR(opp) == -ERANGE) + opp = dev_pm_opp_find_freq_exact(dev, freq, false); + + if (IS_ERR(opp)) { + dev_err_ratelimited(dev, "Failed to find OPP for frequency %lu: %ld\n", + freq, PTR_ERR(opp)); + return 0; + } + + voltage = dev_pm_opp_get_voltage(opp) / 1000; /* mV */ + dev_pm_opp_put(opp); + + if (voltage == 0) { + dev_err_ratelimited(dev, + "Failed to get voltage for frequency %lu\n", + freq); + } + + return voltage; +} + +/** + * get_static_power() - calculate the static power + * @dfc: Pointer to devfreq cooling device + * @freq: Frequency in Hz + * + * Calculate the static power in milliwatts using the supplied + * get_static_power(). The current voltage is calculated using the + * OPP library. If no get_static_power() was supplied, assume the + * static power is negligible. + */ +static unsigned long +get_static_power(struct devfreq_cooling_device *dfc, unsigned long freq) +{ + struct devfreq *df = dfc->devfreq; + unsigned long voltage; + + if (!dfc->power_ops->get_static_power) + return 0; + + voltage = get_voltage(df, freq); + + if (voltage == 0) + return 0; + + return dfc->power_ops->get_static_power(df, voltage); +} + +/** + * get_dynamic_power - calculate the dynamic power + * @dfc: Pointer to devfreq cooling device + * @freq: Frequency in Hz + * @voltage: Voltage in millivolts + * + * Calculate the dynamic power in milliwatts consumed by the device at + * frequency @freq and voltage @voltage. If the get_dynamic_power() + * was supplied as part of the devfreq_cooling_power struct, then that + * function is used. Otherwise, a simple power model (Pdyn = Coeff * + * Voltage^2 * Frequency) is used. + */ +static unsigned long +get_dynamic_power(struct devfreq_cooling_device *dfc, unsigned long freq, + unsigned long voltage) +{ + u64 power; + u32 freq_mhz; + struct devfreq_cooling_power *dfc_power = dfc->power_ops; + + if (dfc_power->get_dynamic_power) + return dfc_power->get_dynamic_power(dfc->devfreq, freq, + voltage); + + freq_mhz = freq / 1000000; + power = (u64)dfc_power->dyn_power_coeff * freq_mhz * voltage * voltage; + do_div(power, 1000000000); + + return power; +} + + +static inline unsigned long get_total_power(struct devfreq_cooling_device *dfc, + unsigned long freq, + unsigned long voltage) +{ + return get_static_power(dfc, freq) + get_dynamic_power(dfc, freq, + voltage); +} + + +static int devfreq_cooling_get_requested_power(struct thermal_cooling_device *cdev, + struct thermal_zone_device *tz, + u32 *power) +{ + struct devfreq_cooling_device *dfc = cdev->devdata; + struct devfreq *df = dfc->devfreq; + struct devfreq_dev_status *status = &df->last_status; + unsigned long state; + unsigned long freq = status->current_frequency; + unsigned long voltage; + u32 dyn_power = 0; + u32 static_power = 0; + int res; + + state = freq_get_state(dfc, freq); + if (state == THERMAL_CSTATE_INVALID) { + res = -EAGAIN; + goto fail; + } + + if (dfc->power_ops->get_real_power) { + voltage = get_voltage(df, freq); + if (voltage == 0) { + res = -EINVAL; + goto fail; + } + + res = dfc->power_ops->get_real_power(df, power, freq, voltage); + if (!res) { + state = dfc->capped_state; + dfc->res_util = dfc->power_table[state]; + dfc->res_util *= SCALE_ERROR_MITIGATION; + + if (*power > 1) + dfc->res_util /= *power; + } else { + goto fail; + } + } else { + dyn_power = dfc->power_table[state]; + + /* Scale dynamic power for utilization */ + dyn_power *= status->busy_time; + dyn_power /= status->total_time; + /* Get static power */ + static_power = get_static_power(dfc, freq); + + *power = dyn_power + static_power; + } + + trace_thermal_power_devfreq_get_power(cdev, status, freq, dyn_power, + static_power, *power); + + return 0; +fail: + /* It is safe to set max in this case */ + dfc->res_util = SCALE_ERROR_MITIGATION; + return res; +} + +static int devfreq_cooling_state2power(struct thermal_cooling_device *cdev, + struct thermal_zone_device *tz, + unsigned long state, + u32 *power) +{ + struct devfreq_cooling_device *dfc = cdev->devdata; + unsigned long freq; + u32 static_power; + + if (state >= dfc->freq_table_size) + return -EINVAL; + + freq = dfc->freq_table[state]; + static_power = get_static_power(dfc, freq); + + *power = dfc->power_table[state] + static_power; + return 0; +} + +static int devfreq_cooling_power2state(struct thermal_cooling_device *cdev, + struct thermal_zone_device *tz, + u32 power, unsigned long *state) +{ + struct devfreq_cooling_device *dfc = cdev->devdata; + struct devfreq *df = dfc->devfreq; + struct devfreq_dev_status *status = &df->last_status; + unsigned long freq = status->current_frequency; + unsigned long busy_time; + s32 dyn_power; + u32 static_power; + s32 est_power; + int i; + + if (dfc->power_ops->get_real_power) { + /* Scale for resource utilization */ + est_power = power * dfc->res_util; + est_power /= SCALE_ERROR_MITIGATION; + } else { + static_power = get_static_power(dfc, freq); + + dyn_power = power - static_power; + dyn_power = dyn_power > 0 ? dyn_power : 0; + + /* Scale dynamic power for utilization */ + busy_time = status->busy_time ?: 1; + est_power = (dyn_power * status->total_time) / busy_time; + } + + /* + * Find the first cooling state that is within the power + * budget for dynamic power. + */ + for (i = 0; i < dfc->freq_table_size - 1; i++) + if (est_power >= dfc->power_table[i]) + break; + + *state = i; + dfc->capped_state = i; + trace_thermal_power_devfreq_limit(cdev, freq, *state, power); + return 0; +} + +static struct thermal_cooling_device_ops devfreq_cooling_ops = { + .get_max_state = devfreq_cooling_get_max_state, + .get_cur_state = devfreq_cooling_get_cur_state, + .set_cur_state = devfreq_cooling_set_cur_state, +}; + +/** + * devfreq_cooling_gen_tables() - Generate power and freq tables. + * @dfc: Pointer to devfreq cooling device. + * + * Generate power and frequency tables: the power table hold the + * device's maximum power usage at each cooling state (OPP). The + * static and dynamic power using the appropriate voltage and + * frequency for the state, is acquired from the struct + * devfreq_cooling_power, and summed to make the maximum power draw. + * + * The frequency table holds the frequencies in descending order. + * That way its indexed by cooling device state. + * + * The tables are malloced, and pointers put in dfc. They must be + * freed when unregistering the devfreq cooling device. + * + * Return: 0 on success, negative error code on failure. + */ +static int devfreq_cooling_gen_tables(struct devfreq_cooling_device *dfc) +{ + struct devfreq *df = dfc->devfreq; + struct device *dev = df->dev.parent; + int ret, num_opps; + unsigned long freq; + u32 *power_table = NULL; + u32 *freq_table; + int i; + + num_opps = dev_pm_opp_get_opp_count(dev); + + if (dfc->power_ops) { + power_table = kcalloc(num_opps, sizeof(*power_table), + GFP_KERNEL); + if (!power_table) + return -ENOMEM; + } + + freq_table = kcalloc(num_opps, sizeof(*freq_table), + GFP_KERNEL); + if (!freq_table) { + ret = -ENOMEM; + goto free_power_table; + } + + for (i = 0, freq = ULONG_MAX; i < num_opps; i++, freq--) { + unsigned long power, voltage; + struct dev_pm_opp *opp; + + opp = dev_pm_opp_find_freq_floor(dev, &freq); + if (IS_ERR(opp)) { + ret = PTR_ERR(opp); + goto free_tables; + } + + voltage = dev_pm_opp_get_voltage(opp) / 1000; /* mV */ + dev_pm_opp_put(opp); + + if (dfc->power_ops) { + if (dfc->power_ops->get_real_power) + power = get_total_power(dfc, freq, voltage); + else + power = get_dynamic_power(dfc, freq, voltage); + + dev_dbg(dev, "Power table: %lu MHz @ %lu mV: %lu = %lu mW\n", + freq / 1000000, voltage, power, power); + + power_table[i] = power; + } + + freq_table[i] = freq; + } + + if (dfc->power_ops) + dfc->power_table = power_table; + + dfc->freq_table = freq_table; + dfc->freq_table_size = num_opps; + + return 0; + +free_tables: + kfree(freq_table); +free_power_table: + kfree(power_table); + + return ret; +} + +/** + * of_devfreq_cooling_register_power() - Register devfreq cooling device, + * with OF and power information. + * @np: Pointer to OF device_node. + * @df: Pointer to devfreq device. + * @dfc_power: Pointer to devfreq_cooling_power. + * + * Register a devfreq cooling device. The available OPPs must be + * registered on the device. + * + * If @dfc_power is provided, the cooling device is registered with the + * power extensions. For the power extensions to work correctly, + * devfreq should use the simple_ondemand governor, other governors + * are not currently supported. + */ +struct thermal_cooling_device * +of_devfreq_cooling_register_power(struct device_node *np, struct devfreq *df, + struct devfreq_cooling_power *dfc_power) +{ + struct thermal_cooling_device *cdev; + struct devfreq_cooling_device *dfc; + char dev_name[THERMAL_NAME_LENGTH]; + int err; + + dfc = kzalloc(sizeof(*dfc), GFP_KERNEL); + if (!dfc) + return ERR_PTR(-ENOMEM); + + dfc->devfreq = df; + + if (dfc_power) { + dfc->power_ops = dfc_power; + + devfreq_cooling_ops.get_requested_power = + devfreq_cooling_get_requested_power; + devfreq_cooling_ops.state2power = devfreq_cooling_state2power; + devfreq_cooling_ops.power2state = devfreq_cooling_power2state; + } + + err = devfreq_cooling_gen_tables(dfc); + if (err) + goto free_dfc; + + err = ida_simple_get(&devfreq_ida, 0, 0, GFP_KERNEL); + if (err < 0) + goto free_tables; + dfc->id = err; + + snprintf(dev_name, sizeof(dev_name), "thermal-devfreq-%d", dfc->id); + + cdev = thermal_of_cooling_device_register(np, dev_name, dfc, + &devfreq_cooling_ops); + if (IS_ERR(cdev)) { + err = PTR_ERR(cdev); + dev_err(df->dev.parent, + "Failed to register devfreq cooling device (%d)\n", + err); + goto release_ida; + } + + dfc->cdev = cdev; + + return cdev; + +release_ida: + ida_simple_remove(&devfreq_ida, dfc->id); +free_tables: + kfree(dfc->power_table); + kfree(dfc->freq_table); +free_dfc: + kfree(dfc); + + return ERR_PTR(err); +} +EXPORT_SYMBOL_GPL(of_devfreq_cooling_register_power); + +/** + * of_devfreq_cooling_register() - Register devfreq cooling device, + * with OF information. + * @np: Pointer to OF device_node. + * @df: Pointer to devfreq device. + */ +struct thermal_cooling_device * +of_devfreq_cooling_register(struct device_node *np, struct devfreq *df) +{ + return of_devfreq_cooling_register_power(np, df, NULL); +} +EXPORT_SYMBOL_GPL(of_devfreq_cooling_register); + +/** + * devfreq_cooling_register() - Register devfreq cooling device. + * @df: Pointer to devfreq device. + */ +struct thermal_cooling_device *devfreq_cooling_register(struct devfreq *df) +{ + return of_devfreq_cooling_register(NULL, df); +} +EXPORT_SYMBOL_GPL(devfreq_cooling_register); + +/** + * devfreq_cooling_unregister() - Unregister devfreq cooling device. + * @dfc: Pointer to devfreq cooling device to unregister. + */ +void devfreq_cooling_unregister(struct thermal_cooling_device *cdev) +{ + struct devfreq_cooling_device *dfc; + + if (!cdev) + return; + + dfc = cdev->devdata; + + thermal_cooling_device_unregister(dfc->cdev); + ida_simple_remove(&devfreq_ida, dfc->id); + kfree(dfc->power_table); + kfree(dfc->freq_table); + + kfree(dfc); +} +EXPORT_SYMBOL_GPL(devfreq_cooling_unregister); diff --git a/drivers/thermal/dove_thermal.c b/drivers/thermal/dove_thermal.c new file mode 100644 index 000000000..a0bc9de42 --- /dev/null +++ b/drivers/thermal/dove_thermal.c @@ -0,0 +1,195 @@ +/* + * Dove thermal sensor driver + * + * Copyright (C) 2013 Andrew Lunn <andrew@lunn.ch> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#include <linux/device.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/of.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/thermal.h> + +#define DOVE_THERMAL_TEMP_OFFSET 1 +#define DOVE_THERMAL_TEMP_MASK 0x1FF + +/* Dove Thermal Manager Control and Status Register */ +#define PMU_TM_DISABLE_OFFS 0 +#define PMU_TM_DISABLE_MASK (0x1 << PMU_TM_DISABLE_OFFS) + +/* Dove Theraml Diode Control 0 Register */ +#define PMU_TDC0_SW_RST_MASK (0x1 << 1) +#define PMU_TDC0_SEL_VCAL_OFFS 5 +#define PMU_TDC0_SEL_VCAL_MASK (0x3 << PMU_TDC0_SEL_VCAL_OFFS) +#define PMU_TDC0_REF_CAL_CNT_OFFS 11 +#define PMU_TDC0_REF_CAL_CNT_MASK (0x1FF << PMU_TDC0_REF_CAL_CNT_OFFS) +#define PMU_TDC0_AVG_NUM_OFFS 25 +#define PMU_TDC0_AVG_NUM_MASK (0x7 << PMU_TDC0_AVG_NUM_OFFS) + +/* Dove Thermal Diode Control 1 Register */ +#define PMU_TEMP_DIOD_CTRL1_REG 0x04 +#define PMU_TDC1_TEMP_VALID_MASK (0x1 << 10) + +/* Dove Thermal Sensor Dev Structure */ +struct dove_thermal_priv { + void __iomem *sensor; + void __iomem *control; +}; + +static int dove_init_sensor(const struct dove_thermal_priv *priv) +{ + u32 reg; + u32 i; + + /* Configure the Diode Control Register #0 */ + reg = readl_relaxed(priv->control); + + /* Use average of 2 */ + reg &= ~PMU_TDC0_AVG_NUM_MASK; + reg |= (0x1 << PMU_TDC0_AVG_NUM_OFFS); + + /* Reference calibration value */ + reg &= ~PMU_TDC0_REF_CAL_CNT_MASK; + reg |= (0x0F1 << PMU_TDC0_REF_CAL_CNT_OFFS); + + /* Set the high level reference for calibration */ + reg &= ~PMU_TDC0_SEL_VCAL_MASK; + reg |= (0x2 << PMU_TDC0_SEL_VCAL_OFFS); + writel(reg, priv->control); + + /* Reset the sensor */ + reg = readl_relaxed(priv->control); + writel((reg | PMU_TDC0_SW_RST_MASK), priv->control); + writel(reg, priv->control); + + /* Enable the sensor */ + reg = readl_relaxed(priv->sensor); + reg &= ~PMU_TM_DISABLE_MASK; + writel(reg, priv->sensor); + + /* Poll the sensor for the first reading */ + for (i = 0; i < 1000000; i++) { + reg = readl_relaxed(priv->sensor); + if (reg & DOVE_THERMAL_TEMP_MASK) + break; + } + + if (i == 1000000) + return -EIO; + + return 0; +} + +static int dove_get_temp(struct thermal_zone_device *thermal, + int *temp) +{ + unsigned long reg; + struct dove_thermal_priv *priv = thermal->devdata; + + /* Valid check */ + reg = readl_relaxed(priv->control + PMU_TEMP_DIOD_CTRL1_REG); + if ((reg & PMU_TDC1_TEMP_VALID_MASK) == 0x0) { + dev_err(&thermal->device, + "Temperature sensor reading not valid\n"); + return -EIO; + } + + /* + * Calculate temperature. According to Marvell internal + * documentation the formula for this is: + * Celsius = (322-reg)/1.3625 + */ + reg = readl_relaxed(priv->sensor); + reg = (reg >> DOVE_THERMAL_TEMP_OFFSET) & DOVE_THERMAL_TEMP_MASK; + *temp = ((3220000000UL - (10000000UL * reg)) / 13625); + + return 0; +} + +static struct thermal_zone_device_ops ops = { + .get_temp = dove_get_temp, +}; + +static const struct of_device_id dove_thermal_id_table[] = { + { .compatible = "marvell,dove-thermal" }, + {} +}; + +static int dove_thermal_probe(struct platform_device *pdev) +{ + struct thermal_zone_device *thermal = NULL; + struct dove_thermal_priv *priv; + struct resource *res; + int ret; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + priv->sensor = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(priv->sensor)) + return PTR_ERR(priv->sensor); + + res = platform_get_resource(pdev, IORESOURCE_MEM, 1); + priv->control = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(priv->control)) + return PTR_ERR(priv->control); + + ret = dove_init_sensor(priv); + if (ret) { + dev_err(&pdev->dev, "Failed to initialize sensor\n"); + return ret; + } + + thermal = thermal_zone_device_register("dove_thermal", 0, 0, + priv, &ops, NULL, 0, 0); + if (IS_ERR(thermal)) { + dev_err(&pdev->dev, + "Failed to register thermal zone device\n"); + return PTR_ERR(thermal); + } + + platform_set_drvdata(pdev, thermal); + + return 0; +} + +static int dove_thermal_exit(struct platform_device *pdev) +{ + struct thermal_zone_device *dove_thermal = + platform_get_drvdata(pdev); + + thermal_zone_device_unregister(dove_thermal); + + return 0; +} + +MODULE_DEVICE_TABLE(of, dove_thermal_id_table); + +static struct platform_driver dove_thermal_driver = { + .probe = dove_thermal_probe, + .remove = dove_thermal_exit, + .driver = { + .name = "dove_thermal", + .of_match_table = dove_thermal_id_table, + }, +}; + +module_platform_driver(dove_thermal_driver); + +MODULE_AUTHOR("Andrew Lunn <andrew@lunn.ch>"); +MODULE_DESCRIPTION("Dove thermal driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/thermal/fair_share.c b/drivers/thermal/fair_share.c new file mode 100644 index 000000000..26d7387f5 --- /dev/null +++ b/drivers/thermal/fair_share.c @@ -0,0 +1,146 @@ +/* + * fair_share.c - A simple weight based Thermal governor + * + * Copyright (C) 2012 Intel Corp + * Copyright (C) 2012 Durgadoss R <durgadoss.r@intel.com> + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include <linux/thermal.h> +#include <trace/events/thermal.h> + +#include "thermal_core.h" + +/** + * get_trip_level: - obtains the current trip level for a zone + * @tz: thermal zone device + */ +static int get_trip_level(struct thermal_zone_device *tz) +{ + int count = 0; + int trip_temp; + enum thermal_trip_type trip_type; + + if (tz->trips == 0 || !tz->ops->get_trip_temp) + return 0; + + for (count = 0; count < tz->trips; count++) { + tz->ops->get_trip_temp(tz, count, &trip_temp); + if (tz->temperature < trip_temp) + break; + } + + /* + * count > 0 only if temperature is greater than first trip + * point, in which case, trip_point = count - 1 + */ + if (count > 0) { + tz->ops->get_trip_type(tz, count - 1, &trip_type); + trace_thermal_zone_trip(tz, count - 1, trip_type); + } + + return count; +} + +static long get_target_state(struct thermal_zone_device *tz, + struct thermal_cooling_device *cdev, int percentage, int level) +{ + unsigned long max_state; + + cdev->ops->get_max_state(cdev, &max_state); + + return (long)(percentage * level * max_state) / (100 * tz->trips); +} + +/** + * fair_share_throttle - throttles devices associated with the given zone + * @tz - thermal_zone_device + * @trip - trip point index + * + * Throttling Logic: This uses three parameters to calculate the new + * throttle state of the cooling devices associated with the given zone. + * + * Parameters used for Throttling: + * P1. max_state: Maximum throttle state exposed by the cooling device. + * P2. percentage[i]/100: + * How 'effective' the 'i'th device is, in cooling the given zone. + * P3. cur_trip_level/max_no_of_trips: + * This describes the extent to which the devices should be throttled. + * We do not want to throttle too much when we trip a lower temperature, + * whereas the throttling is at full swing if we trip critical levels. + * (Heavily assumes the trip points are in ascending order) + * new_state of cooling device = P3 * P2 * P1 + */ +static int fair_share_throttle(struct thermal_zone_device *tz, int trip) +{ + struct thermal_instance *instance; + int total_weight = 0; + int total_instance = 0; + int cur_trip_level = get_trip_level(tz); + + mutex_lock(&tz->lock); + + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + if (instance->trip != trip) + continue; + + total_weight += instance->weight; + total_instance++; + } + + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + int percentage; + struct thermal_cooling_device *cdev = instance->cdev; + + if (instance->trip != trip) + continue; + + if (!total_weight) + percentage = 100 / total_instance; + else + percentage = (instance->weight * 100) / total_weight; + + instance->target = get_target_state(tz, cdev, percentage, + cur_trip_level); + + mutex_lock(&instance->cdev->lock); + instance->cdev->updated = false; + mutex_unlock(&instance->cdev->lock); + thermal_cdev_update(cdev); + } + + mutex_unlock(&tz->lock); + return 0; +} + +static struct thermal_governor thermal_gov_fair_share = { + .name = "fair_share", + .throttle = fair_share_throttle, +}; + +int thermal_gov_fair_share_register(void) +{ + return thermal_register_governor(&thermal_gov_fair_share); +} + +void thermal_gov_fair_share_unregister(void) +{ + thermal_unregister_governor(&thermal_gov_fair_share); +} + diff --git a/drivers/thermal/gov_bang_bang.c b/drivers/thermal/gov_bang_bang.c new file mode 100644 index 000000000..fc5e5057f --- /dev/null +++ b/drivers/thermal/gov_bang_bang.c @@ -0,0 +1,138 @@ +/* + * gov_bang_bang.c - A simple thermal throttling governor using hysteresis + * + * Copyright (C) 2014 Peter Feuerer <peter@piie.net> + * + * Based on step_wise.c with following Copyrights: + * Copyright (C) 2012 Intel Corp + * Copyright (C) 2012 Durgadoss R <durgadoss.r@intel.com> + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See + * the GNU General Public License for more details. + * + */ + +#include <linux/thermal.h> + +#include "thermal_core.h" + +static void thermal_zone_trip_update(struct thermal_zone_device *tz, int trip) +{ + int trip_temp, trip_hyst; + struct thermal_instance *instance; + + tz->ops->get_trip_temp(tz, trip, &trip_temp); + + if (!tz->ops->get_trip_hyst) { + pr_warn_once("Undefined get_trip_hyst for thermal zone %s - " + "running with default hysteresis zero\n", tz->type); + trip_hyst = 0; + } else + tz->ops->get_trip_hyst(tz, trip, &trip_hyst); + + dev_dbg(&tz->device, "Trip%d[temp=%d]:temp=%d:hyst=%d\n", + trip, trip_temp, tz->temperature, + trip_hyst); + + mutex_lock(&tz->lock); + + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + if (instance->trip != trip) + continue; + + /* in case fan is in initial state, switch the fan off */ + if (instance->target == THERMAL_NO_TARGET) + instance->target = 0; + + /* in case fan is neither on nor off set the fan to active */ + if (instance->target != 0 && instance->target != 1) { + pr_warn("Thermal instance %s controlled by bang-bang has unexpected state: %ld\n", + instance->name, instance->target); + instance->target = 1; + } + + /* + * enable fan when temperature exceeds trip_temp and disable + * the fan in case it falls below trip_temp minus hysteresis + */ + if (instance->target == 0 && tz->temperature >= trip_temp) + instance->target = 1; + else if (instance->target == 1 && + tz->temperature <= trip_temp - trip_hyst) + instance->target = 0; + + dev_dbg(&instance->cdev->device, "target=%d\n", + (int)instance->target); + + mutex_lock(&instance->cdev->lock); + instance->cdev->updated = false; /* cdev needs update */ + mutex_unlock(&instance->cdev->lock); + } + + mutex_unlock(&tz->lock); +} + +/** + * bang_bang_control - controls devices associated with the given zone + * @tz - thermal_zone_device + * @trip - the trip point + * + * Regulation Logic: a two point regulation, deliver cooling state depending + * on the previous state shown in this diagram: + * + * Fan: OFF ON + * + * | + * | + * trip_temp: +---->+ + * | | ^ + * | | | + * | | Temperature + * (trip_temp - hyst): +<----+ + * | + * | + * | + * + * * If the fan is not running and temperature exceeds trip_temp, the fan + * gets turned on. + * * In case the fan is running, temperature must fall below + * (trip_temp - hyst) so that the fan gets turned off again. + * + */ +static int bang_bang_control(struct thermal_zone_device *tz, int trip) +{ + struct thermal_instance *instance; + + thermal_zone_trip_update(tz, trip); + + mutex_lock(&tz->lock); + + list_for_each_entry(instance, &tz->thermal_instances, tz_node) + thermal_cdev_update(instance->cdev); + + mutex_unlock(&tz->lock); + + return 0; +} + +static struct thermal_governor thermal_gov_bang_bang = { + .name = "bang_bang", + .throttle = bang_bang_control, +}; + +int thermal_gov_bang_bang_register(void) +{ + return thermal_register_governor(&thermal_gov_bang_bang); +} + +void thermal_gov_bang_bang_unregister(void) +{ + thermal_unregister_governor(&thermal_gov_bang_bang); +} diff --git a/drivers/thermal/hisi_thermal.c b/drivers/thermal/hisi_thermal.c new file mode 100644 index 000000000..761d0559c --- /dev/null +++ b/drivers/thermal/hisi_thermal.c @@ -0,0 +1,627 @@ +/* + * Hisilicon thermal sensor driver + * + * Copyright (c) 2014-2015 Hisilicon Limited. + * Copyright (c) 2014-2015 Linaro Limited. + * + * Xinwei Kong <kong.kongxinwei@hisilicon.com> + * Leo Yan <leo.yan@linaro.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/cpufreq.h> +#include <linux/delay.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/io.h> +#include <linux/of_device.h> + +#include "thermal_core.h" + +#define HI6220_TEMP0_LAG (0x0) +#define HI6220_TEMP0_TH (0x4) +#define HI6220_TEMP0_RST_TH (0x8) +#define HI6220_TEMP0_CFG (0xC) +#define HI6220_TEMP0_CFG_SS_MSK (0xF000) +#define HI6220_TEMP0_CFG_HDAK_MSK (0x30) +#define HI6220_TEMP0_EN (0x10) +#define HI6220_TEMP0_INT_EN (0x14) +#define HI6220_TEMP0_INT_CLR (0x18) +#define HI6220_TEMP0_RST_MSK (0x1C) +#define HI6220_TEMP0_VALUE (0x28) + +#define HI3660_OFFSET(chan) ((chan) * 0x40) +#define HI3660_TEMP(chan) (HI3660_OFFSET(chan) + 0x1C) +#define HI3660_TH(chan) (HI3660_OFFSET(chan) + 0x20) +#define HI3660_LAG(chan) (HI3660_OFFSET(chan) + 0x28) +#define HI3660_INT_EN(chan) (HI3660_OFFSET(chan) + 0x2C) +#define HI3660_INT_CLR(chan) (HI3660_OFFSET(chan) + 0x30) + +#define HI6220_TEMP_BASE (-60000) +#define HI6220_TEMP_RESET (100000) +#define HI6220_TEMP_STEP (785) +#define HI6220_TEMP_LAG (3500) + +#define HI3660_TEMP_BASE (-63780) +#define HI3660_TEMP_STEP (205) +#define HI3660_TEMP_LAG (4000) + +#define HI6220_DEFAULT_SENSOR 2 +#define HI3660_DEFAULT_SENSOR 1 + +struct hisi_thermal_sensor { + struct thermal_zone_device *tzd; + uint32_t id; + uint32_t thres_temp; +}; + +struct hisi_thermal_data { + int (*get_temp)(struct hisi_thermal_data *data); + int (*enable_sensor)(struct hisi_thermal_data *data); + int (*disable_sensor)(struct hisi_thermal_data *data); + int (*irq_handler)(struct hisi_thermal_data *data); + struct platform_device *pdev; + struct clk *clk; + struct hisi_thermal_sensor sensor; + void __iomem *regs; + int irq; +}; + +/* + * The temperature computation on the tsensor is as follow: + * Unit: millidegree Celsius + * Step: 200/255 (0.7843) + * Temperature base: -60°C + * + * The register is programmed in temperature steps, every step is 785 + * millidegree and begins at -60 000 m°C + * + * The temperature from the steps: + * + * Temp = TempBase + (steps x 785) + * + * and the steps from the temperature: + * + * steps = (Temp - TempBase) / 785 + * + */ +static inline int hi6220_thermal_step_to_temp(int step) +{ + return HI6220_TEMP_BASE + (step * HI6220_TEMP_STEP); +} + +static inline int hi6220_thermal_temp_to_step(int temp) +{ + return DIV_ROUND_UP(temp - HI6220_TEMP_BASE, HI6220_TEMP_STEP); +} + +/* + * for Hi3660, + * Step: 189/922 (0.205) + * Temperature base: -63.780°C + * + * The register is programmed in temperature steps, every step is 205 + * millidegree and begins at -63 780 m°C + */ +static inline int hi3660_thermal_step_to_temp(int step) +{ + return HI3660_TEMP_BASE + step * HI3660_TEMP_STEP; +} + +static inline int hi3660_thermal_temp_to_step(int temp) +{ + return DIV_ROUND_UP(temp - HI3660_TEMP_BASE, HI3660_TEMP_STEP); +} + +/* + * The lag register contains 5 bits encoding the temperature in steps. + * + * Each time the temperature crosses the threshold boundary, an + * interrupt is raised. It could be when the temperature is going + * above the threshold or below. However, if the temperature is + * fluctuating around this value due to the load, we can receive + * several interrupts which may not desired. + * + * We can setup a temperature representing the delta between the + * threshold and the current temperature when the temperature is + * decreasing. + * + * For instance: the lag register is 5°C, the threshold is 65°C, when + * the temperature reaches 65°C an interrupt is raised and when the + * temperature decrease to 65°C - 5°C another interrupt is raised. + * + * A very short lag can lead to an interrupt storm, a long lag + * increase the latency to react to the temperature changes. In our + * case, that is not really a problem as we are polling the + * temperature. + * + * [0:4] : lag register + * + * The temperature is coded in steps, cf. HI6220_TEMP_STEP. + * + * Min : 0x00 : 0.0 °C + * Max : 0x1F : 24.3 °C + * + * The 'value' parameter is in milliCelsius. + */ +static inline void hi6220_thermal_set_lag(void __iomem *addr, int value) +{ + writel(DIV_ROUND_UP(value, HI6220_TEMP_STEP) & 0x1F, + addr + HI6220_TEMP0_LAG); +} + +static inline void hi6220_thermal_alarm_clear(void __iomem *addr, int value) +{ + writel(value, addr + HI6220_TEMP0_INT_CLR); +} + +static inline void hi6220_thermal_alarm_enable(void __iomem *addr, int value) +{ + writel(value, addr + HI6220_TEMP0_INT_EN); +} + +static inline void hi6220_thermal_alarm_set(void __iomem *addr, int temp) +{ + writel(hi6220_thermal_temp_to_step(temp) | 0x0FFFFFF00, + addr + HI6220_TEMP0_TH); +} + +static inline void hi6220_thermal_reset_set(void __iomem *addr, int temp) +{ + writel(hi6220_thermal_temp_to_step(temp), addr + HI6220_TEMP0_RST_TH); +} + +static inline void hi6220_thermal_reset_enable(void __iomem *addr, int value) +{ + writel(value, addr + HI6220_TEMP0_RST_MSK); +} + +static inline void hi6220_thermal_enable(void __iomem *addr, int value) +{ + writel(value, addr + HI6220_TEMP0_EN); +} + +static inline int hi6220_thermal_get_temperature(void __iomem *addr) +{ + return hi6220_thermal_step_to_temp(readl(addr + HI6220_TEMP0_VALUE)); +} + +/* + * [0:6] lag register + * + * The temperature is coded in steps, cf. HI3660_TEMP_STEP. + * + * Min : 0x00 : 0.0 °C + * Max : 0x7F : 26.0 °C + * + */ +static inline void hi3660_thermal_set_lag(void __iomem *addr, + int id, int value) +{ + writel(DIV_ROUND_UP(value, HI3660_TEMP_STEP) & 0x7F, + addr + HI3660_LAG(id)); +} + +static inline void hi3660_thermal_alarm_clear(void __iomem *addr, + int id, int value) +{ + writel(value, addr + HI3660_INT_CLR(id)); +} + +static inline void hi3660_thermal_alarm_enable(void __iomem *addr, + int id, int value) +{ + writel(value, addr + HI3660_INT_EN(id)); +} + +static inline void hi3660_thermal_alarm_set(void __iomem *addr, + int id, int value) +{ + writel(value, addr + HI3660_TH(id)); +} + +static inline int hi3660_thermal_get_temperature(void __iomem *addr, int id) +{ + return hi3660_thermal_step_to_temp(readl(addr + HI3660_TEMP(id))); +} + +/* + * Temperature configuration register - Sensor selection + * + * Bits [19:12] + * + * 0x0: local sensor (default) + * 0x1: remote sensor 1 (ACPU cluster 1) + * 0x2: remote sensor 2 (ACPU cluster 0) + * 0x3: remote sensor 3 (G3D) + */ +static inline void hi6220_thermal_sensor_select(void __iomem *addr, int sensor) +{ + writel((readl(addr + HI6220_TEMP0_CFG) & ~HI6220_TEMP0_CFG_SS_MSK) | + (sensor << 12), addr + HI6220_TEMP0_CFG); +} + +/* + * Temperature configuration register - Hdak conversion polling interval + * + * Bits [5:4] + * + * 0x0 : 0.768 ms + * 0x1 : 6.144 ms + * 0x2 : 49.152 ms + * 0x3 : 393.216 ms + */ +static inline void hi6220_thermal_hdak_set(void __iomem *addr, int value) +{ + writel((readl(addr + HI6220_TEMP0_CFG) & ~HI6220_TEMP0_CFG_HDAK_MSK) | + (value << 4), addr + HI6220_TEMP0_CFG); +} + +static int hi6220_thermal_irq_handler(struct hisi_thermal_data *data) +{ + hi6220_thermal_alarm_clear(data->regs, 1); + return 0; +} + +static int hi3660_thermal_irq_handler(struct hisi_thermal_data *data) +{ + hi3660_thermal_alarm_clear(data->regs, data->sensor.id, 1); + return 0; +} + +static int hi6220_thermal_get_temp(struct hisi_thermal_data *data) +{ + return hi6220_thermal_get_temperature(data->regs); +} + +static int hi3660_thermal_get_temp(struct hisi_thermal_data *data) +{ + return hi3660_thermal_get_temperature(data->regs, data->sensor.id); +} + +static int hi6220_thermal_disable_sensor(struct hisi_thermal_data *data) +{ + /* disable sensor module */ + hi6220_thermal_enable(data->regs, 0); + hi6220_thermal_alarm_enable(data->regs, 0); + hi6220_thermal_reset_enable(data->regs, 0); + + clk_disable_unprepare(data->clk); + + return 0; +} + +static int hi3660_thermal_disable_sensor(struct hisi_thermal_data *data) +{ + /* disable sensor module */ + hi3660_thermal_alarm_enable(data->regs, data->sensor.id, 0); + return 0; +} + +static int hi6220_thermal_enable_sensor(struct hisi_thermal_data *data) +{ + struct hisi_thermal_sensor *sensor = &data->sensor; + int ret; + + /* enable clock for tsensor */ + ret = clk_prepare_enable(data->clk); + if (ret) + return ret; + + /* disable module firstly */ + hi6220_thermal_reset_enable(data->regs, 0); + hi6220_thermal_enable(data->regs, 0); + + /* select sensor id */ + hi6220_thermal_sensor_select(data->regs, sensor->id); + + /* setting the hdak time */ + hi6220_thermal_hdak_set(data->regs, 0); + + /* setting lag value between current temp and the threshold */ + hi6220_thermal_set_lag(data->regs, HI6220_TEMP_LAG); + + /* enable for interrupt */ + hi6220_thermal_alarm_set(data->regs, sensor->thres_temp); + + hi6220_thermal_reset_set(data->regs, HI6220_TEMP_RESET); + + /* enable module */ + hi6220_thermal_reset_enable(data->regs, 1); + hi6220_thermal_enable(data->regs, 1); + + hi6220_thermal_alarm_clear(data->regs, 0); + hi6220_thermal_alarm_enable(data->regs, 1); + + return 0; +} + +static int hi3660_thermal_enable_sensor(struct hisi_thermal_data *data) +{ + unsigned int value; + struct hisi_thermal_sensor *sensor = &data->sensor; + + /* disable interrupt */ + hi3660_thermal_alarm_enable(data->regs, sensor->id, 0); + + /* setting lag value between current temp and the threshold */ + hi3660_thermal_set_lag(data->regs, sensor->id, HI3660_TEMP_LAG); + + /* set interrupt threshold */ + value = hi3660_thermal_temp_to_step(sensor->thres_temp); + hi3660_thermal_alarm_set(data->regs, sensor->id, value); + + /* enable interrupt */ + hi3660_thermal_alarm_clear(data->regs, sensor->id, 1); + hi3660_thermal_alarm_enable(data->regs, sensor->id, 1); + + return 0; +} + +static int hi6220_thermal_probe(struct hisi_thermal_data *data) +{ + struct platform_device *pdev = data->pdev; + struct device *dev = &pdev->dev; + struct resource *res; + int ret; + + data->get_temp = hi6220_thermal_get_temp; + data->enable_sensor = hi6220_thermal_enable_sensor; + data->disable_sensor = hi6220_thermal_disable_sensor; + data->irq_handler = hi6220_thermal_irq_handler; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + data->regs = devm_ioremap_resource(dev, res); + if (IS_ERR(data->regs)) { + dev_err(dev, "failed to get io address\n"); + return PTR_ERR(data->regs); + } + + data->clk = devm_clk_get(dev, "thermal_clk"); + if (IS_ERR(data->clk)) { + ret = PTR_ERR(data->clk); + if (ret != -EPROBE_DEFER) + dev_err(dev, "failed to get thermal clk: %d\n", ret); + return ret; + } + + data->irq = platform_get_irq(pdev, 0); + if (data->irq < 0) + return data->irq; + + data->sensor.id = HI6220_DEFAULT_SENSOR; + + return 0; +} + +static int hi3660_thermal_probe(struct hisi_thermal_data *data) +{ + struct platform_device *pdev = data->pdev; + struct device *dev = &pdev->dev; + struct resource *res; + + data->get_temp = hi3660_thermal_get_temp; + data->enable_sensor = hi3660_thermal_enable_sensor; + data->disable_sensor = hi3660_thermal_disable_sensor; + data->irq_handler = hi3660_thermal_irq_handler; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + data->regs = devm_ioremap_resource(dev, res); + if (IS_ERR(data->regs)) { + dev_err(dev, "failed to get io address\n"); + return PTR_ERR(data->regs); + } + + data->irq = platform_get_irq(pdev, 0); + if (data->irq < 0) + return data->irq; + + data->sensor.id = HI3660_DEFAULT_SENSOR; + + return 0; +} + +static int hisi_thermal_get_temp(void *__data, int *temp) +{ + struct hisi_thermal_data *data = __data; + struct hisi_thermal_sensor *sensor = &data->sensor; + + *temp = data->get_temp(data); + + dev_dbg(&data->pdev->dev, "id=%d, temp=%d, thres=%d\n", + sensor->id, *temp, sensor->thres_temp); + + return 0; +} + +static const struct thermal_zone_of_device_ops hisi_of_thermal_ops = { + .get_temp = hisi_thermal_get_temp, +}; + +static irqreturn_t hisi_thermal_alarm_irq_thread(int irq, void *dev) +{ + struct hisi_thermal_data *data = dev; + struct hisi_thermal_sensor *sensor = &data->sensor; + int temp = 0; + + data->irq_handler(data); + + hisi_thermal_get_temp(data, &temp); + + if (temp >= sensor->thres_temp) { + dev_crit(&data->pdev->dev, "THERMAL ALARM: %d > %d\n", + temp, sensor->thres_temp); + + thermal_zone_device_update(data->sensor.tzd, + THERMAL_EVENT_UNSPECIFIED); + + } else { + dev_crit(&data->pdev->dev, "THERMAL ALARM stopped: %d < %d\n", + temp, sensor->thres_temp); + } + + return IRQ_HANDLED; +} + +static int hisi_thermal_register_sensor(struct platform_device *pdev, + struct hisi_thermal_data *data, + struct hisi_thermal_sensor *sensor) +{ + int ret, i; + const struct thermal_trip *trip; + + sensor->tzd = devm_thermal_zone_of_sensor_register(&pdev->dev, + sensor->id, data, + &hisi_of_thermal_ops); + if (IS_ERR(sensor->tzd)) { + ret = PTR_ERR(sensor->tzd); + sensor->tzd = NULL; + dev_err(&pdev->dev, "failed to register sensor id %d: %d\n", + sensor->id, ret); + return ret; + } + + trip = of_thermal_get_trip_points(sensor->tzd); + + for (i = 0; i < of_thermal_get_ntrips(sensor->tzd); i++) { + if (trip[i].type == THERMAL_TRIP_PASSIVE) { + sensor->thres_temp = trip[i].temperature; + break; + } + } + + return 0; +} + +static const struct of_device_id of_hisi_thermal_match[] = { + { + .compatible = "hisilicon,tsensor", + .data = hi6220_thermal_probe + }, + { + .compatible = "hisilicon,hi3660-tsensor", + .data = hi3660_thermal_probe + }, + { /* end */ } +}; +MODULE_DEVICE_TABLE(of, of_hisi_thermal_match); + +static void hisi_thermal_toggle_sensor(struct hisi_thermal_sensor *sensor, + bool on) +{ + struct thermal_zone_device *tzd = sensor->tzd; + + tzd->ops->set_mode(tzd, + on ? THERMAL_DEVICE_ENABLED : THERMAL_DEVICE_DISABLED); +} + +static int hisi_thermal_probe(struct platform_device *pdev) +{ + struct hisi_thermal_data *data; + int (*platform_probe)(struct hisi_thermal_data *); + struct device *dev = &pdev->dev; + int ret; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->pdev = pdev; + platform_set_drvdata(pdev, data); + + platform_probe = of_device_get_match_data(dev); + if (!platform_probe) { + dev_err(dev, "failed to get probe func\n"); + return -EINVAL; + } + + ret = platform_probe(data); + if (ret) + return ret; + + ret = hisi_thermal_register_sensor(pdev, data, + &data->sensor); + if (ret) { + dev_err(dev, "failed to register thermal sensor: %d\n", ret); + return ret; + } + + ret = data->enable_sensor(data); + if (ret) { + dev_err(dev, "Failed to setup the sensor: %d\n", ret); + return ret; + } + + if (data->irq) { + ret = devm_request_threaded_irq(dev, data->irq, NULL, + hisi_thermal_alarm_irq_thread, + IRQF_ONESHOT, "hisi_thermal", data); + if (ret < 0) { + dev_err(dev, "failed to request alarm irq: %d\n", ret); + return ret; + } + } + + hisi_thermal_toggle_sensor(&data->sensor, true); + + return 0; +} + +static int hisi_thermal_remove(struct platform_device *pdev) +{ + struct hisi_thermal_data *data = platform_get_drvdata(pdev); + struct hisi_thermal_sensor *sensor = &data->sensor; + + hisi_thermal_toggle_sensor(sensor, false); + + data->disable_sensor(data); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int hisi_thermal_suspend(struct device *dev) +{ + struct hisi_thermal_data *data = dev_get_drvdata(dev); + + data->disable_sensor(data); + + return 0; +} + +static int hisi_thermal_resume(struct device *dev) +{ + struct hisi_thermal_data *data = dev_get_drvdata(dev); + + return data->enable_sensor(data); +} +#endif + +static SIMPLE_DEV_PM_OPS(hisi_thermal_pm_ops, + hisi_thermal_suspend, hisi_thermal_resume); + +static struct platform_driver hisi_thermal_driver = { + .driver = { + .name = "hisi_thermal", + .pm = &hisi_thermal_pm_ops, + .of_match_table = of_hisi_thermal_match, + }, + .probe = hisi_thermal_probe, + .remove = hisi_thermal_remove, +}; + +module_platform_driver(hisi_thermal_driver); + +MODULE_AUTHOR("Xinwei Kong <kong.kongxinwei@hisilicon.com>"); +MODULE_AUTHOR("Leo Yan <leo.yan@linaro.org>"); +MODULE_DESCRIPTION("Hisilicon thermal driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/imx_thermal.c b/drivers/thermal/imx_thermal.c new file mode 100644 index 000000000..aa452acb6 --- /dev/null +++ b/drivers/thermal/imx_thermal.c @@ -0,0 +1,917 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Copyright 2013 Freescale Semiconductor, Inc. + +#include <linux/clk.h> +#include <linux/cpu.h> +#include <linux/cpufreq.h> +#include <linux/cpu_cooling.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/mfd/syscon.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/thermal.h> +#include <linux/types.h> +#include <linux/nvmem-consumer.h> + +#define REG_SET 0x4 +#define REG_CLR 0x8 +#define REG_TOG 0xc + +/* i.MX6 specific */ +#define IMX6_MISC0 0x0150 +#define IMX6_MISC0_REFTOP_SELBIASOFF (1 << 3) +#define IMX6_MISC1 0x0160 +#define IMX6_MISC1_IRQ_TEMPHIGH (1 << 29) +/* Below LOW and PANIC bits are only for TEMPMON_IMX6SX */ +#define IMX6_MISC1_IRQ_TEMPLOW (1 << 28) +#define IMX6_MISC1_IRQ_TEMPPANIC (1 << 27) + +#define IMX6_TEMPSENSE0 0x0180 +#define IMX6_TEMPSENSE0_ALARM_VALUE_SHIFT 20 +#define IMX6_TEMPSENSE0_ALARM_VALUE_MASK (0xfff << 20) +#define IMX6_TEMPSENSE0_TEMP_CNT_SHIFT 8 +#define IMX6_TEMPSENSE0_TEMP_CNT_MASK (0xfff << 8) +#define IMX6_TEMPSENSE0_FINISHED (1 << 2) +#define IMX6_TEMPSENSE0_MEASURE_TEMP (1 << 1) +#define IMX6_TEMPSENSE0_POWER_DOWN (1 << 0) + +#define IMX6_TEMPSENSE1 0x0190 +#define IMX6_TEMPSENSE1_MEASURE_FREQ 0xffff +#define IMX6_TEMPSENSE1_MEASURE_FREQ_SHIFT 0 + +#define OCOTP_MEM0 0x0480 +#define OCOTP_ANA1 0x04e0 + +/* Below TEMPSENSE2 is only for TEMPMON_IMX6SX */ +#define IMX6_TEMPSENSE2 0x0290 +#define IMX6_TEMPSENSE2_LOW_VALUE_SHIFT 0 +#define IMX6_TEMPSENSE2_LOW_VALUE_MASK 0xfff +#define IMX6_TEMPSENSE2_PANIC_VALUE_SHIFT 16 +#define IMX6_TEMPSENSE2_PANIC_VALUE_MASK 0xfff0000 + +/* i.MX7 specific */ +#define IMX7_ANADIG_DIGPROG 0x800 +#define IMX7_TEMPSENSE0 0x300 +#define IMX7_TEMPSENSE0_PANIC_ALARM_SHIFT 18 +#define IMX7_TEMPSENSE0_PANIC_ALARM_MASK (0x1ff << 18) +#define IMX7_TEMPSENSE0_HIGH_ALARM_SHIFT 9 +#define IMX7_TEMPSENSE0_HIGH_ALARM_MASK (0x1ff << 9) +#define IMX7_TEMPSENSE0_LOW_ALARM_SHIFT 0 +#define IMX7_TEMPSENSE0_LOW_ALARM_MASK 0x1ff + +#define IMX7_TEMPSENSE1 0x310 +#define IMX7_TEMPSENSE1_MEASURE_FREQ_SHIFT 16 +#define IMX7_TEMPSENSE1_MEASURE_FREQ_MASK (0xffff << 16) +#define IMX7_TEMPSENSE1_FINISHED (1 << 11) +#define IMX7_TEMPSENSE1_MEASURE_TEMP (1 << 10) +#define IMX7_TEMPSENSE1_POWER_DOWN (1 << 9) +#define IMX7_TEMPSENSE1_TEMP_VALUE_SHIFT 0 +#define IMX7_TEMPSENSE1_TEMP_VALUE_MASK 0x1ff + +/* The driver supports 1 passive trip point and 1 critical trip point */ +enum imx_thermal_trip { + IMX_TRIP_PASSIVE, + IMX_TRIP_CRITICAL, + IMX_TRIP_NUM, +}; + +#define IMX_POLLING_DELAY 2000 /* millisecond */ +#define IMX_PASSIVE_DELAY 1000 + +#define TEMPMON_IMX6Q 1 +#define TEMPMON_IMX6SX 2 +#define TEMPMON_IMX7D 3 + +struct thermal_soc_data { + u32 version; + + u32 sensor_ctrl; + u32 power_down_mask; + u32 measure_temp_mask; + + u32 measure_freq_ctrl; + u32 measure_freq_mask; + u32 measure_freq_shift; + + u32 temp_data; + u32 temp_value_mask; + u32 temp_value_shift; + u32 temp_valid_mask; + + u32 panic_alarm_ctrl; + u32 panic_alarm_mask; + u32 panic_alarm_shift; + + u32 high_alarm_ctrl; + u32 high_alarm_mask; + u32 high_alarm_shift; + + u32 low_alarm_ctrl; + u32 low_alarm_mask; + u32 low_alarm_shift; +}; + +static struct thermal_soc_data thermal_imx6q_data = { + .version = TEMPMON_IMX6Q, + + .sensor_ctrl = IMX6_TEMPSENSE0, + .power_down_mask = IMX6_TEMPSENSE0_POWER_DOWN, + .measure_temp_mask = IMX6_TEMPSENSE0_MEASURE_TEMP, + + .measure_freq_ctrl = IMX6_TEMPSENSE1, + .measure_freq_shift = IMX6_TEMPSENSE1_MEASURE_FREQ_SHIFT, + .measure_freq_mask = IMX6_TEMPSENSE1_MEASURE_FREQ, + + .temp_data = IMX6_TEMPSENSE0, + .temp_value_mask = IMX6_TEMPSENSE0_TEMP_CNT_MASK, + .temp_value_shift = IMX6_TEMPSENSE0_TEMP_CNT_SHIFT, + .temp_valid_mask = IMX6_TEMPSENSE0_FINISHED, + + .high_alarm_ctrl = IMX6_TEMPSENSE0, + .high_alarm_mask = IMX6_TEMPSENSE0_ALARM_VALUE_MASK, + .high_alarm_shift = IMX6_TEMPSENSE0_ALARM_VALUE_SHIFT, +}; + +static struct thermal_soc_data thermal_imx6sx_data = { + .version = TEMPMON_IMX6SX, + + .sensor_ctrl = IMX6_TEMPSENSE0, + .power_down_mask = IMX6_TEMPSENSE0_POWER_DOWN, + .measure_temp_mask = IMX6_TEMPSENSE0_MEASURE_TEMP, + + .measure_freq_ctrl = IMX6_TEMPSENSE1, + .measure_freq_shift = IMX6_TEMPSENSE1_MEASURE_FREQ_SHIFT, + .measure_freq_mask = IMX6_TEMPSENSE1_MEASURE_FREQ, + + .temp_data = IMX6_TEMPSENSE0, + .temp_value_mask = IMX6_TEMPSENSE0_TEMP_CNT_MASK, + .temp_value_shift = IMX6_TEMPSENSE0_TEMP_CNT_SHIFT, + .temp_valid_mask = IMX6_TEMPSENSE0_FINISHED, + + .high_alarm_ctrl = IMX6_TEMPSENSE0, + .high_alarm_mask = IMX6_TEMPSENSE0_ALARM_VALUE_MASK, + .high_alarm_shift = IMX6_TEMPSENSE0_ALARM_VALUE_SHIFT, + + .panic_alarm_ctrl = IMX6_TEMPSENSE2, + .panic_alarm_mask = IMX6_TEMPSENSE2_PANIC_VALUE_MASK, + .panic_alarm_shift = IMX6_TEMPSENSE2_PANIC_VALUE_SHIFT, + + .low_alarm_ctrl = IMX6_TEMPSENSE2, + .low_alarm_mask = IMX6_TEMPSENSE2_LOW_VALUE_MASK, + .low_alarm_shift = IMX6_TEMPSENSE2_LOW_VALUE_SHIFT, +}; + +static struct thermal_soc_data thermal_imx7d_data = { + .version = TEMPMON_IMX7D, + + .sensor_ctrl = IMX7_TEMPSENSE1, + .power_down_mask = IMX7_TEMPSENSE1_POWER_DOWN, + .measure_temp_mask = IMX7_TEMPSENSE1_MEASURE_TEMP, + + .measure_freq_ctrl = IMX7_TEMPSENSE1, + .measure_freq_shift = IMX7_TEMPSENSE1_MEASURE_FREQ_SHIFT, + .measure_freq_mask = IMX7_TEMPSENSE1_MEASURE_FREQ_MASK, + + .temp_data = IMX7_TEMPSENSE1, + .temp_value_mask = IMX7_TEMPSENSE1_TEMP_VALUE_MASK, + .temp_value_shift = IMX7_TEMPSENSE1_TEMP_VALUE_SHIFT, + .temp_valid_mask = IMX7_TEMPSENSE1_FINISHED, + + .panic_alarm_ctrl = IMX7_TEMPSENSE1, + .panic_alarm_mask = IMX7_TEMPSENSE0_PANIC_ALARM_MASK, + .panic_alarm_shift = IMX7_TEMPSENSE0_PANIC_ALARM_SHIFT, + + .high_alarm_ctrl = IMX7_TEMPSENSE0, + .high_alarm_mask = IMX7_TEMPSENSE0_HIGH_ALARM_MASK, + .high_alarm_shift = IMX7_TEMPSENSE0_HIGH_ALARM_SHIFT, + + .low_alarm_ctrl = IMX7_TEMPSENSE0, + .low_alarm_mask = IMX7_TEMPSENSE0_LOW_ALARM_MASK, + .low_alarm_shift = IMX7_TEMPSENSE0_LOW_ALARM_SHIFT, +}; + +struct imx_thermal_data { + struct cpufreq_policy *policy; + struct thermal_zone_device *tz; + struct thermal_cooling_device *cdev; + enum thermal_device_mode mode; + struct regmap *tempmon; + u32 c1, c2; /* See formula in imx_init_calib() */ + int temp_passive; + int temp_critical; + int temp_max; + int alarm_temp; + int last_temp; + bool irq_enabled; + int irq; + struct clk *thermal_clk; + const struct thermal_soc_data *socdata; + const char *temp_grade; +}; + +static void imx_set_panic_temp(struct imx_thermal_data *data, + int panic_temp) +{ + const struct thermal_soc_data *soc_data = data->socdata; + struct regmap *map = data->tempmon; + int critical_value; + + critical_value = (data->c2 - panic_temp) / data->c1; + + regmap_write(map, soc_data->panic_alarm_ctrl + REG_CLR, + soc_data->panic_alarm_mask); + regmap_write(map, soc_data->panic_alarm_ctrl + REG_SET, + critical_value << soc_data->panic_alarm_shift); +} + +static void imx_set_alarm_temp(struct imx_thermal_data *data, + int alarm_temp) +{ + struct regmap *map = data->tempmon; + const struct thermal_soc_data *soc_data = data->socdata; + int alarm_value; + + data->alarm_temp = alarm_temp; + + if (data->socdata->version == TEMPMON_IMX7D) + alarm_value = alarm_temp / 1000 + data->c1 - 25; + else + alarm_value = (data->c2 - alarm_temp) / data->c1; + + regmap_write(map, soc_data->high_alarm_ctrl + REG_CLR, + soc_data->high_alarm_mask); + regmap_write(map, soc_data->high_alarm_ctrl + REG_SET, + alarm_value << soc_data->high_alarm_shift); +} + +static int imx_get_temp(struct thermal_zone_device *tz, int *temp) +{ + struct imx_thermal_data *data = tz->devdata; + const struct thermal_soc_data *soc_data = data->socdata; + struct regmap *map = data->tempmon; + unsigned int n_meas; + bool wait; + u32 val; + + if (data->mode == THERMAL_DEVICE_ENABLED) { + /* Check if a measurement is currently in progress */ + regmap_read(map, soc_data->temp_data, &val); + wait = !(val & soc_data->temp_valid_mask); + } else { + /* + * Every time we measure the temperature, we will power on the + * temperature sensor, enable measurements, take a reading, + * disable measurements, power off the temperature sensor. + */ + regmap_write(map, soc_data->sensor_ctrl + REG_CLR, + soc_data->power_down_mask); + regmap_write(map, soc_data->sensor_ctrl + REG_SET, + soc_data->measure_temp_mask); + + wait = true; + } + + /* + * According to the temp sensor designers, it may require up to ~17us + * to complete a measurement. + */ + if (wait) + usleep_range(20, 50); + + regmap_read(map, soc_data->temp_data, &val); + + if (data->mode != THERMAL_DEVICE_ENABLED) { + regmap_write(map, soc_data->sensor_ctrl + REG_CLR, + soc_data->measure_temp_mask); + regmap_write(map, soc_data->sensor_ctrl + REG_SET, + soc_data->power_down_mask); + } + + if ((val & soc_data->temp_valid_mask) == 0) { + dev_dbg(&tz->device, "temp measurement never finished\n"); + return -EAGAIN; + } + + n_meas = (val & soc_data->temp_value_mask) + >> soc_data->temp_value_shift; + + /* See imx_init_calib() for formula derivation */ + if (data->socdata->version == TEMPMON_IMX7D) + *temp = (n_meas - data->c1 + 25) * 1000; + else + *temp = data->c2 - n_meas * data->c1; + + /* Update alarm value to next higher trip point for TEMPMON_IMX6Q */ + if (data->socdata->version == TEMPMON_IMX6Q) { + if (data->alarm_temp == data->temp_passive && + *temp >= data->temp_passive) + imx_set_alarm_temp(data, data->temp_critical); + if (data->alarm_temp == data->temp_critical && + *temp < data->temp_passive) { + imx_set_alarm_temp(data, data->temp_passive); + dev_dbg(&tz->device, "thermal alarm off: T < %d\n", + data->alarm_temp / 1000); + } + } + + if (*temp != data->last_temp) { + dev_dbg(&tz->device, "millicelsius: %d\n", *temp); + data->last_temp = *temp; + } + + /* Reenable alarm IRQ if temperature below alarm temperature */ + if (!data->irq_enabled && *temp < data->alarm_temp) { + data->irq_enabled = true; + enable_irq(data->irq); + } + + return 0; +} + +static int imx_get_mode(struct thermal_zone_device *tz, + enum thermal_device_mode *mode) +{ + struct imx_thermal_data *data = tz->devdata; + + *mode = data->mode; + + return 0; +} + +static int imx_set_mode(struct thermal_zone_device *tz, + enum thermal_device_mode mode) +{ + struct imx_thermal_data *data = tz->devdata; + struct regmap *map = data->tempmon; + const struct thermal_soc_data *soc_data = data->socdata; + + if (mode == THERMAL_DEVICE_ENABLED) { + tz->polling_delay = IMX_POLLING_DELAY; + tz->passive_delay = IMX_PASSIVE_DELAY; + + regmap_write(map, soc_data->sensor_ctrl + REG_CLR, + soc_data->power_down_mask); + regmap_write(map, soc_data->sensor_ctrl + REG_SET, + soc_data->measure_temp_mask); + + if (!data->irq_enabled) { + data->irq_enabled = true; + enable_irq(data->irq); + } + } else { + regmap_write(map, soc_data->sensor_ctrl + REG_CLR, + soc_data->measure_temp_mask); + regmap_write(map, soc_data->sensor_ctrl + REG_SET, + soc_data->power_down_mask); + + tz->polling_delay = 0; + tz->passive_delay = 0; + + if (data->irq_enabled) { + disable_irq(data->irq); + data->irq_enabled = false; + } + } + + data->mode = mode; + thermal_zone_device_update(tz, THERMAL_EVENT_UNSPECIFIED); + + return 0; +} + +static int imx_get_trip_type(struct thermal_zone_device *tz, int trip, + enum thermal_trip_type *type) +{ + *type = (trip == IMX_TRIP_PASSIVE) ? THERMAL_TRIP_PASSIVE : + THERMAL_TRIP_CRITICAL; + return 0; +} + +static int imx_get_crit_temp(struct thermal_zone_device *tz, int *temp) +{ + struct imx_thermal_data *data = tz->devdata; + + *temp = data->temp_critical; + return 0; +} + +static int imx_get_trip_temp(struct thermal_zone_device *tz, int trip, + int *temp) +{ + struct imx_thermal_data *data = tz->devdata; + + *temp = (trip == IMX_TRIP_PASSIVE) ? data->temp_passive : + data->temp_critical; + return 0; +} + +static int imx_set_trip_temp(struct thermal_zone_device *tz, int trip, + int temp) +{ + struct imx_thermal_data *data = tz->devdata; + + /* do not allow changing critical threshold */ + if (trip == IMX_TRIP_CRITICAL) + return -EPERM; + + /* do not allow passive to be set higher than critical */ + if (temp < 0 || temp > data->temp_critical) + return -EINVAL; + + data->temp_passive = temp; + + imx_set_alarm_temp(data, temp); + + return 0; +} + +static int imx_bind(struct thermal_zone_device *tz, + struct thermal_cooling_device *cdev) +{ + int ret; + + ret = thermal_zone_bind_cooling_device(tz, IMX_TRIP_PASSIVE, cdev, + THERMAL_NO_LIMIT, + THERMAL_NO_LIMIT, + THERMAL_WEIGHT_DEFAULT); + if (ret) { + dev_err(&tz->device, + "binding zone %s with cdev %s failed:%d\n", + tz->type, cdev->type, ret); + return ret; + } + + return 0; +} + +static int imx_unbind(struct thermal_zone_device *tz, + struct thermal_cooling_device *cdev) +{ + int ret; + + ret = thermal_zone_unbind_cooling_device(tz, IMX_TRIP_PASSIVE, cdev); + if (ret) { + dev_err(&tz->device, + "unbinding zone %s with cdev %s failed:%d\n", + tz->type, cdev->type, ret); + return ret; + } + + return 0; +} + +static struct thermal_zone_device_ops imx_tz_ops = { + .bind = imx_bind, + .unbind = imx_unbind, + .get_temp = imx_get_temp, + .get_mode = imx_get_mode, + .set_mode = imx_set_mode, + .get_trip_type = imx_get_trip_type, + .get_trip_temp = imx_get_trip_temp, + .get_crit_temp = imx_get_crit_temp, + .set_trip_temp = imx_set_trip_temp, +}; + +static int imx_init_calib(struct platform_device *pdev, u32 ocotp_ana1) +{ + struct imx_thermal_data *data = platform_get_drvdata(pdev); + int n1; + u64 temp64; + + if (ocotp_ana1 == 0 || ocotp_ana1 == ~0) { + dev_err(&pdev->dev, "invalid sensor calibration data\n"); + return -EINVAL; + } + + /* + * On i.MX7D, we only use the calibration data at 25C to get the temp, + * Tmeas = ( Nmeas - n1) + 25; n1 is the fuse value for 25C. + */ + if (data->socdata->version == TEMPMON_IMX7D) { + data->c1 = (ocotp_ana1 >> 9) & 0x1ff; + return 0; + } + + /* + * The sensor is calibrated at 25 °C (aka T1) and the value measured + * (aka N1) at this temperature is provided in bits [31:20] in the + * i.MX's OCOTP value ANA1. + * To find the actual temperature T, the following formula has to be used + * when reading value n from the sensor: + * + * T = T1 + (N - N1) / (0.4148468 - 0.0015423 * N1) °C + 3.580661 °C + * = [T1' - N1 / (0.4148468 - 0.0015423 * N1) °C] + N / (0.4148468 - 0.0015423 * N1) °C + * = [T1' + N1 / (0.0015423 * N1 - 0.4148468) °C] - N / (0.0015423 * N1 - 0.4148468) °C + * = c2 - c1 * N + * + * with + * + * T1' = 28.580661 °C + * c1 = 1 / (0.0015423 * N1 - 0.4297157) °C + * c2 = T1' + N1 / (0.0015423 * N1 - 0.4148468) °C + * = T1' + N1 * c1 + */ + n1 = ocotp_ana1 >> 20; + + temp64 = 10000000; /* use 10^7 as fixed point constant for values in formula */ + temp64 *= 1000; /* to get result in °mC */ + do_div(temp64, 15423 * n1 - 4148468); + data->c1 = temp64; + data->c2 = n1 * data->c1 + 28581; + + return 0; +} + +static void imx_init_temp_grade(struct platform_device *pdev, u32 ocotp_mem0) +{ + struct imx_thermal_data *data = platform_get_drvdata(pdev); + + /* The maximum die temp is specified by the Temperature Grade */ + switch ((ocotp_mem0 >> 6) & 0x3) { + case 0: /* Commercial (0 to 95 °C) */ + data->temp_grade = "Commercial"; + data->temp_max = 95000; + break; + case 1: /* Extended Commercial (-20 °C to 105 °C) */ + data->temp_grade = "Extended Commercial"; + data->temp_max = 105000; + break; + case 2: /* Industrial (-40 °C to 105 °C) */ + data->temp_grade = "Industrial"; + data->temp_max = 105000; + break; + case 3: /* Automotive (-40 °C to 125 °C) */ + data->temp_grade = "Automotive"; + data->temp_max = 125000; + break; + } + + /* + * Set the critical trip point at 5 °C under max + * Set the passive trip point at 10 °C under max (changeable via sysfs) + */ + data->temp_critical = data->temp_max - (1000 * 5); + data->temp_passive = data->temp_max - (1000 * 10); +} + +static int imx_init_from_tempmon_data(struct platform_device *pdev) +{ + struct regmap *map; + int ret; + u32 val; + + map = syscon_regmap_lookup_by_phandle(pdev->dev.of_node, + "fsl,tempmon-data"); + if (IS_ERR(map)) { + ret = PTR_ERR(map); + dev_err(&pdev->dev, "failed to get sensor regmap: %d\n", ret); + return ret; + } + + ret = regmap_read(map, OCOTP_ANA1, &val); + if (ret) { + dev_err(&pdev->dev, "failed to read sensor data: %d\n", ret); + return ret; + } + ret = imx_init_calib(pdev, val); + if (ret) + return ret; + + ret = regmap_read(map, OCOTP_MEM0, &val); + if (ret) { + dev_err(&pdev->dev, "failed to read sensor data: %d\n", ret); + return ret; + } + imx_init_temp_grade(pdev, val); + + return 0; +} + +static int imx_init_from_nvmem_cells(struct platform_device *pdev) +{ + int ret; + u32 val; + + ret = nvmem_cell_read_u32(&pdev->dev, "calib", &val); + if (ret) + return ret; + + ret = imx_init_calib(pdev, val); + if (ret) + return ret; + + ret = nvmem_cell_read_u32(&pdev->dev, "temp_grade", &val); + if (ret) + return ret; + imx_init_temp_grade(pdev, val); + + return 0; +} + +static irqreturn_t imx_thermal_alarm_irq(int irq, void *dev) +{ + struct imx_thermal_data *data = dev; + + disable_irq_nosync(irq); + data->irq_enabled = false; + + return IRQ_WAKE_THREAD; +} + +static irqreturn_t imx_thermal_alarm_irq_thread(int irq, void *dev) +{ + struct imx_thermal_data *data = dev; + + dev_dbg(&data->tz->device, "THERMAL ALARM: T > %d\n", + data->alarm_temp / 1000); + + thermal_zone_device_update(data->tz, THERMAL_EVENT_UNSPECIFIED); + + return IRQ_HANDLED; +} + +static const struct of_device_id of_imx_thermal_match[] = { + { .compatible = "fsl,imx6q-tempmon", .data = &thermal_imx6q_data, }, + { .compatible = "fsl,imx6sx-tempmon", .data = &thermal_imx6sx_data, }, + { .compatible = "fsl,imx7d-tempmon", .data = &thermal_imx7d_data, }, + { /* end */ } +}; +MODULE_DEVICE_TABLE(of, of_imx_thermal_match); + +/* + * Create cooling device in case no #cooling-cells property is available in + * CPU node + */ +static int imx_thermal_register_legacy_cooling(struct imx_thermal_data *data) +{ + struct device_node *np = of_get_cpu_node(data->policy->cpu, NULL); + int ret; + + if (!np || !of_find_property(np, "#cooling-cells", NULL)) { + data->cdev = cpufreq_cooling_register(data->policy); + if (IS_ERR(data->cdev)) { + ret = PTR_ERR(data->cdev); + cpufreq_cpu_put(data->policy); + return ret; + } + } + + return 0; +} + +static int imx_thermal_probe(struct platform_device *pdev) +{ + struct imx_thermal_data *data; + struct regmap *map; + int measure_freq; + int ret; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + map = syscon_regmap_lookup_by_phandle(pdev->dev.of_node, "fsl,tempmon"); + if (IS_ERR(map)) { + ret = PTR_ERR(map); + dev_err(&pdev->dev, "failed to get tempmon regmap: %d\n", ret); + return ret; + } + data->tempmon = map; + + data->socdata = of_device_get_match_data(&pdev->dev); + if (!data->socdata) { + dev_err(&pdev->dev, "no device match found\n"); + return -ENODEV; + } + + /* make sure the IRQ flag is clear before enabling irq on i.MX6SX */ + if (data->socdata->version == TEMPMON_IMX6SX) { + regmap_write(map, IMX6_MISC1 + REG_CLR, + IMX6_MISC1_IRQ_TEMPHIGH | IMX6_MISC1_IRQ_TEMPLOW + | IMX6_MISC1_IRQ_TEMPPANIC); + /* + * reset value of LOW ALARM is incorrect, set it to lowest + * value to avoid false trigger of low alarm. + */ + regmap_write(map, data->socdata->low_alarm_ctrl + REG_SET, + data->socdata->low_alarm_mask); + } + + data->irq = platform_get_irq(pdev, 0); + if (data->irq < 0) + return data->irq; + + platform_set_drvdata(pdev, data); + + if (of_find_property(pdev->dev.of_node, "nvmem-cells", NULL)) { + ret = imx_init_from_nvmem_cells(pdev); + if (ret == -EPROBE_DEFER) + return ret; + if (ret) { + dev_err(&pdev->dev, "failed to init from nvmem: %d\n", + ret); + return ret; + } + } else { + ret = imx_init_from_tempmon_data(pdev); + if (ret) { + dev_err(&pdev->dev, "failed to init from from fsl,tempmon-data\n"); + return ret; + } + } + + /* Make sure sensor is in known good state for measurements */ + regmap_write(map, data->socdata->sensor_ctrl + REG_CLR, + data->socdata->power_down_mask); + regmap_write(map, data->socdata->sensor_ctrl + REG_CLR, + data->socdata->measure_temp_mask); + regmap_write(map, data->socdata->measure_freq_ctrl + REG_CLR, + data->socdata->measure_freq_mask); + if (data->socdata->version != TEMPMON_IMX7D) + regmap_write(map, IMX6_MISC0 + REG_SET, + IMX6_MISC0_REFTOP_SELBIASOFF); + regmap_write(map, data->socdata->sensor_ctrl + REG_SET, + data->socdata->power_down_mask); + + data->policy = cpufreq_cpu_get(0); + if (!data->policy) { + pr_debug("%s: CPUFreq policy not found\n", __func__); + return -EPROBE_DEFER; + } + + ret = imx_thermal_register_legacy_cooling(data); + if (ret) { + dev_err(&pdev->dev, + "failed to register cpufreq cooling device: %d\n", ret); + return ret; + } + + data->thermal_clk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(data->thermal_clk)) { + ret = PTR_ERR(data->thermal_clk); + if (ret != -EPROBE_DEFER) + dev_err(&pdev->dev, + "failed to get thermal clk: %d\n", ret); + cpufreq_cooling_unregister(data->cdev); + cpufreq_cpu_put(data->policy); + return ret; + } + + /* + * Thermal sensor needs clk on to get correct value, normally + * we should enable its clk before taking measurement and disable + * clk after measurement is done, but if alarm function is enabled, + * hardware will auto measure the temperature periodically, so we + * need to keep the clk always on for alarm function. + */ + ret = clk_prepare_enable(data->thermal_clk); + if (ret) { + dev_err(&pdev->dev, "failed to enable thermal clk: %d\n", ret); + cpufreq_cooling_unregister(data->cdev); + cpufreq_cpu_put(data->policy); + return ret; + } + + data->tz = thermal_zone_device_register("imx_thermal_zone", + IMX_TRIP_NUM, + BIT(IMX_TRIP_PASSIVE), data, + &imx_tz_ops, NULL, + IMX_PASSIVE_DELAY, + IMX_POLLING_DELAY); + if (IS_ERR(data->tz)) { + ret = PTR_ERR(data->tz); + dev_err(&pdev->dev, + "failed to register thermal zone device %d\n", ret); + clk_disable_unprepare(data->thermal_clk); + cpufreq_cooling_unregister(data->cdev); + cpufreq_cpu_put(data->policy); + return ret; + } + + dev_info(&pdev->dev, "%s CPU temperature grade - max:%dC" + " critical:%dC passive:%dC\n", data->temp_grade, + data->temp_max / 1000, data->temp_critical / 1000, + data->temp_passive / 1000); + + /* Enable measurements at ~ 10 Hz */ + regmap_write(map, data->socdata->measure_freq_ctrl + REG_CLR, + data->socdata->measure_freq_mask); + measure_freq = DIV_ROUND_UP(32768, 10); /* 10 Hz */ + regmap_write(map, data->socdata->measure_freq_ctrl + REG_SET, + measure_freq << data->socdata->measure_freq_shift); + imx_set_alarm_temp(data, data->temp_passive); + + if (data->socdata->version == TEMPMON_IMX6SX) + imx_set_panic_temp(data, data->temp_critical); + + regmap_write(map, data->socdata->sensor_ctrl + REG_CLR, + data->socdata->power_down_mask); + regmap_write(map, data->socdata->sensor_ctrl + REG_SET, + data->socdata->measure_temp_mask); + + data->irq_enabled = true; + data->mode = THERMAL_DEVICE_ENABLED; + + ret = devm_request_threaded_irq(&pdev->dev, data->irq, + imx_thermal_alarm_irq, imx_thermal_alarm_irq_thread, + 0, "imx_thermal", data); + if (ret < 0) { + dev_err(&pdev->dev, "failed to request alarm irq: %d\n", ret); + clk_disable_unprepare(data->thermal_clk); + thermal_zone_device_unregister(data->tz); + cpufreq_cooling_unregister(data->cdev); + cpufreq_cpu_put(data->policy); + return ret; + } + + return 0; +} + +static int imx_thermal_remove(struct platform_device *pdev) +{ + struct imx_thermal_data *data = platform_get_drvdata(pdev); + struct regmap *map = data->tempmon; + + /* Disable measurements */ + regmap_write(map, data->socdata->sensor_ctrl + REG_SET, + data->socdata->power_down_mask); + if (!IS_ERR(data->thermal_clk)) + clk_disable_unprepare(data->thermal_clk); + + thermal_zone_device_unregister(data->tz); + cpufreq_cooling_unregister(data->cdev); + cpufreq_cpu_put(data->policy); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int imx_thermal_suspend(struct device *dev) +{ + struct imx_thermal_data *data = dev_get_drvdata(dev); + struct regmap *map = data->tempmon; + + /* + * Need to disable thermal sensor, otherwise, when thermal core + * try to get temperature before thermal sensor resume, a wrong + * temperature will be read as the thermal sensor is powered + * down. + */ + regmap_write(map, data->socdata->sensor_ctrl + REG_CLR, + data->socdata->measure_temp_mask); + regmap_write(map, data->socdata->sensor_ctrl + REG_SET, + data->socdata->power_down_mask); + data->mode = THERMAL_DEVICE_DISABLED; + clk_disable_unprepare(data->thermal_clk); + + return 0; +} + +static int imx_thermal_resume(struct device *dev) +{ + struct imx_thermal_data *data = dev_get_drvdata(dev); + struct regmap *map = data->tempmon; + int ret; + + ret = clk_prepare_enable(data->thermal_clk); + if (ret) + return ret; + /* Enabled thermal sensor after resume */ + regmap_write(map, data->socdata->sensor_ctrl + REG_CLR, + data->socdata->power_down_mask); + regmap_write(map, data->socdata->sensor_ctrl + REG_SET, + data->socdata->measure_temp_mask); + data->mode = THERMAL_DEVICE_ENABLED; + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(imx_thermal_pm_ops, + imx_thermal_suspend, imx_thermal_resume); + +static struct platform_driver imx_thermal = { + .driver = { + .name = "imx_thermal", + .pm = &imx_thermal_pm_ops, + .of_match_table = of_imx_thermal_match, + }, + .probe = imx_thermal_probe, + .remove = imx_thermal_remove, +}; +module_platform_driver(imx_thermal); + +MODULE_AUTHOR("Freescale Semiconductor, Inc."); +MODULE_DESCRIPTION("Thermal driver for Freescale i.MX SoCs"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:imx-thermal"); diff --git a/drivers/thermal/int340x_thermal/Kconfig b/drivers/thermal/int340x_thermal/Kconfig new file mode 100644 index 000000000..0582bd12a --- /dev/null +++ b/drivers/thermal/int340x_thermal/Kconfig @@ -0,0 +1,42 @@ +# +# ACPI INT340x thermal drivers configuration +# + +config INT340X_THERMAL + tristate "ACPI INT340X thermal drivers" + depends on X86 && ACPI + select THERMAL_GOV_USER_SPACE + select ACPI_THERMAL_REL + select ACPI_FAN + select INTEL_SOC_DTS_IOSF_CORE + help + Newer laptops and tablets that use ACPI may have thermal sensors and + other devices with thermal control capabilities outside the core + CPU/SOC, for thermal safety reasons. + They are exposed for the OS to use via the INT3400 ACPI device object + as the master, and INT3401~INT340B ACPI device objects as the slaves. + Enable this to expose the temperature information and cooling ability + from these objects to userspace via the normal thermal framework. + This means that a wide range of applications and GUI widgets can show + the information to the user or use this information for making + decisions. For example, the Intel Thermal Daemon can use this + information to allow the user to select his laptop to run without + turning on the fans. + +config ACPI_THERMAL_REL + tristate + depends on ACPI + +if INT340X_THERMAL + +config INT3406_THERMAL + tristate "ACPI INT3406 display thermal driver" + depends on ACPI_VIDEO + help + The display thermal device represents the LED/LCD display panel + that may or may not include touch support. The main function of + the display thermal device is to allow control of the display + brightness in order to address a thermal condition or to reduce + power consumed by display device. + +endif diff --git a/drivers/thermal/int340x_thermal/Makefile b/drivers/thermal/int340x_thermal/Makefile new file mode 100644 index 000000000..287eb0a14 --- /dev/null +++ b/drivers/thermal/int340x_thermal/Makefile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_INT340X_THERMAL) += int3400_thermal.o +obj-$(CONFIG_INT340X_THERMAL) += int340x_thermal_zone.o +obj-$(CONFIG_INT340X_THERMAL) += int3402_thermal.o +obj-$(CONFIG_INT340X_THERMAL) += int3403_thermal.o +obj-$(CONFIG_INT340X_THERMAL) += processor_thermal_device.o +obj-$(CONFIG_INT3406_THERMAL) += int3406_thermal.o +obj-$(CONFIG_ACPI_THERMAL_REL) += acpi_thermal_rel.o diff --git a/drivers/thermal/int340x_thermal/acpi_thermal_rel.c b/drivers/thermal/int340x_thermal/acpi_thermal_rel.c new file mode 100644 index 000000000..45e7e5cbd --- /dev/null +++ b/drivers/thermal/int340x_thermal/acpi_thermal_rel.c @@ -0,0 +1,394 @@ +/* acpi_thermal_rel.c driver for exporting ACPI thermal relationship + * + * Copyright (c) 2014 Intel Corp + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published by + * the Free Software Foundation. + * + */ + +/* + * Two functionalities included: + * 1. Export _TRT, _ART, via misc device interface to the userspace. + * 2. Provide parsing result to kernel drivers + * + */ +#include <linux/init.h> +#include <linux/export.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/platform_device.h> +#include <linux/io.h> +#include <linux/acpi.h> +#include <linux/uaccess.h> +#include <linux/miscdevice.h> +#include "acpi_thermal_rel.h" + +static acpi_handle acpi_thermal_rel_handle; +static DEFINE_SPINLOCK(acpi_thermal_rel_chrdev_lock); +static int acpi_thermal_rel_chrdev_count; /* #times opened */ +static int acpi_thermal_rel_chrdev_exclu; /* already open exclusive? */ + +static int acpi_thermal_rel_open(struct inode *inode, struct file *file) +{ + spin_lock(&acpi_thermal_rel_chrdev_lock); + if (acpi_thermal_rel_chrdev_exclu || + (acpi_thermal_rel_chrdev_count && (file->f_flags & O_EXCL))) { + spin_unlock(&acpi_thermal_rel_chrdev_lock); + return -EBUSY; + } + + if (file->f_flags & O_EXCL) + acpi_thermal_rel_chrdev_exclu = 1; + acpi_thermal_rel_chrdev_count++; + + spin_unlock(&acpi_thermal_rel_chrdev_lock); + + return nonseekable_open(inode, file); +} + +static int acpi_thermal_rel_release(struct inode *inode, struct file *file) +{ + spin_lock(&acpi_thermal_rel_chrdev_lock); + acpi_thermal_rel_chrdev_count--; + acpi_thermal_rel_chrdev_exclu = 0; + spin_unlock(&acpi_thermal_rel_chrdev_lock); + + return 0; +} + +/** + * acpi_parse_trt - Thermal Relationship Table _TRT for passive cooling + * + * @handle: ACPI handle of the device contains _TRT + * @trt_count: the number of valid entries resulted from parsing _TRT + * @trtp: pointer to pointer of array of _TRT entries in parsing result + * @create_dev: whether to create platform devices for target and source + * + */ +int acpi_parse_trt(acpi_handle handle, int *trt_count, struct trt **trtp, + bool create_dev) +{ + acpi_status status; + int result = 0; + int i; + int nr_bad_entries = 0; + struct trt *trts; + struct acpi_device *adev; + union acpi_object *p; + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer element = { 0, NULL }; + struct acpi_buffer trt_format = { sizeof("RRNNNNNN"), "RRNNNNNN" }; + + if (!acpi_has_method(handle, "_TRT")) + return -ENODEV; + + status = acpi_evaluate_object(handle, "_TRT", NULL, &buffer); + if (ACPI_FAILURE(status)) + return -ENODEV; + + p = buffer.pointer; + if (!p || (p->type != ACPI_TYPE_PACKAGE)) { + pr_err("Invalid _TRT data\n"); + result = -EFAULT; + goto end; + } + + *trt_count = p->package.count; + trts = kcalloc(*trt_count, sizeof(struct trt), GFP_KERNEL); + if (!trts) { + result = -ENOMEM; + goto end; + } + + for (i = 0; i < *trt_count; i++) { + struct trt *trt = &trts[i - nr_bad_entries]; + + element.length = sizeof(struct trt); + element.pointer = trt; + + status = acpi_extract_package(&(p->package.elements[i]), + &trt_format, &element); + if (ACPI_FAILURE(status)) { + nr_bad_entries++; + pr_warn("_TRT package %d is invalid, ignored\n", i); + continue; + } + if (!create_dev) + continue; + + result = acpi_bus_get_device(trt->source, &adev); + if (result) + pr_warn("Failed to get source ACPI device\n"); + + result = acpi_bus_get_device(trt->target, &adev); + if (result) + pr_warn("Failed to get target ACPI device\n"); + } + + result = 0; + + *trtp = trts; + /* don't count bad entries */ + *trt_count -= nr_bad_entries; +end: + kfree(buffer.pointer); + return result; +} +EXPORT_SYMBOL(acpi_parse_trt); + +/** + * acpi_parse_art - Parse Active Relationship Table _ART + * + * @handle: ACPI handle of the device contains _ART + * @art_count: the number of valid entries resulted from parsing _ART + * @artp: pointer to pointer of array of art entries in parsing result + * @create_dev: whether to create platform devices for target and source + * + */ +int acpi_parse_art(acpi_handle handle, int *art_count, struct art **artp, + bool create_dev) +{ + acpi_status status; + int result = 0; + int i; + int nr_bad_entries = 0; + struct art *arts; + struct acpi_device *adev; + union acpi_object *p; + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + struct acpi_buffer element = { 0, NULL }; + struct acpi_buffer art_format = { + sizeof("RRNNNNNNNNNNN"), "RRNNNNNNNNNNN" }; + + if (!acpi_has_method(handle, "_ART")) + return -ENODEV; + + status = acpi_evaluate_object(handle, "_ART", NULL, &buffer); + if (ACPI_FAILURE(status)) + return -ENODEV; + + p = buffer.pointer; + if (!p || (p->type != ACPI_TYPE_PACKAGE)) { + pr_err("Invalid _ART data\n"); + result = -EFAULT; + goto end; + } + + /* ignore p->package.elements[0], as this is _ART Revision field */ + *art_count = p->package.count - 1; + arts = kcalloc(*art_count, sizeof(struct art), GFP_KERNEL); + if (!arts) { + result = -ENOMEM; + goto end; + } + + for (i = 0; i < *art_count; i++) { + struct art *art = &arts[i - nr_bad_entries]; + + element.length = sizeof(struct art); + element.pointer = art; + + status = acpi_extract_package(&(p->package.elements[i + 1]), + &art_format, &element); + if (ACPI_FAILURE(status)) { + pr_warn("_ART package %d is invalid, ignored", i); + nr_bad_entries++; + continue; + } + if (!create_dev) + continue; + + if (art->source) { + result = acpi_bus_get_device(art->source, &adev); + if (result) + pr_warn("Failed to get source ACPI device\n"); + } + if (art->target) { + result = acpi_bus_get_device(art->target, &adev); + if (result) + pr_warn("Failed to get target ACPI device\n"); + } + } + + *artp = arts; + /* don't count bad entries */ + *art_count -= nr_bad_entries; +end: + kfree(buffer.pointer); + return result; +} +EXPORT_SYMBOL(acpi_parse_art); + + +/* get device name from acpi handle */ +static void get_single_name(acpi_handle handle, char *name) +{ + struct acpi_buffer buffer = {ACPI_ALLOCATE_BUFFER}; + + if (ACPI_FAILURE(acpi_get_name(handle, ACPI_SINGLE_NAME, &buffer))) + pr_warn("Failed to get device name from acpi handle\n"); + else { + memcpy(name, buffer.pointer, ACPI_NAME_SIZE); + kfree(buffer.pointer); + } +} + +static int fill_art(char __user *ubuf) +{ + int i; + int ret; + int count; + int art_len; + struct art *arts = NULL; + union art_object *art_user; + + ret = acpi_parse_art(acpi_thermal_rel_handle, &count, &arts, false); + if (ret) + goto free_art; + art_len = count * sizeof(union art_object); + art_user = kzalloc(art_len, GFP_KERNEL); + if (!art_user) { + ret = -ENOMEM; + goto free_art; + } + /* now fill in user art data */ + for (i = 0; i < count; i++) { + /* userspace art needs device name instead of acpi reference */ + get_single_name(arts[i].source, art_user[i].source_device); + get_single_name(arts[i].target, art_user[i].target_device); + /* copy the rest int data in addition to source and target */ + memcpy(&art_user[i].weight, &arts[i].weight, + sizeof(u64) * (ACPI_NR_ART_ELEMENTS - 2)); + } + + if (copy_to_user(ubuf, art_user, art_len)) + ret = -EFAULT; + kfree(art_user); +free_art: + kfree(arts); + return ret; +} + +static int fill_trt(char __user *ubuf) +{ + int i; + int ret; + int count; + int trt_len; + struct trt *trts = NULL; + union trt_object *trt_user; + + ret = acpi_parse_trt(acpi_thermal_rel_handle, &count, &trts, false); + if (ret) + goto free_trt; + trt_len = count * sizeof(union trt_object); + trt_user = kzalloc(trt_len, GFP_KERNEL); + if (!trt_user) { + ret = -ENOMEM; + goto free_trt; + } + /* now fill in user trt data */ + for (i = 0; i < count; i++) { + /* userspace trt needs device name instead of acpi reference */ + get_single_name(trts[i].source, trt_user[i].source_device); + get_single_name(trts[i].target, trt_user[i].target_device); + trt_user[i].sample_period = trts[i].sample_period; + trt_user[i].influence = trts[i].influence; + } + + if (copy_to_user(ubuf, trt_user, trt_len)) + ret = -EFAULT; + kfree(trt_user); +free_trt: + kfree(trts); + return ret; +} + +static long acpi_thermal_rel_ioctl(struct file *f, unsigned int cmd, + unsigned long __arg) +{ + int ret = 0; + unsigned long length = 0; + int count = 0; + char __user *arg = (void __user *)__arg; + struct trt *trts = NULL; + struct art *arts = NULL; + + switch (cmd) { + case ACPI_THERMAL_GET_TRT_COUNT: + ret = acpi_parse_trt(acpi_thermal_rel_handle, &count, + &trts, false); + kfree(trts); + if (!ret) + return put_user(count, (unsigned long __user *)__arg); + return ret; + case ACPI_THERMAL_GET_TRT_LEN: + ret = acpi_parse_trt(acpi_thermal_rel_handle, &count, + &trts, false); + kfree(trts); + length = count * sizeof(union trt_object); + if (!ret) + return put_user(length, (unsigned long __user *)__arg); + return ret; + case ACPI_THERMAL_GET_TRT: + return fill_trt(arg); + case ACPI_THERMAL_GET_ART_COUNT: + ret = acpi_parse_art(acpi_thermal_rel_handle, &count, + &arts, false); + kfree(arts); + if (!ret) + return put_user(count, (unsigned long __user *)__arg); + return ret; + case ACPI_THERMAL_GET_ART_LEN: + ret = acpi_parse_art(acpi_thermal_rel_handle, &count, + &arts, false); + kfree(arts); + length = count * sizeof(union art_object); + if (!ret) + return put_user(length, (unsigned long __user *)__arg); + return ret; + + case ACPI_THERMAL_GET_ART: + return fill_art(arg); + + default: + return -ENOTTY; + } +} + +static const struct file_operations acpi_thermal_rel_fops = { + .owner = THIS_MODULE, + .open = acpi_thermal_rel_open, + .release = acpi_thermal_rel_release, + .unlocked_ioctl = acpi_thermal_rel_ioctl, + .llseek = no_llseek, +}; + +static struct miscdevice acpi_thermal_rel_misc_device = { + .minor = MISC_DYNAMIC_MINOR, + "acpi_thermal_rel", + &acpi_thermal_rel_fops +}; + +int acpi_thermal_rel_misc_device_add(acpi_handle handle) +{ + acpi_thermal_rel_handle = handle; + + return misc_register(&acpi_thermal_rel_misc_device); +} +EXPORT_SYMBOL(acpi_thermal_rel_misc_device_add); + +int acpi_thermal_rel_misc_device_remove(acpi_handle handle) +{ + misc_deregister(&acpi_thermal_rel_misc_device); + + return 0; +} +EXPORT_SYMBOL(acpi_thermal_rel_misc_device_remove); + +MODULE_AUTHOR("Zhang Rui <rui.zhang@intel.com>"); +MODULE_AUTHOR("Jacob Pan <jacob.jun.pan@intel.com"); +MODULE_DESCRIPTION("Intel acpi thermal rel misc dev driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/int340x_thermal/acpi_thermal_rel.h b/drivers/thermal/int340x_thermal/acpi_thermal_rel.h new file mode 100644 index 000000000..58822575f --- /dev/null +++ b/drivers/thermal/int340x_thermal/acpi_thermal_rel.h @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __ACPI_ACPI_THERMAL_H +#define __ACPI_ACPI_THERMAL_H + +#include <asm/ioctl.h> + +#define ACPI_THERMAL_MAGIC 's' + +#define ACPI_THERMAL_GET_TRT_LEN _IOR(ACPI_THERMAL_MAGIC, 1, unsigned long) +#define ACPI_THERMAL_GET_ART_LEN _IOR(ACPI_THERMAL_MAGIC, 2, unsigned long) +#define ACPI_THERMAL_GET_TRT_COUNT _IOR(ACPI_THERMAL_MAGIC, 3, unsigned long) +#define ACPI_THERMAL_GET_ART_COUNT _IOR(ACPI_THERMAL_MAGIC, 4, unsigned long) + +#define ACPI_THERMAL_GET_TRT _IOR(ACPI_THERMAL_MAGIC, 5, unsigned long) +#define ACPI_THERMAL_GET_ART _IOR(ACPI_THERMAL_MAGIC, 6, unsigned long) + +struct art { + acpi_handle source; + acpi_handle target; + u64 weight; + u64 ac0_max; + u64 ac1_max; + u64 ac2_max; + u64 ac3_max; + u64 ac4_max; + u64 ac5_max; + u64 ac6_max; + u64 ac7_max; + u64 ac8_max; + u64 ac9_max; +} __packed; + +struct trt { + acpi_handle source; + acpi_handle target; + u64 influence; + u64 sample_period; + u64 reserved1; + u64 reserved2; + u64 reserved3; + u64 reserved4; +} __packed; + +#define ACPI_NR_ART_ELEMENTS 13 +/* for usrspace */ +union art_object { + struct { + char source_device[8]; /* ACPI single name */ + char target_device[8]; /* ACPI single name */ + u64 weight; + u64 ac0_max_level; + u64 ac1_max_level; + u64 ac2_max_level; + u64 ac3_max_level; + u64 ac4_max_level; + u64 ac5_max_level; + u64 ac6_max_level; + u64 ac7_max_level; + u64 ac8_max_level; + u64 ac9_max_level; + }; + u64 __data[ACPI_NR_ART_ELEMENTS]; +}; + +union trt_object { + struct { + char source_device[8]; /* ACPI single name */ + char target_device[8]; /* ACPI single name */ + u64 influence; + u64 sample_period; + u64 reserved[4]; + }; + u64 __data[8]; +}; + +#ifdef __KERNEL__ +int acpi_thermal_rel_misc_device_add(acpi_handle handle); +int acpi_thermal_rel_misc_device_remove(acpi_handle handle); +int acpi_parse_art(acpi_handle handle, int *art_count, struct art **arts, + bool create_dev); +int acpi_parse_trt(acpi_handle handle, int *trt_count, struct trt **trts, + bool create_dev); +#endif + +#endif /* __ACPI_ACPI_THERMAL_H */ diff --git a/drivers/thermal/int340x_thermal/int3400_thermal.c b/drivers/thermal/int340x_thermal/int3400_thermal.c new file mode 100644 index 000000000..68393c3c1 --- /dev/null +++ b/drivers/thermal/int340x_thermal/int3400_thermal.c @@ -0,0 +1,402 @@ +/* + * INT3400 thermal driver + * + * Copyright (C) 2014, Intel Corporation + * Authors: Zhang Rui <rui.zhang@intel.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/acpi.h> +#include <linux/thermal.h> +#include "acpi_thermal_rel.h" + +#define INT3400_THERMAL_TABLE_CHANGED 0x83 + +enum int3400_thermal_uuid { + INT3400_THERMAL_PASSIVE_1, + INT3400_THERMAL_ACTIVE, + INT3400_THERMAL_CRITICAL, + INT3400_THERMAL_ADAPTIVE_PERFORMANCE, + INT3400_THERMAL_EMERGENCY_CALL_MODE, + INT3400_THERMAL_PASSIVE_2, + INT3400_THERMAL_POWER_BOSS, + INT3400_THERMAL_VIRTUAL_SENSOR, + INT3400_THERMAL_COOLING_MODE, + INT3400_THERMAL_HARDWARE_DUTY_CYCLING, + INT3400_THERMAL_MAXIMUM_UUID, +}; + +static char *int3400_thermal_uuids[INT3400_THERMAL_MAXIMUM_UUID] = { + "42A441D6-AE6A-462b-A84B-4A8CE79027D3", + "3A95C389-E4B8-4629-A526-C52C88626BAE", + "97C68AE7-15FA-499c-B8C9-5DA81D606E0A", + "63BE270F-1C11-48FD-A6F7-3AF253FF3E2D", + "5349962F-71E6-431D-9AE8-0A635B710AEE", + "9E04115A-AE87-4D1C-9500-0F3E340BFE75", + "F5A35014-C209-46A4-993A-EB56DE7530A1", + "6ED722A7-9240-48A5-B479-31EEF723D7CF", + "16CAF1B7-DD38-40ED-B1C1-1B8A1913D531", + "BE84BABF-C4D4-403D-B495-3128FD44dAC1", +}; + +struct int3400_thermal_priv { + struct acpi_device *adev; + struct thermal_zone_device *thermal; + int mode; + int art_count; + struct art *arts; + int trt_count; + struct trt *trts; + u32 uuid_bitmap; + int rel_misc_dev_res; + int current_uuid_index; +}; + +static ssize_t available_uuids_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct platform_device *pdev = to_platform_device(dev); + struct int3400_thermal_priv *priv = platform_get_drvdata(pdev); + int i; + int length = 0; + + for (i = 0; i < INT3400_THERMAL_MAXIMUM_UUID; i++) { + if (priv->uuid_bitmap & (1 << i)) + if (PAGE_SIZE - length > 0) + length += snprintf(&buf[length], + PAGE_SIZE - length, + "%s\n", + int3400_thermal_uuids[i]); + } + + return length; +} + +static ssize_t current_uuid_show(struct device *dev, + struct device_attribute *devattr, char *buf) +{ + struct platform_device *pdev = to_platform_device(dev); + struct int3400_thermal_priv *priv = platform_get_drvdata(pdev); + + if (priv->uuid_bitmap & (1 << priv->current_uuid_index)) + return sprintf(buf, "%s\n", + int3400_thermal_uuids[priv->current_uuid_index]); + else + return sprintf(buf, "INVALID\n"); +} + +static ssize_t current_uuid_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct platform_device *pdev = to_platform_device(dev); + struct int3400_thermal_priv *priv = platform_get_drvdata(pdev); + int i; + + for (i = 0; i < INT3400_THERMAL_MAXIMUM_UUID; ++i) { + if ((priv->uuid_bitmap & (1 << i)) && + !(strncmp(buf, int3400_thermal_uuids[i], + sizeof(int3400_thermal_uuids[i]) - 1))) { + priv->current_uuid_index = i; + return count; + } + } + + return -EINVAL; +} + +static DEVICE_ATTR_RW(current_uuid); +static DEVICE_ATTR_RO(available_uuids); +static struct attribute *uuid_attrs[] = { + &dev_attr_available_uuids.attr, + &dev_attr_current_uuid.attr, + NULL +}; + +static const struct attribute_group uuid_attribute_group = { + .attrs = uuid_attrs, + .name = "uuids" +}; + +static int int3400_thermal_get_uuids(struct int3400_thermal_priv *priv) +{ + struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL}; + union acpi_object *obja, *objb; + int i, j; + int result = 0; + acpi_status status; + + status = acpi_evaluate_object(priv->adev->handle, "IDSP", NULL, &buf); + if (ACPI_FAILURE(status)) + return -ENODEV; + + obja = (union acpi_object *)buf.pointer; + if (obja->type != ACPI_TYPE_PACKAGE) { + result = -EINVAL; + goto end; + } + + for (i = 0; i < obja->package.count; i++) { + objb = &obja->package.elements[i]; + if (objb->type != ACPI_TYPE_BUFFER) { + result = -EINVAL; + goto end; + } + + /* UUID must be 16 bytes */ + if (objb->buffer.length != 16) { + result = -EINVAL; + goto end; + } + + for (j = 0; j < INT3400_THERMAL_MAXIMUM_UUID; j++) { + guid_t guid; + + guid_parse(int3400_thermal_uuids[j], &guid); + if (guid_equal((guid_t *)objb->buffer.pointer, &guid)) { + priv->uuid_bitmap |= (1 << j); + break; + } + } + } + +end: + kfree(buf.pointer); + return result; +} + +static int int3400_thermal_run_osc(acpi_handle handle, + enum int3400_thermal_uuid uuid, bool enable) +{ + u32 ret, buf[2]; + acpi_status status; + int result = 0; + struct acpi_osc_context context = { + .uuid_str = int3400_thermal_uuids[uuid], + .rev = 1, + .cap.length = 8, + }; + + buf[OSC_QUERY_DWORD] = 0; + buf[OSC_SUPPORT_DWORD] = enable; + + context.cap.pointer = buf; + + status = acpi_run_osc(handle, &context); + if (ACPI_SUCCESS(status)) { + ret = *((u32 *)(context.ret.pointer + 4)); + if (ret != enable) + result = -EPERM; + } else + result = -EPERM; + + kfree(context.ret.pointer); + return result; +} + +static void int3400_notify(acpi_handle handle, + u32 event, + void *data) +{ + struct int3400_thermal_priv *priv = data; + char *thermal_prop[5]; + + if (!priv) + return; + + switch (event) { + case INT3400_THERMAL_TABLE_CHANGED: + thermal_prop[0] = kasprintf(GFP_KERNEL, "NAME=%s", + priv->thermal->type); + thermal_prop[1] = kasprintf(GFP_KERNEL, "TEMP=%d", + priv->thermal->temperature); + thermal_prop[2] = kasprintf(GFP_KERNEL, "TRIP="); + thermal_prop[3] = kasprintf(GFP_KERNEL, "EVENT=%d", + THERMAL_TABLE_CHANGED); + thermal_prop[4] = NULL; + kobject_uevent_env(&priv->thermal->device.kobj, KOBJ_CHANGE, + thermal_prop); + kfree(thermal_prop[0]); + kfree(thermal_prop[1]); + kfree(thermal_prop[2]); + kfree(thermal_prop[3]); + break; + default: + /* Ignore unknown notification codes sent to INT3400 device */ + break; + } +} + +static int int3400_thermal_get_temp(struct thermal_zone_device *thermal, + int *temp) +{ + *temp = 20 * 1000; /* faked temp sensor with 20C */ + return 0; +} + +static int int3400_thermal_get_mode(struct thermal_zone_device *thermal, + enum thermal_device_mode *mode) +{ + struct int3400_thermal_priv *priv = thermal->devdata; + + if (!priv) + return -EINVAL; + + *mode = priv->mode; + + return 0; +} + +static int int3400_thermal_set_mode(struct thermal_zone_device *thermal, + enum thermal_device_mode mode) +{ + struct int3400_thermal_priv *priv = thermal->devdata; + bool enable; + int result = 0; + + if (!priv) + return -EINVAL; + + if (mode == THERMAL_DEVICE_ENABLED) + enable = true; + else if (mode == THERMAL_DEVICE_DISABLED) + enable = false; + else + return -EINVAL; + + if (enable != priv->mode) { + priv->mode = enable; + result = int3400_thermal_run_osc(priv->adev->handle, + priv->current_uuid_index, + enable); + } + return result; +} + +static struct thermal_zone_device_ops int3400_thermal_ops = { + .get_temp = int3400_thermal_get_temp, +}; + +static struct thermal_zone_params int3400_thermal_params = { + .governor_name = "user_space", + .no_hwmon = true, +}; + +static int int3400_thermal_probe(struct platform_device *pdev) +{ + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + struct int3400_thermal_priv *priv; + int result; + + if (!adev) + return -ENODEV; + + priv = kzalloc(sizeof(struct int3400_thermal_priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->adev = adev; + + result = int3400_thermal_get_uuids(priv); + if (result) + goto free_priv; + + result = acpi_parse_art(priv->adev->handle, &priv->art_count, + &priv->arts, true); + if (result) + dev_dbg(&pdev->dev, "_ART table parsing error\n"); + + result = acpi_parse_trt(priv->adev->handle, &priv->trt_count, + &priv->trts, true); + if (result) + dev_dbg(&pdev->dev, "_TRT table parsing error\n"); + + platform_set_drvdata(pdev, priv); + + int3400_thermal_ops.get_mode = int3400_thermal_get_mode; + int3400_thermal_ops.set_mode = int3400_thermal_set_mode; + + priv->thermal = thermal_zone_device_register("INT3400 Thermal", 0, 0, + priv, &int3400_thermal_ops, + &int3400_thermal_params, 0, 0); + if (IS_ERR(priv->thermal)) { + result = PTR_ERR(priv->thermal); + goto free_art_trt; + } + + priv->rel_misc_dev_res = acpi_thermal_rel_misc_device_add( + priv->adev->handle); + + result = sysfs_create_group(&pdev->dev.kobj, &uuid_attribute_group); + if (result) + goto free_rel_misc; + + result = acpi_install_notify_handler( + priv->adev->handle, ACPI_DEVICE_NOTIFY, int3400_notify, + (void *)priv); + if (result) + goto free_sysfs; + + return 0; + +free_sysfs: + sysfs_remove_group(&pdev->dev.kobj, &uuid_attribute_group); +free_rel_misc: + if (!priv->rel_misc_dev_res) + acpi_thermal_rel_misc_device_remove(priv->adev->handle); + thermal_zone_device_unregister(priv->thermal); +free_art_trt: + kfree(priv->trts); + kfree(priv->arts); +free_priv: + kfree(priv); + return result; +} + +static int int3400_thermal_remove(struct platform_device *pdev) +{ + struct int3400_thermal_priv *priv = platform_get_drvdata(pdev); + + acpi_remove_notify_handler( + priv->adev->handle, ACPI_DEVICE_NOTIFY, + int3400_notify); + + if (!priv->rel_misc_dev_res) + acpi_thermal_rel_misc_device_remove(priv->adev->handle); + + sysfs_remove_group(&pdev->dev.kobj, &uuid_attribute_group); + thermal_zone_device_unregister(priv->thermal); + kfree(priv->trts); + kfree(priv->arts); + kfree(priv); + return 0; +} + +static const struct acpi_device_id int3400_thermal_match[] = { + {"INT3400", 0}, + {} +}; + +MODULE_DEVICE_TABLE(acpi, int3400_thermal_match); + +static struct platform_driver int3400_thermal_driver = { + .probe = int3400_thermal_probe, + .remove = int3400_thermal_remove, + .driver = { + .name = "int3400 thermal", + .acpi_match_table = ACPI_PTR(int3400_thermal_match), + }, +}; + +module_platform_driver(int3400_thermal_driver); + +MODULE_DESCRIPTION("INT3400 Thermal driver"); +MODULE_AUTHOR("Zhang Rui <rui.zhang@intel.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/thermal/int340x_thermal/int3402_thermal.c b/drivers/thermal/int340x_thermal/int3402_thermal.c new file mode 100644 index 000000000..8e90b3151 --- /dev/null +++ b/drivers/thermal/int340x_thermal/int3402_thermal.c @@ -0,0 +1,108 @@ +/* + * INT3402 thermal driver for memory temperature reporting + * + * Copyright (C) 2014, Intel Corporation + * Authors: Aaron Lu <aaron.lu@intel.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/acpi.h> +#include <linux/thermal.h> +#include "int340x_thermal_zone.h" + +#define INT3402_PERF_CHANGED_EVENT 0x80 +#define INT3402_THERMAL_EVENT 0x90 + +struct int3402_thermal_data { + acpi_handle *handle; + struct int34x_thermal_zone *int340x_zone; +}; + +static void int3402_notify(acpi_handle handle, u32 event, void *data) +{ + struct int3402_thermal_data *priv = data; + + if (!priv) + return; + + switch (event) { + case INT3402_PERF_CHANGED_EVENT: + break; + case INT3402_THERMAL_EVENT: + int340x_thermal_zone_device_update(priv->int340x_zone, + THERMAL_TRIP_VIOLATED); + break; + default: + break; + } +} + +static int int3402_thermal_probe(struct platform_device *pdev) +{ + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + struct int3402_thermal_data *d; + int ret; + + if (!acpi_has_method(adev->handle, "_TMP")) + return -ENODEV; + + d = devm_kzalloc(&pdev->dev, sizeof(*d), GFP_KERNEL); + if (!d) + return -ENOMEM; + + d->int340x_zone = int340x_thermal_zone_add(adev, NULL); + if (IS_ERR(d->int340x_zone)) + return PTR_ERR(d->int340x_zone); + + ret = acpi_install_notify_handler(adev->handle, + ACPI_DEVICE_NOTIFY, + int3402_notify, + d); + if (ret) { + int340x_thermal_zone_remove(d->int340x_zone); + return ret; + } + + d->handle = adev->handle; + platform_set_drvdata(pdev, d); + + return 0; +} + +static int int3402_thermal_remove(struct platform_device *pdev) +{ + struct int3402_thermal_data *d = platform_get_drvdata(pdev); + + acpi_remove_notify_handler(d->handle, + ACPI_DEVICE_NOTIFY, int3402_notify); + int340x_thermal_zone_remove(d->int340x_zone); + + return 0; +} + +static const struct acpi_device_id int3402_thermal_match[] = { + {"INT3402", 0}, + {} +}; + +MODULE_DEVICE_TABLE(acpi, int3402_thermal_match); + +static struct platform_driver int3402_thermal_driver = { + .probe = int3402_thermal_probe, + .remove = int3402_thermal_remove, + .driver = { + .name = "int3402 thermal", + .acpi_match_table = int3402_thermal_match, + }, +}; + +module_platform_driver(int3402_thermal_driver); + +MODULE_DESCRIPTION("INT3402 Thermal driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/thermal/int340x_thermal/int3403_thermal.c b/drivers/thermal/int340x_thermal/int3403_thermal.c new file mode 100644 index 000000000..0c19fcd56 --- /dev/null +++ b/drivers/thermal/int340x_thermal/int3403_thermal.c @@ -0,0 +1,311 @@ +/* + * ACPI INT3403 thermal driver + * Copyright (c) 2013, Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/types.h> +#include <linux/acpi.h> +#include <linux/thermal.h> +#include <linux/platform_device.h> +#include "int340x_thermal_zone.h" + +#define INT3403_TYPE_SENSOR 0x03 +#define INT3403_TYPE_CHARGER 0x0B +#define INT3403_TYPE_BATTERY 0x0C +#define INT3403_PERF_CHANGED_EVENT 0x80 +#define INT3403_PERF_TRIP_POINT_CHANGED 0x81 +#define INT3403_THERMAL_EVENT 0x90 + +/* Preserved structure for future expandbility */ +struct int3403_sensor { + struct int34x_thermal_zone *int340x_zone; +}; + +struct int3403_performance_state { + u64 performance; + u64 power; + u64 latency; + u64 linear; + u64 control; + u64 raw_performace; + char *raw_unit; + int reserved; +}; + +struct int3403_cdev { + struct thermal_cooling_device *cdev; + unsigned long max_state; +}; + +struct int3403_priv { + struct platform_device *pdev; + struct acpi_device *adev; + unsigned long long type; + void *priv; +}; + +static void int3403_notify(acpi_handle handle, + u32 event, void *data) +{ + struct int3403_priv *priv = data; + struct int3403_sensor *obj; + + if (!priv) + return; + + obj = priv->priv; + if (priv->type != INT3403_TYPE_SENSOR || !obj) + return; + + switch (event) { + case INT3403_PERF_CHANGED_EVENT: + break; + case INT3403_THERMAL_EVENT: + int340x_thermal_zone_device_update(obj->int340x_zone, + THERMAL_TRIP_VIOLATED); + break; + case INT3403_PERF_TRIP_POINT_CHANGED: + int340x_thermal_read_trips(obj->int340x_zone); + int340x_thermal_zone_device_update(obj->int340x_zone, + THERMAL_TRIP_CHANGED); + break; + default: + dev_err(&priv->pdev->dev, "Unsupported event [0x%x]\n", event); + break; + } +} + +static int int3403_sensor_add(struct int3403_priv *priv) +{ + int result = 0; + struct int3403_sensor *obj; + + obj = devm_kzalloc(&priv->pdev->dev, sizeof(*obj), GFP_KERNEL); + if (!obj) + return -ENOMEM; + + priv->priv = obj; + + obj->int340x_zone = int340x_thermal_zone_add(priv->adev, NULL); + if (IS_ERR(obj->int340x_zone)) + return PTR_ERR(obj->int340x_zone); + + result = acpi_install_notify_handler(priv->adev->handle, + ACPI_DEVICE_NOTIFY, int3403_notify, + (void *)priv); + if (result) + goto err_free_obj; + + return 0; + + err_free_obj: + int340x_thermal_zone_remove(obj->int340x_zone); + return result; +} + +static int int3403_sensor_remove(struct int3403_priv *priv) +{ + struct int3403_sensor *obj = priv->priv; + + acpi_remove_notify_handler(priv->adev->handle, + ACPI_DEVICE_NOTIFY, int3403_notify); + int340x_thermal_zone_remove(obj->int340x_zone); + + return 0; +} + +/* INT3403 Cooling devices */ +static int int3403_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct int3403_priv *priv = cdev->devdata; + struct int3403_cdev *obj = priv->priv; + + *state = obj->max_state; + return 0; +} + +static int int3403_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + struct int3403_priv *priv = cdev->devdata; + unsigned long long level; + acpi_status status; + + status = acpi_evaluate_integer(priv->adev->handle, "PPPC", NULL, &level); + if (ACPI_SUCCESS(status)) { + *state = level; + return 0; + } else + return -EINVAL; +} + +static int +int3403_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) +{ + struct int3403_priv *priv = cdev->devdata; + acpi_status status; + + status = acpi_execute_simple_method(priv->adev->handle, "SPPC", state); + if (ACPI_SUCCESS(status)) + return 0; + else + return -EINVAL; +} + +static const struct thermal_cooling_device_ops int3403_cooling_ops = { + .get_max_state = int3403_get_max_state, + .get_cur_state = int3403_get_cur_state, + .set_cur_state = int3403_set_cur_state, +}; + +static int int3403_cdev_add(struct int3403_priv *priv) +{ + int result = 0; + acpi_status status; + struct int3403_cdev *obj; + struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *p; + + obj = devm_kzalloc(&priv->pdev->dev, sizeof(*obj), GFP_KERNEL); + if (!obj) + return -ENOMEM; + + status = acpi_evaluate_object(priv->adev->handle, "PPSS", NULL, &buf); + if (ACPI_FAILURE(status)) + return -ENODEV; + + p = buf.pointer; + if (!p || (p->type != ACPI_TYPE_PACKAGE)) { + printk(KERN_WARNING "Invalid PPSS data\n"); + kfree(buf.pointer); + return -EFAULT; + } + + priv->priv = obj; + obj->max_state = p->package.count - 1; + obj->cdev = + thermal_cooling_device_register(acpi_device_bid(priv->adev), + priv, &int3403_cooling_ops); + if (IS_ERR(obj->cdev)) + result = PTR_ERR(obj->cdev); + + kfree(buf.pointer); + /* TODO: add ACPI notification support */ + + return result; +} + +static int int3403_cdev_remove(struct int3403_priv *priv) +{ + struct int3403_cdev *obj = priv->priv; + + thermal_cooling_device_unregister(obj->cdev); + return 0; +} + +static int int3403_add(struct platform_device *pdev) +{ + struct int3403_priv *priv; + int result = 0; + acpi_status status; + + priv = devm_kzalloc(&pdev->dev, sizeof(struct int3403_priv), + GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->pdev = pdev; + priv->adev = ACPI_COMPANION(&(pdev->dev)); + if (!priv->adev) { + result = -EINVAL; + goto err; + } + + status = acpi_evaluate_integer(priv->adev->handle, "PTYP", + NULL, &priv->type); + if (ACPI_FAILURE(status)) { + unsigned long long tmp; + + status = acpi_evaluate_integer(priv->adev->handle, "_TMP", + NULL, &tmp); + if (ACPI_FAILURE(status)) { + result = -EINVAL; + goto err; + } else { + priv->type = INT3403_TYPE_SENSOR; + } + } + + platform_set_drvdata(pdev, priv); + switch (priv->type) { + case INT3403_TYPE_SENSOR: + result = int3403_sensor_add(priv); + break; + case INT3403_TYPE_CHARGER: + case INT3403_TYPE_BATTERY: + result = int3403_cdev_add(priv); + break; + default: + result = -EINVAL; + } + + if (result) + goto err; + return result; + +err: + return result; +} + +static int int3403_remove(struct platform_device *pdev) +{ + struct int3403_priv *priv = platform_get_drvdata(pdev); + + switch (priv->type) { + case INT3403_TYPE_SENSOR: + int3403_sensor_remove(priv); + break; + case INT3403_TYPE_CHARGER: + case INT3403_TYPE_BATTERY: + int3403_cdev_remove(priv); + break; + default: + break; + } + + return 0; +} + +static const struct acpi_device_id int3403_device_ids[] = { + {"INT3403", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, int3403_device_ids); + +static struct platform_driver int3403_driver = { + .probe = int3403_add, + .remove = int3403_remove, + .driver = { + .name = "int3403 thermal", + .acpi_match_table = int3403_device_ids, + }, +}; + +module_platform_driver(int3403_driver); + +MODULE_AUTHOR("Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com>"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("ACPI INT3403 thermal driver"); diff --git a/drivers/thermal/int340x_thermal/int3406_thermal.c b/drivers/thermal/int340x_thermal/int3406_thermal.c new file mode 100644 index 000000000..f69ab026b --- /dev/null +++ b/drivers/thermal/int340x_thermal/int3406_thermal.c @@ -0,0 +1,213 @@ +/* + * INT3406 thermal driver for display participant device + * + * Copyright (C) 2016, Intel Corporation + * Authors: Aaron Lu <aaron.lu@intel.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/acpi.h> +#include <linux/backlight.h> +#include <linux/thermal.h> +#include <acpi/video.h> + +#define INT3406_BRIGHTNESS_LIMITS_CHANGED 0x80 + +struct int3406_thermal_data { + int upper_limit; + int lower_limit; + acpi_handle handle; + struct acpi_video_device_brightness *br; + struct backlight_device *raw_bd; + struct thermal_cooling_device *cooling_dev; +}; + +/* + * According to the ACPI spec, + * "Each brightness level is represented by a number between 0 and 100, + * and can be thought of as a percentage. For example, 50 can be 50% + * power consumption or 50% brightness, as defined by the OEM." + * + * As int3406 device uses this value to communicate with the native + * graphics driver, we make the assumption that it represents + * the percentage of brightness only + */ +#define ACPI_TO_RAW(v, d) (d->raw_bd->props.max_brightness * v / 100) +#define RAW_TO_ACPI(v, d) (v * 100 / d->raw_bd->props.max_brightness) + +static int +int3406_thermal_get_max_state(struct thermal_cooling_device *cooling_dev, + unsigned long *state) +{ + struct int3406_thermal_data *d = cooling_dev->devdata; + + *state = d->upper_limit - d->lower_limit; + return 0; +} + +static int +int3406_thermal_set_cur_state(struct thermal_cooling_device *cooling_dev, + unsigned long state) +{ + struct int3406_thermal_data *d = cooling_dev->devdata; + int acpi_level, raw_level; + + if (state > d->upper_limit - d->lower_limit) + return -EINVAL; + + acpi_level = d->br->levels[d->upper_limit - state]; + + raw_level = ACPI_TO_RAW(acpi_level, d); + + return backlight_device_set_brightness(d->raw_bd, raw_level); +} + +static int +int3406_thermal_get_cur_state(struct thermal_cooling_device *cooling_dev, + unsigned long *state) +{ + struct int3406_thermal_data *d = cooling_dev->devdata; + int acpi_level; + int index; + + acpi_level = RAW_TO_ACPI(d->raw_bd->props.brightness, d); + + /* + * There is no 1:1 mapping between the firmware interface level + * with the raw interface level, we will have to find one that is + * right above it. + */ + for (index = d->lower_limit; index < d->upper_limit; index++) { + if (acpi_level <= d->br->levels[index]) + break; + } + + *state = d->upper_limit - index; + return 0; +} + +static const struct thermal_cooling_device_ops video_cooling_ops = { + .get_max_state = int3406_thermal_get_max_state, + .get_cur_state = int3406_thermal_get_cur_state, + .set_cur_state = int3406_thermal_set_cur_state, +}; + +static int int3406_thermal_get_index(int *array, int nr, int value) +{ + int i; + + for (i = 2; i < nr; i++) { + if (array[i] == value) + break; + } + return i == nr ? -ENOENT : i; +} + +static void int3406_thermal_get_limit(struct int3406_thermal_data *d) +{ + acpi_status status; + unsigned long long lower_limit, upper_limit; + + status = acpi_evaluate_integer(d->handle, "DDDL", NULL, &lower_limit); + if (ACPI_SUCCESS(status)) + d->lower_limit = int3406_thermal_get_index(d->br->levels, + d->br->count, lower_limit); + + status = acpi_evaluate_integer(d->handle, "DDPC", NULL, &upper_limit); + if (ACPI_SUCCESS(status)) + d->upper_limit = int3406_thermal_get_index(d->br->levels, + d->br->count, upper_limit); + + /* lower_limit and upper_limit should be always set */ + d->lower_limit = d->lower_limit > 0 ? d->lower_limit : 2; + d->upper_limit = d->upper_limit > 0 ? d->upper_limit : d->br->count - 1; +} + +static void int3406_notify(acpi_handle handle, u32 event, void *data) +{ + if (event == INT3406_BRIGHTNESS_LIMITS_CHANGED) + int3406_thermal_get_limit(data); +} + +static int int3406_thermal_probe(struct platform_device *pdev) +{ + struct acpi_device *adev = ACPI_COMPANION(&pdev->dev); + struct int3406_thermal_data *d; + struct backlight_device *bd; + int ret; + + if (!ACPI_HANDLE(&pdev->dev)) + return -ENODEV; + + d = devm_kzalloc(&pdev->dev, sizeof(*d), GFP_KERNEL); + if (!d) + return -ENOMEM; + d->handle = ACPI_HANDLE(&pdev->dev); + + bd = backlight_device_get_by_type(BACKLIGHT_RAW); + if (!bd) + return -ENODEV; + d->raw_bd = bd; + + ret = acpi_video_get_levels(ACPI_COMPANION(&pdev->dev), &d->br, NULL); + if (ret) + return ret; + + int3406_thermal_get_limit(d); + + d->cooling_dev = thermal_cooling_device_register(acpi_device_bid(adev), + d, &video_cooling_ops); + if (IS_ERR(d->cooling_dev)) + goto err; + + ret = acpi_install_notify_handler(adev->handle, ACPI_DEVICE_NOTIFY, + int3406_notify, d); + if (ret) + goto err_cdev; + + platform_set_drvdata(pdev, d); + + return 0; + +err_cdev: + thermal_cooling_device_unregister(d->cooling_dev); +err: + kfree(d->br); + return -ENODEV; +} + +static int int3406_thermal_remove(struct platform_device *pdev) +{ + struct int3406_thermal_data *d = platform_get_drvdata(pdev); + + thermal_cooling_device_unregister(d->cooling_dev); + kfree(d->br); + return 0; +} + +static const struct acpi_device_id int3406_thermal_match[] = { + {"INT3406", 0}, + {} +}; + +MODULE_DEVICE_TABLE(acpi, int3406_thermal_match); + +static struct platform_driver int3406_thermal_driver = { + .probe = int3406_thermal_probe, + .remove = int3406_thermal_remove, + .driver = { + .name = "int3406 thermal", + .acpi_match_table = int3406_thermal_match, + }, +}; + +module_platform_driver(int3406_thermal_driver); + +MODULE_DESCRIPTION("INT3406 Thermal driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/int340x_thermal/int340x_thermal_zone.c b/drivers/thermal/int340x_thermal/int340x_thermal_zone.c new file mode 100644 index 000000000..9ec27ac18 --- /dev/null +++ b/drivers/thermal/int340x_thermal/int340x_thermal_zone.c @@ -0,0 +1,295 @@ +/* + * int340x_thermal_zone.c + * Copyright (c) 2015, Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + */ +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/acpi.h> +#include <linux/thermal.h> +#include "int340x_thermal_zone.h" + +static int int340x_thermal_get_zone_temp(struct thermal_zone_device *zone, + int *temp) +{ + struct int34x_thermal_zone *d = zone->devdata; + unsigned long long tmp; + acpi_status status; + + if (d->override_ops && d->override_ops->get_temp) + return d->override_ops->get_temp(zone, temp); + + status = acpi_evaluate_integer(d->adev->handle, "_TMP", NULL, &tmp); + if (ACPI_FAILURE(status)) + return -EIO; + + if (d->lpat_table) { + int conv_temp; + + conv_temp = acpi_lpat_raw_to_temp(d->lpat_table, (int)tmp); + if (conv_temp < 0) + return conv_temp; + + *temp = (unsigned long)conv_temp * 10; + } else + /* _TMP returns the temperature in tenths of degrees Kelvin */ + *temp = DECI_KELVIN_TO_MILLICELSIUS(tmp); + + return 0; +} + +static int int340x_thermal_get_trip_temp(struct thermal_zone_device *zone, + int trip, int *temp) +{ + struct int34x_thermal_zone *d = zone->devdata; + int i; + + if (d->override_ops && d->override_ops->get_trip_temp) + return d->override_ops->get_trip_temp(zone, trip, temp); + + if (trip < d->aux_trip_nr) + *temp = d->aux_trips[trip]; + else if (trip == d->crt_trip_id) + *temp = d->crt_temp; + else if (trip == d->psv_trip_id) + *temp = d->psv_temp; + else if (trip == d->hot_trip_id) + *temp = d->hot_temp; + else { + for (i = 0; i < INT340X_THERMAL_MAX_ACT_TRIP_COUNT; i++) { + if (d->act_trips[i].valid && + d->act_trips[i].id == trip) { + *temp = d->act_trips[i].temp; + break; + } + } + if (i == INT340X_THERMAL_MAX_ACT_TRIP_COUNT) + return -EINVAL; + } + + return 0; +} + +static int int340x_thermal_get_trip_type(struct thermal_zone_device *zone, + int trip, + enum thermal_trip_type *type) +{ + struct int34x_thermal_zone *d = zone->devdata; + int i; + + if (d->override_ops && d->override_ops->get_trip_type) + return d->override_ops->get_trip_type(zone, trip, type); + + if (trip < d->aux_trip_nr) + *type = THERMAL_TRIP_PASSIVE; + else if (trip == d->crt_trip_id) + *type = THERMAL_TRIP_CRITICAL; + else if (trip == d->hot_trip_id) + *type = THERMAL_TRIP_HOT; + else if (trip == d->psv_trip_id) + *type = THERMAL_TRIP_PASSIVE; + else { + for (i = 0; i < INT340X_THERMAL_MAX_ACT_TRIP_COUNT; i++) { + if (d->act_trips[i].valid && + d->act_trips[i].id == trip) { + *type = THERMAL_TRIP_ACTIVE; + break; + } + } + if (i == INT340X_THERMAL_MAX_ACT_TRIP_COUNT) + return -EINVAL; + } + + return 0; +} + +static int int340x_thermal_set_trip_temp(struct thermal_zone_device *zone, + int trip, int temp) +{ + struct int34x_thermal_zone *d = zone->devdata; + acpi_status status; + char name[10]; + + if (d->override_ops && d->override_ops->set_trip_temp) + return d->override_ops->set_trip_temp(zone, trip, temp); + + snprintf(name, sizeof(name), "PAT%d", trip); + status = acpi_execute_simple_method(d->adev->handle, name, + MILLICELSIUS_TO_DECI_KELVIN(temp)); + if (ACPI_FAILURE(status)) + return -EIO; + + d->aux_trips[trip] = temp; + + return 0; +} + + +static int int340x_thermal_get_trip_hyst(struct thermal_zone_device *zone, + int trip, int *temp) +{ + struct int34x_thermal_zone *d = zone->devdata; + acpi_status status; + unsigned long long hyst; + + if (d->override_ops && d->override_ops->get_trip_hyst) + return d->override_ops->get_trip_hyst(zone, trip, temp); + + status = acpi_evaluate_integer(d->adev->handle, "GTSH", NULL, &hyst); + if (ACPI_FAILURE(status)) + *temp = 0; + else + *temp = hyst * 100; + + return 0; +} + +static struct thermal_zone_device_ops int340x_thermal_zone_ops = { + .get_temp = int340x_thermal_get_zone_temp, + .get_trip_temp = int340x_thermal_get_trip_temp, + .get_trip_type = int340x_thermal_get_trip_type, + .set_trip_temp = int340x_thermal_set_trip_temp, + .get_trip_hyst = int340x_thermal_get_trip_hyst, +}; + +static int int340x_thermal_get_trip_config(acpi_handle handle, char *name, + int *temp) +{ + unsigned long long r; + acpi_status status; + + status = acpi_evaluate_integer(handle, name, NULL, &r); + if (ACPI_FAILURE(status)) + return -EIO; + + *temp = DECI_KELVIN_TO_MILLICELSIUS(r); + + return 0; +} + +int int340x_thermal_read_trips(struct int34x_thermal_zone *int34x_zone) +{ + int trip_cnt = int34x_zone->aux_trip_nr; + int i; + + int34x_zone->crt_trip_id = -1; + if (!int340x_thermal_get_trip_config(int34x_zone->adev->handle, "_CRT", + &int34x_zone->crt_temp)) + int34x_zone->crt_trip_id = trip_cnt++; + + int34x_zone->hot_trip_id = -1; + if (!int340x_thermal_get_trip_config(int34x_zone->adev->handle, "_HOT", + &int34x_zone->hot_temp)) + int34x_zone->hot_trip_id = trip_cnt++; + + int34x_zone->psv_trip_id = -1; + if (!int340x_thermal_get_trip_config(int34x_zone->adev->handle, "_PSV", + &int34x_zone->psv_temp)) + int34x_zone->psv_trip_id = trip_cnt++; + + for (i = 0; i < INT340X_THERMAL_MAX_ACT_TRIP_COUNT; i++) { + char name[5] = { '_', 'A', 'C', '0' + i, '\0' }; + + if (int340x_thermal_get_trip_config(int34x_zone->adev->handle, + name, + &int34x_zone->act_trips[i].temp)) + break; + + int34x_zone->act_trips[i].id = trip_cnt++; + int34x_zone->act_trips[i].valid = true; + } + + return trip_cnt; +} +EXPORT_SYMBOL_GPL(int340x_thermal_read_trips); + +static struct thermal_zone_params int340x_thermal_params = { + .governor_name = "user_space", + .no_hwmon = true, +}; + +struct int34x_thermal_zone *int340x_thermal_zone_add(struct acpi_device *adev, + struct thermal_zone_device_ops *override_ops) +{ + struct int34x_thermal_zone *int34x_thermal_zone; + acpi_status status; + unsigned long long trip_cnt; + int trip_mask = 0; + int ret; + + int34x_thermal_zone = kzalloc(sizeof(*int34x_thermal_zone), + GFP_KERNEL); + if (!int34x_thermal_zone) + return ERR_PTR(-ENOMEM); + + int34x_thermal_zone->adev = adev; + int34x_thermal_zone->override_ops = override_ops; + + status = acpi_evaluate_integer(adev->handle, "PATC", NULL, &trip_cnt); + if (ACPI_FAILURE(status)) + trip_cnt = 0; + else { + int34x_thermal_zone->aux_trips = + kcalloc(trip_cnt, + sizeof(*int34x_thermal_zone->aux_trips), + GFP_KERNEL); + if (!int34x_thermal_zone->aux_trips) { + ret = -ENOMEM; + goto err_trip_alloc; + } + trip_mask = BIT(trip_cnt) - 1; + int34x_thermal_zone->aux_trip_nr = trip_cnt; + } + + trip_cnt = int340x_thermal_read_trips(int34x_thermal_zone); + + int34x_thermal_zone->lpat_table = acpi_lpat_get_conversion_table( + adev->handle); + + int34x_thermal_zone->zone = thermal_zone_device_register( + acpi_device_bid(adev), + trip_cnt, + trip_mask, int34x_thermal_zone, + &int340x_thermal_zone_ops, + &int340x_thermal_params, + 0, 0); + if (IS_ERR(int34x_thermal_zone->zone)) { + ret = PTR_ERR(int34x_thermal_zone->zone); + goto err_thermal_zone; + } + + return int34x_thermal_zone; + +err_thermal_zone: + acpi_lpat_free_conversion_table(int34x_thermal_zone->lpat_table); + kfree(int34x_thermal_zone->aux_trips); +err_trip_alloc: + kfree(int34x_thermal_zone); + return ERR_PTR(ret); +} +EXPORT_SYMBOL_GPL(int340x_thermal_zone_add); + +void int340x_thermal_zone_remove(struct int34x_thermal_zone + *int34x_thermal_zone) +{ + thermal_zone_device_unregister(int34x_thermal_zone->zone); + acpi_lpat_free_conversion_table(int34x_thermal_zone->lpat_table); + kfree(int34x_thermal_zone->aux_trips); + kfree(int34x_thermal_zone); +} +EXPORT_SYMBOL_GPL(int340x_thermal_zone_remove); + +MODULE_AUTHOR("Aaron Lu <aaron.lu@intel.com>"); +MODULE_AUTHOR("Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com>"); +MODULE_DESCRIPTION("Intel INT340x common thermal zone handler"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/int340x_thermal/int340x_thermal_zone.h b/drivers/thermal/int340x_thermal/int340x_thermal_zone.h new file mode 100644 index 000000000..5f3ba4775 --- /dev/null +++ b/drivers/thermal/int340x_thermal/int340x_thermal_zone.h @@ -0,0 +1,70 @@ +/* + * int340x_thermal_zone.h + * Copyright (c) 2015, Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + */ + +#ifndef __INT340X_THERMAL_ZONE_H__ +#define __INT340X_THERMAL_ZONE_H__ + +#include <acpi/acpi_lpat.h> + +#define INT340X_THERMAL_MAX_ACT_TRIP_COUNT 10 + +struct active_trip { + int temp; + int id; + bool valid; +}; + +struct int34x_thermal_zone { + struct acpi_device *adev; + struct active_trip act_trips[INT340X_THERMAL_MAX_ACT_TRIP_COUNT]; + unsigned long *aux_trips; + int aux_trip_nr; + int psv_temp; + int psv_trip_id; + int crt_temp; + int crt_trip_id; + int hot_temp; + int hot_trip_id; + struct thermal_zone_device *zone; + struct thermal_zone_device_ops *override_ops; + void *priv_data; + struct acpi_lpat_conversion_table *lpat_table; +}; + +struct int34x_thermal_zone *int340x_thermal_zone_add(struct acpi_device *, + struct thermal_zone_device_ops *override_ops); +void int340x_thermal_zone_remove(struct int34x_thermal_zone *); +int int340x_thermal_read_trips(struct int34x_thermal_zone *int34x_zone); + +static inline void int340x_thermal_zone_set_priv_data( + struct int34x_thermal_zone *tzone, void *priv_data) +{ + tzone->priv_data = priv_data; +} + +static inline void *int340x_thermal_zone_get_priv_data( + struct int34x_thermal_zone *tzone) +{ + return tzone->priv_data; +} + +static inline void int340x_thermal_zone_device_update( + struct int34x_thermal_zone *tzone, + enum thermal_notify_event event) +{ + thermal_zone_device_update(tzone->zone, event); +} + +#endif diff --git a/drivers/thermal/int340x_thermal/processor_thermal_device.c b/drivers/thermal/int340x_thermal/processor_thermal_device.c new file mode 100644 index 000000000..8e1cf4d78 --- /dev/null +++ b/drivers/thermal/int340x_thermal/processor_thermal_device.c @@ -0,0 +1,527 @@ +/* + * processor_thermal_device.c + * Copyright (c) 2014, Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + */ +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/pci.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/acpi.h> +#include <linux/thermal.h> +#include "int340x_thermal_zone.h" +#include "../intel_soc_dts_iosf.h" + +/* Broadwell-U/HSB thermal reporting device */ +#define PCI_DEVICE_ID_PROC_BDW_THERMAL 0x1603 +#define PCI_DEVICE_ID_PROC_HSB_THERMAL 0x0A03 + +/* Skylake thermal reporting device */ +#define PCI_DEVICE_ID_PROC_SKL_THERMAL 0x1903 + +/* CannonLake thermal reporting device */ +#define PCI_DEVICE_ID_PROC_CNL_THERMAL 0x5a03 +#define PCI_DEVICE_ID_PROC_CFL_THERMAL 0x3E83 + +/* Braswell thermal reporting device */ +#define PCI_DEVICE_ID_PROC_BSW_THERMAL 0x22DC + +/* Broxton thermal reporting device */ +#define PCI_DEVICE_ID_PROC_BXT0_THERMAL 0x0A8C +#define PCI_DEVICE_ID_PROC_BXT1_THERMAL 0x1A8C +#define PCI_DEVICE_ID_PROC_BXTX_THERMAL 0x4A8C +#define PCI_DEVICE_ID_PROC_BXTP_THERMAL 0x5A8C + +/* GeminiLake thermal reporting device */ +#define PCI_DEVICE_ID_PROC_GLK_THERMAL 0x318C + +struct power_config { + u32 index; + u32 min_uw; + u32 max_uw; + u32 tmin_us; + u32 tmax_us; + u32 step_uw; +}; + +struct proc_thermal_device { + struct device *dev; + struct acpi_device *adev; + struct power_config power_limits[2]; + struct int34x_thermal_zone *int340x_zone; + struct intel_soc_dts_sensors *soc_dts; +}; + +enum proc_thermal_emum_mode_type { + PROC_THERMAL_NONE, + PROC_THERMAL_PCI, + PROC_THERMAL_PLATFORM_DEV +}; + +/* + * We can have only one type of enumeration, PCI or Platform, + * not both. So we don't need instance specific data. + */ +static enum proc_thermal_emum_mode_type proc_thermal_emum_mode = + PROC_THERMAL_NONE; + +#define POWER_LIMIT_SHOW(index, suffix) \ +static ssize_t power_limit_##index##_##suffix##_show(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ +{ \ + struct pci_dev *pci_dev; \ + struct platform_device *pdev; \ + struct proc_thermal_device *proc_dev; \ + \ + if (proc_thermal_emum_mode == PROC_THERMAL_NONE) { \ + dev_warn(dev, "Attempted to get power limit before device was initialized!\n"); \ + return 0; \ + } \ + \ + if (proc_thermal_emum_mode == PROC_THERMAL_PLATFORM_DEV) { \ + pdev = to_platform_device(dev); \ + proc_dev = platform_get_drvdata(pdev); \ + } else { \ + pci_dev = to_pci_dev(dev); \ + proc_dev = pci_get_drvdata(pci_dev); \ + } \ + return sprintf(buf, "%lu\n",\ + (unsigned long)proc_dev->power_limits[index].suffix * 1000); \ +} + +POWER_LIMIT_SHOW(0, min_uw) +POWER_LIMIT_SHOW(0, max_uw) +POWER_LIMIT_SHOW(0, step_uw) +POWER_LIMIT_SHOW(0, tmin_us) +POWER_LIMIT_SHOW(0, tmax_us) + +POWER_LIMIT_SHOW(1, min_uw) +POWER_LIMIT_SHOW(1, max_uw) +POWER_LIMIT_SHOW(1, step_uw) +POWER_LIMIT_SHOW(1, tmin_us) +POWER_LIMIT_SHOW(1, tmax_us) + +static DEVICE_ATTR_RO(power_limit_0_min_uw); +static DEVICE_ATTR_RO(power_limit_0_max_uw); +static DEVICE_ATTR_RO(power_limit_0_step_uw); +static DEVICE_ATTR_RO(power_limit_0_tmin_us); +static DEVICE_ATTR_RO(power_limit_0_tmax_us); + +static DEVICE_ATTR_RO(power_limit_1_min_uw); +static DEVICE_ATTR_RO(power_limit_1_max_uw); +static DEVICE_ATTR_RO(power_limit_1_step_uw); +static DEVICE_ATTR_RO(power_limit_1_tmin_us); +static DEVICE_ATTR_RO(power_limit_1_tmax_us); + +static struct attribute *power_limit_attrs[] = { + &dev_attr_power_limit_0_min_uw.attr, + &dev_attr_power_limit_1_min_uw.attr, + &dev_attr_power_limit_0_max_uw.attr, + &dev_attr_power_limit_1_max_uw.attr, + &dev_attr_power_limit_0_step_uw.attr, + &dev_attr_power_limit_1_step_uw.attr, + &dev_attr_power_limit_0_tmin_us.attr, + &dev_attr_power_limit_1_tmin_us.attr, + &dev_attr_power_limit_0_tmax_us.attr, + &dev_attr_power_limit_1_tmax_us.attr, + NULL +}; + +static const struct attribute_group power_limit_attribute_group = { + .attrs = power_limit_attrs, + .name = "power_limits" +}; + +static int stored_tjmax; /* since it is fixed, we can have local storage */ + +static int get_tjmax(void) +{ + u32 eax, edx; + u32 val; + int err; + + err = rdmsr_safe(MSR_IA32_TEMPERATURE_TARGET, &eax, &edx); + if (err) + return err; + + val = (eax >> 16) & 0xff; + if (val) + return val; + + return -EINVAL; +} + +static int read_temp_msr(int *temp) +{ + int cpu; + u32 eax, edx; + int err; + unsigned long curr_temp_off = 0; + + *temp = 0; + + for_each_online_cpu(cpu) { + err = rdmsr_safe_on_cpu(cpu, MSR_IA32_THERM_STATUS, &eax, + &edx); + if (err) + goto err_ret; + else { + if (eax & 0x80000000) { + curr_temp_off = (eax >> 16) & 0x7f; + if (!*temp || curr_temp_off < *temp) + *temp = curr_temp_off; + } else { + err = -EINVAL; + goto err_ret; + } + } + } + + return 0; +err_ret: + return err; +} + +static int proc_thermal_get_zone_temp(struct thermal_zone_device *zone, + int *temp) +{ + int ret; + + ret = read_temp_msr(temp); + if (!ret) + *temp = (stored_tjmax - *temp) * 1000; + + return ret; +} + +static struct thermal_zone_device_ops proc_thermal_local_ops = { + .get_temp = proc_thermal_get_zone_temp, +}; + +static int proc_thermal_read_ppcc(struct proc_thermal_device *proc_priv) +{ + int i; + acpi_status status; + struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL }; + union acpi_object *elements, *ppcc; + union acpi_object *p; + int ret = 0; + + status = acpi_evaluate_object(proc_priv->adev->handle, "PPCC", + NULL, &buf); + if (ACPI_FAILURE(status)) + return -ENODEV; + + p = buf.pointer; + if (!p || (p->type != ACPI_TYPE_PACKAGE)) { + dev_err(proc_priv->dev, "Invalid PPCC data\n"); + ret = -EFAULT; + goto free_buffer; + } + + if (!p->package.count) { + dev_err(proc_priv->dev, "Invalid PPCC package size\n"); + ret = -EFAULT; + goto free_buffer; + } + + for (i = 0; i < min((int)p->package.count - 1, 2); ++i) { + elements = &(p->package.elements[i+1]); + if (elements->type != ACPI_TYPE_PACKAGE || + elements->package.count != 6) { + ret = -EFAULT; + goto free_buffer; + } + ppcc = elements->package.elements; + proc_priv->power_limits[i].index = ppcc[0].integer.value; + proc_priv->power_limits[i].min_uw = ppcc[1].integer.value; + proc_priv->power_limits[i].max_uw = ppcc[2].integer.value; + proc_priv->power_limits[i].tmin_us = ppcc[3].integer.value; + proc_priv->power_limits[i].tmax_us = ppcc[4].integer.value; + proc_priv->power_limits[i].step_uw = ppcc[5].integer.value; + } + +free_buffer: + kfree(buf.pointer); + + return ret; +} + +#define PROC_POWER_CAPABILITY_CHANGED 0x83 +static void proc_thermal_notify(acpi_handle handle, u32 event, void *data) +{ + struct proc_thermal_device *proc_priv = data; + + if (!proc_priv) + return; + + switch (event) { + case PROC_POWER_CAPABILITY_CHANGED: + proc_thermal_read_ppcc(proc_priv); + int340x_thermal_zone_device_update(proc_priv->int340x_zone, + THERMAL_DEVICE_POWER_CAPABILITY_CHANGED); + break; + default: + dev_err(proc_priv->dev, "Unsupported event [0x%x]\n", event); + break; + } +} + + +static int proc_thermal_add(struct device *dev, + struct proc_thermal_device **priv) +{ + struct proc_thermal_device *proc_priv; + struct acpi_device *adev; + acpi_status status; + unsigned long long tmp; + struct thermal_zone_device_ops *ops = NULL; + int ret; + + adev = ACPI_COMPANION(dev); + if (!adev) + return -ENODEV; + + proc_priv = devm_kzalloc(dev, sizeof(*proc_priv), GFP_KERNEL); + if (!proc_priv) + return -ENOMEM; + + proc_priv->dev = dev; + proc_priv->adev = adev; + *priv = proc_priv; + + ret = proc_thermal_read_ppcc(proc_priv); + if (ret) + return ret; + + status = acpi_evaluate_integer(adev->handle, "_TMP", NULL, &tmp); + if (ACPI_FAILURE(status)) { + /* there is no _TMP method, add local method */ + stored_tjmax = get_tjmax(); + if (stored_tjmax > 0) + ops = &proc_thermal_local_ops; + } + + proc_priv->int340x_zone = int340x_thermal_zone_add(adev, ops); + if (IS_ERR(proc_priv->int340x_zone)) { + return PTR_ERR(proc_priv->int340x_zone); + } else + ret = 0; + + ret = acpi_install_notify_handler(adev->handle, ACPI_DEVICE_NOTIFY, + proc_thermal_notify, + (void *)proc_priv); + if (ret) + goto remove_zone; + + return 0; + +remove_zone: + int340x_thermal_zone_remove(proc_priv->int340x_zone); + + return ret; +} + +static void proc_thermal_remove(struct proc_thermal_device *proc_priv) +{ + acpi_remove_notify_handler(proc_priv->adev->handle, + ACPI_DEVICE_NOTIFY, proc_thermal_notify); + int340x_thermal_zone_remove(proc_priv->int340x_zone); + sysfs_remove_group(&proc_priv->dev->kobj, + &power_limit_attribute_group); +} + +static int int3401_add(struct platform_device *pdev) +{ + struct proc_thermal_device *proc_priv; + int ret; + + if (proc_thermal_emum_mode == PROC_THERMAL_PCI) { + dev_err(&pdev->dev, "error: enumerated as PCI dev\n"); + return -ENODEV; + } + + ret = proc_thermal_add(&pdev->dev, &proc_priv); + if (ret) + return ret; + + platform_set_drvdata(pdev, proc_priv); + proc_thermal_emum_mode = PROC_THERMAL_PLATFORM_DEV; + + dev_info(&pdev->dev, "Creating sysfs group for PROC_THERMAL_PLATFORM_DEV\n"); + + return sysfs_create_group(&pdev->dev.kobj, + &power_limit_attribute_group); +} + +static int int3401_remove(struct platform_device *pdev) +{ + proc_thermal_remove(platform_get_drvdata(pdev)); + + return 0; +} + +static irqreturn_t proc_thermal_pci_msi_irq(int irq, void *devid) +{ + struct proc_thermal_device *proc_priv; + struct pci_dev *pdev = devid; + + proc_priv = pci_get_drvdata(pdev); + + intel_soc_dts_iosf_interrupt_handler(proc_priv->soc_dts); + + return IRQ_HANDLED; +} + +static int proc_thermal_pci_probe(struct pci_dev *pdev, + const struct pci_device_id *unused) +{ + struct proc_thermal_device *proc_priv; + int ret; + + if (proc_thermal_emum_mode == PROC_THERMAL_PLATFORM_DEV) { + dev_err(&pdev->dev, "error: enumerated as platform dev\n"); + return -ENODEV; + } + + ret = pci_enable_device(pdev); + if (ret < 0) { + dev_err(&pdev->dev, "error: could not enable device\n"); + return ret; + } + + ret = proc_thermal_add(&pdev->dev, &proc_priv); + if (ret) { + pci_disable_device(pdev); + return ret; + } + + pci_set_drvdata(pdev, proc_priv); + proc_thermal_emum_mode = PROC_THERMAL_PCI; + + if (pdev->device == PCI_DEVICE_ID_PROC_BSW_THERMAL) { + /* + * Enumerate additional DTS sensors available via IOSF. + * But we are not treating as a failure condition, if + * there are no aux DTSs enabled or fails. This driver + * already exposes sensors, which can be accessed via + * ACPI/MSR. So we don't want to fail for auxiliary DTSs. + */ + proc_priv->soc_dts = intel_soc_dts_iosf_init( + INTEL_SOC_DTS_INTERRUPT_MSI, 2, 0); + + if (!IS_ERR(proc_priv->soc_dts) && pdev->irq) { + ret = pci_enable_msi(pdev); + if (!ret) { + ret = request_threaded_irq(pdev->irq, NULL, + proc_thermal_pci_msi_irq, + IRQF_ONESHOT, "proc_thermal", + pdev); + if (ret) { + intel_soc_dts_iosf_exit( + proc_priv->soc_dts); + pci_disable_msi(pdev); + proc_priv->soc_dts = NULL; + } + } + } else + dev_err(&pdev->dev, "No auxiliary DTSs enabled\n"); + } + + dev_info(&pdev->dev, "Creating sysfs group for PROC_THERMAL_PCI\n"); + + return sysfs_create_group(&pdev->dev.kobj, + &power_limit_attribute_group); +} + +static void proc_thermal_pci_remove(struct pci_dev *pdev) +{ + struct proc_thermal_device *proc_priv = pci_get_drvdata(pdev); + + if (proc_priv->soc_dts) { + intel_soc_dts_iosf_exit(proc_priv->soc_dts); + if (pdev->irq) { + free_irq(pdev->irq, pdev); + pci_disable_msi(pdev); + } + } + proc_thermal_remove(proc_priv); + pci_disable_device(pdev); +} + +static const struct pci_device_id proc_thermal_pci_ids[] = { + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_BDW_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_HSB_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_SKL_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_BSW_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_BXT0_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_BXT1_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_BXTX_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_BXTP_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_CNL_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_CFL_THERMAL)}, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_PROC_GLK_THERMAL)}, + { 0, }, +}; + +MODULE_DEVICE_TABLE(pci, proc_thermal_pci_ids); + +static struct pci_driver proc_thermal_pci_driver = { + .name = "proc_thermal", + .probe = proc_thermal_pci_probe, + .remove = proc_thermal_pci_remove, + .id_table = proc_thermal_pci_ids, +}; + +static const struct acpi_device_id int3401_device_ids[] = { + {"INT3401", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, int3401_device_ids); + +static struct platform_driver int3401_driver = { + .probe = int3401_add, + .remove = int3401_remove, + .driver = { + .name = "int3401 thermal", + .acpi_match_table = int3401_device_ids, + }, +}; + +static int __init proc_thermal_init(void) +{ + int ret; + + ret = platform_driver_register(&int3401_driver); + if (ret) + return ret; + + ret = pci_register_driver(&proc_thermal_pci_driver); + + return ret; +} + +static void __exit proc_thermal_exit(void) +{ + platform_driver_unregister(&int3401_driver); + pci_unregister_driver(&proc_thermal_pci_driver); +} + +module_init(proc_thermal_init); +module_exit(proc_thermal_exit); + +MODULE_AUTHOR("Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com>"); +MODULE_DESCRIPTION("Processor Thermal Reporting Device Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/intel_bxt_pmic_thermal.c b/drivers/thermal/intel_bxt_pmic_thermal.c new file mode 100644 index 000000000..94cfd0064 --- /dev/null +++ b/drivers/thermal/intel_bxt_pmic_thermal.c @@ -0,0 +1,299 @@ +/* + * Intel Broxton PMIC thermal driver + * + * Copyright (C) 2016 Intel Corporation. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/delay.h> +#include <linux/interrupt.h> +#include <linux/device.h> +#include <linux/thermal.h> +#include <linux/platform_device.h> +#include <linux/sched.h> +#include <linux/mfd/intel_soc_pmic.h> + +#define BXTWC_THRM0IRQ 0x4E04 +#define BXTWC_THRM1IRQ 0x4E05 +#define BXTWC_THRM2IRQ 0x4E06 +#define BXTWC_MTHRM0IRQ 0x4E12 +#define BXTWC_MTHRM1IRQ 0x4E13 +#define BXTWC_MTHRM2IRQ 0x4E14 +#define BXTWC_STHRM0IRQ 0x4F19 +#define BXTWC_STHRM1IRQ 0x4F1A +#define BXTWC_STHRM2IRQ 0x4F1B + +struct trip_config_map { + u16 irq_reg; + u16 irq_en; + u16 evt_stat; + u8 irq_mask; + u8 irq_en_mask; + u8 evt_mask; + u8 trip_num; +}; + +struct thermal_irq_map { + char handle[20]; + int num_trips; + const struct trip_config_map *trip_config; +}; + +struct pmic_thermal_data { + const struct thermal_irq_map *maps; + int num_maps; +}; + +static const struct trip_config_map bxtwc_str0_trip_config[] = { + { + .irq_reg = BXTWC_THRM0IRQ, + .irq_mask = 0x01, + .irq_en = BXTWC_MTHRM0IRQ, + .irq_en_mask = 0x01, + .evt_stat = BXTWC_STHRM0IRQ, + .evt_mask = 0x01, + .trip_num = 0 + }, + { + .irq_reg = BXTWC_THRM0IRQ, + .irq_mask = 0x10, + .irq_en = BXTWC_MTHRM0IRQ, + .irq_en_mask = 0x10, + .evt_stat = BXTWC_STHRM0IRQ, + .evt_mask = 0x10, + .trip_num = 1 + } +}; + +static const struct trip_config_map bxtwc_str1_trip_config[] = { + { + .irq_reg = BXTWC_THRM0IRQ, + .irq_mask = 0x02, + .irq_en = BXTWC_MTHRM0IRQ, + .irq_en_mask = 0x02, + .evt_stat = BXTWC_STHRM0IRQ, + .evt_mask = 0x02, + .trip_num = 0 + }, + { + .irq_reg = BXTWC_THRM0IRQ, + .irq_mask = 0x20, + .irq_en = BXTWC_MTHRM0IRQ, + .irq_en_mask = 0x20, + .evt_stat = BXTWC_STHRM0IRQ, + .evt_mask = 0x20, + .trip_num = 1 + }, +}; + +static const struct trip_config_map bxtwc_str2_trip_config[] = { + { + .irq_reg = BXTWC_THRM0IRQ, + .irq_mask = 0x04, + .irq_en = BXTWC_MTHRM0IRQ, + .irq_en_mask = 0x04, + .evt_stat = BXTWC_STHRM0IRQ, + .evt_mask = 0x04, + .trip_num = 0 + }, + { + .irq_reg = BXTWC_THRM0IRQ, + .irq_mask = 0x40, + .irq_en = BXTWC_MTHRM0IRQ, + .irq_en_mask = 0x40, + .evt_stat = BXTWC_STHRM0IRQ, + .evt_mask = 0x40, + .trip_num = 1 + }, +}; + +static const struct trip_config_map bxtwc_str3_trip_config[] = { + { + .irq_reg = BXTWC_THRM2IRQ, + .irq_mask = 0x10, + .irq_en = BXTWC_MTHRM2IRQ, + .irq_en_mask = 0x10, + .evt_stat = BXTWC_STHRM2IRQ, + .evt_mask = 0x10, + .trip_num = 0 + }, +}; + +static const struct thermal_irq_map bxtwc_thermal_irq_map[] = { + { + .handle = "STR0", + .trip_config = bxtwc_str0_trip_config, + .num_trips = ARRAY_SIZE(bxtwc_str0_trip_config), + }, + { + .handle = "STR1", + .trip_config = bxtwc_str1_trip_config, + .num_trips = ARRAY_SIZE(bxtwc_str1_trip_config), + }, + { + .handle = "STR2", + .trip_config = bxtwc_str2_trip_config, + .num_trips = ARRAY_SIZE(bxtwc_str2_trip_config), + }, + { + .handle = "STR3", + .trip_config = bxtwc_str3_trip_config, + .num_trips = ARRAY_SIZE(bxtwc_str3_trip_config), + }, +}; + +static const struct pmic_thermal_data bxtwc_thermal_data = { + .maps = bxtwc_thermal_irq_map, + .num_maps = ARRAY_SIZE(bxtwc_thermal_irq_map), +}; + +static irqreturn_t pmic_thermal_irq_handler(int irq, void *data) +{ + struct platform_device *pdev = data; + struct thermal_zone_device *tzd; + struct pmic_thermal_data *td; + struct intel_soc_pmic *pmic; + struct regmap *regmap; + u8 reg_val, mask, irq_stat; + u16 reg, evt_stat_reg; + int i, j, ret; + + pmic = dev_get_drvdata(pdev->dev.parent); + regmap = pmic->regmap; + td = (struct pmic_thermal_data *) + platform_get_device_id(pdev)->driver_data; + + /* Resolve thermal irqs */ + for (i = 0; i < td->num_maps; i++) { + for (j = 0; j < td->maps[i].num_trips; j++) { + reg = td->maps[i].trip_config[j].irq_reg; + mask = td->maps[i].trip_config[j].irq_mask; + /* + * Read the irq register to resolve whether the + * interrupt was triggered for this sensor + */ + if (regmap_read(regmap, reg, &ret)) + return IRQ_HANDLED; + + reg_val = (u8)ret; + irq_stat = ((u8)ret & mask); + + if (!irq_stat) + continue; + + /* + * Read the status register to find out what + * event occurred i.e a high or a low + */ + evt_stat_reg = td->maps[i].trip_config[j].evt_stat; + if (regmap_read(regmap, evt_stat_reg, &ret)) + return IRQ_HANDLED; + + tzd = thermal_zone_get_zone_by_name(td->maps[i].handle); + if (!IS_ERR(tzd)) + thermal_zone_device_update(tzd, + THERMAL_EVENT_UNSPECIFIED); + + /* Clear the appropriate irq */ + regmap_write(regmap, reg, reg_val & mask); + } + } + + return IRQ_HANDLED; +} + +static int pmic_thermal_probe(struct platform_device *pdev) +{ + struct regmap_irq_chip_data *regmap_irq_chip; + struct pmic_thermal_data *thermal_data; + int ret, irq, virq, i, j, pmic_irq_count; + struct intel_soc_pmic *pmic; + struct regmap *regmap; + struct device *dev; + u16 reg; + u8 mask; + + dev = &pdev->dev; + pmic = dev_get_drvdata(pdev->dev.parent); + if (!pmic) { + dev_err(dev, "Failed to get struct intel_soc_pmic pointer\n"); + return -ENODEV; + } + + thermal_data = (struct pmic_thermal_data *) + platform_get_device_id(pdev)->driver_data; + if (!thermal_data) { + dev_err(dev, "No thermal data initialized!!\n"); + return -ENODEV; + } + + regmap = pmic->regmap; + regmap_irq_chip = pmic->irq_chip_data; + + pmic_irq_count = 0; + while ((irq = platform_get_irq(pdev, pmic_irq_count)) != -ENXIO) { + virq = regmap_irq_get_virq(regmap_irq_chip, irq); + if (virq < 0) { + dev_err(dev, "failed to get virq by irq %d\n", irq); + return virq; + } + + ret = devm_request_threaded_irq(&pdev->dev, virq, + NULL, pmic_thermal_irq_handler, + IRQF_ONESHOT, "pmic_thermal", pdev); + + if (ret) { + dev_err(dev, "request irq(%d) failed: %d\n", virq, ret); + return ret; + } + pmic_irq_count++; + } + + /* Enable thermal interrupts */ + for (i = 0; i < thermal_data->num_maps; i++) { + for (j = 0; j < thermal_data->maps[i].num_trips; j++) { + reg = thermal_data->maps[i].trip_config[j].irq_en; + mask = thermal_data->maps[i].trip_config[j].irq_en_mask; + ret = regmap_update_bits(regmap, reg, mask, 0x00); + if (ret) + return ret; + } + } + + return 0; +} + +static const struct platform_device_id pmic_thermal_id_table[] = { + { + .name = "bxt_wcove_thermal", + .driver_data = (kernel_ulong_t)&bxtwc_thermal_data, + }, + {}, +}; + +static struct platform_driver pmic_thermal_driver = { + .probe = pmic_thermal_probe, + .driver = { + .name = "pmic_thermal", + }, + .id_table = pmic_thermal_id_table, +}; + +MODULE_DEVICE_TABLE(platform, pmic_thermal_id_table); +module_platform_driver(pmic_thermal_driver); + +MODULE_AUTHOR("Yegnesh S Iyer <yegnesh.s.iyer@intel.com>"); +MODULE_DESCRIPTION("Intel Broxton PMIC Thermal Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/intel_pch_thermal.c b/drivers/thermal/intel_pch_thermal.c new file mode 100644 index 000000000..8a7f69b4b --- /dev/null +++ b/drivers/thermal/intel_pch_thermal.c @@ -0,0 +1,432 @@ +/* intel_pch_thermal.c - Intel PCH Thermal driver + * + * Copyright (c) 2015, Intel Corporation. + * + * Authors: + * Tushar Dave <tushar.n.dave@intel.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + */ + +#include <linux/module.h> +#include <linux/types.h> +#include <linux/init.h> +#include <linux/pci.h> +#include <linux/acpi.h> +#include <linux/thermal.h> +#include <linux/pm.h> + +/* Intel PCH thermal Device IDs */ +#define PCH_THERMAL_DID_HSW_1 0x9C24 /* Haswell PCH */ +#define PCH_THERMAL_DID_HSW_2 0x8C24 /* Haswell PCH */ +#define PCH_THERMAL_DID_WPT 0x9CA4 /* Wildcat Point */ +#define PCH_THERMAL_DID_SKL 0x9D31 /* Skylake PCH */ +#define PCH_THERMAL_DID_SKL_H 0xA131 /* Skylake PCH 100 series */ +#define PCH_THERMAL_DID_CNL 0x9Df9 /* CNL PCH */ +#define PCH_THERMAL_DID_CNL_H 0xA379 /* CNL-H PCH */ + +/* Wildcat Point-LP PCH Thermal registers */ +#define WPT_TEMP 0x0000 /* Temperature */ +#define WPT_TSC 0x04 /* Thermal Sensor Control */ +#define WPT_TSS 0x06 /* Thermal Sensor Status */ +#define WPT_TSEL 0x08 /* Thermal Sensor Enable and Lock */ +#define WPT_TSREL 0x0A /* Thermal Sensor Report Enable and Lock */ +#define WPT_TSMIC 0x0C /* Thermal Sensor SMI Control */ +#define WPT_CTT 0x0010 /* Catastrophic Trip Point */ +#define WPT_TAHV 0x0014 /* Thermal Alert High Value */ +#define WPT_TALV 0x0018 /* Thermal Alert Low Value */ +#define WPT_TL 0x00000040 /* Throttle Value */ +#define WPT_PHL 0x0060 /* PCH Hot Level */ +#define WPT_PHLC 0x62 /* PHL Control */ +#define WPT_TAS 0x80 /* Thermal Alert Status */ +#define WPT_TSPIEN 0x82 /* PCI Interrupt Event Enables */ +#define WPT_TSGPEN 0x84 /* General Purpose Event Enables */ + +/* Wildcat Point-LP PCH Thermal Register bit definitions */ +#define WPT_TEMP_TSR 0x01ff /* Temp TS Reading */ +#define WPT_TSC_CPDE 0x01 /* Catastrophic Power-Down Enable */ +#define WPT_TSS_TSDSS 0x10 /* Thermal Sensor Dynamic Shutdown Status */ +#define WPT_TSS_GPES 0x08 /* GPE status */ +#define WPT_TSEL_ETS 0x01 /* Enable TS */ +#define WPT_TSEL_PLDB 0x80 /* TSEL Policy Lock-Down Bit */ +#define WPT_TL_TOL 0x000001FF /* T0 Level */ +#define WPT_TL_T1L 0x1ff00000 /* T1 Level */ +#define WPT_TL_TTEN 0x20000000 /* TT Enable */ + +static char driver_name[] = "Intel PCH thermal driver"; + +struct pch_thermal_device { + void __iomem *hw_base; + const struct pch_dev_ops *ops; + struct pci_dev *pdev; + struct thermal_zone_device *tzd; + int crt_trip_id; + unsigned long crt_temp; + int hot_trip_id; + unsigned long hot_temp; + int psv_trip_id; + unsigned long psv_temp; + bool bios_enabled; +}; + +#ifdef CONFIG_ACPI + +/* + * On some platforms, there is a companion ACPI device, which adds + * passive trip temperature using _PSV method. There is no specific + * passive temperature setting in MMIO interface of this PCI device. + */ +static void pch_wpt_add_acpi_psv_trip(struct pch_thermal_device *ptd, + int *nr_trips) +{ + struct acpi_device *adev; + + ptd->psv_trip_id = -1; + + adev = ACPI_COMPANION(&ptd->pdev->dev); + if (adev) { + unsigned long long r; + acpi_status status; + + status = acpi_evaluate_integer(adev->handle, "_PSV", NULL, + &r); + if (ACPI_SUCCESS(status)) { + unsigned long trip_temp; + + trip_temp = DECI_KELVIN_TO_MILLICELSIUS(r); + if (trip_temp) { + ptd->psv_temp = trip_temp; + ptd->psv_trip_id = *nr_trips; + ++(*nr_trips); + } + } + } +} +#else +static void pch_wpt_add_acpi_psv_trip(struct pch_thermal_device *ptd, + int *nr_trips) +{ + ptd->psv_trip_id = -1; + +} +#endif + +static int pch_wpt_init(struct pch_thermal_device *ptd, int *nr_trips) +{ + u8 tsel; + u16 trip_temp; + + *nr_trips = 0; + + /* Check if BIOS has already enabled thermal sensor */ + if (WPT_TSEL_ETS & readb(ptd->hw_base + WPT_TSEL)) { + ptd->bios_enabled = true; + goto read_trips; + } + + tsel = readb(ptd->hw_base + WPT_TSEL); + /* + * When TSEL's Policy Lock-Down bit is 1, TSEL become RO. + * If so, thermal sensor cannot enable. Bail out. + */ + if (tsel & WPT_TSEL_PLDB) { + dev_err(&ptd->pdev->dev, "Sensor can't be enabled\n"); + return -ENODEV; + } + + writeb(tsel|WPT_TSEL_ETS, ptd->hw_base + WPT_TSEL); + if (!(WPT_TSEL_ETS & readb(ptd->hw_base + WPT_TSEL))) { + dev_err(&ptd->pdev->dev, "Sensor can't be enabled\n"); + return -ENODEV; + } + +read_trips: + ptd->crt_trip_id = -1; + trip_temp = readw(ptd->hw_base + WPT_CTT); + trip_temp &= 0x1FF; + if (trip_temp) { + /* Resolution of 1/2 degree C and an offset of -50C */ + ptd->crt_temp = trip_temp * 1000 / 2 - 50000; + ptd->crt_trip_id = 0; + ++(*nr_trips); + } + + ptd->hot_trip_id = -1; + trip_temp = readw(ptd->hw_base + WPT_PHL); + trip_temp &= 0x1FF; + if (trip_temp) { + /* Resolution of 1/2 degree C and an offset of -50C */ + ptd->hot_temp = trip_temp * 1000 / 2 - 50000; + ptd->hot_trip_id = *nr_trips; + ++(*nr_trips); + } + + pch_wpt_add_acpi_psv_trip(ptd, nr_trips); + + return 0; +} + +static int pch_wpt_get_temp(struct pch_thermal_device *ptd, int *temp) +{ + u16 wpt_temp; + + wpt_temp = WPT_TEMP_TSR & readw(ptd->hw_base + WPT_TEMP); + + /* Resolution of 1/2 degree C and an offset of -50C */ + *temp = (wpt_temp * 1000 / 2 - 50000); + + return 0; +} + +static int pch_wpt_suspend(struct pch_thermal_device *ptd) +{ + u8 tsel; + + if (ptd->bios_enabled) + return 0; + + tsel = readb(ptd->hw_base + WPT_TSEL); + + writeb(tsel & 0xFE, ptd->hw_base + WPT_TSEL); + + return 0; +} + +static int pch_wpt_resume(struct pch_thermal_device *ptd) +{ + u8 tsel; + + if (ptd->bios_enabled) + return 0; + + tsel = readb(ptd->hw_base + WPT_TSEL); + + writeb(tsel | WPT_TSEL_ETS, ptd->hw_base + WPT_TSEL); + + return 0; +} + +struct pch_dev_ops { + int (*hw_init)(struct pch_thermal_device *ptd, int *nr_trips); + int (*get_temp)(struct pch_thermal_device *ptd, int *temp); + int (*suspend)(struct pch_thermal_device *ptd); + int (*resume)(struct pch_thermal_device *ptd); +}; + + +/* dev ops for Wildcat Point */ +static const struct pch_dev_ops pch_dev_ops_wpt = { + .hw_init = pch_wpt_init, + .get_temp = pch_wpt_get_temp, + .suspend = pch_wpt_suspend, + .resume = pch_wpt_resume, +}; + +static int pch_thermal_get_temp(struct thermal_zone_device *tzd, int *temp) +{ + struct pch_thermal_device *ptd = tzd->devdata; + + return ptd->ops->get_temp(ptd, temp); +} + +static int pch_get_trip_type(struct thermal_zone_device *tzd, int trip, + enum thermal_trip_type *type) +{ + struct pch_thermal_device *ptd = tzd->devdata; + + if (ptd->crt_trip_id == trip) + *type = THERMAL_TRIP_CRITICAL; + else if (ptd->hot_trip_id == trip) + *type = THERMAL_TRIP_HOT; + else if (ptd->psv_trip_id == trip) + *type = THERMAL_TRIP_PASSIVE; + else + return -EINVAL; + + return 0; +} + +static int pch_get_trip_temp(struct thermal_zone_device *tzd, int trip, int *temp) +{ + struct pch_thermal_device *ptd = tzd->devdata; + + if (ptd->crt_trip_id == trip) + *temp = ptd->crt_temp; + else if (ptd->hot_trip_id == trip) + *temp = ptd->hot_temp; + else if (ptd->psv_trip_id == trip) + *temp = ptd->psv_temp; + else + return -EINVAL; + + return 0; +} + +static struct thermal_zone_device_ops tzd_ops = { + .get_temp = pch_thermal_get_temp, + .get_trip_type = pch_get_trip_type, + .get_trip_temp = pch_get_trip_temp, +}; + +enum board_ids { + board_hsw, + board_wpt, + board_skl, + board_cnl, +}; + +static const struct board_info { + const char *name; + const struct pch_dev_ops *ops; +} board_info[] = { + [board_hsw] = { + .name = "pch_haswell", + .ops = &pch_dev_ops_wpt, + }, + [board_wpt] = { + .name = "pch_wildcat_point", + .ops = &pch_dev_ops_wpt, + }, + [board_skl] = { + .name = "pch_skylake", + .ops = &pch_dev_ops_wpt, + }, + [board_cnl] = { + .name = "pch_cannonlake", + .ops = &pch_dev_ops_wpt, + }, +}; + +static int intel_pch_thermal_probe(struct pci_dev *pdev, + const struct pci_device_id *id) +{ + enum board_ids board_id = id->driver_data; + const struct board_info *bi = &board_info[board_id]; + struct pch_thermal_device *ptd; + int err; + int nr_trips; + + ptd = devm_kzalloc(&pdev->dev, sizeof(*ptd), GFP_KERNEL); + if (!ptd) + return -ENOMEM; + + ptd->ops = bi->ops; + + pci_set_drvdata(pdev, ptd); + ptd->pdev = pdev; + + err = pci_enable_device(pdev); + if (err) { + dev_err(&pdev->dev, "failed to enable pci device\n"); + return err; + } + + err = pci_request_regions(pdev, driver_name); + if (err) { + dev_err(&pdev->dev, "failed to request pci region\n"); + goto error_disable; + } + + ptd->hw_base = pci_ioremap_bar(pdev, 0); + if (!ptd->hw_base) { + err = -ENOMEM; + dev_err(&pdev->dev, "failed to map mem base\n"); + goto error_release; + } + + err = ptd->ops->hw_init(ptd, &nr_trips); + if (err) + goto error_cleanup; + + ptd->tzd = thermal_zone_device_register(bi->name, nr_trips, 0, ptd, + &tzd_ops, NULL, 0, 0); + if (IS_ERR(ptd->tzd)) { + dev_err(&pdev->dev, "Failed to register thermal zone %s\n", + bi->name); + err = PTR_ERR(ptd->tzd); + goto error_cleanup; + } + + return 0; + +error_cleanup: + iounmap(ptd->hw_base); +error_release: + pci_release_regions(pdev); +error_disable: + pci_disable_device(pdev); + dev_err(&pdev->dev, "pci device failed to probe\n"); + return err; +} + +static void intel_pch_thermal_remove(struct pci_dev *pdev) +{ + struct pch_thermal_device *ptd = pci_get_drvdata(pdev); + + thermal_zone_device_unregister(ptd->tzd); + iounmap(ptd->hw_base); + pci_set_drvdata(pdev, NULL); + pci_release_region(pdev, 0); + pci_disable_device(pdev); +} + +static int intel_pch_thermal_suspend(struct device *device) +{ + struct pci_dev *pdev = to_pci_dev(device); + struct pch_thermal_device *ptd = pci_get_drvdata(pdev); + + return ptd->ops->suspend(ptd); +} + +static int intel_pch_thermal_resume(struct device *device) +{ + struct pci_dev *pdev = to_pci_dev(device); + struct pch_thermal_device *ptd = pci_get_drvdata(pdev); + + return ptd->ops->resume(ptd); +} + +static const struct pci_device_id intel_pch_thermal_id[] = { + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCH_THERMAL_DID_HSW_1), + .driver_data = board_hsw, }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCH_THERMAL_DID_HSW_2), + .driver_data = board_hsw, }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCH_THERMAL_DID_WPT), + .driver_data = board_wpt, }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCH_THERMAL_DID_SKL), + .driver_data = board_skl, }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCH_THERMAL_DID_SKL_H), + .driver_data = board_skl, }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCH_THERMAL_DID_CNL), + .driver_data = board_cnl, }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCH_THERMAL_DID_CNL_H), + .driver_data = board_cnl, }, + { 0, }, +}; +MODULE_DEVICE_TABLE(pci, intel_pch_thermal_id); + +static const struct dev_pm_ops intel_pch_pm_ops = { + .suspend = intel_pch_thermal_suspend, + .resume = intel_pch_thermal_resume, +}; + +static struct pci_driver intel_pch_thermal_driver = { + .name = "intel_pch_thermal", + .id_table = intel_pch_thermal_id, + .probe = intel_pch_thermal_probe, + .remove = intel_pch_thermal_remove, + .driver.pm = &intel_pch_pm_ops, +}; + +module_pci_driver(intel_pch_thermal_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Intel PCH Thermal driver"); diff --git a/drivers/thermal/intel_powerclamp.c b/drivers/thermal/intel_powerclamp.c new file mode 100644 index 000000000..8e8328347 --- /dev/null +++ b/drivers/thermal/intel_powerclamp.c @@ -0,0 +1,815 @@ +/* + * intel_powerclamp.c - package c-state idle injection + * + * Copyright (c) 2012, Intel Corporation. + * + * Authors: + * Arjan van de Ven <arjan@linux.intel.com> + * Jacob Pan <jacob.jun.pan@linux.intel.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + * + * + * TODO: + * 1. better handle wakeup from external interrupts, currently a fixed + * compensation is added to clamping duration when excessive amount + * of wakeups are observed during idle time. the reason is that in + * case of external interrupts without need for ack, clamping down + * cpu in non-irq context does not reduce irq. for majority of the + * cases, clamping down cpu does help reduce irq as well, we should + * be able to differentiate the two cases and give a quantitative + * solution for the irqs that we can control. perhaps based on + * get_cpu_iowait_time_us() + * + * 2. synchronization with other hw blocks + * + * + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/delay.h> +#include <linux/kthread.h> +#include <linux/cpu.h> +#include <linux/thermal.h> +#include <linux/slab.h> +#include <linux/tick.h> +#include <linux/debugfs.h> +#include <linux/seq_file.h> +#include <linux/sched/rt.h> +#include <uapi/linux/sched/types.h> + +#include <asm/nmi.h> +#include <asm/msr.h> +#include <asm/mwait.h> +#include <asm/cpu_device_id.h> +#include <asm/hardirq.h> + +#define MAX_TARGET_RATIO (50U) +/* For each undisturbed clamping period (no extra wake ups during idle time), + * we increment the confidence counter for the given target ratio. + * CONFIDENCE_OK defines the level where runtime calibration results are + * valid. + */ +#define CONFIDENCE_OK (3) +/* Default idle injection duration, driver adjust sleep time to meet target + * idle ratio. Similar to frequency modulation. + */ +#define DEFAULT_DURATION_JIFFIES (6) + +static unsigned int target_mwait; +static struct dentry *debug_dir; + +/* user selected target */ +static unsigned int set_target_ratio; +static unsigned int current_ratio; +static bool should_skip; +static bool reduce_irq; +static atomic_t idle_wakeup_counter; +static unsigned int control_cpu; /* The cpu assigned to collect stat and update + * control parameters. default to BSP but BSP + * can be offlined. + */ +static bool clamping; + +static const struct sched_param sparam = { + .sched_priority = MAX_USER_RT_PRIO / 2, +}; +struct powerclamp_worker_data { + struct kthread_worker *worker; + struct kthread_work balancing_work; + struct kthread_delayed_work idle_injection_work; + unsigned int cpu; + unsigned int count; + unsigned int guard; + unsigned int window_size_now; + unsigned int target_ratio; + unsigned int duration_jiffies; + bool clamping; +}; + +static struct powerclamp_worker_data __percpu *worker_data; +static struct thermal_cooling_device *cooling_dev; +static unsigned long *cpu_clamping_mask; /* bit map for tracking per cpu + * clamping kthread worker + */ + +static unsigned int duration; +static unsigned int pkg_cstate_ratio_cur; +static unsigned int window_size; + +static int duration_set(const char *arg, const struct kernel_param *kp) +{ + int ret = 0; + unsigned long new_duration; + + ret = kstrtoul(arg, 10, &new_duration); + if (ret) + goto exit; + if (new_duration > 25 || new_duration < 6) { + pr_err("Out of recommended range %lu, between 6-25ms\n", + new_duration); + ret = -EINVAL; + } + + duration = clamp(new_duration, 6ul, 25ul); + smp_mb(); + +exit: + + return ret; +} + +static const struct kernel_param_ops duration_ops = { + .set = duration_set, + .get = param_get_int, +}; + + +module_param_cb(duration, &duration_ops, &duration, 0644); +MODULE_PARM_DESC(duration, "forced idle time for each attempt in msec."); + +struct powerclamp_calibration_data { + unsigned long confidence; /* used for calibration, basically a counter + * gets incremented each time a clamping + * period is completed without extra wakeups + * once that counter is reached given level, + * compensation is deemed usable. + */ + unsigned long steady_comp; /* steady state compensation used when + * no extra wakeups occurred. + */ + unsigned long dynamic_comp; /* compensate excessive wakeup from idle + * mostly from external interrupts. + */ +}; + +static struct powerclamp_calibration_data cal_data[MAX_TARGET_RATIO]; + +static int window_size_set(const char *arg, const struct kernel_param *kp) +{ + int ret = 0; + unsigned long new_window_size; + + ret = kstrtoul(arg, 10, &new_window_size); + if (ret) + goto exit_win; + if (new_window_size > 10 || new_window_size < 2) { + pr_err("Out of recommended window size %lu, between 2-10\n", + new_window_size); + ret = -EINVAL; + } + + window_size = clamp(new_window_size, 2ul, 10ul); + smp_mb(); + +exit_win: + + return ret; +} + +static const struct kernel_param_ops window_size_ops = { + .set = window_size_set, + .get = param_get_int, +}; + +module_param_cb(window_size, &window_size_ops, &window_size, 0644); +MODULE_PARM_DESC(window_size, "sliding window in number of clamping cycles\n" + "\tpowerclamp controls idle ratio within this window. larger\n" + "\twindow size results in slower response time but more smooth\n" + "\tclamping results. default to 2."); + +static void find_target_mwait(void) +{ + unsigned int eax, ebx, ecx, edx; + unsigned int highest_cstate = 0; + unsigned int highest_subcstate = 0; + int i; + + if (boot_cpu_data.cpuid_level < CPUID_MWAIT_LEAF) + return; + + cpuid(CPUID_MWAIT_LEAF, &eax, &ebx, &ecx, &edx); + + if (!(ecx & CPUID5_ECX_EXTENSIONS_SUPPORTED) || + !(ecx & CPUID5_ECX_INTERRUPT_BREAK)) + return; + + edx >>= MWAIT_SUBSTATE_SIZE; + for (i = 0; i < 7 && edx; i++, edx >>= MWAIT_SUBSTATE_SIZE) { + if (edx & MWAIT_SUBSTATE_MASK) { + highest_cstate = i; + highest_subcstate = edx & MWAIT_SUBSTATE_MASK; + } + } + target_mwait = (highest_cstate << MWAIT_SUBSTATE_SIZE) | + (highest_subcstate - 1); + +} + +struct pkg_cstate_info { + bool skip; + int msr_index; + int cstate_id; +}; + +#define PKG_CSTATE_INIT(id) { \ + .msr_index = MSR_PKG_C##id##_RESIDENCY, \ + .cstate_id = id \ + } + +static struct pkg_cstate_info pkg_cstates[] = { + PKG_CSTATE_INIT(2), + PKG_CSTATE_INIT(3), + PKG_CSTATE_INIT(6), + PKG_CSTATE_INIT(7), + PKG_CSTATE_INIT(8), + PKG_CSTATE_INIT(9), + PKG_CSTATE_INIT(10), + {NULL}, +}; + +static bool has_pkg_state_counter(void) +{ + u64 val; + struct pkg_cstate_info *info = pkg_cstates; + + /* check if any one of the counter msrs exists */ + while (info->msr_index) { + if (!rdmsrl_safe(info->msr_index, &val)) + return true; + info++; + } + + return false; +} + +static u64 pkg_state_counter(void) +{ + u64 val; + u64 count = 0; + struct pkg_cstate_info *info = pkg_cstates; + + while (info->msr_index) { + if (!info->skip) { + if (!rdmsrl_safe(info->msr_index, &val)) + count += val; + else + info->skip = true; + } + info++; + } + + return count; +} + +static unsigned int get_compensation(int ratio) +{ + unsigned int comp = 0; + + /* we only use compensation if all adjacent ones are good */ + if (ratio == 1 && + cal_data[ratio].confidence >= CONFIDENCE_OK && + cal_data[ratio + 1].confidence >= CONFIDENCE_OK && + cal_data[ratio + 2].confidence >= CONFIDENCE_OK) { + comp = (cal_data[ratio].steady_comp + + cal_data[ratio + 1].steady_comp + + cal_data[ratio + 2].steady_comp) / 3; + } else if (ratio == MAX_TARGET_RATIO - 1 && + cal_data[ratio].confidence >= CONFIDENCE_OK && + cal_data[ratio - 1].confidence >= CONFIDENCE_OK && + cal_data[ratio - 2].confidence >= CONFIDENCE_OK) { + comp = (cal_data[ratio].steady_comp + + cal_data[ratio - 1].steady_comp + + cal_data[ratio - 2].steady_comp) / 3; + } else if (cal_data[ratio].confidence >= CONFIDENCE_OK && + cal_data[ratio - 1].confidence >= CONFIDENCE_OK && + cal_data[ratio + 1].confidence >= CONFIDENCE_OK) { + comp = (cal_data[ratio].steady_comp + + cal_data[ratio - 1].steady_comp + + cal_data[ratio + 1].steady_comp) / 3; + } + + /* REVISIT: simple penalty of double idle injection */ + if (reduce_irq) + comp = ratio; + /* do not exceed limit */ + if (comp + ratio >= MAX_TARGET_RATIO) + comp = MAX_TARGET_RATIO - ratio - 1; + + return comp; +} + +static void adjust_compensation(int target_ratio, unsigned int win) +{ + int delta; + struct powerclamp_calibration_data *d = &cal_data[target_ratio]; + + /* + * adjust compensations if confidence level has not been reached or + * there are too many wakeups during the last idle injection period, we + * cannot trust the data for compensation. + */ + if (d->confidence >= CONFIDENCE_OK || + atomic_read(&idle_wakeup_counter) > + win * num_online_cpus()) + return; + + delta = set_target_ratio - current_ratio; + /* filter out bad data */ + if (delta >= 0 && delta <= (1+target_ratio/10)) { + if (d->steady_comp) + d->steady_comp = + roundup(delta+d->steady_comp, 2)/2; + else + d->steady_comp = delta; + d->confidence++; + } +} + +static bool powerclamp_adjust_controls(unsigned int target_ratio, + unsigned int guard, unsigned int win) +{ + static u64 msr_last, tsc_last; + u64 msr_now, tsc_now; + u64 val64; + + /* check result for the last window */ + msr_now = pkg_state_counter(); + tsc_now = rdtsc(); + + /* calculate pkg cstate vs tsc ratio */ + if (!msr_last || !tsc_last) + current_ratio = 1; + else if (tsc_now-tsc_last) { + val64 = 100*(msr_now-msr_last); + do_div(val64, (tsc_now-tsc_last)); + current_ratio = val64; + } + + /* update record */ + msr_last = msr_now; + tsc_last = tsc_now; + + adjust_compensation(target_ratio, win); + /* + * too many external interrupts, set flag such + * that we can take measure later. + */ + reduce_irq = atomic_read(&idle_wakeup_counter) >= + 2 * win * num_online_cpus(); + + atomic_set(&idle_wakeup_counter, 0); + /* if we are above target+guard, skip */ + return set_target_ratio + guard <= current_ratio; +} + +static void clamp_balancing_func(struct kthread_work *work) +{ + struct powerclamp_worker_data *w_data; + int sleeptime; + unsigned long target_jiffies; + unsigned int compensated_ratio; + int interval; /* jiffies to sleep for each attempt */ + + w_data = container_of(work, struct powerclamp_worker_data, + balancing_work); + + /* + * make sure user selected ratio does not take effect until + * the next round. adjust target_ratio if user has changed + * target such that we can converge quickly. + */ + w_data->target_ratio = READ_ONCE(set_target_ratio); + w_data->guard = 1 + w_data->target_ratio / 20; + w_data->window_size_now = window_size; + w_data->duration_jiffies = msecs_to_jiffies(duration); + w_data->count++; + + /* + * systems may have different ability to enter package level + * c-states, thus we need to compensate the injected idle ratio + * to achieve the actual target reported by the HW. + */ + compensated_ratio = w_data->target_ratio + + get_compensation(w_data->target_ratio); + if (compensated_ratio <= 0) + compensated_ratio = 1; + interval = w_data->duration_jiffies * 100 / compensated_ratio; + + /* align idle time */ + target_jiffies = roundup(jiffies, interval); + sleeptime = target_jiffies - jiffies; + if (sleeptime <= 0) + sleeptime = 1; + + if (clamping && w_data->clamping && cpu_online(w_data->cpu)) + kthread_queue_delayed_work(w_data->worker, + &w_data->idle_injection_work, + sleeptime); +} + +static void clamp_idle_injection_func(struct kthread_work *work) +{ + struct powerclamp_worker_data *w_data; + + w_data = container_of(work, struct powerclamp_worker_data, + idle_injection_work.work); + + /* + * only elected controlling cpu can collect stats and update + * control parameters. + */ + if (w_data->cpu == control_cpu && + !(w_data->count % w_data->window_size_now)) { + should_skip = + powerclamp_adjust_controls(w_data->target_ratio, + w_data->guard, + w_data->window_size_now); + smp_mb(); + } + + if (should_skip) + goto balance; + + play_idle(jiffies_to_msecs(w_data->duration_jiffies)); + +balance: + if (clamping && w_data->clamping && cpu_online(w_data->cpu)) + kthread_queue_work(w_data->worker, &w_data->balancing_work); +} + +/* + * 1 HZ polling while clamping is active, useful for userspace + * to monitor actual idle ratio. + */ +static void poll_pkg_cstate(struct work_struct *dummy); +static DECLARE_DELAYED_WORK(poll_pkg_cstate_work, poll_pkg_cstate); +static void poll_pkg_cstate(struct work_struct *dummy) +{ + static u64 msr_last; + static u64 tsc_last; + + u64 msr_now; + u64 tsc_now; + u64 val64; + + msr_now = pkg_state_counter(); + tsc_now = rdtsc(); + + /* calculate pkg cstate vs tsc ratio */ + if (!msr_last || !tsc_last) + pkg_cstate_ratio_cur = 1; + else { + if (tsc_now - tsc_last) { + val64 = 100 * (msr_now - msr_last); + do_div(val64, (tsc_now - tsc_last)); + pkg_cstate_ratio_cur = val64; + } + } + + /* update record */ + msr_last = msr_now; + tsc_last = tsc_now; + + if (true == clamping) + schedule_delayed_work(&poll_pkg_cstate_work, HZ); +} + +static void start_power_clamp_worker(unsigned long cpu) +{ + struct powerclamp_worker_data *w_data = per_cpu_ptr(worker_data, cpu); + struct kthread_worker *worker; + + worker = kthread_create_worker_on_cpu(cpu, 0, "kidle_inj/%ld", cpu); + if (IS_ERR(worker)) + return; + + w_data->worker = worker; + w_data->count = 0; + w_data->cpu = cpu; + w_data->clamping = true; + set_bit(cpu, cpu_clamping_mask); + sched_setscheduler(worker->task, SCHED_FIFO, &sparam); + kthread_init_work(&w_data->balancing_work, clamp_balancing_func); + kthread_init_delayed_work(&w_data->idle_injection_work, + clamp_idle_injection_func); + kthread_queue_work(w_data->worker, &w_data->balancing_work); +} + +static void stop_power_clamp_worker(unsigned long cpu) +{ + struct powerclamp_worker_data *w_data = per_cpu_ptr(worker_data, cpu); + + if (!w_data->worker) + return; + + w_data->clamping = false; + /* + * Make sure that all works that get queued after this point see + * the clamping disabled. The counter part is not needed because + * there is an implicit memory barrier when the queued work + * is proceed. + */ + smp_wmb(); + kthread_cancel_work_sync(&w_data->balancing_work); + kthread_cancel_delayed_work_sync(&w_data->idle_injection_work); + /* + * The balancing work still might be queued here because + * the handling of the "clapming" variable, cancel, and queue + * operations are not synchronized via a lock. But it is not + * a big deal. The balancing work is fast and destroy kthread + * will wait for it. + */ + clear_bit(w_data->cpu, cpu_clamping_mask); + kthread_destroy_worker(w_data->worker); + + w_data->worker = NULL; +} + +static int start_power_clamp(void) +{ + unsigned long cpu; + + set_target_ratio = clamp(set_target_ratio, 0U, MAX_TARGET_RATIO - 1); + /* prevent cpu hotplug */ + get_online_cpus(); + + /* prefer BSP */ + control_cpu = 0; + if (!cpu_online(control_cpu)) + control_cpu = smp_processor_id(); + + clamping = true; + schedule_delayed_work(&poll_pkg_cstate_work, 0); + + /* start one kthread worker per online cpu */ + for_each_online_cpu(cpu) { + start_power_clamp_worker(cpu); + } + put_online_cpus(); + + return 0; +} + +static void end_power_clamp(void) +{ + int i; + + /* + * Block requeuing in all the kthread workers. They will flush and + * stop faster. + */ + clamping = false; + if (bitmap_weight(cpu_clamping_mask, num_possible_cpus())) { + for_each_set_bit(i, cpu_clamping_mask, num_possible_cpus()) { + pr_debug("clamping worker for cpu %d alive, destroy\n", + i); + stop_power_clamp_worker(i); + } + } +} + +static int powerclamp_cpu_online(unsigned int cpu) +{ + if (clamping == false) + return 0; + start_power_clamp_worker(cpu); + /* prefer BSP as controlling CPU */ + if (cpu == 0) { + control_cpu = 0; + smp_mb(); + } + return 0; +} + +static int powerclamp_cpu_predown(unsigned int cpu) +{ + if (clamping == false) + return 0; + + stop_power_clamp_worker(cpu); + if (cpu != control_cpu) + return 0; + + control_cpu = cpumask_first(cpu_online_mask); + if (control_cpu == cpu) + control_cpu = cpumask_next(cpu, cpu_online_mask); + smp_mb(); + return 0; +} + +static int powerclamp_get_max_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + *state = MAX_TARGET_RATIO; + + return 0; +} + +static int powerclamp_get_cur_state(struct thermal_cooling_device *cdev, + unsigned long *state) +{ + if (true == clamping) + *state = pkg_cstate_ratio_cur; + else + /* to save power, do not poll idle ratio while not clamping */ + *state = -1; /* indicates invalid state */ + + return 0; +} + +static int powerclamp_set_cur_state(struct thermal_cooling_device *cdev, + unsigned long new_target_ratio) +{ + int ret = 0; + + new_target_ratio = clamp(new_target_ratio, 0UL, + (unsigned long) (MAX_TARGET_RATIO-1)); + if (set_target_ratio == 0 && new_target_ratio > 0) { + pr_info("Start idle injection to reduce power\n"); + set_target_ratio = new_target_ratio; + ret = start_power_clamp(); + goto exit_set; + } else if (set_target_ratio > 0 && new_target_ratio == 0) { + pr_info("Stop forced idle injection\n"); + end_power_clamp(); + set_target_ratio = 0; + } else /* adjust currently running */ { + set_target_ratio = new_target_ratio; + /* make new set_target_ratio visible to other cpus */ + smp_mb(); + } + +exit_set: + return ret; +} + +/* bind to generic thermal layer as cooling device*/ +static struct thermal_cooling_device_ops powerclamp_cooling_ops = { + .get_max_state = powerclamp_get_max_state, + .get_cur_state = powerclamp_get_cur_state, + .set_cur_state = powerclamp_set_cur_state, +}; + +static const struct x86_cpu_id __initconst intel_powerclamp_ids[] = { + { X86_VENDOR_INTEL, X86_FAMILY_ANY, X86_MODEL_ANY, X86_FEATURE_MWAIT }, + {} +}; +MODULE_DEVICE_TABLE(x86cpu, intel_powerclamp_ids); + +static int __init powerclamp_probe(void) +{ + + if (!x86_match_cpu(intel_powerclamp_ids)) { + pr_err("CPU does not support MWAIT\n"); + return -ENODEV; + } + + /* The goal for idle time alignment is to achieve package cstate. */ + if (!has_pkg_state_counter()) { + pr_info("No package C-state available\n"); + return -ENODEV; + } + + /* find the deepest mwait value */ + find_target_mwait(); + + return 0; +} + +static int powerclamp_debug_show(struct seq_file *m, void *unused) +{ + int i = 0; + + seq_printf(m, "controlling cpu: %d\n", control_cpu); + seq_printf(m, "pct confidence steady dynamic (compensation)\n"); + for (i = 0; i < MAX_TARGET_RATIO; i++) { + seq_printf(m, "%d\t%lu\t%lu\t%lu\n", + i, + cal_data[i].confidence, + cal_data[i].steady_comp, + cal_data[i].dynamic_comp); + } + + return 0; +} + +static int powerclamp_debug_open(struct inode *inode, + struct file *file) +{ + return single_open(file, powerclamp_debug_show, inode->i_private); +} + +static const struct file_operations powerclamp_debug_fops = { + .open = powerclamp_debug_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, + .owner = THIS_MODULE, +}; + +static inline void powerclamp_create_debug_files(void) +{ + debug_dir = debugfs_create_dir("intel_powerclamp", NULL); + if (!debug_dir) + return; + + if (!debugfs_create_file("powerclamp_calib", S_IRUGO, debug_dir, + cal_data, &powerclamp_debug_fops)) + goto file_error; + + return; + +file_error: + debugfs_remove_recursive(debug_dir); +} + +static enum cpuhp_state hp_state; + +static int __init powerclamp_init(void) +{ + int retval; + int bitmap_size; + + bitmap_size = BITS_TO_LONGS(num_possible_cpus()) * sizeof(long); + cpu_clamping_mask = kzalloc(bitmap_size, GFP_KERNEL); + if (!cpu_clamping_mask) + return -ENOMEM; + + /* probe cpu features and ids here */ + retval = powerclamp_probe(); + if (retval) + goto exit_free; + + /* set default limit, maybe adjusted during runtime based on feedback */ + window_size = 2; + retval = cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN, + "thermal/intel_powerclamp:online", + powerclamp_cpu_online, + powerclamp_cpu_predown); + if (retval < 0) + goto exit_free; + + hp_state = retval; + + worker_data = alloc_percpu(struct powerclamp_worker_data); + if (!worker_data) { + retval = -ENOMEM; + goto exit_unregister; + } + + cooling_dev = thermal_cooling_device_register("intel_powerclamp", NULL, + &powerclamp_cooling_ops); + if (IS_ERR(cooling_dev)) { + retval = -ENODEV; + goto exit_free_thread; + } + + if (!duration) + duration = jiffies_to_msecs(DEFAULT_DURATION_JIFFIES); + + powerclamp_create_debug_files(); + + return 0; + +exit_free_thread: + free_percpu(worker_data); +exit_unregister: + cpuhp_remove_state_nocalls(hp_state); +exit_free: + kfree(cpu_clamping_mask); + return retval; +} +module_init(powerclamp_init); + +static void __exit powerclamp_exit(void) +{ + end_power_clamp(); + cpuhp_remove_state_nocalls(hp_state); + free_percpu(worker_data); + thermal_cooling_device_unregister(cooling_dev); + kfree(cpu_clamping_mask); + + cancel_delayed_work_sync(&poll_pkg_cstate_work); + debugfs_remove_recursive(debug_dir); +} +module_exit(powerclamp_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Arjan van de Ven <arjan@linux.intel.com>"); +MODULE_AUTHOR("Jacob Pan <jacob.jun.pan@linux.intel.com>"); +MODULE_DESCRIPTION("Package Level C-state Idle Injection for Intel CPUs"); diff --git a/drivers/thermal/intel_quark_dts_thermal.c b/drivers/thermal/intel_quark_dts_thermal.c new file mode 100644 index 000000000..5d33b350d --- /dev/null +++ b/drivers/thermal/intel_quark_dts_thermal.c @@ -0,0 +1,471 @@ +/* + * intel_quark_dts_thermal.c + * + * This file is provided under a dual BSD/GPLv2 license. When using or + * redistributing this file, you may do so under either license. + * + * GPL LICENSE SUMMARY + * + * Copyright(c) 2015 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * Contact Information: + * Ong Boon Leong <boon.leong.ong@intel.com> + * Intel Malaysia, Penang + * + * BSD LICENSE + * + * Copyright(c) 2015 Intel Corporation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Intel Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Quark DTS thermal driver is implemented by referencing + * intel_soc_dts_thermal.c. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <linux/thermal.h> +#include <asm/cpu_device_id.h> +#include <asm/iosf_mbi.h> + +#define X86_FAMILY_QUARK 0x5 +#define X86_MODEL_QUARK_X1000 0x9 + +/* DTS reset is programmed via QRK_MBI_UNIT_SOC */ +#define QRK_DTS_REG_OFFSET_RESET 0x34 +#define QRK_DTS_RESET_BIT BIT(0) + +/* DTS enable is programmed via QRK_MBI_UNIT_RMU */ +#define QRK_DTS_REG_OFFSET_ENABLE 0xB0 +#define QRK_DTS_ENABLE_BIT BIT(15) + +/* Temperature Register is read via QRK_MBI_UNIT_RMU */ +#define QRK_DTS_REG_OFFSET_TEMP 0xB1 +#define QRK_DTS_MASK_TEMP 0xFF +#define QRK_DTS_OFFSET_TEMP 0 +#define QRK_DTS_OFFSET_REL_TEMP 16 +#define QRK_DTS_TEMP_BASE 50 + +/* Programmable Trip Point Register is configured via QRK_MBI_UNIT_RMU */ +#define QRK_DTS_REG_OFFSET_PTPS 0xB2 +#define QRK_DTS_MASK_TP_THRES 0xFF +#define QRK_DTS_SHIFT_TP 8 +#define QRK_DTS_ID_TP_CRITICAL 0 +#define QRK_DTS_SAFE_TP_THRES 105 + +/* Thermal Sensor Register Lock */ +#define QRK_DTS_REG_OFFSET_LOCK 0x71 +#define QRK_DTS_LOCK_BIT BIT(5) + +/* Quark DTS has 2 trip points: hot & catastrophic */ +#define QRK_MAX_DTS_TRIPS 2 +/* If DTS not locked, all trip points are configurable */ +#define QRK_DTS_WR_MASK_SET 0x3 +/* If DTS locked, all trip points are not configurable */ +#define QRK_DTS_WR_MASK_CLR 0 + +#define DEFAULT_POLL_DELAY 2000 + +struct soc_sensor_entry { + bool locked; + u32 store_ptps; + u32 store_dts_enable; + enum thermal_device_mode mode; + struct thermal_zone_device *tzone; +}; + +static struct soc_sensor_entry *soc_dts; + +static int polling_delay = DEFAULT_POLL_DELAY; +module_param(polling_delay, int, 0644); +MODULE_PARM_DESC(polling_delay, + "Polling interval for checking trip points (in milliseconds)"); + +static DEFINE_MUTEX(dts_update_mutex); + +static int soc_dts_enable(struct thermal_zone_device *tzd) +{ + u32 out; + struct soc_sensor_entry *aux_entry = tzd->devdata; + int ret; + + ret = iosf_mbi_read(QRK_MBI_UNIT_RMU, MBI_REG_READ, + QRK_DTS_REG_OFFSET_ENABLE, &out); + if (ret) + return ret; + + if (out & QRK_DTS_ENABLE_BIT) { + aux_entry->mode = THERMAL_DEVICE_ENABLED; + return 0; + } + + if (!aux_entry->locked) { + out |= QRK_DTS_ENABLE_BIT; + ret = iosf_mbi_write(QRK_MBI_UNIT_RMU, MBI_REG_WRITE, + QRK_DTS_REG_OFFSET_ENABLE, out); + if (ret) + return ret; + + aux_entry->mode = THERMAL_DEVICE_ENABLED; + } else { + aux_entry->mode = THERMAL_DEVICE_DISABLED; + pr_info("DTS is locked. Cannot enable DTS\n"); + ret = -EPERM; + } + + return ret; +} + +static int soc_dts_disable(struct thermal_zone_device *tzd) +{ + u32 out; + struct soc_sensor_entry *aux_entry = tzd->devdata; + int ret; + + ret = iosf_mbi_read(QRK_MBI_UNIT_RMU, MBI_REG_READ, + QRK_DTS_REG_OFFSET_ENABLE, &out); + if (ret) + return ret; + + if (!(out & QRK_DTS_ENABLE_BIT)) { + aux_entry->mode = THERMAL_DEVICE_DISABLED; + return 0; + } + + if (!aux_entry->locked) { + out &= ~QRK_DTS_ENABLE_BIT; + ret = iosf_mbi_write(QRK_MBI_UNIT_RMU, MBI_REG_WRITE, + QRK_DTS_REG_OFFSET_ENABLE, out); + + if (ret) + return ret; + + aux_entry->mode = THERMAL_DEVICE_DISABLED; + } else { + aux_entry->mode = THERMAL_DEVICE_ENABLED; + pr_info("DTS is locked. Cannot disable DTS\n"); + ret = -EPERM; + } + + return ret; +} + +static int _get_trip_temp(int trip, int *temp) +{ + int status; + u32 out; + + mutex_lock(&dts_update_mutex); + status = iosf_mbi_read(QRK_MBI_UNIT_RMU, MBI_REG_READ, + QRK_DTS_REG_OFFSET_PTPS, &out); + mutex_unlock(&dts_update_mutex); + + if (status) + return status; + + /* + * Thermal Sensor Programmable Trip Point Register has 8-bit + * fields for critical (catastrophic) and hot set trip point + * thresholds. The threshold value is always offset by its + * temperature base (50 degree Celsius). + */ + *temp = (out >> (trip * QRK_DTS_SHIFT_TP)) & QRK_DTS_MASK_TP_THRES; + *temp -= QRK_DTS_TEMP_BASE; + + return 0; +} + +static inline int sys_get_trip_temp(struct thermal_zone_device *tzd, + int trip, int *temp) +{ + return _get_trip_temp(trip, temp); +} + +static inline int sys_get_crit_temp(struct thermal_zone_device *tzd, int *temp) +{ + return _get_trip_temp(QRK_DTS_ID_TP_CRITICAL, temp); +} + +static int update_trip_temp(struct soc_sensor_entry *aux_entry, + int trip, int temp) +{ + u32 out; + u32 temp_out; + u32 store_ptps; + int ret; + + mutex_lock(&dts_update_mutex); + if (aux_entry->locked) { + ret = -EPERM; + goto failed; + } + + ret = iosf_mbi_read(QRK_MBI_UNIT_RMU, MBI_REG_READ, + QRK_DTS_REG_OFFSET_PTPS, &store_ptps); + if (ret) + goto failed; + + /* + * Protection against unsafe trip point thresdhold value. + * As Quark X1000 data-sheet does not provide any recommendation + * regarding the safe trip point threshold value to use, we choose + * the safe value according to the threshold value set by UEFI BIOS. + */ + if (temp > QRK_DTS_SAFE_TP_THRES) + temp = QRK_DTS_SAFE_TP_THRES; + + /* + * Thermal Sensor Programmable Trip Point Register has 8-bit + * fields for critical (catastrophic) and hot set trip point + * thresholds. The threshold value is always offset by its + * temperature base (50 degree Celsius). + */ + temp_out = temp + QRK_DTS_TEMP_BASE; + out = (store_ptps & ~(QRK_DTS_MASK_TP_THRES << + (trip * QRK_DTS_SHIFT_TP))); + out |= (temp_out & QRK_DTS_MASK_TP_THRES) << + (trip * QRK_DTS_SHIFT_TP); + + ret = iosf_mbi_write(QRK_MBI_UNIT_RMU, MBI_REG_WRITE, + QRK_DTS_REG_OFFSET_PTPS, out); + +failed: + mutex_unlock(&dts_update_mutex); + return ret; +} + +static inline int sys_set_trip_temp(struct thermal_zone_device *tzd, int trip, + int temp) +{ + return update_trip_temp(tzd->devdata, trip, temp); +} + +static int sys_get_trip_type(struct thermal_zone_device *thermal, + int trip, enum thermal_trip_type *type) +{ + if (trip) + *type = THERMAL_TRIP_HOT; + else + *type = THERMAL_TRIP_CRITICAL; + + return 0; +} + +static int sys_get_curr_temp(struct thermal_zone_device *tzd, + int *temp) +{ + u32 out; + int ret; + + mutex_lock(&dts_update_mutex); + ret = iosf_mbi_read(QRK_MBI_UNIT_RMU, MBI_REG_READ, + QRK_DTS_REG_OFFSET_TEMP, &out); + mutex_unlock(&dts_update_mutex); + + if (ret) + return ret; + + /* + * Thermal Sensor Temperature Register has 8-bit field + * for temperature value (offset by temperature base + * 50 degree Celsius). + */ + out = (out >> QRK_DTS_OFFSET_TEMP) & QRK_DTS_MASK_TEMP; + *temp = out - QRK_DTS_TEMP_BASE; + + return 0; +} + +static int sys_get_mode(struct thermal_zone_device *tzd, + enum thermal_device_mode *mode) +{ + struct soc_sensor_entry *aux_entry = tzd->devdata; + *mode = aux_entry->mode; + return 0; +} + +static int sys_set_mode(struct thermal_zone_device *tzd, + enum thermal_device_mode mode) +{ + int ret; + + mutex_lock(&dts_update_mutex); + if (mode == THERMAL_DEVICE_ENABLED) + ret = soc_dts_enable(tzd); + else + ret = soc_dts_disable(tzd); + mutex_unlock(&dts_update_mutex); + + return ret; +} + +static struct thermal_zone_device_ops tzone_ops = { + .get_temp = sys_get_curr_temp, + .get_trip_temp = sys_get_trip_temp, + .get_trip_type = sys_get_trip_type, + .set_trip_temp = sys_set_trip_temp, + .get_crit_temp = sys_get_crit_temp, + .get_mode = sys_get_mode, + .set_mode = sys_set_mode, +}; + +static void free_soc_dts(struct soc_sensor_entry *aux_entry) +{ + if (aux_entry) { + if (!aux_entry->locked) { + mutex_lock(&dts_update_mutex); + iosf_mbi_write(QRK_MBI_UNIT_RMU, MBI_REG_WRITE, + QRK_DTS_REG_OFFSET_ENABLE, + aux_entry->store_dts_enable); + + iosf_mbi_write(QRK_MBI_UNIT_RMU, MBI_REG_WRITE, + QRK_DTS_REG_OFFSET_PTPS, + aux_entry->store_ptps); + mutex_unlock(&dts_update_mutex); + } + thermal_zone_device_unregister(aux_entry->tzone); + kfree(aux_entry); + } +} + +static struct soc_sensor_entry *alloc_soc_dts(void) +{ + struct soc_sensor_entry *aux_entry; + int err; + u32 out; + int wr_mask; + + aux_entry = kzalloc(sizeof(*aux_entry), GFP_KERNEL); + if (!aux_entry) { + err = -ENOMEM; + return ERR_PTR(-ENOMEM); + } + + /* Check if DTS register is locked */ + err = iosf_mbi_read(QRK_MBI_UNIT_RMU, MBI_REG_READ, + QRK_DTS_REG_OFFSET_LOCK, &out); + if (err) + goto err_ret; + + if (out & QRK_DTS_LOCK_BIT) { + aux_entry->locked = true; + wr_mask = QRK_DTS_WR_MASK_CLR; + } else { + aux_entry->locked = false; + wr_mask = QRK_DTS_WR_MASK_SET; + } + + /* Store DTS default state if DTS registers are not locked */ + if (!aux_entry->locked) { + /* Store DTS default enable for restore on exit */ + err = iosf_mbi_read(QRK_MBI_UNIT_RMU, MBI_REG_READ, + QRK_DTS_REG_OFFSET_ENABLE, + &aux_entry->store_dts_enable); + if (err) + goto err_ret; + + /* Store DTS default PTPS register for restore on exit */ + err = iosf_mbi_read(QRK_MBI_UNIT_RMU, MBI_REG_READ, + QRK_DTS_REG_OFFSET_PTPS, + &aux_entry->store_ptps); + if (err) + goto err_ret; + } + + aux_entry->tzone = thermal_zone_device_register("quark_dts", + QRK_MAX_DTS_TRIPS, + wr_mask, + aux_entry, &tzone_ops, NULL, 0, polling_delay); + if (IS_ERR(aux_entry->tzone)) { + err = PTR_ERR(aux_entry->tzone); + goto err_ret; + } + + mutex_lock(&dts_update_mutex); + err = soc_dts_enable(aux_entry->tzone); + mutex_unlock(&dts_update_mutex); + if (err) + goto err_aux_status; + + return aux_entry; + +err_aux_status: + thermal_zone_device_unregister(aux_entry->tzone); +err_ret: + kfree(aux_entry); + return ERR_PTR(err); +} + +static const struct x86_cpu_id qrk_thermal_ids[] __initconst = { + { X86_VENDOR_INTEL, X86_FAMILY_QUARK, X86_MODEL_QUARK_X1000 }, + {} +}; +MODULE_DEVICE_TABLE(x86cpu, qrk_thermal_ids); + +static int __init intel_quark_thermal_init(void) +{ + int err = 0; + + if (!x86_match_cpu(qrk_thermal_ids) || !iosf_mbi_available()) + return -ENODEV; + + soc_dts = alloc_soc_dts(); + if (IS_ERR(soc_dts)) { + err = PTR_ERR(soc_dts); + goto err_free; + } + + return 0; + +err_free: + free_soc_dts(soc_dts); + return err; +} + +static void __exit intel_quark_thermal_exit(void) +{ + free_soc_dts(soc_dts); +} + +module_init(intel_quark_thermal_init) +module_exit(intel_quark_thermal_exit) + +MODULE_DESCRIPTION("Intel Quark DTS Thermal Driver"); +MODULE_AUTHOR("Ong Boon Leong <boon.leong.ong@intel.com>"); +MODULE_LICENSE("Dual BSD/GPL"); diff --git a/drivers/thermal/intel_soc_dts_iosf.c b/drivers/thermal/intel_soc_dts_iosf.c new file mode 100644 index 000000000..e0813dfaa --- /dev/null +++ b/drivers/thermal/intel_soc_dts_iosf.c @@ -0,0 +1,478 @@ +/* + * intel_soc_dts_iosf.c + * Copyright (c) 2015, Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/interrupt.h> +#include <asm/iosf_mbi.h> +#include "intel_soc_dts_iosf.h" + +#define SOC_DTS_OFFSET_ENABLE 0xB0 +#define SOC_DTS_OFFSET_TEMP 0xB1 + +#define SOC_DTS_OFFSET_PTPS 0xB2 +#define SOC_DTS_OFFSET_PTTS 0xB3 +#define SOC_DTS_OFFSET_PTTSS 0xB4 +#define SOC_DTS_OFFSET_PTMC 0x80 +#define SOC_DTS_TE_AUX0 0xB5 +#define SOC_DTS_TE_AUX1 0xB6 + +#define SOC_DTS_AUX0_ENABLE_BIT BIT(0) +#define SOC_DTS_AUX1_ENABLE_BIT BIT(1) +#define SOC_DTS_CPU_MODULE0_ENABLE_BIT BIT(16) +#define SOC_DTS_CPU_MODULE1_ENABLE_BIT BIT(17) +#define SOC_DTS_TE_SCI_ENABLE BIT(9) +#define SOC_DTS_TE_SMI_ENABLE BIT(10) +#define SOC_DTS_TE_MSI_ENABLE BIT(11) +#define SOC_DTS_TE_APICA_ENABLE BIT(14) +#define SOC_DTS_PTMC_APIC_DEASSERT_BIT BIT(4) + +/* DTS encoding for TJ MAX temperature */ +#define SOC_DTS_TJMAX_ENCODING 0x7F + +/* Only 2 out of 4 is allowed for OSPM */ +#define SOC_MAX_DTS_TRIPS 2 + +/* Mask for two trips in status bits */ +#define SOC_DTS_TRIP_MASK 0x03 + +/* DTS0 and DTS 1 */ +#define SOC_MAX_DTS_SENSORS 2 + +static int get_tj_max(u32 *tj_max) +{ + u32 eax, edx; + u32 val; + int err; + + err = rdmsr_safe(MSR_IA32_TEMPERATURE_TARGET, &eax, &edx); + if (err) + goto err_ret; + else { + val = (eax >> 16) & 0xff; + if (val) + *tj_max = val * 1000; + else { + err = -EINVAL; + goto err_ret; + } + } + + return 0; +err_ret: + *tj_max = 0; + + return err; +} + +static int sys_get_trip_temp(struct thermal_zone_device *tzd, int trip, + int *temp) +{ + int status; + u32 out; + struct intel_soc_dts_sensor_entry *dts; + struct intel_soc_dts_sensors *sensors; + + dts = tzd->devdata; + sensors = dts->sensors; + mutex_lock(&sensors->dts_update_lock); + status = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_PTPS, &out); + mutex_unlock(&sensors->dts_update_lock); + if (status) + return status; + + out = (out >> (trip * 8)) & SOC_DTS_TJMAX_ENCODING; + if (!out) + *temp = 0; + else + *temp = sensors->tj_max - out * 1000; + + return 0; +} + +static int update_trip_temp(struct intel_soc_dts_sensor_entry *dts, + int thres_index, int temp, + enum thermal_trip_type trip_type) +{ + int status; + u32 temp_out; + u32 out; + u32 store_ptps; + u32 store_ptmc; + u32 store_te_out; + u32 te_out; + u32 int_enable_bit = SOC_DTS_TE_APICA_ENABLE; + struct intel_soc_dts_sensors *sensors = dts->sensors; + + if (sensors->intr_type == INTEL_SOC_DTS_INTERRUPT_MSI) + int_enable_bit |= SOC_DTS_TE_MSI_ENABLE; + + temp_out = (sensors->tj_max - temp) / 1000; + + status = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_PTPS, &store_ptps); + if (status) + return status; + + out = (store_ptps & ~(0xFF << (thres_index * 8))); + out |= (temp_out & 0xFF) << (thres_index * 8); + status = iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_PTPS, out); + if (status) + return status; + + pr_debug("update_trip_temp PTPS = %x\n", out); + status = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_PTMC, &out); + if (status) + goto err_restore_ptps; + + store_ptmc = out; + + status = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_TE_AUX0 + thres_index, + &te_out); + if (status) + goto err_restore_ptmc; + + store_te_out = te_out; + /* Enable for CPU module 0 and module 1 */ + out |= (SOC_DTS_CPU_MODULE0_ENABLE_BIT | + SOC_DTS_CPU_MODULE1_ENABLE_BIT); + if (temp) { + if (thres_index) + out |= SOC_DTS_AUX1_ENABLE_BIT; + else + out |= SOC_DTS_AUX0_ENABLE_BIT; + te_out |= int_enable_bit; + } else { + if (thres_index) + out &= ~SOC_DTS_AUX1_ENABLE_BIT; + else + out &= ~SOC_DTS_AUX0_ENABLE_BIT; + te_out &= ~int_enable_bit; + } + status = iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_PTMC, out); + if (status) + goto err_restore_te_out; + + status = iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_TE_AUX0 + thres_index, + te_out); + if (status) + goto err_restore_te_out; + + dts->trip_types[thres_index] = trip_type; + + return 0; +err_restore_te_out: + iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_PTMC, store_te_out); +err_restore_ptmc: + iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_PTMC, store_ptmc); +err_restore_ptps: + iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_PTPS, store_ptps); + /* Nothing we can do if restore fails */ + + return status; +} + +static int sys_set_trip_temp(struct thermal_zone_device *tzd, int trip, + int temp) +{ + struct intel_soc_dts_sensor_entry *dts = tzd->devdata; + struct intel_soc_dts_sensors *sensors = dts->sensors; + int status; + + if (temp > sensors->tj_max) + return -EINVAL; + + mutex_lock(&sensors->dts_update_lock); + status = update_trip_temp(tzd->devdata, trip, temp, + dts->trip_types[trip]); + mutex_unlock(&sensors->dts_update_lock); + + return status; +} + +static int sys_get_trip_type(struct thermal_zone_device *tzd, + int trip, enum thermal_trip_type *type) +{ + struct intel_soc_dts_sensor_entry *dts; + + dts = tzd->devdata; + + *type = dts->trip_types[trip]; + + return 0; +} + +static int sys_get_curr_temp(struct thermal_zone_device *tzd, + int *temp) +{ + int status; + u32 out; + struct intel_soc_dts_sensor_entry *dts; + struct intel_soc_dts_sensors *sensors; + + dts = tzd->devdata; + sensors = dts->sensors; + status = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_TEMP, &out); + if (status) + return status; + + out = (out & dts->temp_mask) >> dts->temp_shift; + out -= SOC_DTS_TJMAX_ENCODING; + *temp = sensors->tj_max - out * 1000; + + return 0; +} + +static struct thermal_zone_device_ops tzone_ops = { + .get_temp = sys_get_curr_temp, + .get_trip_temp = sys_get_trip_temp, + .get_trip_type = sys_get_trip_type, + .set_trip_temp = sys_set_trip_temp, +}; + +static int soc_dts_enable(int id) +{ + u32 out; + int ret; + + ret = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_ENABLE, &out); + if (ret) + return ret; + + if (!(out & BIT(id))) { + out |= BIT(id); + ret = iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_ENABLE, out); + if (ret) + return ret; + } + + return ret; +} + +static void remove_dts_thermal_zone(struct intel_soc_dts_sensor_entry *dts) +{ + if (dts) { + iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_ENABLE, dts->store_status); + thermal_zone_device_unregister(dts->tzone); + } +} + +static int add_dts_thermal_zone(int id, struct intel_soc_dts_sensor_entry *dts, + bool notification_support, int trip_cnt, + int read_only_trip_cnt) +{ + char name[10]; + int trip_count = 0; + int trip_mask = 0; + u32 store_ptps; + int ret; + int i; + + /* Store status to restor on exit */ + ret = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_ENABLE, &dts->store_status); + if (ret) + goto err_ret; + + dts->id = id; + dts->temp_mask = 0x00FF << (id * 8); + dts->temp_shift = id * 8; + if (notification_support) { + trip_count = min(SOC_MAX_DTS_TRIPS, trip_cnt); + trip_mask = BIT(trip_count - read_only_trip_cnt) - 1; + } + + /* Check if the writable trip we provide is not used by BIOS */ + ret = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_PTPS, &store_ptps); + if (ret) + trip_mask = 0; + else { + for (i = 0; i < trip_count; ++i) { + if (trip_mask & BIT(i)) + if (store_ptps & (0xff << (i * 8))) + trip_mask &= ~BIT(i); + } + } + dts->trip_mask = trip_mask; + dts->trip_count = trip_count; + snprintf(name, sizeof(name), "soc_dts%d", id); + dts->tzone = thermal_zone_device_register(name, + trip_count, + trip_mask, + dts, &tzone_ops, + NULL, 0, 0); + if (IS_ERR(dts->tzone)) { + ret = PTR_ERR(dts->tzone); + goto err_ret; + } + + ret = soc_dts_enable(id); + if (ret) + goto err_enable; + + return 0; +err_enable: + thermal_zone_device_unregister(dts->tzone); +err_ret: + return ret; +} + +int intel_soc_dts_iosf_add_read_only_critical_trip( + struct intel_soc_dts_sensors *sensors, int critical_offset) +{ + int i, j; + + for (i = 0; i < SOC_MAX_DTS_SENSORS; ++i) { + for (j = 0; j < sensors->soc_dts[i].trip_count; ++j) { + if (!(sensors->soc_dts[i].trip_mask & BIT(j))) { + return update_trip_temp(&sensors->soc_dts[i], j, + sensors->tj_max - critical_offset, + THERMAL_TRIP_CRITICAL); + } + } + } + + return -EINVAL; +} +EXPORT_SYMBOL_GPL(intel_soc_dts_iosf_add_read_only_critical_trip); + +void intel_soc_dts_iosf_interrupt_handler(struct intel_soc_dts_sensors *sensors) +{ + u32 sticky_out; + int status; + u32 ptmc_out; + unsigned long flags; + + spin_lock_irqsave(&sensors->intr_notify_lock, flags); + + status = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_PTMC, &ptmc_out); + ptmc_out |= SOC_DTS_PTMC_APIC_DEASSERT_BIT; + status = iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_PTMC, ptmc_out); + + status = iosf_mbi_read(BT_MBI_UNIT_PMC, MBI_REG_READ, + SOC_DTS_OFFSET_PTTSS, &sticky_out); + pr_debug("status %d PTTSS %x\n", status, sticky_out); + if (sticky_out & SOC_DTS_TRIP_MASK) { + int i; + /* reset sticky bit */ + status = iosf_mbi_write(BT_MBI_UNIT_PMC, MBI_REG_WRITE, + SOC_DTS_OFFSET_PTTSS, sticky_out); + spin_unlock_irqrestore(&sensors->intr_notify_lock, flags); + + for (i = 0; i < SOC_MAX_DTS_SENSORS; ++i) { + pr_debug("TZD update for zone %d\n", i); + thermal_zone_device_update(sensors->soc_dts[i].tzone, + THERMAL_EVENT_UNSPECIFIED); + } + } else + spin_unlock_irqrestore(&sensors->intr_notify_lock, flags); +} +EXPORT_SYMBOL_GPL(intel_soc_dts_iosf_interrupt_handler); + +struct intel_soc_dts_sensors *intel_soc_dts_iosf_init( + enum intel_soc_dts_interrupt_type intr_type, int trip_count, + int read_only_trip_count) +{ + struct intel_soc_dts_sensors *sensors; + bool notification; + u32 tj_max; + int ret; + int i; + + if (!iosf_mbi_available()) + return ERR_PTR(-ENODEV); + + if (!trip_count || read_only_trip_count > trip_count) + return ERR_PTR(-EINVAL); + + if (get_tj_max(&tj_max)) + return ERR_PTR(-EINVAL); + + sensors = kzalloc(sizeof(*sensors), GFP_KERNEL); + if (!sensors) + return ERR_PTR(-ENOMEM); + + spin_lock_init(&sensors->intr_notify_lock); + mutex_init(&sensors->dts_update_lock); + sensors->intr_type = intr_type; + sensors->tj_max = tj_max; + if (intr_type == INTEL_SOC_DTS_INTERRUPT_NONE) + notification = false; + else + notification = true; + for (i = 0; i < SOC_MAX_DTS_SENSORS; ++i) { + sensors->soc_dts[i].sensors = sensors; + ret = add_dts_thermal_zone(i, &sensors->soc_dts[i], + notification, trip_count, + read_only_trip_count); + if (ret) + goto err_free; + } + + for (i = 0; i < SOC_MAX_DTS_SENSORS; ++i) { + ret = update_trip_temp(&sensors->soc_dts[i], 0, 0, + THERMAL_TRIP_PASSIVE); + if (ret) + goto err_remove_zone; + + ret = update_trip_temp(&sensors->soc_dts[i], 1, 0, + THERMAL_TRIP_PASSIVE); + if (ret) + goto err_remove_zone; + } + + return sensors; +err_remove_zone: + for (i = 0; i < SOC_MAX_DTS_SENSORS; ++i) + remove_dts_thermal_zone(&sensors->soc_dts[i]); + +err_free: + kfree(sensors); + return ERR_PTR(ret); +} +EXPORT_SYMBOL_GPL(intel_soc_dts_iosf_init); + +void intel_soc_dts_iosf_exit(struct intel_soc_dts_sensors *sensors) +{ + int i; + + for (i = 0; i < SOC_MAX_DTS_SENSORS; ++i) { + update_trip_temp(&sensors->soc_dts[i], 0, 0, 0); + update_trip_temp(&sensors->soc_dts[i], 1, 0, 0); + remove_dts_thermal_zone(&sensors->soc_dts[i]); + } + kfree(sensors); +} +EXPORT_SYMBOL_GPL(intel_soc_dts_iosf_exit); + +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/intel_soc_dts_iosf.h b/drivers/thermal/intel_soc_dts_iosf.h new file mode 100644 index 000000000..625e37bf9 --- /dev/null +++ b/drivers/thermal/intel_soc_dts_iosf.h @@ -0,0 +1,62 @@ +/* + * intel_soc_dts_iosf.h + * Copyright (c) 2015, Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + */ + +#ifndef _INTEL_SOC_DTS_IOSF_CORE_H +#define _INTEL_SOC_DTS_IOSF_CORE_H + +#include <linux/thermal.h> + +/* DTS0 and DTS 1 */ +#define SOC_MAX_DTS_SENSORS 2 + +enum intel_soc_dts_interrupt_type { + INTEL_SOC_DTS_INTERRUPT_NONE, + INTEL_SOC_DTS_INTERRUPT_APIC, + INTEL_SOC_DTS_INTERRUPT_MSI, + INTEL_SOC_DTS_INTERRUPT_SCI, + INTEL_SOC_DTS_INTERRUPT_SMI, +}; + +struct intel_soc_dts_sensors; + +struct intel_soc_dts_sensor_entry { + int id; + u32 temp_mask; + u32 temp_shift; + u32 store_status; + u32 trip_mask; + u32 trip_count; + enum thermal_trip_type trip_types[2]; + struct thermal_zone_device *tzone; + struct intel_soc_dts_sensors *sensors; +}; + +struct intel_soc_dts_sensors { + u32 tj_max; + spinlock_t intr_notify_lock; + struct mutex dts_update_lock; + enum intel_soc_dts_interrupt_type intr_type; + struct intel_soc_dts_sensor_entry soc_dts[SOC_MAX_DTS_SENSORS]; +}; + +struct intel_soc_dts_sensors *intel_soc_dts_iosf_init( + enum intel_soc_dts_interrupt_type intr_type, int trip_count, + int read_only_trip_count); +void intel_soc_dts_iosf_exit(struct intel_soc_dts_sensors *sensors); +void intel_soc_dts_iosf_interrupt_handler( + struct intel_soc_dts_sensors *sensors); +int intel_soc_dts_iosf_add_read_only_critical_trip( + struct intel_soc_dts_sensors *sensors, int critical_offset); +#endif diff --git a/drivers/thermal/intel_soc_dts_thermal.c b/drivers/thermal/intel_soc_dts_thermal.c new file mode 100644 index 000000000..d748527d7 --- /dev/null +++ b/drivers/thermal/intel_soc_dts_thermal.c @@ -0,0 +1,132 @@ +/* + * intel_soc_dts_thermal.c + * Copyright (c) 2014, Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/acpi.h> +#include <linux/module.h> +#include <linux/interrupt.h> +#include <asm/cpu_device_id.h> +#include <asm/intel-family.h> +#include "intel_soc_dts_iosf.h" + +#define CRITICAL_OFFSET_FROM_TJ_MAX 5000 + +static int crit_offset = CRITICAL_OFFSET_FROM_TJ_MAX; +module_param(crit_offset, int, 0644); +MODULE_PARM_DESC(crit_offset, + "Critical Temperature offset from tj max in millidegree Celsius."); + +/* IRQ 86 is a fixed APIC interrupt for BYT DTS Aux threshold notifications */ +#define BYT_SOC_DTS_APIC_IRQ 86 + +static int soc_dts_thres_gsi; +static int soc_dts_thres_irq; +static struct intel_soc_dts_sensors *soc_dts; + +static irqreturn_t soc_irq_thread_fn(int irq, void *dev_data) +{ + pr_debug("proc_thermal_interrupt\n"); + intel_soc_dts_iosf_interrupt_handler(soc_dts); + + return IRQ_HANDLED; +} + +static const struct x86_cpu_id soc_thermal_ids[] = { + { X86_VENDOR_INTEL, 6, INTEL_FAM6_ATOM_SILVERMONT, 0, + BYT_SOC_DTS_APIC_IRQ}, + {} +}; +MODULE_DEVICE_TABLE(x86cpu, soc_thermal_ids); + +static int __init intel_soc_thermal_init(void) +{ + int err = 0; + const struct x86_cpu_id *match_cpu; + + match_cpu = x86_match_cpu(soc_thermal_ids); + if (!match_cpu) + return -ENODEV; + + /* Create a zone with 2 trips with marked as read only */ + soc_dts = intel_soc_dts_iosf_init(INTEL_SOC_DTS_INTERRUPT_APIC, 2, 1); + if (IS_ERR(soc_dts)) { + err = PTR_ERR(soc_dts); + return err; + } + + soc_dts_thres_gsi = (int)match_cpu->driver_data; + if (soc_dts_thres_gsi) { + /* + * Note the flags here MUST match the firmware defaults, rather + * then the request_irq flags, otherwise we get an EBUSY error. + */ + soc_dts_thres_irq = acpi_register_gsi(NULL, soc_dts_thres_gsi, + ACPI_LEVEL_SENSITIVE, + ACPI_ACTIVE_LOW); + if (soc_dts_thres_irq < 0) { + pr_warn("intel_soc_dts: Could not get IRQ for GSI %d, err %d\n", + soc_dts_thres_gsi, soc_dts_thres_irq); + soc_dts_thres_irq = 0; + } + } + + if (soc_dts_thres_irq) { + err = request_threaded_irq(soc_dts_thres_irq, NULL, + soc_irq_thread_fn, + IRQF_TRIGGER_RISING | IRQF_ONESHOT, + "soc_dts", soc_dts); + if (err) { + /* + * Do not just error out because the user space thermal + * daemon such as DPTF may use polling instead of being + * interrupt driven. + */ + pr_warn("request_threaded_irq ret %d\n", err); + } + } + + err = intel_soc_dts_iosf_add_read_only_critical_trip(soc_dts, + crit_offset); + if (err) + goto error_trips; + + return 0; + +error_trips: + if (soc_dts_thres_irq) { + free_irq(soc_dts_thres_irq, soc_dts); + acpi_unregister_gsi(soc_dts_thres_gsi); + } + intel_soc_dts_iosf_exit(soc_dts); + + return err; +} + +static void __exit intel_soc_thermal_exit(void) +{ + if (soc_dts_thres_irq) { + free_irq(soc_dts_thres_irq, soc_dts); + acpi_unregister_gsi(soc_dts_thres_gsi); + } + intel_soc_dts_iosf_exit(soc_dts); +} + +module_init(intel_soc_thermal_init) +module_exit(intel_soc_thermal_exit) + +MODULE_DESCRIPTION("Intel SoC DTS Thermal Driver"); +MODULE_AUTHOR("Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/kirkwood_thermal.c b/drivers/thermal/kirkwood_thermal.c new file mode 100644 index 000000000..892236621 --- /dev/null +++ b/drivers/thermal/kirkwood_thermal.c @@ -0,0 +1,125 @@ +/* + * Kirkwood thermal sensor driver + * + * Copyright (C) 2012 Nobuhiro Iwamatsu <iwamatsu@nigauri.org> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +#include <linux/device.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/of.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/thermal.h> + +#define KIRKWOOD_THERMAL_VALID_OFFSET 9 +#define KIRKWOOD_THERMAL_VALID_MASK 0x1 +#define KIRKWOOD_THERMAL_TEMP_OFFSET 10 +#define KIRKWOOD_THERMAL_TEMP_MASK 0x1FF + +/* Kirkwood Thermal Sensor Dev Structure */ +struct kirkwood_thermal_priv { + void __iomem *sensor; +}; + +static int kirkwood_get_temp(struct thermal_zone_device *thermal, + int *temp) +{ + unsigned long reg; + struct kirkwood_thermal_priv *priv = thermal->devdata; + + reg = readl_relaxed(priv->sensor); + + /* Valid check */ + if (!((reg >> KIRKWOOD_THERMAL_VALID_OFFSET) & + KIRKWOOD_THERMAL_VALID_MASK)) { + dev_err(&thermal->device, + "Temperature sensor reading not valid\n"); + return -EIO; + } + + /* + * Calculate temperature. According to Marvell internal + * documentation the formula for this is: + * Celsius = (322-reg)/1.3625 + */ + reg = (reg >> KIRKWOOD_THERMAL_TEMP_OFFSET) & + KIRKWOOD_THERMAL_TEMP_MASK; + *temp = ((3220000000UL - (10000000UL * reg)) / 13625); + + return 0; +} + +static struct thermal_zone_device_ops ops = { + .get_temp = kirkwood_get_temp, +}; + +static const struct of_device_id kirkwood_thermal_id_table[] = { + { .compatible = "marvell,kirkwood-thermal" }, + {} +}; + +static int kirkwood_thermal_probe(struct platform_device *pdev) +{ + struct thermal_zone_device *thermal = NULL; + struct kirkwood_thermal_priv *priv; + struct resource *res; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + priv->sensor = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(priv->sensor)) + return PTR_ERR(priv->sensor); + + thermal = thermal_zone_device_register("kirkwood_thermal", 0, 0, + priv, &ops, NULL, 0, 0); + if (IS_ERR(thermal)) { + dev_err(&pdev->dev, + "Failed to register thermal zone device\n"); + return PTR_ERR(thermal); + } + + platform_set_drvdata(pdev, thermal); + + return 0; +} + +static int kirkwood_thermal_exit(struct platform_device *pdev) +{ + struct thermal_zone_device *kirkwood_thermal = + platform_get_drvdata(pdev); + + thermal_zone_device_unregister(kirkwood_thermal); + + return 0; +} + +MODULE_DEVICE_TABLE(of, kirkwood_thermal_id_table); + +static struct platform_driver kirkwood_thermal_driver = { + .probe = kirkwood_thermal_probe, + .remove = kirkwood_thermal_exit, + .driver = { + .name = "kirkwood_thermal", + .of_match_table = kirkwood_thermal_id_table, + }, +}; + +module_platform_driver(kirkwood_thermal_driver); + +MODULE_AUTHOR("Nobuhiro Iwamatsu <iwamatsu@nigauri.org>"); +MODULE_DESCRIPTION("kirkwood thermal driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/thermal/max77620_thermal.c b/drivers/thermal/max77620_thermal.c new file mode 100644 index 000000000..159bbcee8 --- /dev/null +++ b/drivers/thermal/max77620_thermal.c @@ -0,0 +1,171 @@ +/* + * Junction temperature thermal driver for Maxim Max77620. + * + * Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved. + * + * Author: Laxman Dewangan <ldewangan@nvidia.com> + * Mallikarjun Kasoju <mkasoju@nvidia.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + */ + +#include <linux/irq.h> +#include <linux/interrupt.h> +#include <linux/mfd/max77620.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/thermal.h> + +#define MAX77620_NORMAL_OPERATING_TEMP 100000 +#define MAX77620_TJALARM1_TEMP 120000 +#define MAX77620_TJALARM2_TEMP 140000 + +struct max77620_therm_info { + struct device *dev; + struct regmap *rmap; + struct thermal_zone_device *tz_device; + int irq_tjalarm1; + int irq_tjalarm2; +}; + +/** + * max77620_thermal_read_temp: Read PMIC die temperatue. + * @data: Device specific data. + * temp: Temperature in millidegrees Celsius + * + * The actual temperature of PMIC die is not available from PMIC. + * PMIC only tells the status if it has crossed or not the threshold level + * of 120degC or 140degC. + * If threshold has not been crossed then assume die temperature as 100degC + * else 120degC or 140deG based on the PMIC die temp threshold status. + * + * Return 0 on success otherwise error number to show reason of failure. + */ + +static int max77620_thermal_read_temp(void *data, int *temp) +{ + struct max77620_therm_info *mtherm = data; + unsigned int val; + int ret; + + ret = regmap_read(mtherm->rmap, MAX77620_REG_STATLBT, &val); + if (ret < 0) { + dev_err(mtherm->dev, "Failed to read STATLBT: %d\n", ret); + return ret; + } + + if (val & MAX77620_IRQ_TJALRM2_MASK) + *temp = MAX77620_TJALARM2_TEMP; + else if (val & MAX77620_IRQ_TJALRM1_MASK) + *temp = MAX77620_TJALARM1_TEMP; + else + *temp = MAX77620_NORMAL_OPERATING_TEMP; + + return 0; +} + +static const struct thermal_zone_of_device_ops max77620_thermal_ops = { + .get_temp = max77620_thermal_read_temp, +}; + +static irqreturn_t max77620_thermal_irq(int irq, void *data) +{ + struct max77620_therm_info *mtherm = data; + + if (irq == mtherm->irq_tjalarm1) + dev_warn(mtherm->dev, "Junction Temp Alarm1(120C) occurred\n"); + else if (irq == mtherm->irq_tjalarm2) + dev_crit(mtherm->dev, "Junction Temp Alarm2(140C) occurred\n"); + + thermal_zone_device_update(mtherm->tz_device, + THERMAL_EVENT_UNSPECIFIED); + + return IRQ_HANDLED; +} + +static int max77620_thermal_probe(struct platform_device *pdev) +{ + struct max77620_therm_info *mtherm; + int ret; + + mtherm = devm_kzalloc(&pdev->dev, sizeof(*mtherm), GFP_KERNEL); + if (!mtherm) + return -ENOMEM; + + mtherm->irq_tjalarm1 = platform_get_irq(pdev, 0); + mtherm->irq_tjalarm2 = platform_get_irq(pdev, 1); + if ((mtherm->irq_tjalarm1 < 0) || (mtherm->irq_tjalarm2 < 0)) { + dev_err(&pdev->dev, "Alarm irq number not available\n"); + return -EINVAL; + } + + mtherm->dev = &pdev->dev; + mtherm->rmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!mtherm->rmap) { + dev_err(&pdev->dev, "Failed to get parent regmap\n"); + return -ENODEV; + } + + /* + * The reference taken to the parent's node which will be balanced on + * reprobe or on platform-device release. + */ + device_set_of_node_from_dev(&pdev->dev, pdev->dev.parent); + + mtherm->tz_device = devm_thermal_zone_of_sensor_register(&pdev->dev, 0, + mtherm, &max77620_thermal_ops); + if (IS_ERR(mtherm->tz_device)) { + ret = PTR_ERR(mtherm->tz_device); + dev_err(&pdev->dev, "Failed to register thermal zone: %d\n", + ret); + return ret; + } + + ret = devm_request_threaded_irq(&pdev->dev, mtherm->irq_tjalarm1, NULL, + max77620_thermal_irq, + IRQF_ONESHOT | IRQF_SHARED, + dev_name(&pdev->dev), mtherm); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to request irq1: %d\n", ret); + return ret; + } + + ret = devm_request_threaded_irq(&pdev->dev, mtherm->irq_tjalarm2, NULL, + max77620_thermal_irq, + IRQF_ONESHOT | IRQF_SHARED, + dev_name(&pdev->dev), mtherm); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to request irq2: %d\n", ret); + return ret; + } + + platform_set_drvdata(pdev, mtherm); + + return 0; +} + +static struct platform_device_id max77620_thermal_devtype[] = { + { .name = "max77620-thermal", }, + {}, +}; +MODULE_DEVICE_TABLE(platform, max77620_thermal_devtype); + +static struct platform_driver max77620_thermal_driver = { + .driver = { + .name = "max77620-thermal", + }, + .probe = max77620_thermal_probe, + .id_table = max77620_thermal_devtype, +}; + +module_platform_driver(max77620_thermal_driver); + +MODULE_DESCRIPTION("Max77620 Junction temperature Thermal driver"); +MODULE_AUTHOR("Laxman Dewangan <ldewangan@nvidia.com>"); +MODULE_AUTHOR("Mallikarjun Kasoju <mkasoju@nvidia.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/mtk_thermal.c b/drivers/thermal/mtk_thermal.c new file mode 100644 index 000000000..0691f260f --- /dev/null +++ b/drivers/thermal/mtk_thermal.c @@ -0,0 +1,805 @@ +/* + * Copyright (c) 2015 MediaTek Inc. + * Author: Hanyi Wu <hanyi.wu@mediatek.com> + * Sascha Hauer <s.hauer@pengutronix.de> + * Dawei Chien <dawei.chien@mediatek.com> + * Louis Yu <louis.yu@mediatek.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/nvmem-consumer.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/io.h> +#include <linux/thermal.h> +#include <linux/reset.h> +#include <linux/types.h> + +/* AUXADC Registers */ +#define AUXADC_CON1_SET_V 0x008 +#define AUXADC_CON1_CLR_V 0x00c +#define AUXADC_CON2_V 0x010 +#define AUXADC_DATA(channel) (0x14 + (channel) * 4) + +#define APMIXED_SYS_TS_CON1 0x604 + +/* Thermal Controller Registers */ +#define TEMP_MONCTL0 0x000 +#define TEMP_MONCTL1 0x004 +#define TEMP_MONCTL2 0x008 +#define TEMP_MONIDET0 0x014 +#define TEMP_MONIDET1 0x018 +#define TEMP_MSRCTL0 0x038 +#define TEMP_AHBPOLL 0x040 +#define TEMP_AHBTO 0x044 +#define TEMP_ADCPNP0 0x048 +#define TEMP_ADCPNP1 0x04c +#define TEMP_ADCPNP2 0x050 +#define TEMP_ADCPNP3 0x0b4 + +#define TEMP_ADCMUX 0x054 +#define TEMP_ADCEN 0x060 +#define TEMP_PNPMUXADDR 0x064 +#define TEMP_ADCMUXADDR 0x068 +#define TEMP_ADCENADDR 0x074 +#define TEMP_ADCVALIDADDR 0x078 +#define TEMP_ADCVOLTADDR 0x07c +#define TEMP_RDCTRL 0x080 +#define TEMP_ADCVALIDMASK 0x084 +#define TEMP_ADCVOLTAGESHIFT 0x088 +#define TEMP_ADCWRITECTRL 0x08c +#define TEMP_MSR0 0x090 +#define TEMP_MSR1 0x094 +#define TEMP_MSR2 0x098 +#define TEMP_MSR3 0x0B8 + +#define TEMP_SPARE0 0x0f0 + +#define PTPCORESEL 0x400 + +#define TEMP_MONCTL1_PERIOD_UNIT(x) ((x) & 0x3ff) + +#define TEMP_MONCTL2_FILTER_INTERVAL(x) (((x) & 0x3ff) << 16) +#define TEMP_MONCTL2_SENSOR_INTERVAL(x) ((x) & 0x3ff) + +#define TEMP_AHBPOLL_ADC_POLL_INTERVAL(x) (x) + +#define TEMP_ADCWRITECTRL_ADC_PNP_WRITE BIT(0) +#define TEMP_ADCWRITECTRL_ADC_MUX_WRITE BIT(1) + +#define TEMP_ADCVALIDMASK_VALID_HIGH BIT(5) +#define TEMP_ADCVALIDMASK_VALID_POS(bit) (bit) + +/* MT8173 thermal sensors */ +#define MT8173_TS1 0 +#define MT8173_TS2 1 +#define MT8173_TS3 2 +#define MT8173_TS4 3 +#define MT8173_TSABB 4 + +/* AUXADC channel 11 is used for the temperature sensors */ +#define MT8173_TEMP_AUXADC_CHANNEL 11 + +/* The total number of temperature sensors in the MT8173 */ +#define MT8173_NUM_SENSORS 5 + +/* The number of banks in the MT8173 */ +#define MT8173_NUM_ZONES 4 + +/* The number of sensing points per bank */ +#define MT8173_NUM_SENSORS_PER_ZONE 4 + +/* + * Layout of the fuses providing the calibration data + * These macros could be used for MT8173, MT2701, and MT2712. + * MT8173 has 5 sensors and needs 5 VTS calibration data. + * MT2701 has 3 sensors and needs 3 VTS calibration data. + * MT2712 has 4 sensors and needs 4 VTS calibration data. + */ +#define MT8173_CALIB_BUF0_VALID BIT(0) +#define MT8173_CALIB_BUF1_ADC_GE(x) (((x) >> 22) & 0x3ff) +#define MT8173_CALIB_BUF0_VTS_TS1(x) (((x) >> 17) & 0x1ff) +#define MT8173_CALIB_BUF0_VTS_TS2(x) (((x) >> 8) & 0x1ff) +#define MT8173_CALIB_BUF1_VTS_TS3(x) (((x) >> 0) & 0x1ff) +#define MT8173_CALIB_BUF2_VTS_TS4(x) (((x) >> 23) & 0x1ff) +#define MT8173_CALIB_BUF2_VTS_TSABB(x) (((x) >> 14) & 0x1ff) +#define MT8173_CALIB_BUF0_DEGC_CALI(x) (((x) >> 1) & 0x3f) +#define MT8173_CALIB_BUF0_O_SLOPE(x) (((x) >> 26) & 0x3f) +#define MT8173_CALIB_BUF0_O_SLOPE_SIGN(x) (((x) >> 7) & 0x1) +#define MT8173_CALIB_BUF1_ID(x) (((x) >> 9) & 0x1) + +/* MT2701 thermal sensors */ +#define MT2701_TS1 0 +#define MT2701_TS2 1 +#define MT2701_TSABB 2 + +/* AUXADC channel 11 is used for the temperature sensors */ +#define MT2701_TEMP_AUXADC_CHANNEL 11 + +/* The total number of temperature sensors in the MT2701 */ +#define MT2701_NUM_SENSORS 3 + +/* The number of sensing points per bank */ +#define MT2701_NUM_SENSORS_PER_ZONE 3 + +/* MT2712 thermal sensors */ +#define MT2712_TS1 0 +#define MT2712_TS2 1 +#define MT2712_TS3 2 +#define MT2712_TS4 3 + +/* AUXADC channel 11 is used for the temperature sensors */ +#define MT2712_TEMP_AUXADC_CHANNEL 11 + +/* The total number of temperature sensors in the MT2712 */ +#define MT2712_NUM_SENSORS 4 + +/* The number of sensing points per bank */ +#define MT2712_NUM_SENSORS_PER_ZONE 4 + +#define MT7622_TEMP_AUXADC_CHANNEL 11 +#define MT7622_NUM_SENSORS 1 +#define MT7622_NUM_ZONES 1 +#define MT7622_NUM_SENSORS_PER_ZONE 1 +#define MT7622_TS1 0 + +struct mtk_thermal; + +struct thermal_bank_cfg { + unsigned int num_sensors; + const int *sensors; +}; + +struct mtk_thermal_bank { + struct mtk_thermal *mt; + int id; +}; + +struct mtk_thermal_data { + s32 num_banks; + s32 num_sensors; + s32 auxadc_channel; + const int *sensor_mux_values; + const int *msr; + const int *adcpnp; + struct thermal_bank_cfg bank_data[]; +}; + +struct mtk_thermal { + struct device *dev; + void __iomem *thermal_base; + + struct clk *clk_peri_therm; + struct clk *clk_auxadc; + /* lock: for getting and putting banks */ + struct mutex lock; + + /* Calibration values */ + s32 adc_ge; + s32 degc_cali; + s32 o_slope; + s32 vts[MT8173_NUM_SENSORS]; + + const struct mtk_thermal_data *conf; + struct mtk_thermal_bank banks[]; +}; + +/* MT8173 thermal sensor data */ +static const int mt8173_bank_data[MT8173_NUM_ZONES][3] = { + { MT8173_TS2, MT8173_TS3 }, + { MT8173_TS2, MT8173_TS4 }, + { MT8173_TS1, MT8173_TS2, MT8173_TSABB }, + { MT8173_TS2 }, +}; + +static const int mt8173_msr[MT8173_NUM_SENSORS_PER_ZONE] = { + TEMP_MSR0, TEMP_MSR1, TEMP_MSR2, TEMP_MSR3 +}; + +static const int mt8173_adcpnp[MT8173_NUM_SENSORS_PER_ZONE] = { + TEMP_ADCPNP0, TEMP_ADCPNP1, TEMP_ADCPNP2, TEMP_ADCPNP3 +}; + +static const int mt8173_mux_values[MT8173_NUM_SENSORS] = { 0, 1, 2, 3, 16 }; + +/* MT2701 thermal sensor data */ +static const int mt2701_bank_data[MT2701_NUM_SENSORS] = { + MT2701_TS1, MT2701_TS2, MT2701_TSABB +}; + +static const int mt2701_msr[MT2701_NUM_SENSORS_PER_ZONE] = { + TEMP_MSR0, TEMP_MSR1, TEMP_MSR2 +}; + +static const int mt2701_adcpnp[MT2701_NUM_SENSORS_PER_ZONE] = { + TEMP_ADCPNP0, TEMP_ADCPNP1, TEMP_ADCPNP2 +}; + +static const int mt2701_mux_values[MT2701_NUM_SENSORS] = { 0, 1, 16 }; + +/* MT2712 thermal sensor data */ +static const int mt2712_bank_data[MT2712_NUM_SENSORS] = { + MT2712_TS1, MT2712_TS2, MT2712_TS3, MT2712_TS4 +}; + +static const int mt2712_msr[MT2712_NUM_SENSORS_PER_ZONE] = { + TEMP_MSR0, TEMP_MSR1, TEMP_MSR2, TEMP_MSR3 +}; + +static const int mt2712_adcpnp[MT2712_NUM_SENSORS_PER_ZONE] = { + TEMP_ADCPNP0, TEMP_ADCPNP1, TEMP_ADCPNP2, TEMP_ADCPNP3 +}; + +static const int mt2712_mux_values[MT2712_NUM_SENSORS] = { 0, 1, 2, 3 }; + +/* MT7622 thermal sensor data */ +static const int mt7622_bank_data[MT7622_NUM_SENSORS] = { MT7622_TS1, }; +static const int mt7622_msr[MT7622_NUM_SENSORS_PER_ZONE] = { TEMP_MSR0, }; +static const int mt7622_adcpnp[MT7622_NUM_SENSORS_PER_ZONE] = { TEMP_ADCPNP0, }; +static const int mt7622_mux_values[MT7622_NUM_SENSORS] = { 0, }; + +/** + * The MT8173 thermal controller has four banks. Each bank can read up to + * four temperature sensors simultaneously. The MT8173 has a total of 5 + * temperature sensors. We use each bank to measure a certain area of the + * SoC. Since TS2 is located centrally in the SoC it is influenced by multiple + * areas, hence is used in different banks. + * + * The thermal core only gets the maximum temperature of all banks, so + * the bank concept wouldn't be necessary here. However, the SVS (Smart + * Voltage Scaling) unit makes its decisions based on the same bank + * data, and this indeed needs the temperatures of the individual banks + * for making better decisions. + */ +static const struct mtk_thermal_data mt8173_thermal_data = { + .auxadc_channel = MT8173_TEMP_AUXADC_CHANNEL, + .num_banks = MT8173_NUM_ZONES, + .num_sensors = MT8173_NUM_SENSORS, + .bank_data = { + { + .num_sensors = 2, + .sensors = mt8173_bank_data[0], + }, { + .num_sensors = 2, + .sensors = mt8173_bank_data[1], + }, { + .num_sensors = 3, + .sensors = mt8173_bank_data[2], + }, { + .num_sensors = 1, + .sensors = mt8173_bank_data[3], + }, + }, + .msr = mt8173_msr, + .adcpnp = mt8173_adcpnp, + .sensor_mux_values = mt8173_mux_values, +}; + +/** + * The MT2701 thermal controller has one bank, which can read up to + * three temperature sensors simultaneously. The MT2701 has a total of 3 + * temperature sensors. + * + * The thermal core only gets the maximum temperature of this one bank, + * so the bank concept wouldn't be necessary here. However, the SVS (Smart + * Voltage Scaling) unit makes its decisions based on the same bank + * data. + */ +static const struct mtk_thermal_data mt2701_thermal_data = { + .auxadc_channel = MT2701_TEMP_AUXADC_CHANNEL, + .num_banks = 1, + .num_sensors = MT2701_NUM_SENSORS, + .bank_data = { + { + .num_sensors = 3, + .sensors = mt2701_bank_data, + }, + }, + .msr = mt2701_msr, + .adcpnp = mt2701_adcpnp, + .sensor_mux_values = mt2701_mux_values, +}; + +/** + * The MT2712 thermal controller has one bank, which can read up to + * four temperature sensors simultaneously. The MT2712 has a total of 4 + * temperature sensors. + * + * The thermal core only gets the maximum temperature of this one bank, + * so the bank concept wouldn't be necessary here. However, the SVS (Smart + * Voltage Scaling) unit makes its decisions based on the same bank + * data. + */ +static const struct mtk_thermal_data mt2712_thermal_data = { + .auxadc_channel = MT2712_TEMP_AUXADC_CHANNEL, + .num_banks = 1, + .num_sensors = MT2712_NUM_SENSORS, + .bank_data = { + { + .num_sensors = 4, + .sensors = mt2712_bank_data, + }, + }, + .msr = mt2712_msr, + .adcpnp = mt2712_adcpnp, + .sensor_mux_values = mt2712_mux_values, +}; + +/* + * MT7622 have only one sensing point which uses AUXADC Channel 11 for raw data + * access. + */ +static const struct mtk_thermal_data mt7622_thermal_data = { + .auxadc_channel = MT7622_TEMP_AUXADC_CHANNEL, + .num_banks = MT7622_NUM_ZONES, + .num_sensors = MT7622_NUM_SENSORS, + .bank_data = { + { + .num_sensors = 1, + .sensors = mt7622_bank_data, + }, + }, + .msr = mt7622_msr, + .adcpnp = mt7622_adcpnp, + .sensor_mux_values = mt7622_mux_values, +}; + +/** + * raw_to_mcelsius - convert a raw ADC value to mcelsius + * @mt: The thermal controller + * @raw: raw ADC value + * + * This converts the raw ADC value to mcelsius using the SoC specific + * calibration constants + */ +static int raw_to_mcelsius(struct mtk_thermal *mt, int sensno, s32 raw) +{ + s32 tmp; + + raw &= 0xfff; + + tmp = 203450520 << 3; + tmp /= 165 + mt->o_slope; + tmp /= 10000 + mt->adc_ge; + tmp *= raw - mt->vts[sensno] - 3350; + tmp >>= 3; + + return mt->degc_cali * 500 - tmp; +} + +/** + * mtk_thermal_get_bank - get bank + * @bank: The bank + * + * The bank registers are banked, we have to select a bank in the + * PTPCORESEL register to access it. + */ +static void mtk_thermal_get_bank(struct mtk_thermal_bank *bank) +{ + struct mtk_thermal *mt = bank->mt; + u32 val; + + mutex_lock(&mt->lock); + + val = readl(mt->thermal_base + PTPCORESEL); + val &= ~0xf; + val |= bank->id; + writel(val, mt->thermal_base + PTPCORESEL); +} + +/** + * mtk_thermal_put_bank - release bank + * @bank: The bank + * + * release a bank previously taken with mtk_thermal_get_bank, + */ +static void mtk_thermal_put_bank(struct mtk_thermal_bank *bank) +{ + struct mtk_thermal *mt = bank->mt; + + mutex_unlock(&mt->lock); +} + +/** + * mtk_thermal_bank_temperature - get the temperature of a bank + * @bank: The bank + * + * The temperature of a bank is considered the maximum temperature of + * the sensors associated to the bank. + */ +static int mtk_thermal_bank_temperature(struct mtk_thermal_bank *bank) +{ + struct mtk_thermal *mt = bank->mt; + const struct mtk_thermal_data *conf = mt->conf; + int i, temp = INT_MIN, max = INT_MIN; + u32 raw; + + for (i = 0; i < conf->bank_data[bank->id].num_sensors; i++) { + raw = readl(mt->thermal_base + conf->msr[i]); + + temp = raw_to_mcelsius(mt, + conf->bank_data[bank->id].sensors[i], + raw); + + /* + * The first read of a sensor often contains very high bogus + * temperature value. Filter these out so that the system does + * not immediately shut down. + */ + if (temp > 200000) + temp = 0; + + if (temp > max) + max = temp; + } + + return max; +} + +static int mtk_read_temp(void *data, int *temperature) +{ + struct mtk_thermal *mt = data; + int i; + int tempmax = INT_MIN; + + for (i = 0; i < mt->conf->num_banks; i++) { + struct mtk_thermal_bank *bank = &mt->banks[i]; + + mtk_thermal_get_bank(bank); + + tempmax = max(tempmax, mtk_thermal_bank_temperature(bank)); + + mtk_thermal_put_bank(bank); + } + + *temperature = tempmax; + + return 0; +} + +static const struct thermal_zone_of_device_ops mtk_thermal_ops = { + .get_temp = mtk_read_temp, +}; + +static void mtk_thermal_init_bank(struct mtk_thermal *mt, int num, + u32 apmixed_phys_base, u32 auxadc_phys_base) +{ + struct mtk_thermal_bank *bank = &mt->banks[num]; + const struct mtk_thermal_data *conf = mt->conf; + int i; + + bank->id = num; + bank->mt = mt; + + mtk_thermal_get_bank(bank); + + /* bus clock 66M counting unit is 12 * 15.15ns * 256 = 46.540us */ + writel(TEMP_MONCTL1_PERIOD_UNIT(12), mt->thermal_base + TEMP_MONCTL1); + + /* + * filt interval is 1 * 46.540us = 46.54us, + * sen interval is 429 * 46.540us = 19.96ms + */ + writel(TEMP_MONCTL2_FILTER_INTERVAL(1) | + TEMP_MONCTL2_SENSOR_INTERVAL(429), + mt->thermal_base + TEMP_MONCTL2); + + /* poll is set to 10u */ + writel(TEMP_AHBPOLL_ADC_POLL_INTERVAL(768), + mt->thermal_base + TEMP_AHBPOLL); + + /* temperature sampling control, 1 sample */ + writel(0x0, mt->thermal_base + TEMP_MSRCTL0); + + /* exceed this polling time, IRQ would be inserted */ + writel(0xffffffff, mt->thermal_base + TEMP_AHBTO); + + /* number of interrupts per event, 1 is enough */ + writel(0x0, mt->thermal_base + TEMP_MONIDET0); + writel(0x0, mt->thermal_base + TEMP_MONIDET1); + + /* + * The MT8173 thermal controller does not have its own ADC. Instead it + * uses AHB bus accesses to control the AUXADC. To do this the thermal + * controller has to be programmed with the physical addresses of the + * AUXADC registers and with the various bit positions in the AUXADC. + * Also the thermal controller controls a mux in the APMIXEDSYS register + * space. + */ + + /* + * this value will be stored to TEMP_PNPMUXADDR (TEMP_SPARE0) + * automatically by hw + */ + writel(BIT(conf->auxadc_channel), mt->thermal_base + TEMP_ADCMUX); + + /* AHB address for auxadc mux selection */ + writel(auxadc_phys_base + AUXADC_CON1_CLR_V, + mt->thermal_base + TEMP_ADCMUXADDR); + + /* AHB address for pnp sensor mux selection */ + writel(apmixed_phys_base + APMIXED_SYS_TS_CON1, + mt->thermal_base + TEMP_PNPMUXADDR); + + /* AHB value for auxadc enable */ + writel(BIT(conf->auxadc_channel), mt->thermal_base + TEMP_ADCEN); + + /* AHB address for auxadc enable (channel 0 immediate mode selected) */ + writel(auxadc_phys_base + AUXADC_CON1_SET_V, + mt->thermal_base + TEMP_ADCENADDR); + + /* AHB address for auxadc valid bit */ + writel(auxadc_phys_base + AUXADC_DATA(conf->auxadc_channel), + mt->thermal_base + TEMP_ADCVALIDADDR); + + /* AHB address for auxadc voltage output */ + writel(auxadc_phys_base + AUXADC_DATA(conf->auxadc_channel), + mt->thermal_base + TEMP_ADCVOLTADDR); + + /* read valid & voltage are at the same register */ + writel(0x0, mt->thermal_base + TEMP_RDCTRL); + + /* indicate where the valid bit is */ + writel(TEMP_ADCVALIDMASK_VALID_HIGH | TEMP_ADCVALIDMASK_VALID_POS(12), + mt->thermal_base + TEMP_ADCVALIDMASK); + + /* no shift */ + writel(0x0, mt->thermal_base + TEMP_ADCVOLTAGESHIFT); + + /* enable auxadc mux write transaction */ + writel(TEMP_ADCWRITECTRL_ADC_MUX_WRITE, + mt->thermal_base + TEMP_ADCWRITECTRL); + + for (i = 0; i < conf->bank_data[num].num_sensors; i++) + writel(conf->sensor_mux_values[conf->bank_data[num].sensors[i]], + mt->thermal_base + conf->adcpnp[i]); + + writel((1 << conf->bank_data[num].num_sensors) - 1, + mt->thermal_base + TEMP_MONCTL0); + + writel(TEMP_ADCWRITECTRL_ADC_PNP_WRITE | + TEMP_ADCWRITECTRL_ADC_MUX_WRITE, + mt->thermal_base + TEMP_ADCWRITECTRL); + + mtk_thermal_put_bank(bank); +} + +static u64 of_get_phys_base(struct device_node *np) +{ + u64 size64; + const __be32 *regaddr_p; + + regaddr_p = of_get_address(np, 0, &size64, NULL); + if (!regaddr_p) + return OF_BAD_ADDR; + + return of_translate_address(np, regaddr_p); +} + +static int mtk_thermal_get_calibration_data(struct device *dev, + struct mtk_thermal *mt) +{ + struct nvmem_cell *cell; + u32 *buf; + size_t len; + int i, ret = 0; + + /* Start with default values */ + mt->adc_ge = 512; + for (i = 0; i < mt->conf->num_sensors; i++) + mt->vts[i] = 260; + mt->degc_cali = 40; + mt->o_slope = 0; + + cell = nvmem_cell_get(dev, "calibration-data"); + if (IS_ERR(cell)) { + if (PTR_ERR(cell) == -EPROBE_DEFER) + return PTR_ERR(cell); + return 0; + } + + buf = (u32 *)nvmem_cell_read(cell, &len); + + nvmem_cell_put(cell); + + if (IS_ERR(buf)) + return PTR_ERR(buf); + + if (len < 3 * sizeof(u32)) { + dev_warn(dev, "invalid calibration data\n"); + ret = -EINVAL; + goto out; + } + + if (buf[0] & MT8173_CALIB_BUF0_VALID) { + mt->adc_ge = MT8173_CALIB_BUF1_ADC_GE(buf[1]); + mt->vts[MT8173_TS1] = MT8173_CALIB_BUF0_VTS_TS1(buf[0]); + mt->vts[MT8173_TS2] = MT8173_CALIB_BUF0_VTS_TS2(buf[0]); + mt->vts[MT8173_TS3] = MT8173_CALIB_BUF1_VTS_TS3(buf[1]); + mt->vts[MT8173_TS4] = MT8173_CALIB_BUF2_VTS_TS4(buf[2]); + mt->vts[MT8173_TSABB] = MT8173_CALIB_BUF2_VTS_TSABB(buf[2]); + mt->degc_cali = MT8173_CALIB_BUF0_DEGC_CALI(buf[0]); + if (MT8173_CALIB_BUF1_ID(buf[1]) & + MT8173_CALIB_BUF0_O_SLOPE_SIGN(buf[0])) + mt->o_slope = -MT8173_CALIB_BUF0_O_SLOPE(buf[0]); + else + mt->o_slope = MT8173_CALIB_BUF0_O_SLOPE(buf[0]); + } else { + dev_info(dev, "Device not calibrated, using default calibration values\n"); + } + +out: + kfree(buf); + + return ret; +} + +static const struct of_device_id mtk_thermal_of_match[] = { + { + .compatible = "mediatek,mt8173-thermal", + .data = (void *)&mt8173_thermal_data, + }, + { + .compatible = "mediatek,mt2701-thermal", + .data = (void *)&mt2701_thermal_data, + }, + { + .compatible = "mediatek,mt2712-thermal", + .data = (void *)&mt2712_thermal_data, + }, + { + .compatible = "mediatek,mt7622-thermal", + .data = (void *)&mt7622_thermal_data, + }, { + }, +}; +MODULE_DEVICE_TABLE(of, mtk_thermal_of_match); + +static int mtk_thermal_probe(struct platform_device *pdev) +{ + int ret, i; + struct device_node *auxadc, *apmixedsys, *np = pdev->dev.of_node; + struct mtk_thermal *mt; + struct resource *res; + u64 auxadc_phys_base, apmixed_phys_base; + struct thermal_zone_device *tzdev; + + mt = devm_kzalloc(&pdev->dev, sizeof(*mt), GFP_KERNEL); + if (!mt) + return -ENOMEM; + + mt->conf = of_device_get_match_data(&pdev->dev); + + mt->clk_peri_therm = devm_clk_get(&pdev->dev, "therm"); + if (IS_ERR(mt->clk_peri_therm)) + return PTR_ERR(mt->clk_peri_therm); + + mt->clk_auxadc = devm_clk_get(&pdev->dev, "auxadc"); + if (IS_ERR(mt->clk_auxadc)) + return PTR_ERR(mt->clk_auxadc); + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + mt->thermal_base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(mt->thermal_base)) + return PTR_ERR(mt->thermal_base); + + ret = mtk_thermal_get_calibration_data(&pdev->dev, mt); + if (ret) + return ret; + + mutex_init(&mt->lock); + + mt->dev = &pdev->dev; + + auxadc = of_parse_phandle(np, "mediatek,auxadc", 0); + if (!auxadc) { + dev_err(&pdev->dev, "missing auxadc node\n"); + return -ENODEV; + } + + auxadc_phys_base = of_get_phys_base(auxadc); + + of_node_put(auxadc); + + if (auxadc_phys_base == OF_BAD_ADDR) { + dev_err(&pdev->dev, "Can't get auxadc phys address\n"); + return -EINVAL; + } + + apmixedsys = of_parse_phandle(np, "mediatek,apmixedsys", 0); + if (!apmixedsys) { + dev_err(&pdev->dev, "missing apmixedsys node\n"); + return -ENODEV; + } + + apmixed_phys_base = of_get_phys_base(apmixedsys); + + of_node_put(apmixedsys); + + if (apmixed_phys_base == OF_BAD_ADDR) { + dev_err(&pdev->dev, "Can't get auxadc phys address\n"); + return -EINVAL; + } + + ret = device_reset(&pdev->dev); + if (ret) + return ret; + + ret = clk_prepare_enable(mt->clk_auxadc); + if (ret) { + dev_err(&pdev->dev, "Can't enable auxadc clk: %d\n", ret); + return ret; + } + + ret = clk_prepare_enable(mt->clk_peri_therm); + if (ret) { + dev_err(&pdev->dev, "Can't enable peri clk: %d\n", ret); + goto err_disable_clk_auxadc; + } + + for (i = 0; i < mt->conf->num_banks; i++) + mtk_thermal_init_bank(mt, i, apmixed_phys_base, + auxadc_phys_base); + + platform_set_drvdata(pdev, mt); + + tzdev = devm_thermal_zone_of_sensor_register(&pdev->dev, 0, mt, + &mtk_thermal_ops); + if (IS_ERR(tzdev)) { + ret = PTR_ERR(tzdev); + goto err_disable_clk_peri_therm; + } + + return 0; + +err_disable_clk_peri_therm: + clk_disable_unprepare(mt->clk_peri_therm); +err_disable_clk_auxadc: + clk_disable_unprepare(mt->clk_auxadc); + + return ret; +} + +static int mtk_thermal_remove(struct platform_device *pdev) +{ + struct mtk_thermal *mt = platform_get_drvdata(pdev); + + clk_disable_unprepare(mt->clk_peri_therm); + clk_disable_unprepare(mt->clk_auxadc); + + return 0; +} + +static struct platform_driver mtk_thermal_driver = { + .probe = mtk_thermal_probe, + .remove = mtk_thermal_remove, + .driver = { + .name = "mtk-thermal", + .of_match_table = mtk_thermal_of_match, + }, +}; + +module_platform_driver(mtk_thermal_driver); + +MODULE_AUTHOR("Louis Yu <louis.yu@mediatek.com>"); +MODULE_AUTHOR("Dawei Chien <dawei.chien@mediatek.com>"); +MODULE_AUTHOR("Sascha Hauer <s.hauer@pengutronix.de>"); +MODULE_AUTHOR("Hanyi Wu <hanyi.wu@mediatek.com>"); +MODULE_DESCRIPTION("Mediatek thermal driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/of-thermal.c b/drivers/thermal/of-thermal.c new file mode 100644 index 000000000..4f2816559 --- /dev/null +++ b/drivers/thermal/of-thermal.c @@ -0,0 +1,1054 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * of-thermal.c - Generic Thermal Management device tree support. + * + * Copyright (C) 2013 Texas Instruments + * Copyright (C) 2013 Eduardo Valentin <eduardo.valentin@ti.com> + */ +#include <linux/thermal.h> +#include <linux/slab.h> +#include <linux/types.h> +#include <linux/of_device.h> +#include <linux/of_platform.h> +#include <linux/err.h> +#include <linux/export.h> +#include <linux/string.h> + +#include "thermal_core.h" + +/*** Private data structures to represent thermal device tree data ***/ + +/** + * struct __thermal_bind_param - a match between trip and cooling device + * @cooling_device: a pointer to identify the referred cooling device + * @trip_id: the trip point index + * @usage: the percentage (from 0 to 100) of cooling contribution + * @min: minimum cooling state used at this trip point + * @max: maximum cooling state used at this trip point + */ + +struct __thermal_bind_params { + struct device_node *cooling_device; + unsigned int trip_id; + unsigned int usage; + unsigned long min; + unsigned long max; +}; + +/** + * struct __thermal_zone - internal representation of a thermal zone + * @mode: current thermal zone device mode (enabled/disabled) + * @passive_delay: polling interval while passive cooling is activated + * @polling_delay: zone polling interval + * @slope: slope of the temperature adjustment curve + * @offset: offset of the temperature adjustment curve + * @ntrips: number of trip points + * @trips: an array of trip points (0..ntrips - 1) + * @num_tbps: number of thermal bind params + * @tbps: an array of thermal bind params (0..num_tbps - 1) + * @sensor_data: sensor private data used while reading temperature and trend + * @ops: set of callbacks to handle the thermal zone based on DT + */ + +struct __thermal_zone { + enum thermal_device_mode mode; + int passive_delay; + int polling_delay; + int slope; + int offset; + + /* trip data */ + int ntrips; + struct thermal_trip *trips; + + /* cooling binding data */ + int num_tbps; + struct __thermal_bind_params *tbps; + + /* sensor interface */ + void *sensor_data; + const struct thermal_zone_of_device_ops *ops; +}; + +/*** DT thermal zone device callbacks ***/ + +static int of_thermal_get_temp(struct thermal_zone_device *tz, + int *temp) +{ + struct __thermal_zone *data = tz->devdata; + + if (!data->ops->get_temp) + return -EINVAL; + + return data->ops->get_temp(data->sensor_data, temp); +} + +static int of_thermal_set_trips(struct thermal_zone_device *tz, + int low, int high) +{ + struct __thermal_zone *data = tz->devdata; + + if (!data->ops || !data->ops->set_trips) + return -EINVAL; + + return data->ops->set_trips(data->sensor_data, low, high); +} + +/** + * of_thermal_get_ntrips - function to export number of available trip + * points. + * @tz: pointer to a thermal zone + * + * This function is a globally visible wrapper to get number of trip points + * stored in the local struct __thermal_zone + * + * Return: number of available trip points, -ENODEV when data not available + */ +int of_thermal_get_ntrips(struct thermal_zone_device *tz) +{ + struct __thermal_zone *data = tz->devdata; + + if (!data || IS_ERR(data)) + return -ENODEV; + + return data->ntrips; +} +EXPORT_SYMBOL_GPL(of_thermal_get_ntrips); + +/** + * of_thermal_is_trip_valid - function to check if trip point is valid + * + * @tz: pointer to a thermal zone + * @trip: trip point to evaluate + * + * This function is responsible for checking if passed trip point is valid + * + * Return: true if trip point is valid, false otherwise + */ +bool of_thermal_is_trip_valid(struct thermal_zone_device *tz, int trip) +{ + struct __thermal_zone *data = tz->devdata; + + if (!data || trip >= data->ntrips || trip < 0) + return false; + + return true; +} +EXPORT_SYMBOL_GPL(of_thermal_is_trip_valid); + +/** + * of_thermal_get_trip_points - function to get access to a globally exported + * trip points + * + * @tz: pointer to a thermal zone + * + * This function provides a pointer to trip points table + * + * Return: pointer to trip points table, NULL otherwise + */ +const struct thermal_trip * +of_thermal_get_trip_points(struct thermal_zone_device *tz) +{ + struct __thermal_zone *data = tz->devdata; + + if (!data) + return NULL; + + return data->trips; +} +EXPORT_SYMBOL_GPL(of_thermal_get_trip_points); + +/** + * of_thermal_set_emul_temp - function to set emulated temperature + * + * @tz: pointer to a thermal zone + * @temp: temperature to set + * + * This function gives the ability to set emulated value of temperature, + * which is handy for debugging + * + * Return: zero on success, error code otherwise + */ +static int of_thermal_set_emul_temp(struct thermal_zone_device *tz, + int temp) +{ + struct __thermal_zone *data = tz->devdata; + + return data->ops->set_emul_temp(data->sensor_data, temp); +} + +static int of_thermal_get_trend(struct thermal_zone_device *tz, int trip, + enum thermal_trend *trend) +{ + struct __thermal_zone *data = tz->devdata; + + if (!data->ops->get_trend) + return -EINVAL; + + return data->ops->get_trend(data->sensor_data, trip, trend); +} + +static int of_thermal_bind(struct thermal_zone_device *thermal, + struct thermal_cooling_device *cdev) +{ + struct __thermal_zone *data = thermal->devdata; + int i; + + if (!data || IS_ERR(data)) + return -ENODEV; + + /* find where to bind */ + for (i = 0; i < data->num_tbps; i++) { + struct __thermal_bind_params *tbp = data->tbps + i; + + if (tbp->cooling_device == cdev->np) { + int ret; + + ret = thermal_zone_bind_cooling_device(thermal, + tbp->trip_id, cdev, + tbp->max, + tbp->min, + tbp->usage); + if (ret) + return ret; + } + } + + return 0; +} + +static int of_thermal_unbind(struct thermal_zone_device *thermal, + struct thermal_cooling_device *cdev) +{ + struct __thermal_zone *data = thermal->devdata; + int i; + + if (!data || IS_ERR(data)) + return -ENODEV; + + /* find where to unbind */ + for (i = 0; i < data->num_tbps; i++) { + struct __thermal_bind_params *tbp = data->tbps + i; + + if (tbp->cooling_device == cdev->np) { + int ret; + + ret = thermal_zone_unbind_cooling_device(thermal, + tbp->trip_id, cdev); + if (ret) + return ret; + } + } + + return 0; +} + +static int of_thermal_get_mode(struct thermal_zone_device *tz, + enum thermal_device_mode *mode) +{ + struct __thermal_zone *data = tz->devdata; + + *mode = data->mode; + + return 0; +} + +static int of_thermal_set_mode(struct thermal_zone_device *tz, + enum thermal_device_mode mode) +{ + struct __thermal_zone *data = tz->devdata; + + mutex_lock(&tz->lock); + + if (mode == THERMAL_DEVICE_ENABLED) { + tz->polling_delay = data->polling_delay; + tz->passive_delay = data->passive_delay; + } else { + tz->polling_delay = 0; + tz->passive_delay = 0; + } + + mutex_unlock(&tz->lock); + + data->mode = mode; + thermal_zone_device_update(tz, THERMAL_EVENT_UNSPECIFIED); + + return 0; +} + +static int of_thermal_get_trip_type(struct thermal_zone_device *tz, int trip, + enum thermal_trip_type *type) +{ + struct __thermal_zone *data = tz->devdata; + + if (trip >= data->ntrips || trip < 0) + return -EDOM; + + *type = data->trips[trip].type; + + return 0; +} + +static int of_thermal_get_trip_temp(struct thermal_zone_device *tz, int trip, + int *temp) +{ + struct __thermal_zone *data = tz->devdata; + + if (trip >= data->ntrips || trip < 0) + return -EDOM; + + *temp = data->trips[trip].temperature; + + return 0; +} + +static int of_thermal_set_trip_temp(struct thermal_zone_device *tz, int trip, + int temp) +{ + struct __thermal_zone *data = tz->devdata; + + if (trip >= data->ntrips || trip < 0) + return -EDOM; + + if (data->ops->set_trip_temp) { + int ret; + + ret = data->ops->set_trip_temp(data->sensor_data, trip, temp); + if (ret) + return ret; + } + + /* thermal framework should take care of data->mask & (1 << trip) */ + data->trips[trip].temperature = temp; + + return 0; +} + +static int of_thermal_get_trip_hyst(struct thermal_zone_device *tz, int trip, + int *hyst) +{ + struct __thermal_zone *data = tz->devdata; + + if (trip >= data->ntrips || trip < 0) + return -EDOM; + + *hyst = data->trips[trip].hysteresis; + + return 0; +} + +static int of_thermal_set_trip_hyst(struct thermal_zone_device *tz, int trip, + int hyst) +{ + struct __thermal_zone *data = tz->devdata; + + if (trip >= data->ntrips || trip < 0) + return -EDOM; + + /* thermal framework should take care of data->mask & (1 << trip) */ + data->trips[trip].hysteresis = hyst; + + return 0; +} + +static int of_thermal_get_crit_temp(struct thermal_zone_device *tz, + int *temp) +{ + struct __thermal_zone *data = tz->devdata; + int i; + + for (i = 0; i < data->ntrips; i++) + if (data->trips[i].type == THERMAL_TRIP_CRITICAL) { + *temp = data->trips[i].temperature; + return 0; + } + + return -EINVAL; +} + +static struct thermal_zone_device_ops of_thermal_ops = { + .get_mode = of_thermal_get_mode, + .set_mode = of_thermal_set_mode, + + .get_trip_type = of_thermal_get_trip_type, + .get_trip_temp = of_thermal_get_trip_temp, + .set_trip_temp = of_thermal_set_trip_temp, + .get_trip_hyst = of_thermal_get_trip_hyst, + .set_trip_hyst = of_thermal_set_trip_hyst, + .get_crit_temp = of_thermal_get_crit_temp, + + .bind = of_thermal_bind, + .unbind = of_thermal_unbind, +}; + +/*** sensor API ***/ + +static struct thermal_zone_device * +thermal_zone_of_add_sensor(struct device_node *zone, + struct device_node *sensor, void *data, + const struct thermal_zone_of_device_ops *ops) +{ + struct thermal_zone_device *tzd; + struct __thermal_zone *tz; + + tzd = thermal_zone_get_zone_by_name(zone->name); + if (IS_ERR(tzd)) + return ERR_PTR(-EPROBE_DEFER); + + tz = tzd->devdata; + + if (!ops) + return ERR_PTR(-EINVAL); + + mutex_lock(&tzd->lock); + tz->ops = ops; + tz->sensor_data = data; + + tzd->ops->get_temp = of_thermal_get_temp; + tzd->ops->get_trend = of_thermal_get_trend; + + /* + * The thermal zone core will calculate the window if they have set the + * optional set_trips pointer. + */ + if (ops->set_trips) + tzd->ops->set_trips = of_thermal_set_trips; + + if (ops->set_emul_temp) + tzd->ops->set_emul_temp = of_thermal_set_emul_temp; + + mutex_unlock(&tzd->lock); + + return tzd; +} + +/** + * thermal_zone_of_sensor_register - registers a sensor to a DT thermal zone + * @dev: a valid struct device pointer of a sensor device. Must contain + * a valid .of_node, for the sensor node. + * @sensor_id: a sensor identifier, in case the sensor IP has more + * than one sensors + * @data: a private pointer (owned by the caller) that will be passed + * back, when a temperature reading is needed. + * @ops: struct thermal_zone_of_device_ops *. Must contain at least .get_temp. + * + * This function will search the list of thermal zones described in device + * tree and look for the zone that refer to the sensor device pointed by + * @dev->of_node as temperature providers. For the zone pointing to the + * sensor node, the sensor will be added to the DT thermal zone device. + * + * The thermal zone temperature is provided by the @get_temp function + * pointer. When called, it will have the private pointer @data back. + * + * The thermal zone temperature trend is provided by the @get_trend function + * pointer. When called, it will have the private pointer @data back. + * + * TODO: + * 01 - This function must enqueue the new sensor instead of using + * it as the only source of temperature values. + * + * 02 - There must be a way to match the sensor with all thermal zones + * that refer to it. + * + * Return: On success returns a valid struct thermal_zone_device, + * otherwise, it returns a corresponding ERR_PTR(). Caller must + * check the return value with help of IS_ERR() helper. + */ +struct thermal_zone_device * +thermal_zone_of_sensor_register(struct device *dev, int sensor_id, void *data, + const struct thermal_zone_of_device_ops *ops) +{ + struct device_node *np, *child, *sensor_np; + struct thermal_zone_device *tzd = ERR_PTR(-ENODEV); + + np = of_find_node_by_name(NULL, "thermal-zones"); + if (!np) + return ERR_PTR(-ENODEV); + + if (!dev || !dev->of_node) { + of_node_put(np); + return ERR_PTR(-EINVAL); + } + + sensor_np = of_node_get(dev->of_node); + + for_each_available_child_of_node(np, child) { + struct of_phandle_args sensor_specs; + int ret, id; + + /* For now, thermal framework supports only 1 sensor per zone */ + ret = of_parse_phandle_with_args(child, "thermal-sensors", + "#thermal-sensor-cells", + 0, &sensor_specs); + if (ret) + continue; + + if (sensor_specs.args_count >= 1) { + id = sensor_specs.args[0]; + WARN(sensor_specs.args_count > 1, + "%s: too many cells in sensor specifier %d\n", + sensor_specs.np->name, sensor_specs.args_count); + } else { + id = 0; + } + + if (sensor_specs.np == sensor_np && id == sensor_id) { + tzd = thermal_zone_of_add_sensor(child, sensor_np, + data, ops); + if (!IS_ERR(tzd)) + tzd->ops->set_mode(tzd, THERMAL_DEVICE_ENABLED); + + of_node_put(sensor_specs.np); + of_node_put(child); + goto exit; + } + of_node_put(sensor_specs.np); + } +exit: + of_node_put(sensor_np); + of_node_put(np); + + return tzd; +} +EXPORT_SYMBOL_GPL(thermal_zone_of_sensor_register); + +/** + * thermal_zone_of_sensor_unregister - unregisters a sensor from a DT thermal zone + * @dev: a valid struct device pointer of a sensor device. Must contain + * a valid .of_node, for the sensor node. + * @tzd: a pointer to struct thermal_zone_device where the sensor is registered. + * + * This function removes the sensor callbacks and private data from the + * thermal zone device registered with thermal_zone_of_sensor_register() + * API. It will also silent the zone by remove the .get_temp() and .get_trend() + * thermal zone device callbacks. + * + * TODO: When the support to several sensors per zone is added, this + * function must search the sensor list based on @dev parameter. + * + */ +void thermal_zone_of_sensor_unregister(struct device *dev, + struct thermal_zone_device *tzd) +{ + struct __thermal_zone *tz; + + if (!dev || !tzd || !tzd->devdata) + return; + + tz = tzd->devdata; + + /* no __thermal_zone, nothing to be done */ + if (!tz) + return; + + mutex_lock(&tzd->lock); + tzd->ops->get_temp = NULL; + tzd->ops->get_trend = NULL; + tzd->ops->set_emul_temp = NULL; + + tz->ops = NULL; + tz->sensor_data = NULL; + mutex_unlock(&tzd->lock); +} +EXPORT_SYMBOL_GPL(thermal_zone_of_sensor_unregister); + +static void devm_thermal_zone_of_sensor_release(struct device *dev, void *res) +{ + thermal_zone_of_sensor_unregister(dev, + *(struct thermal_zone_device **)res); +} + +static int devm_thermal_zone_of_sensor_match(struct device *dev, void *res, + void *data) +{ + struct thermal_zone_device **r = res; + + if (WARN_ON(!r || !*r)) + return 0; + + return *r == data; +} + +/** + * devm_thermal_zone_of_sensor_register - Resource managed version of + * thermal_zone_of_sensor_register() + * @dev: a valid struct device pointer of a sensor device. Must contain + * a valid .of_node, for the sensor node. + * @sensor_id: a sensor identifier, in case the sensor IP has more + * than one sensors + * @data: a private pointer (owned by the caller) that will be passed + * back, when a temperature reading is needed. + * @ops: struct thermal_zone_of_device_ops *. Must contain at least .get_temp. + * + * Refer thermal_zone_of_sensor_register() for more details. + * + * Return: On success returns a valid struct thermal_zone_device, + * otherwise, it returns a corresponding ERR_PTR(). Caller must + * check the return value with help of IS_ERR() helper. + * Registered thermal_zone_device device will automatically be + * released when device is unbounded. + */ +struct thermal_zone_device *devm_thermal_zone_of_sensor_register( + struct device *dev, int sensor_id, + void *data, const struct thermal_zone_of_device_ops *ops) +{ + struct thermal_zone_device **ptr, *tzd; + + ptr = devres_alloc(devm_thermal_zone_of_sensor_release, sizeof(*ptr), + GFP_KERNEL); + if (!ptr) + return ERR_PTR(-ENOMEM); + + tzd = thermal_zone_of_sensor_register(dev, sensor_id, data, ops); + if (IS_ERR(tzd)) { + devres_free(ptr); + return tzd; + } + + *ptr = tzd; + devres_add(dev, ptr); + + return tzd; +} +EXPORT_SYMBOL_GPL(devm_thermal_zone_of_sensor_register); + +/** + * devm_thermal_zone_of_sensor_unregister - Resource managed version of + * thermal_zone_of_sensor_unregister(). + * @dev: Device for which which resource was allocated. + * @tzd: a pointer to struct thermal_zone_device where the sensor is registered. + * + * This function removes the sensor callbacks and private data from the + * thermal zone device registered with devm_thermal_zone_of_sensor_register() + * API. It will also silent the zone by remove the .get_temp() and .get_trend() + * thermal zone device callbacks. + * Normally this function will not need to be called and the resource + * management code will ensure that the resource is freed. + */ +void devm_thermal_zone_of_sensor_unregister(struct device *dev, + struct thermal_zone_device *tzd) +{ + WARN_ON(devres_release(dev, devm_thermal_zone_of_sensor_release, + devm_thermal_zone_of_sensor_match, tzd)); +} +EXPORT_SYMBOL_GPL(devm_thermal_zone_of_sensor_unregister); + +/*** functions parsing device tree nodes ***/ + +/** + * thermal_of_populate_bind_params - parse and fill cooling map data + * @np: DT node containing a cooling-map node + * @__tbp: data structure to be filled with cooling map info + * @trips: array of thermal zone trip points + * @ntrips: number of trip points inside trips. + * + * This function parses a cooling-map type of node represented by + * @np parameter and fills the read data into @__tbp data structure. + * It needs the already parsed array of trip points of the thermal zone + * in consideration. + * + * Return: 0 on success, proper error code otherwise + */ +static int thermal_of_populate_bind_params(struct device_node *np, + struct __thermal_bind_params *__tbp, + struct thermal_trip *trips, + int ntrips) +{ + struct of_phandle_args cooling_spec; + struct device_node *trip; + int ret, i; + u32 prop; + + /* Default weight. Usage is optional */ + __tbp->usage = THERMAL_WEIGHT_DEFAULT; + ret = of_property_read_u32(np, "contribution", &prop); + if (ret == 0) + __tbp->usage = prop; + + trip = of_parse_phandle(np, "trip", 0); + if (!trip) { + pr_err("missing trip property\n"); + return -ENODEV; + } + + /* match using device_node */ + for (i = 0; i < ntrips; i++) + if (trip == trips[i].np) { + __tbp->trip_id = i; + break; + } + + if (i == ntrips) { + ret = -ENODEV; + goto end; + } + + ret = of_parse_phandle_with_args(np, "cooling-device", "#cooling-cells", + 0, &cooling_spec); + if (ret < 0) { + pr_err("missing cooling_device property\n"); + goto end; + } + __tbp->cooling_device = cooling_spec.np; + if (cooling_spec.args_count >= 2) { /* at least min and max */ + __tbp->min = cooling_spec.args[0]; + __tbp->max = cooling_spec.args[1]; + } else { + pr_err("wrong reference to cooling device, missing limits\n"); + } + +end: + of_node_put(trip); + + return ret; +} + +/** + * It maps 'enum thermal_trip_type' found in include/linux/thermal.h + * into the device tree binding of 'trip', property type. + */ +static const char * const trip_types[] = { + [THERMAL_TRIP_ACTIVE] = "active", + [THERMAL_TRIP_PASSIVE] = "passive", + [THERMAL_TRIP_HOT] = "hot", + [THERMAL_TRIP_CRITICAL] = "critical", +}; + +/** + * thermal_of_get_trip_type - Get phy mode for given device_node + * @np: Pointer to the given device_node + * @type: Pointer to resulting trip type + * + * The function gets trip type string from property 'type', + * and store its index in trip_types table in @type, + * + * Return: 0 on success, or errno in error case. + */ +static int thermal_of_get_trip_type(struct device_node *np, + enum thermal_trip_type *type) +{ + const char *t; + int err, i; + + err = of_property_read_string(np, "type", &t); + if (err < 0) + return err; + + for (i = 0; i < ARRAY_SIZE(trip_types); i++) + if (!strcasecmp(t, trip_types[i])) { + *type = i; + return 0; + } + + return -ENODEV; +} + +/** + * thermal_of_populate_trip - parse and fill one trip point data + * @np: DT node containing a trip point node + * @trip: trip point data structure to be filled up + * + * This function parses a trip point type of node represented by + * @np parameter and fills the read data into @trip data structure. + * + * Return: 0 on success, proper error code otherwise + */ +static int thermal_of_populate_trip(struct device_node *np, + struct thermal_trip *trip) +{ + int prop; + int ret; + + ret = of_property_read_u32(np, "temperature", &prop); + if (ret < 0) { + pr_err("missing temperature property\n"); + return ret; + } + trip->temperature = prop; + + ret = of_property_read_u32(np, "hysteresis", &prop); + if (ret < 0) { + pr_err("missing hysteresis property\n"); + return ret; + } + trip->hysteresis = prop; + + ret = thermal_of_get_trip_type(np, &trip->type); + if (ret < 0) { + pr_err("wrong trip type property\n"); + return ret; + } + + /* Required for cooling map matching */ + trip->np = np; + of_node_get(np); + + return 0; +} + +/** + * thermal_of_build_thermal_zone - parse and fill one thermal zone data + * @np: DT node containing a thermal zone node + * + * This function parses a thermal zone type of node represented by + * @np parameter and fills the read data into a __thermal_zone data structure + * and return this pointer. + * + * TODO: Missing properties to parse: thermal-sensor-names + * + * Return: On success returns a valid struct __thermal_zone, + * otherwise, it returns a corresponding ERR_PTR(). Caller must + * check the return value with help of IS_ERR() helper. + */ +static struct __thermal_zone +__init *thermal_of_build_thermal_zone(struct device_node *np) +{ + struct device_node *child = NULL, *gchild; + struct __thermal_zone *tz; + int ret, i; + u32 prop, coef[2]; + + if (!np) { + pr_err("no thermal zone np\n"); + return ERR_PTR(-EINVAL); + } + + tz = kzalloc(sizeof(*tz), GFP_KERNEL); + if (!tz) + return ERR_PTR(-ENOMEM); + + ret = of_property_read_u32(np, "polling-delay-passive", &prop); + if (ret < 0) { + pr_err("missing polling-delay-passive property\n"); + goto free_tz; + } + tz->passive_delay = prop; + + ret = of_property_read_u32(np, "polling-delay", &prop); + if (ret < 0) { + pr_err("missing polling-delay property\n"); + goto free_tz; + } + tz->polling_delay = prop; + + /* + * REVIST: for now, the thermal framework supports only + * one sensor per thermal zone. Thus, we are considering + * only the first two values as slope and offset. + */ + ret = of_property_read_u32_array(np, "coefficients", coef, 2); + if (ret == 0) { + tz->slope = coef[0]; + tz->offset = coef[1]; + } else { + tz->slope = 1; + tz->offset = 0; + } + + /* trips */ + child = of_get_child_by_name(np, "trips"); + + /* No trips provided */ + if (!child) + goto finish; + + tz->ntrips = of_get_child_count(child); + if (tz->ntrips == 0) /* must have at least one child */ + goto finish; + + tz->trips = kcalloc(tz->ntrips, sizeof(*tz->trips), GFP_KERNEL); + if (!tz->trips) { + ret = -ENOMEM; + goto free_tz; + } + + i = 0; + for_each_child_of_node(child, gchild) { + ret = thermal_of_populate_trip(gchild, &tz->trips[i++]); + if (ret) + goto free_trips; + } + + of_node_put(child); + + /* cooling-maps */ + child = of_get_child_by_name(np, "cooling-maps"); + + /* cooling-maps not provided */ + if (!child) + goto finish; + + tz->num_tbps = of_get_child_count(child); + if (tz->num_tbps == 0) + goto finish; + + tz->tbps = kcalloc(tz->num_tbps, sizeof(*tz->tbps), GFP_KERNEL); + if (!tz->tbps) { + ret = -ENOMEM; + goto free_trips; + } + + i = 0; + for_each_child_of_node(child, gchild) { + ret = thermal_of_populate_bind_params(gchild, &tz->tbps[i++], + tz->trips, tz->ntrips); + if (ret) + goto free_tbps; + } + +finish: + of_node_put(child); + tz->mode = THERMAL_DEVICE_DISABLED; + + return tz; + +free_tbps: + for (i = i - 1; i >= 0; i--) + of_node_put(tz->tbps[i].cooling_device); + kfree(tz->tbps); +free_trips: + for (i = 0; i < tz->ntrips; i++) + of_node_put(tz->trips[i].np); + kfree(tz->trips); + of_node_put(gchild); +free_tz: + kfree(tz); + of_node_put(child); + + return ERR_PTR(ret); +} + +static inline void of_thermal_free_zone(struct __thermal_zone *tz) +{ + int i; + + for (i = 0; i < tz->num_tbps; i++) + of_node_put(tz->tbps[i].cooling_device); + kfree(tz->tbps); + for (i = 0; i < tz->ntrips; i++) + of_node_put(tz->trips[i].np); + kfree(tz->trips); + kfree(tz); +} + +/** + * of_parse_thermal_zones - parse device tree thermal data + * + * Initialization function that can be called by machine initialization + * code to parse thermal data and populate the thermal framework + * with hardware thermal zones info. This function only parses thermal zones. + * Cooling devices and sensor devices nodes are supposed to be parsed + * by their respective drivers. + * + * Return: 0 on success, proper error code otherwise + * + */ +int __init of_parse_thermal_zones(void) +{ + struct device_node *np, *child; + struct __thermal_zone *tz; + struct thermal_zone_device_ops *ops; + + np = of_find_node_by_name(NULL, "thermal-zones"); + if (!np) { + pr_debug("unable to find thermal zones\n"); + return 0; /* Run successfully on systems without thermal DT */ + } + + for_each_available_child_of_node(np, child) { + struct thermal_zone_device *zone; + struct thermal_zone_params *tzp; + int i, mask = 0; + u32 prop; + + tz = thermal_of_build_thermal_zone(child); + if (IS_ERR(tz)) { + pr_err("failed to build thermal zone %s: %ld\n", + child->name, + PTR_ERR(tz)); + continue; + } + + ops = kmemdup(&of_thermal_ops, sizeof(*ops), GFP_KERNEL); + if (!ops) + goto exit_free; + + tzp = kzalloc(sizeof(*tzp), GFP_KERNEL); + if (!tzp) { + kfree(ops); + goto exit_free; + } + + /* No hwmon because there might be hwmon drivers registering */ + tzp->no_hwmon = true; + + if (!of_property_read_u32(child, "sustainable-power", &prop)) + tzp->sustainable_power = prop; + + for (i = 0; i < tz->ntrips; i++) + mask |= 1 << i; + + /* these two are left for temperature drivers to use */ + tzp->slope = tz->slope; + tzp->offset = tz->offset; + + zone = thermal_zone_device_register(child->name, tz->ntrips, + mask, tz, + ops, tzp, + tz->passive_delay, + tz->polling_delay); + if (IS_ERR(zone)) { + pr_err("Failed to build %s zone %ld\n", child->name, + PTR_ERR(zone)); + kfree(tzp); + kfree(ops); + of_thermal_free_zone(tz); + /* attempting to build remaining zones still */ + } + } + of_node_put(np); + + return 0; + +exit_free: + of_node_put(child); + of_node_put(np); + of_thermal_free_zone(tz); + + /* no memory available, so free what we have built */ + of_thermal_destroy_zones(); + + return -ENOMEM; +} + +/** + * of_thermal_destroy_zones - remove all zones parsed and allocated resources + * + * Finds all zones parsed and added to the thermal framework and remove them + * from the system, together with their resources. + * + */ +void of_thermal_destroy_zones(void) +{ + struct device_node *np, *child; + + np = of_find_node_by_name(NULL, "thermal-zones"); + if (!np) { + pr_debug("unable to find thermal zones\n"); + return; + } + + for_each_available_child_of_node(np, child) { + struct thermal_zone_device *zone; + + zone = thermal_zone_get_zone_by_name(child->name); + if (IS_ERR(zone)) + continue; + + thermal_zone_device_unregister(zone); + kfree(zone->tzp); + kfree(zone->ops); + of_thermal_free_zone(zone->devdata); + } + of_node_put(np); +} diff --git a/drivers/thermal/power_allocator.c b/drivers/thermal/power_allocator.c new file mode 100644 index 000000000..3055f9a12 --- /dev/null +++ b/drivers/thermal/power_allocator.c @@ -0,0 +1,663 @@ +/* + * A power allocator to manage temperature + * + * Copyright (C) 2014 ARM Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed "as is" WITHOUT ANY WARRANTY of any + * kind, whether express or implied; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#define pr_fmt(fmt) "Power allocator: " fmt + +#include <linux/rculist.h> +#include <linux/slab.h> +#include <linux/thermal.h> + +#define CREATE_TRACE_POINTS +#include <trace/events/thermal_power_allocator.h> + +#include "thermal_core.h" + +#define INVALID_TRIP -1 + +#define FRAC_BITS 10 +#define int_to_frac(x) ((x) << FRAC_BITS) +#define frac_to_int(x) ((x) >> FRAC_BITS) + +/** + * mul_frac() - multiply two fixed-point numbers + * @x: first multiplicand + * @y: second multiplicand + * + * Return: the result of multiplying two fixed-point numbers. The + * result is also a fixed-point number. + */ +static inline s64 mul_frac(s64 x, s64 y) +{ + return (x * y) >> FRAC_BITS; +} + +/** + * div_frac() - divide two fixed-point numbers + * @x: the dividend + * @y: the divisor + * + * Return: the result of dividing two fixed-point numbers. The + * result is also a fixed-point number. + */ +static inline s64 div_frac(s64 x, s64 y) +{ + return div_s64(x << FRAC_BITS, y); +} + +/** + * struct power_allocator_params - parameters for the power allocator governor + * @allocated_tzp: whether we have allocated tzp for this thermal zone and + * it needs to be freed on unbind + * @err_integral: accumulated error in the PID controller. + * @prev_err: error in the previous iteration of the PID controller. + * Used to calculate the derivative term. + * @trip_switch_on: first passive trip point of the thermal zone. The + * governor switches on when this trip point is crossed. + * If the thermal zone only has one passive trip point, + * @trip_switch_on should be INVALID_TRIP. + * @trip_max_desired_temperature: last passive trip point of the thermal + * zone. The temperature we are + * controlling for. + */ +struct power_allocator_params { + bool allocated_tzp; + s64 err_integral; + s32 prev_err; + int trip_switch_on; + int trip_max_desired_temperature; +}; + +/** + * estimate_sustainable_power() - Estimate the sustainable power of a thermal zone + * @tz: thermal zone we are operating in + * + * For thermal zones that don't provide a sustainable_power in their + * thermal_zone_params, estimate one. Calculate it using the minimum + * power of all the cooling devices as that gives a valid value that + * can give some degree of functionality. For optimal performance of + * this governor, provide a sustainable_power in the thermal zone's + * thermal_zone_params. + */ +static u32 estimate_sustainable_power(struct thermal_zone_device *tz) +{ + u32 sustainable_power = 0; + struct thermal_instance *instance; + struct power_allocator_params *params = tz->governor_data; + + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + struct thermal_cooling_device *cdev = instance->cdev; + u32 min_power; + + if (instance->trip != params->trip_max_desired_temperature) + continue; + + if (power_actor_get_min_power(cdev, tz, &min_power)) + continue; + + sustainable_power += min_power; + } + + return sustainable_power; +} + +/** + * estimate_pid_constants() - Estimate the constants for the PID controller + * @tz: thermal zone for which to estimate the constants + * @sustainable_power: sustainable power for the thermal zone + * @trip_switch_on: trip point number for the switch on temperature + * @control_temp: target temperature for the power allocator governor + * @force: whether to force the update of the constants + * + * This function is used to update the estimation of the PID + * controller constants in struct thermal_zone_parameters. + * Sustainable power is provided in case it was estimated. The + * estimated sustainable_power should not be stored in the + * thermal_zone_parameters so it has to be passed explicitly to this + * function. + * + * If @force is not set, the values in the thermal zone's parameters + * are preserved if they are not zero. If @force is set, the values + * in thermal zone's parameters are overwritten. + */ +static void estimate_pid_constants(struct thermal_zone_device *tz, + u32 sustainable_power, int trip_switch_on, + int control_temp, bool force) +{ + int ret; + int switch_on_temp; + u32 temperature_threshold; + + ret = tz->ops->get_trip_temp(tz, trip_switch_on, &switch_on_temp); + if (ret) + switch_on_temp = 0; + + temperature_threshold = control_temp - switch_on_temp; + /* + * estimate_pid_constants() tries to find appropriate default + * values for thermal zones that don't provide them. If a + * system integrator has configured a thermal zone with two + * passive trip points at the same temperature, that person + * hasn't put any effort to set up the thermal zone properly + * so just give up. + */ + if (!temperature_threshold) + return; + + if (!tz->tzp->k_po || force) + tz->tzp->k_po = int_to_frac(sustainable_power) / + temperature_threshold; + + if (!tz->tzp->k_pu || force) + tz->tzp->k_pu = int_to_frac(2 * sustainable_power) / + temperature_threshold; + + if (!tz->tzp->k_i || force) + tz->tzp->k_i = int_to_frac(10) / 1000; + /* + * The default for k_d and integral_cutoff is 0, so we can + * leave them as they are. + */ +} + +/** + * pid_controller() - PID controller + * @tz: thermal zone we are operating in + * @control_temp: the target temperature in millicelsius + * @max_allocatable_power: maximum allocatable power for this thermal zone + * + * This PID controller increases the available power budget so that the + * temperature of the thermal zone gets as close as possible to + * @control_temp and limits the power if it exceeds it. k_po is the + * proportional term when we are overshooting, k_pu is the + * proportional term when we are undershooting. integral_cutoff is a + * threshold below which we stop accumulating the error. The + * accumulated error is only valid if the requested power will make + * the system warmer. If the system is mostly idle, there's no point + * in accumulating positive error. + * + * Return: The power budget for the next period. + */ +static u32 pid_controller(struct thermal_zone_device *tz, + int control_temp, + u32 max_allocatable_power) +{ + s64 p, i, d, power_range; + s32 err, max_power_frac; + u32 sustainable_power; + struct power_allocator_params *params = tz->governor_data; + + max_power_frac = int_to_frac(max_allocatable_power); + + if (tz->tzp->sustainable_power) { + sustainable_power = tz->tzp->sustainable_power; + } else { + sustainable_power = estimate_sustainable_power(tz); + estimate_pid_constants(tz, sustainable_power, + params->trip_switch_on, control_temp, + true); + } + + err = control_temp - tz->temperature; + err = int_to_frac(err); + + /* Calculate the proportional term */ + p = mul_frac(err < 0 ? tz->tzp->k_po : tz->tzp->k_pu, err); + + /* + * Calculate the integral term + * + * if the error is less than cut off allow integration (but + * the integral is limited to max power) + */ + i = mul_frac(tz->tzp->k_i, params->err_integral); + + if (err < int_to_frac(tz->tzp->integral_cutoff)) { + s64 i_next = i + mul_frac(tz->tzp->k_i, err); + + if (abs(i_next) < max_power_frac) { + i = i_next; + params->err_integral += err; + } + } + + /* + * Calculate the derivative term + * + * We do err - prev_err, so with a positive k_d, a decreasing + * error (i.e. driving closer to the line) results in less + * power being applied, slowing down the controller) + */ + d = mul_frac(tz->tzp->k_d, err - params->prev_err); + d = div_frac(d, tz->passive_delay); + params->prev_err = err; + + power_range = p + i + d; + + /* feed-forward the known sustainable dissipatable power */ + power_range = sustainable_power + frac_to_int(power_range); + + power_range = clamp(power_range, (s64)0, (s64)max_allocatable_power); + + trace_thermal_power_allocator_pid(tz, frac_to_int(err), + frac_to_int(params->err_integral), + frac_to_int(p), frac_to_int(i), + frac_to_int(d), power_range); + + return power_range; +} + +/** + * divvy_up_power() - divvy the allocated power between the actors + * @req_power: each actor's requested power + * @max_power: each actor's maximum available power + * @num_actors: size of the @req_power, @max_power and @granted_power's array + * @total_req_power: sum of @req_power + * @power_range: total allocated power + * @granted_power: output array: each actor's granted power + * @extra_actor_power: an appropriately sized array to be used in the + * function as temporary storage of the extra power given + * to the actors + * + * This function divides the total allocated power (@power_range) + * fairly between the actors. It first tries to give each actor a + * share of the @power_range according to how much power it requested + * compared to the rest of the actors. For example, if only one actor + * requests power, then it receives all the @power_range. If + * three actors each requests 1mW, each receives a third of the + * @power_range. + * + * If any actor received more than their maximum power, then that + * surplus is re-divvied among the actors based on how far they are + * from their respective maximums. + * + * Granted power for each actor is written to @granted_power, which + * should've been allocated by the calling function. + */ +static void divvy_up_power(u32 *req_power, u32 *max_power, int num_actors, + u32 total_req_power, u32 power_range, + u32 *granted_power, u32 *extra_actor_power) +{ + u32 extra_power, capped_extra_power; + int i; + + /* + * Prevent division by 0 if none of the actors request power. + */ + if (!total_req_power) + total_req_power = 1; + + capped_extra_power = 0; + extra_power = 0; + for (i = 0; i < num_actors; i++) { + u64 req_range = (u64)req_power[i] * power_range; + + granted_power[i] = DIV_ROUND_CLOSEST_ULL(req_range, + total_req_power); + + if (granted_power[i] > max_power[i]) { + extra_power += granted_power[i] - max_power[i]; + granted_power[i] = max_power[i]; + } + + extra_actor_power[i] = max_power[i] - granted_power[i]; + capped_extra_power += extra_actor_power[i]; + } + + if (!extra_power) + return; + + /* + * Re-divvy the reclaimed extra among actors based on + * how far they are from the max + */ + extra_power = min(extra_power, capped_extra_power); + if (capped_extra_power > 0) + for (i = 0; i < num_actors; i++) + granted_power[i] += (extra_actor_power[i] * + extra_power) / capped_extra_power; +} + +static int allocate_power(struct thermal_zone_device *tz, + int control_temp) +{ + struct thermal_instance *instance; + struct power_allocator_params *params = tz->governor_data; + u32 *req_power, *max_power, *granted_power, *extra_actor_power; + u32 *weighted_req_power; + u32 total_req_power, max_allocatable_power, total_weighted_req_power; + u32 total_granted_power, power_range; + int i, num_actors, total_weight, ret = 0; + int trip_max_desired_temperature = params->trip_max_desired_temperature; + + mutex_lock(&tz->lock); + + num_actors = 0; + total_weight = 0; + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + if ((instance->trip == trip_max_desired_temperature) && + cdev_is_power_actor(instance->cdev)) { + num_actors++; + total_weight += instance->weight; + } + } + + if (!num_actors) { + ret = -ENODEV; + goto unlock; + } + + /* + * We need to allocate five arrays of the same size: + * req_power, max_power, granted_power, extra_actor_power and + * weighted_req_power. They are going to be needed until this + * function returns. Allocate them all in one go to simplify + * the allocation and deallocation logic. + */ + BUILD_BUG_ON(sizeof(*req_power) != sizeof(*max_power)); + BUILD_BUG_ON(sizeof(*req_power) != sizeof(*granted_power)); + BUILD_BUG_ON(sizeof(*req_power) != sizeof(*extra_actor_power)); + BUILD_BUG_ON(sizeof(*req_power) != sizeof(*weighted_req_power)); + req_power = kcalloc(num_actors * 5, sizeof(*req_power), GFP_KERNEL); + if (!req_power) { + ret = -ENOMEM; + goto unlock; + } + + max_power = &req_power[num_actors]; + granted_power = &req_power[2 * num_actors]; + extra_actor_power = &req_power[3 * num_actors]; + weighted_req_power = &req_power[4 * num_actors]; + + i = 0; + total_weighted_req_power = 0; + total_req_power = 0; + max_allocatable_power = 0; + + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + int weight; + struct thermal_cooling_device *cdev = instance->cdev; + + if (instance->trip != trip_max_desired_temperature) + continue; + + if (!cdev_is_power_actor(cdev)) + continue; + + if (cdev->ops->get_requested_power(cdev, tz, &req_power[i])) + continue; + + if (!total_weight) + weight = 1 << FRAC_BITS; + else + weight = instance->weight; + + weighted_req_power[i] = frac_to_int(weight * req_power[i]); + + if (power_actor_get_max_power(cdev, tz, &max_power[i])) + continue; + + total_req_power += req_power[i]; + max_allocatable_power += max_power[i]; + total_weighted_req_power += weighted_req_power[i]; + + i++; + } + + power_range = pid_controller(tz, control_temp, max_allocatable_power); + + divvy_up_power(weighted_req_power, max_power, num_actors, + total_weighted_req_power, power_range, granted_power, + extra_actor_power); + + total_granted_power = 0; + i = 0; + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + if (instance->trip != trip_max_desired_temperature) + continue; + + if (!cdev_is_power_actor(instance->cdev)) + continue; + + power_actor_set_power(instance->cdev, instance, + granted_power[i]); + total_granted_power += granted_power[i]; + + i++; + } + + trace_thermal_power_allocator(tz, req_power, total_req_power, + granted_power, total_granted_power, + num_actors, power_range, + max_allocatable_power, tz->temperature, + control_temp - tz->temperature); + + kfree(req_power); +unlock: + mutex_unlock(&tz->lock); + + return ret; +} + +/** + * get_governor_trips() - get the number of the two trip points that are key for this governor + * @tz: thermal zone to operate on + * @params: pointer to private data for this governor + * + * The power allocator governor works optimally with two trips points: + * a "switch on" trip point and a "maximum desired temperature". These + * are defined as the first and last passive trip points. + * + * If there is only one trip point, then that's considered to be the + * "maximum desired temperature" trip point and the governor is always + * on. If there are no passive or active trip points, then the + * governor won't do anything. In fact, its throttle function + * won't be called at all. + */ +static void get_governor_trips(struct thermal_zone_device *tz, + struct power_allocator_params *params) +{ + int i, last_active, last_passive; + bool found_first_passive; + + found_first_passive = false; + last_active = INVALID_TRIP; + last_passive = INVALID_TRIP; + + for (i = 0; i < tz->trips; i++) { + enum thermal_trip_type type; + int ret; + + ret = tz->ops->get_trip_type(tz, i, &type); + if (ret) { + dev_warn(&tz->device, + "Failed to get trip point %d type: %d\n", i, + ret); + continue; + } + + if (type == THERMAL_TRIP_PASSIVE) { + if (!found_first_passive) { + params->trip_switch_on = i; + found_first_passive = true; + } else { + last_passive = i; + } + } else if (type == THERMAL_TRIP_ACTIVE) { + last_active = i; + } else { + break; + } + } + + if (last_passive != INVALID_TRIP) { + params->trip_max_desired_temperature = last_passive; + } else if (found_first_passive) { + params->trip_max_desired_temperature = params->trip_switch_on; + params->trip_switch_on = INVALID_TRIP; + } else { + params->trip_switch_on = INVALID_TRIP; + params->trip_max_desired_temperature = last_active; + } +} + +static void reset_pid_controller(struct power_allocator_params *params) +{ + params->err_integral = 0; + params->prev_err = 0; +} + +static void allow_maximum_power(struct thermal_zone_device *tz) +{ + struct thermal_instance *instance; + struct power_allocator_params *params = tz->governor_data; + + mutex_lock(&tz->lock); + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + if ((instance->trip != params->trip_max_desired_temperature) || + (!cdev_is_power_actor(instance->cdev))) + continue; + + instance->target = 0; + mutex_lock(&instance->cdev->lock); + instance->cdev->updated = false; + mutex_unlock(&instance->cdev->lock); + thermal_cdev_update(instance->cdev); + } + mutex_unlock(&tz->lock); +} + +/** + * power_allocator_bind() - bind the power_allocator governor to a thermal zone + * @tz: thermal zone to bind it to + * + * Initialize the PID controller parameters and bind it to the thermal + * zone. + * + * Return: 0 on success, or -ENOMEM if we ran out of memory. + */ +static int power_allocator_bind(struct thermal_zone_device *tz) +{ + int ret; + struct power_allocator_params *params; + int control_temp; + + params = kzalloc(sizeof(*params), GFP_KERNEL); + if (!params) + return -ENOMEM; + + if (!tz->tzp) { + tz->tzp = kzalloc(sizeof(*tz->tzp), GFP_KERNEL); + if (!tz->tzp) { + ret = -ENOMEM; + goto free_params; + } + + params->allocated_tzp = true; + } + + if (!tz->tzp->sustainable_power) + dev_warn(&tz->device, "power_allocator: sustainable_power will be estimated\n"); + + get_governor_trips(tz, params); + + if (tz->trips > 0) { + ret = tz->ops->get_trip_temp(tz, + params->trip_max_desired_temperature, + &control_temp); + if (!ret) + estimate_pid_constants(tz, tz->tzp->sustainable_power, + params->trip_switch_on, + control_temp, false); + } + + reset_pid_controller(params); + + tz->governor_data = params; + + return 0; + +free_params: + kfree(params); + + return ret; +} + +static void power_allocator_unbind(struct thermal_zone_device *tz) +{ + struct power_allocator_params *params = tz->governor_data; + + dev_dbg(&tz->device, "Unbinding from thermal zone %d\n", tz->id); + + if (params->allocated_tzp) { + kfree(tz->tzp); + tz->tzp = NULL; + } + + kfree(tz->governor_data); + tz->governor_data = NULL; +} + +static int power_allocator_throttle(struct thermal_zone_device *tz, int trip) +{ + int ret; + int switch_on_temp, control_temp; + struct power_allocator_params *params = tz->governor_data; + + /* + * We get called for every trip point but we only need to do + * our calculations once + */ + if (trip != params->trip_max_desired_temperature) + return 0; + + ret = tz->ops->get_trip_temp(tz, params->trip_switch_on, + &switch_on_temp); + if (!ret && (tz->temperature < switch_on_temp)) { + tz->passive = 0; + reset_pid_controller(params); + allow_maximum_power(tz); + return 0; + } + + tz->passive = 1; + + ret = tz->ops->get_trip_temp(tz, params->trip_max_desired_temperature, + &control_temp); + if (ret) { + dev_warn(&tz->device, + "Failed to get the maximum desired temperature: %d\n", + ret); + return ret; + } + + return allocate_power(tz, control_temp); +} + +static struct thermal_governor thermal_gov_power_allocator = { + .name = "power_allocator", + .bind_to_tz = power_allocator_bind, + .unbind_from_tz = power_allocator_unbind, + .throttle = power_allocator_throttle, +}; + +int thermal_gov_power_allocator_register(void) +{ + return thermal_register_governor(&thermal_gov_power_allocator); +} + +void thermal_gov_power_allocator_unregister(void) +{ + thermal_unregister_governor(&thermal_gov_power_allocator); +} diff --git a/drivers/thermal/qcom-spmi-temp-alarm.c b/drivers/thermal/qcom-spmi-temp-alarm.c new file mode 100644 index 000000000..ad4f3a8d6 --- /dev/null +++ b/drivers/thermal/qcom-spmi-temp-alarm.c @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2011-2015, 2017, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/bitops.h> +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/iio/consumer.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/thermal.h> + +#define QPNP_TM_REG_TYPE 0x04 +#define QPNP_TM_REG_SUBTYPE 0x05 +#define QPNP_TM_REG_STATUS 0x08 +#define QPNP_TM_REG_SHUTDOWN_CTRL1 0x40 +#define QPNP_TM_REG_ALARM_CTRL 0x46 + +#define QPNP_TM_TYPE 0x09 +#define QPNP_TM_SUBTYPE_GEN1 0x08 +#define QPNP_TM_SUBTYPE_GEN2 0x09 + +#define STATUS_GEN1_STAGE_MASK GENMASK(1, 0) +#define STATUS_GEN2_STATE_MASK GENMASK(6, 4) +#define STATUS_GEN2_STATE_SHIFT 4 + +#define SHUTDOWN_CTRL1_OVERRIDE_MASK GENMASK(7, 6) +#define SHUTDOWN_CTRL1_THRESHOLD_MASK GENMASK(1, 0) + +#define ALARM_CTRL_FORCE_ENABLE BIT(7) + +/* + * Trip point values based on threshold control + * 0 = {105 C, 125 C, 145 C} + * 1 = {110 C, 130 C, 150 C} + * 2 = {115 C, 135 C, 155 C} + * 3 = {120 C, 140 C, 160 C} +*/ +#define TEMP_STAGE_STEP 20000 /* Stage step: 20.000 C */ +#define TEMP_STAGE_HYSTERESIS 2000 + +#define TEMP_THRESH_MIN 105000 /* Threshold Min: 105 C */ +#define TEMP_THRESH_STEP 5000 /* Threshold step: 5 C */ + +#define THRESH_MIN 0 + +/* Temperature in Milli Celsius reported during stage 0 if no ADC is present */ +#define DEFAULT_TEMP 37000 + +struct qpnp_tm_chip { + struct regmap *map; + struct thermal_zone_device *tz_dev; + unsigned int subtype; + long temp; + unsigned int thresh; + unsigned int stage; + unsigned int prev_stage; + unsigned int base; + struct iio_channel *adc; +}; + +/* This array maps from GEN2 alarm state to GEN1 alarm stage */ +static const unsigned int alarm_state_map[8] = {0, 1, 1, 2, 2, 3, 3, 3}; + +static int qpnp_tm_read(struct qpnp_tm_chip *chip, u16 addr, u8 *data) +{ + unsigned int val; + int ret; + + ret = regmap_read(chip->map, chip->base + addr, &val); + if (ret < 0) + return ret; + + *data = val; + return 0; +} + +static int qpnp_tm_write(struct qpnp_tm_chip *chip, u16 addr, u8 data) +{ + return regmap_write(chip->map, chip->base + addr, data); +} + +/** + * qpnp_tm_get_temp_stage() - return over-temperature stage + * @chip: Pointer to the qpnp_tm chip + * + * Return: stage (GEN1) or state (GEN2) on success, or errno on failure. + */ +static int qpnp_tm_get_temp_stage(struct qpnp_tm_chip *chip) +{ + int ret; + u8 reg = 0; + + ret = qpnp_tm_read(chip, QPNP_TM_REG_STATUS, ®); + if (ret < 0) + return ret; + + if (chip->subtype == QPNP_TM_SUBTYPE_GEN1) + ret = reg & STATUS_GEN1_STAGE_MASK; + else + ret = (reg & STATUS_GEN2_STATE_MASK) >> STATUS_GEN2_STATE_SHIFT; + + return ret; +} + +/* + * This function updates the internal temp value based on the + * current thermal stage and threshold as well as the previous stage + */ +static int qpnp_tm_update_temp_no_adc(struct qpnp_tm_chip *chip) +{ + unsigned int stage, stage_new, stage_old; + int ret; + + ret = qpnp_tm_get_temp_stage(chip); + if (ret < 0) + return ret; + stage = ret; + + if (chip->subtype == QPNP_TM_SUBTYPE_GEN1) { + stage_new = stage; + stage_old = chip->stage; + } else { + stage_new = alarm_state_map[stage]; + stage_old = alarm_state_map[chip->stage]; + } + + if (stage_new > stage_old) { + /* increasing stage, use lower bound */ + chip->temp = (stage_new - 1) * TEMP_STAGE_STEP + + chip->thresh * TEMP_THRESH_STEP + + TEMP_STAGE_HYSTERESIS + TEMP_THRESH_MIN; + } else if (stage_new < stage_old) { + /* decreasing stage, use upper bound */ + chip->temp = stage_new * TEMP_STAGE_STEP + + chip->thresh * TEMP_THRESH_STEP - + TEMP_STAGE_HYSTERESIS + TEMP_THRESH_MIN; + } + + chip->stage = stage; + + return 0; +} + +static int qpnp_tm_get_temp(void *data, int *temp) +{ + struct qpnp_tm_chip *chip = data; + int ret, mili_celsius; + + if (!temp) + return -EINVAL; + + if (!chip->adc) { + ret = qpnp_tm_update_temp_no_adc(chip); + if (ret < 0) + return ret; + } else { + ret = iio_read_channel_processed(chip->adc, &mili_celsius); + if (ret < 0) + return ret; + + chip->temp = mili_celsius; + } + + *temp = chip->temp < 0 ? 0 : chip->temp; + + return 0; +} + +static const struct thermal_zone_of_device_ops qpnp_tm_sensor_ops = { + .get_temp = qpnp_tm_get_temp, +}; + +static irqreturn_t qpnp_tm_isr(int irq, void *data) +{ + struct qpnp_tm_chip *chip = data; + + thermal_zone_device_update(chip->tz_dev, THERMAL_EVENT_UNSPECIFIED); + + return IRQ_HANDLED; +} + +/* + * This function initializes the internal temp value based on only the + * current thermal stage and threshold. Setup threshold control and + * disable shutdown override. + */ +static int qpnp_tm_init(struct qpnp_tm_chip *chip) +{ + unsigned int stage; + int ret; + u8 reg = 0; + + ret = qpnp_tm_read(chip, QPNP_TM_REG_SHUTDOWN_CTRL1, ®); + if (ret < 0) + return ret; + + chip->thresh = reg & SHUTDOWN_CTRL1_THRESHOLD_MASK; + chip->temp = DEFAULT_TEMP; + + ret = qpnp_tm_get_temp_stage(chip); + if (ret < 0) + return ret; + chip->stage = ret; + + stage = chip->subtype == QPNP_TM_SUBTYPE_GEN1 + ? chip->stage : alarm_state_map[chip->stage]; + + if (stage) + chip->temp = chip->thresh * TEMP_THRESH_STEP + + (stage - 1) * TEMP_STAGE_STEP + + TEMP_THRESH_MIN; + + /* + * Set threshold and disable software override of stage 2 and 3 + * shutdowns. + */ + chip->thresh = THRESH_MIN; + reg &= ~(SHUTDOWN_CTRL1_OVERRIDE_MASK | SHUTDOWN_CTRL1_THRESHOLD_MASK); + reg |= chip->thresh & SHUTDOWN_CTRL1_THRESHOLD_MASK; + ret = qpnp_tm_write(chip, QPNP_TM_REG_SHUTDOWN_CTRL1, reg); + if (ret < 0) + return ret; + + /* Enable the thermal alarm PMIC module in always-on mode. */ + reg = ALARM_CTRL_FORCE_ENABLE; + ret = qpnp_tm_write(chip, QPNP_TM_REG_ALARM_CTRL, reg); + + return ret; +} + +static int qpnp_tm_probe(struct platform_device *pdev) +{ + struct qpnp_tm_chip *chip; + struct device_node *node; + u8 type, subtype; + u32 res; + int ret, irq; + + node = pdev->dev.of_node; + + chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + dev_set_drvdata(&pdev->dev, chip); + + chip->map = dev_get_regmap(pdev->dev.parent, NULL); + if (!chip->map) + return -ENXIO; + + ret = of_property_read_u32(node, "reg", &res); + if (ret < 0) + return ret; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + /* ADC based measurements are optional */ + chip->adc = devm_iio_channel_get(&pdev->dev, "thermal"); + if (IS_ERR(chip->adc)) { + ret = PTR_ERR(chip->adc); + chip->adc = NULL; + if (ret == -EPROBE_DEFER) + return ret; + } + + chip->base = res; + + ret = qpnp_tm_read(chip, QPNP_TM_REG_TYPE, &type); + if (ret < 0) { + dev_err(&pdev->dev, "could not read type\n"); + return ret; + } + + ret = qpnp_tm_read(chip, QPNP_TM_REG_SUBTYPE, &subtype); + if (ret < 0) { + dev_err(&pdev->dev, "could not read subtype\n"); + return ret; + } + + if (type != QPNP_TM_TYPE || (subtype != QPNP_TM_SUBTYPE_GEN1 + && subtype != QPNP_TM_SUBTYPE_GEN2)) { + dev_err(&pdev->dev, "invalid type 0x%02x or subtype 0x%02x\n", + type, subtype); + return -ENODEV; + } + + chip->subtype = subtype; + + ret = qpnp_tm_init(chip); + if (ret < 0) { + dev_err(&pdev->dev, "init failed\n"); + return ret; + } + + ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, qpnp_tm_isr, + IRQF_ONESHOT, node->name, chip); + if (ret < 0) + return ret; + + chip->tz_dev = devm_thermal_zone_of_sensor_register(&pdev->dev, 0, chip, + &qpnp_tm_sensor_ops); + if (IS_ERR(chip->tz_dev)) { + dev_err(&pdev->dev, "failed to register sensor\n"); + return PTR_ERR(chip->tz_dev); + } + + return 0; +} + +static const struct of_device_id qpnp_tm_match_table[] = { + { .compatible = "qcom,spmi-temp-alarm" }, + { } +}; +MODULE_DEVICE_TABLE(of, qpnp_tm_match_table); + +static struct platform_driver qpnp_tm_driver = { + .driver = { + .name = "spmi-temp-alarm", + .of_match_table = qpnp_tm_match_table, + }, + .probe = qpnp_tm_probe, +}; +module_platform_driver(qpnp_tm_driver); + +MODULE_ALIAS("platform:spmi-temp-alarm"); +MODULE_DESCRIPTION("QPNP PMIC Temperature Alarm driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/qcom/Kconfig b/drivers/thermal/qcom/Kconfig new file mode 100644 index 000000000..be32e5abc --- /dev/null +++ b/drivers/thermal/qcom/Kconfig @@ -0,0 +1,11 @@ +config QCOM_TSENS + tristate "Qualcomm TSENS Temperature Alarm" + depends on THERMAL + depends on QCOM_QFPROM + depends on ARCH_QCOM || COMPILE_TEST + help + This enables the thermal sysfs driver for the TSENS device. It shows + up in Sysfs as a thermal zone with multiple trip points. Disabling the + thermal zone device via the mode file results in disabling the sensor. + Also able to set threshold temperature for both hot and cold and update + when a threshold is reached. diff --git a/drivers/thermal/qcom/Makefile b/drivers/thermal/qcom/Makefile new file mode 100644 index 000000000..a821929ed --- /dev/null +++ b/drivers/thermal/qcom/Makefile @@ -0,0 +1,2 @@ +obj-$(CONFIG_QCOM_TSENS) += qcom_tsens.o +qcom_tsens-y += tsens.o tsens-common.o tsens-8916.o tsens-8974.o tsens-8960.o tsens-v2.o diff --git a/drivers/thermal/qcom/tsens-8916.c b/drivers/thermal/qcom/tsens-8916.c new file mode 100644 index 000000000..fdf561b8b --- /dev/null +++ b/drivers/thermal/qcom/tsens-8916.c @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/platform_device.h> +#include "tsens.h" + +/* eeprom layout data for 8916 */ +#define BASE0_MASK 0x0000007f +#define BASE1_MASK 0xfe000000 +#define BASE0_SHIFT 0 +#define BASE1_SHIFT 25 + +#define S0_P1_MASK 0x00000f80 +#define S1_P1_MASK 0x003e0000 +#define S2_P1_MASK 0xf8000000 +#define S3_P1_MASK 0x000003e0 +#define S4_P1_MASK 0x000f8000 + +#define S0_P2_MASK 0x0001f000 +#define S1_P2_MASK 0x07c00000 +#define S2_P2_MASK 0x0000001f +#define S3_P2_MASK 0x00007c00 +#define S4_P2_MASK 0x01f00000 + +#define S0_P1_SHIFT 7 +#define S1_P1_SHIFT 17 +#define S2_P1_SHIFT 27 +#define S3_P1_SHIFT 5 +#define S4_P1_SHIFT 15 + +#define S0_P2_SHIFT 12 +#define S1_P2_SHIFT 22 +#define S2_P2_SHIFT 0 +#define S3_P2_SHIFT 10 +#define S4_P2_SHIFT 20 + +#define CAL_SEL_MASK 0xe0000000 +#define CAL_SEL_SHIFT 29 + +static int calibrate_8916(struct tsens_device *tmdev) +{ + int base0 = 0, base1 = 0, i; + u32 p1[5], p2[5]; + int mode = 0; + u32 *qfprom_cdata, *qfprom_csel; + + qfprom_cdata = (u32 *)qfprom_read(tmdev->dev, "calib"); + if (IS_ERR(qfprom_cdata)) + return PTR_ERR(qfprom_cdata); + + qfprom_csel = (u32 *)qfprom_read(tmdev->dev, "calib_sel"); + if (IS_ERR(qfprom_csel)) + return PTR_ERR(qfprom_csel); + + mode = (qfprom_csel[0] & CAL_SEL_MASK) >> CAL_SEL_SHIFT; + dev_dbg(tmdev->dev, "calibration mode is %d\n", mode); + + switch (mode) { + case TWO_PT_CALIB: + base1 = (qfprom_cdata[1] & BASE1_MASK) >> BASE1_SHIFT; + p2[0] = (qfprom_cdata[0] & S0_P2_MASK) >> S0_P2_SHIFT; + p2[1] = (qfprom_cdata[0] & S1_P2_MASK) >> S1_P2_SHIFT; + p2[2] = (qfprom_cdata[1] & S2_P2_MASK) >> S2_P2_SHIFT; + p2[3] = (qfprom_cdata[1] & S3_P2_MASK) >> S3_P2_SHIFT; + p2[4] = (qfprom_cdata[1] & S4_P2_MASK) >> S4_P2_SHIFT; + for (i = 0; i < tmdev->num_sensors; i++) + p2[i] = ((base1 + p2[i]) << 3); + /* Fall through */ + case ONE_PT_CALIB2: + base0 = (qfprom_cdata[0] & BASE0_MASK); + p1[0] = (qfprom_cdata[0] & S0_P1_MASK) >> S0_P1_SHIFT; + p1[1] = (qfprom_cdata[0] & S1_P1_MASK) >> S1_P1_SHIFT; + p1[2] = (qfprom_cdata[0] & S2_P1_MASK) >> S2_P1_SHIFT; + p1[3] = (qfprom_cdata[1] & S3_P1_MASK) >> S3_P1_SHIFT; + p1[4] = (qfprom_cdata[1] & S4_P1_MASK) >> S4_P1_SHIFT; + for (i = 0; i < tmdev->num_sensors; i++) + p1[i] = (((base0) + p1[i]) << 3); + break; + default: + for (i = 0; i < tmdev->num_sensors; i++) { + p1[i] = 500; + p2[i] = 780; + } + break; + } + + compute_intercept_slope(tmdev, p1, p2, mode); + + return 0; +} + +static const struct tsens_ops ops_8916 = { + .init = init_common, + .calibrate = calibrate_8916, + .get_temp = get_temp_common, +}; + +const struct tsens_data data_8916 = { + .num_sensors = 5, + .ops = &ops_8916, + .hw_ids = (unsigned int []){0, 1, 2, 4, 5 }, +}; diff --git a/drivers/thermal/qcom/tsens-8960.c b/drivers/thermal/qcom/tsens-8960.c new file mode 100644 index 000000000..0451277d3 --- /dev/null +++ b/drivers/thermal/qcom/tsens-8960.c @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/platform_device.h> +#include <linux/delay.h> +#include <linux/bitops.h> +#include <linux/regmap.h> +#include <linux/thermal.h> +#include "tsens.h" + +#define CAL_MDEGC 30000 + +#define CONFIG_ADDR 0x3640 +#define CONFIG_ADDR_8660 0x3620 +/* CONFIG_ADDR bitmasks */ +#define CONFIG 0x9b +#define CONFIG_MASK 0xf +#define CONFIG_8660 1 +#define CONFIG_SHIFT_8660 28 +#define CONFIG_MASK_8660 (3 << CONFIG_SHIFT_8660) + +#define STATUS_CNTL_ADDR_8064 0x3660 +#define CNTL_ADDR 0x3620 +/* CNTL_ADDR bitmasks */ +#define EN BIT(0) +#define SW_RST BIT(1) +#define SENSOR0_EN BIT(3) +#define SLP_CLK_ENA BIT(26) +#define SLP_CLK_ENA_8660 BIT(24) +#define MEASURE_PERIOD 1 +#define SENSOR0_SHIFT 3 + +/* INT_STATUS_ADDR bitmasks */ +#define MIN_STATUS_MASK BIT(0) +#define LOWER_STATUS_CLR BIT(1) +#define UPPER_STATUS_CLR BIT(2) +#define MAX_STATUS_MASK BIT(3) + +#define THRESHOLD_ADDR 0x3624 +/* THRESHOLD_ADDR bitmasks */ +#define THRESHOLD_MAX_LIMIT_SHIFT 24 +#define THRESHOLD_MIN_LIMIT_SHIFT 16 +#define THRESHOLD_UPPER_LIMIT_SHIFT 8 +#define THRESHOLD_LOWER_LIMIT_SHIFT 0 + +/* Initial temperature threshold values */ +#define LOWER_LIMIT_TH 0x50 +#define UPPER_LIMIT_TH 0xdf +#define MIN_LIMIT_TH 0x0 +#define MAX_LIMIT_TH 0xff + +#define S0_STATUS_ADDR 0x3628 +#define INT_STATUS_ADDR 0x363c +#define TRDY_MASK BIT(7) +#define TIMEOUT_US 100 + +static int suspend_8960(struct tsens_device *tmdev) +{ + int ret; + unsigned int mask; + struct regmap *map = tmdev->map; + + ret = regmap_read(map, THRESHOLD_ADDR, &tmdev->ctx.threshold); + if (ret) + return ret; + + ret = regmap_read(map, CNTL_ADDR, &tmdev->ctx.control); + if (ret) + return ret; + + if (tmdev->num_sensors > 1) + mask = SLP_CLK_ENA | EN; + else + mask = SLP_CLK_ENA_8660 | EN; + + ret = regmap_update_bits(map, CNTL_ADDR, mask, 0); + if (ret) + return ret; + + return 0; +} + +static int resume_8960(struct tsens_device *tmdev) +{ + int ret; + struct regmap *map = tmdev->map; + + ret = regmap_update_bits(map, CNTL_ADDR, SW_RST, SW_RST); + if (ret) + return ret; + + /* + * Separate CONFIG restore is not needed only for 8660 as + * config is part of CTRL Addr and its restored as such + */ + if (tmdev->num_sensors > 1) { + ret = regmap_update_bits(map, CONFIG_ADDR, CONFIG_MASK, CONFIG); + if (ret) + return ret; + } + + ret = regmap_write(map, THRESHOLD_ADDR, tmdev->ctx.threshold); + if (ret) + return ret; + + ret = regmap_write(map, CNTL_ADDR, tmdev->ctx.control); + if (ret) + return ret; + + return 0; +} + +static int enable_8960(struct tsens_device *tmdev, int id) +{ + int ret; + u32 reg, mask; + + ret = regmap_read(tmdev->map, CNTL_ADDR, ®); + if (ret) + return ret; + + mask = BIT(id + SENSOR0_SHIFT); + ret = regmap_write(tmdev->map, CNTL_ADDR, reg | SW_RST); + if (ret) + return ret; + + if (tmdev->num_sensors > 1) + reg |= mask | SLP_CLK_ENA | EN; + else + reg |= mask | SLP_CLK_ENA_8660 | EN; + + ret = regmap_write(tmdev->map, CNTL_ADDR, reg); + if (ret) + return ret; + + return 0; +} + +static void disable_8960(struct tsens_device *tmdev) +{ + int ret; + u32 reg_cntl; + u32 mask; + + mask = GENMASK(tmdev->num_sensors - 1, 0); + mask <<= SENSOR0_SHIFT; + mask |= EN; + + ret = regmap_read(tmdev->map, CNTL_ADDR, ®_cntl); + if (ret) + return; + + reg_cntl &= ~mask; + + if (tmdev->num_sensors > 1) + reg_cntl &= ~SLP_CLK_ENA; + else + reg_cntl &= ~SLP_CLK_ENA_8660; + + regmap_write(tmdev->map, CNTL_ADDR, reg_cntl); +} + +static int init_8960(struct tsens_device *tmdev) +{ + int ret, i; + u32 reg_cntl; + + tmdev->map = dev_get_regmap(tmdev->dev, NULL); + if (!tmdev->map) + return -ENODEV; + + /* + * The status registers for each sensor are discontiguous + * because some SoCs have 5 sensors while others have more + * but the control registers stay in the same place, i.e + * directly after the first 5 status registers. + */ + for (i = 0; i < tmdev->num_sensors; i++) { + if (i >= 5) + tmdev->sensor[i].status = S0_STATUS_ADDR + 40; + tmdev->sensor[i].status += i * 4; + } + + reg_cntl = SW_RST; + ret = regmap_update_bits(tmdev->map, CNTL_ADDR, SW_RST, reg_cntl); + if (ret) + return ret; + + if (tmdev->num_sensors > 1) { + reg_cntl |= SLP_CLK_ENA | (MEASURE_PERIOD << 18); + reg_cntl &= ~SW_RST; + ret = regmap_update_bits(tmdev->map, CONFIG_ADDR, + CONFIG_MASK, CONFIG); + } else { + reg_cntl |= SLP_CLK_ENA_8660 | (MEASURE_PERIOD << 16); + reg_cntl &= ~CONFIG_MASK_8660; + reg_cntl |= CONFIG_8660 << CONFIG_SHIFT_8660; + } + + reg_cntl |= GENMASK(tmdev->num_sensors - 1, 0) << SENSOR0_SHIFT; + ret = regmap_write(tmdev->map, CNTL_ADDR, reg_cntl); + if (ret) + return ret; + + reg_cntl |= EN; + ret = regmap_write(tmdev->map, CNTL_ADDR, reg_cntl); + if (ret) + return ret; + + return 0; +} + +static int calibrate_8960(struct tsens_device *tmdev) +{ + int i; + char *data; + + ssize_t num_read = tmdev->num_sensors; + struct tsens_sensor *s = tmdev->sensor; + + data = qfprom_read(tmdev->dev, "calib"); + if (IS_ERR(data)) + data = qfprom_read(tmdev->dev, "calib_backup"); + if (IS_ERR(data)) + return PTR_ERR(data); + + for (i = 0; i < num_read; i++, s++) + s->offset = data[i]; + + return 0; +} + +/* Temperature on y axis and ADC-code on x-axis */ +static inline int code_to_mdegC(u32 adc_code, const struct tsens_sensor *s) +{ + int slope, offset; + + slope = thermal_zone_get_slope(s->tzd); + offset = CAL_MDEGC - slope * s->offset; + + return adc_code * slope + offset; +} + +static int get_temp_8960(struct tsens_device *tmdev, int id, int *temp) +{ + int ret; + u32 code, trdy; + const struct tsens_sensor *s = &tmdev->sensor[id]; + unsigned long timeout; + + timeout = jiffies + usecs_to_jiffies(TIMEOUT_US); + do { + ret = regmap_read(tmdev->map, INT_STATUS_ADDR, &trdy); + if (ret) + return ret; + if (!(trdy & TRDY_MASK)) + continue; + ret = regmap_read(tmdev->map, s->status, &code); + if (ret) + return ret; + *temp = code_to_mdegC(code, s); + return 0; + } while (time_before(jiffies, timeout)); + + return -ETIMEDOUT; +} + +static const struct tsens_ops ops_8960 = { + .init = init_8960, + .calibrate = calibrate_8960, + .get_temp = get_temp_8960, + .enable = enable_8960, + .disable = disable_8960, + .suspend = suspend_8960, + .resume = resume_8960, +}; + +const struct tsens_data data_8960 = { + .num_sensors = 11, + .ops = &ops_8960, +}; diff --git a/drivers/thermal/qcom/tsens-8974.c b/drivers/thermal/qcom/tsens-8974.c new file mode 100644 index 000000000..9baf77e8c --- /dev/null +++ b/drivers/thermal/qcom/tsens-8974.c @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/platform_device.h> +#include "tsens.h" + +/* eeprom layout data for 8974 */ +#define BASE1_MASK 0xff +#define S0_P1_MASK 0x3f00 +#define S1_P1_MASK 0xfc000 +#define S2_P1_MASK 0x3f00000 +#define S3_P1_MASK 0xfc000000 +#define S4_P1_MASK 0x3f +#define S5_P1_MASK 0xfc0 +#define S6_P1_MASK 0x3f000 +#define S7_P1_MASK 0xfc0000 +#define S8_P1_MASK 0x3f000000 +#define S8_P1_MASK_BKP 0x3f +#define S9_P1_MASK 0x3f +#define S9_P1_MASK_BKP 0xfc0 +#define S10_P1_MASK 0xfc0 +#define S10_P1_MASK_BKP 0x3f000 +#define CAL_SEL_0_1 0xc0000000 +#define CAL_SEL_2 0x40000000 +#define CAL_SEL_SHIFT 30 +#define CAL_SEL_SHIFT_2 28 + +#define S0_P1_SHIFT 8 +#define S1_P1_SHIFT 14 +#define S2_P1_SHIFT 20 +#define S3_P1_SHIFT 26 +#define S5_P1_SHIFT 6 +#define S6_P1_SHIFT 12 +#define S7_P1_SHIFT 18 +#define S8_P1_SHIFT 24 +#define S9_P1_BKP_SHIFT 6 +#define S10_P1_SHIFT 6 +#define S10_P1_BKP_SHIFT 12 + +#define BASE2_SHIFT 12 +#define BASE2_BKP_SHIFT 18 +#define S0_P2_SHIFT 20 +#define S0_P2_BKP_SHIFT 26 +#define S1_P2_SHIFT 26 +#define S2_P2_BKP_SHIFT 6 +#define S3_P2_SHIFT 6 +#define S3_P2_BKP_SHIFT 12 +#define S4_P2_SHIFT 12 +#define S4_P2_BKP_SHIFT 18 +#define S5_P2_SHIFT 18 +#define S5_P2_BKP_SHIFT 24 +#define S6_P2_SHIFT 24 +#define S7_P2_BKP_SHIFT 6 +#define S8_P2_SHIFT 6 +#define S8_P2_BKP_SHIFT 12 +#define S9_P2_SHIFT 12 +#define S9_P2_BKP_SHIFT 18 +#define S10_P2_SHIFT 18 +#define S10_P2_BKP_SHIFT 24 + +#define BASE2_MASK 0xff000 +#define BASE2_BKP_MASK 0xfc0000 +#define S0_P2_MASK 0x3f00000 +#define S0_P2_BKP_MASK 0xfc000000 +#define S1_P2_MASK 0xfc000000 +#define S1_P2_BKP_MASK 0x3f +#define S2_P2_MASK 0x3f +#define S2_P2_BKP_MASK 0xfc0 +#define S3_P2_MASK 0xfc0 +#define S3_P2_BKP_MASK 0x3f000 +#define S4_P2_MASK 0x3f000 +#define S4_P2_BKP_MASK 0xfc0000 +#define S5_P2_MASK 0xfc0000 +#define S5_P2_BKP_MASK 0x3f000000 +#define S6_P2_MASK 0x3f000000 +#define S6_P2_BKP_MASK 0x3f +#define S7_P2_MASK 0x3f +#define S7_P2_BKP_MASK 0xfc0 +#define S8_P2_MASK 0xfc0 +#define S8_P2_BKP_MASK 0x3f000 +#define S9_P2_MASK 0x3f000 +#define S9_P2_BKP_MASK 0xfc0000 +#define S10_P2_MASK 0xfc0000 +#define S10_P2_BKP_MASK 0x3f000000 + +#define BKP_SEL 0x3 +#define BKP_REDUN_SEL 0xe0000000 +#define BKP_REDUN_SHIFT 29 + +#define BIT_APPEND 0x3 + +static int calibrate_8974(struct tsens_device *tmdev) +{ + int base1 = 0, base2 = 0, i; + u32 p1[11], p2[11]; + int mode = 0; + u32 *calib, *bkp; + u32 calib_redun_sel; + + calib = (u32 *)qfprom_read(tmdev->dev, "calib"); + if (IS_ERR(calib)) + return PTR_ERR(calib); + + bkp = (u32 *)qfprom_read(tmdev->dev, "calib_backup"); + if (IS_ERR(bkp)) + return PTR_ERR(bkp); + + calib_redun_sel = bkp[1] & BKP_REDUN_SEL; + calib_redun_sel >>= BKP_REDUN_SHIFT; + + if (calib_redun_sel == BKP_SEL) { + mode = (calib[4] & CAL_SEL_0_1) >> CAL_SEL_SHIFT; + mode |= (calib[5] & CAL_SEL_2) >> CAL_SEL_SHIFT_2; + + switch (mode) { + case TWO_PT_CALIB: + base2 = (bkp[2] & BASE2_BKP_MASK) >> BASE2_BKP_SHIFT; + p2[0] = (bkp[2] & S0_P2_BKP_MASK) >> S0_P2_BKP_SHIFT; + p2[1] = (bkp[3] & S1_P2_BKP_MASK); + p2[2] = (bkp[3] & S2_P2_BKP_MASK) >> S2_P2_BKP_SHIFT; + p2[3] = (bkp[3] & S3_P2_BKP_MASK) >> S3_P2_BKP_SHIFT; + p2[4] = (bkp[3] & S4_P2_BKP_MASK) >> S4_P2_BKP_SHIFT; + p2[5] = (calib[4] & S5_P2_BKP_MASK) >> S5_P2_BKP_SHIFT; + p2[6] = (calib[5] & S6_P2_BKP_MASK); + p2[7] = (calib[5] & S7_P2_BKP_MASK) >> S7_P2_BKP_SHIFT; + p2[8] = (calib[5] & S8_P2_BKP_MASK) >> S8_P2_BKP_SHIFT; + p2[9] = (calib[5] & S9_P2_BKP_MASK) >> S9_P2_BKP_SHIFT; + p2[10] = (calib[5] & S10_P2_BKP_MASK) >> S10_P2_BKP_SHIFT; + /* Fall through */ + case ONE_PT_CALIB: + case ONE_PT_CALIB2: + base1 = bkp[0] & BASE1_MASK; + p1[0] = (bkp[0] & S0_P1_MASK) >> S0_P1_SHIFT; + p1[1] = (bkp[0] & S1_P1_MASK) >> S1_P1_SHIFT; + p1[2] = (bkp[0] & S2_P1_MASK) >> S2_P1_SHIFT; + p1[3] = (bkp[0] & S3_P1_MASK) >> S3_P1_SHIFT; + p1[4] = (bkp[1] & S4_P1_MASK); + p1[5] = (bkp[1] & S5_P1_MASK) >> S5_P1_SHIFT; + p1[6] = (bkp[1] & S6_P1_MASK) >> S6_P1_SHIFT; + p1[7] = (bkp[1] & S7_P1_MASK) >> S7_P1_SHIFT; + p1[8] = (bkp[2] & S8_P1_MASK_BKP) >> S8_P1_SHIFT; + p1[9] = (bkp[2] & S9_P1_MASK_BKP) >> S9_P1_BKP_SHIFT; + p1[10] = (bkp[2] & S10_P1_MASK_BKP) >> S10_P1_BKP_SHIFT; + break; + } + } else { + mode = (calib[1] & CAL_SEL_0_1) >> CAL_SEL_SHIFT; + mode |= (calib[3] & CAL_SEL_2) >> CAL_SEL_SHIFT_2; + + switch (mode) { + case TWO_PT_CALIB: + base2 = (calib[2] & BASE2_MASK) >> BASE2_SHIFT; + p2[0] = (calib[2] & S0_P2_MASK) >> S0_P2_SHIFT; + p2[1] = (calib[2] & S1_P2_MASK) >> S1_P2_SHIFT; + p2[2] = (calib[3] & S2_P2_MASK); + p2[3] = (calib[3] & S3_P2_MASK) >> S3_P2_SHIFT; + p2[4] = (calib[3] & S4_P2_MASK) >> S4_P2_SHIFT; + p2[5] = (calib[3] & S5_P2_MASK) >> S5_P2_SHIFT; + p2[6] = (calib[3] & S6_P2_MASK) >> S6_P2_SHIFT; + p2[7] = (calib[4] & S7_P2_MASK); + p2[8] = (calib[4] & S8_P2_MASK) >> S8_P2_SHIFT; + p2[9] = (calib[4] & S9_P2_MASK) >> S9_P2_SHIFT; + p2[10] = (calib[4] & S10_P2_MASK) >> S10_P2_SHIFT; + /* Fall through */ + case ONE_PT_CALIB: + case ONE_PT_CALIB2: + base1 = calib[0] & BASE1_MASK; + p1[0] = (calib[0] & S0_P1_MASK) >> S0_P1_SHIFT; + p1[1] = (calib[0] & S1_P1_MASK) >> S1_P1_SHIFT; + p1[2] = (calib[0] & S2_P1_MASK) >> S2_P1_SHIFT; + p1[3] = (calib[0] & S3_P1_MASK) >> S3_P1_SHIFT; + p1[4] = (calib[1] & S4_P1_MASK); + p1[5] = (calib[1] & S5_P1_MASK) >> S5_P1_SHIFT; + p1[6] = (calib[1] & S6_P1_MASK) >> S6_P1_SHIFT; + p1[7] = (calib[1] & S7_P1_MASK) >> S7_P1_SHIFT; + p1[8] = (calib[1] & S8_P1_MASK) >> S8_P1_SHIFT; + p1[9] = (calib[2] & S9_P1_MASK); + p1[10] = (calib[2] & S10_P1_MASK) >> S10_P1_SHIFT; + break; + } + } + + switch (mode) { + case ONE_PT_CALIB: + for (i = 0; i < tmdev->num_sensors; i++) + p1[i] += (base1 << 2) | BIT_APPEND; + break; + case TWO_PT_CALIB: + for (i = 0; i < tmdev->num_sensors; i++) { + p2[i] += base2; + p2[i] <<= 2; + p2[i] |= BIT_APPEND; + } + /* Fall through */ + case ONE_PT_CALIB2: + for (i = 0; i < tmdev->num_sensors; i++) { + p1[i] += base1; + p1[i] <<= 2; + p1[i] |= BIT_APPEND; + } + break; + default: + for (i = 0; i < tmdev->num_sensors; i++) + p2[i] = 780; + p1[0] = 502; + p1[1] = 509; + p1[2] = 503; + p1[3] = 509; + p1[4] = 505; + p1[5] = 509; + p1[6] = 507; + p1[7] = 510; + p1[8] = 508; + p1[9] = 509; + p1[10] = 508; + break; + } + + compute_intercept_slope(tmdev, p1, p2, mode); + + return 0; +} + +static const struct tsens_ops ops_8974 = { + .init = init_common, + .calibrate = calibrate_8974, + .get_temp = get_temp_common, +}; + +const struct tsens_data data_8974 = { + .num_sensors = 11, + .ops = &ops_8974, +}; diff --git a/drivers/thermal/qcom/tsens-common.c b/drivers/thermal/qcom/tsens-common.c new file mode 100644 index 000000000..6207d8d92 --- /dev/null +++ b/drivers/thermal/qcom/tsens-common.c @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/err.h> +#include <linux/io.h> +#include <linux/nvmem-consumer.h> +#include <linux/of_address.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include "tsens.h" + +#define S0_ST_ADDR 0x1030 +#define SN_ADDR_OFFSET 0x4 +#define SN_ST_TEMP_MASK 0x3ff +#define CAL_DEGC_PT1 30 +#define CAL_DEGC_PT2 120 +#define SLOPE_FACTOR 1000 +#define SLOPE_DEFAULT 3200 + +char *qfprom_read(struct device *dev, const char *cname) +{ + struct nvmem_cell *cell; + ssize_t data; + char *ret; + + cell = nvmem_cell_get(dev, cname); + if (IS_ERR(cell)) + return ERR_CAST(cell); + + ret = nvmem_cell_read(cell, &data); + nvmem_cell_put(cell); + + return ret; +} + +/* + * Use this function on devices where slope and offset calculations + * depend on calibration data read from qfprom. On others the slope + * and offset values are derived from tz->tzp->slope and tz->tzp->offset + * resp. + */ +void compute_intercept_slope(struct tsens_device *tmdev, u32 *p1, + u32 *p2, u32 mode) +{ + int i; + int num, den; + + for (i = 0; i < tmdev->num_sensors; i++) { + dev_dbg(tmdev->dev, + "sensor%d - data_point1:%#x data_point2:%#x\n", + i, p1[i], p2[i]); + + tmdev->sensor[i].slope = SLOPE_DEFAULT; + if (mode == TWO_PT_CALIB) { + /* + * slope (m) = adc_code2 - adc_code1 (y2 - y1)/ + * temp_120_degc - temp_30_degc (x2 - x1) + */ + num = p2[i] - p1[i]; + num *= SLOPE_FACTOR; + den = CAL_DEGC_PT2 - CAL_DEGC_PT1; + tmdev->sensor[i].slope = num / den; + } + + tmdev->sensor[i].offset = (p1[i] * SLOPE_FACTOR) - + (CAL_DEGC_PT1 * + tmdev->sensor[i].slope); + dev_dbg(tmdev->dev, "offset:%d\n", tmdev->sensor[i].offset); + } +} + +static inline int code_to_degc(u32 adc_code, const struct tsens_sensor *s) +{ + int degc, num, den; + + num = (adc_code * SLOPE_FACTOR) - s->offset; + den = s->slope; + + if (num > 0) + degc = num + (den / 2); + else if (num < 0) + degc = num - (den / 2); + else + degc = num; + + degc /= den; + + return degc; +} + +int get_temp_common(struct tsens_device *tmdev, int id, int *temp) +{ + struct tsens_sensor *s = &tmdev->sensor[id]; + u32 code; + unsigned int status_reg; + int last_temp = 0, ret; + + status_reg = S0_ST_ADDR + s->hw_id * SN_ADDR_OFFSET; + ret = regmap_read(tmdev->map, status_reg, &code); + if (ret) + return ret; + last_temp = code & SN_ST_TEMP_MASK; + + *temp = code_to_degc(last_temp, s) * 1000; + + return 0; +} + +static const struct regmap_config tsens_config = { + .reg_bits = 32, + .val_bits = 32, + .reg_stride = 4, +}; + +int __init init_common(struct tsens_device *tmdev) +{ + void __iomem *base; + struct resource *res; + struct platform_device *op = of_find_device_by_node(tmdev->dev->of_node); + + if (!op) + return -EINVAL; + + /* The driver only uses the TM register address space for now */ + if (op->num_resources > 1) { + tmdev->tm_offset = 0; + } else { + /* old DTs where SROT and TM were in a contiguous 2K block */ + tmdev->tm_offset = 0x1000; + } + + res = platform_get_resource(op, IORESOURCE_MEM, 0); + base = devm_ioremap_resource(&op->dev, res); + if (IS_ERR(base)) + return PTR_ERR(base); + + tmdev->map = devm_regmap_init_mmio(tmdev->dev, base, &tsens_config); + if (IS_ERR(tmdev->map)) + return PTR_ERR(tmdev->map); + + return 0; +} diff --git a/drivers/thermal/qcom/tsens-v2.c b/drivers/thermal/qcom/tsens-v2.c new file mode 100644 index 000000000..44da02f59 --- /dev/null +++ b/drivers/thermal/qcom/tsens-v2.c @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * Copyright (c) 2018, Linaro Limited + */ + +#include <linux/regmap.h> +#include <linux/bitops.h> +#include "tsens.h" + +#define STATUS_OFFSET 0xa0 +#define LAST_TEMP_MASK 0xfff +#define STATUS_VALID_BIT BIT(21) + +static int get_temp_tsens_v2(struct tsens_device *tmdev, int id, int *temp) +{ + struct tsens_sensor *s = &tmdev->sensor[id]; + u32 code; + unsigned int status_reg; + u32 last_temp = 0, last_temp2 = 0, last_temp3 = 0; + int ret; + + status_reg = tmdev->tm_offset + STATUS_OFFSET + s->hw_id * 4; + ret = regmap_read(tmdev->map, status_reg, &code); + if (ret) + return ret; + last_temp = code & LAST_TEMP_MASK; + if (code & STATUS_VALID_BIT) + goto done; + + /* Try a second time */ + ret = regmap_read(tmdev->map, status_reg, &code); + if (ret) + return ret; + if (code & STATUS_VALID_BIT) { + last_temp = code & LAST_TEMP_MASK; + goto done; + } else { + last_temp2 = code & LAST_TEMP_MASK; + } + + /* Try a third/last time */ + ret = regmap_read(tmdev->map, status_reg, &code); + if (ret) + return ret; + if (code & STATUS_VALID_BIT) { + last_temp = code & LAST_TEMP_MASK; + goto done; + } else { + last_temp3 = code & LAST_TEMP_MASK; + } + + if (last_temp == last_temp2) + last_temp = last_temp2; + else if (last_temp2 == last_temp3) + last_temp = last_temp3; +done: + /* Convert temperature from deciCelsius to milliCelsius */ + *temp = sign_extend32(last_temp, fls(LAST_TEMP_MASK) - 1) * 100; + + return 0; +} + +static const struct tsens_ops ops_generic_v2 = { + .init = init_common, + .get_temp = get_temp_tsens_v2, +}; + +const struct tsens_data data_tsens_v2 = { + .ops = &ops_generic_v2, +}; + +/* Kept around for backward compatibility with old msm8996.dtsi */ +const struct tsens_data data_8996 = { + .num_sensors = 13, + .ops = &ops_generic_v2, +}; diff --git a/drivers/thermal/qcom/tsens.c b/drivers/thermal/qcom/tsens.c new file mode 100644 index 000000000..b13971328 --- /dev/null +++ b/drivers/thermal/qcom/tsens.c @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/err.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/slab.h> +#include <linux/thermal.h> +#include "tsens.h" + +static int tsens_get_temp(void *data, int *temp) +{ + const struct tsens_sensor *s = data; + struct tsens_device *tmdev = s->tmdev; + + return tmdev->ops->get_temp(tmdev, s->id, temp); +} + +static int tsens_get_trend(void *p, int trip, enum thermal_trend *trend) +{ + const struct tsens_sensor *s = p; + struct tsens_device *tmdev = s->tmdev; + + if (tmdev->ops->get_trend) + return tmdev->ops->get_trend(tmdev, s->id, trend); + + return -ENOTSUPP; +} + +static int __maybe_unused tsens_suspend(struct device *dev) +{ + struct tsens_device *tmdev = dev_get_drvdata(dev); + + if (tmdev->ops && tmdev->ops->suspend) + return tmdev->ops->suspend(tmdev); + + return 0; +} + +static int __maybe_unused tsens_resume(struct device *dev) +{ + struct tsens_device *tmdev = dev_get_drvdata(dev); + + if (tmdev->ops && tmdev->ops->resume) + return tmdev->ops->resume(tmdev); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(tsens_pm_ops, tsens_suspend, tsens_resume); + +static const struct of_device_id tsens_table[] = { + { + .compatible = "qcom,msm8916-tsens", + .data = &data_8916, + }, { + .compatible = "qcom,msm8974-tsens", + .data = &data_8974, + }, { + .compatible = "qcom,msm8996-tsens", + .data = &data_8996, + }, { + .compatible = "qcom,tsens-v2", + .data = &data_tsens_v2, + }, + {} +}; +MODULE_DEVICE_TABLE(of, tsens_table); + +static const struct thermal_zone_of_device_ops tsens_of_ops = { + .get_temp = tsens_get_temp, + .get_trend = tsens_get_trend, +}; + +static int tsens_register(struct tsens_device *tmdev) +{ + int i; + struct thermal_zone_device *tzd; + u32 *hw_id, n = tmdev->num_sensors; + + hw_id = devm_kcalloc(tmdev->dev, n, sizeof(u32), GFP_KERNEL); + if (!hw_id) + return -ENOMEM; + + for (i = 0; i < tmdev->num_sensors; i++) { + tmdev->sensor[i].tmdev = tmdev; + tmdev->sensor[i].id = i; + tzd = devm_thermal_zone_of_sensor_register(tmdev->dev, i, + &tmdev->sensor[i], + &tsens_of_ops); + if (IS_ERR(tzd)) + continue; + tmdev->sensor[i].tzd = tzd; + if (tmdev->ops->enable) + tmdev->ops->enable(tmdev, i); + } + return 0; +} + +static int tsens_probe(struct platform_device *pdev) +{ + int ret, i; + struct device *dev; + struct device_node *np; + struct tsens_device *tmdev; + const struct tsens_data *data; + const struct of_device_id *id; + u32 num_sensors; + + if (pdev->dev.of_node) + dev = &pdev->dev; + else + dev = pdev->dev.parent; + + np = dev->of_node; + + id = of_match_node(tsens_table, np); + if (id) + data = id->data; + else + data = &data_8960; + + num_sensors = data->num_sensors; + + if (np) + of_property_read_u32(np, "#qcom,sensors", &num_sensors); + + if (num_sensors <= 0) { + dev_err(dev, "invalid number of sensors\n"); + return -EINVAL; + } + + tmdev = devm_kzalloc(dev, + struct_size(tmdev, sensor, num_sensors), + GFP_KERNEL); + if (!tmdev) + return -ENOMEM; + + tmdev->dev = dev; + tmdev->num_sensors = num_sensors; + tmdev->ops = data->ops; + for (i = 0; i < tmdev->num_sensors; i++) { + if (data->hw_ids) + tmdev->sensor[i].hw_id = data->hw_ids[i]; + else + tmdev->sensor[i].hw_id = i; + } + + if (!tmdev->ops || !tmdev->ops->init || !tmdev->ops->get_temp) + return -EINVAL; + + ret = tmdev->ops->init(tmdev); + if (ret < 0) { + dev_err(dev, "tsens init failed\n"); + return ret; + } + + if (tmdev->ops->calibrate) { + ret = tmdev->ops->calibrate(tmdev); + if (ret < 0) { + if (ret != -EPROBE_DEFER) + dev_err(dev, "tsens calibration failed\n"); + return ret; + } + } + + ret = tsens_register(tmdev); + + platform_set_drvdata(pdev, tmdev); + + return ret; +} + +static int tsens_remove(struct platform_device *pdev) +{ + struct tsens_device *tmdev = platform_get_drvdata(pdev); + + if (tmdev->ops->disable) + tmdev->ops->disable(tmdev); + + return 0; +} + +static struct platform_driver tsens_driver = { + .probe = tsens_probe, + .remove = tsens_remove, + .driver = { + .name = "qcom-tsens", + .pm = &tsens_pm_ops, + .of_match_table = tsens_table, + }, +}; +module_platform_driver(tsens_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("QCOM Temperature Sensor driver"); +MODULE_ALIAS("platform:qcom-tsens"); diff --git a/drivers/thermal/qcom/tsens.h b/drivers/thermal/qcom/tsens.h new file mode 100644 index 000000000..14331eb45 --- /dev/null +++ b/drivers/thermal/qcom/tsens.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ +#ifndef __QCOM_TSENS_H__ +#define __QCOM_TSENS_H__ + +#define ONE_PT_CALIB 0x1 +#define ONE_PT_CALIB2 0x2 +#define TWO_PT_CALIB 0x3 + +#include <linux/thermal.h> + +struct tsens_device; + +struct tsens_sensor { + struct tsens_device *tmdev; + struct thermal_zone_device *tzd; + int offset; + int id; + int hw_id; + int slope; + u32 status; +}; + +/** + * struct tsens_ops - operations as supported by the tsens device + * @init: Function to initialize the tsens device + * @calibrate: Function to calibrate the tsens device + * @get_temp: Function which returns the temp in millidegC + * @enable: Function to enable (clocks/power) tsens device + * @disable: Function to disable the tsens device + * @suspend: Function to suspend the tsens device + * @resume: Function to resume the tsens device + * @get_trend: Function to get the thermal/temp trend + */ +struct tsens_ops { + /* mandatory callbacks */ + int (*init)(struct tsens_device *); + int (*calibrate)(struct tsens_device *); + int (*get_temp)(struct tsens_device *, int, int *); + /* optional callbacks */ + int (*enable)(struct tsens_device *, int); + void (*disable)(struct tsens_device *); + int (*suspend)(struct tsens_device *); + int (*resume)(struct tsens_device *); + int (*get_trend)(struct tsens_device *, int, enum thermal_trend *); +}; + +/** + * struct tsens_data - tsens instance specific data + * @num_sensors: Max number of sensors supported by platform + * @ops: operations the tsens instance supports + * @hw_ids: Subset of sensors ids supported by platform, if not the first n + */ +struct tsens_data { + const u32 num_sensors; + const struct tsens_ops *ops; + unsigned int *hw_ids; +}; + +/* Registers to be saved/restored across a context loss */ +struct tsens_context { + int threshold; + int control; +}; + +struct tsens_device { + struct device *dev; + u32 num_sensors; + struct regmap *map; + u32 tm_offset; + struct tsens_context ctx; + const struct tsens_ops *ops; + struct tsens_sensor sensor[0]; +}; + +char *qfprom_read(struct device *, const char *); +void compute_intercept_slope(struct tsens_device *, u32 *, u32 *, u32); +int init_common(struct tsens_device *); +int get_temp_common(struct tsens_device *, int, int *); + +/* TSENS v1 targets */ +extern const struct tsens_data data_8916, data_8974, data_8960; +/* TSENS v2 targets */ +extern const struct tsens_data data_8996, data_tsens_v2; + +#endif /* __QCOM_TSENS_H__ */ diff --git a/drivers/thermal/qoriq_thermal.c b/drivers/thermal/qoriq_thermal.c new file mode 100644 index 000000000..450ed66ed --- /dev/null +++ b/drivers/thermal/qoriq_thermal.c @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Copyright 2016 Freescale Semiconductor, Inc. + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/thermal.h> + +#include "thermal_core.h" + +#define SITES_MAX 16 + +/* + * QorIQ TMU Registers + */ +struct qoriq_tmu_site_regs { + u32 tritsr; /* Immediate Temperature Site Register */ + u32 tratsr; /* Average Temperature Site Register */ + u8 res0[0x8]; +}; + +struct qoriq_tmu_regs { + u32 tmr; /* Mode Register */ +#define TMR_DISABLE 0x0 +#define TMR_ME 0x80000000 +#define TMR_ALPF 0x0c000000 + u32 tsr; /* Status Register */ + u32 tmtmir; /* Temperature measurement interval Register */ +#define TMTMIR_DEFAULT 0x0000000f + u8 res0[0x14]; + u32 tier; /* Interrupt Enable Register */ +#define TIER_DISABLE 0x0 + u32 tidr; /* Interrupt Detect Register */ + u32 tiscr; /* Interrupt Site Capture Register */ + u32 ticscr; /* Interrupt Critical Site Capture Register */ + u8 res1[0x10]; + u32 tmhtcrh; /* High Temperature Capture Register */ + u32 tmhtcrl; /* Low Temperature Capture Register */ + u8 res2[0x8]; + u32 tmhtitr; /* High Temperature Immediate Threshold */ + u32 tmhtatr; /* High Temperature Average Threshold */ + u32 tmhtactr; /* High Temperature Average Crit Threshold */ + u8 res3[0x24]; + u32 ttcfgr; /* Temperature Configuration Register */ + u32 tscfgr; /* Sensor Configuration Register */ + u8 res4[0x78]; + struct qoriq_tmu_site_regs site[SITES_MAX]; + u8 res5[0x9f8]; + u32 ipbrr0; /* IP Block Revision Register 0 */ + u32 ipbrr1; /* IP Block Revision Register 1 */ + u8 res6[0x310]; + u32 ttr0cr; /* Temperature Range 0 Control Register */ + u32 ttr1cr; /* Temperature Range 1 Control Register */ + u32 ttr2cr; /* Temperature Range 2 Control Register */ + u32 ttr3cr; /* Temperature Range 3 Control Register */ +}; + +/* + * Thermal zone data + */ +struct qoriq_tmu_data { + struct thermal_zone_device *tz; + struct qoriq_tmu_regs __iomem *regs; + int sensor_id; + bool little_endian; +}; + +static void tmu_write(struct qoriq_tmu_data *p, u32 val, void __iomem *addr) +{ + if (p->little_endian) + iowrite32(val, addr); + else + iowrite32be(val, addr); +} + +static u32 tmu_read(struct qoriq_tmu_data *p, void __iomem *addr) +{ + if (p->little_endian) + return ioread32(addr); + else + return ioread32be(addr); +} + +static int tmu_get_temp(void *p, int *temp) +{ + u32 val; + struct qoriq_tmu_data *data = p; + + val = tmu_read(data, &data->regs->site[data->sensor_id].tritsr); + *temp = (val & 0xff) * 1000; + + return 0; +} + +static int qoriq_tmu_get_sensor_id(void) +{ + int ret, id; + struct of_phandle_args sensor_specs; + struct device_node *np, *sensor_np; + + np = of_find_node_by_name(NULL, "thermal-zones"); + if (!np) + return -ENODEV; + + sensor_np = of_get_next_child(np, NULL); + ret = of_parse_phandle_with_args(sensor_np, "thermal-sensors", + "#thermal-sensor-cells", + 0, &sensor_specs); + if (ret) { + of_node_put(np); + of_node_put(sensor_np); + return ret; + } + + if (sensor_specs.args_count >= 1) { + id = sensor_specs.args[0]; + WARN(sensor_specs.args_count > 1, + "%s: too many cells in sensor specifier %d\n", + sensor_specs.np->name, sensor_specs.args_count); + } else { + id = 0; + } + + of_node_put(np); + of_node_put(sensor_np); + + return id; +} + +static int qoriq_tmu_calibration(struct platform_device *pdev) +{ + int i, val, len; + u32 range[4]; + const u32 *calibration; + struct device_node *np = pdev->dev.of_node; + struct qoriq_tmu_data *data = platform_get_drvdata(pdev); + + if (of_property_read_u32_array(np, "fsl,tmu-range", range, 4)) { + dev_err(&pdev->dev, "missing calibration range.\n"); + return -ENODEV; + } + + /* Init temperature range registers */ + tmu_write(data, range[0], &data->regs->ttr0cr); + tmu_write(data, range[1], &data->regs->ttr1cr); + tmu_write(data, range[2], &data->regs->ttr2cr); + tmu_write(data, range[3], &data->regs->ttr3cr); + + calibration = of_get_property(np, "fsl,tmu-calibration", &len); + if (calibration == NULL || len % 8) { + dev_err(&pdev->dev, "invalid calibration data.\n"); + return -ENODEV; + } + + for (i = 0; i < len; i += 8, calibration += 2) { + val = of_read_number(calibration, 1); + tmu_write(data, val, &data->regs->ttcfgr); + val = of_read_number(calibration + 1, 1); + tmu_write(data, val, &data->regs->tscfgr); + } + + return 0; +} + +static void qoriq_tmu_init_device(struct qoriq_tmu_data *data) +{ + /* Disable interrupt, using polling instead */ + tmu_write(data, TIER_DISABLE, &data->regs->tier); + + /* Set update_interval */ + tmu_write(data, TMTMIR_DEFAULT, &data->regs->tmtmir); + + /* Disable monitoring */ + tmu_write(data, TMR_DISABLE, &data->regs->tmr); +} + +static const struct thermal_zone_of_device_ops tmu_tz_ops = { + .get_temp = tmu_get_temp, +}; + +static int qoriq_tmu_probe(struct platform_device *pdev) +{ + int ret; + struct qoriq_tmu_data *data; + struct device_node *np = pdev->dev.of_node; + u32 site; + + if (!np) { + dev_err(&pdev->dev, "Device OF-Node is NULL"); + return -ENODEV; + } + + data = devm_kzalloc(&pdev->dev, sizeof(struct qoriq_tmu_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + platform_set_drvdata(pdev, data); + + data->little_endian = of_property_read_bool(np, "little-endian"); + + data->sensor_id = qoriq_tmu_get_sensor_id(); + if (data->sensor_id < 0) { + dev_err(&pdev->dev, "Failed to get sensor id\n"); + ret = -ENODEV; + goto err_iomap; + } + + data->regs = of_iomap(np, 0); + if (!data->regs) { + dev_err(&pdev->dev, "Failed to get memory region\n"); + ret = -ENODEV; + goto err_iomap; + } + + qoriq_tmu_init_device(data); /* TMU initialization */ + + ret = qoriq_tmu_calibration(pdev); /* TMU calibration */ + if (ret < 0) + goto err_tmu; + + data->tz = devm_thermal_zone_of_sensor_register(&pdev->dev, + data->sensor_id, + data, &tmu_tz_ops); + if (IS_ERR(data->tz)) { + ret = PTR_ERR(data->tz); + dev_err(&pdev->dev, + "Failed to register thermal zone device %d\n", ret); + goto err_tmu; + } + + /* Enable monitoring */ + site = 0x1 << (15 - data->sensor_id); + tmu_write(data, site | TMR_ME | TMR_ALPF, &data->regs->tmr); + + return 0; + +err_tmu: + iounmap(data->regs); + +err_iomap: + platform_set_drvdata(pdev, NULL); + + return ret; +} + +static int qoriq_tmu_remove(struct platform_device *pdev) +{ + struct qoriq_tmu_data *data = platform_get_drvdata(pdev); + + /* Disable monitoring */ + tmu_write(data, TMR_DISABLE, &data->regs->tmr); + + iounmap(data->regs); + platform_set_drvdata(pdev, NULL); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int qoriq_tmu_suspend(struct device *dev) +{ + u32 tmr; + struct qoriq_tmu_data *data = dev_get_drvdata(dev); + + /* Disable monitoring */ + tmr = tmu_read(data, &data->regs->tmr); + tmr &= ~TMR_ME; + tmu_write(data, tmr, &data->regs->tmr); + + return 0; +} + +static int qoriq_tmu_resume(struct device *dev) +{ + u32 tmr; + struct qoriq_tmu_data *data = dev_get_drvdata(dev); + + /* Enable monitoring */ + tmr = tmu_read(data, &data->regs->tmr); + tmr |= TMR_ME; + tmu_write(data, tmr, &data->regs->tmr); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(qoriq_tmu_pm_ops, + qoriq_tmu_suspend, qoriq_tmu_resume); + +static const struct of_device_id qoriq_tmu_match[] = { + { .compatible = "fsl,qoriq-tmu", }, + {}, +}; +MODULE_DEVICE_TABLE(of, qoriq_tmu_match); + +static struct platform_driver qoriq_tmu = { + .driver = { + .name = "qoriq_thermal", + .pm = &qoriq_tmu_pm_ops, + .of_match_table = qoriq_tmu_match, + }, + .probe = qoriq_tmu_probe, + .remove = qoriq_tmu_remove, +}; +module_platform_driver(qoriq_tmu); + +MODULE_AUTHOR("Jia Hongtao <hongtao.jia@nxp.com>"); +MODULE_DESCRIPTION("QorIQ Thermal Monitoring Unit driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/rcar_gen3_thermal.c b/drivers/thermal/rcar_gen3_thermal.c new file mode 100644 index 000000000..8f553453d --- /dev/null +++ b/drivers/thermal/rcar_gen3_thermal.c @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * R-Car Gen3 THS thermal sensor driver + * Based on rcar_thermal.c and work from Hien Dang and Khiem Nguyen. + * + * Copyright (C) 2016 Renesas Electronics Corporation. + * Copyright (C) 2016 Sang Engineering + */ +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/sys_soc.h> +#include <linux/thermal.h> + +#include "thermal_core.h" + +/* Register offsets */ +#define REG_GEN3_IRQSTR 0x04 +#define REG_GEN3_IRQMSK 0x08 +#define REG_GEN3_IRQCTL 0x0C +#define REG_GEN3_IRQEN 0x10 +#define REG_GEN3_IRQTEMP1 0x14 +#define REG_GEN3_IRQTEMP2 0x18 +#define REG_GEN3_IRQTEMP3 0x1C +#define REG_GEN3_CTSR 0x20 +#define REG_GEN3_THCTR 0x20 +#define REG_GEN3_TEMP 0x28 +#define REG_GEN3_THCODE1 0x50 +#define REG_GEN3_THCODE2 0x54 +#define REG_GEN3_THCODE3 0x58 + +/* IRQ{STR,MSK,EN} bits */ +#define IRQ_TEMP1 BIT(0) +#define IRQ_TEMP2 BIT(1) +#define IRQ_TEMP3 BIT(2) +#define IRQ_TEMPD1 BIT(3) +#define IRQ_TEMPD2 BIT(4) +#define IRQ_TEMPD3 BIT(5) + +/* CTSR bits */ +#define CTSR_PONM BIT(8) +#define CTSR_AOUT BIT(7) +#define CTSR_THBGR BIT(5) +#define CTSR_VMEN BIT(4) +#define CTSR_VMST BIT(1) +#define CTSR_THSST BIT(0) + +/* THCTR bits */ +#define THCTR_PONM BIT(6) +#define THCTR_THSST BIT(0) + +#define CTEMP_MASK 0xFFF + +#define MCELSIUS(temp) ((temp) * 1000) +#define GEN3_FUSE_MASK 0xFFF + +#define TSC_MAX_NUM 3 + +/* Structure for thermal temperature calculation */ +struct equation_coefs { + int a1; + int b1; + int a2; + int b2; +}; + +struct rcar_gen3_thermal_tsc { + void __iomem *base; + struct thermal_zone_device *zone; + struct equation_coefs coef; + int low; + int high; +}; + +struct rcar_gen3_thermal_priv { + struct rcar_gen3_thermal_tsc *tscs[TSC_MAX_NUM]; + unsigned int num_tscs; + void (*thermal_init)(struct rcar_gen3_thermal_tsc *tsc); +}; + +static inline u32 rcar_gen3_thermal_read(struct rcar_gen3_thermal_tsc *tsc, + u32 reg) +{ + return ioread32(tsc->base + reg); +} + +static inline void rcar_gen3_thermal_write(struct rcar_gen3_thermal_tsc *tsc, + u32 reg, u32 data) +{ + iowrite32(data, tsc->base + reg); +} + +/* + * Linear approximation for temperature + * + * [reg] = [temp] * a + b => [temp] = ([reg] - b) / a + * + * The constants a and b are calculated using two triplets of int values PTAT + * and THCODE. PTAT and THCODE can either be read from hardware or use hard + * coded values from driver. The formula to calculate a and b are taken from + * BSP and sparsely documented and understood. + * + * Examining the linear formula and the formula used to calculate constants a + * and b while knowing that the span for PTAT and THCODE values are between + * 0x000 and 0xfff the largest integer possible is 0xfff * 0xfff == 0xffe001. + * Integer also needs to be signed so that leaves 7 bits for binary + * fixed point scaling. + */ + +#define FIXPT_SHIFT 7 +#define FIXPT_INT(_x) ((_x) << FIXPT_SHIFT) +#define INT_FIXPT(_x) ((_x) >> FIXPT_SHIFT) +#define FIXPT_DIV(_a, _b) DIV_ROUND_CLOSEST(((_a) << FIXPT_SHIFT), (_b)) +#define FIXPT_TO_MCELSIUS(_x) ((_x) * 1000 >> FIXPT_SHIFT) + +#define RCAR3_THERMAL_GRAN 500 /* mili Celsius */ + +/* no idea where these constants come from */ +#define TJ_1 116 +#define TJ_3 -41 + +static void rcar_gen3_thermal_calc_coefs(struct equation_coefs *coef, + int *ptat, int *thcode) +{ + int tj_2; + + /* TODO: Find documentation and document constant calculation formula */ + + /* + * Division is not scaled in BSP and if scaled it might overflow + * the dividend (4095 * 4095 << 14 > INT_MAX) so keep it unscaled + */ + tj_2 = (FIXPT_INT((ptat[1] - ptat[2]) * 157) + / (ptat[0] - ptat[2])) - FIXPT_INT(41); + + coef->a1 = FIXPT_DIV(FIXPT_INT(thcode[1] - thcode[2]), + tj_2 - FIXPT_INT(TJ_3)); + coef->b1 = FIXPT_INT(thcode[2]) - coef->a1 * TJ_3; + + coef->a2 = FIXPT_DIV(FIXPT_INT(thcode[1] - thcode[0]), + tj_2 - FIXPT_INT(TJ_1)); + coef->b2 = FIXPT_INT(thcode[0]) - coef->a2 * TJ_1; +} + +static int rcar_gen3_thermal_round(int temp) +{ + int result, round_offs; + + round_offs = temp >= 0 ? RCAR3_THERMAL_GRAN / 2 : + -RCAR3_THERMAL_GRAN / 2; + result = (temp + round_offs) / RCAR3_THERMAL_GRAN; + return result * RCAR3_THERMAL_GRAN; +} + +static int rcar_gen3_thermal_get_temp(void *devdata, int *temp) +{ + struct rcar_gen3_thermal_tsc *tsc = devdata; + int mcelsius, val1, val2; + u32 reg; + + /* Read register and convert to mili Celsius */ + reg = rcar_gen3_thermal_read(tsc, REG_GEN3_TEMP) & CTEMP_MASK; + + val1 = FIXPT_DIV(FIXPT_INT(reg) - tsc->coef.b1, tsc->coef.a1); + val2 = FIXPT_DIV(FIXPT_INT(reg) - tsc->coef.b2, tsc->coef.a2); + mcelsius = FIXPT_TO_MCELSIUS((val1 + val2) / 2); + + /* Make sure we are inside specifications */ + if ((mcelsius < MCELSIUS(-40)) || (mcelsius > MCELSIUS(125))) + return -EIO; + + /* Round value to device granularity setting */ + *temp = rcar_gen3_thermal_round(mcelsius); + + return 0; +} + +static int rcar_gen3_thermal_mcelsius_to_temp(struct rcar_gen3_thermal_tsc *tsc, + int mcelsius) +{ + int celsius, val1, val2; + + celsius = DIV_ROUND_CLOSEST(mcelsius, 1000); + val1 = celsius * tsc->coef.a1 + tsc->coef.b1; + val2 = celsius * tsc->coef.a2 + tsc->coef.b2; + + return INT_FIXPT((val1 + val2) / 2); +} + +static int rcar_gen3_thermal_set_trips(void *devdata, int low, int high) +{ + struct rcar_gen3_thermal_tsc *tsc = devdata; + + low = clamp_val(low, -40000, 120000); + high = clamp_val(high, -40000, 120000); + + rcar_gen3_thermal_write(tsc, REG_GEN3_IRQTEMP1, + rcar_gen3_thermal_mcelsius_to_temp(tsc, low)); + + rcar_gen3_thermal_write(tsc, REG_GEN3_IRQTEMP2, + rcar_gen3_thermal_mcelsius_to_temp(tsc, high)); + + tsc->low = low; + tsc->high = high; + + return 0; +} + +static const struct thermal_zone_of_device_ops rcar_gen3_tz_of_ops = { + .get_temp = rcar_gen3_thermal_get_temp, + .set_trips = rcar_gen3_thermal_set_trips, +}; + +static void rcar_thermal_irq_set(struct rcar_gen3_thermal_priv *priv, bool on) +{ + unsigned int i; + u32 val = on ? IRQ_TEMPD1 | IRQ_TEMP2 : 0; + + for (i = 0; i < priv->num_tscs; i++) + rcar_gen3_thermal_write(priv->tscs[i], REG_GEN3_IRQMSK, val); +} + +static irqreturn_t rcar_gen3_thermal_irq(int irq, void *data) +{ + struct rcar_gen3_thermal_priv *priv = data; + u32 status; + int i; + + for (i = 0; i < priv->num_tscs; i++) { + status = rcar_gen3_thermal_read(priv->tscs[i], REG_GEN3_IRQSTR); + rcar_gen3_thermal_write(priv->tscs[i], REG_GEN3_IRQSTR, 0); + if (status) + thermal_zone_device_update(priv->tscs[i]->zone, + THERMAL_EVENT_UNSPECIFIED); + } + + return IRQ_HANDLED; +} + +static const struct soc_device_attribute r8a7795es1[] = { + { .soc_id = "r8a7795", .revision = "ES1.*" }, + { /* sentinel */ } +}; + +static void rcar_gen3_thermal_init_r8a7795es1(struct rcar_gen3_thermal_tsc *tsc) +{ + rcar_gen3_thermal_write(tsc, REG_GEN3_CTSR, CTSR_THBGR); + rcar_gen3_thermal_write(tsc, REG_GEN3_CTSR, 0x0); + + usleep_range(1000, 2000); + + rcar_gen3_thermal_write(tsc, REG_GEN3_CTSR, CTSR_PONM); + + rcar_gen3_thermal_write(tsc, REG_GEN3_IRQCTL, 0x3F); + rcar_gen3_thermal_write(tsc, REG_GEN3_IRQMSK, 0); + rcar_gen3_thermal_write(tsc, REG_GEN3_IRQEN, IRQ_TEMPD1 | IRQ_TEMP2); + + rcar_gen3_thermal_write(tsc, REG_GEN3_CTSR, + CTSR_PONM | CTSR_AOUT | CTSR_THBGR | CTSR_VMEN); + + usleep_range(100, 200); + + rcar_gen3_thermal_write(tsc, REG_GEN3_CTSR, + CTSR_PONM | CTSR_AOUT | CTSR_THBGR | CTSR_VMEN | + CTSR_VMST | CTSR_THSST); + + usleep_range(1000, 2000); +} + +static void rcar_gen3_thermal_init(struct rcar_gen3_thermal_tsc *tsc) +{ + u32 reg_val; + + reg_val = rcar_gen3_thermal_read(tsc, REG_GEN3_THCTR); + reg_val &= ~THCTR_PONM; + rcar_gen3_thermal_write(tsc, REG_GEN3_THCTR, reg_val); + + usleep_range(1000, 2000); + + rcar_gen3_thermal_write(tsc, REG_GEN3_IRQCTL, 0x3F); + rcar_gen3_thermal_write(tsc, REG_GEN3_IRQMSK, 0); + rcar_gen3_thermal_write(tsc, REG_GEN3_IRQEN, IRQ_TEMPD1 | IRQ_TEMP2); + + reg_val = rcar_gen3_thermal_read(tsc, REG_GEN3_THCTR); + reg_val |= THCTR_THSST; + rcar_gen3_thermal_write(tsc, REG_GEN3_THCTR, reg_val); + + usleep_range(1000, 2000); +} + +static const struct of_device_id rcar_gen3_thermal_dt_ids[] = { + { .compatible = "renesas,r8a7795-thermal", }, + { .compatible = "renesas,r8a7796-thermal", }, + { .compatible = "renesas,r8a77965-thermal", }, + {}, +}; +MODULE_DEVICE_TABLE(of, rcar_gen3_thermal_dt_ids); + +static int rcar_gen3_thermal_remove(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct rcar_gen3_thermal_priv *priv = dev_get_drvdata(dev); + + rcar_thermal_irq_set(priv, false); + + pm_runtime_put(dev); + pm_runtime_disable(dev); + + return 0; +} + +static int rcar_gen3_thermal_probe(struct platform_device *pdev) +{ + struct rcar_gen3_thermal_priv *priv; + struct device *dev = &pdev->dev; + struct resource *res; + struct thermal_zone_device *zone; + int ret, irq, i; + char *irqname; + + /* default values if FUSEs are missing */ + /* TODO: Read values from hardware on supported platforms */ + int ptat[3] = { 2631, 1509, 435 }; + int thcode[TSC_MAX_NUM][3] = { + { 3397, 2800, 2221 }, + { 3393, 2795, 2216 }, + { 3389, 2805, 2237 }, + }; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->thermal_init = rcar_gen3_thermal_init; + if (soc_device_match(r8a7795es1)) + priv->thermal_init = rcar_gen3_thermal_init_r8a7795es1; + + platform_set_drvdata(pdev, priv); + + /* + * Request 2 (of the 3 possible) IRQs, the driver only needs to + * to trigger on the low and high trip points of the current + * temp window at this point. + */ + for (i = 0; i < 2; i++) { + irq = platform_get_irq(pdev, i); + if (irq < 0) + return irq; + + irqname = devm_kasprintf(dev, GFP_KERNEL, "%s:ch%d", + dev_name(dev), i); + if (!irqname) + return -ENOMEM; + + ret = devm_request_threaded_irq(dev, irq, NULL, + rcar_gen3_thermal_irq, + IRQF_ONESHOT, irqname, priv); + if (ret) + return ret; + } + + pm_runtime_enable(dev); + pm_runtime_get_sync(dev); + + for (i = 0; i < TSC_MAX_NUM; i++) { + struct rcar_gen3_thermal_tsc *tsc; + + res = platform_get_resource(pdev, IORESOURCE_MEM, i); + if (!res) + break; + + tsc = devm_kzalloc(dev, sizeof(*tsc), GFP_KERNEL); + if (!tsc) { + ret = -ENOMEM; + goto error_unregister; + } + + tsc->base = devm_ioremap_resource(dev, res); + if (IS_ERR(tsc->base)) { + ret = PTR_ERR(tsc->base); + goto error_unregister; + } + + priv->tscs[i] = tsc; + + priv->thermal_init(tsc); + rcar_gen3_thermal_calc_coefs(&tsc->coef, ptat, thcode[i]); + + zone = devm_thermal_zone_of_sensor_register(dev, i, tsc, + &rcar_gen3_tz_of_ops); + if (IS_ERR(zone)) { + dev_err(dev, "Can't register thermal zone\n"); + ret = PTR_ERR(zone); + goto error_unregister; + } + tsc->zone = zone; + + ret = of_thermal_get_ntrips(tsc->zone); + if (ret < 0) + goto error_unregister; + + dev_info(dev, "TSC%d: Loaded %d trip points\n", i, ret); + } + + priv->num_tscs = i; + + if (!priv->num_tscs) { + ret = -ENODEV; + goto error_unregister; + } + + rcar_thermal_irq_set(priv, true); + + return 0; + +error_unregister: + rcar_gen3_thermal_remove(pdev); + + return ret; +} + +static int __maybe_unused rcar_gen3_thermal_suspend(struct device *dev) +{ + struct rcar_gen3_thermal_priv *priv = dev_get_drvdata(dev); + + rcar_thermal_irq_set(priv, false); + + return 0; +} + +static int __maybe_unused rcar_gen3_thermal_resume(struct device *dev) +{ + struct rcar_gen3_thermal_priv *priv = dev_get_drvdata(dev); + unsigned int i; + + for (i = 0; i < priv->num_tscs; i++) { + struct rcar_gen3_thermal_tsc *tsc = priv->tscs[i]; + + priv->thermal_init(tsc); + rcar_gen3_thermal_set_trips(tsc, tsc->low, tsc->high); + } + + rcar_thermal_irq_set(priv, true); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(rcar_gen3_thermal_pm_ops, rcar_gen3_thermal_suspend, + rcar_gen3_thermal_resume); + +static struct platform_driver rcar_gen3_thermal_driver = { + .driver = { + .name = "rcar_gen3_thermal", + .pm = &rcar_gen3_thermal_pm_ops, + .of_match_table = rcar_gen3_thermal_dt_ids, + }, + .probe = rcar_gen3_thermal_probe, + .remove = rcar_gen3_thermal_remove, +}; +module_platform_driver(rcar_gen3_thermal_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("R-Car Gen3 THS thermal sensor driver"); +MODULE_AUTHOR("Wolfram Sang <wsa+renesas@sang-engineering.com>"); diff --git a/drivers/thermal/rcar_thermal.c b/drivers/thermal/rcar_thermal.c new file mode 100644 index 000000000..140386d7c --- /dev/null +++ b/drivers/thermal/rcar_thermal.c @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * R-Car THS/TSC thermal sensor driver + * + * Copyright (C) 2012 Renesas Solutions Corp. + * Kuninori Morimoto <kuninori.morimoto.gx@renesas.com> + */ +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/irq.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/reboot.h> +#include <linux/slab.h> +#include <linux/spinlock.h> +#include <linux/thermal.h> + +#include "thermal_hwmon.h" + +#define IDLE_INTERVAL 5000 + +#define COMMON_STR 0x00 +#define COMMON_ENR 0x04 +#define COMMON_INTMSK 0x0c + +#define REG_POSNEG 0x20 +#define REG_FILONOFF 0x28 +#define REG_THSCR 0x2c +#define REG_THSSR 0x30 +#define REG_INTCTRL 0x34 + +/* THSCR */ +#define CPCTL (1 << 12) + +/* THSSR */ +#define CTEMP 0x3f + +struct rcar_thermal_common { + void __iomem *base; + struct device *dev; + struct list_head head; + spinlock_t lock; +}; + +struct rcar_thermal_chip { + unsigned int use_of_thermal : 1; + unsigned int has_filonoff : 1; + unsigned int irq_per_ch : 1; + unsigned int needs_suspend_resume : 1; + unsigned int nirqs; +}; + +static const struct rcar_thermal_chip rcar_thermal = { + .use_of_thermal = 0, + .has_filonoff = 1, + .irq_per_ch = 0, + .needs_suspend_resume = 0, + .nirqs = 1, +}; + +static const struct rcar_thermal_chip rcar_gen2_thermal = { + .use_of_thermal = 1, + .has_filonoff = 1, + .irq_per_ch = 0, + .needs_suspend_resume = 0, + .nirqs = 1, +}; + +static const struct rcar_thermal_chip rcar_gen3_thermal = { + .use_of_thermal = 1, + .has_filonoff = 0, + .irq_per_ch = 1, + .needs_suspend_resume = 1, + /* + * The Gen3 chip has 3 interrupts, but this driver uses only 2 + * interrupts to detect a temperature change, rise or fall. + */ + .nirqs = 2, +}; + +struct rcar_thermal_priv { + void __iomem *base; + struct rcar_thermal_common *common; + struct thermal_zone_device *zone; + const struct rcar_thermal_chip *chip; + struct delayed_work work; + struct mutex lock; + struct list_head list; + int id; + u32 ctemp; +}; + +#define rcar_thermal_for_each_priv(pos, common) \ + list_for_each_entry(pos, &common->head, list) + +#define MCELSIUS(temp) ((temp) * 1000) +#define rcar_zone_to_priv(zone) ((zone)->devdata) +#define rcar_priv_to_dev(priv) ((priv)->common->dev) +#define rcar_has_irq_support(priv) ((priv)->common->base) +#define rcar_id_to_shift(priv) ((priv)->id * 8) + +static const struct of_device_id rcar_thermal_dt_ids[] = { + { + .compatible = "renesas,rcar-thermal", + .data = &rcar_thermal, + }, + { + .compatible = "renesas,rcar-gen2-thermal", + .data = &rcar_gen2_thermal, + }, + { + .compatible = "renesas,thermal-r8a77995", + .data = &rcar_gen3_thermal, + }, + {}, +}; +MODULE_DEVICE_TABLE(of, rcar_thermal_dt_ids); + +/* + * basic functions + */ +#define rcar_thermal_common_read(c, r) \ + _rcar_thermal_common_read(c, COMMON_ ##r) +static u32 _rcar_thermal_common_read(struct rcar_thermal_common *common, + u32 reg) +{ + return ioread32(common->base + reg); +} + +#define rcar_thermal_common_write(c, r, d) \ + _rcar_thermal_common_write(c, COMMON_ ##r, d) +static void _rcar_thermal_common_write(struct rcar_thermal_common *common, + u32 reg, u32 data) +{ + iowrite32(data, common->base + reg); +} + +#define rcar_thermal_common_bset(c, r, m, d) \ + _rcar_thermal_common_bset(c, COMMON_ ##r, m, d) +static void _rcar_thermal_common_bset(struct rcar_thermal_common *common, + u32 reg, u32 mask, u32 data) +{ + u32 val; + + val = ioread32(common->base + reg); + val &= ~mask; + val |= (data & mask); + iowrite32(val, common->base + reg); +} + +#define rcar_thermal_read(p, r) _rcar_thermal_read(p, REG_ ##r) +static u32 _rcar_thermal_read(struct rcar_thermal_priv *priv, u32 reg) +{ + return ioread32(priv->base + reg); +} + +#define rcar_thermal_write(p, r, d) _rcar_thermal_write(p, REG_ ##r, d) +static void _rcar_thermal_write(struct rcar_thermal_priv *priv, + u32 reg, u32 data) +{ + iowrite32(data, priv->base + reg); +} + +#define rcar_thermal_bset(p, r, m, d) _rcar_thermal_bset(p, REG_ ##r, m, d) +static void _rcar_thermal_bset(struct rcar_thermal_priv *priv, u32 reg, + u32 mask, u32 data) +{ + u32 val; + + val = ioread32(priv->base + reg); + val &= ~mask; + val |= (data & mask); + iowrite32(val, priv->base + reg); +} + +/* + * zone device functions + */ +static int rcar_thermal_update_temp(struct rcar_thermal_priv *priv) +{ + struct device *dev = rcar_priv_to_dev(priv); + int i; + u32 ctemp, old, new; + int ret = -EINVAL; + + mutex_lock(&priv->lock); + + /* + * TSC decides a value of CPTAP automatically, + * and this is the conditions which validate interrupt. + */ + rcar_thermal_bset(priv, THSCR, CPCTL, CPCTL); + + ctemp = 0; + old = ~0; + for (i = 0; i < 128; i++) { + /* + * we need to wait 300us after changing comparator offset + * to get stable temperature. + * see "Usage Notes" on datasheet + */ + udelay(300); + + new = rcar_thermal_read(priv, THSSR) & CTEMP; + if (new == old) { + ctemp = new; + break; + } + old = new; + } + + if (!ctemp) { + dev_err(dev, "thermal sensor was broken\n"); + goto err_out_unlock; + } + + /* + * enable IRQ + */ + if (rcar_has_irq_support(priv)) { + if (priv->chip->has_filonoff) + rcar_thermal_write(priv, FILONOFF, 0); + + /* enable Rising/Falling edge interrupt */ + rcar_thermal_write(priv, POSNEG, 0x1); + rcar_thermal_write(priv, INTCTRL, (((ctemp - 0) << 8) | + ((ctemp - 1) << 0))); + } + + dev_dbg(dev, "thermal%d %d -> %d\n", priv->id, priv->ctemp, ctemp); + + priv->ctemp = ctemp; + ret = 0; +err_out_unlock: + mutex_unlock(&priv->lock); + return ret; +} + +static int rcar_thermal_get_current_temp(struct rcar_thermal_priv *priv, + int *temp) +{ + int tmp; + int ret; + + ret = rcar_thermal_update_temp(priv); + if (ret < 0) + return ret; + + mutex_lock(&priv->lock); + tmp = MCELSIUS((priv->ctemp * 5) - 65); + mutex_unlock(&priv->lock); + + if ((tmp < MCELSIUS(-45)) || (tmp > MCELSIUS(125))) { + struct device *dev = rcar_priv_to_dev(priv); + + dev_err(dev, "it couldn't measure temperature correctly\n"); + return -EIO; + } + + *temp = tmp; + + return 0; +} + +static int rcar_thermal_of_get_temp(void *data, int *temp) +{ + struct rcar_thermal_priv *priv = data; + + return rcar_thermal_get_current_temp(priv, temp); +} + +static int rcar_thermal_get_temp(struct thermal_zone_device *zone, int *temp) +{ + struct rcar_thermal_priv *priv = rcar_zone_to_priv(zone); + + return rcar_thermal_get_current_temp(priv, temp); +} + +static int rcar_thermal_get_trip_type(struct thermal_zone_device *zone, + int trip, enum thermal_trip_type *type) +{ + struct rcar_thermal_priv *priv = rcar_zone_to_priv(zone); + struct device *dev = rcar_priv_to_dev(priv); + + /* see rcar_thermal_get_temp() */ + switch (trip) { + case 0: /* +90 <= temp */ + *type = THERMAL_TRIP_CRITICAL; + break; + default: + dev_err(dev, "rcar driver trip error\n"); + return -EINVAL; + } + + return 0; +} + +static int rcar_thermal_get_trip_temp(struct thermal_zone_device *zone, + int trip, int *temp) +{ + struct rcar_thermal_priv *priv = rcar_zone_to_priv(zone); + struct device *dev = rcar_priv_to_dev(priv); + + /* see rcar_thermal_get_temp() */ + switch (trip) { + case 0: /* +90 <= temp */ + *temp = MCELSIUS(90); + break; + default: + dev_err(dev, "rcar driver trip error\n"); + return -EINVAL; + } + + return 0; +} + +static int rcar_thermal_notify(struct thermal_zone_device *zone, + int trip, enum thermal_trip_type type) +{ + struct rcar_thermal_priv *priv = rcar_zone_to_priv(zone); + struct device *dev = rcar_priv_to_dev(priv); + + switch (type) { + case THERMAL_TRIP_CRITICAL: + /* FIXME */ + dev_warn(dev, "Thermal reached to critical temperature\n"); + break; + default: + break; + } + + return 0; +} + +static const struct thermal_zone_of_device_ops rcar_thermal_zone_of_ops = { + .get_temp = rcar_thermal_of_get_temp, +}; + +static struct thermal_zone_device_ops rcar_thermal_zone_ops = { + .get_temp = rcar_thermal_get_temp, + .get_trip_type = rcar_thermal_get_trip_type, + .get_trip_temp = rcar_thermal_get_trip_temp, + .notify = rcar_thermal_notify, +}; + +/* + * interrupt + */ +#define rcar_thermal_irq_enable(p) _rcar_thermal_irq_ctrl(p, 1) +#define rcar_thermal_irq_disable(p) _rcar_thermal_irq_ctrl(p, 0) +static void _rcar_thermal_irq_ctrl(struct rcar_thermal_priv *priv, int enable) +{ + struct rcar_thermal_common *common = priv->common; + unsigned long flags; + u32 mask = 0x3 << rcar_id_to_shift(priv); /* enable Rising/Falling */ + + if (!rcar_has_irq_support(priv)) + return; + + spin_lock_irqsave(&common->lock, flags); + + rcar_thermal_common_bset(common, INTMSK, mask, enable ? 0 : mask); + + spin_unlock_irqrestore(&common->lock, flags); +} + +static void rcar_thermal_work(struct work_struct *work) +{ + struct rcar_thermal_priv *priv; + int cctemp, nctemp; + int ret; + + priv = container_of(work, struct rcar_thermal_priv, work.work); + + ret = rcar_thermal_get_current_temp(priv, &cctemp); + if (ret < 0) + return; + + ret = rcar_thermal_update_temp(priv); + if (ret < 0) + return; + + rcar_thermal_irq_enable(priv); + + ret = rcar_thermal_get_current_temp(priv, &nctemp); + if (ret < 0) + return; + + if (nctemp != cctemp) + thermal_zone_device_update(priv->zone, + THERMAL_EVENT_UNSPECIFIED); +} + +static u32 rcar_thermal_had_changed(struct rcar_thermal_priv *priv, u32 status) +{ + struct device *dev = rcar_priv_to_dev(priv); + + status = (status >> rcar_id_to_shift(priv)) & 0x3; + + if (status) { + dev_dbg(dev, "thermal%d %s%s\n", + priv->id, + (status & 0x2) ? "Rising " : "", + (status & 0x1) ? "Falling" : ""); + } + + return status; +} + +static irqreturn_t rcar_thermal_irq(int irq, void *data) +{ + struct rcar_thermal_common *common = data; + struct rcar_thermal_priv *priv; + unsigned long flags; + u32 status, mask; + + spin_lock_irqsave(&common->lock, flags); + + mask = rcar_thermal_common_read(common, INTMSK); + status = rcar_thermal_common_read(common, STR); + rcar_thermal_common_write(common, STR, 0x000F0F0F & mask); + + spin_unlock_irqrestore(&common->lock, flags); + + status = status & ~mask; + + /* + * check the status + */ + rcar_thermal_for_each_priv(priv, common) { + if (rcar_thermal_had_changed(priv, status)) { + rcar_thermal_irq_disable(priv); + queue_delayed_work(system_freezable_wq, &priv->work, + msecs_to_jiffies(300)); + } + } + + return IRQ_HANDLED; +} + +/* + * platform functions + */ +static int rcar_thermal_remove(struct platform_device *pdev) +{ + struct rcar_thermal_common *common = platform_get_drvdata(pdev); + struct device *dev = &pdev->dev; + struct rcar_thermal_priv *priv; + + rcar_thermal_for_each_priv(priv, common) { + rcar_thermal_irq_disable(priv); + cancel_delayed_work_sync(&priv->work); + if (priv->chip->use_of_thermal) + thermal_remove_hwmon_sysfs(priv->zone); + else + thermal_zone_device_unregister(priv->zone); + } + + pm_runtime_put(dev); + pm_runtime_disable(dev); + + return 0; +} + +static int rcar_thermal_probe(struct platform_device *pdev) +{ + struct rcar_thermal_common *common; + struct rcar_thermal_priv *priv; + struct device *dev = &pdev->dev; + struct resource *res, *irq; + const struct rcar_thermal_chip *chip = of_device_get_match_data(dev); + int mres = 0; + int i; + int ret = -ENODEV; + int idle = IDLE_INTERVAL; + u32 enr_bits = 0; + + common = devm_kzalloc(dev, sizeof(*common), GFP_KERNEL); + if (!common) + return -ENOMEM; + + platform_set_drvdata(pdev, common); + + INIT_LIST_HEAD(&common->head); + spin_lock_init(&common->lock); + common->dev = dev; + + pm_runtime_enable(dev); + pm_runtime_get_sync(dev); + + for (i = 0; i < chip->nirqs; i++) { + irq = platform_get_resource(pdev, IORESOURCE_IRQ, i); + if (!irq) + continue; + if (!common->base) { + /* + * platform has IRQ support. + * Then, driver uses common registers + * rcar_has_irq_support() will be enabled + */ + res = platform_get_resource(pdev, IORESOURCE_MEM, + mres++); + common->base = devm_ioremap_resource(dev, res); + if (IS_ERR(common->base)) { + ret = PTR_ERR(common->base); + goto error_unregister; + } + + idle = 0; /* polling delay is not needed */ + } + + ret = devm_request_irq(dev, irq->start, rcar_thermal_irq, + IRQF_SHARED, dev_name(dev), common); + if (ret) { + dev_err(dev, "irq request failed\n "); + goto error_unregister; + } + + /* update ENR bits */ + if (chip->irq_per_ch) + enr_bits |= 1 << i; + } + + for (i = 0;; i++) { + res = platform_get_resource(pdev, IORESOURCE_MEM, mres++); + if (!res) + break; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) { + ret = -ENOMEM; + goto error_unregister; + } + + priv->base = devm_ioremap_resource(dev, res); + if (IS_ERR(priv->base)) { + ret = PTR_ERR(priv->base); + goto error_unregister; + } + + priv->common = common; + priv->id = i; + priv->chip = chip; + mutex_init(&priv->lock); + INIT_LIST_HEAD(&priv->list); + INIT_DELAYED_WORK(&priv->work, rcar_thermal_work); + ret = rcar_thermal_update_temp(priv); + if (ret < 0) + goto error_unregister; + + if (chip->use_of_thermal) + priv->zone = devm_thermal_zone_of_sensor_register( + dev, i, priv, + &rcar_thermal_zone_of_ops); + else + priv->zone = thermal_zone_device_register( + "rcar_thermal", + 1, 0, priv, + &rcar_thermal_zone_ops, NULL, 0, + idle); + if (IS_ERR(priv->zone)) { + dev_err(dev, "can't register thermal zone\n"); + ret = PTR_ERR(priv->zone); + priv->zone = NULL; + goto error_unregister; + } + + if (chip->use_of_thermal) { + /* + * thermal_zone doesn't enable hwmon as default, + * but, enable it here to keep compatible + */ + priv->zone->tzp->no_hwmon = false; + ret = thermal_add_hwmon_sysfs(priv->zone); + if (ret) + goto error_unregister; + } + + rcar_thermal_irq_enable(priv); + + list_move_tail(&priv->list, &common->head); + + /* update ENR bits */ + if (!chip->irq_per_ch) + enr_bits |= 3 << (i * 8); + } + + if (common->base && enr_bits) + rcar_thermal_common_write(common, ENR, enr_bits); + + dev_info(dev, "%d sensor probed\n", i); + + return 0; + +error_unregister: + rcar_thermal_remove(pdev); + + return ret; +} + +#ifdef CONFIG_PM_SLEEP +static int rcar_thermal_suspend(struct device *dev) +{ + struct rcar_thermal_common *common = dev_get_drvdata(dev); + struct rcar_thermal_priv *priv = list_first_entry(&common->head, + typeof(*priv), list); + + if (priv->chip->needs_suspend_resume) { + rcar_thermal_common_write(common, ENR, 0); + rcar_thermal_irq_disable(priv); + rcar_thermal_bset(priv, THSCR, CPCTL, 0); + } + + return 0; +} + +static int rcar_thermal_resume(struct device *dev) +{ + struct rcar_thermal_common *common = dev_get_drvdata(dev); + struct rcar_thermal_priv *priv = list_first_entry(&common->head, + typeof(*priv), list); + int ret; + + if (priv->chip->needs_suspend_resume) { + ret = rcar_thermal_update_temp(priv); + if (ret < 0) + return ret; + rcar_thermal_irq_enable(priv); + rcar_thermal_common_write(common, ENR, 0x03); + } + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(rcar_thermal_pm_ops, rcar_thermal_suspend, + rcar_thermal_resume); + +static struct platform_driver rcar_thermal_driver = { + .driver = { + .name = "rcar_thermal", + .pm = &rcar_thermal_pm_ops, + .of_match_table = rcar_thermal_dt_ids, + }, + .probe = rcar_thermal_probe, + .remove = rcar_thermal_remove, +}; +module_platform_driver(rcar_thermal_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("R-Car THS/TSC thermal sensor driver"); +MODULE_AUTHOR("Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>"); diff --git a/drivers/thermal/rockchip_thermal.c b/drivers/thermal/rockchip_thermal.c new file mode 100644 index 000000000..f36375d5a --- /dev/null +++ b/drivers/thermal/rockchip_thermal.c @@ -0,0 +1,1411 @@ +/* + * Copyright (c) 2014-2016, Fuzhou Rockchip Electronics Co., Ltd + * Caesar Wang <wxt@rock-chips.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + */ + +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_irq.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/reset.h> +#include <linux/thermal.h> +#include <linux/mfd/syscon.h> +#include <linux/pinctrl/consumer.h> + +/** + * If the temperature over a period of time High, + * the resulting TSHUT gave CRU module,let it reset the entire chip, + * or via GPIO give PMIC. + */ +enum tshut_mode { + TSHUT_MODE_CRU = 0, + TSHUT_MODE_GPIO, +}; + +/** + * The system Temperature Sensors tshut(tshut) polarity + * the bit 8 is tshut polarity. + * 0: low active, 1: high active + */ +enum tshut_polarity { + TSHUT_LOW_ACTIVE = 0, + TSHUT_HIGH_ACTIVE, +}; + +/** + * The system has two Temperature Sensors. + * sensor0 is for CPU, and sensor1 is for GPU. + */ +enum sensor_id { + SENSOR_CPU = 0, + SENSOR_GPU, +}; + +/** + * The conversion table has the adc value and temperature. + * ADC_DECREMENT: the adc value is of diminishing.(e.g. rk3288_code_table) + * ADC_INCREMENT: the adc value is incremental.(e.g. rk3368_code_table) + */ +enum adc_sort_mode { + ADC_DECREMENT = 0, + ADC_INCREMENT, +}; + +/** + * The max sensors is two in rockchip SoCs. + * Two sensors: CPU and GPU sensor. + */ +#define SOC_MAX_SENSORS 2 + +/** + * struct chip_tsadc_table - hold information about chip-specific differences + * @id: conversion table + * @length: size of conversion table + * @data_mask: mask to apply on data inputs + * @mode: sort mode of this adc variant (incrementing or decrementing) + */ +struct chip_tsadc_table { + const struct tsadc_table *id; + unsigned int length; + u32 data_mask; + enum adc_sort_mode mode; +}; + +/** + * struct rockchip_tsadc_chip - hold the private data of tsadc chip + * @chn_id[SOC_MAX_SENSORS]: the sensor id of chip correspond to the channel + * @chn_num: the channel number of tsadc chip + * @tshut_temp: the hardware-controlled shutdown temperature value + * @tshut_mode: the hardware-controlled shutdown mode (0:CRU 1:GPIO) + * @tshut_polarity: the hardware-controlled active polarity (0:LOW 1:HIGH) + * @initialize: SoC special initialize tsadc controller method + * @irq_ack: clear the interrupt + * @get_temp: get the temperature + * @set_alarm_temp: set the high temperature interrupt + * @set_tshut_temp: set the hardware-controlled shutdown temperature + * @set_tshut_mode: set the hardware-controlled shutdown mode + * @table: the chip-specific conversion table + */ +struct rockchip_tsadc_chip { + /* The sensor id of chip correspond to the ADC channel */ + int chn_id[SOC_MAX_SENSORS]; + int chn_num; + + /* The hardware-controlled tshut property */ + int tshut_temp; + enum tshut_mode tshut_mode; + enum tshut_polarity tshut_polarity; + + /* Chip-wide methods */ + void (*initialize)(struct regmap *grf, + void __iomem *reg, enum tshut_polarity p); + void (*irq_ack)(void __iomem *reg); + void (*control)(void __iomem *reg, bool on); + + /* Per-sensor methods */ + int (*get_temp)(const struct chip_tsadc_table *table, + int chn, void __iomem *reg, int *temp); + int (*set_alarm_temp)(const struct chip_tsadc_table *table, + int chn, void __iomem *reg, int temp); + int (*set_tshut_temp)(const struct chip_tsadc_table *table, + int chn, void __iomem *reg, int temp); + void (*set_tshut_mode)(int chn, void __iomem *reg, enum tshut_mode m); + + /* Per-table methods */ + struct chip_tsadc_table table; +}; + +/** + * struct rockchip_thermal_sensor - hold the information of thermal sensor + * @thermal: pointer to the platform/configuration data + * @tzd: pointer to a thermal zone + * @id: identifier of the thermal sensor + */ +struct rockchip_thermal_sensor { + struct rockchip_thermal_data *thermal; + struct thermal_zone_device *tzd; + int id; +}; + +/** + * struct rockchip_thermal_data - hold the private data of thermal driver + * @chip: pointer to the platform/configuration data + * @pdev: platform device of thermal + * @reset: the reset controller of tsadc + * @sensors[SOC_MAX_SENSORS]: the thermal sensor + * @clk: the controller clock is divided by the exteral 24MHz + * @pclk: the advanced peripherals bus clock + * @grf: the general register file will be used to do static set by software + * @regs: the base address of tsadc controller + * @tshut_temp: the hardware-controlled shutdown temperature value + * @tshut_mode: the hardware-controlled shutdown mode (0:CRU 1:GPIO) + * @tshut_polarity: the hardware-controlled active polarity (0:LOW 1:HIGH) + */ +struct rockchip_thermal_data { + const struct rockchip_tsadc_chip *chip; + struct platform_device *pdev; + struct reset_control *reset; + + struct rockchip_thermal_sensor sensors[SOC_MAX_SENSORS]; + + struct clk *clk; + struct clk *pclk; + + struct regmap *grf; + void __iomem *regs; + + int tshut_temp; + enum tshut_mode tshut_mode; + enum tshut_polarity tshut_polarity; +}; + +/** + * TSADC Sensor Register description: + * + * TSADCV2_* are used for RK3288 SoCs, the other chips can reuse it. + * TSADCV3_* are used for newer SoCs than RK3288. (e.g: RK3228, RK3399) + * + */ +#define TSADCV2_USER_CON 0x00 +#define TSADCV2_AUTO_CON 0x04 +#define TSADCV2_INT_EN 0x08 +#define TSADCV2_INT_PD 0x0c +#define TSADCV2_DATA(chn) (0x20 + (chn) * 0x04) +#define TSADCV2_COMP_INT(chn) (0x30 + (chn) * 0x04) +#define TSADCV2_COMP_SHUT(chn) (0x40 + (chn) * 0x04) +#define TSADCV2_HIGHT_INT_DEBOUNCE 0x60 +#define TSADCV2_HIGHT_TSHUT_DEBOUNCE 0x64 +#define TSADCV2_AUTO_PERIOD 0x68 +#define TSADCV2_AUTO_PERIOD_HT 0x6c + +#define TSADCV2_AUTO_EN BIT(0) +#define TSADCV2_AUTO_SRC_EN(chn) BIT(4 + (chn)) +#define TSADCV2_AUTO_TSHUT_POLARITY_HIGH BIT(8) + +#define TSADCV3_AUTO_Q_SEL_EN BIT(1) + +#define TSADCV2_INT_SRC_EN(chn) BIT(chn) +#define TSADCV2_SHUT_2GPIO_SRC_EN(chn) BIT(4 + (chn)) +#define TSADCV2_SHUT_2CRU_SRC_EN(chn) BIT(8 + (chn)) + +#define TSADCV2_INT_PD_CLEAR_MASK ~BIT(8) +#define TSADCV3_INT_PD_CLEAR_MASK ~BIT(16) + +#define TSADCV2_DATA_MASK 0xfff +#define TSADCV3_DATA_MASK 0x3ff + +#define TSADCV2_HIGHT_INT_DEBOUNCE_COUNT 4 +#define TSADCV2_HIGHT_TSHUT_DEBOUNCE_COUNT 4 +#define TSADCV2_AUTO_PERIOD_TIME 250 /* 250ms */ +#define TSADCV2_AUTO_PERIOD_HT_TIME 50 /* 50ms */ +#define TSADCV3_AUTO_PERIOD_TIME 1875 /* 2.5ms */ +#define TSADCV3_AUTO_PERIOD_HT_TIME 1875 /* 2.5ms */ + +#define TSADCV2_USER_INTER_PD_SOC 0x340 /* 13 clocks */ + +#define GRF_SARADC_TESTBIT 0x0e644 +#define GRF_TSADC_TESTBIT_L 0x0e648 +#define GRF_TSADC_TESTBIT_H 0x0e64c + +#define GRF_SARADC_TESTBIT_ON (0x10001 << 2) +#define GRF_TSADC_TESTBIT_H_ON (0x10001 << 2) +#define GRF_TSADC_VCM_EN_L (0x10001 << 7) +#define GRF_TSADC_VCM_EN_H (0x10001 << 7) + +/** + * struct tsadc_table - code to temperature conversion table + * @code: the value of adc channel + * @temp: the temperature + * Note: + * code to temperature mapping of the temperature sensor is a piece wise linear + * curve.Any temperature, code faling between to 2 give temperatures can be + * linearly interpolated. + * Code to Temperature mapping should be updated based on manufacturer results. + */ +struct tsadc_table { + u32 code; + int temp; +}; + +static const struct tsadc_table rv1108_table[] = { + {0, -40000}, + {374, -40000}, + {382, -35000}, + {389, -30000}, + {397, -25000}, + {405, -20000}, + {413, -15000}, + {421, -10000}, + {429, -5000}, + {436, 0}, + {444, 5000}, + {452, 10000}, + {460, 15000}, + {468, 20000}, + {476, 25000}, + {483, 30000}, + {491, 35000}, + {499, 40000}, + {507, 45000}, + {515, 50000}, + {523, 55000}, + {531, 60000}, + {539, 65000}, + {547, 70000}, + {555, 75000}, + {562, 80000}, + {570, 85000}, + {578, 90000}, + {586, 95000}, + {594, 100000}, + {602, 105000}, + {610, 110000}, + {618, 115000}, + {626, 120000}, + {634, 125000}, + {TSADCV2_DATA_MASK, 125000}, +}; + +static const struct tsadc_table rk3228_code_table[] = { + {0, -40000}, + {588, -40000}, + {593, -35000}, + {598, -30000}, + {603, -25000}, + {608, -20000}, + {613, -15000}, + {618, -10000}, + {623, -5000}, + {629, 0}, + {634, 5000}, + {639, 10000}, + {644, 15000}, + {649, 20000}, + {654, 25000}, + {660, 30000}, + {665, 35000}, + {670, 40000}, + {675, 45000}, + {681, 50000}, + {686, 55000}, + {691, 60000}, + {696, 65000}, + {702, 70000}, + {707, 75000}, + {712, 80000}, + {717, 85000}, + {723, 90000}, + {728, 95000}, + {733, 100000}, + {738, 105000}, + {744, 110000}, + {749, 115000}, + {754, 120000}, + {760, 125000}, + {TSADCV2_DATA_MASK, 125000}, +}; + +static const struct tsadc_table rk3288_code_table[] = { + {TSADCV2_DATA_MASK, -40000}, + {3800, -40000}, + {3792, -35000}, + {3783, -30000}, + {3774, -25000}, + {3765, -20000}, + {3756, -15000}, + {3747, -10000}, + {3737, -5000}, + {3728, 0}, + {3718, 5000}, + {3708, 10000}, + {3698, 15000}, + {3688, 20000}, + {3678, 25000}, + {3667, 30000}, + {3656, 35000}, + {3645, 40000}, + {3634, 45000}, + {3623, 50000}, + {3611, 55000}, + {3600, 60000}, + {3588, 65000}, + {3575, 70000}, + {3563, 75000}, + {3550, 80000}, + {3537, 85000}, + {3524, 90000}, + {3510, 95000}, + {3496, 100000}, + {3482, 105000}, + {3467, 110000}, + {3452, 115000}, + {3437, 120000}, + {3421, 125000}, + {0, 125000}, +}; + +static const struct tsadc_table rk3328_code_table[] = { + {0, -40000}, + {296, -40000}, + {304, -35000}, + {313, -30000}, + {331, -20000}, + {340, -15000}, + {349, -10000}, + {359, -5000}, + {368, 0}, + {378, 5000}, + {388, 10000}, + {398, 15000}, + {408, 20000}, + {418, 25000}, + {429, 30000}, + {440, 35000}, + {451, 40000}, + {462, 45000}, + {473, 50000}, + {485, 55000}, + {496, 60000}, + {508, 65000}, + {521, 70000}, + {533, 75000}, + {546, 80000}, + {559, 85000}, + {572, 90000}, + {586, 95000}, + {600, 100000}, + {614, 105000}, + {629, 110000}, + {644, 115000}, + {659, 120000}, + {675, 125000}, + {TSADCV2_DATA_MASK, 125000}, +}; + +static const struct tsadc_table rk3368_code_table[] = { + {0, -40000}, + {106, -40000}, + {108, -35000}, + {110, -30000}, + {112, -25000}, + {114, -20000}, + {116, -15000}, + {118, -10000}, + {120, -5000}, + {122, 0}, + {124, 5000}, + {126, 10000}, + {128, 15000}, + {130, 20000}, + {132, 25000}, + {134, 30000}, + {136, 35000}, + {138, 40000}, + {140, 45000}, + {142, 50000}, + {144, 55000}, + {146, 60000}, + {148, 65000}, + {150, 70000}, + {152, 75000}, + {154, 80000}, + {156, 85000}, + {158, 90000}, + {160, 95000}, + {162, 100000}, + {163, 105000}, + {165, 110000}, + {167, 115000}, + {169, 120000}, + {171, 125000}, + {TSADCV3_DATA_MASK, 125000}, +}; + +static const struct tsadc_table rk3399_code_table[] = { + {0, -40000}, + {402, -40000}, + {410, -35000}, + {419, -30000}, + {427, -25000}, + {436, -20000}, + {444, -15000}, + {453, -10000}, + {461, -5000}, + {470, 0}, + {478, 5000}, + {487, 10000}, + {496, 15000}, + {504, 20000}, + {513, 25000}, + {521, 30000}, + {530, 35000}, + {538, 40000}, + {547, 45000}, + {555, 50000}, + {564, 55000}, + {573, 60000}, + {581, 65000}, + {590, 70000}, + {599, 75000}, + {607, 80000}, + {616, 85000}, + {624, 90000}, + {633, 95000}, + {642, 100000}, + {650, 105000}, + {659, 110000}, + {668, 115000}, + {677, 120000}, + {685, 125000}, + {TSADCV3_DATA_MASK, 125000}, +}; + +static u32 rk_tsadcv2_temp_to_code(const struct chip_tsadc_table *table, + int temp) +{ + int high, low, mid; + unsigned long num; + unsigned int denom; + u32 error = table->data_mask; + + low = 0; + high = (table->length - 1) - 1; /* ignore the last check for table */ + mid = (high + low) / 2; + + /* Return mask code data when the temp is over table range */ + if (temp < table->id[low].temp || temp > table->id[high].temp) + goto exit; + + while (low <= high) { + if (temp == table->id[mid].temp) + return table->id[mid].code; + else if (temp < table->id[mid].temp) + high = mid - 1; + else + low = mid + 1; + mid = (low + high) / 2; + } + + /* + * The conversion code granularity provided by the table. Let's + * assume that the relationship between temperature and + * analog value between 2 table entries is linear and interpolate + * to produce less granular result. + */ + num = abs(table->id[mid + 1].code - table->id[mid].code); + num *= temp - table->id[mid].temp; + denom = table->id[mid + 1].temp - table->id[mid].temp; + + switch (table->mode) { + case ADC_DECREMENT: + return table->id[mid].code - (num / denom); + case ADC_INCREMENT: + return table->id[mid].code + (num / denom); + default: + pr_err("%s: unknown table mode: %d\n", __func__, table->mode); + return error; + } + +exit: + pr_err("%s: invalid temperature, temp=%d error=%d\n", + __func__, temp, error); + return error; +} + +static int rk_tsadcv2_code_to_temp(const struct chip_tsadc_table *table, + u32 code, int *temp) +{ + unsigned int low = 1; + unsigned int high = table->length - 1; + unsigned int mid = (low + high) / 2; + unsigned int num; + unsigned long denom; + + WARN_ON(table->length < 2); + + switch (table->mode) { + case ADC_DECREMENT: + code &= table->data_mask; + if (code <= table->id[high].code) + return -EAGAIN; /* Incorrect reading */ + + while (low <= high) { + if (code >= table->id[mid].code && + code < table->id[mid - 1].code) + break; + else if (code < table->id[mid].code) + low = mid + 1; + else + high = mid - 1; + + mid = (low + high) / 2; + } + break; + case ADC_INCREMENT: + code &= table->data_mask; + if (code < table->id[low].code) + return -EAGAIN; /* Incorrect reading */ + + while (low <= high) { + if (code <= table->id[mid].code && + code > table->id[mid - 1].code) + break; + else if (code > table->id[mid].code) + low = mid + 1; + else + high = mid - 1; + + mid = (low + high) / 2; + } + break; + default: + pr_err("%s: unknown table mode: %d\n", __func__, table->mode); + return -EINVAL; + } + + /* + * The 5C granularity provided by the table is too much. Let's + * assume that the relationship between sensor readings and + * temperature between 2 table entries is linear and interpolate + * to produce less granular result. + */ + num = table->id[mid].temp - table->id[mid - 1].temp; + num *= abs(table->id[mid - 1].code - code); + denom = abs(table->id[mid - 1].code - table->id[mid].code); + *temp = table->id[mid - 1].temp + (num / denom); + + return 0; +} + +/** + * rk_tsadcv2_initialize - initialize TASDC Controller. + * + * (1) Set TSADC_V2_AUTO_PERIOD: + * Configure the interleave between every two accessing of + * TSADC in normal operation. + * + * (2) Set TSADCV2_AUTO_PERIOD_HT: + * Configure the interleave between every two accessing of + * TSADC after the temperature is higher than COM_SHUT or COM_INT. + * + * (3) Set TSADCV2_HIGH_INT_DEBOUNCE and TSADC_HIGHT_TSHUT_DEBOUNCE: + * If the temperature is higher than COMP_INT or COMP_SHUT for + * "debounce" times, TSADC controller will generate interrupt or TSHUT. + */ +static void rk_tsadcv2_initialize(struct regmap *grf, void __iomem *regs, + enum tshut_polarity tshut_polarity) +{ + if (tshut_polarity == TSHUT_HIGH_ACTIVE) + writel_relaxed(0U | TSADCV2_AUTO_TSHUT_POLARITY_HIGH, + regs + TSADCV2_AUTO_CON); + else + writel_relaxed(0U & ~TSADCV2_AUTO_TSHUT_POLARITY_HIGH, + regs + TSADCV2_AUTO_CON); + + writel_relaxed(TSADCV2_AUTO_PERIOD_TIME, regs + TSADCV2_AUTO_PERIOD); + writel_relaxed(TSADCV2_HIGHT_INT_DEBOUNCE_COUNT, + regs + TSADCV2_HIGHT_INT_DEBOUNCE); + writel_relaxed(TSADCV2_AUTO_PERIOD_HT_TIME, + regs + TSADCV2_AUTO_PERIOD_HT); + writel_relaxed(TSADCV2_HIGHT_TSHUT_DEBOUNCE_COUNT, + regs + TSADCV2_HIGHT_TSHUT_DEBOUNCE); +} + +/** + * rk_tsadcv3_initialize - initialize TASDC Controller. + * + * (1) The tsadc control power sequence. + * + * (2) Set TSADC_V2_AUTO_PERIOD: + * Configure the interleave between every two accessing of + * TSADC in normal operation. + * + * (2) Set TSADCV2_AUTO_PERIOD_HT: + * Configure the interleave between every two accessing of + * TSADC after the temperature is higher than COM_SHUT or COM_INT. + * + * (3) Set TSADCV2_HIGH_INT_DEBOUNCE and TSADC_HIGHT_TSHUT_DEBOUNCE: + * If the temperature is higher than COMP_INT or COMP_SHUT for + * "debounce" times, TSADC controller will generate interrupt or TSHUT. + */ +static void rk_tsadcv3_initialize(struct regmap *grf, void __iomem *regs, + enum tshut_polarity tshut_polarity) +{ + /* The tsadc control power sequence */ + if (IS_ERR(grf)) { + /* Set interleave value to workround ic time sync issue */ + writel_relaxed(TSADCV2_USER_INTER_PD_SOC, regs + + TSADCV2_USER_CON); + + writel_relaxed(TSADCV2_AUTO_PERIOD_TIME, + regs + TSADCV2_AUTO_PERIOD); + writel_relaxed(TSADCV2_HIGHT_INT_DEBOUNCE_COUNT, + regs + TSADCV2_HIGHT_INT_DEBOUNCE); + writel_relaxed(TSADCV2_AUTO_PERIOD_HT_TIME, + regs + TSADCV2_AUTO_PERIOD_HT); + writel_relaxed(TSADCV2_HIGHT_TSHUT_DEBOUNCE_COUNT, + regs + TSADCV2_HIGHT_TSHUT_DEBOUNCE); + + } else { + /* Enable the voltage common mode feature */ + regmap_write(grf, GRF_TSADC_TESTBIT_L, GRF_TSADC_VCM_EN_L); + regmap_write(grf, GRF_TSADC_TESTBIT_H, GRF_TSADC_VCM_EN_H); + + usleep_range(15, 100); /* The spec note says at least 15 us */ + regmap_write(grf, GRF_SARADC_TESTBIT, GRF_SARADC_TESTBIT_ON); + regmap_write(grf, GRF_TSADC_TESTBIT_H, GRF_TSADC_TESTBIT_H_ON); + usleep_range(90, 200); /* The spec note says at least 90 us */ + + writel_relaxed(TSADCV3_AUTO_PERIOD_TIME, + regs + TSADCV2_AUTO_PERIOD); + writel_relaxed(TSADCV2_HIGHT_INT_DEBOUNCE_COUNT, + regs + TSADCV2_HIGHT_INT_DEBOUNCE); + writel_relaxed(TSADCV3_AUTO_PERIOD_HT_TIME, + regs + TSADCV2_AUTO_PERIOD_HT); + writel_relaxed(TSADCV2_HIGHT_TSHUT_DEBOUNCE_COUNT, + regs + TSADCV2_HIGHT_TSHUT_DEBOUNCE); + } + + if (tshut_polarity == TSHUT_HIGH_ACTIVE) + writel_relaxed(0U | TSADCV2_AUTO_TSHUT_POLARITY_HIGH, + regs + TSADCV2_AUTO_CON); + else + writel_relaxed(0U & ~TSADCV2_AUTO_TSHUT_POLARITY_HIGH, + regs + TSADCV2_AUTO_CON); +} + +static void rk_tsadcv2_irq_ack(void __iomem *regs) +{ + u32 val; + + val = readl_relaxed(regs + TSADCV2_INT_PD); + writel_relaxed(val & TSADCV2_INT_PD_CLEAR_MASK, regs + TSADCV2_INT_PD); +} + +static void rk_tsadcv3_irq_ack(void __iomem *regs) +{ + u32 val; + + val = readl_relaxed(regs + TSADCV2_INT_PD); + writel_relaxed(val & TSADCV3_INT_PD_CLEAR_MASK, regs + TSADCV2_INT_PD); +} + +static void rk_tsadcv2_control(void __iomem *regs, bool enable) +{ + u32 val; + + val = readl_relaxed(regs + TSADCV2_AUTO_CON); + if (enable) + val |= TSADCV2_AUTO_EN; + else + val &= ~TSADCV2_AUTO_EN; + + writel_relaxed(val, regs + TSADCV2_AUTO_CON); +} + +/** + * rk_tsadcv3_control - the tsadc controller is enabled or disabled. + * + * NOTE: TSADC controller works at auto mode, and some SoCs need set the + * tsadc_q_sel bit on TSADCV2_AUTO_CON[1]. The (1024 - tsadc_q) as output + * adc value if setting this bit to enable. + */ +static void rk_tsadcv3_control(void __iomem *regs, bool enable) +{ + u32 val; + + val = readl_relaxed(regs + TSADCV2_AUTO_CON); + if (enable) + val |= TSADCV2_AUTO_EN | TSADCV3_AUTO_Q_SEL_EN; + else + val &= ~TSADCV2_AUTO_EN; + + writel_relaxed(val, regs + TSADCV2_AUTO_CON); +} + +static int rk_tsadcv2_get_temp(const struct chip_tsadc_table *table, + int chn, void __iomem *regs, int *temp) +{ + u32 val; + + val = readl_relaxed(regs + TSADCV2_DATA(chn)); + + return rk_tsadcv2_code_to_temp(table, val, temp); +} + +static int rk_tsadcv2_alarm_temp(const struct chip_tsadc_table *table, + int chn, void __iomem *regs, int temp) +{ + u32 alarm_value; + u32 int_en, int_clr; + + /* + * In some cases, some sensors didn't need the trip points, the + * set_trips will pass {-INT_MAX, INT_MAX} to trigger tsadc alarm + * in the end, ignore this case and disable the high temperature + * interrupt. + */ + if (temp == INT_MAX) { + int_clr = readl_relaxed(regs + TSADCV2_INT_EN); + int_clr &= ~TSADCV2_INT_SRC_EN(chn); + writel_relaxed(int_clr, regs + TSADCV2_INT_EN); + return 0; + } + + /* Make sure the value is valid */ + alarm_value = rk_tsadcv2_temp_to_code(table, temp); + if (alarm_value == table->data_mask) + return -ERANGE; + + writel_relaxed(alarm_value & table->data_mask, + regs + TSADCV2_COMP_INT(chn)); + + int_en = readl_relaxed(regs + TSADCV2_INT_EN); + int_en |= TSADCV2_INT_SRC_EN(chn); + writel_relaxed(int_en, regs + TSADCV2_INT_EN); + + return 0; +} + +static int rk_tsadcv2_tshut_temp(const struct chip_tsadc_table *table, + int chn, void __iomem *regs, int temp) +{ + u32 tshut_value, val; + + /* Make sure the value is valid */ + tshut_value = rk_tsadcv2_temp_to_code(table, temp); + if (tshut_value == table->data_mask) + return -ERANGE; + + writel_relaxed(tshut_value, regs + TSADCV2_COMP_SHUT(chn)); + + /* TSHUT will be valid */ + val = readl_relaxed(regs + TSADCV2_AUTO_CON); + writel_relaxed(val | TSADCV2_AUTO_SRC_EN(chn), regs + TSADCV2_AUTO_CON); + + return 0; +} + +static void rk_tsadcv2_tshut_mode(int chn, void __iomem *regs, + enum tshut_mode mode) +{ + u32 val; + + val = readl_relaxed(regs + TSADCV2_INT_EN); + if (mode == TSHUT_MODE_GPIO) { + val &= ~TSADCV2_SHUT_2CRU_SRC_EN(chn); + val |= TSADCV2_SHUT_2GPIO_SRC_EN(chn); + } else { + val &= ~TSADCV2_SHUT_2GPIO_SRC_EN(chn); + val |= TSADCV2_SHUT_2CRU_SRC_EN(chn); + } + + writel_relaxed(val, regs + TSADCV2_INT_EN); +} + +static const struct rockchip_tsadc_chip rv1108_tsadc_data = { + .chn_id[SENSOR_CPU] = 0, /* cpu sensor is channel 0 */ + .chn_num = 1, /* one channel for tsadc */ + + .tshut_mode = TSHUT_MODE_GPIO, /* default TSHUT via GPIO give PMIC */ + .tshut_polarity = TSHUT_LOW_ACTIVE, /* default TSHUT LOW ACTIVE */ + .tshut_temp = 95000, + + .initialize = rk_tsadcv2_initialize, + .irq_ack = rk_tsadcv3_irq_ack, + .control = rk_tsadcv3_control, + .get_temp = rk_tsadcv2_get_temp, + .set_alarm_temp = rk_tsadcv2_alarm_temp, + .set_tshut_temp = rk_tsadcv2_tshut_temp, + .set_tshut_mode = rk_tsadcv2_tshut_mode, + + .table = { + .id = rv1108_table, + .length = ARRAY_SIZE(rv1108_table), + .data_mask = TSADCV2_DATA_MASK, + .mode = ADC_INCREMENT, + }, +}; + +static const struct rockchip_tsadc_chip rk3228_tsadc_data = { + .chn_id[SENSOR_CPU] = 0, /* cpu sensor is channel 0 */ + .chn_num = 1, /* one channel for tsadc */ + + .tshut_mode = TSHUT_MODE_GPIO, /* default TSHUT via GPIO give PMIC */ + .tshut_polarity = TSHUT_LOW_ACTIVE, /* default TSHUT LOW ACTIVE */ + .tshut_temp = 95000, + + .initialize = rk_tsadcv2_initialize, + .irq_ack = rk_tsadcv3_irq_ack, + .control = rk_tsadcv3_control, + .get_temp = rk_tsadcv2_get_temp, + .set_alarm_temp = rk_tsadcv2_alarm_temp, + .set_tshut_temp = rk_tsadcv2_tshut_temp, + .set_tshut_mode = rk_tsadcv2_tshut_mode, + + .table = { + .id = rk3228_code_table, + .length = ARRAY_SIZE(rk3228_code_table), + .data_mask = TSADCV3_DATA_MASK, + .mode = ADC_INCREMENT, + }, +}; + +static const struct rockchip_tsadc_chip rk3288_tsadc_data = { + .chn_id[SENSOR_CPU] = 1, /* cpu sensor is channel 1 */ + .chn_id[SENSOR_GPU] = 2, /* gpu sensor is channel 2 */ + .chn_num = 2, /* two channels for tsadc */ + + .tshut_mode = TSHUT_MODE_GPIO, /* default TSHUT via GPIO give PMIC */ + .tshut_polarity = TSHUT_LOW_ACTIVE, /* default TSHUT LOW ACTIVE */ + .tshut_temp = 95000, + + .initialize = rk_tsadcv2_initialize, + .irq_ack = rk_tsadcv2_irq_ack, + .control = rk_tsadcv2_control, + .get_temp = rk_tsadcv2_get_temp, + .set_alarm_temp = rk_tsadcv2_alarm_temp, + .set_tshut_temp = rk_tsadcv2_tshut_temp, + .set_tshut_mode = rk_tsadcv2_tshut_mode, + + .table = { + .id = rk3288_code_table, + .length = ARRAY_SIZE(rk3288_code_table), + .data_mask = TSADCV2_DATA_MASK, + .mode = ADC_DECREMENT, + }, +}; + +static const struct rockchip_tsadc_chip rk3328_tsadc_data = { + .chn_id[SENSOR_CPU] = 0, /* cpu sensor is channel 0 */ + .chn_num = 1, /* one channels for tsadc */ + + .tshut_mode = TSHUT_MODE_CRU, /* default TSHUT via CRU */ + .tshut_temp = 95000, + + .initialize = rk_tsadcv2_initialize, + .irq_ack = rk_tsadcv3_irq_ack, + .control = rk_tsadcv3_control, + .get_temp = rk_tsadcv2_get_temp, + .set_alarm_temp = rk_tsadcv2_alarm_temp, + .set_tshut_temp = rk_tsadcv2_tshut_temp, + .set_tshut_mode = rk_tsadcv2_tshut_mode, + + .table = { + .id = rk3328_code_table, + .length = ARRAY_SIZE(rk3328_code_table), + .data_mask = TSADCV2_DATA_MASK, + .mode = ADC_INCREMENT, + }, +}; + +static const struct rockchip_tsadc_chip rk3366_tsadc_data = { + .chn_id[SENSOR_CPU] = 0, /* cpu sensor is channel 0 */ + .chn_id[SENSOR_GPU] = 1, /* gpu sensor is channel 1 */ + .chn_num = 2, /* two channels for tsadc */ + + .tshut_mode = TSHUT_MODE_GPIO, /* default TSHUT via GPIO give PMIC */ + .tshut_polarity = TSHUT_LOW_ACTIVE, /* default TSHUT LOW ACTIVE */ + .tshut_temp = 95000, + + .initialize = rk_tsadcv3_initialize, + .irq_ack = rk_tsadcv3_irq_ack, + .control = rk_tsadcv3_control, + .get_temp = rk_tsadcv2_get_temp, + .set_alarm_temp = rk_tsadcv2_alarm_temp, + .set_tshut_temp = rk_tsadcv2_tshut_temp, + .set_tshut_mode = rk_tsadcv2_tshut_mode, + + .table = { + .id = rk3228_code_table, + .length = ARRAY_SIZE(rk3228_code_table), + .data_mask = TSADCV3_DATA_MASK, + .mode = ADC_INCREMENT, + }, +}; + +static const struct rockchip_tsadc_chip rk3368_tsadc_data = { + .chn_id[SENSOR_CPU] = 0, /* cpu sensor is channel 0 */ + .chn_id[SENSOR_GPU] = 1, /* gpu sensor is channel 1 */ + .chn_num = 2, /* two channels for tsadc */ + + .tshut_mode = TSHUT_MODE_GPIO, /* default TSHUT via GPIO give PMIC */ + .tshut_polarity = TSHUT_LOW_ACTIVE, /* default TSHUT LOW ACTIVE */ + .tshut_temp = 95000, + + .initialize = rk_tsadcv2_initialize, + .irq_ack = rk_tsadcv2_irq_ack, + .control = rk_tsadcv2_control, + .get_temp = rk_tsadcv2_get_temp, + .set_alarm_temp = rk_tsadcv2_alarm_temp, + .set_tshut_temp = rk_tsadcv2_tshut_temp, + .set_tshut_mode = rk_tsadcv2_tshut_mode, + + .table = { + .id = rk3368_code_table, + .length = ARRAY_SIZE(rk3368_code_table), + .data_mask = TSADCV3_DATA_MASK, + .mode = ADC_INCREMENT, + }, +}; + +static const struct rockchip_tsadc_chip rk3399_tsadc_data = { + .chn_id[SENSOR_CPU] = 0, /* cpu sensor is channel 0 */ + .chn_id[SENSOR_GPU] = 1, /* gpu sensor is channel 1 */ + .chn_num = 2, /* two channels for tsadc */ + + .tshut_mode = TSHUT_MODE_GPIO, /* default TSHUT via GPIO give PMIC */ + .tshut_polarity = TSHUT_LOW_ACTIVE, /* default TSHUT LOW ACTIVE */ + .tshut_temp = 95000, + + .initialize = rk_tsadcv3_initialize, + .irq_ack = rk_tsadcv3_irq_ack, + .control = rk_tsadcv3_control, + .get_temp = rk_tsadcv2_get_temp, + .set_alarm_temp = rk_tsadcv2_alarm_temp, + .set_tshut_temp = rk_tsadcv2_tshut_temp, + .set_tshut_mode = rk_tsadcv2_tshut_mode, + + .table = { + .id = rk3399_code_table, + .length = ARRAY_SIZE(rk3399_code_table), + .data_mask = TSADCV3_DATA_MASK, + .mode = ADC_INCREMENT, + }, +}; + +static const struct of_device_id of_rockchip_thermal_match[] = { + { + .compatible = "rockchip,rv1108-tsadc", + .data = (void *)&rv1108_tsadc_data, + }, + { + .compatible = "rockchip,rk3228-tsadc", + .data = (void *)&rk3228_tsadc_data, + }, + { + .compatible = "rockchip,rk3288-tsadc", + .data = (void *)&rk3288_tsadc_data, + }, + { + .compatible = "rockchip,rk3328-tsadc", + .data = (void *)&rk3328_tsadc_data, + }, + { + .compatible = "rockchip,rk3366-tsadc", + .data = (void *)&rk3366_tsadc_data, + }, + { + .compatible = "rockchip,rk3368-tsadc", + .data = (void *)&rk3368_tsadc_data, + }, + { + .compatible = "rockchip,rk3399-tsadc", + .data = (void *)&rk3399_tsadc_data, + }, + { /* end */ }, +}; +MODULE_DEVICE_TABLE(of, of_rockchip_thermal_match); + +static void +rockchip_thermal_toggle_sensor(struct rockchip_thermal_sensor *sensor, bool on) +{ + struct thermal_zone_device *tzd = sensor->tzd; + + tzd->ops->set_mode(tzd, + on ? THERMAL_DEVICE_ENABLED : THERMAL_DEVICE_DISABLED); +} + +static irqreturn_t rockchip_thermal_alarm_irq_thread(int irq, void *dev) +{ + struct rockchip_thermal_data *thermal = dev; + int i; + + dev_dbg(&thermal->pdev->dev, "thermal alarm\n"); + + thermal->chip->irq_ack(thermal->regs); + + for (i = 0; i < thermal->chip->chn_num; i++) + thermal_zone_device_update(thermal->sensors[i].tzd, + THERMAL_EVENT_UNSPECIFIED); + + return IRQ_HANDLED; +} + +static int rockchip_thermal_set_trips(void *_sensor, int low, int high) +{ + struct rockchip_thermal_sensor *sensor = _sensor; + struct rockchip_thermal_data *thermal = sensor->thermal; + const struct rockchip_tsadc_chip *tsadc = thermal->chip; + + dev_dbg(&thermal->pdev->dev, "%s: sensor %d: low: %d, high %d\n", + __func__, sensor->id, low, high); + + return tsadc->set_alarm_temp(&tsadc->table, + sensor->id, thermal->regs, high); +} + +static int rockchip_thermal_get_temp(void *_sensor, int *out_temp) +{ + struct rockchip_thermal_sensor *sensor = _sensor; + struct rockchip_thermal_data *thermal = sensor->thermal; + const struct rockchip_tsadc_chip *tsadc = sensor->thermal->chip; + int retval; + + retval = tsadc->get_temp(&tsadc->table, + sensor->id, thermal->regs, out_temp); + dev_dbg(&thermal->pdev->dev, "sensor %d - temp: %d, retval: %d\n", + sensor->id, *out_temp, retval); + + return retval; +} + +static const struct thermal_zone_of_device_ops rockchip_of_thermal_ops = { + .get_temp = rockchip_thermal_get_temp, + .set_trips = rockchip_thermal_set_trips, +}; + +static int rockchip_configure_from_dt(struct device *dev, + struct device_node *np, + struct rockchip_thermal_data *thermal) +{ + u32 shut_temp, tshut_mode, tshut_polarity; + + if (of_property_read_u32(np, "rockchip,hw-tshut-temp", &shut_temp)) { + dev_warn(dev, + "Missing tshut temp property, using default %d\n", + thermal->chip->tshut_temp); + thermal->tshut_temp = thermal->chip->tshut_temp; + } else { + if (shut_temp > INT_MAX) { + dev_err(dev, "Invalid tshut temperature specified: %d\n", + shut_temp); + return -ERANGE; + } + thermal->tshut_temp = shut_temp; + } + + if (of_property_read_u32(np, "rockchip,hw-tshut-mode", &tshut_mode)) { + dev_warn(dev, + "Missing tshut mode property, using default (%s)\n", + thermal->chip->tshut_mode == TSHUT_MODE_GPIO ? + "gpio" : "cru"); + thermal->tshut_mode = thermal->chip->tshut_mode; + } else { + thermal->tshut_mode = tshut_mode; + } + + if (thermal->tshut_mode > 1) { + dev_err(dev, "Invalid tshut mode specified: %d\n", + thermal->tshut_mode); + return -EINVAL; + } + + if (of_property_read_u32(np, "rockchip,hw-tshut-polarity", + &tshut_polarity)) { + dev_warn(dev, + "Missing tshut-polarity property, using default (%s)\n", + thermal->chip->tshut_polarity == TSHUT_LOW_ACTIVE ? + "low" : "high"); + thermal->tshut_polarity = thermal->chip->tshut_polarity; + } else { + thermal->tshut_polarity = tshut_polarity; + } + + if (thermal->tshut_polarity > 1) { + dev_err(dev, "Invalid tshut-polarity specified: %d\n", + thermal->tshut_polarity); + return -EINVAL; + } + + /* The tsadc wont to handle the error in here since some SoCs didn't + * need this property. + */ + thermal->grf = syscon_regmap_lookup_by_phandle(np, "rockchip,grf"); + if (IS_ERR(thermal->grf)) + dev_warn(dev, "Missing rockchip,grf property\n"); + + return 0; +} + +static int +rockchip_thermal_register_sensor(struct platform_device *pdev, + struct rockchip_thermal_data *thermal, + struct rockchip_thermal_sensor *sensor, + int id) +{ + const struct rockchip_tsadc_chip *tsadc = thermal->chip; + int error; + + tsadc->set_tshut_mode(id, thermal->regs, thermal->tshut_mode); + + error = tsadc->set_tshut_temp(&tsadc->table, id, thermal->regs, + thermal->tshut_temp); + if (error) + dev_err(&pdev->dev, "%s: invalid tshut=%d, error=%d\n", + __func__, thermal->tshut_temp, error); + + sensor->thermal = thermal; + sensor->id = id; + sensor->tzd = devm_thermal_zone_of_sensor_register(&pdev->dev, id, + sensor, &rockchip_of_thermal_ops); + if (IS_ERR(sensor->tzd)) { + error = PTR_ERR(sensor->tzd); + dev_err(&pdev->dev, "failed to register sensor %d: %d\n", + id, error); + return error; + } + + return 0; +} + +/** + * Reset TSADC Controller, reset all tsadc registers. + */ +static void rockchip_thermal_reset_controller(struct reset_control *reset) +{ + reset_control_assert(reset); + usleep_range(10, 20); + reset_control_deassert(reset); +} + +static int rockchip_thermal_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct rockchip_thermal_data *thermal; + const struct of_device_id *match; + struct resource *res; + int irq; + int i; + int error; + + match = of_match_node(of_rockchip_thermal_match, np); + if (!match) + return -ENXIO; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) { + dev_err(&pdev->dev, "no irq resource?\n"); + return -EINVAL; + } + + thermal = devm_kzalloc(&pdev->dev, sizeof(struct rockchip_thermal_data), + GFP_KERNEL); + if (!thermal) + return -ENOMEM; + + thermal->pdev = pdev; + + thermal->chip = (const struct rockchip_tsadc_chip *)match->data; + if (!thermal->chip) + return -EINVAL; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + thermal->regs = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(thermal->regs)) + return PTR_ERR(thermal->regs); + + thermal->reset = devm_reset_control_get(&pdev->dev, "tsadc-apb"); + if (IS_ERR(thermal->reset)) { + error = PTR_ERR(thermal->reset); + dev_err(&pdev->dev, "failed to get tsadc reset: %d\n", error); + return error; + } + + thermal->clk = devm_clk_get(&pdev->dev, "tsadc"); + if (IS_ERR(thermal->clk)) { + error = PTR_ERR(thermal->clk); + dev_err(&pdev->dev, "failed to get tsadc clock: %d\n", error); + return error; + } + + thermal->pclk = devm_clk_get(&pdev->dev, "apb_pclk"); + if (IS_ERR(thermal->pclk)) { + error = PTR_ERR(thermal->pclk); + dev_err(&pdev->dev, "failed to get apb_pclk clock: %d\n", + error); + return error; + } + + error = clk_prepare_enable(thermal->clk); + if (error) { + dev_err(&pdev->dev, "failed to enable converter clock: %d\n", + error); + return error; + } + + error = clk_prepare_enable(thermal->pclk); + if (error) { + dev_err(&pdev->dev, "failed to enable pclk: %d\n", error); + goto err_disable_clk; + } + + rockchip_thermal_reset_controller(thermal->reset); + + error = rockchip_configure_from_dt(&pdev->dev, np, thermal); + if (error) { + dev_err(&pdev->dev, "failed to parse device tree data: %d\n", + error); + goto err_disable_pclk; + } + + thermal->chip->initialize(thermal->grf, thermal->regs, + thermal->tshut_polarity); + + for (i = 0; i < thermal->chip->chn_num; i++) { + error = rockchip_thermal_register_sensor(pdev, thermal, + &thermal->sensors[i], + thermal->chip->chn_id[i]); + if (error) { + dev_err(&pdev->dev, + "failed to register sensor[%d] : error = %d\n", + i, error); + goto err_disable_pclk; + } + } + + error = devm_request_threaded_irq(&pdev->dev, irq, NULL, + &rockchip_thermal_alarm_irq_thread, + IRQF_ONESHOT, + "rockchip_thermal", thermal); + if (error) { + dev_err(&pdev->dev, + "failed to request tsadc irq: %d\n", error); + goto err_disable_pclk; + } + + thermal->chip->control(thermal->regs, true); + + for (i = 0; i < thermal->chip->chn_num; i++) + rockchip_thermal_toggle_sensor(&thermal->sensors[i], true); + + platform_set_drvdata(pdev, thermal); + + return 0; + +err_disable_pclk: + clk_disable_unprepare(thermal->pclk); +err_disable_clk: + clk_disable_unprepare(thermal->clk); + + return error; +} + +static int rockchip_thermal_remove(struct platform_device *pdev) +{ + struct rockchip_thermal_data *thermal = platform_get_drvdata(pdev); + int i; + + for (i = 0; i < thermal->chip->chn_num; i++) { + struct rockchip_thermal_sensor *sensor = &thermal->sensors[i]; + + rockchip_thermal_toggle_sensor(sensor, false); + } + + thermal->chip->control(thermal->regs, false); + + clk_disable_unprepare(thermal->pclk); + clk_disable_unprepare(thermal->clk); + + return 0; +} + +static int __maybe_unused rockchip_thermal_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct rockchip_thermal_data *thermal = platform_get_drvdata(pdev); + int i; + + for (i = 0; i < thermal->chip->chn_num; i++) + rockchip_thermal_toggle_sensor(&thermal->sensors[i], false); + + thermal->chip->control(thermal->regs, false); + + clk_disable(thermal->pclk); + clk_disable(thermal->clk); + + pinctrl_pm_select_sleep_state(dev); + + return 0; +} + +static int __maybe_unused rockchip_thermal_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct rockchip_thermal_data *thermal = platform_get_drvdata(pdev); + int i; + int error; + + error = clk_enable(thermal->clk); + if (error) + return error; + + error = clk_enable(thermal->pclk); + if (error) { + clk_disable(thermal->clk); + return error; + } + + rockchip_thermal_reset_controller(thermal->reset); + + thermal->chip->initialize(thermal->grf, thermal->regs, + thermal->tshut_polarity); + + for (i = 0; i < thermal->chip->chn_num; i++) { + int id = thermal->sensors[i].id; + + thermal->chip->set_tshut_mode(id, thermal->regs, + thermal->tshut_mode); + + error = thermal->chip->set_tshut_temp(&thermal->chip->table, + id, thermal->regs, + thermal->tshut_temp); + if (error) + dev_err(&pdev->dev, "%s: invalid tshut=%d, error=%d\n", + __func__, thermal->tshut_temp, error); + } + + thermal->chip->control(thermal->regs, true); + + for (i = 0; i < thermal->chip->chn_num; i++) + rockchip_thermal_toggle_sensor(&thermal->sensors[i], true); + + pinctrl_pm_select_default_state(dev); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(rockchip_thermal_pm_ops, + rockchip_thermal_suspend, rockchip_thermal_resume); + +static struct platform_driver rockchip_thermal_driver = { + .driver = { + .name = "rockchip-thermal", + .pm = &rockchip_thermal_pm_ops, + .of_match_table = of_rockchip_thermal_match, + }, + .probe = rockchip_thermal_probe, + .remove = rockchip_thermal_remove, +}; + +module_platform_driver(rockchip_thermal_driver); + +MODULE_DESCRIPTION("ROCKCHIP THERMAL Driver"); +MODULE_AUTHOR("Rockchip, Inc."); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:rockchip-thermal"); diff --git a/drivers/thermal/samsung/Kconfig b/drivers/thermal/samsung/Kconfig new file mode 100644 index 000000000..222e64416 --- /dev/null +++ b/drivers/thermal/samsung/Kconfig @@ -0,0 +1,10 @@ +config EXYNOS_THERMAL + tristate "Exynos thermal management unit driver" + depends on THERMAL_OF + depends on HAS_IOMEM + help + If you say yes here you get support for the TMU (Thermal Management + Unit) driver for SAMSUNG EXYNOS series of SoCs. This driver initialises + the TMU, reports temperature and handles cooling action if defined. + This driver uses the Exynos core thermal APIs and TMU configuration + data from the supported SoCs. diff --git a/drivers/thermal/samsung/Makefile b/drivers/thermal/samsung/Makefile new file mode 100644 index 000000000..1e47d0d89 --- /dev/null +++ b/drivers/thermal/samsung/Makefile @@ -0,0 +1,5 @@ +# +# Samsung thermal specific Makefile +# +obj-$(CONFIG_EXYNOS_THERMAL) += exynos_thermal.o +exynos_thermal-y := exynos_tmu.o diff --git a/drivers/thermal/samsung/exynos_tmu.c b/drivers/thermal/samsung/exynos_tmu.c new file mode 100644 index 000000000..3376be686 --- /dev/null +++ b/drivers/thermal/samsung/exynos_tmu.c @@ -0,0 +1,1204 @@ +/* + * exynos_tmu.c - Samsung EXYNOS TMU (Thermal Management Unit) + * + * Copyright (C) 2014 Samsung Electronics + * Bartlomiej Zolnierkiewicz <b.zolnierkie@samsung.com> + * Lukasz Majewski <l.majewski@samsung.com> + * + * Copyright (C) 2011 Samsung Electronics + * Donggeun Kim <dg77.kim@samsung.com> + * Amit Daniel Kachhap <amit.kachhap@linaro.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include <linux/clk.h> +#include <linux/io.h> +#include <linux/interrupt.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/of_address.h> +#include <linux/of_irq.h> +#include <linux/platform_device.h> +#include <linux/regulator/consumer.h> + +#include <dt-bindings/thermal/thermal_exynos.h> + +#include "../thermal_core.h" + +/* Exynos generic registers */ +#define EXYNOS_TMU_REG_TRIMINFO 0x0 +#define EXYNOS_TMU_REG_CONTROL 0x20 +#define EXYNOS_TMU_REG_STATUS 0x28 +#define EXYNOS_TMU_REG_CURRENT_TEMP 0x40 +#define EXYNOS_TMU_REG_INTEN 0x70 +#define EXYNOS_TMU_REG_INTSTAT 0x74 +#define EXYNOS_TMU_REG_INTCLEAR 0x78 + +#define EXYNOS_TMU_TEMP_MASK 0xff +#define EXYNOS_TMU_REF_VOLTAGE_SHIFT 24 +#define EXYNOS_TMU_REF_VOLTAGE_MASK 0x1f +#define EXYNOS_TMU_BUF_SLOPE_SEL_MASK 0xf +#define EXYNOS_TMU_BUF_SLOPE_SEL_SHIFT 8 +#define EXYNOS_TMU_CORE_EN_SHIFT 0 + +/* Exynos3250 specific registers */ +#define EXYNOS_TMU_TRIMINFO_CON1 0x10 + +/* Exynos4210 specific registers */ +#define EXYNOS4210_TMU_REG_THRESHOLD_TEMP 0x44 +#define EXYNOS4210_TMU_REG_TRIG_LEVEL0 0x50 + +/* Exynos5250, Exynos4412, Exynos3250 specific registers */ +#define EXYNOS_TMU_TRIMINFO_CON2 0x14 +#define EXYNOS_THD_TEMP_RISE 0x50 +#define EXYNOS_THD_TEMP_FALL 0x54 +#define EXYNOS_EMUL_CON 0x80 + +#define EXYNOS_TRIMINFO_RELOAD_ENABLE 1 +#define EXYNOS_TRIMINFO_25_SHIFT 0 +#define EXYNOS_TRIMINFO_85_SHIFT 8 +#define EXYNOS_TMU_TRIP_MODE_SHIFT 13 +#define EXYNOS_TMU_TRIP_MODE_MASK 0x7 +#define EXYNOS_TMU_THERM_TRIP_EN_SHIFT 12 + +#define EXYNOS_TMU_INTEN_RISE0_SHIFT 0 +#define EXYNOS_TMU_INTEN_FALL0_SHIFT 16 + +#define EXYNOS_EMUL_TIME 0x57F0 +#define EXYNOS_EMUL_TIME_MASK 0xffff +#define EXYNOS_EMUL_TIME_SHIFT 16 +#define EXYNOS_EMUL_DATA_SHIFT 8 +#define EXYNOS_EMUL_DATA_MASK 0xFF +#define EXYNOS_EMUL_ENABLE 0x1 + +/* Exynos5260 specific */ +#define EXYNOS5260_TMU_REG_INTEN 0xC0 +#define EXYNOS5260_TMU_REG_INTSTAT 0xC4 +#define EXYNOS5260_TMU_REG_INTCLEAR 0xC8 +#define EXYNOS5260_EMUL_CON 0x100 + +/* Exynos4412 specific */ +#define EXYNOS4412_MUX_ADDR_VALUE 6 +#define EXYNOS4412_MUX_ADDR_SHIFT 20 + +/* Exynos5433 specific registers */ +#define EXYNOS5433_THD_TEMP_RISE3_0 0x050 +#define EXYNOS5433_THD_TEMP_RISE7_4 0x054 +#define EXYNOS5433_THD_TEMP_FALL3_0 0x060 +#define EXYNOS5433_THD_TEMP_FALL7_4 0x064 +#define EXYNOS5433_TMU_REG_INTEN 0x0c0 +#define EXYNOS5433_TMU_REG_INTPEND 0x0c8 +#define EXYNOS5433_TMU_EMUL_CON 0x110 +#define EXYNOS5433_TMU_PD_DET_EN 0x130 + +#define EXYNOS5433_TRIMINFO_SENSOR_ID_SHIFT 16 +#define EXYNOS5433_TRIMINFO_CALIB_SEL_SHIFT 23 +#define EXYNOS5433_TRIMINFO_SENSOR_ID_MASK \ + (0xf << EXYNOS5433_TRIMINFO_SENSOR_ID_SHIFT) +#define EXYNOS5433_TRIMINFO_CALIB_SEL_MASK BIT(23) + +#define EXYNOS5433_TRIMINFO_ONE_POINT_TRIMMING 0 +#define EXYNOS5433_TRIMINFO_TWO_POINT_TRIMMING 1 + +#define EXYNOS5433_PD_DET_EN 1 + +#define EXYNOS5433_G3D_BASE 0x10070000 + +/* Exynos7 specific registers */ +#define EXYNOS7_THD_TEMP_RISE7_6 0x50 +#define EXYNOS7_THD_TEMP_FALL7_6 0x60 +#define EXYNOS7_TMU_REG_INTEN 0x110 +#define EXYNOS7_TMU_REG_INTPEND 0x118 +#define EXYNOS7_TMU_REG_EMUL_CON 0x160 + +#define EXYNOS7_TMU_TEMP_MASK 0x1ff +#define EXYNOS7_PD_DET_EN_SHIFT 23 +#define EXYNOS7_TMU_INTEN_RISE0_SHIFT 0 +#define EXYNOS7_EMUL_DATA_SHIFT 7 +#define EXYNOS7_EMUL_DATA_MASK 0x1ff + +#define EXYNOS_FIRST_POINT_TRIM 25 +#define EXYNOS_SECOND_POINT_TRIM 85 + +#define EXYNOS_NOISE_CANCEL_MODE 4 + +#define MCELSIUS 1000 + +enum soc_type { + SOC_ARCH_EXYNOS3250 = 1, + SOC_ARCH_EXYNOS4210, + SOC_ARCH_EXYNOS4412, + SOC_ARCH_EXYNOS5250, + SOC_ARCH_EXYNOS5260, + SOC_ARCH_EXYNOS5420, + SOC_ARCH_EXYNOS5420_TRIMINFO, + SOC_ARCH_EXYNOS5433, + SOC_ARCH_EXYNOS7, +}; + +/** + * struct exynos_tmu_data : A structure to hold the private data of the TMU + driver + * @id: identifier of the one instance of the TMU controller. + * @base: base address of the single instance of the TMU controller. + * @base_second: base address of the common registers of the TMU controller. + * @irq: irq number of the TMU controller. + * @soc: id of the SOC type. + * @irq_work: pointer to the irq work structure. + * @lock: lock to implement synchronization. + * @clk: pointer to the clock structure. + * @clk_sec: pointer to the clock structure for accessing the base_second. + * @sclk: pointer to the clock structure for accessing the tmu special clk. + * @cal_type: calibration type for temperature + * @efuse_value: SoC defined fuse value + * @min_efuse_value: minimum valid trimming data + * @max_efuse_value: maximum valid trimming data + * @temp_error1: fused value of the first point trim. + * @temp_error2: fused value of the second point trim. + * @gain: gain of amplifier in the positive-TC generator block + * 0 < gain <= 15 + * @reference_voltage: reference voltage of amplifier + * in the positive-TC generator block + * 0 < reference_voltage <= 31 + * @regulator: pointer to the TMU regulator structure. + * @reg_conf: pointer to structure to register with core thermal. + * @ntrip: number of supported trip points. + * @enabled: current status of TMU device + * @tmu_initialize: SoC specific TMU initialization method + * @tmu_control: SoC specific TMU control method + * @tmu_read: SoC specific TMU temperature read method + * @tmu_set_emulation: SoC specific TMU emulation setting method + * @tmu_clear_irqs: SoC specific TMU interrupts clearing method + */ +struct exynos_tmu_data { + int id; + void __iomem *base; + void __iomem *base_second; + int irq; + enum soc_type soc; + struct work_struct irq_work; + struct mutex lock; + struct clk *clk, *clk_sec, *sclk; + u32 cal_type; + u32 efuse_value; + u32 min_efuse_value; + u32 max_efuse_value; + u16 temp_error1, temp_error2; + u8 gain; + u8 reference_voltage; + struct regulator *regulator; + struct thermal_zone_device *tzd; + unsigned int ntrip; + bool enabled; + + void (*tmu_set_trip_temp)(struct exynos_tmu_data *data, int trip, + u8 temp); + void (*tmu_set_trip_hyst)(struct exynos_tmu_data *data, int trip, + u8 temp, u8 hyst); + void (*tmu_initialize)(struct platform_device *pdev); + void (*tmu_control)(struct platform_device *pdev, bool on); + int (*tmu_read)(struct exynos_tmu_data *data); + void (*tmu_set_emulation)(struct exynos_tmu_data *data, int temp); + void (*tmu_clear_irqs)(struct exynos_tmu_data *data); +}; + +/* + * TMU treats temperature as a mapped temperature code. + * The temperature is converted differently depending on the calibration type. + */ +static int temp_to_code(struct exynos_tmu_data *data, u8 temp) +{ + if (data->cal_type == TYPE_ONE_POINT_TRIMMING) + return temp + data->temp_error1 - EXYNOS_FIRST_POINT_TRIM; + + return (temp - EXYNOS_FIRST_POINT_TRIM) * + (data->temp_error2 - data->temp_error1) / + (EXYNOS_SECOND_POINT_TRIM - EXYNOS_FIRST_POINT_TRIM) + + data->temp_error1; +} + +/* + * Calculate a temperature value from a temperature code. + * The unit of the temperature is degree Celsius. + */ +static int code_to_temp(struct exynos_tmu_data *data, u16 temp_code) +{ + if (data->cal_type == TYPE_ONE_POINT_TRIMMING) + return temp_code - data->temp_error1 + EXYNOS_FIRST_POINT_TRIM; + + return (temp_code - data->temp_error1) * + (EXYNOS_SECOND_POINT_TRIM - EXYNOS_FIRST_POINT_TRIM) / + (data->temp_error2 - data->temp_error1) + + EXYNOS_FIRST_POINT_TRIM; +} + +static void sanitize_temp_error(struct exynos_tmu_data *data, u32 trim_info) +{ + u16 tmu_temp_mask = + (data->soc == SOC_ARCH_EXYNOS7) ? EXYNOS7_TMU_TEMP_MASK + : EXYNOS_TMU_TEMP_MASK; + + data->temp_error1 = trim_info & tmu_temp_mask; + data->temp_error2 = ((trim_info >> EXYNOS_TRIMINFO_85_SHIFT) & + EXYNOS_TMU_TEMP_MASK); + + if (!data->temp_error1 || + (data->min_efuse_value > data->temp_error1) || + (data->temp_error1 > data->max_efuse_value)) + data->temp_error1 = data->efuse_value & EXYNOS_TMU_TEMP_MASK; + + if (!data->temp_error2) + data->temp_error2 = + (data->efuse_value >> EXYNOS_TRIMINFO_85_SHIFT) & + EXYNOS_TMU_TEMP_MASK; +} + +static int exynos_tmu_initialize(struct platform_device *pdev) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + struct thermal_zone_device *tzd = data->tzd; + const struct thermal_trip * const trips = + of_thermal_get_trip_points(tzd); + unsigned int status; + int ret = 0, temp, hyst; + + if (!trips) { + dev_err(&pdev->dev, + "Cannot get trip points from device tree!\n"); + return -ENODEV; + } + + if (data->soc != SOC_ARCH_EXYNOS5433) /* FIXME */ + ret = tzd->ops->get_crit_temp(tzd, &temp); + if (ret) { + dev_err(&pdev->dev, + "No CRITICAL trip point defined in device tree!\n"); + goto out; + } + + if (of_thermal_get_ntrips(tzd) > data->ntrip) { + dev_info(&pdev->dev, + "More trip points than supported by this TMU.\n"); + dev_info(&pdev->dev, + "%d trip points should be configured in polling mode.\n", + (of_thermal_get_ntrips(tzd) - data->ntrip)); + } + + mutex_lock(&data->lock); + clk_enable(data->clk); + if (!IS_ERR(data->clk_sec)) + clk_enable(data->clk_sec); + + status = readb(data->base + EXYNOS_TMU_REG_STATUS); + if (!status) { + ret = -EBUSY; + } else { + int i, ntrips = + min_t(int, of_thermal_get_ntrips(tzd), data->ntrip); + + data->tmu_initialize(pdev); + + /* Write temperature code for rising and falling threshold */ + for (i = 0; i < ntrips; i++) { + /* Write temperature code for rising threshold */ + ret = tzd->ops->get_trip_temp(tzd, i, &temp); + if (ret) + goto err; + temp /= MCELSIUS; + data->tmu_set_trip_temp(data, i, temp); + + /* Write temperature code for falling threshold */ + ret = tzd->ops->get_trip_hyst(tzd, i, &hyst); + if (ret) + goto err; + hyst /= MCELSIUS; + data->tmu_set_trip_hyst(data, i, temp, hyst); + } + + data->tmu_clear_irqs(data); + } +err: + clk_disable(data->clk); + mutex_unlock(&data->lock); + if (!IS_ERR(data->clk_sec)) + clk_disable(data->clk_sec); +out: + return ret; +} + +static u32 get_con_reg(struct exynos_tmu_data *data, u32 con) +{ + if (data->soc == SOC_ARCH_EXYNOS4412 || + data->soc == SOC_ARCH_EXYNOS3250) + con |= (EXYNOS4412_MUX_ADDR_VALUE << EXYNOS4412_MUX_ADDR_SHIFT); + + con &= ~(EXYNOS_TMU_REF_VOLTAGE_MASK << EXYNOS_TMU_REF_VOLTAGE_SHIFT); + con |= data->reference_voltage << EXYNOS_TMU_REF_VOLTAGE_SHIFT; + + con &= ~(EXYNOS_TMU_BUF_SLOPE_SEL_MASK << EXYNOS_TMU_BUF_SLOPE_SEL_SHIFT); + con |= (data->gain << EXYNOS_TMU_BUF_SLOPE_SEL_SHIFT); + + con &= ~(EXYNOS_TMU_TRIP_MODE_MASK << EXYNOS_TMU_TRIP_MODE_SHIFT); + con |= (EXYNOS_NOISE_CANCEL_MODE << EXYNOS_TMU_TRIP_MODE_SHIFT); + + return con; +} + +static void exynos_tmu_control(struct platform_device *pdev, bool on) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + + mutex_lock(&data->lock); + clk_enable(data->clk); + data->tmu_control(pdev, on); + data->enabled = on; + clk_disable(data->clk); + mutex_unlock(&data->lock); +} + +static void exynos4210_tmu_set_trip_temp(struct exynos_tmu_data *data, + int trip, u8 temp) +{ + const struct thermal_trip * const trips = + of_thermal_get_trip_points(data->tzd); + u8 ref, th_code; + + ref = trips[0].temperature / MCELSIUS; + + if (trip == 0) { + th_code = temp_to_code(data, ref); + writeb(th_code, data->base + EXYNOS4210_TMU_REG_THRESHOLD_TEMP); + } + + temp -= ref; + writeb(temp, data->base + EXYNOS4210_TMU_REG_TRIG_LEVEL0 + trip * 4); +} + +/* failing thresholds are not supported on Exynos4210 */ +static void exynos4210_tmu_set_trip_hyst(struct exynos_tmu_data *data, + int trip, u8 temp, u8 hyst) +{ +} + +static void exynos4210_tmu_initialize(struct platform_device *pdev) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + + sanitize_temp_error(data, readl(data->base + EXYNOS_TMU_REG_TRIMINFO)); +} + +static void exynos4412_tmu_set_trip_temp(struct exynos_tmu_data *data, + int trip, u8 temp) +{ + u32 th, con; + + th = readl(data->base + EXYNOS_THD_TEMP_RISE); + th &= ~(0xff << 8 * trip); + th |= temp_to_code(data, temp) << 8 * trip; + writel(th, data->base + EXYNOS_THD_TEMP_RISE); + + if (trip == 3) { + con = readl(data->base + EXYNOS_TMU_REG_CONTROL); + con |= (1 << EXYNOS_TMU_THERM_TRIP_EN_SHIFT); + writel(con, data->base + EXYNOS_TMU_REG_CONTROL); + } +} + +static void exynos4412_tmu_set_trip_hyst(struct exynos_tmu_data *data, + int trip, u8 temp, u8 hyst) +{ + u32 th; + + th = readl(data->base + EXYNOS_THD_TEMP_FALL); + th &= ~(0xff << 8 * trip); + if (hyst) + th |= temp_to_code(data, temp - hyst) << 8 * trip; + writel(th, data->base + EXYNOS_THD_TEMP_FALL); +} + +static void exynos4412_tmu_initialize(struct platform_device *pdev) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + unsigned int trim_info, ctrl; + + if (data->soc == SOC_ARCH_EXYNOS3250 || + data->soc == SOC_ARCH_EXYNOS4412 || + data->soc == SOC_ARCH_EXYNOS5250) { + if (data->soc == SOC_ARCH_EXYNOS3250) { + ctrl = readl(data->base + EXYNOS_TMU_TRIMINFO_CON1); + ctrl |= EXYNOS_TRIMINFO_RELOAD_ENABLE; + writel(ctrl, data->base + EXYNOS_TMU_TRIMINFO_CON1); + } + ctrl = readl(data->base + EXYNOS_TMU_TRIMINFO_CON2); + ctrl |= EXYNOS_TRIMINFO_RELOAD_ENABLE; + writel(ctrl, data->base + EXYNOS_TMU_TRIMINFO_CON2); + } + + /* On exynos5420 the triminfo register is in the shared space */ + if (data->soc == SOC_ARCH_EXYNOS5420_TRIMINFO) + trim_info = readl(data->base_second + EXYNOS_TMU_REG_TRIMINFO); + else + trim_info = readl(data->base + EXYNOS_TMU_REG_TRIMINFO); + + sanitize_temp_error(data, trim_info); +} + +static void exynos5433_tmu_set_trip_temp(struct exynos_tmu_data *data, + int trip, u8 temp) +{ + unsigned int reg_off, j; + u32 th; + + if (trip > 3) { + reg_off = EXYNOS5433_THD_TEMP_RISE7_4; + j = trip - 4; + } else { + reg_off = EXYNOS5433_THD_TEMP_RISE3_0; + j = trip; + } + + th = readl(data->base + reg_off); + th &= ~(0xff << j * 8); + th |= (temp_to_code(data, temp) << j * 8); + writel(th, data->base + reg_off); +} + +static void exynos5433_tmu_set_trip_hyst(struct exynos_tmu_data *data, + int trip, u8 temp, u8 hyst) +{ + unsigned int reg_off, j; + u32 th; + + if (trip > 3) { + reg_off = EXYNOS5433_THD_TEMP_FALL7_4; + j = trip - 4; + } else { + reg_off = EXYNOS5433_THD_TEMP_FALL3_0; + j = trip; + } + + th = readl(data->base + reg_off); + th &= ~(0xff << j * 8); + th |= (temp_to_code(data, temp - hyst) << j * 8); + writel(th, data->base + reg_off); +} + +static void exynos5433_tmu_initialize(struct platform_device *pdev) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + unsigned int trim_info; + int sensor_id, cal_type; + + trim_info = readl(data->base + EXYNOS_TMU_REG_TRIMINFO); + sanitize_temp_error(data, trim_info); + + /* Read the temperature sensor id */ + sensor_id = (trim_info & EXYNOS5433_TRIMINFO_SENSOR_ID_MASK) + >> EXYNOS5433_TRIMINFO_SENSOR_ID_SHIFT; + dev_info(&pdev->dev, "Temperature sensor ID: 0x%x\n", sensor_id); + + /* Read the calibration mode */ + writel(trim_info, data->base + EXYNOS_TMU_REG_TRIMINFO); + cal_type = (trim_info & EXYNOS5433_TRIMINFO_CALIB_SEL_MASK) + >> EXYNOS5433_TRIMINFO_CALIB_SEL_SHIFT; + + switch (cal_type) { + case EXYNOS5433_TRIMINFO_TWO_POINT_TRIMMING: + data->cal_type = TYPE_TWO_POINT_TRIMMING; + break; + case EXYNOS5433_TRIMINFO_ONE_POINT_TRIMMING: + default: + data->cal_type = TYPE_ONE_POINT_TRIMMING; + break; + } + + dev_info(&pdev->dev, "Calibration type is %d-point calibration\n", + cal_type ? 2 : 1); +} + +static void exynos7_tmu_set_trip_temp(struct exynos_tmu_data *data, + int trip, u8 temp) +{ + unsigned int reg_off, bit_off; + u32 th; + + reg_off = ((7 - trip) / 2) * 4; + bit_off = ((8 - trip) % 2); + + th = readl(data->base + EXYNOS7_THD_TEMP_RISE7_6 + reg_off); + th &= ~(EXYNOS7_TMU_TEMP_MASK << (16 * bit_off)); + th |= temp_to_code(data, temp) << (16 * bit_off); + writel(th, data->base + EXYNOS7_THD_TEMP_RISE7_6 + reg_off); +} + +static void exynos7_tmu_set_trip_hyst(struct exynos_tmu_data *data, + int trip, u8 temp, u8 hyst) +{ + unsigned int reg_off, bit_off; + u32 th; + + reg_off = ((7 - trip) / 2) * 4; + bit_off = ((8 - trip) % 2); + + th = readl(data->base + EXYNOS7_THD_TEMP_FALL7_6 + reg_off); + th &= ~(EXYNOS7_TMU_TEMP_MASK << (16 * bit_off)); + th |= temp_to_code(data, temp - hyst) << (16 * bit_off); + writel(th, data->base + EXYNOS7_THD_TEMP_FALL7_6 + reg_off); +} + +static void exynos7_tmu_initialize(struct platform_device *pdev) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + unsigned int trim_info; + + trim_info = readl(data->base + EXYNOS_TMU_REG_TRIMINFO); + sanitize_temp_error(data, trim_info); +} + +static void exynos4210_tmu_control(struct platform_device *pdev, bool on) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + struct thermal_zone_device *tz = data->tzd; + unsigned int con, interrupt_en = 0, i; + + con = get_con_reg(data, readl(data->base + EXYNOS_TMU_REG_CONTROL)); + + if (on) { + for (i = 0; i < data->ntrip; i++) { + if (!of_thermal_is_trip_valid(tz, i)) + continue; + + interrupt_en |= + (1 << (EXYNOS_TMU_INTEN_RISE0_SHIFT + i * 4)); + } + + if (data->soc != SOC_ARCH_EXYNOS4210) + interrupt_en |= + interrupt_en << EXYNOS_TMU_INTEN_FALL0_SHIFT; + + con |= (1 << EXYNOS_TMU_CORE_EN_SHIFT); + } else { + con &= ~(1 << EXYNOS_TMU_CORE_EN_SHIFT); + } + + writel(interrupt_en, data->base + EXYNOS_TMU_REG_INTEN); + writel(con, data->base + EXYNOS_TMU_REG_CONTROL); +} + +static void exynos5433_tmu_control(struct platform_device *pdev, bool on) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + struct thermal_zone_device *tz = data->tzd; + unsigned int con, interrupt_en = 0, pd_det_en, i; + + con = get_con_reg(data, readl(data->base + EXYNOS_TMU_REG_CONTROL)); + + if (on) { + for (i = 0; i < data->ntrip; i++) { + if (!of_thermal_is_trip_valid(tz, i)) + continue; + + interrupt_en |= + (1 << (EXYNOS7_TMU_INTEN_RISE0_SHIFT + i)); + } + + interrupt_en |= + interrupt_en << EXYNOS_TMU_INTEN_FALL0_SHIFT; + + con |= (1 << EXYNOS_TMU_CORE_EN_SHIFT); + } else + con &= ~(1 << EXYNOS_TMU_CORE_EN_SHIFT); + + pd_det_en = on ? EXYNOS5433_PD_DET_EN : 0; + + writel(pd_det_en, data->base + EXYNOS5433_TMU_PD_DET_EN); + writel(interrupt_en, data->base + EXYNOS5433_TMU_REG_INTEN); + writel(con, data->base + EXYNOS_TMU_REG_CONTROL); +} + +static void exynos7_tmu_control(struct platform_device *pdev, bool on) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + struct thermal_zone_device *tz = data->tzd; + unsigned int con, interrupt_en = 0, i; + + con = get_con_reg(data, readl(data->base + EXYNOS_TMU_REG_CONTROL)); + + if (on) { + for (i = 0; i < data->ntrip; i++) { + if (!of_thermal_is_trip_valid(tz, i)) + continue; + + interrupt_en |= + (1 << (EXYNOS7_TMU_INTEN_RISE0_SHIFT + i)); + } + + interrupt_en |= + interrupt_en << EXYNOS_TMU_INTEN_FALL0_SHIFT; + + con |= (1 << EXYNOS_TMU_CORE_EN_SHIFT); + con |= (1 << EXYNOS7_PD_DET_EN_SHIFT); + } else { + con &= ~(1 << EXYNOS_TMU_CORE_EN_SHIFT); + con &= ~(1 << EXYNOS7_PD_DET_EN_SHIFT); + } + + writel(interrupt_en, data->base + EXYNOS7_TMU_REG_INTEN); + writel(con, data->base + EXYNOS_TMU_REG_CONTROL); +} + +static int exynos_get_temp(void *p, int *temp) +{ + struct exynos_tmu_data *data = p; + int value, ret = 0; + + if (!data || !data->tmu_read) + return -EINVAL; + else if (!data->enabled) + /* + * Called too early, probably + * from thermal_zone_of_sensor_register(). + */ + return -EAGAIN; + + mutex_lock(&data->lock); + clk_enable(data->clk); + + value = data->tmu_read(data); + if (value < 0) + ret = value; + else + *temp = code_to_temp(data, value) * MCELSIUS; + + clk_disable(data->clk); + mutex_unlock(&data->lock); + + return ret; +} + +#ifdef CONFIG_THERMAL_EMULATION +static u32 get_emul_con_reg(struct exynos_tmu_data *data, unsigned int val, + int temp) +{ + if (temp) { + temp /= MCELSIUS; + + val &= ~(EXYNOS_EMUL_TIME_MASK << EXYNOS_EMUL_TIME_SHIFT); + val |= (EXYNOS_EMUL_TIME << EXYNOS_EMUL_TIME_SHIFT); + if (data->soc == SOC_ARCH_EXYNOS7) { + val &= ~(EXYNOS7_EMUL_DATA_MASK << + EXYNOS7_EMUL_DATA_SHIFT); + val |= (temp_to_code(data, temp) << + EXYNOS7_EMUL_DATA_SHIFT) | + EXYNOS_EMUL_ENABLE; + } else { + val &= ~(EXYNOS_EMUL_DATA_MASK << + EXYNOS_EMUL_DATA_SHIFT); + val |= (temp_to_code(data, temp) << + EXYNOS_EMUL_DATA_SHIFT) | + EXYNOS_EMUL_ENABLE; + } + } else { + val &= ~EXYNOS_EMUL_ENABLE; + } + + return val; +} + +static void exynos4412_tmu_set_emulation(struct exynos_tmu_data *data, + int temp) +{ + unsigned int val; + u32 emul_con; + + if (data->soc == SOC_ARCH_EXYNOS5260) + emul_con = EXYNOS5260_EMUL_CON; + else if (data->soc == SOC_ARCH_EXYNOS5433) + emul_con = EXYNOS5433_TMU_EMUL_CON; + else if (data->soc == SOC_ARCH_EXYNOS7) + emul_con = EXYNOS7_TMU_REG_EMUL_CON; + else + emul_con = EXYNOS_EMUL_CON; + + val = readl(data->base + emul_con); + val = get_emul_con_reg(data, val, temp); + writel(val, data->base + emul_con); +} + +static int exynos_tmu_set_emulation(void *drv_data, int temp) +{ + struct exynos_tmu_data *data = drv_data; + int ret = -EINVAL; + + if (data->soc == SOC_ARCH_EXYNOS4210) + goto out; + + if (temp && temp < MCELSIUS) + goto out; + + mutex_lock(&data->lock); + clk_enable(data->clk); + data->tmu_set_emulation(data, temp); + clk_disable(data->clk); + mutex_unlock(&data->lock); + return 0; +out: + return ret; +} +#else +#define exynos4412_tmu_set_emulation NULL +static int exynos_tmu_set_emulation(void *drv_data, int temp) + { return -EINVAL; } +#endif /* CONFIG_THERMAL_EMULATION */ + +static int exynos4210_tmu_read(struct exynos_tmu_data *data) +{ + int ret = readb(data->base + EXYNOS_TMU_REG_CURRENT_TEMP); + + /* "temp_code" should range between 75 and 175 */ + return (ret < 75 || ret > 175) ? -ENODATA : ret; +} + +static int exynos4412_tmu_read(struct exynos_tmu_data *data) +{ + return readb(data->base + EXYNOS_TMU_REG_CURRENT_TEMP); +} + +static int exynos7_tmu_read(struct exynos_tmu_data *data) +{ + return readw(data->base + EXYNOS_TMU_REG_CURRENT_TEMP) & + EXYNOS7_TMU_TEMP_MASK; +} + +static void exynos_tmu_work(struct work_struct *work) +{ + struct exynos_tmu_data *data = container_of(work, + struct exynos_tmu_data, irq_work); + + thermal_zone_device_update(data->tzd, THERMAL_EVENT_UNSPECIFIED); + + mutex_lock(&data->lock); + clk_enable(data->clk); + + /* TODO: take action based on particular interrupt */ + data->tmu_clear_irqs(data); + + clk_disable(data->clk); + mutex_unlock(&data->lock); + enable_irq(data->irq); +} + +static void exynos4210_tmu_clear_irqs(struct exynos_tmu_data *data) +{ + unsigned int val_irq; + u32 tmu_intstat, tmu_intclear; + + if (data->soc == SOC_ARCH_EXYNOS5260) { + tmu_intstat = EXYNOS5260_TMU_REG_INTSTAT; + tmu_intclear = EXYNOS5260_TMU_REG_INTCLEAR; + } else if (data->soc == SOC_ARCH_EXYNOS7) { + tmu_intstat = EXYNOS7_TMU_REG_INTPEND; + tmu_intclear = EXYNOS7_TMU_REG_INTPEND; + } else if (data->soc == SOC_ARCH_EXYNOS5433) { + tmu_intstat = EXYNOS5433_TMU_REG_INTPEND; + tmu_intclear = EXYNOS5433_TMU_REG_INTPEND; + } else { + tmu_intstat = EXYNOS_TMU_REG_INTSTAT; + tmu_intclear = EXYNOS_TMU_REG_INTCLEAR; + } + + val_irq = readl(data->base + tmu_intstat); + /* + * Clear the interrupts. Please note that the documentation for + * Exynos3250, Exynos4412, Exynos5250 and Exynos5260 incorrectly + * states that INTCLEAR register has a different placing of bits + * responsible for FALL IRQs than INTSTAT register. Exynos5420 + * and Exynos5440 documentation is correct (Exynos4210 doesn't + * support FALL IRQs at all). + */ + writel(val_irq, data->base + tmu_intclear); +} + +static irqreturn_t exynos_tmu_irq(int irq, void *id) +{ + struct exynos_tmu_data *data = id; + + disable_irq_nosync(irq); + schedule_work(&data->irq_work); + + return IRQ_HANDLED; +} + +static const struct of_device_id exynos_tmu_match[] = { + { + .compatible = "samsung,exynos3250-tmu", + .data = (const void *)SOC_ARCH_EXYNOS3250, + }, { + .compatible = "samsung,exynos4210-tmu", + .data = (const void *)SOC_ARCH_EXYNOS4210, + }, { + .compatible = "samsung,exynos4412-tmu", + .data = (const void *)SOC_ARCH_EXYNOS4412, + }, { + .compatible = "samsung,exynos5250-tmu", + .data = (const void *)SOC_ARCH_EXYNOS5250, + }, { + .compatible = "samsung,exynos5260-tmu", + .data = (const void *)SOC_ARCH_EXYNOS5260, + }, { + .compatible = "samsung,exynos5420-tmu", + .data = (const void *)SOC_ARCH_EXYNOS5420, + }, { + .compatible = "samsung,exynos5420-tmu-ext-triminfo", + .data = (const void *)SOC_ARCH_EXYNOS5420_TRIMINFO, + }, { + .compatible = "samsung,exynos5433-tmu", + .data = (const void *)SOC_ARCH_EXYNOS5433, + }, { + .compatible = "samsung,exynos7-tmu", + .data = (const void *)SOC_ARCH_EXYNOS7, + }, + { }, +}; +MODULE_DEVICE_TABLE(of, exynos_tmu_match); + +static int exynos_map_dt_data(struct platform_device *pdev) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + struct resource res; + + if (!data || !pdev->dev.of_node) + return -ENODEV; + + data->id = of_alias_get_id(pdev->dev.of_node, "tmuctrl"); + if (data->id < 0) + data->id = 0; + + data->irq = irq_of_parse_and_map(pdev->dev.of_node, 0); + if (data->irq <= 0) { + dev_err(&pdev->dev, "failed to get IRQ\n"); + return -ENODEV; + } + + if (of_address_to_resource(pdev->dev.of_node, 0, &res)) { + dev_err(&pdev->dev, "failed to get Resource 0\n"); + return -ENODEV; + } + + data->base = devm_ioremap(&pdev->dev, res.start, resource_size(&res)); + if (!data->base) { + dev_err(&pdev->dev, "Failed to ioremap memory\n"); + return -EADDRNOTAVAIL; + } + + data->soc = (enum soc_type)of_device_get_match_data(&pdev->dev); + + switch (data->soc) { + case SOC_ARCH_EXYNOS4210: + data->tmu_set_trip_temp = exynos4210_tmu_set_trip_temp; + data->tmu_set_trip_hyst = exynos4210_tmu_set_trip_hyst; + data->tmu_initialize = exynos4210_tmu_initialize; + data->tmu_control = exynos4210_tmu_control; + data->tmu_read = exynos4210_tmu_read; + data->tmu_clear_irqs = exynos4210_tmu_clear_irqs; + data->ntrip = 4; + data->gain = 15; + data->reference_voltage = 7; + data->efuse_value = 55; + data->min_efuse_value = 40; + data->max_efuse_value = 100; + break; + case SOC_ARCH_EXYNOS3250: + case SOC_ARCH_EXYNOS4412: + case SOC_ARCH_EXYNOS5250: + case SOC_ARCH_EXYNOS5260: + case SOC_ARCH_EXYNOS5420: + case SOC_ARCH_EXYNOS5420_TRIMINFO: + data->tmu_set_trip_temp = exynos4412_tmu_set_trip_temp; + data->tmu_set_trip_hyst = exynos4412_tmu_set_trip_hyst; + data->tmu_initialize = exynos4412_tmu_initialize; + data->tmu_control = exynos4210_tmu_control; + data->tmu_read = exynos4412_tmu_read; + data->tmu_set_emulation = exynos4412_tmu_set_emulation; + data->tmu_clear_irqs = exynos4210_tmu_clear_irqs; + data->ntrip = 4; + data->gain = 8; + data->reference_voltage = 16; + data->efuse_value = 55; + if (data->soc != SOC_ARCH_EXYNOS5420 && + data->soc != SOC_ARCH_EXYNOS5420_TRIMINFO) + data->min_efuse_value = 40; + else + data->min_efuse_value = 0; + data->max_efuse_value = 100; + break; + case SOC_ARCH_EXYNOS5433: + data->tmu_set_trip_temp = exynos5433_tmu_set_trip_temp; + data->tmu_set_trip_hyst = exynos5433_tmu_set_trip_hyst; + data->tmu_initialize = exynos5433_tmu_initialize; + data->tmu_control = exynos5433_tmu_control; + data->tmu_read = exynos4412_tmu_read; + data->tmu_set_emulation = exynos4412_tmu_set_emulation; + data->tmu_clear_irqs = exynos4210_tmu_clear_irqs; + data->ntrip = 8; + data->gain = 8; + if (res.start == EXYNOS5433_G3D_BASE) + data->reference_voltage = 23; + else + data->reference_voltage = 16; + data->efuse_value = 75; + data->min_efuse_value = 40; + data->max_efuse_value = 150; + break; + case SOC_ARCH_EXYNOS7: + data->tmu_set_trip_temp = exynos7_tmu_set_trip_temp; + data->tmu_set_trip_hyst = exynos7_tmu_set_trip_hyst; + data->tmu_initialize = exynos7_tmu_initialize; + data->tmu_control = exynos7_tmu_control; + data->tmu_read = exynos7_tmu_read; + data->tmu_set_emulation = exynos4412_tmu_set_emulation; + data->tmu_clear_irqs = exynos4210_tmu_clear_irqs; + data->ntrip = 8; + data->gain = 9; + data->reference_voltage = 17; + data->efuse_value = 75; + data->min_efuse_value = 15; + data->max_efuse_value = 100; + break; + default: + dev_err(&pdev->dev, "Platform not supported\n"); + return -EINVAL; + } + + data->cal_type = TYPE_ONE_POINT_TRIMMING; + + /* + * Check if the TMU shares some registers and then try to map the + * memory of common registers. + */ + if (data->soc != SOC_ARCH_EXYNOS5420_TRIMINFO) + return 0; + + if (of_address_to_resource(pdev->dev.of_node, 1, &res)) { + dev_err(&pdev->dev, "failed to get Resource 1\n"); + return -ENODEV; + } + + data->base_second = devm_ioremap(&pdev->dev, res.start, + resource_size(&res)); + if (!data->base_second) { + dev_err(&pdev->dev, "Failed to ioremap memory\n"); + return -ENOMEM; + } + + return 0; +} + +static const struct thermal_zone_of_device_ops exynos_sensor_ops = { + .get_temp = exynos_get_temp, + .set_emul_temp = exynos_tmu_set_emulation, +}; + +static int exynos_tmu_probe(struct platform_device *pdev) +{ + struct exynos_tmu_data *data; + int ret; + + data = devm_kzalloc(&pdev->dev, sizeof(struct exynos_tmu_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + platform_set_drvdata(pdev, data); + mutex_init(&data->lock); + + /* + * Try enabling the regulator if found + * TODO: Add regulator as an SOC feature, so that regulator enable + * is a compulsory call. + */ + data->regulator = devm_regulator_get_optional(&pdev->dev, "vtmu"); + if (!IS_ERR(data->regulator)) { + ret = regulator_enable(data->regulator); + if (ret) { + dev_err(&pdev->dev, "failed to enable vtmu\n"); + return ret; + } + } else { + if (PTR_ERR(data->regulator) == -EPROBE_DEFER) + return -EPROBE_DEFER; + dev_info(&pdev->dev, "Regulator node (vtmu) not found\n"); + } + + ret = exynos_map_dt_data(pdev); + if (ret) + goto err_sensor; + + INIT_WORK(&data->irq_work, exynos_tmu_work); + + data->clk = devm_clk_get(&pdev->dev, "tmu_apbif"); + if (IS_ERR(data->clk)) { + dev_err(&pdev->dev, "Failed to get clock\n"); + ret = PTR_ERR(data->clk); + goto err_sensor; + } + + data->clk_sec = devm_clk_get(&pdev->dev, "tmu_triminfo_apbif"); + if (IS_ERR(data->clk_sec)) { + if (data->soc == SOC_ARCH_EXYNOS5420_TRIMINFO) { + dev_err(&pdev->dev, "Failed to get triminfo clock\n"); + ret = PTR_ERR(data->clk_sec); + goto err_sensor; + } + } else { + ret = clk_prepare(data->clk_sec); + if (ret) { + dev_err(&pdev->dev, "Failed to get clock\n"); + goto err_sensor; + } + } + + ret = clk_prepare(data->clk); + if (ret) { + dev_err(&pdev->dev, "Failed to get clock\n"); + goto err_clk_sec; + } + + switch (data->soc) { + case SOC_ARCH_EXYNOS5433: + case SOC_ARCH_EXYNOS7: + data->sclk = devm_clk_get(&pdev->dev, "tmu_sclk"); + if (IS_ERR(data->sclk)) { + dev_err(&pdev->dev, "Failed to get sclk\n"); + ret = PTR_ERR(data->sclk); + goto err_clk; + } else { + ret = clk_prepare_enable(data->sclk); + if (ret) { + dev_err(&pdev->dev, "Failed to enable sclk\n"); + goto err_clk; + } + } + break; + default: + break; + } + + /* + * data->tzd must be registered before calling exynos_tmu_initialize(), + * requesting irq and calling exynos_tmu_control(). + */ + data->tzd = thermal_zone_of_sensor_register(&pdev->dev, 0, data, + &exynos_sensor_ops); + if (IS_ERR(data->tzd)) { + ret = PTR_ERR(data->tzd); + dev_err(&pdev->dev, "Failed to register sensor: %d\n", ret); + goto err_sclk; + } + + ret = exynos_tmu_initialize(pdev); + if (ret) { + dev_err(&pdev->dev, "Failed to initialize TMU\n"); + goto err_thermal; + } + + ret = devm_request_irq(&pdev->dev, data->irq, exynos_tmu_irq, + IRQF_TRIGGER_RISING | IRQF_SHARED, dev_name(&pdev->dev), data); + if (ret) { + dev_err(&pdev->dev, "Failed to request irq: %d\n", data->irq); + goto err_thermal; + } + + exynos_tmu_control(pdev, true); + return 0; + +err_thermal: + thermal_zone_of_sensor_unregister(&pdev->dev, data->tzd); +err_sclk: + clk_disable_unprepare(data->sclk); +err_clk: + clk_unprepare(data->clk); +err_clk_sec: + if (!IS_ERR(data->clk_sec)) + clk_unprepare(data->clk_sec); +err_sensor: + if (!IS_ERR(data->regulator)) + regulator_disable(data->regulator); + + return ret; +} + +static int exynos_tmu_remove(struct platform_device *pdev) +{ + struct exynos_tmu_data *data = platform_get_drvdata(pdev); + struct thermal_zone_device *tzd = data->tzd; + + thermal_zone_of_sensor_unregister(&pdev->dev, tzd); + exynos_tmu_control(pdev, false); + + clk_disable_unprepare(data->sclk); + clk_unprepare(data->clk); + if (!IS_ERR(data->clk_sec)) + clk_unprepare(data->clk_sec); + + if (!IS_ERR(data->regulator)) + regulator_disable(data->regulator); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int exynos_tmu_suspend(struct device *dev) +{ + exynos_tmu_control(to_platform_device(dev), false); + + return 0; +} + +static int exynos_tmu_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + + exynos_tmu_initialize(pdev); + exynos_tmu_control(pdev, true); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(exynos_tmu_pm, + exynos_tmu_suspend, exynos_tmu_resume); +#define EXYNOS_TMU_PM (&exynos_tmu_pm) +#else +#define EXYNOS_TMU_PM NULL +#endif + +static struct platform_driver exynos_tmu_driver = { + .driver = { + .name = "exynos-tmu", + .pm = EXYNOS_TMU_PM, + .of_match_table = exynos_tmu_match, + }, + .probe = exynos_tmu_probe, + .remove = exynos_tmu_remove, +}; + +module_platform_driver(exynos_tmu_driver); + +MODULE_DESCRIPTION("EXYNOS TMU Driver"); +MODULE_AUTHOR("Donggeun Kim <dg77.kim@samsung.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:exynos-tmu"); diff --git a/drivers/thermal/spear_thermal.c b/drivers/thermal/spear_thermal.c new file mode 100644 index 000000000..81b35aace --- /dev/null +++ b/drivers/thermal/spear_thermal.c @@ -0,0 +1,196 @@ +/* + * SPEAr thermal driver. + * + * Copyright (C) 2011-2012 ST Microelectronics + * Author: Vincenzo Frascino <vincenzo.frascino@st.com> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/clk.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/of.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/thermal.h> + +#define MD_FACTOR 1000 + +/* SPEAr Thermal Sensor Dev Structure */ +struct spear_thermal_dev { + /* pointer to base address of the thermal sensor */ + void __iomem *thermal_base; + /* clk structure */ + struct clk *clk; + /* pointer to thermal flags */ + unsigned int flags; +}; + +static inline int thermal_get_temp(struct thermal_zone_device *thermal, + int *temp) +{ + struct spear_thermal_dev *stdev = thermal->devdata; + + /* + * Data are ready to be read after 628 usec from POWERDOWN signal + * (PDN) = 1 + */ + *temp = (readl_relaxed(stdev->thermal_base) & 0x7F) * MD_FACTOR; + return 0; +} + +static struct thermal_zone_device_ops ops = { + .get_temp = thermal_get_temp, +}; + +static int __maybe_unused spear_thermal_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct thermal_zone_device *spear_thermal = platform_get_drvdata(pdev); + struct spear_thermal_dev *stdev = spear_thermal->devdata; + unsigned int actual_mask = 0; + + /* Disable SPEAr Thermal Sensor */ + actual_mask = readl_relaxed(stdev->thermal_base); + writel_relaxed(actual_mask & ~stdev->flags, stdev->thermal_base); + + clk_disable(stdev->clk); + dev_info(dev, "Suspended.\n"); + + return 0; +} + +static int __maybe_unused spear_thermal_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct thermal_zone_device *spear_thermal = platform_get_drvdata(pdev); + struct spear_thermal_dev *stdev = spear_thermal->devdata; + unsigned int actual_mask = 0; + int ret = 0; + + ret = clk_enable(stdev->clk); + if (ret) { + dev_err(&pdev->dev, "Can't enable clock\n"); + return ret; + } + + /* Enable SPEAr Thermal Sensor */ + actual_mask = readl_relaxed(stdev->thermal_base); + writel_relaxed(actual_mask | stdev->flags, stdev->thermal_base); + + dev_info(dev, "Resumed.\n"); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(spear_thermal_pm_ops, spear_thermal_suspend, + spear_thermal_resume); + +static int spear_thermal_probe(struct platform_device *pdev) +{ + struct thermal_zone_device *spear_thermal = NULL; + struct spear_thermal_dev *stdev; + struct device_node *np = pdev->dev.of_node; + struct resource *res; + int ret = 0, val; + + if (!np || !of_property_read_u32(np, "st,thermal-flags", &val)) { + dev_err(&pdev->dev, "Failed: DT Pdata not passed\n"); + return -EINVAL; + } + + stdev = devm_kzalloc(&pdev->dev, sizeof(*stdev), GFP_KERNEL); + if (!stdev) + return -ENOMEM; + + /* Enable thermal sensor */ + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + stdev->thermal_base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(stdev->thermal_base)) + return PTR_ERR(stdev->thermal_base); + + stdev->clk = devm_clk_get(&pdev->dev, NULL); + if (IS_ERR(stdev->clk)) { + dev_err(&pdev->dev, "Can't get clock\n"); + return PTR_ERR(stdev->clk); + } + + ret = clk_enable(stdev->clk); + if (ret) { + dev_err(&pdev->dev, "Can't enable clock\n"); + return ret; + } + + stdev->flags = val; + writel_relaxed(stdev->flags, stdev->thermal_base); + + spear_thermal = thermal_zone_device_register("spear_thermal", 0, 0, + stdev, &ops, NULL, 0, 0); + if (IS_ERR(spear_thermal)) { + dev_err(&pdev->dev, "thermal zone device is NULL\n"); + ret = PTR_ERR(spear_thermal); + goto disable_clk; + } + + platform_set_drvdata(pdev, spear_thermal); + + dev_info(&spear_thermal->device, "Thermal Sensor Loaded at: 0x%p.\n", + stdev->thermal_base); + + return 0; + +disable_clk: + clk_disable(stdev->clk); + + return ret; +} + +static int spear_thermal_exit(struct platform_device *pdev) +{ + unsigned int actual_mask = 0; + struct thermal_zone_device *spear_thermal = platform_get_drvdata(pdev); + struct spear_thermal_dev *stdev = spear_thermal->devdata; + + thermal_zone_device_unregister(spear_thermal); + + /* Disable SPEAr Thermal Sensor */ + actual_mask = readl_relaxed(stdev->thermal_base); + writel_relaxed(actual_mask & ~stdev->flags, stdev->thermal_base); + + clk_disable(stdev->clk); + + return 0; +} + +static const struct of_device_id spear_thermal_id_table[] = { + { .compatible = "st,thermal-spear1340" }, + {} +}; +MODULE_DEVICE_TABLE(of, spear_thermal_id_table); + +static struct platform_driver spear_thermal_driver = { + .probe = spear_thermal_probe, + .remove = spear_thermal_exit, + .driver = { + .name = "spear_thermal", + .pm = &spear_thermal_pm_ops, + .of_match_table = spear_thermal_id_table, + }, +}; + +module_platform_driver(spear_thermal_driver); + +MODULE_AUTHOR("Vincenzo Frascino <vincenzo.frascino@st.com>"); +MODULE_DESCRIPTION("SPEAr thermal driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/thermal/st/Kconfig b/drivers/thermal/st/Kconfig new file mode 100644 index 000000000..490fdbe22 --- /dev/null +++ b/drivers/thermal/st/Kconfig @@ -0,0 +1,12 @@ +config ST_THERMAL + tristate "Thermal sensors on STMicroelectronics STi series of SoCs" + help + Support for thermal sensors on STMicroelectronics STi series of SoCs. + +config ST_THERMAL_SYSCFG + select ST_THERMAL + tristate "STi series syscfg register access based thermal sensors" + +config ST_THERMAL_MEMMAP + select ST_THERMAL + tristate "STi series memory mapped access based thermal sensors" diff --git a/drivers/thermal/st/Makefile b/drivers/thermal/st/Makefile new file mode 100644 index 000000000..b38878977 --- /dev/null +++ b/drivers/thermal/st/Makefile @@ -0,0 +1,3 @@ +obj-$(CONFIG_ST_THERMAL) := st_thermal.o +obj-$(CONFIG_ST_THERMAL_SYSCFG) += st_thermal_syscfg.o +obj-$(CONFIG_ST_THERMAL_MEMMAP) += st_thermal_memmap.o diff --git a/drivers/thermal/st/st_thermal.c b/drivers/thermal/st/st_thermal.c new file mode 100644 index 000000000..be637e6b0 --- /dev/null +++ b/drivers/thermal/st/st_thermal.c @@ -0,0 +1,315 @@ +/* + * ST Thermal Sensor Driver core routines + * Author: Ajit Pal Singh <ajitpal.singh@st.com> + * + * Copyright (C) 2003-2014 STMicroelectronics (R&D) Limited + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + */ + +#include <linux/clk.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> + +#include "st_thermal.h" + +/* The Thermal Framework expects millidegrees */ +#define mcelsius(temp) ((temp) * 1000) + +/* + * Function to allocate regfields which are common + * between syscfg and memory mapped based sensors + */ +static int st_thermal_alloc_regfields(struct st_thermal_sensor *sensor) +{ + struct device *dev = sensor->dev; + struct regmap *regmap = sensor->regmap; + const struct reg_field *reg_fields = sensor->cdata->reg_fields; + + sensor->dcorrect = devm_regmap_field_alloc(dev, regmap, + reg_fields[DCORRECT]); + + sensor->overflow = devm_regmap_field_alloc(dev, regmap, + reg_fields[OVERFLOW]); + + sensor->temp_data = devm_regmap_field_alloc(dev, regmap, + reg_fields[DATA]); + + if (IS_ERR(sensor->dcorrect) || + IS_ERR(sensor->overflow) || + IS_ERR(sensor->temp_data)) { + dev_err(dev, "failed to allocate common regfields\n"); + return -EINVAL; + } + + return sensor->ops->alloc_regfields(sensor); +} + +static int st_thermal_sensor_on(struct st_thermal_sensor *sensor) +{ + int ret; + struct device *dev = sensor->dev; + + ret = clk_prepare_enable(sensor->clk); + if (ret) { + dev_err(dev, "failed to enable clk\n"); + return ret; + } + + ret = sensor->ops->power_ctrl(sensor, POWER_ON); + if (ret) { + dev_err(dev, "failed to power on sensor\n"); + clk_disable_unprepare(sensor->clk); + } + + return ret; +} + +static int st_thermal_sensor_off(struct st_thermal_sensor *sensor) +{ + int ret; + + ret = sensor->ops->power_ctrl(sensor, POWER_OFF); + if (ret) + return ret; + + clk_disable_unprepare(sensor->clk); + + return 0; +} + +static int st_thermal_calibration(struct st_thermal_sensor *sensor) +{ + int ret; + unsigned int val; + struct device *dev = sensor->dev; + + /* Check if sensor calibration data is already written */ + ret = regmap_field_read(sensor->dcorrect, &val); + if (ret) { + dev_err(dev, "failed to read calibration data\n"); + return ret; + } + + if (!val) { + /* + * Sensor calibration value not set by bootloader, + * default calibration data to be used + */ + ret = regmap_field_write(sensor->dcorrect, + sensor->cdata->calibration_val); + if (ret) + dev_err(dev, "failed to set calibration data\n"); + } + + return ret; +} + +/* Callback to get temperature from HW*/ +static int st_thermal_get_temp(struct thermal_zone_device *th, int *temperature) +{ + struct st_thermal_sensor *sensor = th->devdata; + struct device *dev = sensor->dev; + unsigned int temp; + unsigned int overflow; + int ret; + + ret = regmap_field_read(sensor->overflow, &overflow); + if (ret) + return ret; + if (overflow) + return -EIO; + + ret = regmap_field_read(sensor->temp_data, &temp); + if (ret) + return ret; + + temp += sensor->cdata->temp_adjust_val; + temp = mcelsius(temp); + + dev_dbg(dev, "temperature: %d\n", temp); + + *temperature = temp; + + return 0; +} + +static int st_thermal_get_trip_type(struct thermal_zone_device *th, + int trip, enum thermal_trip_type *type) +{ + struct st_thermal_sensor *sensor = th->devdata; + struct device *dev = sensor->dev; + + switch (trip) { + case 0: + *type = THERMAL_TRIP_CRITICAL; + break; + default: + dev_err(dev, "invalid trip point\n"); + return -EINVAL; + } + + return 0; +} + +static int st_thermal_get_trip_temp(struct thermal_zone_device *th, + int trip, int *temp) +{ + struct st_thermal_sensor *sensor = th->devdata; + struct device *dev = sensor->dev; + + switch (trip) { + case 0: + *temp = mcelsius(sensor->cdata->crit_temp); + break; + default: + dev_err(dev, "Invalid trip point\n"); + return -EINVAL; + } + + return 0; +} + +static struct thermal_zone_device_ops st_tz_ops = { + .get_temp = st_thermal_get_temp, + .get_trip_type = st_thermal_get_trip_type, + .get_trip_temp = st_thermal_get_trip_temp, +}; + +int st_thermal_register(struct platform_device *pdev, + const struct of_device_id *st_thermal_of_match) +{ + struct st_thermal_sensor *sensor; + struct device *dev = &pdev->dev; + struct device_node *np = dev->of_node; + const struct of_device_id *match; + + int polling_delay; + int ret; + + if (!np) { + dev_err(dev, "device tree node not found\n"); + return -EINVAL; + } + + sensor = devm_kzalloc(dev, sizeof(*sensor), GFP_KERNEL); + if (!sensor) + return -ENOMEM; + + sensor->dev = dev; + + match = of_match_device(st_thermal_of_match, dev); + if (!(match && match->data)) + return -EINVAL; + + sensor->cdata = match->data; + if (!sensor->cdata->ops) + return -EINVAL; + + sensor->ops = sensor->cdata->ops; + + ret = (sensor->ops->regmap_init)(sensor); + if (ret) + return ret; + + ret = st_thermal_alloc_regfields(sensor); + if (ret) + return ret; + + sensor->clk = devm_clk_get(dev, "thermal"); + if (IS_ERR(sensor->clk)) { + dev_err(dev, "failed to fetch clock\n"); + return PTR_ERR(sensor->clk); + } + + if (sensor->ops->register_enable_irq) { + ret = sensor->ops->register_enable_irq(sensor); + if (ret) + return ret; + } + + ret = st_thermal_sensor_on(sensor); + if (ret) + return ret; + + ret = st_thermal_calibration(sensor); + if (ret) + goto sensor_off; + + polling_delay = sensor->ops->register_enable_irq ? 0 : 1000; + + sensor->thermal_dev = + thermal_zone_device_register(dev_name(dev), 1, 0, sensor, + &st_tz_ops, NULL, 0, polling_delay); + if (IS_ERR(sensor->thermal_dev)) { + dev_err(dev, "failed to register thermal zone device\n"); + ret = PTR_ERR(sensor->thermal_dev); + goto sensor_off; + } + + platform_set_drvdata(pdev, sensor); + + return 0; + +sensor_off: + st_thermal_sensor_off(sensor); + + return ret; +} +EXPORT_SYMBOL_GPL(st_thermal_register); + +int st_thermal_unregister(struct platform_device *pdev) +{ + struct st_thermal_sensor *sensor = platform_get_drvdata(pdev); + + st_thermal_sensor_off(sensor); + thermal_zone_device_unregister(sensor->thermal_dev); + + return 0; +} +EXPORT_SYMBOL_GPL(st_thermal_unregister); + +#ifdef CONFIG_PM_SLEEP +static int st_thermal_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct st_thermal_sensor *sensor = platform_get_drvdata(pdev); + + return st_thermal_sensor_off(sensor); +} + +static int st_thermal_resume(struct device *dev) +{ + int ret; + struct platform_device *pdev = to_platform_device(dev); + struct st_thermal_sensor *sensor = platform_get_drvdata(pdev); + + ret = st_thermal_sensor_on(sensor); + if (ret) + return ret; + + ret = st_thermal_calibration(sensor); + if (ret) + return ret; + + if (sensor->ops->enable_irq) { + ret = sensor->ops->enable_irq(sensor); + if (ret) + return ret; + } + + return 0; +} +#endif + +SIMPLE_DEV_PM_OPS(st_thermal_pm_ops, st_thermal_suspend, st_thermal_resume); +EXPORT_SYMBOL_GPL(st_thermal_pm_ops); + +MODULE_AUTHOR("STMicroelectronics (R&D) Limited <ajitpal.singh@st.com>"); +MODULE_DESCRIPTION("STMicroelectronics STi SoC Thermal Sensor Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/st/st_thermal.h b/drivers/thermal/st/st_thermal.h new file mode 100644 index 000000000..fecafbe10 --- /dev/null +++ b/drivers/thermal/st/st_thermal.h @@ -0,0 +1,104 @@ +/* + * ST Thermal Sensor Driver for STi series of SoCs + * Author: Ajit Pal Singh <ajitpal.singh@st.com> + * + * Copyright (C) 2003-2014 STMicroelectronics (R&D) Limited + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef __STI_THERMAL_SYSCFG_H +#define __STI_THERMAL_SYSCFG_H + +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/thermal.h> + +enum st_thermal_regfield_ids { + INT_THRESH_HI = 0, /* Top two regfield IDs are mutually exclusive */ + TEMP_PWR = 0, + DCORRECT, + OVERFLOW, + DATA, + INT_ENABLE, + + MAX_REGFIELDS +}; + +/* Thermal sensor power states */ +enum st_thermal_power_state { + POWER_OFF = 0, + POWER_ON +}; + +struct st_thermal_sensor; + +/** + * Description of private thermal sensor ops. + * + * @power_ctrl: Function for powering on/off a sensor. Clock to the + * sensor is also controlled from this function. + * @alloc_regfields: Allocate regmap register fields, specific to a sensor. + * @do_memmap_regmap: Memory map the thermal register space and init regmap + * instance or find regmap instance. + * @register_irq: Register an interrupt handler for a sensor. + */ +struct st_thermal_sensor_ops { + int (*power_ctrl)(struct st_thermal_sensor *, enum st_thermal_power_state); + int (*alloc_regfields)(struct st_thermal_sensor *); + int (*regmap_init)(struct st_thermal_sensor *); + int (*register_enable_irq)(struct st_thermal_sensor *); + int (*enable_irq)(struct st_thermal_sensor *); +}; + +/** + * Description of thermal driver compatible data. + * + * @reg_fields: Pointer to the regfields array for a sensor. + * @sys_compat: Pointer to the syscon node compatible string. + * @ops: Pointer to private thermal ops for a sensor. + * @calibration_val: Default calibration value to be written to the DCORRECT + * register field for a sensor. + * @temp_adjust_val: Value to be added/subtracted from the data read from + * the sensor. If value needs to be added please provide a + * positive value and if it is to be subtracted please + * provide a negative value. + * @crit_temp: The temperature beyond which the SoC should be shutdown + * to prevent damage. + */ +struct st_thermal_compat_data { + char *sys_compat; + const struct reg_field *reg_fields; + const struct st_thermal_sensor_ops *ops; + unsigned int calibration_val; + int temp_adjust_val; + int crit_temp; +}; + +struct st_thermal_sensor { + struct device *dev; + struct thermal_zone_device *thermal_dev; + const struct st_thermal_sensor_ops *ops; + const struct st_thermal_compat_data *cdata; + struct clk *clk; + struct regmap *regmap; + struct regmap_field *pwr; + struct regmap_field *dcorrect; + struct regmap_field *overflow; + struct regmap_field *temp_data; + struct regmap_field *int_thresh_hi; + struct regmap_field *int_enable; + int irq; + void __iomem *mmio_base; +}; + +extern int st_thermal_register(struct platform_device *pdev, + const struct of_device_id *st_thermal_of_match); +extern int st_thermal_unregister(struct platform_device *pdev); +extern const struct dev_pm_ops st_thermal_pm_ops; + +#endif /* __STI_RESET_SYSCFG_H */ diff --git a/drivers/thermal/st/st_thermal_memmap.c b/drivers/thermal/st/st_thermal_memmap.c new file mode 100644 index 000000000..91d42319d --- /dev/null +++ b/drivers/thermal/st/st_thermal_memmap.c @@ -0,0 +1,209 @@ +/* + * ST Thermal Sensor Driver for memory mapped sensors. + * Author: Ajit Pal Singh <ajitpal.singh@st.com> + * + * Copyright (C) 2003-2014 STMicroelectronics (R&D) Limited + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <linux/of.h> +#include <linux/module.h> + +#include "st_thermal.h" + +#define STIH416_MPE_CONF 0x0 +#define STIH416_MPE_STATUS 0x4 +#define STIH416_MPE_INT_THRESH 0x8 +#define STIH416_MPE_INT_EN 0xC + +/* Power control bits for the memory mapped thermal sensor */ +#define THERMAL_PDN BIT(4) +#define THERMAL_SRSTN BIT(10) + +static const struct reg_field st_mmap_thermal_regfields[MAX_REGFIELDS] = { + /* + * According to the STIH416 MPE temp sensor data sheet - + * the PDN (Power Down Bit) and SRSTN (Soft Reset Bit) need to be + * written simultaneously for powering on and off the temperature + * sensor. regmap_update_bits() will be used to update the register. + */ + [INT_THRESH_HI] = REG_FIELD(STIH416_MPE_INT_THRESH, 0, 7), + [DCORRECT] = REG_FIELD(STIH416_MPE_CONF, 5, 9), + [OVERFLOW] = REG_FIELD(STIH416_MPE_STATUS, 9, 9), + [DATA] = REG_FIELD(STIH416_MPE_STATUS, 11, 18), + [INT_ENABLE] = REG_FIELD(STIH416_MPE_INT_EN, 0, 0), +}; + +static irqreturn_t st_mmap_thermal_trip_handler(int irq, void *sdata) +{ + struct st_thermal_sensor *sensor = sdata; + + thermal_zone_device_update(sensor->thermal_dev, + THERMAL_EVENT_UNSPECIFIED); + + return IRQ_HANDLED; +} + +/* Private ops for the Memory Mapped based thermal sensors */ +static int st_mmap_power_ctrl(struct st_thermal_sensor *sensor, + enum st_thermal_power_state power_state) +{ + const unsigned int mask = (THERMAL_PDN | THERMAL_SRSTN); + const unsigned int val = power_state ? mask : 0; + + return regmap_update_bits(sensor->regmap, STIH416_MPE_CONF, mask, val); +} + +static int st_mmap_alloc_regfields(struct st_thermal_sensor *sensor) +{ + struct device *dev = sensor->dev; + struct regmap *regmap = sensor->regmap; + const struct reg_field *reg_fields = sensor->cdata->reg_fields; + + sensor->int_thresh_hi = devm_regmap_field_alloc(dev, regmap, + reg_fields[INT_THRESH_HI]); + sensor->int_enable = devm_regmap_field_alloc(dev, regmap, + reg_fields[INT_ENABLE]); + + if (IS_ERR(sensor->int_thresh_hi) || IS_ERR(sensor->int_enable)) { + dev_err(dev, "failed to alloc mmap regfields\n"); + return -EINVAL; + } + + return 0; +} + +static int st_mmap_enable_irq(struct st_thermal_sensor *sensor) +{ + int ret; + + /* Set upper critical threshold */ + ret = regmap_field_write(sensor->int_thresh_hi, + sensor->cdata->crit_temp - + sensor->cdata->temp_adjust_val); + if (ret) + return ret; + + return regmap_field_write(sensor->int_enable, 1); +} + +static int st_mmap_register_enable_irq(struct st_thermal_sensor *sensor) +{ + struct device *dev = sensor->dev; + struct platform_device *pdev = to_platform_device(dev); + int ret; + + sensor->irq = platform_get_irq(pdev, 0); + if (sensor->irq < 0) { + dev_err(dev, "failed to register IRQ\n"); + return sensor->irq; + } + + ret = devm_request_threaded_irq(dev, sensor->irq, + NULL, st_mmap_thermal_trip_handler, + IRQF_TRIGGER_RISING | IRQF_ONESHOT, + dev->driver->name, sensor); + if (ret) { + dev_err(dev, "failed to register IRQ %d\n", sensor->irq); + return ret; + } + + return st_mmap_enable_irq(sensor); +} + +static const struct regmap_config st_416mpe_regmap_config = { + .reg_bits = 32, + .val_bits = 32, + .reg_stride = 4, +}; + +static int st_mmap_regmap_init(struct st_thermal_sensor *sensor) +{ + struct device *dev = sensor->dev; + struct platform_device *pdev = to_platform_device(dev); + struct resource *res; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + dev_err(dev, "no memory resources defined\n"); + return -ENODEV; + } + + sensor->mmio_base = devm_ioremap_resource(dev, res); + if (IS_ERR(sensor->mmio_base)) { + dev_err(dev, "failed to remap IO\n"); + return PTR_ERR(sensor->mmio_base); + } + + sensor->regmap = devm_regmap_init_mmio(dev, sensor->mmio_base, + &st_416mpe_regmap_config); + if (IS_ERR(sensor->regmap)) { + dev_err(dev, "failed to initialise regmap\n"); + return PTR_ERR(sensor->regmap); + } + + return 0; +} + +static const struct st_thermal_sensor_ops st_mmap_sensor_ops = { + .power_ctrl = st_mmap_power_ctrl, + .alloc_regfields = st_mmap_alloc_regfields, + .regmap_init = st_mmap_regmap_init, + .register_enable_irq = st_mmap_register_enable_irq, + .enable_irq = st_mmap_enable_irq, +}; + +/* Compatible device data stih416 mpe thermal sensor */ +static const struct st_thermal_compat_data st_416mpe_cdata = { + .reg_fields = st_mmap_thermal_regfields, + .ops = &st_mmap_sensor_ops, + .calibration_val = 14, + .temp_adjust_val = -95, + .crit_temp = 120, +}; + +/* Compatible device data stih407 thermal sensor */ +static const struct st_thermal_compat_data st_407_cdata = { + .reg_fields = st_mmap_thermal_regfields, + .ops = &st_mmap_sensor_ops, + .calibration_val = 16, + .temp_adjust_val = -95, + .crit_temp = 120, +}; + +static const struct of_device_id st_mmap_thermal_of_match[] = { + { .compatible = "st,stih416-mpe-thermal", .data = &st_416mpe_cdata }, + { .compatible = "st,stih407-thermal", .data = &st_407_cdata }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, st_mmap_thermal_of_match); + +static int st_mmap_probe(struct platform_device *pdev) +{ + return st_thermal_register(pdev, st_mmap_thermal_of_match); +} + +static int st_mmap_remove(struct platform_device *pdev) +{ + return st_thermal_unregister(pdev); +} + +static struct platform_driver st_mmap_thermal_driver = { + .driver = { + .name = "st_thermal_mmap", + .pm = &st_thermal_pm_ops, + .of_match_table = st_mmap_thermal_of_match, + }, + .probe = st_mmap_probe, + .remove = st_mmap_remove, +}; + +module_platform_driver(st_mmap_thermal_driver); + +MODULE_AUTHOR("STMicroelectronics (R&D) Limited <ajitpal.singh@st.com>"); +MODULE_DESCRIPTION("STMicroelectronics STi SoC Thermal Sensor Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/st/st_thermal_syscfg.c b/drivers/thermal/st/st_thermal_syscfg.c new file mode 100644 index 000000000..3df5b7890 --- /dev/null +++ b/drivers/thermal/st/st_thermal_syscfg.c @@ -0,0 +1,178 @@ +/* + * ST Thermal Sensor Driver for syscfg based sensors. + * Author: Ajit Pal Singh <ajitpal.singh@st.com> + * + * Copyright (C) 2003-2014 STMicroelectronics (R&D) Limited + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <linux/of.h> +#include <linux/module.h> +#include <linux/mfd/syscon.h> + +#include "st_thermal.h" + +/* STiH415 */ +#define STIH415_SYSCFG_FRONT(num) ((num - 100) * 4) +#define STIH415_SAS_THSENS_CONF STIH415_SYSCFG_FRONT(178) +#define STIH415_SAS_THSENS_STATUS STIH415_SYSCFG_FRONT(198) +#define STIH415_SYSCFG_MPE(num) ((num - 600) * 4) +#define STIH415_MPE_THSENS_CONF STIH415_SYSCFG_MPE(607) +#define STIH415_MPE_THSENS_STATUS STIH415_SYSCFG_MPE(667) + +/* STiH416 */ +#define STIH416_SYSCFG_FRONT(num) ((num - 1000) * 4) +#define STIH416_SAS_THSENS_CONF STIH416_SYSCFG_FRONT(1552) +#define STIH416_SAS_THSENS_STATUS1 STIH416_SYSCFG_FRONT(1554) +#define STIH416_SAS_THSENS_STATUS2 STIH416_SYSCFG_FRONT(1594) + +/* STiD127 */ +#define STID127_SYSCFG_CPU(num) ((num - 700) * 4) +#define STID127_THSENS_CONF STID127_SYSCFG_CPU(743) +#define STID127_THSENS_STATUS STID127_SYSCFG_CPU(767) + +static const struct reg_field st_415sas_regfields[MAX_REGFIELDS] = { + [TEMP_PWR] = REG_FIELD(STIH415_SAS_THSENS_CONF, 9, 9), + [DCORRECT] = REG_FIELD(STIH415_SAS_THSENS_CONF, 4, 8), + [OVERFLOW] = REG_FIELD(STIH415_SAS_THSENS_STATUS, 8, 8), + [DATA] = REG_FIELD(STIH415_SAS_THSENS_STATUS, 10, 16), +}; + +static const struct reg_field st_415mpe_regfields[MAX_REGFIELDS] = { + [TEMP_PWR] = REG_FIELD(STIH415_MPE_THSENS_CONF, 8, 8), + [DCORRECT] = REG_FIELD(STIH415_MPE_THSENS_CONF, 3, 7), + [OVERFLOW] = REG_FIELD(STIH415_MPE_THSENS_STATUS, 9, 9), + [DATA] = REG_FIELD(STIH415_MPE_THSENS_STATUS, 11, 18), +}; + +static const struct reg_field st_416sas_regfields[MAX_REGFIELDS] = { + [TEMP_PWR] = REG_FIELD(STIH416_SAS_THSENS_CONF, 9, 9), + [DCORRECT] = REG_FIELD(STIH416_SAS_THSENS_CONF, 4, 8), + [OVERFLOW] = REG_FIELD(STIH416_SAS_THSENS_STATUS1, 8, 8), + [DATA] = REG_FIELD(STIH416_SAS_THSENS_STATUS2, 10, 16), +}; + +static const struct reg_field st_127_regfields[MAX_REGFIELDS] = { + [TEMP_PWR] = REG_FIELD(STID127_THSENS_CONF, 7, 7), + [DCORRECT] = REG_FIELD(STID127_THSENS_CONF, 2, 6), + [OVERFLOW] = REG_FIELD(STID127_THSENS_STATUS, 9, 9), + [DATA] = REG_FIELD(STID127_THSENS_STATUS, 11, 18), +}; + +/* Private OPs for System Configuration Register based thermal sensors */ +static int st_syscfg_power_ctrl(struct st_thermal_sensor *sensor, + enum st_thermal_power_state power_state) +{ + return regmap_field_write(sensor->pwr, power_state); +} + +static int st_syscfg_alloc_regfields(struct st_thermal_sensor *sensor) +{ + struct device *dev = sensor->dev; + + sensor->pwr = devm_regmap_field_alloc(dev, sensor->regmap, + sensor->cdata->reg_fields[TEMP_PWR]); + + if (IS_ERR(sensor->pwr)) { + dev_err(dev, "failed to alloc syscfg regfields\n"); + return PTR_ERR(sensor->pwr); + } + + return 0; +} + +static int st_syscfg_regmap_init(struct st_thermal_sensor *sensor) +{ + sensor->regmap = + syscon_regmap_lookup_by_compatible(sensor->cdata->sys_compat); + if (IS_ERR(sensor->regmap)) { + dev_err(sensor->dev, "failed to find syscfg regmap\n"); + return PTR_ERR(sensor->regmap); + } + + return 0; +} + +static const struct st_thermal_sensor_ops st_syscfg_sensor_ops = { + .power_ctrl = st_syscfg_power_ctrl, + .alloc_regfields = st_syscfg_alloc_regfields, + .regmap_init = st_syscfg_regmap_init, +}; + +/* Compatible device data for stih415 sas thermal sensor */ +static const struct st_thermal_compat_data st_415sas_cdata = { + .sys_compat = "st,stih415-front-syscfg", + .reg_fields = st_415sas_regfields, + .ops = &st_syscfg_sensor_ops, + .calibration_val = 16, + .temp_adjust_val = 20, + .crit_temp = 120, +}; + +/* Compatible device data for stih415 mpe thermal sensor */ +static const struct st_thermal_compat_data st_415mpe_cdata = { + .sys_compat = "st,stih415-system-syscfg", + .reg_fields = st_415mpe_regfields, + .ops = &st_syscfg_sensor_ops, + .calibration_val = 16, + .temp_adjust_val = -103, + .crit_temp = 120, +}; + +/* Compatible device data for stih416 sas thermal sensor */ +static const struct st_thermal_compat_data st_416sas_cdata = { + .sys_compat = "st,stih416-front-syscfg", + .reg_fields = st_416sas_regfields, + .ops = &st_syscfg_sensor_ops, + .calibration_val = 16, + .temp_adjust_val = 20, + .crit_temp = 120, +}; + +/* Compatible device data for stid127 thermal sensor */ +static const struct st_thermal_compat_data st_127_cdata = { + .sys_compat = "st,stid127-cpu-syscfg", + .reg_fields = st_127_regfields, + .ops = &st_syscfg_sensor_ops, + .calibration_val = 8, + .temp_adjust_val = -103, + .crit_temp = 120, +}; + +static const struct of_device_id st_syscfg_thermal_of_match[] = { + { .compatible = "st,stih415-sas-thermal", .data = &st_415sas_cdata }, + { .compatible = "st,stih415-mpe-thermal", .data = &st_415mpe_cdata }, + { .compatible = "st,stih416-sas-thermal", .data = &st_416sas_cdata }, + { .compatible = "st,stid127-thermal", .data = &st_127_cdata }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, st_syscfg_thermal_of_match); + +static int st_syscfg_probe(struct platform_device *pdev) +{ + return st_thermal_register(pdev, st_syscfg_thermal_of_match); +} + +static int st_syscfg_remove(struct platform_device *pdev) +{ + return st_thermal_unregister(pdev); +} + +static struct platform_driver st_syscfg_thermal_driver = { + .driver = { + .name = "st_syscfg_thermal", + .pm = &st_thermal_pm_ops, + .of_match_table = st_syscfg_thermal_of_match, + }, + .probe = st_syscfg_probe, + .remove = st_syscfg_remove, +}; +module_platform_driver(st_syscfg_thermal_driver); + +MODULE_AUTHOR("STMicroelectronics (R&D) Limited <ajitpal.singh@st.com>"); +MODULE_DESCRIPTION("STMicroelectronics STi SoC Thermal Sensor Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/step_wise.c b/drivers/thermal/step_wise.c new file mode 100644 index 000000000..ee047ca43 --- /dev/null +++ b/drivers/thermal/step_wise.c @@ -0,0 +1,230 @@ +/* + * step_wise.c - A step-by-step Thermal throttling governor + * + * Copyright (C) 2012 Intel Corp + * Copyright (C) 2012 Durgadoss R <durgadoss.r@intel.com> + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include <linux/thermal.h> +#include <trace/events/thermal.h> + +#include "thermal_core.h" + +/* + * If the temperature is higher than a trip point, + * a. if the trend is THERMAL_TREND_RAISING, use higher cooling + * state for this trip point + * b. if the trend is THERMAL_TREND_DROPPING, do nothing + * c. if the trend is THERMAL_TREND_RAISE_FULL, use upper limit + * for this trip point + * d. if the trend is THERMAL_TREND_DROP_FULL, use lower limit + * for this trip point + * If the temperature is lower than a trip point, + * a. if the trend is THERMAL_TREND_RAISING, do nothing + * b. if the trend is THERMAL_TREND_DROPPING, use lower cooling + * state for this trip point, if the cooling state already + * equals lower limit, deactivate the thermal instance + * c. if the trend is THERMAL_TREND_RAISE_FULL, do nothing + * d. if the trend is THERMAL_TREND_DROP_FULL, use lower limit, + * if the cooling state already equals lower limit, + * deactivate the thermal instance + */ +static unsigned long get_target_state(struct thermal_instance *instance, + enum thermal_trend trend, bool throttle) +{ + struct thermal_cooling_device *cdev = instance->cdev; + unsigned long cur_state; + unsigned long next_target; + + /* + * We keep this instance the way it is by default. + * Otherwise, we use the current state of the + * cdev in use to determine the next_target. + */ + cdev->ops->get_cur_state(cdev, &cur_state); + next_target = instance->target; + dev_dbg(&cdev->device, "cur_state=%ld\n", cur_state); + + if (!instance->initialized) { + if (throttle) { + next_target = (cur_state + 1) >= instance->upper ? + instance->upper : + ((cur_state + 1) < instance->lower ? + instance->lower : (cur_state + 1)); + } else { + next_target = THERMAL_NO_TARGET; + } + + return next_target; + } + + switch (trend) { + case THERMAL_TREND_RAISING: + if (throttle) { + next_target = cur_state < instance->upper ? + (cur_state + 1) : instance->upper; + if (next_target < instance->lower) + next_target = instance->lower; + } + break; + case THERMAL_TREND_RAISE_FULL: + if (throttle) + next_target = instance->upper; + break; + case THERMAL_TREND_DROPPING: + if (cur_state <= instance->lower) { + if (!throttle) + next_target = THERMAL_NO_TARGET; + } else { + if (!throttle) { + next_target = cur_state - 1; + if (next_target > instance->upper) + next_target = instance->upper; + } + } + break; + case THERMAL_TREND_DROP_FULL: + if (cur_state == instance->lower) { + if (!throttle) + next_target = THERMAL_NO_TARGET; + } else + next_target = instance->lower; + break; + default: + break; + } + + return next_target; +} + +static void update_passive_instance(struct thermal_zone_device *tz, + enum thermal_trip_type type, int value) +{ + /* + * If value is +1, activate a passive instance. + * If value is -1, deactivate a passive instance. + */ + if (type == THERMAL_TRIP_PASSIVE || type == THERMAL_TRIPS_NONE) + tz->passive += value; +} + +static void thermal_zone_trip_update(struct thermal_zone_device *tz, int trip) +{ + int trip_temp; + enum thermal_trip_type trip_type; + enum thermal_trend trend; + struct thermal_instance *instance; + bool throttle = false; + int old_target; + + if (trip == THERMAL_TRIPS_NONE) { + trip_temp = tz->forced_passive; + trip_type = THERMAL_TRIPS_NONE; + } else { + tz->ops->get_trip_temp(tz, trip, &trip_temp); + tz->ops->get_trip_type(tz, trip, &trip_type); + } + + trend = get_tz_trend(tz, trip); + + if (tz->temperature >= trip_temp) { + throttle = true; + trace_thermal_zone_trip(tz, trip, trip_type); + } + + dev_dbg(&tz->device, "Trip%d[type=%d,temp=%d]:trend=%d,throttle=%d\n", + trip, trip_type, trip_temp, trend, throttle); + + mutex_lock(&tz->lock); + + list_for_each_entry(instance, &tz->thermal_instances, tz_node) { + if (instance->trip != trip) + continue; + + old_target = instance->target; + instance->target = get_target_state(instance, trend, throttle); + dev_dbg(&instance->cdev->device, "old_target=%d, target=%d\n", + old_target, (int)instance->target); + + if (instance->initialized && old_target == instance->target) + continue; + + /* Activate a passive thermal instance */ + if (old_target == THERMAL_NO_TARGET && + instance->target != THERMAL_NO_TARGET) + update_passive_instance(tz, trip_type, 1); + /* Deactivate a passive thermal instance */ + else if (old_target != THERMAL_NO_TARGET && + instance->target == THERMAL_NO_TARGET) + update_passive_instance(tz, trip_type, -1); + + instance->initialized = true; + mutex_lock(&instance->cdev->lock); + instance->cdev->updated = false; /* cdev needs update */ + mutex_unlock(&instance->cdev->lock); + } + + mutex_unlock(&tz->lock); +} + +/** + * step_wise_throttle - throttles devices associated with the given zone + * @tz - thermal_zone_device + * @trip - trip point index + * + * Throttling Logic: This uses the trend of the thermal zone to throttle. + * If the thermal zone is 'heating up' this throttles all the cooling + * devices associated with the zone and its particular trip point, by one + * step. If the zone is 'cooling down' it brings back the performance of + * the devices by one step. + */ +static int step_wise_throttle(struct thermal_zone_device *tz, int trip) +{ + struct thermal_instance *instance; + + thermal_zone_trip_update(tz, trip); + + if (tz->forced_passive) + thermal_zone_trip_update(tz, THERMAL_TRIPS_NONE); + + mutex_lock(&tz->lock); + + list_for_each_entry(instance, &tz->thermal_instances, tz_node) + thermal_cdev_update(instance->cdev); + + mutex_unlock(&tz->lock); + + return 0; +} + +static struct thermal_governor thermal_gov_step_wise = { + .name = "step_wise", + .throttle = step_wise_throttle, +}; + +int thermal_gov_step_wise_register(void) +{ + return thermal_register_governor(&thermal_gov_step_wise); +} + +void thermal_gov_step_wise_unregister(void) +{ + thermal_unregister_governor(&thermal_gov_step_wise); +} diff --git a/drivers/thermal/tango_thermal.c b/drivers/thermal/tango_thermal.c new file mode 100644 index 000000000..4e67795cb --- /dev/null +++ b/drivers/thermal/tango_thermal.c @@ -0,0 +1,125 @@ +#include <linux/io.h> +#include <linux/delay.h> +#include <linux/module.h> +#include <linux/thermal.h> +#include <linux/platform_device.h> + +/* + * According to a data sheet draft, "this temperature sensor uses a bandgap + * type of circuit to compare a voltage which has a negative temperature + * coefficient with a voltage that is proportional to absolute temperature. + * A resistor bank allows 41 different temperature thresholds to be selected + * and the logic output will then indicate whether the actual die temperature + * lies above or below the selected threshold." + */ + +#define TEMPSI_CMD 0 +#define TEMPSI_RES 4 +#define TEMPSI_CFG 8 + +#define CMD_OFF 0 +#define CMD_ON 1 +#define CMD_READ 2 + +#define IDX_MIN 15 +#define IDX_MAX 40 + +struct tango_thermal_priv { + void __iomem *base; + int thresh_idx; +}; + +static bool temp_above_thresh(void __iomem *base, int thresh_idx) +{ + writel(CMD_READ | thresh_idx << 8, base + TEMPSI_CMD); + usleep_range(10, 20); + writel(CMD_READ | thresh_idx << 8, base + TEMPSI_CMD); + + return readl(base + TEMPSI_RES); +} + +static int tango_get_temp(void *arg, int *res) +{ + struct tango_thermal_priv *priv = arg; + int idx = priv->thresh_idx; + + if (temp_above_thresh(priv->base, idx)) { + /* Search upward by incrementing thresh_idx */ + while (idx < IDX_MAX && temp_above_thresh(priv->base, ++idx)) + cpu_relax(); + idx = idx - 1; /* always return lower bound */ + } else { + /* Search downward by decrementing thresh_idx */ + while (idx > IDX_MIN && !temp_above_thresh(priv->base, --idx)) + cpu_relax(); + } + + *res = (idx * 9 / 2 - 38) * 1000; /* millidegrees Celsius */ + priv->thresh_idx = idx; + + return 0; +} + +static const struct thermal_zone_of_device_ops ops = { + .get_temp = tango_get_temp, +}; + +static void tango_thermal_init(struct tango_thermal_priv *priv) +{ + writel(0, priv->base + TEMPSI_CFG); + writel(CMD_ON, priv->base + TEMPSI_CMD); +} + +static int tango_thermal_probe(struct platform_device *pdev) +{ + struct resource *res; + struct tango_thermal_priv *priv; + struct thermal_zone_device *tzdev; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + priv->base = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(priv->base)) + return PTR_ERR(priv->base); + + platform_set_drvdata(pdev, priv); + priv->thresh_idx = IDX_MIN; + tango_thermal_init(priv); + + tzdev = devm_thermal_zone_of_sensor_register(&pdev->dev, 0, priv, &ops); + return PTR_ERR_OR_ZERO(tzdev); +} + +static int __maybe_unused tango_thermal_resume(struct device *dev) +{ + tango_thermal_init(dev_get_drvdata(dev)); + return 0; +} + +static SIMPLE_DEV_PM_OPS(tango_thermal_pm, NULL, tango_thermal_resume); + +static const struct of_device_id tango_sensor_ids[] = { + { + .compatible = "sigma,smp8758-thermal", + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, tango_sensor_ids); + +static struct platform_driver tango_thermal_driver = { + .probe = tango_thermal_probe, + .driver = { + .name = "tango-thermal", + .of_match_table = tango_sensor_ids, + .pm = &tango_thermal_pm, + }, +}; + +module_platform_driver(tango_thermal_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Sigma Designs"); +MODULE_DESCRIPTION("Tango temperature sensor"); diff --git a/drivers/thermal/tegra/Kconfig b/drivers/thermal/tegra/Kconfig new file mode 100644 index 000000000..f8740f785 --- /dev/null +++ b/drivers/thermal/tegra/Kconfig @@ -0,0 +1,20 @@ +menu "NVIDIA Tegra thermal drivers" +depends on ARCH_TEGRA + +config TEGRA_SOCTHERM + tristate "Tegra SOCTHERM thermal management" + help + Enable this option for integrated thermal management support on NVIDIA + Tegra systems-on-chip. The driver supports four thermal zones + (CPU, GPU, MEM, PLLX). Cooling devices can be bound to the thermal + zones to manage temperatures. This option is also required for the + emergency thermal reset (thermtrip) feature to function. + +config TEGRA_BPMP_THERMAL + tristate "Tegra BPMP thermal sensing" + depends on TEGRA_BPMP || COMPILE_TEST + help + Enable this option for support for sensing system temperature of NVIDIA + Tegra systems-on-chip with the BPMP coprocessor (Tegra186). + +endmenu diff --git a/drivers/thermal/tegra/Makefile b/drivers/thermal/tegra/Makefile new file mode 100644 index 000000000..0f2b66edf --- /dev/null +++ b/drivers/thermal/tegra/Makefile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_TEGRA_SOCTHERM) += tegra-soctherm.o +obj-$(CONFIG_TEGRA_BPMP_THERMAL) += tegra-bpmp-thermal.o + +tegra-soctherm-y := soctherm.o soctherm-fuse.o +tegra-soctherm-$(CONFIG_ARCH_TEGRA_124_SOC) += tegra124-soctherm.o +tegra-soctherm-$(CONFIG_ARCH_TEGRA_132_SOC) += tegra132-soctherm.o +tegra-soctherm-$(CONFIG_ARCH_TEGRA_210_SOC) += tegra210-soctherm.o diff --git a/drivers/thermal/tegra/soctherm-fuse.c b/drivers/thermal/tegra/soctherm-fuse.c new file mode 100644 index 000000000..29963180c --- /dev/null +++ b/drivers/thermal/tegra/soctherm-fuse.c @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2014-2016, NVIDIA CORPORATION. All rights reserved. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <soc/tegra/fuse.h> + +#include "soctherm.h" + +#define NOMINAL_CALIB_FT 105 +#define NOMINAL_CALIB_CP 25 + +#define FUSE_TSENSOR_CALIB_CP_TS_BASE_MASK 0x1fff +#define FUSE_TSENSOR_CALIB_FT_TS_BASE_MASK (0x1fff << 13) +#define FUSE_TSENSOR_CALIB_FT_TS_BASE_SHIFT 13 + +#define FUSE_TSENSOR_COMMON 0x180 + +/* + * Tegra210: Layout of bits in FUSE_TSENSOR_COMMON: + * 3 2 1 0 + * 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | BASE_FT | BASE_CP | SHFT_FT | SHIFT_CP | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Tegra12x, etc: + * In chips prior to Tegra210, this fuse was incorrectly sized as 26 bits, + * and didn't hold SHIFT_CP in [31:26]. Therefore these missing six bits + * were obtained via the FUSE_SPARE_REALIGNMENT_REG register [5:0]. + * + * FUSE_TSENSOR_COMMON: + * 3 2 1 0 + * 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |-----------| SHFT_FT | BASE_FT | BASE_CP | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * FUSE_SPARE_REALIGNMENT_REG: + * 3 2 1 0 + * 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |---------------------------------------------------| SHIFT_CP | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + +#define CALIB_COEFFICIENT 1000000LL + +/** + * div64_s64_precise() - wrapper for div64_s64() + * @a: the dividend + * @b: the divisor + * + * Implements division with fairly accurate rounding instead of truncation by + * shifting the dividend to the left by 16 so that the quotient has a + * much higher precision. + * + * Return: the quotient of a / b. + */ +static s64 div64_s64_precise(s64 a, s32 b) +{ + s64 r, al; + + /* Scale up for increased precision division */ + al = a << 16; + + r = div64_s64(al * 2 + 1, 2 * b); + return r >> 16; +} + +int tegra_calc_shared_calib(const struct tegra_soctherm_fuse *tfuse, + struct tsensor_shared_calib *shared) +{ + u32 val; + s32 shifted_cp, shifted_ft; + int err; + + err = tegra_fuse_readl(FUSE_TSENSOR_COMMON, &val); + if (err) + return err; + + shared->base_cp = (val & tfuse->fuse_base_cp_mask) >> + tfuse->fuse_base_cp_shift; + shared->base_ft = (val & tfuse->fuse_base_ft_mask) >> + tfuse->fuse_base_ft_shift; + + shifted_ft = (val & tfuse->fuse_shift_ft_mask) >> + tfuse->fuse_shift_ft_shift; + shifted_ft = sign_extend32(shifted_ft, 4); + + if (tfuse->fuse_spare_realignment) { + err = tegra_fuse_readl(tfuse->fuse_spare_realignment, &val); + if (err) + return err; + } + + shifted_cp = sign_extend32(val, 5); + + shared->actual_temp_cp = 2 * NOMINAL_CALIB_CP + shifted_cp; + shared->actual_temp_ft = 2 * NOMINAL_CALIB_FT + shifted_ft; + + return 0; +} + +int tegra_calc_tsensor_calib(const struct tegra_tsensor *sensor, + const struct tsensor_shared_calib *shared, + u32 *calibration) +{ + const struct tegra_tsensor_group *sensor_group; + u32 val, calib; + s32 actual_tsensor_ft, actual_tsensor_cp; + s32 delta_sens, delta_temp; + s32 mult, div; + s16 therma, thermb; + s64 temp; + int err; + + sensor_group = sensor->group; + + err = tegra_fuse_readl(sensor->calib_fuse_offset, &val); + if (err) + return err; + + actual_tsensor_cp = (shared->base_cp * 64) + sign_extend32(val, 12); + val = (val & FUSE_TSENSOR_CALIB_FT_TS_BASE_MASK) >> + FUSE_TSENSOR_CALIB_FT_TS_BASE_SHIFT; + actual_tsensor_ft = (shared->base_ft * 32) + sign_extend32(val, 12); + + delta_sens = actual_tsensor_ft - actual_tsensor_cp; + delta_temp = shared->actual_temp_ft - shared->actual_temp_cp; + + mult = sensor_group->pdiv * sensor->config->tsample_ate; + div = sensor->config->tsample * sensor_group->pdiv_ate; + + temp = (s64)delta_temp * (1LL << 13) * mult; + therma = div64_s64_precise(temp, (s64)delta_sens * div); + + temp = ((s64)actual_tsensor_ft * shared->actual_temp_cp) - + ((s64)actual_tsensor_cp * shared->actual_temp_ft); + thermb = div64_s64_precise(temp, delta_sens); + + temp = (s64)therma * sensor->fuse_corr_alpha; + therma = div64_s64_precise(temp, CALIB_COEFFICIENT); + + temp = (s64)thermb * sensor->fuse_corr_alpha + sensor->fuse_corr_beta; + thermb = div64_s64_precise(temp, CALIB_COEFFICIENT); + + calib = ((u16)therma << SENSOR_CONFIG2_THERMA_SHIFT) | + ((u16)thermb << SENSOR_CONFIG2_THERMB_SHIFT); + + *calibration = calib; + + return 0; +} + +MODULE_AUTHOR("Wei Ni <wni@nvidia.com>"); +MODULE_DESCRIPTION("Tegra SOCTHERM fuse management"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/tegra/soctherm.c b/drivers/thermal/tegra/soctherm.c new file mode 100644 index 000000000..ed28110a3 --- /dev/null +++ b/drivers/thermal/tegra/soctherm.c @@ -0,0 +1,1463 @@ +/* + * Copyright (c) 2014, NVIDIA CORPORATION. All rights reserved. + * + * Author: + * Mikko Perttunen <mperttunen@nvidia.com> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/debugfs.h> +#include <linux/bitops.h> +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/reset.h> +#include <linux/thermal.h> + +#include <dt-bindings/thermal/tegra124-soctherm.h> + +#include "../thermal_core.h" +#include "soctherm.h" + +#define SENSOR_CONFIG0 0 +#define SENSOR_CONFIG0_STOP BIT(0) +#define SENSOR_CONFIG0_CPTR_OVER BIT(2) +#define SENSOR_CONFIG0_OVER BIT(3) +#define SENSOR_CONFIG0_TCALC_OVER BIT(4) +#define SENSOR_CONFIG0_TALL_MASK (0xfffff << 8) +#define SENSOR_CONFIG0_TALL_SHIFT 8 + +#define SENSOR_CONFIG1 4 +#define SENSOR_CONFIG1_TSAMPLE_MASK 0x3ff +#define SENSOR_CONFIG1_TSAMPLE_SHIFT 0 +#define SENSOR_CONFIG1_TIDDQ_EN_MASK (0x3f << 15) +#define SENSOR_CONFIG1_TIDDQ_EN_SHIFT 15 +#define SENSOR_CONFIG1_TEN_COUNT_MASK (0x3f << 24) +#define SENSOR_CONFIG1_TEN_COUNT_SHIFT 24 +#define SENSOR_CONFIG1_TEMP_ENABLE BIT(31) + +/* + * SENSOR_CONFIG2 is defined in soctherm.h + * because, it will be used by tegra_soctherm_fuse.c + */ + +#define SENSOR_STATUS0 0xc +#define SENSOR_STATUS0_VALID_MASK BIT(31) +#define SENSOR_STATUS0_CAPTURE_MASK 0xffff + +#define SENSOR_STATUS1 0x10 +#define SENSOR_STATUS1_TEMP_VALID_MASK BIT(31) +#define SENSOR_STATUS1_TEMP_MASK 0xffff + +#define READBACK_VALUE_MASK 0xff00 +#define READBACK_VALUE_SHIFT 8 +#define READBACK_ADD_HALF BIT(7) +#define READBACK_NEGATE BIT(0) + +/* + * THERMCTL_LEVEL0_GROUP_CPU is defined in soctherm.h + * because it will be used by tegraxxx_soctherm.c + */ +#define THERMCTL_LVL0_CPU0_EN_MASK BIT(8) +#define THERMCTL_LVL0_CPU0_CPU_THROT_MASK (0x3 << 5) +#define THERMCTL_LVL0_CPU0_CPU_THROT_LIGHT 0x1 +#define THERMCTL_LVL0_CPU0_CPU_THROT_HEAVY 0x2 +#define THERMCTL_LVL0_CPU0_GPU_THROT_MASK (0x3 << 3) +#define THERMCTL_LVL0_CPU0_GPU_THROT_LIGHT 0x1 +#define THERMCTL_LVL0_CPU0_GPU_THROT_HEAVY 0x2 +#define THERMCTL_LVL0_CPU0_MEM_THROT_MASK BIT(2) +#define THERMCTL_LVL0_CPU0_STATUS_MASK 0x3 + +#define THERMCTL_LVL0_UP_STATS 0x10 +#define THERMCTL_LVL0_DN_STATS 0x14 + +#define THERMCTL_STATS_CTL 0x94 +#define STATS_CTL_CLR_DN 0x8 +#define STATS_CTL_EN_DN 0x4 +#define STATS_CTL_CLR_UP 0x2 +#define STATS_CTL_EN_UP 0x1 + +#define THROT_GLOBAL_CFG 0x400 +#define THROT_GLOBAL_ENB_MASK BIT(0) + +#define CPU_PSKIP_STATUS 0x418 +#define XPU_PSKIP_STATUS_M_MASK (0xff << 12) +#define XPU_PSKIP_STATUS_N_MASK (0xff << 4) +#define XPU_PSKIP_STATUS_SW_OVERRIDE_MASK BIT(1) +#define XPU_PSKIP_STATUS_ENABLED_MASK BIT(0) + +#define THROT_PRIORITY_LOCK 0x424 +#define THROT_PRIORITY_LOCK_PRIORITY_MASK 0xff + +#define THROT_STATUS 0x428 +#define THROT_STATUS_BREACH_MASK BIT(12) +#define THROT_STATUS_STATE_MASK (0xff << 4) +#define THROT_STATUS_ENABLED_MASK BIT(0) + +#define THROT_PSKIP_CTRL_LITE_CPU 0x430 +#define THROT_PSKIP_CTRL_ENABLE_MASK BIT(31) +#define THROT_PSKIP_CTRL_DIVIDEND_MASK (0xff << 8) +#define THROT_PSKIP_CTRL_DIVISOR_MASK 0xff +#define THROT_PSKIP_CTRL_VECT_GPU_MASK (0x7 << 16) +#define THROT_PSKIP_CTRL_VECT_CPU_MASK (0x7 << 8) +#define THROT_PSKIP_CTRL_VECT2_CPU_MASK 0x7 + +#define THROT_VECT_NONE 0x0 /* 3'b000 */ +#define THROT_VECT_LOW 0x1 /* 3'b001 */ +#define THROT_VECT_MED 0x3 /* 3'b011 */ +#define THROT_VECT_HIGH 0x7 /* 3'b111 */ + +#define THROT_PSKIP_RAMP_LITE_CPU 0x434 +#define THROT_PSKIP_RAMP_SEQ_BYPASS_MODE_MASK BIT(31) +#define THROT_PSKIP_RAMP_DURATION_MASK (0xffff << 8) +#define THROT_PSKIP_RAMP_STEP_MASK 0xff + +#define THROT_PRIORITY_LITE 0x444 +#define THROT_PRIORITY_LITE_PRIO_MASK 0xff + +#define THROT_DELAY_LITE 0x448 +#define THROT_DELAY_LITE_DELAY_MASK 0xff + +/* car register offsets needed for enabling HW throttling */ +#define CAR_SUPER_CCLKG_DIVIDER 0x36c +#define CDIVG_USE_THERM_CONTROLS_MASK BIT(30) + +/* ccroc register offsets needed for enabling HW throttling for Tegra132 */ +#define CCROC_SUPER_CCLKG_DIVIDER 0x024 + +#define CCROC_GLOBAL_CFG 0x148 + +#define CCROC_THROT_PSKIP_RAMP_CPU 0x150 +#define CCROC_THROT_PSKIP_RAMP_SEQ_BYPASS_MODE_MASK BIT(31) +#define CCROC_THROT_PSKIP_RAMP_DURATION_MASK (0xffff << 8) +#define CCROC_THROT_PSKIP_RAMP_STEP_MASK 0xff + +#define CCROC_THROT_PSKIP_CTRL_CPU 0x154 +#define CCROC_THROT_PSKIP_CTRL_ENB_MASK BIT(31) +#define CCROC_THROT_PSKIP_CTRL_DIVIDEND_MASK (0xff << 8) +#define CCROC_THROT_PSKIP_CTRL_DIVISOR_MASK 0xff + +/* get val from register(r) mask bits(m) */ +#define REG_GET_MASK(r, m) (((r) & (m)) >> (ffs(m) - 1)) +/* set val(v) to mask bits(m) of register(r) */ +#define REG_SET_MASK(r, m, v) (((r) & ~(m)) | \ + (((v) & (m >> (ffs(m) - 1))) << (ffs(m) - 1))) + +/* get dividend from the depth */ +#define THROT_DEPTH_DIVIDEND(depth) ((256 * (100 - (depth)) / 100) - 1) + +/* get THROT_PSKIP_xxx offset per LIGHT/HEAVY throt and CPU/GPU dev */ +#define THROT_OFFSET 0x30 +#define THROT_PSKIP_CTRL(throt, dev) (THROT_PSKIP_CTRL_LITE_CPU + \ + (THROT_OFFSET * throt) + (8 * dev)) +#define THROT_PSKIP_RAMP(throt, dev) (THROT_PSKIP_RAMP_LITE_CPU + \ + (THROT_OFFSET * throt) + (8 * dev)) + +/* get THROT_xxx_CTRL offset per LIGHT/HEAVY throt */ +#define THROT_PRIORITY_CTRL(throt) (THROT_PRIORITY_LITE + \ + (THROT_OFFSET * throt)) +#define THROT_DELAY_CTRL(throt) (THROT_DELAY_LITE + \ + (THROT_OFFSET * throt)) + +/* get CCROC_THROT_PSKIP_xxx offset per HIGH/MED/LOW vect*/ +#define CCROC_THROT_OFFSET 0x0c +#define CCROC_THROT_PSKIP_CTRL_CPU_REG(vect) (CCROC_THROT_PSKIP_CTRL_CPU + \ + (CCROC_THROT_OFFSET * vect)) +#define CCROC_THROT_PSKIP_RAMP_CPU_REG(vect) (CCROC_THROT_PSKIP_RAMP_CPU + \ + (CCROC_THROT_OFFSET * vect)) + +/* get THERMCTL_LEVELx offset per CPU/GPU/MEM/TSENSE rg and LEVEL0~3 lv */ +#define THERMCTL_LVL_REGS_SIZE 0x20 +#define THERMCTL_LVL_REG(rg, lv) ((rg) + ((lv) * THERMCTL_LVL_REGS_SIZE)) + +static const int min_low_temp = -127000; +static const int max_high_temp = 127000; + +enum soctherm_throttle_id { + THROTTLE_LIGHT = 0, + THROTTLE_HEAVY, + THROTTLE_SIZE, +}; + +enum soctherm_throttle_dev_id { + THROTTLE_DEV_CPU = 0, + THROTTLE_DEV_GPU, + THROTTLE_DEV_SIZE, +}; + +static const char *const throt_names[] = { + [THROTTLE_LIGHT] = "light", + [THROTTLE_HEAVY] = "heavy", +}; + +struct tegra_soctherm; +struct tegra_thermctl_zone { + void __iomem *reg; + struct device *dev; + struct tegra_soctherm *ts; + struct thermal_zone_device *tz; + const struct tegra_tsensor_group *sg; +}; + +struct soctherm_throt_cfg { + const char *name; + unsigned int id; + u8 priority; + u8 cpu_throt_level; + u32 cpu_throt_depth; + struct thermal_cooling_device *cdev; + bool init; +}; + +struct tegra_soctherm { + struct reset_control *reset; + struct clk *clock_tsensor; + struct clk *clock_soctherm; + void __iomem *regs; + void __iomem *clk_regs; + void __iomem *ccroc_regs; + + u32 *calib; + struct thermal_zone_device **thermctl_tzs; + struct tegra_soctherm_soc *soc; + + struct soctherm_throt_cfg throt_cfgs[THROTTLE_SIZE]; + + struct dentry *debugfs_dir; +}; + +/** + * ccroc_writel() - writes a value to a CCROC register + * @ts: pointer to a struct tegra_soctherm + * @v: the value to write + * @reg: the register offset + * + * Writes @v to @reg. No return value. + */ +static inline void ccroc_writel(struct tegra_soctherm *ts, u32 value, u32 reg) +{ + writel(value, (ts->ccroc_regs + reg)); +} + +/** + * ccroc_readl() - reads specified register from CCROC IP block + * @ts: pointer to a struct tegra_soctherm + * @reg: register address to be read + * + * Return: the value of the register + */ +static inline u32 ccroc_readl(struct tegra_soctherm *ts, u32 reg) +{ + return readl(ts->ccroc_regs + reg); +} + +static void enable_tsensor(struct tegra_soctherm *tegra, unsigned int i) +{ + const struct tegra_tsensor *sensor = &tegra->soc->tsensors[i]; + void __iomem *base = tegra->regs + sensor->base; + unsigned int val; + + val = sensor->config->tall << SENSOR_CONFIG0_TALL_SHIFT; + writel(val, base + SENSOR_CONFIG0); + + val = (sensor->config->tsample - 1) << SENSOR_CONFIG1_TSAMPLE_SHIFT; + val |= sensor->config->tiddq_en << SENSOR_CONFIG1_TIDDQ_EN_SHIFT; + val |= sensor->config->ten_count << SENSOR_CONFIG1_TEN_COUNT_SHIFT; + val |= SENSOR_CONFIG1_TEMP_ENABLE; + writel(val, base + SENSOR_CONFIG1); + + writel(tegra->calib[i], base + SENSOR_CONFIG2); +} + +/* + * Translate from soctherm readback format to millicelsius. + * The soctherm readback format in bits is as follows: + * TTTTTTTT H______N + * where T's contain the temperature in Celsius, + * H denotes an addition of 0.5 Celsius and N denotes negation + * of the final value. + */ +static int translate_temp(u16 val) +{ + int t; + + t = ((val & READBACK_VALUE_MASK) >> READBACK_VALUE_SHIFT) * 1000; + if (val & READBACK_ADD_HALF) + t += 500; + if (val & READBACK_NEGATE) + t *= -1; + + return t; +} + +static int tegra_thermctl_get_temp(void *data, int *out_temp) +{ + struct tegra_thermctl_zone *zone = data; + u32 val; + + val = readl(zone->reg); + val = REG_GET_MASK(val, zone->sg->sensor_temp_mask); + *out_temp = translate_temp(val); + + return 0; +} + +/** + * enforce_temp_range() - check and enforce temperature range [min, max] + * @trip_temp: the trip temperature to check + * + * Checks and enforces the permitted temperature range that SOC_THERM + * HW can support This is + * done while taking care of precision. + * + * Return: The precision adjusted capped temperature in millicelsius. + */ +static int enforce_temp_range(struct device *dev, int trip_temp) +{ + int temp; + + temp = clamp_val(trip_temp, min_low_temp, max_high_temp); + if (temp != trip_temp) + dev_info(dev, "soctherm: trip temperature %d forced to %d\n", + trip_temp, temp); + return temp; +} + +/** + * thermtrip_program() - Configures the hardware to shut down the + * system if a given sensor group reaches a given temperature + * @dev: ptr to the struct device for the SOC_THERM IP block + * @sg: pointer to the sensor group to set the thermtrip temperature for + * @trip_temp: the temperature in millicelsius to trigger the thermal trip at + * + * Sets the thermal trip threshold of the given sensor group to be the + * @trip_temp. If this threshold is crossed, the hardware will shut + * down. + * + * Note that, although @trip_temp is specified in millicelsius, the + * hardware is programmed in degrees Celsius. + * + * Return: 0 upon success, or %-EINVAL upon failure. + */ +static int thermtrip_program(struct device *dev, + const struct tegra_tsensor_group *sg, + int trip_temp) +{ + struct tegra_soctherm *ts = dev_get_drvdata(dev); + int temp; + u32 r; + + if (!sg || !sg->thermtrip_threshold_mask) + return -EINVAL; + + temp = enforce_temp_range(dev, trip_temp) / ts->soc->thresh_grain; + + r = readl(ts->regs + THERMCTL_THERMTRIP_CTL); + r = REG_SET_MASK(r, sg->thermtrip_threshold_mask, temp); + r = REG_SET_MASK(r, sg->thermtrip_enable_mask, 1); + r = REG_SET_MASK(r, sg->thermtrip_any_en_mask, 0); + writel(r, ts->regs + THERMCTL_THERMTRIP_CTL); + + return 0; +} + +/** + * throttrip_program() - Configures the hardware to throttle the + * pulse if a given sensor group reaches a given temperature + * @dev: ptr to the struct device for the SOC_THERM IP block + * @sg: pointer to the sensor group to set the thermtrip temperature for + * @stc: pointer to the throttle need to be triggered + * @trip_temp: the temperature in millicelsius to trigger the thermal trip at + * + * Sets the thermal trip threshold and throttle event of the given sensor + * group. If this threshold is crossed, the hardware will trigger the + * throttle. + * + * Note that, although @trip_temp is specified in millicelsius, the + * hardware is programmed in degrees Celsius. + * + * Return: 0 upon success, or %-EINVAL upon failure. + */ +static int throttrip_program(struct device *dev, + const struct tegra_tsensor_group *sg, + struct soctherm_throt_cfg *stc, + int trip_temp) +{ + struct tegra_soctherm *ts = dev_get_drvdata(dev); + int temp, cpu_throt, gpu_throt; + unsigned int throt; + u32 r, reg_off; + + if (!sg || !stc || !stc->init) + return -EINVAL; + + temp = enforce_temp_range(dev, trip_temp) / ts->soc->thresh_grain; + + /* Hardcode LIGHT on LEVEL1 and HEAVY on LEVEL2 */ + throt = stc->id; + reg_off = THERMCTL_LVL_REG(sg->thermctl_lvl0_offset, throt + 1); + + if (throt == THROTTLE_LIGHT) { + cpu_throt = THERMCTL_LVL0_CPU0_CPU_THROT_LIGHT; + gpu_throt = THERMCTL_LVL0_CPU0_GPU_THROT_LIGHT; + } else { + cpu_throt = THERMCTL_LVL0_CPU0_CPU_THROT_HEAVY; + gpu_throt = THERMCTL_LVL0_CPU0_GPU_THROT_HEAVY; + if (throt != THROTTLE_HEAVY) + dev_warn(dev, + "invalid throt id %d - assuming HEAVY", + throt); + } + + r = readl(ts->regs + reg_off); + r = REG_SET_MASK(r, sg->thermctl_lvl0_up_thresh_mask, temp); + r = REG_SET_MASK(r, sg->thermctl_lvl0_dn_thresh_mask, temp); + r = REG_SET_MASK(r, THERMCTL_LVL0_CPU0_CPU_THROT_MASK, cpu_throt); + r = REG_SET_MASK(r, THERMCTL_LVL0_CPU0_GPU_THROT_MASK, gpu_throt); + r = REG_SET_MASK(r, THERMCTL_LVL0_CPU0_EN_MASK, 1); + writel(r, ts->regs + reg_off); + + return 0; +} + +static struct soctherm_throt_cfg * +find_throttle_cfg_by_name(struct tegra_soctherm *ts, const char *name) +{ + unsigned int i; + + for (i = 0; ts->throt_cfgs[i].name; i++) + if (!strcmp(ts->throt_cfgs[i].name, name)) + return &ts->throt_cfgs[i]; + + return NULL; +} + +static int tegra_thermctl_set_trip_temp(void *data, int trip, int temp) +{ + struct tegra_thermctl_zone *zone = data; + struct thermal_zone_device *tz = zone->tz; + struct tegra_soctherm *ts = zone->ts; + const struct tegra_tsensor_group *sg = zone->sg; + struct device *dev = zone->dev; + enum thermal_trip_type type; + int ret; + + if (!tz) + return -EINVAL; + + ret = tz->ops->get_trip_type(tz, trip, &type); + if (ret) + return ret; + + if (type == THERMAL_TRIP_CRITICAL) { + return thermtrip_program(dev, sg, temp); + } else if (type == THERMAL_TRIP_HOT) { + int i; + + for (i = 0; i < THROTTLE_SIZE; i++) { + struct thermal_cooling_device *cdev; + struct soctherm_throt_cfg *stc; + + if (!ts->throt_cfgs[i].init) + continue; + + cdev = ts->throt_cfgs[i].cdev; + if (get_thermal_instance(tz, cdev, trip)) + stc = find_throttle_cfg_by_name(ts, cdev->type); + else + continue; + + return throttrip_program(dev, sg, stc, temp); + } + } + + return 0; +} + +static const struct thermal_zone_of_device_ops tegra_of_thermal_ops = { + .get_temp = tegra_thermctl_get_temp, + .set_trip_temp = tegra_thermctl_set_trip_temp, +}; + +static int get_hot_temp(struct thermal_zone_device *tz, int *trip, int *temp) +{ + int ntrips, i, ret; + enum thermal_trip_type type; + + ntrips = of_thermal_get_ntrips(tz); + if (ntrips <= 0) + return -EINVAL; + + for (i = 0; i < ntrips; i++) { + ret = tz->ops->get_trip_type(tz, i, &type); + if (ret) + return -EINVAL; + if (type == THERMAL_TRIP_HOT) { + ret = tz->ops->get_trip_temp(tz, i, temp); + if (!ret) + *trip = i; + + return ret; + } + } + + return -EINVAL; +} + +/** + * tegra_soctherm_set_hwtrips() - set HW trip point from DT data + * @dev: struct device * of the SOC_THERM instance + * + * Configure the SOC_THERM HW trip points, setting "THERMTRIP" + * "THROTTLE" trip points , using "critical" or "hot" type trip_temp + * from thermal zone. + * After they have been configured, THERMTRIP or THROTTLE will take + * action when the configured SoC thermal sensor group reaches a + * certain temperature. + * + * Return: 0 upon success, or a negative error code on failure. + * "Success" does not mean that trips was enabled; it could also + * mean that no node was found in DT. + * THERMTRIP has been enabled successfully when a message similar to + * this one appears on the serial console: + * "thermtrip: will shut down when sensor group XXX reaches YYYYYY mC" + * THROTTLE has been enabled successfully when a message similar to + * this one appears on the serial console: + * ""throttrip: will throttle when sensor group XXX reaches YYYYYY mC" + */ +static int tegra_soctherm_set_hwtrips(struct device *dev, + const struct tegra_tsensor_group *sg, + struct thermal_zone_device *tz) +{ + struct tegra_soctherm *ts = dev_get_drvdata(dev); + struct soctherm_throt_cfg *stc; + int i, trip, temperature; + int ret; + + ret = tz->ops->get_crit_temp(tz, &temperature); + if (ret) { + dev_warn(dev, "thermtrip: %s: missing critical temperature\n", + sg->name); + goto set_throttle; + } + + ret = thermtrip_program(dev, sg, temperature); + if (ret) { + dev_err(dev, "thermtrip: %s: error during enable\n", + sg->name); + return ret; + } + + dev_info(dev, + "thermtrip: will shut down when %s reaches %d mC\n", + sg->name, temperature); + +set_throttle: + ret = get_hot_temp(tz, &trip, &temperature); + if (ret) { + dev_warn(dev, "throttrip: %s: missing hot temperature\n", + sg->name); + return 0; + } + + for (i = 0; i < THROTTLE_SIZE; i++) { + struct thermal_cooling_device *cdev; + + if (!ts->throt_cfgs[i].init) + continue; + + cdev = ts->throt_cfgs[i].cdev; + if (get_thermal_instance(tz, cdev, trip)) + stc = find_throttle_cfg_by_name(ts, cdev->type); + else + continue; + + ret = throttrip_program(dev, sg, stc, temperature); + if (ret) { + dev_err(dev, "throttrip: %s: error during enable\n", + sg->name); + return ret; + } + + dev_info(dev, + "throttrip: will throttle when %s reaches %d mC\n", + sg->name, temperature); + break; + } + + if (i == THROTTLE_SIZE) + dev_warn(dev, "throttrip: %s: missing throttle cdev\n", + sg->name); + + return 0; +} + +#ifdef CONFIG_DEBUG_FS +static int regs_show(struct seq_file *s, void *data) +{ + struct platform_device *pdev = s->private; + struct tegra_soctherm *ts = platform_get_drvdata(pdev); + const struct tegra_tsensor *tsensors = ts->soc->tsensors; + const struct tegra_tsensor_group **ttgs = ts->soc->ttgs; + u32 r, state; + int i, level; + + seq_puts(s, "-----TSENSE (convert HW)-----\n"); + + for (i = 0; i < ts->soc->num_tsensors; i++) { + r = readl(ts->regs + tsensors[i].base + SENSOR_CONFIG1); + state = REG_GET_MASK(r, SENSOR_CONFIG1_TEMP_ENABLE); + + seq_printf(s, "%s: ", tsensors[i].name); + seq_printf(s, "En(%d) ", state); + + if (!state) { + seq_puts(s, "\n"); + continue; + } + + state = REG_GET_MASK(r, SENSOR_CONFIG1_TIDDQ_EN_MASK); + seq_printf(s, "tiddq(%d) ", state); + state = REG_GET_MASK(r, SENSOR_CONFIG1_TEN_COUNT_MASK); + seq_printf(s, "ten_count(%d) ", state); + state = REG_GET_MASK(r, SENSOR_CONFIG1_TSAMPLE_MASK); + seq_printf(s, "tsample(%d) ", state + 1); + + r = readl(ts->regs + tsensors[i].base + SENSOR_STATUS1); + state = REG_GET_MASK(r, SENSOR_STATUS1_TEMP_VALID_MASK); + seq_printf(s, "Temp(%d/", state); + state = REG_GET_MASK(r, SENSOR_STATUS1_TEMP_MASK); + seq_printf(s, "%d) ", translate_temp(state)); + + r = readl(ts->regs + tsensors[i].base + SENSOR_STATUS0); + state = REG_GET_MASK(r, SENSOR_STATUS0_VALID_MASK); + seq_printf(s, "Capture(%d/", state); + state = REG_GET_MASK(r, SENSOR_STATUS0_CAPTURE_MASK); + seq_printf(s, "%d) ", state); + + r = readl(ts->regs + tsensors[i].base + SENSOR_CONFIG0); + state = REG_GET_MASK(r, SENSOR_CONFIG0_STOP); + seq_printf(s, "Stop(%d) ", state); + state = REG_GET_MASK(r, SENSOR_CONFIG0_TALL_MASK); + seq_printf(s, "Tall(%d) ", state); + state = REG_GET_MASK(r, SENSOR_CONFIG0_TCALC_OVER); + seq_printf(s, "Over(%d/", state); + state = REG_GET_MASK(r, SENSOR_CONFIG0_OVER); + seq_printf(s, "%d/", state); + state = REG_GET_MASK(r, SENSOR_CONFIG0_CPTR_OVER); + seq_printf(s, "%d) ", state); + + r = readl(ts->regs + tsensors[i].base + SENSOR_CONFIG2); + state = REG_GET_MASK(r, SENSOR_CONFIG2_THERMA_MASK); + seq_printf(s, "Therm_A/B(%d/", state); + state = REG_GET_MASK(r, SENSOR_CONFIG2_THERMB_MASK); + seq_printf(s, "%d)\n", (s16)state); + } + + r = readl(ts->regs + SENSOR_PDIV); + seq_printf(s, "PDIV: 0x%x\n", r); + + r = readl(ts->regs + SENSOR_HOTSPOT_OFF); + seq_printf(s, "HOTSPOT: 0x%x\n", r); + + seq_puts(s, "\n"); + seq_puts(s, "-----SOC_THERM-----\n"); + + r = readl(ts->regs + SENSOR_TEMP1); + state = REG_GET_MASK(r, SENSOR_TEMP1_CPU_TEMP_MASK); + seq_printf(s, "Temperatures: CPU(%d) ", translate_temp(state)); + state = REG_GET_MASK(r, SENSOR_TEMP1_GPU_TEMP_MASK); + seq_printf(s, " GPU(%d) ", translate_temp(state)); + r = readl(ts->regs + SENSOR_TEMP2); + state = REG_GET_MASK(r, SENSOR_TEMP2_PLLX_TEMP_MASK); + seq_printf(s, " PLLX(%d) ", translate_temp(state)); + state = REG_GET_MASK(r, SENSOR_TEMP2_MEM_TEMP_MASK); + seq_printf(s, " MEM(%d)\n", translate_temp(state)); + + for (i = 0; i < ts->soc->num_ttgs; i++) { + seq_printf(s, "%s:\n", ttgs[i]->name); + for (level = 0; level < 4; level++) { + s32 v; + u32 mask; + u16 off = ttgs[i]->thermctl_lvl0_offset; + + r = readl(ts->regs + THERMCTL_LVL_REG(off, level)); + + mask = ttgs[i]->thermctl_lvl0_up_thresh_mask; + state = REG_GET_MASK(r, mask); + v = sign_extend32(state, ts->soc->bptt - 1); + v *= ts->soc->thresh_grain; + seq_printf(s, " %d: Up/Dn(%d /", level, v); + + mask = ttgs[i]->thermctl_lvl0_dn_thresh_mask; + state = REG_GET_MASK(r, mask); + v = sign_extend32(state, ts->soc->bptt - 1); + v *= ts->soc->thresh_grain; + seq_printf(s, "%d ) ", v); + + mask = THERMCTL_LVL0_CPU0_EN_MASK; + state = REG_GET_MASK(r, mask); + seq_printf(s, "En(%d) ", state); + + mask = THERMCTL_LVL0_CPU0_CPU_THROT_MASK; + state = REG_GET_MASK(r, mask); + seq_puts(s, "CPU Throt"); + if (!state) + seq_printf(s, "(%s) ", "none"); + else if (state == THERMCTL_LVL0_CPU0_CPU_THROT_LIGHT) + seq_printf(s, "(%s) ", "L"); + else if (state == THERMCTL_LVL0_CPU0_CPU_THROT_HEAVY) + seq_printf(s, "(%s) ", "H"); + else + seq_printf(s, "(%s) ", "H+L"); + + mask = THERMCTL_LVL0_CPU0_GPU_THROT_MASK; + state = REG_GET_MASK(r, mask); + seq_puts(s, "GPU Throt"); + if (!state) + seq_printf(s, "(%s) ", "none"); + else if (state == THERMCTL_LVL0_CPU0_GPU_THROT_LIGHT) + seq_printf(s, "(%s) ", "L"); + else if (state == THERMCTL_LVL0_CPU0_GPU_THROT_HEAVY) + seq_printf(s, "(%s) ", "H"); + else + seq_printf(s, "(%s) ", "H+L"); + + mask = THERMCTL_LVL0_CPU0_STATUS_MASK; + state = REG_GET_MASK(r, mask); + seq_printf(s, "Status(%s)\n", + state == 0 ? "LO" : + state == 1 ? "In" : + state == 2 ? "Res" : "HI"); + } + } + + r = readl(ts->regs + THERMCTL_STATS_CTL); + seq_printf(s, "STATS: Up(%s) Dn(%s)\n", + r & STATS_CTL_EN_UP ? "En" : "--", + r & STATS_CTL_EN_DN ? "En" : "--"); + + for (level = 0; level < 4; level++) { + u16 off; + + off = THERMCTL_LVL0_UP_STATS; + r = readl(ts->regs + THERMCTL_LVL_REG(off, level)); + seq_printf(s, " Level_%d Up(%d) ", level, r); + + off = THERMCTL_LVL0_DN_STATS; + r = readl(ts->regs + THERMCTL_LVL_REG(off, level)); + seq_printf(s, "Dn(%d)\n", r); + } + + r = readl(ts->regs + THERMCTL_THERMTRIP_CTL); + state = REG_GET_MASK(r, ttgs[0]->thermtrip_any_en_mask); + seq_printf(s, "Thermtrip Any En(%d)\n", state); + for (i = 0; i < ts->soc->num_ttgs; i++) { + state = REG_GET_MASK(r, ttgs[i]->thermtrip_enable_mask); + seq_printf(s, " %s En(%d) ", ttgs[i]->name, state); + state = REG_GET_MASK(r, ttgs[i]->thermtrip_threshold_mask); + state *= ts->soc->thresh_grain; + seq_printf(s, "Thresh(%d)\n", state); + } + + r = readl(ts->regs + THROT_GLOBAL_CFG); + seq_puts(s, "\n"); + seq_printf(s, "GLOBAL THROTTLE CONFIG: 0x%08x\n", r); + + seq_puts(s, "---------------------------------------------------\n"); + r = readl(ts->regs + THROT_STATUS); + state = REG_GET_MASK(r, THROT_STATUS_BREACH_MASK); + seq_printf(s, "THROT STATUS: breach(%d) ", state); + state = REG_GET_MASK(r, THROT_STATUS_STATE_MASK); + seq_printf(s, "state(%d) ", state); + state = REG_GET_MASK(r, THROT_STATUS_ENABLED_MASK); + seq_printf(s, "enabled(%d)\n", state); + + r = readl(ts->regs + CPU_PSKIP_STATUS); + if (ts->soc->use_ccroc) { + state = REG_GET_MASK(r, XPU_PSKIP_STATUS_ENABLED_MASK); + seq_printf(s, "CPU PSKIP STATUS: enabled(%d)\n", state); + } else { + state = REG_GET_MASK(r, XPU_PSKIP_STATUS_M_MASK); + seq_printf(s, "CPU PSKIP STATUS: M(%d) ", state); + state = REG_GET_MASK(r, XPU_PSKIP_STATUS_N_MASK); + seq_printf(s, "N(%d) ", state); + state = REG_GET_MASK(r, XPU_PSKIP_STATUS_ENABLED_MASK); + seq_printf(s, "enabled(%d)\n", state); + } + + return 0; +} + +static int regs_open(struct inode *inode, struct file *file) +{ + return single_open(file, regs_show, inode->i_private); +} + +static const struct file_operations regs_fops = { + .open = regs_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static void soctherm_debug_init(struct platform_device *pdev) +{ + struct tegra_soctherm *tegra = platform_get_drvdata(pdev); + struct dentry *root, *file; + + root = debugfs_create_dir("soctherm", NULL); + if (!root) { + dev_err(&pdev->dev, "failed to create debugfs directory\n"); + return; + } + + tegra->debugfs_dir = root; + + file = debugfs_create_file("reg_contents", 0644, root, + pdev, ®s_fops); + if (!file) { + dev_err(&pdev->dev, "failed to create debugfs file\n"); + debugfs_remove_recursive(tegra->debugfs_dir); + tegra->debugfs_dir = NULL; + } +} +#else +static inline void soctherm_debug_init(struct platform_device *pdev) {} +#endif + +static int soctherm_clk_enable(struct platform_device *pdev, bool enable) +{ + struct tegra_soctherm *tegra = platform_get_drvdata(pdev); + int err; + + if (!tegra->clock_soctherm || !tegra->clock_tsensor) + return -EINVAL; + + reset_control_assert(tegra->reset); + + if (enable) { + err = clk_prepare_enable(tegra->clock_soctherm); + if (err) { + reset_control_deassert(tegra->reset); + return err; + } + + err = clk_prepare_enable(tegra->clock_tsensor); + if (err) { + clk_disable_unprepare(tegra->clock_soctherm); + reset_control_deassert(tegra->reset); + return err; + } + } else { + clk_disable_unprepare(tegra->clock_tsensor); + clk_disable_unprepare(tegra->clock_soctherm); + } + + reset_control_deassert(tegra->reset); + + return 0; +} + +static int throt_get_cdev_max_state(struct thermal_cooling_device *cdev, + unsigned long *max_state) +{ + *max_state = 1; + return 0; +} + +static int throt_get_cdev_cur_state(struct thermal_cooling_device *cdev, + unsigned long *cur_state) +{ + struct tegra_soctherm *ts = cdev->devdata; + u32 r; + + r = readl(ts->regs + THROT_STATUS); + if (REG_GET_MASK(r, THROT_STATUS_STATE_MASK)) + *cur_state = 1; + else + *cur_state = 0; + + return 0; +} + +static int throt_set_cdev_state(struct thermal_cooling_device *cdev, + unsigned long cur_state) +{ + return 0; +} + +static const struct thermal_cooling_device_ops throt_cooling_ops = { + .get_max_state = throt_get_cdev_max_state, + .get_cur_state = throt_get_cdev_cur_state, + .set_cur_state = throt_set_cdev_state, +}; + +/** + * soctherm_init_hw_throt_cdev() - Parse the HW throttle configurations + * and register them as cooling devices. + */ +static void soctherm_init_hw_throt_cdev(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct tegra_soctherm *ts = dev_get_drvdata(dev); + struct device_node *np_stc, *np_stcc; + const char *name; + u32 val; + int i, r; + + for (i = 0; i < THROTTLE_SIZE; i++) { + ts->throt_cfgs[i].name = throt_names[i]; + ts->throt_cfgs[i].id = i; + ts->throt_cfgs[i].init = false; + } + + np_stc = of_get_child_by_name(dev->of_node, "throttle-cfgs"); + if (!np_stc) { + dev_info(dev, + "throttle-cfg: no throttle-cfgs - not enabling\n"); + return; + } + + for_each_child_of_node(np_stc, np_stcc) { + struct soctherm_throt_cfg *stc; + struct thermal_cooling_device *tcd; + + name = np_stcc->name; + stc = find_throttle_cfg_by_name(ts, name); + if (!stc) { + dev_err(dev, + "throttle-cfg: could not find %s\n", name); + continue; + } + + r = of_property_read_u32(np_stcc, "nvidia,priority", &val); + if (r) { + dev_info(dev, + "throttle-cfg: %s: missing priority\n", name); + continue; + } + stc->priority = val; + + if (ts->soc->use_ccroc) { + r = of_property_read_u32(np_stcc, + "nvidia,cpu-throt-level", + &val); + if (r) { + dev_info(dev, + "throttle-cfg: %s: missing cpu-throt-level\n", + name); + continue; + } + stc->cpu_throt_level = val; + } else { + r = of_property_read_u32(np_stcc, + "nvidia,cpu-throt-percent", + &val); + if (r) { + dev_info(dev, + "throttle-cfg: %s: missing cpu-throt-percent\n", + name); + continue; + } + stc->cpu_throt_depth = val; + } + + tcd = thermal_of_cooling_device_register(np_stcc, + (char *)name, ts, + &throt_cooling_ops); + of_node_put(np_stcc); + if (IS_ERR_OR_NULL(tcd)) { + dev_err(dev, + "throttle-cfg: %s: failed to register cooling device\n", + name); + continue; + } + + stc->cdev = tcd; + stc->init = true; + } + + of_node_put(np_stc); +} + +/** + * throttlectl_cpu_level_cfg() - programs CCROC NV_THERM level config + * @level: describing the level LOW/MED/HIGH of throttling + * + * It's necessary to set up the CPU-local CCROC NV_THERM instance with + * the M/N values desired for each level. This function does this. + * + * This function pre-programs the CCROC NV_THERM levels in terms of + * pre-configured "Low", "Medium" or "Heavy" throttle levels which are + * mapped to THROT_LEVEL_LOW, THROT_LEVEL_MED and THROT_LEVEL_HVY. + */ +static void throttlectl_cpu_level_cfg(struct tegra_soctherm *ts, int level) +{ + u8 depth, dividend; + u32 r; + + switch (level) { + case TEGRA_SOCTHERM_THROT_LEVEL_LOW: + depth = 50; + break; + case TEGRA_SOCTHERM_THROT_LEVEL_MED: + depth = 75; + break; + case TEGRA_SOCTHERM_THROT_LEVEL_HIGH: + depth = 80; + break; + case TEGRA_SOCTHERM_THROT_LEVEL_NONE: + return; + default: + return; + } + + dividend = THROT_DEPTH_DIVIDEND(depth); + + /* setup PSKIP in ccroc nv_therm registers */ + r = ccroc_readl(ts, CCROC_THROT_PSKIP_RAMP_CPU_REG(level)); + r = REG_SET_MASK(r, CCROC_THROT_PSKIP_RAMP_DURATION_MASK, 0xff); + r = REG_SET_MASK(r, CCROC_THROT_PSKIP_RAMP_STEP_MASK, 0xf); + ccroc_writel(ts, r, CCROC_THROT_PSKIP_RAMP_CPU_REG(level)); + + r = ccroc_readl(ts, CCROC_THROT_PSKIP_CTRL_CPU_REG(level)); + r = REG_SET_MASK(r, CCROC_THROT_PSKIP_CTRL_ENB_MASK, 1); + r = REG_SET_MASK(r, CCROC_THROT_PSKIP_CTRL_DIVIDEND_MASK, dividend); + r = REG_SET_MASK(r, CCROC_THROT_PSKIP_CTRL_DIVISOR_MASK, 0xff); + ccroc_writel(ts, r, CCROC_THROT_PSKIP_CTRL_CPU_REG(level)); +} + +/** + * throttlectl_cpu_level_select() - program CPU pulse skipper config + * @throt: the LIGHT/HEAVY of throttle event id + * + * Pulse skippers are used to throttle clock frequencies. This + * function programs the pulse skippers based on @throt and platform + * data. This function is used on SoCs which have CPU-local pulse + * skipper control, such as T13x. It programs soctherm's interface to + * Denver:CCROC NV_THERM in terms of Low, Medium and HIGH throttling + * vectors. PSKIP_BYPASS mode is set as required per HW spec. + */ +static void throttlectl_cpu_level_select(struct tegra_soctherm *ts, + enum soctherm_throttle_id throt) +{ + u32 r, throt_vect; + + /* Denver:CCROC NV_THERM interface N:3 Mapping */ + switch (ts->throt_cfgs[throt].cpu_throt_level) { + case TEGRA_SOCTHERM_THROT_LEVEL_LOW: + throt_vect = THROT_VECT_LOW; + break; + case TEGRA_SOCTHERM_THROT_LEVEL_MED: + throt_vect = THROT_VECT_MED; + break; + case TEGRA_SOCTHERM_THROT_LEVEL_HIGH: + throt_vect = THROT_VECT_HIGH; + break; + default: + throt_vect = THROT_VECT_NONE; + break; + } + + r = readl(ts->regs + THROT_PSKIP_CTRL(throt, THROTTLE_DEV_CPU)); + r = REG_SET_MASK(r, THROT_PSKIP_CTRL_ENABLE_MASK, 1); + r = REG_SET_MASK(r, THROT_PSKIP_CTRL_VECT_CPU_MASK, throt_vect); + r = REG_SET_MASK(r, THROT_PSKIP_CTRL_VECT2_CPU_MASK, throt_vect); + writel(r, ts->regs + THROT_PSKIP_CTRL(throt, THROTTLE_DEV_CPU)); + + /* bypass sequencer in soc_therm as it is programmed in ccroc */ + r = REG_SET_MASK(0, THROT_PSKIP_RAMP_SEQ_BYPASS_MODE_MASK, 1); + writel(r, ts->regs + THROT_PSKIP_RAMP(throt, THROTTLE_DEV_CPU)); +} + +/** + * throttlectl_cpu_mn() - program CPU pulse skipper configuration + * @throt: the LIGHT/HEAVY of throttle event id + * + * Pulse skippers are used to throttle clock frequencies. This + * function programs the pulse skippers based on @throt and platform + * data. This function is used for CPUs that have "remote" pulse + * skipper control, e.g., the CPU pulse skipper is controlled by the + * SOC_THERM IP block. (SOC_THERM is located outside the CPU + * complex.) + */ +static void throttlectl_cpu_mn(struct tegra_soctherm *ts, + enum soctherm_throttle_id throt) +{ + u32 r; + int depth; + u8 dividend; + + depth = ts->throt_cfgs[throt].cpu_throt_depth; + dividend = THROT_DEPTH_DIVIDEND(depth); + + r = readl(ts->regs + THROT_PSKIP_CTRL(throt, THROTTLE_DEV_CPU)); + r = REG_SET_MASK(r, THROT_PSKIP_CTRL_ENABLE_MASK, 1); + r = REG_SET_MASK(r, THROT_PSKIP_CTRL_DIVIDEND_MASK, dividend); + r = REG_SET_MASK(r, THROT_PSKIP_CTRL_DIVISOR_MASK, 0xff); + writel(r, ts->regs + THROT_PSKIP_CTRL(throt, THROTTLE_DEV_CPU)); + + r = readl(ts->regs + THROT_PSKIP_RAMP(throt, THROTTLE_DEV_CPU)); + r = REG_SET_MASK(r, THROT_PSKIP_RAMP_DURATION_MASK, 0xff); + r = REG_SET_MASK(r, THROT_PSKIP_RAMP_STEP_MASK, 0xf); + writel(r, ts->regs + THROT_PSKIP_RAMP(throt, THROTTLE_DEV_CPU)); +} + +/** + * soctherm_throttle_program() - programs pulse skippers' configuration + * @throt: the LIGHT/HEAVY of the throttle event id. + * + * Pulse skippers are used to throttle clock frequencies. + * This function programs the pulse skippers. + */ +static void soctherm_throttle_program(struct tegra_soctherm *ts, + enum soctherm_throttle_id throt) +{ + u32 r; + struct soctherm_throt_cfg stc = ts->throt_cfgs[throt]; + + if (!stc.init) + return; + + /* Setup PSKIP parameters */ + if (ts->soc->use_ccroc) + throttlectl_cpu_level_select(ts, throt); + else + throttlectl_cpu_mn(ts, throt); + + r = REG_SET_MASK(0, THROT_PRIORITY_LITE_PRIO_MASK, stc.priority); + writel(r, ts->regs + THROT_PRIORITY_CTRL(throt)); + + r = REG_SET_MASK(0, THROT_DELAY_LITE_DELAY_MASK, 0); + writel(r, ts->regs + THROT_DELAY_CTRL(throt)); + + r = readl(ts->regs + THROT_PRIORITY_LOCK); + r = REG_GET_MASK(r, THROT_PRIORITY_LOCK_PRIORITY_MASK); + if (r >= stc.priority) + return; + r = REG_SET_MASK(0, THROT_PRIORITY_LOCK_PRIORITY_MASK, + stc.priority); + writel(r, ts->regs + THROT_PRIORITY_LOCK); +} + +static void tegra_soctherm_throttle(struct device *dev) +{ + struct tegra_soctherm *ts = dev_get_drvdata(dev); + u32 v; + int i; + + /* configure LOW, MED and HIGH levels for CCROC NV_THERM */ + if (ts->soc->use_ccroc) { + throttlectl_cpu_level_cfg(ts, TEGRA_SOCTHERM_THROT_LEVEL_LOW); + throttlectl_cpu_level_cfg(ts, TEGRA_SOCTHERM_THROT_LEVEL_MED); + throttlectl_cpu_level_cfg(ts, TEGRA_SOCTHERM_THROT_LEVEL_HIGH); + } + + /* Thermal HW throttle programming */ + for (i = 0; i < THROTTLE_SIZE; i++) + soctherm_throttle_program(ts, i); + + v = REG_SET_MASK(0, THROT_GLOBAL_ENB_MASK, 1); + if (ts->soc->use_ccroc) { + ccroc_writel(ts, v, CCROC_GLOBAL_CFG); + + v = ccroc_readl(ts, CCROC_SUPER_CCLKG_DIVIDER); + v = REG_SET_MASK(v, CDIVG_USE_THERM_CONTROLS_MASK, 1); + ccroc_writel(ts, v, CCROC_SUPER_CCLKG_DIVIDER); + } else { + writel(v, ts->regs + THROT_GLOBAL_CFG); + + v = readl(ts->clk_regs + CAR_SUPER_CCLKG_DIVIDER); + v = REG_SET_MASK(v, CDIVG_USE_THERM_CONTROLS_MASK, 1); + writel(v, ts->clk_regs + CAR_SUPER_CCLKG_DIVIDER); + } + + /* initialize stats collection */ + v = STATS_CTL_CLR_DN | STATS_CTL_EN_DN | + STATS_CTL_CLR_UP | STATS_CTL_EN_UP; + writel(v, ts->regs + THERMCTL_STATS_CTL); +} + +static void soctherm_init(struct platform_device *pdev) +{ + struct tegra_soctherm *tegra = platform_get_drvdata(pdev); + const struct tegra_tsensor_group **ttgs = tegra->soc->ttgs; + int i; + u32 pdiv, hotspot; + + /* Initialize raw sensors */ + for (i = 0; i < tegra->soc->num_tsensors; ++i) + enable_tsensor(tegra, i); + + /* program pdiv and hotspot offsets per THERM */ + pdiv = readl(tegra->regs + SENSOR_PDIV); + hotspot = readl(tegra->regs + SENSOR_HOTSPOT_OFF); + for (i = 0; i < tegra->soc->num_ttgs; ++i) { + pdiv = REG_SET_MASK(pdiv, ttgs[i]->pdiv_mask, + ttgs[i]->pdiv); + /* hotspot offset from PLLX, doesn't need to configure PLLX */ + if (ttgs[i]->id == TEGRA124_SOCTHERM_SENSOR_PLLX) + continue; + hotspot = REG_SET_MASK(hotspot, + ttgs[i]->pllx_hotspot_mask, + ttgs[i]->pllx_hotspot_diff); + } + writel(pdiv, tegra->regs + SENSOR_PDIV); + writel(hotspot, tegra->regs + SENSOR_HOTSPOT_OFF); + + /* Configure hw throttle */ + tegra_soctherm_throttle(&pdev->dev); +} + +static const struct of_device_id tegra_soctherm_of_match[] = { +#ifdef CONFIG_ARCH_TEGRA_124_SOC + { + .compatible = "nvidia,tegra124-soctherm", + .data = &tegra124_soctherm, + }, +#endif +#ifdef CONFIG_ARCH_TEGRA_132_SOC + { + .compatible = "nvidia,tegra132-soctherm", + .data = &tegra132_soctherm, + }, +#endif +#ifdef CONFIG_ARCH_TEGRA_210_SOC + { + .compatible = "nvidia,tegra210-soctherm", + .data = &tegra210_soctherm, + }, +#endif + { }, +}; +MODULE_DEVICE_TABLE(of, tegra_soctherm_of_match); + +static int tegra_soctherm_probe(struct platform_device *pdev) +{ + const struct of_device_id *match; + struct tegra_soctherm *tegra; + struct thermal_zone_device *z; + struct tsensor_shared_calib shared_calib; + struct resource *res; + struct tegra_soctherm_soc *soc; + unsigned int i; + int err; + + match = of_match_node(tegra_soctherm_of_match, pdev->dev.of_node); + if (!match) + return -ENODEV; + + soc = (struct tegra_soctherm_soc *)match->data; + if (soc->num_ttgs > TEGRA124_SOCTHERM_SENSOR_NUM) + return -EINVAL; + + tegra = devm_kzalloc(&pdev->dev, sizeof(*tegra), GFP_KERNEL); + if (!tegra) + return -ENOMEM; + + dev_set_drvdata(&pdev->dev, tegra); + + tegra->soc = soc; + + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, + "soctherm-reg"); + tegra->regs = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(tegra->regs)) { + dev_err(&pdev->dev, "can't get soctherm registers"); + return PTR_ERR(tegra->regs); + } + + if (!tegra->soc->use_ccroc) { + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, + "car-reg"); + tegra->clk_regs = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(tegra->clk_regs)) { + dev_err(&pdev->dev, "can't get car clk registers"); + return PTR_ERR(tegra->clk_regs); + } + } else { + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, + "ccroc-reg"); + tegra->ccroc_regs = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(tegra->ccroc_regs)) { + dev_err(&pdev->dev, "can't get ccroc registers"); + return PTR_ERR(tegra->ccroc_regs); + } + } + + tegra->reset = devm_reset_control_get(&pdev->dev, "soctherm"); + if (IS_ERR(tegra->reset)) { + dev_err(&pdev->dev, "can't get soctherm reset\n"); + return PTR_ERR(tegra->reset); + } + + tegra->clock_tsensor = devm_clk_get(&pdev->dev, "tsensor"); + if (IS_ERR(tegra->clock_tsensor)) { + dev_err(&pdev->dev, "can't get tsensor clock\n"); + return PTR_ERR(tegra->clock_tsensor); + } + + tegra->clock_soctherm = devm_clk_get(&pdev->dev, "soctherm"); + if (IS_ERR(tegra->clock_soctherm)) { + dev_err(&pdev->dev, "can't get soctherm clock\n"); + return PTR_ERR(tegra->clock_soctherm); + } + + tegra->calib = devm_kcalloc(&pdev->dev, + soc->num_tsensors, sizeof(u32), + GFP_KERNEL); + if (!tegra->calib) + return -ENOMEM; + + /* calculate shared calibration data */ + err = tegra_calc_shared_calib(soc->tfuse, &shared_calib); + if (err) + return err; + + /* calculate tsensor calibaration data */ + for (i = 0; i < soc->num_tsensors; ++i) { + err = tegra_calc_tsensor_calib(&soc->tsensors[i], + &shared_calib, + &tegra->calib[i]); + if (err) + return err; + } + + tegra->thermctl_tzs = devm_kcalloc(&pdev->dev, + soc->num_ttgs, sizeof(*z), + GFP_KERNEL); + if (!tegra->thermctl_tzs) + return -ENOMEM; + + err = soctherm_clk_enable(pdev, true); + if (err) + return err; + + soctherm_init_hw_throt_cdev(pdev); + + soctherm_init(pdev); + + for (i = 0; i < soc->num_ttgs; ++i) { + struct tegra_thermctl_zone *zone = + devm_kzalloc(&pdev->dev, sizeof(*zone), GFP_KERNEL); + if (!zone) { + err = -ENOMEM; + goto disable_clocks; + } + + zone->reg = tegra->regs + soc->ttgs[i]->sensor_temp_offset; + zone->dev = &pdev->dev; + zone->sg = soc->ttgs[i]; + zone->ts = tegra; + + z = devm_thermal_zone_of_sensor_register(&pdev->dev, + soc->ttgs[i]->id, zone, + &tegra_of_thermal_ops); + if (IS_ERR(z)) { + err = PTR_ERR(z); + dev_err(&pdev->dev, "failed to register sensor: %d\n", + err); + goto disable_clocks; + } + + zone->tz = z; + tegra->thermctl_tzs[soc->ttgs[i]->id] = z; + + /* Configure hw trip points */ + err = tegra_soctherm_set_hwtrips(&pdev->dev, soc->ttgs[i], z); + if (err) + goto disable_clocks; + } + + soctherm_debug_init(pdev); + + return 0; + +disable_clocks: + soctherm_clk_enable(pdev, false); + + return err; +} + +static int tegra_soctherm_remove(struct platform_device *pdev) +{ + struct tegra_soctherm *tegra = platform_get_drvdata(pdev); + + debugfs_remove_recursive(tegra->debugfs_dir); + + soctherm_clk_enable(pdev, false); + + return 0; +} + +static int __maybe_unused soctherm_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + + soctherm_clk_enable(pdev, false); + + return 0; +} + +static int __maybe_unused soctherm_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct tegra_soctherm *tegra = platform_get_drvdata(pdev); + struct tegra_soctherm_soc *soc = tegra->soc; + int err, i; + + err = soctherm_clk_enable(pdev, true); + if (err) { + dev_err(&pdev->dev, + "Resume failed: enable clocks failed\n"); + return err; + } + + soctherm_init(pdev); + + for (i = 0; i < soc->num_ttgs; ++i) { + struct thermal_zone_device *tz; + + tz = tegra->thermctl_tzs[soc->ttgs[i]->id]; + err = tegra_soctherm_set_hwtrips(dev, soc->ttgs[i], tz); + if (err) { + dev_err(&pdev->dev, + "Resume failed: set hwtrips failed\n"); + return err; + } + } + + return 0; +} + +static SIMPLE_DEV_PM_OPS(tegra_soctherm_pm, soctherm_suspend, soctherm_resume); + +static struct platform_driver tegra_soctherm_driver = { + .probe = tegra_soctherm_probe, + .remove = tegra_soctherm_remove, + .driver = { + .name = "tegra_soctherm", + .pm = &tegra_soctherm_pm, + .of_match_table = tegra_soctherm_of_match, + }, +}; +module_platform_driver(tegra_soctherm_driver); + +MODULE_AUTHOR("Mikko Perttunen <mperttunen@nvidia.com>"); +MODULE_DESCRIPTION("NVIDIA Tegra SOCTHERM thermal management driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/tegra/soctherm.h b/drivers/thermal/tegra/soctherm.h new file mode 100644 index 000000000..e96ca73fd --- /dev/null +++ b/drivers/thermal/tegra/soctherm.h @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2014-2016, NVIDIA CORPORATION. All rights reserved. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#ifndef __DRIVERS_THERMAL_TEGRA_SOCTHERM_H +#define __DRIVERS_THERMAL_TEGRA_SOCTHERM_H + +#define THERMCTL_LEVEL0_GROUP_CPU 0x0 +#define THERMCTL_LEVEL0_GROUP_GPU 0x4 +#define THERMCTL_LEVEL0_GROUP_MEM 0x8 +#define THERMCTL_LEVEL0_GROUP_TSENSE 0xc + +#define SENSOR_CONFIG2 8 +#define SENSOR_CONFIG2_THERMA_MASK (0xffff << 16) +#define SENSOR_CONFIG2_THERMA_SHIFT 16 +#define SENSOR_CONFIG2_THERMB_MASK 0xffff +#define SENSOR_CONFIG2_THERMB_SHIFT 0 + +#define THERMCTL_THERMTRIP_CTL 0x80 +/* BITs are defined in device file */ + +#define SENSOR_PDIV 0x1c0 +#define SENSOR_PDIV_CPU_MASK (0xf << 12) +#define SENSOR_PDIV_GPU_MASK (0xf << 8) +#define SENSOR_PDIV_MEM_MASK (0xf << 4) +#define SENSOR_PDIV_PLLX_MASK (0xf << 0) + +#define SENSOR_HOTSPOT_OFF 0x1c4 +#define SENSOR_HOTSPOT_CPU_MASK (0xff << 16) +#define SENSOR_HOTSPOT_GPU_MASK (0xff << 8) +#define SENSOR_HOTSPOT_MEM_MASK (0xff << 0) + +#define SENSOR_TEMP1 0x1c8 +#define SENSOR_TEMP1_CPU_TEMP_MASK (0xffff << 16) +#define SENSOR_TEMP1_GPU_TEMP_MASK 0xffff +#define SENSOR_TEMP2 0x1cc +#define SENSOR_TEMP2_MEM_TEMP_MASK (0xffff << 16) +#define SENSOR_TEMP2_PLLX_TEMP_MASK 0xffff + +/** + * struct tegra_tsensor_group - SOC_THERM sensor group data + * @name: short name of the temperature sensor group + * @id: numeric ID of the temperature sensor group + * @sensor_temp_offset: offset of the SENSOR_TEMP* register + * @sensor_temp_mask: bit mask for this sensor group in SENSOR_TEMP* register + * @pdiv: the sensor count post-divider to use during runtime + * @pdiv_ate: the sensor count post-divider used during automated test + * @pdiv_mask: register bitfield mask for the PDIV field for this sensor + * @pllx_hotspot_diff: hotspot offset from the PLLX sensor, must be 0 for + PLLX sensor group + * @pllx_hotspot_mask: register bitfield mask for the HOTSPOT field + */ +struct tegra_tsensor_group { + const char *name; + u8 id; + u16 sensor_temp_offset; + u32 sensor_temp_mask; + u32 pdiv, pdiv_ate, pdiv_mask; + u32 pllx_hotspot_diff, pllx_hotspot_mask; + u32 thermtrip_enable_mask; + u32 thermtrip_any_en_mask; + u32 thermtrip_threshold_mask; + u16 thermctl_lvl0_offset; + u32 thermctl_lvl0_up_thresh_mask; + u32 thermctl_lvl0_dn_thresh_mask; +}; + +struct tegra_tsensor_configuration { + u32 tall, tiddq_en, ten_count, pdiv, pdiv_ate, tsample, tsample_ate; +}; + +struct tegra_tsensor { + const char *name; + const u32 base; + const struct tegra_tsensor_configuration *config; + const u32 calib_fuse_offset; + /* + * Correction values used to modify values read from + * calibration fuses + */ + const s32 fuse_corr_alpha, fuse_corr_beta; + const struct tegra_tsensor_group *group; +}; + +struct tegra_soctherm_fuse { + u32 fuse_base_cp_mask, fuse_base_cp_shift; + u32 fuse_base_ft_mask, fuse_base_ft_shift; + u32 fuse_shift_ft_mask, fuse_shift_ft_shift; + u32 fuse_spare_realignment; +}; + +struct tsensor_shared_calib { + u32 base_cp, base_ft; + u32 actual_temp_cp, actual_temp_ft; +}; + +struct tegra_soctherm_soc { + const struct tegra_tsensor *tsensors; + const unsigned int num_tsensors; + const struct tegra_tsensor_group **ttgs; + const unsigned int num_ttgs; + const struct tegra_soctherm_fuse *tfuse; + const int thresh_grain; + const unsigned int bptt; + const bool use_ccroc; +}; + +int tegra_calc_shared_calib(const struct tegra_soctherm_fuse *tfuse, + struct tsensor_shared_calib *shared); +int tegra_calc_tsensor_calib(const struct tegra_tsensor *sensor, + const struct tsensor_shared_calib *shared, + u32 *calib); + +#ifdef CONFIG_ARCH_TEGRA_124_SOC +extern const struct tegra_soctherm_soc tegra124_soctherm; +#endif + +#ifdef CONFIG_ARCH_TEGRA_132_SOC +extern const struct tegra_soctherm_soc tegra132_soctherm; +#endif + +#ifdef CONFIG_ARCH_TEGRA_210_SOC +extern const struct tegra_soctherm_soc tegra210_soctherm; +#endif + +#endif + diff --git a/drivers/thermal/tegra/tegra-bpmp-thermal.c b/drivers/thermal/tegra/tegra-bpmp-thermal.c new file mode 100644 index 000000000..b0980dbca --- /dev/null +++ b/drivers/thermal/tegra/tegra-bpmp-thermal.c @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2015-2017, NVIDIA CORPORATION. All rights reserved. + * + * Author: + * Mikko Perttunen <mperttunen@nvidia.com> + * Aapo Vienamo <avienamo@nvidia.com> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/err.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/thermal.h> +#include <linux/workqueue.h> + +#include <soc/tegra/bpmp.h> +#include <soc/tegra/bpmp-abi.h> + +struct tegra_bpmp_thermal_zone { + struct tegra_bpmp_thermal *tegra; + struct thermal_zone_device *tzd; + struct work_struct tz_device_update_work; + unsigned int idx; +}; + +struct tegra_bpmp_thermal { + struct device *dev; + struct tegra_bpmp *bpmp; + unsigned int num_zones; + struct tegra_bpmp_thermal_zone **zones; +}; + +static int tegra_bpmp_thermal_get_temp(void *data, int *out_temp) +{ + struct tegra_bpmp_thermal_zone *zone = data; + struct mrq_thermal_host_to_bpmp_request req; + union mrq_thermal_bpmp_to_host_response reply; + struct tegra_bpmp_message msg; + int err; + + memset(&req, 0, sizeof(req)); + req.type = CMD_THERMAL_GET_TEMP; + req.get_temp.zone = zone->idx; + + memset(&msg, 0, sizeof(msg)); + msg.mrq = MRQ_THERMAL; + msg.tx.data = &req; + msg.tx.size = sizeof(req); + msg.rx.data = &reply; + msg.rx.size = sizeof(reply); + + err = tegra_bpmp_transfer(zone->tegra->bpmp, &msg); + if (err) + return err; + + *out_temp = reply.get_temp.temp; + + return 0; +} + +static int tegra_bpmp_thermal_set_trips(void *data, int low, int high) +{ + struct tegra_bpmp_thermal_zone *zone = data; + struct mrq_thermal_host_to_bpmp_request req; + struct tegra_bpmp_message msg; + + memset(&req, 0, sizeof(req)); + req.type = CMD_THERMAL_SET_TRIP; + req.set_trip.zone = zone->idx; + req.set_trip.enabled = true; + req.set_trip.low = low; + req.set_trip.high = high; + + memset(&msg, 0, sizeof(msg)); + msg.mrq = MRQ_THERMAL; + msg.tx.data = &req; + msg.tx.size = sizeof(req); + + return tegra_bpmp_transfer(zone->tegra->bpmp, &msg); +} + +static void tz_device_update_work_fn(struct work_struct *work) +{ + struct tegra_bpmp_thermal_zone *zone; + + zone = container_of(work, struct tegra_bpmp_thermal_zone, + tz_device_update_work); + + thermal_zone_device_update(zone->tzd, THERMAL_TRIP_VIOLATED); +} + +static void bpmp_mrq_thermal(unsigned int mrq, struct tegra_bpmp_channel *ch, + void *data) +{ + struct mrq_thermal_bpmp_to_host_request *req; + struct tegra_bpmp_thermal *tegra = data; + int i; + + req = (struct mrq_thermal_bpmp_to_host_request *)ch->ib->data; + + if (req->type != CMD_THERMAL_HOST_TRIP_REACHED) { + dev_err(tegra->dev, "%s: invalid request type: %d\n", + __func__, req->type); + tegra_bpmp_mrq_return(ch, -EINVAL, NULL, 0); + return; + } + + for (i = 0; i < tegra->num_zones; ++i) { + if (tegra->zones[i]->idx != req->host_trip_reached.zone) + continue; + + schedule_work(&tegra->zones[i]->tz_device_update_work); + tegra_bpmp_mrq_return(ch, 0, NULL, 0); + return; + } + + dev_err(tegra->dev, "%s: invalid thermal zone: %d\n", __func__, + req->host_trip_reached.zone); + tegra_bpmp_mrq_return(ch, -EINVAL, NULL, 0); +} + +static int tegra_bpmp_thermal_get_num_zones(struct tegra_bpmp *bpmp, + int *num_zones) +{ + struct mrq_thermal_host_to_bpmp_request req; + union mrq_thermal_bpmp_to_host_response reply; + struct tegra_bpmp_message msg; + int err; + + memset(&req, 0, sizeof(req)); + req.type = CMD_THERMAL_GET_NUM_ZONES; + + memset(&msg, 0, sizeof(msg)); + msg.mrq = MRQ_THERMAL; + msg.tx.data = &req; + msg.tx.size = sizeof(req); + msg.rx.data = &reply; + msg.rx.size = sizeof(reply); + + err = tegra_bpmp_transfer(bpmp, &msg); + if (err) + return err; + + *num_zones = reply.get_num_zones.num; + + return 0; +} + +static const struct thermal_zone_of_device_ops tegra_bpmp_of_thermal_ops = { + .get_temp = tegra_bpmp_thermal_get_temp, + .set_trips = tegra_bpmp_thermal_set_trips, +}; + +static int tegra_bpmp_thermal_probe(struct platform_device *pdev) +{ + struct tegra_bpmp *bpmp = dev_get_drvdata(pdev->dev.parent); + struct tegra_bpmp_thermal *tegra; + struct thermal_zone_device *tzd; + unsigned int i, max_num_zones; + int err; + + tegra = devm_kzalloc(&pdev->dev, sizeof(*tegra), GFP_KERNEL); + if (!tegra) + return -ENOMEM; + + tegra->dev = &pdev->dev; + tegra->bpmp = bpmp; + + err = tegra_bpmp_thermal_get_num_zones(bpmp, &max_num_zones); + if (err) { + dev_err(&pdev->dev, "failed to get the number of zones: %d\n", + err); + return err; + } + + tegra->zones = devm_kcalloc(&pdev->dev, max_num_zones, + sizeof(*tegra->zones), GFP_KERNEL); + if (!tegra->zones) + return -ENOMEM; + + for (i = 0; i < max_num_zones; ++i) { + struct tegra_bpmp_thermal_zone *zone; + int temp; + + zone = devm_kzalloc(&pdev->dev, sizeof(*zone), GFP_KERNEL); + if (!zone) + return -ENOMEM; + + zone->idx = i; + zone->tegra = tegra; + + err = tegra_bpmp_thermal_get_temp(zone, &temp); + if (err < 0) { + devm_kfree(&pdev->dev, zone); + continue; + } + + tzd = devm_thermal_zone_of_sensor_register( + &pdev->dev, i, zone, &tegra_bpmp_of_thermal_ops); + if (IS_ERR(tzd)) { + if (PTR_ERR(tzd) == -EPROBE_DEFER) + return -EPROBE_DEFER; + devm_kfree(&pdev->dev, zone); + continue; + } + + zone->tzd = tzd; + INIT_WORK(&zone->tz_device_update_work, + tz_device_update_work_fn); + + tegra->zones[tegra->num_zones++] = zone; + } + + err = tegra_bpmp_request_mrq(bpmp, MRQ_THERMAL, bpmp_mrq_thermal, + tegra); + if (err) { + dev_err(&pdev->dev, "failed to register mrq handler: %d\n", + err); + return err; + } + + platform_set_drvdata(pdev, tegra); + + return 0; +} + +static int tegra_bpmp_thermal_remove(struct platform_device *pdev) +{ + struct tegra_bpmp_thermal *tegra = platform_get_drvdata(pdev); + + tegra_bpmp_free_mrq(tegra->bpmp, MRQ_THERMAL, tegra); + + return 0; +} + +static const struct of_device_id tegra_bpmp_thermal_of_match[] = { + { .compatible = "nvidia,tegra186-bpmp-thermal" }, + { }, +}; +MODULE_DEVICE_TABLE(of, tegra_bpmp_thermal_of_match); + +static struct platform_driver tegra_bpmp_thermal_driver = { + .probe = tegra_bpmp_thermal_probe, + .remove = tegra_bpmp_thermal_remove, + .driver = { + .name = "tegra-bpmp-thermal", + .of_match_table = tegra_bpmp_thermal_of_match, + }, +}; +module_platform_driver(tegra_bpmp_thermal_driver); + +MODULE_AUTHOR("Mikko Perttunen <mperttunen@nvidia.com>"); +MODULE_DESCRIPTION("NVIDIA Tegra BPMP thermal sensor driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/tegra/tegra124-soctherm.c b/drivers/thermal/tegra/tegra124-soctherm.c new file mode 100644 index 000000000..36768630f --- /dev/null +++ b/drivers/thermal/tegra/tegra124-soctherm.c @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2014-2016, NVIDIA CORPORATION. All rights reserved. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/module.h> +#include <linux/platform_device.h> + +#include <dt-bindings/thermal/tegra124-soctherm.h> + +#include "soctherm.h" + +#define TEGRA124_THERMTRIP_ANY_EN_MASK (0x1 << 28) +#define TEGRA124_THERMTRIP_MEM_EN_MASK (0x1 << 27) +#define TEGRA124_THERMTRIP_GPU_EN_MASK (0x1 << 26) +#define TEGRA124_THERMTRIP_CPU_EN_MASK (0x1 << 25) +#define TEGRA124_THERMTRIP_TSENSE_EN_MASK (0x1 << 24) +#define TEGRA124_THERMTRIP_GPUMEM_THRESH_MASK (0xff << 16) +#define TEGRA124_THERMTRIP_CPU_THRESH_MASK (0xff << 8) +#define TEGRA124_THERMTRIP_TSENSE_THRESH_MASK 0xff + +#define TEGRA124_THERMCTL_LVL0_UP_THRESH_MASK (0xff << 17) +#define TEGRA124_THERMCTL_LVL0_DN_THRESH_MASK (0xff << 9) + +#define TEGRA124_THRESH_GRAIN 1000 +#define TEGRA124_BPTT 8 + +static const struct tegra_tsensor_configuration tegra124_tsensor_config = { + .tall = 16300, + .tiddq_en = 1, + .ten_count = 1, + .tsample = 120, + .tsample_ate = 480, +}; + +static const struct tegra_tsensor_group tegra124_tsensor_group_cpu = { + .id = TEGRA124_SOCTHERM_SENSOR_CPU, + .name = "cpu", + .sensor_temp_offset = SENSOR_TEMP1, + .sensor_temp_mask = SENSOR_TEMP1_CPU_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_CPU_MASK, + .pllx_hotspot_diff = 10, + .pllx_hotspot_mask = SENSOR_HOTSPOT_CPU_MASK, + .thermtrip_any_en_mask = TEGRA124_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA124_THERMTRIP_CPU_EN_MASK, + .thermtrip_threshold_mask = TEGRA124_THERMTRIP_CPU_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_CPU, + .thermctl_lvl0_up_thresh_mask = TEGRA124_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA124_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra124_tsensor_group_gpu = { + .id = TEGRA124_SOCTHERM_SENSOR_GPU, + .name = "gpu", + .sensor_temp_offset = SENSOR_TEMP1, + .sensor_temp_mask = SENSOR_TEMP1_GPU_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_GPU_MASK, + .pllx_hotspot_diff = 5, + .pllx_hotspot_mask = SENSOR_HOTSPOT_GPU_MASK, + .thermtrip_any_en_mask = TEGRA124_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA124_THERMTRIP_GPU_EN_MASK, + .thermtrip_threshold_mask = TEGRA124_THERMTRIP_GPUMEM_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_GPU, + .thermctl_lvl0_up_thresh_mask = TEGRA124_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA124_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra124_tsensor_group_pll = { + .id = TEGRA124_SOCTHERM_SENSOR_PLLX, + .name = "pll", + .sensor_temp_offset = SENSOR_TEMP2, + .sensor_temp_mask = SENSOR_TEMP2_PLLX_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_PLLX_MASK, + .thermtrip_any_en_mask = TEGRA124_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA124_THERMTRIP_TSENSE_EN_MASK, + .thermtrip_threshold_mask = TEGRA124_THERMTRIP_TSENSE_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_TSENSE, + .thermctl_lvl0_up_thresh_mask = TEGRA124_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA124_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra124_tsensor_group_mem = { + .id = TEGRA124_SOCTHERM_SENSOR_MEM, + .name = "mem", + .sensor_temp_offset = SENSOR_TEMP2, + .sensor_temp_mask = SENSOR_TEMP2_MEM_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_MEM_MASK, + .pllx_hotspot_diff = 0, + .pllx_hotspot_mask = SENSOR_HOTSPOT_MEM_MASK, + .thermtrip_any_en_mask = TEGRA124_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA124_THERMTRIP_MEM_EN_MASK, + .thermtrip_threshold_mask = TEGRA124_THERMTRIP_GPUMEM_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_MEM, + .thermctl_lvl0_up_thresh_mask = TEGRA124_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA124_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group *tegra124_tsensor_groups[] = { + &tegra124_tsensor_group_cpu, + &tegra124_tsensor_group_gpu, + &tegra124_tsensor_group_pll, + &tegra124_tsensor_group_mem, +}; + +static const struct tegra_tsensor tegra124_tsensors[] = { + { + .name = "cpu0", + .base = 0xc0, + .config = &tegra124_tsensor_config, + .calib_fuse_offset = 0x098, + .fuse_corr_alpha = 1135400, + .fuse_corr_beta = -6266900, + .group = &tegra124_tsensor_group_cpu, + }, { + .name = "cpu1", + .base = 0xe0, + .config = &tegra124_tsensor_config, + .calib_fuse_offset = 0x084, + .fuse_corr_alpha = 1122220, + .fuse_corr_beta = -5700700, + .group = &tegra124_tsensor_group_cpu, + }, { + .name = "cpu2", + .base = 0x100, + .config = &tegra124_tsensor_config, + .calib_fuse_offset = 0x088, + .fuse_corr_alpha = 1127000, + .fuse_corr_beta = -6768200, + .group = &tegra124_tsensor_group_cpu, + }, { + .name = "cpu3", + .base = 0x120, + .config = &tegra124_tsensor_config, + .calib_fuse_offset = 0x12c, + .fuse_corr_alpha = 1110900, + .fuse_corr_beta = -6232000, + .group = &tegra124_tsensor_group_cpu, + }, { + .name = "mem0", + .base = 0x140, + .config = &tegra124_tsensor_config, + .calib_fuse_offset = 0x158, + .fuse_corr_alpha = 1122300, + .fuse_corr_beta = -5936400, + .group = &tegra124_tsensor_group_mem, + }, { + .name = "mem1", + .base = 0x160, + .config = &tegra124_tsensor_config, + .calib_fuse_offset = 0x15c, + .fuse_corr_alpha = 1145700, + .fuse_corr_beta = -7124600, + .group = &tegra124_tsensor_group_mem, + }, { + .name = "gpu", + .base = 0x180, + .config = &tegra124_tsensor_config, + .calib_fuse_offset = 0x154, + .fuse_corr_alpha = 1120100, + .fuse_corr_beta = -6000500, + .group = &tegra124_tsensor_group_gpu, + }, { + .name = "pllx", + .base = 0x1a0, + .config = &tegra124_tsensor_config, + .calib_fuse_offset = 0x160, + .fuse_corr_alpha = 1106500, + .fuse_corr_beta = -6729300, + .group = &tegra124_tsensor_group_pll, + }, +}; + +/* + * Mask/shift bits in FUSE_TSENSOR_COMMON and + * FUSE_TSENSOR_COMMON, which are described in + * tegra_soctherm_fuse.c + */ +static const struct tegra_soctherm_fuse tegra124_soctherm_fuse = { + .fuse_base_cp_mask = 0x3ff, + .fuse_base_cp_shift = 0, + .fuse_base_ft_mask = 0x7ff << 10, + .fuse_base_ft_shift = 10, + .fuse_shift_ft_mask = 0x1f << 21, + .fuse_shift_ft_shift = 21, + .fuse_spare_realignment = 0x1fc, +}; + +const struct tegra_soctherm_soc tegra124_soctherm = { + .tsensors = tegra124_tsensors, + .num_tsensors = ARRAY_SIZE(tegra124_tsensors), + .ttgs = tegra124_tsensor_groups, + .num_ttgs = ARRAY_SIZE(tegra124_tsensor_groups), + .tfuse = &tegra124_soctherm_fuse, + .thresh_grain = TEGRA124_THRESH_GRAIN, + .bptt = TEGRA124_BPTT, + .use_ccroc = false, +}; diff --git a/drivers/thermal/tegra/tegra132-soctherm.c b/drivers/thermal/tegra/tegra132-soctherm.c new file mode 100644 index 000000000..97fa30501 --- /dev/null +++ b/drivers/thermal/tegra/tegra132-soctherm.c @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2014-2016, NVIDIA CORPORATION. All rights reserved. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/module.h> +#include <linux/platform_device.h> + +#include <dt-bindings/thermal/tegra124-soctherm.h> + +#include "soctherm.h" + +#define TEGRA132_THERMTRIP_ANY_EN_MASK (0x1 << 28) +#define TEGRA132_THERMTRIP_MEM_EN_MASK (0x1 << 27) +#define TEGRA132_THERMTRIP_GPU_EN_MASK (0x1 << 26) +#define TEGRA132_THERMTRIP_CPU_EN_MASK (0x1 << 25) +#define TEGRA132_THERMTRIP_TSENSE_EN_MASK (0x1 << 24) +#define TEGRA132_THERMTRIP_GPUMEM_THRESH_MASK (0xff << 16) +#define TEGRA132_THERMTRIP_CPU_THRESH_MASK (0xff << 8) +#define TEGRA132_THERMTRIP_TSENSE_THRESH_MASK 0xff + +#define TEGRA132_THERMCTL_LVL0_UP_THRESH_MASK (0xff << 17) +#define TEGRA132_THERMCTL_LVL0_DN_THRESH_MASK (0xff << 9) + +#define TEGRA132_THRESH_GRAIN 1000 +#define TEGRA132_BPTT 8 + +static const struct tegra_tsensor_configuration tegra132_tsensor_config = { + .tall = 16300, + .tiddq_en = 1, + .ten_count = 1, + .tsample = 120, + .tsample_ate = 480, +}; + +static const struct tegra_tsensor_group tegra132_tsensor_group_cpu = { + .id = TEGRA124_SOCTHERM_SENSOR_CPU, + .name = "cpu", + .sensor_temp_offset = SENSOR_TEMP1, + .sensor_temp_mask = SENSOR_TEMP1_CPU_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_CPU_MASK, + .pllx_hotspot_diff = 10, + .pllx_hotspot_mask = SENSOR_HOTSPOT_CPU_MASK, + .thermtrip_any_en_mask = TEGRA132_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA132_THERMTRIP_CPU_EN_MASK, + .thermtrip_threshold_mask = TEGRA132_THERMTRIP_CPU_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_CPU, + .thermctl_lvl0_up_thresh_mask = TEGRA132_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA132_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra132_tsensor_group_gpu = { + .id = TEGRA124_SOCTHERM_SENSOR_GPU, + .name = "gpu", + .sensor_temp_offset = SENSOR_TEMP1, + .sensor_temp_mask = SENSOR_TEMP1_GPU_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_GPU_MASK, + .pllx_hotspot_diff = 5, + .pllx_hotspot_mask = SENSOR_HOTSPOT_GPU_MASK, + .thermtrip_any_en_mask = TEGRA132_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA132_THERMTRIP_GPU_EN_MASK, + .thermtrip_threshold_mask = TEGRA132_THERMTRIP_GPUMEM_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_GPU, + .thermctl_lvl0_up_thresh_mask = TEGRA132_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA132_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra132_tsensor_group_pll = { + .id = TEGRA124_SOCTHERM_SENSOR_PLLX, + .name = "pll", + .sensor_temp_offset = SENSOR_TEMP2, + .sensor_temp_mask = SENSOR_TEMP2_PLLX_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_PLLX_MASK, + .thermtrip_any_en_mask = TEGRA132_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA132_THERMTRIP_TSENSE_EN_MASK, + .thermtrip_threshold_mask = TEGRA132_THERMTRIP_TSENSE_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_TSENSE, + .thermctl_lvl0_up_thresh_mask = TEGRA132_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA132_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra132_tsensor_group_mem = { + .id = TEGRA124_SOCTHERM_SENSOR_MEM, + .name = "mem", + .sensor_temp_offset = SENSOR_TEMP2, + .sensor_temp_mask = SENSOR_TEMP2_MEM_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_MEM_MASK, + .pllx_hotspot_diff = 0, + .pllx_hotspot_mask = SENSOR_HOTSPOT_MEM_MASK, + .thermtrip_any_en_mask = TEGRA132_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA132_THERMTRIP_MEM_EN_MASK, + .thermtrip_threshold_mask = TEGRA132_THERMTRIP_GPUMEM_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_MEM, + .thermctl_lvl0_up_thresh_mask = TEGRA132_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA132_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group *tegra132_tsensor_groups[] = { + &tegra132_tsensor_group_cpu, + &tegra132_tsensor_group_gpu, + &tegra132_tsensor_group_pll, + &tegra132_tsensor_group_mem, +}; + +static struct tegra_tsensor tegra132_tsensors[] = { + { + .name = "cpu0", + .base = 0xc0, + .config = &tegra132_tsensor_config, + .calib_fuse_offset = 0x098, + .fuse_corr_alpha = 1126600, + .fuse_corr_beta = -9433500, + .group = &tegra132_tsensor_group_cpu, + }, { + .name = "cpu1", + .base = 0xe0, + .config = &tegra132_tsensor_config, + .calib_fuse_offset = 0x084, + .fuse_corr_alpha = 1110800, + .fuse_corr_beta = -7383000, + .group = &tegra132_tsensor_group_cpu, + }, { + .name = "cpu2", + .base = 0x100, + .config = &tegra132_tsensor_config, + .calib_fuse_offset = 0x088, + .fuse_corr_alpha = 1113800, + .fuse_corr_beta = -6215200, + .group = &tegra132_tsensor_group_cpu, + }, { + .name = "cpu3", + .base = 0x120, + .config = &tegra132_tsensor_config, + .calib_fuse_offset = 0x12c, + .fuse_corr_alpha = 1129600, + .fuse_corr_beta = -8196100, + .group = &tegra132_tsensor_group_cpu, + }, { + .name = "mem0", + .base = 0x140, + .config = &tegra132_tsensor_config, + .calib_fuse_offset = 0x158, + .fuse_corr_alpha = 1132900, + .fuse_corr_beta = -6755300, + .group = &tegra132_tsensor_group_mem, + }, { + .name = "mem1", + .base = 0x160, + .config = &tegra132_tsensor_config, + .calib_fuse_offset = 0x15c, + .fuse_corr_alpha = 1142300, + .fuse_corr_beta = -7374200, + .group = &tegra132_tsensor_group_mem, + }, { + .name = "gpu", + .base = 0x180, + .config = &tegra132_tsensor_config, + .calib_fuse_offset = 0x154, + .fuse_corr_alpha = 1125100, + .fuse_corr_beta = -6350400, + .group = &tegra132_tsensor_group_gpu, + }, { + .name = "pllx", + .base = 0x1a0, + .config = &tegra132_tsensor_config, + .calib_fuse_offset = 0x160, + .fuse_corr_alpha = 1118100, + .fuse_corr_beta = -8208800, + .group = &tegra132_tsensor_group_pll, + }, +}; + +/* + * Mask/shift bits in FUSE_TSENSOR_COMMON and + * FUSE_TSENSOR_COMMON, which are described in + * tegra_soctherm_fuse.c + */ +static const struct tegra_soctherm_fuse tegra132_soctherm_fuse = { + .fuse_base_cp_mask = 0x3ff, + .fuse_base_cp_shift = 0, + .fuse_base_ft_mask = 0x7ff << 10, + .fuse_base_ft_shift = 10, + .fuse_shift_ft_mask = 0x1f << 21, + .fuse_shift_ft_shift = 21, + .fuse_spare_realignment = 0x1fc, +}; + +const struct tegra_soctherm_soc tegra132_soctherm = { + .tsensors = tegra132_tsensors, + .num_tsensors = ARRAY_SIZE(tegra132_tsensors), + .ttgs = tegra132_tsensor_groups, + .num_ttgs = ARRAY_SIZE(tegra132_tsensor_groups), + .tfuse = &tegra132_soctherm_fuse, + .thresh_grain = TEGRA132_THRESH_GRAIN, + .bptt = TEGRA132_BPTT, + .use_ccroc = true, +}; diff --git a/drivers/thermal/tegra/tegra210-soctherm.c b/drivers/thermal/tegra/tegra210-soctherm.c new file mode 100644 index 000000000..ad53169a8 --- /dev/null +++ b/drivers/thermal/tegra/tegra210-soctherm.c @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2014-2016, NVIDIA CORPORATION. All rights reserved. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <soc/tegra/fuse.h> + +#include <dt-bindings/thermal/tegra124-soctherm.h> + +#include "soctherm.h" + +#define TEGRA210_THERMTRIP_ANY_EN_MASK (0x1 << 31) +#define TEGRA210_THERMTRIP_MEM_EN_MASK (0x1 << 30) +#define TEGRA210_THERMTRIP_GPU_EN_MASK (0x1 << 29) +#define TEGRA210_THERMTRIP_CPU_EN_MASK (0x1 << 28) +#define TEGRA210_THERMTRIP_TSENSE_EN_MASK (0x1 << 27) +#define TEGRA210_THERMTRIP_GPUMEM_THRESH_MASK (0x1ff << 18) +#define TEGRA210_THERMTRIP_CPU_THRESH_MASK (0x1ff << 9) +#define TEGRA210_THERMTRIP_TSENSE_THRESH_MASK 0x1ff + +#define TEGRA210_THERMCTL_LVL0_UP_THRESH_MASK (0x1ff << 18) +#define TEGRA210_THERMCTL_LVL0_DN_THRESH_MASK (0x1ff << 9) + +#define TEGRA210_THRESH_GRAIN 500 +#define TEGRA210_BPTT 9 + +static const struct tegra_tsensor_configuration tegra210_tsensor_config = { + .tall = 16300, + .tiddq_en = 1, + .ten_count = 1, + .tsample = 120, + .tsample_ate = 480, +}; + +static const struct tegra_tsensor_group tegra210_tsensor_group_cpu = { + .id = TEGRA124_SOCTHERM_SENSOR_CPU, + .name = "cpu", + .sensor_temp_offset = SENSOR_TEMP1, + .sensor_temp_mask = SENSOR_TEMP1_CPU_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_CPU_MASK, + .pllx_hotspot_diff = 10, + .pllx_hotspot_mask = SENSOR_HOTSPOT_CPU_MASK, + .thermtrip_any_en_mask = TEGRA210_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA210_THERMTRIP_CPU_EN_MASK, + .thermtrip_threshold_mask = TEGRA210_THERMTRIP_CPU_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_CPU, + .thermctl_lvl0_up_thresh_mask = TEGRA210_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA210_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra210_tsensor_group_gpu = { + .id = TEGRA124_SOCTHERM_SENSOR_GPU, + .name = "gpu", + .sensor_temp_offset = SENSOR_TEMP1, + .sensor_temp_mask = SENSOR_TEMP1_GPU_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_GPU_MASK, + .pllx_hotspot_diff = 5, + .pllx_hotspot_mask = SENSOR_HOTSPOT_GPU_MASK, + .thermtrip_any_en_mask = TEGRA210_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA210_THERMTRIP_GPU_EN_MASK, + .thermtrip_threshold_mask = TEGRA210_THERMTRIP_GPUMEM_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_GPU, + .thermctl_lvl0_up_thresh_mask = TEGRA210_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA210_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra210_tsensor_group_pll = { + .id = TEGRA124_SOCTHERM_SENSOR_PLLX, + .name = "pll", + .sensor_temp_offset = SENSOR_TEMP2, + .sensor_temp_mask = SENSOR_TEMP2_PLLX_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_PLLX_MASK, + .thermtrip_any_en_mask = TEGRA210_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA210_THERMTRIP_TSENSE_EN_MASK, + .thermtrip_threshold_mask = TEGRA210_THERMTRIP_TSENSE_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_TSENSE, + .thermctl_lvl0_up_thresh_mask = TEGRA210_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA210_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group tegra210_tsensor_group_mem = { + .id = TEGRA124_SOCTHERM_SENSOR_MEM, + .name = "mem", + .sensor_temp_offset = SENSOR_TEMP2, + .sensor_temp_mask = SENSOR_TEMP2_MEM_TEMP_MASK, + .pdiv = 8, + .pdiv_ate = 8, + .pdiv_mask = SENSOR_PDIV_MEM_MASK, + .pllx_hotspot_diff = 0, + .pllx_hotspot_mask = SENSOR_HOTSPOT_MEM_MASK, + .thermtrip_any_en_mask = TEGRA210_THERMTRIP_ANY_EN_MASK, + .thermtrip_enable_mask = TEGRA210_THERMTRIP_MEM_EN_MASK, + .thermtrip_threshold_mask = TEGRA210_THERMTRIP_GPUMEM_THRESH_MASK, + .thermctl_lvl0_offset = THERMCTL_LEVEL0_GROUP_MEM, + .thermctl_lvl0_up_thresh_mask = TEGRA210_THERMCTL_LVL0_UP_THRESH_MASK, + .thermctl_lvl0_dn_thresh_mask = TEGRA210_THERMCTL_LVL0_DN_THRESH_MASK, +}; + +static const struct tegra_tsensor_group *tegra210_tsensor_groups[] = { + &tegra210_tsensor_group_cpu, + &tegra210_tsensor_group_gpu, + &tegra210_tsensor_group_pll, + &tegra210_tsensor_group_mem, +}; + +static const struct tegra_tsensor tegra210_tsensors[] = { + { + .name = "cpu0", + .base = 0xc0, + .config = &tegra210_tsensor_config, + .calib_fuse_offset = 0x098, + .fuse_corr_alpha = 1085000, + .fuse_corr_beta = 3244200, + .group = &tegra210_tsensor_group_cpu, + }, { + .name = "cpu1", + .base = 0xe0, + .config = &tegra210_tsensor_config, + .calib_fuse_offset = 0x084, + .fuse_corr_alpha = 1126200, + .fuse_corr_beta = -67500, + .group = &tegra210_tsensor_group_cpu, + }, { + .name = "cpu2", + .base = 0x100, + .config = &tegra210_tsensor_config, + .calib_fuse_offset = 0x088, + .fuse_corr_alpha = 1098400, + .fuse_corr_beta = 2251100, + .group = &tegra210_tsensor_group_cpu, + }, { + .name = "cpu3", + .base = 0x120, + .config = &tegra210_tsensor_config, + .calib_fuse_offset = 0x12c, + .fuse_corr_alpha = 1108000, + .fuse_corr_beta = 602700, + .group = &tegra210_tsensor_group_cpu, + }, { + .name = "mem0", + .base = 0x140, + .config = &tegra210_tsensor_config, + .calib_fuse_offset = 0x158, + .fuse_corr_alpha = 1069200, + .fuse_corr_beta = 3549900, + .group = &tegra210_tsensor_group_mem, + }, { + .name = "mem1", + .base = 0x160, + .config = &tegra210_tsensor_config, + .calib_fuse_offset = 0x15c, + .fuse_corr_alpha = 1173700, + .fuse_corr_beta = -6263600, + .group = &tegra210_tsensor_group_mem, + }, { + .name = "gpu", + .base = 0x180, + .config = &tegra210_tsensor_config, + .calib_fuse_offset = 0x154, + .fuse_corr_alpha = 1074300, + .fuse_corr_beta = 2734900, + .group = &tegra210_tsensor_group_gpu, + }, { + .name = "pllx", + .base = 0x1a0, + .config = &tegra210_tsensor_config, + .calib_fuse_offset = 0x160, + .fuse_corr_alpha = 1039700, + .fuse_corr_beta = 6829100, + .group = &tegra210_tsensor_group_pll, + }, +}; + +/* + * Mask/shift bits in FUSE_TSENSOR_COMMON and + * FUSE_TSENSOR_COMMON, which are described in + * tegra_soctherm_fuse.c + */ +static const struct tegra_soctherm_fuse tegra210_soctherm_fuse = { + .fuse_base_cp_mask = 0x3ff << 11, + .fuse_base_cp_shift = 11, + .fuse_base_ft_mask = 0x7ff << 21, + .fuse_base_ft_shift = 21, + .fuse_shift_ft_mask = 0x1f << 6, + .fuse_shift_ft_shift = 6, + .fuse_spare_realignment = 0, +}; + +const struct tegra_soctherm_soc tegra210_soctherm = { + .tsensors = tegra210_tsensors, + .num_tsensors = ARRAY_SIZE(tegra210_tsensors), + .ttgs = tegra210_tsensor_groups, + .num_ttgs = ARRAY_SIZE(tegra210_tsensor_groups), + .tfuse = &tegra210_soctherm_fuse, + .thresh_grain = TEGRA210_THRESH_GRAIN, + .bptt = TEGRA210_BPTT, + .use_ccroc = false, +}; diff --git a/drivers/thermal/thermal-generic-adc.c b/drivers/thermal/thermal-generic-adc.c new file mode 100644 index 000000000..e22fc60ad --- /dev/null +++ b/drivers/thermal/thermal-generic-adc.c @@ -0,0 +1,171 @@ +/* + * Generic ADC thermal driver + * + * Copyright (C) 2016 NVIDIA CORPORATION. All rights reserved. + * + * Author: Laxman Dewangan <ldewangan@nvidia.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ +#include <linux/iio/consumer.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/thermal.h> + +struct gadc_thermal_info { + struct device *dev; + struct thermal_zone_device *tz_dev; + struct iio_channel *channel; + s32 *lookup_table; + int nlookup_table; +}; + +static int gadc_thermal_adc_to_temp(struct gadc_thermal_info *gti, int val) +{ + int temp, temp_hi, temp_lo, adc_hi, adc_lo; + int i; + + for (i = 0; i < gti->nlookup_table; i++) { + if (val >= gti->lookup_table[2 * i + 1]) + break; + } + + if (i == 0) { + temp = gti->lookup_table[0]; + } else if (i >= gti->nlookup_table) { + temp = gti->lookup_table[2 * (gti->nlookup_table - 1)]; + } else { + adc_hi = gti->lookup_table[2 * i - 1]; + adc_lo = gti->lookup_table[2 * i + 1]; + + temp_hi = gti->lookup_table[2 * i - 2]; + temp_lo = gti->lookup_table[2 * i]; + + temp = temp_hi + mult_frac(temp_lo - temp_hi, val - adc_hi, + adc_lo - adc_hi); + } + + return temp; +} + +static int gadc_thermal_get_temp(void *data, int *temp) +{ + struct gadc_thermal_info *gti = data; + int val; + int ret; + + ret = iio_read_channel_processed(gti->channel, &val); + if (ret < 0) { + dev_err(gti->dev, "IIO channel read failed %d\n", ret); + return ret; + } + *temp = gadc_thermal_adc_to_temp(gti, val); + + return 0; +} + +static const struct thermal_zone_of_device_ops gadc_thermal_ops = { + .get_temp = gadc_thermal_get_temp, +}; + +static int gadc_thermal_read_linear_lookup_table(struct device *dev, + struct gadc_thermal_info *gti) +{ + struct device_node *np = dev->of_node; + int ntable; + int ret; + + ntable = of_property_count_elems_of_size(np, "temperature-lookup-table", + sizeof(u32)); + if (ntable < 0) { + dev_err(dev, "Lookup table is not provided\n"); + return ntable; + } + + if (ntable % 2) { + dev_err(dev, "Pair of temperature vs ADC read value missing\n"); + return -EINVAL; + } + + gti->lookup_table = devm_kcalloc(dev, + ntable, sizeof(*gti->lookup_table), + GFP_KERNEL); + if (!gti->lookup_table) + return -ENOMEM; + + ret = of_property_read_u32_array(np, "temperature-lookup-table", + (u32 *)gti->lookup_table, ntable); + if (ret < 0) { + dev_err(dev, "Failed to read temperature lookup table: %d\n", + ret); + return ret; + } + + gti->nlookup_table = ntable / 2; + + return 0; +} + +static int gadc_thermal_probe(struct platform_device *pdev) +{ + struct gadc_thermal_info *gti; + int ret; + + if (!pdev->dev.of_node) { + dev_err(&pdev->dev, "Only DT based supported\n"); + return -ENODEV; + } + + gti = devm_kzalloc(&pdev->dev, sizeof(*gti), GFP_KERNEL); + if (!gti) + return -ENOMEM; + + ret = gadc_thermal_read_linear_lookup_table(&pdev->dev, gti); + if (ret < 0) + return ret; + + gti->dev = &pdev->dev; + platform_set_drvdata(pdev, gti); + + gti->channel = devm_iio_channel_get(&pdev->dev, "sensor-channel"); + if (IS_ERR(gti->channel)) { + ret = PTR_ERR(gti->channel); + dev_err(&pdev->dev, "IIO channel not found: %d\n", ret); + return ret; + } + + gti->tz_dev = devm_thermal_zone_of_sensor_register(&pdev->dev, 0, gti, + &gadc_thermal_ops); + if (IS_ERR(gti->tz_dev)) { + ret = PTR_ERR(gti->tz_dev); + dev_err(&pdev->dev, "Thermal zone sensor register failed: %d\n", + ret); + return ret; + } + + return 0; +} + +static const struct of_device_id of_adc_thermal_match[] = { + { .compatible = "generic-adc-thermal", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_adc_thermal_match); + +static struct platform_driver gadc_thermal_driver = { + .driver = { + .name = "generic-adc-thermal", + .of_match_table = of_adc_thermal_match, + }, + .probe = gadc_thermal_probe, +}; + +module_platform_driver(gadc_thermal_driver); + +MODULE_AUTHOR("Laxman Dewangan <ldewangan@nvidia.com>"); +MODULE_DESCRIPTION("Generic ADC thermal driver using IIO framework with DT"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/thermal_core.c b/drivers/thermal/thermal_core.c new file mode 100644 index 000000000..6c7825c58 --- /dev/null +++ b/drivers/thermal/thermal_core.c @@ -0,0 +1,1582 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * thermal.c - Generic Thermal Management Sysfs support. + * + * Copyright (C) 2008 Intel Corp + * Copyright (C) 2008 Zhang Rui <rui.zhang@intel.com> + * Copyright (C) 2008 Sujith Thomas <sujith.thomas@intel.com> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/kdev_t.h> +#include <linux/idr.h> +#include <linux/thermal.h> +#include <linux/reboot.h> +#include <linux/string.h> +#include <linux/of.h> +#include <net/netlink.h> +#include <net/genetlink.h> +#include <linux/suspend.h> + +#define CREATE_TRACE_POINTS +#include <trace/events/thermal.h> + +#include "thermal_core.h" +#include "thermal_hwmon.h" + +MODULE_AUTHOR("Zhang Rui"); +MODULE_DESCRIPTION("Generic thermal management sysfs support"); +MODULE_LICENSE("GPL v2"); + +static DEFINE_IDA(thermal_tz_ida); +static DEFINE_IDA(thermal_cdev_ida); + +static LIST_HEAD(thermal_tz_list); +static LIST_HEAD(thermal_cdev_list); +static LIST_HEAD(thermal_governor_list); + +static DEFINE_MUTEX(thermal_list_lock); +static DEFINE_MUTEX(thermal_governor_lock); +static DEFINE_MUTEX(poweroff_lock); + +static atomic_t in_suspend; +static bool power_off_triggered; + +static struct thermal_governor *def_governor; + +/* + * Governor section: set of functions to handle thermal governors + * + * Functions to help in the life cycle of thermal governors within + * the thermal core and by the thermal governor code. + */ + +static struct thermal_governor *__find_governor(const char *name) +{ + struct thermal_governor *pos; + + if (!name || !name[0]) + return def_governor; + + list_for_each_entry(pos, &thermal_governor_list, governor_list) + if (!strncasecmp(name, pos->name, THERMAL_NAME_LENGTH)) + return pos; + + return NULL; +} + +/** + * bind_previous_governor() - bind the previous governor of the thermal zone + * @tz: a valid pointer to a struct thermal_zone_device + * @failed_gov_name: the name of the governor that failed to register + * + * Register the previous governor of the thermal zone after a new + * governor has failed to be bound. + */ +static void bind_previous_governor(struct thermal_zone_device *tz, + const char *failed_gov_name) +{ + if (tz->governor && tz->governor->bind_to_tz) { + if (tz->governor->bind_to_tz(tz)) { + dev_err(&tz->device, + "governor %s failed to bind and the previous one (%s) failed to bind again, thermal zone %s has no governor\n", + failed_gov_name, tz->governor->name, tz->type); + tz->governor = NULL; + } + } +} + +/** + * thermal_set_governor() - Switch to another governor + * @tz: a valid pointer to a struct thermal_zone_device + * @new_gov: pointer to the new governor + * + * Change the governor of thermal zone @tz. + * + * Return: 0 on success, an error if the new governor's bind_to_tz() failed. + */ +static int thermal_set_governor(struct thermal_zone_device *tz, + struct thermal_governor *new_gov) +{ + int ret = 0; + + if (tz->governor && tz->governor->unbind_from_tz) + tz->governor->unbind_from_tz(tz); + + if (new_gov && new_gov->bind_to_tz) { + ret = new_gov->bind_to_tz(tz); + if (ret) { + bind_previous_governor(tz, new_gov->name); + + return ret; + } + } + + tz->governor = new_gov; + + return ret; +} + +int thermal_register_governor(struct thermal_governor *governor) +{ + int err; + const char *name; + struct thermal_zone_device *pos; + + if (!governor) + return -EINVAL; + + mutex_lock(&thermal_governor_lock); + + err = -EBUSY; + if (!__find_governor(governor->name)) { + bool match_default; + + err = 0; + list_add(&governor->governor_list, &thermal_governor_list); + match_default = !strncmp(governor->name, + DEFAULT_THERMAL_GOVERNOR, + THERMAL_NAME_LENGTH); + + if (!def_governor && match_default) + def_governor = governor; + } + + mutex_lock(&thermal_list_lock); + + list_for_each_entry(pos, &thermal_tz_list, node) { + /* + * only thermal zones with specified tz->tzp->governor_name + * may run with tz->govenor unset + */ + if (pos->governor) + continue; + + name = pos->tzp->governor_name; + + if (!strncasecmp(name, governor->name, THERMAL_NAME_LENGTH)) { + int ret; + + ret = thermal_set_governor(pos, governor); + if (ret) + dev_err(&pos->device, + "Failed to set governor %s for thermal zone %s: %d\n", + governor->name, pos->type, ret); + } + } + + mutex_unlock(&thermal_list_lock); + mutex_unlock(&thermal_governor_lock); + + return err; +} + +void thermal_unregister_governor(struct thermal_governor *governor) +{ + struct thermal_zone_device *pos; + + if (!governor) + return; + + mutex_lock(&thermal_governor_lock); + + if (!__find_governor(governor->name)) + goto exit; + + mutex_lock(&thermal_list_lock); + + list_for_each_entry(pos, &thermal_tz_list, node) { + if (!strncasecmp(pos->governor->name, governor->name, + THERMAL_NAME_LENGTH)) + thermal_set_governor(pos, NULL); + } + + mutex_unlock(&thermal_list_lock); + list_del(&governor->governor_list); +exit: + mutex_unlock(&thermal_governor_lock); +} + +int thermal_zone_device_set_policy(struct thermal_zone_device *tz, + char *policy) +{ + struct thermal_governor *gov; + int ret = -EINVAL; + + mutex_lock(&thermal_governor_lock); + mutex_lock(&tz->lock); + + gov = __find_governor(strim(policy)); + if (!gov) + goto exit; + + ret = thermal_set_governor(tz, gov); + +exit: + mutex_unlock(&tz->lock); + mutex_unlock(&thermal_governor_lock); + + return ret; +} + +int thermal_build_list_of_policies(char *buf) +{ + struct thermal_governor *pos; + ssize_t count = 0; + + mutex_lock(&thermal_governor_lock); + + list_for_each_entry(pos, &thermal_governor_list, governor_list) { + count += scnprintf(buf + count, PAGE_SIZE - count, "%s ", + pos->name); + } + count += scnprintf(buf + count, PAGE_SIZE - count, "\n"); + + mutex_unlock(&thermal_governor_lock); + + return count; +} + +static int __init thermal_register_governors(void) +{ + int result; + + result = thermal_gov_step_wise_register(); + if (result) + return result; + + result = thermal_gov_fair_share_register(); + if (result) + return result; + + result = thermal_gov_bang_bang_register(); + if (result) + return result; + + result = thermal_gov_user_space_register(); + if (result) + return result; + + return thermal_gov_power_allocator_register(); +} + +static void thermal_unregister_governors(void) +{ + thermal_gov_step_wise_unregister(); + thermal_gov_fair_share_unregister(); + thermal_gov_bang_bang_unregister(); + thermal_gov_user_space_unregister(); + thermal_gov_power_allocator_unregister(); +} + +/* + * Zone update section: main control loop applied to each zone while monitoring + * + * in polling mode. The monitoring is done using a workqueue. + * Same update may be done on a zone by calling thermal_zone_device_update(). + * + * An update means: + * - Non-critical trips will invoke the governor responsible for that zone; + * - Hot trips will produce a notification to userspace; + * - Critical trip point will cause a system shutdown. + */ +static void thermal_zone_device_set_polling(struct thermal_zone_device *tz, + int delay) +{ + if (delay > 1000) + mod_delayed_work(system_freezable_wq, &tz->poll_queue, + round_jiffies(msecs_to_jiffies(delay))); + else if (delay) + mod_delayed_work(system_freezable_wq, &tz->poll_queue, + msecs_to_jiffies(delay)); + else + cancel_delayed_work(&tz->poll_queue); +} + +static void monitor_thermal_zone(struct thermal_zone_device *tz) +{ + mutex_lock(&tz->lock); + + if (tz->passive) + thermal_zone_device_set_polling(tz, tz->passive_delay); + else if (tz->polling_delay) + thermal_zone_device_set_polling(tz, tz->polling_delay); + else + thermal_zone_device_set_polling(tz, 0); + + mutex_unlock(&tz->lock); +} + +static void handle_non_critical_trips(struct thermal_zone_device *tz, + int trip, + enum thermal_trip_type trip_type) +{ + tz->governor ? tz->governor->throttle(tz, trip) : + def_governor->throttle(tz, trip); +} + +/** + * thermal_emergency_poweroff_func - emergency poweroff work after a known delay + * @work: work_struct associated with the emergency poweroff function + * + * This function is called in very critical situations to force + * a kernel poweroff after a configurable timeout value. + */ +static void thermal_emergency_poweroff_func(struct work_struct *work) +{ + /* + * We have reached here after the emergency thermal shutdown + * Waiting period has expired. This means orderly_poweroff has + * not been able to shut off the system for some reason. + * Try to shut down the system immediately using kernel_power_off + * if populated + */ + WARN(1, "Attempting kernel_power_off: Temperature too high\n"); + kernel_power_off(); + + /* + * Worst of the worst case trigger emergency restart + */ + WARN(1, "Attempting emergency_restart: Temperature too high\n"); + emergency_restart(); +} + +static DECLARE_DELAYED_WORK(thermal_emergency_poweroff_work, + thermal_emergency_poweroff_func); + +/** + * thermal_emergency_poweroff - Trigger an emergency system poweroff + * + * This may be called from any critical situation to trigger a system shutdown + * after a known period of time. By default this is not scheduled. + */ +static void thermal_emergency_poweroff(void) +{ + int poweroff_delay_ms = CONFIG_THERMAL_EMERGENCY_POWEROFF_DELAY_MS; + /* + * poweroff_delay_ms must be a carefully profiled positive value. + * Its a must for thermal_emergency_poweroff_work to be scheduled + */ + if (poweroff_delay_ms <= 0) + return; + schedule_delayed_work(&thermal_emergency_poweroff_work, + msecs_to_jiffies(poweroff_delay_ms)); +} + +static void handle_critical_trips(struct thermal_zone_device *tz, + int trip, enum thermal_trip_type trip_type) +{ + int trip_temp; + + tz->ops->get_trip_temp(tz, trip, &trip_temp); + + /* If we have not crossed the trip_temp, we do not care. */ + if (trip_temp <= 0 || tz->temperature < trip_temp) + return; + + trace_thermal_zone_trip(tz, trip, trip_type); + + if (tz->ops->notify) + tz->ops->notify(tz, trip, trip_type); + + if (trip_type == THERMAL_TRIP_CRITICAL) { + dev_emerg(&tz->device, + "critical temperature reached (%d C), shutting down\n", + tz->temperature / 1000); + mutex_lock(&poweroff_lock); + if (!power_off_triggered) { + /* + * Queue a backup emergency shutdown in the event of + * orderly_poweroff failure + */ + thermal_emergency_poweroff(); + orderly_poweroff(true); + power_off_triggered = true; + } + mutex_unlock(&poweroff_lock); + } +} + +static void handle_thermal_trip(struct thermal_zone_device *tz, int trip) +{ + enum thermal_trip_type type; + + /* Ignore disabled trip points */ + if (test_bit(trip, &tz->trips_disabled)) + return; + + tz->ops->get_trip_type(tz, trip, &type); + + if (type == THERMAL_TRIP_CRITICAL || type == THERMAL_TRIP_HOT) + handle_critical_trips(tz, trip, type); + else + handle_non_critical_trips(tz, trip, type); + /* + * Alright, we handled this trip successfully. + * So, start monitoring again. + */ + monitor_thermal_zone(tz); +} + +static void update_temperature(struct thermal_zone_device *tz) +{ + int temp, ret; + + ret = thermal_zone_get_temp(tz, &temp); + if (ret) { + if (ret != -EAGAIN) + dev_warn(&tz->device, + "failed to read out thermal zone (%d)\n", + ret); + return; + } + + mutex_lock(&tz->lock); + tz->last_temperature = tz->temperature; + tz->temperature = temp; + mutex_unlock(&tz->lock); + + trace_thermal_temperature(tz); + if (tz->last_temperature == THERMAL_TEMP_INVALID) + dev_dbg(&tz->device, "last_temperature N/A, current_temperature=%d\n", + tz->temperature); + else + dev_dbg(&tz->device, "last_temperature=%d, current_temperature=%d\n", + tz->last_temperature, tz->temperature); +} + +static void thermal_zone_device_init(struct thermal_zone_device *tz) +{ + struct thermal_instance *pos; + tz->temperature = THERMAL_TEMP_INVALID; + tz->prev_low_trip = -INT_MAX; + tz->prev_high_trip = INT_MAX; + list_for_each_entry(pos, &tz->thermal_instances, tz_node) + pos->initialized = false; +} + +static void thermal_zone_device_reset(struct thermal_zone_device *tz) +{ + tz->passive = 0; + thermal_zone_device_init(tz); +} + +void thermal_zone_device_update(struct thermal_zone_device *tz, + enum thermal_notify_event event) +{ + int count; + + if (atomic_read(&in_suspend)) + return; + + if (!tz->ops->get_temp) + return; + + update_temperature(tz); + + thermal_zone_set_trips(tz); + + tz->notify_event = event; + + for (count = 0; count < tz->trips; count++) + handle_thermal_trip(tz, count); +} +EXPORT_SYMBOL_GPL(thermal_zone_device_update); + +/** + * thermal_notify_framework - Sensor drivers use this API to notify framework + * @tz: thermal zone device + * @trip: indicates which trip point has been crossed + * + * This function handles the trip events from sensor drivers. It starts + * throttling the cooling devices according to the policy configured. + * For CRITICAL and HOT trip points, this notifies the respective drivers, + * and does actual throttling for other trip points i.e ACTIVE and PASSIVE. + * The throttling policy is based on the configured platform data; if no + * platform data is provided, this uses the step_wise throttling policy. + */ +void thermal_notify_framework(struct thermal_zone_device *tz, int trip) +{ + handle_thermal_trip(tz, trip); +} +EXPORT_SYMBOL_GPL(thermal_notify_framework); + +static void thermal_zone_device_check(struct work_struct *work) +{ + struct thermal_zone_device *tz = container_of(work, struct + thermal_zone_device, + poll_queue.work); + thermal_zone_device_update(tz, THERMAL_EVENT_UNSPECIFIED); +} + +/* + * Power actor section: interface to power actors to estimate power + * + * Set of functions used to interact to cooling devices that know + * how to estimate their devices power consumption. + */ + +/** + * power_actor_get_max_power() - get the maximum power that a cdev can consume + * @cdev: pointer to &thermal_cooling_device + * @tz: a valid thermal zone device pointer + * @max_power: pointer in which to store the maximum power + * + * Calculate the maximum power consumption in milliwats that the + * cooling device can currently consume and store it in @max_power. + * + * Return: 0 on success, -EINVAL if @cdev doesn't support the + * power_actor API or -E* on other error. + */ +int power_actor_get_max_power(struct thermal_cooling_device *cdev, + struct thermal_zone_device *tz, u32 *max_power) +{ + if (!cdev_is_power_actor(cdev)) + return -EINVAL; + + return cdev->ops->state2power(cdev, tz, 0, max_power); +} + +/** + * power_actor_get_min_power() - get the mainimum power that a cdev can consume + * @cdev: pointer to &thermal_cooling_device + * @tz: a valid thermal zone device pointer + * @min_power: pointer in which to store the minimum power + * + * Calculate the minimum power consumption in milliwatts that the + * cooling device can currently consume and store it in @min_power. + * + * Return: 0 on success, -EINVAL if @cdev doesn't support the + * power_actor API or -E* on other error. + */ +int power_actor_get_min_power(struct thermal_cooling_device *cdev, + struct thermal_zone_device *tz, u32 *min_power) +{ + unsigned long max_state; + int ret; + + if (!cdev_is_power_actor(cdev)) + return -EINVAL; + + ret = cdev->ops->get_max_state(cdev, &max_state); + if (ret) + return ret; + + return cdev->ops->state2power(cdev, tz, max_state, min_power); +} + +/** + * power_actor_set_power() - limit the maximum power a cooling device consumes + * @cdev: pointer to &thermal_cooling_device + * @instance: thermal instance to update + * @power: the power in milliwatts + * + * Set the cooling device to consume at most @power milliwatts. The limit is + * expected to be a cap at the maximum power consumption. + * + * Return: 0 on success, -EINVAL if the cooling device does not + * implement the power actor API or -E* for other failures. + */ +int power_actor_set_power(struct thermal_cooling_device *cdev, + struct thermal_instance *instance, u32 power) +{ + unsigned long state; + int ret; + + if (!cdev_is_power_actor(cdev)) + return -EINVAL; + + ret = cdev->ops->power2state(cdev, instance->tz, power, &state); + if (ret) + return ret; + + instance->target = state; + mutex_lock(&cdev->lock); + cdev->updated = false; + mutex_unlock(&cdev->lock); + thermal_cdev_update(cdev); + + return 0; +} + +void thermal_zone_device_rebind_exception(struct thermal_zone_device *tz, + const char *cdev_type, size_t size) +{ + struct thermal_cooling_device *cdev = NULL; + + mutex_lock(&thermal_list_lock); + list_for_each_entry(cdev, &thermal_cdev_list, node) { + /* skip non matching cdevs */ + if (strncmp(cdev_type, cdev->type, size)) + continue; + + /* re binding the exception matching the type pattern */ + thermal_zone_bind_cooling_device(tz, THERMAL_TRIPS_NONE, cdev, + THERMAL_NO_LIMIT, + THERMAL_NO_LIMIT, + THERMAL_WEIGHT_DEFAULT); + } + mutex_unlock(&thermal_list_lock); +} + +void thermal_zone_device_unbind_exception(struct thermal_zone_device *tz, + const char *cdev_type, size_t size) +{ + struct thermal_cooling_device *cdev = NULL; + + mutex_lock(&thermal_list_lock); + list_for_each_entry(cdev, &thermal_cdev_list, node) { + /* skip non matching cdevs */ + if (strncmp(cdev_type, cdev->type, size)) + continue; + /* unbinding the exception matching the type pattern */ + thermal_zone_unbind_cooling_device(tz, THERMAL_TRIPS_NONE, + cdev); + } + mutex_unlock(&thermal_list_lock); +} + +/* + * Device management section: cooling devices, zones devices, and binding + * + * Set of functions provided by the thermal core for: + * - cooling devices lifecycle: registration, unregistration, + * binding, and unbinding. + * - thermal zone devices lifecycle: registration, unregistration, + * binding, and unbinding. + */ + +/** + * thermal_zone_bind_cooling_device() - bind a cooling device to a thermal zone + * @tz: pointer to struct thermal_zone_device + * @trip: indicates which trip point the cooling devices is + * associated with in this thermal zone. + * @cdev: pointer to struct thermal_cooling_device + * @upper: the Maximum cooling state for this trip point. + * THERMAL_NO_LIMIT means no upper limit, + * and the cooling device can be in max_state. + * @lower: the Minimum cooling state can be used for this trip point. + * THERMAL_NO_LIMIT means no lower limit, + * and the cooling device can be in cooling state 0. + * @weight: The weight of the cooling device to be bound to the + * thermal zone. Use THERMAL_WEIGHT_DEFAULT for the + * default value + * + * This interface function bind a thermal cooling device to the certain trip + * point of a thermal zone device. + * This function is usually called in the thermal zone device .bind callback. + * + * Return: 0 on success, the proper error value otherwise. + */ +int thermal_zone_bind_cooling_device(struct thermal_zone_device *tz, + int trip, + struct thermal_cooling_device *cdev, + unsigned long upper, unsigned long lower, + unsigned int weight) +{ + struct thermal_instance *dev; + struct thermal_instance *pos; + struct thermal_zone_device *pos1; + struct thermal_cooling_device *pos2; + unsigned long max_state; + int result, ret; + + if (trip >= tz->trips || (trip < 0 && trip != THERMAL_TRIPS_NONE)) + return -EINVAL; + + list_for_each_entry(pos1, &thermal_tz_list, node) { + if (pos1 == tz) + break; + } + list_for_each_entry(pos2, &thermal_cdev_list, node) { + if (pos2 == cdev) + break; + } + + if (tz != pos1 || cdev != pos2) + return -EINVAL; + + ret = cdev->ops->get_max_state(cdev, &max_state); + if (ret) + return ret; + + /* lower default 0, upper default max_state */ + lower = lower == THERMAL_NO_LIMIT ? 0 : lower; + upper = upper == THERMAL_NO_LIMIT ? max_state : upper; + + if (lower > upper || upper > max_state) + return -EINVAL; + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + dev->tz = tz; + dev->cdev = cdev; + dev->trip = trip; + dev->upper = upper; + dev->lower = lower; + dev->target = THERMAL_NO_TARGET; + dev->weight = weight; + + result = ida_simple_get(&tz->ida, 0, 0, GFP_KERNEL); + if (result < 0) + goto free_mem; + + dev->id = result; + sprintf(dev->name, "cdev%d", dev->id); + result = + sysfs_create_link(&tz->device.kobj, &cdev->device.kobj, dev->name); + if (result) + goto release_ida; + + sprintf(dev->attr_name, "cdev%d_trip_point", dev->id); + sysfs_attr_init(&dev->attr.attr); + dev->attr.attr.name = dev->attr_name; + dev->attr.attr.mode = 0444; + dev->attr.show = trip_point_show; + result = device_create_file(&tz->device, &dev->attr); + if (result) + goto remove_symbol_link; + + sprintf(dev->weight_attr_name, "cdev%d_weight", dev->id); + sysfs_attr_init(&dev->weight_attr.attr); + dev->weight_attr.attr.name = dev->weight_attr_name; + dev->weight_attr.attr.mode = S_IWUSR | S_IRUGO; + dev->weight_attr.show = weight_show; + dev->weight_attr.store = weight_store; + result = device_create_file(&tz->device, &dev->weight_attr); + if (result) + goto remove_trip_file; + + mutex_lock(&tz->lock); + mutex_lock(&cdev->lock); + list_for_each_entry(pos, &tz->thermal_instances, tz_node) + if (pos->tz == tz && pos->trip == trip && pos->cdev == cdev) { + result = -EEXIST; + break; + } + if (!result) { + list_add_tail(&dev->tz_node, &tz->thermal_instances); + list_add_tail(&dev->cdev_node, &cdev->thermal_instances); + atomic_set(&tz->need_update, 1); + } + mutex_unlock(&cdev->lock); + mutex_unlock(&tz->lock); + + if (!result) + return 0; + + device_remove_file(&tz->device, &dev->weight_attr); +remove_trip_file: + device_remove_file(&tz->device, &dev->attr); +remove_symbol_link: + sysfs_remove_link(&tz->device.kobj, dev->name); +release_ida: + ida_simple_remove(&tz->ida, dev->id); +free_mem: + kfree(dev); + return result; +} +EXPORT_SYMBOL_GPL(thermal_zone_bind_cooling_device); + +/** + * thermal_zone_unbind_cooling_device() - unbind a cooling device from a + * thermal zone. + * @tz: pointer to a struct thermal_zone_device. + * @trip: indicates which trip point the cooling devices is + * associated with in this thermal zone. + * @cdev: pointer to a struct thermal_cooling_device. + * + * This interface function unbind a thermal cooling device from the certain + * trip point of a thermal zone device. + * This function is usually called in the thermal zone device .unbind callback. + * + * Return: 0 on success, the proper error value otherwise. + */ +int thermal_zone_unbind_cooling_device(struct thermal_zone_device *tz, + int trip, + struct thermal_cooling_device *cdev) +{ + struct thermal_instance *pos, *next; + + mutex_lock(&tz->lock); + mutex_lock(&cdev->lock); + list_for_each_entry_safe(pos, next, &tz->thermal_instances, tz_node) { + if (pos->tz == tz && pos->trip == trip && pos->cdev == cdev) { + list_del(&pos->tz_node); + list_del(&pos->cdev_node); + mutex_unlock(&cdev->lock); + mutex_unlock(&tz->lock); + goto unbind; + } + } + mutex_unlock(&cdev->lock); + mutex_unlock(&tz->lock); + + return -ENODEV; + +unbind: + device_remove_file(&tz->device, &pos->weight_attr); + device_remove_file(&tz->device, &pos->attr); + sysfs_remove_link(&tz->device.kobj, pos->name); + ida_simple_remove(&tz->ida, pos->id); + kfree(pos); + return 0; +} +EXPORT_SYMBOL_GPL(thermal_zone_unbind_cooling_device); + +static void thermal_release(struct device *dev) +{ + struct thermal_zone_device *tz; + struct thermal_cooling_device *cdev; + + if (!strncmp(dev_name(dev), "thermal_zone", + sizeof("thermal_zone") - 1)) { + tz = to_thermal_zone(dev); + thermal_zone_destroy_device_groups(tz); + kfree(tz); + } else if (!strncmp(dev_name(dev), "cooling_device", + sizeof("cooling_device") - 1)) { + cdev = to_cooling_device(dev); + kfree(cdev); + } +} + +static struct class thermal_class = { + .name = "thermal", + .dev_release = thermal_release, +}; + +static inline +void print_bind_err_msg(struct thermal_zone_device *tz, + struct thermal_cooling_device *cdev, int ret) +{ + dev_err(&tz->device, "binding zone %s with cdev %s failed:%d\n", + tz->type, cdev->type, ret); +} + +static void __bind(struct thermal_zone_device *tz, int mask, + struct thermal_cooling_device *cdev, + unsigned long *limits, + unsigned int weight) +{ + int i, ret; + + for (i = 0; i < tz->trips; i++) { + if (mask & (1 << i)) { + unsigned long upper, lower; + + upper = THERMAL_NO_LIMIT; + lower = THERMAL_NO_LIMIT; + if (limits) { + lower = limits[i * 2]; + upper = limits[i * 2 + 1]; + } + ret = thermal_zone_bind_cooling_device(tz, i, cdev, + upper, lower, + weight); + if (ret) + print_bind_err_msg(tz, cdev, ret); + } + } +} + +static void bind_cdev(struct thermal_cooling_device *cdev) +{ + int i, ret; + const struct thermal_zone_params *tzp; + struct thermal_zone_device *pos = NULL; + + mutex_lock(&thermal_list_lock); + + list_for_each_entry(pos, &thermal_tz_list, node) { + if (!pos->tzp && !pos->ops->bind) + continue; + + if (pos->ops->bind) { + ret = pos->ops->bind(pos, cdev); + if (ret) + print_bind_err_msg(pos, cdev, ret); + continue; + } + + tzp = pos->tzp; + if (!tzp || !tzp->tbp) + continue; + + for (i = 0; i < tzp->num_tbps; i++) { + if (tzp->tbp[i].cdev || !tzp->tbp[i].match) + continue; + if (tzp->tbp[i].match(pos, cdev)) + continue; + tzp->tbp[i].cdev = cdev; + __bind(pos, tzp->tbp[i].trip_mask, cdev, + tzp->tbp[i].binding_limits, + tzp->tbp[i].weight); + } + } + + mutex_unlock(&thermal_list_lock); +} + +/** + * __thermal_cooling_device_register() - register a new thermal cooling device + * @np: a pointer to a device tree node. + * @type: the thermal cooling device type. + * @devdata: device private data. + * @ops: standard thermal cooling devices callbacks. + * + * This interface function adds a new thermal cooling device (fan/processor/...) + * to /sys/class/thermal/ folder as cooling_device[0-*]. It tries to bind itself + * to all the thermal zone devices registered at the same time. + * It also gives the opportunity to link the cooling device to a device tree + * node, so that it can be bound to a thermal zone created out of device tree. + * + * Return: a pointer to the created struct thermal_cooling_device or an + * ERR_PTR. Caller must check return value with IS_ERR*() helpers. + */ +static struct thermal_cooling_device * +__thermal_cooling_device_register(struct device_node *np, + char *type, void *devdata, + const struct thermal_cooling_device_ops *ops) +{ + struct thermal_cooling_device *cdev; + struct thermal_zone_device *pos = NULL; + int result; + + if (type && strlen(type) >= THERMAL_NAME_LENGTH) + return ERR_PTR(-EINVAL); + + if (!ops || !ops->get_max_state || !ops->get_cur_state || + !ops->set_cur_state) + return ERR_PTR(-EINVAL); + + cdev = kzalloc(sizeof(*cdev), GFP_KERNEL); + if (!cdev) + return ERR_PTR(-ENOMEM); + + result = ida_simple_get(&thermal_cdev_ida, 0, 0, GFP_KERNEL); + if (result < 0) { + kfree(cdev); + return ERR_PTR(result); + } + + cdev->id = result; + strlcpy(cdev->type, type ? : "", sizeof(cdev->type)); + mutex_init(&cdev->lock); + INIT_LIST_HEAD(&cdev->thermal_instances); + cdev->np = np; + cdev->ops = ops; + cdev->updated = false; + cdev->device.class = &thermal_class; + cdev->devdata = devdata; + thermal_cooling_device_setup_sysfs(cdev); + dev_set_name(&cdev->device, "cooling_device%d", cdev->id); + result = device_register(&cdev->device); + if (result) { + ida_simple_remove(&thermal_cdev_ida, cdev->id); + kfree(cdev); + return ERR_PTR(result); + } + + /* Add 'this' new cdev to the global cdev list */ + mutex_lock(&thermal_list_lock); + list_add(&cdev->node, &thermal_cdev_list); + mutex_unlock(&thermal_list_lock); + + /* Update binding information for 'this' new cdev */ + bind_cdev(cdev); + + mutex_lock(&thermal_list_lock); + list_for_each_entry(pos, &thermal_tz_list, node) + if (atomic_cmpxchg(&pos->need_update, 1, 0)) + thermal_zone_device_update(pos, + THERMAL_EVENT_UNSPECIFIED); + mutex_unlock(&thermal_list_lock); + + return cdev; +} + +/** + * thermal_cooling_device_register() - register a new thermal cooling device + * @type: the thermal cooling device type. + * @devdata: device private data. + * @ops: standard thermal cooling devices callbacks. + * + * This interface function adds a new thermal cooling device (fan/processor/...) + * to /sys/class/thermal/ folder as cooling_device[0-*]. It tries to bind itself + * to all the thermal zone devices registered at the same time. + * + * Return: a pointer to the created struct thermal_cooling_device or an + * ERR_PTR. Caller must check return value with IS_ERR*() helpers. + */ +struct thermal_cooling_device * +thermal_cooling_device_register(char *type, void *devdata, + const struct thermal_cooling_device_ops *ops) +{ + return __thermal_cooling_device_register(NULL, type, devdata, ops); +} +EXPORT_SYMBOL_GPL(thermal_cooling_device_register); + +/** + * thermal_of_cooling_device_register() - register an OF thermal cooling device + * @np: a pointer to a device tree node. + * @type: the thermal cooling device type. + * @devdata: device private data. + * @ops: standard thermal cooling devices callbacks. + * + * This function will register a cooling device with device tree node reference. + * This interface function adds a new thermal cooling device (fan/processor/...) + * to /sys/class/thermal/ folder as cooling_device[0-*]. It tries to bind itself + * to all the thermal zone devices registered at the same time. + * + * Return: a pointer to the created struct thermal_cooling_device or an + * ERR_PTR. Caller must check return value with IS_ERR*() helpers. + */ +struct thermal_cooling_device * +thermal_of_cooling_device_register(struct device_node *np, + char *type, void *devdata, + const struct thermal_cooling_device_ops *ops) +{ + return __thermal_cooling_device_register(np, type, devdata, ops); +} +EXPORT_SYMBOL_GPL(thermal_of_cooling_device_register); + +static void __unbind(struct thermal_zone_device *tz, int mask, + struct thermal_cooling_device *cdev) +{ + int i; + + for (i = 0; i < tz->trips; i++) + if (mask & (1 << i)) + thermal_zone_unbind_cooling_device(tz, i, cdev); +} + +/** + * thermal_cooling_device_unregister - removes a thermal cooling device + * @cdev: the thermal cooling device to remove. + * + * thermal_cooling_device_unregister() must be called when a registered + * thermal cooling device is no longer needed. + */ +void thermal_cooling_device_unregister(struct thermal_cooling_device *cdev) +{ + int i; + const struct thermal_zone_params *tzp; + struct thermal_zone_device *tz; + struct thermal_cooling_device *pos = NULL; + + if (!cdev) + return; + + mutex_lock(&thermal_list_lock); + list_for_each_entry(pos, &thermal_cdev_list, node) + if (pos == cdev) + break; + if (pos != cdev) { + /* thermal cooling device not found */ + mutex_unlock(&thermal_list_lock); + return; + } + list_del(&cdev->node); + + /* Unbind all thermal zones associated with 'this' cdev */ + list_for_each_entry(tz, &thermal_tz_list, node) { + if (tz->ops->unbind) { + tz->ops->unbind(tz, cdev); + continue; + } + + if (!tz->tzp || !tz->tzp->tbp) + continue; + + tzp = tz->tzp; + for (i = 0; i < tzp->num_tbps; i++) { + if (tzp->tbp[i].cdev == cdev) { + __unbind(tz, tzp->tbp[i].trip_mask, cdev); + tzp->tbp[i].cdev = NULL; + } + } + } + + mutex_unlock(&thermal_list_lock); + + ida_simple_remove(&thermal_cdev_ida, cdev->id); + device_del(&cdev->device); + thermal_cooling_device_destroy_sysfs(cdev); + put_device(&cdev->device); +} +EXPORT_SYMBOL_GPL(thermal_cooling_device_unregister); + +static void bind_tz(struct thermal_zone_device *tz) +{ + int i, ret; + struct thermal_cooling_device *pos = NULL; + const struct thermal_zone_params *tzp = tz->tzp; + + if (!tzp && !tz->ops->bind) + return; + + mutex_lock(&thermal_list_lock); + + /* If there is ops->bind, try to use ops->bind */ + if (tz->ops->bind) { + list_for_each_entry(pos, &thermal_cdev_list, node) { + ret = tz->ops->bind(tz, pos); + if (ret) + print_bind_err_msg(tz, pos, ret); + } + goto exit; + } + + if (!tzp || !tzp->tbp) + goto exit; + + list_for_each_entry(pos, &thermal_cdev_list, node) { + for (i = 0; i < tzp->num_tbps; i++) { + if (tzp->tbp[i].cdev || !tzp->tbp[i].match) + continue; + if (tzp->tbp[i].match(tz, pos)) + continue; + tzp->tbp[i].cdev = pos; + __bind(tz, tzp->tbp[i].trip_mask, pos, + tzp->tbp[i].binding_limits, + tzp->tbp[i].weight); + } + } +exit: + mutex_unlock(&thermal_list_lock); +} + +/** + * thermal_zone_device_register() - register a new thermal zone device + * @type: the thermal zone device type + * @trips: the number of trip points the thermal zone support + * @mask: a bit string indicating the writeablility of trip points + * @devdata: private device data + * @ops: standard thermal zone device callbacks + * @tzp: thermal zone platform parameters + * @passive_delay: number of milliseconds to wait between polls when + * performing passive cooling + * @polling_delay: number of milliseconds to wait between polls when checking + * whether trip points have been crossed (0 for interrupt + * driven systems) + * + * This interface function adds a new thermal zone device (sensor) to + * /sys/class/thermal folder as thermal_zone[0-*]. It tries to bind all the + * thermal cooling devices registered at the same time. + * thermal_zone_device_unregister() must be called when the device is no + * longer needed. The passive cooling depends on the .get_trend() return value. + * + * Return: a pointer to the created struct thermal_zone_device or an + * in case of error, an ERR_PTR. Caller must check return value with + * IS_ERR*() helpers. + */ +struct thermal_zone_device * +thermal_zone_device_register(const char *type, int trips, int mask, + void *devdata, struct thermal_zone_device_ops *ops, + struct thermal_zone_params *tzp, int passive_delay, + int polling_delay) +{ + struct thermal_zone_device *tz; + enum thermal_trip_type trip_type; + int trip_temp; + int result; + int count; + struct thermal_governor *governor; + + if (!type || strlen(type) == 0) + return ERR_PTR(-EINVAL); + + if (type && strlen(type) >= THERMAL_NAME_LENGTH) + return ERR_PTR(-EINVAL); + + if (trips > THERMAL_MAX_TRIPS || trips < 0 || mask >> trips) + return ERR_PTR(-EINVAL); + + if (!ops) + return ERR_PTR(-EINVAL); + + if (trips > 0 && (!ops->get_trip_type || !ops->get_trip_temp)) + return ERR_PTR(-EINVAL); + + tz = kzalloc(sizeof(*tz), GFP_KERNEL); + if (!tz) + return ERR_PTR(-ENOMEM); + + INIT_LIST_HEAD(&tz->thermal_instances); + ida_init(&tz->ida); + mutex_init(&tz->lock); + result = ida_simple_get(&thermal_tz_ida, 0, 0, GFP_KERNEL); + if (result < 0) + goto free_tz; + + tz->id = result; + strlcpy(tz->type, type, sizeof(tz->type)); + tz->ops = ops; + tz->tzp = tzp; + tz->device.class = &thermal_class; + tz->devdata = devdata; + tz->trips = trips; + tz->passive_delay = passive_delay; + tz->polling_delay = polling_delay; + + /* sys I/F */ + /* Add nodes that are always present via .groups */ + result = thermal_zone_create_device_groups(tz, mask); + if (result) + goto remove_id; + + /* A new thermal zone needs to be updated anyway. */ + atomic_set(&tz->need_update, 1); + + dev_set_name(&tz->device, "thermal_zone%d", tz->id); + result = device_register(&tz->device); + if (result) + goto remove_device_groups; + + for (count = 0; count < trips; count++) { + if (tz->ops->get_trip_type(tz, count, &trip_type)) + set_bit(count, &tz->trips_disabled); + if (tz->ops->get_trip_temp(tz, count, &trip_temp)) + set_bit(count, &tz->trips_disabled); + /* Check for bogus trip points */ + if (trip_temp == 0) + set_bit(count, &tz->trips_disabled); + } + + /* Update 'this' zone's governor information */ + mutex_lock(&thermal_governor_lock); + + if (tz->tzp) + governor = __find_governor(tz->tzp->governor_name); + else + governor = def_governor; + + result = thermal_set_governor(tz, governor); + if (result) { + mutex_unlock(&thermal_governor_lock); + goto unregister; + } + + mutex_unlock(&thermal_governor_lock); + + if (!tz->tzp || !tz->tzp->no_hwmon) { + result = thermal_add_hwmon_sysfs(tz); + if (result) + goto unregister; + } + + mutex_lock(&thermal_list_lock); + list_add_tail(&tz->node, &thermal_tz_list); + mutex_unlock(&thermal_list_lock); + + /* Bind cooling devices for this zone */ + bind_tz(tz); + + INIT_DELAYED_WORK(&tz->poll_queue, thermal_zone_device_check); + + thermal_zone_device_reset(tz); + /* Update the new thermal zone and mark it as already updated. */ + if (atomic_cmpxchg(&tz->need_update, 1, 0)) + thermal_zone_device_update(tz, THERMAL_EVENT_UNSPECIFIED); + + return tz; + +unregister: + ida_simple_remove(&thermal_tz_ida, tz->id); + device_unregister(&tz->device); + return ERR_PTR(result); + +remove_device_groups: + thermal_zone_destroy_device_groups(tz); +remove_id: + ida_simple_remove(&thermal_tz_ida, tz->id); +free_tz: + kfree(tz); + return ERR_PTR(result); +} +EXPORT_SYMBOL_GPL(thermal_zone_device_register); + +/** + * thermal_zone_device_unregister - removes the registered thermal zone device + * @tz: the thermal zone device to remove + */ +void thermal_zone_device_unregister(struct thermal_zone_device *tz) +{ + int i; + const struct thermal_zone_params *tzp; + struct thermal_cooling_device *cdev; + struct thermal_zone_device *pos = NULL; + + if (!tz) + return; + + tzp = tz->tzp; + + mutex_lock(&thermal_list_lock); + list_for_each_entry(pos, &thermal_tz_list, node) + if (pos == tz) + break; + if (pos != tz) { + /* thermal zone device not found */ + mutex_unlock(&thermal_list_lock); + return; + } + list_del(&tz->node); + + /* Unbind all cdevs associated with 'this' thermal zone */ + list_for_each_entry(cdev, &thermal_cdev_list, node) { + if (tz->ops->unbind) { + tz->ops->unbind(tz, cdev); + continue; + } + + if (!tzp || !tzp->tbp) + break; + + for (i = 0; i < tzp->num_tbps; i++) { + if (tzp->tbp[i].cdev == cdev) { + __unbind(tz, tzp->tbp[i].trip_mask, cdev); + tzp->tbp[i].cdev = NULL; + } + } + } + + mutex_unlock(&thermal_list_lock); + + cancel_delayed_work_sync(&tz->poll_queue); + + thermal_set_governor(tz, NULL); + + thermal_remove_hwmon_sysfs(tz); + ida_simple_remove(&thermal_tz_ida, tz->id); + ida_destroy(&tz->ida); + mutex_destroy(&tz->lock); + device_unregister(&tz->device); +} +EXPORT_SYMBOL_GPL(thermal_zone_device_unregister); + +/** + * thermal_zone_get_zone_by_name() - search for a zone and returns its ref + * @name: thermal zone name to fetch the temperature + * + * When only one zone is found with the passed name, returns a reference to it. + * + * Return: On success returns a reference to an unique thermal zone with + * matching name equals to @name, an ERR_PTR otherwise (-EINVAL for invalid + * paramenters, -ENODEV for not found and -EEXIST for multiple matches). + */ +struct thermal_zone_device *thermal_zone_get_zone_by_name(const char *name) +{ + struct thermal_zone_device *pos = NULL, *ref = ERR_PTR(-EINVAL); + unsigned int found = 0; + + if (!name) + goto exit; + + mutex_lock(&thermal_list_lock); + list_for_each_entry(pos, &thermal_tz_list, node) + if (!strncasecmp(name, pos->type, THERMAL_NAME_LENGTH)) { + found++; + ref = pos; + } + mutex_unlock(&thermal_list_lock); + + /* nothing has been found, thus an error code for it */ + if (found == 0) + ref = ERR_PTR(-ENODEV); + else if (found > 1) + /* Success only when an unique zone is found */ + ref = ERR_PTR(-EEXIST); + +exit: + return ref; +} +EXPORT_SYMBOL_GPL(thermal_zone_get_zone_by_name); + +#ifdef CONFIG_NET +static const struct genl_multicast_group thermal_event_mcgrps[] = { + { .name = THERMAL_GENL_MCAST_GROUP_NAME, }, +}; + +static struct genl_family thermal_event_genl_family __ro_after_init = { + .module = THIS_MODULE, + .name = THERMAL_GENL_FAMILY_NAME, + .version = THERMAL_GENL_VERSION, + .maxattr = THERMAL_GENL_ATTR_MAX, + .mcgrps = thermal_event_mcgrps, + .n_mcgrps = ARRAY_SIZE(thermal_event_mcgrps), +}; + +int thermal_generate_netlink_event(struct thermal_zone_device *tz, + enum events event) +{ + struct sk_buff *skb; + struct nlattr *attr; + struct thermal_genl_event *thermal_event; + void *msg_header; + int size; + int result; + static unsigned int thermal_event_seqnum; + + if (!tz) + return -EINVAL; + + /* allocate memory */ + size = nla_total_size(sizeof(struct thermal_genl_event)) + + nla_total_size(0); + + skb = genlmsg_new(size, GFP_ATOMIC); + if (!skb) + return -ENOMEM; + + /* add the genetlink message header */ + msg_header = genlmsg_put(skb, 0, thermal_event_seqnum++, + &thermal_event_genl_family, 0, + THERMAL_GENL_CMD_EVENT); + if (!msg_header) { + nlmsg_free(skb); + return -ENOMEM; + } + + /* fill the data */ + attr = nla_reserve(skb, THERMAL_GENL_ATTR_EVENT, + sizeof(struct thermal_genl_event)); + + if (!attr) { + nlmsg_free(skb); + return -EINVAL; + } + + thermal_event = nla_data(attr); + if (!thermal_event) { + nlmsg_free(skb); + return -EINVAL; + } + + memset(thermal_event, 0, sizeof(struct thermal_genl_event)); + + thermal_event->orig = tz->id; + thermal_event->event = event; + + /* send multicast genetlink message */ + genlmsg_end(skb, msg_header); + + result = genlmsg_multicast(&thermal_event_genl_family, skb, 0, + 0, GFP_ATOMIC); + if (result) + dev_err(&tz->device, "Failed to send netlink event:%d", result); + + return result; +} +EXPORT_SYMBOL_GPL(thermal_generate_netlink_event); + +static int __init genetlink_init(void) +{ + return genl_register_family(&thermal_event_genl_family); +} + +static void genetlink_exit(void) +{ + genl_unregister_family(&thermal_event_genl_family); +} +#else /* !CONFIG_NET */ +static inline int genetlink_init(void) { return 0; } +static inline void genetlink_exit(void) {} +#endif /* !CONFIG_NET */ + +static int thermal_pm_notify(struct notifier_block *nb, + unsigned long mode, void *_unused) +{ + struct thermal_zone_device *tz; + + switch (mode) { + case PM_HIBERNATION_PREPARE: + case PM_RESTORE_PREPARE: + case PM_SUSPEND_PREPARE: + atomic_set(&in_suspend, 1); + break; + case PM_POST_HIBERNATION: + case PM_POST_RESTORE: + case PM_POST_SUSPEND: + atomic_set(&in_suspend, 0); + list_for_each_entry(tz, &thermal_tz_list, node) { + thermal_zone_device_init(tz); + thermal_zone_device_update(tz, + THERMAL_EVENT_UNSPECIFIED); + } + break; + default: + break; + } + return 0; +} + +static struct notifier_block thermal_pm_nb = { + .notifier_call = thermal_pm_notify, +}; + +static int __init thermal_init(void) +{ + int result; + + mutex_init(&poweroff_lock); + result = thermal_register_governors(); + if (result) + goto error; + + result = class_register(&thermal_class); + if (result) + goto unregister_governors; + + result = genetlink_init(); + if (result) + goto unregister_class; + + result = of_parse_thermal_zones(); + if (result) + goto exit_netlink; + + result = register_pm_notifier(&thermal_pm_nb); + if (result) + pr_warn("Thermal: Can not register suspend notifier, return %d\n", + result); + + return 0; + +exit_netlink: + genetlink_exit(); +unregister_class: + class_unregister(&thermal_class); +unregister_governors: + thermal_unregister_governors(); +error: + ida_destroy(&thermal_tz_ida); + ida_destroy(&thermal_cdev_ida); + mutex_destroy(&thermal_list_lock); + mutex_destroy(&thermal_governor_lock); + mutex_destroy(&poweroff_lock); + return result; +} + +static void __exit thermal_exit(void) +{ + unregister_pm_notifier(&thermal_pm_nb); + of_thermal_destroy_zones(); + genetlink_exit(); + class_unregister(&thermal_class); + thermal_unregister_governors(); + ida_destroy(&thermal_tz_ida); + ida_destroy(&thermal_cdev_ida); + mutex_destroy(&thermal_list_lock); + mutex_destroy(&thermal_governor_lock); +} + +fs_initcall(thermal_init); +module_exit(thermal_exit); diff --git a/drivers/thermal/thermal_core.h b/drivers/thermal/thermal_core.h new file mode 100644 index 000000000..0df190ed8 --- /dev/null +++ b/drivers/thermal/thermal_core.h @@ -0,0 +1,144 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * thermal_core.h + * + * Copyright (C) 2012 Intel Corp + * Author: Durgadoss R <durgadoss.r@intel.com> + */ + +#ifndef __THERMAL_CORE_H__ +#define __THERMAL_CORE_H__ + +#include <linux/device.h> +#include <linux/thermal.h> + +/* Initial state of a cooling device during binding */ +#define THERMAL_NO_TARGET -1UL + +/* + * This structure is used to describe the behavior of + * a certain cooling device on a certain trip point + * in a certain thermal zone + */ +struct thermal_instance { + int id; + char name[THERMAL_NAME_LENGTH]; + struct thermal_zone_device *tz; + struct thermal_cooling_device *cdev; + int trip; + bool initialized; + unsigned long upper; /* Highest cooling state for this trip point */ + unsigned long lower; /* Lowest cooling state for this trip point */ + unsigned long target; /* expected cooling state */ + char attr_name[THERMAL_NAME_LENGTH]; + struct device_attribute attr; + char weight_attr_name[THERMAL_NAME_LENGTH]; + struct device_attribute weight_attr; + struct list_head tz_node; /* node in tz->thermal_instances */ + struct list_head cdev_node; /* node in cdev->thermal_instances */ + unsigned int weight; /* The weight of the cooling device */ +}; + +#define to_thermal_zone(_dev) \ + container_of(_dev, struct thermal_zone_device, device) + +#define to_cooling_device(_dev) \ + container_of(_dev, struct thermal_cooling_device, device) + +int thermal_register_governor(struct thermal_governor *); +void thermal_unregister_governor(struct thermal_governor *); +void thermal_zone_device_rebind_exception(struct thermal_zone_device *, + const char *, size_t); +void thermal_zone_device_unbind_exception(struct thermal_zone_device *, + const char *, size_t); +int thermal_zone_device_set_policy(struct thermal_zone_device *, char *); +int thermal_build_list_of_policies(char *buf); + +/* sysfs I/F */ +int thermal_zone_create_device_groups(struct thermal_zone_device *, int); +void thermal_zone_destroy_device_groups(struct thermal_zone_device *); +void thermal_cooling_device_setup_sysfs(struct thermal_cooling_device *); +void thermal_cooling_device_destroy_sysfs(struct thermal_cooling_device *cdev); +/* used only at binding time */ +ssize_t trip_point_show(struct device *, struct device_attribute *, char *); +ssize_t weight_show(struct device *, struct device_attribute *, char *); +ssize_t weight_store(struct device *, struct device_attribute *, const char *, + size_t); + +#ifdef CONFIG_THERMAL_STATISTICS +void thermal_cooling_device_stats_update(struct thermal_cooling_device *cdev, + unsigned long new_state); +#else +static inline void +thermal_cooling_device_stats_update(struct thermal_cooling_device *cdev, + unsigned long new_state) {} +#endif /* CONFIG_THERMAL_STATISTICS */ + +#ifdef CONFIG_THERMAL_GOV_STEP_WISE +int thermal_gov_step_wise_register(void); +void thermal_gov_step_wise_unregister(void); +#else +static inline int thermal_gov_step_wise_register(void) { return 0; } +static inline void thermal_gov_step_wise_unregister(void) {} +#endif /* CONFIG_THERMAL_GOV_STEP_WISE */ + +#ifdef CONFIG_THERMAL_GOV_FAIR_SHARE +int thermal_gov_fair_share_register(void); +void thermal_gov_fair_share_unregister(void); +#else +static inline int thermal_gov_fair_share_register(void) { return 0; } +static inline void thermal_gov_fair_share_unregister(void) {} +#endif /* CONFIG_THERMAL_GOV_FAIR_SHARE */ + +#ifdef CONFIG_THERMAL_GOV_BANG_BANG +int thermal_gov_bang_bang_register(void); +void thermal_gov_bang_bang_unregister(void); +#else +static inline int thermal_gov_bang_bang_register(void) { return 0; } +static inline void thermal_gov_bang_bang_unregister(void) {} +#endif /* CONFIG_THERMAL_GOV_BANG_BANG */ + +#ifdef CONFIG_THERMAL_GOV_USER_SPACE +int thermal_gov_user_space_register(void); +void thermal_gov_user_space_unregister(void); +#else +static inline int thermal_gov_user_space_register(void) { return 0; } +static inline void thermal_gov_user_space_unregister(void) {} +#endif /* CONFIG_THERMAL_GOV_USER_SPACE */ + +#ifdef CONFIG_THERMAL_GOV_POWER_ALLOCATOR +int thermal_gov_power_allocator_register(void); +void thermal_gov_power_allocator_unregister(void); +#else +static inline int thermal_gov_power_allocator_register(void) { return 0; } +static inline void thermal_gov_power_allocator_unregister(void) {} +#endif /* CONFIG_THERMAL_GOV_POWER_ALLOCATOR */ + +/* device tree support */ +#ifdef CONFIG_THERMAL_OF +int of_parse_thermal_zones(void); +void of_thermal_destroy_zones(void); +int of_thermal_get_ntrips(struct thermal_zone_device *); +bool of_thermal_is_trip_valid(struct thermal_zone_device *, int); +const struct thermal_trip * +of_thermal_get_trip_points(struct thermal_zone_device *); +#else +static inline int of_parse_thermal_zones(void) { return 0; } +static inline void of_thermal_destroy_zones(void) { } +static inline int of_thermal_get_ntrips(struct thermal_zone_device *tz) +{ + return 0; +} +static inline bool of_thermal_is_trip_valid(struct thermal_zone_device *tz, + int trip) +{ + return false; +} +static inline const struct thermal_trip * +of_thermal_get_trip_points(struct thermal_zone_device *tz) +{ + return NULL; +} +#endif + +#endif /* __THERMAL_CORE_H__ */ diff --git a/drivers/thermal/thermal_helpers.c b/drivers/thermal/thermal_helpers.c new file mode 100644 index 000000000..2ba756af7 --- /dev/null +++ b/drivers/thermal/thermal_helpers.c @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * thermal_helpers.c - helper functions to handle thermal devices + * + * Copyright (C) 2016 Eduardo Valentin <edubezval@gmail.com> + * + * Highly based on original thermal_core.c + * Copyright (C) 2008 Intel Corp + * Copyright (C) 2008 Zhang Rui <rui.zhang@intel.com> + * Copyright (C) 2008 Sujith Thomas <sujith.thomas@intel.com> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/sysfs.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/string.h> + +#include <trace/events/thermal.h> + +#include "thermal_core.h" + +int get_tz_trend(struct thermal_zone_device *tz, int trip) +{ + enum thermal_trend trend; + + if (tz->emul_temperature || !tz->ops->get_trend || + tz->ops->get_trend(tz, trip, &trend)) { + if (tz->temperature > tz->last_temperature) + trend = THERMAL_TREND_RAISING; + else if (tz->temperature < tz->last_temperature) + trend = THERMAL_TREND_DROPPING; + else + trend = THERMAL_TREND_STABLE; + } + + return trend; +} +EXPORT_SYMBOL(get_tz_trend); + +struct thermal_instance * +get_thermal_instance(struct thermal_zone_device *tz, + struct thermal_cooling_device *cdev, int trip) +{ + struct thermal_instance *pos = NULL; + struct thermal_instance *target_instance = NULL; + + mutex_lock(&tz->lock); + mutex_lock(&cdev->lock); + + list_for_each_entry(pos, &tz->thermal_instances, tz_node) { + if (pos->tz == tz && pos->trip == trip && pos->cdev == cdev) { + target_instance = pos; + break; + } + } + + mutex_unlock(&cdev->lock); + mutex_unlock(&tz->lock); + + return target_instance; +} +EXPORT_SYMBOL(get_thermal_instance); + +/** + * thermal_zone_get_temp() - returns the temperature of a thermal zone + * @tz: a valid pointer to a struct thermal_zone_device + * @temp: a valid pointer to where to store the resulting temperature. + * + * When a valid thermal zone reference is passed, it will fetch its + * temperature and fill @temp. + * + * Return: On success returns 0, an error code otherwise + */ +int thermal_zone_get_temp(struct thermal_zone_device *tz, int *temp) +{ + int ret = -EINVAL; + int count; + int crit_temp = INT_MAX; + enum thermal_trip_type type; + + if (!tz || IS_ERR(tz) || !tz->ops->get_temp) + goto exit; + + mutex_lock(&tz->lock); + + ret = tz->ops->get_temp(tz, temp); + + if (IS_ENABLED(CONFIG_THERMAL_EMULATION) && tz->emul_temperature) { + for (count = 0; count < tz->trips; count++) { + ret = tz->ops->get_trip_type(tz, count, &type); + if (!ret && type == THERMAL_TRIP_CRITICAL) { + ret = tz->ops->get_trip_temp(tz, count, + &crit_temp); + break; + } + } + + /* + * Only allow emulating a temperature when the real temperature + * is below the critical temperature so that the emulation code + * cannot hide critical conditions. + */ + if (!ret && *temp < crit_temp) + *temp = tz->emul_temperature; + } + + mutex_unlock(&tz->lock); +exit: + return ret; +} +EXPORT_SYMBOL_GPL(thermal_zone_get_temp); + +void thermal_zone_set_trips(struct thermal_zone_device *tz) +{ + int low = -INT_MAX; + int high = INT_MAX; + int trip_temp, hysteresis; + int i, ret; + + mutex_lock(&tz->lock); + + if (!tz->ops->set_trips || !tz->ops->get_trip_hyst) + goto exit; + + for (i = 0; i < tz->trips; i++) { + int trip_low; + + tz->ops->get_trip_temp(tz, i, &trip_temp); + tz->ops->get_trip_hyst(tz, i, &hysteresis); + + trip_low = trip_temp - hysteresis; + + if (trip_low < tz->temperature && trip_low > low) + low = trip_low; + + if (trip_temp > tz->temperature && trip_temp < high) + high = trip_temp; + } + + /* No need to change trip points */ + if (tz->prev_low_trip == low && tz->prev_high_trip == high) + goto exit; + + tz->prev_low_trip = low; + tz->prev_high_trip = high; + + dev_dbg(&tz->device, + "new temperature boundaries: %d < x < %d\n", low, high); + + /* + * Set a temperature window. When this window is left the driver + * must inform the thermal core via thermal_zone_device_update. + */ + ret = tz->ops->set_trips(tz, low, high); + if (ret) + dev_err(&tz->device, "Failed to set trips: %d\n", ret); + +exit: + mutex_unlock(&tz->lock); +} +EXPORT_SYMBOL_GPL(thermal_zone_set_trips); + +void thermal_cdev_update(struct thermal_cooling_device *cdev) +{ + struct thermal_instance *instance; + unsigned long target = 0; + + mutex_lock(&cdev->lock); + /* cooling device is updated*/ + if (cdev->updated) { + mutex_unlock(&cdev->lock); + return; + } + + /* Make sure cdev enters the deepest cooling state */ + list_for_each_entry(instance, &cdev->thermal_instances, cdev_node) { + dev_dbg(&cdev->device, "zone%d->target=%lu\n", + instance->tz->id, instance->target); + if (instance->target == THERMAL_NO_TARGET) + continue; + if (instance->target > target) + target = instance->target; + } + + if (!cdev->ops->set_cur_state(cdev, target)) + thermal_cooling_device_stats_update(cdev, target); + + cdev->updated = true; + mutex_unlock(&cdev->lock); + trace_cdev_update(cdev, target); + dev_dbg(&cdev->device, "set to state %lu\n", target); +} +EXPORT_SYMBOL(thermal_cdev_update); + +/** + * thermal_zone_get_slope - return the slope attribute of the thermal zone + * @tz: thermal zone device with the slope attribute + * + * Return: If the thermal zone device has a slope attribute, return it, else + * return 1. + */ +int thermal_zone_get_slope(struct thermal_zone_device *tz) +{ + if (tz && tz->tzp) + return tz->tzp->slope; + return 1; +} +EXPORT_SYMBOL_GPL(thermal_zone_get_slope); + +/** + * thermal_zone_get_offset - return the offset attribute of the thermal zone + * @tz: thermal zone device with the offset attribute + * + * Return: If the thermal zone device has a offset attribute, return it, else + * return 0. + */ +int thermal_zone_get_offset(struct thermal_zone_device *tz) +{ + if (tz && tz->tzp) + return tz->tzp->offset; + return 0; +} +EXPORT_SYMBOL_GPL(thermal_zone_get_offset); diff --git a/drivers/thermal/thermal_hwmon.c b/drivers/thermal/thermal_hwmon.c new file mode 100644 index 000000000..dd5d8ee37 --- /dev/null +++ b/drivers/thermal/thermal_hwmon.c @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * thermal_hwmon.c - Generic Thermal Management hwmon support. + * + * Code based on Intel thermal_core.c. Copyrights of the original code: + * Copyright (C) 2008 Intel Corp + * Copyright (C) 2008 Zhang Rui <rui.zhang@intel.com> + * Copyright (C) 2008 Sujith Thomas <sujith.thomas@intel.com> + * + * Copyright (C) 2013 Texas Instruments + * Copyright (C) 2013 Eduardo Valentin <eduardo.valentin@ti.com> + */ +#include <linux/hwmon.h> +#include <linux/thermal.h> +#include <linux/slab.h> +#include <linux/err.h> +#include "thermal_hwmon.h" + +/* hwmon sys I/F */ +/* thermal zone devices with the same type share one hwmon device */ +struct thermal_hwmon_device { + char type[THERMAL_NAME_LENGTH]; + struct device *device; + int count; + struct list_head tz_list; + struct list_head node; +}; + +struct thermal_hwmon_attr { + struct device_attribute attr; + char name[16]; +}; + +/* one temperature input for each thermal zone */ +struct thermal_hwmon_temp { + struct list_head hwmon_node; + struct thermal_zone_device *tz; + struct thermal_hwmon_attr temp_input; /* hwmon sys attr */ + struct thermal_hwmon_attr temp_crit; /* hwmon sys attr */ +}; + +static LIST_HEAD(thermal_hwmon_list); + +static DEFINE_MUTEX(thermal_hwmon_list_lock); + +static ssize_t +temp_input_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + int temperature; + int ret; + struct thermal_hwmon_attr *hwmon_attr + = container_of(attr, struct thermal_hwmon_attr, attr); + struct thermal_hwmon_temp *temp + = container_of(hwmon_attr, struct thermal_hwmon_temp, + temp_input); + struct thermal_zone_device *tz = temp->tz; + + ret = thermal_zone_get_temp(tz, &temperature); + + if (ret) + return ret; + + return sprintf(buf, "%d\n", temperature); +} + +static ssize_t +temp_crit_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct thermal_hwmon_attr *hwmon_attr + = container_of(attr, struct thermal_hwmon_attr, attr); + struct thermal_hwmon_temp *temp + = container_of(hwmon_attr, struct thermal_hwmon_temp, + temp_crit); + struct thermal_zone_device *tz = temp->tz; + int temperature; + int ret; + + ret = tz->ops->get_crit_temp(tz, &temperature); + if (ret) + return ret; + + return sprintf(buf, "%d\n", temperature); +} + + +static struct thermal_hwmon_device * +thermal_hwmon_lookup_by_type(const struct thermal_zone_device *tz) +{ + struct thermal_hwmon_device *hwmon; + char type[THERMAL_NAME_LENGTH]; + + mutex_lock(&thermal_hwmon_list_lock); + list_for_each_entry(hwmon, &thermal_hwmon_list, node) { + strcpy(type, tz->type); + strreplace(type, '-', '_'); + if (!strcmp(hwmon->type, type)) { + mutex_unlock(&thermal_hwmon_list_lock); + return hwmon; + } + } + mutex_unlock(&thermal_hwmon_list_lock); + + return NULL; +} + +/* Find the temperature input matching a given thermal zone */ +static struct thermal_hwmon_temp * +thermal_hwmon_lookup_temp(const struct thermal_hwmon_device *hwmon, + const struct thermal_zone_device *tz) +{ + struct thermal_hwmon_temp *temp; + + mutex_lock(&thermal_hwmon_list_lock); + list_for_each_entry(temp, &hwmon->tz_list, hwmon_node) + if (temp->tz == tz) { + mutex_unlock(&thermal_hwmon_list_lock); + return temp; + } + mutex_unlock(&thermal_hwmon_list_lock); + + return NULL; +} + +static bool thermal_zone_crit_temp_valid(struct thermal_zone_device *tz) +{ + int temp; + return tz->ops->get_crit_temp && !tz->ops->get_crit_temp(tz, &temp); +} + +int thermal_add_hwmon_sysfs(struct thermal_zone_device *tz) +{ + struct thermal_hwmon_device *hwmon; + struct thermal_hwmon_temp *temp; + int new_hwmon_device = 1; + int result; + + hwmon = thermal_hwmon_lookup_by_type(tz); + if (hwmon) { + new_hwmon_device = 0; + goto register_sys_interface; + } + + hwmon = kzalloc(sizeof(*hwmon), GFP_KERNEL); + if (!hwmon) + return -ENOMEM; + + INIT_LIST_HEAD(&hwmon->tz_list); + strlcpy(hwmon->type, tz->type, THERMAL_NAME_LENGTH); + strreplace(hwmon->type, '-', '_'); + hwmon->device = hwmon_device_register_with_info(&tz->device, hwmon->type, + hwmon, NULL, NULL); + if (IS_ERR(hwmon->device)) { + result = PTR_ERR(hwmon->device); + goto free_mem; + } + + register_sys_interface: + temp = kzalloc(sizeof(*temp), GFP_KERNEL); + if (!temp) { + result = -ENOMEM; + goto unregister_name; + } + + temp->tz = tz; + hwmon->count++; + + snprintf(temp->temp_input.name, sizeof(temp->temp_input.name), + "temp%d_input", hwmon->count); + temp->temp_input.attr.attr.name = temp->temp_input.name; + temp->temp_input.attr.attr.mode = 0444; + temp->temp_input.attr.show = temp_input_show; + sysfs_attr_init(&temp->temp_input.attr.attr); + result = device_create_file(hwmon->device, &temp->temp_input.attr); + if (result) + goto free_temp_mem; + + if (thermal_zone_crit_temp_valid(tz)) { + snprintf(temp->temp_crit.name, + sizeof(temp->temp_crit.name), + "temp%d_crit", hwmon->count); + temp->temp_crit.attr.attr.name = temp->temp_crit.name; + temp->temp_crit.attr.attr.mode = 0444; + temp->temp_crit.attr.show = temp_crit_show; + sysfs_attr_init(&temp->temp_crit.attr.attr); + result = device_create_file(hwmon->device, + &temp->temp_crit.attr); + if (result) + goto unregister_input; + } + + mutex_lock(&thermal_hwmon_list_lock); + if (new_hwmon_device) + list_add_tail(&hwmon->node, &thermal_hwmon_list); + list_add_tail(&temp->hwmon_node, &hwmon->tz_list); + mutex_unlock(&thermal_hwmon_list_lock); + + return 0; + + unregister_input: + device_remove_file(hwmon->device, &temp->temp_input.attr); + free_temp_mem: + kfree(temp); + unregister_name: + if (new_hwmon_device) + hwmon_device_unregister(hwmon->device); + free_mem: + if (new_hwmon_device) + kfree(hwmon); + + return result; +} +EXPORT_SYMBOL_GPL(thermal_add_hwmon_sysfs); + +void thermal_remove_hwmon_sysfs(struct thermal_zone_device *tz) +{ + struct thermal_hwmon_device *hwmon; + struct thermal_hwmon_temp *temp; + + hwmon = thermal_hwmon_lookup_by_type(tz); + if (unlikely(!hwmon)) { + /* Should never happen... */ + dev_dbg(&tz->device, "hwmon device lookup failed!\n"); + return; + } + + temp = thermal_hwmon_lookup_temp(hwmon, tz); + if (unlikely(!temp)) { + /* Should never happen... */ + dev_dbg(&tz->device, "temperature input lookup failed!\n"); + return; + } + + device_remove_file(hwmon->device, &temp->temp_input.attr); + if (thermal_zone_crit_temp_valid(tz)) + device_remove_file(hwmon->device, &temp->temp_crit.attr); + + mutex_lock(&thermal_hwmon_list_lock); + list_del(&temp->hwmon_node); + kfree(temp); + if (!list_empty(&hwmon->tz_list)) { + mutex_unlock(&thermal_hwmon_list_lock); + return; + } + list_del(&hwmon->node); + mutex_unlock(&thermal_hwmon_list_lock); + + hwmon_device_unregister(hwmon->device); + kfree(hwmon); +} +EXPORT_SYMBOL_GPL(thermal_remove_hwmon_sysfs); diff --git a/drivers/thermal/thermal_hwmon.h b/drivers/thermal/thermal_hwmon.h new file mode 100644 index 000000000..a160b9d62 --- /dev/null +++ b/drivers/thermal/thermal_hwmon.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * thermal_hwmon.h - Generic Thermal Management hwmon support. + * + * Code based on Intel thermal_core.c. Copyrights of the original code: + * Copyright (C) 2008 Intel Corp + * Copyright (C) 2008 Zhang Rui <rui.zhang@intel.com> + * Copyright (C) 2008 Sujith Thomas <sujith.thomas@intel.com> + * + * Copyright (C) 2013 Texas Instruments + * Copyright (C) 2013 Eduardo Valentin <eduardo.valentin@ti.com> + */ +#ifndef __THERMAL_HWMON_H__ +#define __THERMAL_HWMON_H__ + +#include <linux/thermal.h> + +#ifdef CONFIG_THERMAL_HWMON +int thermal_add_hwmon_sysfs(struct thermal_zone_device *tz); +void thermal_remove_hwmon_sysfs(struct thermal_zone_device *tz); +#else +static inline int +thermal_add_hwmon_sysfs(struct thermal_zone_device *tz) +{ + return 0; +} + +static inline void +thermal_remove_hwmon_sysfs(struct thermal_zone_device *tz) +{ +} +#endif + +#endif /* __THERMAL_HWMON_H__ */ diff --git a/drivers/thermal/thermal_sysfs.c b/drivers/thermal/thermal_sysfs.c new file mode 100644 index 000000000..4dce4a8f7 --- /dev/null +++ b/drivers/thermal/thermal_sysfs.c @@ -0,0 +1,1007 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * thermal.c - sysfs interface of thermal devices + * + * Copyright (C) 2016 Eduardo Valentin <edubezval@gmail.com> + * + * Highly based on original thermal_core.c + * Copyright (C) 2008 Intel Corp + * Copyright (C) 2008 Zhang Rui <rui.zhang@intel.com> + * Copyright (C) 2008 Sujith Thomas <sujith.thomas@intel.com> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/sysfs.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/jiffies.h> + +#include "thermal_core.h" + +/* sys I/F for thermal zone */ + +static ssize_t +type_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + + return sprintf(buf, "%s\n", tz->type); +} + +static ssize_t +temp_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + int temperature, ret; + + ret = thermal_zone_get_temp(tz, &temperature); + + if (ret) + return ret; + + return sprintf(buf, "%d\n", temperature); +} + +static ssize_t +mode_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + enum thermal_device_mode mode; + int result; + + if (!tz->ops->get_mode) + return -EPERM; + + result = tz->ops->get_mode(tz, &mode); + if (result) + return result; + + return sprintf(buf, "%s\n", mode == THERMAL_DEVICE_ENABLED ? "enabled" + : "disabled"); +} + +static ssize_t +mode_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + int result; + + if (!tz->ops->set_mode) + return -EPERM; + + if (!strncmp(buf, "enabled", sizeof("enabled") - 1)) + result = tz->ops->set_mode(tz, THERMAL_DEVICE_ENABLED); + else if (!strncmp(buf, "disabled", sizeof("disabled") - 1)) + result = tz->ops->set_mode(tz, THERMAL_DEVICE_DISABLED); + else + result = -EINVAL; + + if (result) + return result; + + return count; +} + +static ssize_t +trip_point_type_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + enum thermal_trip_type type; + int trip, result; + + if (!tz->ops->get_trip_type) + return -EPERM; + + if (sscanf(attr->attr.name, "trip_point_%d_type", &trip) != 1) + return -EINVAL; + + result = tz->ops->get_trip_type(tz, trip, &type); + if (result) + return result; + + switch (type) { + case THERMAL_TRIP_CRITICAL: + return sprintf(buf, "critical\n"); + case THERMAL_TRIP_HOT: + return sprintf(buf, "hot\n"); + case THERMAL_TRIP_PASSIVE: + return sprintf(buf, "passive\n"); + case THERMAL_TRIP_ACTIVE: + return sprintf(buf, "active\n"); + default: + return sprintf(buf, "unknown\n"); + } +} + +static ssize_t +trip_point_temp_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + int trip, ret; + int temperature; + + if (!tz->ops->set_trip_temp) + return -EPERM; + + if (sscanf(attr->attr.name, "trip_point_%d_temp", &trip) != 1) + return -EINVAL; + + if (kstrtoint(buf, 10, &temperature)) + return -EINVAL; + + ret = tz->ops->set_trip_temp(tz, trip, temperature); + if (ret) + return ret; + + thermal_zone_device_update(tz, THERMAL_EVENT_UNSPECIFIED); + + return count; +} + +static ssize_t +trip_point_temp_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + int trip, ret; + int temperature; + + if (!tz->ops->get_trip_temp) + return -EPERM; + + if (sscanf(attr->attr.name, "trip_point_%d_temp", &trip) != 1) + return -EINVAL; + + ret = tz->ops->get_trip_temp(tz, trip, &temperature); + + if (ret) + return ret; + + return sprintf(buf, "%d\n", temperature); +} + +static ssize_t +trip_point_hyst_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + int trip, ret; + int temperature; + + if (!tz->ops->set_trip_hyst) + return -EPERM; + + if (sscanf(attr->attr.name, "trip_point_%d_hyst", &trip) != 1) + return -EINVAL; + + if (kstrtoint(buf, 10, &temperature)) + return -EINVAL; + + /* + * We are not doing any check on the 'temperature' value + * here. The driver implementing 'set_trip_hyst' has to + * take care of this. + */ + ret = tz->ops->set_trip_hyst(tz, trip, temperature); + + if (!ret) + thermal_zone_set_trips(tz); + + return ret ? ret : count; +} + +static ssize_t +trip_point_hyst_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + int trip, ret; + int temperature; + + if (!tz->ops->get_trip_hyst) + return -EPERM; + + if (sscanf(attr->attr.name, "trip_point_%d_hyst", &trip) != 1) + return -EINVAL; + + ret = tz->ops->get_trip_hyst(tz, trip, &temperature); + + return ret ? ret : sprintf(buf, "%d\n", temperature); +} + +static ssize_t +passive_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + int state; + + if (sscanf(buf, "%d\n", &state) != 1) + return -EINVAL; + + /* sanity check: values below 1000 millicelcius don't make sense + * and can cause the system to go into a thermal heart attack + */ + if (state && state < 1000) + return -EINVAL; + + if (state && !tz->forced_passive) { + if (!tz->passive_delay) + tz->passive_delay = 1000; + thermal_zone_device_rebind_exception(tz, "Processor", + sizeof("Processor")); + } else if (!state && tz->forced_passive) { + tz->passive_delay = 0; + thermal_zone_device_unbind_exception(tz, "Processor", + sizeof("Processor")); + } + + tz->forced_passive = state; + + thermal_zone_device_update(tz, THERMAL_EVENT_UNSPECIFIED); + + return count; +} + +static ssize_t +passive_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + + return sprintf(buf, "%d\n", tz->forced_passive); +} + +static ssize_t +policy_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + char name[THERMAL_NAME_LENGTH]; + int ret; + + snprintf(name, sizeof(name), "%s", buf); + + ret = thermal_zone_device_set_policy(tz, name); + if (!ret) + ret = count; + + return ret; +} + +static ssize_t +policy_show(struct device *dev, struct device_attribute *devattr, char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + + return sprintf(buf, "%s\n", tz->governor->name); +} + +static ssize_t +available_policies_show(struct device *dev, struct device_attribute *devattr, + char *buf) +{ + return thermal_build_list_of_policies(buf); +} + +#if (IS_ENABLED(CONFIG_THERMAL_EMULATION)) +static ssize_t +emul_temp_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + int ret = 0; + int temperature; + + if (kstrtoint(buf, 10, &temperature)) + return -EINVAL; + + if (!tz->ops->set_emul_temp) { + mutex_lock(&tz->lock); + tz->emul_temperature = temperature; + mutex_unlock(&tz->lock); + } else { + ret = tz->ops->set_emul_temp(tz, temperature); + } + + if (!ret) + thermal_zone_device_update(tz, THERMAL_EVENT_UNSPECIFIED); + + return ret ? ret : count; +} +static DEVICE_ATTR_WO(emul_temp); +#endif + +static ssize_t +sustainable_power_show(struct device *dev, struct device_attribute *devattr, + char *buf) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + + if (tz->tzp) + return sprintf(buf, "%u\n", tz->tzp->sustainable_power); + else + return -EIO; +} + +static ssize_t +sustainable_power_store(struct device *dev, struct device_attribute *devattr, + const char *buf, size_t count) +{ + struct thermal_zone_device *tz = to_thermal_zone(dev); + u32 sustainable_power; + + if (!tz->tzp) + return -EIO; + + if (kstrtou32(buf, 10, &sustainable_power)) + return -EINVAL; + + tz->tzp->sustainable_power = sustainable_power; + + return count; +} + +#define create_s32_tzp_attr(name) \ + static ssize_t \ + name##_show(struct device *dev, struct device_attribute *devattr, \ + char *buf) \ + { \ + struct thermal_zone_device *tz = to_thermal_zone(dev); \ + \ + if (tz->tzp) \ + return sprintf(buf, "%d\n", tz->tzp->name); \ + else \ + return -EIO; \ + } \ + \ + static ssize_t \ + name##_store(struct device *dev, struct device_attribute *devattr, \ + const char *buf, size_t count) \ + { \ + struct thermal_zone_device *tz = to_thermal_zone(dev); \ + s32 value; \ + \ + if (!tz->tzp) \ + return -EIO; \ + \ + if (kstrtos32(buf, 10, &value)) \ + return -EINVAL; \ + \ + tz->tzp->name = value; \ + \ + return count; \ + } \ + static DEVICE_ATTR_RW(name) + +create_s32_tzp_attr(k_po); +create_s32_tzp_attr(k_pu); +create_s32_tzp_attr(k_i); +create_s32_tzp_attr(k_d); +create_s32_tzp_attr(integral_cutoff); +create_s32_tzp_attr(slope); +create_s32_tzp_attr(offset); +#undef create_s32_tzp_attr + +/* + * These are thermal zone device attributes that will always be present. + * All the attributes created for tzp (create_s32_tzp_attr) also are always + * present on the sysfs interface. + */ +static DEVICE_ATTR_RO(type); +static DEVICE_ATTR_RO(temp); +static DEVICE_ATTR_RW(policy); +static DEVICE_ATTR_RO(available_policies); +static DEVICE_ATTR_RW(sustainable_power); + +/* These thermal zone device attributes are created based on conditions */ +static DEVICE_ATTR_RW(mode); +static DEVICE_ATTR_RW(passive); + +/* These attributes are unconditionally added to a thermal zone */ +static struct attribute *thermal_zone_dev_attrs[] = { + &dev_attr_type.attr, + &dev_attr_temp.attr, +#if (IS_ENABLED(CONFIG_THERMAL_EMULATION)) + &dev_attr_emul_temp.attr, +#endif + &dev_attr_policy.attr, + &dev_attr_available_policies.attr, + &dev_attr_sustainable_power.attr, + &dev_attr_k_po.attr, + &dev_attr_k_pu.attr, + &dev_attr_k_i.attr, + &dev_attr_k_d.attr, + &dev_attr_integral_cutoff.attr, + &dev_attr_slope.attr, + &dev_attr_offset.attr, + NULL, +}; + +static struct attribute_group thermal_zone_attribute_group = { + .attrs = thermal_zone_dev_attrs, +}; + +/* We expose mode only if .get_mode is present */ +static struct attribute *thermal_zone_mode_attrs[] = { + &dev_attr_mode.attr, + NULL, +}; + +static umode_t thermal_zone_mode_is_visible(struct kobject *kobj, + struct attribute *attr, + int attrno) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct thermal_zone_device *tz; + + tz = container_of(dev, struct thermal_zone_device, device); + + if (tz->ops->get_mode) + return attr->mode; + + return 0; +} + +static struct attribute_group thermal_zone_mode_attribute_group = { + .attrs = thermal_zone_mode_attrs, + .is_visible = thermal_zone_mode_is_visible, +}; + +/* We expose passive only if passive trips are present */ +static struct attribute *thermal_zone_passive_attrs[] = { + &dev_attr_passive.attr, + NULL, +}; + +static umode_t thermal_zone_passive_is_visible(struct kobject *kobj, + struct attribute *attr, + int attrno) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct thermal_zone_device *tz; + enum thermal_trip_type trip_type; + int count, passive = 0; + + tz = container_of(dev, struct thermal_zone_device, device); + + for (count = 0; count < tz->trips && !passive; count++) { + tz->ops->get_trip_type(tz, count, &trip_type); + + if (trip_type == THERMAL_TRIP_PASSIVE) + passive = 1; + } + + if (!passive) + return attr->mode; + + return 0; +} + +static struct attribute_group thermal_zone_passive_attribute_group = { + .attrs = thermal_zone_passive_attrs, + .is_visible = thermal_zone_passive_is_visible, +}; + +static const struct attribute_group *thermal_zone_attribute_groups[] = { + &thermal_zone_attribute_group, + &thermal_zone_mode_attribute_group, + &thermal_zone_passive_attribute_group, + /* This is not NULL terminated as we create the group dynamically */ +}; + +/** + * create_trip_attrs() - create attributes for trip points + * @tz: the thermal zone device + * @mask: Writeable trip point bitmap. + * + * helper function to instantiate sysfs entries for every trip + * point and its properties of a struct thermal_zone_device. + * + * Return: 0 on success, the proper error value otherwise. + */ +static int create_trip_attrs(struct thermal_zone_device *tz, int mask) +{ + struct attribute **attrs; + int indx; + + /* This function works only for zones with at least one trip */ + if (tz->trips <= 0) + return -EINVAL; + + tz->trip_type_attrs = kcalloc(tz->trips, sizeof(*tz->trip_type_attrs), + GFP_KERNEL); + if (!tz->trip_type_attrs) + return -ENOMEM; + + tz->trip_temp_attrs = kcalloc(tz->trips, sizeof(*tz->trip_temp_attrs), + GFP_KERNEL); + if (!tz->trip_temp_attrs) { + kfree(tz->trip_type_attrs); + return -ENOMEM; + } + + if (tz->ops->get_trip_hyst) { + tz->trip_hyst_attrs = kcalloc(tz->trips, + sizeof(*tz->trip_hyst_attrs), + GFP_KERNEL); + if (!tz->trip_hyst_attrs) { + kfree(tz->trip_type_attrs); + kfree(tz->trip_temp_attrs); + return -ENOMEM; + } + } + + attrs = kcalloc(tz->trips * 3 + 1, sizeof(*attrs), GFP_KERNEL); + if (!attrs) { + kfree(tz->trip_type_attrs); + kfree(tz->trip_temp_attrs); + if (tz->ops->get_trip_hyst) + kfree(tz->trip_hyst_attrs); + return -ENOMEM; + } + + for (indx = 0; indx < tz->trips; indx++) { + /* create trip type attribute */ + snprintf(tz->trip_type_attrs[indx].name, THERMAL_NAME_LENGTH, + "trip_point_%d_type", indx); + + sysfs_attr_init(&tz->trip_type_attrs[indx].attr.attr); + tz->trip_type_attrs[indx].attr.attr.name = + tz->trip_type_attrs[indx].name; + tz->trip_type_attrs[indx].attr.attr.mode = S_IRUGO; + tz->trip_type_attrs[indx].attr.show = trip_point_type_show; + attrs[indx] = &tz->trip_type_attrs[indx].attr.attr; + + /* create trip temp attribute */ + snprintf(tz->trip_temp_attrs[indx].name, THERMAL_NAME_LENGTH, + "trip_point_%d_temp", indx); + + sysfs_attr_init(&tz->trip_temp_attrs[indx].attr.attr); + tz->trip_temp_attrs[indx].attr.attr.name = + tz->trip_temp_attrs[indx].name; + tz->trip_temp_attrs[indx].attr.attr.mode = S_IRUGO; + tz->trip_temp_attrs[indx].attr.show = trip_point_temp_show; + if (IS_ENABLED(CONFIG_THERMAL_WRITABLE_TRIPS) && + mask & (1 << indx)) { + tz->trip_temp_attrs[indx].attr.attr.mode |= S_IWUSR; + tz->trip_temp_attrs[indx].attr.store = + trip_point_temp_store; + } + attrs[indx + tz->trips] = &tz->trip_temp_attrs[indx].attr.attr; + + /* create Optional trip hyst attribute */ + if (!tz->ops->get_trip_hyst) + continue; + snprintf(tz->trip_hyst_attrs[indx].name, THERMAL_NAME_LENGTH, + "trip_point_%d_hyst", indx); + + sysfs_attr_init(&tz->trip_hyst_attrs[indx].attr.attr); + tz->trip_hyst_attrs[indx].attr.attr.name = + tz->trip_hyst_attrs[indx].name; + tz->trip_hyst_attrs[indx].attr.attr.mode = S_IRUGO; + tz->trip_hyst_attrs[indx].attr.show = trip_point_hyst_show; + if (tz->ops->set_trip_hyst) { + tz->trip_hyst_attrs[indx].attr.attr.mode |= S_IWUSR; + tz->trip_hyst_attrs[indx].attr.store = + trip_point_hyst_store; + } + attrs[indx + tz->trips * 2] = + &tz->trip_hyst_attrs[indx].attr.attr; + } + attrs[tz->trips * 3] = NULL; + + tz->trips_attribute_group.attrs = attrs; + + return 0; +} + +/** + * destroy_trip_attrs() - destroy attributes for trip points + * @tz: the thermal zone device + * + * helper function to free resources allocated by create_trip_attrs() + */ +static void destroy_trip_attrs(struct thermal_zone_device *tz) +{ + if (!tz) + return; + + kfree(tz->trip_type_attrs); + kfree(tz->trip_temp_attrs); + if (tz->ops->get_trip_hyst) + kfree(tz->trip_hyst_attrs); + kfree(tz->trips_attribute_group.attrs); +} + +int thermal_zone_create_device_groups(struct thermal_zone_device *tz, + int mask) +{ + const struct attribute_group **groups; + int i, size, result; + + /* we need one extra for trips and the NULL to terminate the array */ + size = ARRAY_SIZE(thermal_zone_attribute_groups) + 2; + /* This also takes care of API requirement to be NULL terminated */ + groups = kcalloc(size, sizeof(*groups), GFP_KERNEL); + if (!groups) + return -ENOMEM; + + for (i = 0; i < size - 2; i++) + groups[i] = thermal_zone_attribute_groups[i]; + + if (tz->trips) { + result = create_trip_attrs(tz, mask); + if (result) { + kfree(groups); + + return result; + } + + groups[size - 2] = &tz->trips_attribute_group; + } + + tz->device.groups = groups; + + return 0; +} + +void thermal_zone_destroy_device_groups(struct thermal_zone_device *tz) +{ + if (!tz) + return; + + if (tz->trips) + destroy_trip_attrs(tz); + + kfree(tz->device.groups); +} + +/* sys I/F for cooling device */ +static ssize_t +cdev_type_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct thermal_cooling_device *cdev = to_cooling_device(dev); + + return sprintf(buf, "%s\n", cdev->type); +} + +static ssize_t max_state_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct thermal_cooling_device *cdev = to_cooling_device(dev); + unsigned long state; + int ret; + + ret = cdev->ops->get_max_state(cdev, &state); + if (ret) + return ret; + return sprintf(buf, "%ld\n", state); +} + +static ssize_t cur_state_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct thermal_cooling_device *cdev = to_cooling_device(dev); + unsigned long state; + int ret; + + ret = cdev->ops->get_cur_state(cdev, &state); + if (ret) + return ret; + return sprintf(buf, "%ld\n", state); +} + +static ssize_t +cur_state_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thermal_cooling_device *cdev = to_cooling_device(dev); + unsigned long state; + int result; + + if (sscanf(buf, "%ld\n", &state) != 1) + return -EINVAL; + + if ((long)state < 0) + return -EINVAL; + + mutex_lock(&cdev->lock); + + result = cdev->ops->set_cur_state(cdev, state); + if (!result) + thermal_cooling_device_stats_update(cdev, state); + + mutex_unlock(&cdev->lock); + return result ? result : count; +} + +static struct device_attribute +dev_attr_cdev_type = __ATTR(type, 0444, cdev_type_show, NULL); +static DEVICE_ATTR_RO(max_state); +static DEVICE_ATTR_RW(cur_state); + +static struct attribute *cooling_device_attrs[] = { + &dev_attr_cdev_type.attr, + &dev_attr_max_state.attr, + &dev_attr_cur_state.attr, + NULL, +}; + +static const struct attribute_group cooling_device_attr_group = { + .attrs = cooling_device_attrs, +}; + +static const struct attribute_group *cooling_device_attr_groups[] = { + &cooling_device_attr_group, + NULL, /* Space allocated for cooling_device_stats_attr_group */ + NULL, +}; + +#ifdef CONFIG_THERMAL_STATISTICS +struct cooling_dev_stats { + spinlock_t lock; + unsigned int total_trans; + unsigned long state; + unsigned long max_states; + ktime_t last_time; + ktime_t *time_in_state; + unsigned int *trans_table; +}; + +static void update_time_in_state(struct cooling_dev_stats *stats) +{ + ktime_t now = ktime_get(), delta; + + delta = ktime_sub(now, stats->last_time); + stats->time_in_state[stats->state] = + ktime_add(stats->time_in_state[stats->state], delta); + stats->last_time = now; +} + +void thermal_cooling_device_stats_update(struct thermal_cooling_device *cdev, + unsigned long new_state) +{ + struct cooling_dev_stats *stats = cdev->stats; + + if (!stats) + return; + + spin_lock(&stats->lock); + + if (stats->state == new_state) + goto unlock; + + update_time_in_state(stats); + stats->trans_table[stats->state * stats->max_states + new_state]++; + stats->state = new_state; + stats->total_trans++; + +unlock: + spin_unlock(&stats->lock); +} + +static ssize_t total_trans_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct thermal_cooling_device *cdev = to_cooling_device(dev); + struct cooling_dev_stats *stats = cdev->stats; + int ret; + + spin_lock(&stats->lock); + ret = sprintf(buf, "%u\n", stats->total_trans); + spin_unlock(&stats->lock); + + return ret; +} + +static ssize_t +time_in_state_ms_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct thermal_cooling_device *cdev = to_cooling_device(dev); + struct cooling_dev_stats *stats = cdev->stats; + ssize_t len = 0; + int i; + + spin_lock(&stats->lock); + update_time_in_state(stats); + + for (i = 0; i < stats->max_states; i++) { + len += sprintf(buf + len, "state%u\t%llu\n", i, + ktime_to_ms(stats->time_in_state[i])); + } + spin_unlock(&stats->lock); + + return len; +} + +static ssize_t +reset_store(struct device *dev, struct device_attribute *attr, const char *buf, + size_t count) +{ + struct thermal_cooling_device *cdev = to_cooling_device(dev); + struct cooling_dev_stats *stats = cdev->stats; + int i, states = stats->max_states; + + spin_lock(&stats->lock); + + stats->total_trans = 0; + stats->last_time = ktime_get(); + memset(stats->trans_table, 0, + states * states * sizeof(*stats->trans_table)); + + for (i = 0; i < stats->max_states; i++) + stats->time_in_state[i] = ktime_set(0, 0); + + spin_unlock(&stats->lock); + + return count; +} + +static ssize_t trans_table_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct thermal_cooling_device *cdev = to_cooling_device(dev); + struct cooling_dev_stats *stats = cdev->stats; + ssize_t len = 0; + int i, j; + + len += snprintf(buf + len, PAGE_SIZE - len, " From : To\n"); + len += snprintf(buf + len, PAGE_SIZE - len, " : "); + for (i = 0; i < stats->max_states; i++) { + if (len >= PAGE_SIZE) + break; + len += snprintf(buf + len, PAGE_SIZE - len, "state%2u ", i); + } + if (len >= PAGE_SIZE) + return PAGE_SIZE; + + len += snprintf(buf + len, PAGE_SIZE - len, "\n"); + + for (i = 0; i < stats->max_states; i++) { + if (len >= PAGE_SIZE) + break; + + len += snprintf(buf + len, PAGE_SIZE - len, "state%2u:", i); + + for (j = 0; j < stats->max_states; j++) { + if (len >= PAGE_SIZE) + break; + len += snprintf(buf + len, PAGE_SIZE - len, "%8u ", + stats->trans_table[i * stats->max_states + j]); + } + if (len >= PAGE_SIZE) + break; + len += snprintf(buf + len, PAGE_SIZE - len, "\n"); + } + + if (len >= PAGE_SIZE) { + pr_warn_once("Thermal transition table exceeds PAGE_SIZE. Disabling\n"); + return -EFBIG; + } + return len; +} + +static DEVICE_ATTR_RO(total_trans); +static DEVICE_ATTR_RO(time_in_state_ms); +static DEVICE_ATTR_WO(reset); +static DEVICE_ATTR_RO(trans_table); + +static struct attribute *cooling_device_stats_attrs[] = { + &dev_attr_total_trans.attr, + &dev_attr_time_in_state_ms.attr, + &dev_attr_reset.attr, + &dev_attr_trans_table.attr, + NULL +}; + +static const struct attribute_group cooling_device_stats_attr_group = { + .attrs = cooling_device_stats_attrs, + .name = "stats" +}; + +static void cooling_device_stats_setup(struct thermal_cooling_device *cdev) +{ + struct cooling_dev_stats *stats; + unsigned long states; + int var; + + if (cdev->ops->get_max_state(cdev, &states)) + return; + + states++; /* Total number of states is highest state + 1 */ + + var = sizeof(*stats); + var += sizeof(*stats->time_in_state) * states; + var += sizeof(*stats->trans_table) * states * states; + + stats = kzalloc(var, GFP_KERNEL); + if (!stats) + return; + + stats->time_in_state = (ktime_t *)(stats + 1); + stats->trans_table = (unsigned int *)(stats->time_in_state + states); + cdev->stats = stats; + stats->last_time = ktime_get(); + stats->max_states = states; + + spin_lock_init(&stats->lock); + + /* Fill the empty slot left in cooling_device_attr_groups */ + var = ARRAY_SIZE(cooling_device_attr_groups) - 2; + cooling_device_attr_groups[var] = &cooling_device_stats_attr_group; +} + +static void cooling_device_stats_destroy(struct thermal_cooling_device *cdev) +{ + kfree(cdev->stats); + cdev->stats = NULL; +} + +#else + +static inline void +cooling_device_stats_setup(struct thermal_cooling_device *cdev) {} +static inline void +cooling_device_stats_destroy(struct thermal_cooling_device *cdev) {} + +#endif /* CONFIG_THERMAL_STATISTICS */ + +void thermal_cooling_device_setup_sysfs(struct thermal_cooling_device *cdev) +{ + cooling_device_stats_setup(cdev); + cdev->device.groups = cooling_device_attr_groups; +} + +void thermal_cooling_device_destroy_sysfs(struct thermal_cooling_device *cdev) +{ + cooling_device_stats_destroy(cdev); +} + +/* these helper will be used only at the time of bindig */ +ssize_t +trip_point_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct thermal_instance *instance; + + instance = + container_of(attr, struct thermal_instance, attr); + + if (instance->trip == THERMAL_TRIPS_NONE) + return sprintf(buf, "-1\n"); + else + return sprintf(buf, "%d\n", instance->trip); +} + +ssize_t +weight_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct thermal_instance *instance; + + instance = container_of(attr, struct thermal_instance, weight_attr); + + return sprintf(buf, "%d\n", instance->weight); +} + +ssize_t weight_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct thermal_instance *instance; + int ret, weight; + + ret = kstrtoint(buf, 0, &weight); + if (ret) + return ret; + + instance = container_of(attr, struct thermal_instance, weight_attr); + instance->weight = weight; + + return count; +} diff --git a/drivers/thermal/ti-soc-thermal/Kconfig b/drivers/thermal/ti-soc-thermal/Kconfig new file mode 100644 index 000000000..fe0e877f8 --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/Kconfig @@ -0,0 +1,72 @@ +config TI_SOC_THERMAL + tristate "Texas Instruments SoCs temperature sensor driver" + help + If you say yes here you get support for the Texas Instruments + OMAP4460+ on die bandgap temperature sensor support. The register + set is part of system control module. + + This includes alert interrupts generation and also the TSHUT + support. + +config TI_THERMAL + bool "Texas Instruments SoCs thermal framework support" + depends on TI_SOC_THERMAL + help + If you say yes here you want to get support for generic thermal + framework for the Texas Instruments on die bandgap temperature sensor. + + This includes trip points definitions, extrapolation rules and + CPU cooling device bindings. + +config OMAP3_THERMAL + bool "Texas Instruments OMAP3 thermal support" + depends on TI_SOC_THERMAL + depends on ARCH_OMAP3 || COMPILE_TEST + help + If you say yes here you get thermal support for the Texas Instruments + OMAP3 SoC family. The current chips supported are: + - OMAP3430 + + OMAP3 chips normally don't need thermal management, and sensors in + this generation are not accurate, nor they are very close to + the important hotspots. + + Say 'N' here. + +config OMAP4_THERMAL + bool "Texas Instruments OMAP4 thermal support" + depends on TI_SOC_THERMAL + depends on ARCH_OMAP4 || COMPILE_TEST + help + If you say yes here you get thermal support for the Texas Instruments + OMAP4 SoC family. The current chip supported are: + - OMAP4430 + - OMAP4460 + - OMAP4470 + + This includes alert interrupts generation and also the TSHUT + support. + +config OMAP5_THERMAL + bool "Texas Instruments OMAP5 thermal support" + depends on TI_SOC_THERMAL + depends on SOC_OMAP5 || COMPILE_TEST + help + If you say yes here you get thermal support for the Texas Instruments + OMAP5 SoC family. The current chip supported are: + - OMAP5430 + + This includes alert interrupts generation and also the TSHUT + support. + +config DRA752_THERMAL + bool "Texas Instruments DRA752 thermal support" + depends on TI_SOC_THERMAL + depends on SOC_DRA7XX || COMPILE_TEST + help + If you say yes here you get thermal support for the Texas Instruments + DRA752 SoC family. The current chip supported are: + - DRA752 + + This includes alert interrupts generation and also the TSHUT + support. diff --git a/drivers/thermal/ti-soc-thermal/Makefile b/drivers/thermal/ti-soc-thermal/Makefile new file mode 100644 index 000000000..f180ebead --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/Makefile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_TI_SOC_THERMAL) += ti-soc-thermal.o +ti-soc-thermal-y := ti-bandgap.o +ti-soc-thermal-$(CONFIG_TI_THERMAL) += ti-thermal-common.o +ti-soc-thermal-$(CONFIG_DRA752_THERMAL) += dra752-thermal-data.o +ti-soc-thermal-$(CONFIG_OMAP3_THERMAL) += omap3-thermal-data.o +ti-soc-thermal-$(CONFIG_OMAP4_THERMAL) += omap4-thermal-data.o +ti-soc-thermal-$(CONFIG_OMAP5_THERMAL) += omap5-thermal-data.o diff --git a/drivers/thermal/ti-soc-thermal/TODO b/drivers/thermal/ti-soc-thermal/TODO new file mode 100644 index 000000000..7da787d19 --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/TODO @@ -0,0 +1,12 @@ +List of TODOs (by Eduardo Valentin) + +on ti-bandgap.c: +- Revisit PM support + +on ti-thermal-common.c/ti-thermal.h: +- Revisit need for locking + +generally: +- make sure this code works on OMAP4430, OMAP4460 and OMAP5430 + +Copy patches to Eduardo Valentin <eduardo.valentin@ti.com> diff --git a/drivers/thermal/ti-soc-thermal/dra752-bandgap.h b/drivers/thermal/ti-soc-thermal/dra752-bandgap.h new file mode 100644 index 000000000..9490cd63f --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/dra752-bandgap.h @@ -0,0 +1,193 @@ +/* + * DRA752 bandgap registers, bitfields and temperature definitions + * + * Copyright (C) 2013 Texas Instruments Incorporated - http://www.ti.com/ + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * Tero Kristo <t-kristo@ti.com> + * + * This is an auto generated file. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ +#ifndef __DRA752_BANDGAP_H +#define __DRA752_BANDGAP_H + +/** + * *** DRA752 *** + * + * Below, in sequence, are the Register definitions, + * the bitfields and the temperature definitions for DRA752. + */ + +/** + * DRA752 register definitions + * + * Registers are defined as offsets. The offsets are + * relative to FUSE_OPP_BGAP_GPU on DRA752. + * DRA752_BANDGAP_BASE 0x4a0021e0 + * + * Register below are grouped by domain (not necessarily in offset order) + */ + + +/* DRA752.common register offsets */ +#define DRA752_BANDGAP_CTRL_1_OFFSET 0x1a0 +#define DRA752_BANDGAP_STATUS_1_OFFSET 0x1c8 +#define DRA752_BANDGAP_CTRL_2_OFFSET 0x39c +#define DRA752_BANDGAP_STATUS_2_OFFSET 0x3b8 + +/* DRA752.core register offsets */ +#define DRA752_STD_FUSE_OPP_BGAP_CORE_OFFSET 0x8 +#define DRA752_TEMP_SENSOR_CORE_OFFSET 0x154 +#define DRA752_BANDGAP_THRESHOLD_CORE_OFFSET 0x1ac +#define DRA752_DTEMP_CORE_1_OFFSET 0x20c +#define DRA752_DTEMP_CORE_2_OFFSET 0x210 + +/* DRA752.iva register offsets */ +#define DRA752_STD_FUSE_OPP_BGAP_IVA_OFFSET 0x388 +#define DRA752_TEMP_SENSOR_IVA_OFFSET 0x398 +#define DRA752_BANDGAP_THRESHOLD_IVA_OFFSET 0x3a4 +#define DRA752_DTEMP_IVA_1_OFFSET 0x3d4 +#define DRA752_DTEMP_IVA_2_OFFSET 0x3d8 + +/* DRA752.mpu register offsets */ +#define DRA752_STD_FUSE_OPP_BGAP_MPU_OFFSET 0x4 +#define DRA752_TEMP_SENSOR_MPU_OFFSET 0x14c +#define DRA752_BANDGAP_THRESHOLD_MPU_OFFSET 0x1a4 +#define DRA752_DTEMP_MPU_1_OFFSET 0x1e4 +#define DRA752_DTEMP_MPU_2_OFFSET 0x1e8 + +/* DRA752.dspeve register offsets */ +#define DRA752_STD_FUSE_OPP_BGAP_DSPEVE_OFFSET 0x384 +#define DRA752_TEMP_SENSOR_DSPEVE_OFFSET 0x394 +#define DRA752_BANDGAP_THRESHOLD_DSPEVE_OFFSET 0x3a0 +#define DRA752_DTEMP_DSPEVE_1_OFFSET 0x3c0 +#define DRA752_DTEMP_DSPEVE_2_OFFSET 0x3c4 + +/* DRA752.gpu register offsets */ +#define DRA752_STD_FUSE_OPP_BGAP_GPU_OFFSET 0x0 +#define DRA752_TEMP_SENSOR_GPU_OFFSET 0x150 +#define DRA752_BANDGAP_THRESHOLD_GPU_OFFSET 0x1a8 +#define DRA752_DTEMP_GPU_1_OFFSET 0x1f8 +#define DRA752_DTEMP_GPU_2_OFFSET 0x1fc + +/** + * Register bitfields for DRA752 + * + * All the macros bellow define the required bits for + * controlling temperature on DRA752. Bit defines are + * grouped by register. + */ + +/* DRA752.BANDGAP_STATUS_1 */ +#define DRA752_BANDGAP_STATUS_1_HOT_CORE_MASK BIT(5) +#define DRA752_BANDGAP_STATUS_1_COLD_CORE_MASK BIT(4) +#define DRA752_BANDGAP_STATUS_1_HOT_GPU_MASK BIT(3) +#define DRA752_BANDGAP_STATUS_1_COLD_GPU_MASK BIT(2) +#define DRA752_BANDGAP_STATUS_1_HOT_MPU_MASK BIT(1) +#define DRA752_BANDGAP_STATUS_1_COLD_MPU_MASK BIT(0) + +/* DRA752.BANDGAP_CTRL_2 */ +#define DRA752_BANDGAP_CTRL_2_FREEZE_IVA_MASK BIT(22) +#define DRA752_BANDGAP_CTRL_2_FREEZE_DSPEVE_MASK BIT(21) +#define DRA752_BANDGAP_CTRL_2_MASK_HOT_IVA_MASK BIT(3) +#define DRA752_BANDGAP_CTRL_2_MASK_COLD_IVA_MASK BIT(2) +#define DRA752_BANDGAP_CTRL_2_MASK_HOT_DSPEVE_MASK BIT(1) +#define DRA752_BANDGAP_CTRL_2_MASK_COLD_DSPEVE_MASK BIT(0) + +/* DRA752.BANDGAP_STATUS_2 */ +#define DRA752_BANDGAP_STATUS_2_HOT_IVA_MASK BIT(3) +#define DRA752_BANDGAP_STATUS_2_COLD_IVA_MASK BIT(2) +#define DRA752_BANDGAP_STATUS_2_HOT_DSPEVE_MASK BIT(1) +#define DRA752_BANDGAP_STATUS_2_COLD_DSPEVE_MASK BIT(0) + +/* DRA752.BANDGAP_CTRL_1 */ +#define DRA752_BANDGAP_CTRL_1_COUNTER_DELAY_MASK (0x7 << 27) +#define DRA752_BANDGAP_CTRL_1_FREEZE_CORE_MASK BIT(23) +#define DRA752_BANDGAP_CTRL_1_FREEZE_GPU_MASK BIT(22) +#define DRA752_BANDGAP_CTRL_1_FREEZE_MPU_MASK BIT(21) +#define DRA752_BANDGAP_CTRL_1_MASK_HOT_CORE_MASK BIT(5) +#define DRA752_BANDGAP_CTRL_1_MASK_COLD_CORE_MASK BIT(4) +#define DRA752_BANDGAP_CTRL_1_MASK_HOT_GPU_MASK BIT(3) +#define DRA752_BANDGAP_CTRL_1_MASK_COLD_GPU_MASK BIT(2) +#define DRA752_BANDGAP_CTRL_1_MASK_HOT_MPU_MASK BIT(1) +#define DRA752_BANDGAP_CTRL_1_MASK_COLD_MPU_MASK BIT(0) + +/* DRA752.TEMP_SENSOR */ +#define DRA752_TEMP_SENSOR_TMPSOFF_MASK BIT(11) +#define DRA752_TEMP_SENSOR_EOCZ_MASK BIT(10) +#define DRA752_TEMP_SENSOR_DTEMP_MASK (0x3ff << 0) + +/* DRA752.BANDGAP_THRESHOLD */ +#define DRA752_BANDGAP_THRESHOLD_HOT_MASK (0x3ff << 16) +#define DRA752_BANDGAP_THRESHOLD_COLD_MASK (0x3ff << 0) + +/** + * Temperature limits and thresholds for DRA752 + * + * All the macros bellow are definitions for handling the + * ADC conversions and representation of temperature limits + * and thresholds for DRA752. Definitions are grouped + * by temperature domain. + */ + +/* DRA752.common temperature definitions */ +/* ADC conversion table limits */ +#define DRA752_ADC_START_VALUE 540 +#define DRA752_ADC_END_VALUE 945 + +/* DRA752.GPU temperature definitions */ +/* bandgap clock limits */ +#define DRA752_GPU_MAX_FREQ 1500000 +#define DRA752_GPU_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define DRA752_GPU_T_HOT 800 +#define DRA752_GPU_T_COLD 795 + +/* DRA752.MPU temperature definitions */ +/* bandgap clock limits */ +#define DRA752_MPU_MAX_FREQ 1500000 +#define DRA752_MPU_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define DRA752_MPU_T_HOT 800 +#define DRA752_MPU_T_COLD 795 + +/* DRA752.CORE temperature definitions */ +/* bandgap clock limits */ +#define DRA752_CORE_MAX_FREQ 1500000 +#define DRA752_CORE_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define DRA752_CORE_T_HOT 800 +#define DRA752_CORE_T_COLD 795 + +/* DRA752.DSPEVE temperature definitions */ +/* bandgap clock limits */ +#define DRA752_DSPEVE_MAX_FREQ 1500000 +#define DRA752_DSPEVE_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define DRA752_DSPEVE_T_HOT 800 +#define DRA752_DSPEVE_T_COLD 795 + +/* DRA752.IVA temperature definitions */ +/* bandgap clock limits */ +#define DRA752_IVA_MAX_FREQ 1500000 +#define DRA752_IVA_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define DRA752_IVA_T_HOT 800 +#define DRA752_IVA_T_COLD 795 + +#endif /* __DRA752_BANDGAP_H */ diff --git a/drivers/thermal/ti-soc-thermal/dra752-thermal-data.c b/drivers/thermal/ti-soc-thermal/dra752-thermal-data.c new file mode 100644 index 000000000..33a3030aa --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/dra752-thermal-data.c @@ -0,0 +1,381 @@ +/* + * DRA752 thermal data. + * + * Copyright (C) 2013 Texas Instruments Inc. + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * Tero Kristo <t-kristo@ti.com> + * + * This file is partially autogenerated. + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "ti-thermal.h" +#include "ti-bandgap.h" +#include "dra752-bandgap.h" + +/* + * DRA752 has five instances of thermal sensor: MPU, GPU, CORE, + * IVA and DSPEVE need to describe the individual registers and + * bit fields. + */ + +/* + * DRA752 CORE thermal sensor register offsets and bit-fields + */ +static struct temp_sensor_registers +dra752_core_temp_sensor_registers = { + .temp_sensor_ctrl = DRA752_TEMP_SENSOR_CORE_OFFSET, + .bgap_tempsoff_mask = DRA752_TEMP_SENSOR_TMPSOFF_MASK, + .bgap_eocz_mask = DRA752_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = DRA752_TEMP_SENSOR_DTEMP_MASK, + .bgap_mask_ctrl = DRA752_BANDGAP_CTRL_1_OFFSET, + .mask_hot_mask = DRA752_BANDGAP_CTRL_1_MASK_HOT_CORE_MASK, + .mask_cold_mask = DRA752_BANDGAP_CTRL_1_MASK_COLD_CORE_MASK, + .mask_counter_delay_mask = DRA752_BANDGAP_CTRL_1_COUNTER_DELAY_MASK, + .mask_freeze_mask = DRA752_BANDGAP_CTRL_1_FREEZE_CORE_MASK, + .bgap_threshold = DRA752_BANDGAP_THRESHOLD_CORE_OFFSET, + .threshold_thot_mask = DRA752_BANDGAP_THRESHOLD_HOT_MASK, + .threshold_tcold_mask = DRA752_BANDGAP_THRESHOLD_COLD_MASK, + .bgap_status = DRA752_BANDGAP_STATUS_1_OFFSET, + .status_hot_mask = DRA752_BANDGAP_STATUS_1_HOT_CORE_MASK, + .status_cold_mask = DRA752_BANDGAP_STATUS_1_COLD_CORE_MASK, + .ctrl_dtemp_1 = DRA752_DTEMP_CORE_1_OFFSET, + .ctrl_dtemp_2 = DRA752_DTEMP_CORE_2_OFFSET, + .bgap_efuse = DRA752_STD_FUSE_OPP_BGAP_CORE_OFFSET, +}; + +/* + * DRA752 IVA thermal sensor register offsets and bit-fields + */ +static struct temp_sensor_registers +dra752_iva_temp_sensor_registers = { + .temp_sensor_ctrl = DRA752_TEMP_SENSOR_IVA_OFFSET, + .bgap_tempsoff_mask = DRA752_TEMP_SENSOR_TMPSOFF_MASK, + .bgap_eocz_mask = DRA752_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = DRA752_TEMP_SENSOR_DTEMP_MASK, + .bgap_mask_ctrl = DRA752_BANDGAP_CTRL_2_OFFSET, + .mask_hot_mask = DRA752_BANDGAP_CTRL_2_MASK_HOT_IVA_MASK, + .mask_cold_mask = DRA752_BANDGAP_CTRL_2_MASK_COLD_IVA_MASK, + .mask_counter_delay_mask = DRA752_BANDGAP_CTRL_1_COUNTER_DELAY_MASK, + .mask_freeze_mask = DRA752_BANDGAP_CTRL_2_FREEZE_IVA_MASK, + .bgap_threshold = DRA752_BANDGAP_THRESHOLD_IVA_OFFSET, + .threshold_thot_mask = DRA752_BANDGAP_THRESHOLD_HOT_MASK, + .threshold_tcold_mask = DRA752_BANDGAP_THRESHOLD_COLD_MASK, + .bgap_status = DRA752_BANDGAP_STATUS_2_OFFSET, + .status_hot_mask = DRA752_BANDGAP_STATUS_2_HOT_IVA_MASK, + .status_cold_mask = DRA752_BANDGAP_STATUS_2_COLD_IVA_MASK, + .ctrl_dtemp_1 = DRA752_DTEMP_IVA_1_OFFSET, + .ctrl_dtemp_2 = DRA752_DTEMP_IVA_2_OFFSET, + .bgap_efuse = DRA752_STD_FUSE_OPP_BGAP_IVA_OFFSET, +}; + +/* + * DRA752 MPU thermal sensor register offsets and bit-fields + */ +static struct temp_sensor_registers +dra752_mpu_temp_sensor_registers = { + .temp_sensor_ctrl = DRA752_TEMP_SENSOR_MPU_OFFSET, + .bgap_tempsoff_mask = DRA752_TEMP_SENSOR_TMPSOFF_MASK, + .bgap_eocz_mask = DRA752_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = DRA752_TEMP_SENSOR_DTEMP_MASK, + .bgap_mask_ctrl = DRA752_BANDGAP_CTRL_1_OFFSET, + .mask_hot_mask = DRA752_BANDGAP_CTRL_1_MASK_HOT_MPU_MASK, + .mask_cold_mask = DRA752_BANDGAP_CTRL_1_MASK_COLD_MPU_MASK, + .mask_counter_delay_mask = DRA752_BANDGAP_CTRL_1_COUNTER_DELAY_MASK, + .mask_freeze_mask = DRA752_BANDGAP_CTRL_1_FREEZE_MPU_MASK, + .bgap_threshold = DRA752_BANDGAP_THRESHOLD_MPU_OFFSET, + .threshold_thot_mask = DRA752_BANDGAP_THRESHOLD_HOT_MASK, + .threshold_tcold_mask = DRA752_BANDGAP_THRESHOLD_COLD_MASK, + .bgap_status = DRA752_BANDGAP_STATUS_1_OFFSET, + .status_hot_mask = DRA752_BANDGAP_STATUS_1_HOT_MPU_MASK, + .status_cold_mask = DRA752_BANDGAP_STATUS_1_COLD_MPU_MASK, + .ctrl_dtemp_1 = DRA752_DTEMP_MPU_1_OFFSET, + .ctrl_dtemp_2 = DRA752_DTEMP_MPU_2_OFFSET, + .bgap_efuse = DRA752_STD_FUSE_OPP_BGAP_MPU_OFFSET, +}; + +/* + * DRA752 DSPEVE thermal sensor register offsets and bit-fields + */ +static struct temp_sensor_registers +dra752_dspeve_temp_sensor_registers = { + .temp_sensor_ctrl = DRA752_TEMP_SENSOR_DSPEVE_OFFSET, + .bgap_tempsoff_mask = DRA752_TEMP_SENSOR_TMPSOFF_MASK, + .bgap_eocz_mask = DRA752_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = DRA752_TEMP_SENSOR_DTEMP_MASK, + .bgap_mask_ctrl = DRA752_BANDGAP_CTRL_2_OFFSET, + .mask_hot_mask = DRA752_BANDGAP_CTRL_2_MASK_HOT_DSPEVE_MASK, + .mask_cold_mask = DRA752_BANDGAP_CTRL_2_MASK_COLD_DSPEVE_MASK, + .mask_counter_delay_mask = DRA752_BANDGAP_CTRL_1_COUNTER_DELAY_MASK, + .mask_freeze_mask = DRA752_BANDGAP_CTRL_2_FREEZE_DSPEVE_MASK, + .bgap_threshold = DRA752_BANDGAP_THRESHOLD_DSPEVE_OFFSET, + .threshold_thot_mask = DRA752_BANDGAP_THRESHOLD_HOT_MASK, + .threshold_tcold_mask = DRA752_BANDGAP_THRESHOLD_COLD_MASK, + .bgap_status = DRA752_BANDGAP_STATUS_2_OFFSET, + .status_hot_mask = DRA752_BANDGAP_STATUS_2_HOT_DSPEVE_MASK, + .status_cold_mask = DRA752_BANDGAP_STATUS_2_COLD_DSPEVE_MASK, + .ctrl_dtemp_1 = DRA752_DTEMP_DSPEVE_1_OFFSET, + .ctrl_dtemp_2 = DRA752_DTEMP_DSPEVE_2_OFFSET, + .bgap_efuse = DRA752_STD_FUSE_OPP_BGAP_DSPEVE_OFFSET, +}; + +/* + * DRA752 GPU thermal sensor register offsets and bit-fields + */ +static struct temp_sensor_registers +dra752_gpu_temp_sensor_registers = { + .temp_sensor_ctrl = DRA752_TEMP_SENSOR_GPU_OFFSET, + .bgap_tempsoff_mask = DRA752_TEMP_SENSOR_TMPSOFF_MASK, + .bgap_eocz_mask = DRA752_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = DRA752_TEMP_SENSOR_DTEMP_MASK, + .bgap_mask_ctrl = DRA752_BANDGAP_CTRL_1_OFFSET, + .mask_hot_mask = DRA752_BANDGAP_CTRL_1_MASK_HOT_GPU_MASK, + .mask_cold_mask = DRA752_BANDGAP_CTRL_1_MASK_COLD_GPU_MASK, + .mask_counter_delay_mask = DRA752_BANDGAP_CTRL_1_COUNTER_DELAY_MASK, + .mask_freeze_mask = DRA752_BANDGAP_CTRL_1_FREEZE_GPU_MASK, + .bgap_threshold = DRA752_BANDGAP_THRESHOLD_GPU_OFFSET, + .threshold_thot_mask = DRA752_BANDGAP_THRESHOLD_HOT_MASK, + .threshold_tcold_mask = DRA752_BANDGAP_THRESHOLD_COLD_MASK, + .bgap_status = DRA752_BANDGAP_STATUS_1_OFFSET, + .status_hot_mask = DRA752_BANDGAP_STATUS_1_HOT_GPU_MASK, + .status_cold_mask = DRA752_BANDGAP_STATUS_1_COLD_GPU_MASK, + .ctrl_dtemp_1 = DRA752_DTEMP_GPU_1_OFFSET, + .ctrl_dtemp_2 = DRA752_DTEMP_GPU_2_OFFSET, + .bgap_efuse = DRA752_STD_FUSE_OPP_BGAP_GPU_OFFSET, +}; + +/* Thresholds and limits for DRA752 MPU temperature sensor */ +static struct temp_sensor_data dra752_mpu_temp_sensor_data = { + .t_hot = DRA752_MPU_T_HOT, + .t_cold = DRA752_MPU_T_COLD, + .min_freq = DRA752_MPU_MIN_FREQ, + .max_freq = DRA752_MPU_MAX_FREQ, +}; + +/* Thresholds and limits for DRA752 GPU temperature sensor */ +static struct temp_sensor_data dra752_gpu_temp_sensor_data = { + .t_hot = DRA752_GPU_T_HOT, + .t_cold = DRA752_GPU_T_COLD, + .min_freq = DRA752_GPU_MIN_FREQ, + .max_freq = DRA752_GPU_MAX_FREQ, +}; + +/* Thresholds and limits for DRA752 CORE temperature sensor */ +static struct temp_sensor_data dra752_core_temp_sensor_data = { + .t_hot = DRA752_CORE_T_HOT, + .t_cold = DRA752_CORE_T_COLD, + .min_freq = DRA752_CORE_MIN_FREQ, + .max_freq = DRA752_CORE_MAX_FREQ, +}; + +/* Thresholds and limits for DRA752 DSPEVE temperature sensor */ +static struct temp_sensor_data dra752_dspeve_temp_sensor_data = { + .t_hot = DRA752_DSPEVE_T_HOT, + .t_cold = DRA752_DSPEVE_T_COLD, + .min_freq = DRA752_DSPEVE_MIN_FREQ, + .max_freq = DRA752_DSPEVE_MAX_FREQ, +}; + +/* Thresholds and limits for DRA752 IVA temperature sensor */ +static struct temp_sensor_data dra752_iva_temp_sensor_data = { + .t_hot = DRA752_IVA_T_HOT, + .t_cold = DRA752_IVA_T_COLD, + .min_freq = DRA752_IVA_MIN_FREQ, + .max_freq = DRA752_IVA_MAX_FREQ, +}; + +/* + * DRA752 : Temperature values in milli degree celsius + * ADC code values from 540 to 945 + */ +static +int dra752_adc_to_temp[DRA752_ADC_END_VALUE - DRA752_ADC_START_VALUE + 1] = { + /* Index 540 - 549 */ + -40000, -40000, -40000, -40000, -39800, -39400, -39000, -38600, -38200, + -37800, + /* Index 550 - 559 */ + -37400, -37000, -36600, -36200, -35800, -35300, -34700, -34200, -33800, + -33400, + /* Index 560 - 569 */ + -33000, -32600, -32200, -31800, -31400, -31000, -30600, -30200, -29800, + -29400, + /* Index 570 - 579 */ + -29000, -28600, -28200, -27700, -27100, -26600, -26200, -25800, -25400, + -25000, + /* Index 580 - 589 */ + -24600, -24200, -23800, -23400, -23000, -22600, -22200, -21800, -21400, + -21000, + /* Index 590 - 599 */ + -20500, -19900, -19400, -19000, -18600, -18200, -17800, -17400, -17000, + -16600, + /* Index 600 - 609 */ + -16200, -15800, -15400, -15000, -14600, -14200, -13800, -13400, -13000, + -12500, + /* Index 610 - 619 */ + -11900, -11400, -11000, -10600, -10200, -9800, -9400, -9000, -8600, + -8200, + /* Index 620 - 629 */ + -7800, -7400, -7000, -6600, -6200, -5800, -5400, -5000, -4500, + -3900, + /* Index 630 - 639 */ + -3400, -3000, -2600, -2200, -1800, -1400, -1000, -600, -200, + 200, + /* Index 640 - 649 */ + 600, 1000, 1400, 1800, 2200, 2600, 3000, 3400, 3900, + 4500, + /* Index 650 - 659 */ + 5000, 5400, 5800, 6200, 6600, 7000, 7400, 7800, 8200, + 8600, + /* Index 660 - 669 */ + 9000, 9400, 9800, 10200, 10600, 11000, 11400, 11800, 12200, + 12700, + /* Index 670 - 679 */ + 13300, 13800, 14200, 14600, 15000, 15400, 15800, 16200, 16600, + 17000, + /* Index 680 - 689 */ + 17400, 17800, 18200, 18600, 19000, 19400, 19800, 20200, 20600, + 21000, + /* Index 690 - 699 */ + 21400, 21900, 22500, 23000, 23400, 23800, 24200, 24600, 25000, + 25400, + /* Index 700 - 709 */ + 25800, 26200, 26600, 27000, 27400, 27800, 28200, 28600, 29000, + 29400, + /* Index 710 - 719 */ + 29800, 30200, 30600, 31000, 31400, 31900, 32500, 33000, 33400, + 33800, + /* Index 720 - 729 */ + 34200, 34600, 35000, 35400, 35800, 36200, 36600, 37000, 37400, + 37800, + /* Index 730 - 739 */ + 38200, 38600, 39000, 39400, 39800, 40200, 40600, 41000, 41400, + 41800, + /* Index 740 - 749 */ + 42200, 42600, 43100, 43700, 44200, 44600, 45000, 45400, 45800, + 46200, + /* Index 750 - 759 */ + 46600, 47000, 47400, 47800, 48200, 48600, 49000, 49400, 49800, + 50200, + /* Index 760 - 769 */ + 50600, 51000, 51400, 51800, 52200, 52600, 53000, 53400, 53800, + 54200, + /* Index 770 - 779 */ + 54600, 55000, 55400, 55900, 56500, 57000, 57400, 57800, 58200, + 58600, + /* Index 780 - 789 */ + 59000, 59400, 59800, 60200, 60600, 61000, 61400, 61800, 62200, + 62600, + /* Index 790 - 799 */ + 63000, 63400, 63800, 64200, 64600, 65000, 65400, 65800, 66200, + 66600, + /* Index 800 - 809 */ + 67000, 67400, 67800, 68200, 68600, 69000, 69400, 69800, 70200, + 70600, + /* Index 810 - 819 */ + 71000, 71500, 72100, 72600, 73000, 73400, 73800, 74200, 74600, + 75000, + /* Index 820 - 829 */ + 75400, 75800, 76200, 76600, 77000, 77400, 77800, 78200, 78600, + 79000, + /* Index 830 - 839 */ + 79400, 79800, 80200, 80600, 81000, 81400, 81800, 82200, 82600, + 83000, + /* Index 840 - 849 */ + 83400, 83800, 84200, 84600, 85000, 85400, 85800, 86200, 86600, + 87000, + /* Index 850 - 859 */ + 87400, 87800, 88200, 88600, 89000, 89400, 89800, 90200, 90600, + 91000, + /* Index 860 - 869 */ + 91400, 91800, 92200, 92600, 93000, 93400, 93800, 94200, 94600, + 95000, + /* Index 870 - 879 */ + 95400, 95800, 96200, 96600, 97000, 97500, 98100, 98600, 99000, + 99400, + /* Index 880 - 889 */ + 99800, 100200, 100600, 101000, 101400, 101800, 102200, 102600, 103000, + 103400, + /* Index 890 - 899 */ + 103800, 104200, 104600, 105000, 105400, 105800, 106200, 106600, 107000, + 107400, + /* Index 900 - 909 */ + 107800, 108200, 108600, 109000, 109400, 109800, 110200, 110600, 111000, + 111400, + /* Index 910 - 919 */ + 111800, 112200, 112600, 113000, 113400, 113800, 114200, 114600, 115000, + 115400, + /* Index 920 - 929 */ + 115800, 116200, 116600, 117000, 117400, 117800, 118200, 118600, 119000, + 119400, + /* Index 930 - 939 */ + 119800, 120200, 120600, 121000, 121400, 121800, 122200, 122600, 123000, + 123400, + /* Index 940 - 945 */ + 123800, 124200, 124600, 124900, 125000, 125000, +}; + +/* DRA752 data */ +const struct ti_bandgap_data dra752_data = { + .features = TI_BANDGAP_FEATURE_FREEZE_BIT | + TI_BANDGAP_FEATURE_TALERT | + TI_BANDGAP_FEATURE_COUNTER_DELAY | + TI_BANDGAP_FEATURE_HISTORY_BUFFER | + TI_BANDGAP_FEATURE_ERRATA_814, + .fclock_name = "l3instr_ts_gclk_div", + .div_ck_name = "l3instr_ts_gclk_div", + .conv_table = dra752_adc_to_temp, + .adc_start_val = DRA752_ADC_START_VALUE, + .adc_end_val = DRA752_ADC_END_VALUE, + .expose_sensor = ti_thermal_expose_sensor, + .remove_sensor = ti_thermal_remove_sensor, + .sensors = { + { + .registers = &dra752_mpu_temp_sensor_registers, + .ts_data = &dra752_mpu_temp_sensor_data, + .domain = "cpu", + .register_cooling = ti_thermal_register_cpu_cooling, + .unregister_cooling = ti_thermal_unregister_cpu_cooling, + .slope_pcb = DRA752_GRADIENT_SLOPE_W_PCB, + .constant_pcb = DRA752_GRADIENT_CONST_W_PCB, + }, + { + .registers = &dra752_gpu_temp_sensor_registers, + .ts_data = &dra752_gpu_temp_sensor_data, + .domain = "gpu", + .slope_pcb = DRA752_GRADIENT_SLOPE_W_PCB, + .constant_pcb = DRA752_GRADIENT_CONST_W_PCB, + }, + { + .registers = &dra752_core_temp_sensor_registers, + .ts_data = &dra752_core_temp_sensor_data, + .domain = "core", + .slope_pcb = DRA752_GRADIENT_SLOPE_W_PCB, + .constant_pcb = DRA752_GRADIENT_CONST_W_PCB, + }, + { + .registers = &dra752_dspeve_temp_sensor_registers, + .ts_data = &dra752_dspeve_temp_sensor_data, + .domain = "dspeve", + .slope_pcb = DRA752_GRADIENT_SLOPE_W_PCB, + .constant_pcb = DRA752_GRADIENT_CONST_W_PCB, + }, + { + .registers = &dra752_iva_temp_sensor_registers, + .ts_data = &dra752_iva_temp_sensor_data, + .domain = "iva", + .slope_pcb = DRA752_GRADIENT_SLOPE_W_PCB, + .constant_pcb = DRA752_GRADIENT_CONST_W_PCB, + }, + }, + .sensor_count = 5, +}; diff --git a/drivers/thermal/ti-soc-thermal/omap3-thermal-data.c b/drivers/thermal/ti-soc-thermal/omap3-thermal-data.c new file mode 100644 index 000000000..f5366807d --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/omap3-thermal-data.c @@ -0,0 +1,166 @@ +/* + * OMAP3 thermal driver. + * + * Copyright (C) 2011-2012 Texas Instruments Inc. + * Copyright (C) 2014 Pavel Machek <pavel@ucw.cz> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * Note + * http://www.ti.com/lit/er/sprz278f/sprz278f.pdf "Advisory + * 3.1.1.186 MMC OCP Clock Not Gated When Thermal Sensor Is Used" + * + * Also TI says: + * Just be careful when you try to make thermal policy like decisions + * based on this sensor. Placement of the sensor w.r.t the actual logic + * generating heat has to be a factor as well. If you are just looking + * for an approximation temperature (thermometerish kind), you might be + * ok with this. I am not sure we'd find any TI data around this.. just a + * heads up. + */ + +#include "ti-thermal.h" +#include "ti-bandgap.h" + +/* + * OMAP34XX has one instance of thermal sensor for MPU + * need to describe the individual bit fields + */ +static struct temp_sensor_registers +omap34xx_mpu_temp_sensor_registers = { + .temp_sensor_ctrl = 0, + .bgap_soc_mask = BIT(8), + .bgap_eocz_mask = BIT(7), + .bgap_dtemp_mask = 0x7f, + + .bgap_mode_ctrl = 0, + .mode_ctrl_mask = BIT(9), +}; + +/* Thresholds and limits for OMAP34XX MPU temperature sensor */ +static struct temp_sensor_data omap34xx_mpu_temp_sensor_data = { + .min_freq = 32768, + .max_freq = 32768, +}; + +/* + * Temperature values in milli degree celsius + */ +static const int +omap34xx_adc_to_temp[128] = { + -40000, -40000, -40000, -40000, -40000, -39000, -38000, -36000, + -34000, -32000, -31000, -29000, -28000, -26000, -25000, -24000, + -22000, -21000, -19000, -18000, -17000, -15000, -14000, -12000, + -11000, -9000, -8000, -7000, -5000, -4000, -2000, -1000, 0000, + 1000, 3000, 4000, 5000, 7000, 8000, 10000, 11000, 13000, 14000, + 15000, 17000, 18000, 20000, 21000, 22000, 24000, 25000, 27000, + 28000, 30000, 31000, 32000, 34000, 35000, 37000, 38000, 39000, + 41000, 42000, 44000, 45000, 47000, 48000, 49000, 51000, 52000, + 53000, 55000, 56000, 58000, 59000, 60000, 62000, 63000, 65000, + 66000, 67000, 69000, 70000, 72000, 73000, 74000, 76000, 77000, + 79000, 80000, 81000, 83000, 84000, 85000, 87000, 88000, 89000, + 91000, 92000, 94000, 95000, 96000, 98000, 99000, 100000, + 102000, 103000, 105000, 106000, 107000, 109000, 110000, 111000, + 113000, 114000, 116000, 117000, 118000, 120000, 121000, 122000, + 124000, 124000, 125000, 125000, 125000, 125000, 125000 +}; + +/* OMAP34XX data */ +const struct ti_bandgap_data omap34xx_data = { + .features = TI_BANDGAP_FEATURE_CLK_CTRL | TI_BANDGAP_FEATURE_UNRELIABLE, + .fclock_name = "ts_fck", + .div_ck_name = "ts_fck", + .conv_table = omap34xx_adc_to_temp, + .adc_start_val = 0, + .adc_end_val = 127, + .expose_sensor = ti_thermal_expose_sensor, + .remove_sensor = ti_thermal_remove_sensor, + + .sensors = { + { + .registers = &omap34xx_mpu_temp_sensor_registers, + .ts_data = &omap34xx_mpu_temp_sensor_data, + .domain = "cpu", + .slope_pcb = 0, + .constant_pcb = 20000, + .register_cooling = NULL, + .unregister_cooling = NULL, + }, + }, + .sensor_count = 1, +}; + +/* + * OMAP36XX has one instance of thermal sensor for MPU + * need to describe the individual bit fields + */ +static struct temp_sensor_registers +omap36xx_mpu_temp_sensor_registers = { + .temp_sensor_ctrl = 0, + .bgap_soc_mask = BIT(9), + .bgap_eocz_mask = BIT(8), + .bgap_dtemp_mask = 0xFF, + + .bgap_mode_ctrl = 0, + .mode_ctrl_mask = BIT(10), +}; + +/* Thresholds and limits for OMAP36XX MPU temperature sensor */ +static struct temp_sensor_data omap36xx_mpu_temp_sensor_data = { + .min_freq = 32768, + .max_freq = 32768, +}; + +/* + * Temperature values in milli degree celsius + */ +static const int +omap36xx_adc_to_temp[128] = { + -40000, -40000, -40000, -40000, -40000, -40000, -40000, -40000, + -40000, -40000, -40000, -40000, -40000, -38000, -35000, -34000, + -32000, -30000, -28000, -26000, -24000, -22000, -20000, -18500, + -17000, -15000, -13500, -12000, -10000, -8000, -6500, -5000, -3500, + -1500, 0, 2000, 3500, 5000, 6500, 8500, 10000, 12000, 13500, + 15000, 17000, 19000, 21000, 23000, 25000, 27000, 28500, 30000, + 32000, 33500, 35000, 37000, 38500, 40000, 42000, 43500, 45000, + 47000, 48500, 50000, 52000, 53500, 55000, 57000, 58500, 60000, + 62000, 64000, 66000, 68000, 70000, 71500, 73500, 75000, 77000, + 78500, 80000, 82000, 83500, 85000, 87000, 88500, 90000, 92000, + 93500, 95000, 97000, 98500, 100000, 102000, 103500, 105000, 107000, + 109000, 111000, 113000, 115000, 117000, 118500, 120000, 122000, + 123500, 125000, 125000, 125000, 125000, 125000, 125000, 125000, + 125000, 125000, 125000, 125000, 125000, 125000, 125000, 125000, + 125000, 125000, 125000, 125000, 125000, 125000, 125000 +}; + +/* OMAP36XX data */ +const struct ti_bandgap_data omap36xx_data = { + .features = TI_BANDGAP_FEATURE_CLK_CTRL | TI_BANDGAP_FEATURE_UNRELIABLE, + .fclock_name = "ts_fck", + .div_ck_name = "ts_fck", + .conv_table = omap36xx_adc_to_temp, + .adc_start_val = 0, + .adc_end_val = 127, + .expose_sensor = ti_thermal_expose_sensor, + .remove_sensor = ti_thermal_remove_sensor, + + .sensors = { + { + .registers = &omap36xx_mpu_temp_sensor_registers, + .ts_data = &omap36xx_mpu_temp_sensor_data, + .domain = "cpu", + .slope_pcb = 0, + .constant_pcb = 20000, + .register_cooling = NULL, + .unregister_cooling = NULL, + }, + }, + .sensor_count = 1, +}; diff --git a/drivers/thermal/ti-soc-thermal/omap4-thermal-data.c b/drivers/thermal/ti-soc-thermal/omap4-thermal-data.c new file mode 100644 index 000000000..0b9f835d9 --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/omap4-thermal-data.c @@ -0,0 +1,252 @@ +/* + * OMAP4 thermal driver. + * + * Copyright (C) 2011-2012 Texas Instruments Inc. + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "ti-thermal.h" +#include "ti-bandgap.h" +#include "omap4xxx-bandgap.h" + +/* + * OMAP4430 has one instance of thermal sensor for MPU + * need to describe the individual bit fields + */ +static struct temp_sensor_registers +omap4430_mpu_temp_sensor_registers = { + .temp_sensor_ctrl = OMAP4430_TEMP_SENSOR_CTRL_OFFSET, + .bgap_tempsoff_mask = OMAP4430_BGAP_TEMPSOFF_MASK, + .bgap_soc_mask = OMAP4430_BGAP_TEMP_SENSOR_SOC_MASK, + .bgap_eocz_mask = OMAP4430_BGAP_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = OMAP4430_BGAP_TEMP_SENSOR_DTEMP_MASK, + + .bgap_mode_ctrl = OMAP4430_TEMP_SENSOR_CTRL_OFFSET, + .mode_ctrl_mask = OMAP4430_SINGLE_MODE_MASK, + + .bgap_efuse = OMAP4430_FUSE_OPP_BGAP, +}; + +/* Thresholds and limits for OMAP4430 MPU temperature sensor */ +static struct temp_sensor_data omap4430_mpu_temp_sensor_data = { + .min_freq = OMAP4430_MIN_FREQ, + .max_freq = OMAP4430_MAX_FREQ, +}; + +/* + * Temperature values in milli degree celsius + * ADC code values from 13 to 107, see TRM + * "18.4.10.2.3 ADC Codes Versus Temperature". + */ +static const int +omap4430_adc_to_temp[OMAP4430_ADC_END_VALUE - OMAP4430_ADC_START_VALUE + 1] = { + -40000, -38000, -35000, -34000, -32000, -30000, -28000, -26000, -24000, + -22000, -20000, -18500, -17000, -15000, -13500, -12000, -10000, -8000, + -6500, -5000, -3500, -1500, 0, 2000, 3500, 5000, 6500, 8500, 10000, + 12000, 13500, 15000, 17000, 19000, 21000, 23000, 25000, 27000, 28500, + 30000, 32000, 33500, 35000, 37000, 38500, 40000, 42000, 43500, 45000, + 47000, 48500, 50000, 52000, 53500, 55000, 57000, 58500, 60000, 62000, + 64000, 66000, 68000, 70000, 71500, 73500, 75000, 77000, 78500, 80000, + 82000, 83500, 85000, 87000, 88500, 90000, 92000, 93500, 95000, 97000, + 98500, 100000, 102000, 103500, 105000, 107000, 109000, 111000, 113000, + 115000, 117000, 118500, 120000, 122000, 123500, 125000, +}; + +/* OMAP4430 data */ +const struct ti_bandgap_data omap4430_data = { + .features = TI_BANDGAP_FEATURE_MODE_CONFIG | + TI_BANDGAP_FEATURE_CLK_CTRL | + TI_BANDGAP_FEATURE_POWER_SWITCH, + .fclock_name = "bandgap_fclk", + .div_ck_name = "bandgap_fclk", + .conv_table = omap4430_adc_to_temp, + .adc_start_val = OMAP4430_ADC_START_VALUE, + .adc_end_val = OMAP4430_ADC_END_VALUE, + .expose_sensor = ti_thermal_expose_sensor, + .remove_sensor = ti_thermal_remove_sensor, + .sensors = { + { + .registers = &omap4430_mpu_temp_sensor_registers, + .ts_data = &omap4430_mpu_temp_sensor_data, + .domain = "cpu", + .slope_pcb = OMAP_GRADIENT_SLOPE_W_PCB_4430, + .constant_pcb = OMAP_GRADIENT_CONST_W_PCB_4430, + .register_cooling = ti_thermal_register_cpu_cooling, + .unregister_cooling = ti_thermal_unregister_cpu_cooling, + }, + }, + .sensor_count = 1, +}; +/* + * OMAP4460 has one instance of thermal sensor for MPU + * need to describe the individual bit fields + */ +static struct temp_sensor_registers +omap4460_mpu_temp_sensor_registers = { + .temp_sensor_ctrl = OMAP4460_TEMP_SENSOR_CTRL_OFFSET, + .bgap_tempsoff_mask = OMAP4460_BGAP_TEMPSOFF_MASK, + .bgap_soc_mask = OMAP4460_BGAP_TEMP_SENSOR_SOC_MASK, + .bgap_eocz_mask = OMAP4460_BGAP_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = OMAP4460_BGAP_TEMP_SENSOR_DTEMP_MASK, + + .bgap_mask_ctrl = OMAP4460_BGAP_CTRL_OFFSET, + .mask_hot_mask = OMAP4460_MASK_HOT_MASK, + .mask_cold_mask = OMAP4460_MASK_COLD_MASK, + + .bgap_mode_ctrl = OMAP4460_BGAP_CTRL_OFFSET, + .mode_ctrl_mask = OMAP4460_SINGLE_MODE_MASK, + + .bgap_counter = OMAP4460_BGAP_COUNTER_OFFSET, + .counter_mask = OMAP4460_COUNTER_MASK, + + .bgap_threshold = OMAP4460_BGAP_THRESHOLD_OFFSET, + .threshold_thot_mask = OMAP4460_T_HOT_MASK, + .threshold_tcold_mask = OMAP4460_T_COLD_MASK, + + .tshut_threshold = OMAP4460_BGAP_TSHUT_OFFSET, + .tshut_hot_mask = OMAP4460_TSHUT_HOT_MASK, + .tshut_cold_mask = OMAP4460_TSHUT_COLD_MASK, + + .bgap_status = OMAP4460_BGAP_STATUS_OFFSET, + .status_hot_mask = OMAP4460_HOT_FLAG_MASK, + .status_cold_mask = OMAP4460_COLD_FLAG_MASK, + + .bgap_efuse = OMAP4460_FUSE_OPP_BGAP, +}; + +/* Thresholds and limits for OMAP4460 MPU temperature sensor */ +static struct temp_sensor_data omap4460_mpu_temp_sensor_data = { + .tshut_hot = OMAP4460_TSHUT_HOT, + .tshut_cold = OMAP4460_TSHUT_COLD, + .t_hot = OMAP4460_T_HOT, + .t_cold = OMAP4460_T_COLD, + .min_freq = OMAP4460_MIN_FREQ, + .max_freq = OMAP4460_MAX_FREQ, +}; + +/* + * Temperature values in milli degree celsius + * ADC code values from 530 to 923 + */ +static const int +omap4460_adc_to_temp[OMAP4460_ADC_END_VALUE - OMAP4460_ADC_START_VALUE + 1] = { + -40000, -40000, -40000, -40000, -39800, -39400, -39000, -38600, -38200, + -37800, -37300, -36800, -36400, -36000, -35600, -35200, -34800, + -34300, -33800, -33400, -33000, -32600, -32200, -31800, -31300, + -30800, -30400, -30000, -29600, -29200, -28700, -28200, -27800, + -27400, -27000, -26600, -26200, -25700, -25200, -24800, -24400, + -24000, -23600, -23200, -22700, -22200, -21800, -21400, -21000, + -20600, -20200, -19700, -19200, -18800, -18400, -18000, -17600, + -17200, -16700, -16200, -15800, -15400, -15000, -14600, -14200, + -13700, -13200, -12800, -12400, -12000, -11600, -11200, -10700, + -10200, -9800, -9400, -9000, -8600, -8200, -7700, -7200, -6800, + -6400, -6000, -5600, -5200, -4800, -4300, -3800, -3400, -3000, + -2600, -2200, -1800, -1300, -800, -400, 0, 400, 800, 1200, 1600, + 2100, 2600, 3000, 3400, 3800, 4200, 4600, 5100, 5600, 6000, 6400, + 6800, 7200, 7600, 8000, 8500, 9000, 9400, 9800, 10200, 10600, 11000, + 11400, 11900, 12400, 12800, 13200, 13600, 14000, 14400, 14800, + 15300, 15800, 16200, 16600, 17000, 17400, 17800, 18200, 18700, + 19200, 19600, 20000, 20400, 20800, 21200, 21600, 22100, 22600, + 23000, 23400, 23800, 24200, 24600, 25000, 25400, 25900, 26400, + 26800, 27200, 27600, 28000, 28400, 28800, 29300, 29800, 30200, + 30600, 31000, 31400, 31800, 32200, 32600, 33100, 33600, 34000, + 34400, 34800, 35200, 35600, 36000, 36400, 36800, 37300, 37800, + 38200, 38600, 39000, 39400, 39800, 40200, 40600, 41100, 41600, + 42000, 42400, 42800, 43200, 43600, 44000, 44400, 44800, 45300, + 45800, 46200, 46600, 47000, 47400, 47800, 48200, 48600, 49000, + 49500, 50000, 50400, 50800, 51200, 51600, 52000, 52400, 52800, + 53200, 53700, 54200, 54600, 55000, 55400, 55800, 56200, 56600, + 57000, 57400, 57800, 58200, 58700, 59200, 59600, 60000, 60400, + 60800, 61200, 61600, 62000, 62400, 62800, 63300, 63800, 64200, + 64600, 65000, 65400, 65800, 66200, 66600, 67000, 67400, 67800, + 68200, 68700, 69200, 69600, 70000, 70400, 70800, 71200, 71600, + 72000, 72400, 72800, 73200, 73600, 74100, 74600, 75000, 75400, + 75800, 76200, 76600, 77000, 77400, 77800, 78200, 78600, 79000, + 79400, 79800, 80300, 80800, 81200, 81600, 82000, 82400, 82800, + 83200, 83600, 84000, 84400, 84800, 85200, 85600, 86000, 86400, + 86800, 87300, 87800, 88200, 88600, 89000, 89400, 89800, 90200, + 90600, 91000, 91400, 91800, 92200, 92600, 93000, 93400, 93800, + 94200, 94600, 95000, 95500, 96000, 96400, 96800, 97200, 97600, + 98000, 98400, 98800, 99200, 99600, 100000, 100400, 100800, 101200, + 101600, 102000, 102400, 102800, 103200, 103600, 104000, 104400, + 104800, 105200, 105600, 106100, 106600, 107000, 107400, 107800, + 108200, 108600, 109000, 109400, 109800, 110200, 110600, 111000, + 111400, 111800, 112200, 112600, 113000, 113400, 113800, 114200, + 114600, 115000, 115400, 115800, 116200, 116600, 117000, 117400, + 117800, 118200, 118600, 119000, 119400, 119800, 120200, 120600, + 121000, 121400, 121800, 122200, 122600, 123000, 123400, 123800, 124200, + 124600, 124900, 125000, 125000, 125000, 125000 +}; + +/* OMAP4460 data */ +const struct ti_bandgap_data omap4460_data = { + .features = TI_BANDGAP_FEATURE_TSHUT | + TI_BANDGAP_FEATURE_TSHUT_CONFIG | + TI_BANDGAP_FEATURE_TALERT | + TI_BANDGAP_FEATURE_MODE_CONFIG | + TI_BANDGAP_FEATURE_POWER_SWITCH | + TI_BANDGAP_FEATURE_CLK_CTRL | + TI_BANDGAP_FEATURE_COUNTER, + .fclock_name = "bandgap_ts_fclk", + .div_ck_name = "div_ts_ck", + .conv_table = omap4460_adc_to_temp, + .adc_start_val = OMAP4460_ADC_START_VALUE, + .adc_end_val = OMAP4460_ADC_END_VALUE, + .expose_sensor = ti_thermal_expose_sensor, + .remove_sensor = ti_thermal_remove_sensor, + .report_temperature = ti_thermal_report_sensor_temperature, + .sensors = { + { + .registers = &omap4460_mpu_temp_sensor_registers, + .ts_data = &omap4460_mpu_temp_sensor_data, + .domain = "cpu", + .slope_pcb = OMAP_GRADIENT_SLOPE_W_PCB_4460, + .constant_pcb = OMAP_GRADIENT_CONST_W_PCB_4460, + .register_cooling = ti_thermal_register_cpu_cooling, + .unregister_cooling = ti_thermal_unregister_cpu_cooling, + }, + }, + .sensor_count = 1, +}; + +/* OMAP4470 data */ +const struct ti_bandgap_data omap4470_data = { + .features = TI_BANDGAP_FEATURE_TSHUT | + TI_BANDGAP_FEATURE_TSHUT_CONFIG | + TI_BANDGAP_FEATURE_TALERT | + TI_BANDGAP_FEATURE_MODE_CONFIG | + TI_BANDGAP_FEATURE_POWER_SWITCH | + TI_BANDGAP_FEATURE_CLK_CTRL | + TI_BANDGAP_FEATURE_COUNTER, + .fclock_name = "bandgap_ts_fclk", + .div_ck_name = "div_ts_ck", + .conv_table = omap4460_adc_to_temp, + .adc_start_val = OMAP4460_ADC_START_VALUE, + .adc_end_val = OMAP4460_ADC_END_VALUE, + .expose_sensor = ti_thermal_expose_sensor, + .remove_sensor = ti_thermal_remove_sensor, + .report_temperature = ti_thermal_report_sensor_temperature, + .sensors = { + { + .registers = &omap4460_mpu_temp_sensor_registers, + .ts_data = &omap4460_mpu_temp_sensor_data, + .domain = "cpu", + .slope_pcb = OMAP_GRADIENT_SLOPE_W_PCB_4470, + .constant_pcb = OMAP_GRADIENT_CONST_W_PCB_4470, + .register_cooling = ti_thermal_register_cpu_cooling, + .unregister_cooling = ti_thermal_unregister_cpu_cooling, + }, + }, + .sensor_count = 1, +}; diff --git a/drivers/thermal/ti-soc-thermal/omap4xxx-bandgap.h b/drivers/thermal/ti-soc-thermal/omap4xxx-bandgap.h new file mode 100644 index 000000000..8a081abce --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/omap4xxx-bandgap.h @@ -0,0 +1,169 @@ +/* + * OMAP4xxx bandgap registers, bitfields and temperature definitions + * + * Copyright (C) 2013 Texas Instruments Incorporated - http://www.ti.com/ + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ +#ifndef __OMAP4XXX_BANDGAP_H +#define __OMAP4XXX_BANDGAP_H + +/** + * *** OMAP4430 *** + * + * Below, in sequence, are the Register definitions, + * the bitfields and the temperature definitions for OMAP4430. + */ + +/** + * OMAP4430 register definitions + * + * Registers are defined as offsets. The offsets are + * relative to FUSE_OPP_BGAP on 4430. + */ + +/* OMAP4430.FUSE_OPP_BGAP */ +#define OMAP4430_FUSE_OPP_BGAP 0x0 + +/* OMAP4430.TEMP_SENSOR */ +#define OMAP4430_TEMP_SENSOR_CTRL_OFFSET 0xCC + +/** + * Register and bit definitions for OMAP4430 + * + * All the macros bellow define the required bits for + * controlling temperature on OMAP4430. Bit defines are + * grouped by register. + */ + +/* OMAP4430.TEMP_SENSOR bits */ +#define OMAP4430_BGAP_TEMPSOFF_MASK BIT(12) +#define OMAP4430_BGAP_TSHUT_MASK BIT(11) +#define OMAP4430_SINGLE_MODE_MASK BIT(10) +#define OMAP4430_BGAP_TEMP_SENSOR_SOC_MASK BIT(9) +#define OMAP4430_BGAP_TEMP_SENSOR_EOCZ_MASK BIT(8) +#define OMAP4430_BGAP_TEMP_SENSOR_DTEMP_MASK (0xff << 0) + +/** + * Temperature limits and thresholds for OMAP4430 + * + * All the macros bellow are definitions for handling the + * ADC conversions and representation of temperature limits + * and thresholds for OMAP4430. + */ + +/* + * ADC conversion table limits. Ignore values outside the TRM listed + * range to avoid bogus thermal shutdowns. See omap4430 TRM chapter + * "18.4.10.2.3 ADC Codes Versus Temperature". + */ +#define OMAP4430_ADC_START_VALUE 13 +#define OMAP4430_ADC_END_VALUE 107 +/* bandgap clock limits (no control on 4430) */ +#define OMAP4430_MAX_FREQ 32768 +#define OMAP4430_MIN_FREQ 32768 + +/** + * *** OMAP4460 *** Applicable for OMAP4470 + * + * Below, in sequence, are the Register definitions, + * the bitfields and the temperature definitions for OMAP4460. + */ + +/** + * OMAP4460 register definitions + * + * Registers are defined as offsets. The offsets are + * relative to FUSE_OPP_BGAP on 4460. + */ + +/* OMAP4460.FUSE_OPP_BGAP */ +#define OMAP4460_FUSE_OPP_BGAP 0x0 + +/* OMAP4460.TEMP_SENSOR */ +#define OMAP4460_TEMP_SENSOR_CTRL_OFFSET 0xCC + +/* OMAP4460.BANDGAP_CTRL */ +#define OMAP4460_BGAP_CTRL_OFFSET 0x118 + +/* OMAP4460.BANDGAP_COUNTER */ +#define OMAP4460_BGAP_COUNTER_OFFSET 0x11C + +/* OMAP4460.BANDGAP_THRESHOLD */ +#define OMAP4460_BGAP_THRESHOLD_OFFSET 0x120 + +/* OMAP4460.TSHUT_THRESHOLD */ +#define OMAP4460_BGAP_TSHUT_OFFSET 0x124 + +/* OMAP4460.BANDGAP_STATUS */ +#define OMAP4460_BGAP_STATUS_OFFSET 0x128 + +/** + * Register bitfields for OMAP4460 + * + * All the macros bellow define the required bits for + * controlling temperature on OMAP4460. Bit defines are + * grouped by register. + */ +/* OMAP4460.TEMP_SENSOR bits */ +#define OMAP4460_BGAP_TEMPSOFF_MASK BIT(13) +#define OMAP4460_BGAP_TEMP_SENSOR_SOC_MASK BIT(11) +#define OMAP4460_BGAP_TEMP_SENSOR_EOCZ_MASK BIT(10) +#define OMAP4460_BGAP_TEMP_SENSOR_DTEMP_MASK (0x3ff << 0) + +/* OMAP4460.BANDGAP_CTRL bits */ +#define OMAP4460_SINGLE_MODE_MASK BIT(31) +#define OMAP4460_MASK_HOT_MASK BIT(1) +#define OMAP4460_MASK_COLD_MASK BIT(0) + +/* OMAP4460.BANDGAP_COUNTER bits */ +#define OMAP4460_COUNTER_MASK (0xffffff << 0) + +/* OMAP4460.BANDGAP_THRESHOLD bits */ +#define OMAP4460_T_HOT_MASK (0x3ff << 16) +#define OMAP4460_T_COLD_MASK (0x3ff << 0) + +/* OMAP4460.TSHUT_THRESHOLD bits */ +#define OMAP4460_TSHUT_HOT_MASK (0x3ff << 16) +#define OMAP4460_TSHUT_COLD_MASK (0x3ff << 0) + +/* OMAP4460.BANDGAP_STATUS bits */ +#define OMAP4460_HOT_FLAG_MASK BIT(1) +#define OMAP4460_COLD_FLAG_MASK BIT(0) + +/** + * Temperature limits and thresholds for OMAP4460 + * + * All the macros bellow are definitions for handling the + * ADC conversions and representation of temperature limits + * and thresholds for OMAP4460. + */ + +/* ADC conversion table limits */ +#define OMAP4460_ADC_START_VALUE 530 +#define OMAP4460_ADC_END_VALUE 932 +/* bandgap clock limits */ +#define OMAP4460_MAX_FREQ 1500000 +#define OMAP4460_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define OMAP4460_TSHUT_HOT 900 /* 122 deg C */ +#define OMAP4460_TSHUT_COLD 895 /* 100 deg C */ +#define OMAP4460_T_HOT 800 /* 73 deg C */ +#define OMAP4460_T_COLD 795 /* 71 deg C */ + +#endif /* __OMAP4XXX_BANDGAP_H */ diff --git a/drivers/thermal/ti-soc-thermal/omap5-thermal-data.c b/drivers/thermal/ti-soc-thermal/omap5-thermal-data.c new file mode 100644 index 000000000..8191bae83 --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/omap5-thermal-data.c @@ -0,0 +1,312 @@ +/* + * OMAP5 thermal driver. + * + * Copyright (C) 2011-2012 Texas Instruments Inc. + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#include "ti-thermal.h" +#include "ti-bandgap.h" +#include "omap5xxx-bandgap.h" + +/* + * OMAP5430 has three instances of thermal sensor for MPU, GPU & CORE, + * need to describe the individual registers and bit fields. + */ + +/* + * OMAP5430 MPU thermal sensor register offset and bit-fields + */ +static struct temp_sensor_registers +omap5430_mpu_temp_sensor_registers = { + .temp_sensor_ctrl = OMAP5430_TEMP_SENSOR_MPU_OFFSET, + .bgap_tempsoff_mask = OMAP5430_BGAP_TEMPSOFF_MASK, + .bgap_eocz_mask = OMAP5430_BGAP_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = OMAP5430_BGAP_TEMP_SENSOR_DTEMP_MASK, + + .bgap_mask_ctrl = OMAP5430_BGAP_CTRL_OFFSET, + .mask_hot_mask = OMAP5430_MASK_HOT_MPU_MASK, + .mask_cold_mask = OMAP5430_MASK_COLD_MPU_MASK, + .mask_counter_delay_mask = OMAP5430_MASK_COUNTER_DELAY_MASK, + .mask_freeze_mask = OMAP5430_MASK_FREEZE_MPU_MASK, + + .bgap_counter = OMAP5430_BGAP_CTRL_OFFSET, + .counter_mask = OMAP5430_COUNTER_MASK, + + .bgap_threshold = OMAP5430_BGAP_THRESHOLD_MPU_OFFSET, + .threshold_thot_mask = OMAP5430_T_HOT_MASK, + .threshold_tcold_mask = OMAP5430_T_COLD_MASK, + + .tshut_threshold = OMAP5430_BGAP_TSHUT_MPU_OFFSET, + .tshut_hot_mask = OMAP5430_TSHUT_HOT_MASK, + .tshut_cold_mask = OMAP5430_TSHUT_COLD_MASK, + + .bgap_status = OMAP5430_BGAP_STATUS_OFFSET, + .status_hot_mask = OMAP5430_HOT_MPU_FLAG_MASK, + .status_cold_mask = OMAP5430_COLD_MPU_FLAG_MASK, + + .ctrl_dtemp_1 = OMAP5430_BGAP_DTEMP_MPU_1_OFFSET, + .ctrl_dtemp_2 = OMAP5430_BGAP_DTEMP_MPU_2_OFFSET, + .bgap_efuse = OMAP5430_FUSE_OPP_BGAP_MPU, +}; + +/* + * OMAP5430 GPU thermal sensor register offset and bit-fields + */ +static struct temp_sensor_registers +omap5430_gpu_temp_sensor_registers = { + .temp_sensor_ctrl = OMAP5430_TEMP_SENSOR_GPU_OFFSET, + .bgap_tempsoff_mask = OMAP5430_BGAP_TEMPSOFF_MASK, + .bgap_eocz_mask = OMAP5430_BGAP_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = OMAP5430_BGAP_TEMP_SENSOR_DTEMP_MASK, + + .bgap_mask_ctrl = OMAP5430_BGAP_CTRL_OFFSET, + .mask_hot_mask = OMAP5430_MASK_HOT_GPU_MASK, + .mask_cold_mask = OMAP5430_MASK_COLD_GPU_MASK, + .mask_counter_delay_mask = OMAP5430_MASK_COUNTER_DELAY_MASK, + .mask_freeze_mask = OMAP5430_MASK_FREEZE_GPU_MASK, + + .bgap_counter = OMAP5430_BGAP_CTRL_OFFSET, + .counter_mask = OMAP5430_COUNTER_MASK, + + .bgap_threshold = OMAP5430_BGAP_THRESHOLD_GPU_OFFSET, + .threshold_thot_mask = OMAP5430_T_HOT_MASK, + .threshold_tcold_mask = OMAP5430_T_COLD_MASK, + + .tshut_threshold = OMAP5430_BGAP_TSHUT_GPU_OFFSET, + .tshut_hot_mask = OMAP5430_TSHUT_HOT_MASK, + .tshut_cold_mask = OMAP5430_TSHUT_COLD_MASK, + + .bgap_status = OMAP5430_BGAP_STATUS_OFFSET, + .status_hot_mask = OMAP5430_HOT_GPU_FLAG_MASK, + .status_cold_mask = OMAP5430_COLD_GPU_FLAG_MASK, + + .ctrl_dtemp_1 = OMAP5430_BGAP_DTEMP_GPU_1_OFFSET, + .ctrl_dtemp_2 = OMAP5430_BGAP_DTEMP_GPU_2_OFFSET, + + .bgap_efuse = OMAP5430_FUSE_OPP_BGAP_GPU, +}; + +/* + * OMAP5430 CORE thermal sensor register offset and bit-fields + */ +static struct temp_sensor_registers +omap5430_core_temp_sensor_registers = { + .temp_sensor_ctrl = OMAP5430_TEMP_SENSOR_CORE_OFFSET, + .bgap_tempsoff_mask = OMAP5430_BGAP_TEMPSOFF_MASK, + .bgap_eocz_mask = OMAP5430_BGAP_TEMP_SENSOR_EOCZ_MASK, + .bgap_dtemp_mask = OMAP5430_BGAP_TEMP_SENSOR_DTEMP_MASK, + + .bgap_mask_ctrl = OMAP5430_BGAP_CTRL_OFFSET, + .mask_hot_mask = OMAP5430_MASK_HOT_CORE_MASK, + .mask_cold_mask = OMAP5430_MASK_COLD_CORE_MASK, + .mask_counter_delay_mask = OMAP5430_MASK_COUNTER_DELAY_MASK, + .mask_freeze_mask = OMAP5430_MASK_FREEZE_CORE_MASK, + + .bgap_counter = OMAP5430_BGAP_CTRL_OFFSET, + .counter_mask = OMAP5430_COUNTER_MASK, + + .bgap_threshold = OMAP5430_BGAP_THRESHOLD_CORE_OFFSET, + .threshold_thot_mask = OMAP5430_T_HOT_MASK, + .threshold_tcold_mask = OMAP5430_T_COLD_MASK, + + .tshut_threshold = OMAP5430_BGAP_TSHUT_CORE_OFFSET, + .tshut_hot_mask = OMAP5430_TSHUT_HOT_MASK, + .tshut_cold_mask = OMAP5430_TSHUT_COLD_MASK, + + .bgap_status = OMAP5430_BGAP_STATUS_OFFSET, + .status_hot_mask = OMAP5430_HOT_CORE_FLAG_MASK, + .status_cold_mask = OMAP5430_COLD_CORE_FLAG_MASK, + + .ctrl_dtemp_1 = OMAP5430_BGAP_DTEMP_CORE_1_OFFSET, + .ctrl_dtemp_2 = OMAP5430_BGAP_DTEMP_CORE_2_OFFSET, + + .bgap_efuse = OMAP5430_FUSE_OPP_BGAP_CORE, +}; + +/* Thresholds and limits for OMAP5430 MPU temperature sensor */ +static struct temp_sensor_data omap5430_mpu_temp_sensor_data = { + .tshut_hot = OMAP5430_MPU_TSHUT_HOT, + .tshut_cold = OMAP5430_MPU_TSHUT_COLD, + .t_hot = OMAP5430_MPU_T_HOT, + .t_cold = OMAP5430_MPU_T_COLD, + .min_freq = OMAP5430_MPU_MIN_FREQ, + .max_freq = OMAP5430_MPU_MAX_FREQ, +}; + +/* Thresholds and limits for OMAP5430 GPU temperature sensor */ +static struct temp_sensor_data omap5430_gpu_temp_sensor_data = { + .tshut_hot = OMAP5430_GPU_TSHUT_HOT, + .tshut_cold = OMAP5430_GPU_TSHUT_COLD, + .t_hot = OMAP5430_GPU_T_HOT, + .t_cold = OMAP5430_GPU_T_COLD, + .min_freq = OMAP5430_GPU_MIN_FREQ, + .max_freq = OMAP5430_GPU_MAX_FREQ, +}; + +/* Thresholds and limits for OMAP5430 CORE temperature sensor */ +static struct temp_sensor_data omap5430_core_temp_sensor_data = { + .tshut_hot = OMAP5430_CORE_TSHUT_HOT, + .tshut_cold = OMAP5430_CORE_TSHUT_COLD, + .t_hot = OMAP5430_CORE_T_HOT, + .t_cold = OMAP5430_CORE_T_COLD, + .min_freq = OMAP5430_CORE_MIN_FREQ, + .max_freq = OMAP5430_CORE_MAX_FREQ, +}; + +/* + * OMAP54xx ES2.0 : Temperature values in milli degree celsius + * ADC code values from 540 to 945 + */ +static int +omap5430_adc_to_temp[ + OMAP5430_ADC_END_VALUE - OMAP5430_ADC_START_VALUE + 1] = { + /* Index 540 - 549 */ + -40000, -40000, -40000, -40000, -39800, -39400, -39000, -38600, -38200, + -37800, + /* Index 550 - 559 */ + -37400, -37000, -36600, -36200, -35800, -35300, -34700, -34200, -33800, + -33400, + /* Index 560 - 569 */ + -33000, -32600, -32200, -31800, -31400, -31000, -30600, -30200, -29800, + -29400, + /* Index 570 - 579 */ + -29000, -28600, -28200, -27700, -27100, -26600, -26200, -25800, -25400, + -25000, + /* Index 580 - 589 */ + -24600, -24200, -23800, -23400, -23000, -22600, -22200, -21600, -21400, + -21000, + /* Index 590 - 599 */ + -20500, -19900, -19400, -19000, -18600, -18200, -17800, -17400, -17000, + -16600, + /* Index 600 - 609 */ + -16200, -15800, -15400, -15000, -14600, -14200, -13800, -13400, -13000, + -12500, + /* Index 610 - 619 */ + -11900, -11400, -11000, -10600, -10200, -9800, -9400, -9000, -8600, + -8200, + /* Index 620 - 629 */ + -7800, -7400, -7000, -6600, -6200, -5800, -5400, -5000, -4500, -3900, + /* Index 630 - 639 */ + -3400, -3000, -2600, -2200, -1800, -1400, -1000, -600, -200, 200, + /* Index 640 - 649 */ + 600, 1000, 1400, 1800, 2200, 2600, 3000, 3400, 3900, 4500, + /* Index 650 - 659 */ + 5000, 5400, 5800, 6200, 6600, 7000, 7400, 7800, 8200, 8600, + /* Index 660 - 669 */ + 9000, 9400, 9800, 10200, 10600, 11000, 11400, 11800, 12200, 12700, + /* Index 670 - 679 */ + 13300, 13800, 14200, 14600, 15000, 15400, 15800, 16200, 16600, 17000, + /* Index 680 - 689 */ + 17400, 17800, 18200, 18600, 19000, 19400, 19800, 20200, 20600, 21100, + /* Index 690 - 699 */ + 21400, 21900, 22500, 23000, 23400, 23800, 24200, 24600, 25000, 25400, + /* Index 700 - 709 */ + 25800, 26200, 26600, 27000, 27400, 27800, 28200, 28600, 29000, 29400, + /* Index 710 - 719 */ + 29800, 30200, 30600, 31000, 31400, 31900, 32500, 33000, 33400, 33800, + /* Index 720 - 729 */ + 34200, 34600, 35000, 35400, 35800, 36200, 36600, 37000, 37400, 37800, + /* Index 730 - 739 */ + 38200, 38600, 39000, 39400, 39800, 40200, 40600, 41000, 41400, 41800, + /* Index 740 - 749 */ + 42200, 42600, 43100, 43700, 44200, 44600, 45000, 45400, 45800, 46200, + /* Index 750 - 759 */ + 46600, 47000, 47400, 47800, 48200, 48600, 49000, 49400, 49800, 50200, + /* Index 760 - 769 */ + 50600, 51000, 51400, 51800, 52200, 52600, 53000, 53400, 53800, 54200, + /* Index 770 - 779 */ + 54600, 55000, 55400, 55900, 56500, 57000, 57400, 57800, 58200, 58600, + /* Index 780 - 789 */ + 59000, 59400, 59800, 60200, 60600, 61000, 61400, 61800, 62200, 62600, + /* Index 790 - 799 */ + 63000, 63400, 63800, 64200, 64600, 65000, 65400, 65800, 66200, 66600, + /* Index 800 - 809 */ + 67000, 67400, 67800, 68200, 68600, 69000, 69400, 69800, 70200, 70600, + /* Index 810 - 819 */ + 71000, 71500, 72100, 72600, 73000, 73400, 73800, 74200, 74600, 75000, + /* Index 820 - 829 */ + 75400, 75800, 76200, 76600, 77000, 77400, 77800, 78200, 78600, 79000, + /* Index 830 - 839 */ + 79400, 79800, 80200, 80600, 81000, 81400, 81800, 82200, 82600, 83000, + /* Index 840 - 849 */ + 83400, 83800, 84200, 84600, 85000, 85400, 85800, 86200, 86600, 87000, + /* Index 850 - 859 */ + 87400, 87800, 88200, 88600, 89000, 89400, 89800, 90200, 90600, 91000, + /* Index 860 - 869 */ + 91400, 91800, 92200, 92600, 93000, 93400, 93800, 94200, 94600, 95000, + /* Index 870 - 879 */ + 95400, 95800, 96200, 96600, 97000, 97500, 98100, 98600, 99000, 99400, + /* Index 880 - 889 */ + 99800, 100200, 100600, 101000, 101400, 101800, 102200, 102600, 103000, + 103400, + /* Index 890 - 899 */ + 103800, 104200, 104600, 105000, 105400, 105800, 106200, 106600, 107000, + 107400, + /* Index 900 - 909 */ + 107800, 108200, 108600, 109000, 109400, 109800, 110200, 110600, 111000, + 111400, + /* Index 910 - 919 */ + 111800, 112200, 112600, 113000, 113400, 113800, 114200, 114600, 115000, + 115400, + /* Index 920 - 929 */ + 115800, 116200, 116600, 117000, 117400, 117800, 118200, 118600, 119000, + 119400, + /* Index 930 - 939 */ + 119800, 120200, 120600, 121000, 121400, 121800, 122400, 122600, 123000, + 123400, + /* Index 940 - 945 */ + 123800, 124200, 124600, 124900, 125000, 125000, +}; + +/* OMAP54xx ES2.0 data */ +const struct ti_bandgap_data omap5430_data = { + .features = TI_BANDGAP_FEATURE_TSHUT_CONFIG | + TI_BANDGAP_FEATURE_FREEZE_BIT | + TI_BANDGAP_FEATURE_TALERT | + TI_BANDGAP_FEATURE_COUNTER_DELAY | + TI_BANDGAP_FEATURE_HISTORY_BUFFER, + .fclock_name = "l3instr_ts_gclk_div", + .div_ck_name = "l3instr_ts_gclk_div", + .conv_table = omap5430_adc_to_temp, + .adc_start_val = OMAP5430_ADC_START_VALUE, + .adc_end_val = OMAP5430_ADC_END_VALUE, + .expose_sensor = ti_thermal_expose_sensor, + .remove_sensor = ti_thermal_remove_sensor, + .report_temperature = ti_thermal_report_sensor_temperature, + .sensors = { + { + .registers = &omap5430_mpu_temp_sensor_registers, + .ts_data = &omap5430_mpu_temp_sensor_data, + .domain = "cpu", + .register_cooling = ti_thermal_register_cpu_cooling, + .unregister_cooling = ti_thermal_unregister_cpu_cooling, + .slope_pcb = OMAP_GRADIENT_SLOPE_W_PCB_5430_CPU, + .constant_pcb = OMAP_GRADIENT_CONST_W_PCB_5430_CPU, + }, + { + .registers = &omap5430_gpu_temp_sensor_registers, + .ts_data = &omap5430_gpu_temp_sensor_data, + .domain = "gpu", + .slope_pcb = OMAP_GRADIENT_SLOPE_W_PCB_5430_GPU, + .constant_pcb = OMAP_GRADIENT_CONST_W_PCB_5430_GPU, + }, + { + .registers = &omap5430_core_temp_sensor_registers, + .ts_data = &omap5430_core_temp_sensor_data, + .domain = "core", + }, + }, + .sensor_count = 3, +}; diff --git a/drivers/thermal/ti-soc-thermal/omap5xxx-bandgap.h b/drivers/thermal/ti-soc-thermal/omap5xxx-bandgap.h new file mode 100644 index 000000000..9096403b7 --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/omap5xxx-bandgap.h @@ -0,0 +1,159 @@ +/* + * OMAP5xxx bandgap registers, bitfields and temperature definitions + * + * Copyright (C) 2013 Texas Instruments Incorporated - http://www.ti.com/ + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ +#ifndef __OMAP5XXX_BANDGAP_H +#define __OMAP5XXX_BANDGAP_H + +/** + * *** OMAP5430 *** + * + * Below, in sequence, are the Register definitions, + * the bitfields and the temperature definitions for OMAP5430. + */ + +/** + * OMAP5430 register definitions + * + * Registers are defined as offsets. The offsets are + * relative to FUSE_OPP_BGAP_GPU on 5430. + * + * Register below are grouped by domain (not necessarily in offset order) + */ + +/* OMAP5430.GPU register offsets */ +#define OMAP5430_FUSE_OPP_BGAP_GPU 0x0 +#define OMAP5430_TEMP_SENSOR_GPU_OFFSET 0x150 +#define OMAP5430_BGAP_THRESHOLD_GPU_OFFSET 0x1A8 +#define OMAP5430_BGAP_TSHUT_GPU_OFFSET 0x1B4 +#define OMAP5430_BGAP_DTEMP_GPU_1_OFFSET 0x1F8 +#define OMAP5430_BGAP_DTEMP_GPU_2_OFFSET 0x1FC + +/* OMAP5430.MPU register offsets */ +#define OMAP5430_FUSE_OPP_BGAP_MPU 0x4 +#define OMAP5430_TEMP_SENSOR_MPU_OFFSET 0x14C +#define OMAP5430_BGAP_THRESHOLD_MPU_OFFSET 0x1A4 +#define OMAP5430_BGAP_TSHUT_MPU_OFFSET 0x1B0 +#define OMAP5430_BGAP_DTEMP_MPU_1_OFFSET 0x1E4 +#define OMAP5430_BGAP_DTEMP_MPU_2_OFFSET 0x1E8 + +/* OMAP5430.MPU register offsets */ +#define OMAP5430_FUSE_OPP_BGAP_CORE 0x8 +#define OMAP5430_TEMP_SENSOR_CORE_OFFSET 0x154 +#define OMAP5430_BGAP_THRESHOLD_CORE_OFFSET 0x1AC +#define OMAP5430_BGAP_TSHUT_CORE_OFFSET 0x1B8 +#define OMAP5430_BGAP_DTEMP_CORE_1_OFFSET 0x20C +#define OMAP5430_BGAP_DTEMP_CORE_2_OFFSET 0x210 + +/* OMAP5430.common register offsets */ +#define OMAP5430_BGAP_CTRL_OFFSET 0x1A0 +#define OMAP5430_BGAP_STATUS_OFFSET 0x1C8 + +/** + * Register bitfields for OMAP5430 + * + * All the macros bellow define the required bits for + * controlling temperature on OMAP5430. Bit defines are + * grouped by register. + */ + +/* OMAP5430.TEMP_SENSOR */ +#define OMAP5430_BGAP_TEMP_SENSOR_SOC_MASK BIT(12) +#define OMAP5430_BGAP_TEMPSOFF_MASK BIT(11) +#define OMAP5430_BGAP_TEMP_SENSOR_EOCZ_MASK BIT(10) +#define OMAP5430_BGAP_TEMP_SENSOR_DTEMP_MASK (0x3ff << 0) + +/* OMAP5430.BANDGAP_CTRL */ +#define OMAP5430_MASK_COUNTER_DELAY_MASK (0x7 << 27) +#define OMAP5430_MASK_FREEZE_CORE_MASK BIT(23) +#define OMAP5430_MASK_FREEZE_GPU_MASK BIT(22) +#define OMAP5430_MASK_FREEZE_MPU_MASK BIT(21) +#define OMAP5430_MASK_HOT_CORE_MASK BIT(5) +#define OMAP5430_MASK_COLD_CORE_MASK BIT(4) +#define OMAP5430_MASK_HOT_GPU_MASK BIT(3) +#define OMAP5430_MASK_COLD_GPU_MASK BIT(2) +#define OMAP5430_MASK_HOT_MPU_MASK BIT(1) +#define OMAP5430_MASK_COLD_MPU_MASK BIT(0) + +/* OMAP5430.BANDGAP_COUNTER */ +#define OMAP5430_COUNTER_MASK (0xffffff << 0) + +/* OMAP5430.BANDGAP_THRESHOLD */ +#define OMAP5430_T_HOT_MASK (0x3ff << 16) +#define OMAP5430_T_COLD_MASK (0x3ff << 0) + +/* OMAP5430.TSHUT_THRESHOLD */ +#define OMAP5430_TSHUT_HOT_MASK (0x3ff << 16) +#define OMAP5430_TSHUT_COLD_MASK (0x3ff << 0) + +/* OMAP5430.BANDGAP_STATUS */ +#define OMAP5430_HOT_CORE_FLAG_MASK BIT(5) +#define OMAP5430_COLD_CORE_FLAG_MASK BIT(4) +#define OMAP5430_HOT_GPU_FLAG_MASK BIT(3) +#define OMAP5430_COLD_GPU_FLAG_MASK BIT(2) +#define OMAP5430_HOT_MPU_FLAG_MASK BIT(1) +#define OMAP5430_COLD_MPU_FLAG_MASK BIT(0) + +/** + * Temperature limits and thresholds for OMAP5430 + * + * All the macros bellow are definitions for handling the + * ADC conversions and representation of temperature limits + * and thresholds for OMAP5430. Definitions are grouped + * by temperature domain. + */ + +/* OMAP5430.common temperature definitions */ +/* ADC conversion table limits */ +#define OMAP5430_ADC_START_VALUE 540 +#define OMAP5430_ADC_END_VALUE 945 + +/* OMAP5430.GPU temperature definitions */ +/* bandgap clock limits */ +#define OMAP5430_GPU_MAX_FREQ 1500000 +#define OMAP5430_GPU_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define OMAP5430_GPU_TSHUT_HOT 915 +#define OMAP5430_GPU_TSHUT_COLD 900 +#define OMAP5430_GPU_T_HOT 800 +#define OMAP5430_GPU_T_COLD 795 + +/* OMAP5430.MPU temperature definitions */ +/* bandgap clock limits */ +#define OMAP5430_MPU_MAX_FREQ 1500000 +#define OMAP5430_MPU_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define OMAP5430_MPU_TSHUT_HOT 915 +#define OMAP5430_MPU_TSHUT_COLD 900 +#define OMAP5430_MPU_T_HOT 800 +#define OMAP5430_MPU_T_COLD 795 + +/* OMAP5430.CORE temperature definitions */ +/* bandgap clock limits */ +#define OMAP5430_CORE_MAX_FREQ 1500000 +#define OMAP5430_CORE_MIN_FREQ 1000000 +/* interrupts thresholds */ +#define OMAP5430_CORE_TSHUT_HOT 915 +#define OMAP5430_CORE_TSHUT_COLD 900 +#define OMAP5430_CORE_T_HOT 800 +#define OMAP5430_CORE_T_COLD 795 + +#endif /* __OMAP5XXX_BANDGAP_H */ diff --git a/drivers/thermal/ti-soc-thermal/ti-bandgap.c b/drivers/thermal/ti-soc-thermal/ti-bandgap.c new file mode 100644 index 000000000..097328d8e --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/ti-bandgap.c @@ -0,0 +1,1267 @@ +/* + * TI Bandgap temperature sensor driver + * + * Copyright (C) 2011-2012 Texas Instruments Incorporated - http://www.ti.com/ + * Author: J Keerthy <j-keerthy@ti.com> + * Author: Moiz Sonasath <m-sonasath@ti.com> + * Couple of fixes, DT and MFD adaptation: + * Eduardo Valentin <eduardo.valentin@ti.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ + +#include <linux/module.h> +#include <linux/export.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/interrupt.h> +#include <linux/clk.h> +#include <linux/gpio.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/types.h> +#include <linux/spinlock.h> +#include <linux/reboot.h> +#include <linux/of_device.h> +#include <linux/of_platform.h> +#include <linux/of_irq.h> +#include <linux/of_gpio.h> +#include <linux/io.h> + +#include "ti-bandgap.h" + +static int ti_bandgap_force_single_read(struct ti_bandgap *bgp, int id); + +/*** Helper functions to access registers and their bitfields ***/ + +/** + * ti_bandgap_readl() - simple read helper function + * @bgp: pointer to ti_bandgap structure + * @reg: desired register (offset) to be read + * + * Helper function to read bandgap registers. It uses the io remapped area. + * Return: the register value. + */ +static u32 ti_bandgap_readl(struct ti_bandgap *bgp, u32 reg) +{ + return readl(bgp->base + reg); +} + +/** + * ti_bandgap_writel() - simple write helper function + * @bgp: pointer to ti_bandgap structure + * @val: desired register value to be written + * @reg: desired register (offset) to be written + * + * Helper function to write bandgap registers. It uses the io remapped area. + */ +static void ti_bandgap_writel(struct ti_bandgap *bgp, u32 val, u32 reg) +{ + writel(val, bgp->base + reg); +} + +/** + * DOC: macro to update bits. + * + * RMW_BITS() - used to read, modify and update bandgap bitfields. + * The value passed will be shifted. + */ +#define RMW_BITS(bgp, id, reg, mask, val) \ +do { \ + struct temp_sensor_registers *t; \ + u32 r; \ + \ + t = bgp->conf->sensors[(id)].registers; \ + r = ti_bandgap_readl(bgp, t->reg); \ + r &= ~t->mask; \ + r |= (val) << __ffs(t->mask); \ + ti_bandgap_writel(bgp, r, t->reg); \ +} while (0) + +/*** Basic helper functions ***/ + +/** + * ti_bandgap_power() - controls the power state of a bandgap device + * @bgp: pointer to ti_bandgap structure + * @on: desired power state (1 - on, 0 - off) + * + * Used to power on/off a bandgap device instance. Only used on those + * that features tempsoff bit. + * + * Return: 0 on success, -ENOTSUPP if tempsoff is not supported. + */ +static int ti_bandgap_power(struct ti_bandgap *bgp, bool on) +{ + int i; + + if (!TI_BANDGAP_HAS(bgp, POWER_SWITCH)) + return -ENOTSUPP; + + for (i = 0; i < bgp->conf->sensor_count; i++) + /* active on 0 */ + RMW_BITS(bgp, i, temp_sensor_ctrl, bgap_tempsoff_mask, !on); + return 0; +} + +/** + * ti_errata814_bandgap_read_temp() - helper function to read dra7 sensor temperature + * @bgp: pointer to ti_bandgap structure + * @reg: desired register (offset) to be read + * + * Function to read dra7 bandgap sensor temperature. This is done separately + * so as to workaround the errata "Bandgap Temperature read Dtemp can be + * corrupted" - Errata ID: i814". + * Read accesses to registers listed below can be corrupted due to incorrect + * resynchronization between clock domains. + * Read access to registers below can be corrupted : + * CTRL_CORE_DTEMP_MPU/GPU/CORE/DSPEVE/IVA_n (n = 0 to 4) + * CTRL_CORE_TEMP_SENSOR_MPU/GPU/CORE/DSPEVE/IVA_n + * + * Return: the register value. + */ +static u32 ti_errata814_bandgap_read_temp(struct ti_bandgap *bgp, u32 reg) +{ + u32 val1, val2; + + val1 = ti_bandgap_readl(bgp, reg); + val2 = ti_bandgap_readl(bgp, reg); + + /* If both times we read the same value then that is right */ + if (val1 == val2) + return val1; + + /* if val1 and val2 are different read it third time */ + return ti_bandgap_readl(bgp, reg); +} + +/** + * ti_bandgap_read_temp() - helper function to read sensor temperature + * @bgp: pointer to ti_bandgap structure + * @id: bandgap sensor id + * + * Function to concentrate the steps to read sensor temperature register. + * This function is desired because, depending on bandgap device version, + * it might be needed to freeze the bandgap state machine, before fetching + * the register value. + * + * Return: temperature in ADC values. + */ +static u32 ti_bandgap_read_temp(struct ti_bandgap *bgp, int id) +{ + struct temp_sensor_registers *tsr; + u32 temp, reg; + + tsr = bgp->conf->sensors[id].registers; + reg = tsr->temp_sensor_ctrl; + + if (TI_BANDGAP_HAS(bgp, FREEZE_BIT)) { + RMW_BITS(bgp, id, bgap_mask_ctrl, mask_freeze_mask, 1); + /* + * In case we cannot read from cur_dtemp / dtemp_0, + * then we read from the last valid temp read + */ + reg = tsr->ctrl_dtemp_1; + } + + /* read temperature */ + if (TI_BANDGAP_HAS(bgp, ERRATA_814)) + temp = ti_errata814_bandgap_read_temp(bgp, reg); + else + temp = ti_bandgap_readl(bgp, reg); + + temp &= tsr->bgap_dtemp_mask; + + if (TI_BANDGAP_HAS(bgp, FREEZE_BIT)) + RMW_BITS(bgp, id, bgap_mask_ctrl, mask_freeze_mask, 0); + + return temp; +} + +/*** IRQ handlers ***/ + +/** + * ti_bandgap_talert_irq_handler() - handles Temperature alert IRQs + * @irq: IRQ number + * @data: private data (struct ti_bandgap *) + * + * This is the Talert handler. Use it only if bandgap device features + * HAS(TALERT). This handler goes over all sensors and checks their + * conditions and acts accordingly. In case there are events pending, + * it will reset the event mask to wait for the opposite event (next event). + * Every time there is a new event, it will be reported to thermal layer. + * + * Return: IRQ_HANDLED + */ +static irqreturn_t ti_bandgap_talert_irq_handler(int irq, void *data) +{ + struct ti_bandgap *bgp = data; + struct temp_sensor_registers *tsr; + u32 t_hot = 0, t_cold = 0, ctrl; + int i; + + spin_lock(&bgp->lock); + for (i = 0; i < bgp->conf->sensor_count; i++) { + tsr = bgp->conf->sensors[i].registers; + ctrl = ti_bandgap_readl(bgp, tsr->bgap_status); + + /* Read the status of t_hot */ + t_hot = ctrl & tsr->status_hot_mask; + + /* Read the status of t_cold */ + t_cold = ctrl & tsr->status_cold_mask; + + if (!t_cold && !t_hot) + continue; + + ctrl = ti_bandgap_readl(bgp, tsr->bgap_mask_ctrl); + /* + * One TALERT interrupt: Two sources + * If the interrupt is due to t_hot then mask t_hot and + * and unmask t_cold else mask t_cold and unmask t_hot + */ + if (t_hot) { + ctrl &= ~tsr->mask_hot_mask; + ctrl |= tsr->mask_cold_mask; + } else if (t_cold) { + ctrl &= ~tsr->mask_cold_mask; + ctrl |= tsr->mask_hot_mask; + } + + ti_bandgap_writel(bgp, ctrl, tsr->bgap_mask_ctrl); + + dev_dbg(bgp->dev, + "%s: IRQ from %s sensor: hotevent %d coldevent %d\n", + __func__, bgp->conf->sensors[i].domain, + t_hot, t_cold); + + /* report temperature to whom may concern */ + if (bgp->conf->report_temperature) + bgp->conf->report_temperature(bgp, i); + } + spin_unlock(&bgp->lock); + + return IRQ_HANDLED; +} + +/** + * ti_bandgap_tshut_irq_handler() - handles Temperature shutdown signal + * @irq: IRQ number + * @data: private data (unused) + * + * This is the Tshut handler. Use it only if bandgap device features + * HAS(TSHUT). If any sensor fires the Tshut signal, we simply shutdown + * the system. + * + * Return: IRQ_HANDLED + */ +static irqreturn_t ti_bandgap_tshut_irq_handler(int irq, void *data) +{ + pr_emerg("%s: TSHUT temperature reached. Needs shut down...\n", + __func__); + + orderly_poweroff(true); + + return IRQ_HANDLED; +} + +/*** Helper functions which manipulate conversion ADC <-> mi Celsius ***/ + +/** + * ti_bandgap_adc_to_mcelsius() - converts an ADC value to mCelsius scale + * @bgp: struct ti_bandgap pointer + * @adc_val: value in ADC representation + * @t: address where to write the resulting temperature in mCelsius + * + * Simple conversion from ADC representation to mCelsius. In case the ADC value + * is out of the ADC conv table range, it returns -ERANGE, 0 on success. + * The conversion table is indexed by the ADC values. + * + * Return: 0 if conversion was successful, else -ERANGE in case the @adc_val + * argument is out of the ADC conv table range. + */ +static +int ti_bandgap_adc_to_mcelsius(struct ti_bandgap *bgp, int adc_val, int *t) +{ + const struct ti_bandgap_data *conf = bgp->conf; + + /* look up for temperature in the table and return the temperature */ + if (adc_val < conf->adc_start_val || adc_val > conf->adc_end_val) + return -ERANGE; + + *t = bgp->conf->conv_table[adc_val - conf->adc_start_val]; + return 0; +} + +/** + * ti_bandgap_validate() - helper to check the sanity of a struct ti_bandgap + * @bgp: struct ti_bandgap pointer + * @id: bandgap sensor id + * + * Checks if the bandgap pointer is valid and if the sensor id is also + * applicable. + * + * Return: 0 if no errors, -EINVAL for invalid @bgp pointer or -ERANGE if + * @id cannot index @bgp sensors. + */ +static inline int ti_bandgap_validate(struct ti_bandgap *bgp, int id) +{ + if (!bgp || IS_ERR(bgp)) { + pr_err("%s: invalid bandgap pointer\n", __func__); + return -EINVAL; + } + + if ((id < 0) || (id >= bgp->conf->sensor_count)) { + dev_err(bgp->dev, "%s: sensor id out of range (%d)\n", + __func__, id); + return -ERANGE; + } + + return 0; +} + +/** + * ti_bandgap_read_counter() - read the sensor counter + * @bgp: pointer to bandgap instance + * @id: sensor id + * @interval: resulting update interval in miliseconds + */ +static void ti_bandgap_read_counter(struct ti_bandgap *bgp, int id, + int *interval) +{ + struct temp_sensor_registers *tsr; + int time; + + tsr = bgp->conf->sensors[id].registers; + time = ti_bandgap_readl(bgp, tsr->bgap_counter); + time = (time & tsr->counter_mask) >> + __ffs(tsr->counter_mask); + time = time * 1000 / bgp->clk_rate; + *interval = time; +} + +/** + * ti_bandgap_read_counter_delay() - read the sensor counter delay + * @bgp: pointer to bandgap instance + * @id: sensor id + * @interval: resulting update interval in miliseconds + */ +static void ti_bandgap_read_counter_delay(struct ti_bandgap *bgp, int id, + int *interval) +{ + struct temp_sensor_registers *tsr; + int reg_val; + + tsr = bgp->conf->sensors[id].registers; + + reg_val = ti_bandgap_readl(bgp, tsr->bgap_mask_ctrl); + reg_val = (reg_val & tsr->mask_counter_delay_mask) >> + __ffs(tsr->mask_counter_delay_mask); + switch (reg_val) { + case 0: + *interval = 0; + break; + case 1: + *interval = 1; + break; + case 2: + *interval = 10; + break; + case 3: + *interval = 100; + break; + case 4: + *interval = 250; + break; + case 5: + *interval = 500; + break; + default: + dev_warn(bgp->dev, "Wrong counter delay value read from register %X", + reg_val); + } +} + +/** + * ti_bandgap_read_update_interval() - read the sensor update interval + * @bgp: pointer to bandgap instance + * @id: sensor id + * @interval: resulting update interval in miliseconds + * + * Return: 0 on success or the proper error code + */ +int ti_bandgap_read_update_interval(struct ti_bandgap *bgp, int id, + int *interval) +{ + int ret = 0; + + ret = ti_bandgap_validate(bgp, id); + if (ret) + goto exit; + + if (!TI_BANDGAP_HAS(bgp, COUNTER) && + !TI_BANDGAP_HAS(bgp, COUNTER_DELAY)) { + ret = -ENOTSUPP; + goto exit; + } + + if (TI_BANDGAP_HAS(bgp, COUNTER)) { + ti_bandgap_read_counter(bgp, id, interval); + goto exit; + } + + ti_bandgap_read_counter_delay(bgp, id, interval); +exit: + return ret; +} + +/** + * ti_bandgap_write_counter_delay() - set the counter_delay + * @bgp: pointer to bandgap instance + * @id: sensor id + * @interval: desired update interval in miliseconds + * + * Return: 0 on success or the proper error code + */ +static int ti_bandgap_write_counter_delay(struct ti_bandgap *bgp, int id, + u32 interval) +{ + int rval; + + switch (interval) { + case 0: /* Immediate conversion */ + rval = 0x0; + break; + case 1: /* Conversion after ever 1ms */ + rval = 0x1; + break; + case 10: /* Conversion after ever 10ms */ + rval = 0x2; + break; + case 100: /* Conversion after ever 100ms */ + rval = 0x3; + break; + case 250: /* Conversion after ever 250ms */ + rval = 0x4; + break; + case 500: /* Conversion after ever 500ms */ + rval = 0x5; + break; + default: + dev_warn(bgp->dev, "Delay %d ms is not supported\n", interval); + return -EINVAL; + } + + spin_lock(&bgp->lock); + RMW_BITS(bgp, id, bgap_mask_ctrl, mask_counter_delay_mask, rval); + spin_unlock(&bgp->lock); + + return 0; +} + +/** + * ti_bandgap_write_counter() - set the bandgap sensor counter + * @bgp: pointer to bandgap instance + * @id: sensor id + * @interval: desired update interval in miliseconds + */ +static void ti_bandgap_write_counter(struct ti_bandgap *bgp, int id, + u32 interval) +{ + interval = interval * bgp->clk_rate / 1000; + spin_lock(&bgp->lock); + RMW_BITS(bgp, id, bgap_counter, counter_mask, interval); + spin_unlock(&bgp->lock); +} + +/** + * ti_bandgap_write_update_interval() - set the update interval + * @bgp: pointer to bandgap instance + * @id: sensor id + * @interval: desired update interval in miliseconds + * + * Return: 0 on success or the proper error code + */ +int ti_bandgap_write_update_interval(struct ti_bandgap *bgp, + int id, u32 interval) +{ + int ret = ti_bandgap_validate(bgp, id); + if (ret) + goto exit; + + if (!TI_BANDGAP_HAS(bgp, COUNTER) && + !TI_BANDGAP_HAS(bgp, COUNTER_DELAY)) { + ret = -ENOTSUPP; + goto exit; + } + + if (TI_BANDGAP_HAS(bgp, COUNTER)) { + ti_bandgap_write_counter(bgp, id, interval); + goto exit; + } + + ret = ti_bandgap_write_counter_delay(bgp, id, interval); +exit: + return ret; +} + +/** + * ti_bandgap_read_temperature() - report current temperature + * @bgp: pointer to bandgap instance + * @id: sensor id + * @temperature: resulting temperature + * + * Return: 0 on success or the proper error code + */ +int ti_bandgap_read_temperature(struct ti_bandgap *bgp, int id, + int *temperature) +{ + u32 temp; + int ret; + + ret = ti_bandgap_validate(bgp, id); + if (ret) + return ret; + + if (!TI_BANDGAP_HAS(bgp, MODE_CONFIG)) { + ret = ti_bandgap_force_single_read(bgp, id); + if (ret) + return ret; + } + + spin_lock(&bgp->lock); + temp = ti_bandgap_read_temp(bgp, id); + spin_unlock(&bgp->lock); + + ret = ti_bandgap_adc_to_mcelsius(bgp, temp, &temp); + if (ret) + return -EIO; + + *temperature = temp; + + return 0; +} + +/** + * ti_bandgap_set_sensor_data() - helper function to store thermal + * framework related data. + * @bgp: pointer to bandgap instance + * @id: sensor id + * @data: thermal framework related data to be stored + * + * Return: 0 on success or the proper error code + */ +int ti_bandgap_set_sensor_data(struct ti_bandgap *bgp, int id, void *data) +{ + int ret = ti_bandgap_validate(bgp, id); + if (ret) + return ret; + + bgp->regval[id].data = data; + + return 0; +} + +/** + * ti_bandgap_get_sensor_data() - helper function to get thermal + * framework related data. + * @bgp: pointer to bandgap instance + * @id: sensor id + * + * Return: data stored by set function with sensor id on success or NULL + */ +void *ti_bandgap_get_sensor_data(struct ti_bandgap *bgp, int id) +{ + int ret = ti_bandgap_validate(bgp, id); + if (ret) + return ERR_PTR(ret); + + return bgp->regval[id].data; +} + +/*** Helper functions used during device initialization ***/ + +/** + * ti_bandgap_force_single_read() - executes 1 single ADC conversion + * @bgp: pointer to struct ti_bandgap + * @id: sensor id which it is desired to read 1 temperature + * + * Used to initialize the conversion state machine and set it to a valid + * state. Called during device initialization and context restore events. + * + * Return: 0 + */ +static int +ti_bandgap_force_single_read(struct ti_bandgap *bgp, int id) +{ + u32 counter = 1000; + struct temp_sensor_registers *tsr; + + /* Select single conversion mode */ + if (TI_BANDGAP_HAS(bgp, MODE_CONFIG)) + RMW_BITS(bgp, id, bgap_mode_ctrl, mode_ctrl_mask, 0); + + /* Start of Conversion = 1 */ + RMW_BITS(bgp, id, temp_sensor_ctrl, bgap_soc_mask, 1); + + /* Wait for EOCZ going up */ + tsr = bgp->conf->sensors[id].registers; + + while (--counter) { + if (ti_bandgap_readl(bgp, tsr->temp_sensor_ctrl) & + tsr->bgap_eocz_mask) + break; + } + + /* Start of Conversion = 0 */ + RMW_BITS(bgp, id, temp_sensor_ctrl, bgap_soc_mask, 0); + + /* Wait for EOCZ going down */ + counter = 1000; + while (--counter) { + if (!(ti_bandgap_readl(bgp, tsr->temp_sensor_ctrl) & + tsr->bgap_eocz_mask)) + break; + } + + return 0; +} + +/** + * ti_bandgap_set_continuous_mode() - One time enabling of continuous mode + * @bgp: pointer to struct ti_bandgap + * + * Call this function only if HAS(MODE_CONFIG) is set. As this driver may + * be used for junction temperature monitoring, it is desirable that the + * sensors are operational all the time, so that alerts are generated + * properly. + * + * Return: 0 + */ +static int ti_bandgap_set_continuous_mode(struct ti_bandgap *bgp) +{ + int i; + + for (i = 0; i < bgp->conf->sensor_count; i++) { + /* Perform a single read just before enabling continuous */ + ti_bandgap_force_single_read(bgp, i); + RMW_BITS(bgp, i, bgap_mode_ctrl, mode_ctrl_mask, 1); + } + + return 0; +} + +/** + * ti_bandgap_get_trend() - To fetch the temperature trend of a sensor + * @bgp: pointer to struct ti_bandgap + * @id: id of the individual sensor + * @trend: Pointer to trend. + * + * This function needs to be called to fetch the temperature trend of a + * Particular sensor. The function computes the difference in temperature + * w.r.t time. For the bandgaps with built in history buffer the temperatures + * are read from the buffer and for those without the Buffer -ENOTSUPP is + * returned. + * + * Return: 0 if no error, else return corresponding error. If no + * error then the trend value is passed on to trend parameter + */ +int ti_bandgap_get_trend(struct ti_bandgap *bgp, int id, int *trend) +{ + struct temp_sensor_registers *tsr; + u32 temp1, temp2, reg1, reg2; + int t1, t2, interval, ret = 0; + + ret = ti_bandgap_validate(bgp, id); + if (ret) + goto exit; + + if (!TI_BANDGAP_HAS(bgp, HISTORY_BUFFER) || + !TI_BANDGAP_HAS(bgp, FREEZE_BIT)) { + ret = -ENOTSUPP; + goto exit; + } + + spin_lock(&bgp->lock); + + tsr = bgp->conf->sensors[id].registers; + + /* Freeze and read the last 2 valid readings */ + RMW_BITS(bgp, id, bgap_mask_ctrl, mask_freeze_mask, 1); + reg1 = tsr->ctrl_dtemp_1; + reg2 = tsr->ctrl_dtemp_2; + + /* read temperature from history buffer */ + temp1 = ti_bandgap_readl(bgp, reg1); + temp1 &= tsr->bgap_dtemp_mask; + + temp2 = ti_bandgap_readl(bgp, reg2); + temp2 &= tsr->bgap_dtemp_mask; + + /* Convert from adc values to mCelsius temperature */ + ret = ti_bandgap_adc_to_mcelsius(bgp, temp1, &t1); + if (ret) + goto unfreeze; + + ret = ti_bandgap_adc_to_mcelsius(bgp, temp2, &t2); + if (ret) + goto unfreeze; + + /* Fetch the update interval */ + ret = ti_bandgap_read_update_interval(bgp, id, &interval); + if (ret) + goto unfreeze; + + /* Set the interval to 1 ms if bandgap counter delay is not set */ + if (interval == 0) + interval = 1; + + *trend = (t1 - t2) / interval; + + dev_dbg(bgp->dev, "The temperatures are t1 = %d and t2 = %d and trend =%d\n", + t1, t2, *trend); + +unfreeze: + RMW_BITS(bgp, id, bgap_mask_ctrl, mask_freeze_mask, 0); + spin_unlock(&bgp->lock); +exit: + return ret; +} + +/** + * ti_bandgap_tshut_init() - setup and initialize tshut handling + * @bgp: pointer to struct ti_bandgap + * @pdev: pointer to device struct platform_device + * + * Call this function only in case the bandgap features HAS(TSHUT). + * In this case, the driver needs to handle the TSHUT signal as an IRQ. + * The IRQ is wired as a GPIO, and for this purpose, it is required + * to specify which GPIO line is used. TSHUT IRQ is fired anytime + * one of the bandgap sensors violates the TSHUT high/hot threshold. + * And in that case, the system must go off. + * + * Return: 0 if no error, else error status + */ +static int ti_bandgap_tshut_init(struct ti_bandgap *bgp, + struct platform_device *pdev) +{ + int gpio_nr = bgp->tshut_gpio; + int status; + + /* Request for gpio_86 line */ + status = gpio_request(gpio_nr, "tshut"); + if (status < 0) { + dev_err(bgp->dev, "Could not request for TSHUT GPIO:%i\n", 86); + return status; + } + status = gpio_direction_input(gpio_nr); + if (status) { + dev_err(bgp->dev, "Cannot set input TSHUT GPIO %d\n", gpio_nr); + return status; + } + + status = request_irq(gpio_to_irq(gpio_nr), ti_bandgap_tshut_irq_handler, + IRQF_TRIGGER_RISING, "tshut", NULL); + if (status) { + gpio_free(gpio_nr); + dev_err(bgp->dev, "request irq failed for TSHUT"); + } + + return 0; +} + +/** + * ti_bandgap_alert_init() - setup and initialize talert handling + * @bgp: pointer to struct ti_bandgap + * @pdev: pointer to device struct platform_device + * + * Call this function only in case the bandgap features HAS(TALERT). + * In this case, the driver needs to handle the TALERT signals as an IRQs. + * TALERT is a normal IRQ and it is fired any time thresholds (hot or cold) + * are violated. In these situation, the driver must reprogram the thresholds, + * accordingly to specified policy. + * + * Return: 0 if no error, else return corresponding error. + */ +static int ti_bandgap_talert_init(struct ti_bandgap *bgp, + struct platform_device *pdev) +{ + int ret; + + bgp->irq = platform_get_irq(pdev, 0); + if (bgp->irq < 0) { + dev_err(&pdev->dev, "get_irq failed\n"); + return bgp->irq; + } + ret = request_threaded_irq(bgp->irq, NULL, + ti_bandgap_talert_irq_handler, + IRQF_TRIGGER_HIGH | IRQF_ONESHOT, + "talert", bgp); + if (ret) { + dev_err(&pdev->dev, "Request threaded irq failed.\n"); + return ret; + } + + return 0; +} + +static const struct of_device_id of_ti_bandgap_match[]; +/** + * ti_bandgap_build() - parse DT and setup a struct ti_bandgap + * @pdev: pointer to device struct platform_device + * + * Used to read the device tree properties accordingly to the bandgap + * matching version. Based on bandgap version and its capabilities it + * will build a struct ti_bandgap out of the required DT entries. + * + * Return: valid bandgap structure if successful, else returns ERR_PTR + * return value must be verified with IS_ERR. + */ +static struct ti_bandgap *ti_bandgap_build(struct platform_device *pdev) +{ + struct device_node *node = pdev->dev.of_node; + const struct of_device_id *of_id; + struct ti_bandgap *bgp; + struct resource *res; + int i; + + /* just for the sake */ + if (!node) { + dev_err(&pdev->dev, "no platform information available\n"); + return ERR_PTR(-EINVAL); + } + + bgp = devm_kzalloc(&pdev->dev, sizeof(*bgp), GFP_KERNEL); + if (!bgp) + return ERR_PTR(-ENOMEM); + + of_id = of_match_device(of_ti_bandgap_match, &pdev->dev); + if (of_id) + bgp->conf = of_id->data; + + /* register shadow for context save and restore */ + bgp->regval = devm_kcalloc(&pdev->dev, bgp->conf->sensor_count, + sizeof(*bgp->regval), GFP_KERNEL); + if (!bgp->regval) + return ERR_PTR(-ENOMEM); + + i = 0; + do { + void __iomem *chunk; + + res = platform_get_resource(pdev, IORESOURCE_MEM, i); + if (!res) + break; + chunk = devm_ioremap_resource(&pdev->dev, res); + if (i == 0) + bgp->base = chunk; + if (IS_ERR(chunk)) + return ERR_CAST(chunk); + + i++; + } while (res); + + if (TI_BANDGAP_HAS(bgp, TSHUT)) { + bgp->tshut_gpio = of_get_gpio(node, 0); + if (!gpio_is_valid(bgp->tshut_gpio)) { + dev_err(&pdev->dev, "invalid gpio for tshut (%d)\n", + bgp->tshut_gpio); + return ERR_PTR(-EINVAL); + } + } + + return bgp; +} + +/*** Device driver call backs ***/ + +static +int ti_bandgap_probe(struct platform_device *pdev) +{ + struct ti_bandgap *bgp; + int clk_rate, ret, i; + + bgp = ti_bandgap_build(pdev); + if (IS_ERR(bgp)) { + dev_err(&pdev->dev, "failed to fetch platform data\n"); + return PTR_ERR(bgp); + } + bgp->dev = &pdev->dev; + + if (TI_BANDGAP_HAS(bgp, UNRELIABLE)) + dev_warn(&pdev->dev, + "This OMAP thermal sensor is unreliable. You've been warned\n"); + + if (TI_BANDGAP_HAS(bgp, TSHUT)) { + ret = ti_bandgap_tshut_init(bgp, pdev); + if (ret) { + dev_err(&pdev->dev, + "failed to initialize system tshut IRQ\n"); + return ret; + } + } + + bgp->fclock = clk_get(NULL, bgp->conf->fclock_name); + if (IS_ERR(bgp->fclock)) { + dev_err(&pdev->dev, "failed to request fclock reference\n"); + ret = PTR_ERR(bgp->fclock); + goto free_irqs; + } + + bgp->div_clk = clk_get(NULL, bgp->conf->div_ck_name); + if (IS_ERR(bgp->div_clk)) { + dev_err(&pdev->dev, "failed to request div_ts_ck clock ref\n"); + ret = PTR_ERR(bgp->div_clk); + goto put_fclock; + } + + for (i = 0; i < bgp->conf->sensor_count; i++) { + struct temp_sensor_registers *tsr; + u32 val; + + tsr = bgp->conf->sensors[i].registers; + /* + * check if the efuse has a non-zero value if not + * it is an untrimmed sample and the temperatures + * may not be accurate + */ + val = ti_bandgap_readl(bgp, tsr->bgap_efuse); + if (!val) + dev_info(&pdev->dev, + "Non-trimmed BGAP, Temp not accurate\n"); + } + + clk_rate = clk_round_rate(bgp->div_clk, + bgp->conf->sensors[0].ts_data->max_freq); + if (clk_rate < bgp->conf->sensors[0].ts_data->min_freq || + clk_rate <= 0) { + ret = -ENODEV; + dev_err(&pdev->dev, "wrong clock rate (%d)\n", clk_rate); + goto put_clks; + } + + ret = clk_set_rate(bgp->div_clk, clk_rate); + if (ret) + dev_err(&pdev->dev, "Cannot re-set clock rate. Continuing\n"); + + bgp->clk_rate = clk_rate; + if (TI_BANDGAP_HAS(bgp, CLK_CTRL)) + clk_prepare_enable(bgp->fclock); + + + spin_lock_init(&bgp->lock); + bgp->dev = &pdev->dev; + platform_set_drvdata(pdev, bgp); + + ti_bandgap_power(bgp, true); + + /* Set default counter to 1 for now */ + if (TI_BANDGAP_HAS(bgp, COUNTER)) + for (i = 0; i < bgp->conf->sensor_count; i++) + RMW_BITS(bgp, i, bgap_counter, counter_mask, 1); + + /* Set default thresholds for alert and shutdown */ + for (i = 0; i < bgp->conf->sensor_count; i++) { + struct temp_sensor_data *ts_data; + + ts_data = bgp->conf->sensors[i].ts_data; + + if (TI_BANDGAP_HAS(bgp, TALERT)) { + /* Set initial Talert thresholds */ + RMW_BITS(bgp, i, bgap_threshold, + threshold_tcold_mask, ts_data->t_cold); + RMW_BITS(bgp, i, bgap_threshold, + threshold_thot_mask, ts_data->t_hot); + /* Enable the alert events */ + RMW_BITS(bgp, i, bgap_mask_ctrl, mask_hot_mask, 1); + RMW_BITS(bgp, i, bgap_mask_ctrl, mask_cold_mask, 1); + } + + if (TI_BANDGAP_HAS(bgp, TSHUT_CONFIG)) { + /* Set initial Tshut thresholds */ + RMW_BITS(bgp, i, tshut_threshold, + tshut_hot_mask, ts_data->tshut_hot); + RMW_BITS(bgp, i, tshut_threshold, + tshut_cold_mask, ts_data->tshut_cold); + } + } + + if (TI_BANDGAP_HAS(bgp, MODE_CONFIG)) + ti_bandgap_set_continuous_mode(bgp); + + /* Set .250 seconds time as default counter */ + if (TI_BANDGAP_HAS(bgp, COUNTER)) + for (i = 0; i < bgp->conf->sensor_count; i++) + RMW_BITS(bgp, i, bgap_counter, counter_mask, + bgp->clk_rate / 4); + + /* Every thing is good? Then expose the sensors */ + for (i = 0; i < bgp->conf->sensor_count; i++) { + char *domain; + + if (bgp->conf->sensors[i].register_cooling) { + ret = bgp->conf->sensors[i].register_cooling(bgp, i); + if (ret) + goto remove_sensors; + } + + if (bgp->conf->expose_sensor) { + domain = bgp->conf->sensors[i].domain; + ret = bgp->conf->expose_sensor(bgp, i, domain); + if (ret) + goto remove_last_cooling; + } + } + + /* + * Enable the Interrupts once everything is set. Otherwise irq handler + * might be called as soon as it is enabled where as rest of framework + * is still getting initialised. + */ + if (TI_BANDGAP_HAS(bgp, TALERT)) { + ret = ti_bandgap_talert_init(bgp, pdev); + if (ret) { + dev_err(&pdev->dev, "failed to initialize Talert IRQ\n"); + i = bgp->conf->sensor_count; + goto disable_clk; + } + } + + return 0; + +remove_last_cooling: + if (bgp->conf->sensors[i].unregister_cooling) + bgp->conf->sensors[i].unregister_cooling(bgp, i); +remove_sensors: + for (i--; i >= 0; i--) { + if (bgp->conf->sensors[i].unregister_cooling) + bgp->conf->sensors[i].unregister_cooling(bgp, i); + if (bgp->conf->remove_sensor) + bgp->conf->remove_sensor(bgp, i); + } + ti_bandgap_power(bgp, false); +disable_clk: + if (TI_BANDGAP_HAS(bgp, CLK_CTRL)) + clk_disable_unprepare(bgp->fclock); +put_clks: + clk_put(bgp->div_clk); +put_fclock: + clk_put(bgp->fclock); +free_irqs: + if (TI_BANDGAP_HAS(bgp, TSHUT)) { + free_irq(gpio_to_irq(bgp->tshut_gpio), NULL); + gpio_free(bgp->tshut_gpio); + } + + return ret; +} + +static +int ti_bandgap_remove(struct platform_device *pdev) +{ + struct ti_bandgap *bgp = platform_get_drvdata(pdev); + int i; + + /* First thing is to remove sensor interfaces */ + for (i = 0; i < bgp->conf->sensor_count; i++) { + if (bgp->conf->sensors[i].unregister_cooling) + bgp->conf->sensors[i].unregister_cooling(bgp, i); + + if (bgp->conf->remove_sensor) + bgp->conf->remove_sensor(bgp, i); + } + + ti_bandgap_power(bgp, false); + + if (TI_BANDGAP_HAS(bgp, CLK_CTRL)) + clk_disable_unprepare(bgp->fclock); + clk_put(bgp->fclock); + clk_put(bgp->div_clk); + + if (TI_BANDGAP_HAS(bgp, TALERT)) + free_irq(bgp->irq, bgp); + + if (TI_BANDGAP_HAS(bgp, TSHUT)) { + free_irq(gpio_to_irq(bgp->tshut_gpio), NULL); + gpio_free(bgp->tshut_gpio); + } + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int ti_bandgap_save_ctxt(struct ti_bandgap *bgp) +{ + int i; + + for (i = 0; i < bgp->conf->sensor_count; i++) { + struct temp_sensor_registers *tsr; + struct temp_sensor_regval *rval; + + rval = &bgp->regval[i]; + tsr = bgp->conf->sensors[i].registers; + + if (TI_BANDGAP_HAS(bgp, MODE_CONFIG)) + rval->bg_mode_ctrl = ti_bandgap_readl(bgp, + tsr->bgap_mode_ctrl); + if (TI_BANDGAP_HAS(bgp, COUNTER)) + rval->bg_counter = ti_bandgap_readl(bgp, + tsr->bgap_counter); + if (TI_BANDGAP_HAS(bgp, TALERT)) { + rval->bg_threshold = ti_bandgap_readl(bgp, + tsr->bgap_threshold); + rval->bg_ctrl = ti_bandgap_readl(bgp, + tsr->bgap_mask_ctrl); + } + + if (TI_BANDGAP_HAS(bgp, TSHUT_CONFIG)) + rval->tshut_threshold = ti_bandgap_readl(bgp, + tsr->tshut_threshold); + } + + return 0; +} + +static int ti_bandgap_restore_ctxt(struct ti_bandgap *bgp) +{ + int i; + + for (i = 0; i < bgp->conf->sensor_count; i++) { + struct temp_sensor_registers *tsr; + struct temp_sensor_regval *rval; + u32 val = 0; + + rval = &bgp->regval[i]; + tsr = bgp->conf->sensors[i].registers; + + if (TI_BANDGAP_HAS(bgp, COUNTER)) + val = ti_bandgap_readl(bgp, tsr->bgap_counter); + + if (TI_BANDGAP_HAS(bgp, TSHUT_CONFIG)) + ti_bandgap_writel(bgp, rval->tshut_threshold, + tsr->tshut_threshold); + /* Force immediate temperature measurement and update + * of the DTEMP field + */ + ti_bandgap_force_single_read(bgp, i); + + if (TI_BANDGAP_HAS(bgp, COUNTER)) + ti_bandgap_writel(bgp, rval->bg_counter, + tsr->bgap_counter); + if (TI_BANDGAP_HAS(bgp, MODE_CONFIG)) + ti_bandgap_writel(bgp, rval->bg_mode_ctrl, + tsr->bgap_mode_ctrl); + if (TI_BANDGAP_HAS(bgp, TALERT)) { + ti_bandgap_writel(bgp, rval->bg_threshold, + tsr->bgap_threshold); + ti_bandgap_writel(bgp, rval->bg_ctrl, + tsr->bgap_mask_ctrl); + } + } + + return 0; +} + +static int ti_bandgap_suspend(struct device *dev) +{ + struct ti_bandgap *bgp = dev_get_drvdata(dev); + int err; + + err = ti_bandgap_save_ctxt(bgp); + ti_bandgap_power(bgp, false); + + if (TI_BANDGAP_HAS(bgp, CLK_CTRL)) + clk_disable_unprepare(bgp->fclock); + + return err; +} + +static int ti_bandgap_resume(struct device *dev) +{ + struct ti_bandgap *bgp = dev_get_drvdata(dev); + + if (TI_BANDGAP_HAS(bgp, CLK_CTRL)) + clk_prepare_enable(bgp->fclock); + + ti_bandgap_power(bgp, true); + + return ti_bandgap_restore_ctxt(bgp); +} +static SIMPLE_DEV_PM_OPS(ti_bandgap_dev_pm_ops, ti_bandgap_suspend, + ti_bandgap_resume); + +#define DEV_PM_OPS (&ti_bandgap_dev_pm_ops) +#else +#define DEV_PM_OPS NULL +#endif + +static const struct of_device_id of_ti_bandgap_match[] = { +#ifdef CONFIG_OMAP3_THERMAL + { + .compatible = "ti,omap34xx-bandgap", + .data = (void *)&omap34xx_data, + }, + { + .compatible = "ti,omap36xx-bandgap", + .data = (void *)&omap36xx_data, + }, +#endif +#ifdef CONFIG_OMAP4_THERMAL + { + .compatible = "ti,omap4430-bandgap", + .data = (void *)&omap4430_data, + }, + { + .compatible = "ti,omap4460-bandgap", + .data = (void *)&omap4460_data, + }, + { + .compatible = "ti,omap4470-bandgap", + .data = (void *)&omap4470_data, + }, +#endif +#ifdef CONFIG_OMAP5_THERMAL + { + .compatible = "ti,omap5430-bandgap", + .data = (void *)&omap5430_data, + }, +#endif +#ifdef CONFIG_DRA752_THERMAL + { + .compatible = "ti,dra752-bandgap", + .data = (void *)&dra752_data, + }, +#endif + /* Sentinel */ + { }, +}; +MODULE_DEVICE_TABLE(of, of_ti_bandgap_match); + +static struct platform_driver ti_bandgap_sensor_driver = { + .probe = ti_bandgap_probe, + .remove = ti_bandgap_remove, + .driver = { + .name = "ti-soc-thermal", + .pm = DEV_PM_OPS, + .of_match_table = of_ti_bandgap_match, + }, +}; + +module_platform_driver(ti_bandgap_sensor_driver); + +MODULE_DESCRIPTION("OMAP4+ bandgap temperature sensor driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:ti-soc-thermal"); +MODULE_AUTHOR("Texas Instrument Inc."); diff --git a/drivers/thermal/ti-soc-thermal/ti-bandgap.h b/drivers/thermal/ti-soc-thermal/ti-bandgap.h new file mode 100644 index 000000000..68d39ad43 --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/ti-bandgap.h @@ -0,0 +1,386 @@ +/* + * OMAP4 Bandgap temperature sensor driver + * + * Copyright (C) 2011 Texas Instruments Incorporated - http://www.ti.com/ + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ +#ifndef __TI_BANDGAP_H +#define __TI_BANDGAP_H + +#include <linux/spinlock.h> +#include <linux/types.h> +#include <linux/err.h> + +/** + * DOC: bandgap driver data structure + * ================================== + * + * +----------+----------------+ + * | struct temp_sensor_regval | + * +---------------------------+ + * * (Array of) + * | + * | + * +-------------------+ +-----------------+ + * | struct ti_bandgap |-->| struct device * | + * +----------+--------+ +-----------------+ + * | + * | + * V + * +------------------------+ + * | struct ti_bandgap_data | + * +------------------------+ + * | + * | + * * (Array of) + * +------------+------------------------------------------------------+ + * | +----------+------------+ +-------------------------+ | + * | | struct ti_temp_sensor |-->| struct temp_sensor_data | | + * | +-----------------------+ +------------+------------+ | + * | | | + * | + | + * | V | + * | +----------+-------------------+ | + * | | struct temp_sensor_registers | | + * | +------------------------------+ | + * | | + * +-------------------------------------------------------------------+ + * + * Above is a simple diagram describing how the data structure below + * are organized. For each bandgap device there should be a ti_bandgap_data + * containing the device instance configuration, as well as, an array of + * sensors, representing every sensor instance present in this bandgap. + */ + +/** + * struct temp_sensor_registers - descriptor to access registers and bitfields + * @temp_sensor_ctrl: TEMP_SENSOR_CTRL register offset + * @bgap_tempsoff_mask: mask to temp_sensor_ctrl.tempsoff + * @bgap_soc_mask: mask to temp_sensor_ctrl.soc + * @bgap_eocz_mask: mask to temp_sensor_ctrl.eocz + * @bgap_dtemp_mask: mask to temp_sensor_ctrl.dtemp + * @bgap_mask_ctrl: BANDGAP_MASK_CTRL register offset + * @mask_hot_mask: mask to bandgap_mask_ctrl.mask_hot + * @mask_cold_mask: mask to bandgap_mask_ctrl.mask_cold + * @mask_counter_delay_mask: mask to bandgap_mask_ctrl.mask_counter_delay + * @mask_freeze_mask: mask to bandgap_mask_ctrl.mask_free + * @bgap_mode_ctrl: BANDGAP_MODE_CTRL register offset + * @mode_ctrl_mask: mask to bandgap_mode_ctrl.mode_ctrl + * @bgap_counter: BANDGAP_COUNTER register offset + * @counter_mask: mask to bandgap_counter.counter + * @bgap_threshold: BANDGAP_THRESHOLD register offset (TALERT thresholds) + * @threshold_thot_mask: mask to bandgap_threhold.thot + * @threshold_tcold_mask: mask to bandgap_threhold.tcold + * @tshut_threshold: TSHUT_THRESHOLD register offset (TSHUT thresholds) + * @tshut_hot_mask: mask to tshut_threhold.thot + * @tshut_cold_mask: mask to tshut_threhold.thot + * @bgap_status: BANDGAP_STATUS register offset + * @status_hot_mask: mask to bandgap_status.hot + * @status_cold_mask: mask to bandgap_status.cold + * @ctrl_dtemp_1: CTRL_DTEMP1 register offset + * @ctrl_dtemp_2: CTRL_DTEMP2 register offset + * @bgap_efuse: BANDGAP_EFUSE register offset + * + * The register offsets and bitfields might change across + * OMAP and variants versions. Hence this struct serves as a + * descriptor map on how to access the registers and the bitfields. + * + * This descriptor contains registers of all versions of bandgap chips. + * Not all versions will use all registers, depending on the available + * features. Please read TRMs for descriptive explanation on each bitfield. + */ + +struct temp_sensor_registers { + u32 temp_sensor_ctrl; + u32 bgap_tempsoff_mask; + u32 bgap_soc_mask; + u32 bgap_eocz_mask; + u32 bgap_dtemp_mask; + + u32 bgap_mask_ctrl; + u32 mask_hot_mask; + u32 mask_cold_mask; + u32 mask_counter_delay_mask; + u32 mask_freeze_mask; + + u32 bgap_mode_ctrl; + u32 mode_ctrl_mask; + + u32 bgap_counter; + u32 counter_mask; + + u32 bgap_threshold; + u32 threshold_thot_mask; + u32 threshold_tcold_mask; + + u32 tshut_threshold; + u32 tshut_hot_mask; + u32 tshut_cold_mask; + + u32 bgap_status; + u32 status_hot_mask; + u32 status_cold_mask; + + u32 ctrl_dtemp_1; + u32 ctrl_dtemp_2; + u32 bgap_efuse; +}; + +/** + * struct temp_sensor_data - The thresholds and limits for temperature sensors. + * @tshut_hot: temperature to trigger a thermal reset (initial value) + * @tshut_cold: temp to get the plat out of reset due to thermal (init val) + * @t_hot: temperature to trigger a thermal alert (high initial value) + * @t_cold: temperature to trigger a thermal alert (low initial value) + * @min_freq: sensor minimum clock rate + * @max_freq: sensor maximum clock rate + * + * This data structure will hold the required thresholds and temperature limits + * for a specific temperature sensor, like shutdown temperature, alert + * temperature, clock / rate used, ADC conversion limits and update intervals + */ +struct temp_sensor_data { + u32 tshut_hot; + u32 tshut_cold; + u32 t_hot; + u32 t_cold; + u32 min_freq; + u32 max_freq; +}; + +struct ti_bandgap_data; + +/** + * struct temp_sensor_regval - temperature sensor register values and priv data + * @bg_mode_ctrl: temp sensor control register value + * @bg_ctrl: bandgap ctrl register value + * @bg_counter: bandgap counter value + * @bg_threshold: bandgap threshold register value + * @tshut_threshold: bandgap tshut register value + * @data: private data + * + * Data structure to save and restore bandgap register set context. Only + * required registers are shadowed, when needed. + */ +struct temp_sensor_regval { + u32 bg_mode_ctrl; + u32 bg_ctrl; + u32 bg_counter; + u32 bg_threshold; + u32 tshut_threshold; + void *data; +}; + +/** + * struct ti_bandgap - bandgap device structure + * @dev: struct device pointer + * @base: io memory base address + * @conf: struct with bandgap configuration set (# sensors, conv_table, etc) + * @regval: temperature sensor register values + * @fclock: pointer to functional clock of temperature sensor + * @div_clk: pointer to divider clock of temperature sensor fclk + * @lock: spinlock for ti_bandgap structure + * @irq: MPU IRQ number for thermal alert + * @tshut_gpio: GPIO where Tshut signal is routed + * @clk_rate: Holds current clock rate + * + * The bandgap device structure representing the bandgap device instance. + * It holds most of the dynamic stuff. Configurations and sensor specific + * entries are inside the @conf structure. + */ +struct ti_bandgap { + struct device *dev; + void __iomem *base; + const struct ti_bandgap_data *conf; + struct temp_sensor_regval *regval; + struct clk *fclock; + struct clk *div_clk; + spinlock_t lock; /* shields this struct */ + int irq; + int tshut_gpio; + u32 clk_rate; +}; + +/** + * struct ti_temp_sensor - bandgap temperature sensor configuration data + * @ts_data: pointer to struct with thresholds, limits of temperature sensor + * @registers: pointer to the list of register offsets and bitfields + * @domain: the name of the domain where the sensor is located + * @slope_pcb: sensor gradient slope info for hotspot extrapolation equation + * with no external influence + * @constant_pcb: sensor gradient const info for hotspot extrapolation equation + * with no external influence + * @register_cooling: function to describe how this sensor is going to be cooled + * @unregister_cooling: function to release cooling data + * + * Data structure to describe a temperature sensor handled by a bandgap device. + * It should provide configuration details on this sensor, such as how to + * access the registers affecting this sensor, shadow register buffer, how to + * assess the gradient from hotspot, how to cooldown the domain when sensor + * reports too hot temperature. + */ +struct ti_temp_sensor { + struct temp_sensor_data *ts_data; + struct temp_sensor_registers *registers; + char *domain; + /* for hotspot extrapolation */ + const int slope_pcb; + const int constant_pcb; + int (*register_cooling)(struct ti_bandgap *bgp, int id); + int (*unregister_cooling)(struct ti_bandgap *bgp, int id); +}; + +/** + * DOC: ti bandgap feature types + * + * TI_BANDGAP_FEATURE_TSHUT - used when the thermal shutdown signal output + * of a bandgap device instance is routed to the processor. This means + * the system must react and perform the shutdown by itself (handle an + * IRQ, for instance). + * + * TI_BANDGAP_FEATURE_TSHUT_CONFIG - used when the bandgap device has control + * over the thermal shutdown configuration. This means that the thermal + * shutdown thresholds are programmable, for instance. + * + * TI_BANDGAP_FEATURE_TALERT - used when the bandgap device instance outputs + * a signal representing violation of programmable alert thresholds. + * + * TI_BANDGAP_FEATURE_MODE_CONFIG - used when it is possible to choose which + * mode, continuous or one shot, the bandgap device instance will operate. + * + * TI_BANDGAP_FEATURE_COUNTER - used when the bandgap device instance allows + * programming the update interval of its internal state machine. + * + * TI_BANDGAP_FEATURE_POWER_SWITCH - used when the bandgap device allows + * itself to be switched on/off. + * + * TI_BANDGAP_FEATURE_CLK_CTRL - used when the clocks feeding the bandgap + * device are gateable or not. + * + * TI_BANDGAP_FEATURE_FREEZE_BIT - used when the bandgap device features + * a history buffer that its update can be freezed/unfreezed. + * + * TI_BANDGAP_FEATURE_COUNTER_DELAY - used when the bandgap device features + * a delay programming based on distinct values. + * + * TI_BANDGAP_FEATURE_HISTORY_BUFFER - used when the bandgap device features + * a history buffer of temperatures. + * + * TI_BANDGAP_FEATURE_ERRATA_814 - used to workaorund when the bandgap device + * has Errata 814 + * TI_BANDGAP_FEATURE_UNRELIABLE - used when the sensor readings are too + * inaccurate. + * TI_BANDGAP_HAS(b, f) - macro to check if a bandgap device is capable of a + * specific feature (above) or not. Return non-zero, if yes. + */ +#define TI_BANDGAP_FEATURE_TSHUT BIT(0) +#define TI_BANDGAP_FEATURE_TSHUT_CONFIG BIT(1) +#define TI_BANDGAP_FEATURE_TALERT BIT(2) +#define TI_BANDGAP_FEATURE_MODE_CONFIG BIT(3) +#define TI_BANDGAP_FEATURE_COUNTER BIT(4) +#define TI_BANDGAP_FEATURE_POWER_SWITCH BIT(5) +#define TI_BANDGAP_FEATURE_CLK_CTRL BIT(6) +#define TI_BANDGAP_FEATURE_FREEZE_BIT BIT(7) +#define TI_BANDGAP_FEATURE_COUNTER_DELAY BIT(8) +#define TI_BANDGAP_FEATURE_HISTORY_BUFFER BIT(9) +#define TI_BANDGAP_FEATURE_ERRATA_814 BIT(10) +#define TI_BANDGAP_FEATURE_UNRELIABLE BIT(11) +#define TI_BANDGAP_HAS(b, f) \ + ((b)->conf->features & TI_BANDGAP_FEATURE_ ## f) + +/** + * struct ti_bandgap_data - ti bandgap data configuration structure + * @features: a bitwise flag set to describe the device features + * @conv_table: Pointer to ADC to temperature conversion table + * @adc_start_val: ADC conversion table starting value + * @adc_end_val: ADC conversion table ending value + * @fclock_name: clock name of the functional clock + * @div_ck_name: clock name of the clock divisor + * @sensor_count: count of temperature sensor within this bandgap device + * @report_temperature: callback to report thermal alert to thermal API + * @expose_sensor: callback to export sensor to thermal API + * @remove_sensor: callback to destroy sensor from thermal API + * @sensors: array of sensors present in this bandgap instance + * + * This is a data structure which should hold most of the static configuration + * of a bandgap device instance. It should describe which features this instance + * is capable of, the clock names to feed this device, the amount of sensors and + * their configuration representation, and how to export and unexport them to + * a thermal API. + */ +struct ti_bandgap_data { + unsigned int features; + const int *conv_table; + u32 adc_start_val; + u32 adc_end_val; + char *fclock_name; + char *div_ck_name; + int sensor_count; + int (*report_temperature)(struct ti_bandgap *bgp, int id); + int (*expose_sensor)(struct ti_bandgap *bgp, int id, char *domain); + int (*remove_sensor)(struct ti_bandgap *bgp, int id); + + /* this needs to be at the end */ + struct ti_temp_sensor sensors[]; +}; + +int ti_bandgap_read_thot(struct ti_bandgap *bgp, int id, int *thot); +int ti_bandgap_write_thot(struct ti_bandgap *bgp, int id, int val); +int ti_bandgap_read_tcold(struct ti_bandgap *bgp, int id, int *tcold); +int ti_bandgap_write_tcold(struct ti_bandgap *bgp, int id, int val); +int ti_bandgap_read_update_interval(struct ti_bandgap *bgp, int id, + int *interval); +int ti_bandgap_write_update_interval(struct ti_bandgap *bgp, int id, + u32 interval); +int ti_bandgap_read_temperature(struct ti_bandgap *bgp, int id, + int *temperature); +int ti_bandgap_set_sensor_data(struct ti_bandgap *bgp, int id, void *data); +void *ti_bandgap_get_sensor_data(struct ti_bandgap *bgp, int id); +int ti_bandgap_get_trend(struct ti_bandgap *bgp, int id, int *trend); + +#ifdef CONFIG_OMAP3_THERMAL +extern const struct ti_bandgap_data omap34xx_data; +extern const struct ti_bandgap_data omap36xx_data; +#else +#define omap34xx_data NULL +#define omap36xx_data NULL +#endif + +#ifdef CONFIG_OMAP4_THERMAL +extern const struct ti_bandgap_data omap4430_data; +extern const struct ti_bandgap_data omap4460_data; +extern const struct ti_bandgap_data omap4470_data; +#else +#define omap4430_data NULL +#define omap4460_data NULL +#define omap4470_data NULL +#endif + +#ifdef CONFIG_OMAP5_THERMAL +extern const struct ti_bandgap_data omap5430_data; +#else +#define omap5430_data NULL +#endif + +#ifdef CONFIG_DRA752_THERMAL +extern const struct ti_bandgap_data dra752_data; +#else +#define dra752_data NULL +#endif +#endif diff --git a/drivers/thermal/ti-soc-thermal/ti-thermal-common.c b/drivers/thermal/ti-soc-thermal/ti-thermal-common.c new file mode 100644 index 000000000..343da0031 --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/ti-thermal-common.c @@ -0,0 +1,286 @@ +/* + * OMAP thermal driver interface + * + * Copyright (C) 2012 Texas Instruments Incorporated - http://www.ti.com/ + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ + +#include <linux/device.h> +#include <linux/err.h> +#include <linux/mutex.h> +#include <linux/gfp.h> +#include <linux/kernel.h> +#include <linux/workqueue.h> +#include <linux/thermal.h> +#include <linux/cpufreq.h> +#include <linux/cpumask.h> +#include <linux/cpu_cooling.h> +#include <linux/of.h> + +#include "ti-thermal.h" +#include "ti-bandgap.h" + +/* common data structures */ +struct ti_thermal_data { + struct cpufreq_policy *policy; + struct thermal_zone_device *ti_thermal; + struct thermal_zone_device *pcb_tz; + struct thermal_cooling_device *cool_dev; + struct ti_bandgap *bgp; + enum thermal_device_mode mode; + struct work_struct thermal_wq; + int sensor_id; + bool our_zone; +}; + +static void ti_thermal_work(struct work_struct *work) +{ + struct ti_thermal_data *data = container_of(work, + struct ti_thermal_data, thermal_wq); + + thermal_zone_device_update(data->ti_thermal, THERMAL_EVENT_UNSPECIFIED); + + dev_dbg(&data->ti_thermal->device, "updated thermal zone %s\n", + data->ti_thermal->type); +} + +/** + * ti_thermal_hotspot_temperature - returns sensor extrapolated temperature + * @t: omap sensor temperature + * @s: omap sensor slope value + * @c: omap sensor const value + */ +static inline int ti_thermal_hotspot_temperature(int t, int s, int c) +{ + int delta = t * s / 1000 + c; + + if (delta < 0) + delta = 0; + + return t + delta; +} + +/* thermal zone ops */ +/* Get temperature callback function for thermal zone */ +static inline int __ti_thermal_get_temp(void *devdata, int *temp) +{ + struct thermal_zone_device *pcb_tz = NULL; + struct ti_thermal_data *data = devdata; + struct ti_bandgap *bgp; + const struct ti_temp_sensor *s; + int ret, tmp, slope, constant; + int pcb_temp; + + if (!data) + return 0; + + bgp = data->bgp; + s = &bgp->conf->sensors[data->sensor_id]; + + ret = ti_bandgap_read_temperature(bgp, data->sensor_id, &tmp); + if (ret) + return ret; + + /* Default constants */ + slope = thermal_zone_get_slope(data->ti_thermal); + constant = thermal_zone_get_offset(data->ti_thermal); + + pcb_tz = data->pcb_tz; + /* In case pcb zone is available, use the extrapolation rule with it */ + if (!IS_ERR(pcb_tz)) { + ret = thermal_zone_get_temp(pcb_tz, &pcb_temp); + if (!ret) { + tmp -= pcb_temp; /* got a valid PCB temp */ + slope = s->slope_pcb; + constant = s->constant_pcb; + } else { + dev_err(bgp->dev, + "Failed to read PCB state. Using defaults\n"); + ret = 0; + } + } + *temp = ti_thermal_hotspot_temperature(tmp, slope, constant); + + return ret; +} + +static inline int ti_thermal_get_temp(struct thermal_zone_device *thermal, + int *temp) +{ + struct ti_thermal_data *data = thermal->devdata; + + return __ti_thermal_get_temp(data, temp); +} + +static int __ti_thermal_get_trend(void *p, int trip, enum thermal_trend *trend) +{ + struct ti_thermal_data *data = p; + struct ti_bandgap *bgp; + int id, tr, ret = 0; + + bgp = data->bgp; + id = data->sensor_id; + + ret = ti_bandgap_get_trend(bgp, id, &tr); + if (ret) + return ret; + + if (tr > 0) + *trend = THERMAL_TREND_RAISING; + else if (tr < 0) + *trend = THERMAL_TREND_DROPPING; + else + *trend = THERMAL_TREND_STABLE; + + return 0; +} + +static const struct thermal_zone_of_device_ops ti_of_thermal_ops = { + .get_temp = __ti_thermal_get_temp, + .get_trend = __ti_thermal_get_trend, +}; + +static struct ti_thermal_data +*ti_thermal_build_data(struct ti_bandgap *bgp, int id) +{ + struct ti_thermal_data *data; + + data = devm_kzalloc(bgp->dev, sizeof(*data), GFP_KERNEL); + if (!data) { + dev_err(bgp->dev, "kzalloc fail\n"); + return NULL; + } + data->sensor_id = id; + data->bgp = bgp; + data->mode = THERMAL_DEVICE_ENABLED; + /* pcb_tz will be either valid or PTR_ERR() */ + data->pcb_tz = thermal_zone_get_zone_by_name("pcb"); + INIT_WORK(&data->thermal_wq, ti_thermal_work); + + return data; +} + +int ti_thermal_expose_sensor(struct ti_bandgap *bgp, int id, + char *domain) +{ + struct ti_thermal_data *data; + + data = ti_bandgap_get_sensor_data(bgp, id); + + if (IS_ERR_OR_NULL(data)) + data = ti_thermal_build_data(bgp, id); + + if (!data) + return -EINVAL; + + /* in case this is specified by DT */ + data->ti_thermal = devm_thermal_zone_of_sensor_register(bgp->dev, id, + data, &ti_of_thermal_ops); + if (IS_ERR(data->ti_thermal)) { + dev_err(bgp->dev, "thermal zone device is NULL\n"); + return PTR_ERR(data->ti_thermal); + } + + ti_bandgap_set_sensor_data(bgp, id, data); + ti_bandgap_write_update_interval(bgp, data->sensor_id, + data->ti_thermal->polling_delay); + + return 0; +} + +int ti_thermal_remove_sensor(struct ti_bandgap *bgp, int id) +{ + struct ti_thermal_data *data; + + data = ti_bandgap_get_sensor_data(bgp, id); + + if (!IS_ERR_OR_NULL(data) && data->ti_thermal) { + if (data->our_zone) + thermal_zone_device_unregister(data->ti_thermal); + } + + return 0; +} + +int ti_thermal_report_sensor_temperature(struct ti_bandgap *bgp, int id) +{ + struct ti_thermal_data *data; + + data = ti_bandgap_get_sensor_data(bgp, id); + + schedule_work(&data->thermal_wq); + + return 0; +} + +int ti_thermal_register_cpu_cooling(struct ti_bandgap *bgp, int id) +{ + struct ti_thermal_data *data; + struct device_node *np = bgp->dev->of_node; + + /* + * We are assuming here that if one deploys the zone + * using DT, then it must be aware that the cooling device + * loading has to happen via cpufreq driver. + */ + if (of_find_property(np, "#thermal-sensor-cells", NULL)) + return 0; + + data = ti_bandgap_get_sensor_data(bgp, id); + if (!data || IS_ERR(data)) + data = ti_thermal_build_data(bgp, id); + + if (!data) + return -EINVAL; + + data->policy = cpufreq_cpu_get(0); + if (!data->policy) { + pr_debug("%s: CPUFreq policy not found\n", __func__); + return -EPROBE_DEFER; + } + + /* Register cooling device */ + data->cool_dev = cpufreq_cooling_register(data->policy); + if (IS_ERR(data->cool_dev)) { + int ret = PTR_ERR(data->cool_dev); + dev_err(bgp->dev, "Failed to register cpu cooling device %d\n", + ret); + cpufreq_cpu_put(data->policy); + + return ret; + } + ti_bandgap_set_sensor_data(bgp, id, data); + + return 0; +} + +int ti_thermal_unregister_cpu_cooling(struct ti_bandgap *bgp, int id) +{ + struct ti_thermal_data *data; + + data = ti_bandgap_get_sensor_data(bgp, id); + + if (!IS_ERR_OR_NULL(data)) { + cpufreq_cooling_unregister(data->cool_dev); + if (data->policy) + cpufreq_cpu_put(data->policy); + } + + return 0; +} diff --git a/drivers/thermal/ti-soc-thermal/ti-thermal.h b/drivers/thermal/ti-soc-thermal/ti-thermal.h new file mode 100644 index 000000000..8e85ca973 --- /dev/null +++ b/drivers/thermal/ti-soc-thermal/ti-thermal.h @@ -0,0 +1,107 @@ +/* + * OMAP thermal definitions + * + * Copyright (C) 2012 Texas Instruments Incorporated - http://www.ti.com/ + * Contact: + * Eduardo Valentin <eduardo.valentin@ti.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ +#ifndef __TI_THERMAL_H +#define __TI_THERMAL_H + +#include "ti-bandgap.h" + +/* PCB sensor calculation constants */ +#define OMAP_GRADIENT_SLOPE_W_PCB_4430 0 +#define OMAP_GRADIENT_CONST_W_PCB_4430 20000 +#define OMAP_GRADIENT_SLOPE_W_PCB_4460 1142 +#define OMAP_GRADIENT_CONST_W_PCB_4460 -393 +#define OMAP_GRADIENT_SLOPE_W_PCB_4470 1063 +#define OMAP_GRADIENT_CONST_W_PCB_4470 -477 + +#define OMAP_GRADIENT_SLOPE_W_PCB_5430_CPU 100 +#define OMAP_GRADIENT_CONST_W_PCB_5430_CPU 484 +#define OMAP_GRADIENT_SLOPE_W_PCB_5430_GPU 464 +#define OMAP_GRADIENT_CONST_W_PCB_5430_GPU -5102 + +#define DRA752_GRADIENT_SLOPE_W_PCB 0 +#define DRA752_GRADIENT_CONST_W_PCB 2000 + +/* trip points of interest in milicelsius (at hotspot level) */ +#define OMAP_TRIP_COLD 100000 +#define OMAP_TRIP_HOT 110000 +#define OMAP_TRIP_SHUTDOWN 125000 +#define OMAP_TRIP_NUMBER 2 +#define OMAP_TRIP_STEP \ + ((OMAP_TRIP_SHUTDOWN - OMAP_TRIP_HOT) / (OMAP_TRIP_NUMBER - 1)) + +/* Update rates */ +#define FAST_TEMP_MONITORING_RATE 250 + +/* helper macros */ +/** + * ti_thermal_get_trip_value - returns trip temperature based on index + * @i: trip index + */ +#define ti_thermal_get_trip_value(i) \ + (OMAP_TRIP_HOT + ((i) * OMAP_TRIP_STEP)) + +/** + * ti_thermal_is_valid_trip - check for trip index + * @i: trip index + */ +#define ti_thermal_is_valid_trip(trip) \ + ((trip) >= 0 && (trip) < OMAP_TRIP_NUMBER) + +#ifdef CONFIG_TI_THERMAL +int ti_thermal_expose_sensor(struct ti_bandgap *bgp, int id, char *domain); +int ti_thermal_remove_sensor(struct ti_bandgap *bgp, int id); +int ti_thermal_report_sensor_temperature(struct ti_bandgap *bgp, int id); +int ti_thermal_register_cpu_cooling(struct ti_bandgap *bgp, int id); +int ti_thermal_unregister_cpu_cooling(struct ti_bandgap *bgp, int id); +#else +static inline +int ti_thermal_expose_sensor(struct ti_bandgap *bgp, int id, char *domain) +{ + return 0; +} + +static inline +int ti_thermal_remove_sensor(struct ti_bandgap *bgp, int id) +{ + return 0; +} + +static inline +int ti_thermal_report_sensor_temperature(struct ti_bandgap *bgp, int id) +{ + return 0; +} + +static inline +int ti_thermal_register_cpu_cooling(struct ti_bandgap *bgp, int id) +{ + return 0; +} + +static inline +int ti_thermal_unregister_cpu_cooling(struct ti_bandgap *bgp, int id) +{ + return 0; +} +#endif +#endif diff --git a/drivers/thermal/uniphier_thermal.c b/drivers/thermal/uniphier_thermal.c new file mode 100644 index 000000000..55477d74d --- /dev/null +++ b/drivers/thermal/uniphier_thermal.c @@ -0,0 +1,388 @@ +/** + * uniphier_thermal.c - Socionext UniPhier thermal driver + * + * Copyright 2014 Panasonic Corporation + * Copyright 2016-2017 Socionext Inc. + * All rights reserved. + * + * Author: + * Kunihiko Hayashi <hayashi.kunihiko@socionext.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 of + * the License as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/bitops.h> +#include <linux/interrupt.h> +#include <linux/mfd/syscon.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/thermal.h> + +#include "thermal_core.h" + +/* + * block registers + * addresses are the offset from .block_base + */ +#define PVTCTLEN 0x0000 +#define PVTCTLEN_EN BIT(0) + +#define PVTCTLMODE 0x0004 +#define PVTCTLMODE_MASK 0xf +#define PVTCTLMODE_TEMPMON 0x5 + +#define EMONREPEAT 0x0040 +#define EMONREPEAT_ENDLESS BIT(24) +#define EMONREPEAT_PERIOD GENMASK(3, 0) +#define EMONREPEAT_PERIOD_1000000 0x9 + +/* + * common registers + * addresses are the offset from .map_base + */ +#define PVTCTLSEL 0x0900 +#define PVTCTLSEL_MASK GENMASK(2, 0) +#define PVTCTLSEL_MONITOR 0 + +#define SETALERT0 0x0910 +#define SETALERT1 0x0914 +#define SETALERT2 0x0918 +#define SETALERT_TEMP_OVF (GENMASK(7, 0) << 16) +#define SETALERT_TEMP_OVF_VALUE(val) (((val) & GENMASK(7, 0)) << 16) +#define SETALERT_EN BIT(0) + +#define PMALERTINTCTL 0x0920 +#define PMALERTINTCTL_CLR(ch) BIT(4 * (ch) + 2) +#define PMALERTINTCTL_SET(ch) BIT(4 * (ch) + 1) +#define PMALERTINTCTL_EN(ch) BIT(4 * (ch) + 0) +#define PMALERTINTCTL_MASK (GENMASK(10, 8) | GENMASK(6, 4) | \ + GENMASK(2, 0)) + +#define TMOD 0x0928 +#define TMOD_WIDTH 9 + +#define TMODCOEF 0x0e5c + +#define TMODSETUP0_EN BIT(30) +#define TMODSETUP0_VAL(val) (((val) & GENMASK(13, 0)) << 16) +#define TMODSETUP1_EN BIT(15) +#define TMODSETUP1_VAL(val) ((val) & GENMASK(14, 0)) + +/* SoC critical temperature */ +#define CRITICAL_TEMP_LIMIT (120 * 1000) + +/* Max # of alert channels */ +#define ALERT_CH_NUM 3 + +/* SoC specific thermal sensor data */ +struct uniphier_tm_soc_data { + u32 map_base; + u32 block_base; + u32 tmod_setup_addr; +}; + +struct uniphier_tm_dev { + struct regmap *regmap; + struct device *dev; + bool alert_en[ALERT_CH_NUM]; + struct thermal_zone_device *tz_dev; + const struct uniphier_tm_soc_data *data; +}; + +static int uniphier_tm_initialize_sensor(struct uniphier_tm_dev *tdev) +{ + struct regmap *map = tdev->regmap; + u32 val; + u32 tmod_calib[2]; + int ret; + + /* stop PVT */ + regmap_write_bits(map, tdev->data->block_base + PVTCTLEN, + PVTCTLEN_EN, 0); + + /* + * Since SoC has a calibrated value that was set in advance, + * TMODCOEF shows non-zero and PVT refers the value internally. + * + * If TMODCOEF shows zero, the boards don't have the calibrated + * value, and the driver has to set default value from DT. + */ + ret = regmap_read(map, tdev->data->map_base + TMODCOEF, &val); + if (ret) + return ret; + if (!val) { + /* look for the default values in DT */ + ret = of_property_read_u32_array(tdev->dev->of_node, + "socionext,tmod-calibration", + tmod_calib, + ARRAY_SIZE(tmod_calib)); + if (ret) + return ret; + + regmap_write(map, tdev->data->tmod_setup_addr, + TMODSETUP0_EN | TMODSETUP0_VAL(tmod_calib[0]) | + TMODSETUP1_EN | TMODSETUP1_VAL(tmod_calib[1])); + } + + /* select temperature mode */ + regmap_write_bits(map, tdev->data->block_base + PVTCTLMODE, + PVTCTLMODE_MASK, PVTCTLMODE_TEMPMON); + + /* set monitoring period */ + regmap_write_bits(map, tdev->data->block_base + EMONREPEAT, + EMONREPEAT_ENDLESS | EMONREPEAT_PERIOD, + EMONREPEAT_ENDLESS | EMONREPEAT_PERIOD_1000000); + + /* set monitor mode */ + regmap_write_bits(map, tdev->data->map_base + PVTCTLSEL, + PVTCTLSEL_MASK, PVTCTLSEL_MONITOR); + + return 0; +} + +static void uniphier_tm_set_alert(struct uniphier_tm_dev *tdev, u32 ch, + u32 temp) +{ + struct regmap *map = tdev->regmap; + + /* set alert temperature */ + regmap_write_bits(map, tdev->data->map_base + SETALERT0 + (ch << 2), + SETALERT_EN | SETALERT_TEMP_OVF, + SETALERT_EN | + SETALERT_TEMP_OVF_VALUE(temp / 1000)); +} + +static void uniphier_tm_enable_sensor(struct uniphier_tm_dev *tdev) +{ + struct regmap *map = tdev->regmap; + int i; + u32 bits = 0; + + for (i = 0; i < ALERT_CH_NUM; i++) + if (tdev->alert_en[i]) + bits |= PMALERTINTCTL_EN(i); + + /* enable alert interrupt */ + regmap_write_bits(map, tdev->data->map_base + PMALERTINTCTL, + PMALERTINTCTL_MASK, bits); + + /* start PVT */ + regmap_write_bits(map, tdev->data->block_base + PVTCTLEN, + PVTCTLEN_EN, PVTCTLEN_EN); + + usleep_range(700, 1500); /* The spec note says at least 700us */ +} + +static void uniphier_tm_disable_sensor(struct uniphier_tm_dev *tdev) +{ + struct regmap *map = tdev->regmap; + + /* disable alert interrupt */ + regmap_write_bits(map, tdev->data->map_base + PMALERTINTCTL, + PMALERTINTCTL_MASK, 0); + + /* stop PVT */ + regmap_write_bits(map, tdev->data->block_base + PVTCTLEN, + PVTCTLEN_EN, 0); + + usleep_range(1000, 2000); /* The spec note says at least 1ms */ +} + +static int uniphier_tm_get_temp(void *data, int *out_temp) +{ + struct uniphier_tm_dev *tdev = data; + struct regmap *map = tdev->regmap; + int ret; + u32 temp; + + ret = regmap_read(map, tdev->data->map_base + TMOD, &temp); + if (ret) + return ret; + + /* MSB of the TMOD field is a sign bit */ + *out_temp = sign_extend32(temp, TMOD_WIDTH - 1) * 1000; + + return 0; +} + +static const struct thermal_zone_of_device_ops uniphier_of_thermal_ops = { + .get_temp = uniphier_tm_get_temp, +}; + +static void uniphier_tm_irq_clear(struct uniphier_tm_dev *tdev) +{ + u32 mask = 0, bits = 0; + int i; + + for (i = 0; i < ALERT_CH_NUM; i++) { + mask |= (PMALERTINTCTL_CLR(i) | PMALERTINTCTL_SET(i)); + bits |= PMALERTINTCTL_CLR(i); + } + + /* clear alert interrupt */ + regmap_write_bits(tdev->regmap, + tdev->data->map_base + PMALERTINTCTL, mask, bits); +} + +static irqreturn_t uniphier_tm_alarm_irq(int irq, void *_tdev) +{ + struct uniphier_tm_dev *tdev = _tdev; + + disable_irq_nosync(irq); + uniphier_tm_irq_clear(tdev); + + return IRQ_WAKE_THREAD; +} + +static irqreturn_t uniphier_tm_alarm_irq_thread(int irq, void *_tdev) +{ + struct uniphier_tm_dev *tdev = _tdev; + + thermal_zone_device_update(tdev->tz_dev, THERMAL_EVENT_UNSPECIFIED); + + return IRQ_HANDLED; +} + +static int uniphier_tm_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct regmap *regmap; + struct device_node *parent; + struct uniphier_tm_dev *tdev; + const struct thermal_trip *trips; + int i, ret, irq, ntrips, crit_temp = INT_MAX; + + tdev = devm_kzalloc(dev, sizeof(*tdev), GFP_KERNEL); + if (!tdev) + return -ENOMEM; + tdev->dev = dev; + + tdev->data = of_device_get_match_data(dev); + if (WARN_ON(!tdev->data)) + return -EINVAL; + + irq = platform_get_irq(pdev, 0); + if (irq < 0) + return irq; + + /* get regmap from syscon node */ + parent = of_get_parent(dev->of_node); /* parent should be syscon node */ + regmap = syscon_node_to_regmap(parent); + of_node_put(parent); + if (IS_ERR(regmap)) { + dev_err(dev, "failed to get regmap (error %ld)\n", + PTR_ERR(regmap)); + return PTR_ERR(regmap); + } + tdev->regmap = regmap; + + ret = uniphier_tm_initialize_sensor(tdev); + if (ret) { + dev_err(dev, "failed to initialize sensor\n"); + return ret; + } + + ret = devm_request_threaded_irq(dev, irq, uniphier_tm_alarm_irq, + uniphier_tm_alarm_irq_thread, + 0, "thermal", tdev); + if (ret) + return ret; + + platform_set_drvdata(pdev, tdev); + + tdev->tz_dev = devm_thermal_zone_of_sensor_register(dev, 0, tdev, + &uniphier_of_thermal_ops); + if (IS_ERR(tdev->tz_dev)) { + dev_err(dev, "failed to register sensor device\n"); + return PTR_ERR(tdev->tz_dev); + } + + /* get trip points */ + trips = of_thermal_get_trip_points(tdev->tz_dev); + ntrips = of_thermal_get_ntrips(tdev->tz_dev); + if (ntrips > ALERT_CH_NUM) { + dev_err(dev, "thermal zone has too many trips\n"); + return -E2BIG; + } + + /* set alert temperatures */ + for (i = 0; i < ntrips; i++) { + if (trips[i].type == THERMAL_TRIP_CRITICAL && + trips[i].temperature < crit_temp) + crit_temp = trips[i].temperature; + uniphier_tm_set_alert(tdev, i, trips[i].temperature); + tdev->alert_en[i] = true; + } + if (crit_temp > CRITICAL_TEMP_LIMIT) { + dev_err(dev, "critical trip is over limit(>%d), or not set\n", + CRITICAL_TEMP_LIMIT); + return -EINVAL; + } + + uniphier_tm_enable_sensor(tdev); + + return 0; +} + +static int uniphier_tm_remove(struct platform_device *pdev) +{ + struct uniphier_tm_dev *tdev = platform_get_drvdata(pdev); + + /* disable sensor */ + uniphier_tm_disable_sensor(tdev); + + return 0; +} + +static const struct uniphier_tm_soc_data uniphier_pxs2_tm_data = { + .map_base = 0xe000, + .block_base = 0xe000, + .tmod_setup_addr = 0xe904, +}; + +static const struct uniphier_tm_soc_data uniphier_ld20_tm_data = { + .map_base = 0xe000, + .block_base = 0xe800, + .tmod_setup_addr = 0xe938, +}; + +static const struct of_device_id uniphier_tm_dt_ids[] = { + { + .compatible = "socionext,uniphier-pxs2-thermal", + .data = &uniphier_pxs2_tm_data, + }, + { + .compatible = "socionext,uniphier-ld20-thermal", + .data = &uniphier_ld20_tm_data, + }, + { + .compatible = "socionext,uniphier-pxs3-thermal", + .data = &uniphier_ld20_tm_data, + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, uniphier_tm_dt_ids); + +static struct platform_driver uniphier_tm_driver = { + .probe = uniphier_tm_probe, + .remove = uniphier_tm_remove, + .driver = { + .name = "uniphier-thermal", + .of_match_table = uniphier_tm_dt_ids, + }, +}; +module_platform_driver(uniphier_tm_driver); + +MODULE_AUTHOR("Kunihiko Hayashi <hayashi.kunihiko@socionext.com>"); +MODULE_DESCRIPTION("UniPhier thermal driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/user_space.c b/drivers/thermal/user_space.c new file mode 100644 index 000000000..8e92a06ef --- /dev/null +++ b/drivers/thermal/user_space.c @@ -0,0 +1,69 @@ +/* + * user_space.c - A simple user space Thermal events notifier + * + * Copyright (C) 2012 Intel Corp + * Copyright (C) 2012 Durgadoss R <durgadoss.r@intel.com> + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +#include <linux/thermal.h> +#include <linux/slab.h> + +#include "thermal_core.h" + +/** + * notify_user_space - Notifies user space about thermal events + * @tz - thermal_zone_device + * @trip - trip point index + * + * This function notifies the user space through UEvents. + */ +static int notify_user_space(struct thermal_zone_device *tz, int trip) +{ + char *thermal_prop[5]; + int i; + + mutex_lock(&tz->lock); + thermal_prop[0] = kasprintf(GFP_KERNEL, "NAME=%s", tz->type); + thermal_prop[1] = kasprintf(GFP_KERNEL, "TEMP=%d", tz->temperature); + thermal_prop[2] = kasprintf(GFP_KERNEL, "TRIP=%d", trip); + thermal_prop[3] = kasprintf(GFP_KERNEL, "EVENT=%d", tz->notify_event); + thermal_prop[4] = NULL; + kobject_uevent_env(&tz->device.kobj, KOBJ_CHANGE, thermal_prop); + for (i = 0; i < 4; ++i) + kfree(thermal_prop[i]); + mutex_unlock(&tz->lock); + return 0; +} + +static struct thermal_governor thermal_gov_user_space = { + .name = "user_space", + .throttle = notify_user_space, +}; + +int thermal_gov_user_space_register(void) +{ + return thermal_register_governor(&thermal_gov_user_space); +} + +void thermal_gov_user_space_unregister(void) +{ + thermal_unregister_governor(&thermal_gov_user_space); +} + diff --git a/drivers/thermal/x86_pkg_temp_thermal.c b/drivers/thermal/x86_pkg_temp_thermal.c new file mode 100644 index 000000000..1ef937d79 --- /dev/null +++ b/drivers/thermal/x86_pkg_temp_thermal.c @@ -0,0 +1,558 @@ +/* + * x86_pkg_temp_thermal driver + * Copyright (c) 2013, Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc. + * + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/param.h> +#include <linux/device.h> +#include <linux/platform_device.h> +#include <linux/cpu.h> +#include <linux/smp.h> +#include <linux/slab.h> +#include <linux/pm.h> +#include <linux/thermal.h> +#include <linux/debugfs.h> +#include <asm/cpu_device_id.h> +#include <asm/mce.h> + +/* +* Rate control delay: Idea is to introduce denounce effect +* This should be long enough to avoid reduce events, when +* threshold is set to a temperature, which is constantly +* violated, but at the short enough to take any action. +* The action can be remove threshold or change it to next +* interesting setting. Based on experiments, in around +* every 5 seconds under load will give us a significant +* temperature change. +*/ +#define PKG_TEMP_THERMAL_NOTIFY_DELAY 5000 +static int notify_delay_ms = PKG_TEMP_THERMAL_NOTIFY_DELAY; +module_param(notify_delay_ms, int, 0644); +MODULE_PARM_DESC(notify_delay_ms, + "User space notification delay in milli seconds."); + +/* Number of trip points in thermal zone. Currently it can't +* be more than 2. MSR can allow setting and getting notifications +* for only 2 thresholds. This define enforces this, if there +* is some wrong values returned by cpuid for number of thresholds. +*/ +#define MAX_NUMBER_OF_TRIPS 2 + +struct pkg_device { + int cpu; + bool work_scheduled; + u32 tj_max; + u32 msr_pkg_therm_low; + u32 msr_pkg_therm_high; + struct delayed_work work; + struct thermal_zone_device *tzone; + struct cpumask cpumask; +}; + +static struct thermal_zone_params pkg_temp_tz_params = { + .no_hwmon = true, +}; + +/* Keep track of how many package pointers we allocated in init() */ +static int max_packages __read_mostly; +/* Array of package pointers */ +static struct pkg_device **packages; +/* Serializes interrupt notification, work and hotplug */ +static DEFINE_SPINLOCK(pkg_temp_lock); +/* Protects zone operation in the work function against hotplug removal */ +static DEFINE_MUTEX(thermal_zone_mutex); + +/* The dynamically assigned cpu hotplug state for module_exit() */ +static enum cpuhp_state pkg_thermal_hp_state __read_mostly; + +/* Debug counters to show using debugfs */ +static struct dentry *debugfs; +static unsigned int pkg_interrupt_cnt; +static unsigned int pkg_work_cnt; + +static int pkg_temp_debugfs_init(void) +{ + struct dentry *d; + + debugfs = debugfs_create_dir("pkg_temp_thermal", NULL); + if (!debugfs) + return -ENOENT; + + d = debugfs_create_u32("pkg_thres_interrupt", S_IRUGO, debugfs, + &pkg_interrupt_cnt); + if (!d) + goto err_out; + + d = debugfs_create_u32("pkg_thres_work", S_IRUGO, debugfs, + &pkg_work_cnt); + if (!d) + goto err_out; + + return 0; + +err_out: + debugfs_remove_recursive(debugfs); + return -ENOENT; +} + +/* + * Protection: + * + * - cpu hotplug: Read serialized by cpu hotplug lock + * Write must hold pkg_temp_lock + * + * - Other callsites: Must hold pkg_temp_lock + */ +static struct pkg_device *pkg_temp_thermal_get_dev(unsigned int cpu) +{ + int pkgid = topology_logical_package_id(cpu); + + if (pkgid >= 0 && pkgid < max_packages) + return packages[pkgid]; + return NULL; +} + +/* +* tj-max is is interesting because threshold is set relative to this +* temperature. +*/ +static int get_tj_max(int cpu, u32 *tj_max) +{ + u32 eax, edx, val; + int err; + + err = rdmsr_safe_on_cpu(cpu, MSR_IA32_TEMPERATURE_TARGET, &eax, &edx); + if (err) + return err; + + val = (eax >> 16) & 0xff; + *tj_max = val * 1000; + + return val ? 0 : -EINVAL; +} + +static int sys_get_curr_temp(struct thermal_zone_device *tzd, int *temp) +{ + struct pkg_device *pkgdev = tzd->devdata; + u32 eax, edx; + + rdmsr_on_cpu(pkgdev->cpu, MSR_IA32_PACKAGE_THERM_STATUS, &eax, &edx); + if (eax & 0x80000000) { + *temp = pkgdev->tj_max - ((eax >> 16) & 0x7f) * 1000; + pr_debug("sys_get_curr_temp %d\n", *temp); + return 0; + } + return -EINVAL; +} + +static int sys_get_trip_temp(struct thermal_zone_device *tzd, + int trip, int *temp) +{ + struct pkg_device *pkgdev = tzd->devdata; + unsigned long thres_reg_value; + u32 mask, shift, eax, edx; + int ret; + + if (trip >= MAX_NUMBER_OF_TRIPS) + return -EINVAL; + + if (trip) { + mask = THERM_MASK_THRESHOLD1; + shift = THERM_SHIFT_THRESHOLD1; + } else { + mask = THERM_MASK_THRESHOLD0; + shift = THERM_SHIFT_THRESHOLD0; + } + + ret = rdmsr_on_cpu(pkgdev->cpu, MSR_IA32_PACKAGE_THERM_INTERRUPT, + &eax, &edx); + if (ret < 0) + return ret; + + thres_reg_value = (eax & mask) >> shift; + if (thres_reg_value) + *temp = pkgdev->tj_max - thres_reg_value * 1000; + else + *temp = 0; + pr_debug("sys_get_trip_temp %d\n", *temp); + + return 0; +} + +static int +sys_set_trip_temp(struct thermal_zone_device *tzd, int trip, int temp) +{ + struct pkg_device *pkgdev = tzd->devdata; + u32 l, h, mask, shift, intr; + int ret; + + if (trip >= MAX_NUMBER_OF_TRIPS || temp >= pkgdev->tj_max) + return -EINVAL; + + ret = rdmsr_on_cpu(pkgdev->cpu, MSR_IA32_PACKAGE_THERM_INTERRUPT, + &l, &h); + if (ret < 0) + return ret; + + if (trip) { + mask = THERM_MASK_THRESHOLD1; + shift = THERM_SHIFT_THRESHOLD1; + intr = THERM_INT_THRESHOLD1_ENABLE; + } else { + mask = THERM_MASK_THRESHOLD0; + shift = THERM_SHIFT_THRESHOLD0; + intr = THERM_INT_THRESHOLD0_ENABLE; + } + l &= ~mask; + /* + * When users space sets a trip temperature == 0, which is indication + * that, it is no longer interested in receiving notifications. + */ + if (!temp) { + l &= ~intr; + } else { + l |= (pkgdev->tj_max - temp)/1000 << shift; + l |= intr; + } + + return wrmsr_on_cpu(pkgdev->cpu, MSR_IA32_PACKAGE_THERM_INTERRUPT, l, h); +} + +static int sys_get_trip_type(struct thermal_zone_device *thermal, int trip, + enum thermal_trip_type *type) +{ + *type = THERMAL_TRIP_PASSIVE; + return 0; +} + +/* Thermal zone callback registry */ +static struct thermal_zone_device_ops tzone_ops = { + .get_temp = sys_get_curr_temp, + .get_trip_temp = sys_get_trip_temp, + .get_trip_type = sys_get_trip_type, + .set_trip_temp = sys_set_trip_temp, +}; + +static bool pkg_thermal_rate_control(void) +{ + return true; +} + +/* Enable threshold interrupt on local package/cpu */ +static inline void enable_pkg_thres_interrupt(void) +{ + u8 thres_0, thres_1; + u32 l, h; + + rdmsr(MSR_IA32_PACKAGE_THERM_INTERRUPT, l, h); + /* only enable/disable if it had valid threshold value */ + thres_0 = (l & THERM_MASK_THRESHOLD0) >> THERM_SHIFT_THRESHOLD0; + thres_1 = (l & THERM_MASK_THRESHOLD1) >> THERM_SHIFT_THRESHOLD1; + if (thres_0) + l |= THERM_INT_THRESHOLD0_ENABLE; + if (thres_1) + l |= THERM_INT_THRESHOLD1_ENABLE; + wrmsr(MSR_IA32_PACKAGE_THERM_INTERRUPT, l, h); +} + +/* Disable threshold interrupt on local package/cpu */ +static inline void disable_pkg_thres_interrupt(void) +{ + u32 l, h; + + rdmsr(MSR_IA32_PACKAGE_THERM_INTERRUPT, l, h); + + l &= ~(THERM_INT_THRESHOLD0_ENABLE | THERM_INT_THRESHOLD1_ENABLE); + wrmsr(MSR_IA32_PACKAGE_THERM_INTERRUPT, l, h); +} + +static void pkg_temp_thermal_threshold_work_fn(struct work_struct *work) +{ + struct thermal_zone_device *tzone = NULL; + int cpu = smp_processor_id(); + struct pkg_device *pkgdev; + u64 msr_val, wr_val; + + mutex_lock(&thermal_zone_mutex); + spin_lock_irq(&pkg_temp_lock); + ++pkg_work_cnt; + + pkgdev = pkg_temp_thermal_get_dev(cpu); + if (!pkgdev) { + spin_unlock_irq(&pkg_temp_lock); + mutex_unlock(&thermal_zone_mutex); + return; + } + pkgdev->work_scheduled = false; + + rdmsrl(MSR_IA32_PACKAGE_THERM_STATUS, msr_val); + wr_val = msr_val & ~(THERM_LOG_THRESHOLD0 | THERM_LOG_THRESHOLD1); + if (wr_val != msr_val) { + wrmsrl(MSR_IA32_PACKAGE_THERM_STATUS, wr_val); + tzone = pkgdev->tzone; + } + + enable_pkg_thres_interrupt(); + spin_unlock_irq(&pkg_temp_lock); + + /* + * If tzone is not NULL, then thermal_zone_mutex will prevent the + * concurrent removal in the cpu offline callback. + */ + if (tzone) + thermal_zone_device_update(tzone, THERMAL_EVENT_UNSPECIFIED); + + mutex_unlock(&thermal_zone_mutex); +} + +static void pkg_thermal_schedule_work(int cpu, struct delayed_work *work) +{ + unsigned long ms = msecs_to_jiffies(notify_delay_ms); + + schedule_delayed_work_on(cpu, work, ms); +} + +static int pkg_thermal_notify(u64 msr_val) +{ + int cpu = smp_processor_id(); + struct pkg_device *pkgdev; + unsigned long flags; + + spin_lock_irqsave(&pkg_temp_lock, flags); + ++pkg_interrupt_cnt; + + disable_pkg_thres_interrupt(); + + /* Work is per package, so scheduling it once is enough. */ + pkgdev = pkg_temp_thermal_get_dev(cpu); + if (pkgdev && !pkgdev->work_scheduled) { + pkgdev->work_scheduled = true; + pkg_thermal_schedule_work(pkgdev->cpu, &pkgdev->work); + } + + spin_unlock_irqrestore(&pkg_temp_lock, flags); + return 0; +} + +static int pkg_temp_thermal_device_add(unsigned int cpu) +{ + int pkgid = topology_logical_package_id(cpu); + u32 tj_max, eax, ebx, ecx, edx; + struct pkg_device *pkgdev; + int thres_count, err; + + if (pkgid >= max_packages) + return -ENOMEM; + + cpuid(6, &eax, &ebx, &ecx, &edx); + thres_count = ebx & 0x07; + if (!thres_count) + return -ENODEV; + + thres_count = clamp_val(thres_count, 0, MAX_NUMBER_OF_TRIPS); + + err = get_tj_max(cpu, &tj_max); + if (err) + return err; + + pkgdev = kzalloc(sizeof(*pkgdev), GFP_KERNEL); + if (!pkgdev) + return -ENOMEM; + + INIT_DELAYED_WORK(&pkgdev->work, pkg_temp_thermal_threshold_work_fn); + pkgdev->cpu = cpu; + pkgdev->tj_max = tj_max; + pkgdev->tzone = thermal_zone_device_register("x86_pkg_temp", + thres_count, + (thres_count == MAX_NUMBER_OF_TRIPS) ? 0x03 : 0x01, + pkgdev, &tzone_ops, &pkg_temp_tz_params, 0, 0); + if (IS_ERR(pkgdev->tzone)) { + err = PTR_ERR(pkgdev->tzone); + kfree(pkgdev); + return err; + } + /* Store MSR value for package thermal interrupt, to restore at exit */ + rdmsr(MSR_IA32_PACKAGE_THERM_INTERRUPT, pkgdev->msr_pkg_therm_low, + pkgdev->msr_pkg_therm_high); + + cpumask_set_cpu(cpu, &pkgdev->cpumask); + spin_lock_irq(&pkg_temp_lock); + packages[pkgid] = pkgdev; + spin_unlock_irq(&pkg_temp_lock); + return 0; +} + +static int pkg_thermal_cpu_offline(unsigned int cpu) +{ + struct pkg_device *pkgdev = pkg_temp_thermal_get_dev(cpu); + bool lastcpu, was_target; + int target; + + if (!pkgdev) + return 0; + + target = cpumask_any_but(&pkgdev->cpumask, cpu); + cpumask_clear_cpu(cpu, &pkgdev->cpumask); + lastcpu = target >= nr_cpu_ids; + /* + * Remove the sysfs files, if this is the last cpu in the package + * before doing further cleanups. + */ + if (lastcpu) { + struct thermal_zone_device *tzone = pkgdev->tzone; + + /* + * We must protect against a work function calling + * thermal_zone_update, after/while unregister. We null out + * the pointer under the zone mutex, so the worker function + * won't try to call. + */ + mutex_lock(&thermal_zone_mutex); + pkgdev->tzone = NULL; + mutex_unlock(&thermal_zone_mutex); + + thermal_zone_device_unregister(tzone); + } + + /* Protect against work and interrupts */ + spin_lock_irq(&pkg_temp_lock); + + /* + * Check whether this cpu was the current target and store the new + * one. When we drop the lock, then the interrupt notify function + * will see the new target. + */ + was_target = pkgdev->cpu == cpu; + pkgdev->cpu = target; + + /* + * If this is the last CPU in the package remove the package + * reference from the array and restore the interrupt MSR. When we + * drop the lock neither the interrupt notify function nor the + * worker will see the package anymore. + */ + if (lastcpu) { + packages[topology_logical_package_id(cpu)] = NULL; + /* After this point nothing touches the MSR anymore. */ + wrmsr(MSR_IA32_PACKAGE_THERM_INTERRUPT, + pkgdev->msr_pkg_therm_low, pkgdev->msr_pkg_therm_high); + } + + /* + * Check whether there is work scheduled and whether the work is + * targeted at the outgoing CPU. + */ + if (pkgdev->work_scheduled && was_target) { + /* + * To cancel the work we need to drop the lock, otherwise + * we might deadlock if the work needs to be flushed. + */ + spin_unlock_irq(&pkg_temp_lock); + cancel_delayed_work_sync(&pkgdev->work); + spin_lock_irq(&pkg_temp_lock); + /* + * If this is not the last cpu in the package and the work + * did not run after we dropped the lock above, then we + * need to reschedule the work, otherwise the interrupt + * stays disabled forever. + */ + if (!lastcpu && pkgdev->work_scheduled) + pkg_thermal_schedule_work(target, &pkgdev->work); + } + + spin_unlock_irq(&pkg_temp_lock); + + /* Final cleanup if this is the last cpu */ + if (lastcpu) + kfree(pkgdev); + return 0; +} + +static int pkg_thermal_cpu_online(unsigned int cpu) +{ + struct pkg_device *pkgdev = pkg_temp_thermal_get_dev(cpu); + struct cpuinfo_x86 *c = &cpu_data(cpu); + + /* Paranoia check */ + if (!cpu_has(c, X86_FEATURE_DTHERM) || !cpu_has(c, X86_FEATURE_PTS)) + return -ENODEV; + + /* If the package exists, nothing to do */ + if (pkgdev) { + cpumask_set_cpu(cpu, &pkgdev->cpumask); + return 0; + } + return pkg_temp_thermal_device_add(cpu); +} + +static const struct x86_cpu_id __initconst pkg_temp_thermal_ids[] = { + { X86_VENDOR_INTEL, X86_FAMILY_ANY, X86_MODEL_ANY, X86_FEATURE_PTS }, + {} +}; +MODULE_DEVICE_TABLE(x86cpu, pkg_temp_thermal_ids); + +static int __init pkg_temp_thermal_init(void) +{ + int ret; + + if (!x86_match_cpu(pkg_temp_thermal_ids)) + return -ENODEV; + + max_packages = topology_max_packages(); + packages = kcalloc(max_packages, sizeof(struct pkg_device *), + GFP_KERNEL); + if (!packages) + return -ENOMEM; + + ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "thermal/x86_pkg:online", + pkg_thermal_cpu_online, pkg_thermal_cpu_offline); + if (ret < 0) + goto err; + + /* Store the state for module exit */ + pkg_thermal_hp_state = ret; + + platform_thermal_package_notify = pkg_thermal_notify; + platform_thermal_package_rate_control = pkg_thermal_rate_control; + + /* Don't care if it fails */ + pkg_temp_debugfs_init(); + return 0; + +err: + kfree(packages); + return ret; +} +module_init(pkg_temp_thermal_init) + +static void __exit pkg_temp_thermal_exit(void) +{ + platform_thermal_package_notify = NULL; + platform_thermal_package_rate_control = NULL; + + cpuhp_remove_state(pkg_thermal_hp_state); + debugfs_remove_recursive(debugfs); + kfree(packages); +} +module_exit(pkg_temp_thermal_exit) + +MODULE_DESCRIPTION("X86 PKG TEMP Thermal Driver"); +MODULE_AUTHOR("Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/thermal/zx2967_thermal.c b/drivers/thermal/zx2967_thermal.c new file mode 100644 index 000000000..6acce0bce --- /dev/null +++ b/drivers/thermal/zx2967_thermal.c @@ -0,0 +1,258 @@ +/* + * ZTE's zx2967 family thermal sensor driver + * + * Copyright (C) 2017 ZTE Ltd. + * + * Author: Baoyou Xie <baoyou.xie@linaro.org> + * + * License terms: GNU General Public License (GPL) version 2 + */ + +#include <linux/clk.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/iopoll.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/thermal.h> + +/* Power Mode: 0->low 1->high */ +#define ZX2967_THERMAL_POWER_MODE 0 +#define ZX2967_POWER_MODE_LOW 0 +#define ZX2967_POWER_MODE_HIGH 1 + +/* DCF Control Register */ +#define ZX2967_THERMAL_DCF 0x4 +#define ZX2967_DCF_EN BIT(1) +#define ZX2967_DCF_FREEZE BIT(0) + +/* Selection Register */ +#define ZX2967_THERMAL_SEL 0x8 + +/* Control Register */ +#define ZX2967_THERMAL_CTRL 0x10 + +#define ZX2967_THERMAL_READY BIT(12) +#define ZX2967_THERMAL_TEMP_MASK GENMASK(11, 0) +#define ZX2967_THERMAL_ID_MASK 0x18 +#define ZX2967_THERMAL_ID 0x10 + +#define ZX2967_GET_TEMP_TIMEOUT_US (100 * 1024) + +/** + * struct zx2967_thermal_priv - zx2967 thermal sensor private structure + * @tzd: struct thermal_zone_device where the sensor is registered + * @lock: prevents read sensor in parallel + * @clk_topcrm: topcrm clk structure + * @clk_apb: apb clk structure + * @regs: pointer to base address of the thermal sensor + */ + +struct zx2967_thermal_priv { + struct thermal_zone_device *tzd; + struct mutex lock; + struct clk *clk_topcrm; + struct clk *clk_apb; + void __iomem *regs; + struct device *dev; +}; + +static int zx2967_thermal_get_temp(void *data, int *temp) +{ + void __iomem *regs; + struct zx2967_thermal_priv *priv = data; + u32 val; + int ret; + + if (!priv->tzd) + return -EAGAIN; + + regs = priv->regs; + mutex_lock(&priv->lock); + writel_relaxed(ZX2967_POWER_MODE_LOW, + regs + ZX2967_THERMAL_POWER_MODE); + writel_relaxed(ZX2967_DCF_EN, regs + ZX2967_THERMAL_DCF); + + val = readl_relaxed(regs + ZX2967_THERMAL_SEL); + val &= ~ZX2967_THERMAL_ID_MASK; + val |= ZX2967_THERMAL_ID; + writel_relaxed(val, regs + ZX2967_THERMAL_SEL); + + /* + * Must wait for a while, surely it's a bit odd. + * otherwise temperature value we got has a few deviation, even if + * the THERMAL_READY bit is set. + */ + usleep_range(100, 300); + ret = readx_poll_timeout(readl, regs + ZX2967_THERMAL_CTRL, + val, val & ZX2967_THERMAL_READY, 300, + ZX2967_GET_TEMP_TIMEOUT_US); + if (ret) { + dev_err(priv->dev, "Thermal sensor data timeout\n"); + goto unlock; + } + + writel_relaxed(ZX2967_DCF_FREEZE | ZX2967_DCF_EN, + regs + ZX2967_THERMAL_DCF); + val = readl_relaxed(regs + ZX2967_THERMAL_CTRL) + & ZX2967_THERMAL_TEMP_MASK; + writel_relaxed(ZX2967_POWER_MODE_HIGH, + regs + ZX2967_THERMAL_POWER_MODE); + + /* + * Calculate temperature + * In dts, slope is multiplied by 1000. + */ + *temp = DIV_ROUND_CLOSEST(((s32)val + priv->tzd->tzp->offset) * 1000, + priv->tzd->tzp->slope); + +unlock: + mutex_unlock(&priv->lock); + return ret; +} + +static const struct thermal_zone_of_device_ops zx2967_of_thermal_ops = { + .get_temp = zx2967_thermal_get_temp, +}; + +static int zx2967_thermal_probe(struct platform_device *pdev) +{ + struct zx2967_thermal_priv *priv; + struct resource *res; + int ret; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + priv->regs = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(priv->regs)) + return PTR_ERR(priv->regs); + + priv->clk_topcrm = devm_clk_get(&pdev->dev, "topcrm"); + if (IS_ERR(priv->clk_topcrm)) { + ret = PTR_ERR(priv->clk_topcrm); + dev_err(&pdev->dev, "failed to get topcrm clock: %d\n", ret); + return ret; + } + + ret = clk_prepare_enable(priv->clk_topcrm); + if (ret) { + dev_err(&pdev->dev, "failed to enable topcrm clock: %d\n", + ret); + return ret; + } + + priv->clk_apb = devm_clk_get(&pdev->dev, "apb"); + if (IS_ERR(priv->clk_apb)) { + ret = PTR_ERR(priv->clk_apb); + dev_err(&pdev->dev, "failed to get apb clock: %d\n", ret); + goto disable_clk_topcrm; + } + + ret = clk_prepare_enable(priv->clk_apb); + if (ret) { + dev_err(&pdev->dev, "failed to enable apb clock: %d\n", + ret); + goto disable_clk_topcrm; + } + + mutex_init(&priv->lock); + priv->tzd = thermal_zone_of_sensor_register(&pdev->dev, + 0, priv, &zx2967_of_thermal_ops); + + if (IS_ERR(priv->tzd)) { + ret = PTR_ERR(priv->tzd); + dev_err(&pdev->dev, "failed to register sensor: %d\n", ret); + goto disable_clk_all; + } + + if (priv->tzd->tzp->slope == 0) { + thermal_zone_of_sensor_unregister(&pdev->dev, priv->tzd); + dev_err(&pdev->dev, "coefficients of sensor is invalid\n"); + ret = -EINVAL; + goto disable_clk_all; + } + + priv->dev = &pdev->dev; + platform_set_drvdata(pdev, priv); + + return 0; + +disable_clk_all: + clk_disable_unprepare(priv->clk_apb); +disable_clk_topcrm: + clk_disable_unprepare(priv->clk_topcrm); + return ret; +} + +static int zx2967_thermal_exit(struct platform_device *pdev) +{ + struct zx2967_thermal_priv *priv = platform_get_drvdata(pdev); + + thermal_zone_of_sensor_unregister(&pdev->dev, priv->tzd); + clk_disable_unprepare(priv->clk_topcrm); + clk_disable_unprepare(priv->clk_apb); + + return 0; +} + +static const struct of_device_id zx2967_thermal_id_table[] = { + { .compatible = "zte,zx296718-thermal" }, + {} +}; +MODULE_DEVICE_TABLE(of, zx2967_thermal_id_table); + +#ifdef CONFIG_PM_SLEEP +static int zx2967_thermal_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct zx2967_thermal_priv *priv = platform_get_drvdata(pdev); + + if (priv && priv->clk_topcrm) + clk_disable_unprepare(priv->clk_topcrm); + + if (priv && priv->clk_apb) + clk_disable_unprepare(priv->clk_apb); + + return 0; +} + +static int zx2967_thermal_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + struct zx2967_thermal_priv *priv = platform_get_drvdata(pdev); + int error; + + error = clk_prepare_enable(priv->clk_topcrm); + if (error) + return error; + + error = clk_prepare_enable(priv->clk_apb); + if (error) { + clk_disable_unprepare(priv->clk_topcrm); + return error; + } + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(zx2967_thermal_pm_ops, + zx2967_thermal_suspend, zx2967_thermal_resume); + +static struct platform_driver zx2967_thermal_driver = { + .probe = zx2967_thermal_probe, + .remove = zx2967_thermal_exit, + .driver = { + .name = "zx2967_thermal", + .of_match_table = zx2967_thermal_id_table, + .pm = &zx2967_thermal_pm_ops, + }, +}; +module_platform_driver(zx2967_thermal_driver); + +MODULE_AUTHOR("Baoyou Xie <baoyou.xie@linaro.org>"); +MODULE_DESCRIPTION("ZTE zx2967 thermal driver"); +MODULE_LICENSE("GPL v2"); |