root/drivers/platform/cznic/turris-omnia-mcu-base.c
// 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;
}
EXPORT_SYMBOL_GPL(omnia_cmd_write_read);

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"),
                _DEF_FEAT(BRIGHTNESS_INT,       "LED panel brightness change interrupt"),
                _DEF_FEAT(LED_GAMMA_CORRECTION, "LED gamma correction"),
#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;

        err = omnia_mcu_register_keyctl(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");