diff options
| author | Mark Brown <broonie@kernel.org> | 2026-03-16 04:13:07 +0300 |
|---|---|---|
| committer | Mark Brown <broonie@kernel.org> | 2026-03-16 04:13:07 +0300 |
| commit | 706d2dc0269e695979943f27b03389007e034db7 (patch) | |
| tree | 7c5096a4f53a584fa3cf03f5901dafc62e6de8bf /sound | |
| parent | 3e9cda2f4a33c6becc99f8a78946cbd02983852f (diff) | |
| parent | af176d0787d219d9e07272988079ebb9be8efe6a (diff) | |
| download | linux-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.c | 95 | ||||
| -rw-r--r-- | sound/soc/codecs/tas2764.h | 11 | ||||
| -rw-r--r-- | sound/soc/codecs/tas2770.c | 75 | ||||
| -rw-r--r-- | sound/soc/codecs/tas2770.h | 12 | ||||
| -rw-r--r-- | sound/soc/soc-dai.c | 40 |
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 |
