From c6a2866482e3ecef1a420d1605bcbb58e0b3a40e Mon Sep 17 00:00:00 2001 From: Philipp Tomsich Date: Mon, 5 Sep 2016 15:19:20 +0200 Subject: ARM: sun9i: Add thermal sensor controller The thermal sensor controller (TSC) on the Allwiner A80 is colocated with the GPADC block (i.e. it shares a clock) and controls 4 on-chip thermals sensors. The DTS entry for the TSC provides human-readable names for each of the sensor points and models the conversion function from raw values to temperature readings (in Celsius). Signed-off-by: Philipp Tomsich --- arch/arm/boot/dts/sun9i-a80.dtsi | 26 +++ drivers/thermal/Kconfig | 12 + drivers/thermal/Makefile | 1 + drivers/thermal/sun9i_thermal.c | 489 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 528 insertions(+) create mode 100644 drivers/thermal/sun9i_thermal.c diff --git a/arch/arm/boot/dts/sun9i-a80.dtsi b/arch/arm/boot/dts/sun9i-a80.dtsi index 50a318260e5a..b86ac490a9ad 100644 --- a/arch/arm/boot/dts/sun9i-a80.dtsi +++ b/arch/arm/boot/dts/sun9i-a80.dtsi @@ -47,6 +47,7 @@ #include "skeleton64.dtsi" #include +#include #include @@ -388,6 +389,14 @@ "mmc3_sample"; }; + gpadc_clk: clk@0600050c { + #clock-cells = <1>; + compatible = "allwinner,sun9i-a80-mod0-clk"; + reg = <0x0600050c 0x4>; + clocks = <&osc24M>; + clock-output-names = "gpadc"; + }; + ahb0_gates: clk@06000580 { #clock-cells = <1>; compatible = "allwinner,sun9i-a80-ahb0-gates-clk"; @@ -1003,6 +1012,23 @@ }; }; + tsc: tsc@6004c40 { + #thermal-sensor-cells = <1>; + compatible = "allwinner,sun9i-tsc"; + reg = <0x06004c40 0x50>; + interrupts = ; + resets = <&apb0_resets 17>; + clocks = <&apb0_gates 17>, <&gpadc_clk 0>; + clock-names = "ahb", "mod"; + assigned-clocks = <&gpadc_clk 0>; + assigned-clock-rates = <1000000>; + + allwinner,sun9i-tsc-channels = <4>; + allwinner,sun9i-tsc-name = "c1cpux", "dramc", "gpu", "c0cpux"; + allwinner,sun9i-tsc-multiplier = <1000>; + allwinner,sun9i-tsc-offset = <(-2794000)>; + allwinner,sun9i-tsc-divisor = <(-14882)>; + }; }; uart0: serial@07000000 { diff --git a/drivers/thermal/Kconfig b/drivers/thermal/Kconfig index 2d702ca6556f..d449cf84ff85 100644 --- a/drivers/thermal/Kconfig +++ b/drivers/thermal/Kconfig @@ -204,6 +204,18 @@ config SPEAR_THERMAL Enable this to plug the SPEAr thermal sensor driver into the Linux thermal framework. +config SUN9I_THERMAL + tristate "SUN9I thermal sensor driver" + depends on MACH_SUN9I + depends on HAS_IOMEM + depends on OF + help + Enable this to support the thermal sensor controller found in the + sun9i (A80) and sun50i (A64). The specifics of each controller + (i.e. number of sensors and conversion function) can be configured + through the device-tree. Cpufreq is used as the cooling device and + will throttle CPUs when the temperature crosses the passive trip point. + config ROCKCHIP_THERMAL tristate "Rockchip thermal driver" depends on ARCH_ROCKCHIP || COMPILE_TEST diff --git a/drivers/thermal/Makefile b/drivers/thermal/Makefile index 10b07c14f8a9..d4dacef6cc33 100644 --- a/drivers/thermal/Makefile +++ b/drivers/thermal/Makefile @@ -51,3 +51,4 @@ obj-$(CONFIG_TEGRA_SOCTHERM) += 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_SUN9I_THERMAL) += sun9i_thermal.o \ No newline at end of file diff --git a/drivers/thermal/sun9i_thermal.c b/drivers/thermal/sun9i_thermal.c new file mode 100644 index 000000000000..82a577a22f30 --- /dev/null +++ b/drivers/thermal/sun9i_thermal.c @@ -0,0 +1,489 @@ +/* + * Copyright (c) 2016, Theobroma Systems Design und Consulting GmbH + * Dr. Philipp Tomsich + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +/** + * struct sun9i_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 sun9i_thermal_sensor { + struct sun9i_thermal_data *thermal; + struct thermal_zone_device *tzd; + const char* name; + int id; +}; + +/** + * struct sun9i_thermal_data - hold the private data of thermal driver + * @pdev: platform device of thermal + * @reset: the reset controller of tsadc + * @sensors[SOC_MAX_SENSORS]: the thermal sensor + * @mclk: the controller clock (GPADC) is divided down from the exteral 24MHz + * @hclk: the AHB bus clock (for register accesses) + * @grf: the general register file will be used to do static set by software + * @regs: the base address of tsadc controller + */ +struct sun9i_thermal_data { + struct platform_device *pdev; + struct reset_control *reset; + + struct sun9i_thermal_sensor *sensors; + u32 num_channels; + + struct clk *hclk; + struct clk *mclk; + + void __iomem *regs; + + /* + * Conversion parameters from the device-tree for the following + * conversion function: + * tCELSIUS = ((MULTIPLIER * data) + OFFSET) / DIVISOR + * + * These should be configured through the DTS keys: + * allwinner,sun9i-tsc-multiplier + * allwinner,sun9i-tsc-offset + * allwinner,sun9i-tsc-divisor + */ + s32 multiplier; + s32 offset; + s32 divisor; +}; + +static const struct of_device_id of_sun9i_thermal_match[] = { + { + .compatible = "allwinner,sun9i-tsc", + }, + { /* end */ }, +}; +MODULE_DEVICE_TABLE(of, of_sun9i_thermal_match); + +#if 0 +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); + + return IRQ_HANDLED; +} +#endif + +#define THSx_DATA(ch) (0x40 + (ch) * 0x4) + +static int sun9i_thermal_get_temp(void *_sensor, int *out_temp) +{ + struct sun9i_thermal_sensor *sensor = _sensor; + struct sun9i_thermal_data *thermal = sensor->thermal; + s32 val; + + val = readl_relaxed(thermal->regs + THSx_DATA(sensor->id)); + *out_temp = (thermal->multiplier * val + thermal->offset) / (thermal->divisor); + dev_dbg(&thermal->pdev->dev, "sensor %d - temp %d (raw 0x%x)\n", + sensor->id, *out_temp, val); + + return 0; +} + +static const struct thermal_zone_of_device_ops sun9i_of_thermal_ops = { + .get_temp = sun9i_thermal_get_temp, +}; + +static int sun9i_configure_from_dt(struct device *dev, + struct device_node *node, + struct sun9i_thermal_data *thermal) +{ + u32 num_channels; + int i; + + if (of_property_read_u32(node, "allwinner,sun9i-tsc-channels", &num_channels)) { + dev_err(dev, "missing sun9i-tsc-channels (number of channels) property\n"); + return -EINVAL; + } + + thermal->sensors = devm_kzalloc(dev, sizeof(struct sun9i_thermal_sensor) * num_channels, GFP_KERNEL); + thermal->num_channels = num_channels; + + for (i = 0; i < num_channels; ++i) + if (of_property_read_string_index(node, "allwinner,sun9i-tsc-name", i, + &thermal->sensors[i].name)) { + dev_err(dev, "missing allwinner,sun9i-tsc-name property\n"); + return -EINVAL; + } + + if (of_property_read_s32(node, "allwinner,sun9i-tsc-multiplier", &thermal->multiplier)) { + dev_warn(dev, "missing sun9i-tsc-multiplier, assuming 1\n"); + thermal->multiplier = 1; + } + + if (of_property_read_s32(node, "allwinner,sun9i-tsc-offset", &thermal->offset)) { + dev_warn(dev, "missing sun9i-tsc-offset, assuming 0\n"); + thermal->offset = 0; + } + + if (of_property_read_s32(node, "allwinner,sun9i-tsc-divisor", &thermal->divisor)) { + dev_warn(dev, "missing sun9i-tsc-divisor, assuming 1\n"); + thermal->divisor = 1; + } + + return 0; +} + +static ssize_t show_temp(struct device *dev, struct device_attribute *devattr, + char *buf) +{ + struct sun9i_thermal_sensor *ts = dev_get_drvdata(dev); + int temp; + int error; + + error = sun9i_thermal_get_temp(ts, &temp); + if (error) + return error; + + return sprintf(buf, "%d\n", temp); +} + +static ssize_t show_temp_label(struct device *dev, + struct device_attribute *devattr, char *buf) +{ + struct sun9i_thermal_sensor *ts = dev_get_drvdata(dev); + return sprintf(buf, "SoC temperature (%s)\n", ts->name); +} + +static DEVICE_ATTR(temp1_input, S_IRUGO, show_temp, NULL); +static DEVICE_ATTR(temp1_label, S_IRUGO, show_temp_label, NULL); + +static struct attribute *sun9i_ts_attrs[] = { + &dev_attr_temp1_input.attr, + &dev_attr_temp1_label.attr, + NULL +}; +ATTRIBUTE_GROUPS(sun9i_ts); + +static int +sun9i_thermal_register_sensor(struct platform_device *pdev, + struct sun9i_thermal_data *thermal, + int id) +{ + int error; + struct sun9i_thermal_sensor *sensor = &thermal->sensors[id]; + struct device *hwmon; + + printk("sun9i_thermal_register_sensor entered for id %d\n", id); + + sensor->thermal = thermal; + sensor->id = id; + sensor->tzd = devm_thermal_zone_of_sensor_register(&pdev->dev, id, + sensor, &sun9i_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; + } + + /* + * The thermal core does not register hwmon devices for DT-based + * thermal zone sensors, such as this one. + */ + printk("registering %s with hwmon\n", sensor->name); + hwmon = devm_hwmon_device_register_with_groups(&pdev->dev, sensor->name, + sensor, sun9i_ts_groups); + if (IS_ERR(hwmon)) { + dev_err(&pdev->dev, "failed to register sensor %s in hwmon: %ld\n", + sensor->name, PTR_ERR(hwmon)); + return PTR_ERR(hwmon); + } + + return 0; +} + +#if 1 +/** + * Reset TSADC Controller, reset all tsadc registers. + */ +static void sun9i_thermal_reset_controller(struct reset_control *reset) +{ + reset_control_assert(reset); + usleep_range(10, 20); + reset_control_deassert(reset); +} +#endif + +static int sun9i_thermal_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct sun9i_thermal_data *thermal; + const struct of_device_id *match; + struct resource *res; + int irq; + int i; + int error; + + match = of_match_node(of_sun9i_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 sun9i_thermal_data), + GFP_KERNEL); + if (!thermal) + return -ENOMEM; + + thermal->pdev = pdev; + + 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, NULL); + 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->hclk = devm_clk_get(&pdev->dev, "ahb"); + if (IS_ERR(thermal->hclk)) { + error = PTR_ERR(thermal->hclk); + dev_err(&pdev->dev, "failed to get sun9i_tsc AHB clock: %d\n", error); + return error; + } + + thermal->mclk = devm_clk_get(&pdev->dev, "mod"); + if (IS_ERR(thermal->mclk)) { + error = PTR_ERR(thermal->mclk); + dev_err(&pdev->dev, "failed to get sun9i_tsc module clock: %d\n", + error); + return error; + } + + error = clk_prepare_enable(thermal->hclk); + if (error) { + dev_err(&pdev->dev, "failed to enable pclk: %d\n", error); + return error; + } + + /* Ensure that our parent clock is slow enough */ + error = sun9i_configure_from_dt(&pdev->dev, np, thermal); + if (error) { + dev_err(&pdev->dev, "failed to parse device tree data: %d\n", + error); + return error; + } + + // clk_set_rate(thermal->mclk, 1000000); + error = clk_prepare_enable(thermal->mclk); + if (error) { + dev_err(&pdev->dev, "failed to enable converter clock: %d\n", + error); + goto err_disable_hclk; + } + + sun9i_thermal_reset_controller(thermal->reset); + +#if 0 + thermal->chip->initialize(thermal->grf, thermal->regs, + thermal->tshut_polarity); +#endif + + for (i = 0; i < thermal->num_channels; i++) { + error = sun9i_thermal_register_sensor(pdev, thermal, i); + + if (error) { + dev_err(&pdev->dev, + "failed to register sensor[%d] : error = %d\n", + i, error); + goto err_disable_mclk; + } + } + +#if 0 + 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; + } +#endif + + // thermal->chip->control(thermal->regs, true); + writel_relaxed(0x4000f, thermal->regs); + +#if 0 + for (i = 0; i < thermal->chip->num_channels; i++) + rockchip_thermal_toggle_sensor(&thermal->sensors[i], true); +#endif + + platform_set_drvdata(pdev, thermal); + + return 0; + +err_disable_mclk: + clk_disable_unprepare(thermal->mclk); +err_disable_hclk: + clk_disable_unprepare(thermal->hclk); + + return error; +} + +static int sun9i_thermal_remove(struct platform_device *pdev) +{ +#if 0 + 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); +#endif + + return 0; +} + +static int __maybe_unused sun9i_thermal_suspend(struct device *dev) +{ +#if 0 + 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); +#endif + + return 0; +} + +static int __maybe_unused sun9i_thermal_resume(struct device *dev) +{ +#if 0 + 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); + thermal->chip->set_tshut_temp(thermal->chip->table, + id, thermal->regs, + thermal->tshut_temp); + } + + 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); +#endif + + return 0; +} + +static SIMPLE_DEV_PM_OPS(sun9i_thermal_pm_ops, + sun9i_thermal_suspend, sun9i_thermal_resume); + +static struct platform_driver sun9i_thermal_driver = { + .driver = { + .name = "allwinner-sun9i-thermal", + .pm = &sun9i_thermal_pm_ops, + .of_match_table = of_sun9i_thermal_match, + }, + .probe = sun9i_thermal_probe, + .remove = sun9i_thermal_remove, +}; + +module_platform_driver(sun9i_thermal_driver); + +MODULE_DESCRIPTION("Allwinner A80 thermal sensor controller driver"); +MODULE_AUTHOR("Theobroma Systems GmbH"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:allwinner-thermal"); -- cgit v1.2.3