From 835d1cd4c586426fa796e4e5fa726d44670f0e56 Mon Sep 17 00:00:00 2001 From: Heinrich Toews Date: Mon, 29 Sep 2025 17:31:10 +0200 Subject: [PATCH] nfc: tag: add NFC tag device st25dvxxk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- drivers/nfc/Kconfig | 1 + drivers/nfc/Makefile | 1 + drivers/nfc/tag/Kconfig | 5 + drivers/nfc/tag/Makefile | 6 + drivers/nfc/tag/st25dv/Kconfig | 6 + drivers/nfc/tag/st25dv/Makefile | 7 + drivers/nfc/tag/st25dv/st25dv.c | 443 ++++++++++++++++++++++++++++++++ 7 files changed, 469 insertions(+) create mode 100644 drivers/nfc/tag/Kconfig create mode 100644 drivers/nfc/tag/Makefile create mode 100644 drivers/nfc/tag/st25dv/Kconfig create mode 100644 drivers/nfc/tag/st25dv/Makefile create mode 100644 drivers/nfc/tag/st25dv/st25dv.c diff --git a/drivers/nfc/Kconfig b/drivers/nfc/Kconfig index 288c6f1c6979..b27791e4c8f1 100644 --- a/drivers/nfc/Kconfig +++ b/drivers/nfc/Kconfig @@ -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 diff --git a/drivers/nfc/Makefile b/drivers/nfc/Makefile index 7b1bfde1d971..fd13c4647899 100644 --- a/drivers/nfc/Makefile +++ b/drivers/nfc/Makefile @@ -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/ diff --git a/drivers/nfc/tag/Kconfig b/drivers/nfc/tag/Kconfig new file mode 100644 index 000000000000..2ea2aa35dade --- /dev/null +++ b/drivers/nfc/tag/Kconfig @@ -0,0 +1,5 @@ +menu "NFC Tag devices" + depends on NFC + +source "drivers/nfc/tag/st25dv/Kconfig" +endmenu diff --git a/drivers/nfc/tag/Makefile b/drivers/nfc/tag/Makefile new file mode 100644 index 000000000000..460fe87e23ea --- /dev/null +++ b/drivers/nfc/tag/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for nfc tag devices +# + +obj-$(CONFIG_NFC_TAG_ST25DV) += st25dv/ diff --git a/drivers/nfc/tag/st25dv/Kconfig b/drivers/nfc/tag/st25dv/Kconfig new file mode 100644 index 000000000000..b20d948fc5f6 --- /dev/null +++ b/drivers/nfc/tag/st25dv/Kconfig @@ -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. diff --git a/drivers/nfc/tag/st25dv/Makefile b/drivers/nfc/tag/st25dv/Makefile new file mode 100644 index 000000000000..b1d01dc63dfa --- /dev/null +++ b/drivers/nfc/tag/st25dv/Makefile @@ -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 diff --git a/drivers/nfc/tag/st25dv/st25dv.c b/drivers/nfc/tag/st25dv/st25dv.c new file mode 100644 index 000000000000..c06425d88168 --- /dev/null +++ b/drivers/nfc/tag/st25dv/st25dv.c @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2018 Loïc Boban + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "); +MODULE_DESCRIPTION("nfc/i2c eeprom st25dv driver"); +MODULE_LICENSE("GPL");