summaryrefslogtreecommitdiff
path: root/sound
diff options
context:
space:
mode:
authorMark Brown <broonie@kernel.org>2026-03-16 04:13:07 +0300
committerMark Brown <broonie@kernel.org>2026-03-16 04:13:07 +0300
commit706d2dc0269e695979943f27b03389007e034db7 (patch)
tree7c5096a4f53a584fa3cf03f5901dafc62e6de8bf /sound
parent3e9cda2f4a33c6becc99f8a78946cbd02983852f (diff)
parentaf176d0787d219d9e07272988079ebb9be8efe6a (diff)
downloadlinux-706d2dc0269e695979943f27b03389007e034db7.tar.xz
ASoC: basic support for configuring bus keepers
James Calligeros <jcalligeros99@gmail.com> says: This series introduces some infrastructure to allow platform drivers to specify what a DAI should be doing when it is not active on the bus. The primary use case for this is configuring bus keepers which may be integrated into various codecs. The instigating use case for this functionality is an interesting bus topology on Apple Silicon laptops with multiple codecs. Most Apple Silicon laptops have six codecs split into groups of three, driving a pair of dual opposed woofers and a tweeter for L/R stereo sound. These codecs report the voltage and current across their connected voice coils back to the SoC via the SDOUT pin, represented as PCM data sent via configurable TDM slots. This data is used in conjunction with the connected speaker's Thiele/Small Parameters to ensure that the speaker is not being driven to levels that would permanently damage them. This is integrated into CoreAudio on macOS. speakersafetyd[1] handles this for Linux. All of the codec SDOUT pins are attached to a single receiver port on the SoC's I2S peripheral, however are split across two physical data lines (one each for the left and right codec groups). The receiver has an OR gate in front of it, which is used to sum the two lines. If at any point a codec is trying to transmit data, and the "opposite" line ends up floating high, the transmitting codec's data will be corrupted. We need to guarantee that the idle line stays idle. In the downstream Asahi Linux kernel[2], we set up one codec in each group to zero-fill or pull down its line while a codec on the opposite line is actively transmitting. This is done entirely in the codec driver, however this approach is over-fit for this one use case. This sort of functionality may also be of use for other hardware, so following previous mailing list discussions[3], I have tried to expose the functionality in a more configurable and generic way. I have integrated this approach into our downstream platform driver and select Devicetrees as an example of how this mechanism is intended to be used[4]. [1] https://github.com/AsahiLinux/speakersafetyd [2] https://github.com/AsahiLinux/linux/tree/bits/070-audio [3] https://lore.kernel.org/asahi/20250227-apple-codec-changes-v3-17-cbb130030acf@gmail.com/ [4] https://github.com/chadmed/tree/tdm-revised2 Link: https://patch.msgid.link/20260301-tdm-idle-slots-v3-0-c6ac5351489a@gmail.com
Diffstat (limited to 'sound')
-rw-r--r--sound/soc/codecs/tas2764.c95
-rw-r--r--sound/soc/codecs/tas2764.h11
-rw-r--r--sound/soc/codecs/tas2770.c75
-rw-r--r--sound/soc/codecs/tas2770.h12
-rw-r--r--sound/soc/soc-dai.c40
5 files changed, 233 insertions, 0 deletions
diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c
index 36e25e48b354..423b7073b302 100644
--- a/sound/soc/codecs/tas2764.c
+++ b/sound/soc/codecs/tas2764.c
@@ -44,6 +44,11 @@ struct tas2764_priv {
bool dac_powered;
bool unmuted;
+
+ struct {
+ int tx_mode;
+ unsigned int tx_mask;
+ } idle_slot_config;
};
#include "tas2764-quirks.h"
@@ -509,11 +514,101 @@ static int tas2764_set_dai_tdm_slot(struct snd_soc_dai *dai,
return 0;
}
+static int tas2764_write_sdout_idle_mask(struct tas2764_priv *tas2764, u32 mask)
+{
+ struct snd_soc_component *component = tas2764->component;
+ int i, ret;
+
+ /* Hardware supports up to 64 slots, but we don't */
+ for (i = 0; i < 4; i++) {
+ ret = snd_soc_component_write(component,
+ TAS2764_SDOUT_HIZ_1 + i,
+ (mask >> (i * 8)) & 0xff);
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int tas2764_set_dai_tdm_idle(struct snd_soc_dai *dai,
+ unsigned int tx_mask, unsigned int rx_mask,
+ int tx_mode, int rx_mode)
+{
+ struct snd_soc_component *component = dai->component;
+ struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component);
+ int ret;
+
+ /* We don't support setting anything on SDIN */
+ if (rx_mode)
+ return -EOPNOTSUPP;
+
+ if (tas2764->idle_slot_config.tx_mask == tx_mask &&
+ tas2764->idle_slot_config.tx_mode == tx_mode)
+ return 0;
+
+ switch (tx_mode) {
+ case SND_SOC_DAI_TDM_IDLE_ZERO:
+ if (!tx_mask)
+ return -EINVAL;
+
+ ret = tas2764_write_sdout_idle_mask(tas2764, tx_mask);
+ if (ret < 0)
+ return ret;
+
+ ret = snd_soc_component_update_bits(component,
+ TAS2764_SDOUT_HIZ_9,
+ TAS2764_SDOUT_HIZ_9_FORCE_0_EN,
+ TAS2764_SDOUT_HIZ_9_FORCE_0_EN);
+ if (ret < 0)
+ return ret;
+
+ tas2764->idle_slot_config.tx_mask = tx_mask;
+ tas2764->idle_slot_config.tx_mode = tx_mode;
+ break;
+ case SND_SOC_DAI_TDM_IDLE_HIZ:
+ case SND_SOC_DAI_TDM_IDLE_OFF:
+ /* HiZ mode does not support a slot mask */
+ ret = tas2764_write_sdout_idle_mask(tas2764, 0);
+ if (ret < 0)
+ return ret;
+
+ ret = snd_soc_component_update_bits(component,
+ TAS2764_SDOUT_HIZ_9,
+ TAS2764_SDOUT_HIZ_9_FORCE_0_EN, 0);
+ if (ret < 0)
+ return ret;
+
+ tas2764->idle_slot_config.tx_mask = 0;
+ tas2764->idle_slot_config.tx_mode = tx_mode;
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ return 0;
+}
+
+/* The SDOUT idle slot mask must be cropped based on the BCLK ratio */
+static int tas2764_set_bclk_ratio(struct snd_soc_dai *dai, unsigned int ratio)
+{
+ struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(dai->component);
+
+ if (!tas2764->idle_slot_config.tx_mask)
+ return 0;
+
+ tas2764->idle_slot_config.tx_mask &= GENMASK((ratio / 8) - 1, 0);
+
+ return tas2764_write_sdout_idle_mask(tas2764, tas2764->idle_slot_config.tx_mask);
+}
+
static const struct snd_soc_dai_ops tas2764_dai_ops = {
.mute_stream = tas2764_mute,
.hw_params = tas2764_hw_params,
.set_fmt = tas2764_set_fmt,
+ .set_bclk_ratio = tas2764_set_bclk_ratio,
.set_tdm_slot = tas2764_set_dai_tdm_slot,
+ .set_tdm_idle = tas2764_set_dai_tdm_idle,
.no_capture_mute = 1,
};
diff --git a/sound/soc/codecs/tas2764.h b/sound/soc/codecs/tas2764.h
index 538290ed3d92..4494bc4889dc 100644
--- a/sound/soc/codecs/tas2764.h
+++ b/sound/soc/codecs/tas2764.h
@@ -126,4 +126,15 @@
#define TAS2764_BOP_CFG0 TAS2764_REG(0X0, 0x1d)
+#define TAS2764_SDOUT_HIZ_1 TAS2764_REG(0x1, 0x3d)
+#define TAS2764_SDOUT_HIZ_2 TAS2764_REG(0x1, 0x3e)
+#define TAS2764_SDOUT_HIZ_3 TAS2764_REG(0x1, 0x3f)
+#define TAS2764_SDOUT_HIZ_4 TAS2764_REG(0x1, 0x40)
+#define TAS2764_SDOUT_HIZ_5 TAS2764_REG(0x1, 0x41)
+#define TAS2764_SDOUT_HIZ_6 TAS2764_REG(0x1, 0x42)
+#define TAS2764_SDOUT_HIZ_7 TAS2764_REG(0x1, 0x43)
+#define TAS2764_SDOUT_HIZ_8 TAS2764_REG(0x1, 0x44)
+#define TAS2764_SDOUT_HIZ_9 TAS2764_REG(0x1, 0x45)
+#define TAS2764_SDOUT_HIZ_9_FORCE_0_EN BIT(7)
+
#endif /* __TAS2764__ */
diff --git a/sound/soc/codecs/tas2770.c b/sound/soc/codecs/tas2770.c
index 6f878b01716f..d4d7d056141b 100644
--- a/sound/soc/codecs/tas2770.c
+++ b/sound/soc/codecs/tas2770.c
@@ -492,11 +492,86 @@ static int tas2770_set_dai_tdm_slot(struct snd_soc_dai *dai,
return 0;
}
+static int tas2770_set_dai_tdm_idle(struct snd_soc_dai *dai,
+ unsigned int tx_mask,
+ unsigned int rx_mask,
+ int tx_mode, int rx_mode)
+{
+ struct snd_soc_component *component = dai->component;
+ struct tas2770_priv *tas2770 = snd_soc_component_get_drvdata(component);
+ int ret;
+
+ /* We don't support setting anything for SDIN */
+ if (rx_mode)
+ return -EOPNOTSUPP;
+
+ if (tas2770->idle_tx_mode == tx_mode)
+ return 0;
+
+ switch (tx_mode) {
+ case SND_SOC_DAI_TDM_IDLE_PULLDOWN:
+ ret = snd_soc_component_update_bits(component, TAS2770_DIN_PD,
+ TAS2770_DIN_PD_SDOUT,
+ TAS2770_DIN_PD_SDOUT);
+ if (ret)
+ return ret;
+
+ break;
+ case SND_SOC_DAI_TDM_IDLE_ZERO:
+ ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4,
+ TAS2770_TDM_CFG_REG4_TX_KEEPER,
+ TAS2770_TDM_CFG_REG4_TX_KEEPER);
+ if (ret)
+ return ret;
+
+ ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4,
+ TAS2770_TDM_CFG_REG4_TX_FILL, 0);
+ if (ret)
+ return ret;
+
+ break;
+ case SND_SOC_DAI_TDM_IDLE_HIZ:
+ ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4,
+ TAS2770_TDM_CFG_REG4_TX_KEEPER,
+ TAS2770_TDM_CFG_REG4_TX_KEEPER);
+ if (ret)
+ return ret;
+
+ ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4,
+ TAS2770_TDM_CFG_REG4_TX_FILL,
+ TAS2770_TDM_CFG_REG4_TX_FILL);
+ if (ret)
+ return ret;
+
+ break;
+ case SND_SOC_DAI_TDM_IDLE_OFF:
+ ret = snd_soc_component_update_bits(component, TAS2770_DIN_PD,
+ TAS2770_DIN_PD_SDOUT, 0);
+ if (ret)
+ return ret;
+
+ ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4,
+ TAS2770_TDM_CFG_REG4_TX_KEEPER, 0);
+ if (ret)
+ return ret;
+
+ break;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ tas2770->idle_tx_mode = tx_mode;
+
+ return 0;
+}
+
static const struct snd_soc_dai_ops tas2770_dai_ops = {
.mute_stream = tas2770_mute,
.hw_params = tas2770_hw_params,
.set_fmt = tas2770_set_fmt,
.set_tdm_slot = tas2770_set_dai_tdm_slot,
+ .set_tdm_idle = tas2770_set_dai_tdm_idle,
.no_capture_mute = 1,
};
diff --git a/sound/soc/codecs/tas2770.h b/sound/soc/codecs/tas2770.h
index 3fd2e7003c50..102040b6bdf8 100644
--- a/sound/soc/codecs/tas2770.h
+++ b/sound/soc/codecs/tas2770.h
@@ -67,6 +67,14 @@
#define TAS2770_TDM_CFG_REG3_RXS_SHIFT 0x4
#define TAS2770_TDM_CFG_REG3_30_MASK GENMASK(3, 0)
#define TAS2770_TDM_CFG_REG3_30_SHIFT 0
+ /* TDM Configuration Reg4 */
+#define TAS2770_TDM_CFG_REG4 TAS2770_REG(0X0, 0x0E)
+#define TAS2770_TDM_CFG_REG4_TX_LSB_CFG BIT(7)
+#define TAS2770_TDM_CFG_REG4_TX_KEEPER_CFG BIT(6)
+#define TAS2770_TDM_CFG_REG4_TX_KEEPER BIT(5)
+#define TAS2770_TDM_CFG_REG4_TX_FILL BIT(4)
+#define TAS2770_TDM_CFG_REG4_TX_OFFSET_MASK GENMASK(3, 1)
+#define TAS2770_TDM_CFG_REG4_TX_EDGE_FALLING BIT(0)
/* TDM Configuration Reg5 */
#define TAS2770_TDM_CFG_REG5 TAS2770_REG(0X0, 0x0F)
#define TAS2770_TDM_CFG_REG5_VSNS_MASK BIT(6)
@@ -115,6 +123,9 @@
#define TAS2770_TEMP_LSB TAS2770_REG(0X0, 0x2A)
/* Interrupt Configuration */
#define TAS2770_INT_CFG TAS2770_REG(0X0, 0x30)
+ /* Data In Pull-Down */
+#define TAS2770_DIN_PD TAS2770_REG(0X0, 0x31)
+#define TAS2770_DIN_PD_SDOUT BIT(7)
/* Misc IRQ */
#define TAS2770_MISC_IRQ TAS2770_REG(0X0, 0x32)
/* Clock Configuration */
@@ -146,6 +157,7 @@ struct tas2770_priv {
int pdm_slot;
bool dac_powered;
bool unmuted;
+ int idle_tx_mode;
};
#endif /* __TAS2770__ */
diff --git a/sound/soc/soc-dai.c b/sound/soc/soc-dai.c
index a1e05307067d..2f370fda1266 100644
--- a/sound/soc/soc-dai.c
+++ b/sound/soc/soc-dai.c
@@ -283,6 +283,46 @@ err:
EXPORT_SYMBOL_GPL(snd_soc_dai_set_tdm_slot);
/**
+ * snd_soc_dai_set_tdm_idle() - Configure a DAI's TDM idle mode
+ * @dai: The DAI to configure
+ * @tx_mask: bitmask representing idle TX slots.
+ * @rx_mask: bitmask representing idle RX slots.
+ * @tx_mode: idle mode to set for TX slots.
+ * @rx_mode: idle mode to set for RX slots.
+ *
+ * This function configures the DAI to handle idle TDM slots in the
+ * specified manner. @tx_mode and @rx_mode can be one of
+ * SND_SOC_DAI_TDM_IDLE_NONE, SND_SOC_DAI_TDM_IDLE_ZERO,
+ * SND_SOC_DAI_TDM_IDLE_PULLDOWN, or SND_SOC_DAI_TDM_IDLE_HIZ.
+ * SND_SOC_TDM_IDLE_NONE represents the DAI's default/unset idle slot
+ * handling state and could be any of the other modes depending on the
+ * hardware behind the DAI. It is therefore undefined behaviour when set
+ * explicitly.
+ *
+ * Mode and mask can be set independently for both the TX and RX direction.
+ * Some hardware may ignore both TX and RX masks depending on its
+ * capabilities.
+ */
+int snd_soc_dai_set_tdm_idle(struct snd_soc_dai *dai,
+ unsigned int tx_mask, unsigned int rx_mask,
+ int tx_mode, int rx_mode)
+{
+ int ret = -EOPNOTSUPP;
+
+ /* You can't write to the RX line */
+ if (rx_mode == SND_SOC_DAI_TDM_IDLE_ZERO)
+ return soc_dai_ret(dai, -EINVAL);
+
+ if (dai->driver->ops &&
+ dai->driver->ops->set_tdm_idle)
+ ret = dai->driver->ops->set_tdm_idle(dai, tx_mask, rx_mask,
+ tx_mode, rx_mode);
+
+ return soc_dai_ret(dai, ret);
+}
+EXPORT_SYMBOL_GPL(snd_soc_dai_set_tdm_idle);
+
+/**
* snd_soc_dai_set_channel_map - configure DAI audio channel map
* @dai: DAI
* @tx_num: how many TX channels