From 9afc945d0cdb101e28b0102fbc7bd55c04d2c77f Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:44:54 +0100 Subject: [PATCH 01/24] dt-bindings: sound: apple,macaudio: Add J813 and J873 Add the MacBook Air (M5, J813) and Mac mini (M6, J873) sound cards. t8142-j813-audio.dtsi already names apple,j813-macaudio. Signed-off-by: Thomas Glanzmann --- Documentation/devicetree/bindings/sound/apple,macaudio.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml index 0d9004fab9c1d..f6e098134ab27 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -51,6 +51,8 @@ properties: - apple,j715-macaudio-jack - apple,j773-macaudio - apple,j773-macaudio-jack + - apple,j813-macaudio + - apple,j873-macaudio - const: apple,macaudio - description: Machines that share the speaker and jack layout of an earlier From 7e364bb1ade152ca1e72ee2a106da7f17d4505ed Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:31 +0100 Subject: [PATCH 02/24] 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 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index 7f446caf407a8..209ee4a347ba1 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -2691,6 +2691,22 @@ 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, + ncal: None, + aopt: 0, + alig: 0, + patch_bootargs: false, + epic_v4: true, + setup_port: false, + quiesce_on_unbind: true, + required_endpoints: &[], + fw_regions: false, + bootargs_word_access: false, + halt_on_failed_start: false, +}; kernel::of_device_table!( OF_TABLE, @@ -2703,6 +2719,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 bb9ad0e1029a0f6ca51c87cc858cffa60e5242a9 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 21:16:38 +0100 Subject: [PATCH 03/24] 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 1f6bd7b4635b6..6071212d17c1e 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 9b5dafed69bcc8dcaf02482181c7e97f42b4d702 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:31 +0100 Subject: [PATCH 04/24] 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 | 67 ++++++++++++++++++++++++++++++++++++++ sound/soc/codecs/tas2764.h | 8 +++++ 2 files changed, 75 insertions(+) diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index d91944c975ce7..5588b5e6cc9b0 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; @@ -1067,6 +1069,26 @@ static const struct hwmon_chip_info tas2764_hwmon_chip_info = { .info = tas2764_hwmon_info, }; +/* + * Program the fixed BCLK/FS ratio from ti,bclk-ratio (tas2764_parse_bclk_ratio()). + * Without one nothing is written: the amplifier auto-detects the ratio. + */ +static int tas2764_apply_bclk_ratio(struct tas2764_priv *tas2764) +{ + int ret; + + if (tas2764->bclk_ratio_code < 0) + return 0; + + ret = regmap_write(tas2764->regmap, TAS2764_CLK_CFG, tas2764->bclk_ratio_code); + if (ret < 0) + return ret; + + return regmap_update_bits(tas2764->regmap, TAS2764_TDM_CFG0, + TAS2764_TDM_CFG0_FIXED_RATIO, + TAS2764_TDM_CFG0_FIXED_RATIO); +} + static int tas2764_codec_probe(struct snd_soc_component *component) { struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); @@ -1181,6 +1203,10 @@ static int tas2764_codec_probe(struct snd_soc_component *component) break; } + ret = tas2764_apply_bclk_ratio(tas2764); + if (ret < 0) + goto err_sdz; + /* * 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 @@ -1499,6 +1525,41 @@ 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 }, +}; + +/* + * ti,bclk-ratio: a fixed BCLK/FS ratio, as its TAS2764_CLK_CFG code in + * bclk_ratio_code. Without the property bclk_ratio_code stays -1, set before + * the device tree is read, and the ratio is auto-detected. + */ +static int tas2764_parse_bclk_ratio(struct device *dev, struct tas2764_priv *tas2764) +{ + u32 ratio; + int i; + + if (device_property_read_u32(dev, "ti,bclk-ratio", &ratio)) + return 0; + + 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 0; +} + static int tas2764_parse_dt(struct device *dev, struct tas2764_priv *tas2764) { int ret = 0; @@ -1559,6 +1620,10 @@ static int tas2764_parse_dt(struct device *dev, struct tas2764_priv *tas2764) if (ret) return ret; + ret = tas2764_parse_bclk_ratio(dev, tas2764); + if (ret) + return ret; + return tas2764_group_join(dev, tas2764); } @@ -1580,6 +1645,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 e4d92b325fabf1b593b73de4bf691e226b3189a2 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 1 Nov 2026 21:36:59 +0100 Subject: [PATCH 05/24] 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 a0d23fb0183cd2c496aae1742704152e90cb4c4d Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:31 +0100 Subject: [PATCH 06/24] 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 7bcebdc0635e5199f45880711e26ea1eb0201f18 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 20:17:32 +0100 Subject: [PATCH 07/24] 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) and no safe level, so macaudio_speakers_allowed() keeps the speaker off. The jack is not affected. Signed-off-by: Thomas Glanzmann --- sound/soc/apple/macaudio.c | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index 4e1b642e4b0a5..0d98c12b6648e 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -176,6 +176,13 @@ struct macaudio_platform_cfg { * along with the safe level. */ bool tweeter_hpf_granted; + /* 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[] = { @@ -2731,6 +2738,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); @@ -3673,6 +3682,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". @@ -4004,6 +4016,34 @@ 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), and there is no safe + * level: macaudio_speakers_allowed() keeps the speaker off. The jack + * is unaffected. + */ + .enable_speakers = false, + .amp = AMP_SN012776, + .speakers = SPKR_1W, + .stereo = false, + .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, }; @@ -4254,6 +4294,8 @@ static const struct of_device_id macaudio_snd_device_id[] = { { .compatible = "apple,j773-macaudio", .data = &macaudio_j575c_j773_cfg }, /* j773 - cs42l84 - the jack, a card of its own (AOP-fed) */ { .compatible = "apple,j773-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 39934b9339db03fc26c6d0f05cd24b7d7523ee94 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 22:14:50 +0100 Subject: [PATCH 08/24] 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 | 89 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index 209ee4a347ba1..718fd69b6a419 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -174,6 +174,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; /// How long the AP power request waits for the last endpoint map fragment. const SETUP_MAP_TIMEOUT_MS: u32 = 1000; @@ -1030,7 +1032,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, @@ -1039,12 +1046,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, @@ -1086,7 +1102,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 @@ -1384,7 +1401,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| { @@ -2203,6 +2220,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; } @@ -2259,7 +2294,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)?; @@ -2508,7 +2543,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); @@ -2576,6 +2611,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, @@ -2598,6 +2635,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, @@ -2614,6 +2652,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, @@ -2629,6 +2668,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, @@ -2643,6 +2683,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, @@ -2662,6 +2703,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, @@ -2685,6 +2727,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, @@ -2701,6 +2744,28 @@ 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, + bootargs_word_access: 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, + ncal: None, + 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, @@ -2719,6 +2784,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), ] ); @@ -2769,7 +2835,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 a5ab733a05c7cb534d116d30cf4823ff635edb90 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 22:15:04 +0100 Subject: [PATCH 09/24] 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 | 108 +++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 7 deletions(-) diff --git a/drivers/dma/apple-admac.c b/drivers/dma/apple-admac.c index b7336f97ee9fc..939e5d5e7bdb9 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 @@ -340,6 +342,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. */ @@ -356,13 +373,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++; @@ -679,6 +690,73 @@ 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); +} + +/* + * Whether a resume restarts the channel aligned (admac_cyclic_realign()): only a + * capture whose client gave a frame size, with a transfer to restart. Every + * other channel, and every playback one, starts again where it paused. + */ +static bool admac_resume_realigns(const struct admac_chan *adchan) +{ + return adchan->realign_bytes && adchan->current_tx && + admac_chan_direction(adchan->no) == DMA_DEV_TO_MEM; +} + static int admac_resume(struct dma_chan *chan) { struct admac_chan *adchan = to_admac_chan(chan); @@ -688,6 +766,8 @@ 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 (admac_resume_realigns(adchan)) + admac_cyclic_realign(adchan); else admac_start_chan(adchan); spin_unlock_irqrestore(&adchan->lock, flags); @@ -987,6 +1067,18 @@ static void admac_chan_tasklet(struct tasklet_struct *t) } } +/* + * The frame size a paused capture is restarted aligned to on resume (see + * admac_cyclic_realign()): an optional u32 in bytes in the peripheral config. + * A client that passes none, as the MCA's PCM does, gets 0: no realignment. + */ +static u32 admac_config_realign_bytes(const struct dma_slave_config *config) +{ + if (config->peripheral_config && config->peripheral_size == sizeof(u32)) + return *(const u32 *)config->peripheral_config; + return 0; +} + static int admac_device_config(struct dma_chan *chan, struct dma_slave_config *config) { @@ -1041,6 +1133,8 @@ static int admac_device_config(struct dma_chan *chan, writel_relaxed(bus_width, ad->base + REG_BUS_WIDTH(ad, adchan->no)); + adchan->realign_bytes = admac_config_realign_bytes(config); + /* * 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 e115f85149a906f4a72b591e9e7b5746cbac929e Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 22:15:04 +0100 Subject: [PATCH 10/24] 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 e526f2bde1c3c7d1334c7b3aa18c0aa87c320827 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 22:15:04 +0100 Subject: [PATCH 11/24] 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 table has a safe level, so the speakers are behind the speaker protection interfaces from the bind: under "Speaker Volume Unlock" their volumes stay at the lock's ceiling until a protection daemon (speakersafetyd) holds them, and under "Speaker Protection" they stay at the floor and no speaker start is admitted until an owner is granted. Each amp's level is its apple,speaker-config from the boot loader. Signed-off-by: Thomas Glanzmann --- sound/soc/apple/macaudio.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index 0d98c12b6648e..cc020da9e6172 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -4044,6 +4044,31 @@ 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 + * speakers are behind the speaker protection interfaces from the bind: the + * volumes stay at the lock's ceiling until a protection daemon + * (speakersafetyd) holds them, or under "Speaker Protection" at the floor + * until an owner is granted. Each amp's level is its apple,speaker-config + * from the boot loader. + */ +struct macaudio_platform_cfg macaudio_j813_cfg = { + .enable_speakers = true, + .amp = AMP_SN012776, + .speakers = SPKR_1W1T, + .stereo = true, + .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, }; @@ -4296,6 +4321,8 @@ static const struct of_device_id macaudio_snd_device_id[] = { { .compatible = "apple,j773-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 bd7da284c87db8081ad16d989f49894340746843 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 23:44:39 +0100 Subject: [PATCH 12/24] 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 ea81c5719dd2164a34ba4f63c14d7e62626e3439 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Tue, 10 Nov 2026 07:57:54 +0100 Subject: [PATCH 13/24] arm64: configs: asahi: build the T8152 AOP audio driver The MacBook Air (M5, J813) gets its jack, speakers and microphones from the T8152 AOP audio driver. Build it beside the T8140 one. Signed-off-by: Thomas Glanzmann --- arch/arm64/configs/asahi.config | 1 + 1 file changed, 1 insertion(+) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 5581318240726..178fb4c200184 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -75,6 +75,7 @@ CONFIG_DRM_ACCEL_ANE=m CONFIG_DRM_SIMPLEDRM=y CONFIG_SYSFB_SIMPLEFB=y CONFIG_SND_SOC_APPLE_AOP_AUDIO=m +CONFIG_SND_SOC_APPLE_T8152_AOP_AUDIO=m CONFIG_SND_SOC_APPLE_MCA=m CONFIG_SND_SOC_APPLE_MACAUDIO=m CONFIG_SND_SOC_CS42L83=m From c531d1cd4b93aba13ac45dab0ff21c2dc06e0568 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Tue, 10 Nov 2026 13:46:22 +0100 Subject: [PATCH 14/24] ASoC: tas2764: test the BCLK/FS ratio with and without ti,bclk-ratio ti,bclk-ratio is only on the AOP-fed amplifiers (J813, J873). Test that an amp without it, as on every MCA machine, keeps the ratio auto-detection and that its probe writes neither TAS2764_CLK_CFG nor TDM_CFG0's fixed-ratio bit, through a register map that records every write; that 250 BCLKs programs code 0x30 and sets only that bit; and that a ratio without a known code fails the probe. Signed-off-by: Thomas Glanzmann --- sound/soc/codecs/tas2764-bclk-ratio-test.h | 158 +++++++++++++++++++++ sound/soc/codecs/tas2764-kunit.dtso | 16 ++- sound/soc/codecs/tas2764.c | 1 + 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 sound/soc/codecs/tas2764-bclk-ratio-test.h diff --git a/sound/soc/codecs/tas2764-bclk-ratio-test.h b/sound/soc/codecs/tas2764-bclk-ratio-test.h new file mode 100644 index 0000000000000..e395d8b099751 --- /dev/null +++ b/sound/soc/codecs/tas2764-bclk-ratio-test.h @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * KUnit tests of tas2764_parse_bclk_ratio() and tas2764_apply_bclk_ratio(): + * an amp whose node has no ti,bclk-ratio, as on every MCA machine (M1, M2, + * M3, M4), keeps the ratio auto-detection and its probe writes neither + * TAS2764_CLK_CFG nor TDM_CFG0's fixed-ratio bit; only the property sets + * them. Uses the nodes of tas2764-kunit.dtso and tas2764_test_priv(). + */ +#include +#include + +/* Page 0, where both registers are */ +#define TAS2764_TEST_NREGS TAS2764_REG(0x1, 0x0) + +/* Register values and every write, behind a regmap without a bus */ +struct tas2764_test_regs { + unsigned int val[TAS2764_TEST_NREGS]; + unsigned int writes; +}; + +static int tas2764_test_reg_read(void *context, unsigned int reg, unsigned int *val) +{ + struct tas2764_test_regs *regs = context; + + if (reg >= TAS2764_TEST_NREGS) + return -EINVAL; + *val = regs->val[reg]; + return 0; +} + +static int tas2764_test_reg_write(void *context, unsigned int reg, unsigned int val) +{ + struct tas2764_test_regs *regs = context; + + if (reg >= TAS2764_TEST_NREGS) + return -EINVAL; + regs->val[reg] = val; + regs->writes++; + return 0; +} + +static const struct regmap_config tas2764_test_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = TAS2764_TEST_NREGS - 1, + .reg_read = tas2764_test_reg_read, + .reg_write = tas2764_test_reg_write, + .cache_type = REGCACHE_NONE, +}; + +/* + * The driver state for test node @node, as tas2764_i2c_probe() leaves it + * before the device tree is read, with a recording register map. + */ +static struct tas2764_priv *tas2764_test_bclk_priv(struct kunit *test, const char *node, + struct device **devp, + struct tas2764_test_regs **regsp) +{ + struct tas2764_test_regs *regs; + struct tas2764_priv *tas2764; + struct device *mapdev; + + tas2764 = tas2764_test_priv(test, node, DEVID_SN012776, devp); + regs = kunit_kzalloc(test, sizeof(*regs), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, regs); + mapdev = kunit_device_register(test, "tas2764-bclk-test"); + KUNIT_ASSERT_FALSE(test, IS_ERR(mapdev)); + tas2764->regmap = devm_regmap_init(mapdev, NULL, regs, &tas2764_test_regmap_config); + KUNIT_ASSERT_FALSE(test, IS_ERR(tas2764->regmap)); + + tas2764->bclk_ratio_code = -1; + /* Whatever TDM_CFG0 held: only the fixed-ratio bit may change */ + regs->val[TAS2764_TDM_CFG0] = TAS2764_TDM_CFG0_44_1_48KHZ | TAS2764_TDM_CFG0_FRAME_START; + *regsp = regs; + return tas2764; +} + +/* Amp nodes without ti,bclk-ratio */ +static const char *const tas2764_bclk_absent_nodes[] = { + /* With the ADT blocks the boot loader copies on the MCA machines */ + "tas2764-kunit-bop-good", + "tas2764-kunit-bop-none", +}; + +static void tas2764_bclk_absent_desc(const char *const *node, char *desc) +{ + strscpy(desc, *node, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(tas2764_bclk_absent, tas2764_bclk_absent_nodes, tas2764_bclk_absent_desc); + +/* No ti,bclk-ratio: auto-detection, and not one register written */ +static void tas2764_bclk_ratio_absent(struct kunit *test) +{ + const char *const *node = test->param_value; + struct tas2764_test_regs *regs; + struct tas2764_priv *tas2764; + struct device *dev; + + tas2764 = tas2764_test_bclk_priv(test, *node, &dev, ®s); + KUNIT_EXPECT_EQ(test, tas2764_parse_bclk_ratio(dev, tas2764), 0); + KUNIT_EXPECT_EQ(test, tas2764->bclk_ratio_code, -1); + KUNIT_EXPECT_EQ(test, tas2764_apply_bclk_ratio(tas2764), 0); + KUNIT_EXPECT_EQ(test, regs->writes, 0); + KUNIT_EXPECT_EQ(test, regs->val[TAS2764_CLK_CFG], 0); + KUNIT_EXPECT_EQ(test, regs->val[TAS2764_TDM_CFG0], + TAS2764_TDM_CFG0_44_1_48KHZ | TAS2764_TDM_CFG0_FRAME_START); +} + +/* 250 BCLKs (J813, J873): its code, and the fixed-ratio bit beside the rest */ +static void tas2764_bclk_ratio_250(struct kunit *test) +{ + struct tas2764_test_regs *regs; + struct tas2764_priv *tas2764; + struct device *dev; + + tas2764 = tas2764_test_bclk_priv(test, "tas2764-kunit-bclk-250", &dev, ®s); + KUNIT_EXPECT_EQ(test, tas2764_parse_bclk_ratio(dev, tas2764), 0); + KUNIT_EXPECT_EQ(test, tas2764->bclk_ratio_code, 0x30); + KUNIT_EXPECT_EQ(test, tas2764_apply_bclk_ratio(tas2764), 0); + KUNIT_EXPECT_EQ(test, regs->val[TAS2764_CLK_CFG], 0x30); + KUNIT_EXPECT_EQ(test, regs->val[TAS2764_TDM_CFG0], + TAS2764_TDM_CFG0_44_1_48KHZ | TAS2764_TDM_CFG0_FRAME_START | + TAS2764_TDM_CFG0_FIXED_RATIO); + KUNIT_EXPECT_EQ(test, regs->writes, 2); + /* The register cache keeps both across a reset and a resume */ + KUNIT_EXPECT_TRUE(test, tas2764_writeable_register(NULL, TAS2764_CLK_CFG)); + KUNIT_EXPECT_TRUE(test, tas2764_writeable_register(NULL, TAS2764_TDM_CFG0)); +} + +/* A ratio without a known code fails the probe instead of guessing */ +static void tas2764_bclk_ratio_unknown(struct kunit *test) +{ + struct tas2764_test_regs *regs; + struct tas2764_priv *tas2764; + struct device *dev; + + tas2764 = tas2764_test_bclk_priv(test, "tas2764-kunit-bclk-64", &dev, ®s); + KUNIT_EXPECT_EQ(test, tas2764_parse_bclk_ratio(dev, tas2764), -EINVAL); + KUNIT_EXPECT_EQ(test, tas2764->bclk_ratio_code, -1); + KUNIT_EXPECT_EQ(test, tas2764_apply_bclk_ratio(tas2764), 0); + KUNIT_EXPECT_EQ(test, regs->writes, 0); +} + +static struct kunit_case tas2764_bclk_ratio_test_cases[] = { + KUNIT_CASE_PARAM(tas2764_bclk_ratio_absent, tas2764_bclk_absent_gen_params), + KUNIT_CASE(tas2764_bclk_ratio_250), + KUNIT_CASE(tas2764_bclk_ratio_unknown), + {} +}; + +static struct kunit_suite tas2764_bclk_ratio_test_suite = { + .name = "tas2764-bclk-ratio", + .init = tas2764_bop_test_init, + .test_cases = tas2764_bclk_ratio_test_cases, +}; + +kunit_test_suite(tas2764_bclk_ratio_test_suite); diff --git a/sound/soc/codecs/tas2764-kunit.dtso b/sound/soc/codecs/tas2764-kunit.dtso index db624a512f563..d24172668d8b5 100644 --- a/sound/soc/codecs/tas2764-kunit.dtso +++ b/sound/soc/codecs/tas2764-kunit.dtso @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Nodes for the TAS2764 KUnit tests (tas2764-bop-test.h). The values are made - * up; only their length and placement matter. + * Nodes for the TAS2764 KUnit tests (tas2764-bop-test.h and + * tas2764-bclk-ratio-test.h). The values are made up; only their length + * and placement matter. */ /dts-v1/; /plugin/; @@ -32,4 +33,15 @@ tas2764-kunit-bop-none { compatible = "test,tas2764-kunit"; }; + + /* tas2764-bclk-ratio-test.h */ + tas2764-kunit-bclk-250 { + compatible = "test,tas2764-kunit"; + ti,bclk-ratio = <250>; + }; + + tas2764-kunit-bclk-64 { + compatible = "test,tas2764-kunit"; + ti,bclk-ratio = <64>; + }; }; diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index 5588b5e6cc9b0..d038e0424659f 100644 --- a/sound/soc/codecs/tas2764.c +++ b/sound/soc/codecs/tas2764.c @@ -1716,4 +1716,5 @@ MODULE_LICENSE("GPL v2"); #if IS_ENABLED(CONFIG_SND_SOC_TAS2764_KUNIT_TEST) #include "tas2764-bop-test.h" +#include "tas2764-bclk-ratio-test.h" #endif From 35ef6d573496cb8ef2de6209f5832957c0d7cf20 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Tue, 10 Nov 2026 13:46:34 +0100 Subject: [PATCH 15/24] dmaengine: apple-admac: test that only a configured capture realigns The paused-capture restart at a frame boundary is the T8152 AOP audio driver's alone: it passes a u32 frame size in the peripheral config for its speaker sense capture. Test that a slave config as mca_hw_params() builds it (zeroed, no peripheral config) and one with a peripheral config of another size give no frame size, and that a resume realigns only a capture channel with a frame size and a transfer, never a playback one, so the MCA machines' channels resume as before. Signed-off-by: Thomas Glanzmann --- drivers/dma/apple-admac-test.h | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/drivers/dma/apple-admac-test.h b/drivers/dma/apple-admac-test.h index 057a7c3597e7a..24d390b9d6d5b 100644 --- a/drivers/dma/apple-admac-test.h +++ b/drivers/dma/apple-admac-test.h @@ -136,3 +136,64 @@ static struct kunit_suite admac_pos_suite = { }; kunit_test_suite(admac_pos_suite); + +/* + * The paused-capture realignment (admac_cyclic_realign()) is the AOP audio + * driver's alone: it passes the frame size in the peripheral config, for its + * speaker sense capture. The MCA's PCM zeroes its slave config, so every MCA + * channel, and every playback channel, resumes where it paused. + */ +static void admac_realign_needs_a_frame_size(struct kunit *test) +{ + struct dma_slave_config config = {}; + u32 frame = 16; + u64 wrong = 16; + + /* As mca_hw_params() configures a channel */ + config.direction = DMA_DEV_TO_MEM; + config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + config.src_port_window_size = 4; + KUNIT_EXPECT_EQ(test, admac_config_realign_bytes(&config), 0); + + config.peripheral_config = &wrong; + config.peripheral_size = sizeof(wrong); + KUNIT_EXPECT_EQ(test, admac_config_realign_bytes(&config), 0); + + config.peripheral_config = &frame; + config.peripheral_size = sizeof(frame); + KUNIT_EXPECT_EQ(test, admac_config_realign_bytes(&config), 16); +} + +static void admac_realign_only_a_configured_capture(struct kunit *test) +{ + struct admac_tx tx = admac_test_tx(0, 0); + struct admac_chan adchan = { .current_tx = &tx }; + + /* Playback (even channels): never, frame size or not */ + adchan.no = 0; + KUNIT_EXPECT_FALSE(test, admac_resume_realigns(&adchan)); + adchan.realign_bytes = 16; + KUNIT_EXPECT_FALSE(test, admac_resume_realigns(&adchan)); + + /* Capture (odd channels): only with a frame size and a transfer */ + adchan.no = 1; + adchan.realign_bytes = 0; + KUNIT_EXPECT_FALSE(test, admac_resume_realigns(&adchan)); + adchan.realign_bytes = 16; + KUNIT_EXPECT_TRUE(test, admac_resume_realigns(&adchan)); + adchan.current_tx = NULL; + KUNIT_EXPECT_FALSE(test, admac_resume_realigns(&adchan)); +} + +static struct kunit_case admac_realign_cases[] = { + KUNIT_CASE(admac_realign_needs_a_frame_size), + KUNIT_CASE(admac_realign_only_a_configured_capture), + {} +}; + +static struct kunit_suite admac_realign_suite = { + .name = "apple-admac-realign", + .test_cases = admac_realign_cases, +}; + +kunit_test_suite(admac_realign_suite); From 114ebfd5a335f7ae374145a8042d47d628d2fb38 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Tue, 10 Nov 2026 13:47:07 +0100 Subject: [PATCH 16/24] ASoC: apple: macaudio: test the J813 and J873 speaker tables Cover the AOP-fed machines in the speaker KUnit suites: - apple-speaker-admission: J813 is admitted (it has a safe level), J873 is not (it has none). - apple-speaker-amp-level: neither carries an SN012776 gain; each amp's level is its apple,speaker-config from the boot loader. - apple-speaker-interface: on the J813 table the four SN012776 volumes start at the volume lock's ceiling, go up only once a daemon holds the lock and pings, and under "Speaker Protection" sit at the floor with no speaker start admitted until an owner is granted. Signed-off-by: Thomas Glanzmann --- sound/soc/apple/macaudio-admission-test.h | 3 ++ sound/soc/apple/macaudio-amp-level-test.h | 1 + sound/soc/apple/macaudio-iface-test.h | 59 +++++++++++++++++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/sound/soc/apple/macaudio-admission-test.h b/sound/soc/apple/macaudio-admission-test.h index b526f460487bf..5ad72fc3d0b32 100644 --- a/sound/soc/apple/macaudio-admission-test.h +++ b/sound/soc/apple/macaudio-admission-test.h @@ -11,6 +11,9 @@ static void macaudio_admission_needs_a_safe_level(struct kunit *test) KUNIT_EXPECT_FALSE(test, macaudio_speakers_allowed(&macaudio_j45x_cfg)); KUNIT_EXPECT_TRUE(test, macaudio_speakers_allowed(&macaudio_j413_cfg)); KUNIT_EXPECT_TRUE(test, macaudio_speakers_allowed(&macaudio_j700_cfg)); + /* The AOP-fed machines: J813 with its safe level, J873 without one */ + KUNIT_EXPECT_TRUE(test, macaudio_speakers_allowed(&macaudio_j813_cfg)); + KUNIT_EXPECT_FALSE(test, macaudio_speakers_allowed(&macaudio_j873_cfg)); cfg = macaudio_j413_cfg; cfg.safe_vol = 0; KUNIT_EXPECT_FALSE(test, macaudio_speakers_allowed(&cfg)); diff --git a/sound/soc/apple/macaudio-amp-level-test.h b/sound/soc/apple/macaudio-amp-level-test.h index 8d8181f96f6cc..007e2bbdefb3e 100644 --- a/sound/soc/apple/macaudio-amp-level-test.h +++ b/sound/soc/apple/macaudio-amp-level-test.h @@ -58,6 +58,7 @@ static void macaudio_level_no_table(struct kunit *test) &macaudio_j37x_j47x_cfg, &macaudio_j413_cfg, &macaudio_j415_cfg, &macaudio_j493_cfg, &macaudio_j575c_j773_cfg, &macaudio_j614_cfg, &macaudio_j616_cfg, &macaudio_j713_cfg, &macaudio_j715_cfg, + &macaudio_j813_cfg, &macaudio_j873_cfg, }; unsigned int i; diff --git a/sound/soc/apple/macaudio-iface-test.h b/sound/soc/apple/macaudio-iface-test.h index 83155d80b04d9..cdaf64dfa4a12 100644 --- a/sound/soc/apple/macaudio-iface-test.h +++ b/sound/soc/apple/macaudio-iface-test.h @@ -15,15 +15,16 @@ struct macaudio_test_iface { struct snd_ctl_file *daemon; }; -/* A bound card with both interfaces: the volume lock in charge, at its ceiling */ -static void macaudio_test_iface_card(struct kunit *test, struct macaudio_test_iface *f) +/* A bound card of @cfg with both interfaces: the volume lock in charge, at its ceiling */ +static void macaudio_test_iface_card_cfg(struct kunit *test, struct macaudio_test_iface *f, + const struct macaudio_platform_cfg *cfg) { struct snd_kcontrol_new slk = macaudio_test_control("Speaker Volume Unlock"); struct snd_kcontrol_new iface = macaudio_test_control("Speaker Protection Interface"); struct macaudio_snd_data *ma; unsigned int i; - macaudio_test_spk_card(test, &f->t, &macaudio_j316_cfg, SN012776_0DB); + macaudio_test_spk_card(test, &f->t, cfg, SN012776_0DB); ma = f->t.ma; f->daemon = kunit_kzalloc(test, sizeof(*f->daemon), GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, f->daemon); @@ -44,6 +45,12 @@ static void macaudio_test_iface_card(struct kunit *test, struct macaudio_test_if mutex_unlock(&ma->volume_lock_mutex); } +/* As macaudio_test_iface_card_cfg(), for J316 */ +static void macaudio_test_iface_card(struct kunit *test, struct macaudio_test_iface *f) +{ + macaudio_test_iface_card_cfg(test, f, &macaudio_j316_cfg); +} + static int macaudio_test_iface_write(struct kunit *test, struct macaudio_test_iface *f, unsigned int item) { @@ -238,6 +245,51 @@ static void macaudio_iface_needs_cap_sys_admin(struct kunit *test) KUNIT_EXPECT_EQ(test, macaudio_test_iface_write(test, &f, 2), -EINVAL); } +/* + * J813: its four SN012776 (the fake card's Left/Right Woofer/Tweeter + * volumes, the J813 amps' prefixes) are behind both interfaces from the + * bind, as on the MCA machines. Under the volume lock they stay at its + * ceiling until a daemon holds the lock and pings; under the lease they are + * at the floor and no speaker start is admitted until an owner is granted. + */ +static void macaudio_iface_j813_speakers_protected(struct kunit *test) +{ + struct macaudio_test_iface f; + + KUNIT_ASSERT_TRUE(test, macaudio_speakers_allowed(&macaudio_j813_cfg)); + macaudio_test_iface_card_cfg(test, &f, &macaudio_j813_cfg); + KUNIT_EXPECT_FALSE(test, macaudio_spk_leased(f.t.ma)); + KUNIT_EXPECT_FALSE(test, f.t.ma->speaker_volume_unlocked); + macaudio_test_expect_limit(test, &f, 1, 1); + + /* A daemon that holds the lock but has not pinged lifts nothing */ + macaudio_test_daemon_take(test, &f); + KUNIT_EXPECT_FALSE(test, f.t.ma->speaker_volume_unlocked); + macaudio_test_expect_limit(test, &f, 1, 1); + KUNIT_EXPECT_EQ(test, macaudio_test_daemon_ping(test, &f), 0); + KUNIT_EXPECT_TRUE(test, f.t.ma->speaker_volume_unlocked); + macaudio_test_expect_limit(test, &f, 1, SN012776_0DB); + + /* Into the lease: the floor, and nothing admitted without an owner */ + KUNIT_EXPECT_EQ(test, macaudio_test_iface_write(test, &f, MACAUDIO_SPK_IFACE_LEASE), 1); + KUNIT_EXPECT_TRUE(test, macaudio_spk_leased(f.t.ma)); + KUNIT_EXPECT_FALSE(test, f.t.ma->speaker_volume_unlocked); + KUNIT_EXPECT_FALSE(test, macaudio_spk_admitted(f.t.ma)); + macaudio_test_expect_limit(test, &f, 0, SN012776_0DB); + KUNIT_EXPECT_EQ(test, macaudio_test_write(test, &f.t, 50), -EPERM); + KUNIT_EXPECT_FALSE(test, macaudio_spk_admitted(f.t.ma)); + + /* Its owner is granted the operating gain, and the floor on release */ + macaudio_test_take(test, &f.t); + KUNIT_EXPECT_EQ(test, macaudio_test_write(test, &f.t, 50), 0); + KUNIT_EXPECT_TRUE(test, macaudio_spk_admitted(f.t.ma)); + macaudio_test_expect_limit(test, &f, SN012776_0DB, SN012776_0DB); + KUNIT_EXPECT_EQ(test, macaudio_test_write(test, &f.t, 0), 0); + macaudio_test_settle(f.t.ma); + KUNIT_EXPECT_FALSE(test, macaudio_spk_admitted(f.t.ma)); + macaudio_test_expect_limit(test, &f, 0, SN012776_0DB); +} + /* The switch is no mixer setting: alsactl neither saves nor restores it */ static void macaudio_iface_control_is_inactive(struct kunit *test) { @@ -254,6 +306,7 @@ static struct kunit_case macaudio_iface_cases[] = { KUNIT_CASE(macaudio_iface_unowned_lease_reverts), KUNIT_CASE(macaudio_iface_needs_cap_sys_admin), KUNIT_CASE(macaudio_iface_control_is_inactive), + KUNIT_CASE(macaudio_iface_j813_speakers_protected), {} }; From 8579c33950cb8b43d575c400cc69557ca982bcfc Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 11 Nov 2026 10:22:22 -0500 Subject: [PATCH 17/24] ASoC: apple: t8152-aop-audio: Describe T8142 and the microphones The driver also runs the J813 (T8142, MacBook Air M5) speakers and its microphone array card. Say so in the Kconfig help and the module description, which still named only the Mac mini jack and speaker. Signed-off-by: Diljit Singh --- sound/soc/apple/Kconfig | 5 +++-- sound/soc/apple/t8152_aop_audio.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index 74ea1fe4de8e8..32265803617ad 100644 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -22,8 +22,9 @@ config SND_SOC_APPLE_T8152_AOP_AUDIO 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. + co-processor on Apple T8152 (Mac mini M6) and T8142 (MacBook Air M5) + SoCs: the headphone jack, the internal speakers and, where present, + the microphone array. config SND_SOC_APPLE_MCA tristate "Apple Silicon MCA driver" diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs index 9976684a8d7d8..6b48d0a0eae21 100644 --- a/sound/soc/apple/t8152_aop_audio.rs +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -1498,6 +1498,6 @@ impl platform::Driver for T8152AopAudioDriver { module_platform_driver! { type: T8152AopAudioDriver, name: "snd_soc_t8152_aop_audio", - description: "Apple T8152 AOP audio: headphone jack and speaker", + description: "Apple T8152 AOP audio: headphone jack, speakers and microphones", license: "Dual MIT/GPL", } From de172b95d6765d42ebb27455ebf8c2bbf94afb27 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 11 Nov 2026 10:22:23 -0500 Subject: [PATCH 18/24] ASoC: tas2764: Keep the IRQZ_CLR bit next to INT_CLK_CFG TAS2764_CLK_CFG went in between TAS2764_INT_CLK_CFG and its TAS2764_INT_CLK_CFG_IRQZ_CLR field. Put the field back under its register. No functional change. Signed-off-by: Diljit Singh --- sound/soc/codecs/tas2764.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sound/soc/codecs/tas2764.h b/sound/soc/codecs/tas2764.h index 7f72f99734512..19691d94c4a1d 100644 --- a/sound/soc/codecs/tas2764.h +++ b/sound/soc/codecs/tas2764.h @@ -129,9 +129,9 @@ /* Clock/IRQ Settings */ #define TAS2764_INT_CLK_CFG TAS2764_REG(0x0, 0x5c) +#define TAS2764_INT_CLK_CFG_IRQZ_CLR BIT(2) /* 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) /* BOP_CFG0 to BOP_CFG24, page 0 0x1d to 0x34 */ From 713c8a249006e3db0a38ea2efaf59a774600a078 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Wed, 11 Nov 2026 10:29:58 -0500 Subject: [PATCH 19/24] ASoC: apple: Synchronize T8152 microphone capture shutdown Flush queued HPAI startup before releasing its dmaengine runtime, and provide sync_stop so ALSA waits for asynchronous DMA termination before reusing or freeing the capture buffer. This carries the microphone lifetime handling already present in the J815 extension into the common J813/T8152 foundation. No speaker admission, gain, protection or device-tree behavior changes. Signed-off-by: Diljit Singh --- sound/soc/apple/t8152_aop_audio.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs index 6b48d0a0eae21..f465a57f2bd2a 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, From 2b671f4ff2b96e6f05076cc31e4365fbd09e05ad Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Wed, 11 Nov 2026 10:33:05 -0500 Subject: [PATCH 20/24] ASoC: apple: Synchronize T8152 front-end PCM shutdown Pair the dmaengine front-end asynchronous STOP with sync_stop before ALSA reuses or releases its buffer. Skip ASoC back-ends, which have no dmaengine runtime, just as the T8140 AOP component does. Signed-off-by: Diljit Singh --- sound/soc/apple/t8152_aop_audio.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs index f465a57f2bd2a..e99a1250015e9 100644 --- a/sound/soc/apple/t8152_aop_audio.rs +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -845,6 +845,23 @@ unsafe extern "C" fn asoc_pcm_trigger( unsafe { bindings::snd_dmaengine_pcm_trigger(substream, cmd) } } +/// Wait for asynchronous DMA termination before ALSA reuses a front-end's buffer. +/// +/// # Safety +/// +/// Called by ASoC on this driver's component with a live substream. +unsafe extern "C" fn asoc_pcm_sync_stop( + _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_sync_stop(substream) } +} + /// # Safety /// /// Called by ASoC on this driver's component with live arguments. @@ -1082,6 +1099,7 @@ impl AsocContext { close: Some(asoc_pcm_close), hw_params: Some(asoc_pcm_hw_params), trigger: Some(asoc_pcm_trigger), + sync_stop: Some(asoc_pcm_sync_stop), pointer: Some(asoc_pcm_pointer), ..Default::default() }; From d4e7bf002801de98d4e1bc1b374364cbb45df6a8 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 11 Nov 2026 03:15:51 -0500 Subject: [PATCH 21/24] soc: apple: aop: Add T6050 The T6050 (M5 Pro and Max) AOP boots through a setup port, as the T8142 one does. Its endpoint map names system endpoint 4 besides the service endpoints 0x20, 0x23 and 0x32, but endpoint 4 never asks for a buffer. With the T8142 configuration the boot therefore times out with 3 of 4 endpoints, and the AFK endpoints never start. Add a T6050 configuration that doesn't wait for system endpoints. A buffer request from one is still answered. On a J714s the AOP then opens its AFK endpoints and announces its services, among them accel, gyro, las, als, als-temp, devmotion6, aop-audio and aop-voicetrigger. Signed-off-by: Diljit Singh --- drivers/soc/apple/aop.rs | 41 +++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index 49c753ad83ac9..c062835a60011 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -1038,6 +1038,8 @@ struct SetupState { expected: [u8; SETUP_MAX_ENDPOINTS], expected_count: usize, epmap_endpoints: bool, + /// Boot also waits for the system endpoints (below 0x20) it maps. + wait_system: bool, map_done: bool, ap_ready: bool, power_sent: bool, @@ -1046,7 +1048,7 @@ struct SetupState { } impl SetupState { - fn new(epmap_endpoints: bool) -> SetupState { + fn new(epmap_endpoints: bool, wait_system: bool) -> SetupState { let mut expected = [0; SETUP_MAX_ENDPOINTS]; let mut expected_count = 0; if !epmap_endpoints { @@ -1061,6 +1063,7 @@ impl SetupState { expected, expected_count, epmap_endpoints, + wait_system, map_done: false, ap_ready: false, power_sent: false, @@ -1100,10 +1103,17 @@ impl SetupState { } /// Boot is complete once the AP power state is on and every service - /// endpoint has its buffers. + /// endpoint has its buffers. A system endpoint that never asks for one + /// is not waited for unless `wait_system` is set. fn ready(&self) -> bool { - self.ap_ready && self.expected_count != 0 - && self.endpoint_count() == self.expected_count + self.ap_ready + && self.expected_count != 0 + && self.expected[..self.expected_count] + .iter() + .zip(self.endpoints.iter()) + .all(|(ep, slot)| { + slot.is_some() || (!self.wait_system && *ep < AFK_ENDPOINT_START) + }) } /// Sends one word to a setup-port endpoint. Callers run in process @@ -1401,7 +1411,10 @@ impl AopData { required_endpoints: cfg.required_endpoints, setup_queue, setup_lost: Atomic::new(false), - setup <- new_mutex!(SetupState::new(cfg.setup_epmap_endpoints)), + setup <- new_mutex!(SetupState::new( + cfg.setup_epmap_endpoints, + cfg.setup_wait_system_endpoints + )), setup_cv <- new_condvar!(), rtkit <- new_mutex!(None), endpoints <- pin_init::pin_init_array_from_fn(|i| { @@ -2624,6 +2637,8 @@ struct AopHwConfig { setup_port: bool, /// Its service endpoints are the ones its EPMAP announces, not T8140's. setup_epmap_endpoints: bool, + /// Its boot waits for the system endpoints the EPMAP names as well. + setup_wait_system_endpoints: bool, /// Shut the co-processor down on unbind and retain its buffers if the /// shutdown cannot be confirmed. quiesce_on_unbind: bool, @@ -2647,6 +2662,7 @@ const HW_CFG_T8103: AopHwConfig = AopHwConfig { epic_v4: false, setup_port: false, setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2664,6 +2680,7 @@ const HW_CFG_T8112: AopHwConfig = AopHwConfig { epic_v4: false, setup_port: false, setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2680,6 +2697,7 @@ const HW_CFG_T6000: AopHwConfig = AopHwConfig { epic_v4: false, setup_port: false, setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2695,6 +2713,7 @@ const HW_CFG_T6020: AopHwConfig = AopHwConfig { epic_v4: false, setup_port: false, setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2715,6 +2734,7 @@ const HW_CFG_T6040: AopHwConfig = AopHwConfig { epic_v4: false, setup_port: false, setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: true, required_endpoints: &[], fw_regions: true, @@ -2739,6 +2759,7 @@ const HW_CFG_T8140: AopHwConfig = AopHwConfig { epic_v4: true, setup_port: true, setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: true, required_endpoints: &[0x20, 0x21, 0x22, 0x23, 0x2b], fw_regions: false, @@ -2756,6 +2777,7 @@ const HW_CFG_T8152: AopHwConfig = AopHwConfig { epic_v4: true, setup_port: false, setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: true, required_endpoints: &[], fw_regions: false, @@ -2777,12 +2799,20 @@ const HW_CFG_T8142: AopHwConfig = AopHwConfig { epic_v4: true, setup_port: true, setup_epmap_endpoints: true, + setup_wait_system_endpoints: true, quiesce_on_unbind: true, required_endpoints: &[], fw_regions: false, bootargs_word_access: false, halt_on_failed_start: false, }; +/// T6050 (M5 Pro and Max): booted as T8142, through the setup port at +/// AOP ASC + 0x4007c000. Its EPMAP also names system endpoint 4, which never +/// asks for a buffer, so boot doesn't wait for it. +const HW_CFG_T6050: AopHwConfig = AopHwConfig { + setup_wait_system_endpoints: false, + ..HW_CFG_T8142 +}; kernel::of_device_table!( OF_TABLE, @@ -2795,6 +2825,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,t6050-aop")), &HW_CFG_T6050), (of::DeviceId::new(c_str!("apple,t8142-aop")), &HW_CFG_T8142), (of::DeviceId::new(c_str!("apple,t8152-aop")), &HW_CFG_T8152), ] From 76b99d5cbac0169276e6841996e8cb5f44131e62 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 11 Nov 2026 03:15:51 -0500 Subject: [PATCH 22/24] dt-bindings: mailbox: apple: Add the T6050 and T8142 AOP setup ports Both use the T8140 setup-port register layout. T8142 already names its own compatible in the J813 device tree. Signed-off-by: Diljit Singh --- .../devicetree/bindings/mailbox/apple,mailbox.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index add726470307d..eb6d3f5580761 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -76,6 +76,14 @@ properties: AOP setup-port mailbox with a separate register layout. const: apple,t8140-aop-setup-mailbox + - description: + Later AOP setup-port mailboxes with the T8140 register layout. + items: + - enum: + - apple,t6050-aop-setup-mailbox + - apple,t8142-aop-setup-mailbox + - const: apple,t8140-aop-setup-mailbox + - description: An older ASC mailbox interface found on T2 and A11 that is also used for the NVMe coprocessor and the system management From f62705c5b9d596f0eb396115354843fe9bd3aeba Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 11 Nov 2026 03:46:43 -0500 Subject: [PATCH 23/24] arm64: dts: apple: t6050: Put the AOP setup port at its real address The ADT reg of aop-exclave-mailbox is not behind the arm-io translation. As on T8140 and T8142 it is the AOP ASC + 0x4007c000, 0x2d067c000. The 0x4d067c000 this tree used takes an SError, which is why the AOP was described as booting without its setup port. At 0x2d067c000 the port's control words read back and the AOP firmware runs its setup handshake on it. Describe it as the setup-port mailbox, with T8142's interrupt names and dart-aop stream 1 (ADT vm-base-1 0x20000000000, vm-size-1 0x2000000000), and give the AOP both mailboxes. The J716s and J873s ADT coverage manifests carried the old address; they now name the real one. Signed-off-by: Diljit Singh --- .../dts/apple/adt-coverage/t6050-j716s.txt | 2 +- .../dts/apple/adt-coverage/t6050-j873s.txt | 2 +- arch/arm64/boot/dts/apple/t6050-die0.dtsi | 14 ++++++-------- .../boot/dts/apple/t6050-peripherals.dtsi | 18 +++++++++++++----- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/arch/arm64/boot/dts/apple/adt-coverage/t6050-j716s.txt b/arch/arm64/boot/dts/apple/adt-coverage/t6050-j716s.txt index 82218ccb772ab..d2d99f8601003 100644 --- a/arch/arm64/boot/dts/apple/adt-coverage/t6050-j716s.txt +++ b/arch/arm64/boot/dts/apple/adt-coverage/t6050-j716s.txt @@ -453,7 +453,7 @@ /arm-io/dart-aop/mapper-admac-leap-s - -> /soc@200000000/iommu@290fc0000 /arm-io/dart-aop/mapper-admac-base-s - -> /soc@200000000/iommu@290fc0000 /arm-io/dart-aop/mapper-scm - -> /soc@200000000/iommu@290fc0000 -/arm-io/aop-exclave-mailbox 0x4d067c000 -> /soc/mailbox@4d067c000 +/arm-io/aop-exclave-mailbox 0x2d067c000 -> /soc/mailbox@2d067c000 /arm-io/aop-exclave-ioreporting - -> MISSING /arm-io/aop2 0x291600000 -> /soc/aop@291600000 /arm-io/aop2/iop-aop2-nub - -> /soc/aop@291600000 diff --git a/arch/arm64/boot/dts/apple/adt-coverage/t6050-j873s.txt b/arch/arm64/boot/dts/apple/adt-coverage/t6050-j873s.txt index c014ce038d1f0..b155feb7c8e31 100644 --- a/arch/arm64/boot/dts/apple/adt-coverage/t6050-j873s.txt +++ b/arch/arm64/boot/dts/apple/adt-coverage/t6050-j873s.txt @@ -377,7 +377,7 @@ /arm-io/dart-aop/mapper-admac-leap-s - -> /soc@200000000/iommu@290fc0000 /arm-io/dart-aop/mapper-admac-base-s - -> /soc@200000000/iommu@290fc0000 /arm-io/dart-aop/mapper-scm - -> /soc@200000000/iommu@290fc0000 -/arm-io/aop-exclave-mailbox 0x4d067c000 -> /soc/mailbox@4d067c000 +/arm-io/aop-exclave-mailbox 0x2d067c000 -> /soc/mailbox@2d067c000 /arm-io/aop-exclave-ioreporting - -> MISSING /arm-io/aop2 0x291600000 -> /soc/aop@291600000 /arm-io/aop2/iop-aop2-nub - -> /soc/aop@291600000 diff --git a/arch/arm64/boot/dts/apple/t6050-die0.dtsi b/arch/arm64/boot/dts/apple/t6050-die0.dtsi index 5b140884a52a0..7a93a3592bd5f 100644 --- a/arch/arm64/boot/dts/apple/t6050-die0.dtsi +++ b/arch/arm64/boot/dts/apple/t6050-die0.dtsi @@ -650,9 +650,9 @@ /* * The AOP's audio path: the AP GPIO block, the I2C1/I2C2 buses and their power states (in * pmgr1, below), and the AOP with its mailbox, dart-aop and the ADMAC its audio streams use. - * From the ADT (arm-io ranges +0x200000000). The AOP, its mailbox, dart-aop and the ADMAC are disabled: - * iBoot leaves the AOP halted on locked dart-aop tables, and Aura enables them only when its - * checks of that state pass (chosen.aurora,t6050-aop=1). + * From the ADT (arm-io ranges +0x200000000). The AOP, its mailboxes, dart-aop and the ADMAC are + * disabled here: iBoot leaves the AOP halted on locked dart-aop tables, and the boards that + * boot it enable them. */ pinctrl_ap: pinctrl@29c000000 { compatible = "apple,t6050-pinctrl", "apple,t8103-pinctrl"; @@ -790,17 +790,15 @@ /* * The AOP: its SRAM window (ADT reg[2]) and its ASC (reg[0]). Its firmware is loaded and - * left halted by iBoot. Booted as on T8152, over the RTKit mailbox alone: the ADT's - * aop-exclave-mailbox (the setup port T8140 and T8142 boot through) is exclave-assigned - * here and takes an SError from the AP. + * left halted by iBoot. Booted as on T8142, over the RTKit mailbox and the setup port. */ aop: aop@290c00000 { compatible = "apple,t6050-aop", "apple,t8152-aop"; reg = <0x2 0x90c00000 0x0 0x1e0000>, <0x2 0x90600000 0x0 0x4000>; reg-names = "mmio", "asc"; - mboxes = <&aop_mbox>; - mbox-names = "mbox"; + mboxes = <&aop_mbox>, <&aop_setup_mbox>; + mbox-names = "mbox", "setup"; iommus = <&aop_dart 0>; apple,dma-range = <0x110 0x0 0x10 0x0>; status = "disabled"; diff --git a/arch/arm64/boot/dts/apple/t6050-peripherals.dtsi b/arch/arm64/boot/dts/apple/t6050-peripherals.dtsi index cfdae30f9062e..fdc1e2fe16d85 100644 --- a/arch/arm64/boot/dts/apple/t6050-peripherals.dtsi +++ b/arch/arm64/boot/dts/apple/t6050-peripherals.dtsi @@ -1965,12 +1965,20 @@ status = "disabled"; }; - aop_exclave_mailbox: mailbox@4d067c000 { - compatible = "apple,t6050-secure-mailbox", "apple,secure-mailbox"; - reg = <0x4 0xd067c000 0x0 0x4000>; - interrupts = , - ; + /* + * The AOP setup port, ADT aop-exclave-mailbox. Its ADT reg is not behind the arm-io + * translation: AOP ASC + 0x4007c000, as on T8140 and T8142. ADT interrupts [585, 582]. + * dart-aop stream 1: ADT vm-base-1 0x20000000000, vm-size-1 0x2000000000. + */ + aop_setup_mbox: mailbox@2d067c000 { + compatible = "apple,t6050-aop-setup-mailbox", "apple,t8140-aop-setup-mailbox"; + reg = <0x2 0xd067c000 0x0 0x4000>; + interrupts = , + ; + interrupt-names = "send-empty", "recv-not-empty"; iommus = <&aop_dart 1>; + apple,dma-range = <0x200 0x0 0x20 0x0>; + #mbox-cells = <0>; status = "disabled"; }; From 3c16dac7500d47f16f0a85d85c2fd755b0d9eda7 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 11 Nov 2026 03:46:43 -0500 Subject: [PATCH 24/24] arm64: dts: apple: t6050-j714s: Enable the AOP Boot the AOP through its setup port: its RTKit mailbox, the setup port, dart-aop and the AOP itself. Signed-off-by: Diljit Singh --- arch/arm64/boot/dts/apple/t6050-j714s.dts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t6050-j714s.dts b/arch/arm64/boot/dts/apple/t6050-j714s.dts index f599a4e16bf1c..f2bdb7d0a1985 100644 --- a/arch/arm64/boot/dts/apple/t6050-j714s.dts +++ b/arch/arm64/boot/dts/apple/t6050-j714s.dts @@ -450,3 +450,20 @@ &i2c2 { status = "disabled"; }; + +/* The AOP, booted through its setup port, with its sensor and audio services. */ +&aop_mbox { + status = "okay"; +}; + +&aop_setup_mbox { + status = "okay"; +}; + +&aop_dart { + status = "okay"; +}; + +&aop { + status = "okay"; +};