Merge branches 'audio' and 'nfc' into combined

combined = flashable working image: v2 speaker audio + NFC reader.
Excludes mic (WCD9378 capture, WIP) and nfc-ce (card emulation, broken).
This commit is contained in:
Jorijn van der Graaf 2026-07-05 21:33:08 +02:00
commit d4c5ebbeee
7 changed files with 377 additions and 18 deletions

View file

@ -698,9 +698,28 @@
};
&i2c1 {
/* Samsung NFC @ 0x27 */
status = "okay";
/*
* Samsung S3NRN4V NFC controller. XI is driven by the RF_CLK2 PMIC
* buffer; the chip has no oscillator of its own and gates the clock
* via its CLK_REQ line, so the clock must be voted on in response
* to it.
*/
nfc@27 {
compatible = "samsung,s3nrn4v";
reg = <0x27>;
clk-req-gpios = <&tlmm 6 GPIO_ACTIVE_HIGH>;
clocks = <&rpmhcc RPMH_RF_CLK2>;
en-gpios = <&tlmm 56 GPIO_ACTIVE_HIGH>;
interrupt-parent = <&tlmm>;
interrupts = <31 IRQ_TYPE_EDGE_RISING>;
pinctrl-0 = <&nfc_clk_req_default>, <&nfc_irq_default>,
<&nfc_pd_default>;
pinctrl-names = "default";
wake-gpios = <&tlmm 7 GPIO_ACTIVE_HIGH>;
};
};
&i2c3 {
@ -1089,6 +1108,13 @@
<13 1>, /* NC */
<63 2>; /* WLAN UART */
nfc_clk_req_default: nfc-clk-req-default-state {
pins = "gpio6";
function = "gpio";
drive-strength = <2>;
bias-disable;
};
ts_active: ts-irq-active-state {
pins = "gpio19";
function = "gpio";
@ -1103,6 +1129,13 @@
bias-disable;
};
nfc_irq_default: nfc-irq-default-state {
pins = "gpio31";
function = "gpio";
drive-strength = <2>;
bias-disable;
};
qup_uart11_sleep_cts: qup-uart11-sleep-cts-state {
pins = "gpio48";
function = "gpio";
@ -1170,6 +1203,17 @@
bias-pull-down;
};
/*
* Pulled up so the NFC chip stays powered down while the line is
* not driven (it is the chip's active-high power-down input).
*/
nfc_pd_default: nfc-pd-default-state {
pins = "gpio56";
function = "gpio";
drive-strength = <2>;
bias-pull-up;
};
sdc2_card_det_n: sdc2-card-det-state {
pins = "gpio65";
function = "gpio";

View file

@ -122,11 +122,47 @@ static int s3fwrn5_nci_send(struct nci_dev *ndev, struct sk_buff *skb)
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;
@ -152,13 +188,14 @@ 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)
const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant)
{
struct s3fwrn5_info *info;
int ret;
@ -170,6 +207,7 @@ int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
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);

View file

@ -23,9 +23,76 @@ struct s3fwrn5_i2c_phy {
struct i2c_client *i2c_dev;
struct clk *clk;
/*
* Optional hardware clock-request handshake. When a CLK_REQ GPIO is
* wired, the chip drives it high while it needs its XI clock -- notably
* to generate the poll/reader carrier -- and the clock is gated on it
* instead of being left always-on (which never lets the chip's TX PLL
* lock on a fresh clock start, leaving it unable to poll).
*/
struct gpio_desc *gpio_clk_req;
bool clk_on;
struct mutex clk_lock; /* serialises clk_on against the CLK_REQ irq */
unsigned int irq_skip:1;
};
static void s3fwrn5_i2c_clk_set_locked(struct s3fwrn5_i2c_phy *phy, bool on)
{
lockdep_assert_held(&phy->clk_lock);
if (on && !phy->clk_on) {
int ret = clk_prepare_enable(phy->clk);
if (ret == 0)
phy->clk_on = true;
else
dev_warn_once(&phy->i2c_dev->dev,
"failed to enable clock (%d); NFC may not poll\n",
ret);
} else if (!on && phy->clk_on) {
clk_disable_unprepare(phy->clk);
phy->clk_on = false;
}
}
/*
* Apply the current CLK_REQ level. Reading the GPIO under clk_lock makes
* concurrent callers (the CLK_REQ irq thread and the probe-time seeding)
* safe: whoever runs last applies a level read after the earlier update,
* never a stale one.
*/
static void s3fwrn5_i2c_clk_sync(struct s3fwrn5_i2c_phy *phy)
{
int level;
mutex_lock(&phy->clk_lock);
level = gpiod_get_value_cansleep(phy->gpio_clk_req);
if (level >= 0)
s3fwrn5_i2c_clk_set_locked(phy, level > 0);
else
dev_warn_once(&phy->i2c_dev->dev,
"failed to read CLK_REQ (%d); keeping clock state\n",
level);
mutex_unlock(&phy->clk_lock);
}
static void s3fwrn5_i2c_clk_disable_action(void *data)
{
struct s3fwrn5_i2c_phy *phy = data;
mutex_lock(&phy->clk_lock);
s3fwrn5_i2c_clk_set_locked(phy, false);
mutex_unlock(&phy->clk_lock);
}
static irqreturn_t s3fwrn5_i2c_clk_req_thread(int irq, void *phy_id)
{
s3fwrn5_i2c_clk_sync(phy_id);
return IRQ_HANDLED;
}
static void s3fwrn5_i2c_set_mode(void *phy_id, enum s3fwrn5_mode mode)
{
struct s3fwrn5_i2c_phy *phy = phy_id;
@ -146,6 +213,7 @@ out:
static int s3fwrn5_i2c_probe(struct i2c_client *client)
{
enum s3fwrn5_variant variant;
struct s3fwrn5_i2c_phy *phy;
int ret;
@ -172,15 +240,61 @@ static int s3fwrn5_i2c_probe(struct i2c_client *client)
* S3FWRN5 depends on a clock input ("XI" pin) to function properly.
* Depending on the hardware configuration this could be an always-on
* oscillator or some external clock that must be explicitly enabled.
* Make sure the clock is running before starting S3FWRN5.
*
* If a CLK_REQ GPIO is wired, the chip gates the clock itself (driving
* CLK_REQ high when it needs XI); service that handshake. Otherwise just
* make sure the clock is running before starting S3FWRN5.
*/
phy->clk = devm_clk_get_optional_enabled(&client->dev, NULL);
if (IS_ERR(phy->clk))
return dev_err_probe(&client->dev, PTR_ERR(phy->clk),
"failed to get clock\n");
mutex_init(&phy->clk_lock);
phy->gpio_clk_req = devm_gpiod_get_optional(&client->dev, "clk-req",
GPIOD_IN);
if (IS_ERR(phy->gpio_clk_req))
return PTR_ERR(phy->gpio_clk_req);
if (phy->gpio_clk_req) {
int clk_req_irq;
phy->clk = devm_clk_get_optional(&client->dev, NULL);
if (IS_ERR(phy->clk))
return dev_err_probe(&client->dev, PTR_ERR(phy->clk),
"failed to get clock\n");
/*
* Unlike the always-on branch below, this clock is enabled by
* hand from the CLK_REQ handler, so devm will not disable it on
* unbind. Gate it off explicitly if it is still on at teardown.
*/
ret = devm_add_action_or_reset(&client->dev,
s3fwrn5_i2c_clk_disable_action,
phy);
if (ret)
return ret;
clk_req_irq = gpiod_to_irq(phy->gpio_clk_req);
if (clk_req_irq < 0)
return clk_req_irq;
ret = devm_request_threaded_irq(&client->dev, clk_req_irq, NULL,
s3fwrn5_i2c_clk_req_thread,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT,
"s3fwrn5_clk_req", phy);
if (ret)
return ret;
/* Seed the clock state from the current CLK_REQ level. */
s3fwrn5_i2c_clk_sync(phy);
} else {
phy->clk = devm_clk_get_optional_enabled(&client->dev, NULL);
if (IS_ERR(phy->clk))
return dev_err_probe(&client->dev, PTR_ERR(phy->clk),
"failed to get clock\n");
}
variant = (uintptr_t)i2c_get_match_data(client);
ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->i2c_dev->dev,
&i2c_phy_ops);
&i2c_phy_ops, variant);
if (ret < 0)
return ret;
@ -205,13 +319,17 @@ static void s3fwrn5_i2c_remove(struct i2c_client *client)
}
static const struct i2c_device_id s3fwrn5_i2c_id_table[] = {
{ S3FWRN5_I2C_DRIVER_NAME },
{}
{ .name = S3FWRN5_I2C_DRIVER_NAME, .driver_data = S3FWRN5_VARIANT_FWDL },
{ .name = "s3nrn4v", .driver_data = S3FWRN5_VARIANT_S3NRN4V },
{ }
};
MODULE_DEVICE_TABLE(i2c, s3fwrn5_i2c_id_table);
static const struct of_device_id of_s3fwrn5_i2c_match[] __maybe_unused = {
{ .compatible = "samsung,s3fwrn5-i2c", },
static const struct of_device_id of_s3fwrn5_i2c_match[] = {
{ .compatible = "samsung,s3fwrn5-i2c",
.data = (void *)S3FWRN5_VARIANT_FWDL, },
{ .compatible = "samsung,s3nrn4v",
.data = (void *)S3FWRN5_VARIANT_S3NRN4V, },
{}
};
MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match);
@ -219,7 +337,7 @@ MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match);
static struct i2c_driver s3fwrn5_i2c_driver = {
.driver = {
.name = S3FWRN5_I2C_DRIVER_NAME,
.of_match_table = of_match_ptr(of_s3fwrn5_i2c_match),
.of_match_table = of_s3fwrn5_i2c_match,
},
.probe = s3fwrn5_i2c_probe,
.remove = s3fwrn5_i2c_remove,

View file

@ -8,6 +8,9 @@
#include <linux/completion.h>
#include <linux/firmware.h>
#include <linux/minmax.h>
#include <linux/slab.h>
#include <linux/unaligned.h>
#include "s3fwrn5.h"
#include "nci.h"
@ -20,7 +23,7 @@ static int s3fwrn5_nci_prop_rsp(struct nci_dev *ndev, struct sk_buff *skb)
return 0;
}
const struct nci_driver_ops s3fwrn5_nci_prop_ops[4] = {
const struct nci_driver_ops s3fwrn5_nci_prop_ops[5] = {
{
.opcode = nci_opcode_pack(NCI_GID_PROPRIETARY,
NCI_PROP_SET_RFREG),
@ -41,6 +44,11 @@ const struct nci_driver_ops s3fwrn5_nci_prop_ops[4] = {
NCI_PROP_FW_CFG),
.rsp = s3fwrn5_nci_prop_rsp,
},
{
.opcode = nci_opcode_pack(NCI_GID_PROPRIETARY,
NCI_PROP_DUAL_OPTION),
.rsp = s3fwrn5_nci_prop_rsp,
},
};
#define S3FWRN5_RFREG_SECTION_SIZE 252
@ -117,3 +125,112 @@ out:
release_firmware(fw);
return ret;
}
/*
* Configure the reference clock. The S3NRN4V expects the single-byte FW_CFG
* form (just the clock-speed selector). The downstream stack sends this in the
* bootloader before CORE_RESET; the earliest the mainline NCI core lets us in
* is the ->setup hook (after CORE_RESET, before CORE_INIT), which works.
*/
int s3fwrn5_nci_clk_cfg(struct s3fwrn5_info *info)
{
u8 clk_speed = NCI_PROP_FW_CFG_CLK_SPEED;
return nci_prop_cmd(info->ndev, NCI_PROP_FW_CFG, 1, &clk_speed);
}
/*
* S3NRN4V RF register update. The HW and SW register blobs are merged into a
* single stream (HW first) and pushed via the DUAL_OPTION command:
* START_UPDATE, one SET_OPTION per 252-byte section, then STOP_UPDATE carrying
* a 16-bit checksum (running sum of the merged stream as 32-bit words).
*/
int s3fwrn5_nci_rf_configure_dual(struct s3fwrn5_info *info,
const char *hw_name, const char *sw_name)
{
const struct firmware *hw_fw = NULL, *sw_fw = NULL;
struct nci_prop_dual_set_option_cmd set_option;
struct device *dev = &info->ndev->nfc_dev->dev;
size_t merged_size, i, len;
u8 *merged = NULL;
u8 stop_cmd[3];
u32 checksum;
u8 sub_oid;
int ret;
ret = request_firmware(&hw_fw, hw_name, dev);
if (ret < 0)
return ret;
ret = request_firmware(&sw_fw, sw_name, dev);
if (ret < 0)
goto out_hw;
merged_size = hw_fw->size + sw_fw->size;
/*
* The stream is checksummed as 32-bit words and pushed in at most 256
* sections (the section index is a single byte); reject blobs that
* would silently break either.
*/
if (merged_size % 4 ||
merged_size > 256 * NCI_PROP_DUAL_SECTION_SIZE) {
dev_err(dev, "invalid rfreg blob size (%zu)\n", merged_size);
ret = -EINVAL;
goto out;
}
merged = kmalloc(merged_size, GFP_KERNEL);
if (!merged) {
ret = -ENOMEM;
goto out;
}
memcpy(merged, hw_fw->data, hw_fw->size);
memcpy(merged + hw_fw->size, sw_fw->data, sw_fw->size);
/* Running sum of the merged stream as little-endian 32-bit words. */
checksum = 0;
for (i = 0; i + 4 <= merged_size; i += 4)
checksum += get_unaligned_le32(merged + i);
dev_dbg(dev, "rfreg dual-option update: %s + %s\n", hw_name, sw_name);
/* START_UPDATE */
sub_oid = NCI_PROP_DUAL_SUB_START_UPDATE;
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, 1, &sub_oid);
if (ret < 0) {
dev_err(dev, "Unable to start rfreg update\n");
goto out;
}
/* SET_OPTION per section */
set_option.sub_oid = NCI_PROP_DUAL_SUB_SET_OPTION;
set_option.index = 0;
for (i = 0; i < merged_size; i += NCI_PROP_DUAL_SECTION_SIZE) {
len = min_t(size_t, merged_size - i, NCI_PROP_DUAL_SECTION_SIZE);
memcpy(set_option.data, merged + i, len);
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION,
len + 2, (__u8 *)&set_option);
if (ret < 0) {
dev_err(dev, "rfreg update error (code=%d)\n", ret);
goto out;
}
set_option.index++;
}
/* STOP_UPDATE with checksum */
stop_cmd[0] = NCI_PROP_DUAL_SUB_STOP_UPDATE;
put_unaligned_le16(checksum, &stop_cmd[1]);
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, 3, stop_cmd);
if (ret < 0) {
dev_err(dev, "Unable to stop rfreg update\n");
goto out;
}
dev_dbg(dev, "rfreg dual-option update: success\n");
out:
kfree(merged);
release_firmware(sw_fw);
out_hw:
release_firmware(hw_fw);
return ret;
}

View file

@ -40,6 +40,13 @@ struct nci_prop_stop_rfreg_rsp {
#define NCI_PROP_FW_CFG 0x28
/*
* Single-byte FW_CFG payload (clock-speed selector) for the S3NRN4V reference
* clock. Taken from the vendor configuration for this part (the encoding is
* not documented).
*/
#define NCI_PROP_FW_CFG_CLK_SPEED 0x11
struct nci_prop_fw_cfg_cmd {
__u8 clk_type;
__u8 clk_speed;
@ -50,7 +57,30 @@ struct nci_prop_fw_cfg_rsp {
__u8 status;
};
extern const struct nci_driver_ops s3fwrn5_nci_prop_ops[4];
/*
* The S3NRN4V updates its RF registers through a single "dual option" command
* (a sub-OID selects the operation) instead of the START/SET/STOP_RFREG
* opcodes above, and expects the HW and SW register blobs merged into one
* stream.
*/
#define NCI_PROP_DUAL_OPTION 0x2a
#define NCI_PROP_DUAL_SUB_START_UPDATE 0x01
#define NCI_PROP_DUAL_SUB_SET_OPTION 0x02
#define NCI_PROP_DUAL_SUB_STOP_UPDATE 0x03
#define NCI_PROP_DUAL_SECTION_SIZE 252
struct nci_prop_dual_set_option_cmd {
__u8 sub_oid; /* NCI_PROP_DUAL_SUB_SET_OPTION */
__u8 index;
__u8 data[NCI_PROP_DUAL_SECTION_SIZE];
};
extern const struct nci_driver_ops s3fwrn5_nci_prop_ops[5];
int s3fwrn5_nci_rf_configure(struct s3fwrn5_info *info, const char *fw_name);
int s3fwrn5_nci_rf_configure_dual(struct s3fwrn5_info *info,
const char *hw_name, const char *sw_name);
int s3fwrn5_nci_clk_cfg(struct s3fwrn5_info *info);
#endif /* __LOCAL_S3FWRN5_NCI_H_ */

View file

@ -21,6 +21,17 @@ enum s3fwrn5_mode {
S3FWRN5_MODE_FW,
};
enum s3fwrn5_variant {
/* S3FWRN5 / S3FWRN82: firmware is downloaded by this driver */
S3FWRN5_VARIANT_FWDL,
/*
* S3NRN4V: ships with working firmware behind a bootloader protocol
* this driver does not implement; skip the download, configure the
* clock (FW_CFG) and update the RF registers via the DUAL_OPTION cmd.
*/
S3FWRN5_VARIANT_S3NRN4V,
};
struct s3fwrn5_phy_ops {
void (*set_wake)(void *id, bool sleep);
void (*set_mode)(void *id, enum s3fwrn5_mode);
@ -36,6 +47,7 @@ struct s3fwrn5_info {
const struct s3fwrn5_phy_ops *phy_ops;
struct s3fwrn5_fw_info fw_info;
enum s3fwrn5_variant variant;
struct mutex mutex;
};
@ -78,7 +90,7 @@ static inline int s3fwrn5_write(struct s3fwrn5_info *info, struct sk_buff *skb)
}
int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
const struct s3fwrn5_phy_ops *phy_ops);
const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant);
void s3fwrn5_remove(struct nci_dev *ndev);
int s3fwrn5_recv_frame(struct nci_dev *ndev, struct sk_buff *skb,

View file

@ -137,7 +137,7 @@ static int s3fwrn82_uart_probe(struct serdev_device *serdev)
}
ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->ser_dev->dev,
&uart_phy_ops);
&uart_phy_ops, S3FWRN5_VARIANT_FWDL);
if (ret < 0)
goto err_serdev;