Skip to content

J616s (M4 Pro) at EL2: hibernation and suspend-to-RAM - #1477

Open
fbalazsy wants to merge 258 commits into
aurora-silicon:integration/aurorafrom
fbalazsy:j616s/el2-sleep
Open

fbalazsy wants to merge 258 commits into
aurora-silicon:integration/aurorafrom
fbalazsy:j616s/el2-sleep

Conversation

@fbalazsy

@fbalazsy fbalazsy commented Nov 10, 2026 •

Copy link
Copy Markdown

J616s (MacBook Pro 16", M4 Pro, T6040) running Linux at EL2 under aura: hibernation and suspend-to-RAM with the desktop session kept. Builds on #1445, which is merged.

Base

  • 258 commits on integration/aurora at aa68b3c. The first 168 are the series as it stood at d24d697, rebased. The base's move of the SoC words into per-SoC data (sleep.c) met three of the clean-up commits: the shared header now carries the base's definitions, the PMU write helper takes the USID as the base's does, and what the base reads from its match data (the PMU write command, the boot stage, the response vector's enable for the sleep) stays with the T6040 part as its own values. The 89 on top are new in this push and are listed under "New in this push".
  • Every commit of mine carries its Signed-off-by; the two commits taken from others (RyanTheTide's phy: apple: t6040: tolerate a busy recovery domain, AxxzyWasTaken's spmi: apple: drop replies left by firmware at a firmware-driven resume) keep their authors and trailers.
  • The aura side, J616s at EL2: hib=1 fixed layout, T6040 S2R windows, the 26.7 display handoff and the S2R wake path aura#148, is merged; aura main has it.
  • Replaces J616s (M4 Pro): hibernation (suspend to disk) under aura's fixed boot layout #906.

What is in it

Area Change
PM core pm_hibernate_is_restored() lets restore callbacks tell a restored image from an aborted hibernation; a boot chain with a fixed layout (aura hib=1) admits hibernation.
arm64 Spin-table CPUs at EL2 are parked in a pen and released across S4 and S2R; EL2 state is restored for the hibernation resume.
soc/apple aura hibernation layout; RTKit cold restart and re-adoption of a coprocessor found running or halted after a power loss; PMP (data SRAM kept across S2R), AOP, SEP, DockChannel.
soc/apple sleep T6040 S2R as a part of the generic driver (sleep.c + sleep-t6040.c, struct apple_sleep_part in sleep-internal.h; one platform driver apple-sleep, the node enabled per board, on the J616s only). The entry takes the hardware down in a fixed order (CPU complexes, power gates, axi2af bridges, APCIe, DCS, the MasterMini S2R timer, then the commit word) and the wake brings the same blocks back, before syscore resume. The coprocessor and enclave registry stays in sleep-registry.c; the driver binds it once in probe and each SoC's part gives the two answers (T8140's rules and T6040's are the ones they had). APPLE_T6040_SLEEP stays an option, now the switch of the T6040 part under APPLE_SOC_SLEEP (with MFD_MACSMC=y). The part keeps its own suspend ops: suspend-to-idle passes through untouched and deep is offered only on a kernel booted with t6040_sleep.allow_entry=1. Four module parameters are left (allow_entry, dry_run, sleep_wd_secs, wd2_secs; 79 before); the progress marks, PMU records, probes, dumps and the experiments that were off by default are gone, the steps that were on by default are unconditional. Dry-run levels are 1 (every step that can be taken back, refused at the commit point) and 2 (also the last steps); the old level 1 never reached the commit point and is removed.
Display (drm/apple) Internal and external T6041 display processors across S4 and S2R: the stop order the firmware expects, power words, DART streams, AFK endpoints paused and resumed, backlight level sent again. External links are given back before the sleep and set up again after it. The HDMI output's link is started once per plug, from the converter's power-up, as macOS does: a second start through an HPD pulse left the display black on some cold boots. A display that the wake's link has just woken can list the modes of its standby first: a Type-C display that lists other modes than before the sleep is read again once, and the hybrid pipeline is not handed over on the first lists after a wake. A display that fits one pipe again no longer counts as waiting for a second one.
GPU (drm/asahi) G17 drain and restart around S4 and S2R, the GPU's PMGR word found through its PMP report entry.
USB / Type-C A T6040 Type-C port keeps its xHCI host through a deep sleep (PHY stopped and started under it), a pin D dock port included while DP is down; USB4 routers get their Type-C PHY in the DT; ACIO, CIO PHY and tipd across sleep.
PCIe, NVMe, radio DART locked tables re-imported; ANS/NVMe; brcmfmac survives a hibernation that was aborted or only thawed. After an S2R the internal host's two links are set up again in the noirq phase, with the port word, the tunables and the root-port link words the boot loader had set, so the PCI core finds the functions. The BCM4388's Bluetooth function comes back without its firmware: hci_bcm4377 sees that at resume (its configuration windows read reset values, boot stage 0), sends nothing and starts the transport's reset recovery at once instead of after command timeouts.
Camera (apple-isp) T6040's ISP is powered down between streams and its firmware started again from its data segment as loaded (the T8132 scheme), so the camera works after a sleep.
ANE H16 firmware 26.7.1, resident programs, system sleep and hibernation, on the base's hung-request recovery: its hung_stop and session quarantine are used unchanged, the hibernation events go through its notifier, and an engine quarantined at bring-up is powered off for a sleep (and left off at the resume) instead of refusing every sleep of that boot.
KVM Resuming from disk is a cold boot, so the physical counter starts again while a VM's counter offsets stay: the guest's counter went backwards and its clock stood still. The offsets of every VM are moved by the lost count at syscore resume.
SMC The sleep's commit writes WKTP = 2 as macOS does; after a full wake WKTP = 0 is written. Without it the SMC treats all the time awake as a power nap: it refuses manual fan control for the rest of the boot and asks for a sleep under load.
Other drivers AIC3, pmdomain, SPMI, PMU RTC, pinctrl (every AP pin and the block's global word put back after S2R), ProRes T6041 across sleep (on #1260's variant: the J616s rows are its built-in ones, the per-Mac file stays an optional override), SIO restart, DockChannel HID, AOP ALS, cs42l84 jack interrupt.
DT T6040 sleep controller, ProRes, JPEG, DP audio and USB4 PHY links on the J616s.
Clean-up (63 commits) Comments, dead code and state nobody reads, per-sleep messages at debug level (the display driver prints 96 lines per wake instead of 141), names and named constants, pm_sleep_ptr() instead of #ifdef CONFIG_HIBERNATION in PM operations, dev_* messages from the T6040 part, the sleep node's unit address, bindings for apple,t6041-display-power and aurora,s2r-mailbox. One of them fixes a build configuration: with CONFIG_HIBERNATION=n a suspend-to-RAM stopped the display firmware and could not start it again; the restart is built for every system sleep now. For the comment, rename and constant commits the objects are identical before and after; sleep.o built without the T6040 part has every symbol unchanged from f259e90.

New in this push (89 commits)

Area Change
SMC (4 commits at the end of the mfd group) A suspend that is called off no longer leaves the machine without its fans. This SMC does not take MBSE = slpw back: after slpw and then waka with no sleep between, it stopped running the fans (0 rpm with the CPU die at 100 to 113 C) and refused a fan request until the reboot; after the whole commit (WKTP, slpa, the AP idle notice) and no power loss its sensor reports stood still as well, so the thermal zones read a constant. A dry run, a device that refuses, a wake event during the entry and a refusal at the commit point all ended there, on the previous head of this series too. On T6041 the whole sequence is now sent from the sleep controller's last stop, with interrupts off and every refusal behind it (apple_rtkit_ap_idle_polled() is new for that); on the other SoCs slpw moves from .prepare to the noirq suspend, right before the commit. A suspend that ends short of the power loss sends the SMC nothing.
USB3 after a suspend-to-idle (phy-apple-t6040) The loss of USB3 until a reboot is fixed at its cause: in a suspend-to-idle genpd powered the PHY coprocessor's domains off under the running host, and the next stop of the coprocessor got AP idle not acknowledged: -62. The PHY now refuses that power-off while its firmware runs. With it: a powered-down sleep counts as the coprocessor's power cycle, a host whose USB3 is not back at a wake stays at USB2 and tries again at the next wake, and the CIO bring-up recovers the coprocessor and shuts the lanes down when it fails.
Display (drm/apple) A resume commit no longer sends a mode set to an external pipe whose sink is reported gone (the mode set failed: -5 of every suspend-to-idle with an HDMI display); the hotplug that reports the sink back sets the mode. A T6041 sleep keeps to the pipe's own power words; a firmware that was given up stays stopped across an S2R, and a dead internal processor no longer refuses every sleep; the sleep registers are mapped once at probe.
Wake time The two PCIe ports share the waits of a wake instead of taking 100 ms twice each (noirq resume 423 -> 118 ms); the dwc3 glue is asynchronous, so the USB hosts no longer start behind whatever the PM core's main thread waits for; the Type-C controllers do not wait 100 ms for a wake interrupt that has already come; a BCM4388 that lost its power is restarted from a freezable work item once the resume of every device is over (its resume callback: 619 ms -> 24 us).
Bluetooth The core is resumed after the cold function's reset, not before it (sending frame failed (-19) at the first wake of a boot).
Hibernation The jack codec, the RTC alarm, the pins, the AIC masks and the SPMI interrupt masks are put back after a resume from disk; tipd revalidates a port restored from disk itself, so the hibernation hook no longer has to write roles.
Thunderbolt / Type-C No wait for the router stop in PM_POST_*, the LSTX level at a router stop keyed on T6040, the T6040 DP IN adapters kept disabled, no watch for a router whose node is disabled. Without a USB4 device here these ran as regressions only.
NVMe, ISP, ProRes, ANE The RTKit object of a restore is configured as probe does; one predicate each for "this suspend takes the power"; timer_wakeups instead of irq_timeouts in the ANE's debugfs.
Tests macsmc_sleep_order_test expects the lid read (it failed at the base); new KUnit cases for the SMC's last stop; the host tests of the PM callbacks follow the code they extract.

Hardware results

J616s (48 GB), firmware 26.7.1, aura main (9887a6b5) at EL2, kernel built from this tree with APPLE_SOC_SLEEP and APPLE_T6040_SLEEP enabled.

This head (278b7b9) is the rebase onto aa68b3c of the head the tables below were run on (4b0be95 on fdef45b), plus one commit (dmaengine: apple-sio: stop the T6041 firmware at the sleep's commit). On the rebase (3d13866) with that commit's module: cold boots, three suspends to RAM, one suspend to idle, a level 2 dry run and two sleeps refused at the enclave's request, with the panel and both external displays, Wi-Fi and Bluetooth back each time. Hibernation and the tables below were not run again.

On 4b0be95, one boot. Set-up: a Samsung G95NC in picture-by-picture with one half on the HDMI port and the other in DP alternate mode, a USB3 disk, charger; no dock. All three displays were seen lit after the boot and after every step.

Test on 4b0be95 Result
Cold boot Panel and both external displays, Wi-Fi, Bluetooth, the USB3 disk; ANE at the first attempt; the SMC registered for the sleep controller's last stop.
Suspend-to-RAM (deep, RTC wake after 25 s) 3 of 3, one of them after the hibernation: the same compositor process, three outputs with their modes, Wi-Fi associated and answering, Bluetooth, the disk. "noirq resume" 118 ms, "resume of devices" 406 ms; no mode-set error. The SMC's sequence goes out at the last stop ("sleep warning at the last stop: 0 messages in 200 ms") and the wake record is the usual one.
Suspend-to-idle with the HDMI display No mode set failed; the HDMI half is back with the firmware's hotplug, about 5 s after the wake.
Dry run level 2 Refused at the commit point; nothing is sent to the SMC, its reports move and it takes a fan request afterwards.
Hibernation from the running desktop, reboot into the resume 1 of 1: image restored, both external displays, the disk, Wi-Fi, Bluetooth, a fan request taken; ANE exact, AES equal in both directions, ProRes frames identical.

On the previous base (6970a51) the same commits ran over five kernels as they were written, most of it with a dock (hub, Ethernet, card reader), the USB3 disk and one USB-C DP display:

Test Result
Suspend-to-RAM 34 in all, none failed; with the dock 12 USB devices on 9 SuperSpeed links after each.
The USB3 failure's recipe (cold boot, suspend-to-idle 15 s, then a dry run) genpd's power-off of the PHY domains refused once per port, no -62, 9 SuperSpeed links before and after. Before the fix: 2 of 2 lost USB3.
PHY fault injection on the dock port (drop_next_power_down_ack) Dropped acknowledge and a real S2R: 9 links. Dropped acknowledge and a dry run: the host stays, at USB2, nothing latched. A real S2R right after: USB3 back, 9 links.
Fans and SMC Before the fix (the 81 commits without the SMC ones): a dry run left both fans at 0 rpm at 100-113 C under load and a fan request refused; in the same boot the SMC had run the fans before it. After it: reports live and a fan request taken after a dry run, after a refusal before the enclave's request (apple_sep sleep_refuse_before_send), and after an RTC alarm that fired during the entry.
Hibernation Two in one boot, each restored, a suspend-to-RAM after them; an RTC alarm set before the first one fired at its time after the restore, one that passed while the machine was off was gone and a new one could be set.
USB disk across a suspend-to-RAM Mounted read-only with a reader running: the same block device and SHA-256. Mounted read-write with a writer: 120 blocks of 1 MiB written with fsync across the sleep, all 120 read back with the right SHA-256, no btrfs or I/O error.
ANE, Bluetooth A held ANE session exact across a sleep; Bluetooth powered off across a sleep and on again, two sleeps back to back: no error line.
Wake time, kernel only, from "Enabling non-boot CPUs" to "PM: suspend exit" Nothing attached: 0.285 s (noirq resume 125 ms, devices 62-88 ms), the panel's firmware back at 0.37 s. HDMI + DP + disk: 0.76 s. With the dock: 2.7 s, of which 2.2 s is the dock's hub tree coming back level by level by reset-resume.

Not run on either of these two heads: anything with the dock (it was run on the previous base only), the KVM guest across the sleeps, a USB4/Thunderbolt device (none here). T8140 was not run by me on any head, and the SMC and wake-time commits change paths it shares: the noirq slpw on every SoC, device_enable_async_suspend() on the dwc3 glue, the BCM4388 restart, the skipped 100 ms in sn201202x_resume.

Review

The comments and commit messages of the 89 new commits follow the same rule as the rest since the last review: they say what the hardware or firmware does and what was measured on the J616s.

Checks

Run on 4b0be95 against its base fdef45b (not repeated after the rebase onto aa68b3c), on an aarch64 host with its own compilers (gcc 16, clang 23, rustc 1.99), not the pinned ones of the workflows.

Check Result
tools/asahi/run-host-tests.py 324 runs, all ok with the host's compiler differences shimmed away (the base's new brcmf-recovery-tests among them). Without the shims 28 runs fail on this host's compilers; that run was not repeated at this base.
KUnit, C suites of the directories the series touches (qemu arm64) 1712 tests: 1703 pass, 4 skipped, and the five apple-display-gate-t6041-max cases that fail at the base as well (base: 1686 tests, those five and macsmc_sleep_order_test, which passes with the series).
KUnit, ANE boot.kunitconfig and transport.kunitconfig 230 of 230 each.
KUnit, drivers/soc/apple/sep.kunitconfig 605 tests, 603 passed, 2 skipped; in the workflow's form (LLVM, apple_sep*) 301 of 301.
scripts/dtc/apple-dt-check.sh (CHECK_DTBS on every Apple tree, dt_binding_check on the changed bindings) No warning the base does not have (402 lines at both, 137 DTBs); the 12 changed bindings pass.
sep-source workflow: the tools/aurora-sep tests, the three DWC3/ATC compile profiles The Python tests pass (14 lines, -O ones and the ABI step included); the three apple-dwc3-atc-compile.sh profiles compile; apple-sep.o builds.
checkpatch.pl --strict on the commits written for this push Clean but for one "prefer kzalloc_obj" note in the brcmfmac commit.

Open

  • A sleep refused at the commit point left the SIO firmware stopped until the next completed sleep: fixed by the last commit. The T6041 firmware now stops in the sleep's last step, as the T8140's does, so a refusal finds it running. After two refused sleeps and a dry run the AES engine behind it still encrypts and decrypts equal to the software implementation.
  • The SMC's last stop cannot refuse. If the SMC does not answer there, each of its three writes waits its 5 s with interrupts off before the sleep goes on. An event the SMC sends during the 240 ms of the sequence is taken from its outbox and handled after the wake.
  • After hot re-cabling (a full-width DP display on both pipes, the HDMI cable plugged while it ran, then the monitor switched to picture-by-picture) the HDMI half stayed dark once: "HDMI: no sink behind the converter yet, link started again (3/3)". A cold boot had both; the same switch of the monitor worked on the next two tries, one of them after a hibernation. Not reproduced, cause not known. A WARNING at typec_altmode_update_active() came once in the same re-cabling and was not looked at.
  • A resume from disk still logs "Unable to change power state from unknown to D0, device inaccessible" for the internal host's functions; they work afterwards.
  • The ANE's H16 firmware start with ignore_loglevel: unchanged from the previous description. Independent of sleep.
  • A mounted USB disk is not carried through a hibernation (the hook that hibernates this machine takes the Type-C controllers down first).
  • With a dock most of a wake is the USB core's reset-resume of the hub tree. usbcore.old_scheme_first=Y and the ports' fast-enumeration quirk took 0.64 s of it here with the same devices and device numbers; that is a setting, not part of this series.
  • Questions the new commits leave for you: whether the T8140 SMC also stays in its dark-wake policy until WKTP is written 0, and whether it loses its fan policy after slpw + waka as this one does (then its noirq path has the same hole after the noirq phase that the last stop closes on T6041); whether sn201202x_resume may skip the 100 ms on every SoC with that controller; whether the AIC mask resync after a resume from disk should be registered for every SoC; whether a host left at USB2 by a wake should try a normal start at once instead of at the next wake. 263989e (DART in an off domain) is not in here.

How to try

Deep sleep stays opt-in, on the kernel command line:

t6040_sleep.allow_entry=1
echo deep > /sys/power/mem_sleep; systemctl suspend

With t6040_sleep.dry_run=1 (writable at run time) the same request stops at the commit point and resumes warm.

@fbalazsy
fbalazsy force-pushed the j616s/el2-sleep branch 2 times, most recently from 3926193 to abc6ee8 Compare November 11, 2026 15:57
@fbalazsy fbalazsy changed the title J616s (M4 Pro) at EL2: hibernation, and suspend-to-RAM (WIP), on #1445 J616s (M4 Pro) at EL2: hibernation and suspend-to-RAM, on #1445 Nov 11, 2026
@fbalazsy
fbalazsy force-pushed the j616s/el2-sleep branch 3 times, most recently from c99d8b7 to 6deb620 Compare November 12, 2026 02:50
@fbalazsy fbalazsy changed the title J616s (M4 Pro) at EL2: hibernation and suspend-to-RAM, on #1445 J616s (M4 Pro) at EL2: hibernation and suspend-to-RAM Nov 12, 2026

@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.

Before this can merge, please reword the comments that name macOS driver classes and methods, mark steps as RE or decompile-verified, or say where a value was read. That covers mainly t6040-sleep.c, plus t6040-usb2.c, dcp.c and macsmc.c. Each comment should say what the hardware or firmware does: the register, the bits, the order, and what you measured on the J616s. Values and code can stay as they are.

@fbalazsy
fbalazsy force-pushed the j616s/el2-sleep branch 3 times, most recently from 2ce0d80 to 7aefde0 Compare November 12, 2026 07:05
@fbalazsy

Copy link
Copy Markdown
Author

Done in 7aefde0, thanks for the pointer to #1625.

  • The comments that named host-driver classes or routines, marked a step as reverse-engineered, or said where a value was read (a trace, a recording, a run, a source file) now describe the hardware or firmware: register, bits, order, and what was measured on the J616s. Mainly t6040-sleep.c and dcp.c, also t6040-usb2.c, macsmc.c, nvme/host/apple.c, pmp.rs, t6040.dtsi and apple-ane.rst. The private labels of my bring-up notes went with them.
  • Values and code are as they were: with comments stripped the files are identical, apart from six strings (five parameter descriptions and one log line) that carried the same labels.
  • One real change came out of it: t6040-sleep.c still printed a list of steps that "had never run on hardware" at every suspend and needed i_know_untested to get past it, although all of them run. The list and the parameter are removed in the last commit; the commit is reached with no live blocker and t6040_sleep.allow_entry=1, and a requested dry run comes first.

Rebased onto aea2deb on the way. Tested on the J616s on this head (suspend-to-RAM, hibernation, suspend-to-RAM again; details in the description). That round was on battery without the dock, so the external displays have not run on this head yet; I will add that run.

What is left in t6040-sleep.c are the module parameters and records marked "Bring-up"; they go when the T6040 becomes match data in sleep.c.

@RyanTheTide

Copy link
Copy Markdown
Contributor

Tested on a second J616s (M4 Pro, 24 GB, firmware of a macOS 27.0 install), Linux at EL2 under aura main 27298d21 (includes aura#148), kernel = integration + this series:

  • Deep sleep, RTC wake after 30 s: 25 of 25 sleeps resumed, each a real commit and mailbox return (wake: resumes now +1 per cycle): 10/10 on c99d8b7, then 4 + 10 on 7aefde0 with the four commits below (two boots). After every wake: Wi-Fi reassociated, gateway ping, an 8 MiB NVMe write + fsync + read, internal panel and the USB-C DP monitor, 7 input devices, CPUs 0-13. Wall time 32.3-33.5 s for a 30 s alarm.
  • One miss: on the first sleep of one boot, the dock port's USB3 side did not come back. phy-apple-t6040-usb2 392a90000.phy: RTKit: AP idle not acknowledged: -62 at the suspend, then PHY not back after the sleep (-5), dwc3-apple 392280000.usb: not resuming xHCI ... -5; the port came back USB2-only (hub on bus 3, the USB3 device gone) and stayed that way, with phymxwrap suspend failed: -11 on each later sleep. 24 of the 25 wakes had all 8 USB devices.
  • One 603 s deep sleep on battery: about 0.34 W average from the fuel gauge (rough: the battery was full and the gauge corrects itself after a wake). Awake with the panels off: 3.9-4.1 W.
  • Kernel log after each wake: the hci_bcm4377 -19 lines you list under Open, plus 1-5 apple_soc_edac 'event-log source 9' reports in the first ~10 s after the wake.

Four commits on top of 7aefde0 are on aurora-silicon/linux branch j6s2r/el2-sleep-fixups. Please pick them into this branch (or say if you want them changed):

  1. soc: apple: t6040-sleep: leave suspend-to-idle alone: the PM core calls suspend_ops->begin/end for s2idle when there are no s2idle_ops, so t6040_sleep_begin() refused every s2idle with -EOPNOTSUPP once the controller probed. .begin now returns 0 for anything but PM_SUSPEND_MEM, .end only undoes a MEM .begin, and .valid offers MEM only with t6040_sleep.allow_entry=1, so mem_sleep keeps s2idle as the default.
  2. soc: apple: share the sleep registry so one kernel builds both controllers: sleep.c and t6040-sleep.c both exported the nine apple_sleep_coproc_*/apple_sleep_enclave_* symbols, hence depends on !APPLE_SOC_SLEEP. The registry moves to sleep-registry.c; each controller binds to it in its probe. asahi.config gets CONFIG_APPLE_T6040_SLEEP=y, so one config builds the T8140 and T6040 controllers.
  3. soc: apple: t6040-sleep: depend on a built-in SMC driver: built in, it calls apple_smc_read_u32/write_u32.
  4. soc: apple: t6040-sleep: name the T8140 driver instead of its pull request: comment wording.

Checks on that branch: universal config builds (both controllers =y, and each one off), sleep KUnit 6 pass / 1 skip, checkpatch --strict clean, s2idle 3/3 with no kernel errors and the deep rows above on the J616s, s2idle 3/3 on an M1 Pro and an M2 with no new kernel errors.

AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
…uspend

The T6041 (M4 Max) has the AICv3 of T8122 and now a suspend-to-RAM
driver of its own. Give "apple,t6041-aic3" its own info, the T8122 one
with fw_suspend, so the AIC state the sleep loses is saved and put
back as on T8140. Nothing else in the driver looks at fw_suspend.

Also in aurora-silicon#1477 (T6040 sleep), which admits apple,t6041-aic3 the same
way; drop this one when that lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
apple_nvme_t6040_hw is only matched by "apple,t6041-nvme-ans2". Give it
the sleep fields T8140 uses: register ANS with the sleep core
(sleep_traced), flush namespace 1 only, the OS's own, and send the
full shutdown notification at suspend. The T6041 sleep driver stops
ANS through the sleep core's coprocessor stop.

On T6041 the sleep core's last check (the shutdown notification has
completed) does not run: the T6041 driver calls only the coprocessor
stop.

Also in aurora-silicon#1477, which sets the same fields with sleep_untraced on the
same struct; drop this one when that lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
The PMP's SRAM loses its contents in a real sleep. On the J616C's
26.6.2 firmware its __DATA segment is at 0x500761000, 0x7d000 bytes:
back up (0x61000, 0x7d000) of the SRAM, as T8140 does for its own.

Also in aurora-silicon#1477, whose HW_CFG_T6041 has the same range; drop this one
when that lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
On apple,t6041, write the keys macOS writes last before MBSE slpa in a
trace of its sleep: WKTP = 2 (AppleSMCEmbedded, going to sleep), then
pmVC. The value written is 0x101, as the bring-up kernel runs it; the
comment, taken over from it, says the trace has 0x100. That difference
is open.

Also in aurora-silicon#1477, which writes WKTP through a sleep_wktp parameter;
drop this one when that lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
A conventional host (internal ports with the radio and the SD reader)
loses its power in a real sleep: the endpoints sit unpowered or in
reset behind a link that is down. On a firmware suspend, save what
only the port registers hold; on the noirq resume, note which ports
lost their link and keep a copy of their functions saved state before
the PCI core spends it on the down link; in the normal resume phase
(after the SMC, which owns the PWREN GPIOs) bring each such port up
again as probe does, then restore its functions.

Only for hosts that are not tunneled, have no managed SIDs, no PIO DMA
supplier and more than a root bus; s2r_conventional turns it off. The
suspend-to-idle paths (domain hold, adopted links) are unchanged.

Also in aurora-silicon#1477 (0ab1a66), which this follows; drop this one when that
lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
@fbalazsy
fbalazsy force-pushed the j616s/el2-sleep branch 2 times, most recently from 1599341 to 9bee928 Compare November 12, 2026 13:35
@fbalazsy

fbalazsy commented Nov 12, 2026 •

Copy link
Copy Markdown
Author

Picked up, thanks for the second machine and the four commits. Head is f259e90 on integration/aurora 107b361 (the brcmfmac .thaw commit dropped out, the base has it now). In order:

  1. 7aefde0 as you tested it, then your four from j6s2r/el2-sleep-fixups, unchanged.
  2. One fix for them: the registry commit moved apple_sleep_commit_stops_coprocs() and the enclave registration out of sleep.c, and tools/asahi/sep-suspend-errors-tests.py reads them from there, so it and its 21 mutation runs failed ("missing function"). The test now reads the three functions from sleep-registry.c.
  3. The follow-up I had announced as a separate pull request, 35 commits; it was finished and it removes what you would otherwise have to review twice:
    • t6040-sleep's bring-up parameters, marks, records, probes and dumps are gone: 79 module parameters down to 4 (allow_entry, dry_run, sleep_wd_secs, wd2_secs); the same in the display, GPU, PCIe, pinctrl, PHY, dwc3 and PMP parts of the series.
    • dry_run=1 never reached the commit point (the PMU RTC refuses a deep sleep that is not firmware-driven in .prepare); it is removed and the levels are 1 and 2 now.
    • The T6040 controller is a part of the generic driver: sleep.c + sleep-t6040.c, one apple-sleep platform driver. Your sleep-registry.c and struct apple_sleep_ctl stay as they are; the driver binds the registry once in probe and the SoC's part supplies the two answers, each SoC with the rules it had. APPLE_T6040_SLEEP stays a real option (the switch of the T6040 part, with your MFD_MACSMC=y), so the line in asahi.config stays valid. Your suspend-to-idle change is kept as written: the part keeps its own suspend ops, .valid offers deep only with allow_entry=1.
    • Two comments that still cited pull request numbers are reworded.

If you would rather merge 9bee928 first and take the 35 commits as a second pull request, say so and I will split it there; they sit on top and nothing below them changed.

On the J616s here, kernel from this head with APPLE_SOC_SLEEP and APPLE_T6040_SLEEP (lid closed, no dock): cold boot, suspend-to-idle, dry run 1 and 2, suspend-to-RAM x3, hibernation, suspend-to-RAM after it; KVM guest, SIO AES and ProRes checked after each. The same list passed on 9bee928 earlier today. Host tests 292 -> 306 ok, KUnit with the same six failures as the base, Rust KUnit 573 of 575, sep-source, the three DWC3/ATC profiles and the dt check pass; details in the description.

Not verified: T8140 on the merged driver (its object differs from before only in the functions that now go through the part; a sleep on a T8140 would be welcome), and external displays, the dock and the kept USB hosts on this head. The USB3 miss on your dock port ("AP idle not acknowledged: -62") is under Open; I could not try for it without a dock.

sithglan added a commit to sithglan/aurora-linux that referenced this pull request Nov 12, 2026
… K116s deep-sleep base

Base: Jens Axboe's K116s series of 2026-11-12 (40 patches on
integration/aurora 2b4523c), the tree his K116s runs at EL2.
aurora-silicon#1477 brings the generic Apple SoC sleep driver with per-SoC parts
(sleep.c, sleep-t6040.c) to start a T6050 part from.

Signed-off-by: Thomas Glanzmann <thomas@glanzmann.de>
@RyanTheTide

Copy link
Copy Markdown
Contributor

@fbalazsy thanks for picking up the fixups and folding the T6040 controller into sleep.c. To merge this we need:

  1. A rebase onto the current integration/aurora (028d762); the branch conflicts now.
  2. Signed-off-by on 9bee928, c92a639, 9794539 and f259e90.
  3. Your external-display run on the rebased head, and whether you have seen the dock port come back USB2-only after a wake (phy-apple-t6040-usb2 "AP idle not acknowledged: -62") on your machine.
  4. Please reword "follows the order of macOS's quiesce and commit" in the description to describe the hardware order itself.
    One PR is fine with us; there is no need to split off the 35 follow-up commits. Once the rebased head is up we will run the J616s sleep rows and a regression pass on our other Macs.

The CIO bring-up stored the plug orientation it had programmed in
cio_flip, and nothing ever read it: the teardown of mode 2 is the same
for both orientations, and the next bring-up takes the orientation from
the Type-C switch again. Drop the field and its one store.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The PHY's tunable tables are applied by their position in one list, and
three places depended on positions written out as numbers: the AUSPLL
table that USB3 applies once more (7), the lane tables that DP writes
again after each link power-up (4 and 5), and the generic tables DP
starts with (the first 6). A static_assert checked that the last two
agree with each other, not that the list has those tables at those
positions.

Name the positions in an enum and build the list with designated
initialisers, so that a table that moves takes its users with it. The
generated code doesn't change.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
checkpatch --strict: "multiple assignments should be avoided" for the
two DP tunnel rates that a restore from disk clears. Clear them one by
one. The generated code doesn't change.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The teardown of a CIO session and the error exit of the CIO bring-up run
the same shutdown: both TX lanes and the common block down, the two
receivers powered down, 200 us, the coprocessor stopped, the CFG block
back in its boot state. Put it into one function for both.

The register accesses of either path and their order are as before.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
When a write of the USB2 power-down sequence does not read back, the PHY
marks itself failed and refuses every later operation: no power cycle of
a PHY in an unknown state. A port that goes down for a sleep with its
host kept logs that and lets the sleep go on, which is right, as the
sleep powers the SoC down and the USB2 PHY with it.

But the mark outlived the sleep. The wake refused to bring USB2 up on a
PHY that had just been through its reset, the host was restarted, its
stop failed on the same mark, and from then on the stop of that host
failed before every sleep, which refuses the sleep: one write that did
not land, and the machine did not sleep again until it was rebooted.

A restore from disk already drops the mark, as the PHY comes back cold.
A sleep that powers the SoC down is the same for the PHY: drop the mark
there too, next to the coprocessor's pending power cycle, in one helper
for both. After a sleep that left the SoC powered the mark stays, and
the wake reports the port as before.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
…idle

A suspend-to-idle leaves a USB host running: xHCI keeps going so that a
device can wake the system, and the controller's glue keeps the
controller's power domain on by being a syscore device. On T8132 the
Type-C PHY shares that domain. On T6040 it does not: the PHY sits in
atcN_common_dp, below atcN_phymxwrap, which powers the PHY's coprocessor
and its mailbox, and in the noirq phase of every system sleep the PM
domain core powers off the domain of each device that has suspended. So
a suspend-to-idle took the power from a coprocessor whose firmware ran,
and the driver went on taking it for running.

The next stop of USB3 then asked a coprocessor that was no longer there:

  RTKit: AP idle not acknowledged: -62
  USB3: PHY coprocessor idle failed: -62
  USB3: no asynchronous Power Down acknowledge

On a J616s this came with the first deep sleep after one suspend-to-idle
of 15 s, on both ports with USB3 up at once, and with no sleep of a boot
that had no suspend-to-idle before it.

Keep the domain on for as long as the firmware runs: refuse the
power-off in the domain's notifier while the coprocessor is between its
start and the end of its stop. The mailbox's domain stays with it, as
the parent of a domain that is on. A host that goes down for a deep
sleep stops the coprocessor first, and so do CIO's teardown for every
sleep and the recovery before its power cycle: none of them is held up.

The other way, to stop the coprocessor for the suspend-to-idle and to
start it again after it, would have the firmware tune the PHY once more
under a link that is up, with devices that are only suspended.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
Across a T6040 suspend-to-RAM the stop of a display coprocessor saves
the power words of its pipe's sub-blocks, which follow the CPU's power
word, and its wake writes back those that were on and now read off.
The loop always covered eleven words. That is the internal pipe's
count (DISP_CPU +0x8 to +0x58). An external pipe has seven (DISPEXT0
+0x358 to +0x388, DISPEXT1 +0x4a8 to +0x4d8), as the sleep
controller's apple,check-ps-b4-sleep lists them, and the test meant to
keep the external processors out of the loop read a flag that is only
set for the T6030 external processor.

So the HDMI processor (DISPEXT0) also saved, and on a wake could
write, ATC0_COMMON, ATC0_CIO, ATC0_PCIE and ATC0_CIO_PCIE at +0x390 to
+0x3a8: the power domains of the first Type-C port, which belong to
the PMGR power-state driver and the devices behind them. With one of
them on at the stop and not yet on again when the display processor
resumes, the display driver raises it behind genpd's back. The USB-C
processor (DISPEXT1) did the same to the SYS, FE and CPU power words
of DISPEXT2 at +0x4e0 to +0x4f0, the power of another display
coprocessor on a board that uses it.

The same flag chose the GAPF slice whose clock protection is cleared
around a write of the CPU power word: every coprocessor cleared the
slice of the internal one's DART. The release of all protected slices
that follows hid that.

Give each pipe its number of sub-blocks and its DART's slice, next to
its power word offsets, and use them for the save, the restore and
the slice.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A display coprocessor whose firmware restart failed is given up on:
its messages are dropped from then on and it drives no display until
the reboot. The stop of the next T6040 suspend-to-RAM powers its CPU
off like any other, and the wake then ran the warm restart on it
again. If that handshake fails, the same error is logged at every
wake. If it succeeds, a firmware runs that nobody listens to: the
pause, resume and power calls to its endpoints time out one after the
other, and an external processor is then reported back, with the HDMI
converter, the DP PHY and the link set up for it.

Raise its power words at the wake, so that the stop of the next sleep
can reach the coprocessor's registers again, and leave its CPU
stopped.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The stop of a T6040 suspend-to-RAM disables the link work of the
internal display processor (Type-C reconnect and relink, HDMI settle,
link recovery, Thunderbolt events), so that none of it reaches a
firmware about to idle. Nothing enabled it again: the wake's restart
of the internal firmware does not, and a stop that fails gets no
resume at all. Each sleep disabled the work items once more, and from
the first sleep of a boot on they never ran again. The internal pipe
of the J616s has no external link, so nothing showed.

Stop it once, under the flag the external processors already use for
theirs, and enable it when the warm restart has the firmware back. A
stop that fails or a firmware that does not return leaves it stopped,
and the stop of the next sleep finds it so.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The internal display processor refused every later sleep of the boot
once its firmware had crashed (-EBUSY from the stop). One whose
restart had failed at an earlier wake ran the whole stop sequence
against a firmware it no longer talks to, timed out in it and was
refused the same way from then on. Only the external processors had a
way through: their CPU powered off, nothing else.

Take the internal one the same way when its pipe is down. The power
word of its CPU must not go down over sub-blocks that are on, and only
a firmware takes a lit pipe down, so a dead firmware that left its
pipe powered still refuses the sleep, now with a message that says so.

A crashed internal firmware is given up on at that stop and the wake
leaves it stopped: started again, it could power the pipe and then
fail, and the next sleep would be refused for good. The panel stays
off until the reboot, as before, and the machine goes on sleeping and
waking.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
pmc-dcp holds the number of powered display coprocessors. The wake
from a T6040 suspend-to-RAM counts each one in when its domains are
up and writes the count, but the stop never counted it out. After the
first sleep of a boot all three stayed counted: the first wake wrote
1, 2, 3 as the processors returned, every later wake wrote 3 three
times, the first two of them with one and two processors powered.

Count a processor out when the power word of its CPU has gone down.
Nothing is written then: the sleep controller zeroes the voter before
the sleep, as it does on T8140, whose stop forgets the processor the
same way. Every wake now writes what the first one did.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The warm restart after a T6040 suspend-to-RAM released the RTKit
receive side in case it was still held for a hibernation image and,
for an external processor, in case it was held for a sleep that keeps
the firmware running. Neither can be held there.

The hold for an image is taken at freeze and power-off only. It is
dropped by the thaw, by the restore that follows a failed power-off,
or by the cold start in the restored kernel, which the suspend
notifier flushes before any later sleep. The other hold is taken by
dcp_hold_for_sleep() for the H17P, 27.x and T8132 firmware only, not
for the desktop 26.x firmware these processors run.

No functional change.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The stop of a T6040 suspend-to-RAM began with the backlight suspend
of the 14.7 firmware and the drain of pending brightness presents.
Only the desktop 26.x firmware reaches this function. The first acts
on a 14.7 panel only. The second returns at once for a processor that
was not adopted running or whose firmware does not present the level
with its frames, and both hold for the two T6041 entries.

No functional change.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The T6040 suspend-to-RAM code asked the device tree twice for the
compatible to learn whether a processor takes part, and told the two
external processors apart by the physical base of their coprocessor
registers, with the power word offsets, the number of sub-blocks and
the GAPF slice of each in a switch.

Put them in a table of the T6041 hw data, one entry per pipe, and
take a processor's entry by its display endpoint (apple,dcp-endpoint:
0 the internal pipe, 1 DISPEXT0, 2 DISPEXT1), which the binding
already gives every external processor. A table in the hw data is
what says a processor takes part. An external processor without the
property, or with an endpoint the table does not have, gets no entry
and is left alone as one with an unknown base was.

The three processors of the J616s get the offsets, counts and slices
they had. No functional change.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
Every stop and every wake of a T6040 suspend-to-RAM looked up the
apple,display-power node and mapped and unmapped its "ps" and
"pmc-dcp" windows, the GAPF bank and, separately, the one slice of it
that belongs to the coprocessor's DART: four mappings per processor
and cycle, in callbacks that have no use for a failed mapping. The
stop also mapped "pmc-dcp" only to learn that the wake would find it.

Look the pipe's power words up and map the three ranges once at probe,
with the mapping types they had, and release them with the device.
A processor without power words or without either window keeps
nothing, and its stop and wake leave the power words alone as before.
The slice is reached through the bank.

The register accesses of the stop and the wake and their order stay
as they were. No functional change.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
Every stop and every wake of a T6040 suspend-to-RAM warned once per
display coprocessor that an AFK endpoint was "not paused" or "not
resumed" with -95: endpoint 0x25 on the external processors, 0x2c on
the internal one. The firmware advertises these endpoints and has
sent no compact message on them. The pause and resume requests are
compact packets and are not sent to such an endpoint at all: the
error comes from the host side, before anything reaches the firmware.
That made six warnings per sleep for endpoints left exactly as they
were.

Count these endpoints in the debug summary and keep the warning for
requests that were sent and went unanswered.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The wake reads the lid's state for the display after it has told the
SMC that the wake was taken, at every wake. The sleep order test still
counted the commands of a wake without that read and has failed on its
count since. Expect the read where it comes: after 'waka', before the
key that ends the wake.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A T6040 dry run of level 2 took the sleep up to the commit point with
the steps counted as "can still be taken back", the SMC's sleep commit
among them: the WKTP key, MBSE = slpa and the AP idle notice. It then
refused, and the resume sent the SMC what a wake sends. That does not
bring the SMC back. On a J616s every sensor the SMC reports stood still
after such a run, the CPU die temperature at one value under load,
until the next suspend that really lost power had woken; then the
reports were live at once. A suspend to idle did not change it either
way.

So the commit belongs with the stops that only a power cycle undoes,
which a dry run leaves out. Send it only when the suspend goes on to
the power loss. A dry run of either level then warns the SMC and takes
the warning back, as level 1 always did. apple_sleep_last_steps() had
this one user and goes.

A real sleep sends what it sent, in the same place. What a real sleep
can still meet after the commit, a refusal at the commit point or a
wake event before it, is not changed by this.

The KUnit case runs a sleep that ends short of the power loss.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The SMC gets its sleep commit in the noirq suspend, as the last of the
coprocessors and ahead of the CPUs' parking. What can still refuse the
sleep comes after that: a later device, a wake event, the checks at
the commit point. The resume then sends the notices of a wake, but an
SMC that took the commit and saw no power loss stays in its sleep
state until a sleep has completed: on a J616s its sensor reports stand
still for that long.

Nothing known to the driver takes the commit back, and sending it later
would change the order a sleep that works depends on. Until one of the
two is settled, say so in the log when it happens, so that reports
which no longer move have their cause next to them.

The KUnit case runs the commit, the refusal and the end of the sleep,
and expects what a wake sends, once.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The driver wrote MBSE = slpw at .prepare and, when the suspend was
called off before the commit, MBSE = waka at .complete. The SMC does
not take the warning back. On a J616s, after a suspend that stopped
before the commit (the SMC got slpw and then waka, about a second
apart), the SMC no longer ran the fans: both stood at 0 rpm with the
CPU die at 100 to 113 C under load, and a fan request was refused,
until the reboot. Before that suspend, in the same boot, the SMC had
started the fans by itself and had taken a request. The sensor reports
stayed live, so this is not the standstill a refused commit leaves.

A device that refuses and a wake event during the device suspend are
ordinary ends of a suspend, and each
left the machine without its fan policy.

Send the warning in the noirq suspend, right before the commit and
only when the suspend goes on to the power loss, which is the path a
sleep that cuts the debug link already takes: slpw, 200 ms of polling,
then the WKTP key, slpa and the AP idle notice. A suspend that ends
before that sends the SMC nothing. What can refuse after the commit
still leaves the SMC in its sleep state; the message for that case
stays.

The KUnit case for a suspend that stops short of the power loss now
expects no command at all.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
apple_rtkit_ap_power_noirq() polls the mailbox, but it waits with
usleep_range() and jiffies and takes the acknowledgment from the
receive work. None of that is to be had at a sleep controller's commit,
where interrupts are off, one CPU is up and timekeeping is suspended.

apple_rtkit_sleep_idle_polled() already sends the AP and IOP idle
notices there for a coprocessor that is then stopped: it polls a bounded
number of times with udelay() and takes the acknowledgments in the
poll. Add apple_rtkit_ap_idle_polled() for a coprocessor that stays up
and is only told that the AP goes idle, as the SMC is. It shares the
start of the polled idle (what is waiting is taken first, an owed OSLOG
counter is echoed) and sends the AP notice alone.

Such a coprocessor's client still has traffic on its own endpoints: a
reply to a command it sent just before, or an event. While the notice
is polled for, a message for an application endpoint therefore goes to
recv_message_early as usual, and one that is not taken there is queued
for the receive work, which runs again after the wake. Everything else
is handled as in the polled idle.

Compiled only: its first user is the SMC's sleep commit in the next
patch, which has not run on hardware yet.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A T6041 SMC takes neither MBSE = slpw nor the commit after it back, and
only a completed sleep or a reboot brings it out again. The driver sends
both in its noirq suspend. A sleep can still be refused after that: a
wake event that the PM core finds pending after the noirq phase, a CPU
that does not park, the sleep controller's checks at its commit, the
enclave before it sends its sleep request. The SMC then has slpw, WKTP,
slpa and the AP idle notice and no power loss, and its driver can only
say so ("sleep refused after the SMC's commit").

The T6040 sleep controller stops coprocessors at its commit: with
interrupts off, one CPU up and every refusal behind it, the enclave's
sleep request being the last of them. Register the SMC there, among the
stops that run first, and send the whole sequence from that stop: slpw,
200 ms in which the mailbox is polled, WKTP = 2, slpa, 6 ms, the AP
idle notice and 30 ms, as the noirq suspend sends them. The noirq
suspend then sends nothing, so a sleep refused anywhere before the last
stops leaves the SMC as it was, and so does a dry run, which never
reaches them. The wake is unchanged: it follows ap_notified, which the
last stop sets.

Against the noirq suspend the sequence now comes after the enclave's
sleep request instead of before it, because that request can still be
refused. It stays ahead of the other coprocessors' stops: those
registered as first run in the order of the registry's table, where the
SMC takes its place at probe and the only other one, the GPU, takes a
free place for each sleep. That place is a later one unless a driver
that registered before the SMC has been unbound since.

Nothing can sleep at the last stop and the RTKit worker does not run,
so the keys go through apple_smc_write_atomic() and the notice through
apple_rtkit_ap_idle_polled(). Atomic mode was only ever entered for a
shutdown; the last stop enters it and leaves it again, giving up an
answer that is still missing, so that the wake's commands are the usual
ones. apple_smc_write_atomic() timed its poll with ktime_get(), which
warns while timekeeping is suspended, as it is here: it reads the fast
monotonic clock instead. An event the SMC sends during the sequence is
taken from its outbox and reaches its handler after the wake.

A last stop cannot refuse, and the sleep goes on whatever it returns.
If the SMC fails a step, the step is logged and the rest is sent all
the same: an SMC that took the warning leaves its sleep state only
through the rest of the sequence and the power loss. The wake then
sends its part as after any sleep. With an SMC that answers nothing,
each write waits out its 5 s timeout with interrupts off.

Only the T6041 SMC registers. Every other SoC, a kernel that does not
run the last stops (below EL2, or without the sleep controller) and a
full registry keep the noirq suspend exactly as it is; T8140's order,
with the debug link's cut before the warning, was measured there. The
cut and its outbox poll stay in the noirq suspend for both paths: the
cut sleeps, and it has to precede the warning. The T6040 controller
has no cut, so that call does nothing there.

The existing KUnit cases describe the noirq path and pass unchanged.
New cases cover the last stop: nothing is sent in the noirq suspend
and the whole sequence comes from the stop, once; a refusal before the
stop sends nothing at the suspend and nothing at the resume; the wake's
sequence follows a stop, also one in which every step failed.

J616s, one boot: a dry run and a sleep refused before the enclave's
request leave the SMC without a single message, with its sensor
reports live and the fans under its control; four sleeps, one of them
right after the refusal and one after a hibernation, go through the
last stop (no message from the SMC in the 200 ms) and wake with the
usual record. A wake event during the entry was not tried.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A suspend gives up the link of an external display, and the resume
commit of its CRTC can come before the sink has answered again. The
firmware then fails the mode set ("setmode failed", "Modeset done, but
pipe not enabled"), which the driver logged as

  [drm] *ERROR* [CRTC:65:crtc-1] mode set failed: -5

On a J616s with an HDMI display this came with every suspend to idle,
2 s after the wake, and now and then after a suspend to RAM. The
display was back 5 s later, when the firmware reported the sink and
the hotplug brought a mode set that worked.

Return the quiet -EPIPE of a pipe without a link when the connector of
an external pipe is reported disconnected. The hotplug that reports
the sink back sets the mode, as it did after the failed one.

J616s, HDMI and USB-C DP displays: two suspends to idle and two to RAM
without the error, both displays back after each.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The glue device has a suspend callback and no resume callback, and
was not marked asynchronous. Its xHCI child is, but an asynchronous
device starts to resume only once the PM core has reached it in the
list, and the core gets past the glue device only in list order. On a
J616s wake from suspend-to-RAM the main PM thread waited 522 ms in a
synchronous device ahead of the glue (an rfkill device behind the
Wi-Fi function's restart), and both USB hosts with a dock's hub tree
below them started only then. On the way in the two glue devices'
suspends ran one after the other, 120 and 68 ms.

Mark the glue asynchronous. What it depends on is given by its parent
and its device links (PHY, DARTs, power domains), which the PM core
keeps for asynchronous devices too.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
After a suspend-to-RAM the ports of the T6040 controller come back one
after the other in the noirq resume, and each takes two waits of
100 ms: after the release of PERST# and after its link has trained.
With two ports (Wi-Fi/Bluetooth and the card reader on a J616s) that
is 414 ms of the 423 ms the whole noirq phase takes.

Bring every port up to the release of PERST# first, wait once, then
restore and train each, and wait once more. Each port still gets at
least the same two waits; the phase loses about 200 ms.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The second 100 ms of a port's wake from suspend-to-RAM follows the
link training. PCIe r5.0, 6.6.1 asks for it on a link that runs above
5 GT/s; up to 5 GT/s the 100 ms since PERST# are the whole
requirement, and the ports have waited those. Both ports of a J616s
have a target speed of 5 GT/s.

Take the wait only when a trained port's target speed is above
5 GT/s.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
…d the resume

A function that lost its power in a firmware suspend is found cold at
the resume (interrupt mask 0), and the resume callback removes and
probes it. On a J616s that takes 619 ms of the callback, about 550 ms
of it for taking the interfaces and the wiphy down. Every device that
follows in the resume order waits for it, and without USB devices it
is what the whole resume phase ends with.

Queue the remove and probe instead and return, on a freezable
workqueue, so that the work starts once the resume of every device is
over: run during it, the removal of the wiphy only moved the wait to
the wiphy's own resume (522 ms there). The work takes the device lock,
which an unbind holds too, and does nothing for a function that has no
driver data any more. A PM notifier flushes the queue before the next
suspend or hibernation, so that one never meets a function in the
middle of its restart. User space sees what it saw before, the wiphy
going away and a new one coming, a little later: after it is thawed.

Hibernation and a suspend to idle keep the restart in the callback.

J616s, suspend-to-RAM with the function associated, no USB hub
attached: the callback returns after 24 us, the resume of the devices
takes 453 ms, against 542 with the restart running during it, and
the interface associates again. With the restart in the callback the
callback alone took 619 ms.

The host test of the PM callbacks gets the two new calls and a case
for the queued restart and for the one that cannot be queued.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The T6041 driver idled the SIO firmware in the late phase of a suspend to
RAM. An idled firmware does not come back with its power kept, so a sleep
refused after that phase left it stopped until the next sleep that went
through: on a J616s a refusal of the enclave's sleep request logged
"sleep refused: firmware not back after a reset (-19), stopped until a
real sleep", and the DMA service and the AES engine behind it were gone.

Register the stop for the sleep's last step, as the T8140 does: the AP
and IOP power idle with interrupts off, after the last refusal. A sleep
refused before that finds the firmware running.

A dry run of the T6040 sleep stops no coprocessor, and counted a
registered one that was not ready as a check that a real commit would
fail. Report the firmware ready in a dry run as well.

On a J616s: after a sleep refused at the enclave's request and after a
dry run the engine still encrypts and decrypts 16 MiB in each of CBC, CTR
and ECB equal to the software implementation, the dry run reports no
failed check, and suspends to RAM and to idle after them complete with
the firmware back.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
@fbalazsy
fbalazsy requested a review from RyanTheTide November 13, 2026 12:39
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