The SoC can never reach VDD-min because mainline msm8998 has no RPM sleep-set handshake, not because something is voting against it
scope: soc:msm8998 · severity: finding · confidence: proven · subsystem: pm
The question — qcom_stats has read vmin Count: 0 and vlow Count: 0
since the port began. Who is blocking VDD-min?
The answer — nobody. There is no handshake to block. Mainline msm8998 has no code that tells the RPM to swap to its sleep set, and no system-level SPM to carry the hardware signal that would do it implicitly.
The vendor’s lpm-levels does three things before PSCI, for any level marked
qcom,notify-rpm – which on msm8998 is exactly one level, system-pc:
msm_spm_config_low_power_mode(ops->spm, mode, notify_rpm); /* arm the SAW handshake */msm_rpm_enter_sleep(0, cpumask); /* swap RPM to the SLEEP set */msm_mpm_enter_sleep(us, from_idle, cpumask); /* MPM wake pins + timer */Mainline has none of the first two. spm.c contains zero occurrences of
notify, and nothing anywhere in drivers/soc/qcom,
drivers/interconnect/qcom or clk-smd-rpm.c mentions an RPM sleep-set
transition. qcom_spm binds only the two L2 SAWs (17812000, 17912000);
the vendor’s system cluster drives a CCI SPM that has no node in
msm8998.dtsi at all. qcom_mpm is present and bound, so of the three, only
the MPM exists.
The RPM has the sleep-set values – icc-rpm sends RPM_SLEEP_TAG requests
– it is never told to use them.
Two things this kills, so nobody spends a session on them.
vlow Client Votesis not a lead. It read0x83818381before one suspend and0x81838183after – the same bytes rotated by one. Decoding it would name nothing, because the mode is never attempted.- The big interconnect votes are active-only.
mas_oxili(GPU, 3 296 000) and1008000.pmu(the CPU/DDR bwmon, 1 144 000) look alarming ininterconnect_summaryat idle and are both tag 1 = RPM_ACTIVE_TAG, which never enters the sleep set. Read the tag column before chasing a number. The only tag-3 votes on this device are USB (dropped insidedwc3_qcom_suspend()) and venus (pm_runtime_forbid(), so it never drops) – and zeroing venus by unbinding it changed nothing.
A retraction of my own retraction, same evening. This note briefly said
system-pc was proven unsafe, because a third cycle went dark for 45 minutes
and needed a power-key press. That attribution was wrong and is withdrawn: the
harness never ran that cycle. The suspend log held no TRY line for that boot,
state3/disable read 1 and state3/s2idle/usage read 0, so system-pc was
never entered. The journal shows what actually happened – Reached target Sleep at 22:22:59 and PM: suspend entry (s2idle) at 22:23:00, a 29-minute
gap, then PM: suspend exit at 22:51:47. The phone idle-suspended by
itself while nobody was driving it, and stayed down because nothing had
scheduled a wake.
So the standing evidence is two clean system-pc cycles and no third data
point. That is unproven, not disproven – and two cycles is still not a
result on a device where a booted phone and a hung one look identical. Arm
tools/ph-afk.sh before any unattended run, or the phone will sleep under the
experiment and the null will be about nothing.
What did change: system-pc reached a state the earlier kernel could not. The 6.18-era result that
cpuidle retention and power-collapse hang s2idle
(docs/campaign/ws-05-suspend.md) does not hold on 7.2. States 1 and 2 are
enabled by default with millions of uses and 11/11 suspends are clean, and
writing 0 to every cpuidle/state3/disable gave two clean suspends that
resumed on the RTC with boot_id unchanged. A third attempt never ran; see
above. The 10-taimen-cpuidle hook that
was written to disable states 1-2 across suspend is not installed – the
device APKBUILD still carries its comment with no install line under it –
and on 7.2 it is not needed.
But do not enable system-pc as a shipped default. With it enabled,
state3/s2idle/usage moved 357 467 -> 772 698 across a single 30 s suspend:
about 14 000 entries per second. PSCI is returning from it almost immediately,
because nothing armed the handshake it exists to trigger. It buys no power and
spins the CPU.
Measured 2026-09-19, late: what system-pc actually does. Three clean
cycles now, all returning on the RTC. With it enabled on all 8 CPUs, against a
control that is the same 31 s suspend with it disabled:
arch_timer IRQs |
system-pc entries |
|
|---|---|---|
control, system-pc off |
1 742 | 0 |
system-pc on |
140 299 | 386 598 |
CONFIG_HZ=1000, so the control’s 1 742 ticks over 31 s across 8 CPUs (~56/s)
is a properly tickless idle. Enabling system-pc multiplies the local-timer
rate 80x. state3/s2idle/time is 9 344 590 us over 386 598 entries –
24 us per entry. That is the round trip of an SMC that does nothing:
the firmware accepts the call and returns immediately without collapsing.
The tick storm is the symptom, not the cause: each enter/exit pair re-arms
the tick that local-timer-stop had just handed to the broadcast device. But
it is self-reinforcing, and it matters for PC-mode firmware, where the system
only collapses when the last CPU enters at affinity 2. Eight CPUs bouncing
on their own ticks never satisfy that condition.
state3/rejected is 4 724 against 386 598 entries, so outright rejection is
not the main path – the calls are being accepted and doing nothing.
CORRECTED 2026-09-20, by reading the code instead of the counters. This note previously said glink-rpm’s ~237 interrupts per suspend were wakes and that masking the RPM channel was the next port. Both were wrong, and the patch built on them (0248) has been reverted.
Tracing events/irq/irq_handler_entry together with
events/power/suspend_resume and counting only what falls between
machine_suspend begin and end – the window where the trace clock is frozen,
so anything firing during the sleep lands inside it – gives 8 IPIs and 2
arch_timer, and zero glink-rpm. All 247 glink-rpm interrupts are outside it:
RPM voting traffic in the suspend and resume device phases, which is what the
channel exists for. The measurement after the patch agreed that it changed
nothing: 237 -> 358, noise. s2idle on this device already sleeps cleanly.
There are essentially no spurious wakes to remove.
And the OSI story is not what it looked like. psci_dt_cpu_init_topology()
only installs psci_enter_s2idle_domain_idle_state under
psci_has_osi_support(), and our CPU nodes carry power-domain-names = "cprh"
so dt_idle_attach_cpu(cpu, "psci") returns NULL and that path bails. The
reason system-pc still runs under s2idle is generic, in
drivers/cpuidle/dt_idle_states.c:38:
/* ... So enter() can be also enter_s2idle() callback. */idle_state->enter_s2idle = match_id->data;Every DT idle state gets enter_s2idle set to the same callback as enter.
So system-pc is entered through plain psci_enter_idle_state(), which hands
0x42000343 to the firmware via CPU_PM_CPU_IDLE_ENTER_PARAM_RCU with no
domain coordination at all. There is no genpd, no OSI, and none is needed
for the call to happen.
Which puts the limit in the firmware. The boot log is explicit:
psci: OSI mode supported.psci: [Firmware Bug]: failed to set PC mode: -3The TZ advertises OS_INITIATED in its CPU_SUSPEND feature bits and then
rejects SET_SUSPEND_MODE in both directions. In Platform-Coordinated mode
the firmware, not Linux, decides the cluster and system state from what the
cores request, and it is free to clamp an affinity-2 request down to a CPU
collapse. That is what the numbers say it does: 386 598 entries, 4 724
rejected outright, the rest returning success in an average of 24 us.
So there is no missing AP-side call to port. msm_rpm_enter_sleep()’s
remaining half is a channel mask that measurably does nothing here, and the
MPM handover already runs. What is left is a firmware that will not perform a
system collapse, on a SoC whose PSCI implementation is already known buggy.
What parity actually needs, in dependency order: a CCI/system SAW node and
spm.c support for it including the notify-RPM bit; something to issue the RPM
sleep-set swap at system-pc entry; and the MPM wake programming
(msm_mpm_enter_sleep’s timed-wake path) hooked to that entry. That is an
unimplemented subsystem, not a tweak – and it is the whole of the BP-13/BP-14
prize.
Trap collected on the way: venus cannot be rebound after unbind. The
probe fails -110 with a WARN and only a reboot brings it back.
