diff --git a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts index f81285d31247..7bbcb280b05f 100644 --- a/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts +++ b/arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts @@ -237,14 +237,34 @@ sound { compatible = "qcom,milos-sndcard", "qcom,sm8450-sndcard"; model = "Fairphone (Gen. 6)"; - // audio-routing = ... + audio-routing = "AMIC1", "MIC BIAS1", + "AMIC2", "MIC BIAS2", + "AMIC3", "MIC BIAS3", + "TX SWR_INPUT0", "ADC1_OUTPUT", + "TX SWR_INPUT1", "ADC2_OUTPUT", + "TX SWR_INPUT2", "ADC3_OUTPUT"; pinctrl-0 = <&lpi_i2s2_active>; pinctrl-1 = <&lpi_i2s2_sleep>; pinctrl-names = "default", "sleep"; - /* + wcd-capture-dai-link { + link-name = "WCD Capture"; + + codec { + sound-dai = <&wcd9378 1>, <&swr2 0>, <&lpass_txmacro 0>; + }; + + cpu { + sound-dai = <&q6apmbedai TX_CODEC_DMA_TX_3>; + }; + + platform { + sound-dai = <&q6apm>; + }; + }; + i2s-dai-link { link-name = "Senary MI2S Playback"; @@ -260,7 +280,29 @@ sound-dai = <&q6apm>; }; }; - */ + }; + + wcd9378: audio-codec { + compatible = "qcom,wcd9378-codec"; + + pinctrl-0 = <&wcd_reset_n_active>; + pinctrl-names = "default"; + + reset-gpios = <&tlmm 162 GPIO_ACTIVE_LOW>; + + vdd-buck-supply = <&vreg_l8b>; + vdd-rxtx-supply = <&vreg_l7b>; + vdd-io-supply = <&vreg_l7b>; + vdd-mic-bias-supply = <&vreg_bob>; + + qcom,micbias1-microvolt = <1800000>; + qcom,micbias2-microvolt = <1800000>; + qcom,micbias3-microvolt = <1800000>; + + qcom,rx-device = <&wcd9378_rx>; + qcom,tx-device = <&wcd9378_tx>; + + #sound-dai-cells = <1>; }; thermal-zones { @@ -728,9 +770,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 { @@ -1114,11 +1175,48 @@ }; }; +&swr1 { + status = "okay"; + + /* WCD9378 RX */ + wcd9378_rx: codec@0,4 { + compatible = "sdw20217011000"; + reg = <0 4>; + qcom,rx-port-mapping = <1 2 3 4 5>; + }; +}; + +&swr2 { + status = "okay"; + + /* WCD9378 TX */ + wcd9378_tx: codec@0,3 { + compatible = "sdw20217011000"; + reg = <0 3>; + + /* + * WCD9378 TX port 1 (ADC1) <=> SWR2 port 1 (SWRM_TX1) + * WCD9378 TX port 2 (ADC2) <=> SWR2 port 1 + * WCD9378 TX port 3 (ADC3) <=> SWR2 port 1 + * WCD9378 TX port 4 (DMIC0,1, MBHC) <=> SWR2 port 2 + * WCD9378 TX port 5 (DMIC2..5) <=> SWR2 port 3 + */ + qcom,tx-port-mapping = <1 1 1 2 3>; + }; +}; + &tlmm { gpio-reserved-ranges = <8 4>, /* Fingerprint SPI */ <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; + }; + sensor_i2c_default: sensor-i2c-default-state { /* SDA, SCL; external pull-ups to vreg_l10b */ pins = "gpio153", "gpio154"; @@ -1141,6 +1239,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"; @@ -1208,6 +1313,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"; @@ -1249,6 +1365,13 @@ drive-strength = <2>; bias-pull-down; }; + + wcd_reset_n_active: wcd-reset-n-active-state { + pins = "gpio162"; + function = "gpio"; + drive-strength = <16>; + output-high; + }; }; &uart5 { diff --git a/arch/arm64/boot/dts/qcom/milos.dtsi b/arch/arm64/boot/dts/qcom/milos.dtsi index 288e0e658629..0f95a9976de7 100644 --- a/arch/arm64/boot/dts/qcom/milos.dtsi +++ b/arch/arm64/boot/dts/qcom/milos.dtsi @@ -548,6 +548,14 @@ no-map; }; + adsp_rpc_heap_mem: adsp-rpc-heap { + compatible = "shared-dma-pool"; + alloc-ranges = <0x0 0x0 0x0 0xffffffff>; + reusable; + alignment = <0x0 0x400000>; + size = <0x0 0xc00000>; + }; + pvm_fw_mem: pvm-fw-region@824a0000 { reg = <0x0 0x824a0000 0x0 0x100000>; no-map; @@ -1398,6 +1406,9 @@ qcom,glink-channels = "fastrpcglink-apps-dsp"; label = "adsp"; qcom,non-secure-domain; + memory-region = <&adsp_rpc_heap_mem>; + qcom,vmids = ; #address-cells = <1>; #size-cells = <0>; @@ -1422,6 +1433,8 @@ reg = <5>; iommus = <&apps_smmu 0x1005 0x0>, <&apps_smmu 0x1065 0x0>; + /* AUDIO_STATICPD bank, per downstream volcano-dsp.dtsi */ + pd-type = <2>; dma-coherent; }; diff --git a/drivers/misc/fastrpc.c b/drivers/misc/fastrpc.c index f3a49384586d..cb29a429af78 100644 --- a/drivers/misc/fastrpc.c +++ b/drivers/misc/fastrpc.c @@ -103,6 +103,13 @@ #define USER_PD (1) #define SENSORS_PD (2) +/* + * Downstream DT "pd-type" context-bank designations (volcano-dsp.dtsi). + * A static PD must be spawned on its designated context bank / SMMU + * stream or the DSP treats it as a generic signed PD. + */ +#define FASTRPC_DT_PD_TYPE_AUDIO_STATICPD 2 + #define miscdev_to_fdevice(d) container_of(d, struct fastrpc_device, miscdev) struct fastrpc_phy_page { @@ -255,6 +262,8 @@ struct fastrpc_session_ctx { int sid; bool used; bool valid; + /* downstream DT pd-type this context bank is dedicated to (0 = generic) */ + u32 pd_type; }; struct fastrpc_soc_data { @@ -314,6 +323,11 @@ struct fastrpc_user { struct kref refcount; }; +static struct fastrpc_session_ctx *fastrpc_session_alloc_pd_type( + struct fastrpc_user *fl, u32 pd_type); +static void fastrpc_session_free(struct fastrpc_channel_ctx *cctx, + struct fastrpc_session_ctx *session); + /* Extract SMMU PA from consolidated IOVA */ static inline dma_addr_t fastrpc_ipa_to_dma_addr(struct fastrpc_channel_ctx *cctx, dma_addr_t iova) { @@ -1343,7 +1357,7 @@ static int fastrpc_init_create_static_process(struct fastrpc_user *fl, struct fastrpc_phy_page pages[1]; char *name; int err; - bool scm_done = false; + bool scm_done = false, heap_created = false; struct { int client_id; u32 namelen; @@ -1371,11 +1385,35 @@ static int fastrpc_init_create_static_process(struct fastrpc_user *fl, goto err; } + /* + * The audio static PD must live on its designated context bank so the + * DSP sees its memory on the audio SMMU stream; on a generic bank the + * aDSP enforces module signing in the PD (dlopen of unsigned dynamic + * modules fails demanding a testsig). Re-route before anything is + * allocated against the session. Falls back to the already-held + * generic session if no bank is tagged in DT. + */ + if (init.namelen >= strlen("audiopd") && + !memcmp(name, "audiopd", strlen("audiopd"))) { + struct fastrpc_session_ctx *audio_sess; + + audio_sess = fastrpc_session_alloc_pd_type(fl, + FASTRPC_DT_PD_TYPE_AUDIO_STATICPD); + if (audio_sess) { + fastrpc_session_free(fl->cctx, fl->sctx); + fl->sctx = audio_sess; + } + dev_info(&fl->cctx->rpdev->dev, + "audiopd: using session sid %d (%s)\n", fl->sctx->sid, + audio_sess ? "dedicated audio CB" : "no audio CB in DT"); + } + if (!fl->cctx->remote_heap) { err = fastrpc_remote_heap_alloc(fl, fl->sctx->dev, init.memlen, &fl->cctx->remote_heap); if (err) goto err_name; + heap_created = true; /* Map if we have any heap VMIDs associated with this ADSP Static Process. */ if (fl->cctx->vmcount) { @@ -1446,8 +1484,16 @@ err_invoke: &fl->cctx->remote_heap->dma_addr, fl->cctx->remote_heap->size, err); } err_map: - fastrpc_buf_free(fl->cctx->remote_heap); - fl->cctx->remote_heap = NULL; + /* + * A heap reused from an earlier spawn is still SCM-assigned to the + * remote VMIDs; freeing it here would return protected pages to the + * allocator and a later touch crashes with a synchronous external + * abort. Keep it for the next spawn attempt. + */ + if (heap_created) { + fastrpc_buf_free(fl->cctx->remote_heap); + fl->cctx->remote_heap = NULL; + } err_name: kfree(name); err: @@ -1582,7 +1628,9 @@ static struct fastrpc_session_ctx *fastrpc_session_alloc( spin_lock_irqsave(&cctx->lock, flags); for (i = 0; i < cctx->sesscount; i++) { - if (!cctx->session[i].used && cctx->session[i].valid) { + /* dedicated static-PD context banks are assigned by pd_type only */ + if (!cctx->session[i].used && cctx->session[i].valid && + !cctx->session[i].pd_type) { cctx->session[i].used = true; session = &cctx->session[i]; /* any non-zero ID will work, session_idx + 1 is the simplest one */ @@ -1595,6 +1643,29 @@ static struct fastrpc_session_ctx *fastrpc_session_alloc( return session; } +static struct fastrpc_session_ctx *fastrpc_session_alloc_pd_type( + struct fastrpc_user *fl, u32 pd_type) +{ + struct fastrpc_channel_ctx *cctx = fl->cctx; + struct fastrpc_session_ctx *session = NULL; + unsigned long flags; + int i; + + spin_lock_irqsave(&cctx->lock, flags); + for (i = 0; i < cctx->sesscount; i++) { + if (!cctx->session[i].used && cctx->session[i].valid && + cctx->session[i].pd_type == pd_type) { + cctx->session[i].used = true; + session = &cctx->session[i]; + fl->client_id = i + 1; + break; + } + } + spin_unlock_irqrestore(&cctx->lock, flags); + + return session; +} + static void fastrpc_session_free(struct fastrpc_channel_ctx *cctx, struct fastrpc_session_ctx *session) { @@ -2251,6 +2322,9 @@ static int fastrpc_cb_probe(struct platform_device *pdev) if (of_property_read_u32(dev->of_node, "reg", &sess->sid)) dev_info(dev, "FastRPC Session ID not specified in DT\n"); + sess->pd_type = 0; + of_property_read_u32(dev->of_node, "pd-type", &sess->pd_type); + if (sessions > 0) { struct fastrpc_session_ctx *dup_sess; 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; diff --git a/drivers/soundwire/qcom.c b/drivers/soundwire/qcom.c index 3d8f5a81eff1..9d8aa6a75c48 100644 --- a/drivers/soundwire/qcom.c +++ b/drivers/soundwire/qcom.c @@ -976,6 +976,20 @@ static enum sdw_command_response qcom_swrm_xfer_msg(struct sdw_bus *bus, struct qcom_swrm_ctrl *ctrl = to_qcom_sdw(bus); int ret, i, len; + if (msg->page) { + ret = qcom_swrm_cmd_fifo_wr_cmd(ctrl, msg->addr_page1, + msg->dev_num, + SDW_SCP_ADDRPAGE1); + if (ret) + return SDW_CMD_IGNORED; + + ret = qcom_swrm_cmd_fifo_wr_cmd(ctrl, msg->addr_page2, + msg->dev_num, + SDW_SCP_ADDRPAGE2); + if (ret) + return SDW_CMD_IGNORED; + } + if (msg->flags == SDW_MSG_FLAG_READ) { for (i = 0; i < msg->len;) { len = min(msg->len - i, QCOM_SWRM_MAX_RD_LEN); diff --git a/net/qrtr/af_qrtr.c b/net/qrtr/af_qrtr.c index db823177e636..aae337572be7 100644 --- a/net/qrtr/af_qrtr.c +++ b/net/qrtr/af_qrtr.c @@ -520,6 +520,54 @@ int qrtr_endpoint_post(struct qrtr_endpoint *ep, const void *data, size_t len) qrtr_node_assign(node, le32_to_cpu(pkt->server.node)); } + /* The DSPs are star-connected through this node: forward packets + * destined to another node onto that node's endpoint (e.g. the + * modem's voice stack talking to the ADSP's audio service). Only + * DATA and RESUME_TX transit; control packets keep going to the + * local ns, which does its own mesh-wide redistribution. RESUME_TX + * additionally releases this hop's flow-control token on the + * arrival link: per-hop counters advance in lockstep with the + * end-to-end ones since every packet of the flow transits here. + */ + if (cb->dst_node != qrtr_local_nid && + cb->dst_node != QRTR_NODE_BCAST && + (cb->type == QRTR_TYPE_DATA || cb->type == QRTR_TYPE_RESUME_TX)) { + struct sockaddr_qrtr from = {AF_QIPCRTR, + cb->src_node, cb->src_port}; + struct sockaddr_qrtr to = {AF_QIPCRTR, + cb->dst_node, cb->dst_port}; + struct qrtr_node *dst; + + dst = qrtr_node_lookup(cb->dst_node); + if (!dst || dst == node) { + if (dst) + qrtr_node_release(dst); + goto err; + } + + if (cb->type == QRTR_TYPE_RESUME_TX) { + struct sk_buff *clone; + + clone = skb_clone(skb, GFP_ATOMIC); + if (clone) + qrtr_tx_resume(node, clone); + } + + pr_debug("qrtr: fwd %u:%u -> %u:%u type %d len %zu\n", + cb->src_node, cb->src_port, + cb->dst_node, cb->dst_port, cb->type, size); + + if (skb_cow_head(skb, sizeof(struct qrtr_hdr_v1))) { + qrtr_node_release(dst); + goto err; + } + + qrtr_node_enqueue(dst, skb, cb->type, &from, &to); + qrtr_node_release(dst); + + return 0; + } + if (cb->type == QRTR_TYPE_RESUME_TX) { qrtr_tx_resume(node, skb); } else { diff --git a/net/qrtr/ns.c b/net/qrtr/ns.c index b3f9bbcf9ab9..47a66db76cf1 100644 --- a/net/qrtr/ns.c +++ b/net/qrtr/ns.c @@ -70,6 +70,7 @@ struct qrtr_node { unsigned int id; struct xarray servers; u32 server_count; + bool hello; }; /* Max nodes, server, lookup limits are chosen based on the current platform @@ -212,6 +213,60 @@ static void lookup_notify(struct sockaddr_qrtr *to, struct qrtr_server *srv, pr_err("failed to send lookup notification\n"); } +/* FP6 voice bring-up: the DSP firmwares expect the downstream IPC-router + * flooding model - they never send NEW_LOOKUP for services on their peers + * and instead rely on the application processor pushing announcements to + * every link. The modem's voice stack locates the ADSP's service-registry + * notifier this way; without it Voice Services never sees avs/audio come + * up and never creates a vocoder session. + * + * Flooding every announcement to every node wedges the phone within + * seconds of the DSPs booting (control packets are exempt from qrtr flow + * control, so any echo between routers runs unbounded). Push exactly the + * one edge voice needs: ADSP-hosted servers, to the modem, only. Any echo + * is structurally loop-free: a re-announcement of an ADSP server arriving + * from the modem is pushed to nobody (target == origin). + */ +#define QRTR_NS_FWD_SRC_NODE 5 /* adsp */ +#define QRTR_NS_FWD_DST_NODE 0 /* modem */ + +/* Runtime kill-switch (qrtr.fp6_ns_push): off by default so a boot is + * always calm; flip it on, then restart the modem rproc so its HELLO + * triggers the replay. + */ +static bool fp6_ns_push; +module_param(fp6_ns_push, bool, 0644); +MODULE_PARM_DESC(fp6_ns_push, "FP6: push adsp service announcements to the modem"); + +static void service_announce_remotes(struct qrtr_server *srv, + unsigned int origin, bool new) +{ + struct sockaddr_qrtr sq; + struct qrtr_node *node; + + if (!fp6_ns_push) + return; + if (srv->node != QRTR_NS_FWD_SRC_NODE) + return; + if (origin == QRTR_NS_FWD_DST_NODE || + srv->node == QRTR_NS_FWD_DST_NODE || + qrtr_ns.local_node == QRTR_NS_FWD_DST_NODE) + return; + + node = xa_load(&nodes, QRTR_NS_FWD_DST_NODE); + if (!node || !node->hello) + return; + + sq.sq_family = AF_QIPCRTR; + sq.sq_node = QRTR_NS_FWD_DST_NODE; + sq.sq_port = QRTR_PORT_CTRL; + + if (new) + service_announce_new(&sq, srv); + else + service_announce_del(&sq, srv); +} + static int announce_servers(struct sockaddr_qrtr *sq) { struct qrtr_server *srv; @@ -234,6 +289,24 @@ static int announce_servers(struct sockaddr_qrtr *sq) return ret; } } + + /* FP6 voice: replay the ADSP's servers to a (re)booting modem (see + * service_announce_remotes()) + */ + if (fp6_ns_push && sq->sq_node == QRTR_NS_FWD_DST_NODE) { + node = xa_load(&nodes, QRTR_NS_FWD_SRC_NODE); + if (!node) + return 0; + + xa_for_each(&node->servers, index, srv) { + ret = service_announce_new(sq, srv); + if (ret < 0 && ret != -ENODEV) { + pr_err("failed to announce new service\n"); + return ret; + } + } + } + return 0; } @@ -308,6 +381,8 @@ static int server_del(struct qrtr_node *node, unsigned int port, bool bcast) /* Broadcast the removal of local servers */ if (srv->node == qrtr_ns.local_node && bcast) service_announce_del(&qrtr_ns.bcast_sq, srv); + else if (bcast) + service_announce_remotes(srv, srv->node, false); /* Announce the service's disappearance to observers */ list_for_each(li, &qrtr_ns.lookups) { @@ -352,8 +427,13 @@ static int say_hello(struct sockaddr_qrtr *dest) /* Announce the list of servers registered on the local node */ static int ctrl_cmd_hello(struct sockaddr_qrtr *sq) { + struct qrtr_node *node; int ret; + node = node_get(sq->sq_node); + if (node) + node->hello = true; + ret = say_hello(sq); if (ret < 0) return ret; @@ -521,6 +601,8 @@ static int ctrl_cmd_new_server(struct sockaddr_qrtr *from, pr_err("failed to announce new service\n"); return ret; } + } else { + service_announce_remotes(srv, from->sq_node, true); } /* Notify any potential lookups about the new server */ diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 269c31ce0814..d902df62b184 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -301,6 +301,7 @@ config SND_SOC_ALL_CODECS imply SND_SOC_WCD9335 imply SND_SOC_WCD934X imply SND_SOC_WCD937X_SDW + imply SND_SOC_WCD9378_SDW imply SND_SOC_WCD938X_SDW imply SND_SOC_WCD939X_SDW imply SND_SOC_LPASS_MACRO_COMMON @@ -2431,6 +2432,24 @@ config SND_SOC_WCD937X_SDW via soundwire. To compile this codec driver say Y or m. +config SND_SOC_WCD9378 + depends on SND_SOC_WCD9378_SDW + tristate + depends on SOUNDWIRE || !SOUNDWIRE + select SND_SOC_WCD_COMMON + +config SND_SOC_WCD9378_SDW + tristate "WCD9378 Codec - SDW" + select SND_SOC_WCD9378 + depends on SOUNDWIRE + select REGMAP_SOUNDWIRE + help + The WCD9378 is an audio codec IC connected to the host over + SoundWire, found on Qualcomm SM7635 boards such as the + Fairphone 6. It has three ADCs for analog microphones, mic + bias supplies, headphone/earpiece outputs and MBHC. + To compile this codec driver say Y or m. + config SND_SOC_WCD938X depends on SND_SOC_WCD938X_SDW tristate diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index 172861d17cfd..73eaf37fd4a5 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -356,6 +356,8 @@ snd-soc-wcd9335-y := wcd9335.o snd-soc-wcd934x-y := wcd934x.o snd-soc-wcd937x-y := wcd937x.o snd-soc-wcd937x-sdw-y := wcd937x-sdw.o +snd-soc-wcd9378-y := wcd9378.o +snd-soc-wcd9378-sdw-y := wcd9378-sdw.o snd-soc-wcd938x-y := wcd938x.o snd-soc-wcd938x-sdw-y := wcd938x-sdw.o snd-soc-wcd939x-y := wcd939x.o @@ -796,6 +798,11 @@ ifdef CONFIG_SND_SOC_WCD937X_SDW # avoid link failure by forcing sdw code built-in when needed obj-$(CONFIG_SND_SOC_WCD937X) += snd-soc-wcd937x-sdw.o endif +obj-$(CONFIG_SND_SOC_WCD9378) += snd-soc-wcd9378.o +ifdef CONFIG_SND_SOC_WCD9378_SDW +# avoid link failure by forcing sdw code built-in when needed +obj-$(CONFIG_SND_SOC_WCD9378) += snd-soc-wcd9378-sdw.o +endif obj-$(CONFIG_SND_SOC_WCD938X) += snd-soc-wcd938x.o ifdef CONFIG_SND_SOC_WCD938X_SDW # avoid link failure by forcing sdw code built-in when needed 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; }; diff --git a/sound/soc/codecs/wcd9378-sdw.c b/sound/soc/codecs/wcd9378-sdw.c new file mode 100644 index 000000000000..d9a99a6db500 --- /dev/null +++ b/sound/soc/codecs/wcd9378-sdw.c @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026, Jorijn van der Graaf + * + * SoundWire slave driver for the Qualcomm WCD9378 audio codec. + * + * The codec presents two SoundWire slaves (RX and TX, mfg 0x0217 part + * 0x0110); the SDCA control space is a 32-bit paged register map accessed + * through the TX slave. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wcd-common.h" +#include "wcd9378.h" + +#define WCD9378_SDW_CH(id, pn, cmask, mmask) \ + [id] = { \ + .port_num = pn, \ + .ch_mask = cmask, \ + .master_ch_mask = mmask, \ + } + +/* + * Each ADC sits alone on its own TX device port (channel 1); by default + * they land on channels 1/2/3 of the same master port (SWRM_TX1 on the + * FP6). DMIC/MBHC masks per the downstream qcom,tx_swr_ch_map. + */ +static struct wcd_sdw_ch_info wcd9378_sdw_tx_ch_info[] = { + WCD9378_SDW_CH(WCD9378_ADC1, WCD9378_ADC_1_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_ADC2, WCD9378_ADC_2_PORT, BIT(0), BIT(1)), + WCD9378_SDW_CH(WCD9378_ADC3, WCD9378_ADC_3_PORT, BIT(0), BIT(2)), + WCD9378_SDW_CH(WCD9378_DMIC0, WCD9378_DMIC_0_1_MBHC_PORT, BIT(2), BIT(0)), + WCD9378_SDW_CH(WCD9378_DMIC1, WCD9378_DMIC_0_1_MBHC_PORT, BIT(3), BIT(1)), + WCD9378_SDW_CH(WCD9378_MBHC, WCD9378_DMIC_0_1_MBHC_PORT, BIT(2), BIT(2)), + WCD9378_SDW_CH(WCD9378_DMIC2, WCD9378_DMIC_2_5_PORT, BIT(0), BIT(2)), + WCD9378_SDW_CH(WCD9378_DMIC3, WCD9378_DMIC_2_5_PORT, BIT(1), BIT(3)), + WCD9378_SDW_CH(WCD9378_DMIC4, WCD9378_DMIC_2_5_PORT, BIT(2), BIT(0)), + WCD9378_SDW_CH(WCD9378_DMIC5, WCD9378_DMIC_2_5_PORT, BIT(3), BIT(1)), +}; + +static struct wcd_sdw_ch_info wcd9378_sdw_rx_ch_info[] = { + WCD9378_SDW_CH(WCD9378_HPH_L, WCD9378_HPH_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_HPH_R, WCD9378_HPH_PORT, BIT(1), BIT(1)), + WCD9378_SDW_CH(WCD9378_CLSH, WCD9378_CLSH_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_COMP_L, WCD9378_COMP_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_COMP_R, WCD9378_COMP_PORT, BIT(1), BIT(1)), + WCD9378_SDW_CH(WCD9378_LO, WCD9378_LO_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_DSD_L, WCD9378_DSD_PORT, BIT(0), BIT(0)), + WCD9378_SDW_CH(WCD9378_DSD_R, WCD9378_DSD_PORT, BIT(1), BIT(1)), +}; + +static struct sdw_dpn_prop wcd9378_dpn_prop[WCD9378_MAX_SWR_PORTS] = { + { + .num = 1, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 8, + .simple_ch_prep_sm = true, + }, { + .num = 2, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 4, + .simple_ch_prep_sm = true, + }, { + .num = 3, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 4, + .simple_ch_prep_sm = true, + }, { + .num = 4, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 4, + .simple_ch_prep_sm = true, + }, { + .num = 5, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 4, + .simple_ch_prep_sm = true, + } +}; + +int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct sdw_port_config port_config[WCD9378_MAX_SWR_PORTS]; + unsigned long ch_mask; + int i, j; + + wcd->sconfig.ch_count = 1; + wcd->active_ports = 0; + for (i = 0; i < WCD9378_MAX_SWR_PORTS; i++) { + ch_mask = wcd->port_config[i].ch_mask; + if (!ch_mask) + continue; + + for_each_set_bit(j, &ch_mask, 4) + wcd->sconfig.ch_count++; + + port_config[wcd->active_ports] = wcd->port_config[i]; + wcd->active_ports++; + } + + wcd->sconfig.bps = 1; + wcd->sconfig.frame_rate = params_rate(params); + wcd->sconfig.direction = wcd->is_tx ? SDW_DATA_DIR_TX : SDW_DATA_DIR_RX; + wcd->sconfig.type = SDW_STREAM_PCM; + + return sdw_stream_add_slave(wcd->sdev, &wcd->sconfig, + &port_config[0], wcd->active_ports, + wcd->sruntime); +} +EXPORT_SYMBOL_GPL(wcd9378_sdw_hw_params); + +/* + * Tell the codec the bus clock: base 19.2 MHz plus a scale (div) per bank. + * The downstream driver writes these raw SCP registers on every capture + * start; here the bus_config callback covers bank switches. + */ +static int wcd9378_bus_config(struct sdw_slave *slave, + struct sdw_bus_params *params) +{ + u8 scale; + + switch (params->curr_dr_freq) { + case 4800000: + scale = WCD9378_SWRS_CLK_SCALE_DIV4; + break; + case 9600000: + default: + scale = WCD9378_SWRS_CLK_SCALE_DIV2; + break; + } + + sdw_write(slave, WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(params->next_bank), + 0x01); + sdw_write(slave, WCD9378_SWRS_SCP_BASE_CLK, + WCD9378_SWRS_BASE_CLK_19P2MHZ); + sdw_write(slave, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0, scale); + sdw_write(slave, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1, scale); + + return 0; +} + +static const struct sdw_slave_ops wcd9378_slave_ops = { + .update_status = wcd_update_status, + .bus_config = wcd9378_bus_config, +}; + +static const struct reg_default wcd9378_defaults[] = { + { WCD9378_ANA_BIAS, 0x00 }, + { WCD9378_ANA_TX_CH1, 0x20 }, + { WCD9378_ANA_TX_CH2, 0x00 }, + { WCD9378_ANA_TX_CH3, 0x20 }, + { WCD9378_ANA_TX_CH3_HPF, 0x00 }, + { WCD9378_ANA_MICB2_RAMP, 0x00 }, + { WCD9378_BIAS_VBG_FINE_ADJ, 0x55 }, + { WCD9378_MBHC_CTL_SPARE_1, 0x02 }, + { WCD9378_MICB1_TEST_CTL_2, 0x00 }, + { WCD9378_MICB2_TEST_CTL_2, 0x00 }, + { WCD9378_MICB3_TEST_CTL_2, 0x80 }, + { WCD9378_TX_COM_TXFE_DIV_CTL, 0x22 }, + { WCD9378_SLEEP_CTL, 0x16 }, + { WCD9378_TX_NEW_CH12_MUX, 0x11 }, + { WCD9378_TX_NEW_CH34_MUX, 0x23 }, + { WCD9378_TOP_CLK_CFG, 0x00 }, + { WCD9378_CDC_ANA_TX_CLK_CTL, 0x0e }, + { WCD9378_CDC_AMIC_CTL, 0x07 }, + { WCD9378_PDM_WD_CTL0, 0x0f }, + { WCD9378_PDM_WD_CTL1, 0x0f }, + { WCD9378_PLATFORM_CTL, 0x01 }, + { WCD9378_SYS_USAGE_CTRL, 0x00 }, + { WCD9378_HPH_UP_T0, 0x02 }, + { WCD9378_HPH_UP_T9, 0x02 }, + { WCD9378_HPH_DN_T0, 0x05 }, + { WCD9378_MICB_REMAP_TABLE_VAL_3, 0x00 }, + { WCD9378_MICB_REMAP_TABLE_VAL_4, 0x00 }, + { WCD9378_MICB_REMAP_TABLE_VAL_5, 0x00 }, + { WCD9378_SM0_MB_SEL, 0x00 }, + { WCD9378_SM1_MB_SEL, 0x00 }, + { WCD9378_SM2_MB_SEL, 0x00 }, + { WCD9378_MB_PULLUP_EN, 0x00 }, + { WCD9378_SMP_AMP_FUNC_ACT, 0x00 }, + { WCD9378_CMT_GRP_MASK, 0x00 }, + { WCD9378_SMP_JACK_IT31_MICB, 0x00 }, + { WCD9378_SMP_JACK_IT31_USAGE, 0x03 }, + { WCD9378_SMP_JACK_PDE34_REQ_PS, 0x03 }, + { WCD9378_SMP_JACK_FUNC_ACT, 0x00 }, + { WCD9378_SMP_MIC_IT11_MICB(0), 0x00 }, + { WCD9378_SMP_MIC_IT11_USAGE(0), 0x03 }, + { WCD9378_SMP_MIC_PDE11_REQ_PS(0), 0x03 }, + { WCD9378_SMP_MIC_FUNC_ACT(0), 0x00 }, + { WCD9378_SMP_MIC_IT11_MICB(1), 0x00 }, + { WCD9378_SMP_MIC_IT11_USAGE(1), 0x03 }, + { WCD9378_SMP_MIC_PDE11_REQ_PS(1), 0x03 }, + { WCD9378_SMP_MIC_FUNC_ACT(1), 0x00 }, + { WCD9378_SMP_MIC_IT11_MICB(2), 0x00 }, + { WCD9378_SMP_MIC_IT11_USAGE(2), 0x03 }, + { WCD9378_SMP_MIC_PDE11_REQ_PS(2), 0x03 }, + { WCD9378_SMP_MIC_FUNC_ACT(2), 0x00 }, +}; + +static bool wcd9378_rdwr_register(struct device *dev, unsigned int reg) +{ + switch (reg) { + case WCD9378_ANA_BIAS: + case WCD9378_ANA_TX_CH1: + case WCD9378_ANA_TX_CH2: + case WCD9378_ANA_TX_CH3: + case WCD9378_ANA_TX_CH3_HPF: + case WCD9378_ANA_MICB2_RAMP: + case WCD9378_BIAS_VBG_FINE_ADJ: + case WCD9378_MBHC_CTL_SPARE_1: + case WCD9378_MICB1_TEST_CTL_2: + case WCD9378_MICB2_TEST_CTL_2: + case WCD9378_MICB3_TEST_CTL_2: + case WCD9378_TX_COM_TXFE_DIV_CTL: + case WCD9378_SLEEP_CTL: + case WCD9378_TX_NEW_CH12_MUX: + case WCD9378_TX_NEW_CH34_MUX: + case WCD9378_HPH_RDAC_GAIN_CTL: + case WCD9378_HPH_RDAC_HD2_CTL_L: + case WCD9378_HPH_RDAC_HD2_CTL_R: + case WCD9378_TOP_CLK_CFG: + case WCD9378_CDC_ANA_TX_CLK_CTL: + case WCD9378_CDC_AMIC_CTL: + case WCD9378_PDM_WD_CTL0: + case WCD9378_PDM_WD_CTL1: + case WCD9378_PLATFORM_CTL: + case WCD9378_SYS_USAGE_CTRL: + case WCD9378_HPH_UP_T0: + case WCD9378_HPH_UP_T9: + case WCD9378_HPH_DN_T0: + case WCD9378_MICB_REMAP_TABLE_VAL_3: + case WCD9378_MICB_REMAP_TABLE_VAL_4: + case WCD9378_MICB_REMAP_TABLE_VAL_5: + case WCD9378_SM0_MB_SEL: + case WCD9378_SM1_MB_SEL: + case WCD9378_SM2_MB_SEL: + case WCD9378_MB_PULLUP_EN: + case WCD9378_SMP_AMP_FUNC_STAT: + case WCD9378_SMP_AMP_FUNC_ACT: + case WCD9378_CMT_GRP_MASK: + case WCD9378_SMP_JACK_IT31_MICB: + case WCD9378_SMP_JACK_IT31_USAGE: + case WCD9378_SMP_JACK_PDE34_REQ_PS: + case WCD9378_SMP_JACK_FUNC_STAT: + case WCD9378_SMP_JACK_FUNC_ACT: + case WCD9378_SMP_MIC_IT11_MICB(0): + case WCD9378_SMP_MIC_IT11_USAGE(0): + case WCD9378_SMP_MIC_PDE11_REQ_PS(0): + case WCD9378_SMP_MIC_FUNC_STAT(0): + case WCD9378_SMP_MIC_FUNC_ACT(0): + case WCD9378_SMP_MIC_IT11_MICB(1): + case WCD9378_SMP_MIC_IT11_USAGE(1): + case WCD9378_SMP_MIC_PDE11_REQ_PS(1): + case WCD9378_SMP_MIC_FUNC_STAT(1): + case WCD9378_SMP_MIC_FUNC_ACT(1): + case WCD9378_SMP_MIC_IT11_MICB(2): + case WCD9378_SMP_MIC_IT11_USAGE(2): + case WCD9378_SMP_MIC_PDE11_REQ_PS(2): + case WCD9378_SMP_MIC_FUNC_STAT(2): + case WCD9378_SMP_MIC_FUNC_ACT(2): + return true; + } + + return false; +} + +static bool wcd9378_volatile_register(struct device *dev, unsigned int reg) +{ + switch (reg) { + case WCD9378_FUNC_EXT_ID_0: + case WCD9378_FUNC_EXT_ID_1: + case WCD9378_FUNC_EXT_VER: + case WCD9378_FUNC_STAT: + case WCD9378_DEV_MANU_ID_0: + case WCD9378_DEV_MANU_ID_1: + case WCD9378_DEV_PART_ID_0: + case WCD9378_DEV_PART_ID_1: + case WCD9378_DEV_VER: + case WCD9378_EFUSE_REG_16: + case WCD9378_EFUSE_REG_29: + case WCD9378_SEQ_TX0_STAT: + case WCD9378_SEQ_TX1_STAT: + case WCD9378_SEQ_TX2_STAT: + case WCD9378_SMP_AMP_FUNC_STAT: + case WCD9378_SMP_JACK_FUNC_STAT: + case WCD9378_SMP_JACK_PDE34_ACT_PS: + case WCD9378_SMP_MIC_FUNC_STAT(0): + case WCD9378_SMP_MIC_FUNC_STAT(1): + case WCD9378_SMP_MIC_FUNC_STAT(2): + case WCD9378_SMP_MIC_OT10_USAGE(0): + case WCD9378_SMP_MIC_PDE11_ACT_PS(0): + case WCD9378_SMP_MIC_OT10_USAGE(1): + case WCD9378_SMP_MIC_PDE11_ACT_PS(1): + case WCD9378_SMP_MIC_OT10_USAGE(2): + case WCD9378_SMP_MIC_PDE11_ACT_PS(2): + return true; + } + + return false; +} + +static bool wcd9378_readable_register(struct device *dev, unsigned int reg) +{ + if (wcd9378_volatile_register(dev, reg)) + return true; + + return wcd9378_rdwr_register(dev, reg); +} + +static const struct regmap_config wcd9378_regmap_config = { + .name = "wcd9378_csr", + .reg_bits = 32, + .val_bits = 8, + .cache_type = REGCACHE_MAPLE, + .reg_defaults = wcd9378_defaults, + .num_reg_defaults = ARRAY_SIZE(wcd9378_defaults), + .max_register = WCD9378_MAX_REGISTER, + .readable_reg = wcd9378_readable_register, + .writeable_reg = wcd9378_rdwr_register, + .volatile_reg = wcd9378_volatile_register, +}; + +static int wcd9378_sdw_probe(struct sdw_slave *pdev, + const struct sdw_device_id *id) +{ + struct device *dev = &pdev->dev; + struct wcd9378_sdw_priv *wcd; + u8 master_ch_mask[WCD9378_MAX_SWR_CH_IDS]; + int master_ch_mask_size = 0; + int ret, i; + + wcd = devm_kzalloc(dev, sizeof(*wcd), GFP_KERNEL); + if (!wcd) + return -ENOMEM; + + /* Port map index starts at 0, however the data ports start at index 1 */ + if (of_property_present(dev->of_node, "qcom,tx-port-mapping")) { + wcd->is_tx = true; + ret = of_property_read_u32_array(dev->of_node, "qcom,tx-port-mapping", + &pdev->m_port_map[1], + WCD9378_MAX_TX_SWR_PORTS); + } else { + ret = of_property_read_u32_array(dev->of_node, "qcom,rx-port-mapping", + &pdev->m_port_map[1], + WCD9378_MAX_SWR_PORTS); + } + if (ret < 0) + dev_info(dev, "Error getting static port mapping for %s (%d)\n", + wcd->is_tx ? "TX" : "RX", ret); + + wcd->sdev = pdev; + dev_set_drvdata(dev, wcd); + + pdev->prop.scp_int1_mask = SDW_SCP_INT1_BUS_CLASH | + SDW_SCP_INT1_PARITY; + pdev->prop.lane_control_support = true; + pdev->prop.simple_clk_stop_capable = true; + /* The SDCA control space sits above the 16-bit address range */ + pdev->prop.paging_support = true; + + memset(master_ch_mask, 0, WCD9378_MAX_SWR_CH_IDS); + + if (wcd->is_tx) { + master_ch_mask_size = of_property_count_u8_elems(dev->of_node, + "qcom,tx-channel-mapping"); + master_ch_mask_size = min_t(int, master_ch_mask_size, + ARRAY_SIZE(wcd9378_sdw_tx_ch_info)); + + if (master_ch_mask_size > 0) + ret = of_property_read_u8_array(dev->of_node, + "qcom,tx-channel-mapping", + master_ch_mask, + master_ch_mask_size); + } else { + master_ch_mask_size = of_property_count_u8_elems(dev->of_node, + "qcom,rx-channel-mapping"); + master_ch_mask_size = min_t(int, master_ch_mask_size, + ARRAY_SIZE(wcd9378_sdw_rx_ch_info)); + + if (master_ch_mask_size > 0) + ret = of_property_read_u8_array(dev->of_node, + "qcom,rx-channel-mapping", + master_ch_mask, + master_ch_mask_size); + } + + if (wcd->is_tx) { + pdev->prop.source_ports = GENMASK(WCD9378_MAX_TX_SWR_PORTS, 1); + pdev->prop.src_dpn_prop = wcd9378_dpn_prop; + wcd->ch_info = &wcd9378_sdw_tx_ch_info[0]; + + for (i = 0; i < master_ch_mask_size; i++) + wcd->ch_info[i].master_ch_mask = WCD9378_SWRM_CH_MASK(master_ch_mask[i]); + + pdev->prop.wake_capable = true; + + wcd->regmap = devm_regmap_init_sdw(pdev, &wcd9378_regmap_config); + if (IS_ERR(wcd->regmap)) + return dev_err_probe(dev, PTR_ERR(wcd->regmap), + "Regmap init failed\n"); + + /* Start in cache-only until device is enumerated */ + regcache_cache_only(wcd->regmap, true); + } else { + pdev->prop.sink_ports = GENMASK(WCD9378_MAX_SWR_PORTS, 1); + pdev->prop.sink_dpn_prop = wcd9378_dpn_prop; + wcd->ch_info = &wcd9378_sdw_rx_ch_info[0]; + + for (i = 0; i < master_ch_mask_size; i++) + wcd->ch_info[i].master_ch_mask = WCD9378_SWRM_CH_MASK(master_ch_mask[i]); + } + + ret = component_add(dev, &wcd_sdw_component_ops); + if (ret) + return ret; + + /* Set suspended until aggregate device is bind */ + pm_runtime_set_suspended(dev); + + return 0; +} + +static void wcd9378_sdw_remove(struct sdw_slave *pdev) +{ + struct device *dev = &pdev->dev; + + component_del(dev, &wcd_sdw_component_ops); +} + +static const struct sdw_device_id wcd9378_sdw_id[] = { + SDW_SLAVE_ENTRY(0x0217, 0x0110, 0), + { }, +}; +MODULE_DEVICE_TABLE(sdw, wcd9378_sdw_id); + +static int wcd9378_sdw_runtime_suspend(struct device *dev) +{ + struct wcd9378_sdw_priv *wcd = dev_get_drvdata(dev); + + if (wcd->regmap) { + regcache_cache_only(wcd->regmap, true); + regcache_mark_dirty(wcd->regmap); + } + + return 0; +} + +static int wcd9378_sdw_runtime_resume(struct device *dev) +{ + struct wcd9378_sdw_priv *wcd = dev_get_drvdata(dev); + + if (wcd->regmap) { + regcache_cache_only(wcd->regmap, false); + regcache_sync(wcd->regmap); + } + + return 0; +} + +static const struct dev_pm_ops wcd9378_sdw_pm_ops = { + RUNTIME_PM_OPS(wcd9378_sdw_runtime_suspend, wcd9378_sdw_runtime_resume, NULL) +}; + +static struct sdw_driver wcd9378_sdw_driver = { + .probe = wcd9378_sdw_probe, + .remove = wcd9378_sdw_remove, + .ops = &wcd9378_slave_ops, + .id_table = wcd9378_sdw_id, + .driver = { + .name = "wcd9378-sdw", + .pm = pm_ptr(&wcd9378_sdw_pm_ops), + } +}; +module_sdw_driver(wcd9378_sdw_driver); + +MODULE_DESCRIPTION("WCD9378 SDW codec driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wcd9378.c b/sound/soc/codecs/wcd9378.c new file mode 100644 index 000000000000..38acb6b20388 --- /dev/null +++ b/sound/soc/codecs/wcd9378.c @@ -0,0 +1,1390 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026, Jorijn van der Graaf + * + * Qualcomm WCD9378 audio codec driver. + * + * The WCD9378 pairs a WCD937x-compatible analog core with SDCA-style + * function blocks (SmartMIC0/1/2, SmartJACK, SmartAMP) whose built-in + * sequencers perform the analog power-up/down autonomously: capture is + * started by programming the ADC usage mode (ITxx_USAGE), requesting + * power state 0 on the function's PDE, and letting the sequencer ramp + * the micbias selected through SMx_MB_SEL. + * + * TX/capture paths only for now; RX (earpiece/headphone), MBHC and the + * SmartAMP function are not implemented. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wcd-common.h" +#include "wcd9378.h" + +enum { + AIF1_PB = 0, + AIF1_CAP, + NUM_CODEC_DAIS, +}; + +enum { + MIC_BIAS_1 = 1, + MIC_BIAS_2, + MIC_BIAS_3, +}; + +enum { + MICB_PULLUP_ENABLE, + MICB_PULLUP_DISABLE, + MICB_ENABLE, + MICB_DISABLE, +}; + +/* sys-usage capability bits (SYS_USAGE_CTRL profile contents) */ +enum { + WCD9378_SYS_USAGE_CLASS_AB = 0, + WCD9378_SYS_USAGE_TX1_FOR_JACK, + WCD9378_SYS_USAGE_TX2_AMIC4, + WCD9378_SYS_USAGE_TX2_AMIC1, + WCD9378_SYS_USAGE_TX1_AMIC3, + WCD9378_SYS_USAGE_TX1_AMIC2, + WCD9378_SYS_USAGE_TX0_AMIC2, + WCD9378_SYS_USAGE_TX0_AMIC1, + WCD9378_SYS_USAGE_RX2_EAR, + WCD9378_SYS_USAGE_RX2_AUX, + WCD9378_SYS_USAGE_RX1_AUX, + WCD9378_SYS_USAGE_RX0_EAR, + WCD9378_SYS_USAGE_RX0_RX1_HPH, +}; + +/* Capability sets of the 13 canned SYS_USAGE_CTRL profiles */ +static const unsigned int wcd9378_sys_usage_profiles[] = { + 0x0c95, 0x12a7, 0x0c99, 0x1aab, 0x0894, 0x11a6, 0x0898, + 0x11ab, 0x126a, 0x116b, 0x1ca7, 0x1195, 0x1296, +}; + +struct wcd9378_priv { + struct sdw_slave *tx_sdw_dev; + struct wcd9378_sdw_priv *sdw_priv[NUM_CODEC_DAIS]; + struct device *txdev; + struct device *rxdev; + struct device_node *rxnode; + struct device_node *txnode; + struct regmap *regmap; + struct wcd_common common; + /* micbias refcount lock */ + struct mutex micb_lock; + u8 micb_usage_val[WCD9378_MAX_MICBIAS]; + int micb_ref[WCD9378_MAX_MICBIAS]; + int pullup_ref[WCD9378_MAX_MICBIAS]; + unsigned long sys_usage_mask; + /* per-ADC profile bit and target function, latched at power-up */ + int tx_sys_bit[3]; + bool tx_is_jack[3]; + u32 tx_mode[3]; + struct gpio_desc *reset_gpio; +}; + +static const char * const wcd9378_supplies[] = { + "vdd-rxtx", "vdd-io", "vdd-buck", "vdd-mic-bias", +}; + +/* SDCA function block registers driving one ADC */ +struct wcd9378_smp_fn { + u32 usage_reg; + u32 micb_reg; + u32 req_reg; + u32 act_reg; + u32 hpf_reg; + u8 hpf_mask; +}; + +/* ADC1/2/3 through SmartMIC0/1/2 */ +static const struct wcd9378_smp_fn wcd9378_smp_mic[] = { + { + .usage_reg = WCD9378_SMP_MIC_IT11_USAGE(0), + .micb_reg = WCD9378_SMP_MIC_IT11_MICB(0), + .req_reg = WCD9378_SMP_MIC_PDE11_REQ_PS(0), + .act_reg = WCD9378_SMP_MIC_PDE11_ACT_PS(0), + .hpf_reg = WCD9378_ANA_TX_CH2, + .hpf_mask = WCD9378_ANA_TX_CH2_HPF1_INIT, + }, { + .usage_reg = WCD9378_SMP_MIC_IT11_USAGE(1), + .micb_reg = WCD9378_SMP_MIC_IT11_MICB(1), + .req_reg = WCD9378_SMP_MIC_PDE11_REQ_PS(1), + .act_reg = WCD9378_SMP_MIC_PDE11_ACT_PS(1), + .hpf_reg = WCD9378_ANA_TX_CH2, + .hpf_mask = WCD9378_ANA_TX_CH2_HPF2_INIT, + }, { + .usage_reg = WCD9378_SMP_MIC_IT11_USAGE(2), + .micb_reg = WCD9378_SMP_MIC_IT11_MICB(2), + .req_reg = WCD9378_SMP_MIC_PDE11_REQ_PS(2), + .act_reg = WCD9378_SMP_MIC_PDE11_ACT_PS(2), + .hpf_reg = WCD9378_ANA_TX_CH3_HPF, + .hpf_mask = WCD9378_ANA_TX_CH3_HPF3_INIT, + }, +}; + +/* ADC2 fed from AMIC2 runs through the SmartJACK function instead */ +static const struct wcd9378_smp_fn wcd9378_smp_jack_adc2 = { + .usage_reg = WCD9378_SMP_JACK_IT31_USAGE, + .micb_reg = WCD9378_SMP_JACK_IT31_MICB, + .req_reg = WCD9378_SMP_JACK_PDE34_REQ_PS, + .act_reg = WCD9378_SMP_JACK_PDE34_ACT_PS, +}; + +/* + * Measured acoustically on the FP6: the TX gain field steps 1.5 dB per + * code (+6 dB per 4 codes, +30 dB over the full 0..20 range). The + * wcd937x-family drivers claim 0.25 dB steps for the same field. + */ +static const DECLARE_TLV_DB_SCALE(analog_gain, 0, 150, 0); + +static void wcd9378_reset(struct wcd9378_priv *wcd9378) +{ + gpiod_set_value(wcd9378->reset_gpio, 1); + usleep_range(20, 30); + gpiod_set_value(wcd9378->reset_gpio, 0); + usleep_range(20, 30); +} + +/* + * Activate the SDCA function classes. Without FUNC_ACT the sequencer + * ignores all PDE power-state requests. + */ +static void wcd9378_class_load(struct snd_soc_component *component) +{ + int i; + + /* + * Plain writes, not update_bits, so the 0->1 activation edge + * always reaches the hardware regardless of regcache state. + * The engine boots from this edge only on a freshly reset + * codec; once it dies (bus clock-stop) no register write + * revives it, see the TX bus PM hold in wcd9378_bind(). + */ + snd_soc_component_write(component, WCD9378_SMP_AMP_FUNC_ACT, 0x00); + snd_soc_component_write(component, WCD9378_SMP_AMP_FUNC_ACT, 0x01); + usleep_range(20000, 20010); + snd_soc_component_write(component, WCD9378_SMP_AMP_FUNC_STAT, 0xff); + + snd_soc_component_write(component, WCD9378_SMP_JACK_FUNC_ACT, 0x00); + snd_soc_component_write(component, WCD9378_SMP_JACK_FUNC_ACT, 0x01); + usleep_range(30000, 30010); + snd_soc_component_update_bits(component, WCD9378_CMT_GRP_MASK, + 0xff, 0x02); + snd_soc_component_write(component, WCD9378_SMP_JACK_FUNC_STAT, 0xff); + + for (i = 0; i < 3; i++) { + snd_soc_component_write(component, + WCD9378_SMP_MIC_FUNC_ACT(i), 0x00); + snd_soc_component_write(component, + WCD9378_SMP_MIC_FUNC_ACT(i), 0x01); + usleep_range(5000, 5010); + snd_soc_component_write(component, + WCD9378_SMP_MIC_FUNC_STAT(i), 0xff); + } +} + +static void wcd9378_io_init(struct snd_soc_component *component) +{ + u32 efuse; + + /* Bandgap and analog master bias, with precharge pulse */ + efuse = snd_soc_component_read(component, WCD9378_EFUSE_REG_16); + snd_soc_component_update_bits(component, WCD9378_MBHC_CTL_SPARE_1, + 0x03, efuse == 0 ? 0x03 : 0x01); + snd_soc_component_update_bits(component, WCD9378_SLEEP_CTL, + WCD9378_SLEEP_CTL_BG_CTL_MASK, 0x0e); + snd_soc_component_update_bits(component, WCD9378_SLEEP_CTL, + WCD9378_SLEEP_CTL_BG_EN, + WCD9378_SLEEP_CTL_BG_EN); + usleep_range(1000, 1010); + snd_soc_component_update_bits(component, WCD9378_SLEEP_CTL, + WCD9378_SLEEP_CTL_LDOL_BG_SEL, + WCD9378_SLEEP_CTL_LDOL_BG_SEL); + usleep_range(1000, 1010); + snd_soc_component_update_bits(component, WCD9378_BIAS_VBG_FINE_ADJ, + 0xf0, 0xb0); + snd_soc_component_update_bits(component, WCD9378_ANA_BIAS, + WCD9378_ANA_BIAS_ANALOG_BIAS_EN, + WCD9378_ANA_BIAS_ANALOG_BIAS_EN); + snd_soc_component_update_bits(component, WCD9378_ANA_BIAS, + WCD9378_ANA_BIAS_PRECHRG_EN, + WCD9378_ANA_BIAS_PRECHRG_EN); + usleep_range(10000, 10010); + snd_soc_component_update_bits(component, WCD9378_ANA_BIAS, + WCD9378_ANA_BIAS_PRECHRG_EN, 0x00); + + /* TX supporting clocks/dividers */ + snd_soc_component_update_bits(component, WCD9378_CDC_ANA_TX_CLK_CTL, + WCD9378_CDC_ANA_TXSCBIAS_CLK_EN, + WCD9378_CDC_ANA_TXSCBIAS_CLK_EN); + snd_soc_component_update_bits(component, WCD9378_TX_COM_TXFE_DIV_CTL, + WCD9378_TX_COM_TXFE_DIV_SEQ_BYPASS, + WCD9378_TX_COM_TXFE_DIV_SEQ_BYPASS); + snd_soc_component_update_bits(component, WCD9378_PDM_WD_CTL0, + 0x18, 0x10); + snd_soc_component_update_bits(component, WCD9378_PDM_WD_CTL1, + 0x18, 0x10); + + /* Micbias LDO driver bias */ + snd_soc_component_update_bits(component, WCD9378_MICB1_TEST_CTL_2, + 0x07, 0x01); + snd_soc_component_update_bits(component, WCD9378_MICB2_TEST_CTL_2, + 0x07, 0x01); + snd_soc_component_update_bits(component, WCD9378_MICB3_TEST_CTL_2, + 0x07, 0x01); + + /* RX defaults (harmless while RX is unimplemented) */ + snd_soc_component_update_bits(component, WCD9378_HPH_RDAC_HD2_CTL_L, + 0x0f, 0x04); + snd_soc_component_update_bits(component, WCD9378_HPH_RDAC_HD2_CTL_R, + 0x0f, 0x04); + snd_soc_component_update_bits(component, WCD9378_HPH_RDAC_GAIN_CTL, + 0xf0, 0x50); + snd_soc_component_update_bits(component, WCD9378_HPH_UP_T0, + 0x07, 0x05); + snd_soc_component_update_bits(component, WCD9378_HPH_UP_T9, + 0x07, 0x05); + snd_soc_component_update_bits(component, WCD9378_HPH_DN_T0, + 0x07, 0x06); + + /* SmartMIC function N powers the micbias SMx_MB_SEL points at */ + snd_soc_component_update_bits(component, WCD9378_SM0_MB_SEL, + WCD9378_SM_MB_SEL_MASK, 0x01); + snd_soc_component_update_bits(component, WCD9378_SM1_MB_SEL, + WCD9378_SM_MB_SEL_MASK, 0x02); + snd_soc_component_update_bits(component, WCD9378_SM2_MB_SEL, + WCD9378_SM_MB_SEL_MASK, 0x03); + snd_soc_component_update_bits(component, WCD9378_SYS_USAGE_CTRL, + WCD9378_SYS_USAGE_CTRL_MASK, 0x00); + + wcd9378_class_load(component); +} + +/* + * Derive the sys-usage capability bit for an ADC from its input mux. + * Only combinations the canned profiles cover are usable. + */ +static int wcd9378_sys_usage_bit_get(struct snd_soc_component *component, + int adc, bool *is_jack) +{ + u32 val; + + *is_jack = false; + + switch (adc) { + case 0: + val = snd_soc_component_read(component, WCD9378_TX_NEW_CH12_MUX) & + WCD9378_TX_NEW_CH12_MUX_CH1_SEL_MASK; + if (val == 0x01) + return WCD9378_SYS_USAGE_TX0_AMIC1; + if (val == 0x02) + return WCD9378_SYS_USAGE_TX0_AMIC2; + break; + case 1: + val = (snd_soc_component_read(component, WCD9378_TX_NEW_CH12_MUX) & + WCD9378_TX_NEW_CH12_MUX_CH2_SEL_MASK) >> 3; + if (val == 0x02) { + *is_jack = true; + return WCD9378_SYS_USAGE_TX1_AMIC2; + } + if (val == 0x03) + return WCD9378_SYS_USAGE_TX1_AMIC3; + break; + case 2: + val = snd_soc_component_read(component, WCD9378_TX_NEW_CH34_MUX) & + WCD9378_TX_NEW_CH34_MUX_CH3_SEL_MASK; + if (val == 0x01) + return WCD9378_SYS_USAGE_TX2_AMIC1; + if (val == 0x03) + return WCD9378_SYS_USAGE_TX2_AMIC4; + break; + } + + dev_err(component->dev, + "ADC%d input mux selection not supported by any sys-usage profile\n", + adc + 1); + return -EINVAL; +} + +static int wcd9378_sys_usage_update(struct snd_soc_component *component, + int bit, bool enable) +{ + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + int i; + + if (!enable) { + clear_bit(bit, &wcd9378->sys_usage_mask); + return 0; + } + + set_bit(bit, &wcd9378->sys_usage_mask); + + for (i = 0; i < ARRAY_SIZE(wcd9378_sys_usage_profiles); i++) { + if ((wcd9378_sys_usage_profiles[i] & wcd9378->sys_usage_mask) == + wcd9378->sys_usage_mask) + break; + } + + if (i == ARRAY_SIZE(wcd9378_sys_usage_profiles)) { + clear_bit(bit, &wcd9378->sys_usage_mask); + dev_err(component->dev, + "no sys-usage profile covers active paths (mask %#lx)\n", + wcd9378->sys_usage_mask); + return -EINVAL; + } + + snd_soc_component_update_bits(component, WCD9378_SYS_USAGE_CTRL, + WCD9378_SYS_USAGE_CTRL_MASK, i); + + return 0; +} + +static u32 wcd9378_get_mode_val(struct wcd9378_priv *wcd9378, int adc) +{ + switch (wcd9378->tx_mode[adc]) { + case 1: + return WCD9378_ADC_USAGE_HIFI; + case 2: + return WCD9378_ADC_USAGE_LO_HIF; + case 4: + return WCD9378_ADC_USAGE_LP; + case 0: /* ADC_INVALID (unset) */ + case 3: + default: + return WCD9378_ADC_USAGE_NORMAL; + } +} + +/* + * Indicate the bus clock to the codec through the standard SCP + * BusClock_Base/Scale registers. The TX sequencers clock off the + * SoundWire bus clock and stall without this; the downstream driver + * writes these on every capture start and clears them on the last + * teardown. + */ +/* Actual bus clock is half the SoundWire double-rate frequency */ +static unsigned int wcd9378_tx_bus_clk(struct wcd9378_priv *wcd9378) +{ + return wcd9378->tx_sdw_dev->bus->params.curr_dr_freq / 2; +} + +static void wcd9378_swr_clk_indicate(struct wcd9378_priv *wcd9378, bool enable) +{ + struct sdw_slave *tx = wcd9378->tx_sdw_dev; + u8 scale; + + if (enable) { + if (wcd9378_tx_bus_clk(wcd9378) >= 9600000) + scale = WCD9378_SWRS_CLK_SCALE_DIV2; + else + scale = WCD9378_SWRS_CLK_SCALE_DIV4; + + /* + * The downstream master broadcasts HOST_CLK_DIV2_CTL = 0x01 + * (both banks) on every capture start; the sequencer does not + * power up without it. + */ + sdw_write(tx, WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(0), 0x01); + sdw_write(tx, WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(1), 0x01); + sdw_write(tx, WCD9378_SWRS_SCP_BASE_CLK, + WCD9378_SWRS_BASE_CLK_19P2MHZ); + sdw_write(tx, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0, scale); + sdw_write(tx, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1, scale); + sdw_write(tx, SDW_SCP_COMMIT, 0x02); + } else { + sdw_write(tx, WCD9378_SWRS_SCP_BASE_CLK, 0x00); + sdw_write(tx, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0, 0x00); + sdw_write(tx, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1, 0x00); + } +} + +#define WCD9378_SYS_USAGE_TX_MASK (BIT(WCD9378_SYS_USAGE_TX2_AMIC4) | \ + BIT(WCD9378_SYS_USAGE_TX2_AMIC1) | \ + BIT(WCD9378_SYS_USAGE_TX1_AMIC3) | \ + BIT(WCD9378_SYS_USAGE_TX1_AMIC2) | \ + BIT(WCD9378_SYS_USAGE_TX0_AMIC2) | \ + BIT(WCD9378_SYS_USAGE_TX0_AMIC1)) + +static int wcd9378_tx_sequencer_enable(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, + int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + const struct wcd9378_smp_fn *fn; + int adc = w->shift; + bool is_jack = false; + int sys_bit, retries; + u32 val; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + sys_bit = wcd9378_sys_usage_bit_get(component, adc, &is_jack); + if (sys_bit < 0) + return sys_bit; + + /* + * Latch the profile bit and the target function: the input + * mux can be rewritten while the path is powered, and + * power-down must tear down what was actually powered up. + */ + wcd9378->tx_sys_bit[adc] = sys_bit; + wcd9378->tx_is_jack[adc] = is_jack; + if (is_jack) + fn = &wcd9378_smp_jack_adc2; + else + fn = &wcd9378_smp_mic[adc]; + + if (wcd9378_sys_usage_update(component, sys_bit, true)) + return -EINVAL; + + /* + * NORMAL/HIFI ADC modes need a 9.6 MHz bus clock; on a + * 4.8 MHz bus only the LP mode is valid and anything else + * makes the sequencer refuse to power up. + */ + if (wcd9378_tx_bus_clk(wcd9378) < 9600000) + val = WCD9378_ADC_USAGE_LP; + else + val = wcd9378_get_mode_val(wcd9378, adc); + + snd_soc_component_update_bits(component, fn->usage_reg, 0xff, val); + if (fn->hpf_reg) + snd_soc_component_update_bits(component, fn->hpf_reg, + fn->hpf_mask, fn->hpf_mask); + snd_soc_component_update_bits(component, fn->req_reg, 0xff, + WCD9378_PDE_PS0_ON); + usleep_range(800, 810); + + wcd9378_swr_clk_indicate(wcd9378, true); + + if (fn->hpf_reg) + snd_soc_component_update_bits(component, fn->hpf_reg, + fn->hpf_mask, 0x00); + + /* Wait for the sequencer to reach PS0 */ + retries = 20; + do { + val = snd_soc_component_read(component, fn->act_reg); + if (val == WCD9378_PDE_PS0_ON) + break; + usleep_range(500, 510); + } while (--retries); + if (val != WCD9378_PDE_PS0_ON) + dev_warn(component->dev, + "TX%d sequencer not in PS0 (act_ps %#x, bus %u Hz)\n", + adc, val, + wcd9378->tx_sdw_dev->bus->params.curr_dr_freq); + break; + case SND_SOC_DAPM_POST_PMD: + sys_bit = wcd9378->tx_sys_bit[adc]; + if (sys_bit < 0) + break; + if (wcd9378->tx_is_jack[adc]) + fn = &wcd9378_smp_jack_adc2; + else + fn = &wcd9378_smp_mic[adc]; + + snd_soc_component_update_bits(component, fn->usage_reg, 0xff, + WCD9378_ADC_USAGE_OFF); + if (fn->hpf_reg) + snd_soc_component_update_bits(component, fn->hpf_reg, + fn->hpf_mask, 0x00); + snd_soc_component_update_bits(component, fn->req_reg, 0xff, + WCD9378_PDE_PS3_OFF); + usleep_range(800, 810); + wcd9378_sys_usage_update(component, sys_bit, false); + wcd9378->tx_sys_bit[adc] = -1; + + if (!(wcd9378->sys_usage_mask & WCD9378_SYS_USAGE_TX_MASK)) + wcd9378_swr_clk_indicate(wcd9378, false); + break; + } + + return 0; +} + +static int wcd9378_micbias_control(struct snd_soc_component *component, + int micb_num, int req) +{ + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + int mb_index = micb_num - 1; + u32 usage_reg; + u8 usage_val; + u8 pullup_bit; + + if (micb_num < MIC_BIAS_1 || micb_num > MIC_BIAS_3) + return -EINVAL; + + usage_reg = wcd9378_smp_mic[mb_index].micb_reg; + usage_val = wcd9378->micb_usage_val[mb_index]; + pullup_bit = BIT(mb_index); + + mutex_lock(&wcd9378->micb_lock); + + switch (req) { + case MICB_ENABLE: + wcd9378->micb_ref[mb_index]++; + if (wcd9378->micb_ref[mb_index] == 1) { + if (micb_num == MIC_BIAS_2) { + snd_soc_component_update_bits(component, + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_SHIFT_CTL_MASK, + 0x0c); + snd_soc_component_update_bits(component, + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_EN, 0x00); + } + snd_soc_component_update_bits(component, usage_reg, + 0xff, usage_val); + if (micb_num == MIC_BIAS_2) + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, + 0xff, usage_val); + } + break; + case MICB_DISABLE: + if (wcd9378->micb_ref[mb_index] > 0) + wcd9378->micb_ref[mb_index]--; + if (wcd9378->micb_ref[mb_index] == 0 && + wcd9378->pullup_ref[mb_index] > 0) { + snd_soc_component_update_bits(component, + WCD9378_MB_PULLUP_EN, + pullup_bit, pullup_bit); + snd_soc_component_update_bits(component, usage_reg, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + if (micb_num == MIC_BIAS_2) + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + } else if (wcd9378->micb_ref[mb_index] == 0) { + snd_soc_component_update_bits(component, usage_reg, + 0xff, WCD9378_MICB_USAGE_OFF); + if (micb_num == MIC_BIAS_2) { + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, + 0xff, WCD9378_MICB_USAGE_OFF); + snd_soc_component_update_bits(component, + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_SHIFT_CTL_MASK, + 0x0c); + snd_soc_component_update_bits(component, + WCD9378_ANA_MICB2_RAMP, + WCD9378_ANA_MICB2_RAMP_EN, + WCD9378_ANA_MICB2_RAMP_EN); + } + } + break; + case MICB_PULLUP_ENABLE: + wcd9378->pullup_ref[mb_index]++; + if (wcd9378->pullup_ref[mb_index] == 1 && + wcd9378->micb_ref[mb_index] == 0) { + snd_soc_component_update_bits(component, + WCD9378_MB_PULLUP_EN, + pullup_bit, pullup_bit); + snd_soc_component_update_bits(component, usage_reg, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + if (micb_num == MIC_BIAS_2) + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, 0xff, + WCD9378_MICB_USAGE_1P8V_OR_PULLUP); + } + break; + case MICB_PULLUP_DISABLE: + if (wcd9378->pullup_ref[mb_index] > 0) + wcd9378->pullup_ref[mb_index]--; + if (wcd9378->pullup_ref[mb_index] == 0 && + wcd9378->micb_ref[mb_index] == 0) { + snd_soc_component_update_bits(component, + WCD9378_MB_PULLUP_EN, pullup_bit, 0x00); + snd_soc_component_update_bits(component, usage_reg, 0xff, + WCD9378_MICB_USAGE_PULL_DOWN); + if (micb_num == MIC_BIAS_2) + snd_soc_component_update_bits(component, + WCD9378_SMP_JACK_IT31_MICB, 0xff, + WCD9378_MICB_USAGE_PULL_DOWN); + } + break; + } + + mutex_unlock(&wcd9378->micb_lock); + + return 0; +} + +static int wcd9378_codec_enable_micbias(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, + int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + int micb_num = w->shift; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + wcd9378_micbias_control(component, micb_num, MICB_ENABLE); + break; + case SND_SOC_DAPM_POST_PMU: + usleep_range(1000, 1100); + break; + case SND_SOC_DAPM_POST_PMD: + wcd9378_micbias_control(component, micb_num, MICB_DISABLE); + break; + } + + return 0; +} + +static int wcd9378_codec_enable_micbias_pullup(struct snd_soc_dapm_widget *w, + struct snd_kcontrol *kcontrol, + int event) +{ + struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm); + int micb_num = w->shift; + + switch (event) { + case SND_SOC_DAPM_PRE_PMU: + wcd9378_micbias_control(component, micb_num, + MICB_PULLUP_ENABLE); + break; + case SND_SOC_DAPM_POST_PMU: + usleep_range(1000, 1100); + break; + case SND_SOC_DAPM_POST_PMD: + wcd9378_micbias_control(component, micb_num, + MICB_PULLUP_DISABLE); + break; + } + + return 0; +} + +static int wcd9378_connect_port(struct wcd9378_sdw_priv *wcd, u8 port_idx, + u8 ch_id, bool enable) +{ + struct sdw_port_config *port_config = &wcd->port_config[port_idx - 1]; + const struct wcd_sdw_ch_info *ch_info = &wcd->ch_info[ch_id]; + u8 port_num = ch_info->port_num; + u8 ch_mask = ch_info->ch_mask; + u8 mstr_port_num, mstr_ch_mask; + struct sdw_slave *sdev = wcd->sdev; + + port_config->num = port_num; + + mstr_port_num = sdev->m_port_map[port_num]; + mstr_ch_mask = ch_info->master_ch_mask; + + if (enable) { + port_config->ch_mask |= ch_mask; + wcd->master_channel_map[mstr_port_num] |= mstr_ch_mask; + } else { + port_config->ch_mask &= ~ch_mask; + wcd->master_channel_map[mstr_port_num] &= ~mstr_ch_mask; + } + + return 0; +} + +static int wcd9378_get_swr_port(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct soc_mixer_control *mixer = (struct soc_mixer_control *)kcontrol->private_value; + struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(comp); + struct wcd9378_sdw_priv *wcd; + int dai_id = mixer->shift; + int ch_idx = mixer->reg; + int portidx; + + wcd = wcd9378->sdw_priv[dai_id]; + portidx = wcd->ch_info[ch_idx].port_num; + + ucontrol->value.integer.value[0] = wcd->port_enable[portidx]; + + return 0; +} + +static int wcd9378_set_swr_port(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct soc_mixer_control *mixer = (struct soc_mixer_control *)kcontrol->private_value; + struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(comp); + struct wcd9378_sdw_priv *wcd; + int dai_id = mixer->shift; + int ch_idx = mixer->reg; + int portidx; + bool enable; + + wcd = wcd9378->sdw_priv[dai_id]; + portidx = wcd->ch_info[ch_idx].port_num; + + enable = ucontrol->value.integer.value[0]; + + if (enable == wcd->port_enable[portidx]) { + wcd9378_connect_port(wcd, portidx, ch_idx, enable); + return 0; + } + + wcd->port_enable[portidx] = enable; + wcd9378_connect_port(wcd, portidx, ch_idx, enable); + + return 1; +} + +static int wcd9378_tx_mode_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + int adc = e->shift_l; + + ucontrol->value.enumerated.item[0] = wcd9378->tx_mode[adc]; + + return 0; +} + +static int wcd9378_tx_mode_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + struct soc_enum *e = (struct soc_enum *)kcontrol->private_value; + int adc = e->shift_l; + u32 mode_val = ucontrol->value.enumerated.item[0]; + + if (mode_val == wcd9378->tx_mode[adc]) + return 0; + + wcd9378->tx_mode[adc] = mode_val; + + return 1; +} + +static const char * const tx_mode_mux_text[] = { + "ADC_INVALID", "ADC_HIFI", "ADC_LO_HIF", "ADC_NORMAL", "ADC_LP", +}; + +static const struct soc_enum tx0_mode_enum = + SOC_ENUM_SINGLE(SND_SOC_NOPM, 0, ARRAY_SIZE(tx_mode_mux_text), + tx_mode_mux_text); +static const struct soc_enum tx1_mode_enum = + SOC_ENUM_SINGLE(SND_SOC_NOPM, 1, ARRAY_SIZE(tx_mode_mux_text), + tx_mode_mux_text); +static const struct soc_enum tx2_mode_enum = + SOC_ENUM_SINGLE(SND_SOC_NOPM, 2, ARRAY_SIZE(tx_mode_mux_text), + tx_mode_mux_text); + +static const struct snd_kcontrol_new wcd9378_snd_controls[] = { + SOC_SINGLE_TLV("ADC1 Volume", WCD9378_ANA_TX_CH1, 0, 20, 0, + analog_gain), + SOC_SINGLE_TLV("ADC2 Volume", WCD9378_ANA_TX_CH2, 0, 20, 0, + analog_gain), + SOC_SINGLE_TLV("ADC3 Volume", WCD9378_ANA_TX_CH3, 0, 20, 0, + analog_gain), + + SOC_ENUM_EXT("TX0 MODE", tx0_mode_enum, + wcd9378_tx_mode_get, wcd9378_tx_mode_put), + SOC_ENUM_EXT("TX1 MODE", tx1_mode_enum, + wcd9378_tx_mode_get, wcd9378_tx_mode_put), + SOC_ENUM_EXT("TX2 MODE", tx2_mode_enum, + wcd9378_tx_mode_get, wcd9378_tx_mode_put), + + SOC_SINGLE_EXT("ADC1 Switch", WCD9378_ADC1, AIF1_CAP, 1, 0, + wcd9378_get_swr_port, wcd9378_set_swr_port), + SOC_SINGLE_EXT("ADC2 Switch", WCD9378_ADC2, AIF1_CAP, 1, 0, + wcd9378_get_swr_port, wcd9378_set_swr_port), + SOC_SINGLE_EXT("ADC3 Switch", WCD9378_ADC3, AIF1_CAP, 1, 0, + wcd9378_get_swr_port, wcd9378_set_swr_port), +}; + +static const char * const adc1_mux_text[] = { + "CH1_AMIC_DISABLE", "CH1_AMIC1", "CH1_AMIC2", "CH1_AMIC3", "CH1_AMIC4", +}; + +static const char * const adc2_mux_text[] = { + "CH2_AMIC_DISABLE", "CH2_AMIC1", "CH2_AMIC2", "CH2_AMIC3", "CH2_AMIC4", +}; + +static const char * const adc3_mux_text[] = { + "CH3_AMIC_DISABLE", "CH3_AMIC1", "CH3_AMIC3", "CH3_AMIC4", +}; + +static const struct soc_enum adc1_mux_enum = + SOC_ENUM_SINGLE(WCD9378_TX_NEW_CH12_MUX, 0, + ARRAY_SIZE(adc1_mux_text), adc1_mux_text); +static const struct soc_enum adc2_mux_enum = + SOC_ENUM_SINGLE(WCD9378_TX_NEW_CH12_MUX, 3, + ARRAY_SIZE(adc2_mux_text), adc2_mux_text); +static const struct soc_enum adc3_mux_enum = + SOC_ENUM_SINGLE(WCD9378_TX_NEW_CH34_MUX, 0, + ARRAY_SIZE(adc3_mux_text), adc3_mux_text); + +static const struct snd_kcontrol_new adc1_mux = SOC_DAPM_ENUM("ADC1 MUX", adc1_mux_enum); +static const struct snd_kcontrol_new adc2_mux = SOC_DAPM_ENUM("ADC2 MUX", adc2_mux_enum); +static const struct snd_kcontrol_new adc3_mux = SOC_DAPM_ENUM("ADC3 MUX", adc3_mux_enum); + +static const struct snd_kcontrol_new tx0_seq_switch = + SOC_DAPM_SINGLE("Switch", SND_SOC_NOPM, 0, 1, 0); +static const struct snd_kcontrol_new tx1_seq_switch = + SOC_DAPM_SINGLE("Switch", SND_SOC_NOPM, 0, 1, 0); +static const struct snd_kcontrol_new tx2_seq_switch = + SOC_DAPM_SINGLE("Switch", SND_SOC_NOPM, 0, 1, 0); + +static const struct snd_soc_dapm_widget wcd9378_dapm_widgets[] = { + /* Analog mic inputs */ + SND_SOC_DAPM_INPUT("AMIC1"), + SND_SOC_DAPM_INPUT("AMIC2"), + SND_SOC_DAPM_INPUT("AMIC3"), + SND_SOC_DAPM_INPUT("AMIC4"), + + /* ADC input muxes */ + SND_SOC_DAPM_MUX("ADC1 MUX", SND_SOC_NOPM, 0, 0, &adc1_mux), + SND_SOC_DAPM_MUX("ADC2 MUX", SND_SOC_NOPM, 0, 0, &adc2_mux), + SND_SOC_DAPM_MUX("ADC3 MUX", SND_SOC_NOPM, 0, 0, &adc3_mux), + + /* SDCA TX sequencers (widget shift = ADC index) */ + SND_SOC_DAPM_MIXER_E("TX0 SEQUENCER", SND_SOC_NOPM, 0, 0, + &tx0_seq_switch, 1, wcd9378_tx_sequencer_enable, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_MIXER_E("TX1 SEQUENCER", SND_SOC_NOPM, 1, 0, + &tx1_seq_switch, 1, wcd9378_tx_sequencer_enable, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_MIXER_E("TX2 SEQUENCER", SND_SOC_NOPM, 2, 0, + &tx2_seq_switch, 1, wcd9378_tx_sequencer_enable, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), + + /* Micbias supplies (widget shift = micbias number) */ + SND_SOC_DAPM_SUPPLY("MIC BIAS1", SND_SOC_NOPM, MIC_BIAS_1, 0, + wcd9378_codec_enable_micbias, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_SUPPLY("MIC BIAS2", SND_SOC_NOPM, MIC_BIAS_2, 0, + wcd9378_codec_enable_micbias, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_SUPPLY("MIC BIAS3", SND_SOC_NOPM, MIC_BIAS_3, 0, + wcd9378_codec_enable_micbias, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + + SND_SOC_DAPM_SUPPLY("VA MIC BIAS1", SND_SOC_NOPM, MIC_BIAS_1, 0, + wcd9378_codec_enable_micbias_pullup, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_SUPPLY("VA MIC BIAS2", SND_SOC_NOPM, MIC_BIAS_2, 0, + wcd9378_codec_enable_micbias_pullup, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_SUPPLY("VA MIC BIAS3", SND_SOC_NOPM, MIC_BIAS_3, 0, + wcd9378_codec_enable_micbias_pullup, + SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | + SND_SOC_DAPM_POST_PMD), + + /* Outputs towards the SoundWire TX bus / LPASS TX macro */ + SND_SOC_DAPM_OUTPUT("ADC1_OUTPUT"), + SND_SOC_DAPM_OUTPUT("ADC2_OUTPUT"), + SND_SOC_DAPM_OUTPUT("ADC3_OUTPUT"), +}; + +static const struct snd_soc_dapm_route wcd9378_audio_map[] = { + { "ADC1_OUTPUT", NULL, "TX0 SEQUENCER" }, + { "TX0 SEQUENCER", "Switch", "ADC1 MUX" }, + { "ADC1 MUX", "CH1_AMIC1", "AMIC1" }, + { "ADC1 MUX", "CH1_AMIC2", "AMIC2" }, + { "ADC1 MUX", "CH1_AMIC3", "AMIC3" }, + { "ADC1 MUX", "CH1_AMIC4", "AMIC4" }, + + { "ADC2_OUTPUT", NULL, "TX1 SEQUENCER" }, + { "TX1 SEQUENCER", "Switch", "ADC2 MUX" }, + { "ADC2 MUX", "CH2_AMIC1", "AMIC1" }, + { "ADC2 MUX", "CH2_AMIC2", "AMIC2" }, + { "ADC2 MUX", "CH2_AMIC3", "AMIC3" }, + { "ADC2 MUX", "CH2_AMIC4", "AMIC4" }, + + { "ADC3_OUTPUT", NULL, "TX2 SEQUENCER" }, + { "TX2 SEQUENCER", "Switch", "ADC3 MUX" }, + { "ADC3 MUX", "CH3_AMIC1", "AMIC1" }, + { "ADC3 MUX", "CH3_AMIC3", "AMIC3" }, + { "ADC3 MUX", "CH3_AMIC4", "AMIC4" }, +}; + +/* + * Map the DT micbias millivolt values onto ITxx_MICB usage codes; + * non-standard voltages go through the sequencer remap table. + */ +static void wcd9378_set_micb_usage_vals(struct snd_soc_component *component) +{ + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + static const u32 remap_regs[] = { + WCD9378_MICB_REMAP_TABLE_VAL_3, + WCD9378_MICB_REMAP_TABLE_VAL_4, + WCD9378_MICB_REMAP_TABLE_VAL_5, + }; + int i, vout; + + for (i = 0; i < WCD9378_MAX_MICBIAS; i++) { + switch (wcd9378->common.micb_mv[i]) { + case 1200: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_1P2V; + break; + case 1800: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_1P8V_OR_PULLUP; + break; + case 2200: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P2V; + break; + case 2500: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P5V; + break; + case 2700: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P7V; + break; + case 2750: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P75V; + break; + case 2800: + wcd9378->micb_usage_val[i] = WCD9378_MICB_USAGE_2P8V; + break; + default: + vout = wcd_get_micb_vout_ctl_val(component->dev, + wcd9378->common.micb_mv[i]); + if (vout < 0) { + wcd9378->micb_usage_val[i] = + WCD9378_MICB_USAGE_1P8V_OR_PULLUP; + break; + } + snd_soc_component_update_bits(component, remap_regs[i], + 0xff, vout); + wcd9378->micb_usage_val[i] = + WCD9378_MICB_USAGE_REMAP_TABLE_3 + i; + break; + } + } +} + +static int wcd9378_soc_codec_probe(struct snd_soc_component *component) +{ + struct wcd9378_priv *wcd9378 = snd_soc_component_get_drvdata(component); + struct device *dev = component->dev; + unsigned int part0 = 0, part1 = 0; + unsigned long time_left; + int ret; + + /* + * Mainline uses sdw_slave_wait_for_init() here; the v7.1.2 base + * predates that helper, open-code it. + */ + time_left = wait_for_completion_timeout(&wcd9378->tx_sdw_dev->initialization_complete, + msecs_to_jiffies(5000)); + if (!time_left) { + dev_err(dev, "soundwire device init timeout\n"); + return -ETIMEDOUT; + } + + snd_soc_component_init_regmap(component, wcd9378->regmap); + + ret = pm_runtime_resume_and_get(dev); + if (ret < 0) + return ret; + + regmap_read(wcd9378->regmap, WCD9378_DEV_PART_ID_0, &part0); + regmap_read(wcd9378->regmap, WCD9378_DEV_PART_ID_1, &part1); + dev_dbg(dev, "WCD9378 part id %#x\n", (part1 << 8) | part0); + + /* + * SDCA interrupt type configuration, mirroring the downstream init + * sequence. Nothing consumes these interrupts yet (no MBHC support); + * kept so the bring-up sequence validated on hardware is unchanged. + */ + sdw_write(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_SDCA_INTRTYPE_1, 0xff); + sdw_write(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_SDCA_INTRTYPE_2, 0x0b); + sdw_write(wcd9378->tx_sdw_dev, WCD9378_SWRS_SCP_SDCA_INTRTYPE_3, 0xff); + + wcd9378_io_init(component); + wcd9378_set_micb_usage_vals(component); + + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); + + return 0; +} + +static const struct snd_soc_component_driver soc_codec_dev_wcd9378 = { + .name = "wcd9378_codec", + .probe = wcd9378_soc_codec_probe, + .controls = wcd9378_snd_controls, + .num_controls = ARRAY_SIZE(wcd9378_snd_controls), + .dapm_widgets = wcd9378_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(wcd9378_dapm_widgets), + .dapm_routes = wcd9378_audio_map, + .num_dapm_routes = ARRAY_SIZE(wcd9378_audio_map), + .endianness = 1, +}; + +static int wcd9378_codec_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dai->dev); + struct wcd9378_sdw_priv *wcd = wcd9378->sdw_priv[dai->id]; + + return wcd9378_sdw_hw_params(wcd, substream, params, dai); +} + +static int wcd9378_codec_free(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dai->dev); + struct wcd9378_sdw_priv *wcd = wcd9378->sdw_priv[dai->id]; + + return sdw_stream_remove_slave(wcd->sdev, wcd->sruntime); +} + +static int wcd9378_codec_set_sdw_stream(struct snd_soc_dai *dai, + void *stream, int direction) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dai->dev); + struct wcd9378_sdw_priv *wcd = wcd9378->sdw_priv[dai->id]; + + wcd->sruntime = stream; + + return 0; +} + +static int wcd9378_get_channel_map(const struct snd_soc_dai *dai, + unsigned int *tx_num, unsigned int *tx_slot, + unsigned int *rx_num, unsigned int *rx_slot) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dai->dev); + struct wcd9378_sdw_priv *wcd = wcd9378->sdw_priv[dai->id]; + int i; + + switch (dai->id) { + case AIF1_PB: + if (!rx_slot || !rx_num) { + dev_err(dai->dev, "Invalid rx_slot %p or rx_num %p\n", + rx_slot, rx_num); + return -EINVAL; + } + + for (i = 0; i < SDW_MAX_PORTS; i++) + rx_slot[i] = wcd->master_channel_map[i]; + + *rx_num = i; + break; + case AIF1_CAP: + if (!tx_slot || !tx_num) { + dev_err(dai->dev, "Invalid tx_slot %p or tx_num %p\n", + tx_slot, tx_num); + return -EINVAL; + } + + for (i = 0; i < SDW_MAX_PORTS; i++) + tx_slot[i] = wcd->master_channel_map[i]; + + *tx_num = i; + break; + default: + break; + } + + return 0; +} + +static const struct snd_soc_dai_ops wcd9378_sdw_dai_ops = { + .hw_params = wcd9378_codec_hw_params, + .hw_free = wcd9378_codec_free, + .set_stream = wcd9378_codec_set_sdw_stream, + .get_channel_map = wcd9378_get_channel_map, +}; + +#define WCD9378_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 |\ + SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_48000 |\ + SNDRV_PCM_RATE_96000 | SNDRV_PCM_RATE_192000) +#define WCD9378_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE |\ + SNDRV_PCM_FMTBIT_S24_3LE | SNDRV_PCM_FMTBIT_S32_LE) + +static struct snd_soc_dai_driver wcd9378_dais[] = { + [0] = { + .name = "wcd9378-sdw-rx", + .playback = { + .stream_name = "WCD AIF Playback", + .rates = WCD9378_RATES, + .formats = WCD9378_FORMATS, + .rate_min = 8000, + .rate_max = 192000, + .channels_min = 1, + .channels_max = 4, + }, + .ops = &wcd9378_sdw_dai_ops, + }, + [1] = { + .name = "wcd9378-sdw-tx", + .capture = { + .stream_name = "WCD AIF Capture", + .rates = WCD9378_RATES, + .formats = WCD9378_FORMATS, + .rate_min = 8000, + .rate_max = 192000, + .channels_min = 1, + .channels_max = 4, + }, + .ops = &wcd9378_sdw_dai_ops, + }, +}; + +static int wcd9378_bind(struct device *dev) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dev); + int ret; + + /* Give the SDW subdevices some more time to settle */ + usleep_range(5000, 5010); + + ret = component_bind_all(dev, wcd9378); + if (ret) { + dev_err(dev, "Slave bind failed, ret = %d\n", ret); + return ret; + } + + wcd9378->rxdev = of_sdw_find_device_by_node(wcd9378->rxnode); + if (!wcd9378->rxdev) { + dev_err(dev, "could not find rx slave with matching of node\n"); + ret = -EINVAL; + goto err_component_unbind; + } + + wcd9378->sdw_priv[AIF1_PB] = dev_get_drvdata(wcd9378->rxdev); + wcd9378->sdw_priv[AIF1_PB]->wcd9378 = wcd9378; + + wcd9378->txdev = of_sdw_find_device_by_node(wcd9378->txnode); + if (!wcd9378->txdev) { + dev_err(dev, "could not find tx slave with matching of node\n"); + ret = -EINVAL; + goto err_put_rxdev; + } + + wcd9378->sdw_priv[AIF1_CAP] = dev_get_drvdata(wcd9378->txdev); + wcd9378->sdw_priv[AIF1_CAP]->wcd9378 = wcd9378; + wcd9378->tx_sdw_dev = dev_to_sdw_dev(wcd9378->txdev); + + /* + * As TX is the main CSR reg interface, it should not be suspended + * first. Explicitly add the dependency link. + */ + if (!device_link_add(wcd9378->rxdev, wcd9378->txdev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME)) { + dev_err(dev, "Could not devlink TX and RX\n"); + ret = -EINVAL; + goto err_put_txdev; + } + + if (!device_link_add(dev, wcd9378->txdev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME)) { + dev_err(dev, "Could not devlink WCD and TX\n"); + ret = -EINVAL; + goto err_remove_link1; + } + + if (!device_link_add(dev, wcd9378->rxdev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME)) { + dev_err(dev, "Could not devlink WCD and RX\n"); + ret = -EINVAL; + goto err_remove_link2; + } + + wcd9378->regmap = wcd9378->sdw_priv[AIF1_CAP]->regmap; + if (!wcd9378->regmap) { + dev_err(dev, "could not get TX device regmap\n"); + ret = -EINVAL; + goto err_remove_link3; + } + + /* + * The SDCA function engine dies when the TX bus enters clock-stop + * and only a codec reset revives it — registers keep their values + * so a regcache sync or a FUNC_ACT re-toggle does not help. The + * downstream stack sidesteps the same problem by marking the TX + * SoundWire master "qcom,is-always-on"; do the equivalent and + * keep the TX slave (and thus its bus) runtime-active while the + * codec is bound. + */ + ret = pm_runtime_resume_and_get(wcd9378->txdev); + if (ret < 0) { + dev_err(dev, "could not resume TX device\n"); + goto err_remove_link3; + } + + ret = snd_soc_register_component(dev, &soc_codec_dev_wcd9378, + wcd9378_dais, ARRAY_SIZE(wcd9378_dais)); + if (ret) { + dev_err(dev, "Codec registration failed\n"); + pm_runtime_put(wcd9378->txdev); + goto err_remove_link3; + } + + return ret; + +err_remove_link3: + device_link_remove(dev, wcd9378->rxdev); +err_remove_link2: + device_link_remove(dev, wcd9378->txdev); +err_remove_link1: + device_link_remove(wcd9378->rxdev, wcd9378->txdev); +err_put_txdev: + put_device(wcd9378->txdev); +err_put_rxdev: + put_device(wcd9378->rxdev); +err_component_unbind: + component_unbind_all(dev, wcd9378); + return ret; +} + +static void wcd9378_unbind(struct device *dev) +{ + struct wcd9378_priv *wcd9378 = dev_get_drvdata(dev); + + snd_soc_unregister_component(dev); + pm_runtime_put(wcd9378->txdev); + device_link_remove(dev, wcd9378->txdev); + device_link_remove(dev, wcd9378->rxdev); + device_link_remove(wcd9378->rxdev, wcd9378->txdev); + component_unbind_all(dev, wcd9378); + mutex_destroy(&wcd9378->micb_lock); + put_device(wcd9378->txdev); + put_device(wcd9378->rxdev); +} + +static const struct component_master_ops wcd9378_comp_ops = { + .bind = wcd9378_bind, + .unbind = wcd9378_unbind, +}; + +static int wcd9378_add_slave_components(struct wcd9378_priv *wcd9378, + struct device *dev, + struct component_match **matchptr) +{ + struct device_node *np = dev->of_node; + + wcd9378->rxnode = of_parse_phandle(np, "qcom,rx-device", 0); + if (!wcd9378->rxnode) { + dev_err(dev, "Couldn't parse phandle to qcom,rx-device!\n"); + return -ENODEV; + } + + component_match_add_release(dev, matchptr, component_release_of, + component_compare_of, wcd9378->rxnode); + + wcd9378->txnode = of_parse_phandle(np, "qcom,tx-device", 0); + if (!wcd9378->txnode) { + dev_err(dev, "Couldn't parse phandle to qcom,tx-device\n"); + return -ENODEV; + } + + component_match_add_release(dev, matchptr, component_release_of, + component_compare_of, wcd9378->txnode); + + return 0; +} + +static int wcd9378_probe(struct platform_device *pdev) +{ + struct component_match *match = NULL; + struct device *dev = &pdev->dev; + struct wcd9378_priv *wcd9378; + int ret, i; + + wcd9378 = devm_kzalloc(dev, sizeof(*wcd9378), GFP_KERNEL); + if (!wcd9378) + return -ENOMEM; + + dev_set_drvdata(dev, wcd9378); + mutex_init(&wcd9378->micb_lock); + for (i = 0; i < ARRAY_SIZE(wcd9378->tx_sys_bit); i++) + wcd9378->tx_sys_bit[i] = -1; + wcd9378->common.dev = dev; + wcd9378->common.max_bias = WCD9378_MAX_MICBIAS; + + wcd9378->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW); + if (IS_ERR(wcd9378->reset_gpio)) + return dev_err_probe(dev, PTR_ERR(wcd9378->reset_gpio), + "failed to get reset gpio\n"); + + ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(wcd9378_supplies), + wcd9378_supplies); + if (ret) + return dev_err_probe(dev, ret, + "Failed to get and enable supplies\n"); + + ret = wcd_dt_parse_micbias_info(&wcd9378->common); + if (ret) + return dev_err_probe(dev, ret, "Failed to get micbias\n"); + + ret = wcd9378_add_slave_components(wcd9378, dev, &match); + if (ret) + return ret; + + wcd9378_reset(wcd9378); + + ret = component_master_add_with_match(dev, &wcd9378_comp_ops, match); + if (ret) + return ret; + + pm_runtime_set_autosuspend_delay(dev, 1000); + pm_runtime_use_autosuspend(dev); + pm_runtime_mark_last_busy(dev); + pm_runtime_set_active(dev); + pm_runtime_enable(dev); + pm_runtime_idle(dev); + + return 0; +} + +static void wcd9378_remove(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + + component_master_del(dev, &wcd9378_comp_ops); + + pm_runtime_disable(dev); + pm_runtime_set_suspended(dev); + pm_runtime_dont_use_autosuspend(dev); +} + +static const struct of_device_id wcd9378_of_match[] = { + { .compatible = "qcom,wcd9378-codec" }, + { } +}; +MODULE_DEVICE_TABLE(of, wcd9378_of_match); + +static struct platform_driver wcd9378_codec_driver = { + .probe = wcd9378_probe, + .remove = wcd9378_remove, + .driver = { + .name = "wcd9378_codec", + .of_match_table = wcd9378_of_match, + .suppress_bind_attrs = true, + }, +}; +module_platform_driver(wcd9378_codec_driver); + +MODULE_DESCRIPTION("WCD9378 Codec driver"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wcd9378.h b/sound/soc/codecs/wcd9378.h new file mode 100644 index 000000000000..fb9e7925968a --- /dev/null +++ b/sound/soc/codecs/wcd9378.h @@ -0,0 +1,246 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2026, Jorijn van der Graaf + * + * Register map for the Qualcomm WCD9378 audio codec. + * + * The codec exposes its control registers in the SoundWire SDCA control + * address space (bit 30 set, SDCA function number in bits 25:22), accessed + * through the TX SoundWire slave. The analog core registers (function 0, + * implementation-defined region at +0x180000) are layout-compatible with + * the WCD937x family; on top of that the chip adds SDCA-style functions + * (SmartMIC0/1/2, SmartJACK, SmartAMP) whose sequencers drive the analog + * power-up autonomously. + */ + +#ifndef __WCD9378_H__ +#define __WCD9378_H__ + +#include +#include +#include +#include + +/* SDCA function 0 (extension unit): device identity */ +#define WCD9378_FUNC_EXT_ID_0 0x40000048 +#define WCD9378_FUNC_EXT_ID_1 0x40000049 +#define WCD9378_FUNC_EXT_VER 0x40000050 +#define WCD9378_FUNC_STAT 0x40080000 +#define WCD9378_DEV_MANU_ID_0 0x40100060 +#define WCD9378_DEV_MANU_ID_1 0x40100061 +#define WCD9378_DEV_PART_ID_0 0x40100068 +#define WCD9378_DEV_PART_ID_1 0x40100069 +#define WCD9378_DEV_VER 0x40100070 + +/* Analog core (WCD937x-compatible layout), function 0 + 0x180000 */ +#define WCD9378_ANA_PAGE 0x40180000 +#define WCD9378_ANA_BIAS 0x40180001 +#define WCD9378_ANA_BIAS_ANALOG_BIAS_EN BIT(7) +#define WCD9378_ANA_BIAS_PRECHRG_EN BIT(6) +#define WCD9378_ANA_RX_SUPPLIES 0x40180008 +#define WCD9378_ANA_TX_CH1 0x4018000e +#define WCD9378_ANA_TX_CH2 0x4018000f +#define WCD9378_ANA_TX_CH2_HPF1_INIT BIT(6) +#define WCD9378_ANA_TX_CH2_HPF2_INIT BIT(5) +#define WCD9378_ANA_TX_CH3 0x40180010 +#define WCD9378_ANA_TX_CH3_HPF 0x40180011 +#define WCD9378_ANA_TX_CH3_HPF3_INIT BIT(6) +#define WCD9378_ANA_TX_GAIN_MASK GENMASK(4, 0) +#define WCD9378_ANA_MICB1 0x40180022 +#define WCD9378_ANA_MICB2 0x40180023 +#define WCD9378_ANA_MICB2_RAMP 0x40180024 +#define WCD9378_ANA_MICB2_RAMP_SHIFT_CTL_MASK GENMASK(4, 2) +#define WCD9378_ANA_MICB2_RAMP_EN BIT(7) +#define WCD9378_ANA_MICB3 0x40180025 +#define WCD9378_BIAS_VBG_FINE_ADJ 0x40180029 +#define WCD9378_MBHC_CTL_SPARE_1 0x40180058 +#define WCD9378_MICB1_TEST_CTL_2 0x4018006c +#define WCD9378_MICB2_TEST_CTL_2 0x4018006f +#define WCD9378_MICB3_TEST_CTL_2 0x40180072 +#define WCD9378_TX_COM_TXFE_DIV_CTL 0x4018007b +#define WCD9378_TX_COM_TXFE_DIV_SEQ_BYPASS BIT(7) +#define WCD9378_SLEEP_CTL 0x40180103 +#define WCD9378_SLEEP_CTL_BG_CTL_MASK GENMASK(3, 1) +#define WCD9378_SLEEP_CTL_BG_EN BIT(7) +#define WCD9378_SLEEP_CTL_LDOL_BG_SEL BIT(6) +#define WCD9378_TX_NEW_CH12_MUX 0x4018012e +#define WCD9378_TX_NEW_CH12_MUX_CH1_SEL_MASK GENMASK(2, 0) +#define WCD9378_TX_NEW_CH12_MUX_CH2_SEL_MASK GENMASK(5, 3) +#define WCD9378_TX_NEW_CH34_MUX 0x4018012f +#define WCD9378_TX_NEW_CH34_MUX_CH3_SEL_MASK GENMASK(2, 0) +#define WCD9378_HPH_RDAC_GAIN_CTL 0x40180132 +#define WCD9378_HPH_RDAC_HD2_CTL_L 0x40180133 +#define WCD9378_HPH_RDAC_HD2_CTL_R 0x40180136 + +/* Digital page */ +#define WCD9378_TOP_CLK_CFG 0x40180407 +#define WCD9378_CDC_ANA_TX_CLK_CTL 0x40180417 +#define WCD9378_CDC_ANA_TXSCBIAS_CLK_EN BIT(0) +#define WCD9378_CDC_AMIC_CTL 0x4018045a +#define WCD9378_PDM_WD_CTL0 0x40180465 +#define WCD9378_PDM_WD_CTL1 0x40180466 +#define WCD9378_EFUSE_REG_16 0x401804c0 +#define WCD9378_EFUSE_REG_29 0x401804cd +#define WCD9378_PLATFORM_CTL 0x401804f0 + +/* Sequencer block (SEQR) */ +#define WCD9378_SYS_USAGE_CTRL 0x40180501 +#define WCD9378_SYS_USAGE_CTRL_MASK GENMASK(3, 0) +#define WCD9378_HPH_UP_T0 0x40180510 +#define WCD9378_HPH_UP_T9 0x40180519 +#define WCD9378_HPH_DN_T0 0x4018051b +#define WCD9378_SEQ_TX0_STAT 0x40180592 +#define WCD9378_SEQ_TX1_STAT 0x40180593 +#define WCD9378_SEQ_TX2_STAT 0x40180594 +#define WCD9378_MICB_REMAP_TABLE_VAL_3 0x401805a3 +#define WCD9378_MICB_REMAP_TABLE_VAL_4 0x401805a4 +#define WCD9378_MICB_REMAP_TABLE_VAL_5 0x401805a5 +#define WCD9378_SM0_MB_SEL 0x401805b0 +#define WCD9378_SM1_MB_SEL 0x401805b1 +#define WCD9378_SM2_MB_SEL 0x401805b2 +#define WCD9378_SM_MB_SEL_MASK GENMASK(1, 0) +#define WCD9378_MB_PULLUP_EN 0x401805b3 + +/* SmartAMP SDCA function */ +#define WCD9378_SMP_AMP_FUNC_STAT 0x40880000 +#define WCD9378_SMP_AMP_FUNC_ACT 0x40880008 + +/* SmartJACK SDCA function (hosts ADC2 when fed from AMIC2) */ +#define WCD9378_CMT_GRP_MASK 0x40c00008 +#define WCD9378_SMP_JACK_IT31_MICB 0x40c00798 +#define WCD9378_SMP_JACK_IT31_USAGE 0x40c007a0 +#define WCD9378_SMP_JACK_PDE34_REQ_PS 0x40c00808 +#define WCD9378_SMP_JACK_FUNC_STAT 0x40c80000 +#define WCD9378_SMP_JACK_FUNC_ACT 0x40c80008 +#define WCD9378_SMP_JACK_PDE34_ACT_PS 0x40c80800 + +/* SmartMIC0/1/2 SDCA functions (ADC1/ADC2/ADC3 sequencers) */ +#define WCD9378_SMP_MIC_BASE(n) (0x41000000 + (n) * 0x400000) +#define WCD9378_SMP_MIC_IT11_MICB(n) (WCD9378_SMP_MIC_BASE(n) + 0x98) +#define WCD9378_SMP_MIC_IT11_USAGE(n) (WCD9378_SMP_MIC_BASE(n) + 0xa0) +#define WCD9378_SMP_MIC_PDE11_REQ_PS(n) (WCD9378_SMP_MIC_BASE(n) + 0x108) +#define WCD9378_SMP_MIC_OT10_USAGE(n) (WCD9378_SMP_MIC_BASE(n) + 0x3a0) +#define WCD9378_SMP_MIC_FUNC_STAT(n) (WCD9378_SMP_MIC_BASE(n) + 0x80000) +#define WCD9378_SMP_MIC_FUNC_ACT(n) (WCD9378_SMP_MIC_BASE(n) + 0x80008) +#define WCD9378_SMP_MIC_PDE11_ACT_PS(n) (WCD9378_SMP_MIC_BASE(n) + 0x80100) + +#define WCD9378_MAX_REGISTER 0x41900070 + +/* + * Raw (16-bit, non-paged) Qualcomm slave SCP registers, written with + * sdw_write() directly. Bus clock indication towards the codec and + * SDCA interrupt type configuration. + */ +#define WCD9378_SWRS_SCP_BASE_CLK 0x4d +#define WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0 0x62 +#define WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1 0x72 +#define WCD9378_SWRS_SCP_SDCA_INTRTYPE_1 0xf4 +#define WCD9378_SWRS_SCP_SDCA_INTRTYPE_2 0xf8 +#define WCD9378_SWRS_SCP_SDCA_INTRTYPE_3 0xfc +#define WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(m) (0xe0 + 0x10 * (m)) +#define WCD9378_SWRS_BASE_CLK_19P2MHZ 0x01 +#define WCD9378_SWRS_CLK_SCALE_DIV2 0x02 /* 9.6 MHz */ +#define WCD9378_SWRS_CLK_SCALE_DIV4 0x03 /* 4.8 MHz */ + +/* ITxx_USAGE ADC mode values */ +#define WCD9378_ADC_USAGE_HIFI 0x01 +#define WCD9378_ADC_USAGE_LO_HIF 0x02 +#define WCD9378_ADC_USAGE_NORMAL 0x03 +#define WCD9378_ADC_USAGE_LP 0x05 +#define WCD9378_ADC_USAGE_OFF 0x00 + +/* ITxx_MICB usage values */ +#define WCD9378_MICB_USAGE_OFF 0x00 +#define WCD9378_MICB_USAGE_PULL_DOWN 0x01 +#define WCD9378_MICB_USAGE_1P2V 0x02 +#define WCD9378_MICB_USAGE_1P8V_OR_PULLUP 0x03 +#define WCD9378_MICB_USAGE_2P5V 0x04 +#define WCD9378_MICB_USAGE_2P75V 0x05 +#define WCD9378_MICB_USAGE_2P2V 0xf0 +#define WCD9378_MICB_USAGE_2P7V 0xf1 +#define WCD9378_MICB_USAGE_2P8V 0xf2 +#define WCD9378_MICB_USAGE_REMAP_TABLE_3 0xf3 +#define WCD9378_MICB_USAGE_REMAP_TABLE_4 0xf4 +#define WCD9378_MICB_USAGE_REMAP_TABLE_5 0xf5 + +/* PDExx_REQ_PS power states */ +#define WCD9378_PDE_PS0_ON 0x00 +#define WCD9378_PDE_PS3_OFF 0x03 + +#define WCD9378_MAX_MICBIAS 3 +#define WCD9378_MAX_SWR_CH_IDS 15 +#define WCD9378_SWRM_CH_MASK(ch_idx) BIT((ch_idx) - 1) + +enum wcd9378_tx_sdw_ports { + WCD9378_ADC_1_PORT = 1, + WCD9378_ADC_2_PORT, + WCD9378_ADC_3_PORT, + WCD9378_DMIC_0_1_MBHC_PORT, + WCD9378_DMIC_2_5_PORT, + WCD9378_MAX_TX_SWR_PORTS = WCD9378_DMIC_2_5_PORT, +}; + +enum wcd9378_rx_sdw_ports { + WCD9378_HPH_PORT = 1, + WCD9378_CLSH_PORT, + WCD9378_COMP_PORT, + WCD9378_LO_PORT, + WCD9378_DSD_PORT, + WCD9378_MAX_SWR_PORTS = WCD9378_DSD_PORT, +}; + +enum wcd9378_tx_sdw_channels { + WCD9378_ADC1, + WCD9378_ADC2, + WCD9378_ADC3, + WCD9378_DMIC0, + WCD9378_DMIC1, + WCD9378_MBHC, + WCD9378_DMIC2, + WCD9378_DMIC3, + WCD9378_DMIC4, + WCD9378_DMIC5, +}; + +enum wcd9378_rx_sdw_channels { + WCD9378_HPH_L, + WCD9378_HPH_R, + WCD9378_CLSH, + WCD9378_COMP_L, + WCD9378_COMP_R, + WCD9378_LO, + WCD9378_DSD_L, + WCD9378_DSD_R, +}; + +struct wcd9378_priv; +struct wcd9378_sdw_priv { + struct sdw_slave *sdev; + struct sdw_stream_config sconfig; + struct sdw_stream_runtime *sruntime; + struct sdw_port_config port_config[WCD9378_MAX_SWR_PORTS]; + struct wcd_sdw_ch_info *ch_info; + bool port_enable[WCD9378_MAX_SWR_CH_IDS]; + unsigned int master_channel_map[SDW_MAX_PORTS]; + int active_ports; + bool is_tx; + struct wcd9378_priv *wcd9378; + struct regmap *regmap; +}; + +#if IS_ENABLED(CONFIG_SND_SOC_WCD9378_SDW) +int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai); +#else +static inline int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + return -EOPNOTSUPP; +} +#endif + +#endif /* __WCD9378_H__ */ diff --git a/sound/soc/qcom/qdsp6/Makefile b/sound/soc/qcom/qdsp6/Makefile index 67267304e7e9..d650e0b0ba5b 100644 --- a/sound/soc/qcom/qdsp6/Makefile +++ b/sound/soc/qcom/qdsp6/Makefile @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-only snd-q6dsp-common-y := q6dsp-common.o q6dsp-lpass-ports.o q6dsp-lpass-clocks.o -snd-q6apm-y := q6apm.o audioreach.o topology.o +snd-q6apm-y := q6apm.o audioreach.o topology.o q6apm-voice-proto.o obj-$(CONFIG_SND_SOC_QDSP6_COMMON) += snd-q6dsp-common.o obj-$(CONFIG_SND_SOC_QDSP6_CORE) += q6core.o diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c index a13f753eff98..4fea75b021e8 100644 --- a/sound/soc/qcom/qdsp6/audioreach.c +++ b/sound/soc/qcom/qdsp6/audioreach.c @@ -576,10 +576,11 @@ void *audioreach_alloc_graph_pkt(struct q6apm *apm, } EXPORT_SYMBOL_GPL(audioreach_alloc_graph_pkt); -int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, +int audioreach_send_cmd_sync_timeout(struct device *dev, gpr_device_t *gdev, struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait, - const struct gpr_pkt *pkt, uint32_t rsp_opcode) + const struct gpr_pkt *pkt, uint32_t rsp_opcode, + unsigned long timeout_s) { const struct gpr_hdr *hdr = &pkt->hdr; @@ -601,9 +602,9 @@ int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, if (rsp_opcode) rc = wait_event_timeout(*cmd_wait, (result->opcode == hdr->opcode) || - (result->opcode == rsp_opcode), 5 * HZ); + (result->opcode == rsp_opcode), timeout_s * HZ); else - rc = wait_event_timeout(*cmd_wait, (result->opcode == hdr->opcode), 5 * HZ); + rc = wait_event_timeout(*cmd_wait, (result->opcode == hdr->opcode), timeout_s * HZ); if (!rc) { dev_err(dev, "CMD timeout for [%x] opcode\n", hdr->opcode); @@ -620,6 +621,16 @@ err: mutex_unlock(cmd_lock); return rc; } +EXPORT_SYMBOL_GPL(audioreach_send_cmd_sync_timeout); + +int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, + struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock, + gpr_port_t *port, wait_queue_head_t *cmd_wait, + const struct gpr_pkt *pkt, uint32_t rsp_opcode) +{ + return audioreach_send_cmd_sync_timeout(dev, gdev, result, cmd_lock, + port, cmd_wait, pkt, rsp_opcode, 30); +} EXPORT_SYMBOL_GPL(audioreach_send_cmd_sync); int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, const struct gpr_pkt *pkt, diff --git a/sound/soc/qcom/qdsp6/audioreach.h b/sound/soc/qcom/qdsp6/audioreach.h index 6859770b38a6..e52c4b564cbc 100644 --- a/sound/soc/qcom/qdsp6/audioreach.h +++ b/sound/soc/qcom/qdsp6/audioreach.h @@ -58,12 +58,15 @@ struct q6apm_graph; #define APM_CMD_GRAPH_FLUSH 0x01001005 #define APM_CMD_SET_CFG 0x01001006 #define APM_CMD_GET_CFG 0x01001007 +#define APM_CMD_REGISTER_CFG 0x01001008 #define APM_CMD_SHARED_MEM_MAP_REGIONS 0x0100100C #define APM_CMD_SHARED_MEM_UNMAP_REGIONS 0x0100100D #define APM_CMD_RSP_SHARED_MEM_MAP_REGIONS 0x02001001 #define APM_CMD_RSP_GET_CFG 0x02001000 #define APM_CMD_CLOSE_ALL 0x01001013 #define APM_CMD_REGISTER_SHARED_CFG 0x0100100A +#define AMDB_CMD_LOAD_MODULES 0x01001020 +#define AMDB_CMD_RSP_LOAD_MODULES 0x02001008 #define APM_MEMORY_MAP_SHMEM8_4K_POOL 3 @@ -842,6 +845,11 @@ void audioreach_graph_free_buf(struct q6apm_graph *graph); int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait, const struct gpr_pkt *pkt, uint32_t rsp_opcode); +int audioreach_send_cmd_sync_timeout(struct device *dev, gpr_device_t *gdev, + struct gpr_ibasic_rsp_result_t *result, + struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait, + const struct gpr_pkt *pkt, uint32_t rsp_opcode, + unsigned long timeout_s); int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, const struct gpr_pkt *pkt, uint32_t rsp_opcode); int audioreach_set_media_format(struct q6apm_graph *graph, 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) { diff --git a/sound/soc/qcom/qdsp6/q6apm-voice-proto.c b/sound/soc/qcom/qdsp6/q6apm-voice-proto.c new file mode 100644 index 000000000000..87055f979e7a --- /dev/null +++ b/sound/soc/qcom/qdsp6/q6apm-voice-proto.c @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Voice-call bring-up prototype for the Fairphone (Gen. 6): replay + * pre-generated APM command sequences through the q6apm GPR channel. + * + * The blobs reproduce, byte for byte, the GRAPH_OPEN/SET_CFG/PREPARE/START + * sequence the stock Android PAL/AGM/GSL stack sends to set up the + * handset voice-call graphs (mined from the device ACDB; generated by + * utilities/mkvoiceblobs.py in the bring-up repo). The modem's voice + * engine and the ADSP VCPM service wire up the vocoder themselves once + * these graphs are running. + * + * Local prototype only, NOT for upstream. Interface: + * echo > /sys/kernel/debug/q6apm-voice-proto/play + * where is relative to /lib/firmware. Playback aborts on the + * first command the DSP rejects. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "audioreach.h" +#include "q6apm.h" + +#define Q6VP_MAGIC 0x50563651 /* "Q6VP" little-endian */ +#define Q6VP_VERSION 1 + +/* + * A record opcode with this bit set is sent out-of-band: the payload is + * placed in an ADSP-mapped shared-memory region and the GPR command carries + * only an apm_cmd_header referencing it. Stock GSL sends every voice + * GRAPH_OPEN this way; it is mandatory because a voice graph must be opened + * atomically as one command (all subgraphs + cross-subgraph module + * connections and control links), and the whole payload (~4.3-4.7 KB) + * exceeds the ~4 KiB in-band GPR/GLINK intent limit. Splitting the open + * per-subgraph to fit in-band makes the cross-referencing TX DevicePP + * subgraph invalid (rejected at open) — see journal/calls.md 2026-07-08. + */ +#define Q6VP_OOB_FLAG 0x80000000 +/* + * A record opcode with this bit set carries the GPR destination port in the + * first payload word (stripped before sending). Lets a blob address static + * services other than APM — e.g. AMDB (port 3) module pre-loading, so the + * dynamic Fluence pp modules can be made resident BEFORE the voice + * GRAPH_OPENs. A first open that has to FastRPC-load them can fail or block + * so long that the client-side retry dance corrupts VCPM's per-VSID voice + * session (TX cal EBADPARAM / TX START EFAILED, journal/calls.md + * 2026-07-10). In-band records only. + */ +#define Q6VP_DSTPORT_FLAG 0x40000000 +#define Q6VP_GRAPH_ID 0xf000 /* private mem-map handle owner */ +#define Q6VP_BUF_SZ 0x800000 /* 8 MiB shared region */ +#define Q6VP_SCRATCH_SZ 0x10000 /* [0, 64K): transient commands */ +#define Q6VP_SID_MASK 0xf + +struct q6vp_hdr { + __le32 magic; + __le32 version; + __le32 num_records; +} __packed; + +struct q6vp_rec { + __le32 opcode; + __le32 size; +} __packed; + +static DEFINE_MUTEX(q6vp_play_lock); +static struct q6apm *q6vp_apm; +static struct dentry *q6vp_dir; +static struct device *q6vp_dais_dev; +static struct audioreach_graph_info *q6vp_info; +static void *q6vp_buf; +static dma_addr_t q6vp_buf_iova; +static phys_addr_t q6vp_buf_dsp_addr; + +static int q6vp_send(uint32_t opcode, const void *payload, uint32_t size) +{ + uint32_t dest_port = APM_MODULE_INSTANCE_ID; + uint32_t rsp_opcode = 0; + struct gpr_pkt *pkt; + int ret; + + if (opcode & Q6VP_DSTPORT_FLAG) { + if (size < sizeof(__le32)) + return -EINVAL; + dest_port = le32_to_cpup(payload); + payload += sizeof(__le32); + size -= sizeof(__le32); + opcode &= ~Q6VP_DSTPORT_FLAG; + } + + /* AMDB load responds with its own opcode, not an ibasic result */ + if (opcode == AMDB_CMD_LOAD_MODULES) + rsp_opcode = AMDB_CMD_RSP_LOAD_MODULES; + /* GET_CFG responds with the param data; payload dumped in callback */ + if (opcode == APM_CMD_GET_CFG) + rsp_opcode = APM_CMD_RSP_GET_CFG; + + pkt = audioreach_alloc_cmd_pkt(size, opcode, 0, GPR_APM_MODULE_IID, + dest_port); + if (IS_ERR(pkt)) + return PTR_ERR(pkt); + + memcpy((void *)pkt + GPR_HDR_SIZE + APM_CMD_HDR_SIZE, payload, size); + + /* + * Voice blobs need the long wait: a cold TX GRAPH_OPEN that has to + * FastRPC-load the Fluence pp modules takes >120 s. Regular audio + * commands (and especially the probe-time GET_SPF_STATE ready poll) + * must keep the short default or they hold the shared command mutex + * hostage at boot: a 240 s stall in apm_probe delays module loading + * past udev's worker timeout and the sound card never assembles. + */ + ret = q6apm_send_cmd_sync_timeout(q6vp_apm, pkt, rsp_opcode, 240); + kfree(pkt); + + return ret; +} + +/* + * Lazily set up the shared-memory region used for OOB commands: allocate a + * DMA buffer against the q6apm-dais child device (it carries the iommus + * mapping the ADSP expects, same as the PCM data path), register a private + * graph_info so the APM_CMD_RSP_SHARED_MEM_MAP_REGIONS callback has a slot + * to store the handle in, and map the region with the DSP. The mapping is + * kept until the module goes away. + */ +static int q6vp_oob_init(void) +{ + struct device *dev = q6vp_apm->dev; + struct of_phandle_args args; + struct platform_device *pdev; + struct device_node *np; + u64 sid = 0; + u32 graph_id = Q6VP_GRAPH_ID; + int ret; + + if (q6vp_info && q6vp_info->mem_map_handle) + return 0; + + if (!q6vp_dais_dev) { + np = of_get_compatible_child(dev->of_node, "qcom,q6apm-dais"); + if (!np) { + dev_err(dev, "voice-proto: no q6apm-dais node\n"); + return -ENODEV; + } + if (!of_parse_phandle_with_fixed_args(np, "iommus", 1, 0, + &args)) { + sid = args.args[0] & Q6VP_SID_MASK; + of_node_put(args.np); + } + pdev = of_find_device_by_node(np); + of_node_put(np); + if (!pdev) { + dev_err(dev, "voice-proto: no q6apm-dais device\n"); + return -ENODEV; + } + q6vp_dais_dev = &pdev->dev; + + q6vp_buf = dma_alloc_coherent(q6vp_dais_dev, Q6VP_BUF_SZ, + &q6vp_buf_iova, GFP_KERNEL); + if (!q6vp_buf) + return -ENOMEM; + q6vp_buf_dsp_addr = q6vp_buf_iova | (sid << 32); + } + + if (!q6vp_info) { + q6vp_info = kzalloc(sizeof(*q6vp_info), GFP_KERNEL); + if (!q6vp_info) + return -ENOMEM; + INIT_LIST_HEAD(&q6vp_info->sg_list); + q6vp_info->id = Q6VP_GRAPH_ID; + ret = idr_alloc_u32(&q6vp_apm->graph_info_idr, q6vp_info, + &graph_id, graph_id, GFP_KERNEL); + if (ret < 0) { + kfree(q6vp_info); + q6vp_info = NULL; + return ret; + } + } + + ret = q6apm_map_memory_fixed_region(q6vp_dais_dev, Q6VP_GRAPH_ID, + q6vp_buf_dsp_addr, Q6VP_BUF_SZ); + if (ret) { + dev_err(dev, "voice-proto: mem map failed (%d)\n", ret); + return ret; + } + + dev_info(dev, "voice-proto: OOB region mapped, handle 0x%x\n", + q6vp_info->mem_map_handle); + return 0; +} + +/* + * Persistent-slice bump offset: APM_CMD_REGISTER_CFG registers the payload + * with the DSP, which keeps reading that memory for as long as the config + * stays registered — those payloads must never be overwritten. They get + * bump-allocated slices above Q6VP_SCRATCH_SZ that live until reboot; + * transient commands (opens etc.) reuse the scratch slot at offset 0. + */ +static u32 q6vp_persist_off = Q6VP_SCRATCH_SZ; + +static int q6vp_send_oob(uint32_t opcode, const void *payload, uint32_t size) +{ + struct apm_cmd_header *cmd_header; + struct gpr_pkt *pkt; + u32 off = 0; + int ret; + + ret = q6vp_oob_init(); + if (ret) + return ret; + + if (opcode == APM_CMD_REGISTER_CFG) { + if (q6vp_persist_off + size > Q6VP_BUF_SZ) + return -EFBIG; + off = q6vp_persist_off; + q6vp_persist_off += ALIGN(size, SZ_4K); + } else if (size > Q6VP_SCRATCH_SZ) { + return -EFBIG; + } + + memcpy(q6vp_buf + off, payload, size); + + pkt = audioreach_alloc_apm_cmd_pkt(0, opcode, 0); + if (IS_ERR(pkt)) + return PTR_ERR(pkt); + + /* + * The region is mapped in absolute-address mode (mainline's + * q6apm_map_memory_fixed_region passes property_flag 0, unlike GSL + * which maps with IS_OFFSET_MODE and then references offset 0), so + * the header carries the full DSP-visible address of the buffer. + */ + cmd_header = (void *)pkt + GPR_HDR_SIZE; + cmd_header->payload_address_lsw = lower_32_bits(q6vp_buf_dsp_addr + off); + cmd_header->payload_address_msw = upper_32_bits(q6vp_buf_dsp_addr + off); + cmd_header->mem_map_handle = q6vp_info->mem_map_handle; + cmd_header->payload_size = size; + + ret = q6apm_send_cmd_sync_timeout(q6vp_apm, pkt, 0, 240); + kfree(pkt); + + return ret; +} + +static int q6vp_play(const char *name) +{ + const struct firmware *fw; + const struct q6vp_hdr *hdr; + const struct q6vp_rec *rec; + struct device *dev = q6vp_apm->dev; + size_t off; + uint32_t i, num, opcode, size; + int ret, err = 0; + + ret = request_firmware(&fw, name, dev); + if (ret) { + dev_err(dev, "voice-proto: cannot load %s (%d)\n", name, ret); + return ret; + } + + hdr = (const struct q6vp_hdr *)fw->data; + if (fw->size < sizeof(*hdr) || + le32_to_cpu(hdr->magic) != Q6VP_MAGIC || + le32_to_cpu(hdr->version) != Q6VP_VERSION) { + dev_err(dev, "voice-proto: %s: bad header\n", name); + ret = -EINVAL; + goto out; + } + + num = le32_to_cpu(hdr->num_records); + off = sizeof(*hdr); + for (i = 0; i < num; i++) { + if (off + sizeof(*rec) > fw->size) { + ret = -EINVAL; + goto out; + } + rec = (const struct q6vp_rec *)(fw->data + off); + opcode = le32_to_cpu(rec->opcode); + size = le32_to_cpu(rec->size); + off += sizeof(*rec); + if (off + size > fw->size) { + ret = -EINVAL; + goto out; + } + + if (opcode & Q6VP_OOB_FLAG) + ret = (opcode & Q6VP_DSTPORT_FLAG) ? -EINVAL : + q6vp_send_oob(opcode & ~Q6VP_OOB_FLAG, + fw->data + off, size); + else + ret = q6vp_send(opcode, fw->data + off, size); + dev_info(dev, "voice-proto: %s[%u] opcode 0x%08x size %u -> %d\n", + name, i, + opcode & ~(Q6VP_OOB_FLAG | Q6VP_DSTPORT_FLAG), + size, ret); + /* keep going on DSP rejections; each result is logged */ + if (ret) + err = ret; + + off += ALIGN(size, 4); + } + +out: + release_firmware(fw); + return ret ? ret : err; +} + +static ssize_t q6vp_play_write(struct file *file, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + char name[128]; + int ret; + + if (!q6vp_apm) + return -ENODEV; + if (count >= sizeof(name)) + return -EINVAL; + if (copy_from_user(name, ubuf, count)) + return -EFAULT; + name[count] = '\0'; + strim(name); + + /* + * Serialize blob playback: concurrent debugfs writers (e.g. a + * dropped ssh session whose write is still stuck in a DSP wait) + * would interleave their records on the GPR channel and corrupt + * both sequences (seen 2026-07-10). + */ + ret = mutex_lock_interruptible(&q6vp_play_lock); + if (ret) + return ret; + ret = q6vp_play(name); + mutex_unlock(&q6vp_play_lock); + + return ret ? ret : count; +} + +static const struct file_operations q6vp_play_fops = { + .open = simple_open, + .write = q6vp_play_write, + .llseek = default_llseek, +}; + +void q6apm_voice_proto_init(struct q6apm *apm) +{ + q6vp_apm = apm; + q6vp_dir = debugfs_create_dir("q6apm-voice-proto", NULL); + debugfs_create_file("play", 0200, q6vp_dir, NULL, &q6vp_play_fops); +} + +void q6apm_voice_proto_exit(void) +{ + debugfs_remove_recursive(q6vp_dir); + q6vp_dir = NULL; + + if (q6vp_info) { + if (q6vp_info->mem_map_handle) + q6apm_unmap_memory_fixed_region(q6vp_dais_dev, + Q6VP_GRAPH_ID); + idr_remove(&q6vp_apm->graph_info_idr, Q6VP_GRAPH_ID); + kfree(q6vp_info); + q6vp_info = NULL; + } + if (q6vp_buf) { + dma_free_coherent(q6vp_dais_dev, Q6VP_BUF_SZ, q6vp_buf, + q6vp_buf_iova); + q6vp_buf = NULL; + } + q6vp_dais_dev = NULL; + q6vp_apm = NULL; +} diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index 2ab378fb5032..220f61cbd4c3 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -38,6 +38,16 @@ int q6apm_send_cmd_sync(struct q6apm *apm, const struct gpr_pkt *pkt, NULL, &apm->wait, pkt, rsp_opcode); } +int q6apm_send_cmd_sync_timeout(struct q6apm *apm, const struct gpr_pkt *pkt, + uint32_t rsp_opcode, unsigned long timeout_s) +{ + gpr_device_t *gdev = apm->gdev; + + return audioreach_send_cmd_sync_timeout(&gdev->dev, gdev, &apm->result, + &apm->lock, NULL, &apm->wait, + pkt, rsp_opcode, timeout_s); +} + static struct audioreach_graph *q6apm_get_audioreach_graph(struct q6apm *apm, uint32_t graph_id) { struct audioreach_graph_info *info; @@ -764,6 +774,8 @@ static int apm_probe(gpr_device_t *gdev) g_apm = apm; + q6apm_voice_proto_init(apm); + q6apm_get_apm_state(apm); ret = snd_soc_register_component(dev, &q6apm_audio_component, NULL, 0); @@ -781,6 +793,7 @@ static int apm_probe(gpr_device_t *gdev) static void apm_remove(gpr_device_t *gdev) { + q6apm_voice_proto_exit(); of_platform_depopulate(&gdev->dev); snd_soc_unregister_component(&gdev->dev); } @@ -817,6 +830,7 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) case GPR_BASIC_RSP_RESULT: switch (result->opcode) { case APM_CMD_SHARED_MEM_MAP_REGIONS: + case APM_CMD_REGISTER_CFG: case APM_CMD_GRAPH_START: case APM_CMD_GRAPH_OPEN: case APM_CMD_GRAPH_PREPARE: @@ -824,6 +838,7 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) case APM_CMD_GRAPH_FLUSH: case APM_CMD_GRAPH_STOP: case APM_CMD_SET_CFG: + case APM_CMD_GET_CFG: apm->result.opcode = result->opcode; apm->result.status = result->status; if (result->status) @@ -846,9 +861,62 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) wake_up(&apm->wait); break; default: + /* + * Unlisted command results (debug/probe opcodes sent + * via voice-proto) must still wake the sync waiter or + * every such send stalls for the full timeout. + */ + apm->result.opcode = result->opcode; + apm->result.status = result->status; + if (result->status) + dev_err(dev, "Error (%d) Processing 0x%08x cmd\n", + result->status, result->opcode); + wake_up(&apm->wait); break; } break; + case 0x03001000: { /* APM_EVENT_MODULE_TO_CLIENT */ + /* apm_module_event_t: event_id, payload_size, payload */ + const u32 *ev = data->payload; + u32 ev_sz = hdr->pkt_size - hdr->hdr_size * 4; + + dev_info(dev, "MODULE EVENT from 0x%x: event_id 0x%08x size %u\n", + hdr->src_port, ev[0], ev[1]); + print_hex_dump(KERN_INFO, "EVENT: ", DUMP_PREFIX_OFFSET, + 16, 4, ev, ev_sz, false); + break; + } + case APM_CMD_RSP_GET_CFG: { + /* status word, then apm_module_param_data_t sections */ + const u32 *rsp_pl = data->payload; + u32 pl_sz = hdr->pkt_size - hdr->hdr_size * 4; + + dev_info(dev, "GET_CFG rsp: status %d, %u bytes\n", + rsp_pl[0], pl_sz); + print_hex_dump(KERN_INFO, "GET_CFG: ", DUMP_PREFIX_OFFSET, + 16, 4, rsp_pl, pl_sz, false); + apm->result.opcode = hdr->opcode; + apm->result.status = rsp_pl[0]; + wake_up(&apm->wait); + break; + } + case AMDB_CMD_RSP_LOAD_MODULES: { + /* amdb_module_load_unload_t: proc_domain, error_code, + * num_modules, {module_id, handle_lsw, handle_msw}[]. + * A zero handle = module not registered with the AMDB. + */ + const u32 *amdb = data->payload; + u32 i, n = amdb[2]; + + for (i = 0; i < n; i++) + dev_info(dev, "AMDB load: module 0x%08x handle 0x%08x%08x\n", + amdb[3 + 3 * i], amdb[5 + 3 * i], + amdb[4 + 3 * i]); + apm->result.opcode = hdr->opcode; + apm->result.status = amdb[1]; + wake_up(&apm->wait); + break; + } case APM_CMD_RSP_SHARED_MEM_MAP_REGIONS: apm->result.opcode = hdr->opcode; apm->result.status = 0; diff --git a/sound/soc/qcom/qdsp6/q6apm.h b/sound/soc/qcom/qdsp6/q6apm.h index 376a36700c53..f8a66ff5f29f 100644 --- a/sound/soc/qcom/qdsp6/q6apm.h +++ b/sound/soc/qcom/qdsp6/q6apm.h @@ -144,6 +144,8 @@ int q6apm_unmap_memory_fixed_region(struct device *dev, unsigned int graph_id); /* Helpers */ int q6apm_send_cmd_sync(struct q6apm *apm, const struct gpr_pkt *pkt, uint32_t rsp_opcode); +int q6apm_send_cmd_sync_timeout(struct q6apm *apm, const struct gpr_pkt *pkt, + uint32_t rsp_opcode, unsigned long timeout_s); /* Callback for graph specific */ struct audioreach_module *q6apm_find_module_by_mid(struct q6apm_graph *graph, @@ -157,4 +159,8 @@ int q6apm_remove_initial_silence(struct device *dev, struct q6apm_graph *graph, int q6apm_remove_trailing_silence(struct device *dev, struct q6apm_graph *graph, uint32_t samples); int q6apm_set_real_module_id(struct device *dev, struct q6apm_graph *graph, uint32_t codec_id); int q6apm_get_hw_pointer(struct q6apm_graph *graph, int dir); + +/* FP6 voice-call bring-up prototype (q6apm-voice-proto.c) */ +void q6apm_voice_proto_init(struct q6apm *apm); +void q6apm_voice_proto_exit(void); #endif /* __APM_GRAPH_ */ 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: