[PATCH v2 8/8] Add Advantech EIO Fan driver

Ramiro Oliveira <[email protected]>
Newsgroups dev.linux.lists.mfd,org.freedesktop.lists.dri-devel,org.kernel.vger.linux-fbdev,org.kernel.vger.linux-gpio,org.kernel.vger.linux-hwmon,org.kernel.vger.linux-i2c,org.kernel.vger.linux-kernel,org.kernel.vger.linux-pm,org.kernel.vger.linux-watchdog
Message-ID <[email protected]>
This commit adds the driver to control the Advantech EIO Fan block,
which is included in the Advantech EIO Embedded Controller.

Signed-off-by: Ramiro Oliveira <[email protected]>
---
 MAINTAINERS               |   1 +
 drivers/thermal/Kconfig   |   8 +
 drivers/thermal/Makefile  |   1 +
 drivers/thermal/eio_fan.c | 478 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 488 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index ded89e70d6cc..5d5d4ac54030 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -616,6 +616,7 @@ F:	drivers/gpio/gpio-eio.c
 F:	drivers/hwmon/eio-hwmon.c
 F:	drivers/i2c/busses/i2c-eio.c
 F:	drivers/mfd/eio_core.c
+F:	drivers/thermal/eio_fan.c
 F:	drivers/thermal/eio_thermal.c
 F:	drivers/video/backlight/eio_bl.c
 F:	drivers/watchdog/eio_wdt.c
diff --git a/drivers/thermal/Kconfig b/drivers/thermal/Kconfig
index fc078012a989..a52ec50c8cc5 100644
--- a/drivers/thermal/Kconfig
+++ b/drivers/thermal/Kconfig
@@ -436,6 +436,14 @@ config EIO_THERMAL
 	  the Linux thermal framework. It communicates with the EC through the
 	  EIO MFD core.
 
+config EIO_FAN
+        tristate "Advantech EIO Fan cooling device"
+        depends on MFD_EIO && THERMAL
+        help
+          Fan cooling device for the Advantech EIO. This driver exposes a
+          thermal cooling device with controllable states (e.g. Auto/Manual/PWM).
+          It communicates with the EC through the EIO MFD core.
+
 menu "Mediatek thermal drivers"
 depends on ARCH_MEDIATEK || COMPILE_TEST
 source "drivers/thermal/mediatek/Kconfig"
diff --git a/drivers/thermal/Makefile b/drivers/thermal/Makefile
index 6abce8933e34..872f733b6ad4 100644
--- a/drivers/thermal/Makefile
+++ b/drivers/thermal/Makefile
@@ -55,6 +55,7 @@ obj-$(CONFIG_IMX91_THERMAL)	+= imx91_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_EIO_FAN)		+= eio_fan.o
 obj-$(CONFIG_EIO_THERMAL)	+= eio_thermal.o
 obj-y				+= intel/
 obj-$(CONFIG_TI_SOC_THERMAL)	+= ti-soc-thermal/
diff --git a/drivers/thermal/eio_fan.c b/drivers/thermal/eio_fan.c
new file mode 100644
index 000000000000..e07b18c2ec44
--- /dev/null
+++ b/drivers/thermal/eio_fan.c
@@ -0,0 +1,478 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * eio_fan
+ * ============
+ * Thermal zone driver for Advantech EIO embedded controller's smart
+ * fan mechanism.
+ *
+ * We create a sysfs 'name' of the zone, point out where the fan is. Such as
+ * CPU0, SYS3, etc.
+ *
+ * The sysfs 'fan_mode' can be one of 'Stop', 'Full', 'Manual' or 'Auto'.
+ * If 'Manual'. You can control fan speed via sysfs 'PWM'.
+ * If it is 'Auto'. It enables the smart fan mechanism as below.
+ *
+ * In EIO chip. The smart fan has 3 trips. When the temperature is:
+ * - Over Temp High(trip0), the Fan runs at the fan PWM High.
+ * - Between Temp Low and Temp High(trip1 - trip0), the fan PWM value slopes
+ *   from PWM Low to PWM High.
+ * - Between Temp Stop and Temp Low(trip2 - trip1), the fan PWM is PWM low.
+ * - Below Temp Stop, the fan stopped.
+ *
+ * (PWM)|
+ *	|
+ * High |............................. ______________
+ * (Max)|			      /:
+ *	|			     / :
+ *	|			    /  :
+ *	|			   /   :
+ *	|			  /    :
+ *	|			 /     :
+ *	|			/      :
+ *	|		       /       :
+ *  Low	|.......... __________/	       :
+ *	|	    |	      :	       :
+ *	|	    |	      :	       :
+ *    0	+===========+---------+--------+-------------
+ *	0	   Stop	     Low      High	(Temp)
+ *
+ * Copyright (C) 2025 Advantech Corporation. All rights reserved.
+ */
+
+#include <linux/ctype.h>
+#include <linux/errno.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/sysfs.h>
+#include <linux/thermal.h>
+
+#define CMD_FAN_WRITE		0x24
+#define CMD_FAN_READ		0x25
+#define FAN_MAX			0x04
+
+#define CMD_THERM_WRITE		0x10
+#define CMD_THERM_READ		0x11
+#define THERM_MAX		0x04
+#define THERM_MULTI		100
+
+#define CTRL_STATE		0x00
+#define CTRL_TYPE		0x01
+#define CTRL_CTRL		0x02
+#define CTRL_ERROR		0x04
+#define CTRL_VALUE		0x10
+#define CTRL_INVERT		0x11
+#define CTRL_FREQ		0x12
+#define CTRL_THERM_HIGH		0x13
+#define CTRL_THERM_LOW		0x14
+#define CTRL_THERM_STOP		0x15
+#define CTRL_PWM_HIGH		0x16
+#define CTRL_PWM_LOW		0x17
+#define CTRL_THERM_SRC		0x20
+
+#define CTRLMODE_STOP		0x00
+#define CTRLMODE_FULL		0x01
+#define CTRLMODE_MANUAL		0x02
+#define CTRLMODE_AUTO		0x03
+
+#define DUTY_MAX		100
+#define UNIT_PER_TEMP		10
+#define NAME_SIZE		4
+
+#define TRIP_HIGH		0
+#define TRIP_LOW		1
+#define TRIP_STOP		2
+#define TRIP_NUM		3
+
+/* Bitfields inside CTRL_CTRL */
+#define FAN_MODE_MASK           GENMASK(1, 0)
+#define FAN_SCM_BIT             BIT(2)
+#define FAN_FRAME_BIT           BIT(3)
+#define FAN_SRC_MASK            GENMASK(7, 4)
+
+#define FAN_SRC(val)            (((int)(val)) >> 4)
+
+#ifndef DECI_KELVIN_TO_MILLI_CELSIUS
+#define DECI_KELVIN_TO_MILLI_CELSIUS(t)  ((((t) - 2731) * 100))
+#endif
+
+#ifndef MILLI_CELSIUS_TO_DECI_KELVIN
+#define MILLI_CELSIUS_TO_DECI_KELVIN(t)  ((((t) / 100) + 2731))
+#endif
+
+static const u8 pmc_len[CTRL_THERM_SRC + 1] = {
+/*      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f */
+	1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 1, 4, 2, 2, 2, 1, 1, 2, 2, 2, 0, 0, 0, 0, 0,
+	1,
+};
+
+static const char fan_name[0x20][NAME_SIZE + 1] = {
+	"CPU0", "CPU1", "CPU2", "CPU3", "SYS0", "SYS1", "SYS2", "SYS3",
+	"", "", "", "", "", "", "", "",
+	"", "", "", "", "", "", "", "",
+	"", "", "", "", "OEM0", "OEM1", "OEM2", "OEM3",
+};
+
+struct eio_fan_trip {
+	u8 trip_ctl;
+};
+
+struct eio_fan_dev {
+	struct device *mfd;
+	struct device *dev;
+	u8 id;
+	struct thermal_zone_device *tzd;
+	struct eio_fan_trip trip_priv[TRIP_NUM];
+};
+
+static int pmc_write(struct device *mfd, u8 ctrl, u8 id, void *data)
+{
+	if (ctrl >= ARRAY_SIZE(pmc_len))
+		return -EINVAL;
+
+	struct pmc_op op = {
+		.cmd       = CMD_FAN_WRITE,
+		.control   = ctrl,
+		.device_id = id,
+		.size	   = pmc_len[ctrl],
+		.payload   = (u8 *)data,
+	};
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int pmc_read(struct device *mfd, u8 ctrl, u8 id, void *data)
+{
+	struct pmc_op op = {
+		.cmd       = CMD_FAN_READ,
+		.control   = ctrl,
+		.device_id = id,
+		.size	   = pmc_len[ctrl],
+		.payload   = (u8 *)data,
+	};
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int pmc_read_therm(struct device *mfd, u8 ctrl, u8 id, void *data)
+{
+	struct pmc_op op = {
+		.cmd       = CMD_THERM_READ,
+		.control   = ctrl,
+		.device_id = id,
+		.size	   = 2,
+		.payload   = (u8 *)data,
+	};
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int eio_fan_get_temp(struct thermal_zone_device *tzd, int *temp)
+{
+	struct eio_fan_dev *fan = thermal_zone_device_priv(tzd);
+	struct device *mfd = fan->mfd;
+	u8 ch = fan->id;
+	int sensor = 0;
+	u16 val = 0;
+	int ret;
+
+	ret = pmc_read(mfd, CTRL_CTRL, ch, &sensor);
+	if (ret)
+		return ret;
+
+	ret = pmc_read_therm(mfd, CTRL_VALUE, (u8)FAN_SRC(sensor), &val);
+	if (ret)
+		return ret;
+
+	*temp = DECI_KELVIN_TO_MILLI_CELSIUS(val);
+	return 0;
+}
+
+static int eio_fan_set_trip_temp(struct thermal_zone_device *tzd,
+				 const struct thermal_trip *trip, int temp)
+{
+	struct eio_fan_dev *fan = thermal_zone_device_priv(tzd);
+	const struct eio_fan_trip *fan_trip = trip->priv;
+	u8 ctl = CTRL_THERM_HIGH + fan_trip->trip_ctl;
+	u16 val;
+
+	if (temp < 1000)
+		return -EINVAL;
+
+	val = MILLI_CELSIUS_TO_DECI_KELVIN(temp);
+	return pmc_write(fan->mfd, ctl, fan->id, &val);
+}
+
+static bool eio_fan_should_bind(struct thermal_zone_device *tzd,
+				const struct thermal_trip *trip,
+				struct thermal_cooling_device *cdev,
+				struct cooling_spec *spec)
+{
+	struct eio_fan_dev *tz_fan  = thermal_zone_device_priv(tzd);
+	struct eio_fan_dev *cd_fan  = cdev->devdata;
+
+	if (!tz_fan || !cd_fan)
+		return false;
+
+	if (tz_fan->mfd != cd_fan->mfd || tz_fan->id != cd_fan->id)
+		return false;
+
+	return true;
+}
+
+static const struct thermal_zone_device_ops zone_ops = {
+	.get_temp = eio_fan_get_temp,
+	.set_trip_temp = eio_fan_set_trip_temp,
+	.should_bind   = eio_fan_should_bind,
+};
+
+static int eio_fan_get_max_state(struct thermal_cooling_device *cdev,
+				 unsigned long *state)
+{
+	*state = 100;
+	return 0;
+}
+
+static int eio_fan_get_cur_state(struct thermal_cooling_device *cdev,
+				 unsigned long *state)
+{
+	struct eio_fan_dev *fan = cdev->devdata;
+	int fan_mode = 0;
+	u8 duty = 0;
+	int ret = 0;
+
+	*state = 0;
+	ret = pmc_read(fan->mfd, CTRL_CTRL, fan->id, &fan_mode);
+	if (ret)
+		return ret;
+
+	switch (fan_mode & FAN_MODE_MASK) {
+	case CTRLMODE_STOP:
+		*state = 0;
+		break;
+	case CTRLMODE_FULL:
+		*state = 100;
+		break;
+	case CTRLMODE_AUTO:
+		*state = 0;
+		ret = 0;
+		break;
+	case CTRLMODE_MANUAL:
+		ret = pmc_read(fan->mfd, CTRL_VALUE, fan->id, &duty);
+		if (ret)
+			return ret;
+		duty = (u8)clamp_val(duty, 0, 100);
+		*state = duty;
+		break;
+	default:
+		*state = 0;
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int eio_fan_set_cur_state(struct thermal_cooling_device *cdev,
+				 unsigned long state)
+{
+	struct eio_fan_dev *fan = cdev->devdata;
+	u8 ctrl = 0;
+	u8 duty;
+	int ret;
+
+	ret = pmc_read(fan->mfd, CTRL_CTRL, fan->id, &ctrl);
+	if (ret)
+		return ret;
+
+	if ((ctrl & FAN_MODE_MASK) != CTRLMODE_MANUAL)
+		return -EOPNOTSUPP;
+
+	duty = (u8)clamp_val(state, 0, 100);
+
+	ret = pmc_write(fan->mfd, CTRL_VALUE, fan->id, &duty);
+
+	return ret;
+}
+
+static const struct thermal_cooling_device_ops cooling_ops = {
+	.get_max_state = eio_fan_get_max_state,
+	.get_cur_state = eio_fan_get_cur_state,
+	.set_cur_state = eio_fan_set_cur_state,
+};
+
+static ssize_t fan_mode_show(struct device *dev, struct device_attribute *attr,
+			     char *buf)
+{
+	static const char * const names[] = { "Stop", "Full", "Manual", "Auto" };
+	struct thermal_zone_device *tzd = dev_get_drvdata(dev);
+	struct eio_fan_dev *fan = thermal_zone_device_priv(tzd);
+	u8 mode = 0;
+
+	int ret = pmc_read(fan->mfd, CTRL_CTRL, fan->id, &mode);
+
+	if (ret)
+		return ret;
+
+	return sysfs_emit(buf, "%s\n", names[mode & 0x03]);
+}
+
+static ssize_t fan_mode_store(struct device *dev, struct device_attribute *attr,
+			      const char *buf, size_t count)
+{
+	static const char * const names[] = { "Stop", "Full", "Manual", "Auto" };
+	struct thermal_zone_device *tzd = dev_get_drvdata(dev);
+	struct eio_fan_dev *fan = thermal_zone_device_priv(tzd);
+	u8 ctrl, newc;
+	int mode_idx, ret;
+
+	for (mode_idx = 0; mode_idx < ARRAY_SIZE(names); mode_idx++) {
+		if (strncasecmp(buf, names[mode_idx], strlen(names[mode_idx])))
+			continue;
+
+		ret = pmc_read(fan->mfd, CTRL_CTRL, fan->id, &ctrl);
+		if (ret)
+			return -EIO;
+
+		newc  = ctrl & FAN_SRC_MASK;
+
+		switch (mode_idx) {
+		case CTRLMODE_AUTO:
+			newc |= FAN_FRAME_BIT;
+			newc &= ~FAN_SCM_BIT;
+			newc |= CTRLMODE_AUTO;
+			break;
+		case CTRLMODE_MANUAL:
+			newc &= ~FAN_FRAME_BIT;
+			newc &= ~FAN_SCM_BIT;
+			newc |= CTRLMODE_MANUAL;
+			break;
+		case CTRLMODE_FULL:
+			newc &= ~FAN_FRAME_BIT;
+			newc &= ~FAN_SCM_BIT;
+			newc |= CTRLMODE_FULL;
+			break;
+		case CTRLMODE_STOP:
+		default:
+			newc &= ~FAN_FRAME_BIT;
+			newc &= ~FAN_SCM_BIT;
+			newc |= CTRLMODE_STOP;
+			break;
+		}
+
+		ret = pmc_write(fan->mfd, CTRL_CTRL, fan->id, &newc);
+		return ret ? ret : count;
+	}
+
+	return -EINVAL;
+}
+
+static DEVICE_ATTR_RW(fan_mode);
+
+static int eio_fan_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	unsigned int fan_id;
+	int ret;
+
+	if (!dev_get_drvdata(dev->parent)) {
+		dev_err(dev, "eio_core not present\n");
+		return -ENODEV;
+	}
+
+	for (fan_id = 0; fan_id < FAN_MAX; fan_id++) {
+		u8 state = 0, name = 0;
+		int trip_hi = 0, trip_lo = 0, trip_stop = 0;
+		int pwm_hi = 0, pwm_lo = 0;
+		struct eio_fan_dev *fan;
+		struct thermal_zone_device *tzd;
+		struct thermal_cooling_device *cdev;
+
+		if (pmc_read(dev->parent, CTRL_STATE, fan_id, &state) ||
+		    pmc_read(dev->parent, CTRL_TYPE, fan_id, &name) ||
+		    pmc_read(dev->parent, CTRL_THERM_HIGH, fan_id, &trip_hi) ||
+		    pmc_read(dev->parent, CTRL_THERM_LOW, fan_id, &trip_lo) ||
+		    pmc_read(dev->parent, CTRL_THERM_STOP, fan_id, &trip_stop) ||
+		    pmc_read(dev->parent, CTRL_PWM_HIGH, fan_id, &pwm_hi) ||
+		    pmc_read(dev->parent, CTRL_PWM_LOW, fan_id, &pwm_lo)) {
+			dev_info(dev, "fan%u: pmc read error, skipping\n", fan_id);
+			continue;
+		}
+
+		if (!(state & 0x1)) {
+			dev_info(dev, "fan%u: firmware reports disabled\n", fan_id);
+			continue;
+		}
+
+		if (!fan_name[name][0]) {
+			dev_info(dev, "fan%u: unknown name index %u\n", fan_id, name);
+			continue;
+		}
+
+		fan = devm_kzalloc(dev, sizeof(*fan), GFP_KERNEL);
+		if (!fan)
+			return -ENOMEM;
+
+		fan->mfd = dev->parent;
+		fan->id  = (u8)fan_id;
+
+		fan->trip_priv[TRIP_HIGH].trip_ctl = CTRL_THERM_HIGH;
+		fan->trip_priv[TRIP_LOW].trip_ctl  = CTRL_THERM_LOW;
+		fan->trip_priv[TRIP_STOP].trip_ctl = CTRL_THERM_STOP;
+
+		struct thermal_trip trips[TRIP_NUM] = {
+			[TRIP_HIGH] = {
+				.type = THERMAL_TRIP_ACTIVE,
+				.temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_hi),
+				.flags = THERMAL_TRIP_FLAG_RW_TEMP,
+				.priv = &fan->trip_priv[TRIP_HIGH],
+			},
+			[TRIP_LOW] = {
+				.type = THERMAL_TRIP_ACTIVE,
+				.temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_lo),
+				.flags = THERMAL_TRIP_FLAG_RW_TEMP,
+				.priv = &fan->trip_priv[TRIP_LOW],
+			},
+			[TRIP_STOP] = {
+				.type = THERMAL_TRIP_ACTIVE,
+				.temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_stop),
+				.flags = THERMAL_TRIP_FLAG_RW_TEMP,
+				.priv = &fan->trip_priv[TRIP_STOP],
+			},
+		};
+
+		tzd = thermal_zone_device_register_with_trips(fan_name[name],
+							      trips, TRIP_NUM,
+							      fan,
+							      &zone_ops,
+							      NULL,
+							      0, 0);
+		if (IS_ERR(tzd))
+			return PTR_ERR(tzd);
+
+		cdev = thermal_cooling_device_register(fan_name[name], fan, &cooling_ops);
+		if (IS_ERR(cdev)) {
+			thermal_zone_device_unregister(tzd);
+			dev_err(dev, "fan%u: cdev register failed: %ld\n",
+				fan_id, PTR_ERR(cdev));
+			return PTR_ERR(cdev);
+		}
+
+		dev_set_drvdata(thermal_zone_device(tzd), tzd);
+		ret = device_create_file(thermal_zone_device(tzd), &dev_attr_fan_mode);
+		if (ret)
+			dev_warn(dev, "Error create thermal zone fan_mode sysfs\n");
+	}
+	return 0;
+}
+
+static struct platform_driver eio_fan_driver = {
+	.probe  = eio_fan_probe,
+	.driver = {
+		.name = "eio_fan",
+	},
+};
+
+module_platform_driver(eio_fan_driver);
+
+MODULE_AUTHOR("Wenkai Chung <[email protected]>");
+MODULE_AUTHOR("Ramiro Oliveira <[email protected]>");
+MODULE_DESCRIPTION("Fan driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");

-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.