mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git
synced 2025-01-07 14:32:23 +00:00
af340b7aa2
Make the GPIO part of the driver optional, under a boolean config
option. Move the dependency to GPIOLIB and OF and the selection of
GPIOLIB_IRQCHIP to this new option.
This makes the turris-omnia-mcu driver available for compilation even if
GPIOLIB or OF are disabled.
Fixes: ed46f1f773
("platform: cznic: turris-omnia-mcu: fix Kconfig dependencies")
Signed-off-by: Marek Behún <kabel@kernel.org>
Link: https://lore.kernel.org/r/20240719085756.30598-5-kabel@kernel.org
Signed-off-by: Arnd Bergmann <arnd@arndb.de>
413 lines
9.9 KiB
C
413 lines
9.9 KiB
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;
|
|
}
|
|
|
|
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");
|