// 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);

	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"),
		},
	},
	{
		.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");
