Skip to content

ASoC: apple: J713 (M4 MacBook Air) microphones, headphone jack and speakers - #820

Merged
erykwieliczko merged 22 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/audio-integ
Nov 4, 2026
Merged

erykwieliczko merged 22 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/audio-integ

Conversation

@erikwb

@erikwb erikwb commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

The J713 (MacBook Air 13" M4, T8132) audio on the AOP: the internal microphones, the headphone jack and the speakers, plus the ASoC fixes the J713 speakers need. It's one ASoC series, and it replaces #437, #439, #663, #580, #759, #760, #765 and #799, plus the audio half of #434. The review points raised on those PRs are answered below.

Depends on #819 (soc: apple: aop: T8132). Its two commits come first here; this PR adds the other 19.

Before this can ship

Commits

Microphones (3–5)

  • The T8132 AOP audio binding. dma-names allows the microphones alone, the microphones with the jack, and the two speaker layouts.
  • The T8132 AOP and audio ADMACs. The channel map is the T6041 one, from the ADT iop-audio-controller: speaker base-ns 0/1, jack 4/5, mics on LEAP.
  • The AOP enabled on the J713, with the microphones (3 channels, 48 kHz).

Headphone jack (6–9)

  • aop_audio registers the jack where the node lists its own channels, without a speaker stream. Creating the unused services doesn't reach the firmware.
  • The J713 jack reuses macaudio_j616_jack_cfg, including its −6 dB headphone DAC cap. This board's analog full scale is unmeasured too.
  • DT:
    • the CS42L84 on i2c1 at 0x4b, with reset on nub GPIO 17 and the interrupt on AP GPIO 98, level-low;
    • the jack's ADMAC channels and its own card;
    • ps_i2c1 enabled. Without that the codec never probes (i2c-apple −110).

Speakers (10–12)

  • macaudio_j713_cfg:
    • SN012776 amps, 1W1T per side, amp gain 15 and safe volume −20.
    • J616s-style AOP front-ends.
    • A new speaker_fe_channels (6). The AOP's speaker front-end carries six channels, the last two silent, and one channel per amp made the open fail with EINVAL.
  • DT:
    • the four amps on i2c3, in provider order 0x38 LW, 0x3b RW, 0x39 LT, 0x3c RT;
    • their sense slots (amp i: imon at RX byte 4i, vmon at 4i+2);
    • the shared SDZ line (AP GPIO 15) as the fail-closed SDZ group's supply, and the shared IRQ (AP GPIO 14);
    • the amp-tx-zd-config idle masks 0xf0f0 / 0x0f0f;
    • the speaker and sense channels;
    • ps_i2c3 enabled.
  • The ADT's speaker entry lists 4 playback and 8 sense slots, but the stream is the J616s one: 6 playback and 12 sense, of which the amps use the first 4 and 8.

Clean sense at stream ends (13–16)

  • A new apple,deferred-shutdown on ti,tas27xx, set on the four J713 amps. The amp is muted at the stream's end as before, and goes to software shutdown 150 ms later. The AOP kept the bus running for up to 3520 frames (73 ms) after a stream ended.
    • A stream that starts in between cancels the pending shutdown.
    • Suspend and removal shut the amp down at once.
    • A clock-only fault while the amp is stopped is expected, and no longer logged.
  • At unmute, a latch that holds only the clock fault picked up while stopped is cleared before the amp counts as running. It's judged under a lock shared with the IRQ handler. Any other latched fault is left for the handler.

Speaker volumes after a system sleep (17)

  • Since 1cbce20, the speaker volumes stay clipped at −100 dB after the first sleep. alsactl saves them that way. After a reboot, they stay clipped unless the daemon's cold start sets them after alsactl's restore; Jonathan's J616s boots clean.
  • At suspend-prepare, the volumes are kept before the lock clips them. They're set again at the first unlock after the resume, which is the daemon's first ping.
    • A second sleep before that ping keeps what the first saved.
    • The card's controls are held for reading across the keep/clip and in the unlock work, so a newer daemon volume can't be overwritten.
    • A new lock owner drops what was kept.
  • The card's remove cancels the lock update work while the snd_card still exists. That work takes controls_rwsem, and a back-end stop during teardown can queue it.

Card rebinds and module reloads (18–21)

  • aop_audio accepts a device that is still attached from a previous load (kIOReturnBusy). The speaker and jack services already did. Before, reloading snd-soc-aop left the machine with no audio until a reboot.
  • tas2764 writes the idle slot setup again after the probe's reset. Before, a rebind left the idle-filling amps without the force-0 enable (SDOUT_HIZ_9 bit 7). That corrupted the right amps' V-sense, and the protection daemon then held the speakers at the safe level.
  • tas2764 frees its fault IRQ in remove, and requests it last in the probe. Before, each rebind added four more handlers on the shared line.
  • A failed probe now lowers SDZ and drops the supply enable it took. ASoC doesn't call the remove after a failed probe.

Testing

This series, on integration/aurora 41c4055

On the J713. The test kernel also carried #429, for the J713 display.

  • Boot: untainted. All three cards register: microphones, jack, and speakers with their sense PCM. The AOP starts (RTKit: Initializing (protocol version 12)) with no DART faults.

  • Speaker protection: speakerguardd takes the lock and unlocks the volumes. Playback through PipeWire's UCM speaker route runs.

  • Amps: 4 handlers on the shared fault line; force-0 is 0x80 on both idle-filling amps; the SDZ supply holds one enable per amp.

  • Microphones: they record room noise at about −50 dBFS.

  • Silent restarts: 0.3 s silent streams with gaps of 40 ms to 1 s before each start. 0 of 64 starts logged an amp fault, and nothing was logged outside a stream.

  • Rebind and reload: after a snd-soc-macaudio rebind and then a full snd-soc-aop reload, all three cards come back. Force-0 is still 0x80, there are still 4 IRQ handlers, the microphones record, the speakers play and the daemon governs again.

  • AOP start failure and rebind (asked for in soc: apple: aop: T8132 (M4 MacBook Air) AOP and microphones #434): unbinding apple_aop itself times out waiting for the endpoints to acknowledge their shutdown (5 s, mask 0xfff). On the rebind, RTKit endpoint discovery fails (−62). The failure path then behaves as designed:

    • the AOP is halted again;
    • its buffers are kept until reboot;
    • the probe fails cleanly, with no oops and an untainted kernel.

    Audio comes back after a reboot. So the AOP core can't be rebound on J713, but its failure path is safe. Consumer reloads, as above, are fine.

On our J713 tree when each part was first posted

Each part ran on integration/aurora with the earlier parts of this series. The commits are unchanged since, except commit 17's card-remove cancel.

Microphones and jack
  • cs42l84 0-004b: Cirrus Logic CS42L84, Revision: 36; the jack card registers.
  • The cap holds: Jack DAC Playback Volume tops out at step 244, i.e. −6 dB.
  • Playback: a 440 Hz tone at −20 dBFS is audible, the DMA runs in real time, and the DAPM path from Headphone Playback Mux through Jack HP to Headphone is on.
  • Stereo: a left-only tone and then a right-only tone came out of the left and then the right earcup.
  • Plug detection: Headphone Jack and Headset Mic Jack flip on unplug and replug, with one codec interrupt per event.
  • Headset microphone: speech at about −10 dBFS over a −50 dBFS floor, at 0 dB PGA and +10 dB preamp.
  • No DART or AOP errors.
Speakers

These ran with speakersafetyd 3.0.0 holding the lease, on the M3 Air's j613 profile with 12 channels. The ADT's per-unit DC resistances (4.00/4.10 Ω woofers, 3.55/3.68 Ω tweeters) match j613's nominals.

  • Probe: all four amps bind; the SDZ regulator has 4 users.
  • Interlock: without the daemon, the volumes stay at the locked maximum (silent). With it, Speaker volumes unlocked. Stopping it gives Speaker volumes locked: Lock timeout.
  • Channel → amp → sense map: a −30 dBFS chord on channels 0–3 (500 / 700 / 1100 / 1300 Hz), with each tone located by Goertzel in the raw sense buffer. Each tone appears in exactly one amp's I/V pair: ch 0 → left woofer, ch 1 → right woofer, ch 2 → left tweeter, ch 3 → right tweeter. Sense channels 8–11 are silent, with no cross-talk.
  • Power per speaker: 15.0 / 15.8 / 12.8 / 13.9 mW.
  • By ear: woofers low and tweeters high, on the right sides.
  • Timing: with 6/12 channels the stream runs in real time; a 4/8 wiring ran 1.5× fast.
Deferred shutdown and the stale clock fault (13–16)
  • Sense tails, from three silent streams and a chord with the volumes locked: about 90 full-scale words per run before, all on the right tweeter's V slot at each stop. None after. Every amp still ends in software shutdown (PWR_CTRL 0x2).

  • Restarts: pairs of short streams 90, 120 and 150 ms apart. All four amps play, the sense stays valid, and there are no pops.

  • Faults at restarts:

    Starts that logged a fault
    Without commit 16 9 of 64, all on the left woofer: 9 of 32 with gaps up to 140 ms, 0 of 32 with gaps of 170 ms or more
    With commit 16 0 of 96 (and 0 of 96 with tracing, where 11 stale latches were cleared at unmute)
  • Under speakerguardd: a recording of 13 streams, including those restarts, has no full-scale sense words, and its replay certifies.

Volumes after sleep (17)
Before After
Speaker volumes after a suspend-to-idle all four at 1 (−100 dB), for good 1 until the daemon pings, then all four back at 201 (0 dB)
Two suspends, the second with no ping before playback — still 1 after the second resume; back at 201 when playback starts
Playback PCM runs, nothing audible audible
speakersafetyd restart needed yes no

Jonathan reproduced the bug on a J616s in #580: six volumes at 1 after one s2idle cycle, with the daemon's PID unchanged.

Reloads and rebinds (18–21)
  • Reload (18): before, rmmod/insmod snd_soc_aop failed the probe on the pdm0 attach. After: five reload cycles, and each time the three cards are back, the speakers and jack play, the mics record at the same level, and the daemon governs.

  • Idle slots (19): SDOUT_HIZ_9 on the idle-filling amps.

    Fresh module After a macaudio rebind After an snd-soc-aop reload
    Without 0x80 0x00 0x00
    With 0x80 0x80 0x80

    Without it, after a reload, the right woofer's and right tweeter's V channels each had about 11,700 of 48,000 words with the top bit set. With it, none.

  • IRQ (20): 12 handlers on the fault line after two rebinds without the fix; 4 with it, fresh, after two rebinds and after a reload. The IRQ still fires and is handled.

  • SDZ (21): I made the first setup write fail, then bound the card twice and then cleanly. Enables on sn012776-sdz: without the fix, 1-0038 holds 2 after the failed binds and 3 after the good one. With it, none and then 1 per amp.

Checks

  • checkpatch --strict: no errors. The one warning and one check are the MAINTAINERS note, which the existing sound/soc/apple/* entry covers, and the kunit_test_suite() blank line, which follows the admission test header.
  • Universal WERROR build (defconfig + asahi.config): all 133 fragment settings survive. Every file this series touches builds with WERROR: aop.o, aop_audio.o, macaudio.o, tas2764.o and the J713 DTB. The tree as a whole stops in drivers/nvme/host/apple.c, mt7932_bt_pcie.c and the asahi Rust core, which are untouched toolchain cases on clang 23 / Rust 1.99 (CI pins clang 18 / Rust 1.96). Not run: the Fedora-base recipe.
  • KUnit, arm64 QEMU with KASAN and lockdep: apple-speaker-pm-volumes (3) and apple-speaker-admission (1) pass, with no reports.
  • run-host-tests: 132/133, the same as the base. apple-dcp fails on bare integration/aurora as well.
  • dt_binding_check is clean for apple,t6041-aop-audio.yaml, apple,macaudio.yaml and ti,tas27xx.yaml.
  • CHECK_DTBS on t8132-j713.dtb gives the same diagnostics as the base.

Not tested: other machines (see Shared code above). Headphone impedance detection, which this driver doesn't do, as on the J616s.

@erykwieliczko

erykwieliczko commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Rebased onto the current integration/aurora, with three fixes (not pushed yet)

#820 no longer merges: on integration/aurora 21f7ffc, 7915d686 (the J614s cards) adds macaudio_j614_cfg at the same place as macaudio_j713_cfg. Rebasing also exposes two changes in the base that git merges without a conflict, and one bug in the series itself.

I have the rebased series locally (j713/audio-integ-next, on the rebased #819). Erik stays the author of every commit. The four I changed carry an [eryk: …] note and my Signed-off-by. I haven't pushed it: the PR allows maintainer edits, so if you're fine with it I'd force-push it to j713/audio-integ, and #819's to j713/aop-t8132.

Changes

ASoC: apple: macaudio: Add the J713 speakers

  • Conflict: kept the base's macaudio_j614_cfg and put macaudio_j713_cfg after it. The jack comment keeps the base's "J614s and J616s".
  • Route-only front-end: f0fb0a51b1e5 ("no PCM for the J616s route-only front-ends") leaves the J616s speaker card's first front-end unbound. The J713 card uses the same front-ends and has no headphone codec either, so it gets the same flag:
     	.fe_cpu_is_platform = true,
    +	/* No headphone codec on this card: the jack is a card of its own. */
    +	.route_only_fes = BIT(0),
     	.speaker_fe_channels = 6,
    The AOP component numbers front-end PCMs by link (use_dai_pcm_id). So the speakers stay hw:AppleJ713,1 and their sense hw:AppleJ713,2, which UCM and speakerguardd open; only the dead hw:AppleJ713,0 goes away.

ASoC: tas2764: Power the amp down again when the component probe fails

  • The base's 33ab1c4a made the SDZ supply optional (devm_regulator_get_optional()), so sdz_reg is NULL on every amp whose SDZ is a GPIO (the M1 and M2 machines).
  • The new error path called regulator_disable(tas2764->sdz_reg) unconditionally: a NULL dereference on exactly the failure this commit handles. git merges it without a conflict.
    -	if (!tas2764_group_failed(tas2764))
    +	/* As in the remove: no supply to drop with a GPIO SDZ, none left in a failed group */
    +	if (tas2764->sdz_reg && !tas2764_group_failed(tas2764))
     		regulator_disable(tas2764->sdz_reg);

ASoC: tas2764: Free the fault IRQ when the component is removed

  • The IRQ was a devres of the I2C device, requested at component probe, after devm_snd_soc_register_component().
  • When the I2C device is unbound while the card is bound (through sysfs, or with the adapter going away), devres releases in reverse order: the IRQ first, then the component. The component's remove then calls devm_free_irq() for a devres that no longer exists, which hits the WARN_ON in devm_free_irq() and taints the kernel.
  • Card rebinds and snd-soc-aop reloads don't hit it.
  • The component owns the IRQ now, so it uses a plain request and free:
    -		ret = devm_request_threaded_irq(tas2764->dev, tas2764->irq, NULL, tas2764_irq,
    +		ret = request_threaded_irq(tas2764->irq, NULL, tas2764_irq,
     ...
    -		devm_free_irq(tas2764->dev, tas2764->irq, tas2764);
    +		free_irq(tas2764->irq, tas2764);

The other 17 commits are unchanged apart from the rebase; one has a new DT context line.

Testing

Separately (not in this update)

On the previous stack, which had the suspend-to-idle series, one system suspend logged tas2764_irq_locked: 15208 callbacks suppressed and interrupt latch N unreadable. The shared fault line keeps firing while I2C is already suspended, and every handler returns IRQ_NONE. Disabling the IRQ across system sleep in the codec's suspend and resume would probably fix it. That's worth a follow-up once #831 is rebased.


🤖 Written by Claude Opus 5.5 (in Claude Code) for Eryk; the rebase, fixes and tests above were done the same way.

erikwb and others added 15 commits November 4, 2026 06:30
The T8132 (M4) AOP runs the same firmware transport as the T6041 one, and
its bootloader hands it over the same way. It also programs the AOP DART's
address filter (DAPF) and reserves the filter's windows, which the firmware
reaches through its DART at their physical addresses. The windows come in
memory-region with the firmware segments, up to 16 of them.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The T8132 (M4) AOP takes the T6040 boot arguments (EC0p 0x100_0000_0000,
nCal 1, alig 64, AOPt 0): iBoot leaves the boot-argument table at its
defaults in the AOP SRAM and the AOP halted, and the ADT gives __ETEXT the
DART address 0x100_0000_0000. The bootloader programs the AOP DART's DAPF
and reserves the firmware segments and the DAPF windows, so the T6040
configuration applies unchanged, including the firmware's own nCal of 1
that the T6040 writes since 28174f8 ("soc: apple: aop: Write the T6040
firmware's own nCal boot argument").

Tested on J713 (MacBook Air 13" M4) with m1n1 handing the AOP over (its
memory-region holds __ETEXT, __OS_LOG and the 15 DAPF windows): the AOP
starts, and the microphones capture (3 channels, 48 kHz) through
aop_audio.

Signed-off-by: Erik Bourget <erik@ebourget.net>
[eryk: rebased onto integration/aurora 21f7ffc; the T8132 config now
 inherits nCal 1 from the T6040 one (28174f8), so the message says so]
Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The T8132 AOP audio service is the T6041 one: the same LPAI microphone
flow, and the same ADMAC channel map for the speakers and the jack. Allow
"apple,t8132-aop-audio" in front of the T6041 compatible.

A machine brought up in steps lists only the streams it has so far, so
allow the microphones alone, and the microphones with the jack, besides
the speaker layouts.

Signed-off-by: Erik Bourget <erik@ebourget.net>
Describe the T8132 AOP with its mailbox and DART, and the audio fabric's
two ADMACs (admac-leap-ns on DART stream 10, admac-base-ns on stream 8),
all disabled. Addresses and interrupts are the ADT's; the ADMAC channel
map is the T6041 one: the microphones on LEAP channel 1.

Signed-off-by: Erik Bourget <erik@ebourget.net>
Enable the AOP, its DART and mailbox, and the ADMACs on the MacBook Air
13" M4, with the AOP audio service for the three microphones.

/chosen already asks for the AOP's handoff: m1n1 programs the AOP DART's
DAPF and adds the firmware segments and the DAPF windows to the AOP's
memory-region, or disables the AOP.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The jack component was only registered along with the speaker one, so a
machine whose audio node lists the jack's DMA channels but not yet the
speakers' got no jack. Register the speaker component only where the node
lists "spkr", and the jack's where it lists "cout" and "cin", each on its
own. Creating the services does not reach the firmware; nothing changes
for the J616s, which lists all of them.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The MacBook Air 13" M4 (J713) headphone jack is a card of its own on the
AOP, like the J616s one.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The J713 jack runs on the AOP firmware's MCA cluster 2 with the same
CS42L84 setup as the J616s jack. Use the J616s jack configuration,
including its -6 dB cap on the headphone DAC: this board's analog full
scale is unmeasured too.

Signed-off-by: Erik Bourget <erik@ebourget.net>
Add the CS42L84 on i2c1 (reset on nub GPIO 17, interrupt on AP GPIO 98,
from the ADT), the jack's ADMAC channels on the AOP audio node (base-ns
TX2 and RX2, as on the J616s) and the jack's own card.

The codec is the first consumer of the i2c1 power domain, so the J713
power-domain policy now enables it; it has no parent domains.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The MacBook Air 13" M4 (J713) speakers are a card of their own on the
AOP's speaker stream, like the J616s ones.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The MacBook Air 13" M4 (J713) runs its four SN012776 amps, a woofer and
a tweeter per side, on the AOP's speaker stream exactly like the J616s:
the same 250-bit frame, I2S with an inverted frame clock, six playback
and twelve sense slots, of which the four amps take the first four and
eight. Amp gain 15 and the -20 dB safe volume as on the other SN012776
machines.

The speaker front-end's channel count was one per amp; add
speaker_fe_channels for a CPU stream with more slots than amps, and give
the J713 six. Its last two channels are silent.

As on the J616s, the first front-end only completes the fixed routes:
the J713 jack is a card of its own, so mark it route-only and leave its
link unbound. The AOP component numbers the front-end PCMs by link, so
the speakers stay hw:AppleJ713,1 and their sense hw:AppleJ713,2.

Signed-off-by: Erik Bourget <erik@ebourget.net>
[eryk: rebased onto integration/aurora 21f7ffc, keeping the new J614s
 configuration beside this one; mark the first front-end route-only, as on
 the J616s]
Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the four SN012776 amps on i2c3 from the ADT (audio-speaker at 0x38
and its speaker1..3: left woofer 0x38, right woofer 0x3b, left tweeter
0x39, right tweeter 0x3c), each with its sense slots from
speaker-config (imon at RX byte 4i, vmon at 4i + 2), the shared
interrupt on AP GPIO 14 and the shared shutdown line on AP GPIO 15 as
the fail-closed SDZ group's fixed regulator. The speaker card lists the
amps in the ADT provider order, so each takes its native playback slot,
with the amp-tx-zd-config idle masks. The AOP audio node gets the
speaker and sense channels (base-ns TX0 and RX0): the J616s stream, of
which the four amps take the first four playback and eight sense
slots. The amps are the first consumer of the i2c3 power domain.

Signed-off-by: Erik Bourget <erik@ebourget.net>
On an AOP-fed speaker stream the TDM bus keeps running for a while after
the playback stream stops, and the amplifiers' current and voltage sense
slots are still sampled in that time. An amplifier put in software
shutdown then can leave garbage in its sense slots.

Add a property for such amplifiers: they must not enter software
shutdown until the bus has stopped.

Signed-off-by: Erik Bourget <erik@ebourget.net>
On the J713 the AOP keeps the speaker TDM bus running for a while after
a playback stream stops, and the speaker protection still reads the
amplifiers' I/V sense words in that time. Each amp is muted and then
shut down in turn as the stream ends, so the shutdowns land in that
tail. The right tweeter's voltage sense slot then reads full scale for
about 20 samples at every stream stop, even for a stream of digital
silence played with the volume locked; the protection daemons charge
that as power.

With apple,deferred-shutdown, mute the amp at the stream's end as
before, but put it in software shutdown only 150 ms later, after the bus
has stopped (it ran on for up to 73 ms on the J713). A stream that
starts in between cancels it. Suspend and removal shut the amp down at
once. Such an amp sees the bus clock stop while it waits, and may see it
latched again after its shutdown; that clock-only fault is expected then
and no longer logged. Amps without the property keep the current
behaviour.

On a J713 the full-scale words in the stream tails go from about 90 per
test (three silent streams and a chord) to none, with no fault logged,
and every amp still ends in software shutdown.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The J713's AOP keeps the speaker bus running briefly after a stream
stops, and the speaker protection reads the amps' sense words in that
time. Have the four amps wait for the bus clock to stop before they go
to software shutdown.

Signed-off-by: Erik Bourget <erik@ebourget.net>
erikwb and others added 7 commits November 4, 2026 06:30
With apple,deferred-shutdown the muted amp stays powered after a stream
until the bus clock stops, and keeps latching TDM clock faults while it
waits; the fault handler ignores them while stream_stopped is set.  The
amps share one fault line and each handler reads six latches over I2C,
so a latch can reach its handler several milliseconds late.  When the
next stream unmutes the amp in that window, stream_stopped is already
clear and the stale latch is reported as a fault on the new stream.

On the J713 this hit the first amp the link unmutes on 9 of 32 silent
streams that started within 140 ms of the end of the last one, and on
none of 32 that started later.

At unmute, clear the latches before clearing stream_stopped when the
first latch register holds only the clock fault (the clear takes every
latch, so any other fault there is left for the handler), and hold a
lock across the handler's read and judgement of the latches so that
the unmute cannot fall between them.  With this, none of 96 silent
streams logged a fault, 48 of them started within 140 ms of the last.

Signed-off-by: Erik Bourget <erik@ebourget.net>
…leep

Since the speaker volumes are locked from suspend-prepare, the speakers
stay silent after the first system sleep. Locking clips every speaker
volume to the lock's ceiling (1, -100 dB on the SN012776), and unlocking
only lifts the ceiling again. After a lost lease that is fine:
speakersafetyd gets -ETIMEDOUT, restarts and sets the volumes itself. The
system-sleep lock is deliberately not reported as a lost lease, so the
daemon keeps its state and never writes the volumes again. They stay at
-100 dB until the daemon restarts. alsactl then stores them that way at
shutdown and restores them at the next boot, where they stay unless the
daemon's cold start sets the volumes again after that restore.

Keep the speaker volumes at suspend-prepare, before the lock clips them,
and set them again at the first unlock after it. That is the daemon's
first ping after the resume, which in practice comes when playback next
starts. These are the values the daemon set and still assumes.

The daemon may not ping between two sleeps. A suspend-prepare that finds
the volumes locked already keeps what an earlier one saved, so a second
sleep before any playback doesn't lose them. Hold the card's controls
for reading from keeping the volumes until they are clipped, so a volume
the daemon writes meanwhile can't be overwritten by an older, higher one
at the unlock. Hold them as well in the work that can unlock after a
back-end starts or stops, so setting the volumes again there doesn't race
a volume write either. Control writes hold them for writing and take
volume_lock_mutex inside, in the same order.

A new owner of the lock, such as a restarted daemon, sets the volumes
itself: drop what was kept then, and when the card is removed. Nothing
is kept for a card that is shutting down. The card's remove waits for
the unlock work, which takes the card's controls, while the snd_card
still exists.

Tested on a J713: after a suspend-to-idle the four speaker volumes are
back at 0 dB and the speakers play, without restarting speakersafetyd.
The KUnit cases go through the lock control and the PM notifier: one
sleep, two sleeps before the daemon pings, and a new owner after a sleep.

Fixes: 1cbce20 ("ASoC: apple: macaudio: lock the speaker volumes before userspace freezes")
Signed-off-by: Erik Bourget <erik@ebourget.net>
The AOP firmware keeps a device attachment for as long as it runs, and
the driver has no way to drop one at removal.  When the module is
loaded again, the probe's attach calls for the microphone devices get
kIOReturnBusy back and the probe fails.  The machine then has no audio
until it reboots: with the T6040 flow the speaker and jack streams are
registered by the same probe and are lost as well.

The speaker and jack services already treat that return as attached.
Do the same in the probe, and share the constant with aop_spkr.  The
probe's property writes that follow are accepted by the still attached
devices.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The component probe resets the amp and reinitialises the register
cache, which clears the SDOUT idle slot mask and the force-0 enable.
The idle slot setup cached in the driver data survives the reset,
though, so when the card binds again and set_tdm_idle() asks for the
same mask and mode, it returns early and the force-0 enable is never
written.  set_bclk_ratio() writes the mask again but not the enable.

On the J713 every rebind of the speaker card left the two amps that
force their idle slots to zero without the enable, and the other two
amps' voltage sense words then came back with the top bit set in about
a quarter of the frames.  The speaker protection daemon reads those as a
voltage without current and holds the speakers at a safe level.

Clear the cached setup after the reset, so that the next set_tdm_idle()
writes it.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The component probe requests the fault IRQ as a devres of the I2C
device, which stays bound when the sound card is torn down.  Every
rebind of the card probes the component again and adds another handler
for the same amp on the line: after two rebinds the J713's four amps
had twelve handlers on their shared IRQ, each fault interrupt ran every
one of them, and each does six I2C reads.

Request the IRQ without devres and free it in the component remove.  A
devres would not do: when the I2C device itself is unbound, its devres
release frees the IRQ first and only then unregisters the component, so
a devm_free_irq() in the remove would find nothing and warn.  Request it
as the probe's last step, so that a probe that fails later does not
leave a handler behind either.

Signed-off-by: Erik Bourget <erik@ebourget.net>
[eryk: request the IRQ without devres: on an I2C unbind the devres
 release frees it before the component is removed, and the devm_free_irq()
 in the remove then warns]
Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tas2764_codec_probe() enables the SDZ supply and raises the shutdown GPIO
before it resets the amp and writes its setup. When one of those writes
fails, or an amp in an SDZ group can't get its fault IRQ, the probe
returns with the supply still enabled. ASoC doesn't call the component's
remove after a failed probe, so nothing balances that enable, and every
later probe attempt adds another. The supply then stays on after the
card is removed and, for an amp outside an SDZ group, through system
sleep as well.

Undo the power-up on those failures: lower the shutdown GPIO and drop
the supply enable, as the remove does. There is no enable to drop when
the amp has no SDZ supply (its SDZ is a GPIO, or not described), or
when its group failed and the supply was already forced off.

Signed-off-by: Erik Bourget <erik@ebourget.net>
[eryk: rebased onto integration/aurora 21f7ffc, where the SDZ supply is
 optional: skip regulator_disable() when the amp has none]
Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the component suspend until its resume, the fault handler can
neither read nor clear the interrupt latches: the regmap is cache-only,
and the I2C controller suspends after the card. A fault latched in that
window, such as the TDM clock fault when the bus clock stops, holds the
shared level-low line asserted. Every amp's handler then fails to read
its latches and returns IRQ_NONE, in a loop until the IRQs are
suspended, and again from resume_device_irqs() until the deferred
component resume. On a J713 one suspend logged "tas2764_irq_locked:
15208 callbacks suppressed" and "interrupt latch N unreadable". Enough
of it can make the spurious-IRQ detector disable the line, and with it
the SDZ group's fault shutdown.

Disable the IRQ at the start of the component suspend and enable it
again at the end of the resume, on every path. disable_irq() nests
across the amps sharing the line, and ASoC resumes every component it
suspended, even after a failed suspend. A clock fault latched in sleep
is then handled after the resume, as before.

Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@erykwieliczko
erykwieliczko merged commit 0cfb301 into aurora-silicon:integration/aurora Nov 4, 2026
@erykwieliczko

Copy link
Copy Markdown
Member

Merged. This comment is from Claude, working for Eryk, not from Eryk himself.

Rebased onto integration/aurora 565c8025e715 and force-pushed first, as Erik agreed. Each changed commit carries an [eryk: …] note. Erik stays the author of his commits.

Changes

  • ASoC: apple: macaudio: Add the J713 speakers: resolved the conflict with the new J614s config, and marked the first front-end route-only (.route_only_fes = BIT(0)), as on the J616s, because the J713 jack is a card of its own.
  • ASoC: tas2764: Free the fault IRQ when the component is removed: the IRQ is now requested without devres. On an I2C unbind, the devres release freed it before the component was removed, and the devm_free_irq() in the remove then warned.
  • ASoC: tas2764: Power the amp down again when the component probe fails: regulator_disable() is skipped when the amp has no SDZ supply.
  • New commit, not in my earlier comment: ASoC: tas2764: Keep the fault IRQ masked across system sleep. From the component suspend until its resume, the handler can't read or clear the latches. A fault latched in that window holds the shared level-low line asserted, and one J713 suspend logged 15208 suppressed callbacks. The IRQ now stays disabled across that window.

Testing

  • Eryk confirmed audio by ear on a kernel with integration/aurora 565c8025e715, these commits, other J713 PRs and some local patches. Everything under sound/ in that kernel is byte-identical to what was merged.
  • The merged tip on its own has not been built or booted.
  • The new tas2764 commit has not been through a real suspend on hardware yet.

🤖 Written by Claude Opus 5.5 (in Claude Code) for Eryk.

erykwieliczko added a commit that referenced this pull request Nov 4, 2026
arm64: dts: apple: t8132-j713: Hold AOP messages in system sleep (depends on #820, #831)
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.

2 participants