The S3NRN4V (e.g. on the Fairphone 6, SM7635) is an S3FWRN5-family NFC controller that needs different bring-up, selected with a new samsung,s3nrn4v compatible: - It ships with working firmware behind a bootloader protocol this driver does not implement (GET_BOOTINFO times out), so the firmware download step is skipped. Its RF registers are (re)loaded with the proprietary DUAL_OPTION command (the HW and SW register blobs merged into a single stream) instead of the START/SET/STOP_RFREG sequence. - Its reference clock speed is configured with the single-byte FW_CFG form, sent from the ->setup hook (after CORE_RESET, before CORE_INIT). The selector value (0x11) is taken from the vendor configuration for this part; its encoding is not documented. - It gates its XI clock through a CLK_REQ line: the chip drives it high when it needs the clock, notably to synthesise the 13.56 MHz poll carrier. Left always-on, the free-running clock never lets the chip's TX PLL lock on a fresh start and it cannot poll (it falls back to listen only). Service the handshake when a clk-req GPIO is described, gating the clock on it; without one the clock stays always-on. The variant is carried as match data by both the OF and the I2C device id tables so the two match paths agree, and the OF table is now referenced unconditionally for its match data, so drop the of_match_ptr()/__maybe_unused annotations from it. The error policy differs between the two configuration steps on purpose: a clock misconfiguration is fatal (a ->setup failure aborts CORE_INIT), whereas an RF-register update failure is only warned about and bring-up continues, since the chip falls back to the RF registers programmed in its flash and NFC may still work. Unlike the host-endian word read in the legacy rfreg path, the DUAL_OPTION checksum is accumulated with get_unaligned_le32() and emitted little-endian explicitly, so it is correct regardless of CPU endianness. Existing S3FWRN5 / S3FWRN82 setups keep the firmware-download path and the always-on clock, unchanged. Assisted-by: Claude:claude-opus-4-8 Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
265 lines
6 KiB
C
265 lines
6 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* NCI based driver for Samsung S3FWRN5 NFC chip
|
|
*
|
|
* Copyright (C) 2015 Samsung Electronics
|
|
* Robert Baldyga <r.baldyga@samsung.com>
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <net/nfc/nci_core.h>
|
|
|
|
#include "s3fwrn5.h"
|
|
#include "firmware.h"
|
|
#include "nci.h"
|
|
|
|
#define S3FWRN5_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 s3fwrn5_firmware_init(struct s3fwrn5_info *info)
|
|
{
|
|
struct s3fwrn5_fw_info *fw_info = &info->fw_info;
|
|
int ret;
|
|
|
|
s3fwrn5_fw_init(fw_info, "sec_s3fwrn5_firmware.bin");
|
|
|
|
/* Get firmware data */
|
|
ret = s3fwrn5_fw_request_firmware(fw_info);
|
|
if (ret < 0)
|
|
dev_err(&fw_info->ndev->nfc_dev->dev,
|
|
"Failed to get fw file, ret=%02x\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
static int s3fwrn5_firmware_update(struct s3fwrn5_info *info)
|
|
{
|
|
bool need_update;
|
|
int ret;
|
|
|
|
/* Update firmware */
|
|
|
|
s3fwrn5_set_wake(info, false);
|
|
s3fwrn5_set_mode(info, S3FWRN5_MODE_FW);
|
|
|
|
ret = s3fwrn5_fw_setup(&info->fw_info);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
need_update = s3fwrn5_fw_check_version(&info->fw_info,
|
|
info->ndev->manufact_specific_info);
|
|
if (!need_update)
|
|
goto out;
|
|
|
|
dev_info(&info->ndev->nfc_dev->dev, "Detected new firmware version\n");
|
|
|
|
ret = s3fwrn5_fw_download(&info->fw_info);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
/* Update RF configuration */
|
|
|
|
s3fwrn5_set_mode(info, S3FWRN5_MODE_NCI);
|
|
|
|
s3fwrn5_set_wake(info, true);
|
|
ret = s3fwrn5_nci_rf_configure(info, "sec_s3fwrn5_rfreg.bin");
|
|
s3fwrn5_set_wake(info, false);
|
|
|
|
out:
|
|
s3fwrn5_set_mode(info, S3FWRN5_MODE_COLD);
|
|
s3fwrn5_fw_cleanup(&info->fw_info);
|
|
return ret;
|
|
}
|
|
|
|
static int s3fwrn5_nci_open(struct nci_dev *ndev)
|
|
{
|
|
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
|
|
|
|
if (s3fwrn5_get_mode(info) != S3FWRN5_MODE_COLD)
|
|
return -EBUSY;
|
|
|
|
s3fwrn5_set_mode(info, S3FWRN5_MODE_NCI);
|
|
s3fwrn5_set_wake(info, true);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s3fwrn5_nci_close(struct nci_dev *ndev)
|
|
{
|
|
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
|
|
|
|
s3fwrn5_set_wake(info, false);
|
|
s3fwrn5_set_mode(info, S3FWRN5_MODE_COLD);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s3fwrn5_nci_send(struct nci_dev *ndev, struct sk_buff *skb)
|
|
{
|
|
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
|
|
int ret;
|
|
|
|
mutex_lock(&info->mutex);
|
|
|
|
if (s3fwrn5_get_mode(info) != S3FWRN5_MODE_NCI) {
|
|
kfree_skb(skb);
|
|
mutex_unlock(&info->mutex);
|
|
return -EINVAL;
|
|
}
|
|
|
|
ret = s3fwrn5_write(info, skb);
|
|
if (ret < 0) {
|
|
kfree_skb(skb);
|
|
mutex_unlock(&info->mutex);
|
|
return ret;
|
|
}
|
|
|
|
consume_skb(skb);
|
|
mutex_unlock(&info->mutex);
|
|
return 0;
|
|
}
|
|
|
|
static int s3fwrn5_nci_setup(struct nci_dev *ndev)
|
|
{
|
|
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
|
|
|
|
/*
|
|
* Runs after CORE_RESET, before CORE_INIT. The S3NRN4V needs its
|
|
* reference clock configured here (the downstream stack does it in the
|
|
* bootloader, before CORE_RESET, but this is the earliest hook the NCI
|
|
* core offers and the chip accepts it).
|
|
*/
|
|
if (info->variant == S3FWRN5_VARIANT_S3NRN4V)
|
|
return s3fwrn5_nci_clk_cfg(info);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s3fwrn5_nci_post_setup(struct nci_dev *ndev)
|
|
{
|
|
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
|
|
int ret;
|
|
|
|
if (info->variant == S3FWRN5_VARIANT_S3NRN4V) {
|
|
/*
|
|
* The S3NRN4V ships with working firmware behind a bootloader
|
|
* protocol this driver does not implement, so there is no
|
|
* download step; the NCI core has already done CORE_RESET +
|
|
* CORE_INIT. Just (re)load the RF registers via DUAL_OPTION.
|
|
*/
|
|
ret = s3fwrn5_nci_rf_configure_dual(info, "sec_s3nrn4v_hwreg.bin",
|
|
"sec_s3nrn4v_swreg.bin");
|
|
/*
|
|
* Keep going even if the blobs could not be loaded: the chip
|
|
* still enumerates and falls back to the RF registers programmed
|
|
* in its flash, so NFC may work anyway.
|
|
*/
|
|
if (ret < 0)
|
|
dev_warn(&ndev->nfc_dev->dev,
|
|
"rfreg configure failed (%d)\n", ret);
|
|
return 0;
|
|
}
|
|
|
|
if (s3fwrn5_firmware_init(info)) {
|
|
//skip bootloader mode
|
|
return 0;
|
|
}
|
|
|
|
ret = s3fwrn5_firmware_update(info);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* NCI core reset */
|
|
|
|
s3fwrn5_set_mode(info, S3FWRN5_MODE_NCI);
|
|
s3fwrn5_set_wake(info, true);
|
|
|
|
ret = nci_core_reset(info->ndev);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return nci_core_init(info->ndev);
|
|
}
|
|
|
|
static const struct nci_ops s3fwrn5_nci_ops = {
|
|
.open = s3fwrn5_nci_open,
|
|
.close = s3fwrn5_nci_close,
|
|
.send = s3fwrn5_nci_send,
|
|
.setup = s3fwrn5_nci_setup,
|
|
.post_setup = s3fwrn5_nci_post_setup,
|
|
.prop_ops = s3fwrn5_nci_prop_ops,
|
|
.n_prop_ops = ARRAY_SIZE(s3fwrn5_nci_prop_ops),
|
|
};
|
|
|
|
int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
|
|
const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant)
|
|
{
|
|
struct s3fwrn5_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;
|
|
info->variant = variant;
|
|
mutex_init(&info->mutex);
|
|
|
|
s3fwrn5_set_mode(info, S3FWRN5_MODE_COLD);
|
|
|
|
info->ndev = nci_allocate_device(&s3fwrn5_nci_ops,
|
|
S3FWRN5_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;
|
|
}
|
|
|
|
info->fw_info.ndev = info->ndev;
|
|
|
|
*ndev = info->ndev;
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(s3fwrn5_probe);
|
|
|
|
void s3fwrn5_remove(struct nci_dev *ndev)
|
|
{
|
|
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
|
|
|
|
s3fwrn5_set_mode(info, S3FWRN5_MODE_COLD);
|
|
|
|
nci_unregister_device(ndev);
|
|
nci_free_device(ndev);
|
|
}
|
|
EXPORT_SYMBOL(s3fwrn5_remove);
|
|
|
|
int s3fwrn5_recv_frame(struct nci_dev *ndev, struct sk_buff *skb,
|
|
enum s3fwrn5_mode mode)
|
|
{
|
|
switch (mode) {
|
|
case S3FWRN5_MODE_NCI:
|
|
return nci_recv_frame(ndev, skb);
|
|
case S3FWRN5_MODE_FW:
|
|
return s3fwrn5_fw_recv_frame(ndev, skb);
|
|
default:
|
|
kfree_skb(skb);
|
|
return -ENODEV;
|
|
}
|
|
}
|
|
EXPORT_SYMBOL(s3fwrn5_recv_frame);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_DESCRIPTION("Samsung S3FWRN5 NFC driver");
|
|
MODULE_AUTHOR("Robert Baldyga <r.baldyga@samsung.com>");
|