--- /dev/null
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Congatec Board Controller core driver.
+ *
+ * The x86 Congatec modules have an embedded micro controller named Board
+ * Controller. This Board Controller has a Watchdog timer, some GPIOs, and two
+ * I2C busses.
+ *
+ * Copyright (C) 2024 Bootlin
+ *
+ * Author: Thomas Richard <thomas.richard@bootlin.com>
+ */
+
+#include <linux/dmi.h>
+#include <linux/iopoll.h>
+#include <linux/mfd/cgbc.h>
+#include <linux/mfd/core.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/sysfs.h>
+
+#define CGBC_IO_SESSION_BASE   0x0E20
+#define CGBC_IO_SESSION_END    0x0E30
+#define CGBC_IO_CMD_BASE       0x0E00
+#define CGBC_IO_CMD_END                0x0E10
+
+#define CGBC_MASK_STATUS       (BIT(6) | BIT(7))
+#define CGBC_MASK_DATA_COUNT   0x1F
+#define CGBC_MASK_ERROR_CODE   0x1F
+
+#define CGBC_STATUS_DATA_READY 0x00
+#define CGBC_STATUS_CMD_READY  BIT(6)
+#define CGBC_STATUS_ERROR      (BIT(6) | BIT(7))
+
+#define CGBC_SESSION_CMD               0x00
+#define CGBC_SESSION_CMD_IDLE          0x00
+#define CGBC_SESSION_CMD_REQUEST       0x01
+#define CGBC_SESSION_DATA              0x01
+#define CGBC_SESSION_STATUS            0x02
+#define CGBC_SESSION_STATUS_FREE       0x03
+#define CGBC_SESSION_ACCESS            0x04
+#define CGBC_SESSION_ACCESS_GAINED     0x00
+
+#define CGBC_SESSION_VALID_MIN  0x02
+#define CGBC_SESSION_VALID_MAX  0xFE
+
+#define CGBC_CMD_STROBE                        0x00
+#define CGBC_CMD_INDEX                 0x02
+#define CGBC_CMD_INDEX_CBM_MAN8                0x00
+#define CGBC_CMD_INDEX_CBM_AUTO32      0x03
+#define CGBC_CMD_DATA                  0x04
+#define CGBC_CMD_ACCESS                        0x0C
+
+#define CGBC_CMD_GET_FW_REV    0x21
+
+static struct platform_device *cgbc_pdev;
+
+/* Wait the Board Controller is ready to receive some session commands */
+static int cgbc_wait_device(struct cgbc_device_data *cgbc)
+{
+       u16 status;
+       int ret;
+
+       ret = readx_poll_timeout(ioread16, cgbc->io_session + CGBC_SESSION_STATUS, status,
+                                status == CGBC_SESSION_STATUS_FREE, 0, 500000);
+
+       if (ret || ioread32(cgbc->io_session + CGBC_SESSION_ACCESS))
+               ret = -ENODEV;
+
+       return ret;
+}
+
+static int cgbc_session_command(struct cgbc_device_data *cgbc, u8 cmd)
+{
+       int ret;
+       u8 val;
+
+       ret = readx_poll_timeout(ioread8, cgbc->io_session + CGBC_SESSION_CMD, val,
+                                val == CGBC_SESSION_CMD_IDLE, 0, 100000);
+       if (ret)
+               return ret;
+
+       iowrite8(cmd, cgbc->io_session + CGBC_SESSION_CMD);
+
+       ret = readx_poll_timeout(ioread8, cgbc->io_session + CGBC_SESSION_CMD, val,
+                                val == CGBC_SESSION_CMD_IDLE, 0, 100000);
+       if (ret)
+               return ret;
+
+       ret = (int)ioread8(cgbc->io_session + CGBC_SESSION_DATA);
+
+       iowrite8(CGBC_SESSION_STATUS_FREE, cgbc->io_session + CGBC_SESSION_STATUS);
+
+       return ret;
+}
+
+static int cgbc_session_request(struct cgbc_device_data *cgbc)
+{
+       unsigned int ret;
+
+       ret = cgbc_wait_device(cgbc);
+
+       if (ret)
+               return dev_err_probe(cgbc->dev, ret, "device not found or not ready\n");
+
+       cgbc->session = cgbc_session_command(cgbc, CGBC_SESSION_CMD_REQUEST);
+
+       /* The Board Controller sent us a wrong session handle, we cannot communicate with it */
+       if (cgbc->session < CGBC_SESSION_VALID_MIN || cgbc->session > CGBC_SESSION_VALID_MAX)
+               return dev_err_probe(cgbc->dev, -ECONNREFUSED,
+                                    "failed to get a valid session handle\n");
+
+       return 0;
+}
+
+static void cgbc_session_release(struct cgbc_device_data *cgbc)
+{
+       if (cgbc_session_command(cgbc, cgbc->session) != cgbc->session)
+               dev_warn(cgbc->dev, "failed to release session\n");
+}
+
+static bool cgbc_command_lock(struct cgbc_device_data *cgbc)
+{
+       iowrite8(cgbc->session, cgbc->io_cmd + CGBC_CMD_ACCESS);
+
+       return ioread8(cgbc->io_cmd + CGBC_CMD_ACCESS) == cgbc->session;
+}
+
+static void cgbc_command_unlock(struct cgbc_device_data *cgbc)
+{
+       iowrite8(cgbc->session, cgbc->io_cmd + CGBC_CMD_ACCESS);
+}
+
+int cgbc_command(struct cgbc_device_data *cgbc, void *cmd, unsigned int cmd_size, void *data,
+                unsigned int data_size, u8 *status)
+{
+       u8 checksum = 0, data_checksum = 0, istatus = 0, val;
+       u8 *_data = (u8 *)data;
+       u8 *_cmd = (u8 *)cmd;
+       int mode_change = -1;
+       bool lock;
+       int ret, i;
+
+       mutex_lock(&cgbc->lock);
+
+       /* Request access */
+       ret = readx_poll_timeout(cgbc_command_lock, cgbc, lock, lock, 0, 100000);
+       if (ret)
+               goto out;
+
+       /* Wait board controller is ready */
+       ret = readx_poll_timeout(ioread8, cgbc->io_cmd + CGBC_CMD_STROBE, val,
+                                val == CGBC_CMD_STROBE, 0, 100000);
+       if (ret)
+               goto release;
+
+       /* Write command packet */
+       if (cmd_size <= 2) {
+               iowrite8(CGBC_CMD_INDEX_CBM_MAN8, cgbc->io_cmd + CGBC_CMD_INDEX);
+       } else {
+               iowrite8(CGBC_CMD_INDEX_CBM_AUTO32, cgbc->io_cmd + CGBC_CMD_INDEX);
+               if ((cmd_size % 4) != 0x03)
+                       mode_change = (cmd_size & 0xFFFC) - 1;
+       }
+
+       for (i = 0; i < cmd_size; i++) {
+               iowrite8(_cmd[i], cgbc->io_cmd + CGBC_CMD_DATA + (i % 4));
+               checksum ^= _cmd[i];
+               if (mode_change == i)
+                       iowrite8((i + 1) | CGBC_CMD_INDEX_CBM_MAN8, cgbc->io_cmd + CGBC_CMD_INDEX);
+       }
+
+       /* Append checksum byte */
+       iowrite8(checksum, cgbc->io_cmd + CGBC_CMD_DATA + (i % 4));
+
+       /* Perform command strobe */
+       iowrite8(cgbc->session, cgbc->io_cmd + CGBC_CMD_STROBE);
+
+       /* Rewind cmd buffer index */
+       iowrite8(CGBC_CMD_INDEX_CBM_AUTO32, cgbc->io_cmd + CGBC_CMD_INDEX);
+
+       /* Wait command completion */
+       ret = read_poll_timeout(ioread8, val, val == CGBC_CMD_STROBE, 0, 100000, false,
+                               cgbc->io_cmd + CGBC_CMD_STROBE);
+       if (ret)
+               goto release;
+
+       istatus = ioread8(cgbc->io_cmd + CGBC_CMD_DATA);
+       checksum = istatus;
+
+       /* Check command status */
+       switch (istatus & CGBC_MASK_STATUS) {
+       case CGBC_STATUS_DATA_READY:
+               if (istatus > data_size)
+                       istatus = data_size;
+               for (i = 0; i < istatus; i++) {
+                       _data[i] = ioread8(cgbc->io_cmd + CGBC_CMD_DATA + ((i + 1) % 4));
+                       checksum ^= _data[i];
+               }
+               data_checksum = ioread8(cgbc->io_cmd + CGBC_CMD_DATA + ((i + 1) % 4));
+               istatus &= CGBC_MASK_DATA_COUNT;
+               break;
+       case CGBC_STATUS_ERROR:
+       case CGBC_STATUS_CMD_READY:
+               data_checksum = ioread8(cgbc->io_cmd + CGBC_CMD_DATA + 1);
+               if ((istatus & CGBC_MASK_STATUS) == CGBC_STATUS_ERROR)
+                       ret = -EIO;
+               istatus = istatus & CGBC_MASK_ERROR_CODE;
+               break;
+       default:
+               data_checksum = ioread8(cgbc->io_cmd + CGBC_CMD_DATA + 1);
+               istatus &= CGBC_MASK_ERROR_CODE;
+               ret = -EIO;
+               break;
+       }
+
+       /* Checksum verification */
+       if (ret == 0 && data_checksum != checksum)
+               ret = -EIO;
+
+release:
+       cgbc_command_unlock(cgbc);
+
+out:
+       mutex_unlock(&cgbc->lock);
+
+       if (status)
+               *status = istatus;
+
+       return ret;
+}
+EXPORT_SYMBOL_GPL(cgbc_command);
+
+static struct mfd_cell cgbc_devs[] = {
+       { .name = "cgbc-wdt"    },
+       { .name = "cgbc-gpio"   },
+       { .name = "cgbc-i2c", .id = 1 },
+       { .name = "cgbc-i2c", .id = 2 },
+};
+
+static int cgbc_map(struct cgbc_device_data *cgbc)
+{
+       struct device *dev = cgbc->dev;
+       struct platform_device *pdev = to_platform_device(dev);
+       struct resource *ioport;
+
+       ioport = platform_get_resource(pdev, IORESOURCE_IO, 0);
+       if (!ioport)
+               return -EINVAL;
+
+       cgbc->io_session = devm_ioport_map(dev, ioport->start, resource_size(ioport));
+       if (!cgbc->io_session)
+               return -ENOMEM;
+
+       ioport = platform_get_resource(pdev, IORESOURCE_IO, 1);
+       if (!ioport)
+               return -EINVAL;
+
+       cgbc->io_cmd = devm_ioport_map(dev, ioport->start, resource_size(ioport));
+       if (!cgbc->io_cmd)
+               return -ENOMEM;
+
+       return 0;
+}
+
+static const struct resource cgbc_resources[] = {
+       {
+               .start  = CGBC_IO_SESSION_BASE,
+               .end    = CGBC_IO_SESSION_END,
+               .flags  = IORESOURCE_IO,
+       },
+       {
+               .start  = CGBC_IO_CMD_BASE,
+               .end    = CGBC_IO_CMD_END,
+               .flags  = IORESOURCE_IO,
+       },
+};
+
+static ssize_t cgbc_version_show(struct device *dev,
+                                struct device_attribute *attr, char *buf)
+{
+       struct cgbc_device_data *cgbc = dev_get_drvdata(dev);
+
+       return sysfs_emit(buf, "CGBCP%c%c%c\n", cgbc->version.feature, cgbc->version.major,
+                         cgbc->version.minor);
+}
+
+static DEVICE_ATTR_RO(cgbc_version);
+
+static struct attribute *cgbc_attrs[] = {
+       &dev_attr_cgbc_version.attr,
+       NULL
+};
+
+ATTRIBUTE_GROUPS(cgbc);
+
+static int cgbc_get_version(struct cgbc_device_data *cgbc)
+{
+       u8 cmd = CGBC_CMD_GET_FW_REV;
+       u8 data[4];
+       int ret;
+
+       ret = cgbc_command(cgbc, &cmd, 1, &data, sizeof(data), NULL);
+       if (ret)
+               return ret;
+
+       cgbc->version.feature = data[0];
+       cgbc->version.major = data[1];
+       cgbc->version.minor = data[2];
+
+       return 0;
+}
+
+static int cgbc_init_device(struct cgbc_device_data *cgbc)
+{
+       int ret;
+
+       ret = cgbc_session_request(cgbc);
+       if (ret)
+               return ret;
+
+       ret = cgbc_get_version(cgbc);
+       if (ret)
+               return ret;
+
+       return mfd_add_devices(cgbc->dev, -1, cgbc_devs, ARRAY_SIZE(cgbc_devs), NULL, 0, NULL);
+}
+
+static int cgbc_probe(struct platform_device *pdev)
+{
+       struct device *dev = &pdev->dev;
+       struct cgbc_device_data *cgbc;
+       int ret;
+
+       cgbc = devm_kzalloc(dev, sizeof(*cgbc), GFP_KERNEL);
+       if (!cgbc)
+               return -ENOMEM;
+
+       cgbc->dev = dev;
+
+       ret = cgbc_map(cgbc);
+       if (ret)
+               return ret;
+
+       mutex_init(&cgbc->lock);
+
+       platform_set_drvdata(pdev, cgbc);
+
+       return cgbc_init_device(cgbc);
+}
+
+static void cgbc_remove(struct platform_device *pdev)
+{
+       struct cgbc_device_data *cgbc = platform_get_drvdata(pdev);
+
+       cgbc_session_release(cgbc);
+
+       mfd_remove_devices(&pdev->dev);
+}
+
+static struct platform_driver cgbc_driver = {
+       .driver         = {
+               .name           = "cgbc",
+               .dev_groups     = cgbc_groups,
+       },
+       .probe          = cgbc_probe,
+       .remove_new     = cgbc_remove,
+};
+
+static const struct dmi_system_id cgbc_dmi_table[] __initconst = {
+       {
+               .ident = "SA7",
+               .matches = {
+                       DMI_MATCH(DMI_BOARD_VENDOR, "congatec"),
+                       DMI_MATCH(DMI_BOARD_NAME, "conga-SA7"),
+               },
+       },
+       {}
+};
+MODULE_DEVICE_TABLE(dmi, cgbc_dmi_table);
+
+static int __init cgbc_init(void)
+{
+       const struct dmi_system_id *id;
+       int ret = -ENODEV;
+
+       id = dmi_first_match(cgbc_dmi_table);
+       if (IS_ERR_OR_NULL(id))
+               return ret;
+
+       cgbc_pdev = platform_device_register_simple("cgbc", PLATFORM_DEVID_NONE, cgbc_resources,
+                                                   ARRAY_SIZE(cgbc_resources));
+       if (IS_ERR(cgbc_pdev))
+               return PTR_ERR(cgbc_pdev);
+
+       return platform_driver_register(&cgbc_driver);
+}
+
+static void __exit cgbc_exit(void)
+{
+       platform_device_unregister(cgbc_pdev);
+       platform_driver_unregister(&cgbc_driver);
+}
+
+module_init(cgbc_init);
+module_exit(cgbc_exit);
+
+MODULE_DESCRIPTION("Congatec Board Controller Core Driver");
+MODULE_AUTHOR("Thomas Richard <thomas.richard@bootlin.com>");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:cgbc-core");