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