blob: 7a9fd45a8e460c48dfb33dd34f5e7c654b9b6a6a [file] [log] [blame]
// 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");