nfc: tag: add NFC tag device st25dvxxk

This driver is actually an external kernel module that was imported
into the kernel tree.

You can find the original driver of Boban Loïc at

https://github.com/2pecshy/eeprom-ST25DV-linux-driver.git

Changes done by Fa. WAGO:

- refactor the probe and detect functions to align with
  modern Linux kernel best practices:

- Switch to Managed Resources: Replaced standard kmalloc/kzalloc with
  devm_kmalloc/devm_kzalloc for all data structures and buffers.
  This simplifies the probe function and eliminates the need for
  most memory-related goto labels and manual kfree calls.
- Automatic Device Management: Integrated devm_add_action_or_reset
  to handle the automatic unregistering of the dummy I2C client,
  ensuring safer resource release during error paths and removal.
- Modernized Logging: Replaced generic pr_warn/pr_info calls with
  device-specific dev_err, dev_info, and dev_dbg for better
  traceability in systems with multiple I2C devices.
- Improved String Handling: Replaced the deprecated strlcpy with
  the safer strscpy in the detection phase.
- Enhanced Status Reporting: Added detailed dev_info upon successful
  probe, including the specific chip model name and detected memory
  capacity.
- Coding Style Fixes: Standardized indentation, removed unnecessary
  braces for single-statement blocks, and structured area-specific
  initializations to improve readability and maintainability.

The driver was tested and seems to be working fine.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
This commit is contained in:
Heinrich Toews
2025-09-29 17:31:10 +02:00
parent 4f6af6494f
commit 835d1cd4c5
7 changed files with 469 additions and 0 deletions
+1
View File
@@ -70,4 +70,5 @@ source "drivers/nfc/st-nci/Kconfig"
source "drivers/nfc/nxp-nci/Kconfig"
source "drivers/nfc/s3fwrn5/Kconfig"
source "drivers/nfc/st95hf/Kconfig"
source "drivers/nfc/tag/Kconfig"
endmenu
+1
View File
@@ -18,3 +18,4 @@ obj-$(CONFIG_NFC_NXP_NCI) += nxp-nci/
obj-$(CONFIG_NFC_S3FWRN5) += s3fwrn5/
obj-$(CONFIG_NFC_ST95HF) += st95hf/
obj-$(CONFIG_NFC_VIRTUAL_NCI) += virtual_ncidev.o
obj-y += tag/
+5
View File
@@ -0,0 +1,5 @@
menu "NFC Tag devices"
depends on NFC
source "drivers/nfc/tag/st25dv/Kconfig"
endmenu
+6
View File
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for nfc tag devices
#
obj-$(CONFIG_NFC_TAG_ST25DV) += st25dv/
+6
View File
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
config NFC_TAG_ST25DV
tristate "STMicroelectronics ST25DVxxK nfc tag driver"
depends on I2C
help
This is the STMicroelectronics NFC Tag driver for the ST25DVxxK devices.
+7
View File
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Makefile for ST25DV NFC driver
#
# st25dv-y += st25dv.o
obj-$(CONFIG_NFC_TAG_ST25DV) += st25dv.o
+443
View File
@@ -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");