diff --git a/Android.mk b/Android.mk index ad6c94d9..a2aa33c7 100644 --- a/Android.mk +++ b/Android.mk @@ -49,7 +49,9 @@ LOCAL_SRC_FILES := \ src/thd_platform_arm.cpp \ src/thd_util.cpp \ src/thd_cdev_rapl_restore.cpp \ - src/thd_features_parse.cpp + src/thd_features_parse.cpp \ + src/thd_cdev_spel.cpp \ + src/thd_cdev_spel_restore.cpp LOCAL_C_INCLUDES += external/libxml2/include diff --git a/Makefile.am b/Makefile.am index e05fa34c..ae4357b1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -69,6 +69,8 @@ thermald_SOURCES = \ src/thd_rapl_power_meter.cpp \ src/thd_trt_art_reader.cpp \ src/thd_cdev_rapl_dram.cpp \ + src/thd_cdev_spel.cpp \ + src/thd_cdev_spel_restore.cpp \ src/thd_cdev_backlight.cpp \ src/thd_int3400.cpp \ src/thd_sensor_rapl_power.cpp \ diff --git a/src/thd_cdev_spel.cpp b/src/thd_cdev_spel.cpp new file mode 100644 index 00000000..433c5a9a --- /dev/null +++ b/src/thd_cdev_spel.cpp @@ -0,0 +1,322 @@ +/* + * cthd_cdev_spel.cpp: thermal cooling class implementation using + * Qualcomm SPEL + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 or later as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * + * Author Name + * + */ +#include "thd_cdev_spel.h" +#include "thd_engine.h" + +/* This uses Qualcomm SPEL driver to cool the system. SPEL driver shows + * max thermal spec power in max_state. Each state can compensate + * spel_power_dec_percent, from the max state. + * + * SPEL supports PL1 (long-term) power limiting for SOC and SYS domains. + * This implementation focuses on set_curr_state functionality as requested. + */ +void cthd_sysfs_cdev_spel::set_curr_state(int state, int control) { + int new_state = state, ret; + + // If request to set a state which less than max_state i.e. lowest spel power limit + // then limit to the max_state. + if (state < max_state) + new_state = max_state; + + // If the state is more or equal to min_state, means that more than the + // max spel power limit, restore the power limit to min_state or + // whatever the power on limit, and restore the power on time window. + if (new_state >= min_state) { + // When PPCC is configured (ppcc_max_pwr > 0), always restore to min_state (PPCC max) + // to ensure we don't exceed PPCC constraints + if (ppcc_max_pwr > 0) { + new_state = min_state; // min_state is set to ppcc_max_pwr from PPCC + } else if (power_on_constraint_pwr) { + new_state = power_on_constraint_pwr; + } else { + new_state = min_state; + } + + curr_state = min_state; + + // Restore time window: use PPCC max if configured, otherwise use boot value + if (ppcc_max_window > 0) + spel_update_time_window(ppcc_max_window); + else + spel_update_time_window(power_on_constraint_time_window); + + constrained = false; + } else if (control) { + if (!constrained) { + + // If it is the first time to activate this device, set the time window. + if (ppcc_min_window) + spel_update_time_window(ppcc_min_window); + + constrained = true; + } + } + thd_log_info("SPEL %s: set cdev state index %d state %d wr:%d\n", + domain_type.c_str(), index, state, new_state); + + ret = spel_update_power_limit(new_state); + if (ret < 0) + thd_log_info("powercap SPEL %s is unable to update power limits\n", + domain_type.c_str()); + + curr_state = new_state; +} + +void cthd_sysfs_cdev_spel::set_curr_state_raw(int state, int arg) { + set_curr_state(state, arg); +} + +// Return the last state or power set during set_curr_state +int cthd_sysfs_cdev_spel::get_curr_state() { + return curr_state; +} + +int cthd_sysfs_cdev_spel::get_max_state() { + return max_state; +} + +int cthd_sysfs_cdev_spel::spel_sysfs_valid() +{ + std::ostringstream temp_str; + + // constraint_index is already set by constructor (0-3 for pl1-pl4) + // Just verify this constraint exists + temp_str << "constraint_" << constraint_index << "_name"; + if (!cdev_sysfs.exists(temp_str.str())) { + thd_log_info("powercap SPEL %s: constraint_%d does not exist\n", + domain_type.c_str(), constraint_index); + return THD_ERROR; + } + + // Verify power limit file exists + temp_str.str(std::string()); + temp_str << "constraint_" << constraint_index << "_power_limit_uw"; + if (!cdev_sysfs.exists(temp_str.str())) { + thd_log_debug("powercap SPEL %s: no power limit uw %s\n", + domain_type.c_str(), temp_str.str().c_str()); + return THD_ERROR; + } + + // Verify time window file exists + temp_str.str(std::string()); + temp_str << "constraint_" << constraint_index << "_time_window_us"; + if (!cdev_sysfs.exists(temp_str.str())) { + thd_log_info("powercap SPEL %s: no time_window_us %s\n", + domain_type.c_str(), temp_str.str().c_str()); + return THD_ERROR; + } + + return 0; +} + +int cthd_sysfs_cdev_spel::spel_read_power_limit_max() +{ + std::ostringstream temp_power_str; + int current_power_limit_max; + + temp_power_str << "constraint_" << constraint_index << "_max_power_uw"; + if (cdev_sysfs.read(temp_power_str.str(), ¤t_power_limit_max) > 0) { + return current_power_limit_max; + } + + return THD_ERROR; +} + +int cthd_sysfs_cdev_spel::spel_read_power_limit() +{ + std::ostringstream temp_power_str; + int current_power_limit; + + temp_power_str << "constraint_" << constraint_index << "_power_limit_uw"; + if (cdev_sysfs.read(temp_power_str.str(), ¤t_power_limit) > 0) { + return current_power_limit; + } + + return THD_ERROR; +} + + + +int cthd_sysfs_cdev_spel::spel_read_time_window() +{ + std::ostringstream temp_time_str; + int tm_window; + + temp_time_str << "constraint_" << constraint_index << "_time_window_us"; + if (cdev_sysfs.read(temp_time_str.str(), &tm_window) > 0) { + return tm_window; + } + + return THD_ERROR; +} + +int cthd_sysfs_cdev_spel::spel_update_time_window(int time_window) +{ + std::ostringstream temp_time_str; + + temp_time_str << "constraint_" << constraint_index << "_time_window_us"; + + if (cdev_sysfs.write(temp_time_str.str(), time_window) <= 0) { + thd_log_info( + "SPEL %s: powercap SPEL time window failed to write %d\n", + domain_type.c_str(), time_window); + return THD_ERROR; + } + + thd_log_debug("SPEL %s: time window set to %d us\n", + domain_type.c_str(), time_window); + return THD_SUCCESS; +} + +int cthd_sysfs_cdev_spel::spel_read_enable_status() +{ + std::ostringstream temp_str; + int enable; + + temp_str << "enabled"; + if (cdev_sysfs.read(temp_str.str(), &enable) > 0) { + return enable; + } + + return THD_ERROR; +} + +int cthd_sysfs_cdev_spel::update() { + bool ppcc = false; + int ret; + + if (spel_sysfs_valid()) + return THD_ERROR; + + register_for_restoration(); + + // Try to read PPCC power limits from configuration + ppcc = read_ppcc_power_limits(); + + if (ppcc) { + // This is a PPCC-configured platform with defined power and time window limits + thd_log_info("SPEL %s: Using PPCC configuration\n", domain_type.c_str()); + + // Set increment/decrement values based on PPCC step size + set_inc_value(-ppcc_step_pwr); + set_dec_value(-ppcc_step_pwr); + + min_state = ppcc_max_pwr; + max_state = ppcc_min_pwr; + + power_on_constraint_pwr = spel_read_power_limit(); + power_on_constraint_time_window = spel_read_time_window(); + power_on_enable_status = spel_read_enable_status(); + + // Align HW with software min_state at init, similar to RAPL behavior. + ret = spel_update_power_limit(min_state); + if (ret < 0) { + thd_log_info("SPEL %s: failed to initialize power limit to %d\n", + domain_type.c_str(), min_state); + return THD_ERROR; + } + + if (ppcc_max_window > ppcc_min_window) { + ret = spel_update_time_window(ppcc_max_window); + if (ret < 0) { + thd_log_info("SPEL %s: failed to initialize time window to %d\n", + domain_type.c_str(), ppcc_max_window); + return THD_ERROR; + } + } + + thd_log_info("SPEL %s: PPCC configured - max_pwr:%d min_pwr:%d time_window:%d\n", + domain_type.c_str(), ppcc_max_pwr, ppcc_min_pwr, ppcc_max_window); + } else { + thd_log_info("SPEL %s: PPCC not found, skipping SPEL cdev\n", + domain_type.c_str()); + return THD_ERROR; + } + + curr_state = min_state; + + thd_log_debug("SPEL %s: power_on_enable_status: %d\n", + domain_type.c_str(), power_on_enable_status); + thd_log_debug("SPEL %s: power_on_constraint_time_window: %d\n", + domain_type.c_str(), power_on_constraint_time_window); + thd_log_debug("SPEL %s: max limit %d increment: %d\n", + domain_type.c_str(), max_state, inc_dec_val); + + thd_log_info("SPEL %s domain initialized successfully\n", domain_type.c_str()); + return THD_SUCCESS; +} + +void cthd_sysfs_cdev_spel::thd_cdev_set_min_state_param(int arg) { + min_state = curr_state = arg; +} + +bool cthd_sysfs_cdev_spel::read_ppcc_power_limits() { + ppcc_t *ppcc; + + ppcc = thd_engine->get_ppcc_param(device_name); + if (ppcc) { + thd_log_info("Reading PPCC from the thermal-conf.xml for SPEL %s\n", domain_type.c_str()); + + // Power limits: XML is in mW, convert to uW (multiply by 1000) + ppcc_max_pwr = ppcc->power_limit_max * 1000; + ppcc_min_pwr = ppcc->power_limit_min * 1000; + ppcc_step_pwr = ppcc->step_size * 1000; + + // Time windows: XML is in ms, convert to us for *_time_window_us sysfs. + ppcc_min_window = ppcc->time_wind_min * 1000; + ppcc_max_window = ppcc->time_wind_max * 1000; + + if (ppcc_max_pwr <= ppcc_min_pwr) { + thd_log_info("SPEL %s: Invalid PPCC limits max:%d min:%d min_win:%d step:%d\n", + domain_type.c_str(), ppcc_max_pwr, ppcc_min_pwr, ppcc_min_window, ppcc_step_pwr); + return false; + } + + thd_log_info("SPEL %s: PPCC limits max:%d min:%d min_win:%d max_win:%d step:%d\n", + domain_type.c_str(), ppcc_max_pwr, ppcc_min_pwr, ppcc_min_window, ppcc_max_window, ppcc_step_pwr); + + return true; + } + + return false; +} + +int cthd_sysfs_cdev_spel::spel_update_power_limit(int power_limit) +{ + std::ostringstream temp_power_str; + int ret; + + temp_power_str << "constraint_" << constraint_index << "_power_limit_uw"; + ret = cdev_sysfs.write(temp_power_str.str(), power_limit); + if (ret <= 0) { + thd_log_info( + "SPEL %s: powercap SPEL power limit failed to write %d\n", + domain_type.c_str(), power_limit); + return ret; + } + + thd_log_debug("SPEL %s: power limit set to %d uW\n", + domain_type.c_str(), power_limit); + return THD_SUCCESS; +} diff --git a/src/thd_cdev_spel.h b/src/thd_cdev_spel.h new file mode 100644 index 00000000..e9a05e5a --- /dev/null +++ b/src/thd_cdev_spel.h @@ -0,0 +1,87 @@ +/* + * cthd_cdev_spel.h: thermal cooling class interface using + * Qualcomm SPEL + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 or later as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * + * Author Name + * + */ + +#ifndef THD_CDEV_SPEL_H_ +#define THD_CDEV_SPEL_H_ + +#include "thd_cdev.h" +#include "thd_sys_fs.h" + +class cthd_sysfs_cdev_spel: public cthd_cdev { +protected: + int domain_id; + int constraint_index; + int ppcc_max_pwr; + int ppcc_min_pwr; + int ppcc_min_window; + int ppcc_max_window; + int ppcc_step_pwr; + bool constrained; + int power_on_constraint_pwr; + int power_on_constraint_time_window; + int power_on_enable_status; + std::string device_name; + std::string domain_type; // "soc" or "sys" + virtual bool read_ppcc_power_limits(); + +private: + int spel_sysfs_valid(); + int spel_read_power_limit(); + int spel_read_power_limit_max(); + int spel_update_power_limit(int power_limit); + int spel_read_time_window(); + int spel_update_time_window(int time_window); + int spel_read_enable_status(); + void register_for_restoration(); + +public: + cthd_sysfs_cdev_spel(unsigned int _index, int domain, std::string _domain_type) : + cthd_sysfs_cdev_spel(_index, domain, std::move(_domain_type), 0, "") + { + } + cthd_sysfs_cdev_spel(unsigned int _index, int domain, std::string _domain_type, + int _constraint_index, std::string control_path) : + cthd_cdev(_index, std::move(control_path)), domain_id( + domain), constraint_index(_constraint_index), + ppcc_max_pwr(0), ppcc_min_pwr(0), ppcc_min_window(0), ppcc_max_window(0), + ppcc_step_pwr(0), constrained(false), + power_on_constraint_pwr(0), power_on_constraint_time_window(0), + power_on_enable_status(0), device_name("SPEL"), + domain_type(std::move(_domain_type)) + { + } + + void set_curr_state(int state, int arg) override; + int get_curr_state() override; + int get_max_state() override; + int update() override; + void set_curr_state_raw(int state, int arg) override; + void thd_cdev_set_min_state_param(int arg) override; + + std::string get_domain_type() const { + return domain_type; + } +}; + +#endif /* THD_CDEV_SPEL_H_ */ diff --git a/src/thd_cdev_spel_restore.cpp b/src/thd_cdev_spel_restore.cpp new file mode 100644 index 00000000..f0f95e6d --- /dev/null +++ b/src/thd_cdev_spel_restore.cpp @@ -0,0 +1,131 @@ +/* + * thd_cdev_spel_restore.cpp: SPEL power limit restoration on exit + * Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version + * 2 or later as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * + * Author Name + * + */ + +#include "thd_cdev_spel.h" +#include "thermald.h" +#include +#include +#include + +// Global registry of SPEL devices that need restoration on exit +namespace { + struct spel_restore_info { + std::string sysfs_path; + int constraint_index; + int power_limit; + int time_window; + }; + + std::vector restore_registry; + std::mutex registry_mutex; + bool atexit_registered = false; + + // Called by atexit() - restores all SPEL power limits + void restore_spel_limits() { + std::vector registry_copy; + { + std::lock_guard lock(registry_mutex); + registry_copy = restore_registry; + } + + thd_log_info("Restoring %zu SPEL power limits on exit\n", + registry_copy.size()); + + for (const auto& info : registry_copy) { + csys_fs sysfs(info.sysfs_path); + + // Restore power limit + std::ostringstream power_limit_path; + power_limit_path << "constraint_" << info.constraint_index + << "_power_limit_uw"; + if (sysfs.exists(power_limit_path.str())) { + sysfs.write(power_limit_path.str(), info.power_limit); + thd_log_info(" Restored SPEL power limit=%d uW for %s constraint_%d\n", + info.power_limit, info.sysfs_path.c_str(), + info.constraint_index); + } + + // Restore time window + std::ostringstream time_window_path; + time_window_path << "constraint_" << info.constraint_index + << "_time_window_us"; + if (sysfs.exists(time_window_path.str())) { + sysfs.write(time_window_path.str(), info.time_window); + thd_log_info(" Restored SPEL time window=%d us for %s constraint_%d\n", + info.time_window, info.sysfs_path.c_str(), + info.constraint_index); + } + } + } +} + +// Register a SPEL device for restoration on exit. +// Called from update() after successful initialization so that the +// unconstrained (min_state) power limit and time window are saved as +// the restoration target. +void cthd_sysfs_cdev_spel::register_for_restoration() { + std::lock_guard lock(registry_mutex); + + // Register atexit handler on first call + if (!atexit_registered) { + if (std::atexit(restore_spel_limits) != 0) { + thd_log_warn("Failed to register SPEL power limit restoration handler\n"); + return; + } + atexit_registered = true; + thd_log_info("Registered SPEL power limit restoration handler\n"); + } + + const std::string sysfs_path = cdev_sysfs.get_base_path(); + + // Avoid duplicate registrations for the same domain/constraint + for (const auto &existing : restore_registry) { + if (existing.sysfs_path == sysfs_path + && existing.constraint_index == constraint_index) { + return; + } + } + + const int power_limit = spel_read_power_limit(); + const int time_window = spel_read_time_window(); + if (power_limit < 0 || time_window < 0) { + thd_log_warn( + "Failed to read initial SPEL state for %s constraint_%d, " + "skipping restoration registration\n", + sysfs_path.c_str(), constraint_index); + return; + } + + spel_restore_info info; + info.sysfs_path = sysfs_path; + info.constraint_index = constraint_index; + info.power_limit = power_limit; + info.time_window = time_window; + + restore_registry.push_back(info); + + thd_log_info("Registered SPEL %s constraint_%d: power_limit=%d uW, " + "time_window=%d us\n", + sysfs_path.c_str(), constraint_index, + info.power_limit, info.time_window); +} diff --git a/src/thd_engine_default.cpp b/src/thd_engine_default.cpp index 6f39c429..c2f7f9c4 100644 --- a/src/thd_engine_default.cpp +++ b/src/thd_engine_default.cpp @@ -35,6 +35,7 @@ #include "thd_cdev_rapl.h" #include "thd_cdev_intel_pstate_driver.h" #include "thd_cdev_rapl_dram.h" +#include "thd_cdev_spel.h" #include "thd_sensor_virtual.h" #include "thd_cdev_backlight.h" #include "thd_int3400.h" @@ -780,6 +781,69 @@ int cthd_engine_default::read_cooling_devices() { ++current_cdev_index; } + // Add SPEL cooling devices for Qualcomm SOCs + // Check for SPEL sysfs nodes in powercap framework + // Create separate cooling devices for each constraint (PL0, PL1, PL2, PL3) + DIR *spel_dir; + struct dirent *spel_entry; + const std::string spel_base_path = "/sys/class/powercap/"; + + if ((spel_dir = opendir(spel_base_path.c_str())) != nullptr) { + while ((spel_entry = readdir(spel_dir)) != nullptr) { + if (strncmp(spel_entry->d_name, "qcom-spel:", 10) == 0) { + std::string spel_path = spel_base_path + spel_entry->d_name + "/"; + // Check if this is a valid SPEL domain by looking for name file + csys_fs spel_sysfs(spel_path); + if (spel_sysfs.exists("name")) { + std::string domain_name; + if (spel_sysfs.read("name", domain_name) >= 0) { + // For each domain (soc/sys), scan for all available constraints + // Constraints are named pl1, pl2, pl3, pl4 and map to constraint_0-3 + if (domain_name == "soc" || domain_name == "sys") { + // Scan for up to 4 constraints (constraint_0-3 = PL1-PL4) + for (int constraint_idx = 0; constraint_idx < 4; constraint_idx++) { + std::ostringstream constraint_file; + constraint_file << "constraint_" << constraint_idx << "_power_limit_uw"; + + if (spel_sysfs.exists(constraint_file.str())) { + // Read constraint name to verify it's a valid PL + std::ostringstream name_file; + name_file << "constraint_" << constraint_idx << "_name"; + std::string constraint_name; + if (spel_sysfs.read(name_file.str(), constraint_name) < 0) { + continue; + } + + // constraint_name should be "sys pl1", "sys pl2", etc. + // Extract the PL number (1-4) + int pl_num = constraint_idx + 1; // constraint_0=PL1, constraint_1=PL2, etc. + + // Create cooling device for this constraint + std::unique_ptr spel_dev( + new cthd_sysfs_cdev_spel(current_cdev_index, 0, + domain_name, constraint_idx, spel_path)); + + // Set type name: spel_controller_soc_pl1, spel_controller_soc_pl2, etc. + std::ostringstream type_name; + type_name << "spel_controller_" << domain_name << "_pl" << pl_num; + spel_dev->set_cdev_type(type_name.str()); + + if (spel_dev->update() == THD_SUCCESS) { + thd_log_info("Added SPEL %s PL%d (constraint_%d) cooling device at %s\n", + domain_name.c_str(), pl_num, constraint_idx, spel_path.c_str()); + cdevs.push_back(std::move(spel_dev)); + ++current_cdev_index; + } + } + } + } + } + } + } + } + closedir(spel_dir); + } + cthd_cdev *cdev = search_cdev("LCD"); if (!cdev) { std::unique_ptr backlight_dev(new cthd_cdev_backlight( diff --git a/src/thd_parse.cpp b/src/thd_parse.cpp index 07793371..114b450f 100644 --- a/src/thd_parse.cpp +++ b/src/thd_parse.cpp @@ -1255,7 +1255,8 @@ thermal_zone_t *cthd_parse::get_zone_dev_index(unsigned int zone_index) { } ppcc_t *cthd_parse::get_ppcc_param(const std::string& name) { - if (name != "TCPU.D0") + // Support both RAPL (TCPU.D0) and SPEL device names + if (name != "TCPU.D0" && name != "SPEL") return nullptr; if (matched_thermal_info_index >= 0 && thermal_info_list[matched_thermal_info_index].ppcc.valid)