From de80616b4d0ca6cb9cf7616e33997001b7640791 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:44:54 +0100 Subject: [PATCH 01/55] dt-bindings: apple: Add the T8152 AOP and its audio service The T8152 (M6) Always-On Processor differs from the T8140 one: its setup port is not reachable from the AP, so it has one mailbox, and its audio service uses six serializer power domains and the jack, speaker and speaker-sense ADMAC channels. Signed-off-by: Thomas Glanzmann --- .../bindings/soc/apple/apple,t8152-aop.yaml | 80 ++++++++++++++++++ .../bindings/sound/apple,t8152-aop-audio.yaml | 82 +++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 Documentation/devicetree/bindings/soc/apple/apple,t8152-aop.yaml create mode 100644 Documentation/devicetree/bindings/sound/apple,t8152-aop-audio.yaml diff --git a/Documentation/devicetree/bindings/soc/apple/apple,t8152-aop.yaml b/Documentation/devicetree/bindings/soc/apple/apple,t8152-aop.yaml new file mode 100644 index 0000000000000..de41937be0259 --- /dev/null +++ b/Documentation/devicetree/bindings/soc/apple/apple,t8152-aop.yaml @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/soc/apple/apple,t8152-aop.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple T8152 Always-On Processor + +maintainers: + - Thomas Glanzmann + +description: + The Always-On Processor provides audio services through firmware loaded by + the bootloader. Unlike on T8140, its setup port is not reachable from the + application processor, so only the primary mailbox is described. + +properties: + compatible: + const: apple,t8152-aop + + reg: + items: + - description: AOP register and boot-argument window + - description: ASC wrapper and processor control registers + + reg-names: + items: + - const: mmio + - const: asc + + mboxes: + maxItems: 1 + + mbox-names: + items: + - const: mbox + + iommus: + maxItems: 1 + + apple,dma-range: + $ref: /schemas/types.yaml#/definitions/uint64-array + minItems: 2 + maxItems: 2 + description: + Host DMA allocation window as a base and nonzero size. The window must + occupy root slots available to the host on this AOP DART stream. + + audio: + type: object + $ref: /schemas/sound/apple,t8152-aop-audio.yaml# + +required: + - compatible + - reg + - reg-names + - mboxes + - mbox-names + - iommus + - apple,dma-range + +additionalProperties: false + +examples: + - | + soc { + #address-cells = <2>; + #size-cells = <2>; + + aop@310c00000 { + compatible = "apple,t8152-aop"; + reg = <0x3 0x10c00000 0x0 0x1e0000>, + <0x3 0x10600000 0x0 0x4000>; + reg-names = "mmio", "asc"; + mboxes = <&aop_mbox>; + mbox-names = "mbox"; + iommus = <&sbr_dart 0>; + apple,dma-range = <0x110 0x0 0x10 0x0>; + }; + }; diff --git a/Documentation/devicetree/bindings/sound/apple,t8152-aop-audio.yaml b/Documentation/devicetree/bindings/sound/apple,t8152-aop-audio.yaml new file mode 100644 index 0000000000000..fa643fe62a9df --- /dev/null +++ b/Documentation/devicetree/bindings/sound/apple,t8152-aop-audio.yaml @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/apple,t8152-aop-audio.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple T8152 AOP audio + +maintainers: + - Thomas Glanzmann + +description: + Audio services of the T8152 Always-On Processor firmware, a child of the + AOP node. The firmware owns the audio fabric; the host feeds and drains the + serializers through ADMAC channels. + +allOf: + - $ref: dai-common.yaml# + +properties: + compatible: + const: apple,t8152-aop-audio + + dmas: + items: + - description: Headset jack output + - description: Headset microphone input + - description: Internal speaker output + - description: Speaker sense input + + dma-names: + items: + - const: cout + - const: cin + - const: spkr + - const: sense + + power-domains: + items: + - description: Serializer 0 + - description: Serializer 1 + - description: Serializer 2 + - description: Serializer 3 + - description: Serializer 4 + - description: Serializer 5 + + power-domain-names: + items: + - const: mca0 + - const: mca1 + - const: mca2 + - const: mca3 + - const: mca4 + - const: mca5 + + "#sound-dai-cells": + const: 1 + description: + AOP audio DAI identifier. 0 is the headset back-end, 3 the speaker + back-end. + +required: + - compatible + - dmas + - dma-names + - power-domains + - power-domain-names + - "#sound-dai-cells" + +unevaluatedProperties: false + +examples: + - | + audio { + compatible = "apple,t8152-aop-audio"; + dmas = <&admac 10>, <&admac 11>, <&admac 2>, <&admac 3>; + dma-names = "cout", "cin", "spkr", "sense"; + power-domains = <&ps_mca0>, <&ps_mca1>, <&ps_mca2>, + <&ps_mca3>, <&ps_mca4>, <&ps_mca5>; + power-domain-names = "mca0", "mca1", "mca2", "mca3", "mca4", "mca5"; + #sound-dai-cells = <1>; + }; From 3fd4c1beb0192540dcdb4439c99ef69be930200c Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:44:54 +0100 Subject: [PATCH 02/55] dt-bindings: sound: apple,macaudio: Add J873 Add the Mac mini M6 (J873) sound card. Signed-off-by: Thomas Glanzmann --- 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 e593b7b6d68c4..4ce5f8f2f0a8b 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -27,6 +27,7 @@ properties: - apple,j700-macaudio - apple,j713-macaudio - apple,j713-macaudio-jack + - apple,j873-macaudio - const: apple,macaudio "#address-cells": From 8dd4d517119dd14590ab7dd61e575144ef9f08c9 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:31 +0100 Subject: [PATCH 03/55] soc: apple: aop: support T8152 The J873 (Mac mini M6) AOP runs the same firmware generation as T8140 (EPIC v4, iBoot-loaded, started by the AP), but the T8140 "setup port" is not reachable from the AP: mailbox 1 of the AOP's mailbox bank and the ADT aop-exclave-mailbox both bus-error. The firmware boots without it: RTKit protocol 12, then its "audio" EPIC service. Add apple,t8152-aop with the T8140 configuration minus the setup port, starting every advertised endpoint. Signed-off-by: Thomas Glanzmann --- drivers/soc/apple/aop.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index 20e78e2b542cb..73df7cf27deaa 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -2665,6 +2665,20 @@ const HW_CFG_T8140: AopHwConfig = AopHwConfig { bootargs_word_access: false, halt_on_failed_start: false, }; +/// T8152: like T8140, but the setup port (mailbox 1 of the AOP bank, and the +/// ADT aop-exclave-mailbox) is not reachable from the AP: both bus-error. +const HW_CFG_T8152: AopHwConfig = AopHwConfig { + ec0p: 0, + aopt: 0, + alig: 0, + patch_bootargs: false, + epic_v4: true, + setup_port: false, + quiesce_on_unbind: true, + required_endpoints: &[], + fw_regions: false, + halt_on_failed_start: false, +}; kernel::of_device_table!( OF_TABLE, @@ -2677,6 +2691,7 @@ kernel::of_device_table!( (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), + (of::DeviceId::new(c_str!("apple,t8152-aop")), &HW_CFG_T8152), ] ); From 53a18f3f84a740088484f9670f5d3d4a8201b464 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:16:38 +0100 Subject: [PATCH 04/55] dt-bindings: sound: ti,tas27xx: add ti,bclk-ratio The Mac mini (M6, J873) speaker runs a 250-BCLK TDM frame, which the SN012776's ratio auto-detection rejects. Describe the property that fixes the ratio instead. Signed-off-by: Thomas Glanzmann --- Documentation/devicetree/bindings/sound/ti,tas27xx.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml b/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml index e9a7e87325448..2a02a98ae7ce9 100644 --- a/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml +++ b/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml @@ -122,6 +122,14 @@ properties: $ref: /schemas/types.yaml#/definitions/uint32 description: TDM TX voltage sense time slot. + ti,bclk-ratio: + $ref: /schemas/types.yaml#/definitions/uint32 + enum: [250] + description: + Fixed BCLK/FS ratio of the TDM frame, for a frame the amplifier's ratio + auto-detection does not accept (250 BCLKs on the Mac mini (M6, J873) + speaker). + '#sound-dai-cells': # The codec has a single DAI, the #sound-dai-cells=<1>; case is left in for backward # compatibility but is deprecated. From 9415ba9f8d608f35770436be3013fd82a3c0266d Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:31 +0100 Subject: [PATCH 05/55] ASoC: tas2764: support a fixed BCLK/FS ratio The J873 (Mac mini M6) AOP runs the speaker frame at 250 BCLKs of 12 MHz. The SN012776 cannot auto-detect that ratio and mutes with "fault: bad TDM clock". With TAS2764_CLK_CFG (register 0x60) set to 0x30 and TDM_CFG0 bit 6 set it accepts the frame and plays. Do that when the new ti,bclk-ratio property names the ratio; only this one ratio is supported. Source of the value: the register write was first seen in the macOS SN012776 driver; it is a single register value, and its effect (no fault, correct playback) was verified on J873 hardware. No table or code was taken from macOS. The field is initialised in probe, so non-OF matches keep auto-detection. Signed-off-by: Thomas Glanzmann --- sound/soc/codecs/tas2764.c | 41 ++++++++++++++++++++++++++++++++++++++ sound/soc/codecs/tas2764.h | 8 ++++++++ 2 files changed, 49 insertions(+) diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index c7886269a2dd4..3cec2ba9da430 100644 --- a/sound/soc/codecs/tas2764.c +++ b/sound/soc/codecs/tas2764.c @@ -66,6 +66,8 @@ struct tas2764_priv { int v_sense_slot; int i_sense_slot; + /* TAS2764_CLK_CFG value for a fixed BCLK/FS ratio, or -1 to auto-detect */ + int bclk_ratio_code; bool dac_powered; bool unmuted; @@ -1155,6 +1157,19 @@ static int tas2764_codec_probe(struct snd_soc_component *component) break; } + if (tas2764->bclk_ratio_code >= 0) { + ret = snd_soc_component_write(component, TAS2764_CLK_CFG, + tas2764->bclk_ratio_code); + if (ret < 0) + return ret; + + ret = snd_soc_component_update_bits(component, TAS2764_TDM_CFG0, + TAS2764_TDM_CFG0_FIXED_RATIO, + TAS2764_TDM_CFG0_FIXED_RATIO); + if (ret < 0) + return ret; + } + /* * 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 @@ -1407,9 +1422,24 @@ static int tas2764_parse_tx_zd_config(struct device *dev, struct tas2764_priv *t return 0; } +/* + * BCLK/FS ratios and the TAS2764_CLK_CFG value that makes the amplifier + * accept them. Only the J873 speaker frame (250 BCLKs, which the ratio + * auto-detection rejects with "bad TDM clock") is verified on hardware; + * other ratios are left to the auto-detection. + */ +static const struct { + u16 ratio; + u8 code; +} tas2764_bclk_ratio_codes[] = { + { 250, 0x30 }, +}; + static int tas2764_parse_dt(struct device *dev, struct tas2764_priv *tas2764) { + u32 ratio; int ret = 0; + int i; /* * The SDZ supply is optional: most boards drive SDZ from a GPIO @@ -1467,6 +1497,15 @@ static int tas2764_parse_dt(struct device *dev, struct tas2764_priv *tas2764) if (ret) return ret; + if (!fwnode_property_read_u32(dev->fwnode, "ti,bclk-ratio", &ratio)) { + for (i = 0; i < ARRAY_SIZE(tas2764_bclk_ratio_codes); i++) + if (tas2764_bclk_ratio_codes[i].ratio == ratio) + tas2764->bclk_ratio_code = tas2764_bclk_ratio_codes[i].code; + if (tas2764->bclk_ratio_code < 0) + return dev_err_probe(dev, -EINVAL, + "unsupported ti,bclk-ratio %u\n", ratio); + } + return tas2764_group_join(dev, tas2764); } @@ -1488,6 +1527,8 @@ static int tas2764_i2c_probe(struct i2c_client *client) result = devm_mutex_init(&client->dev, &tas2764->fault_lock); if (result) return result; + /* Auto-detect the ratio unless the firmware node sets one. */ + tas2764->bclk_ratio_code = -1; i2c_set_clientdata(client, tas2764); dev_set_drvdata(&client->dev, tas2764); diff --git a/sound/soc/codecs/tas2764.h b/sound/soc/codecs/tas2764.h index e6c6b064d6cc2..7f72f99734512 100644 --- a/sound/soc/codecs/tas2764.h +++ b/sound/soc/codecs/tas2764.h @@ -58,6 +58,12 @@ #define TAS2764_TDM_CFG0_44_1_48KHZ BIT(3) #define TAS2764_TDM_CFG0_88_2_96KHZ (BIT(3) | BIT(1)) #define TAS2764_TDM_CFG0_FRAME_START BIT(0) +/* + * Set by the macOS SN012776 driver together with an explicit BCLK/FS + * ratio in TAS2764_CLK_CFG; without it the ratio is auto-detected, which + * fails ("bad TDM clock") for frames such as 250 BCLKs. + */ +#define TAS2764_TDM_CFG0_FIXED_RATIO BIT(6) /* TDM Configuration Reg1 */ #define TAS2764_TDM_CFG1 TAS2764_REG(0X0, 0x09) @@ -123,6 +129,8 @@ /* Clock/IRQ Settings */ #define TAS2764_INT_CLK_CFG TAS2764_REG(0x0, 0x5c) +/* BCLK/FS ratio code, written by macOS for the J873 speaker frame */ +#define TAS2764_CLK_CFG TAS2764_REG(0x0, 0x60) #define TAS2764_INT_CLK_CFG_IRQZ_CLR BIT(2) #define TAS2764_BOP_CFG0 TAS2764_REG(0X0, 0x1d) From 4579f2e7936b7f504e754a6c2881ef0f1f439041 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 1 Nov 2026 21:36:59 +0100 Subject: [PATCH 06/55] ASoC: cs42l84: Route SDOUT1 from the ASP input The SDOUT1 mux offers the ASP receive channels as a source, but no DAPM route connected them, so selecting "ASP RX CH1" or "ASP RX CH2" left the capture path without an input. The J873 and J813 AOP audio services use it to loop the playback stream back. Signed-off-by: Thomas Glanzmann --- sound/soc/codecs/cs42l84.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sound/soc/codecs/cs42l84.c b/sound/soc/codecs/cs42l84.c index 022420be6bfb5..07a47433ef4d9 100644 --- a/sound/soc/codecs/cs42l84.c +++ b/sound/soc/codecs/cs42l84.c @@ -279,6 +279,8 @@ static const struct snd_soc_dapm_route cs42l84_audio_map[] = { {"ADC", NULL, "HS"}, {"SDOUT1 Select", "ADC", "ADC"}, + {"SDOUT1 Select", "ASP RX CH1", "SDIN1"}, + {"SDOUT1 Select", "ASP RX CH2", "SDIN2"}, {"SDOUT1", NULL, "SDOUT1 Select"}, {"Capture", NULL, "SDOUT1"}, From 7981f33f77bd186b4b68fb1e22e74eb3aaa4594c Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:31 +0100 Subject: [PATCH 07/55] ASoC: apple: add the T8152 AOP audio driver On T8152 (J873, Mac mini M6) the AOP firmware owns the audio fabric, as on T8140. Its "audio" EPIC service offers cout/cin (the CS42L84 jack, base-ns ADMAC TX5/RX5, MCA4) and spkr (the SN012776 speaker, TX1, MCA0). Each service is an ASoC back-end DAI: startup attaches it, requests pw0 (which raises the audio fabric) and powers the MCA leaves, all of which are required. pwrd starts the serializer clocks at prepare, before ASoC unmutes the codecs: the jack codec locks its PLL to the bit clock, and the speaker amplifier reports "bad TDM clock" whenever it runs without one. The jack restarts its clocks on every prepare so that a stream restarted after an underrun begins on a frame boundary; the mono speaker (one slot per frame) keeps them running. The front-ends are dmaengine PCMs on the base-ns channels: the jack in stereo, the speaker in mono (its serializer runs one 32-bit slot; stereo frames play at half pitch). The firmware's serializer configuration (property 701) is byte-identical to the one macOS publishes, so the driver leaves it alone. Signed-off-by: Thomas Glanzmann --- sound/soc/apple/Kconfig | 11 + sound/soc/apple/Makefile | 3 + sound/soc/apple/t8152_aop_audio.rs | 995 +++++++++++++++++++++++++++++ 3 files changed, 1009 insertions(+) create mode 100644 sound/soc/apple/t8152_aop_audio.rs diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index 92ffcb6073797..74ea1fe4de8e8 100644 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -14,6 +14,17 @@ config SND_SOC_APPLE_AOP_AUDIO machines, and on T8140 the back-end DAIs for the headphone jack and the internal speaker, which the macaudio driver links into its card. +config SND_SOC_APPLE_T8152_AOP_AUDIO + tristate "Apple T8152 AOP audio driver" + depends on ARCH_APPLE || COMPILE_TEST + depends on RUST + select APPLE_AOP + select SND_DMAENGINE_PCM + help + This option enables a driver for the audio paths owned by the AOP + co-processor on Apple T8152 SoCs (Mac mini M6): the headphone jack + and the internal speaker. + config SND_SOC_APPLE_MCA tristate "Apple Silicon MCA driver" depends on ARCH_APPLE || COMPILE_TEST diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile index 52d4abf30916b..71885319572cf 100644 --- a/sound/soc/apple/Makefile +++ b/sound/soc/apple/Makefile @@ -3,6 +3,9 @@ snd-soc-aop-y := aop_audio.o obj-$(CONFIG_SND_SOC_APPLE_AOP_AUDIO) += snd-soc-aop.o +snd-soc-t8152-aop-audio-y := t8152_aop_audio.o +obj-$(CONFIG_SND_SOC_APPLE_T8152_AOP_AUDIO) += snd-soc-t8152-aop-audio.o + snd-soc-apple-mca-y := mca.o obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs new file mode 100644 index 0000000000000..31f562e7fc64d --- /dev/null +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -0,0 +1,995 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Apple T8152 (J873, Mac mini M6) AOP audio +//! +//! The AOP firmware owns the audio fabric, as on T8140. The ADT +//! (/arm-io/iop-audio-controller) names its services; all of them run on the +//! base-ns ADMAC: +//! +//! * `cout`/`cin`: the CS42L84 headphone jack, base-ns TX5/RX5 (Linux +//! channels 10 and 11). +//! * `spkr`: the SN012776 speaker amplifier, base-ns TX1 (channel 2), its +//! sense data on RX1. +//! +//! Each service is an ASoC back-end DAI: startup attaches it and requests +//! `pw0 ` (which also brings up the audio fabric), prepare requests `pwrd` +//! (the firmware then runs the serializer and its clocks) and shutdown goes +//! back to `pw0 `. The serializer (MCA) leaves are the AP's to power. The +//! front-end PCMs are plain dmaengine PCMs on the base-ns channels; macaudio +//! links them to the codecs. +//! +//! Copyright (C) The Asahi Linux Contributors +//! Copyright (C) 2026 The Aurora Silicon Contributors + +use core::{mem, ptr, slice}; + +use kernel::{ + bindings, c_str, + device, + device::Core, + error::from_err_ptr, + module_platform_driver, of, platform, + prelude::*, + soc::apple::aop::{from_fourcc, EPICService, AOP}, + sync::{ + aref::ARef, + atomic::{Atomic, AtomicFlag, Relaxed}, + Arc, + }, + types::Opaque, +}; + +const EPIC_SUBTYPE_WRAPPED_CALL: u16 = 0x20; +const CALLTYPE_AUDIO_ATTACH_DEVICE: u32 = 0xc3000002; +const CALLTYPE_AUDIO_GET_PROP: u32 = 0xc3000004; +const CALLTYPE_AUDIO_SET_PROP: u32 = 0xc3000005; +const PROP_POWER_STATE: u32 = 200; +const PROP_POWER_REQUEST: u32 = 202; +const POWER_STATE_PW0: u32 = from_fourcc(b"pw0 "); +const POWER_STATE_PWRD: u32 = from_fourcc(b"pwrd"); +/// kIOReturnBusy: the device is already attached (a module reload). +const AUDIO_RET_BUSY: u32 = 0xe00002d5; +const AOP_TIMEBASE_HZ: u64 = 24_000_000; + +/// Leaf power-up attempts while the firmware raises the fabric after `pw0 `. +const PD_GET_ATTEMPTS: u32 = 60; +const PD_GET_RETRY_MS: i64 = 10; +/// The MCA serializer leaves (audio_mca0_m .. audio_mca5_m). +const MCA_COUNT: usize = 6; +const MCA_NAMES: [&CStr; MCA_COUNT] = [c"mca0", c"mca1", c"mca2", c"mca3", c"mca4", c"mca5"]; + +fn aop_timestamp() -> u64 { + // SAFETY: both helpers read the architected timer and take no arguments. + let (counter, rate) = + unsafe { (bindings::arch_timer_counter(), bindings::arch_timer_rate()) }; + let rate = u64::from(rate); + (counter / rate) * AOP_TIMEBASE_HZ + (counter % rate) * AOP_TIMEBASE_HZ / rate +} + +fn le_u32(buf: &[u8], off: usize) -> Result { + let b = buf.get(off..off + 4).ok_or(EINVAL)?; + Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) +} + +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct AudioAttachDevice { + _zero0: u32, + unk0: u32, + calltype: u32, + _zero1: [u8; 16], + cookie: u32, + len: u64, + dev_id: u32, + _zero2: u32, +} + +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct AudioGetDeviceProp { + _zero0: u32, + unk0: u32, + calltype: u32, + _zero1: [u8; 16], + _pad0: u32, + len: u64, + dev_id: u32, + modifier: u32, + unk6: u32, +} + +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct AudioSetDeviceProp { + _zero0: u32, + unk0: u32, + calltype: u32, + _zero1: [u8; 16], + _pad0: u32, + len: u64, + dev_id: u32, + modifier: u32, + len2: u32, + data: T, +} + +/// Property 202: the device, a per-device sequence, the request time in the +/// firmware's 24 MHz timebase and the target state. +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct PowerRequest { + dev_id: u32, + sequence: u32, + timestamp: u64, + _zero0: u32, + state: u32, + param: u32, + _zero1: [u8; 20], +} + +/// A named power domain of this device, runtime-powered around the streams +/// that need it. +struct PowerDomain { + dev: ptr::NonNull, +} + +// SAFETY: the virtual domain device belongs to the PM core, which serializes +// its runtime-PM state; this handle only passes the pointer to those calls. +unsafe impl Send for PowerDomain {} +// SAFETY: as for `Send`. +unsafe impl Sync for PowerDomain {} + +impl PowerDomain { + fn attach(dev: &device::Device, name: &CStr) -> Result> { + // SAFETY: `dev` is live for the call and `name` is NUL-terminated. + let pd = from_err_ptr(unsafe { + bindings::dev_pm_domain_attach_by_name(dev.as_raw(), name.as_char_ptr()) + })?; + Ok(ptr::NonNull::new(pd).map(|dev| PowerDomain { dev })) + } + + /// Takes a runtime-PM reference, retrying while the parent fabric that + /// the firmware brings up after `pw0 ` is still off. + fn get(&self) -> Result<()> { + let mut rc = 0; + for _ in 0..PD_GET_ATTEMPTS { + // SAFETY: the domain device stays attached for the lifetime of `self`. + rc = unsafe { bindings::pm_runtime_get_sync(self.dev.as_ptr()) }; + if rc >= 0 { + return Ok(()); + } + // SAFETY: as above; undo the usage count and the error state. + unsafe { + bindings::pm_runtime_put_noidle(self.dev.as_ptr()); + bindings::__pm_runtime_set_status( + self.dev.as_ptr(), + bindings::rpm_status_RPM_SUSPENDED as u32, + ); + } + kernel::time::delay::fsleep(kernel::time::Delta::from_millis(PD_GET_RETRY_MS)); + } + Err(Error::from_errno(rc)) + } + + fn put(&self) { + // SAFETY: the domain device stays attached for the lifetime of `self`. + unsafe { bindings::pm_runtime_put_sync(self.dev.as_ptr()) }; + } +} + +impl Drop for PowerDomain { + fn drop(&mut self) { + // SAFETY: attached in `attach`; every `get` has been balanced by then. + unsafe { bindings::dev_pm_domain_detach(self.dev.as_ptr(), true) }; + } +} + +/// One firmware audio service driven as an ASoC back-end. +struct Service { + dev_id: u32, + name: &'static str, + sequence: Atomic, + /// In `pwrd`. + running: AtomicFlag, + /// Stop and restart the serializer on every prepare, so that a stream + /// restarted after an underrun begins on a frame boundary. The mono + /// speaker has one slot per frame and keeps its clocks running instead: + /// its amplifier stays unmuted between prepares and reports "bad TDM + /// clock" whenever the bit clock stops under it. + restart_on_prepare: bool, +} + +impl Service { + const fn new(dev_id: u32, name: &'static str, restart_on_prepare: bool) -> Service { + Service { + dev_id, + name, + sequence: Atomic::new(0), + running: AtomicFlag::new(false), + restart_on_prepare, + } + } +} + +#[pin_data] +struct T8152AopAudio { + dev: ARef, + adata: Arc, + service: EPICService, + cout: Service, + cin: Service, + spkr: Service, + mca: KVec, +} + +impl T8152AopAudio { + fn call_ret(&self, data: &T, ret_len: usize) -> Result<(u32, KVec)> { + // SAFETY: T is a plain packed firmware record. + let msg = unsafe { + slice::from_raw_parts(ptr::from_ref(data).cast::(), mem::size_of::()) + }; + self.adata + .epic_call_ret(&self.service, EPIC_SUBTYPE_WRAPPED_CALL, msg, ret_len) + } + + fn call(&self, data: &T) -> Result { + // SAFETY: T is a plain packed firmware record. + let msg = unsafe { + slice::from_raw_parts(ptr::from_ref(data).cast::(), mem::size_of::()) + }; + self.adata + .epic_call(&self.service, EPIC_SUBTYPE_WRAPPED_CALL, msg) + } + + /// The firmware keeps attachments for its lifetime, so a module reload + /// sees kIOReturnBusy. + fn attach(&self, svc: &Service) -> Result<()> { + let ret = self.call(&AudioAttachDevice { + unk0: 0xffffffff, + calltype: CALLTYPE_AUDIO_ATTACH_DEVICE, + len: 0x2c, + dev_id: svc.dev_id, + ..Default::default() + })?; + match ret { + 0 | AUDIO_RET_BUSY => Ok(()), + _ => { + dev_err!(self.dev, "unable to attach {}: {:#x}\n", svc.name, ret); + Err(EIO) + } + } + } + + fn power_state(&self, svc: &Service) -> Result { + let (ret, data) = self.call_ret( + &AudioGetDeviceProp { + unk0: 0xffffffff, + calltype: CALLTYPE_AUDIO_GET_PROP, + len: 0x30, + dev_id: svc.dev_id, + modifier: PROP_POWER_STATE, + unk6: 1, + ..Default::default() + }, + 8, + )?; + if ret != 0 { + return Err(EIO); + } + le_u32(&data, 4) + } + + fn set_power(&self, svc: &Service, state: u32) -> Result<()> { + let req = PowerRequest { + dev_id: svc.dev_id, + sequence: svc.sequence.fetch_add(1, Relaxed) + 1, + timestamp: aop_timestamp(), + state, + ..Default::default() + }; + let ret = self.call(&AudioSetDeviceProp { + unk0: 0xffffffff, + calltype: CALLTYPE_AUDIO_SET_PROP, + len: mem::size_of::() as u64 + 0x30, + dev_id: svc.dev_id, + modifier: PROP_POWER_REQUEST, + len2: mem::size_of::() as u32, + data: req, + ..Default::default() + })?; + if ret != 0 { + dev_err!( + self.dev, + "{}: power request {:#x} rejected: {:#x}\n", + svc.name, + state, + ret + ); + return Err(EIO); + } + let now = self.power_state(svc)?; + if now != state { + dev_err!( + self.dev, + "{}: power state {:#x} after requesting {:#x}\n", + svc.name, + now, + state + ); + return Err(EIO); + } + Ok(()) + } + + fn mca_up(&self) -> Result<()> { + for (i, pd) in self.mca.iter().enumerate() { + if let Err(e) = pd.get() { + dev_err!(self.dev, "MCA leaf {} did not power up: {:?}\n", i, e); + for pd in self.mca[..i].iter().rev() { + pd.put(); + } + return Err(e); + } + } + Ok(()) + } + + fn mca_down(&self) { + for pd in self.mca.iter().rev() { + pd.put(); + } + } + + /// Back-end startup: attach, `pw0 ` (which raises the fabric), then the + /// serializer leaves. + fn startup(&self, svc: &Service) -> Result<()> { + self.attach(svc)?; + self.set_power(svc, POWER_STATE_PW0)?; + if let Err(e) = self.mca_up() { + let _ = self.set_power(svc, POWER_STATE_PW0); + return Err(e); + } + svc.running.store(false, Relaxed); + Ok(()) + } + + /// `pwrd`: the firmware starts the serializer clocks and the stream. + fn run(&self, svc: &Service) -> Result<()> { + if svc.running.load(Relaxed) { + return Ok(()); + } + self.set_power(svc, POWER_STATE_PWRD)?; + svc.running.store(true, Relaxed); + Ok(()) + } + + /// The clocks run from prepare on: the codecs see a bit clock as soon + /// as ASoC powers and unmutes them. A PCM underrun stops the DMA + /// without closing the back-end; a service that restarts on prepare + /// retires its run so that the next start begins afresh. + fn prepare(&self, svc: &Service) -> Result<()> { + if svc.restart_on_prepare && svc.running.load(Relaxed) { + self.set_power(svc, POWER_STATE_PW0)?; + svc.running.store(false, Relaxed); + } + self.run(svc) + } + + fn shutdown(&self, svc: &Service) { + if svc.running.load(Relaxed) { + if let Err(e) = self.set_power(svc, POWER_STATE_PW0) { + dev_err!(self.dev, "{}: pw0 failed: {:?}\n", svc.name, e); + } + svc.running.store(false, Relaxed); + } + self.mca_down(); + } +} + +// ---- the ASoC component ---- + +/// Front-end index of a runtime by CPU DAI id; back-ends have none. +const FE_DAI_ID_BASE: u32 = 1; +const FE_COUNT: usize = 3; +const BE_DAI_ID_COUT: u32 = 10; +const BE_DAI_ID_SPKR: u32 = 11; +/// DMA channel names (dma-names on the audio node) per front-end, [playback, +/// capture]: the jack, the speaker and the speaker's sense data. +const FE_DMA_NAMES: [[Option<&CStr>; 2]; FE_COUNT] = [ + [Some(c"cout"), Some(c"cin")], + [Some(c"spkr"), None], + [None, Some(c"sense")], +]; + +const PCM_BUFFER_MAX: usize = 1024 * 1024; +const PCM_PERIOD_BYTES_MIN: usize = 256; +const PCM_PERIOD_BYTES_MAX: usize = 0x10000; + +/// The AOP-run serializers take 24-bit samples in 32-bit words. +const FORMATS: u64 = bindings::BINDINGS_SNDRV_PCM_FMTBIT_S24_LE; + +/// # Safety +/// +/// `substream` must be live and belong to one of this component's links. +unsafe fn substream_rtd( + substream: *mut bindings::snd_pcm_substream, +) -> *mut bindings::snd_soc_pcm_runtime { + // SAFETY: by the caller's contract. + unsafe { (*substream).private_data.cast() } +} + +/// # Safety +/// +/// `rtd` must be a live ASoC runtime. +unsafe fn rtd_is_be(rtd: *mut bindings::snd_soc_pcm_runtime) -> bool { + // SAFETY: by the caller's contract. + unsafe { (*(*rtd).dai_link).no_pcm() != 0 } +} + +/// # Safety +/// +/// `rtd` must be a live ASoC runtime of one of this component's links. +unsafe fn fe_index(rtd: *mut bindings::snd_soc_pcm_runtime) -> Option { + // SAFETY: by the caller's contract. + let id = unsafe { (**(*rtd).dais).id } as u32; + if (FE_DAI_ID_BASE..FE_DAI_ID_BASE + FE_COUNT as u32).contains(&id) { + Some((id - FE_DAI_ID_BASE) as usize) + } else { + None + } +} + +/// # Safety +/// +/// `substream` must be live. +unsafe fn is_playback(substream: *mut bindings::snd_pcm_substream) -> bool { + // SAFETY: by the caller's contract. + unsafe { (*substream).stream == bindings::SNDRV_PCM_STREAM_PLAYBACK as i32 } +} + +/// The context behind the component's descriptor (its first field). +#[repr(C)] +struct AsocContext { + component: bindings::snd_soc_component_driver, + data: Arc, + dais: KBox>, + _ops: KBox, +} + +const _: () = assert!(mem::offset_of!(AsocContext, component) == 0); + +/// # Safety +/// +/// `component` must be this driver's live registered component. +unsafe fn component_data<'a>(component: *mut bindings::snd_soc_component) -> &'a T8152AopAudio { + // SAFETY: by the caller's contract; C keeps the descriptor pointer we + // registered, which is the start of the `AsocContext`. + unsafe { &(*(*component).driver.cast::()).data } +} + +/// # Safety +/// +/// `dai` must be a live DAI of this driver's registered component. +unsafe fn dai_data<'a>(dai: *mut bindings::snd_soc_dai) -> &'a T8152AopAudio { + // SAFETY: by the caller's contract. + unsafe { component_data((*dai).component) } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_open( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { component_data(component) }; + // SAFETY: as above. + let rtd = unsafe { substream_rtd(substream) }; + // SAFETY: as above. + if unsafe { rtd_is_be(rtd) } { + return 0; + } + // SAFETY: as above. + let Some(fe) = (unsafe { fe_index(rtd) }) else { + return EINVAL.to_errno(); + }; + // SAFETY: as above. + let playback = unsafe { is_playback(substream) }; + let Some(name) = FE_DMA_NAMES[fe][usize::from(!playback)] else { + return ENODEV.to_errno(); + }; + // SAFETY: the device is live and the name is NUL-terminated. + let chan = match from_err_ptr(unsafe { + bindings::dma_request_chan(data.dev.as_raw(), name.as_char_ptr()) + }) { + Ok(chan) => chan, + Err(e) => { + dev_err!(data.dev, "DMA channel {:?} unavailable: {:?}\n", name, e); + return e.to_errno(); + } + }; + let mut hw = bindings::snd_pcm_hardware { + info: bindings::SNDRV_PCM_INFO_MMAP + | bindings::SNDRV_PCM_INFO_MMAP_VALID + | bindings::SNDRV_PCM_INFO_INTERLEAVED, + buffer_bytes_max: PCM_BUFFER_MAX, + period_bytes_min: PCM_PERIOD_BYTES_MIN, + period_bytes_max: PCM_PERIOD_BYTES_MAX, + periods_min: 2, + periods_max: (PCM_BUFFER_MAX / PCM_PERIOD_BYTES_MIN) as u32, + fifo_size: 16, + ..Default::default() + }; + // SAFETY: the substream and the channel are live. + let ret = unsafe { + let mut ret = 0; + if (*substream).dma_buffer.dev.type_ == bindings::SNDRV_DMA_TYPE_UNKNOWN as i32 { + ret = bindings::snd_pcm_set_managed_buffer( + substream, + bindings::SNDRV_DMA_TYPE_DEV as i32, + (*(*chan).device).dev, + 0, + 0, + ); + } + if ret == 0 { + let mut dma_data = bindings::snd_dmaengine_dai_dma_data::default(); + ret = bindings::snd_dmaengine_pcm_refine_runtime_hwparams( + substream, + &mut dma_data, + &mut hw, + chan, + ); + } + if ret == 0 { + hw.formats &= FORMATS; + (*(*substream).runtime).hw = hw; + ret = bindings::snd_dmaengine_pcm_open(substream, chan); + } + ret + }; + if ret < 0 { + // SAFETY: the open failed, so the channel is still ours. + unsafe { bindings::dma_release_channel(chan) }; + } + ret +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_close( + _component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return 0; + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_hw_params( + _component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, + params: *mut bindings::snd_pcm_hw_params, +) -> i32 { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return 0; + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + let chan = unsafe { bindings::snd_dmaengine_pcm_get_chan(substream) }; + let mut cfg = bindings::dma_slave_config::default(); + // SAFETY: the substream and the parameters are live for the op. + let rc = unsafe { bindings::snd_hwparams_to_dma_slave_config(substream, params, &mut cfg) }; + if rc < 0 { + return rc; + } + // SAFETY: ASoC calls hw_params with live parameters. + let channels = unsafe { + (*params).intervals[(bindings::SNDRV_PCM_HW_PARAM_CHANNELS + - bindings::SNDRV_PCM_HW_PARAM_FIRST_INTERVAL) as usize] + .min + }; + // One frame per burst, up to four words. + let window = core::cmp::min(channels, 4); + // SAFETY: the substream is live. + if unsafe { is_playback(substream) } { + cfg.dst_port_window_size = window; + } else { + cfg.src_port_window_size = window; + } + // SAFETY: `chan` is the live channel behind the substream. + unsafe { + match (*(*chan).device).device_config { + Some(f) => f(chan, &mut cfg), + None => ENOSYS.to_errno(), + } + } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_trigger( + _component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, + cmd: i32, +) -> i32 { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return 0; + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_trigger(substream, cmd) } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_pointer( + _component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> bindings::snd_pcm_uframes_t { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return (-i64::from(bindings::ENOTSUPP)) as bindings::snd_pcm_uframes_t; + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_pointer(substream) } +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component. +unsafe extern "C" fn dai_accept_fmt(_dai: *mut bindings::snd_soc_dai, _fmt: u32) -> i32 { + // The firmware runs the serializers; format, slots and clocks are its. + 0 +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component. +unsafe extern "C" fn dai_accept_sysclk( + _dai: *mut bindings::snd_soc_dai, + _id: i32, + _freq: u32, + _dir: i32, +) -> i32 { + 0 +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component. +unsafe extern "C" fn dai_accept_bclk_ratio(_dai: *mut bindings::snd_soc_dai, _ratio: u32) -> i32 { + 0 +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component. +unsafe extern "C" fn dai_accept_tdm_slot( + _dai: *mut bindings::snd_soc_dai, + _tx_mask: u32, + _rx_mask: u32, + _slots: i32, + _slot_width: i32, +) -> i32 { + 0 +} + +/// The back-end's service for a stream; `None` for the speaker's sense +/// capture, which runs with the speaker's playback. +/// +/// # Safety +/// +/// `dai` must be a live DAI of this component, `substream` live. +unsafe fn be_service<'a>( + dai: *mut bindings::snd_soc_dai, + substream: *mut bindings::snd_pcm_substream, +) -> Option<(&'a T8152AopAudio, &'a Service)> { + // SAFETY: by the caller's contract. + let (data, id, playback) = unsafe { (dai_data(dai), (*dai).id as u32, is_playback(substream)) }; + let svc = match (id, playback) { + (BE_DAI_ID_COUT, true) => &data.cout, + (BE_DAI_ID_COUT, false) => &data.cin, + (BE_DAI_ID_SPKR, true) => &data.spkr, + _ => return None, + }; + Some((data, svc)) +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component and a live substream. +unsafe extern "C" fn be_startup( + substream: *mut bindings::snd_pcm_substream, + dai: *mut bindings::snd_soc_dai, +) -> i32 { + // SAFETY: by the caller's contract. + let Some((data, svc)) = (unsafe { be_service(dai, substream) }) else { + return 0; + }; + match data.startup(svc) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component and a live substream. +unsafe extern "C" fn be_prepare( + substream: *mut bindings::snd_pcm_substream, + dai: *mut bindings::snd_soc_dai, +) -> i32 { + // SAFETY: by the caller's contract. + let Some((data, svc)) = (unsafe { be_service(dai, substream) }) else { + return 0; + }; + match data.prepare(svc) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component and a live substream. +unsafe extern "C" fn be_shutdown( + substream: *mut bindings::snd_pcm_substream, + dai: *mut bindings::snd_soc_dai, +) { + // SAFETY: by the caller's contract. + if let Some((data, svc)) = unsafe { be_service(dai, substream) } { + data.shutdown(svc); + } +} + +struct DaiOps { + be: bindings::snd_soc_dai_ops, + fe: bindings::snd_soc_dai_ops, +} + +impl DaiOps { + fn new() -> Self { + let fe = bindings::snd_soc_dai_ops { + set_fmt: Some(dai_accept_fmt), + set_sysclk: Some(dai_accept_sysclk), + set_bclk_ratio: Some(dai_accept_bclk_ratio), + set_tdm_slot: Some(dai_accept_tdm_slot), + ..Default::default() + }; + DaiOps { + be: bindings::snd_soc_dai_ops { + startup: Some(be_startup), + prepare: Some(be_prepare), + shutdown: Some(be_shutdown), + ..fe + }, + fe, + } + } +} + +fn pcm_stream(name: &'static CStr, channels: u32) -> bindings::snd_soc_pcm_stream { + bindings::snd_soc_pcm_stream { + stream_name: name.as_char_ptr(), + formats: FORMATS, + rates: bindings::SNDRV_PCM_RATE_48000, + rate_min: 48000, + rate_max: 48000, + channels_min: channels, + channels_max: channels, + ..Default::default() + } +} + +/// The DAI table, indexed by `sound-dai = <&aop_audio N>` in the DTS: +/// 0 cout (jack back-end), 1 and 2 the playback front-ends, 3 spkr (speaker +/// back-end), 4 the sense front-end. +const DAI_COUNT: usize = 2 + FE_COUNT; + +fn dai_drivers(ops: &DaiOps) -> [bindings::snd_soc_dai_driver; DAI_COUNT] { + [ + bindings::snd_soc_dai_driver { + name: c"cout".as_char_ptr(), + id: BE_DAI_ID_COUT, + ops: &ops.be, + playback: pcm_stream(c"COUT TX", 2), + capture: pcm_stream(c"CIN RX", 1), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c"j873-pcm-0".as_char_ptr(), + id: FE_DAI_ID_BASE, + ops: &ops.fe, + playback: pcm_stream(c"PCM0 TX", 2), + capture: pcm_stream(c"PCM0 RX", 1), + ..Default::default() + }, + // The speaker: one 32-bit slot per frame (the firmware's spkr + // serializer runs a single channel). + bindings::snd_soc_dai_driver { + name: c"j873-pcm-1".as_char_ptr(), + id: FE_DAI_ID_BASE + 1, + ops: &ops.fe, + playback: pcm_stream(c"PCM1 TX", 1), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c"spkr".as_char_ptr(), + id: BE_DAI_ID_SPKR, + ops: &ops.be, + playback: pcm_stream(c"SPKR TX", 1), + capture: pcm_stream(c"SPKR RX", 2), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c"j873-pcm-2".as_char_ptr(), + id: FE_DAI_ID_BASE + 2, + ops: &ops.fe, + capture: pcm_stream(c"PCM2 RX", 2), + ..Default::default() + }, + ] +} + +impl AsocContext { + fn new(data: Arc) -> Result> { + let ops = KBox::new(DaiOps::new(), GFP_KERNEL)?; + let dais = KBox::new(Opaque::new(dai_drivers(&ops)), GFP_KERNEL)?; + let component = bindings::snd_soc_component_driver { + name: c"t8152-aop-audio".as_char_ptr(), + open: Some(asoc_pcm_open), + close: Some(asoc_pcm_close), + hw_params: Some(asoc_pcm_hw_params), + trigger: Some(asoc_pcm_trigger), + pointer: Some(asoc_pcm_pointer), + ..Default::default() + }; + Ok(KBox::new( + Self { + component, + data, + dais, + _ops: ops, + }, + GFP_KERNEL, + )?) + } +} + +struct T8152AopAudioDriver { + dev: ARef, + asoc: KBox, + registered: AtomicFlag, +} + +// SAFETY: the ASoC context is only touched by ASoC callbacks and by probe and +// remove, which the driver core serializes. +unsafe impl Send for T8152AopAudioDriver {} +// SAFETY: as for `Send`. +unsafe impl Sync for T8152AopAudioDriver {} + +impl T8152AopAudioDriver { + /// Unregisters the component. Must run from unbind: the component is + /// device-managed and freed with the device's resources after unbind. + fn unregister(&self) { + if !self.registered.xchg(false, Relaxed) { + return; + } + // SAFETY: registered in probe; the context outlives the unregister. + unsafe { + bindings::snd_soc_unregister_component_by_driver( + self.dev.as_raw(), + &self.asoc.component, + ) + }; + } +} + +kernel::of_device_table!( + OF_TABLE, + ::IdInfo, + [(of::DeviceId::new(c_str!("apple,t8152-aop-audio")), ())] +); + +impl platform::Driver for T8152AopAudioDriver { + type IdInfo = (); + type Data<'bound> = Self; + + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe<'bound>( + pdev: &'bound platform::Device>, + _info: Option<&'bound Self::IdInfo>, + ) -> impl PinInit, Error> + 'bound { + let dev = ARef::::from(pdev.as_ref()); + // SAFETY: this is a service device the AOP core registered. + let adata = unsafe { ::from_child(pdev.as_ref()) }?; + // SAFETY: the AOP core sets the child's platform data to its EPIC service. + let service = unsafe { (*dev.as_raw()).platform_data as *const EPICService }; + if service.is_null() { + return Err(ENODEV); + } + // SAFETY: checked above; it lives as long as the child. + let service = unsafe { *service }; + + // Every serializer leaf is required: the firmware's streams run on + // them, so the driver must not bind without one (or before its + // provider has probed). + let mut mca = KVec::new(); + for name in MCA_NAMES { + match PowerDomain::attach(&dev, name) { + Ok(Some(pd)) => mca.push(pd, GFP_KERNEL)?, + Ok(None) => { + dev_err!(dev, "power domain {:?} is missing\n", name); + return Err(ENODEV); + } + Err(e) => { + if e != EPROBE_DEFER { + dev_err!(dev, "power domain {:?}: {:?}\n", name, e); + } + return Err(e); + } + } + } + + let data_dev = dev.clone(); + let data = Arc::pin_init( + pin_init!(T8152AopAudio { + dev: data_dev, + adata, + service, + cout: Service::new(from_fourcc(b"cout"), "cout", true), + cin: Service::new(from_fourcc(b"cin "), "cin", true), + spkr: Service::new(from_fourcc(b"spkr"), "spkr", false), + mca, + }), + GFP_KERNEL, + )?; + for svc in [&data.cout, &data.cin, &data.spkr] { + data.attach(svc)?; + } + + let asoc = AsocContext::new(data)?; + // SAFETY: the descriptor and the DAI table are heap allocations owned + // by `asoc`, which the driver data keeps until after unregister. + let ret = unsafe { + bindings::snd_soc_register_component_d( + dev.as_raw(), + &asoc.component, + asoc.dais.get().cast(), + DAI_COUNT as i32, + ) + }; + if ret < 0 { + dev_err!(dev, "unable to register the ASoC component: {}\n", ret); + return Err(Error::from_errno(ret)); + } + Ok(Self { + dev, + asoc, + registered: AtomicFlag::new(true), + }) + } + + fn unbind(_dev: &platform::Device>, this: Pin<&Self>) { + this.unregister(); + } +} + +module_platform_driver! { + type: T8152AopAudioDriver, + name: "snd_soc_t8152_aop_audio", + description: "Apple T8152 AOP audio: headphone jack and speaker", + license: "Dual MIT/GPL", +} From 5aa269280d70ec7b21d523b3a0a77e6bad1c3f84 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:32 +0100 Subject: [PATCH 08/55] ASoC: apple: macaudio: add J873 The J873 (Mac mini M6) uses the T8152 AOP audio driver as its CPU component: the CS42L84 jack on "cout" (DSP_A | IB_NF, one-BCLK sync pulse) and one SN012776 on "spkr" (I2S, NB_IF: data sampled on the rising edge, as macOS programs the amp). Each AOP front-end owns the DMA channel of one serializer, so the speaker can only be fed from the secondary front-end; the new spk_secondary_only sets the speaker mux accordingly, and spk_dai_fmt gives the speaker link its own format. There is no qualified speaker protection for J873 yet (no verified sense path, thermal model or power budget), so the speaker fails closed: it is only enabled with please_blow_up_my_speakers. Opted in, it runs as macOS runs it: amp gain 15 (ADT speaker-config) and no volume lock (safe_vol 0), since there is no sense stream for a protection daemon. The jack is not affected. Signed-off-by: Thomas Glanzmann --- sound/soc/apple/macaudio.c | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index ba04b1eec6141..4743ba5db026b 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -136,6 +136,13 @@ struct macaudio_platform_cfg { * keep the first slots in codec order; the others carry silence. */ unsigned int speaker_fe_channels; + /* The speaker links' DAI format, if not dai_fmt */ + unsigned int spk_dai_fmt; + /* + * The speakers can only be fed from the secondary front-end: each + * front-end owns the DMA channel of one serializer (AOP-fed models). + */ + bool spk_secondary_only; }; static const char *volume_control_names[] = { @@ -1215,6 +1222,8 @@ static int macaudio_parse_of(struct macaudio_snd_data *ma) link_props->is_speakers = speakers; link_props->is_headphones = !speakers; + if (speakers && ma->cfg->spk_dai_fmt) + link->dai_fmt = ma->cfg->spk_dai_fmt; link_props->codecs = devm_kcalloc(dev, ncodecs_per_cpu, sizeof(struct ma_codec_idle), GFP_KERNEL); @@ -2035,6 +2044,9 @@ static int macaudio_fixup_controls(struct snd_soc_card *card) return ret; } + if (ma->cfg->spk_secondary_only) + CHECK(snd_soc_set_enum_kctl, "Speaker Playback Mux", "Secondary", 1); + /* * This needs some care to avoid matches against cs42l84's * "Jack HPF Corner Frequency". @@ -2353,6 +2365,39 @@ struct macaudio_platform_cfg macaudio_j700_cfg = { .primary_bclk_ratio = 125, }; +/* + * J873 (T8152, Mac mini M6): as on J700, the AOP firmware owns the audio + * fabric and the CPU component is the T8152 AOP audio driver. The jack is a + * CS42L84 on the AOP's "cout" back-end, the speaker one SN012776 on "spkr"; + * both serializers run on the firmware's clocks. The speaker frame is 250 + * BCLKs of 12 MHz (the amp is told so by its ti,bclk-ratio), with the data + * sampled on the rising edge, as macOS programs the amp. + */ +struct macaudio_platform_cfg macaudio_j873_cfg = { + /* + * No qualified speaker protection exists for J873 yet (no verified + * sense path, thermal model or power budget), so the speaker stays off + * unless please_blow_up_my_speakers is set; the jack is unaffected. + */ + .enable_speakers = false, + .amp = AMP_SN012776, + .speakers = SPKR_1W, + .stereo = false, + /* + * With the speaker opted in, as macOS runs it: ADT speaker-config + * amp_gain 15, and no volume lock (safe_vol 0), since there is no + * sense stream for a protection daemon to unlock it from. + */ + .amp_gain = 15, + .safe_vol = 0, + .fe_dai_names = { "j873-pcm-0", "j873-pcm-1", "j873-pcm-2" }, + /* jack: a one-BCLK sync pulse, data launched on the rising edge */ + .dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBC_CFC | SND_SOC_DAIFMT_IB_NF, + .spk_dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBC_CFC | + SND_SOC_DAIFMT_GATED | SND_SOC_DAIFMT_NB_IF, + .spk_secondary_only = true, +}; + struct macaudio_platform_cfg macaudio_j314_cfg = { true, AMP_SN012776, SPKR_2W1T, true, 0, -20, }; @@ -2539,6 +2584,8 @@ static const struct of_device_id macaudio_snd_device_id[] = { { .compatible = "apple,j713-macaudio", .data = &macaudio_j713_cfg }, /* The J713 jack runs on the AOP like the J616s one. */ { .compatible = "apple,j713-macaudio-jack", .data = &macaudio_j616_jack_cfg }, + /* j873 AID48 sn012776 15 1x 1W (AOP-fed, no protection) */ + { .compatible = "apple,j873-macaudio", .data = &macaudio_j873_cfg }, /* Fallback, jack only */ { .compatible = "apple,macaudio"}, { } From 7c876b1bdf9f509757fb70733434647456f58ddc Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 1 Nov 2026 21:39:33 +0100 Subject: [PATCH 09/55] soc: apple: rtkit: Take a crash buffer the firmware maps itself The T8142 AOP keeps its crash buffer in its own slot of its DART stream and asks for it on endpoint 1 with that IOVA. A client without shmem_setup refused the request, so the firmware never got its crash buffer acknowledged. APPLE_RTKIT_FIRMWARE_OWNED_CRASHLOG takes such a buffer as the firmware's, without a CPU mapping, and acknowledges the request unchanged; a later crash is reported without a log. RtKit::new_with_firmware_crashlog() selects it at runtime, so a driver can use it for one SoC only. Signed-off-by: Thomas Glanzmann --- drivers/soc/apple/rtkit.c | 15 +++++++++++++++ include/linux/soc/apple/rtkit.h | 7 +++++++ rust/bindings/bindings_helper.h | 1 + rust/kernel/soc/apple/rtkit.rs | 16 ++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index d352083aaa26c..9eaa28d16c46d 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -410,6 +410,13 @@ static int apple_rtkit_common_rx_get_buffer(struct apple_rtkit *rtk, dev_dbg(rtk->dev, "RTKit: buffer request for 0x%zx bytes at %pad\n", request.size, &request.iova); + if ((rtk->ops->flags & APPLE_RTKIT_FIRMWARE_OWNED_CRASHLOG) && + ep == APPLE_RTKIT_EP_CRASHLOG && request.iova) { + *buffer = request; + buffer->is_mapped = true; + return apple_rtkit_send_message(rtk, ep, msg, NULL, false); + } + if (request.iova && !rtk->ops->shmem_setup && !rtk->ops->shmem_setup_ep) { err = -EINVAL; goto error; @@ -734,6 +741,14 @@ static void apple_rtkit_crashlog_rx(struct apple_rtkit *rtk, u64 msg) dev_err(rtk->dev, "RTKit: co-processor has crashed\n"); + /* A firmware-owned crash buffer has no CPU mapping to read the log from. */ + if (!rtk->crashlog_buffer.buffer && !rtk->crashlog_buffer.iomem) { + rtk->crashed = true; + if (rtk->ops->crashed) + rtk->ops->crashed(rtk->cookie, NULL, 0); + return; + } + /* * create a shadow copy here to make sure the co-processor isn't able * to change the log while we're dumping it. this also ensures diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h index 65bd28a6f0240..4c27e012c2c75 100644 --- a/include/linux/soc/apple/rtkit.h +++ b/include/linux/soc/apple/rtkit.h @@ -57,6 +57,13 @@ struct apple_rtkit_inherited_buffer { /* This client requires endpoint 7 setup before IOP power can be acknowledged. */ #define APPLE_RTKIT_NEEDS_COREANALYTICS BIT(1) +/* + * The co-processor keeps its crash buffer in memory it maps itself (the T8142 + * AOP) and asks for it with that IOVA: take the buffer as the firmware's, + * without a CPU mapping, and acknowledge the request unchanged. + */ +#define APPLE_RTKIT_FIRMWARE_OWNED_CRASHLOG BIT(2) + /* * Struct to represent implementation-specific RTKit operations. * diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h index 56f9f84689804..2731a39a0255c 100644 --- a/rust/bindings/bindings_helper.h +++ b/rust/bindings/bindings_helper.h @@ -158,6 +158,7 @@ const blk_features_t RUST_CONST_HELPER_BLK_FEAT_ROTATIONAL = BLK_FEAT_ROTATIONAL const fop_flags_t RUST_CONST_HELPER_FOP_UNSIGNED_OFFSET = FOP_UNSIGNED_OFFSET; const u32 RUST_CONST_HELPER_APPLE_RTKIT_COPROC_PLACES_BUFFERS = APPLE_RTKIT_COPROC_PLACES_BUFFERS; const u32 RUST_CONST_HELPER_APPLE_RTKIT_NEEDS_COREANALYTICS = APPLE_RTKIT_NEEDS_COREANALYTICS; +const u32 RUST_CONST_HELPER_APPLE_RTKIT_FIRMWARE_OWNED_CRASHLOG = APPLE_RTKIT_FIRMWARE_OWNED_CRASHLOG; const u64 BINDINGS_SNDRV_PCM_FMTBIT_FLOAT_LE = SNDRV_PCM_FMTBIT_FLOAT_LE; const u64 BINDINGS_SNDRV_PCM_FMTBIT_S32_LE = SNDRV_PCM_FMTBIT_S32_LE; diff --git a/rust/kernel/soc/apple/rtkit.rs b/rust/kernel/soc/apple/rtkit.rs index 73896fea53e00..ef9c9e7f27b0f 100644 --- a/rust/kernel/soc/apple/rtkit.rs +++ b/rust/kernel/soc/apple/rtkit.rs @@ -245,6 +245,11 @@ impl RtKit { ..Self::VTABLE }; + const FIRMWARE_CRASHLOG_VTABLE: bindings::apple_rtkit_ops = bindings::apple_rtkit_ops { + flags: Self::VTABLE.flags | bindings::APPLE_RTKIT_FIRMWARE_OWNED_CRASHLOG, + ..Self::VTABLE + }; + /// Creates a new RTKit client for a given device and optional mailbox name or index. pub fn new( dev: &device::Device, @@ -268,6 +273,17 @@ impl RtKit { Self::new_with_ops(dev, mbox_name, mbox_idx, data, &Self::COREANALYTICS_VTABLE) } + /// Creates a client whose co-processor keeps its crash buffer in memory it + /// maps itself and asks for it with that IOVA (the T8142 AOP). + pub fn new_with_firmware_crashlog( + dev: &device::Device, + mbox_name: Option<&'static CStr>, + mbox_idx: usize, + data: T::Data, + ) -> Result { + Self::new_with_ops(dev, mbox_name, mbox_idx, data, &Self::FIRMWARE_CRASHLOG_VTABLE) + } + fn new_with_ops( dev: &device::Device, mbox_name: Option<&'static CStr>, From faa5ea2e77b24ce198cfad1e79975671b2d54bc6 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 22:14:50 +0100 Subject: [PATCH 10/55] soc: apple: aop: Boot the T8142 (M5) AOP through its setup port The T8142 AOP (MacBook Air J813) waits for the host on a setup port, as the T8140 one does: the ADT's aop-exclave-mailbox at 0x3d067c000 (AOP ASC + 0x4007c000, not behind the arm-io translation). Its EPMAP announces other service endpoints than T8140's fixed set (0x4, 0x20, 0x23 and 0x32 on J813), so take the endpoints that must get buffers from the EPMAP when the match data says so. The firmware places its own crash log buffer in its firmware-mapped DART slot; accept it as the firmware's, as for G17P. After the setup boot all AFK endpoints start, and with the audio fabric left to the firmware (see the J813 device tree) the firmware announces its "audio" EPIC service. Signed-off-by: Thomas Glanzmann --- drivers/soc/apple/aop.rs | 87 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index 73df7cf27deaa..eb99bc4652175 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -169,6 +169,8 @@ const SETUP_BUFFER_ENTRY_SIZE: usize = 16; /// The service endpoints that request buffers; boot completes once all of /// them have theirs. const SETUP_ENDPOINTS: [u8; 5] = [0x20, 0x21, 0x23, 0x30, 0x32]; +/// Service endpoints a setup port may announce in its EPMAP. +const SETUP_MAX_ENDPOINTS: usize = 16; const SETUP_BOOT_TIMEOUT_MS: u32 = 15000; // A request to a service endpoint: the payload goes into the endpoint's // message page, (3 << 60) | length announces it, the firmware answers with a @@ -1021,7 +1023,12 @@ struct SetupState { arena: Option>, /// The next unassigned page of the arena. next_page: usize, - endpoints: [Option; SETUP_ENDPOINTS.len()], + endpoints: [Option; SETUP_MAX_ENDPOINTS], + /// The service endpoints that must get buffers: T8140's fixed set, or + /// those the EPMAP announces. + expected: [u8; SETUP_MAX_ENDPOINTS], + expected_count: usize, + epmap_endpoints: bool, map_done: bool, ap_ready: bool, power_sent: bool, @@ -1030,12 +1037,21 @@ struct SetupState { } impl SetupState { - fn new() -> SetupState { + fn new(epmap_endpoints: bool) -> SetupState { + let mut expected = [0; SETUP_MAX_ENDPOINTS]; + let mut expected_count = 0; + if !epmap_endpoints { + expected[..SETUP_ENDPOINTS.len()].copy_from_slice(&SETUP_ENDPOINTS); + expected_count = SETUP_ENDPOINTS.len(); + } SetupState { mbox: None, arena: None, next_page: 0, - endpoints: [None; SETUP_ENDPOINTS.len()], + endpoints: [None; SETUP_MAX_ENDPOINTS], + expected, + expected_count, + epmap_endpoints, map_done: false, ap_ready: false, power_sent: false, @@ -1077,7 +1093,8 @@ impl SetupState { /// Boot is complete once the AP power state is on and every service /// endpoint has its buffers. fn ready(&self) -> bool { - self.ap_ready && self.endpoint_count() == SETUP_ENDPOINTS.len() + self.ap_ready && self.expected_count != 0 + && self.endpoint_count() == self.expected_count } /// Sends one word to a setup-port endpoint. Callers run in process @@ -1375,7 +1392,7 @@ impl AopData { required_endpoints: cfg.required_endpoints, setup_queue, setup_lost: Atomic::new(false), - setup <- new_mutex!(SetupState::new()), + setup <- new_mutex!(SetupState::new(cfg.setup_epmap_endpoints)), setup_cv <- new_condvar!(), rtkit <- new_mutex!(None), endpoints <- pin_init::pin_init_array_from_fn(|i| { @@ -2188,6 +2205,24 @@ impl AopData { // Each fragment of the endpoint map is acknowledged by // echoing it. st.send(SETUP_MGMT_EP, word)?; + if st.epmap_endpoints { + // RTKit layout: a 32-endpoint bitmap, its base in + // bits 32..34. Endpoint 0 is this management one. + let base = ((word >> 32) & 7) as u8 * 32; + dev_dbg!(self.dev, "setup port: EPMAP {:#x}\n", word); + for bit in 0..32u8 { + let ep = base + bit; + if word & (1 << bit) != 0 && ep != SETUP_MGMT_EP + && !st.expected[..st.expected_count].contains(&ep) + { + if st.expected_count == SETUP_MAX_ENDPOINTS { + return Err(ENOSPC); + } + st.expected[st.expected_count] = ep; + st.expected_count += 1; + } + } + } if word & SETUP_EPMAP_LAST != 0 { st.map_done = true; } @@ -2244,7 +2279,7 @@ impl AopData { /// from the arena. fn setup_endpoint_request(&self, st: &mut SetupState, ep: u8, word: u64) -> Result<()> { let requested = (word & 0xffff_ffff) as usize; - let slot = SETUP_ENDPOINTS + let slot = st.expected[..st.expected_count] .iter() .position(|e| *e == ep) .ok_or(EINVAL)?; @@ -2485,7 +2520,7 @@ impl AopData { "setup port: boot incomplete (AP power on: {}, endpoints: {}/{}): {:?}", st.ap_ready, st.endpoint_count(), - SETUP_ENDPOINTS.len(), + st.expected_count, e ); return Err(e); @@ -2553,6 +2588,8 @@ struct AopHwConfig { epic_v4: bool, /// The firmware boots through a second, "setup", mailbox as well. setup_port: bool, + /// Its service endpoints are the ones its EPMAP announces, not T8140's. + setup_epmap_endpoints: bool, /// Shut the co-processor down on unbind and retain its buffers if the /// shutdown cannot be confirmed. quiesce_on_unbind: bool, @@ -2575,6 +2612,7 @@ const HW_CFG_T8103: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2589,6 +2627,7 @@ const HW_CFG_T8112: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2603,6 +2642,7 @@ const HW_CFG_T6000: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2617,6 +2657,7 @@ const HW_CFG_T6020: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2636,6 +2677,7 @@ const HW_CFG_T6040: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, quiesce_on_unbind: true, required_endpoints: &[], fw_regions: true, @@ -2659,6 +2701,7 @@ const HW_CFG_T8140: AopHwConfig = AopHwConfig { patch_bootargs: false, epic_v4: true, setup_port: true, + setup_epmap_endpoints: false, quiesce_on_unbind: true, required_endpoints: &[0x20, 0x21, 0x22, 0x23, 0x2b], fw_regions: false, @@ -2674,6 +2717,26 @@ const HW_CFG_T8152: AopHwConfig = AopHwConfig { patch_bootargs: false, epic_v4: true, setup_port: false, + setup_epmap_endpoints: false, + quiesce_on_unbind: true, + required_endpoints: &[], + fw_regions: false, + halt_on_failed_start: false, +}; + +/// T8142 (M5): booted through a setup port as on T8140, at the ADT's +/// aop-exclave-mailbox (0x3d067c000, AOP ASC + 0x4007c000, untranslated), +/// whose EPMAP names the service endpoints (0x4, 0x20, 0x23 and 0x32 on the +/// MacBook Air J813, not T8140's set). EPIC and AFK as on T8152; the +/// firmware runs with the boot arguments iBoot left. +const HW_CFG_T8142: AopHwConfig = AopHwConfig { + ec0p: 0, + aopt: 0, + alig: 0, + patch_bootargs: false, + epic_v4: true, + setup_port: true, + setup_epmap_endpoints: true, quiesce_on_unbind: true, required_endpoints: &[], fw_regions: false, @@ -2691,6 +2754,7 @@ kernel::of_device_table!( (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), + (of::DeviceId::new(c_str!("apple,t8142-aop")), &HW_CFG_T8142), (of::DeviceId::new(c_str!("apple,t8152-aop")), &HW_CFG_T8152), ] ); @@ -2741,7 +2805,14 @@ impl platform::Driver for AopDriver { cfg.bootargs_word_access, )?; } - let rtkit = rtkit::RtKit::::new(pdev.as_ref(), None, 0, data.clone())?; + // The T8142 AOP places its own crashlog buffer in its firmware-mapped + // DART slot and asks for it on endpoint 1 with that IOVA; take it as + // the firmware's instead of refusing the request. + let rtkit = if cfg.setup_epmap_endpoints { + rtkit::RtKit::::new_with_firmware_crashlog(pdev.as_ref(), None, 0, data.clone())? + } else { + rtkit::RtKit::::new(pdev.as_ref(), None, 0, data.clone())? + }; data.rtkit.lock().as_mut().set(Some(rtkit)); if cfg.setup_port { AopData::setup_open(&data, pdev.as_ref())?; From b91f6a984cf0fae18d69f546c6d0f2f8438e92a4 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 22:15:04 +0100 Subject: [PATCH 11/55] dmaengine: apple-admac: Restart a paused capture at a frame boundary Behind an AOP the serializer feeding a capture channel is the firmware's, and it stops in the middle of a frame whenever it likes. A capture that stays open across such stops (the speaker V/I sense, which a protection daemon reads for as long as it runs) then carries the partial frame forward: every later frame is off by its words, and the samples of one amplifier are read as another's. On the MacBook Air J813 one restart in three slipped by one 16-bit word. Let a client pass the frame size in bytes as a u32 in dma_slave_config.peripheral_config. A paused cyclic capture on such a channel resumes at the next frame boundary of its current period: the reports completed before the pause are accounted for, the rings are reset and the rest of the period is submitted from the aligned address, then whole periods as before. Without a frame size nothing changes. Signed-off-by: Thomas Glanzmann --- drivers/dma/apple-admac.c | 93 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/drivers/dma/apple-admac.c b/drivers/dma/apple-admac.c index 90d51641a6a35..741d1e27f893d 100644 --- a/drivers/dma/apple-admac.c +++ b/drivers/dma/apple-admac.c @@ -128,6 +128,8 @@ struct admac_chan { spinlock_t lock; struct admac_tx *current_tx; int nperiod_acks; + /* frame size a paused capture restarts aligned to on resume, or 0 */ + u32 realign_bytes; /* * We maintain a 'submitted' and 'issued' list mainly for interface @@ -337,6 +339,21 @@ static struct dma_async_tx_descriptor *admac_prep_dma_cyclic( return &adtx->tx; } +/* + * Write one hardware descriptor. + */ +static void admac_write_hw_desc(struct admac_data *ad, int channo, + dma_addr_t addr, size_t len) +{ + dev_dbg(ad->dev, "ch%d descriptor: addr=0x%pad len=0x%zx flags=0x%lx\n", + channo, &addr, len, FLAG_DESC_NOTIFY); + + writel_relaxed(lower_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); + writel_relaxed(upper_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); + writel_relaxed(len, ad->base + REG_DESC_WRITE(channo)); + writel_relaxed(FLAG_DESC_NOTIFY, ad->base + REG_DESC_WRITE(channo)); +} + /* * Write one hardware descriptor for a dmaengine cyclic transaction. */ @@ -353,13 +370,7 @@ static bool admac_cyclic_write_one_desc(struct admac_data *ad, int channo, /* If happens means we have buggy code */ WARN_ON_ONCE(addr + tx->period_len > tx->buf_end); - dev_dbg(ad->dev, "ch%d descriptor: addr=0x%pad len=0x%zx flags=0x%lx\n", - channo, &addr, tx->period_len, FLAG_DESC_NOTIFY); - - writel_relaxed(lower_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); - writel_relaxed(upper_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); - writel_relaxed(tx->period_len, ad->base + REG_DESC_WRITE(channo)); - writel_relaxed(FLAG_DESC_NOTIFY, ad->base + REG_DESC_WRITE(channo)); + admac_write_hw_desc(ad, channo, addr, tx->period_len); /* Include a descriptor already consumed while the ring is refilled. */ tx->nperiods_outstanding++; @@ -609,6 +620,62 @@ static int admac_pause(struct dma_chan *chan) return 0; } +static int admac_drain_reports(struct admac_data *ad, int channo); + +/* + * Restart a paused cyclic capture at the next frame boundary of the current + * period. A source that stopped in the middle of a frame (a serializer that + * firmware stops whenever it likes) leaves a partial frame in the buffer, and + * without this every later frame would be off by its words. Called with the + * channel stopped and the lock held. + */ +static void admac_cyclic_realign(struct admac_chan *adchan) +{ + struct admac_data *ad = adchan->host; + struct admac_tx *tx = adchan->current_tx; + int ch = adchan->no; + size_t done, aligned; + int nreports; + + /* Periods completed before the pause */ + nreports = admac_drain_reports(ad, ch); + tx->nperiods_outstanding -= min_t(unsigned int, nreports, tx->nperiods_outstanding); + adchan->nperiod_acks += nreports; + tx->reclaimed_pos += nreports * tx->period_len; + tx->reclaimed_pos %= 2 * tx->buf_len; + + done = tx->period_len - + min_t(size_t, readl_relaxed(ad->base + REG_RESIDUE(ad, ch)), tx->period_len); + aligned = roundup(done, adchan->realign_bytes); + if (aligned >= tx->period_len) { + adchan->nperiod_acks++; + tx->reclaimed_pos += tx->period_len; + tx->reclaimed_pos %= 2 * tx->buf_len; + aligned = 0; + } + if (adchan->nperiod_acks) + tasklet_schedule(&adchan->tasklet); + + dev_dbg(ad->dev, "ch%d realign: %zu bytes into the period, restart at %zu\n", + ch, done, aligned); + + admac_reset_rings(adchan); + writel_relaxed(0, ad->base + REG_CHAN_CTL(ad, ch)); + /* The reset dropped every descriptor still in the ring. */ + tx->nperiods_outstanding = 0; + + /* The rest of the current period, then whole periods as before */ + tx->submitted_pos = tx->reclaimed_pos; + admac_write_hw_desc(ad, ch, tx->buf_addr + (tx->submitted_pos % tx->buf_len) + aligned, + tx->period_len - aligned); + tx->nperiods_outstanding++; + tx->submitted_pos += tx->period_len; + tx->submitted_pos %= 2 * tx->buf_len; + + admac_start_chan(adchan); + admac_cyclic_write_desc(ad, ch, tx); +} + static int admac_resume(struct dma_chan *chan) { struct admac_chan *adchan = to_admac_chan(chan); @@ -618,6 +685,9 @@ static int admac_resume(struct dma_chan *chan) spin_lock_irqsave(&adchan->lock, flags); if (adchan->current_tx && adchan->current_tx->error) ret = -EIO; + else if (adchan->realign_bytes && adchan->current_tx && + admac_chan_direction(adchan->no) == DMA_DEV_TO_MEM) + admac_cyclic_realign(adchan); else admac_start_chan(adchan); spin_unlock_irqrestore(&adchan->lock, flags); @@ -971,6 +1041,15 @@ static int admac_device_config(struct dma_chan *chan, writel_relaxed(bus_width, ad->base + REG_BUS_WIDTH(ad, adchan->no)); + /* + * Optional: a u32 frame size in bytes, which a paused capture is + * restarted aligned to on resume (see admac_cyclic_realign()). + */ + if (config->peripheral_config && config->peripheral_size == sizeof(u32)) + adchan->realign_bytes = *(u32 *)config->peripheral_config; + else + adchan->realign_bytes = 0; + /* * By FIFOCTL_LIMIT we seem to set the maximal number of bytes allowed to be * held in controller's per-channel FIFO. Transfers seem to be triggered From 06f1b4a8027408b3b76ea2a27fa04be64ecca7d3 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 22:15:04 +0100 Subject: [PATCH 12/55] ASoC: apple: t8152-aop-audio: Add T8142 (MacBook Air J813) The J813 speaker stream (ADT iop-audio-controller/audio-speaker) differs from J873's: four SN012776 in 32-bit playback slots 0-3, and their voltage and current sense back on the same stream's RX side (base-ns RX0) as eight 16-bit slots. Make the speaker and sense shapes per-SoC match data: J873 keeps one playback channel; T8142 gets four, and an 8-channel S16_LE "Speaker Sense" front-end. The sense capture is the speaker serializer's RX, which the firmware stops anywhere in a frame. Before every `pwrd` of the speaker, restart a running sense capture at a frame boundary (apple-admac realignment, the partial frame reads as zeros), so that a protection daemon that keeps it open attributes each amplifier's voltage and current to the right one. The sense back-end also attaches the speaker service and requests `pw0 ` at startup: its front-end requests the ADMAC channel next, and the ADMAC only answers while the firmware holds the audio fabric up. Opening the sense before any other stream since the AOP started (speakersafetyd at boot) otherwise faults the bus. Signed-off-by: Thomas Glanzmann --- sound/soc/apple/t8152_aop_audio.rs | 160 ++++++++++++++++++++++++++--- 1 file changed, 144 insertions(+), 16 deletions(-) diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs index 31f562e7fc64d..4c61092d0cea1 100644 --- a/sound/soc/apple/t8152_aop_audio.rs +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT -//! Apple T8152 (J873, Mac mini M6) AOP audio +//! Apple T8152 (J873, Mac mini M6) and T8142 (J813, MacBook Air M5) AOP audio //! //! The AOP firmware owns the audio fabric, as on T8140. The ADT //! (/arm-io/iop-audio-controller) names its services; all of them run on the @@ -220,6 +220,9 @@ struct T8152AopAudio { cin: Service, spkr: Service, mca: KVec, + cfg: &'static HwConfig, + /// The open sense capture front-end, if any. + sense: Atomic<*mut bindings::snd_pcm_substream>, } impl T8152AopAudio { @@ -353,16 +356,68 @@ impl T8152AopAudio { Ok(()) } + /// The sense capture's back-end. Its front-end requests the ADMAC + /// channel next, and the ADMAC only answers while the firmware holds + /// the audio fabric up, which it does while a service is in `pw0 ` or + /// `pwrd`. Opening the sense before any other stream since boot (as + /// speakersafetyd does) otherwise faults the bus (SError). Shutdown + /// leaves the service in `pw0 `, as the other streams do. + fn sense_startup(&self) -> Result<()> { + self.attach(&self.spkr)?; + if !self.spkr.running.load(Relaxed) { + self.set_power(&self.spkr, POWER_STATE_PW0)?; + } + Ok(()) + } + /// `pwrd`: the firmware starts the serializer clocks and the stream. fn run(&self, svc: &Service) -> Result<()> { if svc.running.load(Relaxed) { return Ok(()); } + if ptr::eq(svc, &self.spkr) && self.cfg.realign_sense { + self.realign_sense(); + } self.set_power(svc, POWER_STATE_PWRD)?; svc.running.store(true, Relaxed); Ok(()) } + /// Before the speaker's serializer starts, restart a running sense + /// capture at a frame boundary. The sense words are the serializer's RX, + /// which the firmware stops anywhere in a frame; a capture that stays + /// open across speaker streams (speakersafetyd keeps it) would carry + /// the partial frame forward and attribute one amplifier's voltage and + /// current to another. Nothing flows while the serializer is stopped, + /// so this loses no data; the partial frame reads as zeros. + fn realign_sense(&self) { + let ss = self.sense.load(Relaxed); + if ss.is_null() { + return; + } + // SAFETY: the sense front-end's open and close and this back-end's + // prepare all run under the card's DPCM mutex, so `ss` is live; its + // stream lock orders this against the front-end's trigger. + unsafe { + bindings::snd_pcm_stream_lock_irq(ss); + let rt = (*ss).runtime; + if !rt.is_null() && (*rt).state == SNDRV_PCM_STATE_RUNNING { + let chan = bindings::snd_dmaengine_pcm_get_chan(ss); + let ops = &*(*chan).device; + if let (Some(pause), Some(resume)) = (ops.device_pause, ops.device_resume) { + pause(chan); + let frame = ((*rt).frame_bits / 8) as usize; + let pos = bindings::snd_dmaengine_pcm_pointer(ss) as usize; + if frame != 0 && pos < (*rt).buffer_size as usize && !(*rt).dma_area.is_null() { + ptr::write_bytes((*rt).dma_area.add(pos * frame), 0, frame); + } + resume(chan); + } + } + bindings::snd_pcm_stream_unlock_irq(ss); + } + } + /// The clocks run from prepare on: the codecs see a bit clock as soon /// as ASoC powers and unmutes them. A PCM underrun stops the DMA /// without closing the back-end; a service that restarts on prepare @@ -407,6 +462,42 @@ const PCM_PERIOD_BYTES_MAX: usize = 0x10000; /// The AOP-run serializers take 24-bit samples in 32-bit words. const FORMATS: u64 = bindings::BINDINGS_SNDRV_PCM_FMTBIT_S24_LE; +/// SNDRV_PCM_FMTBIT_S16_LE (1 << SNDRV_PCM_FORMAT_S16_LE), which the +/// bindings do not export. +const FMTBIT_S16_LE: u64 = 1 << 2; +/// SNDRV_PCM_STATE_RUNNING, likewise. +const SNDRV_PCM_STATE_RUNNING: bindings::snd_pcm_state_t = 3; + +/// What differs between the machines: the shape of the firmware's `spkr` +/// stream (ADT iop-audio-controller/audio-speaker). +struct HwConfig { + /// Speaker slots (one per amplifier). + spkr_channels: u32, + /// Words the amplifiers send back on the stream's RX side (V/I sense). + sense_channels: u32, + sense_formats: u64, + /// The sense capture is the speaker stream's RX: restart it at a frame + /// boundary whenever the speaker starts (see `realign_sense`). + realign_sense: bool, +} + +/// J873: one amplifier in one 32-bit slot, nothing sent back. +static HW_CFG_T8152: HwConfig = HwConfig { + spkr_channels: 1, + sense_channels: 2, + sense_formats: FORMATS, + realign_sense: false, +}; + +/// J813: four amplifiers (left woofer, left tweeter, right woofer, right +/// tweeter) in 32-bit slots 0-3, their voltage and current sense back as +/// eight 16-bit slots (V, I per amplifier) on the stream's RX (base-ns RX0). +static HW_CFG_T8142: HwConfig = HwConfig { + spkr_channels: 4, + sense_channels: 8, + sense_formats: FMTBIT_S16_LE, + realign_sense: true, +}; /// # Safety /// @@ -543,7 +634,7 @@ unsafe extern "C" fn asoc_pcm_open( ); } if ret == 0 { - hw.formats &= FORMATS; + hw.formats &= if fe == 2 { data.cfg.sense_formats } else { FORMATS }; (*(*substream).runtime).hw = hw; ret = bindings::snd_dmaengine_pcm_open(substream, chan); } @@ -552,6 +643,8 @@ unsafe extern "C" fn asoc_pcm_open( if ret < 0 { // SAFETY: the open failed, so the channel is still ours. unsafe { bindings::dma_release_channel(chan) }; + } else if fe == 2 && !playback { + data.sense.store(substream, Relaxed); } ret } @@ -560,13 +653,18 @@ unsafe extern "C" fn asoc_pcm_open( /// /// Called by ASoC on this driver's component with live arguments. unsafe extern "C" fn asoc_pcm_close( - _component: *mut bindings::snd_soc_component, + component: *mut bindings::snd_soc_component, substream: *mut bindings::snd_pcm_substream, ) -> i32 { // SAFETY: by the caller's contract. if unsafe { rtd_is_be(substream_rtd(substream)) } { return 0; } + // SAFETY: as above. + let data = unsafe { component_data(component) }; + if data.sense.load(Relaxed) == substream { + data.sense.store(ptr::null_mut(), Relaxed); + } // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) } } @@ -575,7 +673,7 @@ unsafe extern "C" fn asoc_pcm_close( /// /// Called by ASoC on this driver's component with live arguments. unsafe extern "C" fn asoc_pcm_hw_params( - _component: *mut bindings::snd_soc_component, + component: *mut bindings::snd_soc_component, substream: *mut bindings::snd_pcm_substream, params: *mut bindings::snd_pcm_hw_params, ) -> i32 { @@ -605,6 +703,15 @@ unsafe extern "C" fn asoc_pcm_hw_params( } else { cfg.src_port_window_size = window; } + // The sense capture: the frame size its channel restarts aligned to + // (apple-admac reads a u32 here; see `realign_sense`). + // SAFETY: by the caller's contract. + let data = unsafe { component_data(component) }; + let mut frame_bytes = channels * cfg.src_addr_width as u32; + if ptr::eq(substream, data.sense.load(Relaxed)) && data.cfg.realign_sense { + cfg.peripheral_config = ptr::from_mut(&mut frame_bytes).cast(); + cfg.peripheral_size = mem::size_of::(); + } // SAFETY: `chan` is the live channel behind the substream. unsafe { match (*(*chan).device).device_config { @@ -715,6 +822,14 @@ unsafe extern "C" fn be_startup( ) -> i32 { // SAFETY: by the caller's contract. let Some((data, svc)) = (unsafe { be_service(dai, substream) }) else { + // SAFETY: as above. + let (data, id) = unsafe { (dai_data(dai), (*dai).id as u32) }; + if id == BE_DAI_ID_SPKR { + return match data.sense_startup() { + Ok(()) => 0, + Err(e) => e.to_errno(), + }; + } return 0; }; match data.startup(svc) { @@ -780,9 +895,17 @@ impl DaiOps { } fn pcm_stream(name: &'static CStr, channels: u32) -> bindings::snd_soc_pcm_stream { + pcm_stream_fmt(name, channels, FORMATS) +} + +fn pcm_stream_fmt( + name: &'static CStr, + channels: u32, + formats: u64, +) -> bindings::snd_soc_pcm_stream { bindings::snd_soc_pcm_stream { stream_name: name.as_char_ptr(), - formats: FORMATS, + formats, rates: bindings::SNDRV_PCM_RATE_48000, rate_min: 48000, rate_max: 48000, @@ -797,7 +920,7 @@ fn pcm_stream(name: &'static CStr, channels: u32) -> bindings::snd_soc_pcm_strea /// back-end), 4 the sense front-end. const DAI_COUNT: usize = 2 + FE_COUNT; -fn dai_drivers(ops: &DaiOps) -> [bindings::snd_soc_dai_driver; DAI_COUNT] { +fn dai_drivers(ops: &DaiOps, cfg: &HwConfig) -> [bindings::snd_soc_dai_driver; DAI_COUNT] { [ bindings::snd_soc_dai_driver { name: c"cout".as_char_ptr(), @@ -815,28 +938,27 @@ fn dai_drivers(ops: &DaiOps) -> [bindings::snd_soc_dai_driver; DAI_COUNT] { capture: pcm_stream(c"PCM0 RX", 1), ..Default::default() }, - // The speaker: one 32-bit slot per frame (the firmware's spkr - // serializer runs a single channel). + // The speaker: one 32-bit slot per amplifier. bindings::snd_soc_dai_driver { name: c"j873-pcm-1".as_char_ptr(), id: FE_DAI_ID_BASE + 1, ops: &ops.fe, - playback: pcm_stream(c"PCM1 TX", 1), + playback: pcm_stream(c"PCM1 TX", cfg.spkr_channels), ..Default::default() }, bindings::snd_soc_dai_driver { name: c"spkr".as_char_ptr(), id: BE_DAI_ID_SPKR, ops: &ops.be, - playback: pcm_stream(c"SPKR TX", 1), - capture: pcm_stream(c"SPKR RX", 2), + playback: pcm_stream(c"SPKR TX", cfg.spkr_channels), + capture: pcm_stream_fmt(c"SPKR RX", cfg.sense_channels, cfg.sense_formats), ..Default::default() }, bindings::snd_soc_dai_driver { name: c"j873-pcm-2".as_char_ptr(), id: FE_DAI_ID_BASE + 2, ops: &ops.fe, - capture: pcm_stream(c"PCM2 RX", 2), + capture: pcm_stream_fmt(c"PCM2 RX", cfg.sense_channels, cfg.sense_formats), ..Default::default() }, ] @@ -845,7 +967,7 @@ fn dai_drivers(ops: &DaiOps) -> [bindings::snd_soc_dai_driver; DAI_COUNT] { impl AsocContext { fn new(data: Arc) -> Result> { let ops = KBox::new(DaiOps::new(), GFP_KERNEL)?; - let dais = KBox::new(Opaque::new(dai_drivers(&ops)), GFP_KERNEL)?; + let dais = KBox::new(Opaque::new(dai_drivers(&ops, data.cfg)), GFP_KERNEL)?; let component = bindings::snd_soc_component_driver { name: c"t8152-aop-audio".as_char_ptr(), open: Some(asoc_pcm_open), @@ -899,20 +1021,24 @@ impl T8152AopAudioDriver { kernel::of_device_table!( OF_TABLE, ::IdInfo, - [(of::DeviceId::new(c_str!("apple,t8152-aop-audio")), ())] + [ + (of::DeviceId::new(c_str!("apple,t8142-aop-audio")), &HW_CFG_T8142), + (of::DeviceId::new(c_str!("apple,t8152-aop-audio")), &HW_CFG_T8152), + ] ); impl platform::Driver for T8152AopAudioDriver { - type IdInfo = (); + type IdInfo = &'static HwConfig; type Data<'bound> = Self; const OF_ID_TABLE: Option> = Some(&OF_TABLE); fn probe<'bound>( pdev: &'bound platform::Device>, - _info: Option<&'bound Self::IdInfo>, + info: Option<&'bound Self::IdInfo>, ) -> impl PinInit, Error> + 'bound { let dev = ARef::::from(pdev.as_ref()); + let cfg: &'static HwConfig = info.copied().unwrap_or(&HW_CFG_T8152); // SAFETY: this is a service device the AOP core registered. let adata = unsafe { ::from_child(pdev.as_ref()) }?; // SAFETY: the AOP core sets the child's platform data to its EPIC service. @@ -953,6 +1079,8 @@ impl platform::Driver for T8152AopAudioDriver { cin: Service::new(from_fourcc(b"cin "), "cin", true), spkr: Service::new(from_fourcc(b"spkr"), "spkr", false), mca, + cfg, + sense: Atomic::new(ptr::null_mut()), }), GFP_KERNEL, )?; From 9bee3709a110999cc0bbf33ed340bef21f814301 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 22:15:04 +0100 Subject: [PATCH 13/55] ASoC: apple: macaudio: Add J813 MacBook Air 13" M5 (T8142): as on J873 the AOP firmware owns the audio fabric and the CPU component is the T8152-style AOP audio driver. The jack is a CS42L84 on "cout", the speakers four SN012776 (one woofer and one tweeter per side) on "spkr", with their V/I sense on the same stream. As on the other SN012776 laptops the speaker volume stays locked until a protection daemon (speakersafetyd) holds it. Signed-off-by: Thomas Glanzmann --- sound/soc/apple/macaudio.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index 4743ba5db026b..58ecbf21e3152 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -2398,6 +2398,29 @@ struct macaudio_platform_cfg macaudio_j873_cfg = { .spk_secondary_only = true, }; +/* + * J813 (T8142, MacBook Air M5 13"): as on J873, the AOP firmware owns the + * audio fabric and the CPU component is the T8152-style AOP audio driver. + * The jack is a CS42L84 on "cout"; the speakers are four SN012776 (one + * woofer and one tweeter per side) on "spkr", whose voltage and current + * sense return on the same stream. As on the other SN012776 laptops, the + * speaker volume stays locked until a protection daemon (speakersafetyd) + * holds it. + */ +struct macaudio_platform_cfg macaudio_j813_cfg = { + .enable_speakers = true, + .amp = AMP_SN012776, + .speakers = SPKR_1W1T, + .stereo = true, + .amp_gain = 15, + .safe_vol = -20, + .fe_dai_names = { "j873-pcm-0", "j873-pcm-1", "j873-pcm-2" }, + .dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBC_CFC | SND_SOC_DAIFMT_IB_NF, + .spk_dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBC_CFC | + SND_SOC_DAIFMT_GATED | SND_SOC_DAIFMT_NB_IF, + .spk_secondary_only = true, +}; + struct macaudio_platform_cfg macaudio_j314_cfg = { true, AMP_SN012776, SPKR_2W1T, true, 0, -20, }; @@ -2586,6 +2609,8 @@ static const struct of_device_id macaudio_snd_device_id[] = { { .compatible = "apple,j713-macaudio-jack", .data = &macaudio_j616_jack_cfg }, /* j873 AID48 sn012776 15 1x 1W (AOP-fed, no protection) */ { .compatible = "apple,j873-macaudio", .data = &macaudio_j873_cfg }, + /* j813 AID? sn012776 15 2x 1W+1T (AOP-fed, V/I sense) */ + { .compatible = "apple,j813-macaudio", .data = &macaudio_j813_cfg }, /* Fallback, jack only */ { .compatible = "apple,macaudio"}, { } From c69b295447bb26fd843235c65f7f348d67ea2338 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 23:44:39 +0100 Subject: [PATCH 14/55] ASoC: apple: t8152-aop-audio: Add the J813 microphone array The MacBook Air J813's three microphones are the AOP's "hpai" service (ADT iop-audio-controller/audio-hp-mic): the firmware runs the PDM part and delivers 48 kHz float32 frames of three channels (the ADT's audio-hp-mic-proxy description: 12 bytes per frame) on leap-s ADMAC RX0. There is no codec, so it gets a card of its own with one capture PCM, named like the other AOP microphone cards (AppleJ813HPAI, "MacBook Air J813 High-Power Audio Interface", driver aop_audio) so that the existing UCM and DSP configurations for those apply. As on T8140: open attaches the service and requests pw0 (which raises the LEAP fabric), powers the LEAP leaves (leap-s ADMAC, RX0, LEAP MCA), and only then requests the channel, whose registers fault without them. The trigger starts the DMA and queues pwrd behind it; started earlier, the LEAP produces into a channel nobody services. Close releases the channel, then pw0 and the leaves. The card exists only where the DT gives the LEAP power domains (all three); J873 is unchanged. The dmaengine substream setup moves into a helper shared with the ASoC front-ends. Tested on J813: a tone from the speakers reads -44 to -49 dBFS on all three channels, 70 dB over the quiet floor, exactly while it plays; PipeWire picks up the card with asahi-audio's J613 beamformer graph. Signed-off-by: Thomas Glanzmann --- sound/soc/apple/t8152_aop_audio.rs | 478 ++++++++++++++++++++++++++--- 1 file changed, 429 insertions(+), 49 deletions(-) diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs index 4c61092d0cea1..9976684a8d7d8 100644 --- a/sound/soc/apple/t8152_aop_audio.rs +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -10,6 +10,9 @@ //! channels 10 and 11). //! * `spkr`: the SN012776 speaker amplifier, base-ns TX1 (channel 2), its //! sense data on RX1. +//! * `hpai` (J813): the microphone array, three PDM microphones the firmware +//! delivers as float32 on leap-s ADMAC RX0. It has no codec, so it is the +//! capture PCM of a small card of its own. //! //! Each service is an ASoC back-end DAI: startup attaches it and requests //! `pw0 ` (which also brings up the audio fabric), prepare requests `pwrd` @@ -31,12 +34,14 @@ use kernel::{ module_platform_driver, of, platform, prelude::*, soc::apple::aop::{from_fourcc, EPICService, AOP}, + str::CString, sync::{ aref::ARef, atomic::{Atomic, AtomicFlag, Relaxed}, - Arc, + Arc, ArcBorrow, }, - types::Opaque, + types::{ForeignOwnable, Opaque}, + workqueue::{self, impl_has_work, new_work, Work, WorkItem}, }; const EPIC_SUBTYPE_WRAPPED_CALL: u16 = 0x20; @@ -57,6 +62,15 @@ const PD_GET_RETRY_MS: i64 = 10; /// The MCA serializer leaves (audio_mca0_m .. audio_mca5_m). const MCA_COUNT: usize = 6; const MCA_NAMES: [&CStr; MCA_COUNT] = [c"mca0", c"mca1", c"mca2", c"mca3", c"mca4", c"mca5"]; +/// The microphone array's LEAP leaves: the leap-s ADMAC, its RX0 channel and +/// the LEAP serializer. A LEAP ADMAC's channel registers only answer with +/// these powered (an access without them faults the bus). +const LEAP_NAMES: [&CStr; 3] = [c"leap_s_a", c"leap_rx0", c"leap_mca"]; +/// The microphone array's stream (ADT audio-hp-mic-proxy physical +/// description): 48 kHz, three float32 channels. +const HPAI_RATE: u32 = 48000; +const HPAI_CHANNELS: u32 = 3; +const HPAI_START_WORK: u64 = 1; fn aop_timestamp() -> u64 { // SAFETY: both helpers read the architected timer and take no arguments. @@ -223,6 +237,30 @@ struct T8152AopAudio { cfg: &'static HwConfig, /// The open sense capture front-end, if any. sense: Atomic<*mut bindings::snd_pcm_substream>, + hpai: Service, + /// The microphone array's leaves (`LEAP_NAMES`); empty without one. + leap: KVec, + /// The microphone capture is triggered. + hpai_want_run: AtomicFlag, + #[pin] + hpai_start_work: Work, +} + +impl_has_work! { + impl HasWork for T8152AopAudio { self.hpai_start_work } +} + +/// The microphone array's `pwrd`, queued by the capture trigger (a sleeping +/// firmware call). The ADMAC is running by then: started before it, the LEAP +/// produces into a channel nobody services and the capture stays noise. +impl WorkItem for T8152AopAudio { + type Pointer = Arc; + + fn run(this: Arc) { + if let Err(e) = this.hpai_go() { + dev_err!(this.dev, "hpai: start failed: {:?}\n", e); + } + } } impl T8152AopAudio { @@ -324,11 +362,11 @@ impl T8152AopAudio { Ok(()) } - fn mca_up(&self) -> Result<()> { - for (i, pd) in self.mca.iter().enumerate() { + fn leaves_up(&self, pds: &[PowerDomain], what: &str) -> Result<()> { + for (i, pd) in pds.iter().enumerate() { if let Err(e) = pd.get() { - dev_err!(self.dev, "MCA leaf {} did not power up: {:?}\n", i, e); - for pd in self.mca[..i].iter().rev() { + dev_err!(self.dev, "{} leaf {} did not power up: {:?}\n", what, i, e); + for pd in pds[..i].iter().rev() { pd.put(); } return Err(e); @@ -337,12 +375,20 @@ impl T8152AopAudio { Ok(()) } - fn mca_down(&self) { - for pd in self.mca.iter().rev() { + fn leaves_down(pds: &[PowerDomain]) { + for pd in pds.iter().rev() { pd.put(); } } + fn mca_up(&self) -> Result<()> { + self.leaves_up(&self.mca, "MCA") + } + + fn mca_down(&self) { + Self::leaves_down(&self.mca) + } + /// Back-end startup: attach, `pw0 ` (which raises the fabric), then the /// serializer leaves. fn startup(&self, svc: &Service) -> Result<()> { @@ -439,6 +485,39 @@ impl T8152AopAudio { } self.mca_down(); } + + /// Microphone open: attach, `pw0 ` (which raises the fabric), then the + /// LEAP leaves, before the capture requests its ADMAC channel. + fn hpai_startup(&self) -> Result<()> { + self.attach(&self.hpai)?; + self.set_power(&self.hpai, POWER_STATE_PW0)?; + self.leaves_up(&self.leap, "LEAP")?; + self.hpai.running.store(false, Relaxed); + Ok(()) + } + + fn hpai_go(&self) -> Result<()> { + if !self.hpai_want_run.load(Relaxed) { + return Ok(()); + } + self.run(&self.hpai) + } + + /// Microphone close, after the channel is released: a queued start + /// finishes first, then `pw0 ` and the leaves. + fn hpai_shutdown(&self) { + self.hpai_want_run.store(false, Relaxed); + // SAFETY: the work item lives in `self`; flushing waits for a queued + // `hpai_go`. + unsafe { bindings::flush_work(Work::raw_get(&self.hpai_start_work)) }; + if self.hpai.running.load(Relaxed) { + if let Err(e) = self.set_power(&self.hpai, POWER_STATE_PW0) { + dev_err!(self.dev, "hpai: pw0 failed: {:?}\n", e); + } + self.hpai.running.store(false, Relaxed); + } + Self::leaves_down(&self.leap); + } } // ---- the ASoC component ---- @@ -566,41 +645,11 @@ unsafe fn dai_data<'a>(dai: *mut bindings::snd_soc_dai) -> &'a T8152AopAudio { unsafe { component_data((*dai).component) } } -/// # Safety -/// -/// Called by ASoC on this driver's component with live arguments. -unsafe extern "C" fn asoc_pcm_open( - component: *mut bindings::snd_soc_component, - substream: *mut bindings::snd_pcm_substream, -) -> i32 { - // SAFETY: by the caller's contract. - let data = unsafe { component_data(component) }; - // SAFETY: as above. - let rtd = unsafe { substream_rtd(substream) }; - // SAFETY: as above. - if unsafe { rtd_is_be(rtd) } { - return 0; - } - // SAFETY: as above. - let Some(fe) = (unsafe { fe_index(rtd) }) else { - return EINVAL.to_errno(); - }; - // SAFETY: as above. - let playback = unsafe { is_playback(substream) }; - let Some(name) = FE_DMA_NAMES[fe][usize::from(!playback)] else { - return ENODEV.to_errno(); - }; - // SAFETY: the device is live and the name is NUL-terminated. - let chan = match from_err_ptr(unsafe { - bindings::dma_request_chan(data.dev.as_raw(), name.as_char_ptr()) - }) { - Ok(chan) => chan, - Err(e) => { - dev_err!(data.dev, "DMA channel {:?} unavailable: {:?}\n", name, e); - return e.to_errno(); - } - }; - let mut hw = bindings::snd_pcm_hardware { +/// The dmaengine PCMs' common description: mmap-able, interleaved, periods +/// from 256 bytes to the ADMAC's segment size. Rates, channels and formats +/// are left to the caller (for a front-end, ASoC fills them in from the DAI). +fn pcm_hw() -> bindings::snd_pcm_hardware { + bindings::snd_pcm_hardware { info: bindings::SNDRV_PCM_INFO_MMAP | bindings::SNDRV_PCM_INFO_MMAP_VALID | bindings::SNDRV_PCM_INFO_INTERLEAVED, @@ -611,9 +660,25 @@ unsafe extern "C" fn asoc_pcm_open( periods_max: (PCM_BUFFER_MAX / PCM_PERIOD_BYTES_MIN) as u32, fifo_size: 16, ..Default::default() - }; - // SAFETY: the substream and the channel are live. - let ret = unsafe { + } +} + +/// Sets up a dmaengine substream on `chan`: a managed buffer on the DMA +/// device (once per substream), `hw` refined by the channel and narrowed to +/// `formats`, then the dmaengine runtime. On failure the channel is still +/// the caller's. +/// +/// # Safety +/// +/// `substream` must be live and `chan` a channel the caller requested. +unsafe fn dmaengine_open( + substream: *mut bindings::snd_pcm_substream, + chan: *mut bindings::dma_chan, + mut hw: bindings::snd_pcm_hardware, + formats: u64, +) -> i32 { + // SAFETY: by the caller's contract. + unsafe { let mut ret = 0; if (*substream).dma_buffer.dev.type_ == bindings::SNDRV_DMA_TYPE_UNKNOWN as i32 { ret = bindings::snd_pcm_set_managed_buffer( @@ -634,12 +699,51 @@ unsafe extern "C" fn asoc_pcm_open( ); } if ret == 0 { - hw.formats &= if fe == 2 { data.cfg.sense_formats } else { FORMATS }; + hw.formats &= formats; (*(*substream).runtime).hw = hw; ret = bindings::snd_dmaengine_pcm_open(substream, chan); } ret + } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_open( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { component_data(component) }; + // SAFETY: as above. + let rtd = unsafe { substream_rtd(substream) }; + // SAFETY: as above. + if unsafe { rtd_is_be(rtd) } { + return 0; + } + // SAFETY: as above. + let Some(fe) = (unsafe { fe_index(rtd) }) else { + return EINVAL.to_errno(); + }; + // SAFETY: as above. + let playback = unsafe { is_playback(substream) }; + let Some(name) = FE_DMA_NAMES[fe][usize::from(!playback)] else { + return ENODEV.to_errno(); }; + // SAFETY: the device is live and the name is NUL-terminated. + let chan = match from_err_ptr(unsafe { + bindings::dma_request_chan(data.dev.as_raw(), name.as_char_ptr()) + }) { + Ok(chan) => chan, + Err(e) => { + dev_err!(data.dev, "DMA channel {:?} unavailable: {:?}\n", name, e); + return e.to_errno(); + } + }; + let formats = if fe == 2 { data.cfg.sense_formats } else { FORMATS }; + // SAFETY: the substream is live and the channel was requested above. + let ret = unsafe { dmaengine_open(substream, chan, pcm_hw(), formats) }; if ret < 0 { // SAFETY: the open failed, so the channel is still ours. unsafe { bindings::dma_release_channel(chan) }; @@ -989,14 +1093,256 @@ impl AsocContext { } } +// ---- the microphone array's card ---- + +/// # Safety +/// +/// `substream` must be a live substream of the microphone PCM, whose private +/// data is the `Arc` leaked into it by `MicCard::new` (released in +/// `mic_pcm_free` after the last op). +unsafe fn mic_data<'a>(substream: *mut bindings::snd_pcm_substream) -> ArcBorrow<'a, T8152AopAudio> { + // SAFETY: by the caller's contract. + unsafe { Arc::::borrow((*(*substream).pcm).private_data.cast()) } +} + +/// The LEAP produces float32 into the channel requested here; the channel's +/// registers only answer with the LEAP leaves up, so the service (and with +/// it the leaves) comes up before the request and goes down after the +/// release. +/// +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM. +unsafe extern "C" fn mic_pcm_open(substream: *mut bindings::snd_pcm_substream) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { mic_data(substream) }; + // A failed startup has undone its leaves; the service stays in `pw0 `. + if let Err(e) = data.hpai_startup() { + return e.to_errno(); + } + // SAFETY: the device is live and the name is NUL-terminated. + let chan = match from_err_ptr(unsafe { + bindings::dma_request_chan(data.dev.as_raw(), c"hpai".as_char_ptr()) + }) { + Ok(chan) => chan, + Err(e) => { + dev_err!(data.dev, "hpai: DMA channel unavailable: {:?}\n", e); + data.hpai_shutdown(); + return e.to_errno(); + } + }; + let hw = bindings::snd_pcm_hardware { + rates: bindings::SNDRV_PCM_RATE_48000, + rate_min: HPAI_RATE, + rate_max: HPAI_RATE, + channels_min: HPAI_CHANNELS, + channels_max: HPAI_CHANNELS, + ..pcm_hw() + }; + // SAFETY: the substream is live and the channel was requested above. + let ret = unsafe { + dmaengine_open(substream, chan, hw, bindings::BINDINGS_SNDRV_PCM_FMTBIT_FLOAT_LE) + }; + if ret < 0 { + // SAFETY: the open failed, so the channel is still ours. + unsafe { bindings::dma_release_channel(chan) }; + data.hpai_shutdown(); + } + ret +} + +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM. +unsafe extern "C" fn mic_pcm_close(substream: *mut bindings::snd_pcm_substream) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { mic_data(substream) }; + // SAFETY: the dmaengine runtime was set up in `mic_pcm_open`. + let ret = unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) }; + data.hpai_shutdown(); + ret +} + +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM and live +/// parameters. +unsafe extern "C" fn mic_pcm_hw_params( + substream: *mut bindings::snd_pcm_substream, + params: *mut bindings::snd_pcm_hw_params, +) -> i32 { + // SAFETY: the dmaengine runtime was set up in `mic_pcm_open`. + let chan = unsafe { bindings::snd_dmaengine_pcm_get_chan(substream) }; + let mut cfg = bindings::dma_slave_config::default(); + // SAFETY: the substream and the parameters are live for the op. + let rc = unsafe { bindings::snd_hwparams_to_dma_slave_config(substream, params, &mut cfg) }; + if rc < 0 { + return rc; + } + // Three words per frame, which the ADMAC cannot mark (it frames 2 or 4): + // a plain word stream. + // SAFETY: `chan` is the live channel behind the substream. + unsafe { + match (*(*chan).device).device_config { + Some(f) => f(chan, &mut cfg), + None => ENOSYS.to_errno(), + } + } +} + +/// ALSA calls prepare unconditionally. Nothing to arm: the DMA start and +/// the queued firmware run belong to trigger. +/// +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM. +unsafe extern "C" fn mic_pcm_prepare(_substream: *mut bindings::snd_pcm_substream) -> i32 { + 0 +} + +/// With the stream lock held: the DMA first, then the firmware's `pwrd` +/// queued behind it. +/// +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM. +unsafe extern "C" fn mic_pcm_trigger(substream: *mut bindings::snd_pcm_substream, cmd: i32) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { mic_data(substream) }; + // SAFETY: the dmaengine runtime was set up in `mic_pcm_open`. + let ret = unsafe { bindings::snd_dmaengine_pcm_trigger(substream, cmd) }; + if ret < 0 { + return ret; + } + match cmd as u32 { + bindings::SNDRV_PCM_TRIGGER_START + | bindings::SNDRV_PCM_TRIGGER_RESUME + | bindings::SNDRV_PCM_TRIGGER_PAUSE_RELEASE => { + data.hpai_want_run.store(true, Relaxed); + // Err: already queued, and that run sees `hpai_want_run`. + let _ = workqueue::system().enqueue::<_, HPAI_START_WORK>(Arc::from(data)); + } + _ => data.hpai_want_run.store(false, Relaxed), + } + 0 +} + +/// # Safety +/// +/// Called once by ALSA when the PCM goes away. +unsafe extern "C" fn mic_pcm_free(pcm: *mut bindings::snd_pcm) { + // SAFETY: the `Arc` leaked into the private data by `MicCard::new`. + drop(unsafe { Arc::::from_foreign((*pcm).private_data.cast()) }); +} + +static MIC_PCM_OPS: bindings::snd_pcm_ops = bindings::snd_pcm_ops { + open: Some(mic_pcm_open), + close: Some(mic_pcm_close), + ioctl: None, + hw_params: Some(mic_pcm_hw_params), + hw_free: None, + prepare: Some(mic_pcm_prepare), + trigger: Some(mic_pcm_trigger), + sync_stop: None, + pointer: Some(bindings::snd_dmaengine_pcm_pointer), + get_time_info: None, + fill_silence: None, + copy: None, + page: None, + mmap: None, + ack: None, +}; + +/// Copies a NUL-terminated string into a fixed-size field, truncated to fit. +fn copy_str(target: &mut [u8], source: &CStr) { + let b = source.to_bytes(); + let n = b.len().min(target.len() - 1); + target[..n].copy_from_slice(&b[..n]); + target[n] = 0; +} + +/// The microphone array's card: one capture PCM, named like the other AOP +/// microphone cards (AppleJxxxHPAI) so that the UCM and DSP configurations +/// for those find it. +struct MicCard(*mut bindings::snd_card); + +impl MicCard { + fn new(data: &Arc) -> Result { + let fwnode = data.dev.fwnode().ok_or(ENODEV)?; + let chassis = fwnode + .property_read::(c_str!("apple,chassis-name")) + .required_by(&data.dev)?; + let kind = fwnode + .property_read::(c_str!("apple,machine-kind")) + .required_by(&data.dev)?; + let id = CString::try_from_fmt(fmt!("Apple{}HPAI", *chassis))?; + let shortname = CString::try_from_fmt(fmt!("{} {} HPAI", *kind, *chassis))?; + let longname = + CString::try_from_fmt(fmt!("{} {} High-Power Audio Interface", *kind, *chassis))?; + let mut card = ptr::null_mut(); + // SAFETY: the device and the module are live; `card` receives the card. + let ret = unsafe { + bindings::snd_card_new( + data.dev.as_raw(), + -1, + ptr::null(), + kernel::module::this_module::().as_ptr(), + 0, + &mut card, + ) + }; + if ret < 0 { + return Err(Error::from_errno(ret)); + } + // From here on, dropping `this` frees the card. + let this = MicCard(card); + // SAFETY: the card is new and unregistered. + unsafe { + copy_str(&mut (*card).driver, c"aop_audio"); + copy_str(&mut (*card).id, &id); + copy_str(&mut (*card).shortname, &shortname); + copy_str(&mut (*card).longname, &longname); + } + let mut pcm = ptr::null_mut(); + // SAFETY: the card is live; `pcm` receives the PCM. + let ret = unsafe { bindings::snd_pcm_new(card, c"hpai".as_char_ptr(), 0, 0, 1, &mut pcm) }; + if ret < 0 { + return Err(Error::from_errno(ret)); + } + // SAFETY: `pcm` is live; the ops are static and the private data is + // an `Arc` that `mic_pcm_free` releases. + unsafe { + bindings::snd_pcm_set_ops(pcm, bindings::SNDRV_PCM_STREAM_CAPTURE as i32, &MIC_PCM_OPS); + (*pcm).private_data = data.clone().into_foreign().cast(); + (*pcm).private_free = Some(mic_pcm_free); + copy_str(&mut (*pcm).name, c"Microphone Array"); + } + // SAFETY: the card is live and complete. + let ret = unsafe { bindings::snd_card_register(card) }; + if ret < 0 { + return Err(Error::from_errno(ret)); + } + Ok(this) + } +} + +impl Drop for MicCard { + fn drop(&mut self) { + // SAFETY: created by `snd_card_new`; this waits for open files. + unsafe { bindings::snd_card_free(self.0) }; + } +} + struct T8152AopAudioDriver { dev: ARef, asoc: KBox, registered: AtomicFlag, + /// Held for its drop, which frees the card after unbind. + _mic: Option, } -// SAFETY: the ASoC context is only touched by ASoC callbacks and by probe and -// remove, which the driver core serializes. +// SAFETY: the ASoC context and the card are only touched by ALSA/ASoC +// callbacks and by probe and remove, which the driver core serializes. unsafe impl Send for T8152AopAudioDriver {} // SAFETY: as for `Send`. unsafe impl Sync for T8152AopAudioDriver {} @@ -1069,6 +1415,21 @@ impl platform::Driver for T8152AopAudioDriver { } } + // The microphone array, where the DT describes it: all of its leaves + // or none. + let mut leap = KVec::new(); + for name in LEAP_NAMES { + match PowerDomain::attach(&dev, name) { + Ok(Some(pd)) => leap.push(pd, GFP_KERNEL)?, + Ok(None) => {} + Err(e) => return Err(e), + } + } + if !leap.is_empty() && leap.len() != LEAP_NAMES.len() { + dev_err!(dev, "incomplete microphone power domains\n"); + return Err(ENODEV); + } + let data_dev = dev.clone(); let data = Arc::pin_init( pin_init!(T8152AopAudio { @@ -1081,6 +1442,10 @@ impl platform::Driver for T8152AopAudioDriver { mca, cfg, sense: Atomic::new(ptr::null_mut()), + hpai: Service::new(from_fourcc(b"hpai"), "hpai", false), + leap, + hpai_want_run: AtomicFlag::new(false), + hpai_start_work <- new_work!("T8152AopAudio::hpai_start_work"), }), GFP_KERNEL, )?; @@ -1088,6 +1453,20 @@ impl platform::Driver for T8152AopAudioDriver { data.attach(svc)?; } + // The microphones are optional: the speakers and the jack stay + // usable without them. + let mic = if data.leap.is_empty() { + None + } else { + match MicCard::new(&data) { + Ok(card) => Some(card), + Err(e) => { + dev_err!(dev, "microphone card: {:?}\n", e); + None + } + } + }; + let asoc = AsocContext::new(data)?; // SAFETY: the descriptor and the DAI table are heap allocations owned // by `asoc`, which the driver data keeps until after unregister. @@ -1107,6 +1486,7 @@ impl platform::Driver for T8152AopAudioDriver { dev, asoc, registered: AtomicFlag::new(true), + _mic: mic, }) } From e42e2b87695ab30e8793dd6c26c332c5d2b2fc01 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Thu, 5 Nov 2026 20:11:17 +0100 Subject: [PATCH 15/55] soc: apple: aop: Set the T8152 and T8142 boot-argument fields integration/aurora added the nCal boot argument and word-wise boot-argument access to AopHwConfig. Both machine configurations keep the firmware's own boot arguments unchanged, so set ncal to zero and leave word access off. Signed-off-by: Thomas Glanzmann --- drivers/soc/apple/aop.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index eb99bc4652175..ec863dfde8feb 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -2712,6 +2712,7 @@ const HW_CFG_T8140: AopHwConfig = AopHwConfig { /// ADT aop-exclave-mailbox) is not reachable from the AP: both bus-error. const HW_CFG_T8152: AopHwConfig = AopHwConfig { ec0p: 0, + ncal: 0, aopt: 0, alig: 0, patch_bootargs: false, @@ -2721,6 +2722,7 @@ const HW_CFG_T8152: AopHwConfig = AopHwConfig { quiesce_on_unbind: true, required_endpoints: &[], fw_regions: false, + bootargs_word_access: false, halt_on_failed_start: false, }; @@ -2731,6 +2733,7 @@ const HW_CFG_T8152: AopHwConfig = AopHwConfig { /// firmware runs with the boot arguments iBoot left. const HW_CFG_T8142: AopHwConfig = AopHwConfig { ec0p: 0, + ncal: 0, aopt: 0, alig: 0, patch_bootargs: false, @@ -2740,6 +2743,7 @@ const HW_CFG_T8142: AopHwConfig = AopHwConfig { quiesce_on_unbind: true, required_endpoints: &[], fw_regions: false, + bootargs_word_access: false, halt_on_failed_start: false, }; From f03c37323e5c1dc1352985fde73cc67035eead39 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 7 Nov 2026 01:13:12 +1100 Subject: [PATCH 16/55] arm64: dts: apple: t604x-j614-j616: Describe the boot NOR flash supply Every boot on the J616s logged "spi-nor spi0.0: supply vcc not found, using dummy regulator". The M1, M2, M3, J713 and J700 trees describe the flash's always-on PMU rail as a fixed regulator; the M4 Pro/Max tree came later and has none. Add the same regulator and point the flash at it. Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Assisted-by: LLM --- arch/arm64/boot/dts/apple/t604x-j614-j616.dtsi | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t604x-j614-j616.dtsi b/arch/arm64/boot/dts/apple/t604x-j614-j616.dtsi index 7f12ea2491967..9453cd912127b 100644 --- a/arch/arm64/boot/dts/apple/t604x-j614-j616.dtsi +++ b/arch/arm64/boot/dts/apple/t604x-j614-j616.dtsi @@ -1625,6 +1625,16 @@ }; }; + /* + * The boot NOR runs from a PMU rail that is on whenever the SoC is + * and that no OS switches. The ADT gives the flash no supply. + */ + nor_flash_vcc: regulator-nor-flash-vcc { + compatible = "regulator-fixed"; + regulator-name = "nor-flash-vcc"; + regulator-always-on; + }; + /* * The speaker amps' shared shutdown line (SDZ, AP pin 54, low = * shutdown). One fixed regulator, the SDZ supply of all six, not @@ -1756,6 +1766,7 @@ spi-max-frequency = <10000000>; spi-rx-bus-width = <1>; spi-tx-bus-width = <1>; + vcc-supply = <&nor_flash_vcc>; partitions { compatible = "fixed-partitions"; From 12165bc25529cb20f8fa1a93c996b5b313e40437 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 12:40:04 -0500 Subject: [PATCH 17/55] ASoC: apple: initialize J813 output standby before microphone registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test whether COUT standby supplies the missing cold-boot fabric setup. A muted zero-data jack stream permitted HPAI capture and subsequent capture-only opens on J813; standby without any playback is the next hardware qualification step. Preserve Thomas Glanzmann’s driver and adapt the T8140 DMA sync-stop pattern from Ryan Murray, also flushing queued HPAI work before DMA teardown. No LPAI buffer binding or codec unmute is performed. --- sound/soc/apple/t8152_aop_audio.rs | 34 +++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs index 9976684a8d7d8..9aad831ad7a61 100644 --- a/sound/soc/apple/t8152_aop_audio.rs +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -503,13 +503,17 @@ impl T8152AopAudio { self.run(&self.hpai) } - /// Microphone close, after the channel is released: a queued start - /// finishes first, then `pw0 ` and the leaves. - fn hpai_shutdown(&self) { + /// Finish a queued firmware start before retiring the DMA runtime. + fn flush_hpai_start(&self) { self.hpai_want_run.store(false, Relaxed); // SAFETY: the work item lives in `self`; flushing waits for a queued // `hpai_go`. unsafe { bindings::flush_work(Work::raw_get(&self.hpai_start_work)) }; + } + + /// Microphone close, after the channel is released: `pw0 ` then leaves. + fn hpai_shutdown(&self) { + self.flush_hpai_start(); if self.hpai.running.load(Relaxed) { if let Err(e) = self.set_power(&self.hpai, POWER_STATE_PW0) { dev_err!(self.dev, "hpai: pw0 failed: {:?}\n", e); @@ -1157,12 +1161,27 @@ unsafe extern "C" fn mic_pcm_open(substream: *mut bindings::snd_pcm_substream) - unsafe extern "C" fn mic_pcm_close(substream: *mut bindings::snd_pcm_substream) -> i32 { // SAFETY: by the caller's contract. let data = unsafe { mic_data(substream) }; + data.flush_hpai_start(); // SAFETY: the dmaengine runtime was set up in `mic_pcm_open`. let ret = unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) }; data.hpai_shutdown(); ret } +/// Finish queued firmware work and asynchronous DMA termination before ALSA +/// reuses or frees the capture buffer, as in the T8140 capture path. +/// +/// # Safety +/// +/// Called by ALSA with a live microphone substream and dmaengine runtime. +unsafe extern "C" fn mic_pcm_sync_stop(substream: *mut bindings::snd_pcm_substream) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { mic_data(substream) }; + data.flush_hpai_start(); + // SAFETY: the runtime remains live until close. + unsafe { bindings::snd_dmaengine_pcm_sync_stop(substream) } +} + /// # Safety /// /// Called by ALSA with a live substream of the microphone PCM and live @@ -1243,7 +1262,7 @@ static MIC_PCM_OPS: bindings::snd_pcm_ops = bindings::snd_pcm_ops { hw_free: None, prepare: Some(mic_pcm_prepare), trigger: Some(mic_pcm_trigger), - sync_stop: None, + sync_stop: Some(mic_pcm_sync_stop), pointer: Some(bindings::snd_dmaengine_pcm_pointer), get_time_info: None, fill_silence: None, @@ -1458,7 +1477,12 @@ impl platform::Driver for T8152AopAudioDriver { let mic = if data.leap.is_empty() { None } else { - match MicCard::new(&data) { + // Initialize the shared output service before exposing HPAI. + // This is standby only: no DMA, running state or codec unmute. + // No userspace stream can race this before card registration. + match data.set_power(&data.cout, POWER_STATE_PW0) + .and_then(|_| MicCard::new(&data)) + { Ok(card) => Some(card), Err(e) => { dev_err!(dev, "microphone card: {:?}\n", e); From ad4910ce6ec1aef2b663e9f6fc8af2436a739742 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Fri, 6 Nov 2026 12:44:24 -0500 Subject: [PATCH 18/55] ASoC: apple: validate J813 audio topology and use the default worker queue Describe the T8142 microphone DMA and LEAP power domains and card names in the shared audio binding. Queue sleeping firmware power requests on system_dfl instead of the deprecated system workqueue, as directed by the runtime warning observed during hardware capture tests. --- .../bindings/sound/apple,t8152-aop-audio.yaml | 56 ++++++++++++++++++- sound/soc/apple/t8152_aop_audio.rs | 7 ++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/Documentation/devicetree/bindings/sound/apple,t8152-aop-audio.yaml b/Documentation/devicetree/bindings/sound/apple,t8152-aop-audio.yaml index fa643fe62a9df..b067077a213a1 100644 --- a/Documentation/devicetree/bindings/sound/apple,t8152-aop-audio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,t8152-aop-audio.yaml @@ -4,7 +4,7 @@ $id: http://devicetree.org/schemas/sound/apple,t8152-aop-audio.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Apple T8152 AOP audio +title: Apple T8142 and T8152 AOP audio maintainers: - Thomas Glanzmann @@ -16,26 +16,63 @@ description: allOf: - $ref: dai-common.yaml# + - if: + properties: + compatible: + contains: + const: apple,t8142-aop-audio + then: + properties: + dmas: + minItems: 5 + dma-names: + minItems: 5 + power-domains: + minItems: 9 + power-domain-names: + minItems: 9 + required: + - apple,chassis-name + - apple,machine-kind + else: + properties: + dmas: + maxItems: 4 + dma-names: + maxItems: 4 + power-domains: + maxItems: 6 + power-domain-names: + maxItems: 6 properties: compatible: - const: apple,t8152-aop-audio + oneOf: + - const: apple,t8152-aop-audio + - items: + - const: apple,t8142-aop-audio + - const: apple,t8152-aop-audio dmas: + minItems: 4 items: - description: Headset jack output - description: Headset microphone input - description: Internal speaker output - description: Speaker sense input + - description: Internal microphone array input dma-names: + minItems: 4 items: - const: cout - const: cin - const: spkr - const: sense + - const: hpai power-domains: + minItems: 6 items: - description: Serializer 0 - description: Serializer 1 @@ -43,8 +80,12 @@ properties: - description: Serializer 3 - description: Serializer 4 - description: Serializer 5 + - description: Secure LEAP ADMAC + - description: LEAP receive channel 0 + - description: LEAP serializer power-domain-names: + minItems: 6 items: - const: mca0 - const: mca1 @@ -52,6 +93,17 @@ properties: - const: mca3 - const: mca4 - const: mca5 + - const: leap_s_a + - const: leap_rx0 + - const: leap_mca + + apple,chassis-name: + $ref: /schemas/types.yaml#/definitions/string + description: Chassis identifier used to name the microphone card + + apple,machine-kind: + $ref: /schemas/types.yaml#/definitions/string + description: Human-readable machine family used to name the microphone card "#sound-dai-cells": const: 1 diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs index 9aad831ad7a61..81a224f697491 100644 --- a/sound/soc/apple/t8152_aop_audio.rs +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -1239,7 +1239,7 @@ unsafe extern "C" fn mic_pcm_trigger(substream: *mut bindings::snd_pcm_substream | bindings::SNDRV_PCM_TRIGGER_PAUSE_RELEASE => { data.hpai_want_run.store(true, Relaxed); // Err: already queued, and that run sees `hpai_want_run`. - let _ = workqueue::system().enqueue::<_, HPAI_START_WORK>(Arc::from(data)); + let _ = workqueue::system_dfl().enqueue::<_, HPAI_START_WORK>(Arc::from(data)); } _ => data.hpai_want_run.store(false, Relaxed), } @@ -1477,7 +1477,8 @@ impl platform::Driver for T8152AopAudioDriver { let mic = if data.leap.is_empty() { None } else { - // Initialize the shared output service before exposing HPAI. + // J813 needs an output service initialized before HPAI can raise + // the shared fabric, even when no playback stream is requested. // This is standby only: no DMA, running state or codec unmute. // No userspace stream can race this before card registration. match data.set_power(&data.cout, POWER_STATE_PW0) @@ -1485,7 +1486,7 @@ impl platform::Driver for T8152AopAudioDriver { { Ok(card) => Some(card), Err(e) => { - dev_err!(dev, "microphone card: {:?}\n", e); + dev_err!(dev, "microphone initialization: {:?}\n", e); None } } From 9b4cf0e9aff84039c9ba2cd83c909c4151d89db1 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:06:54 +0100 Subject: [PATCH 19/55] dt-bindings: iommu: apple,sart: Add T8152 compatible The T8152 (M6) SART (ADT sart-version 4) keeps the per-entry configuration, address and size registers in three separate arrays, so it is not compatible with the earlier SARTs. Add apple,t8152-sart. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- Documentation/devicetree/bindings/iommu/apple,sart.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/devicetree/bindings/iommu/apple,sart.yaml b/Documentation/devicetree/bindings/iommu/apple,sart.yaml index e6482453d6dac..021fb3eb5b8f5 100644 --- a/Documentation/devicetree/bindings/iommu/apple,sart.yaml +++ b/Documentation/devicetree/bindings/iommu/apple,sart.yaml @@ -26,6 +26,9 @@ description: SART3, first seen in M1 Pro/Max, extends both the address space and filter entry size to 42 bit. + SART4, found in T8152 (M6), moves the configuration, address and size + registers into separate arrays. + properties: compatible: oneOf: @@ -45,6 +48,7 @@ properties: - apple,t6000-sart - apple,t8015-sart - apple,t8103-sart + - apple,t8152-sart reg: maxItems: 1 From b16eae52b77a9fdc9d32b0a62597fe2c73e184b1 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:06:54 +0100 Subject: [PATCH 20/55] dt-bindings: nvme: apple,nvme-ans: Add T8152 compatible T8152 (M6) uses the secure ANS2 register layout and is not compatible with the earlier controllers. There is no T8152 PMGR provider yet; the loader leaves ANS powered and the driver treats the reset as optional, so do not require resets and power domains for apple,t8152-nvme-ans2. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- .../devicetree/bindings/nvme/apple,nvme-ans.yaml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml b/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml index a6dafcf594289..b0f21ee126b01 100644 --- a/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml +++ b/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml @@ -22,6 +22,7 @@ properties: - apple,t8122-nvme-ans2 - const: apple,t8103-nvme-ans2 - const: apple,t8132-nvme-ans2 + - const: apple,t8152-nvme-ans2 - items: - const: apple,t8132-nvme-ans2 - const: apple,t8103-nvme-ans2 @@ -110,6 +111,18 @@ properties: buffers must remain reserved and protected by the SART. allOf: + - if: + properties: + compatible: + not: + contains: + const: apple,t8152-nvme-ans2 + then: + required: + - resets + - power-domains + - power-domain-names + - if: properties: compatible: @@ -170,9 +183,6 @@ required: - compatible - reg - reg-names - - resets - - power-domains - - power-domain-names - mboxes - interrupts - apple,sart From 359d13aada8142ec35896d9030aa50ba99ddbb32 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:06:54 +0100 Subject: [PATCH 21/55] dt-bindings: mailbox: apple,mailbox: Add T8152 ANS mailbox The T8152 (M6) ANS co-processor is reached through a 0x80 window in the post-office mailbox bank. No interrupts are described for it and the host polls it. Add apple,t8152-ans-mailbox and only require interrupts for the other mailboxes. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- .../bindings/mailbox/apple,mailbox.yaml | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index cd19124e724c3..1a40e2e38786b 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -75,6 +75,11 @@ properties: AOP setup-port mailbox with a separate register layout. const: apple,t8140-aop-setup-mailbox + - description: + T8152 ANS post-office mailbox, a 0x80 window in the post-office + bank. No interrupts are described for it; the host polls it. + const: apple,t8152-ans-mailbox + - description: An older ASC mailbox interface found on T2 and A11 that is also used for the NVMe coprocessor and the system management @@ -148,6 +153,20 @@ allOf: - const: send-empty - const: recv-not-empty + - if: + properties: + compatible: + contains: + const: apple,t8152-ans-mailbox + then: + properties: + interrupts: false + interrupt-names: false + else: + required: + - interrupts + - interrupt-names + - if: properties: compatible: @@ -211,8 +230,6 @@ allOf: required: - compatible - reg - - interrupts - - interrupt-names - "#mbox-cells" additionalProperties: false From 2c87179a0c0888cb5d9d4b80c2df50754662d93f Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 24 Oct 2026 13:30:24 +0200 Subject: [PATCH 22/55] soc: apple: sart: add the T8152 coastguard SART (sart-version 4) Config, paddr and size registers at 0x00, 0x60 and 0xc0 per entry, in 4 KiB units, as m1n1 src/sart.c. Needed by ANS2 on the M6 Mac mini. Signed-off-by: Thomas Glanzmann [Acelogic: Exercised on the ported code by a read-only probe on the M6; write testing was done on Thomas Glanzmann's original J873 series.] Signed-off-by: Acelogic --- drivers/soc/apple/sart.c | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/drivers/soc/apple/sart.c b/drivers/soc/apple/sart.c index 6cedcb867c61b..c44b14c655be8 100644 --- a/drivers/soc/apple/sart.c +++ b/drivers/soc/apple/sart.c @@ -195,6 +195,43 @@ static void sart3_set_entry(struct apple_sart *sart, int index, u8 flags, writel(flags, sart->regs + APPLE_SART3_CONFIG(index)); } +#define APPLE_SART4_CONFIG(idx) (0x00 + 4 * (idx)) +#define APPLE_SART4_PADDR(idx) (0x60 + 4 * (idx)) +#define APPLE_SART4_PADDR_SHIFT 12 +#define APPLE_SART4_SIZE(idx) (0xc0 + 4 * (idx)) +#define APPLE_SART4_SIZE_SHIFT 12 +#define APPLE_SART4_SIZE_MAX GENMASK(29, 0) +#define APPLE_SART4_FLAGS_ALLOW 0xff + +/* T8152 "coastguard" SART (ADT sart-version 4), layout as m1n1 src/sart.c. */ +static void sart4_get_entry(struct apple_sart *sart, int index, u8 *flags, + phys_addr_t *paddr, size_t *size) +{ + phys_addr_t paddr_ = readl(sart->regs + APPLE_SART4_PADDR(index)); + size_t size_ = readl(sart->regs + APPLE_SART4_SIZE(index)); + + *flags = readl(sart->regs + APPLE_SART4_CONFIG(index)); + *size = size_ << APPLE_SART4_SIZE_SHIFT; + *paddr = paddr_ << APPLE_SART4_PADDR_SHIFT; +} + +static void sart4_set_entry(struct apple_sart *sart, int index, u8 flags, + phys_addr_t paddr_shifted, size_t size_shifted) +{ + writel(paddr_shifted, sart->regs + APPLE_SART4_PADDR(index)); + writel(size_shifted, sart->regs + APPLE_SART4_SIZE(index)); + writel(flags, sart->regs + APPLE_SART4_CONFIG(index)); +} + +static struct apple_sart_ops sart_ops_v4 = { + .get_entry = sart4_get_entry, + .set_entry = sart4_set_entry, + .flags_allow = APPLE_SART4_FLAGS_ALLOW, + .size_shift = APPLE_SART4_SIZE_SHIFT, + .paddr_shift = APPLE_SART4_PADDR_SHIFT, + .size_max = APPLE_SART4_SIZE_MAX, +}; + static struct apple_sart_ops sart_ops_v3 = { .get_entry = sart3_get_entry, .set_entry = sart3_set_entry, @@ -409,6 +446,10 @@ static void apple_sart_shutdown(struct platform_device *pdev) } static const struct of_device_id apple_sart_of_match[] = { + { + .compatible = "apple,t8152-sart", + .data = &sart_ops_v4, + }, { .compatible = "apple,t6000-sart", .data = &sart_ops_v3, From 83e97d02765cd2e67204615e97a732c3a681fec5 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 24 Oct 2026 13:30:24 +0200 Subject: [PATCH 23/55] soc: apple: mailbox: add the J873 ANS post-office mailbox Add a polled, interrupt-free provider for the ANS post-office window. ANS uses mailbox 3 of the non-alias post-office bank (0x316010300), a 0x80-byte window with the same counted-FIFO layout as the T8152 SMC mailbox. No interrupt is described for it, so the provider claims none: receive runs on the receive poller of the GFX ASCWrap v8 provider, send polls the FIFO status, and the receive hold/release hooks do nothing for it. It binds only on the J873, at exactly that window. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip, which has no PMP post-office provider: the original made the PMP provider's J873 window check per hardware entry and added ANS next to PMP; here the polled mode is added for the ANS entry alone. Exercised on the ported code by a read-only probe on the M6; write testing was done on Thomas Glanzmann's original J873 series.] Co-developed-by: Acelogic Signed-off-by: Acelogic --- drivers/soc/apple/mailbox.c | 63 +++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/drivers/soc/apple/mailbox.c b/drivers/soc/apple/mailbox.c index c13c03f9d92ab..8e864229d5efe 100644 --- a/drivers/soc/apple/mailbox.c +++ b/drivers/soc/apple/mailbox.c @@ -220,6 +220,10 @@ struct apple_mbox_hw { unsigned int num_ascwrap_v6_roles; /* T8152 GPU ASCWrap v8: wrapper + iop-vbar + post-office window + PSD. */ bool is_ascwrap_v8; + /* No interrupts are described; receive and send are polled. */ + bool rx_poll_required; + /* J873 post-office window a polled provider must sit at. */ + u64 j873_window; /* Status word counts RX occupancy and free TX slots; paired transfers. */ bool counted_fifo; bool ap_initializes_mailboxes; @@ -251,7 +255,7 @@ struct apple_mbox_priv { struct apple_mbox mbox; /* ASCWrap v8 only: the provider's PMGR2 PSD service block (read-only). */ void __iomem *psd_regs; - /* ASCWrap v8 only: receive by polling; fixed at probe. */ + /* ASCWrap v8 and polled providers: receive by polling; fixed at probe. */ bool rx_poll_only; /* rx_lock: recv interrupt enabled and not yet disabled again. */ bool rx_irq_live; @@ -270,7 +274,7 @@ static struct apple_mbox_priv *apple_mbox_to_priv(struct apple_mbox *mbox) /* Only providers allocated by apple_mbox_probe() have a receive poller. */ static bool apple_mbox_has_rx_poller(const struct apple_mbox *mbox) { - return mbox->hw->is_ascwrap_v8; + return mbox->hw->is_ascwrap_v8 || mbox->hw->rx_poll_required; } /* Both ASCWrap generations share the consumer lifecycle API below. */ @@ -1038,12 +1042,17 @@ EXPORT_SYMBOL(apple_mbox_stop); void apple_mbox_hold_rx(struct apple_mbox *mbox) { + /* A polled provider claims no receive interrupt. */ + if (mbox->hw->rx_poll_required) + return; disable_irq(mbox->irq_recv_not_empty); } EXPORT_SYMBOL(apple_mbox_hold_rx); void apple_mbox_release_rx(struct apple_mbox *mbox) { + if (mbox->hw->rx_poll_required) + return; enable_irq(mbox->irq_recv_not_empty); } EXPORT_SYMBOL(apple_mbox_release_rx); @@ -1105,7 +1114,8 @@ static void apple_mbox_quiesce(void *data) if (mbox->irq_send_empty >= 0) synchronize_irq(mbox->irq_send_empty); complete_all(&mbox->tx_empty); - synchronize_irq(mbox->irq_recv_not_empty); + if (mbox->irq_recv_not_empty >= 0) + synchronize_irq(mbox->irq_recv_not_empty); spin_lock_irqsave(&mbox->rx_lock, flags); had_rx = mbox->rx; @@ -1369,6 +1379,16 @@ static int apple_mbox_probe(struct platform_device *pdev) return ret; } else { res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + /* + * A polled provider must be exactly its own 0x80-byte window of + * the J873 post-office bank; the rest belongs to other roles. + */ + if (mbox->hw->rx_poll_required && + (!of_machine_is_compatible("apple,j873g") || !res || + res->start != mbox->hw->j873_window || + resource_size(res) != APPLE_ASCWRAP_V8_MBOX_SIZE)) + return dev_err_probe(dev, -EINVAL, + "post-office window does not match the J873 profile\n"); if (!res || resource_size(res) < mbox->hw->reg_offset + mbox->hw->i2a_recv1 + sizeof(u64)) return dev_err_probe(dev, -EINVAL, @@ -1380,6 +1400,21 @@ static int apple_mbox_probe(struct platform_device *pdev) mbox->regs = (u8 __iomem *)mbox->regs + mbox->hw->reg_offset; } + if (mbox->hw->rx_poll_required) { + mbox->rx_util_min = -1; + mbox->rx_util_min_applied = -1; + spin_lock_init(&mbox->rx_lock); + spin_lock_init(&mbox->tx_lock); + mutex_init(&mbox->lifecycle_lock); + init_completion(&mbox->tx_empty); + priv->rx_poll_only = true; + INIT_DELAYED_WORK(&priv->rx_poll_work, apple_mbox_v8_rx_poll_work); + mbox->irq_recv_not_empty = -ENXIO; + mbox->irq_send_empty = -ENXIO; + dev_info(dev, "J873 post-office: paired transfers, bounded polling, no IRQ claim\n"); + goto setup_pm; + } + mbox->irq_recv_not_empty = platform_get_irq_byname(pdev, "recv-not-empty"); if (mbox->irq_recv_not_empty < 0) @@ -1431,6 +1466,7 @@ static int apple_mbox_probe(struct platform_device *pdev) return ret; } +setup_pm: ret = devm_pm_runtime_enable(dev); if (ret) return ret; @@ -1706,8 +1742,29 @@ static const struct apple_mbox_hw apple_mbox_t8152_smc_hw = { .has_irq_controls = false, }; +/* + * T8152 ANS: msgbox-mailbox-num 3 in the non-alias post-office bank, the + * same counted FIFO as the SMC mailbox. Its interrupt ordering is not known, + * so no interrupt is described and both directions are polled. + */ +static const struct apple_mbox_hw apple_mbox_t8152_ans_hw = { + .rx_poll_required = true, + .j873_window = 0x316010300ULL, + .counted_fifo = true, + .control_full = GENMASK(11, 8), /* free TX slots */ + .control_empty = GENMASK(3, 0), /* RX occupancy */ + .a2i_control = 0x20, + .a2i_send0 = 0x10, + .a2i_send1 = 0x18, + .i2a_control = 0x20, + .i2a_recv0 = 0x00, + .i2a_recv1 = 0x08, + .has_irq_controls = false, +}; + static const struct of_device_id apple_mbox_of_match[] = { { .compatible = "apple,t8152-smc-mailbox", .data = &apple_mbox_t8152_smc_hw }, + { .compatible = "apple,t8152-ans-mailbox", .data = &apple_mbox_t8152_ans_hw }, { .compatible = "apple,t6031-agx-asc-mailbox", .data = &apple_mbox_asc_hw }, { .compatible = "apple,t8122-agx-asc-mailbox", .data = &apple_mbox_asc_hw }, { .compatible = "apple,t8152-gfx-ascwrap-v8", .data = &apple_mbox_t8152_gfx_ascwrap_v8_hw }, From 81bc2a911813a8367a10fde004b7eb2bce28625c Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 24 Oct 2026 13:30:25 +0200 Subject: [PATCH 24/55] nvme: apple: add T8152 (M6) with the secure register layout The M6 ADT marks ANS2 with nvme-secure-reg-layout: as in macOS AppleANS2NVMeController with its 0x92a flag, AQA/ASQ/ACQ and the I/O queue registers move up by 0x4000 and LINEAR_SQ_CTRL moves to 0x2813c; CC/CSTS, doorbells and NVMMU stay. NVMe and NVMMU share one window, and there is no reset provider yet. Reads and writes verified on J873. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip: the T8152 entry joins the T8142 and T6040 ones and sets NVME_QUIRK_BROKEN_FUA like the T8132, T8140 and T8142 entries, and the moved LINEAR_SQ_CTRL is used inside the existing lsq_in_nvmmu check. The read and write verification above was on Thomas Glanzmann's original J873 series; the ported code is exercised by a read-only probe on the M6.] Signed-off-by: Acelogic --- drivers/nvme/host/apple.c | 68 ++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/drivers/nvme/host/apple.c b/drivers/nvme/host/apple.c index e4c9a1ccd38a0..08ec93ad92780 100644 --- a/drivers/nvme/host/apple.c +++ b/drivers/nvme/host/apple.c @@ -65,6 +65,15 @@ #define APPLE_ANS_LINEAR_ASQ_DB 0x2490c #define APPLE_ANS_LINEAR_IOSQ_DB 0x24910 +/* + * ADT nvme-secure-reg-layout (T8152, macOS AppleANS2NVMeController with its + * 0x92a flag set): AQA/ASQ/ACQ and the I/O queue registers move up by + * 0x4000 and LINEAR_SQ_CTRL moves into the NVMMU block. CC/CSTS, the + * doorbells and the NVMMU TCB registers stay where they are. + */ +#define APPLE_ANS_SECURE_QREG_SHIFT 0x4000 +#define APPLE_ANS_SECURE_LINEAR_SQ_CTRL 0x2813c + #define APPLE_NVMMU_NUM_TCBS 0x28100 #define APPLE_NVMMU_ASQ_TCB_BASE 0x28108 #define APPLE_NVMMU_IOSQ_TCB_BASE 0x28110 @@ -208,6 +217,9 @@ struct apple_nvme_hw { bool needs_ioq_registers; /* T8132 needs a PMGR reset when starting a stopped coprocessor. */ bool cold_reset; + /* No reset provider yet; only an adopted live session can come up. */ + bool optional_reset; + bool secure_reg_layout; u32 max_queue_depth; unsigned long quirks; }; @@ -377,6 +389,17 @@ static int apple_nvme_read_handoff(struct apple_nvme *anv) return apple_nvme_can_adopt_rtkit(anv) ? 0 : -EIO; } +static u32 apple_nvme_qreg(const struct apple_nvme *anv, u32 reg) +{ + return anv->hw->secure_reg_layout ? reg + APPLE_ANS_SECURE_QREG_SHIFT : reg; +} + +static u32 apple_nvme_lsq_ctrl(const struct apple_nvme *anv) +{ + return anv->hw->secure_reg_layout ? APPLE_ANS_SECURE_LINEAR_SQ_CTRL : + APPLE_ANS_LINEAR_SQ_CTRL; +} + static inline struct apple_nvme *queue_to_apple_nvme(struct apple_nvme_queue *q) { if (q->is_adminq) @@ -1786,10 +1809,10 @@ static void apple_nvme_reset_work(struct work_struct *work) */ if (!anv->hw->lsq_in_nvmmu) { writel(APPLE_ANS_LINEAR_SQ_EN, - anv->mmio_nvme + APPLE_ANS_LINEAR_SQ_CTRL); + anv->mmio_nvme + apple_nvme_lsq_ctrl(anv)); if (anv->hw->needs_ioq_registers) { u32 lsq_ctrl = readl(anv->mmio_nvme + - APPLE_ANS_LINEAR_SQ_CTRL); + apple_nvme_lsq_ctrl(anv)); if (!(lsq_ctrl & APPLE_ANS_LINEAR_SQ_EN)) { dev_err(anv->dev, @@ -1812,9 +1835,11 @@ static void apple_nvme_reset_work(struct work_struct *work) if (anv->hw->needs_ioq_registers) ioqa_depth--; ioqa = ioqa_depth | (ioqa_depth << 16); - writel(ioqa, anv->mmio_nvme + APPLE_ANS_MAX_PEND_CMDS_CTRL); + writel(ioqa, anv->mmio_nvme + + apple_nvme_qreg(anv, APPLE_ANS_MAX_PEND_CMDS_CTRL)); if (anv->hw->needs_ioq_registers && - readl(anv->mmio_nvme + APPLE_ANS_MAX_PEND_CMDS_CTRL) != ioqa) { + readl(anv->mmio_nvme + + apple_nvme_qreg(anv, APPLE_ANS_MAX_PEND_CMDS_CTRL)) != ioqa) { dev_err(anv->dev, "failed to program I/O queue aperture\n"); ret = -EIO; goto out; @@ -1828,11 +1853,11 @@ static void apple_nvme_reset_work(struct work_struct *work) /* Setup the admin queue */ aqa = APPLE_NVME_AQ_DEPTH - 1; aqa |= aqa << 16; - writel(aqa, anv->mmio_nvme + NVME_REG_AQA); + writel(aqa, anv->mmio_nvme + apple_nvme_qreg(anv, NVME_REG_AQA)); apple_nvme_writeq(anv, anv->adminq.sq_dma_addr, - anv->mmio_nvme + NVME_REG_ASQ); + anv->mmio_nvme + apple_nvme_qreg(anv, NVME_REG_ASQ)); apple_nvme_writeq(anv, anv->adminq.cq_dma_addr, - anv->mmio_nvme + NVME_REG_ACQ); + anv->mmio_nvme + apple_nvme_qreg(anv, NVME_REG_ACQ)); if (anv->hw->has_lsq_nvmmu) { /* Setup NVMMU for both queues */ @@ -1899,10 +1924,10 @@ static void apple_nvme_reset_work(struct work_struct *work) * dedicated registers. Without them it crashes and its crash log * reports null I/O SQ and CQ addresses. */ - apple_nvme_writeq(anv, anv->ioq.cq_dma_addr, - anv->mmio_nvme + APPLE_ANS_IOCQ_REGISTER); - apple_nvme_writeq(anv, anv->ioq.sq_dma_addr, - anv->mmio_nvme + APPLE_ANS_IOSQ_REGISTER); + apple_nvme_writeq(anv, anv->ioq.cq_dma_addr, anv->mmio_nvme + + apple_nvme_qreg(anv, APPLE_ANS_IOCQ_REGISTER)); + apple_nvme_writeq(anv, anv->ioq.sq_dma_addr, anv->mmio_nvme + + apple_nvme_qreg(anv, APPLE_ANS_IOSQ_REGISTER)); } nr_io_queues = 1; if (anv->hw->has_queue_count) { @@ -2393,7 +2418,10 @@ static struct apple_nvme *apple_nvme_alloc(struct platform_device *pdev) goto out_detach_genpd; } - anv->reset = devm_reset_control_array_get_exclusive(anv->dev); + if (anv->hw->optional_reset) + anv->reset = devm_reset_control_array_get_optional_exclusive(anv->dev); + else + anv->reset = devm_reset_control_array_get_exclusive(anv->dev); if (IS_ERR(anv->reset)) { ret = dev_err_probe(dev, PTR_ERR(anv->reset), "Failed to get reset control"); @@ -2754,6 +2782,21 @@ static const struct apple_nvme_hw apple_nvme_t6040_hw = { .quirks = NVME_QUIRK_NO_APST, }; +/* + * T8152 uses the post-M4 queue setup, but its NVMe registers and NVMMU share + * one window (0x325cc0000 on J873) like T8103, with the secure register + * layout. There is no T8152 PMGR driver yet, so the controller has no reset + * line in the DT. + */ +static const struct apple_nvme_hw apple_nvme_t8152_hw = { + .has_lsq_nvmmu = true, + .needs_ioq_registers = true, + .optional_reset = true, + .secure_reg_layout = true, + .max_queue_depth = 64, + .quirks = NVME_QUIRK_BROKEN_FUA, +}; + static const struct of_device_id apple_nvme_of_match[] = { { .compatible = "apple,t6041-nvme-ans2", .data = &apple_nvme_t6040_hw }, { .compatible = "apple,t8015-nvme-ans2", .data = &apple_nvme_t8015_hw }, @@ -2761,6 +2804,7 @@ static const struct of_device_id apple_nvme_of_match[] = { { .compatible = "apple,t8132-nvme-ans2", .data = &apple_nvme_t8132_hw }, { .compatible = "apple,t8140-nvme-ans2", .data = &apple_nvme_t8140_hw }, { .compatible = "apple,t8142-nvme-ans2", .data = &apple_nvme_t8142_hw }, + { .compatible = "apple,t8152-nvme-ans2", .data = &apple_nvme_t8152_hw }, { .compatible = "apple,nvme-ans2", .data = &apple_nvme_t8103_hw }, {}, }; From a400e257002e585430953a2a24082acd1c716f7f Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 15:11:45 +0100 Subject: [PATCH 25/55] nvme: apple: send the shutdown notification on T8152 The post-M4 disable path only clears CC.EN, so the ANS firmware treats every system shutdown as unclean: on the next boot it logs "uncleanReboot yes" and abandons its open bands (host SLC, parity, CLOG). On T8152, send the NVMe shutdown notification (CC.SHN normal, wait for CSTS.SHST complete) before the CC.EN clear on system shutdown. The RTKit co-processor is still left running, as before. Tested on a J873 (Mac mini M6): after a reboot the firmware no longer reports an unclean reboot (0 instead of 4 "uncleanReboot yes" messages). Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip, which already sends the notification at the final system shutdown of every post-M4 controller: T8152 also sends it when the driver is removed, as the original did, but not for system sleep. The test above was on Thomas Glanzmann's original J873 series; the ported code is exercised by a read-only probe on the M6.] Signed-off-by: Acelogic --- drivers/nvme/host/apple.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/drivers/nvme/host/apple.c b/drivers/nvme/host/apple.c index 08ec93ad92780..5d59726edeb13 100644 --- a/drivers/nvme/host/apple.c +++ b/drivers/nvme/host/apple.c @@ -220,6 +220,8 @@ struct apple_nvme_hw { /* No reset provider yet; only an adopted live session can come up. */ bool optional_reset; bool secure_reg_layout; + /* Also send the NVMe shutdown notification (CC.SHN) on removal. */ + bool shutdown_notify; u32 max_queue_depth; unsigned long quirks; }; @@ -1376,10 +1378,13 @@ static int apple_nvme_disable_locked(struct apple_nvme *anv, bool shutdown) * The final system shutdown is the exception for SHN: without it the * controller counts every Linux shutdown as unsafe (SMART * unsafe_shutdowns) and recovers on the next start, which slows the - * loader and the first seconds of I/O. A failed SHN changes nothing: - * the CC.EN clear below still runs. + * loader and the first seconds of I/O. Where the firmware handles it, + * removal sends it too. A failed SHN changes nothing: the CC.EN clear + * below still runs. */ - if (READ_ONCE(anv->final_shutdown) && !apple_rtkit_is_crashed(anv->rtk)) { + if ((READ_ONCE(anv->final_shutdown) || + (shutdown && anv->hw->shutdown_notify && READ_ONCE(anv->removing))) && + !apple_rtkit_is_crashed(anv->rtk)) { ret = nvme_disable_ctrl(&anv->ctrl, true); if (ret) dev_warn(anv->dev, "shutdown notification failed: %d\n", ret); @@ -2793,6 +2798,7 @@ static const struct apple_nvme_hw apple_nvme_t8152_hw = { .needs_ioq_registers = true, .optional_reset = true, .secure_reg_layout = true, + .shutdown_notify = true, .max_queue_depth = 64, .quirks = NVME_QUIRK_BROKEN_FUA, }; From 8eb5187fd12c0b3798aa3a5ca1c9381c662484f1 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 24 Oct 2026 13:30:25 +0200 Subject: [PATCH 26/55] arm64: dts: apple: j873: ANS2 NVMe Mailbox, SART and NVMe nodes for the internal SSD. The driver wakes iBoot's running ANS firmware with a fresh RTKit handshake. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip: the nodes go into t8152-j873-nvme.dtsi, which t8152-j873g.dts includes, instead of a separate t8152-j873-nvme.dtb layered on the USB tree. Exercised on the ported code by a read-only probe on the M6; write testing was done on Thomas Glanzmann's original J873 series.] Signed-off-by: Acelogic --- .../arm64/boot/dts/apple/t8152-j873-nvme.dtsi | 35 +++++++++++++++++++ arch/arm64/boot/dts/apple/t8152-j873g.dts | 1 + 2 files changed, 36 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t8152-j873-nvme.dtsi diff --git a/arch/arm64/boot/dts/apple/t8152-j873-nvme.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-nvme.dtsi new file mode 100644 index 0000000000000..c53059ab55d34 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-nvme.dtsi @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * J873g ANS2 NVMe. + * iBoot leaves the ANS firmware running with the NVMe controller disabled; + * the driver wakes it with a fresh RTKit handshake (as m1n1 does). NVMe + * registers and NVMMU share the 0x325cc0000 window. The mailbox is the ANS + * post-office window (msgbox-mailbox-num 3, no alias: 0x316000000 + + * 0x10300). No PMGR power or reset yet. + * Resources are from the 26A434 ADT: ans, sart-ans. + */ + +/ { + soc { + ans_mbox: mailbox@316010300 { + compatible = "apple,t8152-ans-mailbox"; + reg = <0x3 0x16010300 0x0 0x80>; + #mbox-cells = <0>; + }; + + sart: iommu@325c50000 { + compatible = "apple,t8152-sart"; + reg = <0x3 0x25c50000 0x0 0xc000>; + }; + + nvme: nvme@325cc0000 { + compatible = "apple,t8152-nvme-ans2"; + reg = <0x3 0x25cc0000 0x0 0x60000>, + <0x3 0x21600000 0x0 0x4000>; + reg-names = "nvme", "ans"; + interrupts = ; + mboxes = <&ans_mbox>; + apple,sart = <&sart>; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index 1513a2abbc83f..77e5e1abdf18a 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -286,3 +286,4 @@ #include "t8152-j873-pmc.dtsi" #include "t8152-j873-qspi.dtsi" +#include "t8152-j873-nvme.dtsi" From 9c56293bc07aab63f5b09dd1502bf5e35f0c0425 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 24 Oct 2026 12:35:44 +0200 Subject: [PATCH 27/55] arm64: dts: apple: j873: USB1 (back-left) USB2 host bring-up Describe usb-drd1 as a plain DWC3 host behind its two T8110-style DARTs. The loader powers the controller, wakes the port's HPM (SSPS 0) and initialises the eUSB2 PHY in host mode before Linux starts. Resources are from the 26A434 ADT. A USB keyboard enumerates. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip: the nodes go into t8152-j873-usb.dtsi, which t8152-j873g.dts includes, instead of a separate t8152-j873-usb.dtb. dtbs_check warns that the snps,dwc3 binding allows one iommus entry; the controller sits behind two DARTs, as described here.] Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi | 37 +++++++++++++++++++ arch/arm64/boot/dts/apple/t8152-j873g.dts | 1 + 2 files changed, 38 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi diff --git a/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi new file mode 100644 index 0000000000000..59f0dcf394617 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * J873g USB1 (usb-drd1, back-left port) USB2 host bring-up. + * The loader powers the controller and initialises the eUSB2 PHY in host + * mode before Linux starts (no PMGR, PHY or Type-C driver yet), so the + * controller is described as a plain DWC3 host behind its two DARTs. + * Resources are from the 26A434 ADT: dart-usb1, usb-drd1. + */ + +/ { + soc { + usb1_dart0: iommu@40b020000 { + compatible = "apple,t8152-dart", "apple,t8110-dart"; + reg = <0x4 0x0b020000 0x0 0xc000>; + interrupts = ; + #iommu-cells = <1>; + }; + + usb1_dart1: iommu@40b040000 { + compatible = "apple,t8152-dart", "apple,t8110-dart"; + reg = <0x4 0x0b040000 0x0 0xc000>; + interrupts = ; + #iommu-cells = <1>; + }; + + usb1: usb@40a430000 { + compatible = "snps,dwc3"; + reg = <0x4 0x0a430000 0x0 0xcd00>; + interrupts = ; + dr_mode = "host"; + maximum-speed = "high-speed"; + iommus = <&usb1_dart0 0>, <&usb1_dart1 1>; + snps,dis_u2_susphy_quirk; + snps,dis_u3_susphy_quirk; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index 77e5e1abdf18a..ac38151e0acb0 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -287,3 +287,4 @@ #include "t8152-j873-pmc.dtsi" #include "t8152-j873-qspi.dtsi" #include "t8152-j873-nvme.dtsi" +#include "t8152-j873-usb.dtsi" From 7c13f45712128843ebce56d9238330339162c627 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 24 Oct 2026 12:42:59 +0200 Subject: [PATCH 28/55] arm64: dts: apple: j873: USB2 host for the front ports (usb-drd2) usb-drd2 feeds the on-board VL822 hub (05ac:800f) behind the two front USB-C ports. Describe it like usb-drd1: plain DWC3 host behind its two DARTs (0x383020000/0x383040000, IRQ 1750), controller IRQ 1763. The loader also releases the hub from reset (nub-gpio 39). Signed-off-by: Thomas Glanzmann [Acelogic: the nodes go into t8152-j873-usb.dtsi; dtbs_check gives the same snps,dwc3 iommus warning as for usb-drd1.] Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi index 59f0dcf394617..e5cd25e427fe4 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi +++ b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-2.0+ OR MIT /* - * J873g USB1 (usb-drd1, back-left port) USB2 host bring-up. - * The loader powers the controller and initialises the eUSB2 PHY in host + * J873g USB2 host bring-up: usb-drd1 (back-left port) and usb-drd2 + * (front ports via the on-board hub). + * The loader powers the controllers and initialises the eUSB2 PHYs in host * mode before Linux starts (no PMGR, PHY or Type-C driver yet), so the - * controller is described as a plain DWC3 host behind its two DARTs. - * Resources are from the 26A434 ADT: dart-usb1, usb-drd1. + * controllers are described as plain DWC3 hosts behind their two DARTs. + * Resources are from the 26A434 ADT: dart-usb1/2, usb-drd1/2. */ / { @@ -33,5 +34,31 @@ snps,dis_u2_susphy_quirk; snps,dis_u3_susphy_quirk; }; + + usb2_dart0: iommu@383020000 { + compatible = "apple,t8152-dart", "apple,t8110-dart"; + reg = <0x3 0x83020000 0x0 0xc000>; + interrupts = ; + #iommu-cells = <1>; + }; + + usb2_dart1: iommu@383040000 { + compatible = "apple,t8152-dart", "apple,t8110-dart"; + reg = <0x3 0x83040000 0x0 0xc000>; + interrupts = ; + #iommu-cells = <1>; + }; + + /* usb-drd2: on-board VL822 hub behind the two front USB-C ports. */ + usb2: usb@382430000 { + compatible = "snps,dwc3"; + reg = <0x3 0x82430000 0x0 0xcd00>; + interrupts = ; + dr_mode = "host"; + maximum-speed = "high-speed"; + iommus = <&usb2_dart0 0>, <&usb2_dart1 1>; + snps,dis_u2_susphy_quirk; + snps,dis_u3_susphy_quirk; + }; }; }; From 199c58c9e45672d45f34bbfe12d4b4f737fa3e59 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 09:45:57 +0100 Subject: [PATCH 29/55] PCI: apple: keep T8152 endpoints at the reset Max_Read_Request_Size On T8152 (J873) the on-board RTL8127A transmits much slower with large read requests: r8169 sets MRRS to 4096, which limits 9000-byte frames to 7.0 Gbit/s (UDP, iperf3 to an ixgbe peer). At 2048 it reaches 7.9, at 1024, 512 or 256 the full 9.93 Gbit/s; at MTU 1500 TCP Tx goes from 9.1 to 9.4 Gbit/s and UDP Tx from 7.9 to 9.5. Set no_inc_mrrs on the T8152 host bridge so devices keep the 512-byte reset value and drivers cannot raise it. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- drivers/pci/controller/pcie-apple.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index 3173186c85209..a378955bb922c 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -224,6 +224,12 @@ struct hw_info { * lane PIPE reset status, then release PORT_T8152_PIPE_RST. */ bool pipe_rst; + /* + * Keep endpoints at their reset Max_Read_Request_Size (512 bytes). + * On T8152 an RTL8127 at MRRS 4096 (what r8169 asks for) transmits + * 9000-byte frames at 7.0 Gbit/s; at 512 it reaches 9.93. + */ + bool no_inc_mrrs; }; static const struct hw_info t8103_hw = { @@ -279,6 +285,7 @@ static const struct hw_info t8152_hw = { .max_rid2sid = 512, .refbuf_off = true, .pipe_rst = true, + .no_inc_mrrs = true, }; static const struct hw_info t8103_pciec_hw = { @@ -3116,6 +3123,7 @@ static int apple_pcie_probe(struct platform_device *pdev) pcie = pci_host_bridge_priv(bridge); pcie->dev = dev; pcie->hw = hw; + bridge->no_inc_mrrs = hw->no_inc_mrrs; if (of_property_present(dev->of_node, "apple,managed-sids")) { u32 ids[15]; int count = of_property_count_u32_elems(dev->of_node, "apple,managed-sids"); From 1ef5ecd7bc670f4a678d4698e36ae8a017da7552 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 24 Oct 2026 14:58:10 +0200 Subject: [PATCH 30/55] arm64: dts: apple: j873: apcie port 2 with the on-board 10GbE NIC Describe apcie port 2 next to port 0 (N1): its DART (dart-apcie2), the port with PERST# on AP GPIO 5 and its INTx controller and bus range like the other Apple root ports, and the NIC (MAC address filled in by the loader). The ADT names an aqc107 there; the device has PCI ID 10ec:8127 and is driven by r8169. pcie-apple brings the port up with the T602x port layout plus two steps from macOS: clear PHY_LANE_CFG bit 4 in the refclk handshake (AppleT8152PCIePort::_initializeRefclkBuffer), and after PERST# wait for the lane PIPE reset status (port 0xa8) and clear port 0x82c bit 16 (APCIECoreRCGen5Port::enablePortHardware). The loader must first ungate the AUS5 PHY and write lane-cfg to RC reg 4. Link: Gen3 x1, 10Gbps. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip, where the T8152 hw entry, both macOS steps, the AP GPIO controller and port 0 (N1) already exist: only the device tree part remains, in t8152-j873-ethernet.dtsi, squashed with the original follow-up that describes the port per the binding, and the subject changed to match. Port 2 is bus 2 (RID 0x200 to dart-apcie2 stream 1), and the per-port interrupts are listed up to port 2. A port 2 setup failure (lanes stuck in PIPE reset, for example) fails the whole host, port 0 included, so the aura loader's j873_pcie_prep() (PHY tunables, AUS5 ungate, lane-cfg, dart-apcie0/2 tunables) must run before Linux starts. At runtime the NIC identifies as RTL8125CP (XID 0x708).] Signed-off-by: Acelogic --- .../boot/dts/apple/t8152-j873-ethernet.dtsi | 67 +++++++++++++++++++ arch/arm64/boot/dts/apple/t8152-j873g.dts | 1 + 2 files changed, 68 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi diff --git a/arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi new file mode 100644 index 0000000000000..a2350ed7bee4e --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * J873g internal PCIe (apcie) port 2: the on-board 10GbE NIC (ADT + * /arm-io/apcie/pci-bridge2/lan0; the ADT names an aqc107, the device has + * PCI ID 10ec:8127 and identifies as a Realtek RTL8125CP, XID 0x708). + * iBoot leaves the APCIE power domains on and + * the root complex enabled, but the AUS5 PHY gated and the lanes assigned + * to port 0; the loader ungates the PHY, writes the ADT lane-cfg and applies + * the dart-apcie2 tunables first. Port setup follows the T602x/T8122 + * register layout. Port 0 (N1) is bus 1, port 2 is bus 2. + * Resources are from the 26A434 ADT: apcie, dart-apcie2, gpio0. + */ +#include + +/ { + soc { + pcie2_dart: iommu@38a000000 { + compatible = "apple,t8152-dart", "apple,t8110-dart"; + reg = <0x3 0x8a000000 0x0 0xc000>; + #iommu-cells = <1>; + interrupts = ; + }; + }; +}; + +&pcie { + reg = <0x1c 0xb0000000 0x0 0x1000000>, /* config */ + <0x3 0x8c000000 0x0 0x4000>, /* rc */ + <0x3 0x88028000 0x0 0x4000>, /* port0 */ + <0x3 0x8f010000 0x0 0x4000>, /* phy0 */ + <0x3 0x8a028000 0x0 0x4000>, /* port2 */ + <0x3 0x8f018000 0x0 0x4000>; /* phy2 */ + reg-names = "config", "rc", "port0", "phy0", "port2", "phy2"; + /* One per root port, indexed by the port number. */ + interrupts = , + , + ; + iommu-map = <0x100 &pcie0_dart 1 1>, + <0x101 &pcie0_dart 2 1>, + <0x102 &pcie0_dart 3 1>, + <0x200 &pcie2_dart 1 1>; + bus-range = <0 7>; + + port02: pci@2,0 { + device_type = "pci"; + reg = <0x1000 0x0 0x0 0x0 0x0>; + reset-gpios = <&pinctrl_ap 5 GPIO_ACTIVE_LOW>; + bus-range = <2 2>; + #address-cells = <3>; + #size-cells = <2>; + ranges; + + interrupt-controller; + #interrupt-cells = <1>; + interrupt-map-mask = <0 0 0 7>; + interrupt-map = <0 0 0 1 &port02 0 0 0 0>, + <0 0 0 2 &port02 0 0 0 1>, + <0 0 0 3 &port02 0 0 0 2>, + <0 0 0 4 &port02 0 0 0 3>; + + ethernet0: ethernet@0,0 { + reg = <0x20000 0x0 0x0 0x0 0x0>; + /* Filled in by the loader from the ADT. */ + local-mac-address = [00 00 00 00 00 00]; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index ac38151e0acb0..67c7c9c0d4f44 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -288,3 +288,4 @@ #include "t8152-j873-qspi.dtsi" #include "t8152-j873-nvme.dtsi" #include "t8152-j873-usb.dtsi" +#include "t8152-j873-ethernet.dtsi" From cef0a05d16eccf6b7f4394d66f4666953eb7b6e8 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 22:53:27 +0100 Subject: [PATCH 31/55] arm64: dts: apple: j873: ethernet0 alias Boot loaders (m1n1, aura) fill in the on-board NIC's MAC address through the ethernet0 alias, as on the other Apple machines. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip: the alias goes into t8152-j873-ethernet.dtsi.] Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi index a2350ed7bee4e..66965f04af13d 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi +++ b/arch/arm64/boot/dts/apple/t8152-j873-ethernet.dtsi @@ -13,6 +13,10 @@ #include / { + aliases { + ethernet0 = ðernet0; + }; + soc { pcie2_dart: iommu@38a000000 { compatible = "apple,t8152-dart", "apple,t8110-dart"; From 4d0152993810f42ce8b172ec5484ce2397ce4d15 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:19:57 +0100 Subject: [PATCH 32/55] dt-bindings: watchdog: apple,wdt: Add T8152 compatible The T8152 (M6) watchdog is compatible with the T8103 one. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- Documentation/devicetree/bindings/watchdog/apple,wdt.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/watchdog/apple,wdt.yaml b/Documentation/devicetree/bindings/watchdog/apple,wdt.yaml index 00b0f2ec2bdb4..0a21eb27ad083 100644 --- a/Documentation/devicetree/bindings/watchdog/apple,wdt.yaml +++ b/Documentation/devicetree/bindings/watchdog/apple,wdt.yaml @@ -24,6 +24,7 @@ properties: - apple,t8122-wdt - apple,t8132-wdt - apple,t8140-wdt + - apple,t8152-wdt - const: apple,t8103-wdt - items: - enum: From c9eeda6528376098cc38f3c5ad4cb3ee41c7a0a1 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:19:57 +0100 Subject: [PATCH 33/55] dt-bindings: spmi: apple,spmi: Add T8152 compatible The T8152 (M6) SPMI controller is a fourth generation with its FIFO registers moved to 0x200, so it is not compatible with the T8103 one. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip, which already describes the gen-4 T8142 controller and drives its layout: T8152 takes an apple,t8142-spmi fallback instead of a driver match of its own.] Signed-off-by: Acelogic --- Documentation/devicetree/bindings/spmi/apple,spmi.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/devicetree/bindings/spmi/apple,spmi.yaml b/Documentation/devicetree/bindings/spmi/apple,spmi.yaml index ac2e7407ef780..e504ed9a8871a 100644 --- a/Documentation/devicetree/bindings/spmi/apple,spmi.yaml +++ b/Documentation/devicetree/bindings/spmi/apple,spmi.yaml @@ -38,6 +38,11 @@ properties: - apple,t8132-spmi - apple,t8140-spmi - const: apple,t8103-spmi + - description: + The gen-4 controller of T8152, with its FIFO registers at 0x200. + items: + - const: apple,t8152-spmi + - const: apple,t8142-spmi reg: minItems: 1 From 8c011c1b0d126d784645a3a7f737e614b43df44c Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:19:57 +0100 Subject: [PATCH 34/55] dt-bindings: nvmem: apple,spmi-nvmem: Add woodcote PMIC The primary PMU of T8152 (M6) machines is "woodcote" (ADT pmu-main), with the boot flags in the same scratchpad layout as the other Apple PMICs. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- Documentation/devicetree/bindings/nvmem/apple,spmi-nvmem.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/nvmem/apple,spmi-nvmem.yaml b/Documentation/devicetree/bindings/nvmem/apple,spmi-nvmem.yaml index fe8b2001f4fa5..4a2ba5158b0f6 100644 --- a/Documentation/devicetree/bindings/nvmem/apple,spmi-nvmem.yaml +++ b/Documentation/devicetree/bindings/nvmem/apple,spmi-nvmem.yaml @@ -28,6 +28,7 @@ properties: - apple,simetra-pmic - apple,stowe-pmic - apple,vale-pmic + - apple,woodcote-pmic - const: apple,spmi-nvmem reg: From 5ba4dea4e64d6eeed57f7409999f37a87e10e976 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 14:56:45 +0100 Subject: [PATCH 35/55] mfd: macsmc: register macsmc-reboot on T8152 T8152 registers only macsmc-hwmon. The reboot function only writes the MBSE key, and key writes already work on this SMC. Register macsmc-reboot as well; notifications, GPIO and the power supply stay unregistered. On a J873 (Mac mini M6), reboot then sends MBSE = rest, then phra, and the machine restarts with the boot chime; poweroff sends offw, then off1, and it switches off. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip, whose T8152 hwmon leaves fan control to the firmware: its fan keys are read-only unless the unsafe fan_control parameter is set (in the original, macsmc-hwmon drove the fan through F0md/F0Tg), and the comment says so. The reboot and power off test above was on Thomas Glanzmann's original J873 series.] Signed-off-by: Acelogic --- drivers/mfd/macsmc.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/drivers/mfd/macsmc.c b/drivers/mfd/macsmc.c index 546e5cdbb72ad..2050127a98c57 100644 --- a/drivers/mfd/macsmc.c +++ b/drivers/mfd/macsmc.c @@ -63,9 +63,15 @@ static const struct mfd_cell apple_smc_devs[] = { static const struct mfd_cell apple_smc_low_gpio_dev = MFD_CELL_OF("macsmc-low-gpio", NULL, NULL, 0, 0, "apple,smc-low-gpio"); -/* T8152 currently exposes read-only telemetry; firmware owns fan control. */ +/* + * T8152: temperatures and fans (hwmon; the fan keys are read-only unless the + * unsafe fan_control parameter is set) and power off/restart (reboot, which + * only writes the MBSE key). Notifications, GPIO and the power supply are not + * qualified there yet. + */ static const struct mfd_cell apple_smc_telemetry_devs[] = { MFD_CELL_OF("macsmc-hwmon", NULL, NULL, 0, 0, "apple,smc-hwmon"), + MFD_CELL_OF("macsmc-reboot", NULL, NULL, 0, 0, "apple,smc-reboot"), }; static void apple_smc_read_sram(struct apple_smc *smc, void *buf, size_t size) @@ -546,7 +552,7 @@ static int apple_smc_probe(struct platform_device *pdev) return dev_err_probe(smc->dev, ret, "Failed to get key count"); smc->key_count = be32_to_cpu(count); - /* Notifications, GPIO and power management are not qualified on T8152. */ + /* T8152: key reads and writes are qualified; notifications and GPIO are not. */ if (of_device_is_compatible(dev->of_node, "apple,t8152-smc")) return devm_mfd_add_devices(smc->dev, PLATFORM_DEVID_NONE, apple_smc_telemetry_devs, From 0c2a84ea8f8df23b8318f8b491ac94c0407a482e Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 14:56:45 +0100 Subject: [PATCH 36/55] arm64: dts: apple: j873: SMC power off/restart and the watchdog Describe the SMC reboot function (apple,smc-reboot) and the watchdog (ADT /arm-io/wdt, wdt,t8152, IRQ 447), with a 24 MHz clkref (the ADT timebase-frequency) as on T8103. The PMU NVMEM boot flags are not described yet; macsmc-reboot treats them as optional. The watchdog registers at 24 MHz and provides /dev/watchdog and a restart fallback. Signed-off-by: Thomas Glanzmann [Acelogic: the nodes use the labels of t8152-j873g.dts.] Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873g.dts | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index 67c7c9c0d4f44..36c14293fb6db 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -205,6 +205,15 @@ opp-level = <3>; }; }; + + /* The 24 MHz timebase (ADT timebase-frequency), as on T8103. */ + clkref: clock-ref { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <24000000>; + clock-output-names = "clkref"; + }; + timer { compatible = "arm,armv8-timer"; interrupts = , @@ -241,6 +250,14 @@ #performance-domain-cells = <0>; }; + /* ADT /arm-io/wdt (wdt,t8152): restart fallback and /dev/watchdog. */ + wdt: watchdog@3082b0000 { + compatible = "apple,t8152-wdt", "apple,t8103-wdt"; + reg = <3 0x082b0000 0 0x4000>; + clocks = <&clkref>; + interrupts = ; + }; + smc_mbox: mailbox@30c710080 { compatible = "apple,t8152-smc-mailbox"; reg = <3 0x0c710080 0 0x80>; @@ -257,6 +274,15 @@ reg-names = "smc", "sram"; mboxes = <&smc_mbox>; + /* + * Power off (MBSE = off1) and restart (MBSE = phra). The + * PMU NVMEM boot flags are not described yet; the driver + * treats them as optional. + */ + smc_reboot: reboot { + compatible = "apple,smc-reboot"; + }; + hwmon { compatible = "apple,smc-hwmon"; apple,discover-temperatures; From 6061934c7964cea3f15b6c06498e511d847abfa3 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 15:11:45 +0100 Subject: [PATCH 37/55] arm64: dts: apple: j873: PMU boot flags on SPMI for power off 'off1' only keeps the machine off when the PMU shutdown flag is set; without it the J873 powered back on into m1n1 at once. Describe nub-spmi0 (gen 4, 0x308714000, 16 KiB) and its primary PMU (pmu-main, "woodcote", slave 0xe) as an apple,spmi-nvmem with the boot flags in its legacy scratchpad (ADT info-leg_scrpad = 0xf800, laid out as on T8103 and T8140): boot_stage 0xf801 (read 0x30, kernel started), boot_error_count and panic_count 0xf802, shutdown_flag 0xf80f bit 3. Hand them to macsmc-reboot. Tested: poweroff now leaves the Mac mini off; reboot still restarts. Signed-off-by: Thomas Glanzmann [Acelogic: the controller takes the apple,t8142-spmi fallback of the aurora-wip binding.] Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873g.dts | 59 +++++++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index 36c14293fb6db..180b435f1d969 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -10,6 +10,7 @@ #include #include +#include / { compatible = "apple,j873g", "apple,t8152", "apple,arm-platform"; @@ -250,6 +251,54 @@ #performance-domain-cells = <0>; }; + /* + * ADT /arm-io/nub-spmi0 (gen 4) and its primary PMU, pmu-main + * ("woodcote", slave 0xe). The boot flags live in the legacy + * scratchpad (info-leg_scrpad = 0xf800), laid out as on T8103 + * and T8140; boot_stage read 0x30 (kernel started) here. + */ + nub_spmi0: spmi@308714000 { + /* + * Gen 4: FIFO registers at 0x200 (the block is 0x4000 + * long); polled, so no interrupts are described yet. + */ + compatible = "apple,t8152-spmi", "apple,t8142-spmi"; + reg = <0x3 0x08714000 0x0 0x4000>; + #address-cells = <2>; + #size-cells = <0>; + + pmic@e { + compatible = "apple,woodcote-pmic", "apple,spmi-nvmem"; + reg = <0xe SPMI_USID>; + + nvmem-layout { + compatible = "fixed-layout"; + #address-cells = <1>; + #size-cells = <1>; + + boot_stage: boot-stage@f801 { + reg = <0xf801 0x1>; + }; + + boot_error_count: boot-error-count@f802,0 { + reg = <0xf802 0x1>; + bits = <0 4>; + }; + + panic_count: panic-count@f802,4 { + reg = <0xf802 0x1>; + bits = <4 4>; + }; + + /* Set before off1, or the machine powers back on. */ + shutdown_flag: shutdown-flag@f80f,3 { + reg = <0xf80f 0x1>; + bits = <3 1>; + }; + }; + }; + }; + /* ADT /arm-io/wdt (wdt,t8152): restart fallback and /dev/watchdog. */ wdt: watchdog@3082b0000 { compatible = "apple,t8152-wdt", "apple,t8103-wdt"; @@ -274,13 +323,13 @@ reg-names = "smc", "sram"; mboxes = <&smc_mbox>; - /* - * Power off (MBSE = off1) and restart (MBSE = phra). The - * PMU NVMEM boot flags are not described yet; the driver - * treats them as optional. - */ + /* Power off (MBSE = off1) and restart (MBSE = phra). */ smc_reboot: reboot { compatible = "apple,smc-reboot"; + nvmem-cells = <&shutdown_flag>, <&boot_stage>, + <&boot_error_count>, <&panic_count>; + nvmem-cell-names = "shutdown_flag", "boot_stage", + "boot_error_count", "panic_count"; }; hwmon { From edd345ad332ce915af32cbb99ae537c50e635fbf Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:44:53 +0100 Subject: [PATCH 38/55] dt-bindings: arm: apple: apple,pmgr: Add T8152 compatible The T8152 (M6) PMGR blocks are compatible with the T8103 ones. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- Documentation/devicetree/bindings/arm/apple/apple,pmgr.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/arm/apple/apple,pmgr.yaml b/Documentation/devicetree/bindings/arm/apple/apple,pmgr.yaml index 5d9f55c3e5899..b059430cee63d 100644 --- a/Documentation/devicetree/bindings/arm/apple/apple,pmgr.yaml +++ b/Documentation/devicetree/bindings/arm/apple/apple,pmgr.yaml @@ -45,6 +45,7 @@ properties: - apple,t8132-pmgr - apple,t8140-pmgr - apple,t8142-pmgr + - apple,t8152-pmgr - const: apple,t8103-pmgr - const: syscon - const: simple-mfd From 2c943f48e8d467f0f8119f61fcdddae8c2270fb0 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:44:53 +0100 Subject: [PATCH 39/55] dt-bindings: power: apple,pmgr-pwrstate: Add T8152 compatible The T8152 (M6) PMGR power state registers are compatible with the T8103 ones. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml b/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml index 1c3bd5a5b5d2c..eeaf5f20827f9 100644 --- a/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml +++ b/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml @@ -69,6 +69,7 @@ properties: - apple,t8132-pmgr-pwrstate - apple,t8140-pmgr-pwrstate - apple,t8142-pmgr-pwrstate + - apple,t8152-pmgr-pwrstate - const: apple,t8103-pmgr-pwrstate reg: From 5e49fec7858a20e35fcf5c9c335c265aecec3513 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:44:54 +0100 Subject: [PATCH 40/55] dt-bindings: i2c: apple,i2c: Add T8152 compatible The T8152 (M6) I2C controllers are compatible with the T8103 ones. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- Documentation/devicetree/bindings/i2c/apple,i2c.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/i2c/apple,i2c.yaml b/Documentation/devicetree/bindings/i2c/apple,i2c.yaml index 5c827af153cb5..f117341886ca5 100644 --- a/Documentation/devicetree/bindings/i2c/apple,i2c.yaml +++ b/Documentation/devicetree/bindings/i2c/apple,i2c.yaml @@ -30,6 +30,7 @@ properties: - apple,t8122-i2c - apple,t8132-i2c - apple,t8140-i2c + - apple,t8152-i2c - const: apple,t8103-i2c - items: - enum: From 523a1caa53ea200de6bdd23aebf912d8149f26af Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:44:53 +0100 Subject: [PATCH 41/55] dt-bindings: mailbox: apple,mailbox: Add T8152 ASC wrapper mailbox The T8152 (M6) SMC and AOP mailboxes are ASC wrapper v8 instances, described with two interrupts (send-empty, recv-not-empty) like the T8140 AOP setup port, but without an IOMMU or DMA window. Add apple,t8152-ascwrap-v8-mailbox and split the conditions into interrupt layout and setup-port resources. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip, where the SMC mailbox already has its own apple,t8152-smc-mailbox compatible with this interrupt layout: the new compatible is described for the AOP mailbox and joins that interrupt condition. The T8140 setup-port conditions are unchanged.] Signed-off-by: Acelogic --- .../devicetree/bindings/mailbox/apple,mailbox.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index 1a40e2e38786b..5c112f77a53a2 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -27,6 +27,12 @@ properties: masking provided by the interrupt controller. const: apple,t8152-smc-mailbox + - description: + T8152 ASC wrapper (v8) mailbox of the AOP, with the counted + FIFOs of the SMC mailbox, described with its send-empty and + recv-not-empty interrupts. + const: apple,t8152-ascwrap-v8-mailbox + - description: ASC mailboxes are the most common variant found on the M1 used for example for the display controller, the system management @@ -142,7 +148,9 @@ allOf: properties: compatible: contains: - const: apple,t8152-smc-mailbox + enum: + - apple,t8152-ascwrap-v8-mailbox + - apple,t8152-smc-mailbox then: properties: interrupts: @@ -215,6 +223,8 @@ allOf: - apple,t6021-ane-mailbox - apple,t8112-ane-mailbox - apple,t8140-aop-setup-mailbox + - apple,t8152-ans-mailbox + - apple,t8152-ascwrap-v8-mailbox - apple,t8152-smc-mailbox then: properties: From d237a9aa748912da5ce1d383decb585bf2639026 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:44:53 +0100 Subject: [PATCH 42/55] dt-bindings: dma: apple,admac: Add T8152 compatible The T8152 (M6) ADMAC (ADT admac-evolution,t8152) has its own register layout, so it is not compatible with the earlier ones. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- Documentation/devicetree/bindings/dma/apple,admac.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/dma/apple,admac.yaml b/Documentation/devicetree/bindings/dma/apple,admac.yaml index 64d80cd5f4bcd..17e3f37e673c5 100644 --- a/Documentation/devicetree/bindings/dma/apple,admac.yaml +++ b/Documentation/devicetree/bindings/dma/apple,admac.yaml @@ -44,6 +44,7 @@ properties: - items: - const: apple,t6041-admac - const: apple,t8132-admac + - const: apple,t8152-admac reg: maxItems: 1 From f1ce875e034a3449bf08a60b2e29729f6826c738 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:16:37 +0100 Subject: [PATCH 43/55] dt-bindings: iommu: apple,dart: add apple,retain-translation On T8152 iBoot hands dart-sbr over translating for the AOP and the audio ADMAC without setting the lock bit, and the AOP firmware keeps using the inherited mappings. Describe the property that tells the OS to inherit such a DART instead of resetting it, and who owns which root entries. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- .../devicetree/bindings/iommu/apple,dart.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Documentation/devicetree/bindings/iommu/apple,dart.yaml b/Documentation/devicetree/bindings/iommu/apple,dart.yaml index 514b17641bf5b..9968b519a6db7 100644 --- a/Documentation/devicetree/bindings/iommu/apple,dart.yaml +++ b/Documentation/devicetree/bindings/iommu/apple,dart.yaml @@ -169,6 +169,17 @@ properties: and filtering state without a separate page-table root. Source and destination must differ, and both must exist on the controller. + apple,retain-translation: + type: boolean + description: > + The bootloader hands this DART over translating, with stream page + tables that a co-processor's firmware keeps using, but without setting + the hardware lock. The operating system then treats the DART as locked: + it does not reset it or replace the inherited stream roots, adds its + own mappings only in root entries that are empty at handoff, and on + detach clears only the entries it added. The entries present at handoff + stay owned by the firmware, which may keep using them at any time. + required: - compatible - reg From 76188ec353b94e2274566d0f736954897d8c54a9 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sat, 7 Nov 2026 21:53:37 -0500 Subject: [PATCH 44/55] soc: apple: mailbox: match the T8152 AOP ASC wrapper mailbox The T8152 AOP mailbox (ADT aop, iop,ascwrap-v8, mailbox 0 of the bank at 0x310740000) is the same counted FIFO as the SMC mailbox, with the same send-empty and recv-not-empty interrupts. Match apple,t8152-ascwrap-v8-mailbox, the compatible of the J873 AOP mailbox node, to the SMC mailbox layout. Signed-off-by: Acelogic --- drivers/soc/apple/mailbox.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/soc/apple/mailbox.c b/drivers/soc/apple/mailbox.c index 8e864229d5efe..4f39be303968a 100644 --- a/drivers/soc/apple/mailbox.c +++ b/drivers/soc/apple/mailbox.c @@ -1764,6 +1764,8 @@ static const struct apple_mbox_hw apple_mbox_t8152_ans_hw = { static const struct of_device_id apple_mbox_of_match[] = { { .compatible = "apple,t8152-smc-mailbox", .data = &apple_mbox_t8152_smc_hw }, + /* The AOP's ASC wrapper mailbox has the SMC mailbox's counted FIFOs. */ + { .compatible = "apple,t8152-ascwrap-v8-mailbox", .data = &apple_mbox_t8152_smc_hw }, { .compatible = "apple,t8152-ans-mailbox", .data = &apple_mbox_t8152_ans_hw }, { .compatible = "apple,t6031-agx-asc-mailbox", .data = &apple_mbox_asc_hw }, { .compatible = "apple,t8122-agx-asc-mailbox", .data = &apple_mbox_asc_hw }, From 613be379ab9198402eecc007409a65e6c29fad81 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:31 +0100 Subject: [PATCH 45/55] iommu: apple-dart: inherit an unlocked bootloader translation on request On T8152 iBoot leaves dart-sbr translating for the AOP and the audio ADMAC: streams 0, 1 and 42 have live page tables, and the firmware's own mappings sit in root slot 16 of each. Unlike the DARTs SPTM locks, the lock bit is not set, so the driver would reset the DART and pull the tables out from under the AOP firmware. With apple,retain-translation the DART is treated as locked: the inherited roots are kept and Linux publishes its mappings into vacant root slots. Signed-off-by: Thomas Glanzmann Signed-off-by: Acelogic --- drivers/iommu/apple-dart.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index d39ee78d9d73c..94c79671db6fd 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -3976,6 +3976,14 @@ static irqreturn_t apple_dart_irq(int irq, void *dev) static bool apple_dart_is_locked(struct apple_dart *dart) { + /* + * apple,retain-translation: the bootloader left live stream tables + * that a co-processor's firmware depends on, but did not set the lock + * bit (T8152 dart-sbr: AOP and audio ADMAC streams). Treat it as + * locked so the tables are inherited rather than reset. + */ + if (of_property_read_bool(dart->dev->of_node, "apple,retain-translation")) + return true; return !!(apple_dart_readl(dart, dart->hw->lock) & dart->hw->lock_bit); } From 5bef582af4beafab3bd4c02e85ec133253f0111c Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:31 +0100 Subject: [PATCH 46/55] dmaengine: apple-admac: support the T8152 layout The T8152 ADMAC (ADT admac-evolution,t8152; worked out from the macOS AudioDMAController-T8152 driver and on hardware) keeps the per-channel register block and the global start/stop/interrupt registers, but: - the channel blocks are 0x4000 apart, TX n at 0x40000 + 0x4000 n and RX n at 0x80000 + 0x4000 n; - a channel is put in state 2 (+0x30 bits 3:0) and +0x4 is written 0x029b0000 before it is started, as macOS does; - the last descriptor word carries a descriptor id 1..255 in its low byte, which macOS never leaves at 0; - the SRAM size registers carry the channel count in bits 31:24, and the AP's carveouts start above the firmware's (base 0x1000, 1 KiB blocks); - only the word size is set in the bus width register. The descriptor and report FIFO ports are the legacy ones: the RX descriptor port answers at 0x14000 + 4n, not at the 0x10040 + 4n the ADT lists. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip: the descriptor id is set in admac_write_hw_desc(), which writes every descriptor there, the SRAM carveout bounds combine with the T8132 split-channel handling, and the state register is the +0x30 one that aurora-wip programs as REG_CHAN_BURST on T8132.] Signed-off-by: Acelogic --- drivers/dma/apple-admac.c | 80 +++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/drivers/dma/apple-admac.c b/drivers/dma/apple-admac.c index 60b8c04530634..bdafb0ff4aecb 100644 --- a/drivers/dma/apple-admac.c +++ b/drivers/dma/apple-admac.c @@ -70,13 +70,29 @@ */ #define REG_CHAN_BASE_LEGACY(ch) (0x8000 + (ch) * 0x200) #define REG_CHAN_BASE_EVOLVED(ch) (((ch) & 1 ? 0xc000 : 0x8000) + ((ch) / 2) * 0x200) -#define REG_CHAN_BASE(ad, ch) ((ad)->evolved_layout ? \ +/* + * T8152 (ADT admac-evolution,t8152; the macOS AudioDMAController-T8152 + * driver): the same per-channel block, 0x4000 apart, TX at 0x40000 and RX + * at 0x80000, and a channel has to be put in state 2 (+0x30 bits 3:0) + * before it is started. The descriptor and report FIFO ports are the legacy + * ones (the RX descriptor port answers at 0x14000 + 4n, not at the ADT's + * 0x10040 + 4n). + */ +#define REG_CHAN_BASE_T8152(ch) (((ch) & 1 ? 0x80000 : 0x40000) + ((ch) / 2) * 0x4000) +#define REG_CHAN_BASE(ad, ch) ((ad)->t8152_layout ? REG_CHAN_BASE_T8152(ch) : \ + (ad)->evolved_layout ? \ REG_CHAN_BASE_EVOLVED(ch) : REG_CHAN_BASE_LEGACY(ch)) #define REG_CHAN_CTL(ad, ch) (REG_CHAN_BASE(ad, ch) + 0x00) #define REG_CHAN_CTL_RST_RINGS BIT(0) #define REG_CHAN_CONFIG(ad, ch) (REG_CHAN_BASE(ad, ch) + 0x04) +/* macOS writes this for the T8152 base-ns channels before every start */ +#define CHAN_CONFIG_T8152 0x029b0000 #define REG_CHAN_BURST(ad, ch) (REG_CHAN_BASE(ad, ch) + 0x30) +/* T8152: the low nibble of the same register is the channel state */ +#define REG_CHAN_STATE(ad, ch) REG_CHAN_BURST(ad, ch) +#define CHAN_STATE_MASK GENMASK(3, 0) +#define CHAN_STATE_READY 2 #define REG_DESC_RING(ad, ch) (REG_CHAN_BASE(ad, ch) + 0x70) #define REG_REPORT_RING(ad, ch) (REG_CHAN_BASE(ad, ch) + 0x74) @@ -104,6 +120,11 @@ #define REG_DESC_WRITE(ch) (0x10000 + ((ch) / 2) * 0x4 + ((ch) & 1) * 0x4000) #define REG_REPORT_READ(ch) (0x10100 + ((ch) / 2) * 0x4 + ((ch) & 1) * 0x4000) +/* T8152: the AP's share of each SRAM pool starts above the firmware's. */ +#define T8152_SRAM_BASE 0x1000 +#define T8152_SRAM_BLOCK 1024 +#define SRAM_SIZE_MASK GENMASK(15, 0) + #define REG_TX_INTSTATE(idx) (0x0030 + (idx) * 4) #define REG_RX_INTSTATE(idx) (0x0040 + (idx) * 4) #define REG_GLOBAL_INTSTATE(idx) (0x0050 + (idx) * 4) @@ -124,6 +145,8 @@ struct admac_chan { struct tasklet_struct tasklet; u32 carveout; + /* T8152: transfer descriptor id, 1..255 */ + u8 desc_id; spinlock_t lock; struct admac_tx *current_tx; @@ -164,6 +187,7 @@ struct admac_data { bool set_unk28; bool evolved_layout; bool split_channels; + bool t8152_layout; bool ctl_programmed; int irq; int irq_index; @@ -193,6 +217,7 @@ struct admac_hw { bool set_unk28; bool evolved_layout; bool split_channels; + bool t8152_layout; }; static int admac_alloc_sram_carveout(struct admac_data *ad, @@ -201,6 +226,8 @@ static int admac_alloc_sram_carveout(struct admac_data *ad, { struct admac_sram *sram; int i, ret = 0, nblocks; + u32 first = 0, block = SRAM_BLOCK; + ad->txcache.size = readl_relaxed(ad->base + REG_TX_SRAM_SIZE); ad->rxcache.size = readl_relaxed(ad->base + REG_RX_SRAM_SIZE); /* T8132 reports channel metadata in the upper half of these words. */ @@ -208,6 +235,13 @@ static int admac_alloc_sram_carveout(struct admac_data *ad, ad->txcache.size &= GENMASK(15, 0); ad->rxcache.size &= GENMASK(15, 0); } + if (ad->t8152_layout) { + /* bits 31:24 are the channel count */ + ad->txcache.size &= SRAM_SIZE_MASK; + ad->rxcache.size &= SRAM_SIZE_MASK; + first = T8152_SRAM_BASE; + block = T8152_SRAM_BLOCK; + } if (dir == DMA_MEM_TO_DEV) sram = &ad->txcache; @@ -216,14 +250,14 @@ static int admac_alloc_sram_carveout(struct admac_data *ad, mutex_lock(&ad->cache_alloc_lock); - nblocks = sram->size / SRAM_BLOCK; + nblocks = sram->size > first ? (sram->size - first) / block : 0; for (i = ad->split_channels ? 1 : 0; i < nblocks; i++) if (!(sram->allocated & BIT(i))) break; if (i < nblocks) { - *out = FIELD_PREP(CHAN_SRAM_CARVEOUT_BASE, i * SRAM_BLOCK) | - FIELD_PREP(CHAN_SRAM_CARVEOUT_SIZE, SRAM_BLOCK); + *out = FIELD_PREP(CHAN_SRAM_CARVEOUT_BASE, first + i * block) | + FIELD_PREP(CHAN_SRAM_CARVEOUT_SIZE, block); sram->allocated |= BIT(i); } else { ret = -EBUSY; @@ -240,18 +274,24 @@ static void admac_free_sram_carveout(struct admac_data *ad, { struct admac_sram *sram; u32 base = FIELD_GET(CHAN_SRAM_CARVEOUT_BASE, carveout); + u32 first = 0, block = SRAM_BLOCK; int i; + if (ad->t8152_layout) { + first = T8152_SRAM_BASE; + block = T8152_SRAM_BLOCK; + } + if (dir == DMA_MEM_TO_DEV) sram = &ad->txcache; else sram = &ad->rxcache; - if (WARN_ON(base >= sram->size)) + if (WARN_ON(base < first || base >= sram->size)) return; mutex_lock(&ad->cache_alloc_lock); - i = base / SRAM_BLOCK; + i = (base - first) / block; sram->allocated &= ~BIT(i); mutex_unlock(&ad->cache_alloc_lock); } @@ -354,7 +394,19 @@ static void admac_write_hw_desc(struct admac_data *ad, int channo, writel_relaxed(lower_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); writel_relaxed(upper_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); writel_relaxed(len, ad->base + REG_DESC_WRITE(channo)); - writel_relaxed(FLAG_DESC_NOTIFY, ad->base + REG_DESC_WRITE(channo)); + if (ad->t8152_layout) { + /* + * T8152: the low byte of the last word is a descriptor id, + * which macOS counts 1..255 and never leaves at 0. + */ + struct admac_chan *adchan = &ad->channels[channo]; + + adchan->desc_id = adchan->desc_id == 255 ? 1 : adchan->desc_id + 1; + writel_relaxed(FLAG_DESC_NOTIFY | adchan->desc_id, + ad->base + REG_DESC_WRITE(channo)); + } else { + writel_relaxed(FLAG_DESC_NOTIFY, ad->base + REG_DESC_WRITE(channo)); + } } /* @@ -570,6 +622,11 @@ static void admac_start_chan(struct admac_chan *adchan) ad->base + REG_CHAN_INTSTATUS(ad, adchan->no, ad->irq_index)); writel_relaxed(STATUS_DESC_DONE | STATUS_ERR, ad->base + REG_CHAN_INTMASK(ad, adchan->no, ad->irq_index)); + if (ad->t8152_layout) { + writel_relaxed(CHAN_CONFIG_T8152, ad->base + REG_CHAN_CONFIG(ad, adchan->no)); + admac_modify(ad, REG_CHAN_STATE(ad, adchan->no), CHAN_STATE_MASK, + CHAN_STATE_READY); + } switch (admac_chan_direction(adchan->no)) { case DMA_MEM_TO_DEV: @@ -1091,7 +1148,8 @@ static int admac_device_config(struct dma_chan *chan, * of words position in a frame. That's where the importance of this control * comes from. */ - switch (is_tx ? config->dst_port_window_size : config->src_port_window_size) { + switch (ad->t8152_layout ? 0 : + is_tx ? config->dst_port_window_size : config->src_port_window_size) { case 0 ... 1: break; case 2: @@ -1202,6 +1260,7 @@ static int admac_probe(struct platform_device *pdev) ad->set_unk28 = hw->set_unk28; ad->evolved_layout = hw->evolved_layout; ad->split_channels = hw->split_channels; + ad->t8152_layout = hw->t8152_layout; mutex_init(&ad->cache_alloc_lock); /* @@ -1353,7 +1412,12 @@ static const struct admac_hw admac_t8140_hw = { .evolved_layout = true, }; +static const struct admac_hw admac_t8152_hw = { + .t8152_layout = true, +}; + static const struct of_device_id admac_of_match[] = { + { .compatible = "apple,t8152-admac", .data = &admac_t8152_hw }, { .compatible = "apple,t8140-admac", .data = &admac_t8140_hw }, { .compatible = "apple,t8132-admac", .data = &admac_t8132_hw }, { .compatible = "apple,t8122-admac", .data = &admac_t8122_hw }, From dd923f2443834d36260286aeb9c27355db9aae05 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:32 +0100 Subject: [PATCH 47/55] arm64: dts: apple: j873: audio Describe the J873 (Mac mini M6) audio: the AOP (mailbox 0x310740080, firmware started by Linux), dart-sbr (shared with the PMP; apple,retain-translation for the firmware's tables), the base-ns ADMAC, the AOP audio node with its DMA channels and MCA power leaves, the I2C1/I2C3 power states and buses with the CS42L84 jack codec and the SN012776 speaker amp (ti,bclk-ratio = <250>), and the macaudio card. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip: the nodes go into t8152-j873-audio.dtsi, which t8152-j873g.dts includes. There is no PMP device tree here, so dart-sbr is described directly (as an apple,t8152-dart with apple,t8110-dart fallback, like the other J873 DARTs), and the I2C power states join the PMGR node that t8152-j873-qspi.dtsi already describes. The speaker stays off by default: the J873 macaudio configuration does not admit it.] Signed-off-by: Acelogic --- .../boot/dts/apple/t8152-j873-audio.dtsi | 261 ++++++++++++++++++ arch/arm64/boot/dts/apple/t8152-j873g.dts | 1 + 2 files changed, 262 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t8152-j873-audio.dtsi diff --git a/arch/arm64/boot/dts/apple/t8152-j873-audio.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-audio.dtsi new file mode 100644 index 0000000000000..0ecfd9746beb9 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-audio.dtsi @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * J873g audio: the AOP (always-on processor) owns the audio fabric, as + * on T8140. iBoot loads the AOP firmware (j873gaop) and leaves the CPU + * stopped; Linux starts it. The jack codec (CS42L84) and the speaker amp + * (SN012776) are on AP I2C buses; their data goes through AOP services on + * the base-ns ADMAC. Resources are from the 26A434 ADT: aop, dart-sbr, + * admac-base-ns, iop-audio-controller, i2c1, i2c3, pmgr. + */ +#include + +/ { + soc { + /* + * ADT aop: iop,ascwrap-v8, msgbox-mailbox-num 0, using-alias, + * not post-office: mailbox 0, aperture 1 of the bank at reg[2] + * (0x310740000) = 0x310740080. ADT interrupts [627, 626, 628]; + * the first two in the SMC order (send-empty, recv-not-empty). + */ + aop_mbox: mailbox@310740080 { + compatible = "apple,t8152-ascwrap-v8-mailbox"; + reg = <0x3 0x10740080 0x0 0x80>; + interrupts = , + ; + interrupt-names = "send-empty", "recv-not-empty"; + #mbox-cells = <0>; + }; + + /* + * ADT dart-sbr, shared with the PMP: mapper-aop 0, + * mapper-admac-base-ns 42. iBoot leaves streams 0, 1 and 42 + * translating (root slot 16 owned by the firmware) without + * setting the lock bit. + */ + sbr_dart: iommu@318100000 { + compatible = "apple,t8152-dart", "apple,t8110-dart"; + reg = <0x3 0x18100000 0x0 0xc000>; + #iommu-cells = <1>; + interrupts = ; + apple,retain-translation; + }; + + /* + * ADT admac-base-ns (admac-evolution,t8152): 13 TX and 13 RX + * channels, interrupt destination 0, dart-sbr stream 42. + */ + admac_base_ns: dma-controller@313900000 { + compatible = "apple,t8152-admac"; + reg = <0x3 0x13900000 0x0 0xb4000>; + dma-channels = <26>; + interrupts-extended = <&aic AIC_IRQ 640 IRQ_TYPE_LEVEL_HIGH>, + <0>, <0>, <0>; + #dma-cells = <1>; + iommus = <&sbr_dart 42>; + /* slot 17: slot 16 of stream 42 belongs to the firmware */ + apple,dma-range = <0x110 0x0 0x0 0x10000000>; + }; + + /* + * The AOP's PMGR (ADT pmgr devices AUDIO_MCA0_M..AUDIO_MCA5_M at + * 0x308284400 + 0x20 * N). Their parent audio_p is the + * firmware's: it is up while a service is in pw0 or pwrd. + */ + pmgr_aop: power-management@308284000 { + compatible = "apple,t8152-pmgr", "apple,t8103-pmgr", + "syscon", "simple-mfd"; + #address-cells = <1>; + #size-cells = <1>; + reg = <0x3 0x08284000 0x0 0x1000>; + + ps_audio_mca0_m: power-controller@400 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0x400 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "audio_mca0_m"; + }; + + ps_audio_mca1_m: power-controller@420 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0x420 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "audio_mca1_m"; + }; + + ps_audio_mca2_m: power-controller@440 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0x440 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "audio_mca2_m"; + }; + + ps_audio_mca3_m: power-controller@460 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0x460 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "audio_mca3_m"; + }; + + ps_audio_mca4_m: power-controller@480 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0x480 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "audio_mca4_m"; + }; + + ps_audio_mca5_m: power-controller@4a0 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0x4a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "audio_mca5_m"; + }; + }; + + /* ADT i2c1: the CS42L84 jack codec (and the aqc NIC's EEPROM). */ + i2c1: i2c@331014000 { + compatible = "apple,t8152-i2c", "apple,t8103-i2c"; + reg = <0x3 0x31014000 0x0 0x4000>; + clocks = <&clkref>; + interrupts = ; + #address-cells = <1>; + #size-cells = <0>; + power-domains = <&ps_i2c1>; + + /* + * ADT audio-codec-output: function-reset GPIO 0xcc, + * interrupt on gpio0 pin 203. + */ + jack_codec: codec@4b { + compatible = "cirrus,cs42l84"; + reg = <0x4b>; + reset-gpios = <&pinctrl_ap 204 GPIO_ACTIVE_HIGH>; + interrupts-extended = <&pinctrl_ap 203 IRQ_TYPE_LEVEL_LOW>; + #sound-dai-cells = <0>; + sound-name-prefix = "Jack"; + }; + }; + + /* ADT i2c3: the SN012776 speaker amplifier. */ + i2c3: i2c@33101c000 { + compatible = "apple,t8152-i2c", "apple,t8103-i2c"; + reg = <0x3 0x3101c000 0x0 0x4000>; + clocks = <&clkref>; + interrupts = ; + #address-cells = <1>; + #size-cells = <0>; + power-domains = <&ps_i2c3>; + + /* + * ADT audio-speaker: function-reset GPIO 0xca, interrupt + * on gpio0 pin 193, speaker-config rx_slot 0, + * vsense_slot 0, isense_slot 2. + */ + speaker_amp: codec@38 { + compatible = "ti,sn012776", "ti,tas2764"; + reg = <0x38>; + shutdown-gpios = <&pinctrl_ap 202 GPIO_ACTIVE_HIGH>; + interrupts-extended = <&pinctrl_ap 193 IRQ_TYPE_LEVEL_LOW>; + #sound-dai-cells = <0>; + ti,imon-slot-no = <2>; + ti,vmon-slot-no = <0>; + /* the AOP's speaker frame: 250 BCLKs at 12 MHz */ + ti,bclk-ratio = <250>; + }; + }; + + aop: aop@310c00000 { + compatible = "apple,t8152-aop"; + reg = <0x3 0x10c00000 0x0 0x1e0000>, + <0x3 0x10600000 0x0 0x4000>; + reg-names = "mmio", "asc"; + mboxes = <&aop_mbox>; + mbox-names = "mbox"; + iommus = <&sbr_dart 0>; + /* slot 17: slot 16 belongs to the firmware */ + apple,dma-range = <0x110 0x0 0x10 0x0>; + + /* + * Bound by name to the firmware's "audio" EPIC service. + * ADT iop-audio-controller: cout/cin on base-ns TX5/RX5, + * spkr on TX1 with its sense data on RX1; Linux ADMAC + * channel = 2 * N for TX N, 2 * N + 1 for RX N. + */ + aop_audio: audio { + compatible = "apple,t8152-aop-audio"; + dmas = <&admac_base_ns 10>, <&admac_base_ns 11>, + <&admac_base_ns 2>, <&admac_base_ns 3>; + dma-names = "cout", "cin", "spkr", "sense"; + power-domains = <&ps_audio_mca0_m>, <&ps_audio_mca1_m>, + <&ps_audio_mca2_m>, <&ps_audio_mca3_m>, + <&ps_audio_mca4_m>, <&ps_audio_mca5_m>; + power-domain-names = "mca0", "mca1", "mca2", + "mca3", "mca4", "mca5"; + #sound-dai-cells = <1>; + }; + }; + }; + + sound { + compatible = "apple,j873-macaudio", "apple,macaudio"; + model = "Mac mini J873"; + #address-cells = <1>; + #size-cells = <0>; + + dai-link@0 { + reg = <0>; + link-name = "Headphone Jack"; + + cpu { + sound-dai = <&aop_audio 0>; + }; + codec { + sound-dai = <&jack_codec>; + }; + }; + + dai-link@1 { + reg = <1>; + link-name = "Speaker"; + + cpu { + sound-dai = <&aop_audio 3>; + }; + codec { + sound-dai = <&speaker_amp>; + }; + }; + }; +}; + +/* The I2C power states (PMGR, parent SIO_CPU is always on). */ +&pmgr { + ps_i2c1: power-controller@cc0 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0xcc0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "i2c1"; + }; + + ps_i2c3: power-controller@d00 { + compatible = "apple,t8152-pmgr-pwrstate", + "apple,t8103-pmgr-pwrstate"; + reg = <0xd00 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "i2c3"; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index 180b435f1d969..2da1a88dc8cf1 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -364,3 +364,4 @@ #include "t8152-j873-nvme.dtsi" #include "t8152-j873-usb.dtsi" #include "t8152-j873-ethernet.dtsi" +#include "t8152-j873-audio.dtsi" From bc141c0a6c9bf48786169920cd2f6ddb74e647e5 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 10:31:26 +0100 Subject: [PATCH 48/55] arm64: dts: apple: j873: describe the fan target and mode keys The J873 SMC has F0Tg (target, flt) and F0md (mode, ui8; lowercase, unlike the F0Md of other machines). With them macsmc_hwmon exposes fan1_target; with fan_control=1 the fan follows it (2500 -> 2499 RPM within 3 s). In automatic mode the SMC keeps the fan at its 1000 RPM minimum even at 93 C under full load, so the OS has to drive it. Signed-off-by: Thomas Glanzmann [Acelogic: adapted for aurora-wip, whose T8152 hwmon leaves fan control to the firmware: the fan node has no apple,fan-manual-control, so the keys only add a read-only fan1_target. The target becomes writable only with the fan_control module parameter (unsafe, off by default); nothing in the kernel writes F0md or F0Tg by itself.] Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873g.dts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index 2da1a88dc8cf1..db37673de92bb 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -340,6 +340,8 @@ apple,key-id = "F0Ac"; apple,fan-minimum = "F0Mn"; apple,fan-maximum = "F0Mx"; + apple,fan-target = "F0Tg"; + apple,fan-mode = "F0md"; label = "Fan"; }; }; From 6962da594d81907a7a1445db331fa95c322ec5ef Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sun, 8 Nov 2026 02:04:43 -0500 Subject: [PATCH 49/55] arm64: dts: apple: j873: leave the USB 2 hosts to a follow-up The J873g USB 2 host nodes describe the back-left (usb-drd1) and front (usb-drd2) controllers as plain "snps,dwc3" with both DART instances in iommus. The generic dwc3 binding allows a single iommu, so these nodes add two dtbs_check warnings. The Apple dwc3 binding, which does allow two DARTs, needs resets, a USB role switch and PHY descriptions that the J873g does not have yet. Return the J873g USB description to the shared, disabled state that aurora-wip has, until the M6 USB controllers can be described properly: - drop t8152-j873-usb.dtsi and its include; - restore aurora-wip's dwc3_1_dart and dwc3_2_dart overrides. The USB 2 hosts worked on the M6 with the earlier description (both xHCI hosts and the front-port hub enumerated), and they will come back in a follow-up together with the binding work. Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi | 67 ------------------- arch/arm64/boot/dts/apple/t8152-j873g.dts | 17 ++++- 2 files changed, 15 insertions(+), 69 deletions(-) delete mode 100644 arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi diff --git a/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi deleted file mode 100644 index 5ecd42f9e7135..0000000000000 --- a/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ OR MIT -/* - * J873g USB2 host bring-up: usb-drd1 (back-left port) and usb-drd2 - * (front ports via the on-board hub). - * The loader powers the controllers and initialises the eUSB2 PHYs in host - * mode before Linux starts (no PMGR, PHY or Type-C driver yet), so the - * controllers are described as plain DWC3 hosts behind their two DARTs. - * Resources are from the 26A434 ADT: dart-usb1/2, usb-drd1/2. - * - * dart-usb1 and dart-usb2 each have two DART instances (ADT reg[0] and - * reg[1], 0xc000 each, sharing one interrupt). t8152-peripherals.dtsi lists - * both windows on one node (dwc3_1_dart, dwc3_2_dart), but the DART driver - * drives one instance per node and the binding reads a second reg entry as - * DAPF or auxiliary registers. Keep instance 0 on the shared node, with its - * own window only, and describe instance 1 as a node of its own. - */ - -&dwc3_1_dart { - reg = <0x4 0x0b020000 0x0 0xc000>; - status = "okay"; -}; - -&dwc3_2_dart { - reg = <0x3 0x83020000 0x0 0xc000>; - status = "okay"; -}; - -/ { - soc { - dwc3_1_dart_1: iommu@40b040000 { - compatible = "apple,t8152-dart", "apple,t8110-dart"; - reg = <0x4 0x0b040000 0x0 0xc000>; - interrupts = ; - #iommu-cells = <1>; - }; - - usb1: usb@40a430000 { - compatible = "snps,dwc3"; - reg = <0x4 0x0a430000 0x0 0xcd00>; - interrupts = ; - dr_mode = "host"; - maximum-speed = "high-speed"; - iommus = <&dwc3_1_dart 0>, <&dwc3_1_dart_1 1>; - snps,dis_u2_susphy_quirk; - snps,dis_u3_susphy_quirk; - }; - - dwc3_2_dart_1: iommu@383040000 { - compatible = "apple,t8152-dart", "apple,t8110-dart"; - reg = <0x3 0x83040000 0x0 0xc000>; - interrupts = ; - #iommu-cells = <1>; - }; - - /* usb-drd2: on-board VL822 hub behind the two front USB-C ports. */ - usb2: usb@382430000 { - compatible = "snps,dwc3"; - reg = <0x3 0x82430000 0x0 0xcd00>; - interrupts = ; - dr_mode = "host"; - maximum-speed = "high-speed"; - iommus = <&dwc3_2_dart 0>, <&dwc3_2_dart_1 1>; - snps,dis_u2_susphy_quirk; - snps,dis_u3_susphy_quirk; - }; - }; -}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index 0f98b07748b01..d4900374f1470 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -372,6 +372,12 @@ <0x4 0x3040000 0x0 0xc000>; }; +/* Retain the register view published by this board's live hardware capture. */ +&dwc3_1_dart { + reg = <0x4 0xb020000 0x0 0xc000>, + <0x4 0xb040000 0x0 0xc000>; +}; + /* Retain the register view published by this board's live hardware capture. */ &dwc3_3_dart { reg = <0x4 0x13020000 0x0 0xc000>, @@ -494,11 +500,18 @@ <0x3 0xa0000000 0x0 0x8000>; }; +/* Unexposed chassis port instances remain described and disabled. */ + +/* Retain the register view published by this board's live hardware capture. */ +&dwc3_2_dart { + reg = <0x3 0x83020000 0x0 0xc000>, + <0x3 0x83040000 0x0 0xc000>; +}; + /* * The J873g fragments enable and refine nodes of t8152-peripherals.dtsi - * (the USB, apcie port 2 and dart-sbr DARTs), so they come after it. + * (the apcie port 2 and dart-sbr DARTs), so they come after it. */ #include "t8152-j873-nvme.dtsi" -#include "t8152-j873-usb.dtsi" #include "t8152-j873-ethernet.dtsi" #include "t8152-j873-audio.dtsi" From e5ac50732aac6366a90bc9c8e9307cb10e37f20b Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sun, 8 Nov 2026 02:33:35 -0500 Subject: [PATCH 50/55] arm64: dts: apple: j873: bring back the USB 2 hosts Restore the J873g USB 2 host description that linux#1197 set aside: the back-left controller (usb-drd1) and the front ports (usb-drd2, behind the on-board hub), each behind its two DART instances. aura prepares these ports before Linux starts (j873_usb_prep()). This is the description that worked on the M6, where both xHCI hosts and the front-port hub enumerated. It still describes the controllers as plain "snps,dwc3" with two iommus, which the generic dwc3 binding rejects. Before it can merge, the nodes need to move to the Apple dwc3 binding, or the binding needs an M6 USB 2 host variant. Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi | 67 +++++++++++++++++++ arch/arm64/boot/dts/apple/t8152-j873g.dts | 17 +---- 2 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi diff --git a/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi new file mode 100644 index 0000000000000..5ecd42f9e7135 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * J873g USB2 host bring-up: usb-drd1 (back-left port) and usb-drd2 + * (front ports via the on-board hub). + * The loader powers the controllers and initialises the eUSB2 PHYs in host + * mode before Linux starts (no PMGR, PHY or Type-C driver yet), so the + * controllers are described as plain DWC3 hosts behind their two DARTs. + * Resources are from the 26A434 ADT: dart-usb1/2, usb-drd1/2. + * + * dart-usb1 and dart-usb2 each have two DART instances (ADT reg[0] and + * reg[1], 0xc000 each, sharing one interrupt). t8152-peripherals.dtsi lists + * both windows on one node (dwc3_1_dart, dwc3_2_dart), but the DART driver + * drives one instance per node and the binding reads a second reg entry as + * DAPF or auxiliary registers. Keep instance 0 on the shared node, with its + * own window only, and describe instance 1 as a node of its own. + */ + +&dwc3_1_dart { + reg = <0x4 0x0b020000 0x0 0xc000>; + status = "okay"; +}; + +&dwc3_2_dart { + reg = <0x3 0x83020000 0x0 0xc000>; + status = "okay"; +}; + +/ { + soc { + dwc3_1_dart_1: iommu@40b040000 { + compatible = "apple,t8152-dart", "apple,t8110-dart"; + reg = <0x4 0x0b040000 0x0 0xc000>; + interrupts = ; + #iommu-cells = <1>; + }; + + usb1: usb@40a430000 { + compatible = "snps,dwc3"; + reg = <0x4 0x0a430000 0x0 0xcd00>; + interrupts = ; + dr_mode = "host"; + maximum-speed = "high-speed"; + iommus = <&dwc3_1_dart 0>, <&dwc3_1_dart_1 1>; + snps,dis_u2_susphy_quirk; + snps,dis_u3_susphy_quirk; + }; + + dwc3_2_dart_1: iommu@383040000 { + compatible = "apple,t8152-dart", "apple,t8110-dart"; + reg = <0x3 0x83040000 0x0 0xc000>; + interrupts = ; + #iommu-cells = <1>; + }; + + /* usb-drd2: on-board VL822 hub behind the two front USB-C ports. */ + usb2: usb@382430000 { + compatible = "snps,dwc3"; + reg = <0x3 0x82430000 0x0 0xcd00>; + interrupts = ; + dr_mode = "host"; + maximum-speed = "high-speed"; + iommus = <&dwc3_2_dart 0>, <&dwc3_2_dart_1 1>; + snps,dis_u2_susphy_quirk; + snps,dis_u3_susphy_quirk; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t8152-j873g.dts b/arch/arm64/boot/dts/apple/t8152-j873g.dts index d4900374f1470..0f98b07748b01 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873g.dts +++ b/arch/arm64/boot/dts/apple/t8152-j873g.dts @@ -372,12 +372,6 @@ <0x4 0x3040000 0x0 0xc000>; }; -/* Retain the register view published by this board's live hardware capture. */ -&dwc3_1_dart { - reg = <0x4 0xb020000 0x0 0xc000>, - <0x4 0xb040000 0x0 0xc000>; -}; - /* Retain the register view published by this board's live hardware capture. */ &dwc3_3_dart { reg = <0x4 0x13020000 0x0 0xc000>, @@ -500,18 +494,11 @@ <0x3 0xa0000000 0x0 0x8000>; }; -/* Unexposed chassis port instances remain described and disabled. */ - -/* Retain the register view published by this board's live hardware capture. */ -&dwc3_2_dart { - reg = <0x3 0x83020000 0x0 0xc000>, - <0x3 0x83040000 0x0 0xc000>; -}; - /* * The J873g fragments enable and refine nodes of t8152-peripherals.dtsi - * (the apcie port 2 and dart-sbr DARTs), so they come after it. + * (the USB, apcie port 2 and dart-sbr DARTs), so they come after it. */ #include "t8152-j873-nvme.dtsi" +#include "t8152-j873-usb.dtsi" #include "t8152-j873-ethernet.dtsi" #include "t8152-j873-audio.dtsi" From 26670aa724b0ebf60114629d9c67e52e6180d588 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sun, 8 Nov 2026 03:58:53 -0500 Subject: [PATCH 51/55] dt-bindings: usb: apple,dwc3: Add T8152 The T8152 (M6) USB-C controllers are Apple DWC3 DRD controllers like those on earlier SoCs, each with its own ATC PHY. There is no T8152 USB power-domain, ATC PHY or Type-C support yet. Instead, the bootloader can hand a controller over already running: powered, with the USB2 side of its ATC PHY set up for host mode. Add a standalone "apple,t8152-dwc3" compatible for such a controller, following the T8132 exception already in this binding. It is described by its DWC3 core region, as a host, so dr_mode = "host" is required. The PHY reset and power domain, and on a Type-C port the USB role switch, exist on T8152 but are left to the bootloader, so they are optional there. Every other compatible still requires reg-names, resets, usb-role-switch and two reg entries. There is no apple,t8103-dwc3 fallback: the T8152 layout of the Apple region is not established, and without its ATC PHY the controller cannot switch roles, so the Apple dwc3 glue must not bind it. When T8152 ATC PHY and Type-C support lands, full Type-C operation can key on resets and usb-role-switch being present, so these minimal descriptions stay valid and the dr_mode rule can be relaxed. Signed-off-by: Acelogic --- .../devicetree/bindings/usb/apple,dwc3.yaml | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/Documentation/devicetree/bindings/usb/apple,dwc3.yaml b/Documentation/devicetree/bindings/usb/apple,dwc3.yaml index d68a4b68d77ca..df755601429cd 100644 --- a/Documentation/devicetree/bindings/usb/apple,dwc3.yaml +++ b/Documentation/devicetree/bindings/usb/apple,dwc3.yaml @@ -13,6 +13,12 @@ description: Apple Silicon SoCs use a Synopsys DesignWare DWC3 based controller for each of their Type-C ports. + On T8152 the bootloader can hand a controller over already running, powered + and with the USB2 side of its ATC PHY set up for host mode. Such a controller + is described by its DWC3 core region, as a host. Its PHY reset and power + domain, and on a Type-C port its USB role switch, exist but are left to the + bootloader, so they are optional. + allOf: - $ref: snps,dwc3-common.yaml# - if: @@ -20,10 +26,33 @@ allOf: compatible: not: contains: - const: apple,t8132-dwc3 + enum: + - apple,t8132-dwc3 + - apple,t8152-dwc3 then: required: - power-domains + - if: + properties: + compatible: + contains: + const: apple,t8152-dwc3 + then: + properties: + dr_mode: + const: host + required: + - dr_mode + else: + properties: + reg: + minItems: 2 + reg-names: + minItems: 2 + required: + - reg-names + - resets + - usb-role-switch $defs: apple,tunable: @@ -56,16 +85,17 @@ properties: - const: apple,t8103-dwc3 - const: apple,t8103-dwc3 - const: apple,t8132-dwc3 + - const: apple,t8152-dwc3 reg: - minItems: 2 + minItems: 1 items: - description: Core DWC3 region - description: Apple-specific DWC3 region - description: Controller configuration block (T6040) reg-names: - minItems: 2 + minItems: 1 items: - const: dwc3-core - const: dwc3-apple @@ -106,11 +136,8 @@ properties: required: - compatible - reg - - reg-names - interrupts - iommus - - resets - - usb-role-switch unevaluatedProperties: false @@ -131,3 +158,18 @@ examples: usb-role-switch; }; + + - | + #include + #include + + usb@82430000 { + compatible = "apple,t8152-dwc3"; + reg = <0x82430000 0xcd00>; + interrupts = ; + iommus = <&dwc3_2_dart_0 0>, <&dwc3_2_dart_1 1>; + dr_mode = "host"; + maximum-speed = "high-speed"; + snps,dis_u2_susphy_quirk; + snps,dis_u3_susphy_quirk; + }; From 846d101cb4c7be32b91ece8890a7965ce3bfd030 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sun, 8 Nov 2026 03:58:53 -0500 Subject: [PATCH 52/55] usb: dwc3: core: Bind the Apple T8152 controller On T8152 (M6) the Apple dwc3 glue cannot drive the controller yet: it needs the ATC PHY as reset provider and a Type-C role switch, and T8152 has no driver for either. The bootloader powers the controller, sets up the USB2 side of its ATC PHY for host mode and selects the host role before the kernel starts, which leaves only the DWC3 core to the kernel. Match "apple,t8152-dwc3" in the dwc3 core driver. dwc3_probe() uses no match data, so the controller probes exactly as a "snps,dwc3" node does, and dr_mode, maximum-speed and the quirks are read from the node as before. dwc3-apple does not match this compatible, so only the core binds it. This entry is to move to dwc3-apple once that driver supports the T8152 ATC PHY and Type-C role switch, so that the two never match the same compatible. Signed-off-by: Acelogic --- drivers/usb/dwc3/core.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c index 7f68eba95e1f1..3566533dda5a2 100644 --- a/drivers/usb/dwc3/core.c +++ b/drivers/usb/dwc3/core.c @@ -2962,6 +2962,10 @@ static const struct of_device_id of_dwc3_match[] = { { .compatible = "synopsys,dwc3" }, + { + /* Handed over in host mode by the bootloader, see apple,dwc3.yaml */ + .compatible = "apple,t8152-dwc3" + }, { }, }; MODULE_DEVICE_TABLE(of, of_dwc3_match); From 35833f6358e400ef01d27c4ef38fb7ecf9b8d88d Mon Sep 17 00:00:00 2001 From: Acelogic Date: Sun, 8 Nov 2026 03:58:53 -0500 Subject: [PATCH 53/55] arm64: dts: apple: j873: Describe the USB 2 hosts as T8152 DWC3 The back-left (usb-drd1) and front (usb-drd2) USB 2 host nodes are "snps,dwc3" with two iommus, one per DART instance. The generic dwc3 binding allows a single iommu, so dtbs_check rejects both nodes. Use the T8152 Apple dwc3 compatible instead. It allows both DART instances and documents the bootloader handover these nodes rely on. Nothing else changes: the nodes keep their core region, interrupt, DARTs, host mode, speed limit and SUSPHY quirks, and the same dwc3 core driver binds them. Signed-off-by: Acelogic --- arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi index 5ecd42f9e7135..6f1a956bd68e3 100644 --- a/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi +++ b/arch/arm64/boot/dts/apple/t8152-j873-usb.dtsi @@ -2,9 +2,12 @@ /* * J873g USB2 host bring-up: usb-drd1 (back-left port) and usb-drd2 * (front ports via the on-board hub). - * The loader powers the controllers and initialises the eUSB2 PHYs in host - * mode before Linux starts (no PMGR, PHY or Type-C driver yet), so the - * controllers are described as plain DWC3 hosts behind their two DARTs. + * The loader powers the controllers and sets up the USB2 side of their ATC + * PHYs for host mode before Linux starts (there is no T8152 USB power-domain, + * ATC PHY or Type-C support yet). + * The controllers are described by their DWC3 core region, as hosts behind + * their two DARTs (apple,t8152-dwc3); their PHY reset and power domain, and on + * the Type-C port the role switch, exist but are left to the loader. * Resources are from the 26A434 ADT: dart-usb1/2, usb-drd1/2. * * dart-usb1 and dart-usb2 each have two DART instances (ADT reg[0] and @@ -35,7 +38,7 @@ }; usb1: usb@40a430000 { - compatible = "snps,dwc3"; + compatible = "apple,t8152-dwc3"; reg = <0x4 0x0a430000 0x0 0xcd00>; interrupts = ; dr_mode = "host"; @@ -54,7 +57,7 @@ /* usb-drd2: on-board VL822 hub behind the two front USB-C ports. */ usb2: usb@382430000 { - compatible = "snps,dwc3"; + compatible = "apple,t8152-dwc3"; reg = <0x3 0x82430000 0x0 0xcd00>; interrupts = ; dr_mode = "host"; From f8c95e25fc069253e1e3baf42d6cd22ff8b6dfab Mon Sep 17 00:00:00 2001 From: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:40:58 +1100 Subject: [PATCH 54/55] dt-bindings: nvme: apple,nvme-ans: exempt T6050 and T8152 from one rule The integration merge brought in two rules for the required resets and power domains. aurora-wip had "required unless T8152", and integration had "forbidden on T6050, else required". Together they required the properties on T6050 nodes that must not have them, and on T8152 nodes where they are optional. dt-checks flagged that on every T6050 board and on J873g. Keep one rule: required unless T6050 or T8152. T6050 still forbids them. Signed-off-by: Ryan Murray <81541646+RyanTheTide@users.noreply.github.com> Assisted-by: LLM --- .../devicetree/bindings/nvme/apple,nvme-ans.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml b/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml index 326d8d538947b..b7fcf03af696f 100644 --- a/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml +++ b/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml @@ -117,7 +117,9 @@ allOf: compatible: not: contains: - const: apple,t8152-nvme-ans2 + enum: + - apple,t6050-nvme-ans2 + - apple,t8152-nvme-ans2 then: required: - resets @@ -195,11 +197,6 @@ allOf: resets: false power-domains: false power-domain-names: false - else: - required: - - resets - - power-domains - - power-domain-names required: - compatible From 210c8f6e6ffd28d41fdf688ab5e3f7ecb674272f Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:42:07 -0600 Subject: [PATCH 55/55] ASoC: apple: t8152-aop-audio: Match the T6050 AOP audio node (#1410) The binding and the J714c device tree name the AOP audio service "apple,t6050-aop-audio", but the driver matched only t8142 and t8152, so that node never bound. Add a T6050 hardware config. The J714c ADT audio-speaker stream header (DeviceTree.j714cap) has the same layout as J813's with six amplifiers in place of four: 6 speaker slots and 12 sense words (V, I per amplifier), at 16 bits, on slots vsense 0,4,..20 / isense 2,6,..22. J813's header has 4 slots, 8 sense words and vsense 0..12 / isense 2..14. HW_CFG_T8152 (one amplifier, two sense words) would expose the wrong channel counts, so it is not reused. The sense width and the sense realignment are taken from J813's config by analogy; they are not tested on a J714c. Tested: builds as a module with Rust enabled (clang 19, arm64 defconfig plus CONFIG_RUST, CONFIG_SND_SOC_APPLE_T8152_AOP_AUDIO=m); the object carries the apple,t6050-aop-audio match string. Not tested: no J714c boot, no sound. --- sound/soc/apple/t8152_aop_audio.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs index 81a224f697491..f0080ccb50669 100644 --- a/sound/soc/apple/t8152_aop_audio.rs +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT -//! Apple T8152 (J873, Mac mini M6) and T8142 (J813, MacBook Air M5) AOP audio +//! Apple T8152 (J873, Mac mini M6), T8142 (J813, MacBook Air M5) and T6050 +//! (J714c, MacBook Pro 14-inch M5 Max) AOP audio //! //! The AOP firmware owns the audio fabric, as on T8140. The ADT //! (/arm-io/iop-audio-controller) names its services; all of them run on the @@ -582,6 +583,17 @@ static HW_CFG_T8142: HwConfig = HwConfig { realign_sense: true, }; +/// J714c: six amplifiers (left and right woofer 1, woofer 2 and tweeter) in +/// 32-bit slots 0-5, their voltage and current sense back as twelve 16-bit +/// slots (V, I per amplifier) on the stream's RX, as on J813. The ADT +/// audio-speaker stream header gives 6 amplifier slots and 12 sense words. +static HW_CFG_T6050: HwConfig = HwConfig { + spkr_channels: 6, + sense_channels: 12, + sense_formats: FMTBIT_S16_LE, + realign_sense: true, +}; + /// # Safety /// /// `substream` must be live and belong to one of this component's links. @@ -1389,6 +1401,7 @@ kernel::of_device_table!( [ (of::DeviceId::new(c_str!("apple,t8142-aop-audio")), &HW_CFG_T8142), (of::DeviceId::new(c_str!("apple,t8152-aop-audio")), &HW_CFG_T8152), + (of::DeviceId::new(c_str!("apple,t6050-aop-audio")), &HW_CFG_T6050), ] );