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 <[email protected]>
---
 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 <[email protected]>
 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 <[email protected]>");
+MODULE_AUTHOR("Ramiro Oliveira <[email protected]>");
+MODULE_DESCRIPTION("Hardware monitor driver for Advantech EIO embedded 
controller");
+MODULE_LICENSE("GPL");

-- 
2.43.0

Reply via email to