summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/hwmon/index.rst1
-rw-r--r--Documentation/hwmon/minisforum-um780xtx.rst68
-rw-r--r--MAINTAINERS7
-rw-r--r--drivers/hwmon/Kconfig16
-rw-r--r--drivers/hwmon/Makefile1
-rw-r--r--drivers/hwmon/minisforum-um780xtx.c501
6 files changed, 594 insertions, 0 deletions
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index 06a7992ce5da..f07977a20271 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -187,6 +187,7 @@ Hardware Monitoring Kernel Drivers
mcp3021
mcp9982
menf21bmc
+ minisforum-um780xtx
mlxreg-fan
mp2856
mp2869
diff --git a/Documentation/hwmon/minisforum-um780xtx.rst b/Documentation/hwmon/minisforum-um780xtx.rst
new file mode 100644
index 000000000000..8237d525a9c7
--- /dev/null
+++ b/Documentation/hwmon/minisforum-um780xtx.rst
@@ -0,0 +1,68 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+====================================
+Kernel driver minisforum-um780xtx
+====================================
+
+Supported systems:
+
+ * Minisforum UM780 XTX
+
+ * DMI product name: ``Venus series``
+ * Mainboard: ``F7BSD``, revision ``1.1``
+ * BIOS version: ``1.06``
+
+Author: Sebastián Peyrott <speyrott@gmail.com>
+
+Description
+-----------
+
+This driver exposes hardware monitoring and fan-control data cached by the
+IT5571E embedded controller. It uses the ACPI EC transport and only binds to
+the exact system and firmware identity listed above.
+
+The two temperature channels are the values used by the EC's fan-control
+loops. Their physical sensor placement is not known. The CPU channel is the
+filtered AMD SB-TSI temperature, while the system channel is an external
+thermistor input.
+
+The fan tachometers are read through OEM EC commands. Since each byte is
+returned by a separate command, the driver uses a high-low-high sequence and
+retries if the high byte changes.
+
+Sysfs entries
+-------------
+
+========================== ==============================================
+``temp1_input`` CPU-fan control temperature
+``temp2_input`` System-fan control temperature
+``fan1_input`` CPU fan speed in RPM
+``fan2_input`` System fan speed in RPM
+``pwm1_enable`` CPU profile: 2 is OEM B1, 3 is OEM B2
+``pwm2_auto_point1_temp`` Off-to-low system-fan transition temperature
+``pwm2_auto_point2_temp`` Low-to-high system-fan transition temperature
+========================== ==============================================
+
+CPU fan profiles
+----------------
+
+Values 2 and 3 of ``pwm1_enable`` select the two complete automatic profiles
+implemented by the firmware. Writing either value reloads the whole CPU fan
+curve, including firmware state which is not visible through the ACPI EC
+window. Other values are rejected.
+
+System fan thresholds
+---------------------
+
+The two writable system-fan temperatures are rounded to whole degrees Celsius
+and clamped to preserve strict ordering between both visible thresholds and
+the firmware's internal third threshold. The third threshold does not select
+a new PWM target and is therefore not exposed as another auto point.
+
+Resume state preservation
+-------------------------
+
+Fan settings are held in EC RAM. On the supported firmware, initialization
+during s2idle resume reloads the factory system-fan curve. The driver retains
+the last coherent CPU profile and system-fan thresholds selected through its
+interface and restores them synchronously at resume.
diff --git a/MAINTAINERS b/MAINTAINERS
index fdd08ebb204a..5e4d3fad9959 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18185,6 +18185,13 @@ F: include/linux/min_heap.h
F: lib/min_heap.c
F: lib/tests/min_heap_kunit.c
+MINISFORUM UM780 XTX EC HARDWARE MONITOR DRIVER
+M: Sebastián Peyrott <speyrott@gmail.com>
+L: linux-hwmon@vger.kernel.org
+S: Maintained
+F: Documentation/hwmon/minisforum-um780xtx.rst
+F: drivers/hwmon/minisforum-um780xtx.c
+
MIPI CCS, SMIA AND SMIA++ IMAGE SENSOR DRIVER
M: Sakari Ailus <sakari.ailus@linux.intel.com>
L: linux-media@vger.kernel.org
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index b19aa16986af..c1d72af52e8f 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1490,6 +1490,22 @@ config SENSORS_MENF21BMC_HWMON
This driver can also be built as a module. If so the module
will be called menf21bmc_hwmon.
+config SENSORS_MINISFORUM_UM780XTX
+ tristate "Minisforum UM780 XTX EC hardware monitoring"
+ depends on ACPI_EC && DMI && X86
+ help
+ If you say yes here you get support for the hardware monitoring
+ features of the embedded controller in the Minisforum UM780 XTX.
+ This includes CPU and system fan speeds, their control temperatures,
+ two CPU fan profiles, system fan temperature thresholds, and
+ preservation of the selected settings across suspend and resume.
+
+ This driver only binds to explicitly supported board and firmware
+ versions.
+
+ This driver can also be built as a module. If so, the module
+ will be called minisforum-um780xtx.
+
config SENSORS_MR75203
tristate "Moortec Semiconductor MR75203 PVT Controller"
select REGMAP_MMIO
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 646813b7aaa8..0243cb62c058 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -181,6 +181,7 @@ obj-$(CONFIG_SENSORS_TC654) += tc654.o
obj-$(CONFIG_SENSORS_TPS23861) += tps23861.o
obj-$(CONFIG_SENSORS_MLXREG_FAN) += mlxreg-fan.o
obj-$(CONFIG_SENSORS_MENF21BMC_HWMON) += menf21bmc_hwmon.o
+obj-$(CONFIG_SENSORS_MINISFORUM_UM780XTX) += minisforum-um780xtx.o
obj-$(CONFIG_SENSORS_MR75203) += mr75203.o
obj-$(CONFIG_SENSORS_NCT6683) += nct6683.o
obj-$(CONFIG_SENSORS_NCT6694) += nct6694-hwmon.o
diff --git a/drivers/hwmon/minisforum-um780xtx.c b/drivers/hwmon/minisforum-um780xtx.c
new file mode 100644
index 000000000000..f5d6e18cb651
--- /dev/null
+++ b/drivers/hwmon/minisforum-um780xtx.c
@@ -0,0 +1,501 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Minisforum UM780 XTX (F7BSD) embedded-controller hwmon driver.
+ *
+ * Copyright (C) 2026 Sebastián Peyrott <speyrott@gmail.com>
+ */
+
+#include <linux/acpi.h>
+#include <linux/cleanup.h>
+#include <linux/dmi.h>
+#include <linux/err.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/pm.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#define UM780XTX_EC_TEMP_SYS 0x05
+#define UM780XTX_EC_TEMP_CPU 0x09
+#define UM780XTX_EC_CPU_PROFILE 0x2f
+#define UM780XTX_EC_SYS_POINT1 0x31
+#define UM780XTX_EC_SYS_POINT2 0x34
+#define UM780XTX_EC_SYS_POINT3 0x37
+
+#define UM780XTX_EC_PROFILE_B1 0xb1
+#define UM780XTX_EC_PROFILE_B2 0xb2
+
+#define UM780XTX_EC_FAN1_LO 0xb6
+#define UM780XTX_EC_FAN1_HI 0xb7
+#define UM780XTX_EC_FAN2_LO 0xb9
+#define UM780XTX_EC_FAN2_HI 0xba
+
+#define UM780XTX_EC_MAX_RPM 9000
+#define UM780XTX_EC_RPM_RETRIES 3
+
+static struct platform_device *um780xtx_pdev;
+
+struct um780xtx_data {
+ struct device *hwmon_dev;
+ u8 saved_profile;
+ u8 saved_sys_point1;
+ u8 saved_sys_point2;
+ u8 sys_point3;
+};
+
+static const struct dmi_system_id um780xtx_dmi_table[] = {
+ {
+ .matches = {
+ DMI_EXACT_MATCH(DMI_SYS_VENDOR,
+ "Micro Computer (HK) Tech Limited"),
+ DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Venus series"),
+ DMI_EXACT_MATCH(DMI_BOARD_VENDOR,
+ "Shenzhen Meigao Electronic Equipment Co.,Ltd"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "F7BSD"),
+ },
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(dmi, um780xtx_dmi_table);
+
+static bool um780xtx_firmware_match(void)
+{
+ const char *board_version = dmi_get_system_info(DMI_BOARD_VERSION);
+ const char *bios_version = dmi_get_system_info(DMI_BIOS_VERSION);
+
+ return board_version && bios_version &&
+ !strcmp(board_version, "1.1") && !strcmp(bios_version, "1.06");
+}
+
+static int um780xtx_oem_read(u8 command, u8 *value)
+{
+ return ec_transaction(command, NULL, 0, value, 1);
+}
+
+static int um780xtx_read_rpm(u8 command_hi, u8 command_lo, long *rpm)
+{
+ u8 hi_before;
+ u8 hi_after;
+ u8 lo;
+ unsigned int value;
+ int attempt;
+ int ret;
+
+ for (attempt = 0; attempt < UM780XTX_EC_RPM_RETRIES; attempt++) {
+ ret = um780xtx_oem_read(command_hi, &hi_before);
+ if (ret)
+ return ret;
+ ret = um780xtx_oem_read(command_lo, &lo);
+ if (ret)
+ return ret;
+ ret = um780xtx_oem_read(command_hi, &hi_after);
+ if (ret)
+ return ret;
+ if (hi_before != hi_after)
+ continue;
+
+ value = (hi_after << 8) | lo;
+ if (value > UM780XTX_EC_MAX_RPM)
+ continue;
+
+ *rpm = value;
+ return 0;
+ }
+
+ return -EIO;
+}
+
+static int um780xtx_read_profile(long *mode)
+{
+ u8 profile;
+ int ret;
+
+ ret = ec_read(UM780XTX_EC_CPU_PROFILE, &profile);
+ if (ret)
+ return ret;
+ if (profile == UM780XTX_EC_PROFILE_B1) {
+ *mode = 2;
+ return 0;
+ }
+ if (profile == UM780XTX_EC_PROFILE_B2) {
+ *mode = 3;
+ return 0;
+ }
+
+ return -ENODATA;
+}
+
+static int um780xtx_write_profile(struct um780xtx_data *data, long mode)
+{
+ u8 expected;
+ u8 profile;
+ int ret;
+
+ if (mode != 2 && mode != 3)
+ return -EINVAL;
+ expected = mode == 2 ? UM780XTX_EC_PROFILE_B1 : UM780XTX_EC_PROFILE_B2;
+
+ ret = ec_transaction(expected, NULL, 0, NULL, 0);
+ if (ret)
+ return ret;
+ ret = ec_read(UM780XTX_EC_CPU_PROFILE, &profile);
+ if (ret)
+ return ret;
+ if (profile != expected)
+ return -EIO;
+
+ data->saved_profile = profile;
+ return 0;
+}
+
+static const u8 um780xtx_sys_point_offsets[] = {
+ UM780XTX_EC_SYS_POINT1,
+ UM780XTX_EC_SYS_POINT2,
+};
+
+static ssize_t um780xtx_sys_point_temp_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr);
+ u8 value;
+ int ret;
+
+ guard(hwmon_lock)(dev);
+ ret = ec_read(um780xtx_sys_point_offsets[sattr->index], &value);
+ if (ret)
+ return ret;
+
+ return sysfs_emit(buf, "%u\n", value * 1000);
+}
+
+static ssize_t um780xtx_sys_point_temp_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct um780xtx_data *data = dev_get_drvdata(dev);
+ struct sensor_device_attribute *sattr = to_sensor_dev_attr(attr);
+ u8 points[2];
+ u8 readback;
+ long value;
+ int ret;
+
+ ret = kstrtol(buf, 10, &value);
+ if (ret)
+ return ret;
+
+ guard(hwmon_lock)(dev);
+ /* Validate and clamp against the live peer threshold. */
+ ret = ec_read(UM780XTX_EC_SYS_POINT1, &points[0]);
+ if (ret)
+ return ret;
+ ret = ec_read(UM780XTX_EC_SYS_POINT2, &points[1]);
+ if (ret)
+ return ret;
+ if (points[0] >= points[1] || points[1] >= data->sys_point3)
+ return -EIO;
+
+ value = DIV_ROUND_CLOSEST(value, 1000);
+ if (!sattr->index)
+ value = clamp_val(value, 0, points[1] - 1);
+ else
+ value = clamp_val(value, points[0] + 1,
+ data->sys_point3 - 1);
+ points[sattr->index] = value;
+
+ ret = ec_write(um780xtx_sys_point_offsets[sattr->index],
+ points[sattr->index]);
+ if (ret)
+ return ret;
+ ret = ec_read(um780xtx_sys_point_offsets[sattr->index], &readback);
+ if (ret)
+ return ret;
+ if (readback != points[sattr->index])
+ return -EIO;
+
+ data->saved_sys_point1 = points[0];
+ data->saved_sys_point2 = points[1];
+ return count;
+}
+
+static SENSOR_DEVICE_ATTR_RW(pwm2_auto_point1_temp,
+ um780xtx_sys_point_temp, 0);
+static SENSOR_DEVICE_ATTR_RW(pwm2_auto_point2_temp,
+ um780xtx_sys_point_temp, 1);
+
+static struct attribute *um780xtx_extra_attrs[] = {
+ &sensor_dev_attr_pwm2_auto_point1_temp.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point2_temp.dev_attr.attr,
+ NULL
+};
+
+static const struct attribute_group um780xtx_extra_group = {
+ .attrs = um780xtx_extra_attrs,
+};
+
+static const struct attribute_group *um780xtx_extra_groups[] = {
+ &um780xtx_extra_group,
+ NULL
+};
+
+static umode_t um780xtx_is_visible(const void *data,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ if (type == hwmon_temp || type == hwmon_fan)
+ return 0444;
+ if (type == hwmon_pwm && attr == hwmon_pwm_enable)
+ return 0644;
+ return 0;
+}
+
+static int um780xtx_read(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long *value)
+{
+ u8 raw;
+ int ret;
+
+ if (type == hwmon_temp) {
+ ret = ec_read(channel ? UM780XTX_EC_TEMP_SYS :
+ UM780XTX_EC_TEMP_CPU, &raw);
+ if (ret)
+ return ret;
+ *value = raw * 1000L;
+ return 0;
+ }
+ if (type == hwmon_fan) {
+ if (!channel)
+ ret = um780xtx_read_rpm(UM780XTX_EC_FAN1_HI,
+ UM780XTX_EC_FAN1_LO, value);
+ else
+ ret = um780xtx_read_rpm(UM780XTX_EC_FAN2_HI,
+ UM780XTX_EC_FAN2_LO, value);
+ return ret;
+ }
+ if (type == hwmon_pwm)
+ return um780xtx_read_profile(value);
+
+ return -EOPNOTSUPP;
+}
+
+static int um780xtx_write(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long value)
+{
+ struct um780xtx_data *data = dev_get_drvdata(dev);
+
+ if (type == hwmon_pwm && attr == hwmon_pwm_enable)
+ return um780xtx_write_profile(data, value);
+
+ return -EOPNOTSUPP;
+}
+
+static int um780xtx_read_string(struct device *dev,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ static const char * const labels[] = { "CPU", "SYS" };
+
+ if (type == hwmon_temp && attr == hwmon_temp_label)
+ *str = labels[channel];
+ else if (type == hwmon_fan && attr == hwmon_fan_label)
+ *str = labels[channel];
+ else
+ return -EOPNOTSUPP;
+ return 0;
+}
+
+static const struct hwmon_ops um780xtx_hwmon_ops = {
+ .is_visible = um780xtx_is_visible,
+ .read = um780xtx_read,
+ .write = um780xtx_write,
+ .read_string = um780xtx_read_string,
+};
+
+static const struct hwmon_channel_info * const um780xtx_info[] = {
+ HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL),
+ HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL,
+ HWMON_F_INPUT | HWMON_F_LABEL),
+ HWMON_CHANNEL_INFO(pwm, HWMON_PWM_ENABLE),
+ NULL
+};
+
+static const struct hwmon_chip_info um780xtx_chip_info = {
+ .ops = &um780xtx_hwmon_ops,
+ .info = um780xtx_info,
+};
+
+static int um780xtx_write_sys_point(u8 offset, u8 value)
+{
+ u8 readback;
+ int ret;
+
+ ret = ec_write(offset, value);
+ if (ret)
+ return ret;
+ ret = ec_read(offset, &readback);
+ if (ret)
+ return ret;
+
+ return readback == value ? 0 : -EIO;
+}
+
+static int um780xtx_restore_sys_points(struct um780xtx_data *data,
+ u8 current_point2)
+{
+ u8 point1 = data->saved_sys_point1;
+ u8 point2 = data->saved_sys_point2;
+ int ret;
+
+ /* Keep strict ordering valid after each individual EC write. */
+ if (point1 >= current_point2) {
+ ret = um780xtx_write_sys_point(UM780XTX_EC_SYS_POINT2, point2);
+ if (ret)
+ return ret;
+ return um780xtx_write_sys_point(UM780XTX_EC_SYS_POINT1,
+ point1);
+ }
+
+ ret = um780xtx_write_sys_point(UM780XTX_EC_SYS_POINT1, point1);
+ if (ret)
+ return ret;
+ return um780xtx_write_sys_point(UM780XTX_EC_SYS_POINT2, point2);
+}
+
+static int um780xtx_read_initial_state(struct um780xtx_data *data)
+{
+ u8 profile;
+ u8 point1;
+ u8 point2;
+ int ret;
+
+ ret = ec_read(UM780XTX_EC_CPU_PROFILE, &profile);
+ if (ret)
+ return ret;
+ if (profile != UM780XTX_EC_PROFILE_B1 &&
+ profile != UM780XTX_EC_PROFILE_B2)
+ return -EINVAL;
+
+ ret = ec_read(UM780XTX_EC_SYS_POINT1, &point1);
+ if (ret)
+ return ret;
+ ret = ec_read(UM780XTX_EC_SYS_POINT2, &point2);
+ if (ret)
+ return ret;
+ if (point1 >= point2 || point2 >= data->sys_point3)
+ return -EINVAL;
+
+ data->saved_profile = profile;
+ data->saved_sys_point1 = point1;
+ data->saved_sys_point2 = point2;
+ return 0;
+}
+
+static int um780xtx_restore_state(struct um780xtx_data *data)
+{
+ u8 readback;
+ u8 point2;
+ int ret;
+
+ ret = ec_transaction(data->saved_profile, NULL, 0, NULL, 0);
+ if (ret)
+ return ret;
+ ret = ec_read(UM780XTX_EC_CPU_PROFILE, &readback);
+ if (ret)
+ return ret;
+ if (readback != data->saved_profile)
+ return -EIO;
+
+ ret = ec_read(UM780XTX_EC_SYS_POINT2, &point2);
+ if (ret)
+ return ret;
+
+ return um780xtx_restore_sys_points(data, point2);
+}
+
+static int um780xtx_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct um780xtx_data *data;
+ struct device *hwmon;
+ int ret;
+
+ data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+ platform_set_drvdata(pdev, data);
+
+ ret = ec_read(UM780XTX_EC_SYS_POINT3, &data->sys_point3);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to read system fan limit\n");
+ ret = um780xtx_read_initial_state(data);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to read initial fan settings\n");
+
+ hwmon = devm_hwmon_device_register_with_info(dev, "um780xtx_ec", data,
+ &um780xtx_chip_info,
+ um780xtx_extra_groups);
+ if (IS_ERR(hwmon))
+ return PTR_ERR(hwmon);
+ data->hwmon_dev = hwmon;
+
+ return 0;
+}
+
+static int um780xtx_resume(struct device *dev)
+{
+ struct um780xtx_data *data = dev_get_drvdata(dev);
+
+ guard(hwmon_lock)(data->hwmon_dev);
+ return um780xtx_restore_state(data);
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(um780xtx_pm_ops, NULL, um780xtx_resume);
+
+static struct platform_driver um780xtx_driver = {
+ .driver = {
+ .name = "um780xtx-ec-hwmon",
+ .pm = pm_sleep_ptr(&um780xtx_pm_ops),
+ },
+ .probe = um780xtx_probe,
+};
+
+static int __init um780xtx_init(void)
+{
+ int ret;
+
+ if (!dmi_check_system(um780xtx_dmi_table) ||
+ !um780xtx_firmware_match() || !ec_get_handle())
+ return -ENODEV;
+ ret = platform_driver_register(&um780xtx_driver);
+ if (ret)
+ return ret;
+
+ um780xtx_pdev = platform_device_register_simple("um780xtx-ec-hwmon",
+ PLATFORM_DEVID_NONE,
+ NULL, 0);
+ if (IS_ERR(um780xtx_pdev)) {
+ ret = PTR_ERR(um780xtx_pdev);
+ platform_driver_unregister(&um780xtx_driver);
+ return ret;
+ }
+ return 0;
+}
+
+static void __exit um780xtx_exit(void)
+{
+ platform_device_unregister(um780xtx_pdev);
+ platform_driver_unregister(&um780xtx_driver);
+}
+
+module_init(um780xtx_init);
+module_exit(um780xtx_exit);
+
+MODULE_AUTHOR("Sebastián Peyrott <speyrott@gmail.com>");
+MODULE_DESCRIPTION("Minisforum UM780 XTX EC hwmon driver");
+MODULE_LICENSE("GPL");