Skip to content

soc: apple: aop, ASoC: J616s (MacBook Pro M4 Pro) audio: microphones, speakers and headphone jack - #406

Merged
RyanTheTide merged 21 commits into
aurora-silicon:integration/aurorafrom
jgoyvaerts:m4pro/audio-j616s
Oct 24, 2026
Merged

RyanTheTide merged 21 commits into
aurora-silicon:integration/aurorafrom
jgoyvaerts:m4pro/audio-j616s

Conversation

@jgoyvaerts

@jgoyvaerts jgoyvaerts commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Audio for the MacBook Pro 16" M4 Pro (J616s, T6040): the AOP driver and its audio and ambient light services on the T6040 (microphones, ambient light and lid angle sensors), the six internal speakers with the SN012776 amp protection they need, and the 3.5 mm headphone jack. It's one series because each part builds on the one before: the speakers on the AOP audio component, the jack on the speaker component's file and machine driver changes. The userspace side goes out with it: the J616s graphs in asahi-audio and the UCM in alsa-ucm-conf-asahi (links below).

Depends on: nothing open. Merged: the T6041 ADMAC (#379), which carries the microphone, speaker and jack DMA channels; the t604x bindings (#372); and two commits of #368 the speakers rely on at run time ("ASoC: tas2764: do not report an unreadable interrupt latch as faults", which keeps a failed latch read from looking like every fault bit to the SDZ group, and "ALSA: pcm_dmaengine: report an XRUN when the DMA channel failed"). The device tree nodes are in #396.

AOP and microphones (commits 1-7)

  • Binding: apple,t6041-aop in its own schema: the four register windows of Apple's device tree (the AOP SRAM and the ASC wrapper, which the driver uses, then the second ASC window and the configuration registers), one mailbox, the DART, and the audio, als and las service children (apple,t6041-aop-als / apple,t6041-aop-las with the existing apple,aop-als / apple,aop-las fallbacks).
  • Binding (new): apple,t6041-aop-audio for the audio child: the compatible (with the apple,t6030-aop-audio fallback the driver matches), the ADMAC channels by name (dma for the microphones, spkr and sense for the speakers, the optional cout/cin for the jack), the microphone card's chassis and machine names, and #sound-dai-cells (3 = the speaker back-end, 4 = the jack back-end). Modelled on apple,t8140-aop-audio.
  • aop (T6040 support):
    • The firmware takes the T6020's four boot arguments (EC0p 0x100_0000_0000, nCal 0, alig 64, AOPt 0) and starts through RTKit over one mailbox, not through the T8140 setup port.
    • Linux resets the AOP's DART, and no bootloader mapping survives that. The firmware fetches its external code (__ETEXT) at the EC0p IOVA and writes its log segment (__OS_LOG) at its physical address; without the latter it stops within a second on a storm of DART faults. The bootloader reserves both as /reserved-memory/aop-etext@… and aop-oslog@…, and the driver maps them before the start. Each node must be unique, page aligned and at most 1 MiB, each mapping is read back, and nothing starts if a check fails. The mappings go on unbind once the coprocessor is quiesced, and are kept, like the other buffers, when the shutdown can't be confirmed.
    • The firmware is shut down on unbind. When the start fails after the CPU was released, the driver tears down as usual and halts the CPU again, as the bootloader left it.
  • aop (every SoC): bounds-check the boot argument table: its offset and size against the SRAM, and every entry's length against the table and the 8-byte value.
  • aop_als (every SoC): the calibration blob (apple/aop-als-cal.bin, extracted from macOS) is optional. Without it the sensor still reports, uncalibrated, with a warning.
  • aop_audio (every SoC): fix the PCM open and close paths. Open sets up the dmaengine PCM before it powers the microphones on, releases the channel on every error, and closes the PCM again if the power-on fails. Close releases the channel before it asks the firmware to power down, so a failing or timed-out call can't leave the DMA running. sync_stop drains the completion callbacks. The card name fields can't overflow, and AudioSetDeviceProp<T> is only Zeroable when T is.
  • aop_audio (T6040 microphones): apple,t6041-aop-audio follows macOS's IOPAudioLPMicDevice flow. On this firmware LPAI property 301 is the read-only stream description (writing it returns kIOReturnNotWritable). The channel masks go to property 300 instead (supported, enabled and history channels, 7 each, from the ADT's /product/audio), followed by the PDM and high-frequency decimator setup. A refused step is logged and doesn't stop the probe, so the next step's answer is still seen.

The AOP's DAPF is not part of this PR: our loader programs it before Linux starts.

Speakers (commits 8-15)

  • Bindings: ti,tas27xx documents SDZ-supply (the driver already takes it) and apple,sdz-group-fail-closed. apple,macaudio gets apple,j616-macaudio.

  • tas2764: keep SDZ high across system sleep (every machine). Pulling SDZ low on suspend put every amp of a shared line through a power-on reset on resume, milliseconds before ASoC unmuted a running stream. The woofer amps' sense then reported negative power for seconds, and speakersafetyd panicked 2-3 times per resume. The Apple quirk writes on page 0xfd are volatile and were lost too. The amp now waits in software shutdown across sleep, as between streams. The cache-only window and the sync stay. This comes before the SDZ group, so the group doesn't touch the resume path this commit removes.

  • tas2764: SDZ fault group. All six amps share one shutdown line, modelled as a fixed regulator that every amp takes as its SDZ-supply. Amps that share the supply and carry apple,sdz-group-fail-closed form a group. An over-temperature or over-current interrupt on any of them forces the shared supply off, which puts the whole group in hardware shutdown, and no amp of the group powers up again until the driver is reloaded. A plain regulator disable can't do this: one amp's disable leaves a shared line up for the others. The group check and the supply enable are one step under the group lock. A member without a fault interrupt, or whose fault IRQ can't be requested, fails its probe, since it could never take the group down, and a member with a shutdown GPIO of its own is refused, since that line would bypass the supply.

  • aop_audio: the J616s speaker streams (new file aop_spkr.rs). The AOP firmware owns the MCA and the audio power states here. spkr runs MCA cluster 0: a 250-bit frame with six 32-bit playback slots and twelve 16-bit V/I sense slots, on base-ns ADMAC channels 0 and 1. The speaker component has front-ends j616-pcm-0..2 and the spkr back-end. Startup attaches the service and raises pw1, prepare raises pwrd, hw_free drops back to pw1, and the last shutdown goes idle, each confirmed by reading the state back. Every front-end holds its own power reference around its channel. Neither slot stream has a frame marker, so every start is frame-aligned:

    • the driver owns the sense channel, armed on the stopped serializer before every pwrd, and the sense front-end copies whole frames out of that ring;
    • every playback prepare restarts the serializer, with the amps muted first;
    • PAUSE/RESUME are not offered;
    • system sleep takes the serializer down and brings it back, ring first, before the amps unmute.

    Without this, the sense pairs one amp's V with another amp's I, and speakersafetyd reads negative power and panics.

  • macaudio: fail closed when a speaker volume lock does not take (every machine). The controls are limited one at a time. If any of them fails while locking, every running playback of the card stops and prepares are refused until the driver is reloaded.

  • macaudio: lock the speaker volumes before userspace freezes (every machine). The volumes are locked from PM_SUSPEND_PREPARE to PM_POST_SUSPEND, so a frozen sound server's last period doesn't play into unmuted amps (a short crackle at every sleep). The speakersafetyd lease ends at suspend-prepare and this lock isn't reported as a lost lease, so the daemon keeps its temperatures across s2idle. The PM notifier lives only while the card is bound. Both lock changes come before the J616s variant, so the J616s speakers are never enabled with the older lock semantics.

  • macaudio: J616s speakers. The apple,j616-macaudio variant: front-ends from the AOP component, I2S with an inverted frame clock on a 250-SCLK frame (secondary bclk ratio 250), amp gain cap 15, safe_vol -20 and V/I sense, as on the J316. Unlocked, the speakers go to 0 dB like on every other machine, with speakersafetyd limiting them. fe_cpu_is_platform skips the front-end platform lookup by OF node, which would add every component on the AOP audio node to every runtime (the jack component sits on the same node).

Headphone jack (commits 16-21)

  • Binding: apple,macaudio gets apple,j616-macaudio-jack.
  • cs42l84: DSP_A pulse frames. The AOP runs the jack's MCA cluster 2 as a 64-SCLK frame with a one-SCLK FSYNC pulse, two 32-bit slots and data one bit clock after the rising edge. The codec now takes DSP_A next to I2S: TDM mode, channel 2 on the frame-start edge like channel 1, starting one slot after it. I2S keeps its register values. The TDM-mode semantics are inferred from the I2S setup, since no datasheet is public.
  • aop_spkr: the headphone jack streams. The jack is two more services of the AOP firmware: cout drives MCA cluster 2 (a 64-bit frame at 48 kHz, two 32-bit slots, 24 valid bits) on base-ns ADMAC channel 4, and cin brings the headset microphone back on the same serializer on channel 5. Each is a service of its own, with the speaker's power sequence (pw1 at startup, pwrd with the output or input flag at prepare) and no sense ring. The jack is a second ASoC component, t6040-aop-jack, for a card of its own: front-ends j616-jack-pcm-0 (2 channels out, 1 in) and -1 (routes only), and the jack back-end, sound-dai argument 4, answered through of_xlate_dai_name while every other argument is deferred to the speaker component. It registers before the speaker component and only when the node lists cout and cin, and a failure costs the jack only. System sleep takes both services down with the streams suspended and brings back what ran before ASoC unmutes the codec.
  • macaudio: each card gets its own jack pins. snd_soc_jack_add_pins() links the pins through their own list heads, and macaudio gave every card the same static array. With the jack as a second card, the card that probes last re-links the pins into its own jack, and the other card's next jack report walks into garbage. On our daily kernel that oopsed the CS42L84 interrupt thread at boot (snd_soc_jack_report → dapm_find_widget → strncmp) and left jack detection dead. The pins are now copied into each card's data.
  • macaudio: the jack card. apple,j616-macaudio-jack is a sound card of its own. A card binds only once all of its codecs have probed, so as a link of the speaker card, a jack codec that never answers would take the speakers down with it. On its own card it costs the jack only. Its front-ends are the AOP jack component's j616-jack-pcm-0/-1, with DSP_A and an inverted bit clock (as on the J700) and a 64× bit clock (3.072 MHz). The card has no sense link, so the fixed route PCM2 RX <- Speaker Sense Capture is now only added when the card has sense. Without that, the card failed to register. Cards with MCA front-ends lose a route to an unused widget.
  • macaudio: cap the J616s headphone volume at -6 dB. A jack_max_vol machine setting applies snd_soc_limit_volume() to "Jack DAC Playback Volume" before the card registers, so userspace only ever sees the capped range. The card fails to bind if the limit doesn't apply. Above 0 dB the DAC adds only digital gain. The 6 dB below that are margin: this board's analog full scale is unmeasured, and macOS adapts it to the detected headphone impedance, which this driver doesn't.

Hardware tested on: Apple MacBook Pro (16-inch, M4 Pro, 2024), J616s / T6040, macOS 26.6.2 firmware, booted by our own loader.

How tested: Our daily kernel 7.3.0-m4.9, on integration/aurora ac6b017bce, carries this code. Its only additions in these files are test-only: a bring-up speaker ceiling parameter (speaker_ceiling_db, left at 0) and M4_SPKR/M4_JACK log lines. On it:

  • the AOP starts at every boot; the microphones are the desktop's default input (3 channels; a recording was checked on an earlier daily build), and the ALS drives the screen brightness;
  • the speakers are the default output through the asahi-audio graph below, with speakersafetyd (apple/j616.conf) running its thermal model on the V/I sense and unlocking the volumes to 0 dB; all six speakers were checked one by one by ear on an earlier daily build;
  • the headphone jack plays and detects plug and unplug (checked by ear);
  • daily s2idle suspend (lid and menu) ran with the microphones and speakers on earlier daily builds.

The sleep, alignment and lease fixes in the speaker part come from those runs, where speakersafetyd used to panic on misaligned or reset sense data.

Not tested / limits:

  • The every-machine changes (aop boot argument check, aop_als, aop_audio teardown, tas2764 sleep, macaudio fail-closed and sleep lock) were only run on the J616s.
  • The tas2764 SDZ group was checked by its probe-time rules and code review: no real amp fault has occurred.
  • The jack's system-sleep path (both services down for suspend, back on resume) has not run on hardware yet, and the jack's headset microphone has not been checked. A jack stream prepared again while its serializer runs (after an XRUN) restarts its DMA on the running serializer, since cout and cin share it; that the two slots still line up then (no left/right swap) has not been checked. Headphone impedance detection and the analog output level are not handled (hence the cap). The cs42l84 DSP_A path was only run on the J616s; I2S machines keep their register values, but that path was not re-run.
  • Integration has no schema for the legacy (T6020) AOP audio node, so apple,t6030-aop-audio is only documented as the fallback. apple,no-beamforming, which aop_audio reads on the AOP node, is left out of the schema: no T604x node uses it and no existing AOP binding documents it.

Also needed (userspace, to go out together): aurora-silicon/asahi-audio#8 (the J616s speaker graph with the pop fix, and the jack card's priorities) and aurora-silicon/alsa-ucm-conf-asahi#5 (the speaker and jack card UCM).

Credits: The AOP, ALS/LAS and AOP audio drivers, the macaudio volume interlock, the tas2764 driver and speakersafetyd are the Asahi Linux and aurora contributors' work. The AOP speaker component's structure (the component context, the DAI tables and the dmaengine front-end helpers) is derived from aurora's t8140_aop_audio.rs, and the power sequence follows the aurora contributors' J616s control test. The T6040 microphone flow follows what macOS's IOPAudioLPMicDevice sends. The T6041 ADMAC support underneath is Niklas Sheth's port (#379).

Review notes: the series folds our bring-up history (frame alignment, sense period fix, pause/resume, a bring-up -10 dB ceiling that was later removed, the PM notifier lifetime fix) into the commits it fixed.

Commits:

  1. dt-bindings: soc: apple: Add the T6040 AOP
  2. ASoC: dt-bindings: Add the Apple T6041 AOP audio
  3. soc: apple: aop: Add T6040 support
  4. soc: apple: aop: Bounds-check the boot argument table
  5. iio: aop_als: make the calibration blob optional
  6. ASoC: apple: aop_audio: fix the PCM open/close error and teardown paths
  7. ASoC: apple: aop_audio: configure the T6040 microphones as macOS does
  8. ASoC: dt-bindings: ti,tas27xx: Add SDZ-supply and an SDZ fault group
  9. ASoC: tas2764: keep SDZ high across system sleep
  10. ASoC: tas2764: shut a whole SDZ group down on an amp fault
  11. ASoC: dt-bindings: apple,macaudio: Add the J616s
  12. ASoC: apple: aop_audio: add the J616s speaker streams
  13. ASoC: macaudio: fail closed when a speaker volume lock does not take
  14. ASoC: apple: macaudio: lock the speaker volumes before userspace freezes
  15. ASoC: macaudio: add the J616s speakers on the AOP audio component
  16. ASoC: dt-bindings: apple,macaudio: Add the J616s headphone jack card
  17. ASoC: cs42l84: support DSP_A pulse frames
  18. ASoC: apple: aop_spkr: add the J616s headphone jack streams
  19. ASoC: macaudio: give each card its own jack pins
  20. ASoC: macaudio: add the J616s headphone jack card
  21. ASoC: macaudio: cap the J616s headphone volume at -6 dB

Base: integration/aurora (ac6b017bce). Built at every commit with our kernel configuration (Rust on; APPLE_AOP, the AOP ALS sensor, SND_SOC_APPLE_AOP_AUDIO, SND_SOC_APPLE_MACAUDIO, SND_SOC_CS42L84 and SND_SOC_TAS2764 as modules), W=1, no warnings. dt_binding_check (dtschema 2026.9) is clean for apple,t6041-aop.yaml, apple,t6041-aop-audio.yaml, apple,macaudio.yaml and ti,tas27xx.yaml. checkpatch --strict shows only: the MAINTAINERS reminder for the new files (the existing sound/soc/apple/* entry covers aop_spkr.rs); CamelCase on the existing CS42L84_ASP_RX_CHx_* register macros; continuation alignment that follows the rest of cs42l84_set_dai_fmt(); and one long commit-log line quoting the kernel's oops.

🤖 Generated with Claude Code

The T6040 (M4 Pro) AOP starts like the T6020 one, through RTKit over a
single mailbox with patched boot arguments, not through the T8140 setup
port. Its firmware's external code and log segments must be mapped into
the AOP's DART again before the start, from /reserved-memory nodes the
bootloader adds. Describe the node with its four register windows, as
Apple's device tree lists them (the driver uses the SRAM and the ASC
wrapper), and its audio, ambient light and lid angle service children.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
Describe the T6041 AOP's audio child, which the AOP binding leaves a
bare object: the compatible, the ADMAC channels by name (the
microphones' "dma", the speakers' "spkr" and "sense", and the optional
headset jack's "cout" and "cin"), the chassis and machine names of the
microphone card, and "#sound-dai-cells", whose values 3 and 4 select
the AOP audio driver's speaker and headset jack back-ends.

The schema is modelled on the T8140 one, and the AOP binding's audio
child references it.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
The T6040 (M4 Pro) AOP firmware takes the same boot arguments as on the
T6020 (EC0p 0x100_0000_0000, nCal 0, alig 64, AOPt 0) and starts through
RTKit over one mailbox, but two things differ:

- Linux resets the AOP's DART, and no mapping the bootloader made
  survives that. The firmware fetches its external code segment
  (__ETEXT) at the EC0p IOVA and writes its log segment (__OS_LOG) at
  its physical address; without the latter it stops within a second on
  a storm of DART faults. The bootloader reserves both segments as
  /reserved-memory/aop-etext@<phys> and aop-oslog@<phys>. Map them into
  the AOP's DMA domain before the coprocessor starts: each node must be
  unique, page aligned and at most 1 MiB, each mapping is read back, and
  nothing is started if any check fails. The mappings are removed on
  unbind once the coprocessor is quiesced, and kept, like the other
  buffers, when the shutdown cannot be confirmed.
- The firmware is shut down on unbind. When its start fails after the
  CPU was released, the driver tears down as usual and then clears the
  CPU run bit again, leaving the AOP halted as the bootloader left it.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
patch_bootargs() takes the offset and size of the firmware's boot
argument table from the AOP's own SRAM and walks its records with
unchecked slice indexing. A table or record length that does not fit,
or a patched record longer than eight bytes, panics the kernel on an
out-of-bounds slice.

Check the table against the mapped region before reading it, and each
record header and length against the table while walking it, and fail
the probe with -EINVAL on a malformed table. A key missing from the
table is still skipped, so firmware without one of them behaves as
before.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
The ambient light sensor's calibration blob (apple/aop-als-cal.bin) is
extracted from macOS and is not installed on every system. Without it
the driver fails to probe, although the sensor reports without it, only
uncalibrated (seen on the M4 Pro as on earlier machines).

Load the blob when present; otherwise warn that the readings are
uncalibrated and carry on. The CT817 path, which gets its calibration
over the setup port, is unchanged.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
Several paths of the AOP microphone PCM leak or race:
- open requested the DMA channel and released it by hand on one error
  path only, and it powered the microphones on before the dmaengine PCM
  was set up, so a failed snd_dmaengine_pcm_open() left them on and
  leaked the channel. Release the channel through a scope guard until
  the PCM helper owns it, open the dmaengine PCM first, and close it
  again if the power-on request fails (ALSA does not call close after a
  failed open).
- close asked the firmware to power down before the DMA channel was
  released; a failing or timed-out firmware call then returned early
  with the channel still running and its callbacks pending, and ALSA
  ignores close's error. Release the channel first, then power down.
- there was no sync_stop, so DMA completion callbacks could still run
  after a stop; use snd_dmaengine_pcm_sync_stop().
- copy_str() overran the card's fixed-size name fields when a name did
  not fit; it now truncates and always NUL-terminates.
- AudioSetDeviceProp<T> was Zeroable for every T; require T: Zeroable.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
On the T6040 (M4 Pro) the AOP firmware follows macOS's
IOPAudioLPMicDevice configuration flow, which differs from the T6030
one this driver uses:
- LPAI property 301 is the read-only stream description; writing the
  channel configuration there is refused with kIOReturnNotWritable.
- The channel masks are set through property 300 instead, as a 12-byte
  record of the supported, enabled and history channel masks from the
  ADT node /product/audio (all 7 on the J616s).

Add the apple,t6041-aop-audio compatible with a T6040 flow: after the
usual device attaches, set the channel control (property 300), the PDM
configuration and the high-frequency decimator configuration. As on
macOS, a refused step is reported but not fatal, so the following steps
still run and the card is still registered.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
The tas2764 driver already takes an optional SDZ supply, which the
binding does not describe. Apple machines drive the SDZ pins of all
their amplifiers from one line: the J616s (MacBook Pro 16" M4 Pro) has
six SN012776 on a single shutdown GPIO, modelled as a fixed regulator
that every amp takes as its SDZ-supply.

Document SDZ-supply, and apple,sdz-group-fail-closed: the amplifiers
that share an SDZ supply and carry the property form a group, and an
over-temperature or over-current fault on any of them turns the shared
supply off for the whole group. A member must have the supply and must
not have a shutdown GPIO of its own, which would bypass it.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
The codec suspend pulled the SDZ line low and the resume raised it again.
On boards where all amps share one SDZ supply (J616s: six SN012776 on one
fixed regulator) the last suspend puts every amp into hardware shutdown,
and the first resume puts every amp through a power-on reset. A few
milliseconds after the register cache sync, soc_resume_deferred() unmutes
the amps of a stream that was running at suspend, with the audio already
flowing.

After such a resume the woofer amps' I/V sense reports a negative average
power (-0.012 to -0.021 W) for several seconds; speakersafetyd treats that
as broken sense data and panics, the speaker volume lock times out, and the
speakers cut out until the daemon restarts (2-3 times per resume). It never
happens on cold boot or on any ordinary stream start: there the amps leave
software shutdown, not a power-on reset that is milliseconds old.

The power cycle also loses state the cache cannot restore: the Apple quirk
writes to page 0xfd sit in the volatile range, so regcache_sync() never
replays them, and after the first sleep the amps run without them until
reboot.

Leave SDZ (supply and per-amp GPIO) as it is across system sleep. The amp
waits in software shutdown, as between streams, and keeps its registers.
The cache-only window and the sync stay, so the bus is not touched while
suspended and a supply the platform cuts anyway is still restored.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
Amps that share one SDZ supply and carry "apple,sdz-group-fail-closed"
form a group (J616s: six SN012776 on one shutdown line). An
over-temperature or over-current interrupt on any of them forces the
shared supply off, which puts every amp of the group in hardware
shutdown, and marks the group failed. From then on no amp of the group
enables the supply, resumes, changes its power mode or starts a stream
until the driver is reloaded.

The supply's enable count cannot express this on its own: one amp's
disable leaves a shared line up while the others hold it.

The safeguards around it:
- the group check and the supply enable are one step under the group
  lock, and so are marking the group failed and forcing the supply off,
  so an enable cannot slip in between;
- a group member without a fault interrupt, or whose fault IRQ cannot
  be requested, fails its probe: it could never take the group down;
- a group with a shutdown GPIO on an amp is refused, since that line
  would bypass the supply.

An unreadable interrupt latch is not taken for a fault since
commit f0e5882 ("ASoC: tas2764: do not report an unreadable
interrupt latch as faults"), so an I2C error cannot shut the group
down.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
The J616s (MacBook Pro 16" M4 Pro) speaker card runs on the AOP audio
component instead of the MCA. Add its compatible.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
On the J616s (MacBook Pro 16" M4 Pro) the AOP firmware owns the MCA and
the audio power states for the internal speakers: its "spkr" service
runs MCA cluster 0 (serializer ms00, 12 MHz crystal), a 250-bit frame
at 48 kHz with six 32-bit playback slots and twelve 16-bit V/I sense
slots. The base-ns ADMAC moves both directions (channel 0, TX0, the
playback; channel 1, RX0, the sense) through AOP DART stream 8.

Add an ASoC component for it next to the microphone card. The service
has its own attachment, users and power states, each request confirmed
by reading property 200 back: back-end startup attaches the service and
raises pw1, which powers the audio fabric and the ADMAC in it; prepare
requests pwrd (output flag 2), which runs the serializer clocks before
the amps unmute; hw_free drops back to pw1 after they muted; the last
shutdown goes idle. Every front-end holds its own power reference from
before it requests its channel until after it releases it: DPCM starts
the back-end before the front-end opens and shuts it down before the
front-end closes, so without it the channel release would touch an
unpowered ADMAC.

The component has DPCM front-ends j616-pcm-0..2 (the names macaudio's
J616 variant binds to; j616-pcm-1 plays six S24_LE channels, j616-pcm-2
captures the twelve S16_LE sense channels) and the spkr back-end (DAI
index 3). Neither slot stream carries a frame marker: a channel lines
up with its slots only when its DMA meets a serializer that starts
afresh (Asahi's MCA driver resets the serializer on every trigger for
this reason). Otherwise the sense pairs one amp's V with another amp's
I, and speakersafetyd reads negative power and panics. So every start
is aligned:

- The driver owns the sense channel. A cyclic ring is armed on the
  stopped serializer before every pwrd, so ring offset 0 is slot 0. The
  sense front-end does not start a channel: the ring callback copies
  whole frames into its buffer while it runs, so a sense start during
  playback attaches at the next frame, and signals it once per period.
- Every playback prepare starts a fresh serializer: a running one goes
  down to pw1 (the amps are muted first; ASoC unmutes them after the
  prepare), the sense ring is re-armed, then pwrd. The playback channel
  starts in the trigger afterwards.
- The front-ends do not offer PAUSE or RESUME, which would restart the
  DMA where it stopped instead of on a frame boundary: after system
  sleep userspace prepares the stream again.
- System sleep: the component suspend takes a running serializer down
  and stops the ring; resume re-arms the ring and raises pwrd again
  before ASoC unmutes the amps of the suspended stream.

The component is registered when the audio node lists a "spkr" DMA
channel; the microphone card is unchanged.

The power sequence follows the aurora contributors' J616s control test
(spkr attached, pw1, pwrd with output flag 2, pw1, idle). The component
structure (the component context with the descriptor as its first
field, the DAI tables and the dmaengine front-end helpers) is derived
from t8140_aop_audio.rs by the aurora contributors.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
Locking the speaker volumes clips each "* Speaker Volume" to the safe
value. snd_soc_limit_volume() stops at the first control it cannot clip,
for instance on an I2C error, and leaves the rest at their old limit and
value. The card still reported the volumes as locked.

Limit the controls one at a time, so that one failure does not skip the
others. If any of them fails while locking, mark the lock failed: stop
every running playback stream of the card and refuse playback prepares
until the driver is reloaded, since a volume above the safe value may be
left behind.

The stream stop shares one helper with the existing software-gain stop,
which only stops the software-gain links prepared before the current
lock epoch. The software-gain limit counts as failed when any control
failed or none matched, as before.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
Going to sleep with music playing crackles briefly. The card's own
suspend is in the right order: snd_soc_suspend() mutes the amps (tas2764
mute: ramp, then software shutdown) before it suspends the PCMs and the
components. The noise comes earlier: the suspend core freezes userspace
first, and the card suspends more than half a second later. The frozen
sound server stops feeding a running playback stream, which plays out up
to a period of stale buffer and stops on an xrun, into amps that are
still unmuted at full volume: the frozen speakersafetyd's lease runs out
only later.

Take the speaker volume lock (the ceiling every lease timeout applies)
from PM_SUSPEND_PREPARE, before the freeze, until PM_POST_SUSPEND.

End the speakersafetyd lease at suspend-prepare as well, and don't
report this lock as a lost lease. Otherwise the daemon's first ping
after the resume gets -ETIMEDOUT, the notice for a lease it lost, and it
restarts cold, forgetting its speaker temperatures. The lease clock
(CLOCK_MONOTONIC) also stops across s2idle with the tick frozen, so a
lease still running at the resume would unlock the volumes and lock them
again when it expired, before the thawed daemon pinged. A ping between
suspend-prepare and the freeze doesn't start a new lease. After the
resume the volumes stay locked until the daemon's first ping, which
unlocks them. Its thermal model measures time with CLOCK_MONOTONIC too,
so across a sleep it assumes the speakers did not cool: continuing with
it is the conservative choice.

The PM notifier walks the card's controls, which exist only while the
card is bound: register it at the end of late_probe and unregister it
from the card's remove callback.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
J616s (MacBook Pro 16" M4 Pro, AID35): the AOP firmware runs MCA
cluster 0, so the front-ends are the AOP audio driver's j616-pcm-0..2
and the back-end is its spkr DAI. One serializer carries all six
SN012776 amps in a 250-bit frame at 12 MHz, so the secondary
(speaker) front-end's bclk ratio is 250, and the amps take I2S with an
inverted frame clock and a normal bit clock, as on the J713 (T8132: the
same amp and frame). The protection settings are the J316's: amp gain
cap 15 (18.5 dBV, the level macOS's speaker model assumes), the volume
lock at safe_vol -20, and V/I sense. Unlocked, the speakers go up to
0 dB like on every other macaudio machine: speakersafetyd models the
coil and magnet temperatures and lowers the volume itself.

The AOP speaker and jack components sit on one OF node, and the
front-ends' platform lookup by OF node adds every component it matches
to a runtime: the speaker card would take the jack component (and call
its PCM ops for its own streams), and a jack card would then fail to
bind it. Add fe_cpu_is_platform to skip the front-end platform: the CPU
DAI's component, which every runtime includes, is the PCM provider
already. Other machines are unchanged.

Add the apple,j616-macaudio variant with these settings, and a
secondary_bclk_ratio next to the existing primary_bclk_ratio for a
speaker front-end whose frame length the firmware sets.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
On the J616s (MacBook Pro 16" M4 Pro) the headphone jack's CS42L84 sits
on a different AOP firmware stream than the speaker amps, with a
different frame, and gets a sound card of its own, so that a codec that
never answers costs the jack only. Add its compatible.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
The driver only takes I2S, where each channel follows its own FSYNC
edge and the frame must be 50 %. On machines whose serial port is run
by the AOP firmware (the M4 Pro MacBook Pro's headphone jack on MCA
cluster 2) the frame is a one-bit FSYNC pulse in a 64-bit frame with
two 32-bit slots, data one bit clock after the rising edge: DSP_A.

Take DSP_A with the same bus polarities as the I2S case (IB_NF, which
for I2S is IB_IF): set the ASP's TDM mode, put channel 2 on the frame-
start edge like channel 1, and start it one physical slot after
channel 1 in hw_params. I2S keeps the register values it had.

The TDM-mode semantics (slot start in bit clocks from the frame-start
edge, the one-bit delay kept) are inferred from the I2S configuration;
no datasheet is public.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
The J616s 3.5 mm jack is two more services of the AOP firmware that
runs the speakers: "cout" drives MCA cluster 2 (serializer ms02, mck2
from the 3.072 MHz NCO: a 64-bit frame at 48 kHz with two 32-bit slots,
24 valid bits) fed by base-ns ADMAC channel 4 (TX2), and "cin " brings
the headset microphone back on the same serializer through channel 5
(RX2). These are the iop-audio-controller streams 'tuoc' and ' nic' of
the machine's device tree. The firmware arbitrates the MCA and the
audio power states between the speaker and jack streams.

Each jack direction is a service of its own, with its own attachment,
users and power states, like the speaker's: back-end startup raises
pw1, prepare requests pwrd (output flag 2 for "cout", input flag 1 for
"cin ", as the microphones use), hw_free drops back to pw1. Neither has
a sense ring, so the service's ring becomes optional, and a jack
service's prepare only raises pwrd when its serializer is not running
yet. The jack's 8- and 4-byte frames need whole 16-byte ADMAC bursts
per period, so its front-ends constrain the period size to them.

The jack is a second component, "t6040-aop-jack", for a sound card of
its own: front-end j616-jack-pcm-0 (playback on "cout", 2 channels,
capture on "cin", 1 channel, S24_LE at 48 kHz), j616-jack-pcm-1 (only
the stream widget macaudio's fixed routes need; it refuses to open) and
the "jack" back-end. A component binds to one card only, and a card
binds only once all of its codecs have probed: on its own card, a jack
codec that never answers costs the jack, not the speakers. Both
components sit on the audio node, and ASoC resolves a sound-dai there
by asking each component in turn. The jack component answers argument
4 through of_xlate_dai_name and defers every other argument, and it
registers before the speaker component, so neither card can probe into
a half-registered state. A jack registration failure is logged and
leaves the speakers alone. The jack component is registered when the
audio node lists the "cout" and "cin" DMA channels.

System sleep takes both jack services down with the streams suspended,
as for the speaker: suspend drops a running service to pw1 and notes
that it ran, resume brings it back to pwrd before ASoC unmutes the
codec. Both services are resumed even if the first fails; the first
error is returned.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
snd_soc_jack_add_pins() links the pins it is given into the jack's pin
list through their own list heads. macaudio passes every card the same
static array. That works with one card per machine, but the J616s
headphone jack becomes a card of its own next to the speaker card.

With two cards, the second card to probe re-links the shared pins into
its own jack. The first card's pin list then runs into the second card's
list head, and its next jack report dereferences garbage as a pin name.
On the J616s that oopsed the CS42L84 interrupt thread at boot and left
jack detection dead until the next boot.

Copy the pins into the card's own data and keep the template const.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
On the J616s the jack's CS42L84 sits on a different AOP firmware
stream than the speaker amps, with a different frame: the jack's
stream on MCA cluster 2 is a 64-SCLK frame with a one-SCLK FSYNC
pulse, two 32-bit slots and data launched on the rising SCLK edge,
which is DSP_A with an inverted bit clock (as on the J700), while the
amps take I2S with an inverted frame clock on a 250-SCLK frame.

Give the jack a card of its own, "apple,j616-macaudio-jack": the AOP
audio driver's jack component provides its front-ends
(j616-jack-pcm-0 and -1) and back-end, and the primary front-end's
64 bit-clock ratio gives the codec its 3.072 MHz. A card binds only
once all of its codecs have probed, so as a link of the speaker card
a codec that never answers would take the speakers down with it; on
its own card it costs the jack only. Like the speaker card, it takes
the front-ends' CPU component as their PCM provider, since the AOP
speaker and jack components share one OF node.

The jack component has no PCM2, so the fixed route PCM2 RX <- Speaker
Sense Capture fails and the card does not register:

  snd-soc-macaudio sound-jack: ASoC: Failed to add route Speaker Sense Capture -> PCM2 RX(*)

Drop the route whenever the sense link is dropped. Cards with MCA
front-ends lose a route to an unused widget.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
The CS42L84 DAC volume goes up to +12 dB, and nothing limits it on the
jack card. Add an optional headphone volume ceiling to the machine
configuration, set through snd_soc_limit_volume() on "Jack DAC Playback
Volume" before the card registers, so userspace sees the capped range
from the start, and the codec refuses writes above it since
commit eaefdaa ("ASoC: cs42l84: honour the platform volume limit").
The card fails to bind if the limit does not apply, as with the speaker
limits.

Cap the J616s jack at -6 dB. Above 0 dB the DAC adds only digital gain,
which clips a full-scale signal. The 6 dB below that are margin: the
analog full scale of this board's headphone output has not been
measured, and macOS adapts it to the headphone impedance it detects,
which this driver does not do.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
@fbalazsy

Copy link
Copy Markdown

Tested the headphone jack on a second J616s (MacBook Pro 16" M4 Pro, macOS 26.6.2 firmware, own m4-loader rev 15 build). Headset: Apple EarPods (3.5 mm, with microphone).

Kernel: 7.1.12-m4.27 = jgoyvaerts/linux m4/aurora (7e0e24f72), which carries an earlier version of this series. That includes the jack streams with "aop_spkr: take the jack's streams down for system sleep" and "macaudio: give each card its own jack pins". aop_spkr.rs differs from this PR's head (b7ab2c5c2) in about 50 lines, mostly the M4_JACK log lines and refactors, so this isn't a run of the exact head. Userspace: the J616 and J616 Jack UCM verbs we ship are identical to those in aurora-silicon/alsa-ucm-conf-asahi#5, apart from comments.

Test Result
Plug detection pass: Headphone Jack and Headset Mic Jack go on; the jack becomes the default sink and source
Headphone playback pass, checked by ear
Headset microphone (cin, through the HiFi-jack verb) pass: speech is recorded and is intelligible on playback. The level is low: speech peaks around -19 dBFS and stays around -49 dBFS RMS, with Jack ADC PGA Capture Volume 24 and Jack ADC Preamp Capture Volume 2 (both at maximum). Not compared with macOS.
Jack across s2idle: a tone playing through the jack, a 10 s s2idle, woken by the keyboard pass: the stream keeps running, cout returns to pwrd on resume, and the tone plays again afterwards (checked by ear). The speaker card logs "Speaker volumes locked: System suspend" as expected.

Not tested: a jack stream re-prepared after an XRUN (left/right slot alignment).

@RyanTheTide

Copy link
Copy Markdown
Contributor

Thanks, this is a big piece and most of it is in good shape. We've reviewed it, built it with our universal config (Fedora base + asahi.config), and booted it on the Neo and on our J616s. We're holding the merge for one reason, and we're working on that part ourselves.

Why it's on hold. On our J616s the AOP doesn't come up: apple_aop 510c10000.aop: Failed to map "aop-etext" (ENOENT), then probe -2, so there are no cards and both sound nodes stay deferred. aop.rs looks for /reserved-memory/aop-etext@... and aop-oslog@..., which your loader creates and ours (Aurora m1n1 for T6040) doesn't. No binding describes those nodes, and the driver then maps them with iommu_map() straight into the DMA-API domain, where the IOVA allocator doesn't know about them. We'd like to do this the way m1n1 and the kernel already handle DCP and ISP firmware: m1n1 reserves the segments from the ADT with iommu-addresses and a memory-region phandle on the AOP node, and the driver takes them from there. We'll add the m1n1 side and a small change to aop.rs on top of your series, and test it on the J616s.

What we checked so far

  • The Neo (A18 Pro) boots this series with no regressions: speakerguardd is fine, "Speaker volumes unlocked" at boot, and across a 30 s s2idle it now logs "locked: System suspend" and then "unlocked", the mic records, and keyboard and trackpad stay up. Our M1 Pro and M2 runs are in progress.
  • No Gravity Linux code in the series as far as we can tell (their audio, tas2764 and cs42l84 code is different).

Fixups we're adding on top (separate commits from us, your commits stay as they are)

  • tas2764: keeping SDZ high across sleep changed every TAS2764/SN012776 machine, but it has only run on the J616s. We keep the new behaviour for amps in an SDZ group and leave the others as before. The codec remove also skips the regulator disable after a group failure, since that supply was already forced off.
  • macaudio: the front-end trigger refuses playback START/RESUME/PAUSE_RELEASE once the speaker lock has failed, so a stream prepared just before the failure can't start, and the stop walk skips back-end links.
  • The apple,t6041-aop-audio binding selects only T6041 nodes. With the apple,t6030-aop-audio fallback in it, dtbs_check started applying it to the T6030, T6031 and T8122 AOP audio nodes and failing them.
  • Two comments named a macOS class and method; they now just say what is sent.

Follow-ups, not blockers: aop_spkr.rs repeats a lot of t8140_aop_audio.rs (we'd like a shared helper later), a failed pwrd on resume should mute the back-end, and the microphone masks should come from the DT before another T604x board uses this compatible.

The userspace PRs (alsa-ucm-conf-asahi #5, asahi-audio #8) wait for this, since they need the cards.

@RyanTheTide RyanTheTide left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: the AOP now comes up on our boot chain, so we're merging this with our follow-ups (ryan/j616s-audio-406).

AOP firmware regions. m1n1 now reserves the AOP's two DRAM segments (__ETEXT at its remap address, __OS_LOG at its physical address) from the ADT segment-ranges. Each gets an apple,asc-mem node with iommu-addresses, plus a memory-region on the AOP node, the same way m1n1 does it for DCP and ISP (aurora-silicon/m1n1#41). Our follow-up to aop.rs takes the regions from memory-region, and the IOMMU core maps them. The node-name lookup of aop-etext/aop-oslog and the raw iommu_map() are gone. This means your m4-loader needs to write the same memory-region and iommu-addresses, or the AOP stops at "No firmware memory-region from the bootloader".

Tested on our J616s with integration/aurora + this series + our follow-ups (7.3.0-aurora-73s3, universal config, 0 warnings):

  • The AOP boots with no errors and no DART faults.
  • Three cards come up: J616 HPAI, J616 and Jack. All six amps bind.
  • The microphones record on all three channels.
  • Your UCM (alsa-ucm-conf-asahi#5) parses, and every control it sets exists.
  • The speaker PCM stays closed because our machine has no speakersafetyd config for the J616 yet. That's the fail-closed lock doing its job, so the speakers themselves aren't tested here.
  • One thing we noticed: the HPAI capture PCM advertises every sample format but always delivers float, so an S32 capture is garbage. We'll look at that separately.

Neo and M1 Pro: no regressions, with speakerguardd/speakersafetyd fine across s2idle and the new suspend lock lines as expected.

Thanks for the big series.

RyanTheTide added a commit that referenced this pull request Oct 24, 2026
@RyanTheTide

Copy link
Copy Markdown
Contributor

A correction to our approval above. We wrote that the speaker PCM stayed closed because our J616s had no speakersafetyd config. That was wrong, and your speaker side was fine.

The open failed because our test opened hw:AppleJ616,1 without the UCM verb. "Speaker Playback Mux" defaults to Primary, which on this card is j616-pcm-0 with no stream. With no back-end routed to Secondary, ASoC's DPCM open returns EINVAL. It logs "no backend DAIs enabled" only once per boot, and that one line had already gone at card creation, so our later attempts failed silently. Our installed system also had Fedora's alsa-ucm-asahi without your J616 UCM, so PipeWire couldn't route it either.

With alsa-ucm-conf-asahi#5 installed, the verb routes the speakers and the PCM opens as S24_LE, 6 channels, 48 kHz. We added a J616 speakersafetyd profile: speakersafetyd's j316.conf values with your channel map (aurora-silicon/fedora-aurora#23). With that, our J616s (7.3.0-aurora-73v, which carries this series and our follow-ups) passes all of these:

  • The daemon unlocks the volumes. Stopping or freezing it locks them again within a second.
  • Band noise on one amp channel at a time shows up only in that speaker's sense channels, with positive V*I power.
  • Quiet steps straight to the speaker PCM at -40, -34 and -28 dBFS track at the built-in microphones (+6.0 and +5.9 dB per step), with no limiting, no daemon restarts and no kernel messages.

One thing to compare on your machine: our left woofer 1 reads about half the current of the other woofers at the same drive, and its voltage reads the same as theirs. Its group shares the gain with left woofer 2, which gets the same signal in the J616 graph, so the protection holds. Still, it would help to know whether yours does the same.

@RyanTheTide

Copy link
Copy Markdown
Contributor

A follow-up on the left woofer 1 point above. It isn't the sense slot as such. On our J316 (M1 Pro), slot 0 is left woofer 1 as well, and in a session where all four woofers got the same program they read the same V*I power: 0.674, 0.663, 0.666 and 0.686 W at peak. On our J616s we swapped the pairings in a test config. LW1's current times LW2's voltage reads 0.012 W, and LW2's current times LW1's voltage reads 0.024 W, the same as the other woofers. So only LW1's current reads low, by about half. If your machine reads its four woofers alike, then it's something on ours.

@fbalazsy

Copy link
Copy Markdown

Checked the left woofer 1 question on our J616s (7.3.0-m4.33, which carries this series' audio code, speakersafetyd 2.0.1 with our j616.conf: is/vs 2i/2i+1, is_scale 3.75, vs_scale 14, volumes unlocked by the daemon). Band noise at -20 dBFS RMS on one channel of hw:AppleJ616,1 at a time, 6 s each (woofers 150-1500 Hz, tweeters 2-8 kHz), power read from speakersafetyd's per-speaker debug output, two runs:

Speaker Run 1 (W) Run 2 (W)
Left Woofer 1 0.330 0.328
Right Woofer 1 0.339 0.338
Left Woofer 2 0.334 0.333
Right Woofer 2 0.337 0.336
Left Tweeter 0.354 0.352
Right Tweeter 0.352 0.352

Only the driven speaker showed power, with no negative power, no gain reduction and no kernel messages. Our four woofers read alike (within 3%), so the low LW1 current looks specific to your machine. We didn't read V and I separately, only their product, but a halved LW1 current would halve its V*I, and it doesn't here.

@jgoyvaerts

Copy link
Copy Markdown
Contributor Author

Our J616s reads left woofer 1 like the others too, so it looks specific to your unit.

This is from our speaker bring-up captures (7.1 kernels with the same tas2764/macaudio code, our own checker reading the V and I sense channels separately, not speakersafetyd's product). A 43 Hz tone at −40 dB (10.51 mV rms commanded) went to one amp at a time:

Speaker V (mV rms) |Z| = V/I (Ω) |Z| on a later boot (Ω)
Left Woofer 1 10.18 3.30 3.37
Right Woofer 1 9.63 3.05 3.10
Left Woofer 2 9.56 3.12 3.19
Right Woofer 2 10.39 3.46 3.50

A halved LW1 current would read about 6.6 Ω here. With pink noise at −40 dB on all six, the four woofers each read 3.1–3.2 mA at 11.2–12.0 mV.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants