sysclk: code cleanup and fan speed

also fix incompatibility with nx fancontrol
This commit is contained in:
souldbminersmwc
2025-12-20 13:45:45 -05:00
parent b125ce6d84
commit 02a50867b3
21 changed files with 176 additions and 310 deletions

View File

@@ -1,163 +1,4 @@
# sys-clk
# hoc-clk
Switch sysmodule allowing you to set cpu/gpu/mem clocks according to the running application and docked state.
## Installation
The following instructions assumes you have a Nintendo Switch running Atmosphère, updated to at least the latest stable version.
Copy the `atmosphere`, and `switch` folders at the root of your sdcard, overwriting files if prompted. Also copy the `config` folder if you're not updating, to include default settings.
**Note:** sys-clk-overlay requires to have [Tesla](https://gbatemp.net/threads/tesla-the-nintendo-switch-overlay-menu.557362/) installed and running
## Relevant files
* Config file allows one to set custom clocks per docked state and title id, described below
`/config/sys-clk/config.ini`
* Log file where the logs are written if enabled
`/config/sys-clk/log.txt`
* Log flag file enables log writing if file exists
`/config/sys-clk/log.flag`
* CSV file where the title id, profile, clocks and temperatures are written if enabled
`/config/sys-clk/context.csv`
* sys-clk manager app (accessible from the hbmenu)
`/switch/sys-clk-manager.nro`
* sys-clk overlay (accessible from anywhere by invoking the [Tesla menu](https://gbatemp.net/threads/tesla-the-nintendo-switch-overlay-menu.557362/))
`/switch/.overlays/sys-clk-overlay.ovl`
* sys-clk core sysmodule
`/atmosphere/contents/00FF0000636C6BFF/exefs.nsp`
`/atmosphere/contents/00FF0000636C6BFF/flags/boot2.flag`
## Config
Presets can be customized by adding them to the ini config file located at `/config/sys-clk/config.ini`, using the following template for each app
```
[Application Title ID]
docked_cpu=
docked_gpu=
docked_mem=
handheld_charging_cpu=
handheld_charging_gpu=
handheld_charging_mem=
handheld_charging_usb_cpu=
handheld_charging_usb_gpu=
handheld_charging_usb_mem=
handheld_charging_official_cpu=
handheld_charging_official_gpu=
handheld_charging_official_mem=
handheld_cpu=
handheld_gpu=
handheld_mem=
```
* Replace `Application Title ID` with the title id of the game/application you're interested in customizing.
A list of games title id can be found in the [Switchbrew wiki](https://switchbrew.org/wiki/Title_list/Games).
* Frequencies are expressed in mhz, and will be scaled to the nearest possible values, described in the clock table below.
* If any key is omitted, value is empty or set to 0, it will be ignored, and stock clocks will apply.
* If charging, sys-clk will look for the frequencies in that order, picking the first found
1. Charger specific config (USB or Official) `handheld_charging_usb_X` or `handheld_charging_official_X`
2. Non specific charging config `handheld_charging_X`
3. Handheld config `handheld_X`
### Example 1: Zelda BOTW
* Overclock CPU when docked or charging
* Overclock MEM to docked clocks when handheld
Leads to a smoother framerate overall (ex: in the korok forest)
```
[01007EF00011E000]
docked_cpu=1224
handheld_charging_cpu=1224
handheld_mem=1600
```
### Example 2: Picross
* Underclocks on handheld to save battery
```
[0100BA0003EEA000]
handheld_cpu=816
handheld_gpu=153
handheld_mem=800
```
### Advanced
The `[values]` section allows you to alter timings in sys-clk, you should not need to edit any of these unless you know what you are doing. Possible values are:
| Key | Desc | Default |
|:-----------------------:|-------------------------------------------------------------------------------|:-------:|
|**temp_log_interval_ms** | Defines how often sys-clk logs temperatures, in milliseconds (`0` to disable) | 0 ms |
|**freq_log_interval_ms** | Defines how often sys-clk logs real freqs, in milliseconds (`0` to disable) | 0 ms |
|**power_log_interval_ms**| Defines how often sys-clk logs power usage, in milliseconds (`0` to disable) | 0 ms |
|**csv_write_interval_ms**| Defines how often sys-clk writes to the CSV, in milliseconds (`0` to disable) | 0 ms |
|**poll_interval_ms** | Defines how fast sys-clk checks and applies profiles, in milliseconds | 300 ms |
## Capping
To protect the battery from excessive strain, clocks requested from config may be capped before applying, depending on your current profile:
| | Handheld | Charging (USB) | Charging (Official) | Docked |
|:-----:|:--------:|:--------------:|:-------------------:|:------:|
|**MEM**| - | - | - | - |
|**CPU**| - | - | - | - |
|**GPU**| 460 MHz* | 768 MHz | - | - |
*\* GPU handheld max for Mariko is increased to 614 MHz*
## Clock table (MHz)
### MEM clocks
* 1600 → official docked, boost mode, max clock
* 1331 → official handheld
* 1065
* 800
* 665
### CPU clocks
* 1785 → max clock, boost mode
* 1683
* 1581
* 1428
* 1326
* 1224 → sdev oc
* 1122
* 1020 → official docked & handheld
* 918
* 816
* 714
* 612
### GPU clocks
* 921 → max clock
* 844
* 768 → official docked
* 691
* 614
* 537
* 460 → max handheld
* 384 → official handheld
* 307 → official handheld
* 230
* 153
* 76 → boost mode
**Notes:**
1. GPU overclock is capped at 460MHz in handheld and capped at 768MHz if charging, unless you're using the PD Charger.
2. Clocks higher than 768MHz need the PD Charger is plugged in.
Modified for Horizon OC

View File

@@ -0,0 +1,22 @@
#pragma once
#include <switch.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
Service s;
} PwmChannelSession;
Result pwmInitialize(void);
void pwmExit(void);
Service* pwmGetServiceSession(void);
Result pwmOpenSession2(PwmChannelSession *out, u32 device_code);
Result pwmChannelSessionGetDutyCycle(PwmChannelSession *c, double* out);
void pwmChannelSessionClose(PwmChannelSession *c);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@@ -0,0 +1,65 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <switch/types.h>
#include <switch/result.h>
#include <switch/kernel/mutex.h>
#include <switch/sf/service.h>
#include <switch/services/sm.h>
typedef struct ServiceGuard {
Mutex mutex;
u32 refCount;
} ServiceGuard;
NX_INLINE bool serviceGuardBeginInit(ServiceGuard* g)
{
mutexLock(&g->mutex);
return (g->refCount++) == 0;
}
NX_INLINE Result serviceGuardEndInit(ServiceGuard* g, Result rc, void (*cleanupFunc)(void))
{
if (R_FAILED(rc)) {
cleanupFunc();
--g->refCount;
}
mutexUnlock(&g->mutex);
return rc;
}
NX_INLINE void serviceGuardExit(ServiceGuard* g, void (*cleanupFunc)(void))
{
mutexLock(&g->mutex);
if (g->refCount && (--g->refCount) == 0)
cleanupFunc();
mutexUnlock(&g->mutex);
}
#define NX_GENERATE_SERVICE_GUARD_PARAMS(name, _paramdecl, _parampass) \
\
static ServiceGuard g_##name##Guard; \
NX_INLINE Result _##name##Initialize _paramdecl; \
static void _##name##Cleanup(void); \
\
Result name##Initialize _paramdecl \
{ \
Result rc = 0; \
if (serviceGuardBeginInit(&g_##name##Guard)) \
rc = _##name##Initialize _parampass; \
return serviceGuardEndInit(&g_##name##Guard, rc, _##name##Cleanup); \
} \
\
void name##Exit(void) \
{ \
serviceGuardExit(&g_##name##Guard, _##name##Cleanup); \
}
#define NX_GENERATE_SERVICE_GUARD(name) NX_GENERATE_SERVICE_GUARD_PARAMS(name, (void), ())
#ifdef __cplusplus
}
#endif

View File

@@ -103,6 +103,7 @@ typedef enum
HocClkPartLoad_GPU,
HocClkPartLoad_CPUAvg,
HocClkPartLoad_BAT,
HocClkPartLoad_FAN,
SysClkPartLoad_EnumMax
} SysClkPartLoad;

View File

@@ -1,4 +1,4 @@
#include "i2c_reg.h"
#include "i2c.h"
Result I2cSet_U8(I2cDevice dev, u8 reg, u8 val) {
// ams::fatal::srv::StopSoundTask::StopSound()

View File

@@ -0,0 +1,35 @@
#define NX_SERVICE_ASSUME_NON_DOMAIN
#include <switch.h>
#include "service_guard.h"
#include "pwm.h"
static Service g_pwmSrv;
NX_GENERATE_SERVICE_GUARD(pwm);
Result _pwmInitialize(void) {
return smGetService(&g_pwmSrv, "pwm");
}
void _pwmCleanup(void) {
serviceClose(&g_pwmSrv);
}
Service* pwmGetServiceSession(void) {
return &g_pwmSrv;
}
Result pwmOpenSession2(PwmChannelSession *out, u32 device_code) {
return serviceDispatchIn(&g_pwmSrv, 2, device_code,
.out_num_objects = 1,
.out_objects = &out->s,
);
}
Result pwmChannelSessionGetDutyCycle(PwmChannelSession *c, double* out) {
return serviceDispatchOut(&c->s, 7, *out);
}
void pwmChannelSessionClose(PwmChannelSession *controller) {
serviceClose(&controller->s);
}

View File

@@ -1,41 +0,0 @@
typedef enum {
PcvPowerDomain_Max77620_Sd0 = 0,
PcvPowerDomain_Max77620_Sd1 = 1,
PcvPowerDomain_Max77620_Sd2 = 2,
PcvPowerDomain_Max77620_Sd3 = 3,
PcvPowerDomain_Max77620_Ldo0 = 4,
PcvPowerDomain_Max77620_Ldo1 = 5,
PcvPowerDomain_Max77620_Ldo2 = 6,
PcvPowerDomain_Max77620_Ldo3 = 7,
PcvPowerDomain_Max77620_Ldo4 = 8,
PcvPowerDomain_Max77620_Ldo5 = 9,
PcvPowerDomain_Max77620_Ldo6 = 10,
PcvPowerDomain_Max77620_Ldo7 = 11,
PcvPowerDomain_Max77620_Ldo8 = 12,
PcvPowerDomain_Max77621_Cpu = 13,
PcvPowerDomain_Max77621_Gpu = 14,
PcvPowerDomain_Max77812_Cpu = 15,
PcvPowerDomain_Max77812_Gpu = 16,
PcvPowerDomain_Max77812_Dram = 17,
} PowerDomain;
typedef enum {
PcvPowerDomainId_Max77620_Sd0 = 0x3A000080,
PcvPowerDomainId_Max77620_Sd1 = 0x3A000081,
PcvPowerDomainId_Max77620_Sd2 = 0x3A000082,
PcvPowerDomainId_Max77620_Sd3 = 0x3A000083,
PcvPowerDomainId_Max77620_Ldo0 = 0x3A0000A0,
PcvPowerDomainId_Max77620_Ldo1 = 0x3A0000A1,
PcvPowerDomainId_Max77620_Ldo2 = 0x3A0000A2,
PcvPowerDomainId_Max77620_Ldo3 = 0x3A0000A3,
PcvPowerDomainId_Max77620_Ldo4 = 0x3A0000A4,
PcvPowerDomainId_Max77620_Ldo5 = 0x3A0000A5,
PcvPowerDomainId_Max77620_Ldo6 = 0x3A0000A6,
PcvPowerDomainId_Max77620_Ldo7 = 0x3A0000A7,
PcvPowerDomainId_Max77620_Ldo8 = 0x3A0000A8,
PcvPowerDomainId_Max77621_Cpu = 0x3A000003,
PcvPowerDomainId_Max77621_Gpu = 0x3A000004,
PcvPowerDomainId_Max77812_Cpu = 0x3A000003,
PcvPowerDomainId_Max77812_Gpu = 0x3A000004,
PcvPowerDomainId_Max77812_Dram = 0x3A000005,
} PowerDomainId;

View File

@@ -1,43 +0,0 @@
// rgltr_services.cpp (no changes needed here—just compile it once)
#include <switch.h>
#include "rgltr.h"
#include "rgltr_services.h" // for extern Service g_rgltrSrv, etc.
// Global service handle
Service g_rgltrSrv;
Result rgltrInitialize(void) {
if (hosversionBefore(8, 0, 0)) {
return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
}
return smGetService(&g_rgltrSrv, "rgltr");
}
void rgltrExit(void) {
serviceClose(&g_rgltrSrv);
}
Result rgltrOpenSession(RgltrSession* session_out, PowerDomainId module_id) {
const u32 in = (u32)module_id;
return serviceDispatchIn(
&g_rgltrSrv,
0,
in,
.out_num_objects = 1,
.out_objects = &session_out->s
);
}
Result rgltrGetVoltage(RgltrSession* session, u32* out_volt) {
// Service returns µV (microvolts) in a local u32:
u32 temp = 0;
Result rc = serviceDispatchOut(&session->s, 4, temp);
if (R_SUCCEEDED(rc)) {
*out_volt = temp;
}
return rc;
}
void rgltrCloseSession(RgltrSession* session) {
serviceClose(&session->s);
}

View File

@@ -1,29 +0,0 @@
// rgltr_services.h
// ========
// Minimal header declarations for rgltrrelated functionality.
// Any file that wants to call rgltrOpenSession(), rgltrGetVoltage(), etc. should
// simply do `#include "infonx.h"` (NOT infonx.cpp).
#pragma once
#include <switch.h> // for Service, Result, hosversionBefore(), smGetService(), serviceClose(), etc.
#include "rgltr.h" // for RgltrSession, PowerDomainId, etc.
// Global service handle for "rgltr". Defined in infonx.cpp.
extern Service g_rgltrSrv;
// Open/close the "rgltr" service. You must call rgltrInitialize() (once) before using
// rgltrOpenSession() & friends. Call rgltrExit() when your app is shutting down.
Result rgltrInitialize(void);
void rgltrExit(void);
// Open a regulator session for the given PowerDomainId (e.g. CPU, GPU, DRAM).
// On success, (*session_out).s will contain a valid Service handle.
Result rgltrOpenSession(RgltrSession* session_out, PowerDomainId module_id);
// Query the current voltage (in microvolts, µV) from a previously opened session.
// Writes the result into *out_volt.
Result rgltrGetVoltage(RgltrSession* session, u32* out_volt);
// Close a previously opened regulator session.
void rgltrCloseSession(RgltrSession* session);

View File

@@ -56,7 +56,7 @@ void BaseMenuGui::preDraw(tsl::gfx::Renderer* renderer) {
// All constants pre-calculated and cached
static constexpr const char* const labels[] = {
"App ID", "Profile", "CPU", "GPU", "MEM", "SoC", "Board", "Skin", "Now", "Avg", "BAT", "PMIC"
"App ID", "Profile", "CPU", "GPU", "MEM", "SoC", "Board", "Skin", "Now", "Avg", "BAT", "PMIC", "FAN"
};
static constexpr u32 dataPositions[6] = {63-3+3, 200-1, 344-1-3, 200-1, 342-1, 321-1};
@@ -159,6 +159,10 @@ void BaseMenuGui::preDraw(tsl::gfx::Renderer* renderer) {
renderer->drawString(displayStrings[21], false, dataPositions[0], y, SMALL_TEXT_SIZE, tsl::infoTextColor); // Bat voltage
renderer->drawString(displayStrings[23], false, positions[2] - 2, y, SMALL_TEXT_SIZE, tsl::infoTextColor); // Bat Age
renderer->drawString(labels[12], false, positions[6], y, SMALL_TEXT_SIZE, tsl::sectionTextColor); // fan label
renderer->drawString(displayStrings[24], false, dataPositions[1], y, SMALL_TEXT_SIZE, tsl::infoTextColor); // fan speed
}
// Optimized refresh - now does all the string formatting once per second
@@ -262,6 +266,8 @@ void BaseMenuGui::refresh()
sprintf(displayStrings[23], "%u%%", context->PartLoad[HocClkPartLoad_BAT] / 1000);
sprintf(displayStrings[24], "%u%%", context->PartLoad[HocClkPartLoad_FAN]);
}
tsl::elm::Element* BaseMenuGui::baseUI()

View File

@@ -27,7 +27,6 @@
#pragma once
#include "../../rgltr_services.h"
#include "../../ipc.h"
#include "base_gui.h"

View File

@@ -33,7 +33,8 @@
#include <algorithm> // for std::clamp
#include <math.h>
#include <numeric>
#include "batLib.h"
#include <battery.h>
#include <pwm.h>
#define HOSSVC_HAS_CLKRST (hosversionAtLeast(8,0,0))
#define HOSSVC_HAS_TC (hosversionAtLeast(5,0,0))
@@ -42,7 +43,6 @@
#define systemtickfrequency 19200000
#define systemtickfrequencyF 19200000.0f
#define CPU_TICK_WAIT (1'000'000'000 / 60)
float fanTemp = 0;
Result nvCheck = 1;
Thread gpuLThread;
@@ -50,9 +50,14 @@ Thread cpuCore0Thread;
Thread cpuCore1Thread;
Thread cpuCore2Thread;
Thread cpuCore3Thread;
Thread miscThread;
double temp = 0;
FanController fanController;
Result fanCheck = 1;
PwmChannelSession g_ICon;
Result pwmCheck = 1;
Result pwmDutyCycleCheck = 1;
double Rotation_Duty = 0;
u8 fanLevel;
uint32_t GPU_Load_u = 0, fd = 0;
BatteryChargeInfo info;
@@ -142,6 +147,21 @@ void gpuLoadThread(void*) {
} while(true);
}
void miscThreadFunc(void*) {
for(;;) {
if (R_SUCCEEDED(pwmCheck)) {
if (R_SUCCEEDED(pwmChannelSessionGetDutyCycle(&g_ICon, &temp))) {
temp *= 10;
temp = trunc(temp);
temp /= 10;
Rotation_Duty = 100.0 - temp;
}
}
fanLevel = (u8)Rotation_Duty;
svcSleepThread(300'000'000);
}
}
void Board::Initialize()
{
@@ -181,10 +201,10 @@ void Board::Initialize()
rc = rgltrInitialize();
ASSERT_RESULT_OK(rc, "rgltrInitialize");
if (R_SUCCEEDED(fanInitialize())) {
if (hosversionAtLeast(7,0,0)) fanCheck = fanOpenController(&fanController, 0x3D000001);
else fanCheck = fanOpenController(&fanController, 1);
}
// if (R_SUCCEEDED(fanInitialize())) {
// if (hosversionAtLeast(7,0,0)) fanCheck = fanOpenController(&fanController, 0x3D000001);
// else fanCheck = fanOpenController(&fanController, 1);
// }
threadCreate(&gpuLThread, gpuLoadThread, NULL, NULL, 0x1000, 0x3F, -2);
@@ -194,12 +214,19 @@ void Board::Initialize()
threadCreate(&cpuCore1Thread, CheckCore, &idletick1, NULL, 0x1000, 0x10, 1);
threadCreate(&cpuCore2Thread, CheckCore, &idletick2, NULL, 0x1000, 0x10, 2);
threadCreate(&cpuCore3Thread, CheckCore, &idletick3, NULL, 0x1000, 0x10, 3);
threadCreate(&miscThread, miscThreadFunc, NULL, NULL, 0x1000, 0x3F, 3);
threadStart(&cpuCore0Thread);
threadStart(&cpuCore1Thread);
threadStart(&cpuCore2Thread);
threadStart(&cpuCore3Thread);
threadStart(&miscThread);
batteryInfoInitialize();
if (hosversionAtLeast(6,0,0) && R_SUCCEEDED(pwmInitialize())) {
pwmCheck = pwmOpenSession2(&g_ICon, 0x3D000001);
}
FetchHardwareInfos();
}
@@ -230,8 +257,10 @@ void Board::Exit()
threadClose(&cpuCore1Thread);
threadClose(&cpuCore2Thread);
threadClose(&cpuCore3Thread);
threadClose(&miscThread);
fanExit();
pwmChannelSessionClose(&g_ICon);
pwmExit();
rgltrExit();
batteryInfoExit();
}
@@ -578,6 +607,8 @@ std::uint32_t Board::GetPartLoad(SysClkPartLoad loadSource)
case HocClkPartLoad_BAT:
batteryInfoGetChargeInfo(&info);
return info.RawBatteryCharge;
case HocClkPartLoad_FAN:
return GetFanRotationLevel();
default:
ASSERT_ENUM_VALID(SysClkPartLoad, loadSource);
}
@@ -726,6 +757,6 @@ std::uint32_t Board::GetVoltage(HocClkVoltage voltage)
}
u8 Board::GetFanRotationLevel() {
fanControllerGetRotationSpeedLevel(&fanController, &fanTemp);
return (u8)fanTemp;
return fanLevel;
}

View File

@@ -33,7 +33,7 @@
#include "errors.h"
#include "ipc_service.h"
#include "kip.h"
#include "i2c_reg.h"
#include <i2c.h>
#include "notification.h"
#define HOSPPC_HAS_BOOST (hosversionAtLeast(7,0,0))
@@ -587,8 +587,6 @@ bool ClockManager::RefreshContext()
FileUtils::WriteContextToCsv(this->context);
}
this->context->fanLevel = Board::GetFanRotationLevel();
return hasChanged;
}

View File

@@ -1,19 +0,0 @@
#pragma once
#include <switch.h>
#include "pcv_types.h"
typedef struct {
Service s;
} RgltrSession;
Result rgltrInitialize(void);
void rgltrExit(void);
Service* rgltrGetServiceSession(void);
Result rgltrOpenSession(RgltrSession* session_out, PowerDomainId module_id);
void rgltrCloseSession(RgltrSession* session);
Result rgltrGetVoltage(RgltrSession* session, u32 *out_volt);
Result rgltrGetPowerModuleNumLimit(u32 *out);
Result rgltrGetVoltageEnabled(RgltrSession* session, u32 *out);