mirror of
https://github.com/torvalds/linux.git
synced 2025-12-01 07:26:02 +07:00
Remove unnecessary semicolons reported by Coccinelle/coccicheck and the semantic patch at scripts/coccinelle/misc/semicolon.cocci. Signed-off-by: Chen Ni <nichen@iscas.ac.cn> Reviewed-by: Abd-Alrhman Masalkhi <abd.masalkhi@gmail.com> Link: https://lore.kernel.org/r/20250821081804.3168680-1-nichen@iscas.ac.cn Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
607 lines
16 KiB
C
607 lines
16 KiB
C
// 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");
|