From b48241dd02fd4b240696134540ebbc406875df5c Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 18:21:23 -0400 Subject: [PATCH 01/16] dt-bindings: soc: apple: apple,t6041-aop: Add T8132 The T8132 (M4) AOP runs the same firmware transport as the T6041 one, and its bootloader hands it over the same way. It also programs the AOP DART's address filter (DAPF) and reserves the filter's windows, which the firmware reaches through its DART at their physical addresses. The windows come in memory-region with the firmware segments, up to 16 of them. Signed-off-by: Erik Bourget --- .../bindings/soc/apple/apple,t6041-aop.yaml | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) 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: From d58ef312ef559765e72dc03f8ff21cd20ee3940b Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 18:21:42 -0400 Subject: [PATCH 02/16] ASoC: dt-bindings: apple,t6041-aop-audio: Add T8132 The T8132 AOP audio service is the T6041 one: the same LPAI microphone flow, and the same ADMAC channel map for the speakers and the jack. Allow "apple,t8132-aop-audio" in front of the T6041 compatible. A machine brought up in steps lists only the streams it has so far, so allow the microphones alone, and the microphones with the jack, besides the speaker layouts. Signed-off-by: Erik Bourget --- .../bindings/sound/apple,t6041-aop-audio.yaml | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) 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 From f297b2d6599d3e858ca150ee94a2a4e873245c75 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 22:14:24 -0400 Subject: [PATCH 03/16] soc: apple: aop: Add T8132 The T8132 (M4) AOP takes the T6040 boot arguments (EC0p 0x100_0000_0000, nCal 0, alig 64, AOPt 0): iBoot leaves the boot-argument table at its defaults in the AOP SRAM and the AOP halted, and the ADT gives __ETEXT the DART address 0x100_0000_0000. The bootloader programs the AOP DART's DAPF and reserves the firmware segments and the DAPF windows, so the T6040 configuration applies unchanged. Tested on J713 (MacBook Air 13" M4) with m1n1 handing the AOP over (its memory-region holds __ETEXT, __OS_LOG and the 15 DAPF windows): the AOP starts, and the microphones capture (3 channels, 48 kHz) through aop_audio. Signed-off-by: Erik Bourget --- drivers/soc/apple/aop.rs | 5 +++++ 1 file changed, 5 insertions(+) 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), ] ); From 93e4dad9882b515b5fdd70277f1fc769d7f10b1f Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 18:22:45 -0400 Subject: [PATCH 04/16] arm64: dts: apple: t8132: Add the AOP and the audio ADMACs Describe the T8132 AOP with its mailbox and DART, and the audio fabric's two ADMACs (admac-leap-ns on DART stream 10, admac-base-ns on stream 8), all disabled. Addresses and interrupts are the ADT's; the ADMAC channel map is the T6041 one: the microphones on LEAP channel 1. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132.dtsi | 78 ++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) 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"; From 890e6a96f3b1441243e7dc07ba1f69ea5fc3caa9 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 18:22:45 -0400 Subject: [PATCH 05/16] arm64: dts: apple: t8132-j713: Enable the AOP and the microphones Enable the AOP, its DART and mailbox, and the ADMACs on the MacBook Air 13" M4, with the AOP audio service for the three microphones. /chosen already asks for the AOP's handoff: m1n1 programs the AOP DART's DAPF and adds the firmware segments and the DAPF windows to the AOP's memory-region, or disables the AOP. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-j713.dts | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132-j713.dts b/arch/arm64/boot/dts/apple/t8132-j713.dts index d878b72fd39ba..56b7285fcf518 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -56,6 +56,36 @@ }; }; +/* + * 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 { + apple,chassis-name = "J713"; + apple,machine-kind = "MacBook Air"; +}; + +&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"; From acdfd796a37d9ebcc3b7c50c42d6a4131f6a4a1c Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 18:42:03 -0400 Subject: [PATCH 06/16] ASoC: apple: aop_audio: Register the jack without a speaker stream The jack component was only registered along with the speaker one, so a machine whose audio node lists the jack's DMA channels but not yet the speakers' got no jack. Register the speaker component only where the node lists "spkr", and the jack's where it lists "cout" and "cin", each on its own. Creating the services does not reach the firmware; nothing changes for the J616s, which lists all of them. Signed-off-by: Erik Bourget --- sound/soc/apple/aop_audio.rs | 14 +++++++------ sound/soc/apple/aop_spkr.rs | 38 +++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 20 deletions(-) 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, From 32ef3e9cba78333c7b2ecb1dfb8fc0847c904ca8 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 18:42:03 -0400 Subject: [PATCH 07/16] ASoC: dt-bindings: apple,macaudio: Add the J713 jack The MacBook Air 13" M4 (J713) headphone jack is a card of its own on the AOP, like the J616s one. Signed-off-by: Erik Bourget --- Documentation/devicetree/bindings/sound/apple,macaudio.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml index fdf3d1883a99d..e9b2d3eb522dc 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -23,6 +23,7 @@ properties: - apple,j616-macaudio - apple,j616-macaudio-jack - apple,j700-macaudio + - apple,j713-macaudio-jack - const: apple,macaudio "#address-cells": From c95e8e830796c0a4c6c63e832daba822ad48ef2e Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 18:42:03 -0400 Subject: [PATCH 08/16] ASoC: apple: macaudio: Add the J713 jack The J713 jack runs on the AOP firmware's MCA cluster 2 with the same CS42L84 setup as the J616s jack. Use the J616s jack configuration, including its -6 dB cap on the headphone DAC: this board's analog full scale is unmeasured too. Signed-off-by: Erik Bourget --- sound/soc/apple/macaudio.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index cc1123e10f398..52a3914f0278d 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -2129,6 +2129,8 @@ 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 }, + /* 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"}, { } From 9472aa51b6b084d197d78f11e716303894bc6778 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 18:42:03 -0400 Subject: [PATCH 09/16] arm64: dts: apple: t8132-j713: Add the headphone jack Add the CS42L84 on i2c1 (reset on nub GPIO 17, interrupt on AP GPIO 98, from the ADT), the jack's ADMAC channels on the AOP audio node (base-ns TX2 and RX2, as on the J616s) and the jack's own card. The codec is the first consumer of the i2c1 power domain, so the J713 power-domain policy now enables it; it has no parent domains. Signed-off-by: Erik Bourget --- .../arm64/boot/dts/apple/t8132-j713-pmgr.dtsi | 3 +- arch/arm64/boot/dts/apple/t8132-j713.dts | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi index 5606ce4d2d3e4..3c5c86706cd54 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi @@ -125,8 +125,9 @@ status = "disabled"; }; +/* The CS42L84 headphone codec. */ &ps_i2c1 { - status = "disabled"; + status = "okay"; }; &ps_i2c2 { diff --git a/arch/arm64/boot/dts/apple/t8132-j713.dts b/arch/arm64/boot/dts/apple/t8132-j713.dts index 56b7285fcf518..6bac56a55d1c2 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -36,6 +36,30 @@ wifi0 = &wifi0; }; + /* + * 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", @@ -66,10 +90,31 @@ }; &aop_audio { + /* The microphones on LEAP, the jack on base-ns TX2 and RX2. */ + dmas = <&admac0 1>, <&admac1 4>, <&admac1 5>; + dma-names = "dma", "cout", "cin"; apple,chassis-name = "J713"; apple,machine-kind = "MacBook Air"; }; +/* + * 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"; }; From 9669295d012eddeb6cd251b0519f0c5cdeb41991 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 19:42:24 -0400 Subject: [PATCH 10/16] ASoC: dt-bindings: apple,macaudio: Add the J713 speakers The MacBook Air 13" M4 (J713) speakers are a card of their own on the AOP's speaker stream, like the J616s ones. Signed-off-by: Erik Bourget --- Documentation/devicetree/bindings/sound/apple,macaudio.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml index e9b2d3eb522dc..fcf97451df97a 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -23,6 +23,7 @@ properties: - apple,j616-macaudio - apple,j616-macaudio-jack - apple,j700-macaudio + - apple,j713-macaudio - apple,j713-macaudio-jack - const: apple,macaudio From 98cad04f81c7f50f90abf46bfc12c603331820ab Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 19:42:24 -0400 Subject: [PATCH 11/16] ASoC: apple: macaudio: Add the J713 speakers The MacBook Air 13" M4 (J713) runs its four SN012776 amps, a woofer and a tweeter per side, on the AOP's speaker stream exactly like the J616s: the same 250-bit frame, I2S with an inverted frame clock, six playback and twelve sense slots, of which the four amps take the first four and eight. Amp gain 15 and the -20 dB safe volume as on the other SN012776 machines. The speaker front-end's channel count was one per amp; add speaker_fe_channels for a CPU stream with more slots than amps, and give the J713 six. Its last two channels are silent. Signed-off-by: Erik Bourget --- sound/soc/apple/macaudio.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index 52a3914f0278d..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,8 @@ 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 */ From ed2e874ada6424ab687dba03c460c4c3a937f4cc Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 24 Oct 2026 19:42:24 -0400 Subject: [PATCH 12/16] arm64: dts: apple: t8132-j713: Add the speakers Add the four SN012776 amps on i2c3 from the ADT (audio-speaker at 0x38 and its speaker1..3: left woofer 0x38, right woofer 0x3b, left tweeter 0x39, right tweeter 0x3c), each with its sense slots from speaker-config (imon at RX byte 4i, vmon at 4i + 2), the shared interrupt on AP GPIO 14 and the shared shutdown line on AP GPIO 15 as the fail-closed SDZ group's fixed regulator. The speaker card lists the amps in the ADT provider order, so each takes its native playback slot, with the amp-tx-zd-config idle masks. The AOP audio node gets the speaker and sense channels (base-ns TX0 and RX0): the J616s stream, of which the four amps take the first four playback and eight sense slots. The amps are the first consumer of the i2c3 power domain. Signed-off-by: Erik Bourget --- .../arm64/boot/dts/apple/t8132-j713-pmgr.dtsi | 3 +- arch/arm64/boot/dts/apple/t8132-j713.dts | 117 +++++++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi index 3c5c86706cd54..1e100e890be0a 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-j713-pmgr.dtsi @@ -134,8 +134,9 @@ 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 6bac56a55d1c2..d92d0b3b71928 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -36,6 +36,52 @@ 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. @@ -90,13 +136,78 @@ }; &aop_audio { - /* The microphones on LEAP, the jack on base-ns TX2 and RX2. */ - dmas = <&admac0 1>, <&admac1 4>, <&admac1 5>; - dma-names = "dma", "cout", "cin"; + /* + * 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; + #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; + #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; + #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; + #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 From 1b7bbd2802abe59f7154e2d3ec027a3248cf40c4 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Fri, 30 Oct 2026 22:35:19 -0400 Subject: [PATCH 13/16] ASoC: dt-bindings: ti,tas27xx: Add apple,deferred-shutdown On an AOP-fed speaker stream the TDM bus keeps running for a while after the playback stream stops, and the amplifiers' current and voltage sense slots are still sampled in that time. An amplifier put in software shutdown then can leave garbage in its sense slots. Add a property for such amplifiers: they must not enter software shutdown until the bus has stopped. Signed-off-by: Erik Bourget --- Documentation/devicetree/bindings/sound/ti,tas27xx.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) 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. From 7f175fba04cd14f821cc18245eb847c6ef9eac49 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Fri, 30 Oct 2026 22:35:34 -0400 Subject: [PATCH 14/16] ASoC: tas2764: Defer software shutdown with apple,deferred-shutdown On the J713 the AOP keeps the speaker TDM bus running for a while after a playback stream stops, and the speaker protection still reads the amplifiers' I/V sense words in that time. Each amp is muted and then shut down in turn as the stream ends, so the shutdowns land in that tail. The right tweeter's voltage sense slot then reads full scale for about 20 samples at every stream stop, even for a stream of digital silence played with the volume locked; the protection daemons charge that as power. With apple,deferred-shutdown, mute the amp at the stream's end as before, but put it in software shutdown only 150 ms later, after the bus has stopped (it ran on for up to 73 ms on the J713). A stream that starts in between cancels it. Suspend and removal shut the amp down at once. Such an amp sees the bus clock stop while it waits, and may see it latched again after its shutdown; that clock-only fault is expected then and no longer logged. Amps without the property keep the current behaviour. On a J713 the full-scale words in the stream tails go from about 90 per test (three silent streams and a chord) to none, with no fault logged, and every amp still ends in software shutdown. Signed-off-by: Erik Bourget --- sound/soc/codecs/tas2764.c | 70 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index 54c1629c410fc..9f7bb79ac1234 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,27 @@ 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; }; #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) { @@ -204,6 +220,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", @@ -267,6 +294,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 +313,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 +418,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 +445,11 @@ static int tas2764_mute(struct snd_soc_dai *dai, int mute, int direction) int ret; if (!mute) { + if (tas2764->deferred_shutdown) { + /* still muted from the last stream: no shutdown in between */ + cancel_delayed_work_sync(&tas2764->shutdown_work); + WRITE_ONCE(tas2764->stream_stopped, false); + } tas2764->dac_powered = true; ret = tas2764_update_pwr_ctrl(tas2764); if (ret) @@ -409,6 +465,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 +1071,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 +1214,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 +1234,7 @@ 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); i2c_set_clientdata(client, tas2764); dev_set_drvdata(&client->dev, tas2764); From 3cb43cd2045c56c29d3051716dc60d43d8964cf4 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Fri, 30 Oct 2026 22:11:32 -0400 Subject: [PATCH 15/16] arm64: dts: apple: t8132-j713: Defer the speaker amps' shutdown The J713's AOP keeps the speaker bus running briefly after a stream stops, and the speaker protection reads the amps' sense words in that time. Have the four amps wait for the bus clock to stop before they go to software shutdown. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-j713.dts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132-j713.dts b/arch/arm64/boot/dts/apple/t8132-j713.dts index d92d0b3b71928..032232b5864a6 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713.dts +++ b/arch/arm64/boot/dts/apple/t8132-j713.dts @@ -164,6 +164,7 @@ 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>; @@ -176,6 +177,7 @@ 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>; @@ -188,6 +190,7 @@ 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>; @@ -200,6 +203,7 @@ 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>; From f5746d19b41908cfb5e88112623eb95dd8acf17a Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sat, 31 Oct 2026 20:12:27 -0400 Subject: [PATCH 16/16] ASoC: tas2764: Drop the clock fault latched while stopped at unmute With apple,deferred-shutdown the muted amp stays powered after a stream until the bus clock stops, and keeps latching TDM clock faults while it waits; the fault handler ignores them while stream_stopped is set. The amps share one fault line and each handler reads six latches over I2C, so a latch can reach its handler several milliseconds late. When the next stream unmutes the amp in that window, stream_stopped is already clear and the stale latch is reported as a fault on the new stream. On the J713 this hit the first amp the link unmutes on 9 of 32 silent streams that started within 140 ms of the end of the last one, and on none of 32 that started later. At unmute, clear the latches before clearing stream_stopped when the first latch register holds only the clock fault (the clear takes every latch, so any other fault there is left for the handler), and hold a lock across the handler's read and judgement of the latches so that the unmute cannot fall between them. With this, none of 96 silent streams logged a fault, 48 of them started within 140 ms of the last. Signed-off-by: Erik Bourget --- sound/soc/codecs/tas2764.c | 43 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index 9f7bb79ac1234..4c7b4bad88f7d 100644 --- a/sound/soc/codecs/tas2764.c +++ b/sound/soc/codecs/tas2764.c @@ -79,6 +79,11 @@ struct tas2764_priv { /* 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" @@ -203,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; @@ -254,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) { @@ -446,9 +463,28 @@ static int tas2764_mute(struct snd_soc_dai *dai, int mute, int direction) 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); @@ -1235,6 +1271,9 @@ 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);