* [PATCH v2 0/8] Add support for Advantech EIO MFD series devices
@ 2026-07-14 15:54 Ramiro Oliveira
2026-07-14 15:54 ` [PATCH v2 1/8] Add Advantech EIO driver Ramiro Oliveira
` (7 more replies)
0 siblings, 8 replies; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
This series implements a set of drivers allowing to support the
Advantech EIO-2xx series of devices.
This includes GPIO, hwmon, I2C bus, backlight controller, watchdog,
thermal and a fan driver.
This series of patches targets several different subsystems, but the MFD
subsystem is the main target.
Changes in v2:
- Rebased on v7.2
MFD
- code cleanup
- Remove timeout from sysfs
- Propagate errors
- Remove version.h
GPIO
- Code cleanup
- Move gpio_chip to probe
- Remove timeout from sysfs
HWMON
- Code cleanup
- Refactor to use hwmon register with info
- Remove tacho, same interfaces as fan
I2C
- Remove timeout from sysfs
Backlight
- Code cleanup, remove ununsed max parameter
- Remove timeout sysfs
- Use helper functions instead of direct calls
- Correct return value in bl_init
- Initializing scal ein backlight_properties
- Return correct value when no backlight is enabled
- Remove log on successful probe
Watchdog
- Remove timeout and IRQ from sysfs
- Remove unnecessary logging from set_timeout/pretimeout wdt_get_type
- Remove timeleft function
- Rewrite wdt_set_irq_io to correctly handle return codes
- Remove unneeded watchdog_init_timeout
Thermal
- Remote timeout from sysfs
Fan
- Remove timeout from sysfs
- Remove unused variable temps_mc
- Link to v1: https://lore.kernel.org/all/20251212-upstream-v1-v1-0-d50d40ec8d8a@advantech.com/
Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
Ramiro Oliveira (8):
Add Advantech EIO driver
Add Advantech EIO GPIO driver
Add Advantech EIO Hardware Monitor driver
Add Advantech EIO I2C driver
Add Advantech EIO Backlight driver
Add Advantech EIO Watchdog driver
Add Advantech EIO Thermal driver
Add Advantech EIO Fan driver
MAINTAINERS | 19 +
drivers/gpio/Kconfig | 6 +
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-eio.c | 252 +++++++++
drivers/hwmon/Kconfig | 10 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/eio-hwmon.c | 461 ++++++++++++++++
drivers/i2c/busses/Kconfig | 6 +
drivers/i2c/busses/Makefile | 1 +
drivers/i2c/busses/i2c-eio.c | 1135 ++++++++++++++++++++++++++++++++++++++
drivers/mfd/Kconfig | 10 +
drivers/mfd/Makefile | 1 +
drivers/mfd/eio_core.c | 644 +++++++++++++++++++++
drivers/thermal/Kconfig | 17 +
drivers/thermal/Makefile | 2 +
drivers/thermal/eio_fan.c | 478 ++++++++++++++++
drivers/thermal/eio_thermal.c | 346 ++++++++++++
drivers/video/backlight/Kconfig | 6 +
drivers/video/backlight/Makefile | 1 +
drivers/video/backlight/eio_bl.c | 243 ++++++++
drivers/watchdog/Kconfig | 7 +
drivers/watchdog/Makefile | 1 +
drivers/watchdog/eio_wdt.c | 641 +++++++++++++++++++++
include/linux/mfd/eio.h | 124 +++++
24 files changed, 4413 insertions(+)
---
base-commit: 50c051a08a731c52933608fa8e9174cd9875fcc4
change-id: 20260714-upstream-v2-beaec2baac0d
Best regards,
--
Ramiro Oliveira <ramiro.oliveira@advantech.com>
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v2 1/8] Add Advantech EIO driver
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
@ 2026-07-14 15:54 ` Ramiro Oliveira
2026-07-14 16:08 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 2/8] Add Advantech EIO GPIO driver Ramiro Oliveira
` (6 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
Creating the core driver for Advantech EIO, all other drivers depend on this.
This core driver provides register access and coordination for the EIO's
subdevices (GPIO, watchdog, hwmon, thermal, backlight, I2C).
This driver supports EIO-IS200, EIO-201, EIO-210 and EIO-211, these
devices are available in several of Advantech x86-based boards, and
provide several different interfaces like I2C, GPIO, Watchdog, Hardware
Monitoring, Thermal Control, and Backlight control.
Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
MAINTAINERS | 6 +
drivers/mfd/Kconfig | 10 +
drivers/mfd/Makefile | 1 +
drivers/mfd/eio_core.c | 644 ++++++++++++++++++++++++++++++++++++++++++++++++
include/linux/mfd/eio.h | 124 ++++++++++
5 files changed, 785 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index dcffe9fc54f6..53b5f7412966 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -615,6 +615,12 @@ L: platform-driver-x86@vger.kernel.org
S: Maintained
F: drivers/platform/x86/adv_swbutton.c
+ADVANTECH EIO DRIVER
+M: Ramiro Oliveira <ramiro.oliveira@advantech.com>
+S: Maintained
+F: drivers/mfd/eio_core.c
+F: include/linux/mfd/eio.h
+
ADXL313 THREE-AXIS DIGITAL ACCELEROMETER DRIVER
M: Lucas Stankus <lucas.p.stankus@gmail.com>
S: Supported
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 35f6e9b76d05..42d133a2eb8d 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -517,6 +517,16 @@ config MFD_DLN2
etc. must be enabled in order to use the functionality of
the device.
+config MFD_EIO
+ tristate "Advantech EIO core"
+ select MFD_CORE
+ help
+ This enables support for the Advantech EIO multi-function device.
+ This core driver provides register access and coordination for the
+ EIO's subdevices (GPIO, watchdog, hwmon, thermal, backlight, I2C).
+ This driver supports EIO-IS200, EIO-201, EIO-210 and EIO-211.
+
+
config MFD_ENE_KB3930
tristate "ENE KB3930 Embedded Controller support"
depends on I2C
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index dd4bb7e77c33..2013de45528c 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -19,6 +19,7 @@ obj-$(CONFIG_MFD_CROS_EC_DEV) += cros_ec_dev.o
obj-$(CONFIG_MFD_CS42L43) += cs42l43.o
obj-$(CONFIG_MFD_CS42L43_I2C) += cs42l43-i2c.o
obj-$(CONFIG_MFD_CS42L43_SDW) += cs42l43-sdw.o
+obj-$(CONFIG_MFD_EIO) += eio_core.o
obj-$(CONFIG_MFD_ENE_KB3930) += ene-kb3930.o
obj-$(CONFIG_MFD_EXYNOS_LPASS) += exynos-lpass.o
obj-$(CONFIG_MFD_GATEWORKS_GSC) += gateworks-gsc.o
diff --git a/drivers/mfd/eio_core.c b/drivers/mfd/eio_core.c
new file mode 100644
index 000000000000..25873c061812
--- /dev/null
+++ b/drivers/mfd/eio_core.c
@@ -0,0 +1,644 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Advantech Embedded Controller base Driver
+ *
+ * This driver provides an interface to access the EIO Series EC
+ * firmware via its own Power Management Channel (PMC) for subdrivers:
+ *
+ * A system may have one or two independent EIO devices.
+ *
+ * Copyright (C) 2025 Advantech Co., Ltd.
+ */
+
+#include <linux/delay.h>
+#include <linux/isa.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/sysfs.h>
+#include <linux/time.h>
+#include <linux/uaccess.h>
+
+#define DEFAULT_TIMEOUT 5000
+#define SLEEP_MAX 200
+#define NUM_EC 1
+
+struct eio_dev_port {
+ u16 idx_port;
+ u16 data_port;
+};
+
+static struct eio_dev_port pnp_port[] = {
+ { .idx_port = EIO_PNP_INDEX, .data_port = EIO_PNP_DATA },
+ { .idx_port = EIO_SUB_PNP_INDEX, .data_port = EIO_SUB_PNP_DATA },
+};
+
+static struct mfd_cell eio_devs[] = {
+ MFD_CELL_NAME("eio_wdt"),
+ MFD_CELL_NAME("gpio_eio"),
+ MFD_CELL_NAME("eio_hwmon"),
+ MFD_CELL_NAME("i2c_eio"),
+ MFD_CELL_NAME("eio_thermal"),
+ MFD_CELL_NAME("eio_fan"),
+ MFD_CELL_NAME("eio_bl"),
+};
+
+static const struct regmap_range eio_range[] = {
+ regmap_reg_range(EIO_PNP_INDEX, EIO_PNP_DATA),
+ regmap_reg_range(EIO_SUB_PNP_INDEX, EIO_SUB_PNP_DATA),
+ regmap_reg_range(0x200, 0x3FF),
+};
+
+static const struct regmap_access_table volatile_regs = {
+ .yes_ranges = eio_range,
+ .n_yes_ranges = ARRAY_SIZE(eio_range),
+};
+
+static const struct regmap_config pnp_regmap_config = {
+ .name = "eio_core",
+ .reg_bits = 16,
+ .val_bits = 8,
+ .volatile_table = &volatile_regs,
+ .io_port = true,
+ .cache_type = REGCACHE_NONE,
+};
+
+static struct {
+ char name[32];
+ int cmd;
+ int ctrl;
+ int dev;
+ int size;
+ enum {
+ HEX,
+ NUMBER,
+ PNP_ID,
+ } type;
+} attrs[] = {
+ { "board_name", 0x53, 0x10, 0, 16 },
+ { "board_serial", 0x53, 0x1F, 0, 16 },
+ { "board_manufacturer", 0x53, 0x11, 0, 16 },
+ { "board_id", 0x53, 0x1E, 0, 4 },
+ { "firmware_version", 0x53, 0x21, 0, 4 },
+ { "firmware_name", 0x53, 0x22, 0, 16 },
+ { "firmware_build", 0x53, 0x23, 0, 26 },
+ { "firmware_date", 0x53, 0x24, 0, 16 },
+ { "chip_id", 0x53, 0x12, 0, 12 },
+ { "chip_detect", 0x53, 0x15, 0, 12 },
+ { "platform_type", 0x53, 0x13, 0, 16 },
+ { "platform_revision", 0x53, 0x04, 0x44, 4 },
+ { "eapi_version", 0x53, 0x04, 0x64, 4 },
+ { "eapi_id", 0x53, 0x31, 0, 4 },
+ { "boot_count", 0x55, 0x10, 0, 4, NUMBER },
+ { "powerup_hour", 0x55, 0x11, 0, 4, NUMBER },
+ { "pnp_id", 0x53, 0x04, 0x68, 4, PNP_ID },
+};
+
+static ssize_t info_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ uint i;
+
+ for (i = 0; i < ARRAY_SIZE(attrs); i++) {
+ int ret;
+ char str[32] = "";
+ int val;
+
+ struct pmc_op op = {
+ .cmd = attrs[i].cmd,
+ .control = attrs[i].ctrl,
+ .device_id = attrs[i].dev,
+ .payload = (u8 *)str,
+ .size = attrs[i].size,
+ };
+
+ if (strcmp(attr->attr.name, attrs[i].name))
+ continue;
+
+ ret = eio_core_pmc_operation(dev, &op);
+ if (ret)
+ return ret;
+
+ if (attrs[i].size != 4)
+ return sysfs_emit(buf, "%s\n", str);
+
+ val = *(u32 *)str;
+
+ if (attrs[i].type == HEX)
+ return sysfs_emit(buf, "0x%08X\n", val);
+
+ if (attrs[i].type == NUMBER)
+ return sysfs_emit(buf, "%d\n", val);
+
+ if (attrs[i].type == PNP_ID)
+ return sysfs_emit(buf, "%c%c%c, %X\n",
+ (val >> 14 & 0x3F) + 0x40,
+ ((val >> 9 & 0x18) | (val >> 25 & 0x07)) + 0x40,
+ (val >> 20 & 0x1F) + 0x40,
+ val & 0xFFF);
+ }
+
+ return -EINVAL;
+}
+
+
+PMC_DEVICE_ATTR_RO(board_name);
+PMC_DEVICE_ATTR_RO(board_serial);
+PMC_DEVICE_ATTR_RO(board_manufacturer);
+PMC_DEVICE_ATTR_RO(firmware_name);
+PMC_DEVICE_ATTR_RO(firmware_version);
+PMC_DEVICE_ATTR_RO(firmware_build);
+PMC_DEVICE_ATTR_RO(firmware_date);
+PMC_DEVICE_ATTR_RO(chip_id);
+PMC_DEVICE_ATTR_RO(chip_detect);
+PMC_DEVICE_ATTR_RO(platform_type);
+PMC_DEVICE_ATTR_RO(platform_revision);
+PMC_DEVICE_ATTR_RO(board_id);
+PMC_DEVICE_ATTR_RO(eapi_version);
+PMC_DEVICE_ATTR_RO(eapi_id);
+PMC_DEVICE_ATTR_RO(boot_count);
+PMC_DEVICE_ATTR_RO(powerup_hour);
+PMC_DEVICE_ATTR_RO(pnp_id);
+
+static struct attribute *pmc_attrs[] = {
+ &dev_attr_board_name.attr,
+ &dev_attr_board_serial.attr,
+ &dev_attr_board_manufacturer.attr,
+ &dev_attr_firmware_name.attr,
+ &dev_attr_firmware_version.attr,
+ &dev_attr_firmware_build.attr,
+ &dev_attr_firmware_date.attr,
+ &dev_attr_chip_id.attr,
+ &dev_attr_chip_detect.attr,
+ &dev_attr_platform_type.attr,
+ &dev_attr_platform_revision.attr,
+ &dev_attr_board_id.attr,
+ &dev_attr_eapi_version.attr,
+ &dev_attr_eapi_id.attr,
+ &dev_attr_boot_count.attr,
+ &dev_attr_powerup_hour.attr,
+ &dev_attr_pnp_id.attr,
+ NULL
+};
+
+ATTRIBUTE_GROUPS(pmc);
+
+static int eio_pnp_read(struct device *dev, struct eio_dev_port *port, u8 idx, unsigned int *val)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ int ret;
+
+ ret = regmap_write(eio->map, port->idx_port, idx);
+ if (ret) {
+ dev_err(dev, "Error port write 0x%X\n", port->idx_port);
+ return ret;
+ }
+
+ ret = regmap_read(eio->map, port->data_port, val);
+ if (ret)
+ dev_err(dev, "Error port read 0x%X\n", port->data_port);
+
+ return ret;
+}
+
+static int eio_pnp_write(struct device *dev, struct eio_dev_port *port, u8 idx, u8 data)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ int ret;
+
+ ret = regmap_write(eio->map, port->idx_port, idx);
+ if (ret) {
+ dev_err(dev, "Error port write 0x%X %X\n", port->idx_port, port->data_port);
+ return ret;
+ }
+
+ ret = regmap_write(eio->map, port->data_port, data);
+ if (ret)
+ dev_err(dev, "Error port write 0x%X %X\n", port->idx_port, port->data_port);
+
+ return ret;
+}
+
+static int eio_unlock_port(struct device *dev, struct eio_dev_port *port)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ int ret;
+
+ /* Write 0x87 to index port twice to unlock IO port */
+ ret = regmap_write(eio->map, port->idx_port, EIO_EXT_MODE_ENTER);
+ if (ret) {
+ dev_err(dev, "Error entering PNP mode on 0x%X: %d\n", port->idx_port, ret);
+ return ret;
+ }
+
+ ret = regmap_write(eio->map, port->idx_port, EIO_EXT_MODE_ENTER);
+ if (ret)
+ dev_err(dev, "Error entering PNP mode on 0x%X: %d\n", port->idx_port, ret);
+
+ return ret;
+}
+
+static int eio_lock_port(struct device *dev, struct eio_dev_port *port)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ int ret;
+ /* Write 0xAA to index port once to lock IO port */
+ ret = regmap_write(eio->map, port->idx_port, EIO_EXT_MODE_EXIT);
+ if (ret)
+ dev_err(dev, "Error port write 0x%X\n", port->idx_port);
+
+ return ret;
+}
+
+static int pmc_write_data(struct device *dev, int id, u8 value)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ int ret;
+
+ ret = eio_core_pmc_wait(dev, id, PMC_WAIT_INPUT);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(eio->map, eio->pmc[id].data, value);
+ if (ret)
+ dev_err(dev, "Error PMC write %X:%X\n",
+ eio->pmc[id].data, value);
+
+ return ret;
+}
+
+static int pmc_write_cmd(struct device *dev, int id, u8 value)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ int ret;
+
+ ret = eio_core_pmc_wait(dev, id, PMC_WAIT_INPUT);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(eio->map, eio->pmc[id].cmd, value);
+ if (ret)
+ dev_err(dev, "Error PMC write %X:%X\n",
+ eio->pmc[id].cmd, value);
+
+ return ret;
+}
+
+static int pmc_read_data(struct device *dev, int id, u8 *value)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ unsigned int val;
+ int ret;
+
+ ret = eio_core_pmc_wait(dev, id, PMC_WAIT_OUTPUT);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(eio->map, eio->pmc[id].data, &val);
+ if (ret)
+ dev_err(dev, "Error PMC read %X\n", eio->pmc[id].data);
+ else
+ *value = (u8)(val & 0xFF);
+
+ return ret;
+}
+
+static int pmc_read_status(struct device *dev, int id)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ unsigned int val;
+
+ if (regmap_read(eio->map, eio->pmc[id].status, &val)) {
+ dev_err(dev, "Error PMC read %X\n",
+ eio->pmc[id].status);
+ return 0;
+ }
+
+ return val;
+}
+
+static void pmc_clear(struct device *dev, int id)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ unsigned int val;
+
+ /* Check if input buffer blocked */
+ if ((pmc_read_status(dev, id) & EIO_PMC_STATUS_IBF) == 0)
+ return;
+
+ /* Read out previous garbage */
+ if (regmap_read(eio->map, eio->pmc[id].data, &val))
+ dev_err(dev, "Error pmc clear\n");
+
+ usleep_range(10, 100);
+}
+
+int eio_core_pmc_wait(struct device *dev, int id, enum eio_pmc_wait wait)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ uint val;
+ int timeout = DEFAULT_TIMEOUT;
+
+ if (wait == PMC_WAIT_INPUT)
+ return regmap_read_poll_timeout(eio->map, eio->pmc[id].status,
+ val, (val & EIO_PMC_STATUS_IBF) == 0,
+ SLEEP_MAX, timeout);
+ return regmap_read_poll_timeout(eio->map,
+ eio->pmc[id].status, val,
+ (val & EIO_PMC_STATUS_OBF) != 0,
+ SLEEP_MAX, timeout);
+}
+EXPORT_SYMBOL_GPL(eio_core_pmc_wait);
+
+int eio_core_pmc_operation(struct device *dev, struct pmc_op *op)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ u8 i;
+ int ret;
+ bool reading = op->cmd & EIO_FLAG_PMC_READ;
+
+ mutex_lock(&eio->mutex);
+
+ pmc_clear(dev, op->chip);
+
+ ret = pmc_write_cmd(dev, op->chip, op->cmd);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(dev, op->chip, op->control);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(dev, op->chip, op->device_id);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(dev, op->chip, op->size);
+ if (ret)
+ goto err;
+
+ for (i = 0; i < op->size; i++) {
+ if (reading)
+ ret = pmc_read_data(dev, op->chip, &op->payload[i]);
+ else
+ ret = pmc_write_data(dev, op->chip, op->payload[i]);
+
+ if (ret)
+ break;
+ }
+
+err:
+ mutex_unlock(&eio->mutex);
+ if (ret)
+ return ret;
+ return 0;
+
+}
+EXPORT_SYMBOL_GPL(eio_core_pmc_operation);
+
+static int get_pmc_port(struct device *dev, int id,
+ struct eio_dev_port *port)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ struct _pmc_port *pmc = &eio->pmc[id];
+ int ret;
+ unsigned int high, low;
+
+ ret = eio_unlock_port(dev, port);
+ if (ret)
+ return ret;
+
+ /* Switch to PMC device page */
+ ret = eio_pnp_write(dev, port, EIO_LDN, EIO_LDN_PMC1);
+ if (ret)
+ return ret;
+
+ /* Active this device */
+ ret = eio_pnp_write(dev, port, EIO_LDAR, EIO_LDAR_LDACT);
+ if (ret)
+ return ret;
+
+ /* Get PMC data port */
+ ret = eio_pnp_read(dev, port, EIO_IOBA0H, &high);
+ if (ret)
+ return ret;
+ ret = eio_pnp_read(dev, port, EIO_IOBA0L, &low);
+ if (ret)
+ return ret;
+ pmc->data = (high << 8) | low;
+
+ /* Get PMC cmd */
+ ret = eio_pnp_read(dev, port, EIO_IOBA1H, &high);
+ if (ret)
+ return ret;
+ ret = eio_pnp_read(dev, port, EIO_IOBA1L, &low);
+ if (ret)
+ return ret;
+ pmc->cmd = (high << 8) | low;
+
+ /* Disable IRQ */
+ ret = eio_pnp_write(dev, port, EIO_IRQCTRL, 0);
+ if (ret)
+ return ret;
+
+ ret = eio_lock_port(dev, port);
+ if (ret)
+ return ret;
+
+ /* Make sure IO ports are not occupied */
+ if (!devm_request_region(dev, pmc->data, 2, KBUILD_MODNAME)) {
+ dev_err(dev, "Request region %X error\n", pmc->data);
+ return -EBUSY;
+ }
+
+ return 0;
+}
+
+static int eio_init(struct device *dev)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ u16 chip_id;
+ unsigned int msb, lsb;
+ int tmp;
+ int ret = -ENOMEM;
+
+ for (int chip = 0; chip < ARRAY_SIZE(pnp_port); chip++) {
+ struct eio_dev_port *port = pnp_port + chip;
+
+ if (!devm_request_region(dev, pnp_port[chip].idx_port,
+ pnp_port[chip].data_port -
+ pnp_port[chip].idx_port,
+ KBUILD_MODNAME))
+ continue;
+
+ ret = eio_unlock_port(dev, port);
+ if (ret)
+ return ret;
+
+ ret = eio_pnp_read(dev, port, EIO_CHIPID1, &msb);
+ if (ret)
+ return ret;
+
+ ret = eio_pnp_read(dev, port, EIO_CHIPID2, &lsb);
+ if (ret)
+ return ret;
+
+ chip_id = (msb << 8) | lsb;
+
+ if (chip_id != EIO200_CHIPID && chip_id != EIO201_211_CHIPID)
+ continue;
+
+ /* Turn on the enable flag */
+ ret = eio_pnp_read(dev, port, EIO_SIOCTRL, &tmp);
+ if (ret)
+ return ret;
+
+ tmp |= EIO_SIOCTRL_SIOEN;
+
+ ret = eio_pnp_write(dev, port, EIO_SIOCTRL, tmp);
+ if (ret)
+ return ret;
+
+ ret = eio_lock_port(dev, port);
+ if (ret)
+ return ret;
+
+ ret = get_pmc_port(dev, chip, port);
+ if (ret)
+ return ret;
+
+ if (chip == 0)
+ eio->flag |= EIO_F_CHIP_EXIST;
+ else
+ eio->flag |= EIO_F_SUB_CHIP_EXIST;
+ }
+
+ return ret;
+}
+
+static uint8_t acpi_ram_access(struct device *dev, uint8_t offset)
+{
+ uint8_t val;
+ int ret;
+ struct eio_dev *eio = dev_get_drvdata(dev);
+
+ /* We only store information on primary EC */
+ int chip = 0;
+
+ mutex_lock(&eio->mutex);
+
+ pmc_clear(dev, chip);
+
+ ret = pmc_write_cmd(dev, chip, EIO_PMC_CMD_ACPIRAM_READ);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(dev, chip, offset);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(dev, chip, sizeof(val));
+ if (ret)
+ goto err;
+
+ ret = pmc_read_data(dev, chip, &val);
+ if (ret)
+ goto err;
+
+ ret = val;
+
+err:
+ mutex_unlock(&eio->mutex);
+ return ret;
+}
+
+static int firmware_code_base(struct device *dev)
+{
+ struct eio_dev *eio = dev_get_drvdata(dev);
+ u8 ic_vendor, ic_code, code_base;
+
+ ic_vendor = acpi_ram_access(dev, EIO_ACPIRAM_ICVENDOR);
+ ic_code = acpi_ram_access(dev, EIO_ACPIRAM_ICCODE);
+ code_base = acpi_ram_access(dev, EIO_ACPIRAM_CODEBASE);
+
+ if (ic_vendor != 'R')
+ return -ENODEV;
+
+ if (ic_code != EIO200_ICCODE && ic_code != EIO201_ICCODE &&
+ ic_code != EIO211_ICCODE)
+ goto err;
+
+ if (code_base == EIO_ACPIRAM_CODEBASE_NEW) {
+ eio->flag |= EIO_F_NEW_CODE_BASE;
+ return 0;
+ }
+
+ if (code_base == 0 &&
+ (ic_code != EIO201_ICCODE && ic_code != EIO211_ICCODE)) {
+ dev_info(dev, "Old code base not supported.");
+ return -ENODEV;
+ }
+
+err:
+ /* Codebase error. This should only happen on firmware error. */
+ dev_err(dev,
+ "Codebase check fail: vendor: 0x%X, code: 0x%X, base: 0x%X\n",
+ ic_vendor, ic_code, code_base);
+ return -ENODEV;
+}
+
+static int eio_probe(struct device *dev, unsigned int id)
+{
+ int ret = 0;
+ struct eio_dev *eio;
+
+ eio = devm_kzalloc(dev, sizeof(*eio), GFP_KERNEL);
+ if (!eio)
+ return -ENOMEM;
+
+ eio->dev = dev;
+ mutex_init(&eio->mutex);
+
+ eio->iomem = devm_ioport_map(dev, 0, EIO_SUB_PNP_DATA + 1);
+ if (IS_ERR(eio->iomem))
+ return PTR_ERR(eio->iomem);
+
+ eio->map = devm_regmap_init_mmio(dev, eio->iomem, &pnp_regmap_config);
+ if (IS_ERR(eio->map))
+ return PTR_ERR(eio->map);
+
+ /* Publish instance for subdrivers (dev_get_drvdata(dev->parent)) */
+ dev_set_drvdata(dev, eio);
+
+ if (eio_init(dev))
+ return -ENODEV;
+
+ ret = firmware_code_base(dev);
+ if (ret) {
+ dev_err(dev, "Chip code base check fail\n");
+ return ret;
+ }
+
+ ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE,
+ eio_devs, ARRAY_SIZE(eio_devs),
+ NULL, 0, NULL);
+ if (ret)
+ dev_err(dev, "Cannot register child devices (error = %d)\n", ret);
+
+ return ret;
+}
+
+static struct isa_driver eio_driver = {
+ .probe = eio_probe,
+ .driver = {
+ .name = "eio_core",
+ .dev_groups = pmc_groups,
+ },
+};
+module_isa_driver(eio_driver, NUM_EC);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Advantech EIO series EC core driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/eio.h b/include/linux/mfd/eio.h
new file mode 100644
index 000000000000..c2daad41318b
--- /dev/null
+++ b/include/linux/mfd/eio.h
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2025 Advantech Co., Ltd.
+ */
+
+#ifndef _MFD_EIO_H_
+#define _MFD_EIO_H_
+
+#include <linux/io.h>
+#include <linux/regmap.h>
+
+/* CHIP ID versions */
+#define EIO_CHIPID1 0x20
+#define EIO_CHIPID2 0x21
+#define EIO_CHIPVER 0x22
+#define EIO_SIOCTRL 0x23
+#define EIO_SIOCTRL_SIOEN BIT(0)
+#define EIO_SIOCTRL_SWRST BIT(1)
+#define EIO_IRQCTRL 0x70
+#define EIO200_CHIPID 0x9610
+#define EIO201_211_CHIPID 0x9620
+#define EIO200_ICCODE 0x10
+#define EIO201_ICCODE 0x20
+#define EIO211_ICCODE 0x21
+
+/* LPC PNP */
+#define EIO_PNP_INDEX 0x299
+#define EIO_PNP_DATA 0x29A
+#define EIO_SUB_PNP_INDEX 0x499
+#define EIO_SUB_PNP_DATA 0x49A
+#define EIO_EXT_MODE_ENTER 0x87
+#define EIO_EXT_MODE_EXIT 0xAA
+
+/* LPC LDN */
+#define EIO_LDN 0x07
+#define EIO_LDN_PMC0 0x0C
+#define EIO_LDN_PMC1 0x0D
+
+/* PMC registers */
+#define EIO_PMC_STATUS_IBF BIT(1)
+#define EIO_PMC_STATUS_OBF BIT(0)
+#define EIO_LDAR 0x30
+#define EIO_LDAR_LDACT BIT(0)
+#define EIO_IOBA0H 0x60
+#define EIO_IOBA0L 0x61
+#define EIO_IOBA1H 0x62
+#define EIO_IOBA1L 0x63
+#define EIO_FLAG_PMC_READ BIT(0)
+
+/* PMC command list */
+#define EIO_PMC_CMD_ACPIRAM_READ 0x31
+#define EIO_PMC_CMD_CFG_SAVE 0x56
+
+/* OLD PMC */
+#define EIO_PMC_NO_INDEX 0xFF
+
+/* ACPI RAM Address Table */
+#define EIO_ACPIRAM_VERSIONSECTION (0xFA)
+#define EIO_ACPIRAM_ICVENDOR (EIO_ACPIRAM_VERSIONSECTION + 0x00)
+#define EIO_ACPIRAM_ICCODE (EIO_ACPIRAM_VERSIONSECTION + 0x01)
+#define EIO_ACPIRAM_CODEBASE (EIO_ACPIRAM_VERSIONSECTION + 0x02)
+
+#define EIO_ACPIRAM_CODEBASE_NEW BIT(7)
+
+/* Firmware */
+#define EIO_F_SUB_NEW_CODE_BASE BIT(6)
+#define EIO_F_SUB_CHANGED BIT(7)
+#define EIO_F_NEW_CODE_BASE BIT(8)
+#define EIO_F_CHANGED BIT(9)
+#define EIO_F_SUB_CHIP_EXIST BIT(30)
+#define EIO_F_CHIP_EXIST BIT(31)
+
+/* Others */
+#define EIO_EC_NUM 2
+
+struct _pmc_port {
+ union {
+ u16 cmd;
+ u16 status;
+ };
+ u16 data;
+};
+
+struct pmc_op {
+ u8 cmd;
+ u8 control;
+ u8 device_id;
+ u8 size;
+ u8 *payload;
+ u8 chip;
+};
+
+enum eio_rw_operation {
+ OPERATION_READ,
+ OPERATION_WRITE,
+};
+
+struct eio_dev {
+ struct device *dev;
+ struct regmap *map;
+ void __iomem *iomem;
+ struct mutex mutex; /* Protects PMC command access */
+ struct _pmc_port pmc[EIO_EC_NUM];
+ u32 flag;
+};
+
+int eio_core_pmc_operation(struct device *dev, struct pmc_op *operation);
+
+enum eio_pmc_wait {
+ PMC_WAIT_INPUT,
+ PMC_WAIT_OUTPUT,
+};
+
+int eio_core_pmc_wait(struct device *dev, int id, enum eio_pmc_wait wait);
+
+#define PMC_DEVICE_ATTR_RO(_name) \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return info_show(dev, attr, buf); \
+ } \
+ static DEVICE_ATTR_RO(_name)
+
+#endif
--
2.43.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 2/8] Add Advantech EIO GPIO driver
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
2026-07-14 15:54 ` [PATCH v2 1/8] Add Advantech EIO driver Ramiro Oliveira
@ 2026-07-14 15:54 ` Ramiro Oliveira
2026-07-14 16:06 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver Ramiro Oliveira
` (5 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
This driver controls the GPIO component of the Advantech EIO chip.
Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
MAINTAINERS | 7 ++
drivers/gpio/Kconfig | 6 ++
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-eio.c | 252 ++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 266 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 53b5f7412966..a7da47393815 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -609,6 +609,13 @@ S: Maintained
F: Documentation/scsi/advansys.rst
F: drivers/scsi/advansys.c
+ADVANTECH EIO DRIVER
+M: Ramiro Oliveira <ramiro.oliveira@advantech.com>
+S: Maintained
+F: drivers/gpio/gpio-eio.c
+F: drivers/mfd/eio_core.c
+F: include/linux/mfd/eio.h
+
ADVANTECH SWBTN DRIVER
M: Andrea Ho <Andrea.Ho@advantech.com.tw>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index f03c05288376..696065d77235 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -295,6 +295,12 @@ config GPIO_DWAPB
Say Y or M here to build support for the Synopsys DesignWare APB
GPIO block.
+config GPIO_EIO
+ tristate "Advantech EIO GPIO"
+ depends on MFD_EIO
+ help
+ Say Y or M to build support for Advantech EIO GPIO block.
+
config GPIO_EIC_SPRD
tristate "Spreadtrum EIC support"
depends on ARCH_SPRD || COMPILE_TEST
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index fa14581e3995..628596705c21 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -64,6 +64,7 @@ obj-$(CONFIG_GPIO_DLN2) += gpio-dln2.o
obj-$(CONFIG_GPIO_DS4520) += gpio-ds4520.o
obj-$(CONFIG_GPIO_DWAPB) += gpio-dwapb.o
obj-$(CONFIG_GPIO_EIC_SPRD) += gpio-eic-sprd.o
+obj-$(CONFIG_GPIO_EIO) += gpio-eio.o
obj-$(CONFIG_GPIO_ELKHARTLAKE) += gpio-elkhartlake.o
obj-$(CONFIG_GPIO_EM) += gpio-em.o
obj-$(CONFIG_GPIO_EN7523) += gpio-en7523.o
diff --git a/drivers/gpio/gpio-eio.c b/drivers/gpio/gpio-eio.c
new file mode 100644
index 000000000000..34e1aefd0716
--- /dev/null
+++ b/drivers/gpio/gpio-eio.c
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * GPIO driver for Advantech EIO Embedded controller.
+ *
+ * Copyright (C) 2025 Advantech Corporation. All rights reserved.
+ */
+
+#include <linux/errno.h>
+#include <linux/gpio/driver.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+
+#define EIO_GPIO_MAX_PINS 48
+#define EIO_GPIO_WRITE 0x18
+#define EIO_GPIO_READ 0x19
+
+struct eio_gpio_dev {
+ u64 avail;
+ int max;
+ struct gpio_chip chip;
+ struct device *dev;
+};
+
+static struct {
+ int size;
+ bool write;
+} ctrl_para[] = {
+ { 0x01, false }, { 0x00, false }, { 0x00, false }, { 0x02, false },
+ { 0x01, false }, { 0x00, false }, { 0x00, false }, { 0x00, false },
+ { 0x00, false }, { 0x00, false }, { 0x00, false }, { 0x00, false },
+ { 0x00, false }, { 0x00, false }, { 0x00, false }, { 0x00, false },
+ { 0x01, true }, { 0x01, true }, { 0x02, true }, { 0x02, true },
+ { 0x02, false }, { 0x10, false }
+};
+
+enum gpio_ctrl {
+ EIO_GPIO_STATUS = 0x0,
+ EIO_GPIO_GROUP_AVAIL = 0x3,
+ EIO_GPIO_ERROR = 0x04,
+ EIO_GPIO_PIN_DIR = 0x10,
+ EIO_GPIO_PIN_LEVEL = 0x11,
+ EIO_GPIO_GROUP_DIR = 0x12,
+ EIO_GPIO_GROUP_LEVEL = 0x13,
+ EIO_GPIO_MAPPING = 0x14,
+ EIO_GPIO_NAME = 0x15
+};
+
+static struct {
+ int group;
+ int port;
+} group_map[] = {
+ { 0, 0 }, { 0, 1 },
+ { 1, 0 }, { 1, 1 },
+ { 2, 0 }, { 2, 1 },
+ { 3, 0 }, { 3, 1 },
+ { 3, 2 }, { 3, 3 },
+ { 3, 4 }, { 3, 5 },
+ { 3, 6 }, { 3, 7 }
+};
+
+static int pmc_write(struct device *mfd_dev, u8 ctrl, u8 dev_id, void *data)
+{
+ struct pmc_op op = {
+ .cmd = EIO_GPIO_WRITE,
+ .control = ctrl,
+ .device_id = dev_id,
+ .payload = (u8 *)data,
+ };
+
+ if (ctrl >= ARRAY_SIZE(ctrl_para))
+ return -ENOMEM;
+
+ if (!ctrl_para[ctrl].write)
+ return -EINVAL;
+
+ op.size = ctrl_para[ctrl].size;
+
+ return eio_core_pmc_operation(mfd_dev, &op);
+}
+
+static int pmc_read(struct device *mfd_dev, u8 ctrl, u8 dev_id, void *data)
+{
+ struct pmc_op op = {
+ .cmd = EIO_GPIO_READ,
+ .control = ctrl,
+ .device_id = dev_id,
+ .payload = (u8 *)data,
+ };
+
+ if (ctrl > ARRAY_SIZE(ctrl_para))
+ return -ENOMEM;
+
+ op.size = ctrl_para[ctrl].size;
+
+ return eio_core_pmc_operation(mfd_dev, &op);
+}
+
+static int get_dir(struct gpio_chip *chip, unsigned int offset)
+{
+ u8 dir;
+ int ret;
+
+ ret = pmc_read(chip->parent, EIO_GPIO_PIN_DIR, offset, &dir);
+ if (ret)
+ return ret;
+
+ return dir ? 0 : 1;
+}
+
+static int dir_input(struct gpio_chip *chip, unsigned int offset)
+{
+ u8 dir = 0;
+
+ return pmc_write(chip->parent, EIO_GPIO_PIN_DIR, offset, &dir);
+}
+
+static int dir_output(struct gpio_chip *chip, unsigned int offset, int value)
+{
+ u8 dir = 1;
+ u8 val = value;
+
+ pmc_write(chip->parent, EIO_GPIO_PIN_DIR, offset, &dir);
+
+ return pmc_write(chip->parent, EIO_GPIO_PIN_LEVEL, offset, &val);
+}
+
+static int gpio_get(struct gpio_chip *chip, unsigned int offset)
+{
+ u8 level;
+ int ret;
+
+ ret = pmc_read(chip->parent, EIO_GPIO_PIN_LEVEL, offset, &level);
+ if (ret)
+ return ret;
+
+ return level;
+}
+
+static int gpio_set(struct gpio_chip *chip, unsigned int offset, int value)
+{
+ u8 val = value;
+
+ return pmc_write(chip->parent, EIO_GPIO_PIN_LEVEL, offset, &val);
+}
+
+static int check_support(struct device *dev)
+{
+ u8 data;
+ int ret;
+
+ ret = pmc_read(dev, EIO_GPIO_STATUS, 0, &data);
+ if (ret)
+ return ret;
+
+ if ((data & 0x01) == 0)
+ return -EOPNOTSUPP;
+
+ return 0;
+}
+
+static int check_pin(struct device *dev, int pin)
+{
+ int ret;
+ int group, bit;
+ u16 data;
+
+ /* Get pin mapping */
+ ret = pmc_read(dev, EIO_GPIO_MAPPING, pin, &data);
+ if (ret)
+ return ret;
+
+ if ((data & 0xFF) > ARRAY_SIZE(group_map))
+ return -EINVAL;
+
+ group = group_map[data & 0xFF].group;
+ bit = data >> 8;
+
+ /* Check mapped pin */
+ ret = pmc_read(dev, EIO_GPIO_GROUP_AVAIL, group, &data);
+ if (ret)
+ return ret;
+
+ return data & BIT(bit) ? 0 : -EOPNOTSUPP;
+}
+
+static int gpio_init(struct device *mfd, struct eio_gpio_dev *eio_gpio)
+{
+ int ret, i;
+
+ ret = check_support(mfd);
+ if (ret)
+ return dev_err_probe(eio_gpio->dev, ret, "GPIO not supported\n");
+
+ eio_gpio->avail = 0;
+
+ for (i = 0 ; i < EIO_GPIO_MAX_PINS ; i++) {
+ ret = check_pin(mfd, i);
+ if (ret)
+ continue;
+
+ eio_gpio->avail |= BIT(i);
+ eio_gpio->max = i + 1;
+ }
+
+ return eio_gpio->max ? 0 : -EOPNOTSUPP;
+}
+
+static int gpio_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct eio_gpio_dev *eio_gpio;
+ struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
+
+ if (!eio_dev)
+ return dev_err_probe(dev, -ENODEV, "Error contact eio_core\n");
+
+ eio_gpio = devm_kzalloc(dev, sizeof(*eio_gpio), GFP_KERNEL);
+ if (!eio_gpio)
+ return -ENOMEM;
+
+ eio_gpio->dev = dev;
+
+ if (gpio_init(dev->parent, eio_gpio))
+ return -EIO;
+
+ eio_gpio->chip.parent = dev->parent;
+ eio_gpio->chip.ngpio = eio_gpio->max;
+ eio_gpio->chip.label = KBUILD_MODNAME;
+ eio_gpio->chip.owner = THIS_MODULE;
+ eio_gpio->chip.direction_input = dir_input;
+ eio_gpio->chip.get = gpio_get;
+ eio_gpio->chip.direction_output = dir_output;
+ eio_gpio->chip.set = gpio_set;
+ eio_gpio->chip.get_direction = get_dir;
+ eio_gpio->chip.base = -1;
+ eio_gpio->chip.can_sleep = true;
+
+ return devm_gpiochip_add_data(dev, &eio_gpio->chip, eio_gpio);
+}
+
+static struct platform_driver gpio_driver = {
+ .probe = gpio_probe,
+ .driver = { .name = KBUILD_MODNAME, },
+};
+
+module_platform_driver(gpio_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("GPIO driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
2026-07-14 15:54 ` [PATCH v2 1/8] Add Advantech EIO driver Ramiro Oliveira
2026-07-14 15:54 ` [PATCH v2 2/8] Add Advantech EIO GPIO driver Ramiro Oliveira
@ 2026-07-14 15:54 ` Ramiro Oliveira
2026-07-14 16:05 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 4/8] Add Advantech EIO I2C driver Ramiro Oliveira
` (4 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
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 <ramiro.oliveira@advantech.com>
---
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
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 4/8] Add Advantech EIO I2C driver
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
` (2 preceding siblings ...)
2026-07-14 15:54 ` [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver Ramiro Oliveira
@ 2026-07-14 15:54 ` Ramiro Oliveira
2026-07-14 16:11 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 5/8] Add Advantech EIO Backlight driver Ramiro Oliveira
` (3 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
This commit adds the driver to control the Advantech EIO I2C block, this
block is included in the Advantech EIO MFD.
Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
MAINTAINERS | 1 +
drivers/i2c/busses/Kconfig | 6 +
drivers/i2c/busses/Makefile | 1 +
drivers/i2c/busses/i2c-eio.c | 1135 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1143 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 6d727d5182e4..e228d12a2326 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -614,6 +614,7 @@ M: Ramiro Oliveira <ramiro.oliveira@advantech.com>
S: Maintained
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: include/linux/mfd/eio.h
diff --git a/drivers/i2c/busses/Kconfig b/drivers/i2c/busses/Kconfig
index d7b89508311f..ba982bb00cf9 100644
--- a/drivers/i2c/busses/Kconfig
+++ b/drivers/i2c/busses/Kconfig
@@ -649,6 +649,12 @@ config I2C_DIGICOLOR
This driver can also be built as a module. If so, the module
will be called i2c-digicolor.
+config I2C_EIO
+ tristate "Advantech EIO I2C bus"
+ depends on MFD_EIO
+ help
+ Say Y or M to build support for Advantech EIO I2C block.
+
config I2C_EG20T
tristate "Intel EG20T PCH/LAPIS Semicon IOH(ML7213/ML7223/ML7831) I2C"
depends on PCI && (X86_32 || MIPS || COMPILE_TEST)
diff --git a/drivers/i2c/busses/Makefile b/drivers/i2c/busses/Makefile
index 3755c54b3d82..2d8d4f37838c 100644
--- a/drivers/i2c/busses/Makefile
+++ b/drivers/i2c/busses/Makefile
@@ -62,6 +62,7 @@ obj-$(CONFIG_I2C_DESIGNWARE_AMDISP) += i2c-designware-amdisp.o
obj-$(CONFIG_I2C_DESIGNWARE_PCI) += i2c-designware-pci.o
i2c-designware-pci-y := i2c-designware-pcidrv.o
obj-$(CONFIG_I2C_DIGICOLOR) += i2c-digicolor.o
+obj-$(CONFIG_I2C_EIO) += i2c-eio.o
obj-$(CONFIG_I2C_EG20T) += i2c-eg20t.o
obj-$(CONFIG_I2C_EMEV2) += i2c-emev2.o
obj-$(CONFIG_I2C_EXYNOS5) += i2c-exynos5.o
diff --git a/drivers/i2c/busses/i2c-eio.c b/drivers/i2c/busses/i2c-eio.c
new file mode 100644
index 000000000000..56a89ca18f32
--- /dev/null
+++ b/drivers/i2c/busses/i2c-eio.c
@@ -0,0 +1,1135 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * I2C and SMBus driver of EIO embedded driver
+ *
+ * Copyright (C) 2025 Advantech Co., Ltd.
+ */
+
+#include <linux/device.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/i2c.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#define SUPPORTED_COMMON (I2C_FUNC_I2C | \
+ I2C_FUNC_SMBUS_QUICK | \
+ I2C_FUNC_SMBUS_BYTE | \
+ I2C_FUNC_SMBUS_BYTE_DATA | \
+ I2C_FUNC_SMBUS_WORD_DATA | \
+ I2C_FUNC_SMBUS_I2C_BLOCK)
+#define SUPPORTED_SMB (SUPPORTED_COMMON | I2C_FUNC_SMBUS_BLOCK_DATA)
+#define SUPPORTED_I2C (SUPPORTED_COMMON | I2C_FUNC_10BIT_ADDR)
+
+#define MAX_I2C_SMB 4
+
+#define REG_PNP_INDEX 0x299
+#define REG_PNP_DATA 0x29A
+#define REG_SUB_PNP_INDEX 0x499
+#define REG_SUB_PNP_DATA 0x49A
+#define REG_EXT_MODE_ENTER 0x87
+#define REG_EXT_MODE_EXIT 0xAA
+#define REG_LDN 0x07
+
+#define LDN_I2C0 0x20
+#define LDN_I2C1 0x21
+#define LDN_SMBUS0 0x22
+#define LDN_SMBUS1 0x23
+
+#define REG_BASE_HI 0x60
+#define REG_BASE_LO 0x61
+
+#define I2C_REG_CTRL 0x00
+#define I2C_CTRL_STOP BIT(1)
+
+#define I2C_REG_STAT 0x01
+#define I2C_STAT_RXREADY BIT(6)
+#define I2C_STAT_TXDONE BIT(5)
+#define I2C_STAT_NAK_ERR BIT(4)
+#define I2C_STAT_ARL_ERR BIT(3)
+#define I2C_STAT_SLV_STP BIT(2)
+#define I2C_STAT_BUSY BIT(1)
+#define I2C_STAT_MST_SLV BIT(0)
+
+#define I2C_REG_MYADDR 0x02
+#define I2C_REG_ADDR 0x03
+#define I2C_REG_DATA 0x04
+#define I2C_REG_PRESCALE1 0x05
+#define I2C_REG_PRESCALE2 0x06
+
+#define I2C_REG_ECTRL 0x07
+#define I2C_ECTRL_RST BIT(7)
+
+#define I2C_REG_SEM 0x08
+#define I2C_SEM_INUSE BIT(1)
+
+#define SMB_REG_HC2 0x0C
+
+#define SMB_REG_HS 0x00
+#define SMB_HS_BUSY BIT(0)
+#define SMB_HS_FINISH BIT(1)
+#define SMB_HS_ARL_ERR BIT(3)
+#define SMB_HS_FAILED BIT(4)
+#define SMB_HS_RX_READY BIT(5)
+#define SMB_HS_INUSE BIT(6)
+#define SMB_HS_TX_DONE BIT(7)
+
+#define SMB_REG_HS2 0x01
+#define SMB_HS2_HNOTIFY BIT(0)
+#define SMB_HS2_PEC_ERR BIT(1)
+#define SMB_HS2_NACK_ERR BIT(2)
+#define SMB_HS2_ALERT_STS BIT(3)
+#define SMB_HS2_TO_ERR BIT(4)
+#define SMB_HS2_SSTOP_STS BIT(5)
+#define SMB_HS2_STX_REQ BIT(6)
+#define SMB_HS2_SMODE BIT(7)
+
+#define SMB_REG_HC 0x02
+#define SMB_HC_I2C_NACKEN BIT(0)
+#define SMB_HC_KILL BIT(1)
+#define SMB_HC_CMD_SHIFT 2
+#define SMB_HC_LAST_BYTE BIT(5)
+#define SMB_HC_START BIT(6)
+#define SMB_HC_PEC_EN BIT(7)
+
+#define SMB_REG_HCMD 0x03
+#define SMB_REG_HADDR 0x04
+#define SMB_REG_HD0 0x05
+#define SMB_REG_HD1 0x06
+#define SMB_REG_HBLOCK 0x07
+#define SMB_REG_HPEC 0x08
+#define SMB_REG_SADDR 0x09
+#define SMB_REG_SD0 0x0A
+#define SMB_REG_SD1 0x0B
+
+#define SMB_REG_HC2 0x0C
+#define SMB_HC2_HNOTIFY_DIS BIT(0)
+#define SMB_HC2_I2C_EN BIT(1)
+#define SMB_HC2_AAPEC BIT(2)
+#define SMB_HC2_E32B BIT(3)
+#define SMB_HC2_SRESET BIT(7)
+
+#define SMB_REG_HPIN 0x0D
+#define SMB_REG_HC3 0x0E
+#define SMB_REG_HC4 0x0F
+#define SMB_REG_NOTIFY_D0 0x11
+#define SMB_REG_NOTIFY_D1 0x12
+#define SMB_REG_HPRESCALE1 0x13
+#define SMB_REG_HPRESCALE2 0x14
+#define SMB_REG_HEXTRA 0x15
+
+#define I2C_TIMEOUT (10 * USEC_PER_MSEC)
+#define USE_DEFAULT -1
+
+#define CHIP_CLK 50000
+#define I2C_SCLH_HIGH 2500
+#define I2C_SCLH_LOW 1000
+#define I2C_SCL_FAST_MODE 0x80
+#define I2C_THRESHOLD_SPEED 100
+#define I2C_THRESHOLD_SCLH 30
+#define I2C_FREQ_MAX 400
+#define I2C_FREQ_MIN 8
+
+enum eio_chan_id {
+ EIO_I2C0 = 0,
+ EIO_I2C1,
+ EIO_SMB0,
+ EIO_SMB1,
+};
+
+struct eio_i2c_dev {
+ struct device *dev;
+ struct device *mfd;
+ struct regmap *regmap;
+ struct mutex pnp_mutex; /* Mutex for PNP acces */
+ struct eio_i2c_chan *chan[MAX_I2C_SMB];
+};
+
+struct eio_i2c_chan {
+ u16 base;
+ enum eio_chan_id id;
+ struct eio_i2c_dev *parent;
+ struct i2c_adapter adap;
+ struct mutex lock; /* Mutex for regmap writes */
+ int freq_override; /* kHz or USE_DEFAULT */
+};
+
+static int i2c0_freq = USE_DEFAULT;
+module_param(i2c0_freq, int, 0444);
+MODULE_PARM_DESC(i2c0_freq, "Set EIO's I2C0 freq.\n");
+
+static int i2c1_freq = USE_DEFAULT;
+module_param(i2c1_freq, int, 0444);
+MODULE_PARM_DESC(i2c1_freq, "Set EIO's I2C1 freq.\n");
+
+static int smb0_freq = USE_DEFAULT;
+module_param(smb0_freq, int, 0444);
+MODULE_PARM_DESC(smb0_freq, "Set EIO's SMB0 freq.\n");
+
+static int smb1_freq = USE_DEFAULT;
+module_param(smb1_freq, int, 0444);
+MODULE_PARM_DESC(smb1_freq, "Set EIO's SMB1 freq.\n");
+
+static inline u16 eio_enc_7bit_addr(u16 x)
+{
+ return ((x & 0x07F) << 1);
+}
+
+static inline u16 eio_enc_10bit_addr(u16 x)
+{
+ return ((x & 0xFF) | ((x & 0x0300) << 1) | 0xF000);
+}
+
+static inline bool is_i2c(const struct eio_i2c_chan *i2c_chan)
+{
+ return i2c_chan->id == EIO_I2C0 || i2c_chan->id == EIO_I2C1;
+}
+
+static inline struct device *eio_dev(const struct eio_i2c_chan *i2c_chan)
+{
+ return i2c_chan->parent->dev;
+}
+
+static inline struct regmap *eio_map(const struct eio_i2c_chan *i2c_chan)
+{
+ return i2c_chan->parent->regmap;
+}
+
+static inline int eio_reg_write(struct eio_i2c_chan *i2c_chan,
+ unsigned int reg_off, unsigned int val)
+{
+ return regmap_write(eio_map(i2c_chan), i2c_chan->base + reg_off, val);
+}
+
+static inline int eio_reg_read(const struct eio_i2c_chan *chan,
+ unsigned int reg, unsigned int *val)
+{
+ int ret;
+
+ ret = regmap_read(chan->parent->regmap, chan->base + reg, val);
+ return ret;
+}
+
+static inline int eio_reg_set_bits(const struct eio_i2c_chan *chan,
+ unsigned int reg, unsigned int mask)
+{
+ return regmap_update_bits(chan->parent->regmap, reg, mask, mask);
+}
+
+static inline int eio_reg_clear_bits(const struct eio_i2c_chan *chan,
+ unsigned int reg, unsigned int mask)
+{
+ return regmap_update_bits(chan->parent->regmap, reg, mask, 0);
+}
+
+static inline int eio_reg_or(struct eio_i2c_chan *chan,
+ unsigned int reg, unsigned int mask)
+{
+ return eio_reg_set_bits(chan, reg, mask);
+}
+
+static inline int eio_reg_and(struct eio_i2c_chan *chan,
+ unsigned int reg, unsigned int mask)
+{
+ return eio_reg_clear_bits(chan, reg, ~mask);
+}
+
+static inline unsigned int eio_chan_reg(const struct eio_i2c_chan *i2c_chan,
+ unsigned int i2c_reg,
+ unsigned int smb_reg)
+{
+ return is_i2c(i2c_chan) ? i2c_reg : smb_reg;
+}
+
+static inline int eio_trigger_read(struct eio_i2c_chan *i2c_chan, u32 *data)
+{
+ unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_DATA, SMB_REG_HD0);
+
+ return eio_reg_read(i2c_chan, reg, data);
+}
+
+static int wait_busy(struct eio_i2c_chan *i2c_chan)
+{
+ ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+ unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_STAT, SMB_REG_HS);
+ unsigned int target = eio_chan_reg(i2c_chan, I2C_STAT_BUSY, SMB_HS_BUSY);
+ unsigned int val;
+ int cnt = 0;
+
+ do {
+ fsleep(cnt++);
+
+ if (ktime_after(ktime_get(), time_end)) {
+ dev_err(eio_dev(i2c_chan), "Wait I2C bus busy timeout\n");
+ return -ETIME;
+ }
+
+ if (eio_reg_read(i2c_chan, reg, &val))
+ return -EIO;
+
+ } while (val & target);
+
+ return 0;
+}
+
+static void reset_bus(struct eio_i2c_chan *i2c_chan)
+{
+ ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+ unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_ECTRL, SMB_REG_HC2);
+ unsigned int target = eio_chan_reg(i2c_chan, I2C_ECTRL_RST, SMB_HC2_SRESET);
+ unsigned int val = 0;
+ unsigned int cnt = 0;
+
+ dev_dbg(eio_dev(i2c_chan), "i2c[%d] bus reset\n", i2c_chan->id);
+
+ if (is_i2c(i2c_chan))
+ eio_reg_write(i2c_chan, I2C_REG_ECTRL, I2C_ECTRL_RST);
+ else
+ eio_reg_or(i2c_chan, SMB_REG_HC2, SMB_HC2_SRESET);
+
+ do {
+ fsleep(cnt++);
+
+ if (ktime_after(ktime_get(), time_end)) {
+ dev_err(eio_dev(i2c_chan), "bus reset timeout\n");
+ return;
+ }
+
+ if (eio_reg_read(i2c_chan, reg, &val))
+ return;
+
+ } while (val & target);
+
+ wait_busy(i2c_chan);
+}
+
+static int wait_bus_free(struct eio_i2c_chan *i2c_chan)
+{
+ ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+ unsigned int val;
+ int cnt = 1;
+
+ /* Wait if channel is resetting */
+ do {
+ fsleep(cnt);
+
+ if (ktime_after(ktime_get(), time_end)) {
+ dev_err(eio_dev(i2c_chan), "Wait bus reset timeout\n");
+ return -ETIME;
+ }
+
+ if (eio_reg_read(i2c_chan,
+ eio_chan_reg(i2c_chan, I2C_REG_ECTRL, SMB_REG_HC2),
+ &val))
+ return -EIO;
+
+ } while (val & eio_chan_reg(i2c_chan, I2C_ECTRL_RST, SMB_HC2_SRESET));
+
+ /* Wait INUSE */
+ time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+
+ do {
+ fsleep(cnt);
+
+ if (ktime_after(ktime_get(), time_end)) {
+ dev_err(eio_dev(i2c_chan), "Timeout: I2C bus in use\n");
+ return -ETIME;
+ }
+
+ if (eio_reg_read(i2c_chan,
+ eio_chan_reg(i2c_chan, I2C_REG_SEM, SMB_REG_HS),
+ &val))
+ return -EIO;
+
+ } while (val & eio_chan_reg(i2c_chan, I2C_SEM_INUSE, SMB_HS_INUSE));
+
+ return 0;
+}
+
+static int let_stop(struct eio_i2c_chan *i2c_chan)
+{
+ unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_CTRL, SMB_REG_HC);
+ unsigned int target = eio_chan_reg(i2c_chan, I2C_CTRL_STOP, SMB_HC_LAST_BYTE);
+
+ return eio_reg_or(i2c_chan, reg, target);
+}
+
+static int clr_inuse(struct eio_i2c_chan *i2c_chan)
+{
+ if (is_i2c(i2c_chan))
+ return eio_reg_write(i2c_chan, I2C_REG_SEM, I2C_SEM_INUSE);
+
+ return eio_reg_or(i2c_chan, SMB_REG_HS, SMB_HS_INUSE);
+}
+
+static int bus_stop(struct eio_i2c_chan *i2c_chan)
+{
+ ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+ unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_CTRL, SMB_REG_HC);
+ unsigned int target = eio_chan_reg(i2c_chan, I2C_CTRL_STOP, SMB_HC_LAST_BYTE);
+ unsigned int val = 0;
+ int cnt = 0;
+
+ /* Set STOP bit */
+ eio_reg_or(i2c_chan, reg, target);
+
+ /* Wait until STOP bit clears */
+ do {
+ fsleep(cnt++);
+
+ if (ktime_after(ktime_get(), time_end))
+ return -ETIME;
+
+ if (eio_reg_read(i2c_chan, reg, &val))
+ return -EIO;
+
+ } while (val & target);
+
+ return 0;
+}
+
+static void switch_i2c_mode(struct eio_i2c_chan *i2c_chan, bool on)
+{
+ u32 tmp;
+
+ if (is_i2c(i2c_chan))
+ return;
+
+ if (eio_reg_read(i2c_chan, SMB_REG_HC2, &tmp))
+ return;
+
+ eio_reg_write(i2c_chan, SMB_REG_HC2,
+ on ? (tmp | SMB_HC2_I2C_EN | SMB_HC2_SRESET)
+ : (tmp & ~SMB_HC2_I2C_EN));
+}
+
+static void i2c_clear(struct eio_i2c_chan *i2c_chan)
+{
+ if (is_i2c(i2c_chan)) {
+ eio_reg_write(i2c_chan, I2C_REG_STAT, 0xFF);
+ } else {
+ eio_reg_or(i2c_chan, SMB_REG_HS, 0xA9);
+ eio_reg_or(i2c_chan, SMB_REG_HS2, 0x4C);
+ }
+}
+
+static int wait_write_done(struct eio_i2c_chan *i2c_chan, bool no_ack)
+{
+ ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+ unsigned int val = 0;
+ int cnt = 0;
+ unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_STAT, SMB_REG_HS);
+ unsigned int target = eio_chan_reg(i2c_chan, I2C_STAT_TXDONE, SMB_HS_TX_DONE);
+
+ do {
+ fsleep(cnt++);
+ if (ktime_after(ktime_get(), time_end)) {
+ if (is_i2c(i2c_chan)) {
+ eio_reg_or(i2c_chan, I2C_REG_STAT, 0);
+ } else {
+ eio_reg_or(i2c_chan, SMB_REG_HS, 0);
+ eio_reg_or(i2c_chan, SMB_REG_HS2, 0);
+ }
+ dev_err(eio_dev(i2c_chan), "wait write complete timeout %X %X\n",
+ val, target);
+ return -ETIME;
+ }
+ if (eio_reg_read(i2c_chan, reg, &val))
+ return -EIO;
+
+ } while ((val & target) == 0);
+
+ if (no_ack)
+ return 0;
+
+ if (is_i2c(i2c_chan)) {
+ eio_reg_or(i2c_chan, I2C_REG_STAT, 0);
+ return (val & I2C_STAT_NAK_ERR) ? -EIO : 0;
+ }
+
+ eio_reg_or(i2c_chan, SMB_REG_HS, 0);
+ if (eio_reg_read(i2c_chan, SMB_REG_HS2, &val))
+ return -EIO;
+ eio_reg_write(i2c_chan, SMB_REG_HS2, val);
+
+ return (val & SMB_HS2_NACK_ERR) ? -EIO : 0;
+}
+
+static int wait_ready(struct eio_i2c_chan *i2c_chan)
+{
+ int ret;
+
+ ret = wait_bus_free(i2c_chan);
+ if (ret)
+ return ret;
+
+ if (wait_busy(i2c_chan) == 0)
+ return 0;
+
+ reset_bus(i2c_chan);
+
+ return wait_busy(i2c_chan);
+}
+
+static int write_addr(struct eio_i2c_chan *i2c_chan, int addr, bool no_ack)
+{
+ eio_reg_write(i2c_chan, eio_chan_reg(i2c_chan, I2C_REG_ADDR, SMB_REG_HADDR),
+ addr);
+
+ return wait_write_done(i2c_chan, no_ack);
+}
+
+static int write_data(struct eio_i2c_chan *i2c_chan, int data, bool no_ack)
+{
+ eio_reg_write(i2c_chan, eio_chan_reg(i2c_chan, I2C_REG_DATA, SMB_REG_HD0),
+ data);
+
+ return wait_write_done(i2c_chan, no_ack);
+}
+
+static int read_data(struct eio_i2c_chan *i2c_chan, u8 *data)
+{
+ unsigned int val = 0, tmp;
+ int cnt = 0;
+ ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+ unsigned int stat = eio_chan_reg(i2c_chan, I2C_REG_STAT, SMB_REG_HS);
+ unsigned int target = eio_chan_reg(i2c_chan, I2C_STAT_RXREADY, SMB_HS_RX_READY);
+ unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_DATA, SMB_REG_HD0);
+
+ do {
+ fsleep(cnt++);
+
+ if (ktime_after(ktime_get(), time_end)) {
+ eio_reg_or(i2c_chan, stat, 0);
+ dev_err(eio_dev(i2c_chan), "read data timeout\n");
+ return -ETIME;
+ }
+
+ if (eio_reg_read(i2c_chan, stat, &val))
+ return -EIO;
+
+ } while ((val & target) != target);
+
+ /* clear status */
+ eio_reg_write(i2c_chan, stat, val);
+
+ /* Must read data after clearing status */
+ if (eio_reg_read(i2c_chan, reg, &tmp))
+ return -EIO;
+ *data = (u8)tmp;
+
+ return 0;
+}
+
+static int set_freq(struct eio_i2c_chan *i2c_chan, int freq)
+{
+ u8 pre1, pre2;
+ u16 speed;
+ unsigned int reg1 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE1, SMB_REG_HPRESCALE1);
+ unsigned int reg2 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE2, SMB_REG_HPRESCALE2);
+
+ dev_dbg(eio_dev(i2c_chan), "set freq: %dkHz\n", freq);
+ if (freq > I2C_FREQ_MAX || freq < I2C_FREQ_MIN) {
+ dev_err(eio_dev(i2c_chan), "Invalid i2c freq: %d\n", freq);
+ return -EINVAL;
+ }
+
+ speed = (freq < I2C_THRESHOLD_SCLH) ? I2C_SCLH_LOW : I2C_SCLH_HIGH;
+
+ pre1 = (u8)(CHIP_CLK / speed);
+ pre2 = (u8)((speed / freq) - 1);
+
+ if (freq > I2C_THRESHOLD_SCLH)
+ pre2 |= I2C_SCL_FAST_MODE;
+
+ eio_reg_write(i2c_chan, reg1, pre1);
+ eio_reg_write(i2c_chan, reg2, pre2);
+
+ return 0;
+}
+
+static int get_freq(struct eio_i2c_chan *i2c_chan, int *freq)
+{
+ int clk;
+ unsigned int pre1 = 0, pre2 = 0;
+ unsigned int reg1 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE1, SMB_REG_HPRESCALE1);
+ unsigned int reg2 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE2, SMB_REG_HPRESCALE2);
+
+ if (eio_reg_read(i2c_chan, reg1, &pre1))
+ return -EIO;
+ if (eio_reg_read(i2c_chan, reg2, &pre2))
+ return -EIO;
+
+ clk = (pre2 & I2C_SCL_FAST_MODE) ? I2C_SCLH_HIGH : I2C_SCLH_LOW;
+ pre2 &= ~I2C_SCL_FAST_MODE;
+
+ *freq = clk / ((int)pre2 + 1);
+
+ return 0;
+}
+
+static int smb_access(struct eio_i2c_chan *i2c_chan, u8 addr, bool is_read, u8 cmd,
+ int size, union i2c_smbus_data *data)
+{
+ int i, tmp, ret = 0;
+ unsigned int st1, st2;
+ int len = 0;
+
+ mutex_lock(&i2c_chan->lock);
+
+ ret = wait_ready(i2c_chan);
+ if (ret)
+ goto exit;
+
+ /* Force SMBus mode */
+ switch_i2c_mode(i2c_chan, false);
+
+ addr = eio_enc_7bit_addr(addr) | (is_read ? 1 : 0);
+ eio_reg_write(i2c_chan, SMB_REG_HADDR, addr);
+ eio_reg_write(i2c_chan, SMB_REG_HCMD, cmd);
+
+ dev_dbg(eio_dev(i2c_chan), "SMB[%d], addr:0x%02X, cmd:0x%02X size=%d\n",
+ i2c_chan->id, addr, cmd, size);
+
+ switch (size) {
+ case I2C_SMBUS_QUICK:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_QUICK\n");
+ break;
+
+ case I2C_SMBUS_BYTE:
+ if (!is_read) {
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE\n");
+ eio_reg_write(i2c_chan, SMB_REG_HCMD, cmd);
+ }
+ break;
+
+ case I2C_SMBUS_BYTE_DATA:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE_DATA\n");
+ if (!is_read) {
+ eio_reg_write(i2c_chan, SMB_REG_HD0, data->byte);
+ dev_dbg(eio_dev(i2c_chan), "write %X\n", data->byte);
+ }
+ break;
+
+ case I2C_SMBUS_WORD_DATA:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_WORD_DATA\n");
+ if (!is_read) {
+ eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
+ eio_reg_write(i2c_chan, SMB_REG_HD1, data->block[1]);
+ }
+ break;
+
+ case I2C_SMBUS_PROC_CALL:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_PROC_CALL\n");
+ eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
+ eio_reg_write(i2c_chan, SMB_REG_HD1, data->block[1]);
+ break;
+
+ case I2C_SMBUS_BLOCK_DATA:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_DATA\n");
+ if (is_read)
+ break;
+
+ /* Program command type */
+ eio_reg_read(i2c_chan, SMB_REG_HC, (unsigned int *)&tmp);
+ tmp &= ~(0x07 << SMB_HC_CMD_SHIFT);
+ tmp |= (size << SMB_HC_CMD_SHIFT);
+ eio_reg_write(i2c_chan, SMB_REG_HC, tmp);
+
+ /* Force write for payload stage */
+ eio_reg_write(i2c_chan, SMB_REG_HADDR, addr & ~0x01);
+
+ /* Reset internal buffer index pointer */
+ eio_reg_and(i2c_chan, SMB_REG_HC2, (int)~SMB_HC2_E32B);
+ eio_reg_or(i2c_chan, SMB_REG_HC2, SMB_HC2_E32B);
+
+ /* Write length + data */
+ eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
+ for (i = 1; i <= data->block[0]; i++)
+ eio_reg_write(i2c_chan, SMB_REG_HBLOCK, data->block[i]);
+ break;
+
+ case I2C_SMBUS_BLOCK_PROC_CALL:
+ /* Set command type field */
+ eio_reg_and(i2c_chan, SMB_REG_HC, (0x07 << SMB_HC_CMD_SHIFT));
+ eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
+
+ /* Reset buffer index */
+ eio_reg_and(i2c_chan, SMB_REG_HC2, (int)~SMB_HC2_E32B);
+ eio_reg_or(i2c_chan, SMB_REG_HC2, SMB_HC2_E32B);
+
+ for (i = 1; i <= data->block[0]; i++)
+ eio_reg_write(i2c_chan, SMB_REG_HBLOCK, data->block[i]);
+ break;
+
+ default:
+ ret = -EINVAL;
+ goto exit;
+ }
+
+ /* Launch transaction */
+ eio_reg_read(i2c_chan, SMB_REG_HC, (unsigned int *)&tmp);
+ tmp &= ~(0x07 << SMB_HC_CMD_SHIFT);
+ tmp |= (size << SMB_HC_CMD_SHIFT) | SMB_HC_START;
+ tmp &= ~(SMB_HC_I2C_NACKEN | SMB_HC_KILL | SMB_HC_PEC_EN);
+ eio_reg_write(i2c_chan, SMB_REG_HC, tmp);
+
+ ret = wait_busy(i2c_chan);
+ if (ret)
+ goto exit;
+
+ eio_reg_read(i2c_chan, SMB_REG_HS, &st1);
+ eio_reg_read(i2c_chan, SMB_REG_HS2, &st2);
+
+ if (st1 & SMB_HS_FAILED) {
+ dev_err(eio_dev(i2c_chan), "HS FAILED\n");
+ ret = -EIO;
+ } else if (st1 & SMB_HS_ARL_ERR) {
+ dev_err(eio_dev(i2c_chan), "ARL FAILED\n");
+ ret = -EIO;
+ } else if (st2 & SMB_HS2_TO_ERR) {
+ dev_err(eio_dev(i2c_chan), "timeout\n");
+ ret = -ETIME;
+ } else if (st2 & SMB_HS2_NACK_ERR) {
+ dev_err(eio_dev(i2c_chan), "NACK err\n");
+ ret = -EIO;
+ } else if (st2 & SMB_HS2_PEC_ERR) {
+ dev_err(eio_dev(i2c_chan), "PEC err\n");
+ ret = -EIO;
+ }
+ if (ret)
+ goto exit;
+
+ switch (size) {
+ case I2C_SMBUS_QUICK:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_QUICK\n");
+ break;
+
+ case I2C_SMBUS_BYTE:
+ case I2C_SMBUS_BYTE_DATA:
+ if (is_read) {
+ unsigned int v;
+
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE/I2C_SMBUS_BYTE_DATA\n");
+ eio_reg_read(i2c_chan, SMB_REG_HD0, &v);
+ data->block[0] = (u8)v;
+ dev_dbg(eio_dev(i2c_chan), "read %X\n", data->block[0]);
+ }
+ break;
+
+ case I2C_SMBUS_WORD_DATA: {
+ unsigned int v0, v1;
+
+ if (is_read) {
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_WORD_DATA\n");
+ eio_reg_read(i2c_chan, SMB_REG_HD0, &v0);
+ eio_reg_read(i2c_chan, SMB_REG_HD1, &v1);
+ data->block[0] = (u8)v0;
+ data->block[1] = (u8)v1;
+ }
+ break;
+ }
+
+ case I2C_SMBUS_PROC_CALL: {
+ unsigned int v0, v1;
+
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_PROC_CALL\n");
+ eio_reg_read(i2c_chan, SMB_REG_HD0, &v0);
+ eio_reg_read(i2c_chan, SMB_REG_HD1, &v1);
+ data->block[0] = (u8)v0;
+ data->block[1] = (u8)v1;
+ break;
+ }
+
+ case I2C_SMBUS_BLOCK_DATA:
+ if (!is_read)
+ break;
+
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_DATA\n");
+ eio_reg_read(i2c_chan, SMB_REG_HD0, (unsigned int *)&len);
+ len = min(len, I2C_SMBUS_BLOCK_MAX);
+ data->block[0] = len;
+
+ for (i = 1; i <= len; i++)
+ eio_reg_read(i2c_chan, SMB_REG_HBLOCK,
+ (unsigned int *)&data->block[i]);
+ break;
+
+ default:
+ ret = -EINVAL;
+ goto exit;
+ }
+
+exit:
+ /* Clear latched status */
+ eio_reg_write(i2c_chan, SMB_REG_HS, 0xFF);
+ eio_reg_write(i2c_chan, SMB_REG_HS2, 0xFF);
+
+ mutex_unlock(&i2c_chan->lock);
+ return ret;
+}
+
+static int i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int nmsgs)
+{
+ int msg, data;
+ int addr = 0;
+ int dummy;
+ int ret = 0;
+ struct eio_i2c_chan *i2c_chan = i2c_get_adapdata(adap);
+
+ mutex_lock(&i2c_chan->lock);
+
+ ret = wait_ready(i2c_chan);
+ if (ret)
+ goto exit;
+
+ switch_i2c_mode(i2c_chan, true);
+
+ dev_dbg(eio_dev(i2c_chan), "Transmit %d I2C messages\n", nmsgs);
+ for (msg = 0; msg < nmsgs; msg++) {
+ int is_read = msgs[msg].flags & I2C_M_RD;
+ bool no_ack = msgs[msg].flags & I2C_M_IGNORE_NAK;
+
+ dev_dbg(eio_dev(i2c_chan), "message %d len=%d\n", msg, msgs[msg].len);
+
+ if (!msgs[msg].len)
+ let_stop(i2c_chan);
+
+ if (msgs[msg].flags & I2C_M_TEN) {
+ addr = eio_enc_10bit_addr(msgs[msg].addr);
+ addr |= is_read;
+ dev_dbg(eio_dev(i2c_chan), "10-bit addr: %X\n", addr);
+
+ ret = write_addr(i2c_chan, addr >> 8, no_ack);
+ if (!ret)
+ ret = write_data(i2c_chan, addr & 0x7F, no_ack);
+ } else {
+ addr = eio_enc_7bit_addr(msgs[msg].addr);
+ addr |= is_read;
+ dev_dbg(eio_dev(i2c_chan), "7-bit addr: %X\n", addr);
+
+ ret = write_addr(i2c_chan, addr, no_ack);
+ }
+
+ if (ret)
+ goto exit;
+
+ if (!msgs[msg].len)
+ goto exit;
+
+ if (is_read)
+ ret = eio_trigger_read(i2c_chan, (u32 *)&dummy);
+
+ /* Transmit all messages */
+ for (data = 0; data < msgs[msg].len; data++) {
+ if (msgs[msg].flags & I2C_M_RD) {
+ bool last = (msgs[msg].len == data + 1);
+
+ if (last)
+ let_stop(i2c_chan);
+
+ ret = read_data(i2c_chan, &msgs[msg].buf[data]);
+ dev_dbg(eio_dev(i2c_chan), "I2C read[%d] = %x\n",
+ data, msgs[msg].buf[data]);
+
+ /* Don't stop twice */
+ if (last && ret == 0)
+ goto exit;
+ } else {
+ ret = write_data(i2c_chan, msgs[msg].buf[data], no_ack);
+ dev_dbg(eio_dev(i2c_chan), "I2C write[%d] = %x\n",
+ data, msgs[msg].buf[data]);
+ }
+ if (ret)
+ goto exit;
+ }
+ }
+
+ if (!ret)
+ ret = bus_stop(i2c_chan);
+
+ if (!ret)
+ goto exit;
+
+exit:
+ if (ret)
+ reset_bus(i2c_chan);
+
+ i2c_clear(i2c_chan);
+ clr_inuse(i2c_chan);
+
+ mutex_unlock(&i2c_chan->lock);
+ return ret ? ret : nmsgs;
+}
+
+static int smbus_xfer(struct i2c_adapter *adap, u16 addr,
+ u16 flags, char is_read, u8 cmd,
+ int size, union i2c_smbus_data *data)
+{
+ int ret;
+ struct eio_i2c_chan *i2c_chan = i2c_get_adapdata(adap);
+ int nmsgs = is_read ? 2 : 1;
+ u8 buf[I2C_SMBUS_BLOCK_MAX + sizeof(u32)] = { cmd, };
+ struct i2c_msg msgs[2] = {
+ { .addr = addr, .flags = flags & ~I2C_M_RD, .buf = buf + 0 },
+ { .addr = addr, .flags = flags | I2C_M_RD, .buf = buf + 1 },
+ };
+
+ /* Non-I2C channels use the SMB engine, except I2C block variants we emulate */
+ if (!is_i2c(i2c_chan) && size != I2C_SMBUS_I2C_BLOCK_DATA)
+ return smb_access(i2c_chan, addr, is_read, cmd, size, data);
+
+ if (data) {
+ buf[0] = cmd;
+ /* FIX: preserve other flags; only toggle I2C_M_RD */
+ msgs[0].flags = is_read ? (flags | I2C_M_RD) : (flags & ~I2C_M_RD);
+ msgs[1].buf = data->block;
+ }
+
+ switch (size) {
+ case I2C_SMBUS_QUICK:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_QUICK on I2C\n");
+ nmsgs = 1;
+ break;
+
+ case I2C_SMBUS_BYTE:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE on I2C\n");
+ nmsgs = 1;
+ msgs[0].len = 1;
+ msgs[0].buf = is_read ? data->block : buf;
+ msgs[0].flags = is_read ? (flags | I2C_M_RD) : (flags & ~I2C_M_RD);
+ break;
+
+ case I2C_SMBUS_BYTE_DATA:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE_DATA on I2C\n");
+ if (!data)
+ return -EINVAL;
+ msgs[0].len = is_read ? 1 : 2;
+ buf[1] = data->block[0];
+ msgs[1].len = 1;
+ break;
+
+ case I2C_SMBUS_WORD_DATA:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_WORD_DATA on I2C\n");
+ if (!data)
+ return -EINVAL;
+ msgs[0].len = is_read ? 1 : 3;
+ msgs[1].len = 2;
+ buf[1] = data->block[0];
+ buf[2] = data->block[1];
+ msgs[1].buf = data->block;
+ break;
+
+ case I2C_SMBUS_I2C_BLOCK_DATA:
+ case I2C_SMBUS_I2C_BLOCK_BROKEN:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_I2C_BLOCK_(DATA/BROKEN) on I2C len=%d\n",
+ data->block[0]);
+ if (!data)
+ return -EINVAL;
+ msgs[0].len = is_read ? 1 : data->block[0] + 1;
+ msgs[1].len = data->block[0];
+ msgs[1].buf = data->block + 1;
+ if (msgs[0].len >= I2C_SMBUS_BLOCK_MAX ||
+ msgs[1].len >= I2C_SMBUS_BLOCK_MAX)
+ return -EINVAL;
+ if (!is_read)
+ memcpy(buf + 1, data->block + 1, msgs[0].len);
+ break;
+
+ case I2C_SMBUS_PROC_CALL:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_PROC_CALL on I2C\n");
+ if (!data)
+ return -EINVAL;
+ nmsgs = 2;
+ msgs[0].flags = flags & ~I2C_M_RD;
+ msgs[0].len = 3;
+ buf[1] = data->block[0];
+ buf[2] = data->block[1];
+ msgs[1].len = 2;
+ break;
+
+ case I2C_SMBUS_BLOCK_DATA:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_DATA on I2C not supported\n");
+ return -EINVAL;
+
+ case I2C_SMBUS_BLOCK_PROC_CALL:
+ dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_PROC_CALL on I2C not supported\n");
+ return -EINVAL;
+
+ default:
+ return -EINVAL;
+ }
+
+ ret = i2c_xfer(adap, msgs, nmsgs);
+ return ret < 0 ? ret : 0;
+}
+
+static int load_i2c(struct device *dev, enum eio_chan_id id,
+ struct eio_i2c_chan *i2c_chan)
+{
+ u32 base_lo, base_hi, base;
+ int ldn = LDN_I2C0 + id;
+ struct eio_i2c_dev *eio_i2c = i2c_chan->parent;
+ struct regmap *map;
+
+ if (!eio_i2c || !eio_i2c->regmap)
+ return dev_err_probe(dev, -ENODEV, "missing parent/regmap\n");
+
+ map = eio_i2c->regmap;
+
+ /* Read channel I/O base via shared PNP window */
+ mutex_lock(&eio_i2c->pnp_mutex);
+ if (regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_ENTER) ||
+ regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_ENTER) ||
+ regmap_write(map, REG_PNP_INDEX, REG_LDN) ||
+ regmap_write(map, REG_PNP_DATA, ldn) ||
+ regmap_write(map, REG_PNP_INDEX, REG_BASE_HI) ||
+ regmap_read(map, REG_PNP_DATA, &base_hi) ||
+ regmap_write(map, REG_PNP_INDEX, REG_BASE_LO) ||
+ regmap_read(map, REG_PNP_DATA, &base_lo) ||
+ regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_EXIT)) {
+ mutex_unlock(&eio_i2c->pnp_mutex);
+ dev_err(dev, "error read/write I2C[%d] IO port\n", id);
+ return -EIO;
+ }
+ mutex_unlock(&eio_i2c->pnp_mutex);
+
+ base = (base_hi << 8) | base_lo;
+ if (base == 0xFFFF || base == 0) {
+ dev_dbg(dev, "i2c[%d] base addr=%#x (not in-use)\n", id, base);
+ return -ENODEV;
+ }
+
+ dev_dbg(dev, "i2c[%d] base addr=%#x\n", id, base);
+
+ /* Bind channel (no per-chan dev) */
+ i2c_chan->base = (u16)base;
+ i2c_chan->id = id;
+
+ /* Per-channel frequency policy */
+ if (i2c_chan->freq_override != USE_DEFAULT)
+ set_freq(i2c_chan, i2c_chan->freq_override);
+
+ get_freq(i2c_chan, &i2c_chan->freq_override);
+
+ return 0;
+}
+
+static u32 functionality(struct i2c_adapter *adap)
+{
+ struct eio_i2c_chan *i2c_chan = i2c_get_adapdata(adap);
+
+ return is_i2c(i2c_chan) ? SUPPORTED_I2C : SUPPORTED_SMB;
+}
+
+static const struct i2c_algorithm algo = {
+ .smbus_xfer = smbus_xfer,
+ .master_xfer = i2c_xfer,
+ .functionality = functionality,
+};
+
+static int eio_i2c_probe(struct platform_device *pdev)
+{
+ static const char * const names[] = { "i2c0", "i2c1", "smb0", "smb1" };
+ struct device *dev = &pdev->dev;
+ struct eio_i2c_dev *eio_i2c;
+ struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
+ int ret = 0;
+ enum eio_chan_id ch;
+
+ if (!eio_dev) {
+ dev_err(dev, "Error contact eio_core\n");
+ return -ENODEV;
+ }
+
+ eio_i2c = devm_kzalloc(dev, sizeof(*eio_i2c), GFP_KERNEL);
+ if (!eio_i2c)
+ return -ENOMEM;
+
+ eio_i2c->dev = dev;
+ eio_i2c->mfd = dev->parent;
+ eio_i2c->regmap = dev_get_regmap(dev->parent, NULL);
+ if (!eio_i2c->regmap)
+ return dev_err_probe(dev, -ENODEV, "parent regmap not found\n");
+
+ mutex_init(&eio_i2c->pnp_mutex);
+ platform_set_drvdata(pdev, eio_i2c);
+
+ for (ch = EIO_I2C0; ch < MAX_I2C_SMB; ch++) {
+ struct eio_i2c_chan *i2c_chan;
+
+ i2c_chan = devm_kzalloc(dev, sizeof(*i2c_chan), GFP_KERNEL);
+ if (!i2c_chan) {
+ ret = -ENOMEM;
+ break;
+ }
+
+ i2c_chan->parent = eio_i2c;
+ i2c_chan->freq_override = USE_DEFAULT;
+ mutex_init(&i2c_chan->lock);
+
+ if (load_i2c(dev, ch, i2c_chan)) {
+ dev_info(dev, "No %s%d!\n", (ch < 2) ? "I2C" : "SMBus", ch & 1);
+ continue;
+ }
+
+ i2c_chan->adap.owner = THIS_MODULE;
+ i2c_chan->adap.class = I2C_CLASS_HWMON;
+ i2c_chan->adap.algo = &algo;
+ i2c_chan->adap.dev.parent = dev;
+ snprintf(i2c_chan->adap.name, sizeof(i2c_chan->adap.name), "eio-%s",
+ names[ch]);
+
+ i2c_set_adapdata(&i2c_chan->adap, i2c_chan);
+
+ ret = i2c_add_adapter(&i2c_chan->adap);
+ dev_info(dev, "Add %s%d %s. %d\n", (ch < 2) ? "I2C" : "SMBus",
+ ch, ret ? "Error" : "Success", ret);
+ if (ret)
+ break;
+
+ eio_i2c->chan[ch] = i2c_chan;
+ }
+
+ if (ret) {
+ for (ch = EIO_I2C0; ch < MAX_I2C_SMB; ch++) {
+ if (eio_i2c->chan[ch]) {
+ i2c_del_adapter(&eio_i2c->chan[ch]->adap);
+ eio_i2c->chan[ch] = NULL;
+ }
+ }
+ }
+
+ return ret;
+}
+
+static void eio_i2c_remove(struct platform_device *pdev)
+{
+ struct eio_i2c_dev *eio_i2c = platform_get_drvdata(pdev);
+ enum eio_chan_id ch;
+
+ for (ch = EIO_I2C0; ch < MAX_I2C_SMB; ch++) {
+ if (eio_i2c->chan[ch]) {
+ i2c_del_adapter(&eio_i2c->chan[ch]->adap);
+ eio_i2c->chan[ch] = NULL;
+ }
+ }
+}
+
+static struct platform_driver eio_i2c_driver = {
+ .probe = eio_i2c_probe,
+ .remove = eio_i2c_remove,
+ .driver = {
+ .name = "i2c_eio",
+ },
+};
+
+module_platform_driver(eio_i2c_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("I2C driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: eio_core");
--
2.43.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 5/8] Add Advantech EIO Backlight driver
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
` (3 preceding siblings ...)
2026-07-14 15:54 ` [PATCH v2 4/8] Add Advantech EIO I2C driver Ramiro Oliveira
@ 2026-07-14 15:54 ` Ramiro Oliveira
2026-07-14 16:05 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 6/8] Add Advantech EIO Watchdog driver Ramiro Oliveira
` (2 subsequent siblings)
7 siblings, 1 reply; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
This driver controls the Video Backlight block of the Advantech EIO chip.
Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
MAINTAINERS | 1 +
drivers/video/backlight/Kconfig | 6 +
drivers/video/backlight/Makefile | 1 +
drivers/video/backlight/eio_bl.c | 243 +++++++++++++++++++++++++++++++++++++++
4 files changed, 251 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index e228d12a2326..d2d7d815ed44 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/video/backlight/eio_bl.c
F: include/linux/mfd/eio.h
ADVANTECH SWBTN DRIVER
diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig
index 7c66b8840d88..a5559a7db75d 100644
--- a/drivers/video/backlight/Kconfig
+++ b/drivers/video/backlight/Kconfig
@@ -522,6 +522,12 @@ config BACKLIGHT_RAVE_SP
help
Support for backlight control on RAVE SP device.
+config BACKLIGHT_EIO
+ tristate "Advantech EIO Backlight"
+ depends on MFD_EIO && BACKLIGHT_CLASS_DEVICE
+ help
+ Backlight driver for Advantech EIO.
+
config BACKLIGHT_LED
tristate "Generic LED based Backlight Driver"
depends on LEDS_CLASS && OF
diff --git a/drivers/video/backlight/Makefile b/drivers/video/backlight/Makefile
index 34469711c6cd..724246242526 100644
--- a/drivers/video/backlight/Makefile
+++ b/drivers/video/backlight/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_BACKLIGHT_CGBC) += cgbc_bl.o
obj-$(CONFIG_BACKLIGHT_CLASS_DEVICE) += backlight.o
obj-$(CONFIG_BACKLIGHT_DA903X) += da903x_bl.o
obj-$(CONFIG_BACKLIGHT_DA9052) += da9052_bl.o
+obj-$(CONFIG_BACKLIGHT_EIO) += eio_bl.o
obj-$(CONFIG_BACKLIGHT_EP93XX) += ep93xx_bl.o
obj-$(CONFIG_BACKLIGHT_GPIO) += gpio_backlight.o
obj-$(CONFIG_BACKLIGHT_HP680) += hp680_bl.o
diff --git a/drivers/video/backlight/eio_bl.c b/drivers/video/backlight/eio_bl.c
new file mode 100644
index 000000000000..c2826400de45
--- /dev/null
+++ b/drivers/video/backlight/eio_bl.c
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Backlight driver for Advantech EIO Embedded controller.
+ *
+ * Copyright (C) 2025 Advantech Corporation. All rights reserved.
+ */
+
+#include <linux/backlight.h>
+#include <linux/errno.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/uaccess.h>
+
+#define PMC_BL_WRITE 0x20
+#define PMC_BL_READ 0x21
+
+#define BL_CTRL_STATUS 0x00
+#define BL_CTRL_ENABLE 0x12
+#define BL_CTRL_ENABLE_INVERT 0x13
+#define BL_CTRL_DUTY 0x14
+#define BL_CTRL_INVERT 0x15
+#define BL_CTRL_FREQ 0x16
+
+#define BL_MAX 2
+
+#define BL_STATUS_AVAIL 0x01
+#define BL_ENABLE_OFF 0x00
+#define BL_ENABLE_ON 0x01
+#define BL_ENABLE_AUTO BIT(1)
+
+#define USE_DEFAULT -1
+#define THERMAL_MAX 100
+
+static uint bri_freq = USE_DEFAULT;
+module_param(bri_freq, uint, 0444);
+MODULE_PARM_DESC(bri_freq, "Setup backlight PWM frequency.\n");
+
+static int bri_invert = USE_DEFAULT;
+module_param(bri_invert, int, 0444);
+MODULE_PARM_DESC(bri_invert, "Setup backlight PWM polarity.\n");
+
+static int bl_power_invert = USE_DEFAULT;
+module_param(bl_power_invert, int, 0444);
+MODULE_PARM_DESC(bl_power_invert, "Setup backlight enable pin polarity.\n");
+
+struct eio_bl_dev {
+ struct device *mfd;
+ u8 id;
+};
+
+static int pmc_write(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
+{
+ struct pmc_op op = {
+ .cmd = PMC_BL_WRITE,
+ .control = ctrl,
+ .device_id = dev_id,
+ .payload = (u8 *)data,
+ .size = (ctrl == BL_CTRL_FREQ) ? 4 : 1,
+ };
+
+ return eio_core_pmc_operation(mfd, &op);
+}
+
+static int pmc_read(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
+{
+ struct pmc_op op = {
+ .cmd = PMC_BL_READ,
+ .control = ctrl,
+ .device_id = dev_id,
+ .payload = (u8 *)data,
+ .size = (ctrl == BL_CTRL_FREQ) ? 4 : 1,
+ };
+
+ return eio_core_pmc_operation(mfd, &op);
+}
+
+static int bl_update_status(struct backlight_device *bl)
+{
+ struct eio_bl_dev *eio_bl = bl_get_data(bl);
+ u8 duty = clamp_val(backlight_get_brightness(bl), 0, THERMAL_MAX);
+ u8 sw = backlight_is_blank(bl);
+ int ret;
+
+ /* Setup PWM duty */
+ ret = pmc_write(eio_bl->mfd, BL_CTRL_DUTY, eio_bl->id, &duty);
+ if (ret)
+ return ret;
+
+ /* Setup backlight enable pin */
+ return pmc_write(eio_bl->mfd, BL_CTRL_ENABLE, eio_bl->id, &sw);
+}
+
+static int bl_get_brightness(struct backlight_device *bl)
+{
+ struct eio_bl_dev *eio_bl = bl_get_data(bl);
+ u8 duty = 0;
+ int ret;
+
+ ret = pmc_read(eio_bl->mfd, BL_CTRL_DUTY, eio_bl->id, &duty);
+
+ if (ret)
+ return ret;
+
+ return duty;
+}
+
+static const struct backlight_ops bl_ops = {
+ .get_brightness = bl_get_brightness,
+ .update_status = bl_update_status,
+ .options = BL_CORE_SUSPENDRESUME,
+};
+
+static int bl_init(struct device *dev, int id,
+ struct backlight_properties *props)
+{
+ int ret;
+ u8 enabled = 0;
+ u8 status = 0;
+
+ /* Check EC-supported backlight */
+ ret = pmc_read(dev, BL_CTRL_STATUS, id, &status);
+ if (ret)
+ return ret;
+
+ if (!(status & BL_STATUS_AVAIL)) {
+ dev_dbg(dev, "eio_bl%d hardware report disabled.\n", id);
+ return -ENODEV;
+ }
+
+ ret = pmc_read(dev, BL_CTRL_DUTY, id, &props->brightness);
+ if (ret)
+ return ret;
+
+ /* Invert PWM */
+ if (bri_invert > USE_DEFAULT) {
+ ret = pmc_write(dev, BL_CTRL_INVERT, id, &bri_invert);
+ if (ret)
+ return ret;
+ }
+
+ ret = pmc_read(dev, BL_CTRL_INVERT, id, &bri_invert);
+ if (ret)
+ return ret;
+
+ if (bri_freq != USE_DEFAULT) {
+ ret = pmc_write(dev, BL_CTRL_FREQ, id, &bri_freq);
+ if (ret)
+ return ret;
+ }
+
+ ret = pmc_read(dev, BL_CTRL_FREQ, id, &bri_freq);
+ if (ret)
+ return ret;
+
+ if (bl_power_invert >= USE_DEFAULT) {
+ ret = pmc_write(dev, BL_CTRL_ENABLE_INVERT, id, &bl_power_invert);
+ if (ret)
+ return ret;
+ }
+
+ ret = pmc_read(dev, BL_CTRL_ENABLE_INVERT, id, &bl_power_invert);
+ if (ret)
+ return ret;
+
+ /* Read power state */
+ ret = pmc_read(dev, BL_CTRL_ENABLE, id, &enabled);
+ if (ret)
+ return ret;
+
+ props->power = enabled ? BACKLIGHT_POWER_OFF : BACKLIGHT_POWER_ON;
+
+ return 0;
+}
+
+static int bl_probe(struct platform_device *pdev)
+{
+ u8 id;
+ int ret;
+ struct device *dev = &pdev->dev;
+ struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
+
+ if (!eio_dev) {
+ dev_err(dev, "eio_core not present\n");
+ return -ENODEV;
+ }
+
+ for (id = 0; id < BL_MAX; id++) {
+ char name[32];
+ struct backlight_properties props;
+ struct eio_bl_dev *eio_bl;
+ struct backlight_device *bl;
+
+ memset(&props, 0, sizeof(props));
+ props.type = BACKLIGHT_RAW;
+ props.max_brightness = THERMAL_MAX;
+ props.power = BACKLIGHT_POWER_OFF;
+ props.brightness = props.max_brightness;
+ props.scale = BACKLIGHT_SCALE_NON_LINEAR;
+
+ eio_bl = devm_kzalloc(dev, sizeof(*eio_bl), GFP_KERNEL);
+ if (!eio_bl)
+ return -ENOMEM;
+
+ eio_bl->mfd = dev->parent;
+ eio_bl->id = id;
+
+ ret = bl_init(eio_bl->mfd, id, &props);
+ if (ret) {
+ dev_info(dev, "%d No Backlight %u enabled!\n", ret, id);
+ continue;
+ }
+
+ snprintf(name, sizeof(name), "%s%u", pdev->name, id);
+
+ bl = devm_backlight_device_register(dev, name, dev, eio_bl,
+ &bl_ops, &props);
+
+ if (IS_ERR(bl)) {
+ ret = PTR_ERR(bl);
+ if (ret == -EPROBE_DEFER)
+ return ret;
+
+ dev_err(dev, "register %s failed: %d\n", name, ret);
+ continue;
+ }
+ }
+ return ret;
+}
+
+static struct platform_driver bl_driver = {
+ .probe = bl_probe,
+ .driver = {
+ .name = "eio_bl",
+ },
+};
+
+module_platform_driver(bl_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Backlight driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 6/8] Add Advantech EIO Watchdog driver
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
` (4 preceding siblings ...)
2026-07-14 15:54 ` [PATCH v2 5/8] Add Advantech EIO Backlight driver Ramiro Oliveira
@ 2026-07-14 15:54 ` Ramiro Oliveira
2026-07-14 16:07 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 7/8] Add Advantech EIO Thermal driver Ramiro Oliveira
2026-07-14 15:54 ` [PATCH v2 8/8] Add Advantech EIO Fan driver Ramiro Oliveira
7 siblings, 1 reply; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
This commit adds the driver to control the Advantech EIO Watchdog block,
this block is included in the Advantech EIO Embedded Controller.
Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
MAINTAINERS | 1 +
drivers/watchdog/Kconfig | 7 +
drivers/watchdog/Makefile | 1 +
drivers/watchdog/eio_wdt.c | 641 +++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 650 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index d2d7d815ed44..8d35fd7c5599 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -617,6 +617,7 @@ F: drivers/hwmon/eio-hwmon.c
F: drivers/i2c/busses/i2c-eio.c
F: drivers/mfd/eio_core.c
F: drivers/video/backlight/eio_bl.c
+F: drivers/watchdog/eio_wdt.c
F: include/linux/mfd/eio.h
ADVANTECH SWBTN DRIVER
diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
index 9f013d774897..bc0f13f4f79c 100644
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -265,6 +265,13 @@ config DA9062_WATCHDOG
This driver can be built as a module. The module name is da9062_wdt.
+config EIO_WATCHDOG
+ tristate "Advantech EIO Watchdog"
+ depends on MFD_EIO
+ help
+ Watchdog timer driver for the Advantech EIO.
+ If unsure, say N.
+
config GPIO_WATCHDOG
tristate "Watchdog device controlled through GPIO-line"
depends on GPIOLIB && (ACPI || OF)
diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile
index cb78e9932eae..eee059cfe1da 100644
--- a/drivers/watchdog/Makefile
+++ b/drivers/watchdog/Makefile
@@ -229,6 +229,7 @@ obj-$(CONFIG_DA9052_WATCHDOG) += da9052_wdt.o
obj-$(CONFIG_DA9055_WATCHDOG) += da9055_wdt.o
obj-$(CONFIG_DA9062_WATCHDOG) += da9062_wdt.o
obj-$(CONFIG_DA9063_WATCHDOG) += da9063_wdt.o
+obj-$(CONFIG_EIO_WATCHDOG) += eio_wdt.o
obj-$(CONFIG_GPIO_WATCHDOG) += gpio_wdt.o
obj-$(CONFIG_WDAT_WDT) += wdat_wdt.o
obj-$(CONFIG_WM831X_WATCHDOG) += wm831x_wdt.o
diff --git a/drivers/watchdog/eio_wdt.c b/drivers/watchdog/eio_wdt.c
new file mode 100644
index 000000000000..b3c036a004b3
--- /dev/null
+++ b/drivers/watchdog/eio_wdt.c
@@ -0,0 +1,641 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Advantech EIO Watchdog Driver
+ *
+ * Copyright (C) 2025 Advantech Co., Ltd.
+ */
+
+#include <linux/interrupt.h>
+#include <linux/jiffies.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/reboot.h>
+#include <linux/uaccess.h>
+#include <linux/watchdog.h>
+
+#define WATCHDOG_TIMEOUT 60
+#define WATCHDOG_PRETIMEOUT 10
+
+/* Support Flags */
+#define SUPPORT_AVAILABLE BIT(0)
+#define SUPPORT_PWRBTN BIT(3)
+#define SUPPORT_IRQ BIT(4)
+#define SUPPORT_SCI BIT(5)
+#define SUPPORT_PIN BIT(6)
+#define SUPPORT_RESET BIT(7)
+
+/* PMC registers */
+#define REG_STATUS 0x00
+#define REG_CONTROL 0x02
+#define REG_EVENT 0x10
+#define REG_PWR_EVENT_TIME 0x12
+#define REG_IRQ_EVENT_TIME 0x13
+#define REG_RESET_EVENT_TIME 0x14
+#define REG_PIN_EVENT_TIME 0x15
+#define REG_SCI_EVENT_TIME 0x16
+#define REG_IRQ_NUMBER 0x17
+
+/* PMC command and control */
+#define CMD_WDT_WRITE 0x2A
+#define CMD_WDT_READ 0x2B
+#define CTRL_STOP 0x00
+#define CTRL_START 0x01
+#define CTRL_TRIGGER 0x02
+
+/* I/O register and its flags */
+#define IOREG_UNLOCK 0x87
+#define IOREG_LOCK 0xAA
+#define IOREG_LDN 0x07
+#define IOREG_LDN_PMCIO 0x0F
+#define IOREG_IRQ 0x70
+#define IOREG_WDT_STATUS 0x30
+
+/* Flags */
+#define FLAG_WDT_ENABLED 0x01
+#define FLAG_TRIGGER_IRQ BIT(4)
+
+/* Mapping event type to supported bit */
+#define EVENT_BIT(type) BIT(type + 2)
+
+enum event_type {
+ EVENT_NONE,
+ EVENT_PWRBTN,
+ EVENT_IRQ,
+ EVENT_SCI,
+ EVENT_PIN
+};
+
+struct eio_wdt_dev {
+ u32 event_type;
+ u32 support;
+ int irq;
+ unsigned long last_time;
+ struct regmap *iomap;
+ struct device *mfd;
+ struct device *dev;
+ struct watchdog_device wdd;
+ struct eio_dev *core;
+};
+
+static char * const type_strs[] = {
+ "NONE",
+ "PWRBTN",
+ "IRQ",
+ "SCI",
+ "PIN",
+};
+
+static u32 type_regs[] = {
+ REG_RESET_EVENT_TIME,
+ REG_PWR_EVENT_TIME,
+ REG_IRQ_EVENT_TIME,
+ REG_SCI_EVENT_TIME,
+ REG_PIN_EVENT_TIME,
+};
+
+/* Specify the pin triggered on pretimeout or timeout */
+static char *event_type = "NONE";
+module_param(event_type, charp, 0);
+MODULE_PARM_DESC(event_type, "Watchdog timeout event type (NONE, PWRBTN, IRQ, SCI, PIN)");
+
+static int pmc_write(struct device *dev, u8 ctrl, void *data)
+{
+ struct pmc_op op = {
+ .cmd = CMD_WDT_WRITE,
+ .control = ctrl,
+ .payload = data,
+ .size = (ctrl <= REG_EVENT) ? 1 :
+ (ctrl >= REG_IRQ_NUMBER) ? 1 : 4,
+ };
+ return eio_core_pmc_operation(dev, &op);
+}
+
+static int pmc_read(struct device *dev, u8 ctrl, void *data)
+{
+ struct pmc_op op = {
+ .cmd = CMD_WDT_READ,
+ .control = ctrl,
+ .payload = data,
+ .size = (ctrl <= REG_EVENT) ? 1 :
+ (ctrl >= REG_IRQ_NUMBER) ? 1 : 4,
+ };
+ return eio_core_pmc_operation(dev, &op);
+}
+
+static int wdt_set_timeout(struct watchdog_device *wdd, unsigned int timeout)
+{
+ wdd->timeout = timeout;
+ return 0;
+}
+
+static int wdt_set_pretimeout(struct watchdog_device *wdd, unsigned int pretimeout)
+{
+ wdd->pretimeout = pretimeout;
+ return 0;
+}
+
+static int wdt_get_type(struct eio_wdt_dev *eio_wdt)
+{
+ int i;
+
+ for (i = 1; i < ARRAY_SIZE(type_strs); i++) {
+ if (strcasecmp(event_type, type_strs[i]) == 0) {
+ if ((eio_wdt->support & EVENT_BIT(i)) == 0) {
+ dev_err(eio_wdt->dev,
+ "This board doesn't support %s trigger type\n",
+ event_type);
+ return -EINVAL;
+ }
+ eio_wdt->event_type = i;
+ return 0;
+ }
+ }
+ return 0;
+}
+
+static int get_time(struct eio_wdt_dev *eio_wdt, u8 ctrl, u32 *val)
+{
+ int ret;
+
+ ret = pmc_read(eio_wdt->mfd, ctrl, val);
+ if (ret)
+ return ret;
+
+ /* ms to sec */
+ *val /= 1000;
+
+ return 0;
+}
+
+static int set_time(struct eio_wdt_dev *eio_wdt, u8 ctrl, u32 time)
+{
+ /* sec to ms */
+ time *= 1000;
+
+ return pmc_write(eio_wdt->mfd, ctrl, &time);
+}
+
+static int wdt_set_config(struct eio_wdt_dev *eio_wdt)
+{
+ int ret, type;
+ u32 event_time = 0;
+ u32 reset_time = 0;
+
+ if (eio_wdt->event_type > EVENT_PIN)
+ return -EFAULT;
+
+ /* Calculate event time and reset time */
+ if (eio_wdt->wdd.pretimeout && eio_wdt->wdd.timeout) {
+ if (eio_wdt->wdd.timeout < eio_wdt->wdd.pretimeout)
+ return -EINVAL;
+
+ reset_time = eio_wdt->wdd.timeout;
+ event_time = eio_wdt->wdd.timeout - eio_wdt->wdd.pretimeout;
+
+ } else if (eio_wdt->wdd.timeout) {
+ reset_time = eio_wdt->event_type ? 0 : eio_wdt->wdd.timeout;
+ event_time = eio_wdt->event_type ? eio_wdt->wdd.timeout : 0;
+ }
+
+ /* Set reset time */
+ ret = set_time(eio_wdt, REG_RESET_EVENT_TIME, reset_time);
+ if (ret)
+ return ret;
+
+ /* Set every other times */
+ for (type = 1; type < ARRAY_SIZE(type_regs); type++) {
+ ret = set_time(eio_wdt, type_regs[type],
+ (eio_wdt->event_type == type) ? event_time : 0);
+ if (ret)
+ return ret;
+ }
+
+ dev_dbg(eio_wdt->dev, "Config wdt reset time %u\n", reset_time);
+ dev_dbg(eio_wdt->dev, "Config wdt event time %u\n", event_time);
+ dev_dbg(eio_wdt->dev, "Config wdt event type %s\n",
+ type_strs[eio_wdt->event_type]);
+
+ return 0;
+}
+
+static int wdt_get_config(struct eio_wdt_dev *eio_wdt)
+{
+ int ret, type;
+ u32 event_time = 0, reset_time = 0;
+
+ /* Get Reset Time */
+ ret = get_time(eio_wdt, REG_RESET_EVENT_TIME, &reset_time);
+ if (ret)
+ return ret;
+
+ dev_dbg(eio_wdt->dev, "Timeout H/W default timeout: %u secs\n", reset_time);
+
+ /* Get every other times */
+ for (type = 1; type < ARRAY_SIZE(type_regs); type++) {
+ if ((eio_wdt->support & EVENT_BIT(type)) == 0)
+ continue;
+
+ ret = get_time(eio_wdt, type_regs[type], &event_time);
+ if (ret)
+ return ret;
+
+ if (event_time == 0)
+ continue;
+
+ if (reset_time) {
+ if (reset_time < event_time)
+ continue;
+
+ eio_wdt->wdd.timeout = reset_time;
+ eio_wdt->wdd.pretimeout = reset_time - event_time;
+
+ dev_dbg(eio_wdt->dev,
+ "Pretimeout H/W enabled with event %s of %u secs\n",
+ type_strs[type], eio_wdt->wdd.pretimeout);
+ } else {
+ eio_wdt->wdd.timeout = event_time;
+ eio_wdt->wdd.pretimeout = 0;
+ }
+
+ eio_wdt->event_type = type;
+
+ dev_dbg(eio_wdt->dev, "Timeout H/W enabled of %u secs\n",
+ eio_wdt->wdd.timeout);
+ return 0;
+ }
+
+ eio_wdt->event_type = EVENT_NONE;
+ eio_wdt->wdd.pretimeout = reset_time ? 0 : WATCHDOG_PRETIMEOUT;
+ eio_wdt->wdd.timeout = reset_time ? reset_time : WATCHDOG_TIMEOUT;
+
+ return 0;
+}
+
+static int set_ctrl(struct eio_wdt_dev *eio_wdt, u8 ctrl)
+{
+ return pmc_write(eio_wdt->mfd, REG_CONTROL, &ctrl);
+}
+
+static int wdt_start(struct watchdog_device *wdd)
+{
+ struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+ int ret;
+
+ ret = wdt_set_config(eio_wdt);
+ if (ret)
+ return ret;
+
+ ret = set_ctrl(eio_wdt, CTRL_START);
+ if (!ret) {
+ eio_wdt->last_time = jiffies;
+ dev_dbg(eio_wdt->dev, "Watchdog started\n");
+ }
+
+ return ret;
+}
+
+static int wdt_stop(struct watchdog_device *wdd)
+{
+ struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+ int ret;
+
+ dev_dbg(eio_wdt->dev, "Watchdog stopped\n");
+ eio_wdt->last_time = 0;
+
+ ret = set_ctrl(eio_wdt, CTRL_STOP);
+ return ret;
+}
+
+static int wdt_ping(struct watchdog_device *wdd)
+{
+ struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+ int ret;
+
+ dev_dbg(eio_wdt->dev, "Watchdog ping\n");
+
+ ret = set_ctrl(eio_wdt, CTRL_TRIGGER);
+ if (!ret)
+ eio_wdt->last_time = jiffies;
+
+ return ret;
+}
+
+static int wdt_support(struct eio_wdt_dev *eio_wdt)
+{
+ u8 support;
+
+ if (pmc_read(eio_wdt->mfd, REG_STATUS, &support))
+ return -EIO;
+
+ if (!(support & SUPPORT_AVAILABLE))
+ return -ENODEV;
+
+ if ((support & SUPPORT_RESET) != SUPPORT_RESET)
+ return -ENODEV;
+
+ eio_wdt->support = support;
+
+ return 0;
+}
+
+static int wdt_get_irq_io(struct eio_wdt_dev *eio_wdt)
+{
+ int ret = 0;
+ int idx = EIO_PNP_INDEX;
+ int data = EIO_PNP_DATA;
+ struct regmap *map = eio_wdt->iomap;
+
+ mutex_lock(&eio_wdt->core->mutex);
+
+ /* Unlock EC IO port */
+ ret |= regmap_write(map, idx, IOREG_UNLOCK);
+ ret |= regmap_write(map, idx, IOREG_UNLOCK);
+
+ /* Select logical device to PMC */
+ ret |= regmap_write(map, idx, IOREG_LDN);
+ ret |= regmap_write(map, data, IOREG_LDN_PMCIO);
+
+ /* Get IRQ number */
+ ret |= regmap_write(map, idx, IOREG_IRQ);
+ ret |= regmap_read(map, data, &eio_wdt->irq);
+
+ /* Lock back */
+ ret |= regmap_write(map, idx, IOREG_LOCK);
+
+ mutex_unlock(&eio_wdt->core->mutex);
+
+ return ret ? -EIO : 0;
+}
+
+static int wdt_get_irq_pmc(struct eio_wdt_dev *eio_wdt)
+{
+ return pmc_read(eio_wdt->mfd, REG_IRQ_NUMBER, &eio_wdt->irq);
+}
+
+static int wdt_get_irq(struct eio_wdt_dev *eio_wdt)
+{
+ int ret;
+
+ if (!(eio_wdt->support & BIT(EVENT_IRQ)))
+ return -ENODEV;
+
+ ret = wdt_get_irq_pmc(eio_wdt);
+ if (ret) {
+ dev_err(eio_wdt->dev, "Error get irq by pmc\n");
+ return ret;
+ }
+
+ if (eio_wdt->irq)
+ return 0;
+
+ /* Fallback: get IRQ number from EC IO space */
+ ret = wdt_get_irq_io(eio_wdt);
+ if (ret) {
+ dev_err(eio_wdt->dev, "Error get irq by io\n");
+ return ret;
+ }
+
+ if (!eio_wdt->irq) {
+ dev_err(eio_wdt->dev, "Error IRQ number = 0\n");
+ return -EIO;
+ }
+
+ return 0;
+}
+
+static int wdt_set_irq_io(struct eio_wdt_dev *eio_wdt)
+{
+ int ret = 0;
+ int idx = EIO_PNP_INDEX;
+ int data = EIO_PNP_DATA;
+ struct regmap *map = eio_wdt->iomap;
+
+ mutex_lock(&eio_wdt->core->mutex);
+
+ /* Unlock EC IO port */
+ ret = regmap_write(map, idx, IOREG_UNLOCK);
+ if (ret)
+ goto unlock;
+ ret = regmap_write(map, idx, IOREG_UNLOCK);
+ if (ret)
+ goto unlock;
+
+ /* Select logical device to PMC */
+ ret = regmap_write(map, idx, IOREG_LDN);
+ if (ret)
+ goto unlock;
+ ret = regmap_write(map, data, IOREG_LDN_PMCIO);
+ if (ret)
+ goto unlock;
+
+ /* Enable WDT */
+ ret = regmap_write(map, idx, IOREG_WDT_STATUS);
+ if (ret)
+ goto unlock;
+ ret = regmap_write(map, data, FLAG_WDT_ENABLED);
+ if (ret)
+ goto unlock;
+
+ /* Set IRQ number */
+ ret = regmap_write(map, idx, IOREG_IRQ);
+ if (ret)
+ goto unlock;
+ ret = regmap_write(map, data, eio_wdt->irq);
+ if (ret)
+ goto unlock;
+
+ /* Lock back */
+ ret = regmap_write(map, idx, IOREG_LOCK);
+
+unlock:
+ mutex_unlock(&eio_wdt->core->mutex);
+ return ret;
+}
+
+static int wdt_set_irq_pmc(struct eio_wdt_dev *eio_wdt)
+{
+ return pmc_write(eio_wdt->mfd, REG_IRQ_NUMBER, &eio_wdt->irq);
+}
+
+static int wdt_set_irq(struct eio_wdt_dev *eio_wdt)
+{
+ int ret;
+
+ if (!(eio_wdt->support & BIT(EVENT_IRQ)))
+ return -ENODEV;
+
+ ret = wdt_set_irq_io(eio_wdt);
+ if (ret) {
+ dev_err(eio_wdt->dev, "Error set irq by io\n");
+ return ret;
+ }
+
+ ret = wdt_set_irq_pmc(eio_wdt);
+ if (ret) {
+ dev_err(eio_wdt->dev, "Error set irq by pmc\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+static int wdt_get_irq_event(struct eio_wdt_dev *eio_wdt)
+{
+ u8 status;
+
+ if (pmc_read(eio_wdt->mfd, REG_EVENT, &status))
+ return 0;
+
+ return status;
+}
+
+static irqreturn_t wdt_isr(int irq, void *arg)
+{
+ return IRQ_WAKE_THREAD;
+}
+
+static irqreturn_t wdt_threaded_isr(int irq, void *arg)
+{
+ struct eio_wdt_dev *eio_wdt = arg;
+ u8 status = wdt_get_irq_event(eio_wdt) & FLAG_TRIGGER_IRQ;
+
+ if (!status)
+ return IRQ_NONE;
+
+ if (eio_wdt->wdd.pretimeout) {
+ watchdog_notify_pretimeout(&eio_wdt->wdd);
+ } else {
+ dev_crit(eio_wdt->dev, "Watchdog expired, rebooting\n");
+ emergency_restart();
+ }
+
+ return IRQ_HANDLED;
+}
+
+static int query_irq(struct eio_wdt_dev *eio_wdt)
+{
+ int ret = 0;
+
+ ret = wdt_get_irq(eio_wdt);
+ if (ret)
+ return ret;
+
+ dev_dbg(eio_wdt->dev, "IRQ = %d\n", eio_wdt->irq);
+
+ return wdt_set_irq(eio_wdt);
+}
+
+static int wdt_init(struct eio_wdt_dev *eio_wdt)
+{
+ int ret;
+
+ ret = wdt_support(eio_wdt);
+ if (ret)
+ return ret;
+
+ ret = wdt_get_config(eio_wdt);
+ if (ret)
+ return ret;
+
+ ret = wdt_get_type(eio_wdt);
+ if (ret)
+ return ret;
+
+ if (eio_wdt->event_type == EVENT_IRQ)
+ ret = query_irq(eio_wdt);
+
+ return ret;
+}
+
+static const struct watchdog_ops wdt_ops = {
+ .owner = THIS_MODULE,
+ .start = wdt_start,
+ .stop = wdt_stop,
+ .ping = wdt_ping,
+ .set_timeout = wdt_set_timeout,
+ .set_pretimeout = wdt_set_pretimeout,
+};
+
+static struct watchdog_info wdinfo = {
+ .identity = KBUILD_MODNAME,
+ .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING |
+ WDIOF_PRETIMEOUT | WDIOF_MAGICCLOSE,
+};
+
+static int eio_wdt_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct eio_wdt_dev *eio_wdt;
+ struct watchdog_device *wdd;
+ int ret = 0;
+
+ eio_wdt = devm_kzalloc(dev, sizeof(*eio_wdt), GFP_KERNEL);
+ if (!eio_wdt)
+ return -ENOMEM;
+
+ eio_wdt->dev = dev;
+ eio_wdt->mfd = dev->parent;
+ eio_wdt->iomap = dev_get_regmap(dev->parent, NULL);
+ if (!eio_wdt->iomap)
+ return dev_err_probe(dev, -ENODEV, "parent regmap missing\n");
+
+ eio_wdt->core = dev_get_drvdata(dev->parent);
+ if (!eio_wdt->core)
+ return dev_err_probe(dev, -ENODEV, "eio_core not present\n");
+
+ ret = wdt_init(eio_wdt);
+ if (ret) {
+ dev_err(dev, "wdt_init fail\n");
+ return -EIO;
+ }
+
+ if (eio_wdt->event_type == EVENT_IRQ) {
+ ret = devm_request_threaded_irq(dev, eio_wdt->irq,
+ wdt_isr, wdt_threaded_isr,
+ IRQF_SHARED | IRQF_ONESHOT, pdev->name,
+ eio_wdt);
+ if (ret) {
+ dev_err_probe(dev, ret, "IRQ %u request fail: Disabled.\n",
+ eio_wdt->irq);
+ return ret;
+ }
+ }
+
+ wdd = &eio_wdt->wdd;
+ wdd->info = &wdinfo;
+ wdd->ops = &wdt_ops;
+ wdd->parent = dev;
+ wdd->min_timeout = 1;
+ wdd->max_timeout = 0x7FFF;
+
+ if (ret) {
+ dev_err(dev, "Init timeout fail\n");
+ return ret;
+ }
+
+ watchdog_stop_on_reboot(&eio_wdt->wdd);
+ watchdog_stop_on_unregister(&eio_wdt->wdd);
+
+ watchdog_set_drvdata(&eio_wdt->wdd, eio_wdt);
+ platform_set_drvdata(pdev, eio_wdt);
+
+ ret = devm_watchdog_register_device(dev, &eio_wdt->wdd);
+ if (ret)
+ dev_err(dev, "Cannot register watchdog device (err: %d)\n", ret);
+
+ return ret;
+}
+
+static struct platform_driver eio_wdt_driver = {
+ .probe = eio_wdt_probe,
+ .driver = {
+ .name = "eio_wdt",
+ },
+};
+module_platform_driver(eio_wdt_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Watchdog interface for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 7/8] Add Advantech EIO Thermal driver
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
` (5 preceding siblings ...)
2026-07-14 15:54 ` [PATCH v2 6/8] Add Advantech EIO Watchdog driver Ramiro Oliveira
@ 2026-07-14 15:54 ` Ramiro Oliveira
2026-07-14 16:05 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 8/8] Add Advantech EIO Fan driver Ramiro Oliveira
7 siblings, 1 reply; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
This commit adds the driver to control the Advantech EIO Thermal block,
this block is included in the Advantech EIO Embedded Controller.
Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
MAINTAINERS | 1 +
drivers/thermal/Kconfig | 9 ++
drivers/thermal/Makefile | 1 +
drivers/thermal/eio_thermal.c | 346 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 357 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 8d35fd7c5599..ded89e70d6cc 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_thermal.c
F: drivers/video/backlight/eio_bl.c
F: drivers/watchdog/eio_wdt.c
F: include/linux/mfd/eio.h
diff --git a/drivers/thermal/Kconfig b/drivers/thermal/Kconfig
index 810eeccedfba..fc078012a989 100644
--- a/drivers/thermal/Kconfig
+++ b/drivers/thermal/Kconfig
@@ -427,6 +427,15 @@ config DA9062_THERMAL
zone.
Compatible with the DA9062 and DA9061 PMICs.
+config EIO_THERMAL
+ tristate "Advantech EIO Thermal zone"
+ depends on MFD_EIO && THERMAL
+ help
+ Thermal zone support for the Advantech EIO. This driver exposes
+ temperature readings, trip points and protection enable/disable via
+ the Linux thermal framework. 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 3b249195c088..6abce8933e34 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_THERMAL) += eio_thermal.o
obj-y += intel/
obj-$(CONFIG_TI_SOC_THERMAL) += ti-soc-thermal/
obj-y += st/
diff --git a/drivers/thermal/eio_thermal.c b/drivers/thermal/eio_thermal.c
new file mode 100644
index 000000000000..ad923acaf138
--- /dev/null
+++ b/drivers/thermal/eio_thermal.c
@@ -0,0 +1,346 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * eio_thermal
+ * ================
+ * Thermal zone driver for Advantech EIO embedded controller's thermal
+ * protect mechanism.
+ *
+ * In EIO chip. The smart fan has 3 trips. While the temperature:
+ * - Touch Trip0: Shutdown --> Cut off the power.
+ * - Touch Trip1: Poweroff --> Send the power button signal.
+ * - between Trip2 and Trip1: Throttle --> Intermittently hold the CPU.
+ *
+ * PowerOff Shutdown
+ * ^ ^
+ * Throttle | |
+ * | | |
+ * +--------+------------+----------+---------
+ * 0 trip2 trip1 trip0 (Temp)
+ *
+ * Copyright (C) 2025 Advantech Corporation. All rights reserved.
+ */
+
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+#include <linux/thermal.h>
+
+#define CMD_THERM_WRITE 0x10
+#define CMD_THERM_READ 0x11
+#define THERM_NUM 0x04
+#define UNIT_PER_TEMP 100
+
+#define CTRL_STATE 0x00
+#define CTRL_TYPE 0x01
+#define CTRL_ERROR 0x04
+#define CTRL_VALUE 0x10
+#define CTRL_MAX 0x11
+#define CTRL_MIN 0x12
+#define CTRL_THROTTLE 0x20
+#define CTRL_THROTTLE_HI 0x21
+#define CTRL_THROTTLE_LO 0x22
+#define CTRL_THROTTLE_DEFAULT 0x28
+#define CTRL_THROTTLE_HI_DEFAULT 0x29
+#define CTRL_THROTTLE_LO_DEFAULT 0x2A
+#define CTRL_POWEROFF 0x30
+#define CTRL_POWEROFF_HI 0x31
+#define CTRL_POWEROFF_LO 0x32
+#define CTRL_POWEROFF_DEFAULT 0x38
+#define CTRL_POWEROFF_HI_DEFAULT 0x39
+#define CTRL_POWEROFF_LO_DEFAULT 0x3A
+#define CTRL_SHUTDOWN 0x40
+#define CTRL_SHUTDOWN_HI 0x41
+#define CTRL_SHUTDOWN_LO 0x42
+#define CTRL_SHUTDOWN_DEFAULT 0x48
+#define CTRL_SHUTDOWN_HI_DEFAULT 0x49
+#define CTRL_SHUTDOWN_LO_DEFAULT 0x4A
+#define CTRL_SB_TSI_STATUS 0x80
+#define CTRL_SB_TSI_ACCESS 0x81
+#define CTRL_WARN_STATUS 0x90
+#define CTRL_WARN_BEEP 0x91
+#define CTRL_WARN_TEMP 0x92
+
+#define THERM_ERR_NO 0x00
+#define THERM_ERR_CHANNEL 0x01
+#define THERM_ERR_HI 0x02
+#define THERM_ERR_LO 0x03
+
+#define NAME_SIZE 5
+
+#define TRIP_NUM 3
+#define TRIP_SHUTDOWN 0
+#define TRIP_POWEROFF 1
+#define TRIP_THROTTLE 2
+/* Beep mechanism no stable. Not supported, yet. */
+#define TRIP_BEEP 3
+
+#define THERMAL_POLLING_DELAY 2000 /* millisecond */
+#define THERMAL_PASSIVE_DELAY 1000
+
+#define DECI_KELVIN_TO_MILLI_CELSIUS(t) (((t) - 2731) * 100)
+#define MILLI_CELSIUS_TO_DECI_KELVIN(t) (((t) / 100) + 2731)
+
+#define THERM_STS_AVAIL BIT(0)
+#define THERM_STS_THROTTLE_AVAIL BIT(1)
+#define THERM_STS_POWEROFF_AVAIL BIT(2)
+#define THERM_STS_SHUTDOWN_AVAIL BIT(3)
+#define THERM_STS_THROTTLE_EVT BIT(4)
+#define THERM_STS_POWEROFF_EVT BIT(5)
+#define THERM_STS_SHUTDOWN_EVT BIT(6)
+/* BIT(7) reserved */
+#define THERM_STS_THROTTLE_ON BIT(8)
+#define THERM_STS_POWEROFF_ON BIT(9)
+#define THERM_STS_SHUTDOWN_ON BIT(10)
+/* BIT(11) reserved */
+#define THERM_STS_THROTTLE_LOG BIT(12)
+#define THERM_STS_POWEROFF_LOG BIT(13)
+#define THERM_STS_SHUTDOWN_LOG BIT(14)
+
+static u8 pmc_len[] = {
+/* 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f */
+/* 0 */ 2, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 1 */ 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 2 */ 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 3 */ 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 4 */ 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 5 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 6 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 7 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 8 */ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 9 */ 2, 1, 2,
+};
+
+static char therm_name[0x20][NAME_SIZE + 1] = {
+ "CPU0", "CPU1", "CPU2", "CPU3", "SYS0", "SYS1", "SYS2", "SYS3",
+ "AUX0", "AUX1", "AUX2", "AUX3", "DIMM0", "DIMM1", "DIMM2", "DIMM3",
+ "PCH", "VGA", "", "", "", "", "", "",
+ "", "", "", "", "OEM0", "OEM1", "OEM2", "OEM3",
+};
+
+static const u8 ctrl_map[] = {
+ CTRL_SHUTDOWN, CTRL_POWEROFF, CTRL_THROTTLE
+};
+
+struct eio_thermal_dev {
+ struct device *mfd;
+ struct device *dev;
+ u8 ch;
+ u8 name;
+};
+
+struct eio_trip_dev {
+ struct device *mfd;
+ u8 ch;
+ u8 idx;
+};
+
+static int pmc_write(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
+{
+ if (ctrl >= ARRAY_SIZE(pmc_len))
+ return -EINVAL;
+
+ struct pmc_op op = {
+ .cmd = CMD_THERM_WRITE,
+ .control = ctrl,
+ .device_id = dev_id,
+ .payload = (u8 *)data,
+ .size = pmc_len[ctrl],
+ };
+
+ return eio_core_pmc_operation(mfd, &op);
+}
+
+static int pmc_read(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
+{
+ if (ctrl >= ARRAY_SIZE(pmc_len))
+ return -EINVAL;
+
+ struct pmc_op op = {
+ .cmd = CMD_THERM_READ,
+ .control = ctrl,
+ .device_id = dev_id,
+ .payload = (u8 *)data,
+ .size = pmc_len[ctrl],
+ };
+
+ return eio_core_pmc_operation(mfd, &op);
+}
+
+static int eio_tz_get_temp(struct thermal_zone_device *tzd, int *temp)
+{
+ struct eio_thermal_dev *eio_thermal = thermal_zone_device_priv(tzd);
+ u16 val = 0;
+ int ret;
+
+ ret = pmc_read(eio_thermal->mfd, CTRL_VALUE, eio_thermal->ch, &val);
+ if (ret)
+ return ret;
+
+ *temp = DECI_KELVIN_TO_MILLI_CELSIUS(val);
+ return 0;
+}
+
+static int eio_tz_set_trip_temp(struct thermal_zone_device *tzd,
+ const struct thermal_trip *trip, int temp)
+{
+ struct eio_thermal_dev *eio_thermal = thermal_zone_device_priv(tzd);
+ const u8 ctl = (uintptr_t)trip->priv;
+ u16 val;
+
+ if (temp < 1000)
+ return -EINVAL;
+
+ val = MILLI_CELSIUS_TO_DECI_KELVIN(temp);
+ return pmc_write(eio_thermal->mfd, ctl, eio_thermal->ch, &val);
+}
+
+static int eio_tz_change_mode(struct thermal_zone_device *tzd,
+ enum thermal_device_mode mode)
+{
+ struct eio_thermal_dev *eio_thermal = thermal_zone_device_priv(tzd);
+ int trip;
+ int ret = 0;
+
+ for (trip = 0; trip < TRIP_NUM; trip++) {
+ ret = pmc_write(eio_thermal->mfd, ctrl_map[trip], eio_thermal->ch, &mode);
+ if (ret)
+ dev_err(eio_thermal->dev, "Error when %s trip num %d\n",
+ mode == THERMAL_DEVICE_ENABLED ? "enabling" : "disabling",
+ trip);
+ }
+
+ return ret;
+}
+
+static struct thermal_zone_device_ops zone_ops = {
+ .get_temp = eio_tz_get_temp,
+ .set_trip_temp = eio_tz_set_trip_temp,
+ .change_mode = eio_tz_change_mode,
+};
+
+static struct thermal_zone_params zone_params = {
+ .no_hwmon = true,
+};
+
+static int eio_thermal_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ int ch;
+
+ if (!dev_get_drvdata(dev->parent)) {
+ dev_err(dev, "eio_core not present\n");
+ return -ENODEV;
+ }
+
+ for (ch = 0; ch < THERM_NUM; ch++) {
+ u16 state = 0;
+ u8 name = 0;
+ u16 hi_shutdown = 0, hi_poweroff = 0, hi_throttle = 0;
+ int t_shutdown = 0, t_poweroff = 0, t_throttle = 0;
+ struct thermal_trip trips[TRIP_NUM];
+ int ntrips = 0;
+ struct eio_thermal_dev *eio_th;
+ struct thermal_zone_device *tzd;
+
+ if (pmc_read(dev->parent, CTRL_STATE, (u8)ch, &state) ||
+ pmc_read(dev->parent, CTRL_TYPE, (u8)ch, &name)) {
+ dev_info(dev, "thermal%d: PMC read error\n", ch);
+ continue;
+ }
+
+ if (!(state & THERM_STS_AVAIL) ||
+ !((state & THERM_STS_THROTTLE_AVAIL) ||
+ (state & THERM_STS_POWEROFF_AVAIL) ||
+ (state & THERM_STS_SHUTDOWN_AVAIL))) {
+ dev_info(dev, "thermal%d: firmware not activated\n", ch);
+ continue;
+ }
+
+ if (name >= ARRAY_SIZE(therm_name) || !therm_name[name][0]) {
+ dev_info(dev, "thermal%d: unknown sensor name idx=%u\n", ch, name);
+ continue;
+ }
+
+ /* Throttle starts a 1C increase it */
+ int throttle_temp = MILLI_CELSIUS_TO_DECI_KELVIN(60000);
+
+ pmc_write(dev->parent, CTRL_THROTTLE_HI, (u8)ch, &throttle_temp);
+
+ pmc_read(dev->parent, CTRL_SHUTDOWN_HI, (u8)ch, &hi_shutdown);
+ pmc_read(dev->parent, CTRL_POWEROFF_HI, (u8)ch, &hi_poweroff);
+ pmc_read(dev->parent, CTRL_THROTTLE_HI, (u8)ch, &hi_throttle);
+
+ t_shutdown = DECI_KELVIN_TO_MILLI_CELSIUS(hi_shutdown);
+ t_poweroff = DECI_KELVIN_TO_MILLI_CELSIUS(hi_poweroff);
+ t_throttle = DECI_KELVIN_TO_MILLI_CELSIUS(hi_throttle);
+
+ ntrips = 0;
+ if (hi_shutdown) {
+ trips[ntrips].type = THERMAL_TRIP_CRITICAL;
+ trips[ntrips].temperature = t_shutdown;
+ trips[ntrips].flags = THERMAL_TRIP_FLAG_RW_TEMP;
+ trips[ntrips].priv = THERMAL_INT_TO_TRIP_PRIV(TRIP_SHUTDOWN),
+ ntrips++;
+ }
+ if (hi_poweroff) {
+ trips[ntrips].type = THERMAL_TRIP_HOT;
+ trips[ntrips].temperature = t_poweroff;
+ trips[ntrips].flags = THERMAL_TRIP_FLAG_RW_TEMP;
+ trips[ntrips].priv = THERMAL_INT_TO_TRIP_PRIV(TRIP_POWEROFF),
+ ntrips++;
+ }
+ if (hi_throttle) {
+ trips[ntrips].type = THERMAL_TRIP_PASSIVE;
+ trips[ntrips].temperature = t_throttle;
+ trips[ntrips].flags = THERMAL_TRIP_FLAG_RW_TEMP;
+ trips[ntrips].priv = THERMAL_INT_TO_TRIP_PRIV(TRIP_THROTTLE),
+ ntrips++;
+ }
+ if (!ntrips) {
+ dev_info(dev, "thermal%d: no valid trips\n", ch);
+ continue;
+ }
+
+ eio_th = devm_kzalloc(dev, sizeof(*eio_th), GFP_KERNEL);
+ if (!eio_th)
+ return -ENOMEM;
+ eio_th->ch = (u8)ch;
+ eio_th->mfd = dev->parent;
+ eio_th->dev = dev;
+
+ tzd = thermal_zone_device_register_with_trips(therm_name[name],
+ trips,
+ ntrips,
+ eio_th,
+ &zone_ops,
+ &zone_params,
+ THERMAL_PASSIVE_DELAY,
+ THERMAL_POLLING_DELAY);
+ if (IS_ERR(tzd))
+ return PTR_ERR(tzd);
+ /* Make sure zones start disabled */
+ thermal_zone_device_disable(tzd);
+
+ dev_info(dev, "%s thermal up (ch=%d)\n", therm_name[name], ch);
+ }
+
+ return 0;
+}
+
+static struct platform_driver eio_thermal_driver = {
+ .probe = eio_thermal_probe,
+ .driver = {
+ .name = "eio_thermal",
+ },
+};
+module_platform_driver(eio_thermal_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Thermal driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH v2 8/8] Add Advantech EIO Fan driver
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
` (6 preceding siblings ...)
2026-07-14 15:54 ` [PATCH v2 7/8] Add Advantech EIO Thermal driver Ramiro Oliveira
@ 2026-07-14 15:54 ` Ramiro Oliveira
2026-07-14 16:14 ` sashiko-bot
7 siblings, 1 reply; 17+ messages in thread
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
Lukasz Luba
Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
Thomas Kastner, Ramiro Oliveira
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 <ramiro.oliveira@advantech.com>
---
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 <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Fan driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* Re: [PATCH v2 7/8] Add Advantech EIO Thermal driver
2026-07-14 15:54 ` [PATCH v2 7/8] Add Advantech EIO Thermal driver Ramiro Oliveira
@ 2026-07-14 16:05 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-07-14 16:05 UTC (permalink / raw)
To: Ramiro Oliveira; +Cc: linux-hwmon, dri-devel, lee, mfd, linux-watchdog
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] The driver registers thermal zones in a loop but fails to unregister them on probe failure and completely lacks a `.remove` callback.
- [High] The `.set_trip_temp` callback incorrectly uses the abstract trip ID as a control register address, overwriting critical read-only state registers.
- [Medium] Multi-byte variable addresses (`int`, `enum`) are passed directly to hardware access functions which read bytes in host memory order, breaking on big-endian architectures.
- [Low] The child driver communicates with the parent using a bespoke inter-driver function instead of utilizing standard MFD mechanisms.
- [Low] Error codes from `pmc_write` are masked during the trip iteration in `eio_tz_change_mode`.
--
--- Patch [7]: [PATCH v2 7/8] Add Advantech EIO Thermal driver ---
commit 001e79fa1d8c0bd96fb7bffbb91c6090c53b08a4
Author: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Add Advantech EIO Thermal driver
This commit adds the driver to control the Advantech EIO Thermal block,
this block is included in the Advantech EIO Embedded Controller.
> diff --git a/drivers/thermal/eio_thermal.c b/drivers/thermal/eio_thermal.c
> index 0000000000000..ad923acaf1389 100644
> --- /dev/null
> +++ b/drivers/thermal/eio_thermal.c
[ ... ]
> +static int pmc_write(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
> +{
> + if (ctrl >= ARRAY_SIZE(pmc_len))
> + return -EINVAL;
> +
> + struct pmc_op op = {
> + .cmd = CMD_THERM_WRITE,
> + .control = ctrl,
> + .device_id = dev_id,
> + .payload = (u8 *)data,
> + .size = pmc_len[ctrl],
> + };
> +
> + return eio_core_pmc_operation(mfd, &op);
[Severity: Low]
This isn't a bug, but does calling a bespoke helper function exported by
the parent MFD driver conflict with the MFD subsystem guidelines? The MFD
documentation suggests child drivers should use standard APIs and avoid
custom accessors or helper functions in the parent to pass state.
> +}
[ ... ]
> +static int eio_tz_set_trip_temp(struct thermal_zone_device *tzd,
> + const struct thermal_trip *trip, int temp)
> +{
> + struct eio_thermal_dev *eio_thermal = thermal_zone_device_priv(tzd);
> + const u8 ctl = (uintptr_t)trip->priv;
> + u16 val;
> +
> + if (temp < 1000)
> + return -EINVAL;
> +
> + val = MILLI_CELSIUS_TO_DECI_KELVIN(temp);
> + return pmc_write(eio_thermal->mfd, ctl, eio_thermal->ch, &val);
[Severity: High]
Does this write to the wrong register?
During probe, trip->priv is initialized with logical indices like
TRIP_SHUTDOWN (which is 0). Here, the ctl variable is passed directly to
pmc_write() as the control register address. Will this overwrite state
registers like CTRL_STATE (0x00) instead of the limit registers like
CTRL_SHUTDOWN_HI (0x41)?
> +}
> +
> +static int eio_tz_change_mode(struct thermal_zone_device *tzd,
> + enum thermal_device_mode mode)
> +{
> + struct eio_thermal_dev *eio_thermal = thermal_zone_device_priv(tzd);
> + int trip;
> + int ret = 0;
> +
> + for (trip = 0; trip < TRIP_NUM; trip++) {
> + ret = pmc_write(eio_thermal->mfd, ctrl_map[trip], eio_thermal->ch, &mode);
[Severity: Medium]
Will this fail to set the mode correctly on big-endian architectures?
The mode variable is an enum (typically 4 bytes), but it's passed as a byte
array payload to pmc_write(). Because pmc_write() only copies 1 or 2 bytes,
won't it read the most significant bytes (usually zeros) on big-endian
systems?
[Severity: Low]
Could an early error in this loop be masked? If pmc_write() fails on the
first iteration but succeeds on the next, ret will be overwritten with 0,
hiding the failure from the caller.
> + if (ret)
> + dev_err(eio_thermal->dev, "Error when %s trip num %d\n",
> + mode == THERMAL_DEVICE_ENABLED ? "enabling" : "disabling",
> + trip);
> + }
> +
> + return ret;
> +}
[ ... ]
> +static int eio_thermal_probe(struct platform_device *pdev)
> +{
[ ... ]
> + /* Throttle starts a 1C increase it */
> + int throttle_temp = MILLI_CELSIUS_TO_DECI_KELVIN(60000);
> +
> + pmc_write(dev->parent, CTRL_THROTTLE_HI, (u8)ch, &throttle_temp);
[Severity: Medium]
Does this have the same big-endian issue as eio_tz_change_mode()?
The throttle_temp variable is a 32-bit int, and taking its address for a
1 or 2 byte payload write in pmc_write() might read the zeroed upper bytes
on big-endian systems.
[ ... ]
> + tzd = thermal_zone_device_register_with_trips(therm_name[name],
> + trips,
> + ntrips,
> + eio_th,
> + &zone_ops,
> + &zone_params,
> + THERMAL_PASSIVE_DELAY,
> + THERMAL_POLLING_DELAY);
> + if (IS_ERR(tzd))
> + return PTR_ERR(tzd);
[Severity: High]
Does this path leak resources if a failure occurs after some zones have been
successfully registered?
If the loop successfully registers a thermal zone on ch=0 but fails on ch=1,
it returns immediately without unregistering the thermal zone created for
ch=0.
> + /* Make sure zones start disabled */
> + thermal_zone_device_disable(tzd);
> +
> + dev_info(dev, "%s thermal up (ch=%d)\n", therm_name[name], ch);
> + }
> +
> + return 0;
> +}
> +
> +static struct platform_driver eio_thermal_driver = {
> + .probe = eio_thermal_probe,
> + .driver = {
> + .name = "eio_thermal",
> + },
> +};
[Severity: High]
Are there use-after-free and resource leak bugs when this driver is unbound?
Because there is no .remove callback in the platform driver, unbinding it via
sysfs will leave the registered thermal zones active in the thermal core
while devres frees the eio_th private data. Could this lead to a
use-after-free when the core polls the zones?
> +module_platform_driver(eio_thermal_driver);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260714-upstream-v2-v2-0-76e5e41026db@advantech.com?part=7
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver
2026-07-14 15:54 ` [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver Ramiro Oliveira
@ 2026-07-14 16:05 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-07-14 16:05 UTC (permalink / raw)
To: Ramiro Oliveira; +Cc: linux-hwmon, lee, dri-devel, mfd, linux-watchdog
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Use of bespoke parent-child accessors instead of standard kernel APIs.
- [Low] Static tables missing `const` qualifier and containing entirely unused fields.
--
--- Patch [3]: [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver ---
commit 63ead2676c728959d0dbe217692c0d9b6f79eaa3
Author: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Add Advantech EIO Hardware Monitor driver
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.
> diff --git a/drivers/hwmon/eio-hwmon.c b/drivers/hwmon/eio-hwmon.c
> index 0000000000000..dd877664e9e89
> --- /dev/null
> +++ b/drivers/hwmon/eio-hwmon.c
[ ... ]
> +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[] = {
[Severity: Low]
This isn't a bug, but should this array be declared as static const so it is
placed in the .rodata section rather than .data?
Also, it looks like the name and item fields are initialized but never read
anywhere in this driver. Can they be removed to reduce the memory footprint?
[ ... ]
> +static struct {
> + enum _sen_type type;
> + u8 ctrl;
> + int size;
> + bool write;
> +} ctrl_para[] = {
[Severity: Low]
Similarly, should this lookup table be declared as static const?
[ ... ]
> +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);
[Severity: Medium]
Does this bypass standard kernel APIs by using a custom parent-child accessor?
The MFD subsystem guidelines recommend avoiding bespoke accessors or helper
functions in the parent to pass state to child devices. Instead, child
drivers are encouraged to use standard subsystem APIs to prevent rigid,
tightly-coupled designs.
Could this communication be refactored to use standard APIs like regmap?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260714-upstream-v2-v2-0-76e5e41026db@advantech.com?part=3
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 5/8] Add Advantech EIO Backlight driver
2026-07-14 15:54 ` [PATCH v2 5/8] Add Advantech EIO Backlight driver Ramiro Oliveira
@ 2026-07-14 16:05 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-07-14 16:05 UTC (permalink / raw)
To: Ramiro Oliveira; +Cc: mfd, linux-hwmon, linux-watchdog, dri-devel, lee
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Partial overwrite of multi-byte integers via 1-byte pointer casts corrupts state logic on different architectures.
- [High] Global module parameters are improperly mutated during the device probe loop, causing cross-device state contamination.
- [High] The probe function arbitrarily fails if the last evaluated backlight is unavailable, tearing down successfully initialized devices.
- [High] The backlight enable/disable logic and state initialization are fundamentally inverted, causing the screen to turn on when blanked and turn off when activated.
- [Medium] A tautological bounds check unconditionally overwrites the hardware's internal default for the enable-invert pin.
--
--- Patch [5]: [PATCH v2 5/8] Add Advantech EIO Backlight driver ---
commit 7087ac6577d7b4e152b12ed2ee29e0940be894fd
Author: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Add Advantech EIO Backlight driver
This commit introduces a new backlight driver for the Advantech EIO Embedded
controller. It supports up to two backlight devices through PMC read and
write operations. Module parameters are also provided for setting PWM
frequency, polarity, and enable pin polarity.
> diff --git a/drivers/video/backlight/eio_bl.c b/drivers/video/backlight/eio_bl.c
> new file mode 100644
> index 0000000000000..c2826400de45b
> --- /dev/null
> +++ b/drivers/video/backlight/eio_bl.c
[ ... ]
> +static int pmc_read(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
> +{
> + struct pmc_op op = {
> + .cmd = PMC_BL_READ,
> + .control = ctrl,
> + .device_id = dev_id,
> + .payload = (u8 *)data,
> + .size = (ctrl == BL_CTRL_FREQ) ? 4 : 1,
> + };
> +
> + return eio_core_pmc_operation(mfd, &op);
> +}
[Severity: High]
Does this cast of the data pointer to a single byte payload cause issues
when reading into 32-bit integer variables? Since variables like bri_invert
and bl_power_invert are initialized to -1 (0xFFFFFFFF), writing only a single
byte to them via this cast will leave the upper bytes intact, potentially
resulting in corrupted values like -256 on little-endian systems.
> +static int bl_update_status(struct backlight_device *bl)
> +{
> + struct eio_bl_dev *eio_bl = bl_get_data(bl);
> + u8 duty = clamp_val(backlight_get_brightness(bl), 0, THERMAL_MAX);
> + u8 sw = backlight_is_blank(bl);
> + int ret;
> +
> + /* Setup PWM duty */
> + ret = pmc_write(eio_bl->mfd, BL_CTRL_DUTY, eio_bl->id, &duty);
> + if (ret)
> + return ret;
> +
> + /* Setup backlight enable pin */
> + return pmc_write(eio_bl->mfd, BL_CTRL_ENABLE, eio_bl->id, &sw);
> +}
[Severity: High]
Is the logic here backwards for enabling the backlight? The function
backlight_is_blank() returns true (1) when the display should be blanked,
but passing this directly to BL_CTRL_ENABLE would turn the screen on
(since BL_ENABLE_ON is 0x01).
[ ... ]
> +static int bl_init(struct device *dev, int id,
> + struct backlight_properties *props)
> +{
> + int ret;
> + u8 enabled = 0;
> + u8 status = 0;
[ ... ]
> + /* Invert PWM */
> + if (bri_invert > USE_DEFAULT) {
> + ret = pmc_write(dev, BL_CTRL_INVERT, id, &bri_invert);
> + if (ret)
> + return ret;
> + }
> +
> + ret = pmc_read(dev, BL_CTRL_INVERT, id, &bri_invert);
[Severity: High]
Could reading the hardware state directly into the global module parameter
bri_invert cause problems for systems with multiple backlight devices?
When bl_probe() loops over multiple devices, the hardware state from the
first device permanently overwrites the global parameter, which would then
contaminate the configuration for the second device.
[ ... ]
> + if (bl_power_invert >= USE_DEFAULT) {
> + ret = pmc_write(dev, BL_CTRL_ENABLE_INVERT, id, &bl_power_invert);
> + if (ret)
> + return ret;
> + }
[Severity: Medium]
Since bl_power_invert is initialized to USE_DEFAULT, does this >= condition
always evaluate to true (-1 >= -1)? This seems to cause an unconditional
pmc_write() that could overwrite the hardware's internal default state for
the enable-invert pin.
> + ret = pmc_read(dev, BL_CTRL_ENABLE_INVERT, id, &bl_power_invert);
> + if (ret)
> + return ret;
> +
> + /* Read power state */
> + ret = pmc_read(dev, BL_CTRL_ENABLE, id, &enabled);
> + if (ret)
> + return ret;
> +
> + props->power = enabled ? BACKLIGHT_POWER_OFF : BACKLIGHT_POWER_ON;
[Severity: High]
Does this initialization map the power state inversely? If enabled is true,
assigning BACKLIGHT_POWER_OFF seems to indicate the backlight is off when
the hardware is actually enabled.
[ ... ]
> +static int bl_probe(struct platform_device *pdev)
> +{
> + u8 id;
> + int ret;
> + struct device *dev = &pdev->dev;
> + struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
[ ... ]
> + for (id = 0; id < BL_MAX; id++) {
> + char name[32];
> + struct backlight_properties props;
> + struct eio_bl_dev *eio_bl;
> + struct backlight_device *bl;
> +
> + memset(&props, 0, sizeof(props));
> + props.type = BACKLIGHT_RAW;
> + props.max_brightness = THERMAL_MAX;
> + props.power = BACKLIGHT_POWER_OFF;
> + props.brightness = props.max_brightness;
> + props.scale = BACKLIGHT_SCALE_NON_LINEAR;
> +
> + eio_bl = devm_kzalloc(dev, sizeof(*eio_bl), GFP_KERNEL);
> + if (!eio_bl)
> + return -ENOMEM;
> +
> + eio_bl->mfd = dev->parent;
> + eio_bl->id = id;
> +
> + ret = bl_init(eio_bl->mfd, id, &props);
> + if (ret) {
> + dev_info(dev, "%d No Backlight %u enabled!\n", ret, id);
> + continue;
> + }
> +
> + snprintf(name, sizeof(name), "%s%u", pdev->name, id);
> +
> + bl = devm_backlight_device_register(dev, name, dev, eio_bl,
> + &bl_ops, &props);
> +
> + if (IS_ERR(bl)) {
> + ret = PTR_ERR(bl);
> + if (ret == -EPROBE_DEFER)
> + return ret;
> +
> + dev_err(dev, "register %s failed: %d\n", name, ret);
> + continue;
> + }
> + }
> + return ret;
> +}
[Severity: High]
If the first device initializes successfully but a subsequent iteration
fails bl_init() (such as when a port is unpopulated), ret is updated and
the loop continues. Does this result in bl_probe() returning an error code
at the end, causing the successfully initialized first device to be torn
down by devres?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260714-upstream-v2-v2-0-76e5e41026db@advantech.com?part=5
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 2/8] Add Advantech EIO GPIO driver
2026-07-14 15:54 ` [PATCH v2 2/8] Add Advantech EIO GPIO driver Ramiro Oliveira
@ 2026-07-14 16:06 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-07-14 16:06 UTC (permalink / raw)
To: Ramiro Oliveira; +Cc: mfd, lee, linux-hwmon, dri-devel, linux-watchdog
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] Off-by-one boundary check in `check_pin()` allows out-of-bounds array read of `group_map`.
- [High] Integer shift overflow when generating a 64-bit mask using the `BIT()` macro on 32-bit architectures.
- [Medium] The supported pins bitmask (`avail`) is calculated but never enforced, allowing interaction with unsupported pins.
- [Medium] Failure to check the return value of `pmc_write` allows silent failures when configuring pin direction.
- [Medium] Meaningful error codes returned by `gpio_init()` are squashed to `-EIO` in `gpio_probe()`.
- [Medium] Setting GPIO direction to output before configuring the output level causes a physical hardware glitch.
- [Medium] Endianness mismatch when reading hardware registers byte-by-byte into a multi-byte integer address.
- [Low] Off-by-one boundary check in `pmc_read()` allows out-of-bounds array read.
- [Low] Returning `-ENOMEM` instead of `-EINVAL` for invalid control arguments in `pmc_read` and `pmc_write`.
--
--- Patch [2]: [PATCH v2 2/8] Add Advantech EIO GPIO driver ---
commit 22717cbc8cf200661da102ffdd6bee43f768b719
Author: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Add Advantech EIO GPIO driver
This driver controls the GPIO component of the Advantech EIO chip.
> diff --git a/drivers/gpio/gpio-eio.c b/drivers/gpio/gpio-eio.c
> new file mode 100644
> index 0000000000000..34e1aefd0716c
> --- /dev/null
> +++ b/drivers/gpio/gpio-eio.c
[ ... ]
> +static int pmc_write(struct device *mfd_dev, u8 ctrl, u8 dev_id, void *data)
> +{
> + struct pmc_op op = {
> + .cmd = EIO_GPIO_WRITE,
> + .control = ctrl,
> + .device_id = dev_id,
> + .payload = (u8 *)data,
> + };
> +
> + if (ctrl >= ARRAY_SIZE(ctrl_para))
> + return -ENOMEM;
[Severity: Low]
Should this bounds check return -EINVAL instead of -ENOMEM?
Returning -ENOMEM signifies a memory allocation failure to the caller, but
no allocation is occurring here.
[ ... ]
> +static int pmc_read(struct device *mfd_dev, u8 ctrl, u8 dev_id, void *data)
> +{
> + struct pmc_op op = {
> + .cmd = EIO_GPIO_READ,
> + .control = ctrl,
> + .device_id = dev_id,
> + .payload = (u8 *)data,
> + };
> +
> + if (ctrl > ARRAY_SIZE(ctrl_para))
> + return -ENOMEM;
[Severity: Low]
Is there an off-by-one error in this boundary check?
Using greater-than allows an input exactly equal to the array size to bypass
the check, which could lead to an out-of-bounds read on the following line.
[Severity: Low]
Additionally, should this return -EINVAL instead of -ENOMEM when the control
argument is invalid?
[ ... ]
> +static int dir_output(struct gpio_chip *chip, unsigned int offset, int value)
> +{
> + u8 dir = 1;
> + u8 val = value;
> +
> + pmc_write(chip->parent, EIO_GPIO_PIN_DIR, offset, &dir);
> +
> + return pmc_write(chip->parent, EIO_GPIO_PIN_LEVEL, offset, &val);
> +}
[Severity: Medium]
Does setting the GPIO direction to output before configuring the output level
cause a physical hardware glitch?
Every time a pin is reconfigured as an output, it might briefly drive whatever
stale default value resides in its output latch until the second write command
completes.
[Severity: Medium]
Does this also silently discard the return value of the initial direction
configuration?
If the first pmc_write() call fails due to an I/O communication error, the
pin direction may remain misconfigured as an input while the driver falsely
reports success and proceeds to drive the level.
[ ... ]
> +static int check_pin(struct device *dev, int pin)
> +{
> + int ret;
> + int group, bit;
> + u16 data;
> +
> + /* Get pin mapping */
> + ret = pmc_read(dev, EIO_GPIO_MAPPING, pin, &data);
[Severity: Medium]
Does reading hardware registers byte-by-byte into a multi-byte integer
introduce an endianness mismatch?
When pmc_read() iterates and places bytes sequentially into the target address,
it assumes a Little-Endian memory layout. On Big-Endian platforms, the
multi-byte integer will be assembled with the wrong byte order.
> + if (ret)
> + return ret;
> +
> + if ((data & 0xFF) > ARRAY_SIZE(group_map))
> + return -EINVAL;
[Severity: High]
Is there an off-by-one boundary check here that could allow an out-of-bounds
array read?
If the hardware embedded controller returns a mapping value where
(data & 0xFF) exactly equals the array size, it would bypass this check and
cause an out-of-bounds access on group_map in the next line.
> +
> + group = group_map[data & 0xFF].group;
> + bit = data >> 8;
> +
> + /* Check mapped pin */
> + ret = pmc_read(dev, EIO_GPIO_GROUP_AVAIL, group, &data);
> + if (ret)
> + return ret;
> +
> + return data & BIT(bit) ? 0 : -EOPNOTSUPP;
[Severity: High]
Can using the unvalidated hardware value for the bit shift amount cause
undefined behavior on 32-bit architectures?
[ ... ]
> +static int gpio_init(struct device *mfd, struct eio_gpio_dev *eio_gpio)
> +{
> + int ret, i;
> +
> + ret = check_support(mfd);
> + if (ret)
> + return dev_err_probe(eio_gpio->dev, ret, "GPIO not supported\n");
> +
> + eio_gpio->avail = 0;
> +
> + for (i = 0 ; i < EIO_GPIO_MAX_PINS ; i++) {
> + ret = check_pin(mfd, i);
> + if (ret)
> + continue;
> +
> + eio_gpio->avail |= BIT(i);
[Severity: High]
Does this cause an integer shift overflow when generating the 64-bit mask?
Since EIO_GPIO_MAX_PINS is 48, using the 32-bit BIT() macro will shift by 32
or more on 32-bit systems, causing undefined behavior. Should this use
BIT_ULL() instead?
[ ... ]
> +static int gpio_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + struct eio_gpio_dev *eio_gpio;
> + struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
> +
> + if (!eio_dev)
> + return dev_err_probe(dev, -ENODEV, "Error contact eio_core\n");
> +
> + eio_gpio = devm_kzalloc(dev, sizeof(*eio_gpio), GFP_KERNEL);
> + if (!eio_gpio)
> + return -ENOMEM;
> +
> + eio_gpio->dev = dev;
> +
> + if (gpio_init(dev->parent, eio_gpio))
> + return -EIO;
[Severity: Medium]
Is it intentional to discard the meaningful error codes returned by gpio_init()
and replace them with -EIO?
This might disrupt the standard kernel error propagation, specifically breaking
deferred probing if gpio_init() returns -EPROBE_DEFER.
> +
> + eio_gpio->chip.parent = dev->parent;
> + eio_gpio->chip.ngpio = eio_gpio->max;
[Severity: Medium]
The supported pins bitmask eio_gpio->avail is computed during gpio_init() but
never seems to be enforced.
Does the gpio_chip need to be populated with an .init_valid_mask callback so
that gpiolib enforces the valid pins mask? Otherwise, gpiolib will implicitly
assume all pins in the range are valid, and unsupported pins could be
accessed.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260714-upstream-v2-v2-0-76e5e41026db@advantech.com?part=2
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 6/8] Add Advantech EIO Watchdog driver
2026-07-14 15:54 ` [PATCH v2 6/8] Add Advantech EIO Watchdog driver Ramiro Oliveira
@ 2026-07-14 16:07 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-07-14 16:07 UTC (permalink / raw)
To: Ramiro Oliveira; +Cc: linux-hwmon, mfd, linux-watchdog, dri-devel, lee
Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [High] The `set_timeout` and `set_pretimeout` operations update software structures but fail to configure hardware registers when active.
- [High] Bypassing core IRQ safety checks for shared interrupts with a dummy primary handler, causing severe system stalls.
- [High] Hardware system reset is entirely disabled if configured for an alternate event without a pretimeout.
- [High] Incorrect bitmask check for IRQ support causes probe failure.
- [High] Driver illegally passes a raw hardware IRQ line number to the IRQ subsystem instead of a mapped virtual IRQ.
- [Medium] Error paths bypass the IOREG_LOCK command, leaving the EC configuration space permanently unlocked.
- [Medium] Accumulating negative error codes with bitwise OR and blindly continuing I/O operations.
- [Medium] Endianness bug when reading single-byte hardware registers into an `int` pointer.
--
--- Patch [6]: [PATCH v2 6/8] Add Advantech EIO Watchdog driver ---
commit a7197d0a8b335e3355964cf7322548e44ad2700e
Author: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Add Advantech EIO Watchdog driver
This commit adds the driver to control the Advantech EIO Watchdog block,
this block is included in the Advantech EIO Embedded Controller.
> diff --git a/drivers/watchdog/eio_wdt.c b/drivers/watchdog/eio_wdt.c
> new file mode 100644
> index 0000000000000..b3c036a004b30
> --- /dev/null
> +++ b/drivers/watchdog/eio_wdt.c
> @@ -0,0 +1,641 @@
[ ... ]
> +static int wdt_set_timeout(struct watchdog_device *wdd, unsigned int timeout)
> +{
> + wdd->timeout = timeout;
> + return 0;
> +}
[Severity: High]
Does this code apply the new timeout to the hardware? It updates the software
structure, but it appears to omit updating the hardware registers. When
userspace calls WDIOC_SETTIMEOUT, the core updates the structures and pings
the watchdog, but without calling wdt_set_config() here, the new timeout might
never be applied to the hardware unless explicitly stopped and started.
> +
> +static int wdt_set_pretimeout(struct watchdog_device *wdd, unsigned int pretimeout)
> +{
> + wdd->pretimeout = pretimeout;
> + return 0;
> +}
[Severity: High]
Similarly to the set_timeout callback, does this code omit configuring the
hardware registers with the new pretimeout value?
[ ... ]
> +static int wdt_set_config(struct eio_wdt_dev *eio_wdt)
> +{
> + int ret, type;
> + u32 event_time = 0;
> + u32 reset_time = 0;
> +
> + if (eio_wdt->event_type > EVENT_PIN)
> + return -EFAULT;
> +
> + /* Calculate event time and reset time */
> + if (eio_wdt->wdd.pretimeout && eio_wdt->wdd.timeout) {
> + if (eio_wdt->wdd.timeout < eio_wdt->wdd.pretimeout)
> + return -EINVAL;
> +
> + reset_time = eio_wdt->wdd.timeout;
> + event_time = eio_wdt->wdd.timeout - eio_wdt->wdd.pretimeout;
> +
> + } else if (eio_wdt->wdd.timeout) {
> + reset_time = eio_wdt->event_type ? 0 : eio_wdt->wdd.timeout;
> + event_time = eio_wdt->event_type ? eio_wdt->wdd.timeout : 0;
> + }
[Severity: High]
If configured for an alternate event without a pretimeout, does this disable
the hardware fallback reset? Setting reset_time to 0 means the hardware reset
is entirely disabled, and the driver relies exclusively on the software ISR to
call emergency_restart(). If the kernel hangs with interrupts disabled, the ISR
will never run, which might leave the system permanently deadlocked.
[ ... ]
> +static int wdt_get_irq_io(struct eio_wdt_dev *eio_wdt)
> +{
> + int ret = 0;
> + int idx = EIO_PNP_INDEX;
> + int data = EIO_PNP_DATA;
> + struct regmap *map = eio_wdt->iomap;
> +
> + mutex_lock(&eio_wdt->core->mutex);
> +
> + /* Unlock EC IO port */
> + ret |= regmap_write(map, idx, IOREG_UNLOCK);
> + ret |= regmap_write(map, idx, IOREG_UNLOCK);
> +
> + /* Select logical device to PMC */
> + ret |= regmap_write(map, idx, IOREG_LDN);
> + ret |= regmap_write(map, data, IOREG_LDN_PMCIO);
> +
> + /* Get IRQ number */
> + ret |= regmap_write(map, idx, IOREG_IRQ);
> + ret |= regmap_read(map, data, &eio_wdt->irq);
[Severity: Medium]
Can accumulating the results of regmap_write() and regmap_read() with a
bitwise OR cause issues with negative error codes?
Furthermore, if a critical step like selecting the logical device fails, this
code doesn't appear to halt, proceeding to write to whatever logical device
was already selected.
> +
> + /* Lock back */
> + ret |= regmap_write(map, idx, IOREG_LOCK);
> +
> + mutex_unlock(&eio_wdt->core->mutex);
> +
> + return ret ? -EIO : 0;
> +}
> +
> +static int wdt_get_irq_pmc(struct eio_wdt_dev *eio_wdt)
> +{
> + return pmc_read(eio_wdt->mfd, REG_IRQ_NUMBER, &eio_wdt->irq);
> +}
[Severity: Medium]
Will this corrupt the integer on big-endian systems?
pmc_read() receives a void pointer and, based on the size (1 byte for
REG_IRQ_NUMBER), copies a single byte into the lowest memory address of the
pointer. Since eio_wdt->irq is an int, a big-endian system would put this byte
in the most significant byte location, leaving the integer value corrupted.
> +
> +static int wdt_get_irq(struct eio_wdt_dev *eio_wdt)
> +{
> + int ret;
> +
> + if (!(eio_wdt->support & BIT(EVENT_IRQ)))
[Severity: High]
Does this check for the incorrect bitmask? EVENT_IRQ is 2, so this checks
BIT(2), but the support flags define SUPPORT_IRQ as BIT(4). This typo could
force the initialization to fail if the hardware supports IRQs but doesn't
set BIT(2).
[ ... ]
> +static int wdt_set_irq_io(struct eio_wdt_dev *eio_wdt)
> +{
> + int ret = 0;
> + int idx = EIO_PNP_INDEX;
> + int data = EIO_PNP_DATA;
> + struct regmap *map = eio_wdt->iomap;
> +
> + mutex_lock(&eio_wdt->core->mutex);
> +
> + /* Unlock EC IO port */
> + ret = regmap_write(map, idx, IOREG_UNLOCK);
> + if (ret)
> + goto unlock;
> + ret = regmap_write(map, idx, IOREG_UNLOCK);
> + if (ret)
> + goto unlock;
[ ... ]
> + /* Lock back */
> + ret = regmap_write(map, idx, IOREG_LOCK);
> +
> +unlock:
> + mutex_unlock(&eio_wdt->core->mutex);
> + return ret;
> +}
[Severity: Medium]
Does the error path here bypass the IOREG_LOCK command? If any regmap_write()
fails after the initial unlock, the jump to unlock only releases the software
mutex, skipping the IOREG_LOCK command (0xAA) and leaving the EC IO port open.
[ ... ]
> +static int wdt_set_irq(struct eio_wdt_dev *eio_wdt)
> +{
> + int ret;
> +
> + if (!(eio_wdt->support & BIT(EVENT_IRQ)))
[Severity: High]
Similar to wdt_get_irq(), does this check BIT(2) instead of BIT(4)?
[ ... ]
> +static irqreturn_t wdt_isr(int irq, void *arg)
> +{
> + return IRQ_WAKE_THREAD;
> +}
[Severity: High]
Could unconditionally returning IRQ_WAKE_THREAD from a dummy handler cause
severe system stalls for shared interrupts?
The kernel generally forbids requesting an IRQF_SHARED | IRQF_ONESHOT
interrupt without a custom primary handler that checks if its device actually
triggered the interrupt. When any device on the shared line fires, this
handler will blindly wake the thread and mask the IRQ line for the duration
of the slow reads in the threaded handler, which could cripple other devices
sharing the line.
[ ... ]
> + if (eio_wdt->event_type == EVENT_IRQ) {
> + ret = devm_request_threaded_irq(dev, eio_wdt->irq,
> + wdt_isr, wdt_threaded_isr,
> + IRQF_SHARED | IRQF_ONESHOT, pdev->name,
> + eio_wdt);
[Severity: High]
Does this pass a raw hardware IRQ line number directly to the IRQ subsystem?
The driver reads the IRQ number directly from the EC into eio_wdt->irq and
passes it here. The Linux IRQ subsystem requires a mapped virtual IRQ (virq).
Without mapping it via an irqdomain or ACPI, the raw hardware number is
unlikely to trigger correctly.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260714-upstream-v2-v2-0-76e5e41026db@advantech.com?part=6
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 1/8] Add Advantech EIO driver
2026-07-14 15:54 ` [PATCH v2 1/8] Add Advantech EIO driver Ramiro Oliveira
@ 2026-07-14 16:08 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-07-14 16:08 UTC (permalink / raw)
To: Ramiro Oliveira; +Cc: linux-hwmon, dri-devel, linux-watchdog, mfd, lee
Thank you for your contribution! Sashiko AI review found 15 potential issue(s) to consider:
- [Critical] Driver uses zero-initialized `pmc[0]` array without verifying if the primary EC was successfully initialized, causing unintended I/O on system port 0x00.
- [High] Missing success return for supported legacy code bases in `firmware_code_base()`.
- [High] Incorrect validation of `devm_ioport_map()` return value.
- [Medium] The `mfd_cell` array `eio_devs` is not declared as `const`.
- [Medium] The driver uses `PLATFORM_DEVID_NONE` instead of `PLATFORM_DEVID_AUTO`, risking sysfs naming collisions.
- [Medium] `acpi_ram_access()` truncates negative error codes into positive 8-bit integers.
- [Medium] The requested region for the PNP base ports is off-by-one and fails to reserve the data port.
- [Medium] Missing resource request for the dynamically discovered PMC command port.
- [Medium] Unaligned memory access and endianness assumptions in `info_show()`.
- [Medium] The IO port memory mapping is sized statically up to `0x49A`, but accesses dynamically discovered hardware registers which can exceed the mapped bounds.
- [Medium] `pmc_clear` checks the wrong status bit (`IBF` instead of `OBF`) before attempting to clear pending garbage data.
- [Medium] Hardware PNP configuration mode is left unlocked on error and skip paths in `eio_init()`.
- [Low] The commit message prefix violates MFD subsystem naming conventions.
- [Low] `MFD_EIO` uses the `isa_driver` API but lacks a Kconfig dependency on `ISA_BUS_API`.
- [Low] Driver-specific header file is improperly placed in the global `include/linux/mfd/` directory.
--
--- Patch [1]: [PATCH v2 1/8] Add Advantech EIO driver ---
commit d4de2a217ca69b28de748d9dadb7323cafed3d8e
Author: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Subject: Add Advantech EIO driver
Creating the core driver for Advantech EIO, all other drivers depend on this.
> Add Advantech EIO driver
[Severity: Low]
This isn't a bug, but the commit message prefix violates MFD subsystem naming
conventions. Should it follow the mfd: <Driver>: <Capitalized description>
format?
> diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
> index 35f6e9b76d056..42d133a2eb8dc 100644
> --- a/drivers/mfd/Kconfig
> +++ b/drivers/mfd/Kconfig
[ ... ]
> @@ -517,6 +517,16 @@ config MFD_DLN2
> etc. must be enabled in order to use the functionality of
> the device.
>
> +config MFD_EIO
> + tristate "Advantech EIO core"
[Severity: Low]
Does this config need a dependency on ISA_BUS_API? This driver uses the
isa_driver API which requires CONFIG_ISA_BUS_API to avoid build errors on
architectures lacking ISA support.
> + select MFD_CORE
[ ... ]
> diff --git a/drivers/mfd/eio_core.c b/drivers/mfd/eio_core.c
> new file mode 100644
> index 0000000000000..25873c0618127
> --- /dev/null
> +++ b/drivers/mfd/eio_core.c
[ ... ]
> +static struct mfd_cell eio_devs[] = {
[Severity: Medium]
Should this array be declared as static const? The MFD subsystem guidelines
require child platform data arrays to reside in read-only memory.
> + MFD_CELL_NAME("eio_wdt"),
> + MFD_CELL_NAME("gpio_eio"),
> + MFD_CELL_NAME("eio_hwmon"),
[ ... ]
> +static ssize_t info_show(struct device *dev, struct device_attribute *attr, char *buf)
> +{
> + uint i;
> +
> + for (i = 0; i < ARRAY_SIZE(attrs); i++) {
> + int ret;
> + char str[32] = "";
> + int val;
[ ... ]
> + if (attrs[i].size != 4)
> + return sysfs_emit(buf, "%s\n", str);
> +
> + val = *(u32 *)str;
[Severity: Medium]
Can this cast cause read corruption on big-endian platforms or potential
alignment faults? Would using get_unaligned_le32() be safer here?
> +
> + if (attrs[i].type == HEX)
[ ... ]
> +static void pmc_clear(struct device *dev, int id)
> +{
> + struct eio_dev *eio = dev_get_drvdata(dev);
> + unsigned int val;
> +
> + /* Check if input buffer blocked */
> + if ((pmc_read_status(dev, id) & EIO_PMC_STATUS_IBF) == 0)
[Severity: Medium]
Is this checking the wrong status bit? To check if there is residual output
data to clear from the EC buffer, shouldn't this check OBF (Output Buffer
Full) instead of IBF (Input Buffer Full)?
> + return;
> +
> + /* Read out previous garbage */
[ ... ]
> +static int get_pmc_port(struct device *dev, int id,
> + struct eio_dev_port *port)
> +{
[ ... ]
> + /* Make sure IO ports are not occupied */
> + if (!devm_request_region(dev, pmc->data, 2, KBUILD_MODNAME)) {
[Severity: Medium]
Does this leave the PMC command port (pmc->cmd) unprotected? It looks like
only the data port is requested here.
> + dev_err(dev, "Request region %X error\n", pmc->data);
> + return -EBUSY;
> + }
> +
> + return 0;
> +}
[ ... ]
> +static int eio_init(struct device *dev)
> +{
> + struct eio_dev *eio = dev_get_drvdata(dev);
> + u16 chip_id;
> + unsigned int msb, lsb;
> + int tmp;
> + int ret = -ENOMEM;
> +
> + for (int chip = 0; chip < ARRAY_SIZE(pnp_port); chip++) {
> + struct eio_dev_port *port = pnp_port + chip;
> +
> + if (!devm_request_region(dev, pnp_port[chip].idx_port,
> + pnp_port[chip].data_port -
> + pnp_port[chip].idx_port,
[Severity: Medium]
Is this calculation off-by-one? The subtraction data_port - idx_port evaluates
to 1, which only requests the index port. Should it include a + 1 to reserve
the data port as well?
> + KBUILD_MODNAME))
> + continue;
> +
> + ret = eio_unlock_port(dev, port);
> + if (ret)
> + return ret;
> +
> + ret = eio_pnp_read(dev, port, EIO_CHIPID1, &msb);
> + if (ret)
> + return ret;
> +
> + ret = eio_pnp_read(dev, port, EIO_CHIPID2, &lsb);
> + if (ret)
> + return ret;
> +
> + chip_id = (msb << 8) | lsb;
> +
> + if (chip_id != EIO200_CHIPID && chip_id != EIO201_211_CHIPID)
> + continue;
[Severity: Medium]
Does this continue bypass eio_lock_port() and leave the hardware PNP
configuration mode unlocked on error and skip paths? This might leave the
index and data ports susceptible to accidental modification.
[Severity: Critical]
Additionally, if this loop completes without finding a valid chip_id, the loop
exits and returns ret = 0. This causes eio_init() to succeed, and the
uninitialized pmc[0] array is then passed to acpi_ram_access(). This could
trigger regmap operations on system port 0x00 (system DMA controller base),
potentially leading to hardware corruption or a system hang on unsupported
hardware.
> +
> + /* Turn on the enable flag */
> + ret = eio_pnp_read(dev, port, EIO_SIOCTRL, &tmp);
[ ... ]
> +static uint8_t acpi_ram_access(struct device *dev, uint8_t offset)
[Severity: Medium]
Does this function truncate negative error codes into positive 8-bit integers?
Since standard negative error codes (like -110 for timeout) can be returned by
PMC operations, the caller might misinterpret corrupted error codes as valid
ACPI RAM data.
> +{
> + uint8_t val;
> + int ret;
> + struct eio_dev *eio = dev_get_drvdata(dev);
[ ... ]
> +static int firmware_code_base(struct device *dev)
> +{
> + struct eio_dev *eio = dev_get_drvdata(dev);
> + u8 ic_vendor, ic_code, code_base;
> +
> + ic_vendor = acpi_ram_access(dev, EIO_ACPIRAM_ICVENDOR);
> + ic_code = acpi_ram_access(dev, EIO_ACPIRAM_ICCODE);
> + code_base = acpi_ram_access(dev, EIO_ACPIRAM_CODEBASE);
> +
> + if (ic_vendor != 'R')
> + return -ENODEV;
> +
> + if (ic_code != EIO200_ICCODE && ic_code != EIO201_ICCODE &&
> + ic_code != EIO211_ICCODE)
> + goto err;
> +
> + if (code_base == EIO_ACPIRAM_CODEBASE_NEW) {
> + eio->flag |= EIO_F_NEW_CODE_BASE;
> + return 0;
> + }
> +
> + if (code_base == 0 &&
> + (ic_code != EIO201_ICCODE && ic_code != EIO211_ICCODE)) {
> + dev_info(dev, "Old code base not supported.");
> + return -ENODEV;
> + }
> +
> +err:
[Severity: High]
Does this logic miss a success return for supported legacy code bases? If
code_base == 0 and ic_code is a supported legacy code, the if block is
skipped and execution falls through to the err: label, erroneously returning
-ENODEV and failing to probe on supported hardware.
> + /* Codebase error. This should only happen on firmware error. */
> + dev_err(dev,
> + "Codebase check fail: vendor: 0x%X, code: 0x%X, base: 0x%X\n",
[ ... ]
> +static int eio_probe(struct device *dev, unsigned int id)
> +{
> + int ret = 0;
> + struct eio_dev *eio;
> +
> + eio = devm_kzalloc(dev, sizeof(*eio), GFP_KERNEL);
> + if (!eio)
> + return -ENOMEM;
> +
> + eio->dev = dev;
> + mutex_init(&eio->mutex);
> +
> + eio->iomem = devm_ioport_map(dev, 0, EIO_SUB_PNP_DATA + 1);
[Severity: Medium]
Is this static mapping bound potentially too small? The mapping strictly covers
up to EIO_SUB_PNP_DATA + 1 (0x49B), but the driver accesses dynamically
discovered hardware registers which can exceed these bounds anywhere in the
16-bit IO space.
> + if (IS_ERR(eio->iomem))
[Severity: High]
Will this error check incorrectly pass on memory exhaustion?
devm_ioport_map() returns NULL on failure, so checking IS_ERR instead of
NULL could lead to a NULL pointer dereference in devm_regmap_init_mmio().
> + return PTR_ERR(eio->iomem);
> +
> + eio->map = devm_regmap_init_mmio(dev, eio->iomem, &pnp_regmap_config);
> + if (IS_ERR(eio->map))
> + return PTR_ERR(eio->map);
> +
> + /* Publish instance for subdrivers (dev_get_drvdata(dev->parent)) */
> + dev_set_drvdata(dev, eio);
> +
> + if (eio_init(dev))
> + return -ENODEV;
> +
> + ret = firmware_code_base(dev);
> + if (ret) {
> + dev_err(dev, "Chip code base check fail\n");
> + return ret;
> + }
> +
> + ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE,
[Severity: Medium]
Should PLATFORM_DEVID_AUTO be used here instead of PLATFORM_DEVID_NONE?
The MFD subsystem guidelines require using PLATFORM_DEVID_AUTO for automatic
cell indexing to avoid sysfs naming collisions.
> + eio_devs, ARRAY_SIZE(eio_devs),
> + NULL, 0, NULL);
[ ... ]
> diff --git a/include/linux/mfd/eio.h b/include/linux/mfd/eio.h
> new file mode 100644
> index 0000000000000..c2daad41318bf
> --- /dev/null
> +++ b/include/linux/mfd/eio.h
> @@ -0,0 +1,124 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +/*
> + * Copyright (C) 2025 Advantech Co., Ltd.
> + */
> +
> +#ifndef _MFD_EIO_H_
> +#define _MFD_EIO_H_
[Severity: Low]
This isn't a bug, but should this driver-specific header file be placed in
drivers/mfd/ instead of the global include/linux/mfd/ directory? The MFD
subsystem guidelines explicitly dictate keeping headers local if they are only
used by the parent and immediate children.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260714-upstream-v2-v2-0-76e5e41026db@advantech.com?part=1
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 4/8] Add Advantech EIO I2C driver
2026-07-14 15:54 ` [PATCH v2 4/8] Add Advantech EIO I2C driver Ramiro Oliveira
@ 2026-07-14 16:11 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-07-14 16:11 UTC (permalink / raw)
To: Ramiro Oliveira; +Cc: linux-hwmon, mfd, dri-devel, lee, linux-watchdog
Thank you for your contribution! Sashiko AI review found 18 potential issue(s) to consider:
- [Critical] Reading SMBus block data via an `unsigned int *` cast causes out-of-bounds memory writes and stack corruption.
- [Critical] The driver does not validate the userspace-provided block length for SMBus block writes, allowing an out-of-bounds read of kernel memory.
- [High] Premature exit in `i2c_xfer` causes silent truncation of multi-message I2C transactions.
- [High] Local mutex `pnp_mutex` fails to protect the shared MFD-level PNP configuration sequence from concurrent access by other child drivers.
- [High] Misuse of `regmap_update_bits` (`eio_reg_or`) on W1C status registers and with 0 masks acts as a no-op, failing to clear hardware errors.
- [High] Off-by-one boundary check strictly rejects valid maximum-length (32-byte) SMBus block transfers.
- [High] The 10-bit I2C addressing implementation is structurally flawed, losing the R/W bit, corrupting the address, and omitting Repeated Start.
- [High] Uninitialized stack variables in `smb_access` are returned to userspace if regmap reads fail.
- [High] SMBus read emulation over I2C incorrectly marks the command byte phase as a read operation.
- [High] The read phase for `I2C_SMBUS_BLOCK_PROC_CALL` is completely omitted, dropping the response data.
- [High] Destructive bitwise operation wipes out `SMB_REG_HC` configuration during Block Process Call.
- [Medium] Incorrect endianness handling in SMBus Word Data and Process Call transfers corrupts data on big-endian architectures.
- [Medium] The hardware is left stuck in PNP Extended Configuration Mode if any register access fails during `load_i2c`.
- [Medium] Clock configuration logic is structurally disjoint when the requested frequency equals exactly 30 kHz (`I2C_THRESHOLD_SCLH`), resulting in massive under-clocking.
- [Medium] Polling loops can falsely trigger timeout errors because they evaluate expiration before executing the final register read.
- [Low] The block write formatting logic in `smbus_xfer` copies one byte past the end of the payload buffer.
- [Low] Module parameters for configuring I2C and SMBus frequencies are declared but completely ignored by the driver logic.
- [Low] Driver-specific header file is inappropriately placed in the global MFD include directory.
--
--- Patch [4]: [PATCH v2 4/8] Add Advantech EIO I2C driver ---
commit 0b05c6638fad42a025d75120571d2def944319c3
Author: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Add Advantech EIO I2C driver
This commit adds the driver to control the Advantech EIO I2C block, this
block is included in the Advantech EIO MFD.
> diff --git a/drivers/i2c/busses/i2c-eio.c b/drivers/i2c/busses/i2c-eio.c
> new file mode 100644
> index 0000000000000..56a89ca18f328
> --- /dev/null
> +++ b/drivers/i2c/busses/i2c-eio.c
> @@ -0,0 +1,1135 @@
> +// SPDX-License-Identifier: GPL-2.0-only
[ ... ]
> +#include <linux/device.h>
> +#include <linux/delay.h>
> +#include <linux/errno.h>
> +#include <linux/i2c.h>
> +#include <linux/mfd/core.h>
> +#include <linux/mfd/eio.h>
[Severity: Low]
Does this include violate MFD architectural guidelines? The subsystem guide
specifies that driver-specific header files should be kept local to
drivers/mfd/ and not placed in the global include/linux/mfd/ directory if
they are only used by the parent and its immediate children.
[ ... ]
> +struct eio_i2c_dev {
> + struct device *dev;
> + struct device *mfd;
> + struct regmap *regmap;
> + struct mutex pnp_mutex; /* Mutex for PNP acces */
[Severity: High]
Since this mutex is initialized locally within the I2C child driver, does it
fail to protect the shared MFD-level PNP configuration registers (0x299/0x29A)
from concurrent access by other sibling drivers (like GPIO or HWMON)?
[ ... ]
> +static int wait_busy(struct eio_i2c_chan *i2c_chan)
> +{
> + ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
> + unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_STAT, SMB_REG_HS);
> + unsigned int target = eio_chan_reg(i2c_chan, I2C_STAT_BUSY, SMB_HS_BUSY);
> + unsigned int val;
> + int cnt = 0;
> +
> + do {
> + fsleep(cnt++);
> +
> + if (ktime_after(ktime_get(), time_end)) {
> + dev_err(eio_dev(i2c_chan), "Wait I2C bus busy timeout\n");
> + return -ETIME;
> + }
> +
> + if (eio_reg_read(i2c_chan, reg, &val))
> + return -EIO;
> +
> + } while (val & target);
[Severity: Medium]
Could this polling loop falsely return a timeout? If the thread experiences
latency during the sleep and the timer expires, the loop will exit with an
error without performing a final register read to check if the hardware
actually completed the operation.
[ ... ]
> +static void i2c_clear(struct eio_i2c_chan *i2c_chan)
> +{
> + if (is_i2c(i2c_chan)) {
> + eio_reg_write(i2c_chan, I2C_REG_STAT, 0xFF);
> + } else {
> + eio_reg_or(i2c_chan, SMB_REG_HS, 0xA9);
> + eio_reg_or(i2c_chan, SMB_REG_HS2, 0x4C);
> + }
> +}
[Severity: High]
Does using a read-modify-write operation (eio_reg_or -> regmap_update_bits)
work for clearing Write-1-to-Clear (W1C) bits? If the bits are already set
in the hardware, reading them and writing back the same value will often be
skipped by regmap, leaving the errors uncleared.
[ ... ]
> +static int wait_write_done(struct eio_i2c_chan *i2c_chan, bool no_ack)
> +{
> + ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
> + unsigned int val = 0;
> + int cnt = 0;
> + unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_STAT, SMB_REG_HS);
> + unsigned int target = eio_chan_reg(i2c_chan, I2C_STAT_TXDONE, SMB_HS_TX_DONE);
> +
> + do {
> + fsleep(cnt++);
> + if (ktime_after(ktime_get(), time_end)) {
> + if (is_i2c(i2c_chan)) {
> + eio_reg_or(i2c_chan, I2C_REG_STAT, 0);
[Severity: High]
Is it possible this fails to clear the status flags? Passing a mask of 0 to
regmap_update_bits will result in no bits being changed, and the write will
likely be optimized out entirely.
[ ... ]
> +static int set_freq(struct eio_i2c_chan *i2c_chan, int freq)
> +{
> + u8 pre1, pre2;
> + u16 speed;
> + unsigned int reg1 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE1, SMB_REG_HPRESCALE1);
> + unsigned int reg2 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE2, SMB_REG_HPRESCALE2);
> +
> + dev_dbg(eio_dev(i2c_chan), "set freq: %dkHz\n", freq);
> + if (freq > I2C_FREQ_MAX || freq < I2C_FREQ_MIN) {
> + dev_err(eio_dev(i2c_chan), "Invalid i2c freq: %d\n", freq);
> + return -EINVAL;
> + }
> +
> + speed = (freq < I2C_THRESHOLD_SCLH) ? I2C_SCLH_LOW : I2C_SCLH_HIGH;
> +
> + pre1 = (u8)(CHIP_CLK / speed);
> + pre2 = (u8)((speed / freq) - 1);
> +
> + if (freq > I2C_THRESHOLD_SCLH)
> + pre2 |= I2C_SCL_FAST_MODE;
[Severity: Medium]
What happens if the requested frequency is exactly 30 kHz (I2C_THRESHOLD_SCLH)?
The first check (< 30) evaluates to false, making speed 2500. However, the
second check (> 30) also evaluates to false, failing to set the FAST_MODE bit.
Will this combination drastically underclock the bus?
[ ... ]
> +static int smb_access(struct eio_i2c_chan *i2c_chan, u8 addr, bool is_read, u8 cmd,
> + int size, union i2c_smbus_data *data)
> +{
> + int i, tmp, ret = 0;
[ ... ]
> + case I2C_SMBUS_BLOCK_DATA:
> + dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_DATA\n");
> + if (is_read)
> + break;
> +
> + /* Program command type */
> + eio_reg_read(i2c_chan, SMB_REG_HC, (unsigned int *)&tmp);
> + tmp &= ~(0x07 << SMB_HC_CMD_SHIFT);
> + tmp |= (size << SMB_HC_CMD_SHIFT);
> + eio_reg_write(i2c_chan, SMB_REG_HC, tmp);
> +
> + /* Force write for payload stage */
> + eio_reg_write(i2c_chan, SMB_REG_HADDR, addr & ~0x01);
> +
> + /* Reset internal buffer index pointer */
> + eio_reg_and(i2c_chan, SMB_REG_HC2, (int)~SMB_HC2_E32B);
> + eio_reg_or(i2c_chan, SMB_REG_HC2, SMB_HC2_E32B);
> +
> + /* Write length + data */
> + eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
> + for (i = 1; i <= data->block[0]; i++)
> + eio_reg_write(i2c_chan, SMB_REG_HBLOCK, data->block[i]);
[Severity: Critical]
Does this loop need to validate the length of data->block[0] against
I2C_SMBUS_BLOCK_MAX before writing? Without bounds checking, this may blindly
read past the union's memory boundary and dump kernel stack contents onto the
I2C bus.
> + break;
> +
> + case I2C_SMBUS_BLOCK_PROC_CALL:
> + /* Set command type field */
> + eio_reg_and(i2c_chan, SMB_REG_HC, (0x07 << SMB_HC_CMD_SHIFT));
[Severity: High]
Does this bitwise operation wipe out configuration flags? Because eio_reg_and
uses regmap_update_bits with an inverted mask, providing only the command bits
implies all other active control bits in the register will be zeroed out.
> + eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
> +
> + /* Reset buffer index */
> + eio_reg_and(i2c_chan, SMB_REG_HC2, (int)~SMB_HC2_E32B);
> + eio_reg_or(i2c_chan, SMB_REG_HC2, SMB_HC2_E32B);
> +
> + for (i = 1; i <= data->block[0]; i++)
> + eio_reg_write(i2c_chan, SMB_REG_HBLOCK, data->block[i]);
[Severity: Critical]
Similar to the block data write above, does data->block[0] need bounds checking
here to prevent out-of-bounds kernel memory reads?
[ ... ]
> + switch (size) {
> + case I2C_SMBUS_QUICK:
> + dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_QUICK\n");
> + break;
> +
> + case I2C_SMBUS_BYTE:
> + case I2C_SMBUS_BYTE_DATA:
> + if (is_read) {
> + unsigned int v;
> +
> + dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE/I2C_SMBUS_BYTE_DATA\n");
> + eio_reg_read(i2c_chan, SMB_REG_HD0, &v);
> + data->block[0] = (u8)v;
[Severity: High]
If eio_reg_read fails (e.g. from a hardware error), the error is ignored and
the uninitialized stack variable 'v' will be copied into data->block[0]. Will
this leak uninitialized kernel memory to userspace during an ioctl?
> + dev_dbg(eio_dev(i2c_chan), "read %X\n", data->block[0]);
> + }
> + break;
> +
> + case I2C_SMBUS_WORD_DATA: {
> + unsigned int v0, v1;
> +
> + if (is_read) {
> + dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_WORD_DATA\n");
> + eio_reg_read(i2c_chan, SMB_REG_HD0, &v0);
> + eio_reg_read(i2c_chan, SMB_REG_HD1, &v1);
> + data->block[0] = (u8)v0;
> + data->block[1] = (u8)v1;
[Severity: Medium]
Is it safe to assign word bytes directly into the data->block byte array?
Because data->block shares a union with the 16-bit data->word, this mappings
changes based on host endianness. Will this corrupt word values on big-endian
architectures?
[Severity: High]
Also, just like the byte read case, an error in eio_reg_read will cause the
uninitialized stack variables v0 and v1 to be returned to the client.
> + }
> + break;
> + }
> +
> + case I2C_SMBUS_PROC_CALL: {
> + unsigned int v0, v1;
> +
> + dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_PROC_CALL\n");
> + eio_reg_read(i2c_chan, SMB_REG_HD0, &v0);
> + eio_reg_read(i2c_chan, SMB_REG_HD1, &v1);
> + data->block[0] = (u8)v0;
> + data->block[1] = (u8)v1;
[Severity: Medium]
Similar endianness handling issue exists here for Process Call responses.
[Severity: High]
And once again, uninitialized memory leaks are possible if the reads fail.
> + break;
> + }
> +
> + case I2C_SMBUS_BLOCK_DATA:
> + if (!is_read)
> + break;
> +
> + dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_DATA\n");
> + eio_reg_read(i2c_chan, SMB_REG_HD0, (unsigned int *)&len);
> + len = min(len, I2C_SMBUS_BLOCK_MAX);
> + data->block[0] = len;
> +
> + for (i = 1; i <= len; i++)
> + eio_reg_read(i2c_chan, SMB_REG_HBLOCK,
> + (unsigned int *)&data->block[i]);
[Severity: Critical]
Is it safe to cast the 8-bit array slot pointer &data->block[i] to a 32-bit
unsigned int * ? When regmap_read populates the result, it will write 32 bits,
causing it to overwrite up to 3 bytes past the end of the union i2c_smbus_data
structure on the final iterations, corrupting the stack.
> + break;
> +
> + default:
> + ret = -EINVAL;
> + goto exit;
> + }
[Severity: High]
Is the read phase for I2C_SMBUS_BLOCK_PROC_CALL intentionally missing? The
switch statement falls through to default, throwing away the response data and
returning an error.
[ ... ]
> +static int i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int nmsgs)
> +{
[ ... ]
> + for (msg = 0; msg < nmsgs; msg++) {
> + int is_read = msgs[msg].flags & I2C_M_RD;
> + bool no_ack = msgs[msg].flags & I2C_M_IGNORE_NAK;
> +
> + dev_dbg(eio_dev(i2c_chan), "message %d len=%d\n", msg, msgs[msg].len);
> +
> + if (!msgs[msg].len)
> + let_stop(i2c_chan);
> +
> + if (msgs[msg].flags & I2C_M_TEN) {
> + addr = eio_enc_10bit_addr(msgs[msg].addr);
> + addr |= is_read;
> + dev_dbg(eio_dev(i2c_chan), "10-bit addr: %X\n", addr);
> +
> + ret = write_addr(i2c_chan, addr >> 8, no_ack);
> + if (!ret)
> + ret = write_data(i2c_chan, addr & 0x7F, no_ack);
[Severity: High]
Is this 10-bit addressing logic accurate? ORing the read flag into the lower
byte, dropping the upper bit via a shift, and masking the bottom bits seems to
corrupt the address format. Additionally, shouldn't there be a Repeated Start
phase explicitly generated for reads?
> + } else {
> + addr = eio_enc_7bit_addr(msgs[msg].addr);
> + addr |= is_read;
> + dev_dbg(eio_dev(i2c_chan), "7-bit addr: %X\n", addr);
> +
> + ret = write_addr(i2c_chan, addr, no_ack);
> + }
> +
> + if (ret)
> + goto exit;
> +
> + if (!msgs[msg].len)
> + goto exit;
[Severity: High]
Will exiting here prematurely abort multi-message transfers? For 0-length probe
messages, this breaks out of the loop over nmsgs, failing to process any
subsequent messages while still returning success.
> +
> + if (is_read)
> + ret = eio_trigger_read(i2c_chan, (u32 *)&dummy);
> +
> + /* Transmit all messages */
> + for (data = 0; data < msgs[msg].len; data++) {
> + if (msgs[msg].flags & I2C_M_RD) {
> + bool last = (msgs[msg].len == data + 1);
> +
> + if (last)
> + let_stop(i2c_chan);
> +
> + ret = read_data(i2c_chan, &msgs[msg].buf[data]);
> + dev_dbg(eio_dev(i2c_chan), "I2C read[%d] = %x\n",
> + data, msgs[msg].buf[data]);
> +
> + /* Don't stop twice */
> + if (last && ret == 0)
> + goto exit;
[Severity: High]
Similarly, doesn't this goto break out of the nmsgs loop entirely? If a read
message is followed by further messages in a single I2C transaction, those
remaining messages will be silently ignored.
[ ... ]
> +static int smbus_xfer(struct i2c_adapter *adap, u16 addr,
> + u16 flags, char is_read, u8 cmd,
> + int size, union i2c_smbus_data *data)
> +{
[ ... ]
> + if (data) {
> + buf[0] = cmd;
> + /* FIX: preserve other flags; only toggle I2C_M_RD */
> + msgs[0].flags = is_read ? (flags | I2C_M_RD) : (flags & ~I2C_M_RD);
[Severity: High]
Does this break the semantic structure of an SMBus read transaction? Setting
I2C_M_RD on the first message (the command byte phase) means it will be sent to
the device as an I2C read rather than an I2C write, causing emulated SMBus reads
to fail entirely.
[ ... ]
> + case I2C_SMBUS_I2C_BLOCK_DATA:
> + case I2C_SMBUS_I2C_BLOCK_BROKEN:
> + dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_I2C_BLOCK_(DATA/BROKEN) on I2C len=%d\n",
> + data->block[0]);
> + if (!data)
> + return -EINVAL;
> + msgs[0].len = is_read ? 1 : data->block[0] + 1;
> + msgs[1].len = data->block[0];
> + msgs[1].buf = data->block + 1;
> + if (msgs[0].len >= I2C_SMBUS_BLOCK_MAX ||
> + msgs[1].len >= I2C_SMBUS_BLOCK_MAX)
> + return -EINVAL;
[Severity: High]
Does this boundary check strictly reject valid 32-byte payloads? Since
I2C_SMBUS_BLOCK_MAX is 32, using >= instead of > causes perfectly valid
maximum-length transfers to fail with -EINVAL.
> + if (!is_read)
> + memcpy(buf + 1, data->block + 1, msgs[0].len);
[Severity: Low]
Since msgs[0].len is set to data->block[0] + 1, won't copying msgs[0].len bytes
from data->block + 1 pull one extra uninitialized byte from past the end of the
populated payload?
[ ... ]
> +static int load_i2c(struct device *dev, enum eio_chan_id id,
> + struct eio_i2c_chan *i2c_chan)
> +{
[ ... ]
> + /* Read channel I/O base via shared PNP window */
> + mutex_lock(&eio_i2c->pnp_mutex);
> + if (regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_ENTER) ||
> + regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_ENTER) ||
> + regmap_write(map, REG_PNP_INDEX, REG_LDN) ||
> + regmap_write(map, REG_PNP_DATA, ldn) ||
> + regmap_write(map, REG_PNP_INDEX, REG_BASE_HI) ||
> + regmap_read(map, REG_PNP_DATA, &base_hi) ||
> + regmap_write(map, REG_PNP_INDEX, REG_BASE_LO) ||
> + regmap_read(map, REG_PNP_DATA, &base_lo) ||
> + regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_EXIT)) {
> + mutex_unlock(&eio_i2c->pnp_mutex);
> + dev_err(dev, "error read/write I2C[%d] IO port\n", id);
> + return -EIO;
> + }
[Severity: Medium]
If any of these hardware accesses fail, the logical OR sequence short-circuits.
Doesn't this mean the REG_EXT_MODE_EXIT command is never sent, leaving the Super
I/O controller permanently stuck in configuration mode?
> + mutex_unlock(&eio_i2c->pnp_mutex);
> +
> + base = (base_hi << 8) | base_lo;
> + if (base == 0xFFFF || base == 0) {
> + dev_dbg(dev, "i2c[%d] base addr=%#x (not in-use)\n", id, base);
> + return -ENODEV;
> + }
> +
> + dev_dbg(dev, "i2c[%d] base addr=%#x\n", id, base);
> +
> + /* Bind channel (no per-chan dev) */
> + i2c_chan->base = (u16)base;
> + i2c_chan->id = id;
> +
> + /* Per-channel frequency policy */
> + if (i2c_chan->freq_override != USE_DEFAULT)
> + set_freq(i2c_chan, i2c_chan->freq_override);
[Severity: Low]
Does this actually have an effect? It appears i2c_chan->freq_override is
unconditionally initialized to USE_DEFAULT in the probe function just before
this is called, rendering the check dead code and ignoring user-provided module
parameters.
[ ... ]
> +static int eio_i2c_probe(struct platform_device *pdev)
> +{
[ ... ]
> + for (ch = EIO_I2C0; ch < MAX_I2C_SMB; ch++) {
> + struct eio_i2c_chan *i2c_chan;
> +
> + i2c_chan = devm_kzalloc(dev, sizeof(*i2c_chan), GFP_KERNEL);
> + if (!i2c_chan) {
> + ret = -ENOMEM;
> + break;
> + }
> +
> + i2c_chan->parent = eio_i2c;
> + i2c_chan->freq_override = USE_DEFAULT;
[Severity: Low]
This initialization unconditionally overrides any module parameters provided by
the system administrator.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260714-upstream-v2-v2-0-76e5e41026db@advantech.com?part=4
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v2 8/8] Add Advantech EIO Fan driver
2026-07-14 15:54 ` [PATCH v2 8/8] Add Advantech EIO Fan driver Ramiro Oliveira
@ 2026-07-14 16:14 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-07-14 16:14 UTC (permalink / raw)
To: Ramiro Oliveira; +Cc: linux-watchdog, linux-hwmon, dri-devel, mfd, lee
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] Missing `.remove` callback and incomplete probe error unwinding leads to thermal zone leaks and use-after-free vulnerabilities.
- [High] Array index out-of-bounds when accessing the `fan_name` array using an unvalidated hardware index.
- [High] `eio_fan_should_bind` casts `cdev->devdata` to `struct eio_fan_dev *` without verifying the cooling device type.
- [Medium] `eio_fan_set_trip_temp` constantly fails because it incorrectly double-adds the `CTRL_THERM_HIGH` offset.
- [Medium] Passing pointers to 32-bit `int` variables for 1- or 2-byte hardware reads results in endianness corruption on big-endian architectures.
- [Medium] Violation of MFD subsystem rules by using a custom parent-child accessor function.
--
--- Patch [8]: [PATCH v2 8/8] Add Advantech EIO Fan driver ---
commit 0e6353f2bcc96ca0d70e6ec50a52311c0213d5ad
Author: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Add Advantech EIO Fan driver
This commit adds the driver to control the Advantech EIO Fan block,
which is included in the Advantech EIO Embedded Controller.
> diff --git a/drivers/thermal/eio_fan.c b/drivers/thermal/eio_fan.c
> new file mode 100644
> index 0000000000000..e07b18c2ec44a
> --- /dev/null
> +++ b/drivers/thermal/eio_fan.c
[ ... ]
> +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);
[Severity: Medium]
Does using eio_core_pmc_operation() directly bypass standard kernel subsystem
APIs like regmap? According to MFD guidelines, child drivers should retrieve
parent driver data using standard APIs rather than custom parent-child
accessors.
[ ... ]
> +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);
[Severity: Medium]
Since pmc_read() seems to read a 1-byte value based on pmc_len[CTRL_CTRL],
does passing a pointer to a 32-bit integer write only to the most-significant
byte on big-endian platforms?
[ ... ]
> +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;
[Severity: Medium]
Since fan_trip->trip_ctl was already initialized to CTRL_THERM_HIGH during
probe, does adding CTRL_THERM_HIGH again cause the offset to exceed the
ARRAY_SIZE(pmc_len) limit of 33, making pmc_write() always return -EINVAL?
[ ... ]
> +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)
[Severity: High]
Is it safe to cast cdev->devdata to struct eio_fan_dev* here without first
verifying that the cooling device actually belongs to this driver (for
example, by checking cdev->ops)? Could this lead to out-of-bounds memory
access if the thermal framework invokes this for a cooling device belonging to
another driver?
[ ... ]
> +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) ||
[Severity: Medium]
Similar to eio_fan_get_temp(), does passing pointers to 32-bit integers for
these hardware reads cause endianness corruption on big-endian architectures?
[ ... ]
> + if (!(state & 0x1)) {
> + dev_info(dev, "fan%u: firmware reports disabled\n", fan_id);
> + continue;
> + }
> +
> + if (!fan_name[name][0]) {
[Severity: High]
The 'name' index is read from hardware as a u8. Does this code need to check
if 'name' exceeds the 32-element bounds of the fan_name[] array to prevent an
out-of-bounds read?
[ ... ]
> + 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);
> + }
[Severity: High]
If this loop fails on a subsequent fan iteration, does returning directly
here leave previously registered thermal zones and cooling devices active?
Should this failure path unwind the already registered instances?
[ ... ]
> +static struct platform_driver eio_fan_driver = {
> + .probe = eio_fan_probe,
> + .driver = {
> + .name = "eio_fan",
> + },
> +};
[Severity: High]
Does the lack of a .remove callback mean that unbinding the driver via sysfs
abandons the registered thermal zones and cooling devices? Because
devm_kzalloc() frees the memory on unbind, could this leave dangling pointers
in the thermal subsystem?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260714-upstream-v2-v2-0-76e5e41026db@advantech.com?part=8
^ permalink raw reply [flat|nested] 17+ messages in thread
end of thread, other threads:[~2026-07-14 16:14 UTC | newest]
Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14 15:54 [PATCH v2 0/8] Add support for Advantech EIO MFD series devices Ramiro Oliveira
2026-07-14 15:54 ` [PATCH v2 1/8] Add Advantech EIO driver Ramiro Oliveira
2026-07-14 16:08 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 2/8] Add Advantech EIO GPIO driver Ramiro Oliveira
2026-07-14 16:06 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver Ramiro Oliveira
2026-07-14 16:05 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 4/8] Add Advantech EIO I2C driver Ramiro Oliveira
2026-07-14 16:11 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 5/8] Add Advantech EIO Backlight driver Ramiro Oliveira
2026-07-14 16:05 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 6/8] Add Advantech EIO Watchdog driver Ramiro Oliveira
2026-07-14 16:07 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 7/8] Add Advantech EIO Thermal driver Ramiro Oliveira
2026-07-14 16:05 ` sashiko-bot
2026-07-14 15:54 ` [PATCH v2 8/8] Add Advantech EIO Fan driver Ramiro Oliveira
2026-07-14 16:14 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.