// 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 */ #include #include #include #include #include #include #include #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) goto release_session; ret = mfd_add_devices(cgbc->dev, -1, cgbc_devs, ARRAY_SIZE(cgbc_devs), NULL, 0, NULL); if (ret) goto release_session; return 0; release_session: cgbc_session_release(cgbc); return ret; } 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 = 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 "); MODULE_LICENSE("GPL"); MODULE_ALIAS("platform:cgbc-core");