Skip to content

macsmc: Leave SMC policy to firmware and expose J616s/Neo thermal pressure - #670

Merged
RyanTheTide merged 6 commits into
aurora-wipfrom
fu-smc-write-hygiene
Oct 31, 2026
Merged

RyanTheTide merged 6 commits into
aurora-wipfrom
fu-smc-write-hygiene

Conversation

@RyanTheTide

@RyanTheTide RyanTheTide commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Linux should leave the SMC's fan and charging policy alone until userspace asks to change it. This series removes automatic fan cooling requests and battery-policy resets at probe, avoids the deliberately refused manual-mode write, and exposes mTPL through the existing DT-selected, read-only thermal_pressure_level attribute on J616s and J700 (Neo).

For J616s, this PR supersedes the kernel-driven fan-ladder approach of #579. Its measured macOS 26.6.2 curve (fans off below about 100 C TCMz, then starting at minimum) describes the observed response; it does not establish that the host must implement that curve. The SMC starts in firmware mode 3, the measured macOS runtime windows contained zero host key writes, and Linux's refused manual-mode write correlated with an increased FOFC count. The policy here therefore leaves automatic fan control with firmware and permits host writes only for explicit user targets and their release. Firmware may itself change mode under load.

#579 is an independent outside-contributor PR against integration/aurora. This PR neither depends on nor modifies it, and no comment was posted there. t-3618 (GPU SMC temperature production/validity) remains a separate task; no GPU temperature producer, conversion, validity filter or power-domain fix is included here.

The macOS evidence has a defined scope: an AppleSMC write-path trace observed zero key writes during 30 seconds idle and 150 seconds across a load burst. A separate sustained-load observation showed the fans ramping without evidence of a missing Linux host handshake. Boot, suspend and shutdown were not traced. This supports removing the extra host policy; it does not establish that macOS never writes any SMC key throughout its lifecycle. FOFC increasing on a refused manual-mode write supports the counter interpretation, but its full semantics and reset conditions remain unproven.

Changes:

  • Remove the fan thermal cooling devices and their asynchronous work. A nonzero fanX_target is now the only entry to host manual fan control. Suspend, shutdown, removal, an explicit zero, and error recovery still release a fan previously taken over by a user request. Resume does not replay it.
  • Read a fan's mode before taking control. For mode 3, write Ftst=1 first and poll using reads until system mode ends, then write mode 1. Without an unlock key, return -EOPNOTSUPP without a known-refused write. Legacy mode 0 firmware keeps its direct manual/auto path. A write-only legacy mode key retains explicit direct control without acquiring a new read prerequisite; this metadata case has its own KUnit test.
  • Remove the J616s active fan trips/maps from the compiled DT through a final policy section, and remove #cooling-cells. Keep the original ladder definitions and call sites intact as source input so independent ladder edits apply without conflict; the final section removes the resulting fan nodes, including an optional fan map on the passive trip. Every passive and critical trip, CPU cooling map, temperature channel and DT manual-control permission stays. The former binding property is deprecated so older DTs still validate. With an old DT, the new driver also cannot register autonomous fan cooling devices.
  • Preserve charging inhibitors and CHLS policy at probe. The prior stale force-discharge repair becomes a read-only warning; explicit charge_behaviour=auto still clears it while preserving the limit byte. Explicit charge behaviour and threshold controls remain serialized and available.
  • Select apple,thermal-pressure-key = "mTPL" on both boards. Both key inventories contain it; the existing driver validates readable four-byte si32 data and hides the attribute if validation/read fails. It is an integer level, not a die temperature or percentage. J616s retains TCMz as its CPU die channel; no temperature channel is replaced or renumbered. hwmon: macsmc: Move SoC die and pressure key data into DT #642 had selected no pressure key for either board.

The existing documented fan_control compatibility override remains; there is no new module parameter. No machine-local override or private capture is included.

SMC write audit

Audited against aurora-wip base 8348f8244b208b5185b0764ebb21183a678e9e86: core, hwmon, power supply, reboot, RTC, input, and GPIO, including combined read/write requests. “Untraced” below means the macOS runtime trace cannot answer that lifecycle case; it does not mean macOS never does it.

Path / key and value Trigger before this series macOS evidence Result
Core NTAP=1 SMC probe enables event notifications Boot untraced Keep required notification setup, including power/lid/battery safety events. No periodic write.
Core NTAP=0 devm notification cleanup; enter atomic mode before restart/poweroff Teardown untraced Keep: prevents notification callbacks during atomic shutdown.
Power CHTE=0, else CH0C=0 Battery probe resets charging inhibitor No writes in measured runtime windows; boot untraced Remove unconditional reset. The same controls remain explicit through charge behaviour.
Power CH0I=0 Battery probe resets discharge inhibitor Same Remove probe reset; retain explicit auto.
Power CH0K=0 Every battery probe No established requirement Remove; no new writer.
Power CH0B=0 Every battery probe No established requirement Remove; no new writer.
Power CHLS &= ~BIT(8) Probe finds force-discharge without a valid limit No observed counterpart Warn without writing. Explicit auto still clears it.
Power CH0I=0/1 Explicit auto/auto-discharge or force-discharge User policy outside trace Keep explicit setter.
Power CHTE=0/1, else CH0C=0/1 Explicit auto/auto-discharge or inhibit-charge User policy outside trace Keep explicit setter; stop on errors.
Power CHLS (limit byte / discharge bit) Explicit end-threshold or auto/auto-discharge User policy outside trace Keep explicit setter and no-limit guard.
Power CHWA flag Explicit end-threshold on CHWA firmware User policy outside trace Keep explicit setter. Start-threshold setter writes no key.
Hwmon F0Md/F1Md=1, F0Tg/F1Tg=clamped rpm, Ftst=1 if locked User target or thermal cooling work, including resume replay No host fan writes in measured windows Delete thermal entry and replay. Keep only explicit nonzero target entry; unlock before mode write instead of provoking refusal.
Hwmon F0Md/F1Md=0, Ftst=0 after the last manual fan Zero target, target-write error, unlock error, suspend, shutdown, removal Manual override outside trace Keep rollback of a user request. Untouched fans cause no writes; no probe reset.
GPIO gP00..gP3f / gp00..gp3f, CMD_OUTPUT | level GPIO consumer output change; direction_output uses the same setter GPIO lifecycle not established by runtime trace Keep the device-control service. Do not infer that radio/reset writes are redundant fan policy.
GPIO pcIO eight-byte service payload Neo trackpad AFE reset or radio REG_ON consumer action Qualified platform-function semantics already implemented Keep functional reset/power control. J713's special gP13 template also stays, outside these boards.
GPIO gPxx/gpxx combined requests: CMD_PINMODE, fallback CMD_IRQ_MODE, CMD_INPUT/OUTPUT GPIO direction/value query Queries, despite their outgoing command word Keep combined read protocol; not autonomous policy writes.
Reboot MBSE=rest / offw Restart/poweroff notifier, including safety-triggered shutdown Shutdown untraced Keep shutdown/restart preparation.
Reboot atomic MBSE=phra / off1 Final restart/poweroff Shutdown untraced Keep required restart/poweroff operation.
RTC / input Probe, runtime reads, PM prepare/complete No direct SMC key writes in code No removal needed. RTC reads CLKM; setting time writes an NVMEM offset, not an SMC key. Input safety shutdown delegates to reboot.
Reboot NVMEM Probe writes boot stage and clears logged error counts; shutdown writes stage/flag Outside the key-write trace These are NVMEM operations, not SMC key writes; unchanged.
RTKit / SMC transport Initialization, mailbox messages and SRAM request payloads Outside this key-write comparison Transport writes remain necessary to implement reads, notifications and explicit operations. No guessed thermal handshake added.

There are no other direct SMC key writers in these drivers. Runtime battery/debug polling, input events and thermal pressure reads do not write keys. The scalar helpers only marshal the callers listed above. FOFC and mTPL are never written.

History reviewed before replacing the deliberate behavior:

  • 33e0e46b6082: unlock was added because mode 3 refused manual writes; keep the qualified unlock/release, remove the initial deliberate refusal.
  • 93c3026d2e0e and 478fc7de86c9: host fan cooling assumed slow/off fans meant Linux had to request cooling. The firmware-policy observations supersede that premise. arm64: dts: apple: t6040-j616s: Run the fans on the CPU die like macOS #579 tunes the host ladder removed here; this series supersedes that approach, while retaining the passive/critical backstops.
  • 0ebf821cf6c7: charging resets originated in the initial driver; they clear inherited policy on bind. Explicit charge controls already provide the appropriate policy-changing interface.
  • ede99b3c1990: CHLS cleanup repaired a stale bit from older kernels. Preserve observation/warning and explicit recovery instead of silently changing it at probe.

Validation

  • All six commits pass strict checkpatch: 0 errors, 0 warnings, 0 checks. Diff whitespace check passes.
  • Source comparison preserves 469 J616s and 193 Neo board sensor blocks, 48/18 passive-or-critical trip blocks, and every existing CPU cooling map. Shared includes are unchanged.
  • Final-head controller build h-46234 passed on 3db457a10d5d2a5911732591570d707e06e264c9: W=1 -j8 SMC/core/hwmon/power objects including KUnit code (0 warnings), binding check (0 warnings), and 33/33 arm64 QEMU KUnit tests across five suites.
  • CHECK_DTBS=y passed for J616s/Neo/M1 Pro/M2 using DT_SCHEMA_FILES=hwmon/apple,smc-hwmon.yaml, with zero diagnostics from that schema. There are 57 DTC warnings; locally retained baseline h-46230 logs reproduce all 57 exactly after normalizing source line numbers, with zero new warnings.
  • Baseline tracking was interrupted by a controller restart. Its overall exit status remains unknown. Read-only inspection h-46246 confirmed the old remote run directory was absent; the original local collector logs, source proof and complete DTBs survived and were checked directly. No build was blindly replayed.
  • Compiled baseline/candidate comparison preserves all 480 J616s and 205 Neo sensor nodes, every passive/critical trip, and every CPU cooling map. Both candidates select mTPL. Only the 40 J616s fan cooling maps are removed, along with the active fan trips and cooling-device specifier; both DT manual fan permissions remain.
  • Initial build h-46216 also passed all then-existing 32 tests before the final write-only legacy-mode compatibility case. No target result is inferred from these models.
  • No target was modified or booted by this worker. Initial J616s requests encountered unregistered/fresh-identity guards; those failures were preserved. After the controller reported a verified fleet12 identity, approved shared read h-46317 obtained both fans at 0 rpm actual / 0 target, 1350 minimum / 5777 maximum. The running DT has no pressure-key property, hwmon has no thermal_pressure_level, and the SMC debugfs value reader is absent. FOFC, F0Md/F1Md and mTPL values were not obtainable through the existing interfaces. No module was loaded, no key written, no workload started, and fan mode is not inferred from zero tach. The candidate remains unbooted; the cold-boot/manual-target protocol below is still pending.

Landing order and separate scope

The aggregate DT diff leaves every location edited by the current #579 patch untouched. The effective policy cleanup lives at the end of the board DTS and deletes fan trip/map instances by node name, including map-fan6 on the passive trip if present. It keeps that passive trip and its CPU cooling maps. There is no dependency on #579 and no copy of its new ladder in this PR.

Both aggregate orders—#670 then #579, and #579 then #670—pass git apply --check, apply without a textual conflict, and produce byte-identical source for all eight changed files. The check is pinned to #579's current head recorded in the local evidence. Published commits were preserved with a follow-up rather than a force push: these checks cover the aggregate PR diff used for landing, not individual replay of the superseded intermediate DT-removal commit. A commit-by-commit rebase should fold that intermediate removal into the final DT change first.

Controller gate h-46275 passed on 52cbd7f4041605e6afe8b969410baaf767f626a8: standalone and combined J616s/J616s-tether DTBs compile with W=1 CHECK_DTBS=y, with no selected hwmon-schema diagnostics, and are byte-identical with or without the current #579 patch. Both aggregate landing orders produce identical source. Both DTBs retain mTPL, 480 sensor nodes and all 40 CPU cooling maps, with zero fan cooling maps. Driver and Neo source is byte-for-byte unchanged from the 33/33-tested head 3db457a10d5d2a5911732591570d707e06e264c9; all six commits pass strict checkpatch.

Lead verification

First verify fresh J616s reference identity attestation in the controller. Boot the baseline kernel + matching DTB through the approved cold-boot workflow with bootguard fallback; capture the following immediately after boot and again after 60 seconds. Repeat after an approved cold boot of this series with its matching DTB, on the same firmware. Save both outputs; compare starting FOFC and its delta, rather than assuming a warm reboot resets it. This worker did not execute any of these target commands.

Read-only command body for aurora-ctl hw request execute --device j616s --mode shared --task <task-id> --timeout 60s -- '<body>':

tr '\000' '\n' < /sys/firmware/devicetree/base/compatible | grep -Fx apple,j616s
uname -r
for h in /sys/class/hwmon/hwmon*; do
    [ -r "$h/name" ] || continue
    [ "$(cat "$h/name")" = macsmc_hwmon ] || continue
    for f in "$h"/fan*_input "$h"/fan*_target "$h"/fan*_min "$h"/fan*_max "$h"/thermal_pressure_level; do
        [ -r "$f" ] || continue
        printf '%s=' "${f##*/}"; cat "$f"
    done
done
# Existing research reader only, if already installed and known fresh.
# Do not load/unload a module or use a generic key writer in this read-only step.
if [ -r /sys/kernel/debug/smckeydump2/keys ]; then
    awk '$2 ~ /^(FOFC|F[01](Md|Ac|Tg|Mn|Mx)|Ftst|mTPL)$/ {print}' \
        /sys/kernel/debug/smckeydump2/keys
else
    echo 'FOFC and mode values unavailable: no existing value reader'
fi

Important measurement limit: stock in-tree macsmc-hwmon debugfs keys is M3-gated and prints metadata, plus T/P floats; it cannot supply FOFC or fan-mode values on these boards. The existing research reader must match the running kernel and must be confirmed to return a fresh snapshot; a cached dump is not an after-test measurement. Provisioning such a reader, if absent, is a separate lead-approved step. Do not decode FOFC by assuming native endianness: retain its raw bytes and compare using the same interpretation as the baseline. Mode 3 is the expected idle firmware state, but firmware can itself change mode under load; a non-3 mode alone does not prove a host write.

For Neo, use the same read-only hwmon loop with explicit controller device neo and identity apple,j700: expect a read-only thermal_pressure_level if runtime key validation succeeds, and no fans.

Only the lead, in a separately approved exclusive J616s request, should run the following manual test. It writes SMC keys through hwmon and is outside the worker's read-only hardware scope. Verify the candidate source identity, matching DTB and initial firmware ownership first. Record fresh FOFC/modes/Ftst immediately before and after it. Expected: no deliberately refused mode write, tach near the requested speed, and release to firmware. The SMC may ramp down slowly after release; do not require an immediate zero tach.

set -eu
tr '\000' '\n' < /sys/firmware/devicetree/base/compatible | grep -Fx apple,j616s
h=
for candidate in /sys/class/hwmon/hwmon*; do
    [ -r "$candidate/name" ] || continue
    if [ "$(cat "$candidate/name")" = macsmc_hwmon ]; then h=$candidate; break; fi
done
test -n "$h"
test "$(cat /sys/module/macsmc_hwmon/parameters/fan_control)" = N
test -w "$h/fan1_target" && test -w "$h/fan2_target"
cleanup() {
    rc=$?; trap - EXIT
    for fan in 1 2; do
        if printf '0\n' > "$h/fan${fan}_target"; then
            echo "ROLLBACK fan$fan: released"
        else
            echo "ROLLBACK fan$fan: FAILED" >&2; rc=1
        fi
    done
    exit "$rc"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
for fan in 1 2; do
    printf '3000\n' > "$h/fan${fan}_target"
    sleep 8
    cat "$h/fan${fan}_target" "$h/fan${fan}_input"
    printf '0\n' > "$h/fan${fan}_target"
    sleep 8
    cat "$h/fan${fan}_target" "$h/fan${fan}_input"
done

Rollback of manual control is an explicit zero to both targets; retain any failed-release output and use the approved recovery workflow if needed. Kernel/DT rollback uses the previous known-good pair through the controller with bootguard fallback. No persistent firmware policy, FOFC reset, or thermal-pressure write is part of this test.

Keep the former cooling-device specifier valid for older device trees,
but describe that fan policy stays with the SMC firmware. Manual target
writes remain available through hwmon.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
…trol

The host thermal ladder assumed the SMC needed a manual request to run
the fans. Sustained macOS measurements instead show firmware ramping the
fans while the host makes no SMC key writes. Remove the autonomous fan
cooling devices and resume work; retain explicit fanX_target control and
release on suspend, shutdown and removal.

Read the mode before requesting manual control. In system mode, unlock
first and wait using reads instead of provoking a refused mode write.
Firmware without an unlock key fails without issuing a known unsupported
write.

Exercise manual targets, zero refused writes, untouched lifecycle paths,
suspend release, resume without reapplication, and unlock failures with
the existing KUnit SMC model.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Probing a battery should not reset its persistent charge policy. Stop
resetting the charging inhibitors and undocumented CH0K/CH0B keys merely
because the driver bound.

The earlier CHLS repair cleared a force-discharge bit left without a
limit by old kernels. Warn about that state without changing it; an
explicit charge_behaviour=auto request still clears it while preserving
the limit byte. Keep the existing serialization and explicit charge
controls. Update the KUnit coverage to verify read-only observation and
explicit recovery.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Both firmware key inventories include the readable signed 32-bit mTPL
key. Select it through the existing thermal-pressure property so the
shared driver exposes its read-only level. On J616s macOS polls this key
once per second; it is distinct from the temperature keys.

Remove the J616s host active fan trips and maps. The SMC ramps the fans
under sustained load without host key writes, so the earlier assumption
behind the Linux fan ladder does not hold. Preserve every temperature
channel, passive CPU/GPU cooling map, critical trip, and explicit manual
fan control property.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The mode preflight should not introduce a read requirement for a legacy
write-only mode key. Use the metadata to keep its existing direct write
path, still solely under an explicit user target. Readable system-mode
keys keep the unlock-before-write sequence.

Cover this metadata case with the SMC model, asserting no mode read and
no unlock operation for the legacy write-only path.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Keep ladder definitions and call sites stable while removing their fan
trip/map instances in a final policy section. This makes independent CPU
ladder updates apply cleanly in either aggregate PR landing order without
restoring automatic fan writes. Drop the optional fan map on the passive
trip as well, preserving the trip and its CPU cooling maps.

The effective device tree retains firmware fan ownership, all existing
sensor channels, and passive/critical protection. No GPU temperature
producer, conversion or validity policy changes are included.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
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