Skip to content

zerofrip/Headwolf_F8_KPM_OC_Kernel

Repository files navigation

Headwolf F8 KPM OC Kernel Module

Kernel module (v11.6) for MediaTek MT8792 (Dimensity 8300 / MT6897) providing:

  • CPU OPP reader — exports CSRAM LUT data to userspace via live sysfs reads (re-reads CSRAM on every access)
  • CPU overclocking — patches CSRAM LUT[0] per cluster + updates cpufreq policy max
  • CPU per-LUT voltage override — direct CSRAM write for any LUT entry, bypassing stock constraints
  • MCUPM CSRAM countermeasure (v7.2) — kprobes on CPU DVFS transition functions resync OC voltages into CSRAM immediately before every frequency transition, preventing MCUPM firmware from silently reverting them
  • GPU overclocking — patches the GPU default + working OPP tables in kernel memory at runtime
  • GPU per-OPP voltage override — direct memory write for any OPP entry, bypassing vendor fix_custom_freq_volt validation
  • GPUEB OPP countermeasure (v7.2) — kprobe on the GPU DVFS commit function re-patches OPP voltages immediately before the commit reads them, preventing GPUEB firmware from reverting OC voltage
  • GPU PLL direct programming (v8.0) — direct MFG PLL CON1 register writes for above-stock GPU frequency; kretprobe on gpufreq_commit reprograms PLL after GPUEB commits
  • FHCTL / MCU PLL hopping (v8.0) — IPI-based PLL frequency control via MCUPM fhctl interface; captures mcupm_hopping_v1 calls for diagnostics
  • SCMI DVFS capture & override (v8.0) — kprobes on scmi_perf_level_set / scmi_dvfs_freq_set for diagnostics and optional performance level override
  • Suspend stability tracer (v11.3–v11.6) — opt-in suspend_trace_enabled module parameter installs KPMTRACE / KPMNOIRQ / KPMPHASE kretprobes on dpm_run_callback, __device_suspend_noirq, and the post-noirq pipeline (dpm_suspend_noirq, syscore_suspend, freeze_secondary_cpus, cpuidle_enter_s2idle, cpuidle_enter_state, psci_cpu_suspend_enter, psci_system_suspend_enter, cpu_suspend, pm_suspend_default_s2idle) so the last unmatched "enter" in console-ramoops-0 after a watchdog reboot pinpoints the hanging stage
  • Sleep-reboot recovery (paired with the APatch f8_sleep_reboot_guard wakelock) — dynamic tracing with this module identified the reboot as a probabilistic PSCI / ATF / MCUPM firmware hang during multi-cluster shutdown on the s2idle path; the kernel side cannot fully patch it, so the APatch module blocks s2idle entry with a permanent wakelock while cpuidle states 0–6 (up to system-bus) remain fully enabled for normal idle power savings

Used by the APatch module (v13.0) which provides: WebUI with 5 tabs (CPU/GPU/RAM/Storage/Profile), multi-language support (EN/JA), thermal mitigation, power profiles (Battery Save / Normal / Performance), auto gaming mode with foreground app detection and app icon display, per-section split config persistence (conf/*.json) with per-section Apply buttons, legacy config auto-migration, boot-time service with gaming monitor daemon, and the permanent sleep-reboot workaround (f8_sleep_reboot_guard wakelock).

Hardware Details

Item Value
SoC MediaTek MT8792 (Dimensity 8300)
Kernel 6.1 GKI (Generic Kernel Image)
CSRAM Base 0x11BC00 (size 0x1400)
DTB Node cpuhvfs@114400 — reg[1] is the CSRAM region
CPU Driver mtk-cpufreq-hw (performance-controller LUT)
GPU Driver mtk_gpufreq_mt6897 + mtk_gpufreq_wrapper + GPUEB
MFG PLL Base 0x13FA0000 (MFGSC: 0x13FA0C00)

CPU Clusters

Cluster Policy Cores Core Type Stock Max
LITTLE (L) 0 0–3 Cortex-A520 2200 MHz
big (B) 4 4–6 Cortex-A720 3200 MHz
PRIME (P) 7 7 Cortex-A720 3350 MHz

CSRAM Domain Layout

Domain Cluster Offset Contains
0 L (policy0) +0x10 Freq LUT (+0x00), EM Power Table (+0x90)
1 B (policy4) +0x130 Freq LUT (+0x00), EM Power Table (+0x90)
2 P (policy7) +0x250 Freq LUT (+0x00), EM Power Table (+0x90)

LUT Entry Format

Each LUT entry is a 4-byte word in descending frequency order (up to 32 entries per domain):

bits[11:0]   = Frequency (MHz)
bits[30:29]  = Gear selector (voltage domain switch — preserved on OC write)
Voltage (µV) = ((raw & 0x9FFFFFFF) >> 12) * 10

GPU OPP Entry Layout

Each GPU OPP entry is 24 bytes (stride in g_gpu_default_opp_table / working table):

u32 freq;     // KHz
u32 volt;     // 10µV steps (e.g. 115040 = 1150.4 mV)
u32 vsram;    // 10µV steps
u32 posdiv;
u32 vaging;
u32 power;

Total OPP count: 69 (SignedOPPNum), 65 working OPPs on this SoC.

MCUPM / GPUEB Countermeasure (v7.2)

Root Cause

The MT8792 has two firmware subsystems that independently write back into DVFS tables:

  • MCUPM (CPU Management Processor, base 0xc070000) — has direct CSRAM access and continuously recalculates LUT voltage fields during cpufreq transitions. Frequency bits [11:0] are preserved (OC freq holds), but voltage bits [28:12] are overwritten with MCUPM-computed values that do not account for frequencies above stock range.
  • GPUEB (GPU Execution Block) — overwrites g_shared_status / working_table voltage on every GPU DVFS commit. The AP-kernel g_gpu_default_opp_table is also reverted by a kernel-side sync from shared_status. Voltage reverts from the OC target (e.g. 105000) to stock (e.g. 40500) within milliseconds of each patch.

A 500 ms periodic relift cannot keep up with these firmware write-backs.

Solution: Event-Driven Resync via kprobes

Instead of polling, v7.2+ installs kprobes that fire on the exact functions called during DVFS transitions. All kprobes are registered at module init:

kprobe symbol Type Module Purpose
freq_qos_update_request kprobe core kernel Intercept vendor freq_qos MAX lowering calls; silently raise argument to OC target so vendor constraints cannot clamp the OC ceiling
update_userlimit_cpufreq_max kprobe platform driver Intercept powerhal / touch_boost calls that restore the stock CPU max; raise the argument to the OC target
scmi_cpufreq_fast_switch kprobe scmi_cpufreq Re-patch CSRAM LUT voltages before fast-switch path (schedutil hot path)
scmi_cpufreq_set_target kprobe scmi_cpufreq Re-patch CSRAM LUT voltages before target-index path (interactive governors)
__gpufreq_generic_commit_gpu kprobe mtk_gpufreq_mt6897 Re-patch GPU default + working OPP[0] before commit reads them
gpufreq_commit kretprobe mtk_gpufreq_mt6897 Reprogram MFG PLL CON1 after GPUEB commits above-stock OC freq
mcupm_hopping_v1 kprobe mtk_fhctl Capture MCU PLL DDS changes via IPI for diagnostics
scmi_perf_level_set kprobe scmi_perf Capture / override SCMI DVFS performance levels
scmi_dvfs_freq_set kprobe scmi_dvfs Capture SCMI DVFS frequency set requests

Note: This SoC uses the mtk-cpufreq-hw driver (performance-controller LUT), not the SCMI cpufreq driver. The scmi_cpufreq_* symbols referenced by these kprobes belong to the cpufreq shim layer used by MCUPM IPI; they fire on every cpufreq transition regardless of governor.

The pre-handler for each CPU DVFS kprobe calls cpu_csram_resync_fast(), which writes OC voltages to CSRAM for all clusters nanoseconds before the HW reads REG_FREQ_PERF_STATE. The GPU kprobe pre-handler re-patches default_table[0] and working_table[0] before __gpufreq_generic_commit_gpu dereferences them.

The 500 ms periodic relift worker is kept as a safety net for edge cases.

Verified Results

After loading with OC targets (L=3800 MHz, B=3800 MHz, P=3800 MHz, GPU=3000 MHz):

Metric Before v7.2 After v7.2
CPU CSRAM L LUT[0] volt 831 250 µV (MCUPM-overwritten) 1 050 000 µV (stable)
CPU CSRAM B LUT[0] volt ~1 006 250 µV (unstable) 1 100 000 µV (stable)
CPU CSRAM P LUT[0] volt ~1 087 500 µV (unstable) 1 150 000 µV (stable)
GPU working OPP[0] volt 40 500 (GPUEB-reverted) 105 000 (stable)

Sysfs Interface

All parameters are under /sys/module/kpm_oc/parameters/.

CPU — Read

Parameter Access Description
opp_table R Pipe-delimited: CPU:policy:freq_khz:volt_uv|... — live CSRAM read on every access
raw R Raw hex dump of LUT + EM entries per domain (debug) — live CSRAM read on every access
apply W Write 1 to trigger a rescan (legacy; reads are now live)

CPU OC — Write target, then apply

Parameter Access Description
cpu_oc_l_freq RW L cluster target freq in KHz (0 = skip)
cpu_oc_l_volt RW L cluster target voltage in µV (0 = keep original)
cpu_oc_b_freq RW B cluster target freq in KHz
cpu_oc_b_volt RW B cluster target voltage in µV
cpu_oc_p_freq RW P cluster target freq in KHz
cpu_oc_p_volt RW P cluster target voltage in µV
cpu_oc_apply W Write 1 to patch CSRAM LUT[0] and update cpufreq policy
cpu_oc_result R Result string of last CPU OC apply

CPU Per-LUT Voltage Override (v7.0)

Parameter Access Description
cpu_volt_override W Write cl:idx:volt_uv ... to patch any CSRAM LUT entry directly
cpu_volt_ov_result R Result string: B[5]=900000uV or cleared

Format: <cluster>:<lut_index>:<voltage_uv> (space-separated for multiple entries)

  • cluster = 0 (L), 1 (B), 2 (P)
  • lut_index = LUT entry index (0 = highest freq in CSRAM descending order)
  • voltage_uv = target voltage in µV

Write clear to restore all entries to their original values.

# Set B cluster LUT[5] to 900000 µV
echo '1:5:900000' > /sys/module/kpm_oc/parameters/cpu_volt_override
# Result: B[5]=900000uV

# Set multiple entries at once
echo '0:0:875000 1:0:1006250 2:0:1100000' > /sys/module/kpm_oc/parameters/cpu_volt_override

# Clear all overrides (restores original CSRAM values)
echo clear > /sys/module/kpm_oc/parameters/cpu_volt_override

GPU OC — Write target, then apply

Parameter Access Description
gpu_target_freq RW GPU OPP[0] target freq in KHz (default 1900000)
gpu_target_volt RW GPU OPP[0] target voltage in 10µV steps (default 115040 = 1150.4 mV)
gpu_target_vsram RW GPU OPP[0] target vsram in 10µV steps (default 95000 = 950.0 mV)
gpu_oc_apply W Write 1 to re-apply GPU OC
gpu_oc_result R Result string: OK:patched=3,freq=... or FAIL:...

patched bitmask:

  • bit 0 (1) = g_gpu_default_opp_table[0] patched
  • bit 1 (2) = working table patched
  • bit 2 (4) = signed table patched
  • bit 3 (8) = per-OPP voltage overrides applied

signed_table patching may be unavailable at runtime; this does not block the normal success path.

GPU Per-OPP Voltage Override (v7.0)

Parameter Access Description
gpu_volt_override W Write idx:volt[:vsram] ... to patch any GPU OPP entry directly
gpu_volt_ov_result R Result string: [1]=85000/85000 or cleared

Format: <opp_index>:<volt>[:<vsram>] (space-separated for multiple entries)

  • opp_index = OPP index (0–68, matching /proc/gpufreqv2/gpu_working_opp_table)
  • volt / vsram = voltage in 10µV steps (same unit as gpufreqv2 output). If vsram is omitted, defaults to volt.

Write clear to restore all entries to their original g_gpu_default_opp_table values.

# Set OPP[1] volt and vsram to 90000 (= 900.0 mV)
echo '1:90000:90000' > /sys/module/kpm_oc/parameters/gpu_volt_override

# Set multiple OPPs
echo '0:95000:95000 1:85000:85000 2:84000:84000' > /sys/module/kpm_oc/parameters/gpu_volt_override

# Clear all overrides (restores original values)
echo clear > /sys/module/kpm_oc/parameters/gpu_volt_override

# Verify
cat /proc/gpufreqv2/gpu_working_opp_table | head -5

GPU PLL Direct Programming (v8.0)

Parameter Access Description
gpu_pll_diag R Read MFG PLL CON1 and compute current frequency
gpu_pll_force_khz W Write KHz to directly program MFG PLL CON1 register
gpu_pll_force_unsafe RW Bool: allow PLL writes above gpu_target_freq (crash risk without matching voltage)

Warning: Writing gpu_pll_force_khz above the voltage-matched frequency range without gpu_pll_force_unsafe=1 will be rejected. Even with the unsafe flag, writing a frequency that exceeds the voltage headroom can cause an immediate GPU hang or device reboot.

FHCTL / MCU PLL (v8.0)

Parameter Access Description
fhctl_scan W Dump fhctl internal _array state (AP + MCU PLLs)
fhctl_oc W FHCTL IPI OC: write fh_id,dds_hex[,postdiv]
fhctl_unlock W Unlock fhctl debugfs ctrl interface
fhctl_capture R Last captured mcupm_hopping_v1 calls
mcupm_hop W Call mcupm_hopping_v1: write array_idx,fh_id,dds[,postdiv]

MCUPM SRAM / DRAM Diagnostics (v8.0)

Parameter Access Description
mcupm_scan_trigger W Search MCUPM SRAM/DRAM for frequency tables
mcupm_hexdump W Hex dump MCUPM memory at offset
mcupm_dram_write W Write DRAM: offset,value_hex

SCMI DVFS Diagnostics (v8.0)

Parameter Access Description
scmi_capture R Captured SCMI DVFS calls from kprobes
scmi_override W Override SCMI perf level: write domain,level (0 to clear)

PLL / Performance Diagnostics (v8.0)

Parameter Access Description
pll_scan W Dump MCU PLL CON0-3, FHCTL registers
perf_scan W Dump CSRAM perf_state + hw_state per domain
kvm_read_trigger W Read 256 bytes at a kernel virtual address (debug)

Suspend Stability & Sleep-Reboot Investigation (v11.0–v11.6)

Stock F8 firmware probabilistically watchdog-resets the device while the screen is off. v11.0–v11.6 iteratively added targeted kretprobe tracers that survive a cold reset via pstore / console-ramoops-0, narrowing the hang down one suspend phase at a time.

Tracer design

All tracers are compiled in but dormant until suspend_trace_enabled is set to Y — they add zero runtime cost in the default shipping configuration.

Log tag Hooks Emits (pr_emerg)
KPMTRACE dpm_run_callback (kretprobe) `enter/exit dev= drv= info=<prepare
KPMNOIRQ __device_suspend_noirq (kretprobe) enter/exit dev=<name> drv=<drv> rc=<ret> — drv=(none) pseudo-devices are filtered
KPMPHASE dpm_suspend_noirq, syscore_suspend, freeze_secondary_cpus, cpuidle_enter_s2idle, cpuidle_enter_state, psci_cpu_suspend_enter, psci_system_suspend_enter, cpu_suspend, pm_suspend_default_s2idle (kretprobes) enter/exit <fn> [rc=<ret>]

After a reboot, the last enter with no matching exit in /sys/fs/pstore/console-ramoops-0 identifies the hanging stage.

# Arm the tracer and drop the APatch sleep-guard wakelock, then screen off:
su -c sh /data/adb/modules/f8_kpm_oc_manager/tools/suspend_trace_arm.sh

# After the next reboot, reconnect USB and run:
su -c sh /data/adb/modules/f8_kpm_oc_manager/tools/suspend_trace_extract.sh

# If the device survived without crashing, disarm and re-apply the guard:
su -c sh /data/adb/modules/f8_kpm_oc_manager/tools/suspend_trace_disarm.sh

Root cause (confirmed)

  1. Full dpm_suspend_noirq completes cleanly — the last KPMNOIRQ line is always exit dev=platform drv=(none) rc=0, and KPMPHASE records exit dpm_suspend_noirq rc=0.
  2. Control passes to cpuidle_enter_s2idlepsci_cpu_suspend_entercpu_suspend, which issues a PSCI SMC into EL3.
  3. After a variable number of successful round-trips (returning rc=6 for system-bus or rc=2 for clusteroff), one PSCI call never returns. Every other CPU is also blocked in PSCI; the hardware watchdog fires ≈20–30 s later.
  4. Hang probability scales with idle state depth: system-bus hangs in ~100 ms, clusteroff in seconds to minutes, cpuoff and WFI not observed to hang.

This pattern is a proprietary-firmware bug in the MT6897 ATF / MCUPM / SPM coordination of multi-cluster shutdown during s2idle. It is not fixable from the kernel side — the PSCI SMC entry returns to a Secure Monitor that already lost its own scheduler invariant.

Permanent workaround (shipped)

Because the firmware cannot be patched, the APatch module holds a persistent f8_sleep_reboot_guard kernel wakelock at boot to block s2idle entry. Normal cpuidle (states 0–6, up to system-bus) is left fully enabled, so idle power consumption during screen-off is unchanged — only the broken suspend-to-idle transition is avoided.

# Verify the live configuration
cat /sys/power/wake_lock                               # f8_sleep_reboot_guard
cat /sys/module/kpm_oc/parameters/suspend_trace_enabled # N (dormant tracer)
for d in /sys/devices/system/cpu/cpu0/cpuidle/state*; do
    echo "$(basename $d) $(cat $d/name) disable=$(cat $d/disable)"
done
# state0 WFI             disable=0
# state1 cpuoff-l        disable=0
# state2 clusteroff-l    disable=0
# state3 mcusysoff-l     disable=0
# state4 system-mem      disable=0
# state5 system-pll      disable=0
# state6 system-bus      disable=0
# state7 system-vcore    disable=1   (MTK default)
# state8 s2idle          disable=1   (MTK default)

Suspend-related module parameter

Parameter Default Description
suspend_trace_enabled N When Y, all KPMTRACE / KPMNOIRQ / KPMPHASE kretprobes emit pr_emerg entry/exit lines during every suspend cycle so console-ramoops-0 preserves the exact stage reached before a watchdog hang. When N, all probe handlers early-return (no overhead).

Not Implemented / Known Limitations

DT2W (Double Tap to Wake)

Double-tap-to-wake gesture support was investigated and a working prototype was confirmed in v7.1 (kprobe on nvt_bootloader_reset pre-handler), but it was removed. The primary reason is a fundamental hardware constraint; additional software-level blockers make a kprobe-based workaround futile regardless.

Root cause: TDDI hardware constraint (cannot be solved in software)

The NT36523 is a TDDI (Touch and Display Driver Integration) chip in which the touch-sensing hardware is physically integrated with the display driver. When the display panel power is cut, the TDDI chip loses power entirely — touch sensing stops at the hardware level. No gesture mode, including the vendor gesture mode (0x13) or the normal-keep mode (0xFF), can detect any touch input while the screen is physically powered off. This is not a driver bug or a missing register write; it is an inherent hardware limitation of the TDDI architecture on this device. No software running on the AP — including kprobes, kernel modules, or userspace daemons — can override it.

Additional software-level blockers

  • Touch driver in vendor module — The NT36523 driver (nvt_ts) is compiled into a vendor kernel module. Its internal symbols (nvt_bootloader_reset, nvt_ts_resume, etc.) are not exported in the GKI symbol table. Kprobe registration relies on kallsyms_lookup_name at runtime and succeeds only when the vendor module loads first; there is no stable ABI guarantee.
  • nvt_ts_pm_resume() does not fire — This device routes display power management through a DRM notifier only. The standard pm_suspend / pm_resume callbacks in the NT36523 driver are never invoked, so the documented gesture-mode API (writing gesture registers in the resume path) cannot be used directly.
  • Fragile internal call sequence — The workaround was a pre-handler kprobe on nvt_bootloader_reset that re-armed the gesture EINT before the firmware reset cleared it. The correct timing depends on nvt_ts_resume()nvt_bootloader_reset() → gesture EINT re-arm, which is an undocumented internal order that can change silently across vendor OTA updates.
  • GKI CFI / KCFI constraints — GKI 6.1 is built with CONFIG_CFI_CLANG=y. Kprobing non-exported vendor module functions can produce KCFI type hash mismatches and trigger an immediate kernel panic and boot loop. Handlers must be carefully annotated __nocfi, and even then, indirect calls through kallsyms-resolved pointers carry risk.

Proximity Sensor

The Headwolf F8 does not have a proximity sensor. There is no proximity sensor IC wired to the SoC on this hardware revision and no corresponding devicetree node is defined. Any code path that reads a proximity sensor event source returns no data.


Build

Requirements

  • Android GKI 6.1 kernel source (android14-6.1 branch, common/ directory)
  • Android Clang r487747c (17.0.2) — required for CONFIG_CFI_CLANG=y compatibility
  • ARCH=arm64 with LLVM=1 LLVM_IAS=1

Using build.sh

# Prepare kernel source
cd /path/to/android14-6.1/common
make ARCH=arm64 LLVM=1 modules_prepare

# Build module
./build.sh KERNEL_DIR=/path/to/android14-6.1/common
# Optionally specify clang:
./build.sh KERNEL_DIR=/path/to/android14-6.1/common CLANG_DIR=/path/to/clang-r487747c

Using Make directly

make ARCH=arm64 LLVM=1 LLVM_IAS=1 \
     KERNEL_DIR=/path/to/android14-6.1/common \
     KBUILD_MODPOST_WARN=1

Important: The -fsanitize=kcfi flag is required. Without it, insmod will trigger a CFI failure at do_one_initcall and reboot the device.

Author

zerofripgithub.com/zerofrip

License

This project is licensed under GPL-2.0 — see the SPDX identifier in the source header.

// SPDX-License-Identifier: GPL-2.0

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages