[PATCH 4/4] mfd: eiois200: Add EIO-IS200 Series EC Core Driver

From: advantech . susiteam
Date: Wed Sep 06 2023 - 23:14:19 EST


From: Wenkai <advantech.susiteam@xxxxxxxxx>

This patch introduces the Advantech EIO-IS200 Series EC Core Driver.
The EIO-IS200 combines hardware functionality with firmware
capabilities to provide key features through a dedicated Power
Management Channel (PMC).

Key Features:
- Implements the core EIO-IS200 IC driver that serves as the
interface to the IC's firmware and hardware functions.
- Contains low-level functions for accessing the IC's Power
Management Channel (PMC).
- Manages PMC command execution, PMC buffer handling, and
communication.
- Expose a regmap and a mutex for low-level access.
- Activates support for GPIO, I2C, Hwmon, and Watchdog functionalities
of the EIO-IS200 IC.
- Provides various sysfs attributes to expose information about the
EC chip, firmware, and motherboard.
- Includes a timeout parameter to allow modification of the default
PMC command timeout value, which is particularly useful when
dealing with an extremely slow-responding device.

Signed-off-by: Wenkai <advantech.susiteam@xxxxxxxxx>
---
drivers/mfd/eiois200_core.c | 590 ++++++++++++++++++++++++++++++++++++
1 file changed, 590 insertions(+)
create mode 100644 drivers/mfd/eiois200_core.c

diff --git a/drivers/mfd/eiois200_core.c b/drivers/mfd/eiois200_core.c
new file mode 100644
index 000000000000..7459dee1ed53
--- /dev/null
+++ b/drivers/mfd/eiois200_core.c
@@ -0,0 +1,590 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Advantech EIO-IS200 Series EC base Driver
+ *
+ * This driver provides an interface to access the EIO-IS200 Series EC
+ * firmware via its own Power Management Channel (PMC) for subdrivers:
+ *
+ * - Watchdog: drivers/watchdog/eiois200_wdt
+ * - GPIO: drivers/gpio/gpio_eiois200
+ * - Hwmon: drivers/hwmon/eiois200_hwmon
+ * - I2C: drivers/i2c/busses/i2c_eiois200
+ * - Thermal: drivers/thermal/eiois200_thermal
+ *
+ * A system may have one or two independent EIO-IS200s.
+ *
+ * Copyright (C) 2023 Advantech Co., Ltd.
+ * Author: wenkai.chung <wenkai.chung@xxxxxxxxxxxxxxxx>
+ */
+
+#include <linux/delay.h>
+#include <linux/isa.h>
+#include <linux/mfd/core.h>
+#include <linux/mutex.h>
+#include <linux/time.h>
+#include <linux/uaccess.h>
+
+#include "eiois200.h"
+
+#define TIMEOUT_MAX (10 * 1000 * 1000)
+#define TIMEOUT_MIN 200
+
+static uint timeout = 5000;
+module_param(timeout, uint, 0664);
+MODULE_PARM_DESC(timeout,
+ "Default PMC command timeout in usec.\n");
+
+struct eiois200_dev_port {
+ u16 idx;
+ u16 data;
+};
+
+struct eiois200_dev_port pnp_port[] = {
+ { .idx = EIOIS200_PNP_INDEX, .data = EIOIS200_PNP_DATA },
+ { .idx = EIOIS200_SUB_PNP_INDEX, .data = EIOIS200_SUB_PNP_DATA },
+};
+
+static struct eiois200_dev *eiois200_dev;
+static struct regmap *regmap_is200;
+
+static struct mfd_cell susi_mfd_devs[] = {
+ { .name = "eiois200_wdt" },
+ { .name = "gpio_eiois200" },
+ { .name = "eiois200_hwmon" },
+ { .name = "eiois200_i2c" },
+ { .name = "eiois200_thermal" },
+};
+
+struct regmap_range is200_range[] = {
+ regmap_reg_range(EIOIS200_PNP_INDEX, EIOIS200_PNP_DATA),
+ regmap_reg_range(EIOIS200_SUB_PNP_INDEX, EIOIS200_SUB_PNP_DATA),
+ regmap_reg_range(EIOIS200_PMC_PORT, EIOIS200_PMC_PORT + 0x0F),
+ regmap_reg_range(EIOIS200_PMC_PORT_SUB, EIOIS200_PMC_PORT_SUB + 0x0F),
+};
+
+static const struct regmap_access_table volatile_regs = {
+ .yes_ranges = is200_range,
+ .n_yes_ranges = ARRAY_SIZE(is200_range),
+};
+
+static const struct regmap_config pnp_regmap_config = {
+ .name = "eiois200-gpio",
+ .reg_bits = 16,
+ .val_bits = 8,
+ .volatile_table = &volatile_regs,
+ .io_port = true,
+};
+
+/* Following are EIO-IS200 pnp io port access functions */
+static int is200_pnp_read(struct eiois200_dev_port *port, u8 idx)
+{
+ int val;
+
+ if (regmap_write(regmap_is200, port->idx, idx))
+ pr_err("Error port write 0x%X\n", port->idx);
+
+ if (regmap_read(regmap_is200, port->data, &val))
+ pr_err("Error port read 0x%X\n", port->data);
+
+ return val;
+}
+
+static void is200_pnp_write(struct eiois200_dev_port *port, u8 idx, u8 data)
+{
+ if (regmap_write(regmap_is200, port->idx, idx) ||
+ regmap_write(regmap_is200, port->data, data))
+ pr_err("Error port write 0x%X %X\n",
+ port->idx, port->data);
+}
+
+static void is200_pnp_enter(struct eiois200_dev_port *port)
+{
+ if (regmap_write(regmap_is200, port->idx, EIOIS200_EXT_MODE_ENTER) ||
+ regmap_write(regmap_is200, port->idx, EIOIS200_EXT_MODE_ENTER))
+ pr_err("Error port write 0x%X\n", port->idx);
+}
+
+static void is200_pnp_leave(struct eiois200_dev_port *port)
+{
+ if (regmap_write(regmap_is200, port->idx, EIOIS200_EXT_MODE_EXIT))
+ pr_err("Error port write 0x%X\n", port->idx);
+}
+
+/* Following are EIO-IS200 io port access functions for pmc command */
+static int pmc_write_data(int id, u8 value, u16 timeout)
+{
+ int ret;
+
+ if (WAIT_IBF(id, timeout))
+ return -ETIME;
+
+ ret = regmap_write(regmap_is200, eiois200_dev->pmc[id].data, value);
+ if (ret)
+ pr_err("Error pmc write %X:%X\n",
+ eiois200_dev->pmc[id].data, value);
+
+ return ret;
+}
+
+static int pmc_write_cmd(int id, u8 value, u16 timeout)
+{
+ int ret;
+
+ if (WAIT_IBF(id, timeout))
+ return -ETIME;
+
+ ret = regmap_write(regmap_is200, eiois200_dev->pmc[id].cmd, value);
+ if (ret)
+ pr_err("Error pmc write %X:%X\n",
+ eiois200_dev->pmc[id].data, value);
+
+ return ret;
+}
+
+static int pmc_read_data(int id, u8 *value, u16 timeout)
+{
+ int val, ret;
+
+ if (WAIT_OBF(id, timeout))
+ return -ETIME;
+
+ ret = regmap_read(regmap_is200, eiois200_dev->pmc[id].data, &val);
+ if (ret)
+ pr_err("Error pmc read %X\n", eiois200_dev->pmc[id].data);
+ else
+ *value = val & 0xFF;
+
+ return ret;
+}
+
+static int pmc_read_status(int id)
+{
+ int val;
+
+ if (regmap_read(regmap_is200, eiois200_dev->pmc[id].data, &val)) {
+ pr_err("Error pmc read %X\n", eiois200_dev->pmc[id].data);
+ return 0;
+ }
+
+ return val;
+}
+
+static void pmc_clear(int id)
+{
+ int val;
+
+ /* Check if input buffer blocked */
+ if ((pmc_read_status(id) & EIOIS200_PMC_STATUS_IBF) == 0)
+ return;
+
+ /* Read out previous garbage */
+ if (regmap_read(regmap_is200, eiois200_dev->pmc[id].data, &val))
+ pr_err("Error pmc clear\n");
+
+ usleep_range(10, 100);
+}
+
+/**
+ * eiois200_core_pmc_wait - Wait for input / output buffer to be ready.
+ * @id: 0 for main chip, 1 for sub chip.
+ * @wait: %PMC_WAIT_INPUT or %PMC_WAIT_OUTPUT.
+ * %PMC_WAIT_INPUT for waiting input buffer data ready.
+ * %PMC_WAIT_OUTPUT for waiting output buffer empty.
+ * max_duration: The timeout value in usec.
+ */
+int eiois200_core_pmc_wait(int id,
+ enum eiois200_pmc_wait wait,
+ uint max_duration)
+{
+ u32 cnt = 0;
+ uint val;
+ int ret;
+ int new_timeout = max_duration ? max_duration : timeout;
+ ktime_t time_end = ktime_add_us(ktime_get(), new_timeout);
+
+ if (new_timeout > TIMEOUT_MAX ||
+ new_timeout < TIMEOUT_MIN) {
+ pr_err("Error timeout value: %dus\nTimeout value should between %d and %d\n",
+ new_timeout, TIMEOUT_MIN, TIMEOUT_MAX);
+ return -ETIME;
+ }
+
+ do {
+ ret = regmap_read(regmap_is200,
+ eiois200_dev->pmc[id].status,
+ &val);
+ if (ret)
+ return ret;
+
+ if (wait == PMC_WAIT_INPUT) {
+ if ((val & EIOIS200_PMC_STATUS_IBF) == 0)
+ return 0;
+ } else {
+ if ((val & EIOIS200_PMC_STATUS_OBF) != 0)
+ return 0;
+ }
+
+ /* Incremental delay */
+ fsleep(cnt++ * 10);
+
+ } while (ktime_before(ktime_get(), time_end));
+
+ return -ETIME;
+}
+EXPORT_SYMBOL_GPL(eiois200_core_pmc_wait);
+
+/**
+ * eiois200_core_pmc_operation - Execute a pmc command
+ * @op: Pointer to an pmc command.
+ */
+int eiois200_core_pmc_operation(struct _pmc_op *op)
+{
+ u8 i;
+ int ret;
+ bool read_cmd = op->cmd & 0x01;
+ ktime_t t = ktime_get();
+
+ mutex_lock(&eiois200_dev->mutex);
+
+ pmc_clear(op->chip);
+
+ ret = pmc_write_cmd(op->chip, op->cmd, op->timeout);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(op->chip, op->control, op->timeout);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(op->chip, op->device_id, op->timeout);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(op->chip, op->size, op->timeout);
+ if (ret)
+ goto err;
+
+ for (i = 0 ; i < op->size ; i++) {
+ if (read_cmd)
+ ret = pmc_read_data(op->chip, &op->payload[i], op->timeout);
+ else
+ ret = pmc_write_data(op->chip, op->payload[i], op->timeout);
+
+ if (ret)
+ goto err;
+ }
+
+ mutex_unlock(&eiois200_dev->mutex);
+
+ return 0;
+
+err:
+ mutex_unlock(&eiois200_dev->mutex);
+
+ pr_err("PMC error duration:%lldus\n",
+ ktime_to_us(ktime_sub(ktime_get(), t)));
+ pr_err(".cmd=0x%02X, .ctrl=0x%02X .id=0x%02X, .size=0x%02X .data=0x%02X%02X\n",
+ op->cmd, op->control, op->device_id,
+ op->size, op->payload[0], op->payload[1]);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(eiois200_core_pmc_operation);
+
+static int get_pmc_port(struct device *dev, int id, struct eiois200_dev_port *port)
+{
+ struct _pmc_port *pmc = &eiois200_dev->pmc[id];
+
+ is200_pnp_enter(port);
+
+ /* Switch to PMC device page */
+ is200_pnp_write(port, EIOIS200_LDN, EIOIS200_LDN_PMC1);
+
+ /* Active this device */
+ is200_pnp_write(port, EIOIS200_LDAR, EIOIS200_LDAR_LDACT);
+
+ /* Get PMC cmd and data port */
+ pmc->data = is200_pnp_read(port, EIOIS200_IOBA0H) << 8;
+ pmc->data |= is200_pnp_read(port, EIOIS200_IOBA0L);
+ pmc->cmd = is200_pnp_read(port, EIOIS200_IOBA1H) << 8;
+ pmc->cmd |= is200_pnp_read(port, EIOIS200_IOBA1L);
+
+ /* Disable IRQ */
+ is200_pnp_write(port, EIOIS200_IRQCTRL, 0);
+
+ is200_pnp_leave(port);
+
+ /* 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 eiois200_exist(struct device *dev)
+{
+ u16 chip_id = 0;
+ u8 tmp = 0;
+ int i = 0;
+ int ret = -ENOMEM;
+ char chip[][8] = { "First", "Second" };
+
+ for (i = 0 ; i < ARRAY_SIZE(pnp_port) ; i++) {
+ struct eiois200_dev_port *port = pnp_port + i;
+
+ if (!devm_request_region(dev,
+ pnp_port[i].idx,
+ 2,
+ KBUILD_MODNAME))
+ continue;
+
+ is200_pnp_enter(port);
+
+ chip_id = is200_pnp_read(port, EIOIS200_CHIPID1) << 8;
+ chip_id |= is200_pnp_read(port, EIOIS200_CHIPID2);
+
+ if (chip_id != EIOIS200_CHIPID &&
+ chip_id != EIO201_211_CHIPID)
+ continue;
+
+ /* Turn on the enable flag */
+ tmp = is200_pnp_read(port, EIOIS200_SIOCTRL);
+ tmp |= EIOIS200_SIOCTRL_SIOEN;
+ is200_pnp_write(port, EIOIS200_SIOCTRL, tmp);
+
+ is200_pnp_leave(port);
+
+ ret = get_pmc_port(dev, i, port);
+ if (ret)
+ return ret;
+
+ eiois200_dev->flag |= i == 0 ? EIOIS200_F_CHIP_EXIST :
+ EIOIS200_F_SUB_CHIP_EXIST;
+
+ pr_info("%s chip detected: %04X\n", chip[i], chip_id);
+ }
+
+ return ret;
+}
+
+/* read information about acpi stored in EC */
+static uint8_t acpiram_access(uint8_t offset)
+{
+ u8 val;
+ int ret;
+
+ mutex_lock(&eiois200_dev->mutex);
+
+ pmc_clear(0);
+
+ ret = pmc_write_cmd(0, EIOIS200_PMC_CMD_ACPIRAM_READ, 0);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(0, offset, 0);
+ if (ret)
+ goto err;
+
+ ret = pmc_write_data(0, 1, 0);
+ if (ret)
+ goto err;
+
+ ret = pmc_read_data(0, &val, 0);
+ if (ret)
+ goto err;
+
+err:
+ mutex_unlock(&eiois200_dev->mutex);
+ return ret ? 0 : val;
+}
+
+static int firmware_code_base(struct device *dev)
+{
+ u8 ic_vendor, ic_code, code_base;
+
+ ic_vendor = acpiram_access(EIOIS200_ACPIRAM_ICVENDOR);
+ ic_code = acpiram_access(EIOIS200_ACPIRAM_ICCODE);
+ code_base = acpiram_access(EIOIS200_ACPIRAM_CODEBASE);
+
+ if (ic_vendor != 'R')
+ return -ENODEV;
+
+ if (ic_code != EIOIS200_ICCODE &&
+ ic_code != EIO201_ICCODE &&
+ ic_code != EIO211_ICCODE)
+ goto err;
+
+ if (code_base == 0x80) {
+ eiois200_dev->flag |= EIOIS200_F_NEW_CODE_BASE;
+ return 0;
+ }
+
+ if (code_base == 0 && (ic_code != EIO201_ICCODE &&
+ ic_code != EIO211_ICCODE)) {
+ pr_info("Old code base not supported, yet.");
+ return -ENODEV;
+ }
+
+ err:
+ dev_err(dev,
+ "Codebase check fail:\n"
+ "ic_vendor: 0x%X ,ic_code : 0x%X ,code_base : 0x%X\n",
+ ic_vendor, ic_code, code_base);
+ return -ENODEV;
+}
+
+int eiois200_probe(struct device *dev, unsigned int id)
+{
+ int ret = 0;
+ void __iomem *iomem;
+
+ iomem = devm_ioport_map(dev, 0, EIOIS200_SUB_PNP_DATA + 1);
+ if (!iomem)
+ return -ENOMEM;
+
+ regmap_is200 = devm_regmap_init_mmio(dev, iomem, &pnp_regmap_config);
+ if (!regmap_is200)
+ return -ENOMEM;
+
+ eiois200_dev = devm_kzalloc(dev,
+ sizeof(struct eiois200_dev),
+ GFP_KERNEL);
+ if (!eiois200_dev)
+ return -ENOMEM;
+
+ mutex_init(&eiois200_dev->mutex);
+
+ if (eiois200_exist(dev))
+ return -ENODEV;
+
+ if (firmware_code_base(dev)) {
+ dev_err(dev, "Chip code base check fail\n");
+ return -EIO;
+ }
+
+ dev_set_drvdata(dev, eiois200_dev);
+
+ ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE, susi_mfd_devs,
+ ARRAY_SIZE(susi_mfd_devs),
+ NULL, 0, NULL);
+ if (ret)
+ dev_err(dev, "Cannot register child devices (error = %d)\n", ret);
+
+ pr_info("%s started", KBUILD_MODNAME);
+
+ return 0;
+}
+
+struct {
+ char name[32];
+ int cmd;
+ int ctrl;
+ int size;
+} attrs[] = {
+ { "board_name", 0x53, 0x10, 16 },
+ { "board_serial", 0x53, 0x1F, 16 },
+ { "board_manufacturer", 0x53, 0x11, 16 },
+ { "board_id", 0x53, 0x1E, 4 },
+ { "firmware_version", 0x53, 0x22, 16 },
+ { "firmware_build", 0x53, 0x23, 26 },
+ { "firmware_date", 0x53, 0x24, 16 },
+ { "chip_id", 0x53, 0x12, 12 },
+ { "chip_detect", 0x53, 0x15, 12 },
+ { "platform_type", 0x53, 0x13, 16 },
+ { "platform_revision", 0x53, 0x14, 4 },
+ { "eapi_version", 0x53, 0x30, 4 },
+ { "eapi_id", 0x53, 0x31, 4 },
+ { "boot_count", 0x55, 0x10, 4 },
+ { "powerup_hour", 0x55, 0x11, 4 },
+};
+
+static ssize_t info_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ uint i;
+
+ for (i = 0 ; i < ARRAY_SIZE(attrs) ; i++)
+ if (!strcmp(attr->attr.name, attrs[i].name)) {
+ int ret;
+ char str[32] = "";
+ struct _pmc_op op = {
+ .cmd = attrs[i].cmd,
+ .control = attrs[i].ctrl,
+ .payload = (u8 *)str,
+ .size = attrs[i].size,
+ };
+
+ ret = eiois200_core_pmc_operation(&op);
+ if (ret)
+ return ret;
+
+ if (attrs[i].size == 4)
+ return sysfs_emit(buf, "%X\n", *(u32 *)str);
+ else
+ return sysfs_emit(buf, "%s\n", str);
+ }
+
+ return -EINVAL;
+}
+
+static DEVICE_ATTR(board_name, 0444, info_show, NULL);
+static DEVICE_ATTR(board_serial, 0444, info_show, NULL);
+static DEVICE_ATTR(board_manufacturer, 0444, info_show, NULL);
+static DEVICE_ATTR(firmware_version, 0444, info_show, NULL);
+static DEVICE_ATTR(firmware_build, 0444, info_show, NULL);
+static DEVICE_ATTR(firmware_date, 0444, info_show, NULL);
+static DEVICE_ATTR(chip_id, 0444, info_show, NULL);
+static DEVICE_ATTR(chip_detect, 0444, info_show, NULL);
+static DEVICE_ATTR(platform_type, 0444, info_show, NULL);
+static DEVICE_ATTR(platform_revision, 0444, info_show, NULL);
+static DEVICE_ATTR(board_id, 0444, info_show, NULL);
+static DEVICE_ATTR(eapi_version, 0444, info_show, NULL);
+static DEVICE_ATTR(eapi_id, 0444, info_show, NULL);
+static DEVICE_ATTR(boot_count, 0444, info_show, NULL);
+static DEVICE_ATTR(powerup_hour, 0444, info_show, NULL);
+
+static struct attribute *pmc_attrs[] = {
+ &dev_attr_board_name.attr,
+ &dev_attr_board_serial.attr,
+ &dev_attr_board_manufacturer.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,
+ NULL
+};
+
+static const struct attribute_group attr_group = {
+ .attrs = pmc_attrs,
+};
+
+static const struct attribute_group *attr_groups[] = {
+ &attr_group,
+ NULL
+};
+
+static struct isa_driver eiois200_driver = {
+ .probe = eiois200_probe,
+
+ .driver = {
+ .owner = THIS_MODULE,
+ .name = KBUILD_MODNAME,
+ .dev_groups = attr_groups,
+ },
+};
+
+module_isa_driver(eiois200_driver, 1);
+
+MODULE_AUTHOR("susi.driver <susi.driver@xxxxxxxxxxxxxxxx>");
+MODULE_DESCRIPTION("Advantech EIO-IS200 series EC core driver");
+MODULE_LICENSE("GPL v2");
--
2.34.1