From d837afcfd60e69ce496f73e48b7007f4b130e9b8 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 29 Jun 2026 00:23:51 +0200 Subject: [PATCH 1/8] arm64: dts: qcom: milos-fairphone-fp6: add NFC Add the Samsung S3NRN4V NFC + eSE controller on i2c1. Its XI clock is provided by the RF_CLK2 PMIC buffer and gated through the controller's CLK_REQ line on tlmm GPIO6. The enable line is routed to the chip's power-down input, which is asserted high to turn the chip off (the downstream driver treats VEN as active-low on this design) -- hence GPIO_ACTIVE_HIGH, unlike the exynos5433-tm2 wiring of the same driver. The pin is pulled up so the chip stays off while the line is not driven. Assisted-by: Claude:claude-opus-4-8 Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- .../boot/dts/qcom/milos-fairphone-fp6.dts | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts index e5944a1b255f..9c765bcc1f2f 100644 --- a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts +++ b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts @@ -700,9 +700,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 { @@ -1091,6 +1110,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"; @@ -1105,6 +1131,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"; @@ -1172,6 +1205,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"; From 9cd00a65e820565407a762bd7876b03bdad0ec15 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Fri, 3 Jul 2026 21:43:28 +0200 Subject: [PATCH 2/8] nfc: s3fwrn5: support the S3NRN4V variant 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 --- drivers/nfc/s3fwrn5/core.c | 40 +++++++++- drivers/nfc/s3fwrn5/i2c.c | 140 +++++++++++++++++++++++++++++++--- drivers/nfc/s3fwrn5/nci.c | 119 ++++++++++++++++++++++++++++- drivers/nfc/s3fwrn5/nci.h | 32 +++++++- drivers/nfc/s3fwrn5/s3fwrn5.h | 14 +++- drivers/nfc/s3fwrn5/uart.c | 2 +- 6 files changed, 331 insertions(+), 16 deletions(-) diff --git a/drivers/nfc/s3fwrn5/core.c b/drivers/nfc/s3fwrn5/core.c index af0fa8bd970b..59317eaad7ac 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 91b8d1445efd..7d20e737e402 100644 --- a/drivers/nfc/s3fwrn5/i2c.c +++ b/drivers/nfc/s3fwrn5/i2c.c @@ -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, diff --git a/drivers/nfc/s3fwrn5/nci.c b/drivers/nfc/s3fwrn5/nci.c index 5a9de11bbece..7034fb810e18 100644 --- a/drivers/nfc/s3fwrn5/nci.c +++ b/drivers/nfc/s3fwrn5/nci.c @@ -8,6 +8,9 @@ #include #include +#include +#include +#include #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; +} diff --git a/drivers/nfc/s3fwrn5/nci.h b/drivers/nfc/s3fwrn5/nci.h index bc4bce2bbc4d..23179ba095a1 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 2b492236090b..2d8c12091fba 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 540a4ddb0b05..47172d739a41 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; From 692c0cd1a495ff101f8ee44a4de1b9c52168374a Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 21:32:11 +0200 Subject: [PATCH 3/8] ASoC: qcom: sc8280xp: support Senary MI2S Extend the clock-provider DAI fmt setup to Senary MI2S; without it q6i2s_set_fmt() is never called, ws_src remains external and the DSP does not drive the I2S clocks. On the Fairphone (Gen. 6) the speaker amplifiers sit on this interface; the board DTS enabling it is headed upstream separately via linux-arm-msm. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/qcom/sc8280xp.c | 1 + 1 file changed, 1 insertion(+) diff --git a/sound/soc/qcom/sc8280xp.c b/sound/soc/qcom/sc8280xp.c index a36666999ac1..ce28bfac329a 100644 --- a/sound/soc/qcom/sc8280xp.c +++ b/sound/soc/qcom/sc8280xp.c @@ -34,6 +34,7 @@ static int sc8280xp_snd_init(struct snd_soc_pcm_runtime *rtd) switch (cpu_dai->id) { case PRIMARY_MI2S_RX...QUATERNARY_MI2S_TX: case QUINARY_MI2S_RX...QUINARY_MI2S_TX: + case SENARY_MI2S_RX...SENARY_MI2S_TX: snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_BP_FP); break; case WSA_CODEC_DMA_RX_0: From 79c7597fc5b6aac251a6f8acbdc3a18956be9b1e Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 21:32:11 +0200 Subject: [PATCH 4/8] ASoC: codecs: aw88261: backport mainline format negotiation + power-up fix The v7.1.2-milos base predates two aw88261 changes this branch needs: the hw_params format-negotiation series (Val Packett's work, in broonie for-7.2) and our power-up SYSST fix. The Senary MI2S carry drives the two AW88261 amps in S16_LE end to end, which relies on the amplifier negotiating its format; without it the machine driver has to force 32-bit slots. The SYSST fix is also required: aw88261_dev_start() otherwise fails "check sysst fail" on this firmware profile because it demands SWS/BSTS while the amp is still muted. Backport aw88261.c/.h wholesale to the mainline state (matching work/linux tag audio-mainline-aw88261) so this branch tracks mainline rather than the older milos base driver. Local carry only; milos absorbs it on the next rebase past those commits. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Jorijn van der Graaf --- sound/soc/codecs/aw88261.c | 646 ++++++++++++++++++++----------------- sound/soc/codecs/aw88261.h | 191 +++++++++-- 2 files changed, 527 insertions(+), 310 deletions(-) diff --git a/sound/soc/codecs/aw88261.c b/sound/soc/codecs/aw88261.c index a6805d5405cd..dbdf51188cf5 100644 --- a/sound/soc/codecs/aw88261.c +++ b/sound/soc/codecs/aw88261.c @@ -10,9 +10,12 @@ #include #include +#include #include #include #include +#include +#include #include "aw88261.h" #include "aw88395/aw88395_data_type.h" #include "aw88395/aw88395_device.h" @@ -27,64 +30,10 @@ static const struct regmap_config aw88261_remap_config = { static void aw88261_dev_set_volume(struct aw_device *aw_dev, unsigned int value) { - struct aw_volume_desc *vol_desc = &aw_dev->volume_desc; - unsigned int real_value, volume; - unsigned int reg_value; + unsigned int volume = min(value, (unsigned int)AW88261_MUTE_VOL); - volume = min((value + vol_desc->init_volume), (unsigned int)AW88261_MUTE_VOL); - real_value = DB_TO_REG_VAL(volume); - - regmap_read(aw_dev->regmap, AW88261_SYSCTRL2_REG, ®_value); - - real_value = (real_value | (reg_value & AW88261_VOL_START_MASK)); - - dev_dbg(aw_dev->dev, "value 0x%x , real_value:0x%x", value, real_value); - - regmap_write(aw_dev->regmap, AW88261_SYSCTRL2_REG, real_value); -} - -static void aw88261_dev_fade_in(struct aw_device *aw_dev) -{ - struct aw_volume_desc *desc = &aw_dev->volume_desc; - int fade_in_vol = desc->ctl_volume; - int fade_step = aw_dev->fade_step; - int i; - - if (fade_step == 0 || aw_dev->fade_in_time == 0) { - aw88261_dev_set_volume(aw_dev, fade_in_vol); - return; - } - - for (i = AW88261_MUTE_VOL; i >= fade_in_vol; i -= fade_step) { - aw88261_dev_set_volume(aw_dev, i); - usleep_range(aw_dev->fade_in_time, - aw_dev->fade_in_time + 10); - } - - if (i != fade_in_vol) - aw88261_dev_set_volume(aw_dev, fade_in_vol); -} - -static void aw88261_dev_fade_out(struct aw_device *aw_dev) -{ - struct aw_volume_desc *desc = &aw_dev->volume_desc; - int fade_step = aw_dev->fade_step; - int i; - - if (fade_step == 0 || aw_dev->fade_out_time == 0) { - aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL); - return; - } - - for (i = desc->ctl_volume; i <= AW88261_MUTE_VOL; i += fade_step) { - aw88261_dev_set_volume(aw_dev, i); - usleep_range(aw_dev->fade_out_time, aw_dev->fade_out_time + 10); - } - - if (i != AW88261_MUTE_VOL) { - aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL); - usleep_range(aw_dev->fade_out_time, aw_dev->fade_out_time + 10); - } + regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL2_REG, + ~AW88261_VOL_MASK, DB_TO_REG_VAL(volume)); } static void aw88261_dev_i2s_tx_enable(struct aw_device *aw_dev, bool flag) @@ -120,13 +69,13 @@ static void aw88261_dev_amppd(struct aw_device *aw_dev, bool amppd) static void aw88261_dev_mute(struct aw_device *aw_dev, bool is_mute) { if (is_mute) { - aw88261_dev_fade_out(aw_dev); + aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL); regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG, ~AW88261_HMUTE_MASK, AW88261_HMUTE_ENABLE_VALUE); } else { regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG, ~AW88261_HMUTE_MASK, AW88261_HMUTE_DISABLE_VALUE); - aw88261_dev_fade_in(aw_dev); + aw88261_dev_set_volume(aw_dev, aw_dev->volume_desc.ctl_volume); } } @@ -151,21 +100,21 @@ static int aw88261_dev_get_iis_status(struct aw_device *aw_dev) if (ret) return ret; if ((reg_val & AW88261_BIT_PLL_CHECK) != AW88261_BIT_PLL_CHECK) { - dev_err(aw_dev->dev, "check pll lock fail,reg_val:0x%04x", reg_val); + dev_dbg(aw_dev->dev, "check pll lock fail,reg_val:0x%04x", reg_val); return -EINVAL; } return ret; } -static int aw88261_dev_check_mode1_pll(struct aw_device *aw_dev) +static int aw88261_dev_check_pll(struct aw_device *aw_dev) { int ret, i; for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) { ret = aw88261_dev_get_iis_status(aw_dev); if (ret) { - dev_err(aw_dev->dev, "mode1 iis signal check error"); + dev_dbg(aw_dev->dev, "mode1 iis signal check error"); usleep_range(AW88261_2000_US, AW88261_2000_US + 10); } else { return ret; @@ -175,71 +124,74 @@ static int aw88261_dev_check_mode1_pll(struct aw_device *aw_dev) return -EPERM; } -static int aw88261_dev_check_mode2_pll(struct aw_device *aw_dev) -{ - unsigned int reg_val; - int ret, i; - - ret = regmap_read(aw_dev->regmap, AW88261_PLLCTRL1_REG, ®_val); - if (ret) - return ret; - - reg_val &= (~AW88261_CCO_MUX_MASK); - if (reg_val == AW88261_CCO_MUX_DIVIDED_VALUE) { - dev_dbg(aw_dev->dev, "CCO_MUX is already divider"); - return -EPERM; - } - - /* change mode2 */ - ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG, - ~AW88261_CCO_MUX_MASK, AW88261_CCO_MUX_DIVIDED_VALUE); - if (ret) - return ret; - - for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) { - ret = aw88261_dev_get_iis_status(aw_dev); - if (ret) { - dev_err(aw_dev->dev, "mode2 iis signal check error"); - usleep_range(AW88261_2000_US, AW88261_2000_US + 10); - } else { - break; - } - } - - /* change mode1 */ - ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG, - ~AW88261_CCO_MUX_MASK, AW88261_CCO_MUX_BYPASS_VALUE); - if (ret == 0) { - usleep_range(AW88261_2000_US, AW88261_2000_US + 10); - for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) { - ret = aw88261_dev_check_mode1_pll(aw_dev); - if (ret) { - dev_err(aw_dev->dev, "mode2 switch to mode1, iis signal check error"); - usleep_range(AW88261_2000_US, AW88261_2000_US + 10); - } else { - break; - } - } - } - - return ret; -} - -static int aw88261_dev_check_syspll(struct aw_device *aw_dev) +static int aw88261_dev_configure_syspll(struct aw88261 *aw88261) { + struct aw_device *aw_dev = aw88261->aw_pa; int ret; - ret = aw88261_dev_check_mode1_pll(aw_dev); - if (ret) { - dev_dbg(aw_dev->dev, "mode1 check iis failed try switch to mode2 check"); - ret = aw88261_dev_check_mode2_pll(aw_dev); - if (ret) { - dev_err(aw_dev->dev, "mode2 check iis failed"); - return ret; - } - } + /* Configure TDM slots (I2S is represented as no slots) */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG, + ~AW88261_SLOT_NUM_MASK, aw88261->slot_num_value); + if (ret) + return ret; - return ret; + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG, + ~AW88261_I2S_TX_SLOTVLD_MASK, + aw88261->tx_slotvld_mask); + if (ret) + return ret; + + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG, + ~AW88261_I2S_RXL_SLOTVLD_MASK, + aw88261->rxl_slotvld_mask); + if (ret) + return ret; + + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG, + ~AW88261_I2S_RXR_SLOTVLD_MASK, + aw88261->rxr_slotvld_mask); + if (ret) + return ret; + + /* PLL divider must be used for 8/16/32 kHz modes */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG, + ~AW88261_CCO_MUX_MASK, aw88261->cco_mux_value); + if (ret) + return ret; + + /* The word clock (WCK) defines the beginning of a frame */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG, + ~AW88261_I2SSR_MASK, aw88261->sr_value); + if (ret) + return ret; + + /* The bit clock (BCK) defines the length of a frame */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG, + ~AW88261_I2SBCK_MASK, + (aw88261->tdm_bck_value != AW88261_TDM_BCK_UNSET) + ? aw88261->tdm_bck_value : aw88261->bck_value); + if (ret) + return ret; + + /* The logical frame size is the width of data for 1 slot */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG, + ~AW88261_I2SFS_MASK, aw88261->fs_value); + if (ret) + return ret; + + /* The I2S interface mode (Philips standard, LSB/MSB justified) */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG, + ~AW88261_I2SMD_MASK, aw88261->md_value); + if (ret) + return ret; + + /* The polarity of the bit clock (BCK) */ + ret = regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG, + ~AW88261_BCKINV_MASK, aw88261->bck_inv_value); + if (ret) + return ret; + + return aw88261_dev_check_pll(aw_dev); } static int aw88261_dev_check_sysst(struct aw_device *aw_dev) @@ -254,10 +206,10 @@ static int aw88261_dev_check_sysst(struct aw_device *aw_dev) return ret; check_val = reg_val & (~AW88261_BIT_SYSST_CHECK_MASK) - & AW88261_BIT_SYSST_CHECK; - if (check_val != AW88261_BIT_SYSST_CHECK) { - dev_err(aw_dev->dev, "check sysst fail, reg_val=0x%04x, check:0x%x", - reg_val, AW88261_BIT_SYSST_CHECK); + & AW88261_BIT_PLL_CHECK; + if (check_val != AW88261_BIT_PLL_CHECK) { + dev_dbg(aw_dev->dev, "check sysst fail, reg_val=0x%04x, check:0x%x", + reg_val, AW88261_BIT_PLL_CHECK); usleep_range(AW88261_2000_US, AW88261_2000_US + 10); } else { return 0; @@ -284,22 +236,22 @@ static void aw88261_reg_force_set(struct aw88261 *aw88261) if (aw88261->frcset_en == AW88261_FRCSET_ENABLE) { /* set FORCE_PWM */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL3_REG, - AW88261_FORCE_PWM_MASK, AW88261_FORCE_PWM_FORCEMINUS_PWM_VALUE); + ~AW88261_FORCE_PWM_MASK, AW88261_FORCE_PWM_FORCEMINUS_PWM_VALUE); /* set BOOST_OS_WIDTH */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL5_REG, - AW88261_BST_OS_WIDTH_MASK, AW88261_BST_OS_WIDTH_50NS_VALUE); + ~AW88261_BST_OS_WIDTH_MASK, AW88261_BST_OS_WIDTH_50NS_VALUE); /* set BURST_LOOPR */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL6_REG, - AW88261_BST_LOOPR_MASK, AW88261_BST_LOOPR_340K_VALUE); + ~AW88261_BST_LOOPR_MASK, AW88261_BST_LOOPR_340K_VALUE); /* set RSQN_DLY */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL7_REG, - AW88261_RSQN_DLY_MASK, AW88261_RSQN_DLY_35NS_VALUE); + ~AW88261_RSQN_DLY_MASK, AW88261_RSQN_DLY_35NS_VALUE); /* set BURST_SSMODE */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL8_REG, - AW88261_BURST_SSMODE_MASK, AW88261_BURST_SSMODE_FAST_VALUE); + ~AW88261_BURST_SSMODE_MASK, AW88261_BURST_SSMODE_FAST_VALUE); /* set BST_BURST */ regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL9_REG, - AW88261_BST_BURST_MASK, AW88261_BST_BURST_30MA_VALUE); + ~AW88261_BST_BURST_MASK, AW88261_BST_BURST_30MA_VALUE); } else { dev_dbg(aw88261->aw_pa->dev, "needn't set reg value"); } @@ -550,7 +502,7 @@ static int aw88261_dev_start(struct aw88261 *aw88261) int ret; if (aw_dev->status == AW88261_DEV_PW_ON) { - dev_info(aw_dev->dev, "already power on"); + dev_dbg(aw_dev->dev, "already power on"); return 0; } @@ -558,9 +510,9 @@ static int aw88261_dev_start(struct aw88261 *aw88261) aw88261_dev_pwd(aw_dev, false); usleep_range(AW88261_2000_US, AW88261_2000_US + 10); - ret = aw88261_dev_check_syspll(aw_dev); + ret = aw88261_dev_configure_syspll(aw88261); if (ret) { - dev_err(aw_dev->dev, "pll check failed cannot start"); + dev_dbg(aw_dev->dev, "pll check failed"); goto pll_check_fail; } @@ -571,7 +523,7 @@ static int aw88261_dev_start(struct aw88261 *aw88261) /* check i2s status */ ret = aw88261_dev_check_sysst(aw_dev); if (ret) { - dev_err(aw_dev->dev, "sysst check failed"); + dev_dbg(aw_dev->dev, "sysst check failed"); goto sysst_check_fail; } @@ -672,31 +624,25 @@ static void aw88261_start_pa(struct aw88261 *aw88261) for (i = 0; i < AW88261_START_RETRIES; i++) { ret = aw88261_reg_update(aw88261, aw88261->phase_sync); if (ret) { - dev_err(aw88261->aw_pa->dev, "fw update failed, cnt:%d\n", i); + dev_dbg(aw88261->aw_pa->dev, + "aw88261_reg_update failed, cnt:%d, ret:%d\n", i, ret); continue; } ret = aw88261_dev_start(aw88261); if (ret) { - dev_err(aw88261->aw_pa->dev, "aw88261 device start failed. retry = %d", i); + dev_dbg(aw88261->aw_pa->dev, + "aw88261_dev_start failed, cnt:%d, ret:%d\n", i, ret); continue; } else { - dev_info(aw88261->aw_pa->dev, "start success\n"); + dev_dbg(aw88261->aw_pa->dev, "start success\n"); break; } } + if (ret != 0) + dev_err(aw88261->aw_pa->dev, "start failure (%d)\n", ret); } -static void aw88261_startup_work(struct work_struct *work) -{ - struct aw88261 *aw88261 = - container_of(work, struct aw88261, start_work.work); - - mutex_lock(&aw88261->lock); - aw88261_start_pa(aw88261); - mutex_unlock(&aw88261->lock); -} - -static void aw88261_start(struct aw88261 *aw88261, bool sync_start) +static void aw88261_start(struct aw88261 *aw88261) { if (aw88261->aw_pa->fw_status != AW88261_DEV_FW_OK) return; @@ -704,14 +650,226 @@ static void aw88261_start(struct aw88261 *aw88261, bool sync_start) if (aw88261->aw_pa->status == AW88261_DEV_PW_ON) return; - if (sync_start == AW88261_SYNC_START) - aw88261_start_pa(aw88261); - else - queue_delayed_work(system_dfl_wq, - &aw88261->start_work, - AW88261_START_WORK_DELAY_MS); + aw88261_start_pa(aw88261); } +static int aw88261_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) +{ + struct snd_soc_component *component = dai->component; + struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + aw88261->bck_inv_value = AW88261_BCKINV_NOT_INVERT_VALUE; + break; + case SND_SOC_DAIFMT_IB_NF: + aw88261->bck_inv_value = AW88261_BCKINV_INVERTED_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported invert mode 0x%x\n", + fmt & SND_SOC_DAIFMT_INV_MASK); + return -EINVAL; + } + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + case SND_SOC_DAIFMT_DSP_A: + aw88261->md_value = AW88261_I2SMD_PHILIPS_STANDARD_VALUE; + break; + case SND_SOC_DAIFMT_MSB: + case SND_SOC_DAIFMT_DSP_B: + aw88261->md_value = AW88261_I2SMD_MSB_JUSTIFIED_VALUE; + break; + case SND_SOC_DAIFMT_LSB: + aw88261->md_value = AW88261_I2SMD_LSB_JUSTIFIED_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported DAI format 0x%x\n", + fmt & SND_SOC_DAIFMT_FORMAT_MASK); + return -EINVAL; + } + + return 0; +} + +static int aw88261_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct snd_soc_component *component = dai->component; + struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + return 0; + + aw88261->cco_mux_value = AW88261_CCO_MUX_BYPASS_VALUE; + switch (params_rate(params)) { + case 8000: + aw88261->sr_value = AW88261_I2SSR_8KHZ_VALUE; + aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE; + break; + case 11025: + aw88261->sr_value = AW88261_I2SSR_11P025KHZ_VALUE; + break; + case 12000: + aw88261->sr_value = AW88261_I2SSR_12KHZ_VALUE; + break; + case 16000: + aw88261->sr_value = AW88261_I2SSR_16KHZ_VALUE; + aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE; + break; + case 22050: + aw88261->sr_value = AW88261_I2SSR_22P05KHZ_VALUE; + break; + case 24000: + aw88261->sr_value = AW88261_I2SSR_24KHZ_VALUE; + break; + case 32000: + aw88261->sr_value = AW88261_I2SSR_32KHZ_VALUE; + aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE; + break; + case 44100: + aw88261->sr_value = AW88261_I2SSR_44P1KHZ_VALUE; + break; + case 48000: + aw88261->sr_value = AW88261_I2SSR_48KHZ_VALUE; + break; + case 96000: + aw88261->sr_value = AW88261_I2SSR_96KHZ_VALUE; + break; + case 192000: + aw88261->sr_value = AW88261_I2SSR_192KHZ_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported sample rate %d\n", + params_rate(params)); + return -EINVAL; + } + + switch (params_width(params)) { + case 16: + aw88261->fs_value = AW88261_I2SFS_16_BITS_VALUE; + break; + case 20: + aw88261->fs_value = AW88261_I2SFS_20_BITS_VALUE; + break; + case 24: + aw88261->fs_value = AW88261_I2SFS_24_BITS_VALUE; + break; + case 32: + aw88261->fs_value = AW88261_I2SFS_32_BITS_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported bit width %d\n", + params_width(params)); + return -EINVAL; + } + + switch (params_physical_width(params)) { + case 16: + aw88261->bck_value = AW88261_I2SBCK_32FS_VALUE; + break; + case 24: + aw88261->bck_value = AW88261_I2SBCK_48FS_VALUE; + break; + case 32: + aw88261->bck_value = AW88261_I2SBCK_64FS_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported physical bit width %d\n", + params_physical_width(params)); + return -EINVAL; + } + + return 0; +} + +static int aw88261_set_tdm_slot(struct snd_soc_dai *dai, + unsigned int tx_mask, unsigned int rx_mask, int slots, int slot_width) +{ + struct snd_soc_component *component = dai->component; + struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); + int chan; + + switch (slots) { + case 0: + /* Just reset everything TDM related to I2S values */ + aw88261->slot_num_value = AW88261_SLOT_NUM_I2S_MODE_VALUE; + aw88261->tdm_bck_value = AW88261_TDM_BCK_UNSET; + aw88261->tx_slotvld_mask = 0 << AW88261_I2S_TX_SLOTVLD_START_BIT; + aw88261->rxl_slotvld_mask = 0 << AW88261_I2S_RXL_SLOTVLD_START_BIT; + aw88261->rxr_slotvld_mask = 1 << AW88261_I2S_RXR_SLOTVLD_START_BIT; + return 0; + case 1: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM1S_VALUE; + break; + case 2: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM2S_VALUE; + break; + case 4: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM4S_VALUE; + break; + case 6: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM6S_VALUE; + break; + case 8: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM8S_VALUE; + break; + case 16: + aw88261->slot_num_value = AW88261_SLOT_NUM_TDM16S_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported slot count %d\n", slots); + return -EINVAL; + } + + switch (slot_width) { + case 16: + aw88261->tdm_bck_value = AW88261_I2SBCK_32FS_VALUE; + break; + case 20: + case 24: + aw88261->tdm_bck_value = AW88261_I2SBCK_48FS_VALUE; + break; + case 32: + aw88261->tdm_bck_value = AW88261_I2SBCK_64FS_VALUE; + break; + default: + dev_err(aw88261->aw_pa->dev, "unsupported slot width %d\n", + slot_width); + return -EINVAL; + } + + if (tx_mask != 0) { + if ((chan = __ffs(tx_mask)) > 16) + return -EINVAL; + + aw88261->tx_slotvld_mask = chan << AW88261_I2S_TX_SLOTVLD_START_BIT; + } + + if (rx_mask != 0) { + if ((chan = __ffs(rx_mask)) > 16) + return -EINVAL; + + aw88261->rxl_slotvld_mask = chan << AW88261_I2S_RXL_SLOTVLD_START_BIT; + } + + if ((rx_mask & ~BIT(chan)) != 0) { + if ((chan = __ffs(rx_mask & ~BIT(chan))) > 16) + return -EINVAL; + + aw88261->rxr_slotvld_mask = chan << AW88261_I2S_RXR_SLOTVLD_START_BIT; + } + + return 0; +} + +static const struct snd_soc_dai_ops aw88261_dai_ops = { + .set_fmt = aw88261_set_fmt, + .hw_params = aw88261_hw_params, + .set_tdm_slot = aw88261_set_tdm_slot, +}; + static struct snd_soc_dai_driver aw88261_dai[] = { { .name = "aw88261-aif", @@ -730,78 +888,10 @@ static struct snd_soc_dai_driver aw88261_dai[] = { .rates = AW88261_RATES, .formats = AW88261_FORMATS, }, + .ops = &aw88261_dai_ops, }, }; -static int aw88261_get_fade_in_time(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); - struct aw_device *aw_dev = aw88261->aw_pa; - - ucontrol->value.integer.value[0] = aw_dev->fade_in_time; - - return 0; -} - -static int aw88261_set_fade_in_time(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); - struct soc_mixer_control *mc = - (struct soc_mixer_control *)kcontrol->private_value; - struct aw_device *aw_dev = aw88261->aw_pa; - int time; - - time = ucontrol->value.integer.value[0]; - - if (time < mc->min || time > mc->max) - return -EINVAL; - - if (time != aw_dev->fade_in_time) { - aw_dev->fade_in_time = time; - return 1; - } - - return 0; -} - -static int aw88261_get_fade_out_time(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); - struct aw_device *aw_dev = aw88261->aw_pa; - - ucontrol->value.integer.value[0] = aw_dev->fade_out_time; - - return 0; -} - -static int aw88261_set_fade_out_time(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); - struct soc_mixer_control *mc = - (struct soc_mixer_control *)kcontrol->private_value; - struct aw_device *aw_dev = aw88261->aw_pa; - int time; - - time = ucontrol->value.integer.value[0]; - if (time < mc->min || time > mc->max) - return -EINVAL; - - if (time != aw_dev->fade_out_time) { - aw_dev->fade_out_time = time; - return 1; - } - - return 0; -} - static int aw88261_dev_set_profile_index(struct aw_device *aw_dev, int index) { /* check the index whether is valid */ @@ -880,7 +970,7 @@ static int aw88261_profile_set(struct snd_kcontrol *kcontrol, if (aw88261->aw_pa->status) { aw88261_dev_stop(aw88261->aw_pa); - aw88261_start(aw88261, AW88261_SYNC_START); + aw88261_start(aw88261); } mutex_unlock(&aw88261->lock); @@ -895,7 +985,8 @@ static int aw88261_volume_get(struct snd_kcontrol *kcontrol, struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec); struct aw_volume_desc *vol_desc = &aw88261->aw_pa->volume_desc; - ucontrol->value.integer.value[0] = vol_desc->ctl_volume; + ucontrol->value.integer.value[0] = + (AW88261_MUTE_VOL - vol_desc->ctl_volume) / 2; return 0; } @@ -908,13 +999,13 @@ static int aw88261_volume_set(struct snd_kcontrol *kcontrol, struct aw_volume_desc *vol_desc = &aw88261->aw_pa->volume_desc; struct soc_mixer_control *mc = (struct soc_mixer_control *)kcontrol->private_value; - int value; - - value = ucontrol->value.integer.value[0]; + int value = ucontrol->value.integer.value[0]; if (value < mc->min || value > mc->max) return -EINVAL; + value = AW88261_MUTE_VOL - (value * 2); + if (vol_desc->ctl_volume != value) { vol_desc->ctl_volume = value; aw88261_dev_set_volume(aw88261->aw_pa, vol_desc->ctl_volume); @@ -925,48 +1016,18 @@ static int aw88261_volume_set(struct snd_kcontrol *kcontrol, return 0; } -static int aw88261_get_fade_step(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *codec = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec); - - ucontrol->value.integer.value[0] = aw88261->aw_pa->fade_step; - - return 0; -} - -static int aw88261_set_fade_step(struct snd_kcontrol *kcontrol, - struct snd_ctl_elem_value *ucontrol) -{ - struct snd_soc_component *codec = snd_kcontrol_chip(kcontrol); - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec); - struct soc_mixer_control *mc = - (struct soc_mixer_control *)kcontrol->private_value; - int value; - - value = ucontrol->value.integer.value[0]; - if (value < mc->min || value > mc->max) - return -EINVAL; - - if (aw88261->aw_pa->fade_step != value) { - aw88261->aw_pa->fade_step = value; - return 1; - } - - return 0; -} +/* + * The field contains 4 bits in units of 6dB + 6 bits in units of 0.125dB + * which is too precise for TLV (!) so we have to multiply the scale by 2. + * + * The range is clamped at -90dB to prevent overflowing the 4-bit part. + */ +static const DECLARE_TLV_DB_SCALE(volume_tlv, -9000, 25, 0); static const struct snd_kcontrol_new aw88261_controls[] = { - SOC_SINGLE_EXT("PCM Playback Volume", AW88261_SYSCTRL2_REG, - 6, AW88261_MUTE_VOL, 0, aw88261_volume_get, - aw88261_volume_set), - SOC_SINGLE_EXT("Fade Step", 0, 0, AW88261_MUTE_VOL, 0, - aw88261_get_fade_step, aw88261_set_fade_step), - SOC_SINGLE_EXT("Volume Ramp Up Step", 0, 0, FADE_TIME_MAX, FADE_TIME_MIN, - aw88261_get_fade_in_time, aw88261_set_fade_in_time), - SOC_SINGLE_EXT("Volume Ramp Down Step", 0, 0, FADE_TIME_MAX, FADE_TIME_MIN, - aw88261_get_fade_out_time, aw88261_set_fade_out_time), + SOC_SINGLE_EXT_TLV("PCM Playback Volume", AW88261_SYSCTRL2_REG, + 6, AW88261_CTL_MAX_VOL, 1, + aw88261_volume_get, aw88261_volume_set, volume_tlv), AW88261_PROFILE_EXT("Profile Set", aw88261_profile_info, aw88261_profile_get, aw88261_profile_set), }; @@ -980,7 +1041,7 @@ static int aw88261_playback_event(struct snd_soc_dapm_widget *w, mutex_lock(&aw88261->lock); switch (event) { case SND_SOC_DAPM_PRE_PMU: - aw88261_start(aw88261, AW88261_ASYNC_START); + aw88261_start(aw88261); break; case SND_SOC_DAPM_POST_PMD: aw88261_dev_stop(aw88261->aw_pa); @@ -1057,8 +1118,6 @@ static int aw88261_dev_init(struct aw88261 *aw88261, struct aw_container *aw_cfg if (ret) return ret; - aw_dev->fade_in_time = AW88261_500_US; - aw_dev->fade_out_time = AW88261_500_US; aw_dev->prof_cur = AW88261_INIT_PROFILE; aw_dev->prof_index = AW88261_INIT_PROFILE; @@ -1094,6 +1153,7 @@ static int aw88261_dev_init(struct aw88261 *aw88261, struct aw_container *aw_cfg static int aw88261_request_firmware_file(struct aw88261 *aw88261) { const struct firmware *cont = NULL; + struct aw_container *aw_cfg; const char *fw_name; int ret; @@ -1111,15 +1171,17 @@ static int aw88261_request_firmware_file(struct aw88261 *aw88261) dev_info(aw88261->aw_pa->dev, "loaded %s - size: %zu\n", fw_name, cont ? cont->size : 0); - aw88261->aw_cfg = devm_kzalloc(aw88261->aw_pa->dev, cont->size + sizeof(int), GFP_KERNEL); - if (!aw88261->aw_cfg) { + aw_cfg = devm_kzalloc(aw88261->aw_pa->dev, struct_size(aw_cfg, data, cont->size), GFP_KERNEL); + if (!aw_cfg) { release_firmware(cont); return -ENOMEM; } - aw88261->aw_cfg->len = (int)cont->size; - memcpy(aw88261->aw_cfg->data, cont->data, cont->size); + aw_cfg->len = (int)cont->size; + memcpy(aw_cfg->data, cont->data, cont->size); release_firmware(cont); + aw88261->aw_cfg = aw_cfg; + ret = aw88395_dev_load_acf_check(aw88261->aw_pa, aw88261->aw_cfg); if (ret) { dev_err(aw88261->aw_pa->dev, "load [%s] failed !", fw_name); @@ -1142,8 +1204,6 @@ static int aw88261_codec_probe(struct snd_soc_component *component) struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); int ret; - INIT_DELAYED_WORK(&aw88261->start_work, aw88261_startup_work); - ret = aw88261_request_firmware_file(aw88261); if (ret) return dev_err_probe(aw88261->aw_pa->dev, ret, @@ -1167,16 +1227,8 @@ static int aw88261_codec_probe(struct snd_soc_component *component) return ret; } -static void aw88261_codec_remove(struct snd_soc_component *aw_codec) -{ - struct aw88261 *aw88261 = snd_soc_component_get_drvdata(aw_codec); - - cancel_delayed_work_sync(&aw88261->start_work); -} - static const struct snd_soc_component_driver soc_codec_dev_aw88261 = { .probe = aw88261_codec_probe, - .remove = aw88261_codec_remove, }; static void aw88261_parse_channel_dt(struct aw88261 *aw88261) @@ -1208,7 +1260,7 @@ static int aw88261_init(struct aw88261 *aw88261, struct i2c_client *i2c, struct return ret; } if (chip_id != AW88261_CHIP_ID) { - dev_err(&i2c->dev, "unsupported device"); + dev_err(&i2c->dev, "unsupported device id = %x", chip_id); return -ENXIO; } @@ -1229,8 +1281,7 @@ static int aw88261_init(struct aw88261 *aw88261, struct i2c_client *i2c, struct aw_dev->prof_info.prof_type = AW88395_DEV_NONE_TYPE_ID; aw_dev->channel = 0; aw_dev->fw_status = AW88261_DEV_FW_FAILED; - aw_dev->fade_step = AW88261_VOLUME_STEP_DB; - aw_dev->volume_desc.ctl_volume = AW88261_VOL_DEFAULT_VALUE; + aw_dev->volume_desc.ctl_volume = AW88261_CTL_DEFAULT_VOL; aw_dev->volume_desc.mute_volume = AW88261_MUTE_VOL; aw88261_parse_channel_dt(aw88261); @@ -1249,6 +1300,17 @@ static int aw88261_i2c_probe(struct i2c_client *i2c) if (!aw88261) return -ENOMEM; + /* set defaults */ + aw88261->slot_num_value = AW88261_SLOT_NUM_I2S_MODE_VALUE; + aw88261->sr_value = AW88261_I2SSR_48KHZ_VALUE; + aw88261->cco_mux_value = AW88261_CCO_MUX_BYPASS_VALUE; + aw88261->fs_value = AW88261_I2SFS_24_BITS_VALUE; + aw88261->bck_value = AW88261_I2SBCK_64FS_VALUE; + aw88261->bck_inv_value = AW88261_BCKINV_NOT_INVERT_VALUE; + aw88261->tdm_bck_value = AW88261_TDM_BCK_UNSET; + aw88261->md_value = AW88261_I2SMD_PHILIPS_STANDARD_VALUE; + aw88261->rxr_slotvld_mask = 1 << AW88261_I2S_RXR_SLOTVLD_START_BIT; + mutex_init(&aw88261->lock); i2c_set_clientdata(i2c, aw88261); @@ -1274,7 +1336,7 @@ static int aw88261_i2c_probe(struct i2c_client *i2c) } static const struct i2c_device_id aw88261_i2c_id[] = { - { "aw88261" }, + { .name = "aw88261" }, { } }; MODULE_DEVICE_TABLE(i2c, aw88261_i2c_id); diff --git a/sound/soc/codecs/aw88261.h b/sound/soc/codecs/aw88261.h index 1fee589608d6..f16f9f8c40a2 100644 --- a/sound/soc/codecs/aw88261.h +++ b/sound/soc/codecs/aw88261.h @@ -116,6 +116,19 @@ #define AW88261_VCALK_SHIFT (0) #define AW88261_VCALKL_SHIFT (0) +#define AW88261_BCKINV_START_BIT (4) +#define AW88261_BCKINV_BITS_LEN (1) +#define AW88261_BCKINV_MASK \ + (~(((1<> AW88261_VOL_6DB_START) * \ @@ -403,11 +551,6 @@ .put = profile_set, \ } -enum { - AW88261_SYNC_START = 0, - AW88261_ASYNC_START, -}; - enum aw88261_id { AW88261_CHIP_ID = 0x2113, }; @@ -442,7 +585,6 @@ struct aw88261 { struct aw_device *aw_pa; struct mutex lock; struct gpio_desc *reset_gpio; - struct delayed_work start_work; struct regmap *regmap; struct aw_container *aw_cfg; @@ -451,6 +593,19 @@ struct aw88261 { unsigned int mute_st; unsigned int amppd_st; + unsigned int sr_value; + unsigned int cco_mux_value; + unsigned int fs_value; + unsigned int bck_value; + unsigned int bck_inv_value; + unsigned int tdm_bck_value; + unsigned int md_value; + + unsigned int slot_num_value; + unsigned int tx_slotvld_mask; + unsigned int rxl_slotvld_mask; + unsigned int rxr_slotvld_mask; + bool phase_sync; }; From d571dd42d3d94b7904b67f28462707be4ce71022 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 21:32:11 +0200 Subject: [PATCH 5/8] ASoC: qcom: q6apm-lpass-dais: start the graph at prepare The DAPM power-up sequence runs during snd_pcm prepare, but the BE port graph is only started at trigger time. A codec that powers up synchronously from a DAPM widget event and needs a running bit clock at that point - such as aw88261 since commit caea99ac809d ("ASoC: codecs: aw88261: remove async start") - can therefore never see a live clock: its power-up check runs before the trigger and fails on every stream start. Start the graph at the end of prepare instead, mirroring what q6afe_dai_prepare() does on the legacy stack, so the interface clocks already run when DAPM powers up the codec. The FE side already starts its own graph at prepare in q6apm_dai_prepare(); only the BE waited for trigger. The trigger-time start is kept as a fallback, guarded by is_port_started. Tested on the Fairphone (Gen. 6) - 2x aw88261 on Senary MI2S: without this the amplifiers fail to power up with SYSST reporting "no clock" on every stream start; with it they start synchronously, including for the first short stream of the boot. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- sound/soc/qcom/qdsp6/q6apm-lpass-dais.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c index 006b283484d9..06946add653a 100644 --- a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c +++ b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c @@ -224,6 +224,21 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s dev_err(dai->dev, "Failed to prepare Graph %d\n", rc); goto err; } + + /* + * Start the port already at prepare, like q6afe does: this starts + * the interface clocks before the DAPM power-up sequence runs, so + * codecs that need a live BCLK at power-up (e.g. aw88261) can + * start synchronously. The trigger callback keeps its start as a + * no-op fallback via is_port_started. + */ + rc = q6apm_graph_start(dai_data->graph[dai->id]); + if (rc < 0) { + dev_err(dai->dev, "Failed to start APM port %d\n", dai->id); + goto err; + } + dai_data->is_port_started[dai->id] = true; + return 0; err: if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { From 52c785c31534cd59d2107133ad6ad8c3a4c406bc Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 5 Jul 2026 21:32:11 +0200 Subject: [PATCH 6/8] arm64: dts: qcom: milos-fairphone-fp6: enable Senary MI2S dai-link Uncomment the Senary MI2S speaker dai-link. The sc8280xp machine driver gained Senary MI2S support and the aw88261 power-up check is fixed (both carried on this branch), so the link is functional: both AW88261 amps load their ACF firmware and play. This restores the enablement that was lost when the audio carries were rebased from the v7.0.8 stack onto v7.1.2-milos: only the two driver patches were carried over, leaving the sound card with zero dai-links (the card registers but stays empty - no PCMs, no controls). Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts | 2 -- 1 file changed, 2 deletions(-) diff --git a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts index e5944a1b255f..08852107c519 100644 --- a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts +++ b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts @@ -216,7 +216,6 @@ pinctrl-names = "default", "sleep"; - /* i2s-dai-link { link-name = "Senary MI2S Playback"; @@ -232,7 +231,6 @@ sound-dai = <&q6apm>; }; }; - */ }; thermal-zones { From 50cd4e823f9cedebac3e2a4e4bced88613e8466f Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 14 Jul 2026 19:26:13 +0200 Subject: [PATCH 7/8] iio: magnetometer: add support for QST QMC6308 The QST QMC6308 is a 3-axis AMR magnetometer on I2C, a single-supply 4-pin WLCSP part with no interrupt/DRDY pin, found e.g. in the Fairphone 6. Its register map differs from the QMC5883L (chip ID at 0x00 instead of 0x0D, data at 0x01..0x06, and the range field living in control register 2), so add a separate driver rather than extending the QMC5883L driver. Support raw X/Y/Z reads, output data rates 10/50/100/200 Hz, field ranges +-30/12/8/2 Gauss, filter oversampling ratios (OSR1) 8/4/2/1, the mount matrix, and runtime PM. The second-stage decimation filter (OSR2) is left at its power-on default. The package has no DRDY pin, so there is no trigger support. Run measurements in the chip's periodic "normal" mode paced by the DRDY flag rather than in its one-shot "single" mode: the datasheet specifies no conversion time that could bound a one-shot wait, while normal mode is paced by the specified output data rates, which also keeps the sampling_frequency ABI meaningful. Runtime PM puts the chip into its suspend mode after 500 ms without a reading, dropping supply current from tens-to-hundreds of microamps to 2-3 uA (datasheet Table 2). The suspended chip retains its registers and keeps responding on I2C, so resuming only rewrites the mode field and discards one stale sample, and configuration changes can be applied even while suspended. VDD is left enabled across runtime suspend: the on-chip suspend draw is already negligible, and register retention is what keeps the resume path trivial. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- drivers/iio/magnetometer/Kconfig | 11 + drivers/iio/magnetometer/Makefile | 2 + drivers/iio/magnetometer/qmc6308.c | 590 +++++++++++++++++++++++++++++ 3 files changed, 603 insertions(+) create mode 100644 drivers/iio/magnetometer/qmc6308.c diff --git a/drivers/iio/magnetometer/Kconfig b/drivers/iio/magnetometer/Kconfig index fb313e591e85..7cd4d751e1d2 100644 --- a/drivers/iio/magnetometer/Kconfig +++ b/drivers/iio/magnetometer/Kconfig @@ -198,6 +198,17 @@ config INFINEON_TLV493D To compile this driver as a module, choose M here: the module will be called tlv493d. +config QMC6308 + tristate "QST QMC6308 3-Axis Magnetic Sensor" + depends on I2C + select REGMAP_I2C + help + Say Y here to add support for the QST QMC6308 3-Axis + Magnetic Sensor. + + To compile this driver as a module, choose M here: the + module will be called qmc6308. + config SENSORS_HMC5843 tristate select IIO_BUFFER diff --git a/drivers/iio/magnetometer/Makefile b/drivers/iio/magnetometer/Makefile index 5bd227f8c120..59ea5cf17287 100644 --- a/drivers/iio/magnetometer/Makefile +++ b/drivers/iio/magnetometer/Makefile @@ -26,6 +26,8 @@ obj-$(CONFIG_IIO_ST_MAGN_SPI_3AXIS) += st_magn_spi.o obj-$(CONFIG_INFINEON_TLV493D) += tlv493d.o +obj-$(CONFIG_QMC6308) += qmc6308.o + obj-$(CONFIG_SENSORS_HMC5843) += hmc5843_core.o obj-$(CONFIG_SENSORS_HMC5843_I2C) += hmc5843_i2c.o obj-$(CONFIG_SENSORS_HMC5843_SPI) += hmc5843_spi.o diff --git a/drivers/iio/magnetometer/qmc6308.c b/drivers/iio/magnetometer/qmc6308.c new file mode 100644 index 000000000000..ceb4b98402bb --- /dev/null +++ b/drivers/iio/magnetometer/qmc6308.c @@ -0,0 +1,590 @@ +// SPDX-License-Identifier: GPL-2.0 OR BSD-2-Clause +/* + * Support for QST QMC6308 3-Axis Magnetic Sensor on I2C bus. + * + * Copyright (C) 2026 Jorijn van der Graaf + * + * Datasheet available at + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#define QMC6308_REG_ID 0x00 +#define QMC6308_REG_X_LSB 0x01 +#define QMC6308_REG_STATUS 0x09 +#define QMC6308_REG_CTRL1 0x0A +#define QMC6308_REG_CTRL2 0x0B +#define QMC6308_REG_CTRL3 0x0D +#define QMC6308_REG_CTRL4 0x29 + +#define QMC6308_CHIP_ID 0x80 + +/* Control register 1 */ +#define QMC6308_MODE_MASK GENMASK(1, 0) +#define QMC6308_ODR_MASK GENMASK(3, 2) +#define QMC6308_OSR1_MASK GENMASK(5, 4) +#define QMC6308_OSR2_MASK GENMASK(7, 6) + +#define QMC6308_MODE_SUSPEND 0x00 +#define QMC6308_MODE_NORMAL 0x01 + +#define QMC6308_ODR_10HZ 0x00 +#define QMC6308_ODR_50HZ 0x01 +#define QMC6308_ODR_100HZ 0x02 +#define QMC6308_ODR_200HZ 0x03 + +#define QMC6308_OSR1_8 0x00 +#define QMC6308_OSR1_4 0x01 +#define QMC6308_OSR1_2 0x02 +#define QMC6308_OSR1_1 0x03 + +/* Control register 2 */ +#define QMC6308_SET_RESET_MASK GENMASK(1, 0) +#define QMC6308_RNG_MASK GENMASK(3, 2) +#define QMC6308_SELF_TEST BIT(6) +#define QMC6308_SOFT_RST BIT(7) + +#define QMC6308_SET_RESET_ON 0x00 + +#define QMC6308_RNG_30G 0x00 +#define QMC6308_RNG_12G 0x01 +#define QMC6308_RNG_8G 0x02 +#define QMC6308_RNG_2G 0x03 + +/* Status register */ +#define QMC6308_STATUS_DRDY BIT(0) +#define QMC6308_STATUS_OVFL BIT(1) + +/* + * Power-on completion time (datasheet Table 7), also used as a + * conservative bound after soft reset, for which the datasheet + * gives no figure. + */ +#define QMC6308_POR_US 250 + +#define QMC6308_AUTOSUSPEND_DELAY_MS 500 + +struct qmc6308_data { + struct regmap *regmap; + /* + * Protect data->range/odr/osr. + * Protect poll and read during measurement (reading the status + * register clears DRDY). + */ + struct mutex mutex; + struct iio_mount_matrix orientation; + u8 range; + u8 odr; + u8 osr; +}; + +enum qmc6308_axis { + QMC6308_AXIS_X, + QMC6308_AXIS_Y, + QMC6308_AXIS_Z, +}; + +static const int qmc6308_odr_avail[] = { + [QMC6308_ODR_10HZ] = 10, + [QMC6308_ODR_50HZ] = 50, + [QMC6308_ODR_100HZ] = 100, + [QMC6308_ODR_200HZ] = 200, +}; + +static const int qmc6308_osr1_avail[] = { + [QMC6308_OSR1_8] = 8, + [QMC6308_OSR1_4] = 4, + [QMC6308_OSR1_2] = 2, + [QMC6308_OSR1_1] = 1, +}; + +/* + * Sensitivity is 1000/2500/3750/15000 LSB/Gauss for the + * +-30/12/8/2 Gauss ranges respectively. + */ +static const int qmc6308_scales[][2] = { + [QMC6308_RNG_30G] = { 0, 1000000 }, + [QMC6308_RNG_12G] = { 0, 400000 }, + [QMC6308_RNG_8G] = { 0, 266667 }, + [QMC6308_RNG_2G] = { 0, 66667 }, +}; + +static int qmc6308_set_mode(struct qmc6308_data *data, unsigned int mode) +{ + return regmap_update_bits(data->regmap, QMC6308_REG_CTRL1, + QMC6308_MODE_MASK, + FIELD_PREP(QMC6308_MODE_MASK, mode)); +} + +static int qmc6308_take_measurement(struct iio_dev *indio_dev, int index, + int *val) +{ + struct qmc6308_data *data = iio_priv(indio_dev); + struct regmap *map = data->regmap; + struct device *dev = regmap_get_device(map); + unsigned int status; + __le16 buf[3]; + int ret; + + ret = pm_runtime_resume_and_get(dev); + if (ret) { + /* EACCES means a read raced runtime PM disable on suspend */ + if (ret != -EACCES) + dev_err(dev, "Failed to power on (%d)\n", ret); + return ret; + } + + scoped_guard(mutex, &data->mutex) { + /* 50ms headroom over the slowest ODR (10Hz) */ + ret = regmap_read_poll_timeout(map, QMC6308_REG_STATUS, + status, + (status & QMC6308_STATUS_DRDY), + 2 * USEC_PER_MSEC, + 150 * USEC_PER_MSEC); + if (ret) + goto out_rpm_put; + + ret = regmap_bulk_read(map, QMC6308_REG_X_LSB, buf, + sizeof(buf)); + if (ret) + goto out_rpm_put; + + if (status & QMC6308_STATUS_OVFL) + ret = -ERANGE; + } + +out_rpm_put: + pm_runtime_put_autosuspend(dev); + if (ret) + return ret; + + *val = (s16)le16_to_cpu(buf[index]); + + return 0; +} + +static int qmc6308_read_raw(struct iio_dev *indio_dev, + const struct iio_chan_spec *chan, + int *val, int *val2, long mask) +{ + struct qmc6308_data *data = iio_priv(indio_dev); + int ret; + + switch (mask) { + case IIO_CHAN_INFO_RAW: + ret = qmc6308_take_measurement(indio_dev, chan->address, val); + if (ret) + return ret; + return IIO_VAL_INT; + case IIO_CHAN_INFO_SCALE: { + guard(mutex)(&data->mutex); + + *val = qmc6308_scales[data->range][0]; + *val2 = qmc6308_scales[data->range][1]; + + return IIO_VAL_INT_PLUS_NANO; + } + case IIO_CHAN_INFO_SAMP_FREQ: { + guard(mutex)(&data->mutex); + + *val = qmc6308_odr_avail[data->odr]; + + return IIO_VAL_INT; + } + case IIO_CHAN_INFO_OVERSAMPLING_RATIO: { + guard(mutex)(&data->mutex); + + *val = qmc6308_osr1_avail[data->osr]; + + return IIO_VAL_INT; + } + default: + return -EINVAL; + } +} + +static int qmc6308_write_raw(struct iio_dev *indio_dev, + const struct iio_chan_spec *chan, + int val, int val2, long mask) +{ + struct qmc6308_data *data = iio_priv(indio_dev); + unsigned int status; + unsigned int i; + int ret; + + switch (mask) { + case IIO_CHAN_INFO_SCALE: { + if (val != 0) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(qmc6308_scales); i++) { + if (val2 == qmc6308_scales[i][1]) + break; + } + if (i == ARRAY_SIZE(qmc6308_scales)) + return -EINVAL; + + guard(mutex)(&data->mutex); + + ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL2, + QMC6308_RNG_MASK, + FIELD_PREP(QMC6308_RNG_MASK, i)); + if (ret) + return ret; + + data->range = i; + + /* + * The data registers still hold (and DRDY still + * advertises) a sample converted at the previous range; + * discard it so that the next read returns data matching + * the new scale. + */ + return regmap_read(data->regmap, QMC6308_REG_STATUS, + &status); + } + case IIO_CHAN_INFO_SAMP_FREQ: { + for (i = 0; i < ARRAY_SIZE(qmc6308_odr_avail); i++) { + if (val == qmc6308_odr_avail[i]) + break; + } + if (i == ARRAY_SIZE(qmc6308_odr_avail)) + return -EINVAL; + + guard(mutex)(&data->mutex); + + ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL1, + QMC6308_ODR_MASK, + FIELD_PREP(QMC6308_ODR_MASK, i)); + if (ret) + return ret; + + data->odr = i; + + return 0; + } + case IIO_CHAN_INFO_OVERSAMPLING_RATIO: { + for (i = 0; i < ARRAY_SIZE(qmc6308_osr1_avail); i++) { + if (val == qmc6308_osr1_avail[i]) + break; + } + if (i == ARRAY_SIZE(qmc6308_osr1_avail)) + return -EINVAL; + + guard(mutex)(&data->mutex); + + ret = regmap_update_bits(data->regmap, QMC6308_REG_CTRL1, + QMC6308_OSR1_MASK, + FIELD_PREP(QMC6308_OSR1_MASK, i)); + if (ret) + return ret; + + data->osr = i; + + return 0; + } + default: + return -EINVAL; + } +} + +static int qmc6308_read_avail(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + const int **vals, int *type, int *length, + long mask) +{ + switch (mask) { + case IIO_CHAN_INFO_SAMP_FREQ: + *vals = qmc6308_odr_avail; + *type = IIO_VAL_INT; + *length = ARRAY_SIZE(qmc6308_odr_avail); + return IIO_AVAIL_LIST; + case IIO_CHAN_INFO_OVERSAMPLING_RATIO: + *vals = qmc6308_osr1_avail; + *type = IIO_VAL_INT; + *length = ARRAY_SIZE(qmc6308_osr1_avail); + return IIO_AVAIL_LIST; + case IIO_CHAN_INFO_SCALE: + *vals = (const int *)qmc6308_scales; + *type = IIO_VAL_INT_PLUS_NANO; + *length = ARRAY_SIZE(qmc6308_scales) * 2; + return IIO_AVAIL_LIST; + default: + return -EINVAL; + } +} + +static int qmc6308_write_raw_get_fmt(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, + long mask) +{ + switch (mask) { + case IIO_CHAN_INFO_SCALE: + return IIO_VAL_INT_PLUS_NANO; + default: + return IIO_VAL_INT; + } +} + +static const struct iio_mount_matrix * +qmc6308_get_mount_matrix(const struct iio_dev *indio_dev, + const struct iio_chan_spec *chan) +{ + struct qmc6308_data *data = iio_priv(indio_dev); + + return &data->orientation; +} + +static const struct iio_chan_spec_ext_info qmc6308_ext_info[] = { + IIO_MOUNT_MATRIX(IIO_SHARED_BY_DIR, qmc6308_get_mount_matrix), + { } +}; + +static const struct iio_info qmc6308_info = { + .read_raw = qmc6308_read_raw, + .write_raw = qmc6308_write_raw, + .read_avail = qmc6308_read_avail, + .write_raw_get_fmt = qmc6308_write_raw_get_fmt, +}; + +static int qmc6308_init(struct qmc6308_data *data) +{ + struct regmap *map = data->regmap; + unsigned int reg; + int ret; + + ret = regmap_read(map, QMC6308_REG_ID, ®); + if (ret) + return ret; + + /* Allow unknown IDs so that fallback compatibles work */ + if (reg != QMC6308_CHIP_ID) + dev_warn(regmap_get_device(map), + "Unknown chip id: 0x%02x, continuing\n", reg); + + /* The SOFT_RST bit is not auto-cleared and must be written back 0 */ + ret = regmap_write(map, QMC6308_REG_CTRL2, QMC6308_SOFT_RST); + if (ret) + return ret; + + fsleep(QMC6308_POR_US); + + data->range = QMC6308_RNG_30G; + + ret = regmap_write(map, QMC6308_REG_CTRL2, + FIELD_PREP(QMC6308_SET_RESET_MASK, + QMC6308_SET_RESET_ON) | + FIELD_PREP(QMC6308_RNG_MASK, data->range)); + if (ret) + return ret; + + data->odr = QMC6308_ODR_50HZ; + data->osr = QMC6308_OSR1_8; + + return regmap_write(map, QMC6308_REG_CTRL1, + FIELD_PREP(QMC6308_MODE_MASK, + QMC6308_MODE_NORMAL) | + FIELD_PREP(QMC6308_ODR_MASK, data->odr) | + FIELD_PREP(QMC6308_OSR1_MASK, data->osr)); +} + +static void qmc6308_power_down_action(void *priv) +{ + struct qmc6308_data *data = priv; + + if (!pm_runtime_status_suspended(regmap_get_device(data->regmap))) + qmc6308_set_mode(data, QMC6308_MODE_SUSPEND); +} + +static bool qmc6308_volatile_reg(struct device *dev, unsigned int reg) +{ + return reg >= QMC6308_REG_X_LSB && reg <= QMC6308_REG_STATUS; +} + +static bool qmc6308_writable_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case QMC6308_REG_CTRL1: + case QMC6308_REG_CTRL2: + case QMC6308_REG_CTRL3: + case QMC6308_REG_CTRL4: + return true; + default: + return false; + } +} + +static const struct regmap_config qmc6308_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = QMC6308_REG_CTRL4, + .cache_type = REGCACHE_MAPLE, + .volatile_reg = qmc6308_volatile_reg, + .writeable_reg = qmc6308_writable_reg, +}; + +#define QMC6308_CHANNEL(_axis) \ + { \ + .type = IIO_MAGN, \ + .modified = 1, \ + .channel2 = IIO_MOD_##_axis, \ + .address = QMC6308_AXIS_##_axis, \ + .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \ + .info_mask_shared_by_type = \ + BIT(IIO_CHAN_INFO_SCALE) | \ + BIT(IIO_CHAN_INFO_SAMP_FREQ) | \ + BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \ + .info_mask_shared_by_type_available = \ + BIT(IIO_CHAN_INFO_SCALE) | \ + BIT(IIO_CHAN_INFO_SAMP_FREQ) | \ + BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \ + .ext_info = qmc6308_ext_info, \ + } + +static const struct iio_chan_spec qmc6308_channels[] = { + QMC6308_CHANNEL(X), + QMC6308_CHANNEL(Y), + QMC6308_CHANNEL(Z), +}; + +static int qmc6308_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct qmc6308_data *data; + struct iio_dev *indio_dev; + struct regmap *map; + int ret; + + indio_dev = devm_iio_device_alloc(dev, sizeof(*data)); + if (!indio_dev) + return -ENOMEM; + + i2c_set_clientdata(client, indio_dev); + + map = devm_regmap_init_i2c(client, &qmc6308_regmap_config); + if (IS_ERR(map)) + return dev_err_probe(dev, PTR_ERR(map), + "regmap initialization failed\n"); + + ret = devm_regulator_get_enable(dev, "vdd"); + if (ret) + return dev_err_probe(dev, ret, + "Failed to enable VDD regulator\n"); + + fsleep(QMC6308_POR_US); + + data = iio_priv(indio_dev); + data->regmap = map; + + ret = devm_mutex_init(dev, &data->mutex); + if (ret) + return ret; + + ret = iio_read_mount_matrix(dev, &data->orientation); + if (ret) + return dev_err_probe(dev, ret, + "Failed to read mount matrix\n"); + + indio_dev->name = "qmc6308"; + indio_dev->info = &qmc6308_info; + indio_dev->channels = qmc6308_channels; + indio_dev->num_channels = ARRAY_SIZE(qmc6308_channels); + indio_dev->modes = INDIO_DIRECT_MODE; + + ret = qmc6308_init(data); + if (ret) + return dev_err_probe(dev, ret, "qmc6308 init failed\n"); + + pm_runtime_set_active(dev); + + ret = devm_add_action_or_reset(dev, qmc6308_power_down_action, data); + if (ret) + return ret; + + pm_runtime_get_noresume(dev); + pm_runtime_use_autosuspend(dev); + pm_runtime_set_autosuspend_delay(dev, QMC6308_AUTOSUSPEND_DELAY_MS); + ret = devm_pm_runtime_enable(dev); + if (ret) + return ret; + + pm_runtime_put_autosuspend(dev); + + return devm_iio_device_register(dev, indio_dev); +} + +static int qmc6308_runtime_suspend(struct device *dev) +{ + struct iio_dev *indio_dev = dev_get_drvdata(dev); + struct qmc6308_data *data = iio_priv(indio_dev); + + return qmc6308_set_mode(data, QMC6308_MODE_SUSPEND); +} + +static int qmc6308_runtime_resume(struct device *dev) +{ + struct iio_dev *indio_dev = dev_get_drvdata(dev); + struct qmc6308_data *data = iio_priv(indio_dev); + unsigned int status; + int ret; + + ret = qmc6308_set_mode(data, QMC6308_MODE_NORMAL); + if (ret) + return ret; + + /* + * DRDY may still be set for a sample converted before the last + * suspend; clear it so the next read waits for fresh data. + */ + return regmap_read(data->regmap, QMC6308_REG_STATUS, &status); +} + +static DEFINE_RUNTIME_DEV_PM_OPS(qmc6308_pm_ops, qmc6308_runtime_suspend, + qmc6308_runtime_resume, NULL); + +static const struct of_device_id qmc6308_match[] = { + { .compatible = "qstcorp,qmc6308" }, + { } +}; +MODULE_DEVICE_TABLE(of, qmc6308_match); + +static const struct i2c_device_id qmc6308_id[] = { + { .name = "qmc6308" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, qmc6308_id); + +static struct i2c_driver qmc6308_driver = { + .driver = { + .name = "qmc6308", + .of_match_table = qmc6308_match, + .pm = pm_ptr(&qmc6308_pm_ops), + }, + .id_table = qmc6308_id, + .probe = qmc6308_probe, +}; +module_i2c_driver(qmc6308_driver); + +MODULE_DESCRIPTION("QST QMC6308 3-Axis Magnetic Sensor driver"); +MODULE_AUTHOR("Jorijn van der Graaf "); +MODULE_LICENSE("Dual BSD/GPL"); From cf984064084f6911b7cf70f966fc478cc78d7dd7 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 14 Jul 2026 19:26:33 +0200 Subject: [PATCH 8/8] arm64: dts: qcom: milos-fairphone-fp6: add sensor i2c bus and magnetometer The FP6 sensor suite (QMC6308 magnetometer @ 0x2c, SPL07 barometer @ 0x76, STK3BCx ALS/proximity @ 0x48) sits on an I2C bus wired to TLMM gpio153/gpio154. Those pads are eGPIOs with no AP serial-engine function: on the stock OS the bus is driven by an island QUP owned by the Snapdragon Sensor Core on the ADSP. From the AP they are only usable as software GPIOs, so describe the bus as i2c-gpio, and add the QMC6308 magnetometer node: powered from the always-on vreg_l10b (per the schematics), mount matrix from the vendor SSC registry (volcano_qmc630x_0.json .orient: sensor x -> device +y, sensor y -> device -x, z -> z), validated against Earth's field with a four-point cardinal rotation. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jorijn van der Graaf --- .../boot/dts/qcom/milos-fairphone-fp6.dts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts index e5944a1b255f..f81285d31247 100644 --- a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts +++ b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts @@ -27,6 +27,34 @@ serial1 = &uart11; }; + /* + * The sensor I2C bus sits on TLMM eGPIO pads that have no AP QUP + * function; on the stock OS it is driven by an island QUP of the + * Snapdragon Sensor Core (ADSP). From the AP the pads are only + * usable as software GPIOs, so bit-bang the bus. + * Devices on the bus: QMC6308 magnetometer @ 0x2c, SPL07 + * barometer @ 0x76, STK3BCx ALS/proximity @ 0x48. + */ + i2c-sensors { + compatible = "i2c-gpio"; + sda-gpios = <&tlmm 153 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; + scl-gpios = <&tlmm 154 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; + i2c-gpio,delay-us = <2>; + pinctrl-0 = <&sensor_i2c_default>; + pinctrl-names = "default"; + #address-cells = <1>; + #size-cells = <0>; + + magnetometer@2c { + compatible = "qstcorp,qmc6308"; + reg = <0x2c>; + vdd-supply = <&vreg_l10b>; + mount-matrix = "0", "-1", "0", + "1", "0", "0", + "0", "0", "1"; + }; + }; + gpio-keys { compatible = "gpio-keys"; @@ -1091,6 +1119,14 @@ <13 1>, /* NC */ <63 2>; /* WLAN UART */ + sensor_i2c_default: sensor-i2c-default-state { + /* SDA, SCL; external pull-ups to vreg_l10b */ + pins = "gpio153", "gpio154"; + function = "gpio"; + drive-strength = <2>; + bias-disable; + }; + ts_active: ts-irq-active-state { pins = "gpio19"; function = "gpio";