Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+312 -47
+3 -6
Documentation/devicetree/bindings/sound/imx-audio-card.yaml
··· 24 24 cpu/codec dais. 25 25 26 26 type: object 27 + $ref: tdm-slot.yaml# 27 28 28 29 properties: 29 30 link-name: ··· 39 38 - i2s 40 39 - dsp_b 41 40 42 - dai-tdm-slot-num: 43 - description: see tdm-slot.txt. 44 - $ref: /schemas/types.yaml#/definitions/uint32 41 + dai-tdm-slot-num: true 45 42 46 - dai-tdm-slot-width: 47 - description: see tdm-slot.txt. 48 - $ref: /schemas/types.yaml#/definitions/uint32 43 + dai-tdm-slot-width: true 49 44 50 45 playback-only: 51 46 description: link is used only for playback
+2 -12
Documentation/devicetree/bindings/sound/simple-card.yaml
··· 27 27 description: dai-link uses bit clock inversion 28 28 $ref: /schemas/types.yaml#/definitions/flag 29 29 30 - dai-tdm-slot-num: 31 - description: see tdm-slot.txt. 32 - $ref: /schemas/types.yaml#/definitions/uint32 33 - 34 - dai-tdm-slot-width: 35 - description: see tdm-slot.txt. 36 - $ref: /schemas/types.yaml#/definitions/uint32 37 - 38 30 system-clock-frequency: 39 31 description: | 40 32 If a clock is specified and a multiplication factor is given with ··· 107 115 108 116 dai: 109 117 type: object 118 + $ref: tdm-slot.yaml# 119 + 110 120 properties: 111 121 sound-dai: 112 122 maxItems: 1 ··· 127 133 bitclock-master: 128 134 $ref: /schemas/types.yaml#/definitions/flag 129 135 130 - dai-tdm-slot-num: 131 - $ref: "#/definitions/dai-tdm-slot-num" 132 - dai-tdm-slot-width: 133 - $ref: "#/definitions/dai-tdm-slot-width" 134 136 clocks: 135 137 maxItems: 1 136 138 system-clock-frequency:
-29
Documentation/devicetree/bindings/sound/tdm-slot.txt
··· 1 - TDM slot: 2 - 3 - This specifies audio DAI's TDM slot. 4 - 5 - TDM slot properties: 6 - dai-tdm-slot-num : Number of slots in use. 7 - dai-tdm-slot-width : Width in bits for each slot. 8 - dai-tdm-slot-tx-mask : Transmit direction slot mask, optional 9 - dai-tdm-slot-rx-mask : Receive direction slot mask, optional 10 - 11 - For instance: 12 - dai-tdm-slot-num = <2>; 13 - dai-tdm-slot-width = <8>; 14 - dai-tdm-slot-tx-mask = <0 1>; 15 - dai-tdm-slot-rx-mask = <1 0>; 16 - 17 - And for each specified driver, there could be one .of_xlate_tdm_slot_mask() 18 - to specify an explicit mapping of the channels and the slots. If it's absent 19 - the default snd_soc_of_xlate_tdm_slot_mask() will be used to generating the 20 - tx and rx masks. 21 - 22 - For snd_soc_of_xlate_tdm_slot_mask(), the tx and rx masks will use a 1 bit 23 - for an active slot as default, and the default active bits are at the LSB of 24 - the masks. 25 - 26 - The explicit masks are given as array of integers, where the first 27 - number presents bit-0 (LSB), second presents bit-1, etc. Any non zero 28 - number is considered 1 and 0 is 0. snd_soc_of_xlate_tdm_slot_mask() 29 - does not do anything, if either mask is set non zero value.
+52
Documentation/devicetree/bindings/sound/tdm-slot.yaml
··· 1 + # SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) 2 + %YAML 1.2 3 + --- 4 + $id: http://devicetree.org/schemas/sound/tdm-slot.yaml# 5 + $schema: http://devicetree.org/meta-schemas/core.yaml# 6 + 7 + title: Time Division Multiplexing (TDM) Slot Parameters 8 + 9 + maintainers: 10 + - Liam Girdwood <lgirdwood@gmail.com> 11 + 12 + select: false 13 + 14 + properties: 15 + dai-tdm-slot-num: 16 + $ref: /schemas/types.yaml#/definitions/uint32 17 + description: Number of slots in use 18 + 19 + dai-tdm-slot-width: 20 + $ref: /schemas/types.yaml#/definitions/uint32 21 + description: Width, in bits, of each slot 22 + 23 + dai-tdm-idle-mode: 24 + $ref: /schemas/types.yaml#/definitions/string 25 + enum: 26 + - none 27 + - off 28 + - zero 29 + - pulldown 30 + - hiz 31 + - pullup 32 + - drivehigh 33 + description: Drive mode for inactive/idle TDM slots. For hardware that 34 + implements .set_tdm_idle(). Optional. "None" represents undefined 35 + behaviour and is the same as not setting this property. 36 + 37 + patternProperties: 38 + '^dai-tdm-slot-[rt]x-mask$': 39 + $ref: /schemas/types.yaml#/definitions/uint32-array 40 + description: Slot mask for active TDM slots. Optional. Drivers may 41 + specify .xlate_tdm_slot_mask() to generate a slot mask dynamically. If 42 + neither this property nor a driver-specific function are specified, the 43 + default snd_soc_xlate_tdm_slot_mask() function will be used to generate 44 + a mask. The first element of the array is slot 0 (LSB). Any nonzero 45 + value will be treated as 1. 46 + 47 + '^dai-tdm-slot-[rt]x-idle-mask$': 48 + $ref: /schemas/types.yaml#/definitions/uint32 49 + description: Idle slot mask. Optional. A bit being set to 1 indicates 50 + that the corresponding TDM slot is inactive/idle. 51 + 52 + additionalProperties: true
+22
include/sound/soc-dai.h
··· 53 53 #define SND_SOC_POSSIBLE_DAIFMT_PDM (1 << SND_SOC_DAI_FORMAT_PDM) 54 54 55 55 /* 56 + * DAI TDM slot idle modes 57 + * 58 + * Describes a CODEC/CPU's behaviour when not actively receiving or 59 + * transmitting on a given TDM slot. NONE is undefined behaviour. 60 + * Add new modes to the end. 61 + */ 62 + #define SND_SOC_DAI_TDM_IDLE_NONE 0 63 + #define SND_SOC_DAI_TDM_IDLE_OFF 1 64 + #define SND_SOC_DAI_TDM_IDLE_ZERO 2 65 + #define SND_SOC_DAI_TDM_IDLE_PULLDOWN 3 66 + #define SND_SOC_DAI_TDM_IDLE_HIZ 4 67 + #define SND_SOC_DAI_TDM_IDLE_PULLUP 5 68 + #define SND_SOC_DAI_TDM_IDLE_DRIVE_HIGH 6 69 + 70 + /* 56 71 * DAI Clock gating. 57 72 * 58 73 * DAI bit clocks can be gated (disabled) when the DAI is not ··· 196 181 int snd_soc_dai_set_tdm_slot(struct snd_soc_dai *dai, 197 182 unsigned int tx_mask, unsigned int rx_mask, int slots, int slot_width); 198 183 184 + int snd_soc_dai_set_tdm_idle(struct snd_soc_dai *dai, 185 + unsigned int tx_mask, unsigned int rx_mask, 186 + int tx_mode, int rx_mode); 187 + 199 188 int snd_soc_dai_set_channel_map(struct snd_soc_dai *dai, 200 189 unsigned int tx_num, const unsigned int *tx_slot, 201 190 unsigned int rx_num, const unsigned int *rx_slot); ··· 316 297 int (*set_tdm_slot)(struct snd_soc_dai *dai, 317 298 unsigned int tx_mask, unsigned int rx_mask, 318 299 int slots, int slot_width); 300 + int (*set_tdm_idle)(struct snd_soc_dai *dai, 301 + unsigned int tx_mask, unsigned int rx_mask, 302 + int tx_mode, int rx_mode); 319 303 int (*set_channel_map)(struct snd_soc_dai *dai, 320 304 unsigned int tx_num, const unsigned int *tx_slot, 321 305 unsigned int rx_num, const unsigned int *rx_slot);
+95
sound/soc/codecs/tas2764.c
··· 44 44 45 45 bool dac_powered; 46 46 bool unmuted; 47 + 48 + struct { 49 + int tx_mode; 50 + unsigned int tx_mask; 51 + } idle_slot_config; 47 52 }; 48 53 49 54 #include "tas2764-quirks.h" ··· 514 509 return 0; 515 510 } 516 511 512 + static int tas2764_write_sdout_idle_mask(struct tas2764_priv *tas2764, u32 mask) 513 + { 514 + struct snd_soc_component *component = tas2764->component; 515 + int i, ret; 516 + 517 + /* Hardware supports up to 64 slots, but we don't */ 518 + for (i = 0; i < 4; i++) { 519 + ret = snd_soc_component_write(component, 520 + TAS2764_SDOUT_HIZ_1 + i, 521 + (mask >> (i * 8)) & 0xff); 522 + if (ret < 0) 523 + return ret; 524 + } 525 + 526 + return 0; 527 + } 528 + 529 + static int tas2764_set_dai_tdm_idle(struct snd_soc_dai *dai, 530 + unsigned int tx_mask, unsigned int rx_mask, 531 + int tx_mode, int rx_mode) 532 + { 533 + struct snd_soc_component *component = dai->component; 534 + struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); 535 + int ret; 536 + 537 + /* We don't support setting anything on SDIN */ 538 + if (rx_mode) 539 + return -EOPNOTSUPP; 540 + 541 + if (tas2764->idle_slot_config.tx_mask == tx_mask && 542 + tas2764->idle_slot_config.tx_mode == tx_mode) 543 + return 0; 544 + 545 + switch (tx_mode) { 546 + case SND_SOC_DAI_TDM_IDLE_ZERO: 547 + if (!tx_mask) 548 + return -EINVAL; 549 + 550 + ret = tas2764_write_sdout_idle_mask(tas2764, tx_mask); 551 + if (ret < 0) 552 + return ret; 553 + 554 + ret = snd_soc_component_update_bits(component, 555 + TAS2764_SDOUT_HIZ_9, 556 + TAS2764_SDOUT_HIZ_9_FORCE_0_EN, 557 + TAS2764_SDOUT_HIZ_9_FORCE_0_EN); 558 + if (ret < 0) 559 + return ret; 560 + 561 + tas2764->idle_slot_config.tx_mask = tx_mask; 562 + tas2764->idle_slot_config.tx_mode = tx_mode; 563 + break; 564 + case SND_SOC_DAI_TDM_IDLE_HIZ: 565 + case SND_SOC_DAI_TDM_IDLE_OFF: 566 + /* HiZ mode does not support a slot mask */ 567 + ret = tas2764_write_sdout_idle_mask(tas2764, 0); 568 + if (ret < 0) 569 + return ret; 570 + 571 + ret = snd_soc_component_update_bits(component, 572 + TAS2764_SDOUT_HIZ_9, 573 + TAS2764_SDOUT_HIZ_9_FORCE_0_EN, 0); 574 + if (ret < 0) 575 + return ret; 576 + 577 + tas2764->idle_slot_config.tx_mask = 0; 578 + tas2764->idle_slot_config.tx_mode = tx_mode; 579 + break; 580 + default: 581 + return -EOPNOTSUPP; 582 + } 583 + 584 + return 0; 585 + } 586 + 587 + /* The SDOUT idle slot mask must be cropped based on the BCLK ratio */ 588 + static int tas2764_set_bclk_ratio(struct snd_soc_dai *dai, unsigned int ratio) 589 + { 590 + struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(dai->component); 591 + 592 + if (!tas2764->idle_slot_config.tx_mask) 593 + return 0; 594 + 595 + tas2764->idle_slot_config.tx_mask &= GENMASK((ratio / 8) - 1, 0); 596 + 597 + return tas2764_write_sdout_idle_mask(tas2764, tas2764->idle_slot_config.tx_mask); 598 + } 599 + 517 600 static const struct snd_soc_dai_ops tas2764_dai_ops = { 518 601 .mute_stream = tas2764_mute, 519 602 .hw_params = tas2764_hw_params, 520 603 .set_fmt = tas2764_set_fmt, 604 + .set_bclk_ratio = tas2764_set_bclk_ratio, 521 605 .set_tdm_slot = tas2764_set_dai_tdm_slot, 606 + .set_tdm_idle = tas2764_set_dai_tdm_idle, 522 607 .no_capture_mute = 1, 523 608 }; 524 609
+11
sound/soc/codecs/tas2764.h
··· 126 126 127 127 #define TAS2764_BOP_CFG0 TAS2764_REG(0X0, 0x1d) 128 128 129 + #define TAS2764_SDOUT_HIZ_1 TAS2764_REG(0x1, 0x3d) 130 + #define TAS2764_SDOUT_HIZ_2 TAS2764_REG(0x1, 0x3e) 131 + #define TAS2764_SDOUT_HIZ_3 TAS2764_REG(0x1, 0x3f) 132 + #define TAS2764_SDOUT_HIZ_4 TAS2764_REG(0x1, 0x40) 133 + #define TAS2764_SDOUT_HIZ_5 TAS2764_REG(0x1, 0x41) 134 + #define TAS2764_SDOUT_HIZ_6 TAS2764_REG(0x1, 0x42) 135 + #define TAS2764_SDOUT_HIZ_7 TAS2764_REG(0x1, 0x43) 136 + #define TAS2764_SDOUT_HIZ_8 TAS2764_REG(0x1, 0x44) 137 + #define TAS2764_SDOUT_HIZ_9 TAS2764_REG(0x1, 0x45) 138 + #define TAS2764_SDOUT_HIZ_9_FORCE_0_EN BIT(7) 139 + 129 140 #endif /* __TAS2764__ */
+75
sound/soc/codecs/tas2770.c
··· 492 492 return 0; 493 493 } 494 494 495 + static int tas2770_set_dai_tdm_idle(struct snd_soc_dai *dai, 496 + unsigned int tx_mask, 497 + unsigned int rx_mask, 498 + int tx_mode, int rx_mode) 499 + { 500 + struct snd_soc_component *component = dai->component; 501 + struct tas2770_priv *tas2770 = snd_soc_component_get_drvdata(component); 502 + int ret; 503 + 504 + /* We don't support setting anything for SDIN */ 505 + if (rx_mode) 506 + return -EOPNOTSUPP; 507 + 508 + if (tas2770->idle_tx_mode == tx_mode) 509 + return 0; 510 + 511 + switch (tx_mode) { 512 + case SND_SOC_DAI_TDM_IDLE_PULLDOWN: 513 + ret = snd_soc_component_update_bits(component, TAS2770_DIN_PD, 514 + TAS2770_DIN_PD_SDOUT, 515 + TAS2770_DIN_PD_SDOUT); 516 + if (ret) 517 + return ret; 518 + 519 + break; 520 + case SND_SOC_DAI_TDM_IDLE_ZERO: 521 + ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4, 522 + TAS2770_TDM_CFG_REG4_TX_KEEPER, 523 + TAS2770_TDM_CFG_REG4_TX_KEEPER); 524 + if (ret) 525 + return ret; 526 + 527 + ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4, 528 + TAS2770_TDM_CFG_REG4_TX_FILL, 0); 529 + if (ret) 530 + return ret; 531 + 532 + break; 533 + case SND_SOC_DAI_TDM_IDLE_HIZ: 534 + ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4, 535 + TAS2770_TDM_CFG_REG4_TX_KEEPER, 536 + TAS2770_TDM_CFG_REG4_TX_KEEPER); 537 + if (ret) 538 + return ret; 539 + 540 + ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4, 541 + TAS2770_TDM_CFG_REG4_TX_FILL, 542 + TAS2770_TDM_CFG_REG4_TX_FILL); 543 + if (ret) 544 + return ret; 545 + 546 + break; 547 + case SND_SOC_DAI_TDM_IDLE_OFF: 548 + ret = snd_soc_component_update_bits(component, TAS2770_DIN_PD, 549 + TAS2770_DIN_PD_SDOUT, 0); 550 + if (ret) 551 + return ret; 552 + 553 + ret = snd_soc_component_update_bits(component, TAS2770_TDM_CFG_REG4, 554 + TAS2770_TDM_CFG_REG4_TX_KEEPER, 0); 555 + if (ret) 556 + return ret; 557 + 558 + break; 559 + 560 + default: 561 + return -EOPNOTSUPP; 562 + } 563 + 564 + tas2770->idle_tx_mode = tx_mode; 565 + 566 + return 0; 567 + } 568 + 495 569 static const struct snd_soc_dai_ops tas2770_dai_ops = { 496 570 .mute_stream = tas2770_mute, 497 571 .hw_params = tas2770_hw_params, 498 572 .set_fmt = tas2770_set_fmt, 499 573 .set_tdm_slot = tas2770_set_dai_tdm_slot, 574 + .set_tdm_idle = tas2770_set_dai_tdm_idle, 500 575 .no_capture_mute = 1, 501 576 }; 502 577
+12
sound/soc/codecs/tas2770.h
··· 67 67 #define TAS2770_TDM_CFG_REG3_RXS_SHIFT 0x4 68 68 #define TAS2770_TDM_CFG_REG3_30_MASK GENMASK(3, 0) 69 69 #define TAS2770_TDM_CFG_REG3_30_SHIFT 0 70 + /* TDM Configuration Reg4 */ 71 + #define TAS2770_TDM_CFG_REG4 TAS2770_REG(0X0, 0x0E) 72 + #define TAS2770_TDM_CFG_REG4_TX_LSB_CFG BIT(7) 73 + #define TAS2770_TDM_CFG_REG4_TX_KEEPER_CFG BIT(6) 74 + #define TAS2770_TDM_CFG_REG4_TX_KEEPER BIT(5) 75 + #define TAS2770_TDM_CFG_REG4_TX_FILL BIT(4) 76 + #define TAS2770_TDM_CFG_REG4_TX_OFFSET_MASK GENMASK(3, 1) 77 + #define TAS2770_TDM_CFG_REG4_TX_EDGE_FALLING BIT(0) 70 78 /* TDM Configuration Reg5 */ 71 79 #define TAS2770_TDM_CFG_REG5 TAS2770_REG(0X0, 0x0F) 72 80 #define TAS2770_TDM_CFG_REG5_VSNS_MASK BIT(6) ··· 123 115 #define TAS2770_TEMP_LSB TAS2770_REG(0X0, 0x2A) 124 116 /* Interrupt Configuration */ 125 117 #define TAS2770_INT_CFG TAS2770_REG(0X0, 0x30) 118 + /* Data In Pull-Down */ 119 + #define TAS2770_DIN_PD TAS2770_REG(0X0, 0x31) 120 + #define TAS2770_DIN_PD_SDOUT BIT(7) 126 121 /* Misc IRQ */ 127 122 #define TAS2770_MISC_IRQ TAS2770_REG(0X0, 0x32) 128 123 /* Clock Configuration */ ··· 157 146 int pdm_slot; 158 147 bool dac_powered; 159 148 bool unmuted; 149 + int idle_tx_mode; 160 150 }; 161 151 162 152 #endif /* __TAS2770__ */
+40
sound/soc/soc-dai.c
··· 283 283 EXPORT_SYMBOL_GPL(snd_soc_dai_set_tdm_slot); 284 284 285 285 /** 286 + * snd_soc_dai_set_tdm_idle() - Configure a DAI's TDM idle mode 287 + * @dai: The DAI to configure 288 + * @tx_mask: bitmask representing idle TX slots. 289 + * @rx_mask: bitmask representing idle RX slots. 290 + * @tx_mode: idle mode to set for TX slots. 291 + * @rx_mode: idle mode to set for RX slots. 292 + * 293 + * This function configures the DAI to handle idle TDM slots in the 294 + * specified manner. @tx_mode and @rx_mode can be one of 295 + * SND_SOC_DAI_TDM_IDLE_NONE, SND_SOC_DAI_TDM_IDLE_ZERO, 296 + * SND_SOC_DAI_TDM_IDLE_PULLDOWN, or SND_SOC_DAI_TDM_IDLE_HIZ. 297 + * SND_SOC_TDM_IDLE_NONE represents the DAI's default/unset idle slot 298 + * handling state and could be any of the other modes depending on the 299 + * hardware behind the DAI. It is therefore undefined behaviour when set 300 + * explicitly. 301 + * 302 + * Mode and mask can be set independently for both the TX and RX direction. 303 + * Some hardware may ignore both TX and RX masks depending on its 304 + * capabilities. 305 + */ 306 + int snd_soc_dai_set_tdm_idle(struct snd_soc_dai *dai, 307 + unsigned int tx_mask, unsigned int rx_mask, 308 + int tx_mode, int rx_mode) 309 + { 310 + int ret = -EOPNOTSUPP; 311 + 312 + /* You can't write to the RX line */ 313 + if (rx_mode == SND_SOC_DAI_TDM_IDLE_ZERO) 314 + return soc_dai_ret(dai, -EINVAL); 315 + 316 + if (dai->driver->ops && 317 + dai->driver->ops->set_tdm_idle) 318 + ret = dai->driver->ops->set_tdm_idle(dai, tx_mask, rx_mask, 319 + tx_mode, rx_mode); 320 + 321 + return soc_dai_ret(dai, ret); 322 + } 323 + EXPORT_SYMBOL_GPL(snd_soc_dai_set_tdm_idle); 324 + 325 + /** 286 326 * snd_soc_dai_set_channel_map - configure DAI audio channel map 287 327 * @dai: DAI 288 328 * @tx_num: how many TX channels