* [PATCH net-next 2/2] nfc: s3fwrn5: support the S3NRN4V variant
2026-07-03 20:25 [PATCH net-next 0/2] nfc: s3fwrn5: support the S3NRN4V variant Jorijn van der Graaf
2026-07-03 20:26 ` [PATCH net-next 1/2] dt-bindings: net: nfc: samsung,s3fwrn5: add S3NRN4V and clk-req-gpios Jorijn van der Graaf
@ 2026-07-03 20:26 ` Jorijn van der Graaf
2026-07-04 20:35 ` sashiko-bot
1 sibling, 1 reply; 5+ messages in thread
From: Jorijn van der Graaf @ 2026-07-03 20:26 UTC (permalink / raw)
To: Krzysztof Kozlowski
Cc: David Heidelberg, Andrew Lunn, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Rob Herring, Conor Dooley,
oe-linux-nfc, netdev, devicetree, linux-kernel,
Jorijn van der Graaf
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-i2c 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 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>
---
drivers/nfc/s3fwrn5/core.c | 40 +++++++++++-
drivers/nfc/s3fwrn5/i2c.c | 114 +++++++++++++++++++++++++++++++---
drivers/nfc/s3fwrn5/nci.c | 111 ++++++++++++++++++++++++++++++++-
drivers/nfc/s3fwrn5/nci.h | 32 +++++++++-
drivers/nfc/s3fwrn5/s3fwrn5.h | 14 ++++-
drivers/nfc/s3fwrn5/uart.c | 2 +-
6 files changed, 299 insertions(+), 14 deletions(-)
diff --git a/drivers/nfc/s3fwrn5/core.c b/drivers/nfc/s3fwrn5/core.c
index af0fa8bd9..59317eaad 100644
--- a/drivers/nfc/s3fwrn5/core.c
+++ b/drivers/nfc/s3fwrn5/core.c
@@ -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);
diff --git a/drivers/nfc/s3fwrn5/i2c.c b/drivers/nfc/s3fwrn5/i2c.c
index e9a34d27a..88a498879 100644
--- a/drivers/nfc/s3fwrn5/i2c.c
+++ b/drivers/nfc/s3fwrn5/i2c.c
@@ -23,9 +23,53 @@ 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(struct s3fwrn5_i2c_phy *phy, bool on)
+{
+ mutex_lock(&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;
+ }
+ mutex_unlock(&phy->clk_lock);
+}
+
+static void s3fwrn5_i2c_clk_disable_action(void *data)
+{
+ s3fwrn5_i2c_clk_set(data, false);
+}
+
+static irqreturn_t s3fwrn5_i2c_clk_req_thread(int irq, void *phy_id)
+{
+ struct s3fwrn5_i2c_phy *phy = phy_id;
+
+ s3fwrn5_i2c_clk_set(phy, gpiod_get_value_cansleep(phy->gpio_clk_req) > 0);
+
+ 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 +190,7 @@ static irqreturn_t s3fwrn5_i2c_irq_thread_fn(int irq, void *phy_id)
static int s3fwrn5_i2c_probe(struct i2c_client *client)
{
+ enum s3fwrn5_variant variant;
struct s3fwrn5_i2c_phy *phy;
int ret;
@@ -172,15 +217,63 @@ 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_set(phy,
+ gpiod_get_value_cansleep(phy->gpio_clk_req) > 0);
+ } 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");
+ }
+ /* No match data (e.g. i2c_device_id binding) means the default FWDL. */
+ 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;
@@ -210,8 +303,11 @@ static const struct i2c_device_id s3fwrn5_i2c_id_table[] = {
};
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-i2c",
+ .data = (void *)S3FWRN5_VARIANT_S3NRN4V, },
{}
};
MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match);
@@ -219,7 +315,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,
diff --git a/drivers/nfc/s3fwrn5/nci.c b/drivers/nfc/s3fwrn5/nci.c
index 5a9de11bb..04f4c3626 100644
--- a/drivers/nfc/s3fwrn5/nci.c
+++ b/drivers/nfc/s3fwrn5/nci.c
@@ -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,104 @@ int s3fwrn5_nci_rf_configure(struct s3fwrn5_info *info, const char *fw_name)
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;
+ 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. The
+ * rfreg blobs are word-aligned, so the loop consumes the whole stream;
+ * should a future blob not be a multiple of 4 bytes its tail would be
+ * ignored here.
+ */
+ 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;
+}
diff --git a/drivers/nfc/s3fwrn5/nci.h b/drivers/nfc/s3fwrn5/nci.h
index bc4bce2bb..23179ba09 100644
--- a/drivers/nfc/s3fwrn5/nci.h
+++ b/drivers/nfc/s3fwrn5/nci.h
@@ -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_ */
diff --git a/drivers/nfc/s3fwrn5/s3fwrn5.h b/drivers/nfc/s3fwrn5/s3fwrn5.h
index 2b4922360..2d8c12091 100644
--- a/drivers/nfc/s3fwrn5/s3fwrn5.h
+++ b/drivers/nfc/s3fwrn5/s3fwrn5.h
@@ -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,
diff --git a/drivers/nfc/s3fwrn5/uart.c b/drivers/nfc/s3fwrn5/uart.c
index 540a4ddb0..47172d739 100644
--- a/drivers/nfc/s3fwrn5/uart.c
+++ b/drivers/nfc/s3fwrn5/uart.c
@@ -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;
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread