// SPDX-License-Identifier: GPL-2.0
/*
 * CZ.NIC's Turris Omnia MCU driver
 *
 * 2024 by Marek Behún <kabel@kernel.org>
 */

#include <linux/array_size.h>
#include <linux/bits.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/hex.h>
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/types.h>

#include <linux/turris-omnia-mcu-interface.h>
#include "turris-omnia-mcu.h"

#define OMNIA_FW_VERSION_LEN		20
#define OMNIA_FW_VERSION_HEX_LEN	(2 * OMNIA_FW_VERSION_LEN + 1)
#define OMNIA_BOARD_INFO_LEN		16

int omnia_cmd_write_read(const struct i2c_client *client,
			 void *cmd, unsigned int cmd_len,
			 void *reply, unsigned int reply_len)
{
	struct i2c_msg msgs[2];
	int ret, num;

	msgs[0].addr = client->addr;
	msgs[0].flags = 0;
	msgs[0].len = cmd_len;
	msgs[0].buf = cmd;
	num = 1;

	if (reply_len) {
		msgs[1].addr = client->addr;
		msgs[1].flags = I2C_M_RD;
		msgs[1].len = reply_len;
		msgs[1].buf = reply;
		num++;
	}

	ret = i2c_transfer(client->adapter, msgs, num);
	if (ret < 0)
		return ret;
	if (ret != num)
		return -EIO;

	return 0;
}

static int omnia_get_version_hash(struct omnia_mcu *mcu, bool bootloader,
				  char version[static OMNIA_FW_VERSION_HEX_LEN])
{
	u8 reply[OMNIA_FW_VERSION_LEN];
	char *p;
	int err;

	err = omnia_cmd_read(mcu->client,
			     bootloader ? OMNIA_CMD_GET_FW_VERSION_BOOT
					: OMNIA_CMD_GET_FW_VERSION_APP,
			     reply, sizeof(reply));
	if (err)
		return err;

	p = bin2hex(version, reply, OMNIA_FW_VERSION_LEN);
	*p = '\0';

	return 0;
}

static ssize_t fw_version_hash_show(struct device *dev, char *buf,
				    bool bootloader)
{
	struct omnia_mcu *mcu = dev_get_drvdata(dev);
	char version[OMNIA_FW_VERSION_HEX_LEN];
	int err;

	err = omnia_get_version_hash(mcu, bootloader, version);
	if (err)
		return err;

	return sysfs_emit(buf, "%s\n", version);
}

static ssize_t fw_version_hash_application_show(struct device *dev,
						struct device_attribute *a,
						char *buf)
{
	return fw_version_hash_show(dev, buf, false);
}
static DEVICE_ATTR_RO(fw_version_hash_application);

static ssize_t fw_version_hash_bootloader_show(struct device *dev,
					       struct device_attribute *a,
					       char *buf)
{
	return fw_version_hash_show(dev, buf, true);
}
static DEVICE_ATTR_RO(fw_version_hash_bootloader);

static ssize_t fw_features_show(struct device *dev, struct device_attribute *a,
				char *buf)
{
	struct omnia_mcu *mcu = dev_get_drvdata(dev);

	return sysfs_emit(buf, "0x%x\n", mcu->features);
}
static DEVICE_ATTR_RO(fw_features);

static ssize_t mcu_type_show(struct device *dev, struct device_attribute *a,
			     char *buf)
{
	struct omnia_mcu *mcu = dev_get_drvdata(dev);

	return sysfs_emit(buf, "%s\n", mcu->type);
}
static DEVICE_ATTR_RO(mcu_type);

static ssize_t reset_selector_show(struct device *dev,
				   struct device_attribute *a, char *buf)
{
	u8 reply;
	int err;

	err = omnia_cmd_read_u8(to_i2c_client(dev), OMNIA_CMD_GET_RESET,
				&reply);
	if (err)
		return err;

	return sysfs_emit(buf, "%d\n", reply);
}
static DEVICE_ATTR_RO(reset_selector);

static ssize_t serial_number_show(struct device *dev,
				  struct device_attribute *a, char *buf)
{
	struct omnia_mcu *mcu = dev_get_drvdata(dev);

	return sysfs_emit(buf, "%016llX\n", mcu->board_serial_number);
}
static DEVICE_ATTR_RO(serial_number);

static ssize_t first_mac_address_show(struct device *dev,
				      struct device_attribute *a, char *buf)
{
	struct omnia_mcu *mcu = dev_get_drvdata(dev);

	return sysfs_emit(buf, "%pM\n", mcu->board_first_mac);
}
static DEVICE_ATTR_RO(first_mac_address);

static ssize_t board_revision_show(struct device *dev,
				   struct device_attribute *a, char *buf)
{
	struct omnia_mcu *mcu = dev_get_drvdata(dev);

	return sysfs_emit(buf, "%u\n", mcu->board_revision);
}
static DEVICE_ATTR_RO(board_revision);

static struct attribute *omnia_mcu_base_attrs[] = {
	&dev_attr_fw_version_hash_application.attr,
	&dev_attr_fw_version_hash_bootloader.attr,
	&dev_attr_fw_features.attr,
	&dev_attr_mcu_type.attr,
	&dev_attr_reset_selector.attr,
	&dev_attr_serial_number.attr,
	&dev_attr_first_mac_address.attr,
	&dev_attr_board_revision.attr,
	NULL
};

static umode_t omnia_mcu_base_attrs_visible(struct kobject *kobj,
					    struct attribute *a, int n)
{
	struct device *dev = kobj_to_dev(kobj);
	struct omnia_mcu *mcu = dev_get_drvdata(dev);

	if ((a == &dev_attr_serial_number.attr ||
	     a == &dev_attr_first_mac_address.attr ||
	     a == &dev_attr_board_revision.attr) &&
	    !(mcu->features & OMNIA_FEAT_BOARD_INFO))
		return 0;

	return a->mode;
}

static const struct attribute_group omnia_mcu_base_group = {
	.attrs = omnia_mcu_base_attrs,
	.is_visible = omnia_mcu_base_attrs_visible,
};

static const struct attribute_group *omnia_mcu_groups[] = {
	&omnia_mcu_base_group,
#ifdef CONFIG_TURRIS_OMNIA_MCU_GPIO
	&omnia_mcu_gpio_group,
#endif
#ifdef CONFIG_TURRIS_OMNIA_MCU_SYSOFF_WAKEUP
	&omnia_mcu_poweroff_group,
#endif
	NULL
};

static void omnia_mcu_print_version_hash(struct omnia_mcu *mcu, bool bootloader)
{
	const char *type = bootloader ? "bootloader" : "application";
	struct device *dev = &mcu->client->dev;
	char version[OMNIA_FW_VERSION_HEX_LEN];
	int err;

	err = omnia_get_version_hash(mcu, bootloader, version);
	if (err) {
		dev_err(dev, "Cannot read MCU %s firmware version: %d\n",
			type, err);
		return;
	}

	dev_info(dev, "MCU %s firmware version hash: %s\n", type, version);
}

static const char *omnia_status_to_mcu_type(u16 status)
{
	switch (status & OMNIA_STS_MCU_TYPE_MASK) {
	case OMNIA_STS_MCU_TYPE_STM32:
		return "STM32";
	case OMNIA_STS_MCU_TYPE_GD32:
		return "GD32";
	case OMNIA_STS_MCU_TYPE_MKL:
		return "MKL";
	default:
		return "unknown";
	}
}

static void omnia_info_missing_feature(struct device *dev, const char *feature)
{
	dev_info(dev,
		 "Your board's MCU firmware does not support the %s feature.\n",
		 feature);
}

static int omnia_mcu_read_features(struct omnia_mcu *mcu)
{
	static const struct {
		u16 mask;
		const char *name;
	} features[] = {
#define _DEF_FEAT(_n, _m) { OMNIA_FEAT_ ## _n, _m }
		_DEF_FEAT(EXT_CMDS,		"extended control and status"),
		_DEF_FEAT(WDT_PING,		"watchdog pinging"),
		_DEF_FEAT(LED_STATE_EXT_MASK,	"peripheral LED pins reading"),
		_DEF_FEAT(NEW_INT_API,		"new interrupt API"),
		_DEF_FEAT(POWEROFF_WAKEUP,	"poweroff and wakeup"),
		_DEF_FEAT(TRNG,			"true random number generator"),
#undef _DEF_FEAT
	};
	struct i2c_client *client = mcu->client;
	struct device *dev = &client->dev;
	bool suggest_fw_upgrade = false;
	u16 status;
	int err;

	/* status word holds MCU type, which we need below */
	err = omnia_cmd_read_u16(client, OMNIA_CMD_GET_STATUS_WORD, &status);
	if (err)
		return err;

	/*
	 * Check whether MCU firmware supports the OMNIA_CMD_GET_FEATURES
	 * command.
	 */
	if (status & OMNIA_STS_FEATURES_SUPPORTED) {
		/* try read 32-bit features */
		err = omnia_cmd_read_u32(client, OMNIA_CMD_GET_FEATURES,
					 &mcu->features);
		if (err) {
			/* try read 16-bit features */
			u16 features16;

			err = omnia_cmd_read_u16(client, OMNIA_CMD_GET_FEATURES,
						 &features16);
			if (err)
				return err;

			mcu->features = features16;
		} else {
			if (mcu->features & OMNIA_FEAT_FROM_BIT_16_INVALID)
				mcu->features &= GENMASK(15, 0);
		}
	} else {
		dev_info(dev,
			 "Your board's MCU firmware does not support feature reading.\n");
		suggest_fw_upgrade = true;
	}

	mcu->type = omnia_status_to_mcu_type(status);
	dev_info(dev, "MCU type %s%s\n", mcu->type,
		 (mcu->features & OMNIA_FEAT_PERIPH_MCU) ?
			", with peripheral resets wired" : "");

	omnia_mcu_print_version_hash(mcu, true);

	if (mcu->features & OMNIA_FEAT_BOOTLOADER)
		dev_warn(dev,
			 "MCU is running bootloader firmware. Was firmware upgrade interrupted?\n");
	else
		omnia_mcu_print_version_hash(mcu, false);

	for (unsigned int i = 0; i < ARRAY_SIZE(features); i++) {
		if (mcu->features & features[i].mask)
			continue;

		omnia_info_missing_feature(dev, features[i].name);
		suggest_fw_upgrade = true;
	}

	if (suggest_fw_upgrade)
		dev_info(dev,
			 "Consider upgrading MCU firmware with the omnia-mcutool utility.\n");

	return 0;
}

static int omnia_mcu_read_board_info(struct omnia_mcu *mcu)
{
	u8 reply[1 + OMNIA_BOARD_INFO_LEN];
	int err;

	err = omnia_cmd_read(mcu->client, OMNIA_CMD_BOARD_INFO_GET, reply,
			     sizeof(reply));
	if (err)
		return err;

	if (reply[0] != OMNIA_BOARD_INFO_LEN)
		return -EIO;

	mcu->board_serial_number = get_unaligned_le64(&reply[1]);

	/* we can't use ether_addr_copy() because reply is not u16-aligned */
	memcpy(mcu->board_first_mac, &reply[9], sizeof(mcu->board_first_mac));

	mcu->board_revision = reply[15];

	return 0;
}

static int omnia_mcu_probe(struct i2c_client *client)
{
	struct device *dev = &client->dev;
	struct omnia_mcu *mcu;
	int err;

	if (!client->irq)
		return dev_err_probe(dev, -EINVAL, "IRQ resource not found\n");

	mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
	if (!mcu)
		return -ENOMEM;

	mcu->client = client;
	i2c_set_clientdata(client, mcu);

	err = omnia_mcu_read_features(mcu);
	if (err)
		return dev_err_probe(dev, err,
				     "Cannot determine MCU supported features\n");

	if (mcu->features & OMNIA_FEAT_BOARD_INFO) {
		err = omnia_mcu_read_board_info(mcu);
		if (err)
			return dev_err_probe(dev, err,
					     "Cannot read board info\n");
	}

	err = omnia_mcu_register_sys_off_and_wakeup(mcu);
	if (err)
		return err;

	err = omnia_mcu_register_watchdog(mcu);
	if (err)
		return err;

	err = omnia_mcu_register_gpiochip(mcu);
	if (err)
		return err;

	return omnia_mcu_register_trng(mcu);
}

static const struct of_device_id of_omnia_mcu_match[] = {
	{ .compatible = "cznic,turris-omnia-mcu" },
	{}
};

static struct i2c_driver omnia_mcu_driver = {
	.probe		= omnia_mcu_probe,
	.driver		= {
		.name	= "turris-omnia-mcu",
		.of_match_table = of_omnia_mcu_match,
		.dev_groups = omnia_mcu_groups,
	},
};
module_i2c_driver(omnia_mcu_driver);

MODULE_AUTHOR("Marek Behun <kabel@kernel.org>");
MODULE_DESCRIPTION("CZ.NIC's Turris Omnia MCU");
MODULE_LICENSE("GPL");
