[PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver
From: Ramiro Oliveira <hidden>
Date: 2026-07-14 15:55:09
Also in:
dri-devel, linux-gpio, linux-hwmon, linux-i2c, linux-pm, linux-watchdog, lkml
Subsystem:
hardware monitoring, the rest · Maintainers:
Guenter Roeck, Linus Torvalds
This driver controls the Hardware Monitor block of the Advantech EIO chip. There is a separate driver for the thermal device driver since it's a use case we support currently, but if it's really necessary we can move it to the same driver. Signed-off-by: Ramiro Oliveira <redacted> --- MAINTAINERS | 1 + drivers/hwmon/Kconfig | 10 + drivers/hwmon/Makefile | 1 + drivers/hwmon/eio-hwmon.c | 461 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 473 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index a7da47393815..6d727d5182e4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS@@ -613,6 +613,7 @@ ADVANTECH EIO DRIVER M: Ramiro Oliveira <ramiro.oliveira@advantech.com> S: Maintained F: drivers/gpio/gpio-eio.c +F: drivers/hwmon/eio-hwmon.c F: drivers/mfd/eio_core.c F: include/linux/mfd/eio.h
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 08c29685126a..b8fd8ded846a 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig@@ -2094,6 +2094,16 @@ config SENSORS_EIC7700_PVT This driver can also be built as a module. If so, the module will be called eic7700-pvt. +config SENSORS_EIO + tristate "Advantech EIO HWMON" + depends on MFD_EIO + help + If you say yes here you get support for the Advantech EIO + temperature, voltage and fan speed monitoring block. + + This driver can also be built as a module. If so, the module + will be called eio-hwmon + config SENSORS_EMC1403 tristate "SMSC EMC1403/23 thermal sensor" depends on I2C
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 63809eeec2f4..8e81a2bdc99b 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile@@ -73,6 +73,7 @@ obj-$(CONFIG_SENSORS_DRIVETEMP) += drivetemp.o obj-$(CONFIG_SENSORS_DS620) += ds620.o obj-$(CONFIG_SENSORS_DS1621) += ds1621.o obj-$(CONFIG_SENSORS_EIC7700_PVT) += eic7700-pvt.o +obj-$(CONFIG_SENSORS_EIO) += eio-hwmon.o obj-$(CONFIG_SENSORS_EMC1403) += emc1403.o obj-$(CONFIG_SENSORS_EMC1812) += emc1812.o obj-$(CONFIG_SENSORS_EMC2103) += emc2103.o
diff --git a/drivers/hwmon/eio-hwmon.c b/drivers/hwmon/eio-hwmon.c
new file mode 100644
index 000000000000..dd877664e9e8
--- /dev/null
+++ b/drivers/hwmon/eio-hwmon.c@@ -0,0 +1,461 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Hardware Monitoring driver for Advantech EIO embedded controller. + * + * Copyright (C) 2025 Advantech Corporation. All rights reserved. + */ + +#include <linux/errno.h> +#include <linux/hwmon.h> +#include <linux/mfd/core.h> +#include <linux/mfd/eio.h> +#include <linux/module.h> + +#define MAX_CMD_SIZE 4 +#define MAX_NAME 32 + +#define EIO_ITEM_INPUT 1 +#define EIO_ITEM_MAX 2 +#define EIO_ITEM_MIN 3 +#define EIO_ITEM_CRIT 4 +#define EIO_ITEM_EMERGENCY 5 + +#define EIO_MAX_IN 8 +#define EIO_MAX_CURR 2 +#define EIO_MAX_TEMP 4 +#define EIO_MAX_FAN 4 +#define EIO_MAX_INTRUSION 1 + +enum _sen_type { + NONE, + VOLTAGE, + CURRENT, + TEMP, + FAN, + CASEOPEN, +}; + +struct eio_chan { + bool available; + u8 label_idx; +}; + +struct eio_hwmon_dev { + struct device *mfd; + struct eio_chan in[EIO_MAX_IN]; + struct eio_chan curr[EIO_MAX_CURR]; + struct eio_chan temp[EIO_MAX_TEMP]; + struct eio_chan fan[EIO_MAX_FAN]; + struct eio_chan intrusion; +}; + +static struct { + u8 cmd; + u8 max; + signed int shift; + char name[MAX_NAME]; + u8 ctrl[16]; + u16 multi[16]; + char item[16][MAX_NAME]; + char labels[32][MAX_NAME]; +} sen_info[] = { + { 0x00, 0, 0, "none" }, + { 0x12, 8, 0, "in", + { 0xFF, 0x10, 0x11, 0x12 }, + { 1, 10, 10, 10 }, + { "label", "input", "max", "min" }, + { "5V", "5Vs5", "12V", "12Vs5", + "3V3", "3V3", "5Vsb", "3Vsb", + "Vcmos", "Vbat", "Vdc", "Vstb", + "Vcore_a", "Vcore_b", "", "", + "Voem0", "Voem1", "Voem2", "Voem3" + }, + }, + { 0x1a, 2, 0, "curr", + { 0xFF, 0x10, 0x11, 0x12 }, + { 1, 10, 10, 10 }, + { "label", "input", "max", "min" }, + { "dc", "oem0" }, + }, + { 0x10, 4, -2731, "temp", + { 0xFF, 0x10, 0x11, 0x12, 0x21, 0x41 }, + { 1, 100, 100, 100, 100, 100 }, + { "label", "input", "max", "min", "crit", "emergency" }, + { "cpu0", "cpu1", "cpu2", "cpu3", + "sys0", "sys1", "sys2", "sys3", + "aux0", "aux1", "aux2", "aux3", + "dimm0", "dimm1", "dimm2", "dimm3", + "pch", "gpu", "", "", + "", "", "", "", + "", "", "", "", + "oem0", "oem1", "oem", "oem3" }, + }, + { 0x24, 4, 0, "fan", + { 0xFF, 0x1A }, + { 1, 1 }, + { "label", "input"}, + { "cpu0", "cpu1", "cpu2", "cpu3", + "sys0", "sys1", "sys2", "sys3", + "", "", "", "", "", "", "", "", + "", "", "", "", "", "", "", "", + "", "", "", "", + "oem0", "oem1", "oem2", "oem3", + }, + }, + { 0x28, 1, 0, "intrusion", + { 0xFF, 0x02 }, + { 1, 1 }, + { "label", "input" }, + { "case_open" } + } +}; + +static struct { + enum _sen_type type; + u8 ctrl; + int size; + bool write; +} ctrl_para[] = { + { NONE, 0x00, 0, false }, + + { VOLTAGE, 0x00, 1, false }, { VOLTAGE, 0x01, 1, false }, + { VOLTAGE, 0x10, 2, false }, { VOLTAGE, 0x11, 2, false }, + { VOLTAGE, 0x12, 2, false }, + + { CURRENT, 0x00, 1, false }, { CURRENT, 0x01, 1, false }, + { CURRENT, 0x10, 2, false }, { CURRENT, 0x11, 2, false }, + { CURRENT, 0x12, 2, false }, + + { TEMP, 0x00, 2, false }, { TEMP, 0x01, 1, false }, + { TEMP, 0x04, 1, false }, { TEMP, 0x10, 2, false }, + { TEMP, 0x11, 2, false }, { TEMP, 0x12, 2, false }, + { TEMP, 0x21, 2, false }, { TEMP, 0x41, 2, false }, + + { FAN, 0x00, 1, false }, { FAN, 0x01, 1, false }, + { FAN, 0x03, 1, true }, { FAN, 0x1A, 2, false }, + + { CASEOPEN, 0x00, 1, false }, { CASEOPEN, 0x02, 1, true }, +}; + +static int para_idx(enum _sen_type type, u8 ctrl) +{ + int i; + + for (i = 1 ; i < ARRAY_SIZE(ctrl_para) ; i++) + if (type == ctrl_para[i].type && + ctrl == ctrl_para[i].ctrl) + return i; + + return 0; +} + +static int pmc_read(struct device *mfd, enum _sen_type type, u8 dev_id, u8 ctrl, void *data) +{ + int idx = para_idx(type, ctrl); + int ret = 0; + + if (idx == 0) + return -EINVAL; + + if (WARN_ON(!data)) + return -EINVAL; + + struct pmc_op op = { + .cmd = sen_info[type].cmd | EIO_FLAG_PMC_READ, + .control = ctrl, + .device_id = dev_id, + .size = ctrl_para[idx].size, + .payload = (u8 *)data, + }; + + ret = eio_core_pmc_operation(mfd, &op); + return ret; +} + + +static umode_t eio_is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + const struct eio_hwmon_dev *eio = drvdata; + + switch (type) { + case hwmon_in: + return (channel < EIO_MAX_IN && eio->in[channel].available) ? 0444 : 0; + case hwmon_curr: + return (channel < EIO_MAX_CURR && eio->curr[channel].available) ? 0444 : 0; + case hwmon_temp: + return (channel < EIO_MAX_TEMP && eio->temp[channel].available) ? 0444 : 0; + case hwmon_fan: + return (channel < EIO_MAX_FAN && eio->fan[channel].available) ? 0444 : 0; + case hwmon_intrusion: + return (channel < EIO_MAX_INTRUSION && eio->intrusion.available) ? 0444 : 0; + default: + return 0; + } +} + +static int eio_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct eio_hwmon_dev *eio = dev_get_drvdata(dev); + u8 data[MAX_CMD_SIZE] = {}; + enum _sen_type ec_type; + u8 item; + u32 raw; + int ret; + + switch (type) { + case hwmon_in: + switch (attr) { + case hwmon_in_input: + item = EIO_ITEM_INPUT; + break; + case hwmon_in_max: + item = EIO_ITEM_MAX; + break; + case hwmon_in_min: + item = EIO_ITEM_MIN; + break; + default: + return -EOPNOTSUPP; + } + ec_type = VOLTAGE; + break; + case hwmon_curr: + switch (attr) { + case hwmon_curr_input: + item = EIO_ITEM_INPUT; + break; + case hwmon_curr_max: + item = EIO_ITEM_MAX; + break; + case hwmon_curr_min: + item = EIO_ITEM_MIN; + break; + default: + return -EOPNOTSUPP; + } + ec_type = CURRENT; + break; + case hwmon_temp: + switch (attr) { + case hwmon_temp_input: + item = EIO_ITEM_INPUT; + break; + case hwmon_temp_max: + item = EIO_ITEM_MAX; + break; + case hwmon_temp_min: + item = EIO_ITEM_MIN; + break; + case hwmon_temp_crit: + item = EIO_ITEM_CRIT; + break; + case hwmon_temp_emergency: + item = EIO_ITEM_EMERGENCY; + break; + default: + return -EOPNOTSUPP; + } + ec_type = TEMP; + break; + case hwmon_fan: + if (attr != hwmon_fan_input) + return -EOPNOTSUPP; + item = EIO_ITEM_INPUT; + ec_type = FAN; + break; + case hwmon_intrusion: + if (attr != hwmon_intrusion_alarm) + return -EOPNOTSUPP; + item = EIO_ITEM_INPUT; + ec_type = CASEOPEN; + channel = 0; + break; + default: + return -EOPNOTSUPP; + } + + ret = pmc_read(eio->mfd, ec_type, channel, sen_info[ec_type].ctrl[item], data); + if (ret) + return ret; + + raw = (u32)data[0] | (u32)data[1] << 8 | (u32)data[2] << 16 | (u32)data[3] << 24; + *val = ((long)(signed int)raw + sen_info[ec_type].shift) * + (long)sen_info[ec_type].multi[item]; + return 0; +} + +static int eio_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + struct eio_hwmon_dev *eio = dev_get_drvdata(dev); + enum _sen_type ec_type; + u8 label_idx; + + switch (type) { + case hwmon_in: + if (attr != hwmon_in_label) + return -EOPNOTSUPP; + ec_type = VOLTAGE; + label_idx = eio->in[channel].label_idx; + break; + case hwmon_curr: + if (attr != hwmon_curr_label) + return -EOPNOTSUPP; + ec_type = CURRENT; + label_idx = eio->curr[channel].label_idx; + break; + case hwmon_temp: + if (attr != hwmon_temp_label) + return -EOPNOTSUPP; + ec_type = TEMP; + label_idx = eio->temp[channel].label_idx; + break; + case hwmon_fan: + if (attr != hwmon_fan_label) + return -EOPNOTSUPP; + ec_type = FAN; + label_idx = eio->fan[channel].label_idx; + break; + default: + return -EOPNOTSUPP; + } + + if (label_idx >= ARRAY_SIZE(sen_info[ec_type].labels)) + return -EOPNOTSUPP; + + *str = sen_info[ec_type].labels[label_idx]; + return 0; +} + +static const struct hwmon_ops eio_hwmon_ops = { + .is_visible = eio_is_visible, + .read = eio_read, + .read_string = eio_read_string, +}; + +static const struct hwmon_channel_info * const eio_hwmon_info[] = { + HWMON_CHANNEL_INFO(in, + HWMON_I_LABEL | HWMON_I_INPUT | HWMON_I_MAX | HWMON_I_MIN, + HWMON_I_LABEL | HWMON_I_INPUT | HWMON_I_MAX | HWMON_I_MIN, + HWMON_I_LABEL | HWMON_I_INPUT | HWMON_I_MAX | HWMON_I_MIN, + HWMON_I_LABEL | HWMON_I_INPUT | HWMON_I_MAX | HWMON_I_MIN, + HWMON_I_LABEL | HWMON_I_INPUT | HWMON_I_MAX | HWMON_I_MIN, + HWMON_I_LABEL | HWMON_I_INPUT | HWMON_I_MAX | HWMON_I_MIN, + HWMON_I_LABEL | HWMON_I_INPUT | HWMON_I_MAX | HWMON_I_MIN, + HWMON_I_LABEL | HWMON_I_INPUT | HWMON_I_MAX | HWMON_I_MIN), + HWMON_CHANNEL_INFO(curr, + HWMON_C_LABEL | HWMON_C_INPUT | HWMON_C_MAX | HWMON_C_MIN, + HWMON_C_LABEL | HWMON_C_INPUT | HWMON_C_MAX | HWMON_C_MIN), + HWMON_CHANNEL_INFO(temp, + HWMON_T_LABEL | HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_MIN | HWMON_T_CRIT | HWMON_T_EMERGENCY, + HWMON_T_LABEL | HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_MIN | HWMON_T_CRIT | HWMON_T_EMERGENCY, + HWMON_T_LABEL | HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_MIN | HWMON_T_CRIT | HWMON_T_EMERGENCY, + HWMON_T_LABEL | HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_MIN | HWMON_T_CRIT | HWMON_T_EMERGENCY), + HWMON_CHANNEL_INFO(fan, + HWMON_F_LABEL | HWMON_F_INPUT, + HWMON_F_LABEL | HWMON_F_INPUT, + HWMON_F_LABEL | HWMON_F_INPUT, + HWMON_F_LABEL | HWMON_F_INPUT), + HWMON_CHANNEL_INFO(intrusion, HWMON_INTRUSION_ALARM), + NULL, +}; + +static const struct hwmon_chip_info eio_chip_info = { + .ops = &eio_hwmon_ops, + .info = eio_hwmon_info, +}; + +static int hwmon_init(struct device *mfd, struct eio_hwmon_dev *eio) +{ + enum _sen_type type; + u8 i, data[MAX_CMD_SIZE]; + int n = 0; + int ret; + + for (type = VOLTAGE ; type <= TEMP ; type++) { + struct eio_chan *chans = (type == VOLTAGE) ? eio->in : + (type == CURRENT) ? eio->curr : eio->temp; + + for (i = 0 ; i < sen_info[type].max ; i++) { + memset(data, 0, sizeof(data)); + if (pmc_read(mfd, type, i, 0x00, data) || + (data[0] & 0x01) == 0) + continue; + + memset(data, 0, sizeof(data)); + ret = pmc_read(mfd, type, i, 0x01, data); + if (ret != 0 && ret != -EINVAL) + continue; + + chans[i].available = true; + chans[i].label_idx = data[0]; + n++; + } + } + + for (i = 0 ; i < sen_info[FAN].max ; i++) { + memset(data, 0, sizeof(data)); + if (pmc_read(mfd, FAN, i, 0x00, data) || + (data[0] & 0x01) == 0) + continue; + + memset(data, 0, sizeof(data)); + ret = pmc_read(mfd, FAN, i, 0x01, data); + if (ret != 0 && ret != -EINVAL) + continue; + + eio->fan[i].available = true; + eio->fan[i].label_idx = data[0]; + n++; + } + + memset(data, 0, sizeof(data)); + if (!pmc_read(mfd, CASEOPEN, 0, 0x00, data) && (data[0] & 0x01)) { + eio->intrusion.available = true; + n++; + } + + return n; +} + +static int hwmon_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct eio_hwmon_dev *eio; + struct eio_dev *eio_dev = dev_get_drvdata(dev->parent); + struct device *hwmon; + + if (!eio_dev) { + dev_err(dev, "Error contact eio_core\n"); + return -ENODEV; + } + + eio = devm_kzalloc(dev, sizeof(*eio), GFP_KERNEL); + if (!eio) + return -ENOMEM; + + eio->mfd = dev->parent; + + if (hwmon_init(dev->parent, eio) <= 0) + return -ENODEV; + + hwmon = devm_hwmon_device_register_with_info(dev, KBUILD_MODNAME, + eio, &eio_chip_info, NULL); + return PTR_ERR_OR_ZERO(hwmon); +} + +static struct platform_driver eio_hwmon_driver = { + .probe = hwmon_probe, + .driver = { + .name = "eio_hwmon", + }, +}; + +module_platform_driver(eio_hwmon_driver); + +MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>"); +MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>"); +MODULE_DESCRIPTION("Hardware monitor driver for Advantech EIO embedded controller"); +MODULE_LICENSE("GPL");
--
2.43.0