]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
eeprom: add driver for ST M24LR series RFID/NFC EEPROM chips
authorAbd-Alrhman Masalkhi <abd.masalkhi@gmail.com>
Thu, 17 Jul 2025 06:39:33 +0000 (06:39 +0000)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Tue, 19 Aug 2025 10:51:22 +0000 (12:51 +0200)
adds support for STMicroelectronics M24LRxx devices, which expose
two separate I2C addresses: one for system control and one for EEPROM
access. The driver implements both a sysfs-based interface for control
registers (e.g. UID, password authentication) and an nvmem provider
for EEPROM access.

Signed-off-by: Abd-Alrhman Masalkhi <abd.masalkhi@gmail.com>
Link: https://lore.kernel.org/r/20250717063934.5083-3-abd.masalkhi@gmail.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/misc/eeprom/Kconfig
drivers/misc/eeprom/Makefile
drivers/misc/eeprom/m24lr.c [new file with mode: 0644]

index 0bef5b93bd6dcfec1185675a742f123559294f9a..4d0ce47aa282c12dc277661cae552cca1923ec0d 100644 (file)
@@ -120,4 +120,22 @@ config EEPROM_EE1004
          This driver can also be built as a module.  If so, the module
          will be called ee1004.
 
+config EEPROM_M24LR
+       tristate "STMicroelectronics M24LR RFID/NFC EEPROM support"
+       depends on I2C && SYSFS
+       select REGMAP_I2C
+       select NVMEM
+       select NVMEM_SYSFS
+       help
+         This enables support for STMicroelectronics M24LR RFID/NFC EEPROM
+         chips. These dual-interface devices expose two I2C addresses:
+         one for EEPROM memory access and another for control and system
+         configuration (e.g. UID, password handling).
+
+         This driver provides a sysfs interface for control functions and
+         integrates with the nvmem subsystem for EEPROM access.
+
+         To compile this driver as a module, choose M here: the
+         module will be called m24lr.
+
 endmenu
index 65794e526d5d9da56cbf52044444b8e6317bd783..8f311fd6a4ce18bd621995811fcf3e971b0988cb 100644 (file)
@@ -7,3 +7,4 @@ obj-$(CONFIG_EEPROM_93XX46)     += eeprom_93xx46.o
 obj-$(CONFIG_EEPROM_DIGSY_MTC_CFG) += digsy_mtc_eeprom.o
 obj-$(CONFIG_EEPROM_IDT_89HPESX) += idt_89hpesx.o
 obj-$(CONFIG_EEPROM_EE1004)    += ee1004.o
+obj-$(CONFIG_EEPROM_M24LR) += m24lr.o
diff --git a/drivers/misc/eeprom/m24lr.c b/drivers/misc/eeprom/m24lr.c
new file mode 100644 (file)
index 0000000..0e37f74
--- /dev/null
@@ -0,0 +1,606 @@
+// 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");