diff options
Diffstat (limited to 'drivers/platform/cznic/turris-omnia-mcu-base.c')
| -rw-r--r-- | drivers/platform/cznic/turris-omnia-mcu-base.c | 408 | 
1 files changed, 408 insertions, 0 deletions
diff --git a/drivers/platform/cznic/turris-omnia-mcu-base.c b/drivers/platform/cznic/turris-omnia-mcu-base.c new file mode 100644 index 000000000000..c68a7a84a951 --- /dev/null +++ b/drivers/platform/cznic/turris-omnia-mcu-base.c @@ -0,0 +1,408 @@ +// 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, +	&omnia_mcu_gpio_group, +	&omnia_mcu_poweroff_group, +	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");  | 
