summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--arch/arm/boot/dts/sun9i-a80.dtsi26
-rw-r--r--drivers/thermal/Kconfig12
-rw-r--r--drivers/thermal/Makefile1
-rw-r--r--drivers/thermal/sun9i_thermal.c489
4 files changed, 528 insertions, 0 deletions
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 <dt-bindings/interrupt-controller/arm-gic.h>
+#include <dt-bindings/thermal/thermal.h>
#include <dt-bindings/pinctrl/sun4i-a10.h>
@@ -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 = <GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>;
+ 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 <philipp.tomsich@theobroma-systems.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/hwmon.h>
+#include <linux/mfd/syscon.h>
+#include <linux/pinctrl/consumer.h>
+
+
+
+/**
+ * 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");