| // 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) | |
| { | |
| 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 }, | |
| { .name = "cgbc-hwmon" }, | |
| { .name = "cgbc-backlight" }, | |
| }; | |
| 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); | |
| mfd_remove_devices(&pdev->dev); | |
| cgbc_session_release(cgbc); | |
| } | |
| 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"), | |
| }, | |
| }, | |
| { | |
| .ident = "SA8", | |
| .matches = { | |
| DMI_MATCH(DMI_BOARD_VENDOR, "congatec"), | |
| DMI_MATCH(DMI_BOARD_NAME, "conga-SA8"), | |
| }, | |
| }, | |
| {} | |
| }; | |
| 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"); |