diff --git a/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml b/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml index e8a2df805059a..8e940d46e6d67 100644 --- a/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml +++ b/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml @@ -4,7 +4,7 @@ $id: http://devicetree.org/schemas/soc/apple/apple,t6041-aop.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Apple T604x Always-On Processor +title: Apple T604x and T8132 Always-On Processor maintainers: - Jonathan Goyvaerts @@ -16,9 +16,15 @@ description: reaches its services through the RTKit endpoints. The firmware's external code and log segments live in DRAM, and the bootloader reserves them. + On T8132 the bootloader also programs the AOP DART's address filter (DAPF) + and reserves its windows, which the firmware reaches through its DART at + their physical addresses. + properties: compatible: - const: apple,t6041-aop + enum: + - apple,t6041-aop + - apple,t8132-aop reg: minItems: 2 @@ -47,11 +53,12 @@ properties: memory-region: minItems: 1 - maxItems: 4 + maxItems: 18 description: - The firmware segments in DRAM (its external code and its log), filled in - by the bootloader from the ADT. The iommu-addresses property of each - region gives the address the firmware uses for it. + The firmware segments in DRAM (its external code and its log) and, on + T8132, the DAPF windows, filled in by the bootloader from the ADT. The + iommu-addresses property of each region gives the address the firmware + uses for it. audio: type: object @@ -93,6 +100,16 @@ required: - mbox-names - iommus +allOf: + - if: + properties: + compatible: + const: apple,t6041-aop + then: + properties: + memory-region: + maxItems: 4 + additionalProperties: false examples: diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml index fdf3d1883a99d..fcf97451df97a 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -23,6 +23,8 @@ properties: - apple,j616-macaudio - apple,j616-macaudio-jack - apple,j700-macaudio + - apple,j713-macaudio + - apple,j713-macaudio-jack - const: apple,macaudio "#address-cells": diff --git a/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml b/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml index 1eadf9ff3b6eb..85e9aca39bf7f 100644 --- a/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml @@ -4,21 +4,21 @@ $id: http://devicetree.org/schemas/sound/apple,t6041-aop-audio.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Apple T6041 AOP audio +title: Apple T6041 and T8132 AOP audio maintainers: - Jonathan Goyvaerts description: | - The Always-On Processor on Apple T6041 SoCs runs the machine's audio + The Always-On Processor on Apple T6041 and T8132 SoCs runs the machine's audio streams in its firmware: the microphone array, the internal speakers with their voltage and current sense, and the 3.5 mm headset jack in both directions. This node is a child of the AOP node and names the ADMAC channels the host moves each stream's data over while the firmware runs its serializer. The parent AOP binding describes the firmware transport. - The headset jack's channels are optional: a node without them has no jack - streams. + The speaker and headset jack channels are optional: a node without them has + no speaker or jack streams. # Only select T6041 nodes: other AOP audio nodes carry the # apple,t6030-aop-audio fallback too and have a different set of streams. @@ -35,27 +35,46 @@ allOf: properties: compatible: - items: - - const: apple,t6041-aop-audio - - const: apple,t6030-aop-audio + oneOf: + - items: + - const: apple,t6041-aop-audio + - const: apple,t6030-aop-audio + - items: + - const: apple,t8132-aop-audio + - const: apple,t6041-aop-audio + - const: apple,t6030-aop-audio dmas: - minItems: 3 - items: - - description: Microphone array input - - description: Internal speaker output - - description: Speaker voltage and current sense input - - description: Headset jack output - - description: Headset microphone input + description: + The ADMAC channels named in dma-names, in that order. "dma" is the + microphone array input, "spkr" the internal speaker output, "sense" the + speakers' voltage and current sense input, "cout" the headset jack output + and "cin" the headset microphone input. + minItems: 1 + maxItems: 5 dma-names: - minItems: 3 - items: - - const: dma - - const: spkr - - const: sense - - const: cout - - const: cin + oneOf: + - description: The microphones only + items: + - const: dma + - description: The microphones and the headset jack + items: + - const: dma + - const: cout + - const: cin + - description: The microphones and the speakers + items: + - const: dma + - const: spkr + - const: sense + - description: The microphones, the speakers and the headset jack + items: + - const: dma + - const: spkr + - const: sense + - const: cout + - const: cin "#sound-dai-cells": const: 1 diff --git a/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml b/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml index 99eebcfabbcba..05c2ab00c4d7c 100644 --- a/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml +++ b/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml @@ -66,6 +66,15 @@ properties: down, and no member enables it again until the driver is reloaded. A member needs its fault interrupt. + apple,deferred-shutdown: + type: boolean + description: + The amplifier's TDM bus keeps running for a while after its playback + stream stops, as on an AOP-fed speaker stream, and its current and + voltage sense slots are still sampled in that time. The amplifier + must not enter software shutdown until the bus has stopped. One shut + down while the bus still runs can leave garbage in its sense slots. + ti,imon-slot-no: $ref: /schemas/types.yaml#/definitions/uint32 description: TDM TX current sense time slot. diff --git a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi index 5606ce4d2d3e4..1e100e890be0a 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi @@ -125,16 +125,18 @@ status = "disabled"; }; +/* The CS42L84 headphone codec. */ &ps_i2c1 { - status = "disabled"; + status = "okay"; }; &ps_i2c2 { status = "disabled"; }; +/* The SN012776 speaker amps. */ &ps_i2c3 { - status = "disabled"; + status = "okay"; }; &ps_i2c4 { diff --git a/arch/arm64/boot/dts/apple/t8132-j713.dts b/arch/arm64/boot/dts/apple/t8132-j713.dts index d878b72fd39ba..032232b5864a6 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -36,6 +36,76 @@ wifi0 = &wifi0; }; + /* + * The speaker amps' shared shutdown line (SDZ, AP GPIO 15, the ADT + * function-reset; low = shutdown). One fixed regulator, the SDZ supply + * of all four, so one amp's fault takes the whole group down. + */ + speaker_sdz: regulator-sn012776-sdz { + compatible = "regulator-fixed"; + regulator-name = "sn012776-sdz"; + gpios = <&pinctrl_ap 15 GPIO_ACTIVE_HIGH>; + enable-active-high; + }; + + /* + * The AOP firmware runs the stream (MCA cluster 0, one serializer for + * the four amps); macaudio's J713 variant links the AOP audio + * component's speaker back-end (DAI 3) to them. The codecs are listed in + * the ADT provider order, so each gets its native playback slot (LW 0, + * RW 1, LT 2, RT 3). The idle masks are the ADT amp-tx-zd-config rows 0 + * and 1 over the 16 sense byte slots: the left woofer drives zeros in + * the right amps' sense slots, the right woofer in the left amps'. + */ + sound: sound { + compatible = "apple,j713-macaudio", "apple,macaudio"; + model = "MacBook Air J713"; + #address-cells = <1>; + #size-cells = <0>; + + dai-link@0 { + reg = <0>; + link-name = "Speakers"; + dai-tdm-idle-mode-0 = "zero"; + dai-tdm-idle-mode-1 = "zero"; + dai-tdm-slot-tx-idle-mask-0 = <0xf0f0>; + dai-tdm-slot-tx-idle-mask-1 = <0x0f0f>; + + cpu { + sound-dai = <&aop_audio 3>; + }; + + codec { + sound-dai = <&speaker_left_woof>, <&speaker_right_woof>, + <&speaker_left_tweet>, <&speaker_right_tweet>; + }; + }; + }; + + /* + * The headphone jack: the AOP audio component's jack back-end (DAI 4) + * to the CS42L84, as a card of its own. + */ + sound_jack: sound-jack { + compatible = "apple,j713-macaudio-jack", "apple,macaudio"; + model = "MacBook Air J713 Jack"; + #address-cells = <1>; + #size-cells = <0>; + + dai-link@0 { + reg = <0>; + link-name = "Headphone Jack"; + + cpu { + sound-dai = <&aop_audio 4>; + }; + + codec { + sound-dai = <&jack_codec>; + }; + }; + }; + chosen { linux-enablement-mac,disk-boot; linux-enablement-mac,dapf-handoff = "/arm-io/aop", "/arm-io/mtp", @@ -56,6 +126,126 @@ }; }; +/* + * m1n1 programs the AOP DART's DAPF and reserves the firmware segments and + * the DAPF windows in memory-region when /chosen asks for the AOP's handoff, + * or disables the AOP. + */ +&aop { + status = "okay"; +}; + +&aop_audio { + /* + * The microphones on LEAP; the speakers and their sense on base-ns TX0 + * and RX0 (the J616s stream: six playback and twelve sense slots, of + * which the four amps take the first four and eight), the jack on TX2 + * and RX2. + */ + dmas = <&admac0 1>, <&admac1 0>, <&admac1 1>, <&admac1 4>, <&admac1 5>; + dma-names = "dma", "spkr", "sense", "cout", "cin"; + apple,chassis-name = "J713"; + apple,machine-kind = "MacBook Air"; +}; + +/* + * The four SN012776 speaker amps, held in hardware shutdown until the sound + * card enables them (ADT /arm-io/i2c3/audio-speaker at 0x38 and its + * speaker1..3): one interrupt line on AP GPIO 14, level low; sense slots from + * speaker-config (amp i at RX bytes 4i and 4i + 2, order LW RW LT RT). Names + * as speakersafetyd's j413/j613 profiles use them. Every amp joins the SDZ + * fail-closed group. + */ +&i2c3 { + status = "okay"; + + speaker_left_woof: codec@38 { + compatible = "ti,sn012776", "ti,tas2764"; + reg = <0x38>; + SDZ-supply = <&speaker_sdz>; + apple,sdz-group-fail-closed; + apple,deferred-shutdown; + #sound-dai-cells = <0>; + sound-name-prefix = "Left Woofer"; + interrupts-extended = <&pinctrl_ap 14 IRQ_TYPE_LEVEL_LOW>; + ti,imon-slot-no = <0>; + ti,vmon-slot-no = <2>; + }; + + speaker_left_tweet: codec@39 { + compatible = "ti,sn012776", "ti,tas2764"; + reg = <0x39>; + SDZ-supply = <&speaker_sdz>; + apple,sdz-group-fail-closed; + apple,deferred-shutdown; + #sound-dai-cells = <0>; + sound-name-prefix = "Left Tweeter"; + interrupts-extended = <&pinctrl_ap 14 IRQ_TYPE_LEVEL_LOW>; + ti,imon-slot-no = <8>; + ti,vmon-slot-no = <10>; + }; + + speaker_right_woof: codec@3b { + compatible = "ti,sn012776", "ti,tas2764"; + reg = <0x3b>; + SDZ-supply = <&speaker_sdz>; + apple,sdz-group-fail-closed; + apple,deferred-shutdown; + #sound-dai-cells = <0>; + sound-name-prefix = "Right Woofer"; + interrupts-extended = <&pinctrl_ap 14 IRQ_TYPE_LEVEL_LOW>; + ti,imon-slot-no = <4>; + ti,vmon-slot-no = <6>; + }; + + speaker_right_tweet: codec@3c { + compatible = "ti,sn012776", "ti,tas2764"; + reg = <0x3c>; + SDZ-supply = <&speaker_sdz>; + apple,sdz-group-fail-closed; + apple,deferred-shutdown; + #sound-dai-cells = <0>; + sound-name-prefix = "Right Tweeter"; + interrupts-extended = <&pinctrl_ap 14 IRQ_TYPE_LEVEL_LOW>; + ti,imon-slot-no = <12>; + ti,vmon-slot-no = <14>; + }; +}; + +/* + * The CS42L84 headphone codec (ADT /arm-io/i2c1/audio-codec-output at 0x4b). + * Its reset line is nub GPIO 17, driven high to run the codec; its interrupt + * is AP GPIO 98, level low. The AOP firmware runs the jack's stream. + */ +&i2c1 { + status = "okay"; + + jack_codec: codec@4b { + compatible = "cirrus,cs42l84"; + reg = <0x4b>; + reset-gpios = <&pinctrl_nub 17 GPIO_ACTIVE_HIGH>; + #sound-dai-cells = <0>; + interrupts-extended = <&pinctrl_ap 98 IRQ_TYPE_LEVEL_LOW>; + sound-name-prefix = "Jack"; + }; +}; + +&aop_dart { + status = "okay"; +}; + +&aop_mbox { + status = "okay"; +}; + +&admac0 { + status = "okay"; +}; + +&admac1 { + status = "okay"; +}; + /* Enable DVFS only with the J713 board thermal owner. */ &cpufreq_e { status = "okay"; diff --git a/arch/arm64/boot/dts/apple/t8132.dtsi b/arch/arm64/boot/dts/apple/t8132.dtsi index c82816625efd0..77dccc298914d 100644 --- a/arch/arm64/boot/dts/apple/t8132.dtsi +++ b/arch/arm64/boot/dts/apple/t8132.dtsi @@ -825,6 +825,84 @@ /* Shared controller resources; board files select the qualified devices. */ &soc { + /* + * AOP (always-on processor). iBoot loads its RTKit firmware and leaves + * it halted; Linux starts it. + */ + aop: aop@390600000 { + compatible = "apple,t8132-aop"; + reg = <0x3 0x90c00000 0x0 0x1e0000>, + <0x3 0x90600000 0x0 0x88000>, + <0x3 0x90050000 0x0 0x4000>, + <0x3 0x882a8000 0x0 0x8>; + reg-names = "firmware", "asc", "asc-aux", "config"; + mboxes = <&aop_mbox>; + mbox-names = "mbox"; + iommus = <&aop_dart 0>; + status = "disabled"; + + /* + * "dma" is the microphones' LEAP ADMAC channel (ADT audio-hp); + * the speaker and jack channels follow the T6041 map. + */ + aop_audio: audio { + compatible = "apple,t8132-aop-audio", "apple,t6041-aop-audio", + "apple,t6030-aop-audio"; + dmas = <&admac0 1>; + dma-names = "dma"; + #sound-dai-cells = <1>; + }; + }; + + /* ADT lists the interrupts as 1, 0, 3, 2 of the block: send-empty is the lowest. */ + aop_mbox: mailbox@390608000 { + compatible = "apple,t8132-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x90608000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + + aop_dart: iommu@390f00000 { + compatible = "apple,t8132-dart", "apple,t8110-dart"; + reg = <0x3 0x90f00000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + /* The audio fabric's ADMACs: admac-leap-ns (DART stream 10) and admac-base-ns (stream 8) */ + admac0: dma-controller@393300000 { + compatible = "apple,t8132-admac"; + reg = <0x3 0x93300000 0x0 0x34000>; + #dma-cells = <1>; + dma-channels = <9>; + /* the ADT gives the first of the four outputs */ + interrupts-extended = <&aic AIC_IRQ 478 IRQ_TYPE_LEVEL_HIGH>, + <0>, <0>, <0>; + iommus = <&aop_dart 10>; + status = "disabled"; + }; + + admac1: dma-controller@393800000 { + compatible = "apple,t8132-admac"; + reg = <0x3 0x93800000 0x0 0x34000>; + #dma-cells = <1>; + dma-channels = <13>; + /* the ADT gives the first of the four outputs */ + interrupts-extended = <&aic AIC_IRQ 475 IRQ_TYPE_LEVEL_HIGH>, + <0>, <0>, <0>; + iommus = <&aop_dart 8>; + status = "disabled"; + }; + nub_spmi_a0: spmi@388908000 { status = "disabled"; compatible = "apple,t8132-spmi", "apple,t8103-spmi"; diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index e72f025bb3459..e220fffc195db 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -2519,6 +2519,10 @@ const HW_CFG_T6040: AopHwConfig = AopHwConfig { fw_regions: true, halt_on_failed_start: true, }; +/// T8132 (J713): as T6040. The bootloader hands the AOP over the same way, with +/// the DART's DAPF programmed and its windows reserved alongside the firmware +/// segments. +const HW_CFG_T8132: AopHwConfig = HW_CFG_T6040; /// T8140: the firmware is started with the boot arguments the bootloader /// left, boots through the setup port and speaks EPIC version 4. Of the /// advertised AFK endpoints, the application map is 0x20 misc, 0x21 @@ -2547,6 +2551,7 @@ kernel::of_device_table!( (of::DeviceId::new(c_str!("apple,t6020-aop")), &HW_CFG_T6020), (of::DeviceId::new(c_str!("apple,t8140-aop")), &HW_CFG_T8140), (of::DeviceId::new(c_str!("apple,t6041-aop")), &HW_CFG_T6040), + (of::DeviceId::new(c_str!("apple,t8132-aop")), &HW_CFG_T8132), ] ); diff --git a/sound/soc/apple/aop_audio.rs b/sound/soc/apple/aop_audio.rs index 5431943d46c88..f148f7a25492c 100644 --- a/sound/soc/apple/aop_audio.rs +++ b/sound/soc/apple/aop_audio.rs @@ -1092,12 +1092,14 @@ impl platform::Driver for SndSocAopDriver { dev_warn!(data.dev, "decimator config refused, continuing\n"); } let mut this = Self::new(data.clone())?; - // The speaker stream, where the node has its DMA channel. - if data - .fwnode - .property_match_string(c_str!("dma-names"), c_str!("spkr")) - .is_ok() - { + // The speaker and jack streams, where the node has their DMA + // channels: a machine may have the jack before its speakers. + let listed = |name: &CStr| { + data.fwnode + .property_match_string(c_str!("dma-names"), name) + .is_ok() + }; + if listed(c_str!("spkr")) || (listed(c_str!("cout")) && listed(c_str!("cin"))) { this.spkr = Some(aop_spkr::SpeakerComponent::register(data)?); } return Ok(this); diff --git a/sound/soc/apple/aop_spkr.rs b/sound/soc/apple/aop_spkr.rs index 0f0de5b0afaa7..1823197b4404d 100644 --- a/sound/soc/apple/aop_spkr.rs +++ b/sound/soc/apple/aop_spkr.rs @@ -1604,9 +1604,9 @@ unsafe extern "C" fn jack_resume(component: *mut bindings::snd_soc_component) -> } } -/// The speaker component, and the jack's where the audio node lists its DMA channels. +/// The speaker and jack components, each where the audio node lists its DMA channels. pub(crate) struct SpeakerComponent { - _spkr: Registration, + _spkr: Option, _jack: Option, } @@ -1641,19 +1641,29 @@ impl SpeakerComponent { } else { None }; - let mut spkr_driver = component_driver(c_str!("t6040-aop-spkr")); - spkr_driver.suspend = Some(spkr_suspend); - spkr_driver.resume = Some(spkr_resume); - let spkr = Registration::new(&aop, services, spkr_driver, |ops| { - to_kvec(speaker_dais(ops)) - }) - .inspect_err(|e| { - dev_err!( - aop.dev, - "Unable to register the speaker ASoC component: {}\n", - e.to_errno() + let has_spkr = aop + .fwnode + .property_match_string(c_str!("dma-names"), c_str!("spkr")) + .is_ok(); + let spkr = if has_spkr { + let mut spkr_driver = component_driver(c_str!("t6040-aop-spkr")); + spkr_driver.suspend = Some(spkr_suspend); + spkr_driver.resume = Some(spkr_resume); + Some( + Registration::new(&aop, services, spkr_driver, |ops| { + to_kvec(speaker_dais(ops)) + }) + .inspect_err(|e| { + dev_err!( + aop.dev, + "Unable to register the speaker ASoC component: {}\n", + e.to_errno() + ) + })?, ) - })?; + } else { + None + }; Ok(Self { _spkr: spkr, _jack: jack, diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index cc1123e10f398..903e8dbd13059 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -123,6 +123,12 @@ struct macaudio_platform_cfg { * (CS42L84_DAC_DB()), 0 for the codec's own maximum. */ int jack_max_vol; + /* + * The speaker front-end's channel count where the CPU component's + * stream has more slots than amps (0: one channel per amp). The amps + * keep the first slots in codec order; the others carry silence. + */ + unsigned int speaker_fe_channels; }; static const char *volume_control_names[] = { @@ -938,6 +944,8 @@ static int macaudio_parse_of(struct macaudio_snd_data *ma) /* Save the max number of channels on the platform */ if (nchannels > ma->max_channels) ma->max_channels = nchannels; + if (speakers && ma->cfg->speaker_fe_channels > ma->max_channels) + ma->max_channels = ma->cfg->speaker_fe_channels; /* * If there is a single speaker, assign two channels to it, because @@ -2041,6 +2049,28 @@ struct macaudio_platform_cfg macaudio_j616_cfg = { .fe_cpu_is_platform = true, }; +/* + * J713 (T8132, MacBook Air 13" M4): the J616s stream, six 32-bit playback + * and twelve 16-bit sense slots in the same 250-bit frame, with four + * SN012776 amps, a woofer and a tweeter per side, on the first four and the + * first eight. The front-end carries all six channels; the last two are + * silent. + */ +struct macaudio_platform_cfg macaudio_j713_cfg = { + .enable_speakers = true, + .amp = AMP_SN012776, + .speakers = SPKR_1W1T, + .stereo = true, + .amp_gain = 15, + .safe_vol = -20, + .fe_dai_names = { "j616-pcm-0", "j616-pcm-1", "j616-pcm-2" }, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBC_CFC | + SND_SOC_DAIFMT_GATED | SND_SOC_DAIFMT_NB_IF, + .secondary_bclk_ratio = 250, + .fe_cpu_is_platform = true, + .speaker_fe_channels = 6, +}; + /* * The J616s headphone jack, a card of its own: a CS42L84 on the AOP's * "jack" back-end, which the firmware runs on MCA cluster 2 as a 64-SCLK @@ -2129,6 +2159,10 @@ static const struct of_device_id macaudio_snd_device_id[] = { { .compatible = "apple,j616-macaudio-jack", .data = &macaudio_j616_jack_cfg }, /* j700 AID44 max98360a - 2x 1W (AOP-fed, software gain) */ { .compatible = "apple,j700-macaudio", .data = &macaudio_j700_cfg }, + /* j713 - sn012776 15 2x 1W+1T (AOP-fed, one serializer) */ + { .compatible = "apple,j713-macaudio", .data = &macaudio_j713_cfg }, + /* The J713 jack runs on the AOP like the J616s one, with the same cap. */ + { .compatible = "apple,j713-macaudio-jack", .data = &macaudio_j616_jack_cfg }, /* Fallback, jack only */ { .compatible = "apple,macaudio"}, { } diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index 54c1629c410fc..4c7b4bad88f7d 100644 --- a/sound/soc/codecs/tas2764.c +++ b/sound/soc/codecs/tas2764.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -72,12 +73,32 @@ struct tas2764_priv { int tx_mode; unsigned int tx_mask; } idle_slot_config; + + /* apple,deferred-shutdown: software shutdown waits for the bus clock to stop */ + bool deferred_shutdown; + /* Muted at a stream's end, until the next one: the bus clock may stop */ + bool stream_stopped; + struct delayed_work shutdown_work; + /* + * Orders the fault handler's read and judgement of the latches against + * an unmute clearing a stale clock fault and stream_stopped + */ + struct mutex fault_lock; }; #include "tas2764-quirks.h" /* LTCH0 faults that shut the whole SDZ group down: over temperature, over current */ #define TAS2764_INT_LTCH0_GROUP_FAULTS (BIT(0) | BIT(1)) +#define TAS2764_INT_LTCH0_TDM_CLOCK BIT(2) + +/* + * With apple,deferred-shutdown, how long after a stream's end the muted amp + * goes to software shutdown. On the J713 the AOP's speaker bus ran on for + * up to 3520 frames (73 ms at 48 kHz) after a stream's end; this leaves + * margin over that. + */ +#define TAS2764_DEFERRED_SHUTDOWN_MS 150 static bool tas2764_group_failed(struct tas2764_priv *tas2764) { @@ -187,9 +208,8 @@ static const unsigned int tas2764_int_readout_regs[6] = { TAS2764_INT_LTCH4, }; -static irqreturn_t tas2764_irq(int irq, void *data) +static irqreturn_t tas2764_irq_locked(struct tas2764_priv *tas2764) { - struct tas2764_priv *tas2764 = data; u8 latched[6] = {0, 0, 0, 0, 0, 0}; unsigned int val; int ret = IRQ_NONE; @@ -204,6 +224,17 @@ static irqreturn_t tas2764_irq(int irq, void *data) latched[i] = val; } + /* + * An amp muted at a stream's end with its shutdown deferred, or shut + * down since, sees the bus clock stop: that is expected, not a fault. + */ + if (latched[0] == TAS2764_INT_LTCH0_TDM_CLOCK && READ_ONCE(tas2764->stream_stopped)) { + snd_soc_component_update_bits(tas2764->component, TAS2764_INT_CLK_CFG, + TAS2764_INT_CLK_CFG_IRQZ_CLR, + TAS2764_INT_CLK_CFG_IRQZ_CLR); + return IRQ_HANDLED; + } + for (i = 0; i < 8; i++) { if (latched[0] & BIT(i)) { dev_crit_ratelimited(tas2764->dev, "%s\n", @@ -227,6 +258,19 @@ static irqreturn_t tas2764_irq(int irq, void *data) return ret; } +static irqreturn_t tas2764_irq(int irq, void *data) +{ + struct tas2764_priv *tas2764 = data; + irqreturn_t ret; + + /* A stream's unmute does not come between reading the latches and judging them */ + mutex_lock(&tas2764->fault_lock); + ret = tas2764_irq_locked(tas2764); + mutex_unlock(&tas2764->fault_lock); + + return ret; +} + static void tas2764_reset(struct tas2764_priv *tas2764) { if (tas2764->reset_gpio) { @@ -267,6 +311,8 @@ static int tas2764_update_pwr_ctrl(struct tas2764_priv *tas2764) return 0; } +static void tas2764_flush_shutdown(struct tas2764_priv *tas2764); + #ifdef CONFIG_PM /* * Members of an SDZ group keep the shared SDZ line high across system sleep: the amp waits in @@ -284,6 +330,8 @@ static int tas2764_codec_suspend(struct snd_soc_component *component) struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); int ret; + tas2764_flush_shutdown(tas2764); + ret = snd_soc_component_update_bits(component, TAS2764_PWR_CTRL, TAS2764_PWR_CTRL_MASK, TAS2764_PWR_CTRL_SHUTDOWN); @@ -387,6 +435,26 @@ static int tas2764_startup(struct snd_pcm_substream *substream, struct snd_soc_d return 0; } +static void tas2764_shutdown_work(struct work_struct *work) +{ + struct tas2764_priv *tas2764 = container_of(to_delayed_work(work), + struct tas2764_priv, shutdown_work); + int ret; + + tas2764->dac_powered = false; + ret = tas2764_update_pwr_ctrl(tas2764); + /* a failed group has its SDZ supply forced off already */ + if (ret && !tas2764_group_failed(tas2764)) + dev_err(tas2764->dev, "deferred shutdown failed: %d\n", ret); +} + +/* A deferred shutdown happens now (suspend, removal) */ +static void tas2764_flush_shutdown(struct tas2764_priv *tas2764) +{ + if (cancel_delayed_work_sync(&tas2764->shutdown_work)) + tas2764_shutdown_work(&tas2764->shutdown_work.work); +} + static int tas2764_mute(struct snd_soc_dai *dai, int mute, int direction) { struct tas2764_priv *tas2764 = @@ -394,6 +462,30 @@ static int tas2764_mute(struct snd_soc_dai *dai, int mute, int direction) int ret; if (!mute) { + if (tas2764->deferred_shutdown) { + unsigned int latched; + + /* still muted from the last stream: no shutdown in between */ + cancel_delayed_work_sync(&tas2764->shutdown_work); + + /* + * A clock fault the amp latched while the bus was stopped is + * not this stream's: the handler, which can run several + * milliseconds behind on the shared line, must not see it + * after stream_stopped clears. + */ + mutex_lock(&tas2764->fault_lock); + if (tas2764->irq && + !regmap_read(tas2764->regmap, TAS2764_INT_LTCH0, &latched) && + latched == TAS2764_INT_LTCH0_TDM_CLOCK) { + snd_soc_component_update_bits(tas2764->component, + TAS2764_INT_CLK_CFG, + TAS2764_INT_CLK_CFG_IRQZ_CLR, + TAS2764_INT_CLK_CFG_IRQZ_CLR); + } + WRITE_ONCE(tas2764->stream_stopped, false); + mutex_unlock(&tas2764->fault_lock); + } tas2764->dac_powered = true; ret = tas2764_update_pwr_ctrl(tas2764); if (ret) @@ -409,6 +501,14 @@ static int tas2764_mute(struct snd_soc_dai *dai, int mute, int direction) /* Wait for ramp-down */ usleep_range(6000, 7000); + if (tas2764->deferred_shutdown) { + /* the bus still runs: shut down once its clock has stopped */ + WRITE_ONCE(tas2764->stream_stopped, true); + schedule_delayed_work(&tas2764->shutdown_work, + msecs_to_jiffies(TAS2764_DEFERRED_SHUTDOWN_MS)); + return 0; + } + tas2764->dac_powered = false; ret = tas2764_update_pwr_ctrl(tas2764); if (ret) @@ -1007,6 +1107,8 @@ static void tas2764_codec_remove(struct snd_soc_component *component) { struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); + tas2764_flush_shutdown(tas2764); + /* A failed group's supply was forced off: there is no enable left to balance. */ if (tas2764_group_failed(tas2764)) return; @@ -1148,6 +1250,9 @@ static int tas2764_parse_dt(struct device *dev, struct tas2764_priv *tas2764) if (ret) tas2764->v_sense_slot = -1; + tas2764->deferred_shutdown = of_property_read_bool(dev->of_node, + "apple,deferred-shutdown"); + return tas2764_group_join(dev, tas2764); } @@ -1165,6 +1270,10 @@ static int tas2764_i2c_probe(struct i2c_client *client) tas2764->dev = &client->dev; tas2764->irq = client->irq; + INIT_DELAYED_WORK(&tas2764->shutdown_work, tas2764_shutdown_work); + result = devm_mutex_init(&client->dev, &tas2764->fault_lock); + if (result) + return result; i2c_set_clientdata(client, tas2764); dev_set_drvdata(&client->dev, tas2764);