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_voltvalidation - 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_commitreprograms PLL after GPUEB commits - FHCTL / MCU PLL hopping (v8.0) — IPI-based PLL frequency control via MCUPM fhctl interface; captures
mcupm_hopping_v1calls for diagnostics - SCMI DVFS capture & override (v8.0) — kprobes on
scmi_perf_level_set/scmi_dvfs_freq_setfor diagnostics and optional performance level override - Suspend stability tracer (v11.3–v11.6) — opt-in
suspend_trace_enabledmodule parameter installsKPMTRACE/KPMNOIRQ/KPMPHASEkretprobes ondpm_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" inconsole-ramoops-0after a watchdog reboot pinpoints the hanging stage - Sleep-reboot recovery (paired with the APatch
f8_sleep_reboot_guardwakelock) — dynamic tracing with this module identified the reboot as a probabilistic PSCI / ATF / MCUPM firmware hang during multi-cluster shutdown on thes2idlepath; the kernel side cannot fully patch it, so the APatch module blockss2idleentry with a permanent wakelock while cpuidle states 0–6 (up tosystem-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).
| 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) |
| 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 |
| 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) |
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
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.
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_tablevoltage on every GPU DVFS commit. The AP-kernelg_gpu_default_opp_tableis 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.
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-hwdriver (performance-controller LUT), not the SCMI cpufreq driver. Thescmi_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.
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) |
All parameters are under /sys/module/kpm_oc/parameters/.
| 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) |
| 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 |
| 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| 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.
| 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). Ifvsramis omitted, defaults tovolt.
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| 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_khzabove the voltage-matched frequency range withoutgpu_pll_force_unsafe=1will be rejected. Even with the unsafe flag, writing a frequency that exceeds the voltage headroom can cause an immediate GPU hang or device reboot.
| 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] |
| 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 |
| 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) |
| 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) |
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.
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- Full
dpm_suspend_noirqcompletes cleanly — the lastKPMNOIRQline is alwaysexit dev=platform drv=(none) rc=0, andKPMPHASErecordsexit dpm_suspend_noirq rc=0. - Control passes to
cpuidle_enter_s2idle→psci_cpu_suspend_enter→cpu_suspend, which issues a PSCI SMC into EL3. - After a variable number of successful round-trips (returning
rc=6forsystem-busorrc=2forclusteroff), one PSCI call never returns. Every other CPU is also blocked in PSCI; the hardware watchdog fires ≈20–30 s later. - Hang probability scales with idle state depth:
system-bushangs in ~100 ms,clusteroffin seconds to minutes,cpuoffandWFInot 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.
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)| 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). |
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.
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.
- 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 onkallsyms_lookup_nameat 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 standardpm_suspend/pm_resumecallbacks 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_resetthat re-armed the gesture EINT before the firmware reset cleared it. The correct timing depends onnvt_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.
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.
- Android GKI 6.1 kernel source (
android14-6.1branch,common/directory) - Android Clang r487747c (17.0.2) — required for
CONFIG_CFI_CLANG=ycompatibility ARCH=arm64withLLVM=1 LLVM_IAS=1
# 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-r487747cmake ARCH=arm64 LLVM=1 LLVM_IAS=1 \
KERNEL_DIR=/path/to/android14-6.1/common \
KBUILD_MODPOST_WARN=1Important: The
-fsanitize=kcfiflag is required. Without it,insmodwill trigger aCFI failure at do_one_initcalland reboot the device.
zerofrip — github.com/zerofrip
This project is licensed under GPL-2.0 — see the SPDX identifier in the source header.
// SPDX-License-Identifier: GPL-2.0