Skip to content

Deep sleep (suspend-to-RAM) for the MacBook Neo (T8140) at EL2 - #1208

Closed
DjDeveloperr wants to merge 82 commits into
integration/aurorafrom
feat/t8140-s2r
Closed

DjDeveloperr wants to merge 82 commits into
integration/aurorafrom
feat/t8140-s2r

Conversation

@DjDeveloperr

@DjDeveloperr DjDeveloperr commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Deep sleep (suspend-to-RAM) for the MacBook Neo (J700, T8140) with Linux at EL2, as 72 commits on integration/aurora. The order is dt-bindings first, then the shared infrastructure, the sleep controller, each subsystem's suspend and wake, and the device tree last. SoC facts live in the DT and in match data wherever shared code would otherwise hard-code the J700, so M4 and later machines need their own tables, not code changes.

Needs from aura

  • The EL2 return path, kboot: let an OS at EL2 return from its own sleep aura#117 (open). iBoot locks every core's reset vector to the boot object, so a core that powers off comes back inside aura. aura#117 handles any OS it starts at EL2 where a secondary's locked RVBAR leads back into aura. It sends a reset secondary back through its spin-table release address, sends the boot core to a resume mailbox, and publishes that mailbox as /chosen/aurora,s2r-mailbox. The CPUs keep spin-table as their enable method. The mailbox is the only signal. With it, Linux gives spin-table CPUs the parking ops for power-off and return. Without it nothing in this series binds, the CPUs stay on plain spin-table, and s2idle stays the default.
  • The APCIe tables for ANS's wake, Publish APCIe ANS recovery tables with atomic packed-DT export aura#112 (open). ANS's NAND link runs on the shared PCIe block, and a real sleep powers that block off. The sleep controller reads apple,tunable-axi2af, -cio3pll, -clkgen, -phy, -phy-ip-pll and -phy-ip-auspma on the PCIe node, and pcie-apple reads apple,tunable-port, -rc, -rc-gen3 and -rc-gen4 on pci@0,0. Without the PCIe node's tables the sleep controller refuses every real sleep with ANS stopped ("no PCIe tunables for ANS"). aura#112 has to publish all ten.

What a system needs

  • apple_sep loaded before the first sleep. A SEP that iBoot started and no driver has bound refuses every sleep ("sep runs with no driver bound"). The aurora-sep package in the Fedora, ALARM and NixOS trees does this. It blacklists the module's alias, and aurora-sep.timer (1 s after boot) starts aurora-sep.service, which runs modprobe apple_sep. Fedora and ALARM enable the timer through a systemd preset that their image builds apply, and NixOS through modules/sep.nix, part of the default module set. A system without that package, or with the service masked, refuses every deep sleep.
  • Nothing else. Once the sleep controller binds, deep is the default sleep state, set through the kernel's usual suspend_set_ops() path, so systemctl suspend takes it. The stock mem_sleep_default=s2idle parameter still keeps s2idle. Without aura#117's mailbox the controller logs that once and doesn't bind, so s2idle is the only sleep. Without ANS's APCIe tables every real sleep would refuse, so the controller says so once and leaves s2idle as the default. Distributions need nothing beyond the aurora-sep timer they already ship.

How it works

The boot object opts in with /chosen/aurora,s2r-mailbox and nothing else. CPUs whose enable method is spin-table then get a parking spin table, so the device tree keeps the method every kernel knows, and a kernel without this series ignores the property. The older aurora,rvbar-spin-table method selects the same ops. Until a platform driver registers its power ops, the parked CPUs refuse hotplug as plain spin-table does, and kexec refuses the load up front instead of failing later with the CPUs still online.

  • The sleep controller runs macOS's last 14 ms in macOS's order: WD2, the PMU's boot stage, perf states, DCS, memory controller, power words, CLLT, DVFM, AMCC error enables, complex words, the commit, AIC masked. On the way back it restores every power word to its value at the commit, before any driver resumes. The cluster power-gate enable, set at boot, is what lets the boot CPU power-gate at all. Without it the sleep engine never started, and the watchdog reset the SoC after 5 s.
  • Refusals come before anything stops. Both DCPs, the PMP and SIO must have stopped, and an enclave that runs unbound refuses. The driver doesn't read a display CPU whose domain is off, and it lowers or refuses on what XNU panics on at the last step (AVD, VENC0/1, GFX, JPG). In .enter the display goes off first, before the GPU, as on macOS. Then come each coprocessor's last check, such as NVMe's shutdown notification, and then the enclave's sleep request. That request is the very last refusal, because a gated enclave comes back only through a real sleep. After it nothing can refuse, and from the GPU's stop on, a failure goes on to the commit.
  • No commit with a CPU awake. Right after the first checks, before the display goes off, every parked CPU answers a hold and waits. Nothing is written to the hardware yet. The sleep refuses right there, before anything that can't be undone, if a CPU doesn't answer within 100 ms, or if a CPU is offline but neither held nor powered off, like one whose start failed after a wake. Every CPU then comes back as after any refusal, and a later refusal releases the hold the same way. At the commit only held CPUs stop, at the same point as before. Without this, measured on a J700 with a test switch, a core that missed its stop kept the sleep engine waiting until the watchdog reset the SoC.
  • Every coprocessor macOS stops is stopped the same way and brought back with a warm restart, with firmware and rings kept in DRAM. That's DCP and DCPEXT, PMP, SIO, GFX/GFX1, ANS and the SEP. The SMC, MTP and AOP keep running and get the AP power notices.
    • An idled firmware doesn't wake with its power kept, so the PMP's and the GPU's idle and stop run in the sleep's last step, after every refusal.
    • After a refused sleep, the DCPs come back from a power cycle, with DCPEXT's DISPEXT0 raised first if the sleep had lowered it. SIO comes back from the sleep engine's own reset of its domain.
    • If a DCP's stop fails after its CPU stopped, the driver starts it again before returning the error. A crashed DCP no longer blocks every later sleep. The driver stops it cold and the wake's power cycle restarts it, which is how macOS recovers one.
  • OSLOG counters are echoed for every T8140 RTKit coprocessor except the GPU, as macOS does. A firmware with one unanswered counter takes no AP power notice. The polled idle in the last step echoes them too.
  • ANS comes back from power-off. The sleep controller lowers its PCIe block with its domains and brings it back in macOS's order with the PHY's PLL and lane tables. It puts the 212 AP pins back and writes the SART again right before ANS's run bit.
  • Radios stay bound and powered. The radio's link goes through L2 and PERST# and is retrained at the wake.
    • Wi-Fi keeps its firmware (CID 0xf6 suspend/resume), and can stay associated and wake on a magic packet.
    • Bluetooth enters the controller's host sleep as macOS does, keeps page-scanning, and wakes the SoC through the SMC. The driver takes the wake event before HCI resumes, and logs it.
    • A radio that didn't survive restarts from cold.
  • The J700's USB host sleeps in place. The core goes through hibernation, and the PIPE is parked at noirq rather than USB3 being taken down.
    • The core has come back without its registers after every sleep so far, so the glue sets it up again in place and xHCI restarts in place. The hub and keyboard reset-resume and keep their input devices.
    • The wake sets the PHY up again in its old mode, USB2 or USB3, and the core's re-init routes the PIPE to it before xHCI starts. SuperSpeed needs no mode change or port cycle afterwards.
    • The glue checks the fixed hub after every sleep.
    • The port controller arms USB wake only when a device behind its connector can wake, meaning wakeup is enabled at or below the fixed hub port that the connector's DT graph leads to.
  • A lid closed during a sleep is a dark wake, as on macOS. The whole system resumes with the panel and DCPEXT's output kept dark, then goes straight back to sleep. The lid and power button stay armed through the dark wake, so opening the lid or pressing the button ends it.
  • An attached USB-C display is released for the sleep. The pipe goes off, HPD goes low and the crossbar output is parked, and the wake sets the DP mode up again. The Type-C route and the DRM connector stay, so the compositor keeps its output.
  • Wake sources are the RTC alarm, USB, the SMC (power button, lid, AC, Wi-Fi, Bluetooth, USB keyboard) and the MTP (internal keyboard and trackpad).
    • The SMC gets its sleep warning with no fixed wait after it.
    • The debug link is cut before the SMC's AP idle notice, so its events don't wake the SoC.
    • Opening the lid or pressing a key while the machine goes down ends the suspend.
    • The log names the SMC's wake reason.
  • Smaller pieces.
    • The ISP's firmware stops the way macOS stops it, and a stream that ran across the sleep restarts once tasks thaw.
    • The mic array's power rail, SMC pcIO output 25 through the existing apple,pcio-templates, switches on and off around each capture.
    • DART tunables and address filters come back after a power-off. DARTs keep the normal sleep phase on every other SoC and for every other kind of sleep.
    • The system level cache is written back before the sleep.
    • The Touch ID sensor gets its sleep command and is powered off for a system sleep. See Testing.

Fitting into integration/aurora

  • t8140-peripherals.dtsi already describes the SIO, its DART and its mailbox, so the series only names the SIO's register blocks (asc, auxiliary, asc-tail) for the sleep controller.
  • The placeholder for the DCP's exclave mailbox (apple,t8140-secure-mailbox) becomes the secure RTBuddy port that the DCP driver polls (apple,t8140-secure-rtbuddy-proxy).
  • The J700's mic array rail uses the SMC GPIO driver's apple,pcio-templates, so the GPIO driver needs no change for it.
  • macaudio's sleep handling stays as its speaker protection lease has it.
  • RTKit's wait during a sleep uses the per-instance handshake timeout.

Measured on a J700

These numbers come from test kernels carrying this code. Those kernels also carried test-only switches and debug code, and the later ones two MTP commits that aren't in this series.

  • 30 of 30 real deep sleeps over two boots, with ANS stopped and restarted each time. With Plasma up and USB down, 10 of 10, and 30 of 30 with the panel DPMS'd off before every sleep.
  • Wakes from the RTC alarm, the power button, the internal keyboard and trackpad, a USB keyboard, a Bluetooth keyboard, the lid, and a Wi-Fi magic packet, still associated after each wake.
  • A key or the lid during the entry brings it straight back. An external display is released and restored.
  • On battery with nothing plugged in, 16 of 16 systemd suspends returned, 15 on the RTC alarm and 1 on a magic packet. Bluetooth, USB, NVMe, audio and the panel were back each time, with no crash lines. Entry took 0.25 s median (0.22-0.26), and the wake to "PM: suspend exit" 1.02 s median (0.99-1.04).
  • With the USB changes, 12 of 12 systemd sleeps and 3 of 3 magic-packet wakes. The SuperSpeed side was already in USB3 at every wake. With nothing on the ports USB wake isn't armed, and a USB keyboard wakes it.
  • With the review fixes below, 8 of 8 sleeps, including 2 of 2 magic-packet wakes, and every device check passed.
    • A forced refusal at the latest point a sleep can still refuse, after the display and the last checks and before the enclave, returned EBUSY. The enclave was never told.
    • The GPU, the SEP with its keystore open, both DCPs, SIO, the AOP and NVMe at 1.4 GB/s all came back, and a real sleep on the same boot then worked.
  • From the SMC's charge counter on battery, a 300 s sleep averaged 11.3 mA, 0.049 W at 4.375 V, entry and wake included. A fit over four sleeps gives about 8.6 mA asleep. macOS under aura measured 0.109 W on this machine with the same counter.

Not in this series

  • Test and debug code: the wake write log, PMU scratch-byte reporting, power-word and register dumps, the no-sleep restart tests and their helpers, every sysfs or debugfs test hook, wake_reason in sysfs, the SMC boot-record log, PM_DEBUG in asahi.config, timing lines, and the test-only switches keep_debug_link, refuse_late and ignore_stop_cpu.
  • The suspend_again dark wake experiment.
  • The sleep-DDR vote for the USB host. macOS uses it, but from Linux it resets the machine at the wake.
  • The MTP's in-place CPU reset and rtkit-helper's own MTP start and stop. They wait for their boot-loader side.

Open

  • A core that answers the hold and then doesn't take its stop at the commit would still keep the sleep engine waiting until the watchdog resets the SoC. To get there it would have to stop running a WFE loop with interrupts masked. macOS doesn't handle that case either.
  • The sleep controller still keeps T8140 offsets in the driver: power-word lists, domain offsets, CLLT, CPM and mux words. Moving them to the DT and a per-SoC table is the next cleanup, and it needs its own hardware run.
  • USB's host loses its registers in every sleep, so the hub and devices reset-resume after each wake.
  • The sleep controller shares its unit address with pmgr, and W=1 dtbs warns about it. Moving it means reordering its binding's ranges and the tree together, as a separate change.
  • With the PMP built as a module, removing it leaves every later sleep refused until a reboot. The built-in default has no unbind on T8140.
  • The DCP's secure port has no interrupts in its binding, so the driver polls it, quickly only around its messages.
  • The GPU's OSLOG counters stay unechoed until a soak shows echoing them is safe.
  • The ATC PHY doesn't acknowledge the PIPE lock at most sleep entries, logs it, and parks the PIPE unlocked. USB came back after every one of those sleeps.
  • One ping right after the sleep that followed the forced refusal went unanswered. The Wi-Fi driver logged nothing, NetworkManager's dispatcher ran at about that moment, and Wi-Fi was fine a minute later. I haven't chased it further.

Review

Two reviews of an earlier version found 25 issues. 24 are fixed in code, each folded into the commit it belongs to or added where it's new. The 25th, the DockChannel host-state order, was right, and only its comment was wrong. The fixes:

  • The display goes off and NVMe's shutdown notification runs before the GPU's last stop. The enclave's request is the last refusal, and last stops can't refuse.
  • DARTs keep the normal sleep phase except in a firmware-driven sleep. The PCIe-C reset table has its earlier value back. Coprocessor iommus are parsed by #iommu-cells.
  • Only the debug port's controller answers the debug link cut. The sleep driver publishes itself only once its probe can't fail.
  • DCP raises DISPEXT0 before a refused cycle, and restarts the firmware when a stop fails after the CPU stopped. A crashed DCP sleeps and restarts, a refused idle is undone, and DCPEXT stays down through a dark wake.
  • An ISP stream across a deep sleep restarts, and T8140's cold STOP is its own command.
  • A radio host that a failed wake left down keeps its saved state. The Wi-Fi module is held while its radio stop runs.
  • An AOP call checks for the sleep again once it has its turn.
  • The PMP has no sysfs unbind on T8140.
  • A core still parked after the wake starts at its reset vector. The hold now catches a core that misses its stop before the commit, so this path shouldn't run any more.
  • The display power words got a binding and their own address.

Testing

  • Build. The tip builds Image modules dtbs with the asahi profile, Rust on and no compiler warnings, on a build guest that verified clean before and after. Wherever this rebase changed a commit's code, or a later commit changes that code again, I also compiled the changed objects at that commit with W=1 and the tip's config. That's #16, #24, #30, #31, #33, #41, #48, #50, #54, #56, #57, #58, #62, and the J700 DTB at #70. All pass. The one W=1 note is t8140_sleep_lower_io() unused at #30 and #31, until its caller arrives in #32. Making deep the default touched #30 and #32 again, and t8140-sleep.o compiles at both with W=1, with that same note only.
  • checkpatch --strict. Nothing new against the series before the rebase. It reports 29 errors, 16 warnings and 7 checks, all of known kinds: a minified host-test fixture, MAINTAINERS reminders on new files, udelay in atomic sections, arm64-only includes in the spin table, and a false positive on function-pointer locals.
  • Host tests and DT checks. tools/asahi/run-host-tests.py passes 195 of 195 at the tip, and dt_binding_check and the DTB schema checks pass. Where a commit changes code that a host test compiles from source (the DCP, ATC and dwc3 slices of the apple-dcp selftests, the PMP bridge in display26-tests, mt7932-association-events-tests and rtkit-oslog-tests), the same commit updates that test's stubs. Each of those suites also passes at the commit that changes it.
  • KUnit. The ATC PHY's suites passed under QEMU, 39 of 39, before the rebase. The base doesn't change that driver.
  • Bluetooth. This base builds mt7932_bt_pcie again, so the series' Bluetooth changes build here too.
  • Touch ID sensor. Its sleep commit does nothing without the sensor, and it hasn't run on hardware. Its KUnit suite compiles with KUnit built in, and its cases pass in a host build of the shim's code.
  • Hardware. This tree, plus three test-only switches that do nothing unless set, on a J700 booted through aura with aura#117 and all ten APCIe tables, apple_sep loaded and deep selected:
    • 6 of 6 sleeps on a 20 s RTC alarm, with keep_debug_link set so the debug cable's events didn't wake it. Each lasted the full 21 s and woke on the PMU (NUB.SPMI0.SW3), with an empty SMC wake record. Entry took 0.41-1.10 s and resume 1.14-1.17 s. After every wake Wi-Fi pinged, Bluetooth was powered, the USB hub and keyboard were back, NVMe was live, both audio cards were there and eDP was connected, with no crash lines.
    • With one CPU set to ignore the hold, the sleep refused with -ETIMEDOUT ("CPU1 did not answer the hold for a suspend") before the display went off. CPUs 0-5 were online after, and the next plain sleep returned on the RTC.
    • With a refusal forced at the latest point, the sleep returned -EBUSY. The PMP, SIO, both DCPs (DCPEXT's DISPEXT0 raised for its cycle) and the GPU came back, the device checks passed with CPUs 0-5 online, and the next plain sleep returned. One ping right after that wake went unanswered; Wi-Fi was fine a minute later.
    • On battery with nothing plugged in, the debug cable out and no test switch set, over ssh on Wi-Fi: 2 of 2 sleeps on a 60 s RTC alarm returned after 61 and 60 s. Entry took 0.21-0.23 s and resume 0.89 s, and every device check passed.
    • Those runs selected deep by hand. The change that makes deep the default came after them. On a boot of that tree with no mem_sleep_default on the command line, /sys/power/mem_sleep read s2idle [deep] untouched, and the controller logged "deep sleep is the default". One plain systemctl suspend on a 30 s RTC alarm entered "PM: suspend entry (deep)" and returned 30.4 s later, woken by the PMU with an empty SMC wake record.
    • After the rebase onto 009c87801b, which brings in upstream's G17 GPU changes, the rebased tree with the same three switches booted on stage2-14. mem_sleep read s2idle [deep] with no mem_sleep_default on the command line, and apple_sep was bound. 6 of 6 sleeps on a 20 s RTC alarm, with keep_debug_link set, returned after 20-21 s, woken by the PMU with an empty SMC wake record. Entry took 0.88-1.14 s and resume 0.86-0.88 s. The GPU firmware restarted at every wake with no fault lines, and every device check passed: Wi-Fi ping, Bluetooth, the USB hub, NVMe, both audio cards and eDP, with no crash lines.
    • A key-press wake from the internal keyboard on this kernel was inconclusive, because the timing of the press against the 300 s backup alarm isn't known. Keyboard wake passed on earlier kernels with this code; a timed row on this kernel will follow.

🤖 Generated with Claude Code

A kernel that runs directly at EL2 on T8140 has no monitor to suspend
the system or power a CPU off for it, so it writes the hardware itself.
The registers it needs belong to several blocks, the PMGR, the watchdog,
the wake controller, the AMCC, the PMC, SPMI, the memory controller and
the clock blocks among them, and other drivers use some of them too.
Describe them as one node whose driver doesn't claim its ranges.

The node also names what the sleep depends on elsewhere: the wake
sources to leave enabled, the display coprocessors that must be off
before the commit, the port controller that carries the debug link, the
PCIe host whose shared block ANS's link runs on, and the AP pin
controller whose pins a deep sleep resets, with the pins to leave alone.

Signed-off-by: Diljit Singh <hey@djdev.me>
Each CPU cluster has a PMGR word whose top bit lets its cores power down
in WFI. A core that PMGR powers on comes up allowed to, but the boot CPU
only follows the word, and iBoot leaves it clear for T8140's P cluster.
Describe the words, so that a driver can set the bit before the
secondaries start.

Signed-off-by: Diljit Singh <hey@djdev.me>
After a firmware-driven suspend the power controller puts each domain
back where genpd's books have it. Some domains need their driver's order
on the way back instead: the GPU's needs its target, then its bridge,
and only then automatic power management. Let the tree mark such a
domain, so that the power controller leaves it to its driver after a
wake.

Signed-off-by: Diljit Singh <hey@djdev.me>
A coprocessor can have a second mailbox for its secure side, the ADT's
iop,secure-rtbuddy-proxy. It has the registers of the AOP setup port,
and its interrupts aren't described yet, so its consumer polls it. Add
its compatible, and require interrupts for every mailbox but this one.

Signed-off-by: Diljit Singh <hey@djdev.me>
… sleep

A sleep that powers the SoC down resets every DART, and some of their
state comes from the boot loader, not from Linux:

- apple,dapf-ranges and apple,dapf-r20: the address filter the loader
  programs, to be written back before the consumer's firmware starts.
- A GAPF slice in place of the DAPF registers: the part of the fabric
  filter that guards a coprocessor CPU's power state, cleared to power
  that CPU down.
- apple,sleep-prepare, apple,sleep-restore and apple,sleep-restore-late:
  register writes around a sleep for a DART whose consumer gates itself
  and saves and restores it in its own order.
- apple,resume-tunable: the loader's tunables for a DART whose loader
  has already applied them, to be written back where a power-off lost
  them.

Signed-off-by: Diljit Singh <hey@djdev.me>
ANS's NAND link runs on the shared PCIe block whether or not the PCIe
host is bound, and after a deep sleep that block has to be brought back
from power-off with the boot loader's settings. On T8140 the host node
has five more register blocks (the AXI bridge, the CIO3 PLL, the clock
generator, the PHY and its IP block) and the loader publishes six
tunables for them, the PHY's PLL and lane tables among them. Allow them,
for T8140 only.

Signed-off-by: Diljit Singh <hey@djdev.me>
A T8140 display coprocessor's driver writes three words around a restart
of its firmware, as macOS does: the display PMC word that counts the
powered DCPs, and the power manager's power state and power gate words.
They belong to the power manager and the display block, so no driver
binds the node that holds them. A DCP names it in its
apple,display-power property.

Describe the node, so it has a compatible, and the PMC word's address as
its own rather than the power manager's.

Signed-off-by: Diljit Singh <hey@djdev.me>
The DCP's firmware takes an IOP idle only with its secure RTBuddy port
answered, so the driver needs that second mailbox. Allow it as "secure".

To restart its firmware after a sleep, the driver writes the display's
power words itself, in the order macOS does. apple,display-power points
at the node whose ps, pwrgate and pmc-dcp registers hold them, such as
an apple,t8140-display-power node.

Signed-off-by: Diljit Singh <hey@djdev.me>
T8140's SIO is stopped for a sleep and started again after, and the
sleep controller finds its coprocessor registers by name. Name T8140's
three blocks: the ASC registers before the mailbox window ("asc"), the
auxiliary ASC window, and the ASC registers after the mailbox window.

Signed-off-by: Diljit Singh <hey@djdev.me>
The PMP has to know when the GPU is in use. Around a sleep the GPU's
driver withdraws that request before it lowers the GPU's power state,
and makes it again after raising it, as macOS does. Let the tree name
the PMP report entry that carries it.

Signed-off-by: Diljit Singh <hey@djdev.me>
… wake

Three things a port controller's driver does for a firmware-driven
system sleep, where the board asks for them:

- apple,sleep-ssps: tell the controller the system's power state, S3
  before the sleep and S0 after, as macOS does.
- apple,debug-lock-key: the board's key that unlocks the controller's
  debug mode, to rearm the debug link after a sleep ended it.
- apple,usb-remote-wake: this controller serves the USB controller's
  root port, and watches its USB2 lines for a remote wakeup during the
  sleep.

Signed-off-by: Diljit Singh <hey@djdev.me>
The block next to the PIPE handler takes the USB controller's core in
and out of hibernation: a power state request, a status with SLEEP, and
event, enable and clear words for VVDD_USB_OFF, D0_ON and two wake
events. macOS's controller driver uses it at every system sleep.

In Linux the PHY drives that controller's reset and suspends after it,
so give the block to the PHY, as an optional seventh window.

Signed-off-by: Diljit Singh <hey@djdev.me>
The microphone array has a power rail of its own, which macOS switches
on for each capture and off after it. Let the tree name it.

Signed-off-by: Diljit Singh <hey@djdev.me>
A platform can find that what woke the system needs nothing from user
space. On Apple laptops the SMC wakes the SoC for the lid closing as well
as for it opening. macOS takes the closing as a dark wake: every driver
wakes as usual, the display stays off, and the power management root
domain puts the system back to sleep once it sees the lid closed. In
macOS's 25G83 kernel, read statically, AppleSMC sends
kIOPMClamshellClosed, and the dark wake policy sleeps with reason
Maintenance when the clamshell is closed and the machine isn't in
desktop mode on AC.

suspend_again() can't do the same. It goes back to sleep after the early
resume only, so drivers that do their sleep work in the normal suspend
and resume phases are left half way.

Add a dark wake flag for the platform to set during the noirq resume.
Devices still resume fully but may leave out what the user would see.
Once dpm_resume_end() is done, enter_state() runs
suspend_devices_and_enter() again with user space still frozen, so user
space sees one sleep. Only a wake from the sleep state can be dark, and
each pass starts with the flags cleared.

Signed-off-by: Diljit Singh <hey@djdev.me>
The enable callbacks of these features run through hotplug, and a
context switch only writes ACTLR when the thread's value changes. A
firmware-driven system suspend really resets the boot CPU, which comes
back through cpu_resume and not through hotplug, so it keeps reset
values with nothing to correct them. With AMX, that can leave EL0 access
in the wrong state for the current thread.

Restore them in __cpu_suspend_exit when pm_resume_via_firmware() says
the CPU was reset. That's only set during such a system suspend, and the
restore does nothing on a CPU without these features, so other machines
don't change.

Signed-off-by: Diljit Singh <hey@djdev.me>
Plain spin-table has no way out of the kernel, so disabling the non-boot
CPUs for system sleep fails with -EOPNOTSUPP. That rules out
suspend-to-RAM on a kernel that a boot monitor enters directly at EL2
with spin-table, with no PSCI below it.

Where iBoot locks a core's reset vector to the boot object, a core that
powers off and is started again goes back through the boot object. If
that keeps the core's release address, the core returns to
secondary_holding_pen on its own, and hotplug needs no more from it.

Such a boot object says so once, with /chosen/aurora,s2r-mailbox, the
mailbox the boot core resumes through. With that property present, a
CPU whose enable method is "spin-table" gets the parking ops added here,
so the device tree keeps the method every kernel knows, and a kernel
without this change ignores the property and stays on plain spin-table.
A boot object can also name the "aurora,rvbar-spin-table" method for a
CPU.

A secondary that takes its deep WFI at hotplug stops the whole machine
within seconds on T8140, and macOS takes that WFI only after the sleep
commit. So a CPU leaving the kernel parks here, still coherent, with
interrupts masked. For a suspend it writes its stop bit at the start of
the last mile and takes the deep WFI after the commit; otherwise it goes
back through secondary_holding_pen, staying at EL2.

Every core loses its power in a real sleep, so after the wake a core
still parked, one that never saw its stop, is taken as off and started
through the platform's on step, which only writes its start bits. Measured
on a J700, though, a sleep committed with a core that missed its stop
doesn't come back at all, so the sleep has to refuse for such a core
while it still can. The sleep controller's hold does that.

The SoC-specific stop, off and on steps come from a platform driver
through spin_table_set_power_ops(). Until one registers, hotplug fails
as on plain spin-table, and so does a kexec load: the parking ops have a
cpu_die, so cpus_are_stuck_in_kernel() no longer catches them, and the
kexec would otherwise fail at machine_kexec() with the CPUs still
online. Without KEXEC_CORE (for cpu_soft_restart) hotplug is refused
too. Other machines keep the ops they have, since only that property or
the explicit method selects these.

Signed-off-by: Diljit Singh <hey@djdev.me>
…pend

T8140's firmware-driven suspend powers the controller down, and the AIC
driver has no suspend code at all. Mask every line before the commit so
that nothing can end the final WFI, and put the controller's state back
after a real power-off. Unmask from the IRQ core's books, since reading
the mask registers has not been shown to work.

The boot CPU doesn't go through hotplug on the way back, so run its
per-CPU setup from resume too, keeping the FIQ state of its existing
users.

The sleep engine also waits for every CPU to stop, and a raised line
ends WFI even with interrupts off at the CPU. macOS masks every line
right after the commit. Export that step, its undo and an event read to
the sleep controller, which runs them with one CPU up and interrupts
off.

The syscore ops are only registered on T8140 and only act on a
firmware-driven suspend, so nothing changes on other machines or in
s2idle.

Signed-off-by: Diljit Singh <hey@djdev.me>
Every command asks for an interrupt on its response, and the driver
leaves that vector enabled. On T8140 the wake logic watches this queue's
line for the PMU's RTC alarm, so our own traffic could end up read as a
wake at the commit. macOS clears the same enable bit shortly before it,
measured under aura.

Mask and clear the vector in the noirq phase of a firmware-driven
suspend and poll for responses until resume. After a real power-off also
write the vector enables back, since whether they survive hasn't been
measured. Only that kind of suspend is affected, so s2idle and other
machines see no change.

Signed-off-by: Diljit Singh <hey@djdev.me>
The driver refuses every sleep state but s2idle, because nothing routed
the alarm as a wake source in any other. A firmware-driven suspend does
that itself: on T8140 the alarm is wake source NUB.SPMI0.SW3, and
macOS's RTC wakes come back through it with the same alarm registers
(measured under aura).

The suspend path clears the driver's refusal only when the platform has
said it is driving the suspend, so other machines keep refusing.

Signed-off-by: Diljit Singh <hey@djdev.me>
A firmware-driven suspend changes domain states behind genpd's back.
macOS's wake record on T8140 shows AUTO_ENABLE gone from 22 domains and
many others left clock gated: it raises the first group and lowers 30 of
the second within 5 ms of a wake. genpd only powers a domain on if it
turned it off, so a domain it left on stays wrong.

On a resume from such a suspend, put each domain where genpd's books say
it is. Follow the books rather than macOS's two lists, since the rule
behind those lists hasn't been worked out. Firmware-owned domains stay
untouched.

Some domains need their driver's order on the way back. For the GPU's
that is the target, the bridge, and only then automatic power
management, and a resync would raise it with AUTO_ENABLE well before the
GPU driver runs. Add apple,sleep-driver-owned for a domain whose driver
lowers it before a sleep and brings it back afterwards; the resync
leaves it alone.

The PMC driver keeps which agents it enabled on each rail and the floors
its domains hold, and writes neither again on its own. Restore them from
the books in resume_early: a register that kept its value isn't written,
and CLPC's sampling starts again only in resume, after this.

Only pm_resume_via_firmware() runs any of this, so other paths are
unchanged.

Signed-off-by: Diljit Singh <hey@djdev.me>
A firmware-driven suspend powers the pin block off, and nothing sets a
pin up again: the pin core thinks its state hasn't changed, so
reselecting it does nothing, and a pin in a consumer's hands isn't
reconfigured by anything. The PCIe host's CLKREQ mux and PERST# are two
that need this.

Mark each pin the driver writes, save those in suspend_noirq and write
back the ones that differ in resume_noirq. Only those: the pins it
hasn't configured belong to the firmware or another processor, and on
T8140 a write to one raises an SError. Only on controllers whose regmap
reaches the hardware, T8140 and T8132, since a cached one would hide the
loss. A plain suspend-to-idle does nothing here, and other SoCs are
unchanged.

Signed-off-by: Diljit Singh <hey@djdev.me>
…d one

A firmware-driven sleep powers the cluster down, and it came back in
pstate 2 with the driver expecting 1, the suspend state. The first
request after the wake then stopped all requests as if another owner
held the cluster, and the next suspend failed. Read the state again on
resume, before the governor starts.

Signed-off-by: Diljit Singh <hey@djdev.me>
A coprocessor can have a second mailbox, the ADT's
iop,secure-rtbuddy-proxy, for its secure side. It has the registers of
the AOP setup port. Its interrupts aren't described yet, so this variant
has none and its consumer polls. Match the generic compatible, since the
node isn't specific to one SoC.

Firmware that powers itself down for a system sleep needs more from its
mailbox:

- A way to clear and set bit 0 of the receive control word around the
  stop, as macOS does.
- A check for a free send slot that works with interrupts off, where
  apple_mbox_send()'s own deadline can't pass.
- For an ASCWrap v6 CPU whose stop moves to the sleep's last step, with
  interrupts off and one CPU up, where neither the lifecycle lock nor
  runtime PM can be taken: its control word for polling its status, the
  stop's register writes alone, and a call that settles the books after
  the sleep without touching the registers, so that the next start goes
  through.

The wait for room to send now ends only on a fatal signal. The freezer
signals every user task as a system sleep begins, and a message half
sent can break a coprocessor's handshake.

Rust bindings cover the receive control and holding and releasing
receive.

Signed-off-by: Diljit Singh <hey@djdev.me>
A firmware-driven sleep powers the co-processors off, and RTKit had no
way to stop one for that or to bring it back without a full reboot of
its firmware. This adds what the drivers need, each step measured
against macOS:

- AP-only power notices. macOS tells the AOP, MTP and SMC about a sleep
  with the AP power state alone, 0x201 before it and 0x20 after, and
  leaves them running. A noirq variant polls.
- A sleep stop. After the IOP's idle ack macOS waits for the CPU status
  to read stopped and idle, clears bit 0 of the mailbox's I2A control
  word, and clears the run bit twice, each time from what it reads back.
  apple_rtkit_sleep_stop() does that, with a 5 ms poll for the status;
  on an error the run bit stays set and the firmware still runs.
- A polled idle for a stop that can't be taken back. ANS doesn't come
  back from idle with its power kept, so its idle follows every refusal,
  with interrupts off and one CPU up. The messages are sent and acked by
  polling, the syslog and IOReport notifications that want an ack are
  answered as they come, and OSLOG counters are echoed, below. A counter
  the receive work still owed, which can't run then, is echoed before
  the first state request, and whichever side takes it first echoes it,
  so none is echoed twice.
- A warm restart. After a real sleep the firmware restarts with its
  buffers and rings in place and sends HELLO unasked. Nothing is freed
  and no endpoint is started again; the endpoint map is echoed as macOS
  does, with bit 1 added to the first part, and a driver can set what
  the last part gets back (ANS gets endpoint 0x20 only).
  apple_rtkit_sleep_return() does that after a sleep that reached the
  hardware. After one that didn't, a firmware that idled gets -EAGAIN:
  an idled firmware doesn't wake with its power kept, so it stays down
  until the next real sleep restarts it. One whose idle failed still
  runs, and gets 0. apple_rtkit_sleep_return_start() is the first half,
  for a driver that starts other firmware before waiting, and
  apple_rtkit_power_cycled_return() starts one its driver power-cycled
  without a sleep.
- Hooks around the AP state change in boot and idle, for a driver that
  has to answer another port in between.
- A polled port that only answers the co-processor: the DCP's secure
  RTBuddy port, whose HELLO and endpoint map macOS's secure side echoes
  back unchanged.
- OSLOG counter echo. A firmware counts its log writes, holds the next
  counter until the last is echoed, and with one outstanding takes no AP
  power notice: a DCP restarted warm refuses its next AP idle, and the
  MTP and the SMC stopped answering after about eight sleeps. macOS
  echoes every one, so on T8140 every RTKit device's counters are
  echoed, and a driver can turn that off.
- A crash on a crash log endpoint the firmware never announced. Such a
  firmware asks for that buffer only when it crashes, at an address it
  chose. The request was taken for a boot-time one and dropped, so the
  driver never learned of the crash and kept sending to it: the AOP
  after a wake, refusing every later sleep. It's reported at once, and
  the buffer is mapped through the device's IOMMU when that's one
  contiguous range and acknowledged, so the log is dumped when it comes.

Management message 0xc, which follows the IOP's idle ack, is no longer
logged as unknown. An adopted session completes its endpoint map at
adoption, as its previous owner did that.

A handshake given up halfway can leave a firmware started that nothing
stops, so RTKit's waits end only on a fatal signal, and during a system
sleep not even that. The freezer signals every user task as a sleep
begins: on a J700 that cut a display power-on short as a sleep began,
left the DCP running while its driver took it as stopped, and every
later sleep was refused.

Rust bindings cover idle, mailbox off, warm restart and the OSLOG echo.

Nothing changes for a driver that doesn't call the new functions.

Signed-off-by: Diljit Singh <hey@djdev.me>
A firmware-driven sleep resets every DART. After the first real wake on
T8140 every RTKit coprocessor but the DCP timed out: the PMP and SIO
restart their firmware in their early resume, and their first DMA went
through a DART that had not been put back yet.

- For a firmware-driven sleep, suspend and resume in the noirq phase,
  which every device finishes before any early resume, and after a real
  sleep write the saved state back even for a DART runtime PM had
  suspended, which pm_runtime_force_resume() leaves alone. Every other
  sleep keeps the normal phase, on every SoC: the PCIe-C tunnel DARTs
  need it, as their host stops the tunnel ports in its suspend_noirq
  and sets the port clock up again in its resume_noirq. On Apple SoCs
  only the T8140 sleep driver, later in this series, sets
  pm_suspend_via_firmware(), from .begin, and it holds until the next
  suspend, so both halves take the same phase.
- Replay the loader's address filter. The PMP's first write after the
  wake lands in a range the loader programs into its DART's DAPF (the
  ADT's dapf-instance-0), which a real sleep clears. Take the ranges
  from apple,dapf-ranges and an optional apple,dapf-r20, and write them
  back wherever the translations are restored, unless the filter still
  holds them or is locked.
- Write the loader's tunables back where a power-off lost them, from
  apple,resume-tunable, only the words that differ. The ISP's DARTs need
  both, as its firmware starts again after the wake.
- A consumer-driven save and restore for a DART whose firmware gates
  itself before the DARTs suspend (the SEP's): apple,sleep-prepare,
  apple,sleep-restore and apple,sleep-restore-late list the writes
  around it, and the consumer calls apple_dart_sleep_prepare() and
  apple_dart_sleep_restore() in its own order. The DART's own callbacks
  then leave it alone. Its config words go back as the DART had them
  before the sleep, since Linux's setup of those words differs from
  macOS's.
- GAPF. On T8140 the fabric rejects a write to a display coprocessor's
  CPU power state while bit 4 of its DART's GAPF slice is set. A display
  stop that flushes the DART waits for the flush, then clears the bit;
  it's set again when the streams come back on.
  apple_dart_display_streams() does macOS's stream writes for a display
  power cycle. A deep sleep resets the GAPF bank too, so the slice's
  value is saved at probe and protected with that, and
  apple_dart_gapf_ready() releases a slice the wake left protected
  before the coprocessor's CPU domain comes up.
- While a display DART's streams are off for a power-down its registers
  may be unpowered: take each stream's table level from a snapshot made
  as they went off, and defer TLB invalidations until the streams are
  back on. apple_dart_power_dark() does the same for a display DART
  whose domain goes off at runtime while others still map into it: the
  display subsystem maps every framebuffer into DCPEXT's DART, and a map
  during DCPEXT's power cycle read a TCR in the unpowered DART.
- A DART that runs on its PCIe port's clock, as the T8140 radio port's
  does, gates itself in its noirq resume after a real sleep, until its
  host has the port back and calls apple_dart_host_resume(). The first
  real sleep with the radio host bound took an SError there.
- apple_dart_state_save() and apple_dart_state_restore() keep a DART's
  state across its consumer's own power cycle, outside a system sleep.
  They return -ENODEV for a device apple-dart isn't bound to.

DARTs with none of these properties, and other sleeps, behave as before.

Signed-off-by: Diljit Singh <hey@djdev.me>
The SART sits in its coprocessor's power domain, which a firmware-driven
suspend powers off, and the driver has no sleep code. A coprocessor that
restarts with an empty SART can't reach its firmware's buffers.

Save the entries in suspend_late, after the consumers have gone quiet,
and write back every one that was in use in resume_noirq, the same or
not, as macOS rewrites entries 0 to 7 at every wake. This includes the
bootloader's protected entries: Linux doesn't change those, it only puts
back what power loss took. macOS leaves unused entries at their power-on
flags, and so does this.

macOS writes the entries back once more right before ANS's run bit,
config first, then address, then size. apple_sart_replay() does that on
SARTv3, for the coprocessor's driver to call there, and warns about an
entry that doesn't read back.

Only a firmware-driven resume restores anything, so suspend-to-idle and
every SoC that doesn't advertise deep sleep are unchanged. The SART also
has throttle registers, which this doesn't touch since nothing in Linux
knows their layout.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
…omains

Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
…-edac-slpddr-wake

Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
…pcie-slpddr

Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
…elect

Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
…neric

Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
…roc-crash

Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
DjDeveloperr added a commit that referenced this pull request Nov 9, 2026
…ug-cut-wakes

Brings in integration/aurora ac11649 (#1311, PCIe multi power domain
support) through #1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
Brings in integration/aurora 15f88e1 through aurora-silicon#1208. The conflicts are
the glue's kernel-doc and the neo-stability host test's mock: this
branch's sleep_ddr is kept next to integration's drd_regs and usb2_phy.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…omains

Brings in integration/aurora 15f88e1 through aurora-silicon#1208. The one conflict is
the PMGR domain's fields: the sleep records and integration's
consumer-switched state are both kept.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…-edac-slpddr-wake

Brings in integration/aurora 15f88e1 through aurora-silicon#1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…pcie-slpddr

Brings in integration/aurora 15f88e1 through aurora-silicon#1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
Brings in integration/aurora 15f88e1 through aurora-silicon#1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…elect

Brings in integration/aurora 15f88e1 through aurora-silicon#1208. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…omains

Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…-edac-slpddr-wake

Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…pcie-slpddr

Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…elect

Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
niteria pushed a commit to niteria/linux that referenced this pull request Nov 10, 2026
…neric

Brings in aurora-silicon#1208's aop-bootargs host test fix. No conflicts.

Signed-off-by: Diljit Singh <hey@djdev.me>
@DjDeveloperr

Copy link
Copy Markdown
Contributor Author

Superseded by #1445 (merged as 2b05915); its changes are in integration/aurora. #1445's d9b438c carries this stack's final diff. The sleep driver is now drivers/soc/apple/sleep.c (#1334 renamed t8140-sleep.c).

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.

1 participant