From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Carlo Szelinsky <github@szelinsky.de>
Date: Sat, 24 May 2026 12:00:00 +0200
Subject: hwmon: add Hasivo management MCU sensor driver

Exposes CPU/system temperatures and fan presence + control for the
external management MCU found on Hasivo / Horaco network switches.
The sensor driver is a child of the hasivo MCU MFD (syscon regmap) and
reads temperature, capability and fan registers over I2C.

The same temperature/fan register map is shared across Hasivo boards
(verified by diffing the stock firmware of the S1300WP-8XGT-4S+ and
S1100W-8XGT-SE); register 0xFD is a per-board firmware revision byte, not
a fixed chip identity, so the driver does not gate on a specific value.

The driver, its device-tree binding and the Kconfig/Makefile wiring are
added to the kernel tree as a realtek target patch and exposed as the
kmod-hasivo-mcu-sensor KernelPackage. Keeping the binding in the kernel
tree lets dt_binding_check exercise it and makes the driver easy to drop
once it lands upstream.

Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
---
--- /dev/null
+++ b/Documentation/devicetree/bindings/hwmon/hasivo,mcu-sensor.yaml
@@ -0,0 +1,43 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/hwmon/hasivo,mcu-sensor.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Hasivo management MCU sensor
+
+maintainers:
+  - Carlo Szelinsky <github@szelinsky.de>
+
+description: |
+  Sensor node of the Hasivo management MCU MFD (compatible
+  "hasivo,stc8-mfd"). The microcontroller reports CPU and system
+  temperatures and provides a 3-state fan control (auto / force-on /
+  force-off) and reports the fan drive state. Fan speed is regulated
+  internally by the microcontroller; there is no host-side PWM duty or
+  tachometer register.
+
+properties:
+  compatible:
+    const: hasivo,mcu-sensor
+
+required:
+  - compatible
+
+additionalProperties: false
+
+examples:
+  - |
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        mcu: mcu-mfd@6f {
+            compatible = "hasivo,stc8-mfd", "syscon";
+            reg = <0x6f>;
+
+            sensor {
+                compatible = "hasivo,mcu-sensor";
+            };
+        };
+    };
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -819,6 +819,21 @@ config SENSORS_GXP_FAN_CTRL
 	  The GXP controls fan function via the CPLD through the use of PWM
 	  registers. This driver reports status and pwm setting of the fans.
 
+config SENSORS_HASIVO_MCU
+	tristate "Hasivo management MCU sensor"
+	depends on MFD_HASIVO_STC8
+	depends on OF
+	select REGMAP
+	help
+	  Sensor child of the Hasivo MCU MFD. Exposes CPU and system
+	  temperatures plus a 3-state fan control knob (auto / force-on
+	  / force-off) plus a fan drive-state reading. Used on Hasivo managed
+	  switches that carry the management MCU for thermal and
+	  fan management (S1300WP-8XGT-4S+ and similar).
+
+	  This driver can also be built as a module, which will be
+	  called hasivo-mcu-sensor.
+
 config SENSORS_HIH6130
 	tristate "Honeywell Humidicon HIH-6130 humidity/temperature sensor"
 	depends on I2C
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -91,6 +91,7 @@ obj-$(CONFIG_SENSORS_GSC)	+= gsc-hwmon.o
 obj-$(CONFIG_SENSORS_GPD)	+= gpd-fan.o
 obj-$(CONFIG_SENSORS_GPIO_FAN)	+= gpio-fan.o
 obj-$(CONFIG_SENSORS_GXP_FAN_CTRL) += gxp-fan-ctrl.o
+obj-$(CONFIG_SENSORS_HASIVO_MCU) += hasivo-mcu-sensor.o
 obj-$(CONFIG_SENSORS_HIH6130)	+= hih6130.o
 obj-$(CONFIG_SENSORS_HS3001)	+= hs3001.o
 obj-$(CONFIG_SENSORS_HTU31)	+= htu31.o
--- /dev/null
+++ b/drivers/hwmon/hasivo-mcu-sensor.c
@@ -0,0 +1,382 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Hasivo management MCU sensor driver
+ *
+ * Exposes CPU/system temperatures and fan control as hwmon attributes via
+ * the parent MFD's syscon regmap. The MCU regulates fan speed internally;
+ * the host can only pick one of three modes (auto / force-on / force-off)
+ * via the fan-control register 0xFA, with pwm_enable following the hwmon
+ * convention:
+ *   0 = no host control (fan forced full speed)
+ *   1 = manual (state determined by pwm_input)
+ *   2 = automatic (MCU regulates based on internal temperature)
+ *
+ * There is no tachometer. The fan-status register 0xFB reflects the MCU's
+ * effective drive state (0x4f = auto-running, 0x5f = force-on, 0x40 = off),
+ * not actual rotation: blocking the blade while force-on still reads 0x5f.
+ * So pwm_input reports the live drive state from 0xFB (accurate in auto
+ * too), and no fanN_alarm is exposed - a stuck/dead fan is not detectable
+ * from these registers, and thermal protection is the MCU's own job.
+ *
+ * Register protocol reverse-engineered from the stock 'imi' binary and
+ * verified on hardware. The same temperature/fan register map is shared
+ * across Hasivo boards (S1300WP-8XGT-4S+, S1100W-8XGT-SE); reg 0xFD is a
+ * per-board firmware revision byte (0x91 / 0xFC), not a fixed chip identity.
+ *
+ * Copyright (C) 2026 Carlo Szelinsky <github@szelinsky.de>
+ */
+
+#include <linux/bitops.h>
+#include <linux/hwmon.h>
+#include <linux/minmax.h>
+#include <linux/mfd/syscon.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#define MCU_REG_CPU_TEMP_SIGN		0x01
+#define MCU_REG_CPU_TEMP_MAG		0x02
+#define MCU_REG_SYS_TEMP_SIGN		0x03
+#define MCU_REG_SYS_TEMP_MAG		0x04
+#define MCU_REG_FAN_CONTROL		0xfa
+#define MCU_REG_FAN_STATUS		0xfb
+#define MCU_REG_REVISION		0xfd
+#define MCU_REG_CAPABILITIES		0xfe
+
+#define MCU_CAP_FAN_PRESENT		BIT(6)
+#define MCU_CAP_TEMP_MASK		GENMASK(5, 0)
+#define MCU_TEMP_FLAG_NONE		1
+#define MCU_TEMP_FLAG_SYSTEM_ONLY	2
+
+/*
+ * Values written to the fan-control register (0xFA); the fan-status
+ * register (0xFB) reports the MCU's effective drive using the same encoding.
+ */
+#define MCU_FAN_VAL_AUTO		0x4f
+#define MCU_FAN_VAL_FORCE_ON		0x5f
+#define MCU_FAN_VAL_FORCE_OFF		0x40
+
+#define MCU_PWM_FORCE_THRESHOLD	128
+
+enum mcu_temp_channel {
+	MCU_TEMP_CPU = 0,
+	MCU_TEMP_SYS,
+};
+
+struct mcu_sensor {
+	struct regmap *regmap;
+	bool fan_present;
+	bool cpu_temp_avail;
+	bool sys_temp_avail;
+};
+
+static int mcu_read_temp(struct regmap *regmap, unsigned int sign_reg,
+			  unsigned int mag_reg, long *temp)
+{
+	unsigned int sign, mag;
+	int ret;
+
+	ret = regmap_read(regmap, sign_reg, &sign);
+	if (ret)
+		return ret;
+	ret = regmap_read(regmap, mag_reg, &mag);
+	if (ret)
+		return ret;
+
+	/*
+	 * The sign register is 0 for positive, non-zero (1 in practice) for
+	 * negative. Treat any non-zero value as negative rather than only
+	 * 0x01, so a stray reading can't be reported as a plausible positive
+	 * temperature.
+	 */
+	*temp = (long)(sign ? -(int)mag : (int)mag) * 1000;
+	return 0;
+}
+
+static umode_t mcu_is_visible(const void *drvdata,
+			       enum hwmon_sensor_types type,
+			       u32 attr, int channel)
+{
+	const struct mcu_sensor *priv = drvdata;
+
+	switch (type) {
+	case hwmon_temp:
+		/*
+		 * Expose both temperature channels unconditionally so
+		 * sysfs numbering stays contiguous (some userspace tools
+		 * assume so). Per-channel availability is reported via
+		 * tempN_fault.
+		 */
+		switch (attr) {
+		case hwmon_temp_input:
+		case hwmon_temp_label:
+		case hwmon_temp_fault:
+			return 0444;
+		}
+		break;
+	case hwmon_pwm:
+		if (!priv->fan_present)
+			return 0;
+		if (attr == hwmon_pwm_enable || attr == hwmon_pwm_input)
+			return 0644;
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+static int mcu_read(struct device *dev, enum hwmon_sensor_types type,
+		     u32 attr, int channel, long *val)
+{
+	struct mcu_sensor *priv = dev_get_drvdata(dev);
+	unsigned int v;
+	int ret;
+
+	switch (type) {
+	case hwmon_temp:
+		switch (attr) {
+		case hwmon_temp_input:
+			if (channel == MCU_TEMP_CPU) {
+				if (!priv->cpu_temp_avail)
+					return -ENODATA;
+				return mcu_read_temp(priv->regmap,
+						      MCU_REG_CPU_TEMP_SIGN,
+						      MCU_REG_CPU_TEMP_MAG,
+						      val);
+			}
+			if (!priv->sys_temp_avail)
+				return -ENODATA;
+			return mcu_read_temp(priv->regmap,
+					      MCU_REG_SYS_TEMP_SIGN,
+					      MCU_REG_SYS_TEMP_MAG,
+					      val);
+		case hwmon_temp_fault:
+			if (channel == MCU_TEMP_CPU)
+				*val = priv->cpu_temp_avail ? 0 : 1;
+			else
+				*val = priv->sys_temp_avail ? 0 : 1;
+			return 0;
+		}
+		break;
+	case hwmon_pwm:
+		switch (attr) {
+		case hwmon_pwm_enable:
+			/*
+			 * Mode from the control register (0xFA), per the hwmon
+			 * convention: 0 = force-on (full speed), 1 = manual
+			 * (force-off; pwm_input drives it), 2 = automatic.
+			 */
+			ret = regmap_read(priv->regmap, MCU_REG_FAN_CONTROL, &v);
+			if (ret)
+				return ret;
+			switch (v) {
+			case MCU_FAN_VAL_AUTO:
+				*val = 2;
+				break;
+			case MCU_FAN_VAL_FORCE_ON:
+				*val = 0;
+				break;
+			default:
+				*val = 1;
+				break;
+			}
+			return 0;
+		case hwmon_pwm_input:
+			/*
+			 * Echo the commanded state from the control register
+			 * (0xFA) so a pwm write reads back consistently. The
+			 * control register carries no duty in automatic mode,
+			 * so there fall back to the live drive state (0xFB):
+			 * 0x4f (auto-running) / 0x5f mean driven on, else off.
+			 */
+			ret = regmap_read(priv->regmap, MCU_REG_FAN_CONTROL, &v);
+			if (ret)
+				return ret;
+			if (v == MCU_FAN_VAL_AUTO) {
+				ret = regmap_read(priv->regmap,
+						  MCU_REG_FAN_STATUS, &v);
+				if (ret)
+					return ret;
+			}
+			*val = (v == MCU_FAN_VAL_AUTO || v == MCU_FAN_VAL_FORCE_ON) ? 255 : 0;
+			return 0;
+		default:
+			return -EOPNOTSUPP;
+		}
+		break;
+	default:
+		break;
+	}
+	return -EOPNOTSUPP;
+}
+
+static int mcu_read_string(struct device *dev, enum hwmon_sensor_types type,
+			    u32 attr, int channel, const char **str)
+{
+	if (type == hwmon_temp && attr == hwmon_temp_label) {
+		*str = (channel == MCU_TEMP_CPU) ? "CPU" : "System";
+		return 0;
+	}
+	return -EOPNOTSUPP;
+}
+
+static int mcu_write(struct device *dev, enum hwmon_sensor_types type,
+		      u32 attr, int channel, long val)
+{
+	struct mcu_sensor *priv = dev_get_drvdata(dev);
+	unsigned int byte;
+
+	if (type != hwmon_pwm)
+		return -EOPNOTSUPP;
+
+	switch (attr) {
+	case hwmon_pwm_enable:
+		switch (val) {
+		case 0:
+			byte = MCU_FAN_VAL_FORCE_ON;
+			break;
+		case 1:
+			/*
+			 * Manual: leave the MCU's automatic regulation
+			 * by switching to an explicit FORCE_OFF state so
+			 * a subsequent pwm_enable read reports 1 (manual)
+			 * even before any pwm_input write. Userspace is
+			 * expected to follow up by writing pwm_input to
+			 * pick the actual fan state.
+			 */
+			byte = MCU_FAN_VAL_FORCE_OFF;
+			break;
+		case 2:
+			byte = MCU_FAN_VAL_AUTO;
+			break;
+		default:
+			return -EINVAL;
+		}
+		break;
+	case hwmon_pwm_input:
+		if (val < 0)
+			return -EINVAL;
+		val = clamp_val(val, 0, 255);
+		byte = (val >= MCU_PWM_FORCE_THRESHOLD) ?
+			MCU_FAN_VAL_FORCE_ON : MCU_FAN_VAL_FORCE_OFF;
+		break;
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	return regmap_write(priv->regmap, MCU_REG_FAN_CONTROL, byte);
+}
+
+static const struct hwmon_channel_info *const mcu_sensor_info[] = {
+	HWMON_CHANNEL_INFO(temp,
+			   HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_FAULT,
+			   HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_FAULT),
+	HWMON_CHANNEL_INFO(pwm, HWMON_PWM_ENABLE | HWMON_PWM_INPUT),
+	NULL
+};
+
+static const struct hwmon_ops mcu_sensor_ops = {
+	.is_visible = mcu_is_visible,
+	.read = mcu_read,
+	.read_string = mcu_read_string,
+	.write = mcu_write,
+};
+
+static const struct hwmon_chip_info mcu_sensor_chip_info = {
+	.ops = &mcu_sensor_ops,
+	.info = mcu_sensor_info,
+};
+
+static int mcu_sensor_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct mcu_sensor *priv;
+	struct device *hwmon_dev;
+	unsigned int rev, caps, flag;
+	int ret;
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	priv->regmap = syscon_node_to_regmap(dev_of_node(dev->parent));
+	if (IS_ERR(priv->regmap))
+		return dev_err_probe(dev, PTR_ERR(priv->regmap),
+				     "failed to get parent regmap\n");
+
+	/*
+	 * Register 0xFD holds a board/firmware revision byte (e.g. 0x91 on the
+	 * S1300WP-8XGT-4S+, 0xFC on the S1100W-8XGT-SE), not a fixed chip ID;
+	 * the stock firmware never gates on a specific value. Only reject the
+	 * all-zero / all-ones reads that mean the MCU isn't responding, and
+	 * leave actual feature detection to the capability register below.
+	 */
+	ret = regmap_read(priv->regmap, MCU_REG_REVISION, &rev);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to read revision\n");
+	if (rev == 0x00 || rev == 0xff)
+		return dev_err_probe(dev, -ENODEV,
+				     "MCU not responding (revision 0x%02x)\n",
+				     rev);
+
+	ret = regmap_read(priv->regmap, MCU_REG_CAPABILITIES, &caps);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to read capabilities\n");
+
+	priv->fan_present = !!(caps & MCU_CAP_FAN_PRESENT);
+	flag = caps & MCU_CAP_TEMP_MASK;
+	priv->cpu_temp_avail = flag != MCU_TEMP_FLAG_NONE &&
+			       flag != MCU_TEMP_FLAG_SYSTEM_ONLY;
+	priv->sys_temp_avail = flag != MCU_TEMP_FLAG_NONE;
+
+	dev_info(dev, "rev 0x%02x: fan=%s cpu_temp=%s sys_temp=%s\n", rev,
+		 priv->fan_present ? "present" : "absent",
+		 priv->cpu_temp_avail ? "yes" : "no",
+		 priv->sys_temp_avail ? "yes" : "no");
+
+	platform_set_drvdata(pdev, priv);
+
+	hwmon_dev = devm_hwmon_device_register_with_info(dev, "hasivo_mcu",
+							 priv,
+							 &mcu_sensor_chip_info,
+							 NULL);
+	return PTR_ERR_OR_ZERO(hwmon_dev);
+}
+
+static void mcu_sensor_remove(struct platform_device *pdev)
+{
+	struct mcu_sensor *priv = platform_get_drvdata(pdev);
+
+	/*
+	 * Restore AUTO mode so the MCU resumes thermal regulation after
+	 * unload. Skipping this would leave a user-forced state in
+	 * effect indefinitely (e.g. fan off after a force-off
+	 * experiment + reboot), which is a real thermal-safety hazard.
+	 */
+	if (priv->fan_present)
+		regmap_write(priv->regmap, MCU_REG_FAN_CONTROL,
+			     MCU_FAN_VAL_AUTO);
+}
+
+static const struct of_device_id mcu_sensor_of_match[] = {
+	{ .compatible = "hasivo,mcu-sensor" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, mcu_sensor_of_match);
+
+static struct platform_driver mcu_sensor_driver = {
+	.driver = {
+		.name = "hasivo-mcu-sensor",
+		.of_match_table = mcu_sensor_of_match,
+	},
+	.probe = mcu_sensor_probe,
+	.remove = mcu_sensor_remove,
+};
+module_platform_driver(mcu_sensor_driver);
+
+MODULE_AUTHOR("Carlo Szelinsky <github@szelinsky.de>");
+MODULE_DESCRIPTION("Hasivo management MCU sensor driver");
+MODULE_LICENSE("GPL");
