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/data/thermal-conf.xml b/data/thermal-conf.xml
index be875dd9..13516ac4 100644
--- a/data/thermal-conf.xml
+++ b/data/thermal-conf.xml
@@ -6,6 +6,7 @@ use "man thermal-conf.xml" for details
+
Generic X86 Laptop Device
EXAMPLE_SYSTEM
@@ -203,6 +204,40 @@ use "man thermal-conf.xml" for details
+
+
+ Qualcomm Glymur CRD Platform
+ Qualcomm Technologies, Inc. Glymur CRD
+ QUIET
+
+
+ cpu-thermal
+ 1
+
+
+
+
+ cpu-thermal
+
+
+ cpu-thermal
+ 85000
+ passive
+ SEQUENTIAL
+
+ 1
+ cpufreq
+ 100
+ 10
+
+
+
+
+
+
-
diff --git a/man/thermal-conf.xml.5 b/man/thermal-conf.xml.5
index 75b828f9..de1a63b1 100644
--- a/man/thermal-conf.xml.5
+++ b/man/thermal-conf.xml.5
@@ -123,6 +123,14 @@ current virtual temperature.
set of tags defined to define platform, sensors, zones, cooling
devices and trip points. -->
Example Product Name
+
+ Example SoC Model Prefix
QUIET|PERFORMANCE
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..ea80cede 100644
--- a/src/thd_parse.cpp
+++ b/src/thd_parse.cpp
@@ -811,6 +811,10 @@ int cthd_parse::parse_new_platform_info(xmlNode * a_node, xmlDoc *doc,
"ProductSku")) {
info_ptr->product_sku.assign((const char*) tmp_value);
string_trim(info_ptr->product_sku);
+ } else if (!thd_strcasecmp_n((const char*) cur_node->name,
+ "SoCModel")) {
+ info_ptr->soc_model.assign((const char*) tmp_value);
+ string_trim(info_ptr->soc_model);
} else if (!thd_strcasecmp_n((const char*) cur_node->name, "Name")) {
info_ptr->name.assign((const char*) tmp_value);
string_trim(info_ptr->name);
@@ -991,6 +995,7 @@ void cthd_parse::dump_thermal_conf() {
thd_log_info("Product Name: %s\n", thermal_info_list[i].product_name.c_str());
thd_log_info("Product SKU: %s\n", thermal_info_list[i].product_sku.c_str());
thd_log_info("UUID: %s\n", thermal_info_list[i].uuid.c_str());
+ thd_log_info("SoC Model: %s\n", thermal_info_list[i].soc_model.c_str());
thd_log_info("type: %d\n", thermal_info_list[i].default_preference);
thd_log_info("Polling Interval: %d seconds\n", thermal_info_list[i].polling_interval);
@@ -1193,6 +1198,30 @@ bool cthd_parse::platform_matched() {
}
}
}
+
+ // Check SoC model via device tree (for ARM/Qualcomm platforms)
+ // /sys/firmware/devicetree/base/model contains the full board model string,
+ // e.g. "Qualcomm Technologies, Inc. Glymur CRD"
+ std::ifstream dt_model_file("/sys/firmware/devicetree/base/model");
+ if (dt_model_file.is_open() && getline(dt_model_file, line)) {
+ string_trim(line);
+ thd_log_debug("Device tree model: [%s]\n", line.c_str());
+ for (unsigned int i = 0; i < thermal_info_list.size(); ++i) {
+ if (!thermal_info_list[i].soc_model.size())
+ continue;
+ thd_log_debug("config SoCModel [%s] match with [%s]\n",
+ thermal_info_list[i].soc_model.c_str(), line.c_str());
+ // Prefix match: sysfs model string must start with the XML SoCModel value
+ if (line.compare(0, thermal_info_list[i].soc_model.size(),
+ thermal_info_list[i].soc_model) == 0) {
+ matched_thermal_info_index = i;
+ thd_log_info("SoC Model matched [%s]\n",
+ thermal_info_list[i].soc_model.c_str());
+ return true;
+ }
+ }
+ }
+
for (unsigned int i = 0; i < thermal_info_list.size(); ++i) {
if (!thermal_info_list[i].uuid.size())
continue;
@@ -1255,7 +1284,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)
diff --git a/src/thd_parse.h b/src/thd_parse.h
index 8d9160ab..1821d6d0 100644
--- a/src/thd_parse.h
+++ b/src/thd_parse.h
@@ -160,6 +160,7 @@ typedef struct {
std::string uuid;
std::string product_name;
std::string product_sku;
+ std::string soc_model;
int default_preference;
int polling_interval;
ppcc_t ppcc;