Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Abd-Alrhman Masalkhi 2396 100.00% 1 100.00%
Total 2396 1


// SPDX-License-Identifier: GPL-2.0
/*
 * m24lr.c - Sysfs control interface for ST M24LR series RFID/NFC chips
 *
 * Copyright (c) 2025 Abd-Alrhman Masalkhi <abd.masalkhi@gmail.com>
 *
 * This driver implements both the sysfs-based control interface and EEPROM
 * access for STMicroelectronics M24LR series chips (e.g., M24LR04E-R).
 * It provides access to control registers for features such as password
 * authentication, memory protection, and device configuration. In addition,
 * it manages read and write operations to the EEPROM region of the chip.
 */

#include <linux/device.h>
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/nvmem-provider.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/regmap.h>

#define M24LR_WRITE_TIMEOUT	  25u
#define M24LR_READ_TIMEOUT	  (M24LR_WRITE_TIMEOUT)

/**
 * struct m24lr_chip - describes chip-specific sysfs layout
 * @sss_len:       the length of the sss region
 * @page_size:	   chip-specific limit on the maximum number of bytes allowed
 *		   in a single write operation.
 * @eeprom_size:   size of the EEPROM in byte
 *
 * Supports multiple M24LR chip variants (e.g., M24LRxx) by allowing each
 * to define its own set of sysfs attributes, depending on its available
 * registers and features.
 */
struct m24lr_chip {
	unsigned int sss_len;
	unsigned int page_size;
	unsigned int eeprom_size;
};

/**
 * struct m24lr - core driver data for M24LR chip control
 * @uid:           64 bits unique identifier stored in the device
 * @sss_len:       the length of the sss region
 * @page_size:	   chip-specific limit on the maximum number of bytes allowed
 *		   in a single write operation.
 * @eeprom_size:   size of the EEPROM in byte
 * @ctl_regmap:	   regmap interface for accessing the system parameter sector
 * @eeprom_regmap: regmap interface for accessing the EEPROM
 * @lock:	   mutex to synchronize operations to the device
 *
 * Central data structure holding the state and resources used by the
 * M24LR device driver.
 */
struct m24lr {
	u64 uid;
	unsigned int sss_len;
	unsigned int page_size;
	unsigned int eeprom_size;
	struct regmap *ctl_regmap;
	struct regmap *eeprom_regmap;
	struct mutex lock;	 /* synchronize operations to the device */
};

static const struct regmap_range m24lr_ctl_vo_ranges[] = {
	regmap_reg_range(0, 63),
};

static const struct regmap_access_table m24lr_ctl_vo_table = {
	.yes_ranges = m24lr_ctl_vo_ranges,
	.n_yes_ranges = ARRAY_SIZE(m24lr_ctl_vo_ranges),
};

static const struct regmap_config m24lr_ctl_regmap_conf = {
	.name = "m24lr_ctl",
	.reg_stride = 1,
	.reg_bits = 16,
	.val_bits = 8,
	.disable_locking = false,
	.cache_type = REGCACHE_RBTREE,/* Flat can't be used, there's huge gap */
	.volatile_table = &m24lr_ctl_vo_table,
};

/* Chip descriptor for M24LR04E-R variant */
static const struct m24lr_chip m24lr04e_r_chip = {
	.page_size = 4,
	.eeprom_size = 512,
	.sss_len = 4,
};

/* Chip descriptor for M24LR16E-R variant */
static const struct m24lr_chip m24lr16e_r_chip = {
	.page_size = 4,
	.eeprom_size = 2048,
	.sss_len = 16,
};

/* Chip descriptor for M24LR64E-R variant */
static const struct m24lr_chip m24lr64e_r_chip = {
	.page_size = 4,
	.eeprom_size = 8192,
	.sss_len = 64,
};

static const struct i2c_device_id m24lr_ids[] = {
	{ "m24lr04e-r", (kernel_ulong_t)&m24lr04e_r_chip},
	{ "m24lr16e-r", (kernel_ulong_t)&m24lr16e_r_chip},
	{ "m24lr64e-r", (kernel_ulong_t)&m24lr64e_r_chip},
	{ }
};
MODULE_DEVICE_TABLE(i2c, m24lr_ids);

static const struct of_device_id m24lr_of_match[] = {
	{ .compatible = "st,m24lr04e-r", .data = &m24lr04e_r_chip},
	{ .compatible = "st,m24lr16e-r", .data = &m24lr16e_r_chip},
	{ .compatible = "st,m24lr64e-r", .data = &m24lr64e_r_chip},
	{ }
};
MODULE_DEVICE_TABLE(of, m24lr_of_match);

/**
 * m24lr_regmap_read - read data using regmap with retry on failure
 * @regmap:  regmap instance for the device
 * @buf:     buffer to store the read data
 * @size:    number of bytes to read
 * @offset:  starting register address
 *
 * Attempts to read a block of data from the device with retries and timeout.
 * Some M24LR chips may transiently NACK reads (e.g., during internal write
 * cycles), so this function retries with a short sleep until the timeout
 * expires.
 *
 * Returns:
 *	 Number of bytes read on success,
 *	 -ETIMEDOUT if the read fails within the timeout window.
 */
static ssize_t m24lr_regmap_read(struct regmap *regmap, u8 *buf,
				 size_t size, unsigned int offset)
{
	int err;
	unsigned long timeout, read_time;
	ssize_t ret = -ETIMEDOUT;

	timeout = jiffies + msecs_to_jiffies(M24LR_READ_TIMEOUT);
	do {
		read_time = jiffies;

		err = regmap_bulk_read(regmap, offset, buf, size);
		if (!err) {
			ret = size;
			break;
		}

		usleep_range(1000, 2000);
	} while (time_before(read_time, timeout));

	return ret;
}

/**
 * m24lr_regmap_write - write data using regmap with retry on failure
 * @regmap: regmap instance for the device
 * @buf:    buffer containing the data to write
 * @size:   number of bytes to write
 * @offset: starting register address
 *
 * Attempts to write a block of data to the device with retries and a timeout.
 * Some M24LR devices may NACK I2C writes while an internal write operation
 * is in progress. This function retries the write operation with a short delay
 * until it succeeds or the timeout is reached.
 *
 * Returns:
 *	 Number of bytes written on success,
 *	 -ETIMEDOUT if the write fails within the timeout window.
 */
static ssize_t m24lr_regmap_write(struct regmap *regmap, const u8 *buf,
				  size_t size, unsigned int offset)
{
	int err;
	unsigned long timeout, write_time;
	ssize_t ret = -ETIMEDOUT;

	timeout = jiffies + msecs_to_jiffies(M24LR_WRITE_TIMEOUT);

	do {
		write_time = jiffies;

		err = regmap_bulk_write(regmap, offset, buf, size);
		if (!err) {
			ret = size;
			break;
		}

		usleep_range(1000, 2000);
	} while (time_before(write_time, timeout));

	return ret;
}

static ssize_t m24lr_read(struct m24lr *m24lr, u8 *buf, size_t size,
			  unsigned int offset, bool is_eeprom)
{
	struct regmap *regmap;
	ssize_t ret;

	if (is_eeprom)
		regmap = m24lr->eeprom_regmap;
	else
		regmap = m24lr->ctl_regmap;

	mutex_lock(&m24lr->lock);
	ret = m24lr_regmap_read(regmap, buf, size, offset);
	mutex_unlock(&m24lr->lock);

	return ret;
}

/**
 * m24lr_write - write buffer to M24LR device with page alignment handling
 * @m24lr:     pointer to driver context
 * @buf:       data buffer to write
 * @size:      number of bytes to write
 * @offset:    target register address in the device
 * @is_eeprom: true if the write should target the EEPROM,
 *             false if it should target the system parameters sector.
 *
 * Writes data to the M24LR device using regmap, split into chunks no larger
 * than page_size to respect device-specific write limitations (e.g., page
 * size or I2C hold-time concerns). Each chunk is aligned to the page boundary
 * defined by page_size.
 *
 * Returns:
 *	 Total number of bytes written on success,
 *	 A negative error code if any write fails.
 */
static ssize_t m24lr_write(struct m24lr *m24lr, const u8 *buf, size_t size,
			   unsigned int offset, bool is_eeprom)
{
	unsigned int n, next_sector;
	struct regmap *regmap;
	ssize_t ret = 0;
	ssize_t err;

	if (is_eeprom)
		regmap = m24lr->eeprom_regmap;
	else
		regmap = m24lr->ctl_regmap;

	n = min_t(unsigned int, size, m24lr->page_size);
	next_sector = roundup(offset + 1, m24lr->page_size);
	if (offset + n > next_sector)
		n = next_sector - offset;

	mutex_lock(&m24lr->lock);
	while (n) {
		err = m24lr_regmap_write(regmap, buf + offset, n, offset);
		if (IS_ERR_VALUE(err)) {
			if (!ret)
				ret = err;

			break;
		}

		offset += n;
		size -= n;
		ret += n;
		n = min_t(unsigned int, size, m24lr->page_size);
	}
	mutex_unlock(&m24lr->lock);

	return ret;
}

/**
 * m24lr_write_pass - Write password to M24LR043-R using secure format
 * @m24lr: Pointer to device control structure
 * @buf:   Input buffer containing hex-encoded password
 * @count: Number of bytes in @buf
 * @code:  Operation code to embed between password copies
 *
 * This function parses a 4-byte password, encodes it in  big-endian format,
 * and constructs a 9-byte sequence of the form:
 *
 *	  [BE(password), code, BE(password)]
 *
 * The result is written to register 0x0900 (2304), which is the password
 * register in M24LR04E-R chip.
 *
 * Return: Number of bytes written on success, or negative error code on failure
 */
static ssize_t m24lr_write_pass(struct m24lr *m24lr, const char *buf,
				size_t count, u8 code)
{
	__be32 be_pass;
	u8 output[9];
	ssize_t ret;
	u32 pass;
	int err;

	if (!count)
		return -EINVAL;

	if (count > 8)
		return -EINVAL;

	err = kstrtou32(buf, 16, &pass);
	if (err)
		return err;

	be_pass = cpu_to_be32(pass);

	memcpy(output, &be_pass, sizeof(be_pass));
	output[4] = code;
	memcpy(output + 5, &be_pass, sizeof(be_pass));

	mutex_lock(&m24lr->lock);
	ret = m24lr_regmap_write(m24lr->ctl_regmap, output, 9, 2304);
	mutex_unlock(&m24lr->lock);

	return ret;
}

static ssize_t m24lr_read_reg_le(struct m24lr *m24lr, u64 *val,
				 unsigned int reg_addr,
				 unsigned int reg_size)
{
	ssize_t ret;
	__le64 input = 0;

	ret = m24lr_read(m24lr, (u8 *)&input, reg_size, reg_addr, false);
	if (IS_ERR_VALUE(ret))
		return ret;

	if (ret != reg_size)
		return -EINVAL;

	switch (reg_size) {
	case 1:
		*val = *(u8 *)&input;
		break;
	case 2:
		*val = le16_to_cpu((__le16)input);
		break;
	case 4:
		*val = le32_to_cpu((__le32)input);
		break;
	case 8:
		*val = le64_to_cpu((__le64)input);
		break;
	default:
		return -EINVAL;
	}

	return 0;
}

static int m24lr_nvmem_read(void *priv, unsigned int offset, void *val,
			    size_t bytes)
{
	ssize_t err;
	struct m24lr *m24lr = priv;

	if (!bytes)
		return bytes;

	if (offset + bytes > m24lr->eeprom_size)
		return -EINVAL;

	err = m24lr_read(m24lr, val, bytes, offset, true);
	if (IS_ERR_VALUE(err))
		return err;

	return 0;
}

static int m24lr_nvmem_write(void *priv, unsigned int offset, void *val,
			     size_t bytes)
{
	ssize_t err;
	struct m24lr *m24lr = priv;

	if (!bytes)
		return -EINVAL;

	if (offset + bytes > m24lr->eeprom_size)
		return -EINVAL;

	err = m24lr_write(m24lr, val, bytes, offset, true);
	if (IS_ERR_VALUE(err))
		return err;

	return 0;
}

static ssize_t m24lr_ctl_sss_read(struct file *filep, struct kobject *kobj,
				  const struct bin_attribute *attr, char *buf,
				  loff_t offset, size_t count)
{
	struct m24lr *m24lr = attr->private;

	if (!count)
		return count;

	if (size_add(offset, count) > m24lr->sss_len)
		return -EINVAL;

	return m24lr_read(m24lr, buf, count, offset, false);
}

static ssize_t m24lr_ctl_sss_write(struct file *filep, struct kobject *kobj,
				   const struct bin_attribute *attr, char *buf,
				   loff_t offset, size_t count)
{
	struct m24lr *m24lr = attr->private;

	if (!count)
		return -EINVAL;

	if (size_add(offset, count) > m24lr->sss_len)
		return -EINVAL;

	return m24lr_write(m24lr, buf, count, offset, false);
}
static BIN_ATTR(sss, 0600, m24lr_ctl_sss_read, m24lr_ctl_sss_write, 0);

static ssize_t new_pass_store(struct device *dev, struct device_attribute *attr,
			      const char *buf, size_t count)
{
	struct m24lr *m24lr = i2c_get_clientdata(to_i2c_client(dev));

	return m24lr_write_pass(m24lr, buf, count, 7);
}
static DEVICE_ATTR_WO(new_pass);

static ssize_t unlock_store(struct device *dev, struct device_attribute *attr,
			    const char *buf, size_t count)
{
	struct m24lr *m24lr = i2c_get_clientdata(to_i2c_client(dev));

	return m24lr_write_pass(m24lr, buf, count, 9);
}
static DEVICE_ATTR_WO(unlock);

static ssize_t uid_show(struct device *dev, struct device_attribute *attr,
			char *buf)
{
	struct m24lr *m24lr = i2c_get_clientdata(to_i2c_client(dev));

	return sysfs_emit(buf, "%llx\n", m24lr->uid);
}
static DEVICE_ATTR_RO(uid);

static ssize_t total_sectors_show(struct device *dev,
				  struct device_attribute *attr, char *buf)
{
	struct m24lr *m24lr = i2c_get_clientdata(to_i2c_client(dev));

	return sysfs_emit(buf, "%x\n", m24lr->sss_len);
}
static DEVICE_ATTR_RO(total_sectors);

static struct attribute *m24lr_ctl_dev_attrs[] = {
	&dev_attr_unlock.attr,
	&dev_attr_new_pass.attr,
	&dev_attr_uid.attr,
	&dev_attr_total_sectors.attr,
	NULL,
};

static const struct m24lr_chip *m24lr_get_chip(struct device *dev)
{
	const struct m24lr_chip *ret;
	const struct i2c_device_id *id;

	id = i2c_match_id(m24lr_ids, to_i2c_client(dev));

	if (dev->of_node && of_match_device(m24lr_of_match, dev))
		ret = of_device_get_match_data(dev);
	else if (id)
		ret = (void *)id->driver_data;
	else
		ret = acpi_device_get_match_data(dev);

	return ret;
}

static int m24lr_probe(struct i2c_client *client)
{
	struct regmap_config eeprom_regmap_conf = {0};
	struct nvmem_config nvmem_conf = {0};
	struct device *dev = &client->dev;
	struct i2c_client *eeprom_client;
	const struct m24lr_chip *chip;
	struct regmap *eeprom_regmap;
	struct nvmem_device *nvmem;
	struct regmap *ctl_regmap;
	struct m24lr *m24lr;
	u32 regs[2];
	long err;

	if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
		return -EOPNOTSUPP;

	chip = m24lr_get_chip(dev);
	if (!chip)
		return -ENODEV;

	m24lr = devm_kzalloc(dev, sizeof(struct m24lr), GFP_KERNEL);
	if (!m24lr)
		return -ENOMEM;

	err = device_property_read_u32_array(dev, "reg", regs, ARRAY_SIZE(regs));
	if (err)
		return dev_err_probe(dev, err, "Failed to read 'reg' property\n");

	/* Create a second I2C client for the eeprom interface */
	eeprom_client = devm_i2c_new_dummy_device(dev, client->adapter, regs[1]);
	if (IS_ERR(eeprom_client))
		return dev_err_probe(dev, PTR_ERR(eeprom_client),
				     "Failed to create dummy I2C client for the EEPROM\n");

	ctl_regmap = devm_regmap_init_i2c(client, &m24lr_ctl_regmap_conf);
	if (IS_ERR(ctl_regmap))
		return dev_err_probe(dev, PTR_ERR(ctl_regmap),
				      "Failed to init regmap\n");

	eeprom_regmap_conf.name = "m24lr_eeprom";
	eeprom_regmap_conf.reg_bits = 16;
	eeprom_regmap_conf.val_bits = 8;
	eeprom_regmap_conf.disable_locking = true;
	eeprom_regmap_conf.max_register = chip->eeprom_size - 1;

	eeprom_regmap = devm_regmap_init_i2c(eeprom_client,
					     &eeprom_regmap_conf);
	if (IS_ERR(eeprom_regmap))
		return dev_err_probe(dev, PTR_ERR(eeprom_regmap),
				     "Failed to init regmap\n");

	mutex_init(&m24lr->lock);
	m24lr->sss_len = chip->sss_len;
	m24lr->page_size = chip->page_size;
	m24lr->eeprom_size = chip->eeprom_size;
	m24lr->eeprom_regmap = eeprom_regmap;
	m24lr->ctl_regmap = ctl_regmap;

	nvmem_conf.dev = &eeprom_client->dev;
	nvmem_conf.owner = THIS_MODULE;
	nvmem_conf.type = NVMEM_TYPE_EEPROM;
	nvmem_conf.reg_read = m24lr_nvmem_read;
	nvmem_conf.reg_write = m24lr_nvmem_write;
	nvmem_conf.size = chip->eeprom_size;
	nvmem_conf.word_size = 1;
	nvmem_conf.stride = 1;
	nvmem_conf.priv = m24lr;

	nvmem = devm_nvmem_register(dev, &nvmem_conf);
	if (IS_ERR(nvmem))
		return dev_err_probe(dev, PTR_ERR(nvmem),
				     "Failed to register nvmem\n");

	i2c_set_clientdata(client, m24lr);
	i2c_set_clientdata(eeprom_client, m24lr);

	bin_attr_sss.size = chip->sss_len;
	bin_attr_sss.private = m24lr;
	err = sysfs_create_bin_file(&dev->kobj, &bin_attr_sss);
	if (err)
		return dev_err_probe(dev, err,
				     "Failed to create sss bin file\n");

	/* test by reading the uid, if success store it */
	err = m24lr_read_reg_le(m24lr, &m24lr->uid, 2324, sizeof(m24lr->uid));
	if (IS_ERR_VALUE(err))
		goto remove_bin_file;

	return 0;

remove_bin_file:
	sysfs_remove_bin_file(&dev->kobj, &bin_attr_sss);

	return err;
}

static void m24lr_remove(struct i2c_client *client)
{
	sysfs_remove_bin_file(&client->dev.kobj, &bin_attr_sss);
}

ATTRIBUTE_GROUPS(m24lr_ctl_dev);

static struct i2c_driver m24lr_driver = {
	.driver = {
		.name = "m24lr",
		.of_match_table = m24lr_of_match,
		.dev_groups = m24lr_ctl_dev_groups,
	},
	.probe	  = m24lr_probe,
	.remove = m24lr_remove,
	.id_table = m24lr_ids,
};
module_i2c_driver(m24lr_driver);

MODULE_AUTHOR("Abd-Alrhman Masalkhi");
MODULE_DESCRIPTION("st m24lr control driver");
MODULE_LICENSE("GPL");