Add driver for Samsung S3FWRN82 NFC controller. S3FWRN82 is using NCI protocol and I2C communication interface. Signed-off-by: Bongsu Jeon <bongsu.jeon@xxxxxxxxxxx> --- drivers/nfc/Kconfig | 1 + drivers/nfc/Makefile | 1 + drivers/nfc/s3fwrn82/Kconfig | 15 ++ drivers/nfc/s3fwrn82/Makefile | 10 ++ drivers/nfc/s3fwrn82/core.c | 133 +++++++++++++++ drivers/nfc/s3fwrn82/i2c.c | 288 ++++++++++++++++++++++++++++++++ drivers/nfc/s3fwrn82/s3fwrn82.h | 86 ++++++++++ 7 files changed, 534 insertions(+) create mode 100644 drivers/nfc/s3fwrn82/Kconfig create mode 100644 drivers/nfc/s3fwrn82/Makefile create mode 100644 drivers/nfc/s3fwrn82/core.c create mode 100644 drivers/nfc/s3fwrn82/i2c.c create mode 100644 drivers/nfc/s3fwrn82/s3fwrn82.h diff --git a/drivers/nfc/Kconfig b/drivers/nfc/Kconfig index 75c65d339018..102654909d3a 100644 --- a/drivers/nfc/Kconfig +++ b/drivers/nfc/Kconfig @@ -59,4 +59,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/s3fwrn82/Kconfig" endmenu diff --git a/drivers/nfc/Makefile b/drivers/nfc/Makefile index 5393ba59b17d..518d83301ad2 100644 --- a/drivers/nfc/Makefile +++ b/drivers/nfc/Makefile @@ -17,3 +17,4 @@ obj-$(CONFIG_NFC_ST_NCI) += st-nci/ obj-$(CONFIG_NFC_NXP_NCI) += nxp-nci/ obj-$(CONFIG_NFC_S3FWRN5) += s3fwrn5/ obj-$(CONFIG_NFC_ST95HF) += st95hf/ +obj-$(CONFIG_NFC_S3FWRN82) += s3fwrn82/ diff --git a/drivers/nfc/s3fwrn82/Kconfig b/drivers/nfc/s3fwrn82/Kconfig new file mode 100644 index 000000000000..afbad659cf07 --- /dev/null +++ b/drivers/nfc/s3fwrn82/Kconfig @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-only +config NFC_S3FWRN82 + tristate + +config NFC_S3FWRN82_I2C + tristate "Samsung S3FWRN82 I2C support" + depends on NFC_NCI && I2C + select NFC_S3FWRN82 + help + This module adds support for an I2C interface to the S3FWRN82 chip. + Select this if your platform is using the I2C bus. + + To compile this driver as a module, choose m here. The module will + be called s3fwrn82_i2c.ko. + Say N if unsure. diff --git a/drivers/nfc/s3fwrn82/Makefile b/drivers/nfc/s3fwrn82/Makefile new file mode 100644 index 000000000000..198e2cd85e91 --- /dev/null +++ b/drivers/nfc/s3fwrn82/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Makefile for Samsung S3FWRN82 NFC driver +# + +s3fwrn82-objs = core.o +s3fwrn82_i2c-objs = i2c.o + +obj-$(CONFIG_NFC_S3FWRN82) += s3fwrn82.o +obj-$(CONFIG_NFC_S3FWRN82_I2C) += s3fwrn82_i2c.o diff --git a/drivers/nfc/s3fwrn82/core.c b/drivers/nfc/s3fwrn82/core.c new file mode 100644 index 000000000000..e01e02c758ab --- /dev/null +++ b/drivers/nfc/s3fwrn82/core.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * NCI based driver for Samsung S3FWRN82 NFC chip + * + * Copyright (C) 2020 Samsung Electrnoics + * Bongsu Jeon <bongsu.jeon@xxxxxxxxxxx> + */ + +#include <linux/module.h> +#include <net/nfc/nci_core.h> + +#include "s3fwrn82.h" + +#define S3FWRN82_NFC_PROTOCOLS (NFC_PROTO_JEWEL_MASK | \ + NFC_PROTO_MIFARE_MASK | \ + NFC_PROTO_FELICA_MASK | \ + NFC_PROTO_ISO14443_MASK | \ + NFC_PROTO_ISO14443_B_MASK | \ + NFC_PROTO_ISO15693_MASK) + +static int s3fwrn82_nci_open(struct nci_dev *ndev) +{ + struct s3fwrn82_info *info = nci_get_drvdata(ndev); + + if (s3fwrn82_get_mode(info) != S3FWRN82_MODE_COLD) + return -EBUSY; + + s3fwrn82_set_mode(info, S3FWRN82_MODE_NCI); + s3fwrn82_set_wake(info, true); + + return 0; +} + +static int s3fwrn82_nci_close(struct nci_dev *ndev) +{ + struct s3fwrn82_info *info = nci_get_drvdata(ndev); + + s3fwrn82_set_wake(info, false); + s3fwrn82_set_mode(info, S3FWRN82_MODE_COLD); + + return 0; +} + +static int s3fwrn82_nci_send(struct nci_dev *ndev, struct sk_buff *skb) +{ + struct s3fwrn82_info *info = nci_get_drvdata(ndev); + int ret; + + mutex_lock(&info->mutex); + + if (s3fwrn82_get_mode(info) != S3FWRN82_MODE_NCI) { + mutex_unlock(&info->mutex); + return -EINVAL; + } + + ret = s3fwrn82_write(info, skb); + if (ret < 0) + kfree_skb(skb); + + mutex_unlock(&info->mutex); + return ret; +} + +static struct nci_ops s3fwrn82_nci_ops = { + .open = s3fwrn82_nci_open, + .close = s3fwrn82_nci_close, + .send = s3fwrn82_nci_send, +}; + +int s3fwrn82_probe(struct nci_dev **ndev, + void *phy_id, + struct device *pdev, + const struct s3fwrn82_phy_ops *phy_ops) +{ + struct s3fwrn82_info *info; + int ret; + + info = devm_kzalloc(pdev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->phy_id = phy_id; + info->pdev = pdev; + info->phy_ops = phy_ops; + mutex_init(&info->mutex); + + s3fwrn82_set_mode(info, S3FWRN82_MODE_COLD); + + info->ndev = nci_allocate_device(&s3fwrn82_nci_ops, S3FWRN82_NFC_PROTOCOLS, 0, 0); + if (!info->ndev) + return -ENOMEM; + + nci_set_parent_dev(info->ndev, pdev); + nci_set_drvdata(info->ndev, info); + + ret = nci_register_device(info->ndev); + if (ret < 0) { + nci_free_device(info->ndev); + return ret; + } + + *ndev = info->ndev; + + return ret; +} +EXPORT_SYMBOL(s3fwrn82_probe); + +void s3fwrn82_remove(struct nci_dev *ndev) +{ + struct s3fwrn82_info *info = nci_get_drvdata(ndev); + + s3fwrn82_set_mode(info, S3FWRN82_MODE_COLD); + + nci_unregister_device(ndev); + nci_free_device(ndev); +} +EXPORT_SYMBOL(s3fwrn82_remove); + +int s3fwrn82_recv_frame(struct nci_dev *ndev, struct sk_buff *skb, enum s3fwrn82_mode mode) +{ + switch (mode) { + case S3FWRN82_MODE_NCI: + return nci_recv_frame(ndev, skb); + default: + kfree_skb(skb); + return -ENODEV; + } +} +EXPORT_SYMBOL(s3fwrn82_recv_frame); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Samsung S3FWRN82 NFC driver"); +MODULE_AUTHOR("Bongsu Jeon <bongsu.jeon@xxxxxxxxxxx>"); diff --git a/drivers/nfc/s3fwrn82/i2c.c b/drivers/nfc/s3fwrn82/i2c.c new file mode 100644 index 000000000000..ea50c0165865 --- /dev/null +++ b/drivers/nfc/s3fwrn82/i2c.c @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * I2C Link Layer for Samsung S3FWRN82 NCI based Driver + * + * Copyright (C) 2020 Samsung Electrnoics + * Bongsu Jeon <bongsu.jeon@xxxxxxxxxxx> + */ + +#include <linux/i2c.h> +#include <linux/gpio.h> +#include <linux/delay.h> +#include <linux/of_gpio.h> +#include <linux/of_irq.h> +#include <linux/module.h> + +#include <net/nfc/nfc.h> + +#include "s3fwrn82.h" + +#define S3FWRN82_I2C_DRIVER_NAME "s3fwrn82_i2c" + +#define S3FWRN82_EN_WAIT_TIME 20 + +struct s3fwrn82_i2c_phy { + struct i2c_client *i2c_dev; + struct nci_dev *ndev; + + unsigned int gpio_en; + unsigned int gpio_fw_wake; + + /* use this mutex for syncronization of phy */ + struct mutex mutex; + + enum s3fwrn82_mode mode; + unsigned int irq_skip:1; +}; + +static void s3fwrn82_i2c_set_wake(void *phy_id, bool wake) +{ + struct s3fwrn82_i2c_phy *phy = phy_id; + + mutex_lock(&phy->mutex); + gpio_set_value(phy->gpio_fw_wake, wake); + if (wake) + msleep(S3FWRN82_EN_WAIT_TIME); + mutex_unlock(&phy->mutex); +} + +static void s3fwrn82_i2c_set_mode(void *phy_id, enum s3fwrn82_mode mode) +{ + struct s3fwrn82_i2c_phy *phy = phy_id; + + mutex_lock(&phy->mutex); + + if (phy->mode == mode) + goto out; + + phy->mode = mode; + + gpio_set_value(phy->gpio_en, 1); + gpio_set_value(phy->gpio_fw_wake, 0); + + if (mode != S3FWRN82_MODE_COLD) { + msleep(S3FWRN82_EN_WAIT_TIME); + gpio_set_value(phy->gpio_en, 0); + msleep(S3FWRN82_EN_WAIT_TIME / 2); + } + + phy->irq_skip = true; + +out: + mutex_unlock(&phy->mutex); +} + +static enum s3fwrn82_mode s3fwrn82_i2c_get_mode(void *phy_id) +{ + struct s3fwrn82_i2c_phy *phy = phy_id; + enum s3fwrn82_mode mode; + + mutex_lock(&phy->mutex); + + mode = phy->mode; + + mutex_unlock(&phy->mutex); + + return mode; +} + +static int s3fwrn82_i2c_write(void *phy_id, struct sk_buff *skb) +{ + struct s3fwrn82_i2c_phy *phy = phy_id; + int ret; + + mutex_lock(&phy->mutex); + + phy->irq_skip = false; + + ret = i2c_master_send(phy->i2c_dev, skb->data, skb->len); + mutex_unlock(&phy->mutex); + + if (ret < 0) + return ret; + + if (ret != skb->len) + return -EREMOTEIO; + + return 0; +} + +static const struct s3fwrn82_phy_ops i2c_phy_ops = { + .set_wake = s3fwrn82_i2c_set_wake, + .set_mode = s3fwrn82_i2c_set_mode, + .get_mode = s3fwrn82_i2c_get_mode, + .write = s3fwrn82_i2c_write, +}; + +static int s3fwrn82_i2c_read(struct s3fwrn82_i2c_phy *phy) +{ + struct sk_buff *skb; + size_t hdr_size; + size_t data_len; + char hdr[4]; + int ret; + + hdr_size = NCI_CTRL_HDR_SIZE; + ret = i2c_master_recv(phy->i2c_dev, hdr, hdr_size); + if (ret < 0) + return ret; + + if (ret < hdr_size) + return -EBADMSG; + + data_len = ((struct nci_ctrl_hdr *)hdr)->plen; + + skb = alloc_skb(hdr_size + data_len, GFP_KERNEL); + if (!skb) + return -ENOMEM; + + skb_put_data(skb, hdr, hdr_size); + + if (data_len == 0) + goto out; + + ret = i2c_master_recv(phy->i2c_dev, skb_put(skb, data_len), data_len); + if (ret != data_len) { + kfree_skb(skb); + return -EBADMSG; + } + +out: + return s3fwrn82_recv_frame(phy->ndev, skb, phy->mode); +} + +static irqreturn_t s3fwrn82_i2c_irq_thread_fn(int irq, void *phy_id) +{ + struct s3fwrn82_i2c_phy *phy = phy_id; + + if (!phy || !phy->ndev) { + WARN_ON_ONCE(1); + return IRQ_NONE; + } + + mutex_lock(&phy->mutex); + + if (phy->irq_skip) + goto out; + + switch (phy->mode) { + case S3FWRN82_MODE_NCI: + s3fwrn82_i2c_read(phy); + break; + case S3FWRN82_MODE_COLD: + break; + } + +out: + mutex_unlock(&phy->mutex); + + return IRQ_HANDLED; +} + +static int s3fwrn82_i2c_parse_dt(struct i2c_client *client) +{ + struct s3fwrn82_i2c_phy *phy = i2c_get_clientdata(client); + struct device_node *np = client->dev.of_node; + + if (!np) + return -ENODEV; + + phy->gpio_en = of_get_named_gpio(np, "en-gpios", 0); + if (!gpio_is_valid(phy->gpio_en)) + return -ENODEV; + + phy->gpio_fw_wake = of_get_named_gpio(np, "wake-gpios", 0); + if (!gpio_is_valid(phy->gpio_fw_wake)) + return -ENODEV; + + return 0; +} + +static int s3fwrn82_i2c_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct s3fwrn82_i2c_phy *phy; + int ret; + + phy = devm_kzalloc(&client->dev, sizeof(*phy), GFP_KERNEL); + if (!phy) + return -ENOMEM; + + mutex_init(&phy->mutex); + phy->mode = S3FWRN82_MODE_COLD; + phy->irq_skip = true; + + phy->i2c_dev = client; + i2c_set_clientdata(client, phy); + + ret = s3fwrn82_i2c_parse_dt(client); + if (ret < 0) + return ret; + + ret = devm_gpio_request_one(&phy->i2c_dev->dev, + phy->gpio_en, + GPIOF_OUT_INIT_HIGH, + "s3fwrn82_en"); + if (ret < 0) + return ret; + + ret = devm_gpio_request_one(&phy->i2c_dev->dev, + phy->gpio_fw_wake, + GPIOF_OUT_INIT_LOW, + "s3fwrn82_fw_wake"); + if (ret < 0) + return ret; + + ret = s3fwrn82_probe(&phy->ndev, phy, &phy->i2c_dev->dev, &i2c_phy_ops); + if (ret < 0) + return ret; + + ret = devm_request_threaded_irq(&client->dev, + phy->i2c_dev->irq, + NULL, + s3fwrn82_i2c_irq_thread_fn, + IRQF_TRIGGER_HIGH | IRQF_ONESHOT, + S3FWRN82_I2C_DRIVER_NAME, + phy); + if (ret) + s3fwrn82_remove(phy->ndev); + + return ret; +} + +static int s3fwrn82_i2c_remove(struct i2c_client *client) +{ + struct s3fwrn82_i2c_phy *phy = i2c_get_clientdata(client); + + s3fwrn82_remove(phy->ndev); + + return 0; +} + +static const struct i2c_device_id s3fwrn82_i2c_id_table[] = { + {S3FWRN82_I2C_DRIVER_NAME, 0}, + {} +}; +MODULE_DEVICE_TABLE(i2c, s3fwrn82_i2c_id_table); + +static const struct of_device_id of_s3fwrn82_i2c_match[] = { + { .compatible = "samsung,s3fwrn82-i2c", }, + {} +}; +MODULE_DEVICE_TABLE(of, of_s3fwrn82_i2c_match); + +static struct i2c_driver s3fwrn82_i2c_driver = { + .driver = { + .name = S3FWRN82_I2C_DRIVER_NAME, + .of_match_table = of_match_ptr(of_s3fwrn82_i2c_match), + }, + .probe = s3fwrn82_i2c_probe, + .remove = s3fwrn82_i2c_remove, + .id_table = s3fwrn82_i2c_id_table, +}; + +module_i2c_driver(s3fwrn82_i2c_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("I2C driver for Samsung S3FWRN82"); +MODULE_AUTHOR("Bongsu Jeon <bongsu.jeon@xxxxxxxxxxx>"); diff --git a/drivers/nfc/s3fwrn82/s3fwrn82.h b/drivers/nfc/s3fwrn82/s3fwrn82.h new file mode 100644 index 000000000000..4c19d1fafde6 --- /dev/null +++ b/drivers/nfc/s3fwrn82/s3fwrn82.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * NCI based driver for Samsung S3FWRN82 NFC chip + * + * Copyright (C) 2020 Samsung Electrnoics + * Bongsu Jeon <bongsu.jeon@xxxxxxxxxxx> + */ + +#ifndef __LOCAL_S3FWRN82_H_ +#define __LOCAL_S3FWRN82_H_ + +#include <linux/nfc.h> + +#include <net/nfc/nci_core.h> + +enum s3fwrn82_mode { + S3FWRN82_MODE_COLD, + S3FWRN82_MODE_NCI, +}; + +struct s3fwrn82_phy_ops { + void (*set_wake)(void *id, bool sleep); + void (*set_mode)(void *id, enum s3fwrn82_mode); + enum s3fwrn82_mode (*get_mode)(void *id); + int (*write)(void *id, struct sk_buff *skb); +}; + +struct s3fwrn82_info { + struct nci_dev *ndev; + void *phy_id; + struct device *pdev; + + const struct s3fwrn82_phy_ops *phy_ops; + + /* use this mutex for syncronization of nci write */ + struct mutex mutex; +}; + +static inline int s3fwrn82_set_mode(struct s3fwrn82_info *info, + enum s3fwrn82_mode mode) +{ + if (!info->phy_ops->set_mode) + return -EOPNOTSUPP; + + info->phy_ops->set_mode(info->phy_id, mode); + + return 0; +} + +static inline enum s3fwrn82_mode s3fwrn82_get_mode(struct s3fwrn82_info *info) +{ + if (!info->phy_ops->get_mode) + return -EOPNOTSUPP; + + return info->phy_ops->get_mode(info->phy_id); +} + +static inline int s3fwrn82_set_wake(struct s3fwrn82_info *info, bool wake) +{ + if (!info->phy_ops->set_wake) + return -EOPNOTSUPP; + + info->phy_ops->set_wake(info->phy_id, wake); + + return 0; +} + +static inline int s3fwrn82_write(struct s3fwrn82_info *info, struct sk_buff *skb) +{ + if (!info->phy_ops->write) + return -EOPNOTSUPP; + + return info->phy_ops->write(info->phy_id, skb); +} + +int s3fwrn82_probe(struct nci_dev **ndev, + void *phy_id, + struct device *pdev, + const struct s3fwrn82_phy_ops *phy_ops); +void s3fwrn82_remove(struct nci_dev *ndev); + +int s3fwrn82_recv_frame(struct nci_dev *ndev, + struct sk_buff *skb, + enum s3fwrn82_mode mode); + +#endif /* __LOCAL_S3FWRN82_H_ */ --