|
|
|
@@ -0,0 +1,443 @@
|
|
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
|
/*
|
|
|
|
|
* Copyright (C) 2018 Loïc Boban <loic.boban@gmail.com>
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <linux/module.h>
|
|
|
|
|
#include <linux/kernel.h>
|
|
|
|
|
#include <linux/slab.h>
|
|
|
|
|
#include <linux/device.h>
|
|
|
|
|
#include <linux/i2c.h>
|
|
|
|
|
#include <linux/uaccess.h>
|
|
|
|
|
#include <linux/delay.h>
|
|
|
|
|
#include <linux/mutex.h>
|
|
|
|
|
|
|
|
|
|
#define DYN_REG_SIZE 0x8
|
|
|
|
|
#define SYS_MEM_SIZE 0x24
|
|
|
|
|
#define USER_MEM_SIZE 0x200
|
|
|
|
|
#define MAILBOX_MEM_SIZE 0x100
|
|
|
|
|
#define SYS_ADDR 0x57
|
|
|
|
|
#define USER_ADDR 0x53
|
|
|
|
|
#define PWD_OFF 0x0900
|
|
|
|
|
#define DYN_REG_OFF 0x2000
|
|
|
|
|
#define MAILBOX_OFF 0x2008
|
|
|
|
|
#define MAX_TRY 10
|
|
|
|
|
|
|
|
|
|
#define PWD_REQ_SIZE 0x13
|
|
|
|
|
#define PWD_SIZE 0x08
|
|
|
|
|
#define CMD_PRESENT_PWD 0x09
|
|
|
|
|
#define CMD_WRITE_PWD 0x07
|
|
|
|
|
#define PWD_CMD_POS 10
|
|
|
|
|
#define PWD1_POS 2
|
|
|
|
|
#define PWD2_POS 11
|
|
|
|
|
|
|
|
|
|
#define MEM_04K 512
|
|
|
|
|
#define MEM_16K 2000
|
|
|
|
|
#define MEM_64K 8000
|
|
|
|
|
|
|
|
|
|
static const struct bin_attribute st25dv_p_pwd_attr;
|
|
|
|
|
|
|
|
|
|
enum area_type {
|
|
|
|
|
USER_AREA = 0,
|
|
|
|
|
SYS_AREA = 1,
|
|
|
|
|
DYN_REG_AREA = 2,
|
|
|
|
|
MAILBOX_AREA = 3,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The ST25DV EEPROM has two areas: the user area and the system
|
|
|
|
|
* area to manage read/write protection for the NFC interface and
|
|
|
|
|
* I2C interface. To drive the system area, a dummy i2c_client is used.
|
|
|
|
|
*/
|
|
|
|
|
static int mem_config[] = {MEM_04K, MEM_04K, MEM_16K, MEM_64K};
|
|
|
|
|
static int area_off[] = {0, 0, DYN_REG_OFF, MAILBOX_OFF};
|
|
|
|
|
|
|
|
|
|
/* one struct is used for each area */
|
|
|
|
|
struct st25dv_data {
|
|
|
|
|
u8 *data; /* area data */
|
|
|
|
|
enum area_type type;
|
|
|
|
|
struct bin_attribute bin_attr;
|
|
|
|
|
struct i2c_client *client;
|
|
|
|
|
struct mutex *update_lock; /* protect for concurrent updates */
|
|
|
|
|
struct st25dv_data *next;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Addresses to scan */
|
|
|
|
|
static const unsigned short normal_i2c[] = { USER_ADDR, SYS_ADDR, I2C_CLIENT_END };
|
|
|
|
|
|
|
|
|
|
static ssize_t st25dv_read(struct file *filp, struct kobject *kobj,
|
|
|
|
|
struct bin_attribute *bin_attr, char *buf, loff_t off, size_t count)
|
|
|
|
|
{
|
|
|
|
|
int r_size, nack;
|
|
|
|
|
struct i2c_client *client = to_i2c_client(kobj_to_dev(kobj));
|
|
|
|
|
struct st25dv_data *data = i2c_get_clientdata(client);
|
|
|
|
|
u16 cur_off;
|
|
|
|
|
|
|
|
|
|
while (&data->bin_attr != bin_attr)
|
|
|
|
|
data = data->next;
|
|
|
|
|
|
|
|
|
|
off += area_off[data->type];
|
|
|
|
|
mutex_lock(data->update_lock);
|
|
|
|
|
for (cur_off = off; cur_off-off < count; cur_off++) {
|
|
|
|
|
nack = 0;
|
|
|
|
|
retry_:
|
|
|
|
|
if (nack > MAX_TRY) {
|
|
|
|
|
mutex_unlock(data->update_lock);
|
|
|
|
|
return r_size;
|
|
|
|
|
}
|
|
|
|
|
r_size = i2c_smbus_write_byte_data(client, (cur_off >> 8) & 0x0ff, cur_off & 0x0ff);
|
|
|
|
|
if (r_size < 0) {
|
|
|
|
|
nack++;
|
|
|
|
|
udelay(150);
|
|
|
|
|
goto retry_;
|
|
|
|
|
}
|
|
|
|
|
r_size = i2c_smbus_read_byte(client);
|
|
|
|
|
if (r_size < 0) {
|
|
|
|
|
nack++;
|
|
|
|
|
udelay(150);
|
|
|
|
|
goto retry_;
|
|
|
|
|
}
|
|
|
|
|
data->data[cur_off] = r_size;
|
|
|
|
|
}
|
|
|
|
|
memcpy(buf, &data->data[off], count);
|
|
|
|
|
mutex_unlock(data->update_lock);
|
|
|
|
|
|
|
|
|
|
dev_dbg(&client->dev, "%zu bytes read\n", count);
|
|
|
|
|
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* I2C_SMBUS_BLOCK_MAX = 9 page writes
|
|
|
|
|
* MAX tw = 9 * 5ms
|
|
|
|
|
* write_block is faster than write single byte but not supported by some adapter
|
|
|
|
|
*/
|
|
|
|
|
static ssize_t st25dv_write_block(struct file *filp, struct kobject *kobj,
|
|
|
|
|
struct bin_attribute *bin_attr, char *buf, loff_t off, size_t count)
|
|
|
|
|
{
|
|
|
|
|
int r_size, to_write, not_write;
|
|
|
|
|
struct i2c_client *client = to_i2c_client(kobj_to_dev(kobj));
|
|
|
|
|
struct st25dv_data *data = i2c_get_clientdata(client);
|
|
|
|
|
u16 cur_off, cur_buf_off;
|
|
|
|
|
u8 nack, tmp[I2C_SMBUS_BLOCK_MAX];
|
|
|
|
|
|
|
|
|
|
while (&data->bin_attr != bin_attr)
|
|
|
|
|
data = data->next;
|
|
|
|
|
|
|
|
|
|
off += area_off[data->type];
|
|
|
|
|
mutex_lock(data->update_lock);
|
|
|
|
|
memcpy(data->data + off, buf, count);
|
|
|
|
|
not_write = count;
|
|
|
|
|
cur_off = off;
|
|
|
|
|
cur_buf_off = 0;
|
|
|
|
|
while (not_write) {
|
|
|
|
|
nack = 0;
|
|
|
|
|
to_write = not_write > I2C_SMBUS_BLOCK_MAX-2 ? I2C_SMBUS_BLOCK_MAX-2 : not_write;
|
|
|
|
|
not_write -= to_write;
|
|
|
|
|
tmp[1] = cur_off;
|
|
|
|
|
tmp[0] = cur_off >> 8;
|
|
|
|
|
memcpy(tmp + 2, buf + cur_buf_off, to_write);
|
|
|
|
|
retry_:
|
|
|
|
|
if (nack > MAX_TRY) {
|
|
|
|
|
mutex_unlock(data->update_lock);
|
|
|
|
|
return r_size;
|
|
|
|
|
}
|
|
|
|
|
r_size = i2c_master_send(client, tmp, to_write+2);
|
|
|
|
|
if (r_size < 0) {
|
|
|
|
|
nack++;
|
|
|
|
|
mdelay(5);
|
|
|
|
|
goto retry_;
|
|
|
|
|
}
|
|
|
|
|
mdelay(20);
|
|
|
|
|
cur_off += to_write;
|
|
|
|
|
cur_buf_off += to_write;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mutex_unlock(data->update_lock);
|
|
|
|
|
dev_dbg(&client->dev, "%zu bytes written\n", count);
|
|
|
|
|
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static ssize_t st25dv_send_pwd_req(struct file *filp, struct kobject *kobj,
|
|
|
|
|
struct bin_attribute *bin_attr,
|
|
|
|
|
char *buf, loff_t off, size_t count)
|
|
|
|
|
{
|
|
|
|
|
u8 pwd_req[PWD_REQ_SIZE], cmd, *pwd_ptr1, *pwd_ptr2;
|
|
|
|
|
int r_size, off_tmp, nack;
|
|
|
|
|
struct i2c_client *client = to_i2c_client(kobj_to_dev(kobj));
|
|
|
|
|
struct st25dv_data *data = i2c_get_clientdata(client);
|
|
|
|
|
|
|
|
|
|
cmd = bin_attr == &st25dv_p_pwd_attr ? CMD_PRESENT_PWD : CMD_WRITE_PWD;
|
|
|
|
|
if (count != PWD_SIZE) {
|
|
|
|
|
dev_err(&client->dev, "send pwd cmd fail: count=%d\n", (int)count);
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
nack = 0;
|
|
|
|
|
pwd_req[0] = 0x09;
|
|
|
|
|
pwd_req[1] = 0x00;
|
|
|
|
|
pwd_ptr1 = &pwd_req[PWD1_POS];
|
|
|
|
|
pwd_ptr2 = &pwd_req[PWD2_POS];
|
|
|
|
|
pwd_req[PWD_CMD_POS] = cmd;
|
|
|
|
|
for (off_tmp = PWD_SIZE-1; off_tmp >= 0; off_tmp--) {
|
|
|
|
|
*pwd_ptr1 = buf[off_tmp];
|
|
|
|
|
*pwd_ptr2 = buf[off_tmp];
|
|
|
|
|
pwd_ptr1++;
|
|
|
|
|
pwd_ptr2++;
|
|
|
|
|
}
|
|
|
|
|
mutex_lock(data->update_lock);
|
|
|
|
|
retry_:
|
|
|
|
|
if (nack > MAX_TRY) {
|
|
|
|
|
mutex_unlock(data->update_lock);
|
|
|
|
|
return r_size;
|
|
|
|
|
}
|
|
|
|
|
r_size = i2c_master_send(client, pwd_req, PWD_REQ_SIZE);
|
|
|
|
|
if (r_size < 0) {
|
|
|
|
|
nack++;
|
|
|
|
|
mdelay(5);
|
|
|
|
|
goto retry_;
|
|
|
|
|
}
|
|
|
|
|
mutex_unlock(data->update_lock);
|
|
|
|
|
|
|
|
|
|
dev_info(&client->dev, "%s password command successful\n",
|
|
|
|
|
(cmd == CMD_PRESENT_PWD) ? "Presentation" : "Write");
|
|
|
|
|
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct bin_attribute st25dv_user_attr = {
|
|
|
|
|
.attr = { .name = "st25dv_user", .mode = 0666 },
|
|
|
|
|
.size = USER_MEM_SIZE,
|
|
|
|
|
.read = st25dv_read,
|
|
|
|
|
.write = st25dv_write_block,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const struct bin_attribute st25dv_sys_attr = {
|
|
|
|
|
.attr = { .name = "st25dv_sys", .mode = 0644 },
|
|
|
|
|
.size = SYS_MEM_SIZE,
|
|
|
|
|
.read = st25dv_read,
|
|
|
|
|
.write = st25dv_write_block,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const struct bin_attribute st25dv_dyn_reg_attr = {
|
|
|
|
|
.attr = { .name = "st25dv_dyn_reg", .mode = 0644 },
|
|
|
|
|
.size = DYN_REG_SIZE,
|
|
|
|
|
.read = st25dv_read,
|
|
|
|
|
.write = st25dv_write_block,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const struct bin_attribute st25dv_mailbox_attr = {
|
|
|
|
|
.attr = { .name = "st25dv_mailbox", .mode = 0666 },
|
|
|
|
|
.size = MAILBOX_MEM_SIZE,
|
|
|
|
|
.read = st25dv_read,
|
|
|
|
|
.write = st25dv_write_block,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const struct bin_attribute st25dv_w_pwd_attr = {
|
|
|
|
|
.attr = { .name = "st25dv_write_pwd", .mode = 0200 },
|
|
|
|
|
.size = PWD_SIZE,
|
|
|
|
|
.write = st25dv_send_pwd_req,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const struct bin_attribute st25dv_p_pwd_attr = {
|
|
|
|
|
.attr = { .name = "st25dv_present_pwd", .mode = 0200 },
|
|
|
|
|
.size = PWD_SIZE,
|
|
|
|
|
.write = st25dv_send_pwd_req,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Return 0 if detection is successful, -ENODEV otherwise */
|
|
|
|
|
static int st25dv_detect(struct i2c_client *client, struct i2c_board_info *info)
|
|
|
|
|
{
|
|
|
|
|
struct i2c_adapter *adapter = client->adapter;
|
|
|
|
|
|
|
|
|
|
if (!(adapter->class & I2C_CLASS_SPD) && (client->addr != USER_ADDR))
|
|
|
|
|
return -ENODEV;
|
|
|
|
|
|
|
|
|
|
if (!i2c_new_dummy_device(client->adapter, SYS_ADDR))
|
|
|
|
|
return -ENODEV;
|
|
|
|
|
|
|
|
|
|
if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_READ_WORD_DATA)
|
|
|
|
|
&& !i2c_check_functionality(adapter, I2C_FUNC_SMBUS_READ_I2C_BLOCK)) {
|
|
|
|
|
dev_err(&client->dev, "ST25DV detected at 0x%02x but lacks required functionality\n",
|
|
|
|
|
client->addr);
|
|
|
|
|
return -ENODEV;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dev_info(&client->dev, "ST25DV EEPROM detected at 0x%02x\n", client->addr);
|
|
|
|
|
|
|
|
|
|
strscpy(info->type, "st25dv", I2C_NAME_SIZE);
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int st25dv_probe(struct i2c_client *client)
|
|
|
|
|
{
|
|
|
|
|
const struct i2c_device_id *id = i2c_client_get_device_id(client);
|
|
|
|
|
struct st25dv_data *data, *sys_data, *dyn_reg_data, *mailbox_data;
|
|
|
|
|
struct i2c_client *client_sys_area;
|
|
|
|
|
struct mutex *st25dv_lock;
|
|
|
|
|
int status;
|
|
|
|
|
|
|
|
|
|
/* Dummy client for the system area at addr 0x57 */
|
|
|
|
|
client_sys_area = i2c_new_dummy_device(client->adapter, SYS_ADDR);
|
|
|
|
|
if (!client_sys_area) {
|
|
|
|
|
dev_err(&client->dev, "st25dv sys eeprom not detected\n");
|
|
|
|
|
return -ENODEV;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dev_set_name(&client_sys_area->dev, "st25dv-sys-%d-%04x",
|
|
|
|
|
i2c_adapter_id(client->adapter), client_sys_area->addr);
|
|
|
|
|
|
|
|
|
|
/* Register action to unregister dummy device automatically */
|
|
|
|
|
status = devm_add_action_or_reset(&client->dev,
|
|
|
|
|
(void(*)(void *))i2c_unregister_device,
|
|
|
|
|
client_sys_area);
|
|
|
|
|
if (status)
|
|
|
|
|
return status;
|
|
|
|
|
|
|
|
|
|
/* Allocate lock and data structures */
|
|
|
|
|
st25dv_lock = devm_kzalloc(&client->dev, sizeof(*st25dv_lock), GFP_KERNEL);
|
|
|
|
|
data = devm_kmalloc(&client->dev, sizeof(*data), GFP_KERNEL);
|
|
|
|
|
sys_data = devm_kmalloc(&client_sys_area->dev, sizeof(*sys_data), GFP_KERNEL);
|
|
|
|
|
dyn_reg_data = devm_kmalloc(&client->dev, sizeof(*dyn_reg_data), GFP_KERNEL);
|
|
|
|
|
mailbox_data = devm_kmalloc(&client->dev, sizeof(*mailbox_data), GFP_KERNEL);
|
|
|
|
|
|
|
|
|
|
if (!st25dv_lock || !data || !sys_data || !dyn_reg_data || !mailbox_data)
|
|
|
|
|
return -ENOMEM;
|
|
|
|
|
|
|
|
|
|
mutex_init(st25dv_lock);
|
|
|
|
|
|
|
|
|
|
/* Allocate and initialize buffers with 0xff */
|
|
|
|
|
data->data = devm_kmalloc(&client->dev, mem_config[id->driver_data], GFP_KERNEL);
|
|
|
|
|
sys_data->data = devm_kmalloc(&client_sys_area->dev, SYS_MEM_SIZE, GFP_KERNEL);
|
|
|
|
|
dyn_reg_data->data = devm_kmalloc(&client->dev, DYN_REG_SIZE, GFP_KERNEL);
|
|
|
|
|
mailbox_data->data = devm_kmalloc(&client->dev, MAILBOX_MEM_SIZE, GFP_KERNEL);
|
|
|
|
|
|
|
|
|
|
if (!data->data || !sys_data->data || !dyn_reg_data->data || !mailbox_data->data)
|
|
|
|
|
return -ENOMEM;
|
|
|
|
|
|
|
|
|
|
memset(data->data, 0xff, mem_config[id->driver_data]);
|
|
|
|
|
memset(sys_data->data, 0xff, SYS_MEM_SIZE);
|
|
|
|
|
memset(dyn_reg_data->data, 0xff, DYN_REG_SIZE);
|
|
|
|
|
memset(mailbox_data->data, 0xff, MAILBOX_MEM_SIZE);
|
|
|
|
|
|
|
|
|
|
/* Setup User Area */
|
|
|
|
|
data->client = client;
|
|
|
|
|
data->update_lock = st25dv_lock;
|
|
|
|
|
data->next = dyn_reg_data;
|
|
|
|
|
data->type = USER_AREA;
|
|
|
|
|
memcpy(&data->bin_attr, &st25dv_user_attr, sizeof(struct bin_attribute));
|
|
|
|
|
data->bin_attr.size = mem_config[id->driver_data];
|
|
|
|
|
|
|
|
|
|
/* Setup Dynamic Registers Area */
|
|
|
|
|
dyn_reg_data->client = client;
|
|
|
|
|
dyn_reg_data->update_lock = st25dv_lock;
|
|
|
|
|
dyn_reg_data->next = mailbox_data;
|
|
|
|
|
dyn_reg_data->type = DYN_REG_AREA;
|
|
|
|
|
memcpy(&dyn_reg_data->bin_attr, &st25dv_dyn_reg_attr, sizeof(struct bin_attribute));
|
|
|
|
|
|
|
|
|
|
/* Setup Mailbox Area */
|
|
|
|
|
mailbox_data->client = client;
|
|
|
|
|
mailbox_data->update_lock = st25dv_lock;
|
|
|
|
|
mailbox_data->next = sys_data;
|
|
|
|
|
mailbox_data->type = MAILBOX_AREA;
|
|
|
|
|
memcpy(&mailbox_data->bin_attr, &st25dv_mailbox_attr, sizeof(struct bin_attribute));
|
|
|
|
|
|
|
|
|
|
/* Setup System Area */
|
|
|
|
|
sys_data->client = client_sys_area;
|
|
|
|
|
sys_data->update_lock = st25dv_lock;
|
|
|
|
|
sys_data->next = data;
|
|
|
|
|
sys_data->type = SYS_AREA;
|
|
|
|
|
memcpy(&sys_data->bin_attr, &st25dv_sys_attr, sizeof(struct bin_attribute));
|
|
|
|
|
|
|
|
|
|
i2c_set_clientdata(client, data);
|
|
|
|
|
i2c_set_clientdata(client_sys_area, sys_data);
|
|
|
|
|
|
|
|
|
|
/* Create sysfs files - labels only for sysfs cleanup */
|
|
|
|
|
status = sysfs_create_bin_file(&client->dev.kobj, &data->bin_attr);
|
|
|
|
|
if (status < 0) goto err_sysfs;
|
|
|
|
|
status = sysfs_create_bin_file(&client_sys_area->dev.kobj, &sys_data->bin_attr);
|
|
|
|
|
if (status < 0) goto err_sysfs4;
|
|
|
|
|
status = sysfs_create_bin_file(&client->dev.kobj, &dyn_reg_data->bin_attr);
|
|
|
|
|
if (status < 0) goto err_sysfs3;
|
|
|
|
|
status = sysfs_create_bin_file(&client_sys_area->dev.kobj, &st25dv_w_pwd_attr);
|
|
|
|
|
if (status < 0) goto err_sysfs2;
|
|
|
|
|
status = sysfs_create_bin_file(&client_sys_area->dev.kobj, &st25dv_p_pwd_attr);
|
|
|
|
|
if (status < 0) goto err_sysfs1;
|
|
|
|
|
status = sysfs_create_bin_file(&client->dev.kobj, &mailbox_data->bin_attr);
|
|
|
|
|
if (status < 0) goto err_sysfs0;
|
|
|
|
|
|
|
|
|
|
dev_info(&client->dev, "ST25DV %s detected, %d bytes user memory\n",
|
|
|
|
|
id->name, mem_config[id->driver_data]);
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
err_sysfs0:
|
|
|
|
|
sysfs_remove_bin_file(&client_sys_area->dev.kobj, &st25dv_p_pwd_attr);
|
|
|
|
|
err_sysfs1:
|
|
|
|
|
sysfs_remove_bin_file(&client_sys_area->dev.kobj, &st25dv_w_pwd_attr);
|
|
|
|
|
err_sysfs2:
|
|
|
|
|
sysfs_remove_bin_file(&client->dev.kobj, &dyn_reg_data->bin_attr);
|
|
|
|
|
err_sysfs3:
|
|
|
|
|
sysfs_remove_bin_file(&client_sys_area->dev.kobj, &sys_data->bin_attr);
|
|
|
|
|
err_sysfs4:
|
|
|
|
|
sysfs_remove_bin_file(&client->dev.kobj, &data->bin_attr);
|
|
|
|
|
err_sysfs:
|
|
|
|
|
dev_err(&client->dev, "failed to create bin file: %d\n", status);
|
|
|
|
|
|
|
|
|
|
return status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void st25dv_remove(struct i2c_client *client)
|
|
|
|
|
{
|
|
|
|
|
struct st25dv_data *data = i2c_get_clientdata(client);
|
|
|
|
|
struct st25dv_data *tmp = data;
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
if (tmp->type == SYS_AREA) {
|
|
|
|
|
sysfs_remove_bin_file(&tmp->client->dev.kobj, &st25dv_p_pwd_attr);
|
|
|
|
|
sysfs_remove_bin_file(&tmp->client->dev.kobj, &st25dv_w_pwd_attr);
|
|
|
|
|
}
|
|
|
|
|
sysfs_remove_bin_file(&tmp->client->dev.kobj, &tmp->bin_attr);
|
|
|
|
|
tmp = tmp->next;
|
|
|
|
|
} while (tmp != data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const struct i2c_device_id st25dv_id[] = {
|
|
|
|
|
{ "st25dv", 0 },
|
|
|
|
|
{ "st25dv04k", 1 },
|
|
|
|
|
{ "st25dv16k", 2 },
|
|
|
|
|
{ "st25dv64k", 3 },
|
|
|
|
|
{ }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static struct i2c_driver st25dv_driver = {
|
|
|
|
|
.driver = {
|
|
|
|
|
.name = "st25dv",
|
|
|
|
|
},
|
|
|
|
|
.probe = st25dv_probe,
|
|
|
|
|
.remove = st25dv_remove,
|
|
|
|
|
.id_table = st25dv_id,
|
|
|
|
|
|
|
|
|
|
.class = I2C_CLASS_DDC | I2C_CLASS_SPD,
|
|
|
|
|
.detect = st25dv_detect,
|
|
|
|
|
.address_list = normal_i2c,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static int __init st25dv_i2c_init_driver(void)
|
|
|
|
|
{
|
|
|
|
|
return i2c_add_driver(&st25dv_driver);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void __exit st25dv_i2c_exit_driver(void)
|
|
|
|
|
{
|
|
|
|
|
i2c_del_driver(&st25dv_driver);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module_init(st25dv_i2c_init_driver);
|
|
|
|
|
module_exit(st25dv_i2c_exit_driver);
|
|
|
|
|
|
|
|
|
|
MODULE_INFO(intree, "y");
|
|
|
|
|
MODULE_AUTHOR("Loïc Boban <loic.boban@gmail.com>");
|
|
|
|
|
MODULE_DESCRIPTION("nfc/i2c eeprom st25dv driver");
|
|
|
|
|
MODULE_LICENSE("GPL");
|