diff --git a/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml b/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml index 6f31a022c6f35..2ffb6e44a5b21 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 @@ -105,6 +112,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 e0b330720fdb7..e593b7b6d68c4 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -25,6 +25,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 2e329cecdbbfe..897929d404c53 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 f62defca0dae5..277254a7e1cb5 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -37,6 +37,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", @@ -67,6 +137,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 e94528277a805..24ca6367c9b87 100644 --- a/arch/arm64/boot/dts/apple/t8132.dtsi +++ b/arch/arm64/boot/dts/apple/t8132.dtsi @@ -853,6 +853,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 380504117e52c..0a63aeb752204 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -2604,6 +2604,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 @@ -2633,6 +2637,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 4ca40688034bb..4326ea6e0d3c5 100644 --- a/sound/soc/apple/aop_audio.rs +++ b/sound/soc/apple/aop_audio.rs @@ -81,6 +81,8 @@ impl fmt::Debug for U32 { const EPIC_SUBTYPE_WRAPPED_CALL: u16 = 0x20; const CALLTYPE_AUDIO_ATTACH_DEVICE: u32 = 0xc3000002; +/// kIOReturnBusy: the firmware keeps an attachment for its lifetime, so a reload sees this. +const AUDIO_RET_BUSY: u32 = 0xe00002d5; const CALLTYPE_AUDIO_SET_PROP: u32 = 0xc3000005; const DECIM_NUM_COEFFS: usize = 120; const DECIM_NUM_COEFFS_HF: usize = 1023; @@ -697,9 +699,14 @@ impl SndSocAopData { } Ok(()) } + /// A device the firmware still holds from before a module reload counts as attached. fn audio_attach_device(&self, dev_id: u32) -> Result<()> { let msg = AudioAttachDevice::new(dev_id); let ret = self.epic_wrapped_call(&msg)?; + if ret == AUDIO_RET_BUSY { + dev_dbg!(self.dev, "device {:#x} still attached\n", dev_id); + return Ok(()); + } if ret != 0 { dev_err!( self.dev, @@ -1166,12 +1173,14 @@ impl 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 64efaa83ce8d6..f7fc73623e9f7 100644 --- a/sound/soc/apple/aop_spkr.rs +++ b/sound/soc/apple/aop_spkr.rs @@ -76,7 +76,8 @@ use kernel::{ }; use super::{ - AudioAttachDevice, AudioSetDeviceProp, PowerSetting, SndSocAopData, EPIC_SUBTYPE_WRAPPED_CALL, + AudioAttachDevice, AudioSetDeviceProp, PowerSetting, SndSocAopData, AUDIO_RET_BUSY, + EPIC_SUBTYPE_WRAPPED_CALL, }; const CALLTYPE_AUDIO_GET_PROP: u32 = 0xc3000004; @@ -93,8 +94,6 @@ const STATE_PWRD: u32 = from_fourcc(b"pwrd"); const PWRD_OUTPUT: u32 = 2; /// The same for an input stream (the microphones' `pwrd` request uses 1). const PWRD_INPUT: u32 = 1; -/// kIOReturnBusy: the firmware keeps an attachment for its lifetime, so a reload sees this. -const AUDIO_RET_BUSY: u32 = 0xe00002d5; /// Property read (0xc3000004): the same header as the property write, with a trailing 1. #[repr(C, packed(4))] @@ -1611,9 +1610,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, } @@ -1648,19 +1647,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-pm-volumes-test.h b/sound/soc/apple/macaudio-pm-volumes-test.h new file mode 100644 index 0000000000000..b6ba3eb1c4096 --- /dev/null +++ b/sound/soc/apple/macaudio-pm-volumes-test.h @@ -0,0 +1,262 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * The speaker volumes the system-sleep lock clips are set again at the + * daemon's first ping after it, through the lock control and the PM notifier. + */ +#include + +struct macaudio_test_vol { + struct soc_mixer_control mc; + long value; +}; + +static struct macaudio_test_vol *macaudio_test_vol(struct snd_kcontrol *kctl) +{ + return container_of((struct soc_mixer_control *)kctl->private_value, + struct macaudio_test_vol, mc); +} + +static int macaudio_test_vol_info(struct snd_kcontrol *kctl, struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.max = SN012776_0DB; + return 0; +} + +static int macaudio_test_vol_get(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *uval) +{ + uval->value.integer.value[0] = macaudio_test_vol(kctl)->value; + return 0; +} + +static int macaudio_test_vol_put(struct snd_kcontrol *kctl, struct snd_ctl_elem_value *uval) +{ + struct macaudio_test_vol *vol = macaudio_test_vol(kctl); + bool changed = vol->value != uval->value.integer.value[0]; + + vol->value = uval->value.integer.value[0]; + return changed; +} + +static void macaudio_test_free_kctl(void *kctl) +{ + snd_ctl_free_one(kctl); +} + +/* Last in, so first out: before the controls go */ +static void macaudio_test_pm_cleanup(void *data) +{ + struct macaudio_snd_data *ma = data; + + cancel_delayed_work_sync(&ma->lock_timeout_work); + macaudio_pm_volumes_drop(ma); +} + +static const char *const macaudio_test_vol_names[] = { + "Left Woofer Speaker Volume", + "Right Woofer Speaker Volume", + "Left Tweeter Speaker Volume", + "Right Tweeter Speaker Volume", + /* Not a speaker volume: never locked, kept or set */ + "Headphone Playback Volume", +}; + +/* As the daemon set them before the sleep */ +static const long macaudio_test_vols[] = { 201, 190, 201, 170, 120 }; + +#define MACAUDIO_TEST_NR_VOLS ARRAY_SIZE(macaudio_test_vol_names) + +struct macaudio_test_pm { + struct macaudio_snd_data *ma; + struct snd_kcontrol *slk; + struct snd_kcontrol *vols[MACAUDIO_TEST_NR_VOLS]; +}; + +static struct snd_kcontrol *macaudio_test_add_kctl(struct kunit *test, struct snd_card *card, + const struct snd_kcontrol_new *tmpl, + void *private_data) +{ + struct snd_kcontrol *kctl = snd_ctl_new1(tmpl, private_data); + int ret; + + KUNIT_ASSERT_NOT_NULL(test, kctl); + ret = kunit_add_action_or_reset(test, macaudio_test_free_kctl, kctl); + KUNIT_ASSERT_EQ(test, ret, 0); + list_add_tail(&kctl->list, &card->controls); + return kctl; +} + +/* A card with speaker safety, its lock control and the volumes, none owned yet */ +static void macaudio_test_pm_card(struct kunit *test, struct macaudio_test_pm *t) +{ + struct snd_kcontrol_new slk = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Speaker Volume Unlock", + .info = snd_ctl_boolean_mono_info, + .get = macaudio_slk_get, + .put = macaudio_slk_put, + }; + struct macaudio_snd_data *ma; + struct snd_card *card; + unsigned int i; + int ret; + + ma = kunit_kzalloc(test, sizeof(*ma), GFP_KERNEL); + card = kunit_kzalloc(test, sizeof(*card), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, ma); + KUNIT_ASSERT_NOT_NULL(test, card); + + INIT_LIST_HEAD(&card->controls); + INIT_LIST_HEAD(&card->ctl_files); + init_rwsem(&card->controls_rwsem); + rwlock_init(&card->controls_rwlock); + ma->card.snd_card = card; + snd_soc_card_set_drvdata(&ma->card, ma); + ma->cfg = &macaudio_j493_cfg; + ma->has_safety = true; + mutex_init(&ma->volume_lock_mutex); + INIT_DELAYED_WORK(&ma->lock_timeout_work, macaudio_vlimit_timeout_work); + + t->ma = ma; + t->slk = macaudio_test_add_kctl(test, card, &slk, &ma->card); + ma->speaker_lock_kctl = t->slk; + + for (i = 0; i < MACAUDIO_TEST_NR_VOLS; i++) { + struct macaudio_test_vol *vol = kunit_kzalloc(test, sizeof(*vol), GFP_KERNEL); + struct snd_kcontrol_new tmpl = { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = macaudio_test_vol_names[i], + .info = macaudio_test_vol_info, + .get = macaudio_test_vol_get, + .put = macaudio_test_vol_put, + }; + + KUNIT_ASSERT_NOT_NULL(test, vol); + vol->mc.max = SN012776_0DB; + vol->value = macaudio_test_vols[i]; + tmpl.private_value = (unsigned long)&vol->mc; + t->vols[i] = macaudio_test_add_kctl(test, card, &tmpl, NULL); + } + + ret = kunit_add_action_or_reset(test, macaudio_test_pm_cleanup, ma); + KUNIT_ASSERT_EQ(test, ret, 0); +} + +/* A daemon takes the lock and every speaker volume, as speakersafetyd does */ +static void macaudio_test_take(struct kunit *test, struct macaudio_test_pm *t) +{ + struct snd_ctl_file *owner = kunit_kzalloc(test, sizeof(*owner), GFP_KERNEL); + unsigned int i; + + KUNIT_ASSERT_NOT_NULL(test, owner); + for (i = 0; i < MACAUDIO_TEST_NR_VOLS - 1; i++) + t->vols[i]->vd[0].owner = owner; + KUNIT_ASSERT_EQ(test, macaudio_slk_lock(t->slk, owner), 0); +} + +static int macaudio_test_ping(struct kunit *test, struct macaudio_test_pm *t) +{ + struct snd_ctl_elem_value *uval = kunit_kzalloc(test, sizeof(*uval), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, uval); + uval->value.integer.value[0] = SPEAKER_MAGIC_VALUE; + return macaudio_slk_put(t->slk, uval); +} + +static void macaudio_test_notify(struct macaudio_test_pm *t, unsigned long action) +{ + macaudio_pm_notify(&t->ma->pm_nb, action, NULL); +} + +/* The speaker volumes as the daemon set them, or clipped to the lock's ceiling */ +static void macaudio_test_expect_vols(struct kunit *test, struct macaudio_test_pm *t, + bool clipped) +{ + unsigned int i; + + for (i = 0; i < MACAUDIO_TEST_NR_VOLS; i++) { + bool speaker = i < MACAUDIO_TEST_NR_VOLS - 1; + + KUNIT_EXPECT_EQ(test, macaudio_test_vol(t->vols[i])->value, + speaker && clipped ? 1L : macaudio_test_vols[i]); + } +} + +static void macaudio_pm_volumes_restored_after_sleep(struct kunit *test) +{ + struct macaudio_test_pm t; + + macaudio_test_pm_card(test, &t); + macaudio_test_take(test, &t); + KUNIT_EXPECT_EQ(test, macaudio_test_ping(test, &t), 0); + KUNIT_EXPECT_TRUE(test, t.ma->speaker_volume_unlocked); + macaudio_test_expect_vols(test, &t, false); + + macaudio_test_notify(&t, PM_SUSPEND_PREPARE); + KUNIT_EXPECT_FALSE(test, t.ma->speaker_volume_unlocked); + macaudio_test_expect_vols(test, &t, true); + /* A ping before the freeze leaves them locked */ + KUNIT_EXPECT_EQ(test, macaudio_test_ping(test, &t), 0); + KUNIT_EXPECT_FALSE(test, t.ma->speaker_volume_unlocked); + + /* Locked after the resume until the daemon pings */ + macaudio_test_notify(&t, PM_POST_SUSPEND); + KUNIT_EXPECT_FALSE(test, t.ma->speaker_volume_unlocked); + macaudio_test_expect_vols(test, &t, true); + + KUNIT_EXPECT_EQ(test, macaudio_test_ping(test, &t), 0); + KUNIT_EXPECT_TRUE(test, t.ma->speaker_volume_unlocked); + macaudio_test_expect_vols(test, &t, false); + KUNIT_EXPECT_NULL(test, t.ma->pm_volumes); +} + +/* The daemon pings only once a stream runs: a second sleep can come first. */ +static void macaudio_pm_volumes_second_sleep_before_ping(struct kunit *test) +{ + struct macaudio_test_pm t; + + macaudio_test_pm_card(test, &t); + macaudio_test_take(test, &t); + KUNIT_EXPECT_EQ(test, macaudio_test_ping(test, &t), 0); + + macaudio_test_notify(&t, PM_SUSPEND_PREPARE); + macaudio_test_notify(&t, PM_POST_SUSPEND); + macaudio_test_notify(&t, PM_SUSPEND_PREPARE); + macaudio_test_notify(&t, PM_POST_SUSPEND); + macaudio_test_expect_vols(test, &t, true); + + KUNIT_EXPECT_EQ(test, macaudio_test_ping(test, &t), 0); + macaudio_test_expect_vols(test, &t, false); +} + +/* A new owner, such as a restarted daemon, sets the volumes itself. */ +static void macaudio_pm_volumes_new_owner(struct kunit *test) +{ + struct macaudio_test_pm t; + + macaudio_test_pm_card(test, &t); + macaudio_test_take(test, &t); + KUNIT_EXPECT_EQ(test, macaudio_test_ping(test, &t), 0); + macaudio_test_notify(&t, PM_SUSPEND_PREPARE); + macaudio_test_notify(&t, PM_POST_SUSPEND); + + macaudio_test_take(test, &t); + KUNIT_EXPECT_NULL(test, t.ma->pm_volumes); + KUNIT_EXPECT_EQ(test, macaudio_test_ping(test, &t), 0); + KUNIT_EXPECT_TRUE(test, t.ma->speaker_volume_unlocked); + macaudio_test_expect_vols(test, &t, true); +} + +static struct kunit_case macaudio_pm_volumes_cases[] = { + KUNIT_CASE(macaudio_pm_volumes_restored_after_sleep), + KUNIT_CASE(macaudio_pm_volumes_second_sleep_before_ping), + KUNIT_CASE(macaudio_pm_volumes_new_owner), + {} +}; + +static struct kunit_suite macaudio_pm_volumes_suite = { + .name = "apple-speaker-pm-volumes", + .test_cases = macaudio_pm_volumes_cases, +}; +kunit_test_suite(macaudio_pm_volumes_suite); diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index 1a7519c2eaa74..3e51478e8797e 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -133,6 +134,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[] = { @@ -164,6 +171,12 @@ struct ma_codec_idle { u32 idle_mask; }; +/* A speaker volume control and its value, as the system-sleep lock found it */ +struct macaudio_pm_volume { + struct snd_kcontrol *kctl; + long value[2]; +}; + struct macaudio_snd_data { struct snd_soc_card card; struct snd_soc_jack jack; @@ -209,6 +222,15 @@ struct macaudio_snd_data { /* Between PM_SUSPEND_PREPARE and PM_POST_SUSPEND: the speaker volumes stay locked */ bool pm_quiesced; struct notifier_block pm_nb; + /* + * The speaker volumes as the system-sleep lock found them. Locking clips + * them to its ceiling and unlocking only lifts the ceiling; the daemon + * keeps its lease across the sleep and does not set them again. + */ + struct macaudio_pm_volume *pm_volumes; + unsigned int pm_nr_volumes; + /* Allocated with them, so that setting them again cannot fail */ + struct snd_ctl_elem_value *pm_uval; /* * Software-gain amplifier: a lower gain cannot change samples that @@ -455,6 +477,113 @@ static void macaudio_lock_fail_work(struct work_struct *work) macaudio_stop_playback(ma, false); } +static void macaudio_pm_volumes_drop(struct macaudio_snd_data *ma) +{ + kfree(ma->pm_volumes); + kfree(ma->pm_uval); + ma->pm_volumes = NULL; + ma->pm_nr_volumes = 0; + ma->pm_uval = NULL; +} + +/* + * Keep the speaker volumes before the system-sleep lock clips them. + * + * Context: volume_lock_mutex and the card's controls_rwsem held, so the daemon + * cannot set a volume between this and the clip. + */ +static void macaudio_pm_volumes_save(struct macaudio_snd_data *ma) +{ + const char *name = volume_control_names[ma->cfg->amp]; + struct snd_ctl_elem_value *uval = NULL; + struct macaudio_pm_volume *vols = NULL; + struct snd_kcontrol *kctl; + unsigned int nr = 0, i = 0; + int ret; + + lockdep_assert_held(&ma->volume_lock_mutex); + lockdep_assert_held(&ma->card.snd_card->controls_rwsem); + + if (!ma->has_safety || !name || ma->card.snd_card->shutdown) + return; + /* + * Locked already, as after a sleep the daemon has not pinged since: + * the volumes are at the ceiling, and those from before it are kept. + */ + if (!ma->speaker_volume_unlocked) + return; + macaudio_pm_volumes_drop(ma); + + list_for_each_entry(kctl, &ma->card.snd_card->controls, list) + if (snd_soc_control_matches(kctl, name)) + nr++; + if (!nr) + return; + + vols = kzalloc_objs(*vols, nr); + uval = kzalloc_obj(*uval); + if (!vols || !uval) + goto out; + + list_for_each_entry(kctl, &ma->card.snd_card->controls, list) { + if (!snd_soc_control_matches(kctl, name)) + continue; + memset(uval, 0, sizeof(*uval)); + uval->id = kctl->id; + ret = kctl->get(kctl, uval); + if (ret < 0) { + dev_warn(ma->card.dev, "Failed to keep volume %s for the system sleep: %d\n", + kctl->id.name, ret); + goto out; + } + vols[i].kctl = kctl; + vols[i].value[0] = uval->value.integer.value[0]; + vols[i].value[1] = uval->value.integer.value[1]; + i++; + } + + ma->pm_volumes = vols; + ma->pm_nr_volumes = i; + ma->pm_uval = uval; + return; +out: + kfree(uval); + kfree(vols); +} + +/* + * At the unlock after a system sleep: the volumes the daemon had set. + * + * Context: volume_lock_mutex and the card's controls_rwsem held, so no volume + * write races these. + */ +static void macaudio_pm_volumes_restore(struct macaudio_snd_data *ma) +{ + struct snd_ctl_elem_value *uval = ma->pm_uval; + unsigned int i; + int ret; + + if (!ma->pm_volumes) + return; + + for (i = 0; i < ma->pm_nr_volumes; i++) { + struct snd_kcontrol *kctl = ma->pm_volumes[i].kctl; + + memset(uval, 0, sizeof(*uval)); + uval->id = kctl->id; + uval->value.integer.value[0] = ma->pm_volumes[i].value[0]; + uval->value.integer.value[1] = ma->pm_volumes[i].value[1]; + ret = kctl->put(kctl, uval); + if (ret < 0) + dev_err(ma->card.dev, "Failed to restore volume %s: %d\n", + kctl->id.name, ret); + else if (ret > 0) + snd_ctl_notify(ma->card.snd_card, SNDRV_CTL_EVENT_MASK_VALUE, + &kctl->id); + } + macaudio_pm_volumes_drop(ma); +} + static void macaudio_vlimit_update(struct macaudio_snd_data *ma) { int i; @@ -515,6 +644,8 @@ static void macaudio_vlimit_update(struct macaudio_snd_data *ma) } macaudio_vlimit_unlock(ma, unlock); + if (unlock) + macaudio_pm_volumes_restore(ma); if (!unlock && ma->cfg->amp == AMP_MAX98360A) { /* Samples already converted at the old gain must not play */ atomic_inc(&ma->sw_gain_lock_epoch); @@ -546,6 +677,12 @@ static int macaudio_pm_notify(struct notifier_block *nb, unsigned long action, return NOTIFY_DONE; } + /* + * Volume writes hold the controls for writing: none can land between + * keeping the volumes and clipping them. Control writes take + * volume_lock_mutex inside it as well. + */ + down_read(&ma->card.snd_card->controls_rwsem); mutex_lock(&ma->volume_lock_mutex); ma->pm_quiesced = quiesce; /* @@ -557,9 +694,11 @@ static int macaudio_pm_notify(struct notifier_block *nb, unsigned long action, if (quiesce) { cancel_delayed_work(&ma->lock_timeout_work); ma->speaker_lock_remain = 0; + macaudio_pm_volumes_save(ma); } macaudio_vlimit_update(ma); mutex_unlock(&ma->volume_lock_mutex); + up_read(&ma->card.snd_card->controls_rwsem); return NOTIFY_OK; } @@ -653,10 +792,13 @@ static void macaudio_vlimit_update_work(struct work_struct *wrk) struct macaudio_snd_data *ma = container_of(wrk, struct macaudio_snd_data, lock_update_work); + /* An unlock here sets the kept volumes again: no volume write may race it */ + down_read(&ma->card.snd_card->controls_rwsem); if (ma->bes_active) macaudio_vlimit_enable_timeout(ma); else macaudio_vlimit_disable_timeout(ma); + up_read(&ma->card.snd_card->controls_rwsem); } static bool macaudio_sw_gain_pcm(struct snd_pcm_substream *substream) @@ -961,6 +1103,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 @@ -1667,6 +1811,15 @@ static int macaudio_card_remove(struct snd_soc_card *card) struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); unregister_pm_notifier(&ma->pm_nb); + /* + * No back-end can trigger any more, but a lock update it queued takes + * the card's controls_rwsem: finish it while the snd_card still exists. + */ + cancel_work_sync(&ma->lock_update_work); + cancel_delayed_work_sync(&ma->lock_timeout_work); + mutex_lock(&ma->volume_lock_mutex); + macaudio_pm_volumes_drop(ma); + mutex_unlock(&ma->volume_lock_mutex); return 0; } @@ -1962,6 +2115,8 @@ static int macaudio_slk_lock(struct snd_kcontrol *kcontrol, struct snd_ctl_file cancel_delayed_work(&ma->lock_timeout_work); ma->speaker_lock_remain = 0; ma->speaker_lock_timeout = 0; + /* A new owner sets the volumes itself */ + macaudio_pm_volumes_drop(ma); ma->speaker_lock_owner = owner; macaudio_vlimit_update(ma); @@ -2164,6 +2319,30 @@ struct macaudio_platform_cfg macaudio_j614_cfg = { .route_only_fes = BIT(0), }; +/* + * 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, + /* No headphone codec on this card: the jack is a card of its own. */ + .route_only_fes = BIT(0), + .speaker_fe_channels = 6, +}; + /* * The J614s and 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 @@ -2257,6 +2436,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"}, { } @@ -2392,4 +2575,5 @@ MODULE_LICENSE("GPL"); #if IS_ENABLED(CONFIG_KUNIT) && !defined(MODULE) #include "macaudio-admission-test.h" +#include "macaudio-pm-volumes-test.h" #endif diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index 4bb710efe35f1..be0e9ce2be82f 100644 --- a/sound/soc/codecs/tas2764.c +++ b/sound/soc/codecs/tas2764.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,8 @@ struct tas2764_priv { struct regmap *regmap; struct device *dev; int irq; + /* Set while the component holds the fault IRQ */ + bool irq_requested; enum tas2764_devid devid; int v_sense_slot; @@ -72,12 +75,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) { @@ -189,9 +212,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; @@ -206,6 +228,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", @@ -229,6 +262,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) { @@ -269,6 +315,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 @@ -286,6 +334,20 @@ static int tas2764_codec_suspend(struct snd_soc_component *component) struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); int ret; + /* + * Until resume the handler can neither read nor clear the latches: the + * regmap goes cache-only below, and the I2C controller suspends after + * the card. A fault latched meanwhile, such as the clock fault when the + * bus clock stops, would hold the shared level-low line asserted and + * run every amp's handler in a loop. Keep the line masked until the + * cache is live again; disable_irq() nests across the amps sharing it. + * ASoC resumes every suspended component, even after a failed suspend. + */ + if (tas2764->irq_requested) + disable_irq(tas2764->irq); + + tas2764_flush_shutdown(tas2764); + ret = snd_soc_component_update_bits(component, TAS2764_PWR_CTRL, TAS2764_PWR_CTRL_MASK, TAS2764_PWR_CTRL_SHUTDOWN); @@ -318,7 +380,7 @@ static int tas2764_codec_resume(struct snd_soc_component *component) ret = tas2764->sdz_reg ? regulator_enable(tas2764->sdz_reg) : 0; if (ret) { dev_err(tas2764->dev, "Failed to enable regulator\n"); - return ret; + goto out_irq; } if (tas2764->sdz_gpio) @@ -330,10 +392,14 @@ static int tas2764_codec_resume(struct snd_soc_component *component) regcache_cache_only(tas2764->regmap, false); ret = regcache_sync(tas2764->regmap); - if (ret < 0) - return ret; + if (ret >= 0) + ret = tas2764_update_pwr_ctrl(tas2764); - return tas2764_update_pwr_ctrl(tas2764); +out_irq: + /* Balance the suspend; a fault latched in sleep is handled now. */ + if (tas2764->irq_requested) + enable_irq(tas2764->irq); + return ret; } #else #define tas2764_codec_suspend NULL @@ -390,6 +456,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 = @@ -397,6 +483,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) @@ -412,6 +522,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) @@ -937,34 +1055,30 @@ static int tas2764_codec_probe(struct snd_soc_component *component) tas2764_reset(tas2764); regmap_reinit_cache(tas2764->regmap, &tas2764_i2c_regmap); + /* The reset cleared the idle slot setup: make the next set_tdm_idle() write it */ + tas2764->idle_slot_config.tx_mask = 0; + tas2764->idle_slot_config.tx_mode = SND_SOC_DAI_TDM_IDLE_NONE; + if (tas2764->irq) { ret = snd_soc_component_write(tas2764->component, TAS2764_INT_MASK0, 0x00); if (ret < 0) - return ret; + goto err_sdz; ret = snd_soc_component_write(tas2764->component, TAS2764_INT_MASK1, 0xff); if (ret < 0) - return ret; + goto err_sdz; ret = snd_soc_component_write(tas2764->component, TAS2764_INT_MASK2, 0xff); if (ret < 0) - return ret; + goto err_sdz; ret = snd_soc_component_write(tas2764->component, TAS2764_INT_MASK3, 0xff); if (ret < 0) - return ret; + goto err_sdz; ret = snd_soc_component_write(tas2764->component, TAS2764_INT_MASK4, 0xff); if (ret < 0) - return ret; - - ret = devm_request_threaded_irq(tas2764->dev, tas2764->irq, NULL, tas2764_irq, - IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW, - "tas2764", tas2764); - if (ret && tas2764->sdz_group) - return dev_err_probe(tas2764->dev, ret, "SDZ group needs its fault IRQ\n"); - if (ret) - dev_warn(tas2764->dev, "failed to request IRQ: %d\n", ret); + goto err_sdz; } if (tas2764->i_sense_slot != -1 && tas2764->v_sense_slot != -1) { @@ -972,7 +1086,7 @@ static int tas2764_codec_probe(struct snd_soc_component *component) tas2764->v_sense_slot); if (ret < 0) - return ret; + goto err_sdz; } switch (tas2764->devid) { @@ -981,7 +1095,7 @@ static int tas2764_codec_probe(struct snd_soc_component *component) TAS2764_PWR_CTRL_BOP_SRC, TAS2764_PWR_CTRL_BOP_SRC); if (ret < 0) - return ret; + goto err_sdz; for (i = 0; i < ARRAY_SIZE(sn012776_bop_presets); i++) { ret = snd_soc_component_write(component, @@ -989,27 +1103,62 @@ static int tas2764_codec_probe(struct snd_soc_component *component) sn012776_bop_presets[i]); if (ret < 0) - return ret; + goto err_sdz; } /* Apply all enabled Apple quirks */ ret = tas2764_apply_init_quirks(tas2764); if (ret < 0) - return ret; + goto err_sdz; break; default: break; } + /* + * Last, so that no failure leaves the handler behind. The component owns the IRQ and + * the remove frees it; not a devres of the I2C device, which outlives the component + * and, on an unbind, would release the IRQ before the component is removed. + */ + if (tas2764->irq) { + ret = request_threaded_irq(tas2764->irq, NULL, tas2764_irq, + IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW, + "tas2764", tas2764); + if (ret && tas2764->sdz_group) { + dev_err_probe(tas2764->dev, ret, "SDZ group needs its fault IRQ\n"); + goto err_sdz; + } + if (ret) + dev_warn(tas2764->dev, "failed to request IRQ: %d\n", ret); + else + tas2764->irq_requested = true; + } + return 0; + +err_sdz: + /* Undo the power-up: ASoC doesn't call the remove after a failed probe */ + if (tas2764->sdz_gpio) + gpiod_set_value_cansleep(tas2764->sdz_gpio, 0); + /* As in the remove: no supply to drop with a GPIO SDZ, none left in a failed group */ + if (tas2764->sdz_reg && !tas2764_group_failed(tas2764)) + regulator_disable(tas2764->sdz_reg); + return ret; } static void tas2764_codec_remove(struct snd_soc_component *component) { struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); + if (tas2764->irq_requested) { + free_irq(tas2764->irq, tas2764); + tas2764->irq_requested = false; + } + + 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; @@ -1161,6 +1310,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); } @@ -1178,6 +1330,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);