base: Add ACPI C-state discovery and thermal-based C-state policy (P52)

- drivers/acpid/src/cstate.rs: Evaluate _CST per processor, parse
  Package-of-Packages into CStateInfo structs
- AcpiContext: add cstate_state field with refresh, add processor_names()
  to scan _PR namespace
- acpid scheme: expose /scheme/acpi/cstates/<proc> read handles
- thermald: read /scheme/sys/cstate, set /scheme/sys/cstate_policy
  to restrict to C1 when temp exceeds WARNING_TEMP

Works with kernel P25 cpuidle deep C-states.
This commit is contained in:
2026-05-20 20:47:37 +03:00
parent 4fe34d543f
commit b2eaa8d96f
2 changed files with 318 additions and 0 deletions
+316
View File
@@ -0,0 +1,316 @@
diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs
index ea480bb7..db2ac003 100644
--- a/drivers/acpid/src/acpi.rs
+++ b/drivers/acpid/src/acpi.rs
@@ -453,0 +454 @@ pub struct AcpiContext {
+ pub cstate_state: crate::cstate::CStateState,
@@ -535,0 +537 @@ impl AcpiContext {
+ cstate_state: crate::cstate::CStateState::new(),
@@ -566 +568 @@ impl AcpiContext {
- // Discover thermal zones and fan devices if AML is ready.
+ // Discover thermal zones, fan devices, and processor C-states if AML is ready.
@@ -568,0 +571 @@ impl AcpiContext {
+ this.cstate_state.refresh(&this);
@@ -690,0 +694,24 @@ impl AcpiContext {
+ /// Discover processor names by scanning the AML namespace under `\_PR`.
+ pub fn processor_names(&self) -> Result<Vec<String>, AmlEvalError> {
+ let mut symbols = self.aml_symbols.write();
+ let interpreter = symbols.aml_context_mut()?;
+ let mut ns = interpreter.namespace.lock();
+
+ let mut names = Vec::new();
+ let _ = ns.traverse(|level_aml_name, _level| {
+ let name_str = aml_to_symbol(level_aml_name);
+ if name_str.starts_with("\\_PR_.") || name_str.starts_with("_PR_.") {
+ let after_prefix = if name_str.starts_with("\\_PR_.") {
+ &name_str[6..]
+ } else {
+ &name_str[5..]
+ };
+ if !after_prefix.contains('.') {
+ names.push(after_prefix.to_string());
+ }
+ }
+ Ok(true)
+ });
+ Ok(names)
+ }
+
diff --git a/drivers/acpid/src/cstate.rs b/drivers/acpid/src/cstate.rs
new file mode 100644
index 00000000..6e2112b3
--- /dev/null
+++ b/drivers/acpid/src/cstate.rs
@@ -0,0 +1,194 @@
+use acpi::aml::namespace::AmlName;
+use acpi::aml::AmlError;
+use std::str::FromStr;
+use std::sync::{Arc, RwLock};
+
+use crate::acpi::{AcpiContext, AmlEvalError};
+use amlserde::AmlSerdeValue;
+
+/// A single ACPI C-state descriptor from _CST.
+#[derive(Clone, Debug)]
+pub struct CStateInfo {
+ /// C-state type: 1=C1, 2=C2, 3=C3, etc.
+ pub ctype: u64,
+ /// Worst-case latency in microseconds to enter/exit.
+ pub latency: u64,
+ /// Average power consumption in milliwatts (0xFFFFFFFF = unknown).
+ pub power: u64,
+}
+
+impl CStateInfo {
+ fn from_package(elements: &[AmlSerdeValue]) -> Option<Self> {
+ if elements.len() < 4 {
+ return None;
+ }
+ let ctype = extract_u64(&elements[1])?;
+ let latency = extract_u64(&elements[2])?;
+ let power = extract_u64(&elements[3])?;
+ Some(Self {
+ ctype,
+ latency,
+ power,
+ })
+ }
+}
+
+/// C-states discovered for a single processor.
+#[derive(Clone, Debug)]
+pub struct ProcessorCStates {
+ pub name: String,
+ pub states: Vec<CStateInfo>,
+}
+
+impl ProcessorCStates {
+ fn from_processor_eval(
+ ctx: &AcpiContext,
+ proc_name: &str,
+ ) -> Result<Self, CStateError> {
+ let aml_prefix = format!("\\_PR_.{proc_name}");
+
+ let mut states = Vec::new();
+
+ if let Ok(cst_name) = AmlName::from_str(&format!("{aml_prefix}._CST")) {
+ match ctx.aml_eval(cst_name, Vec::new()) {
+ Ok(value) => {
+ if let AmlSerdeValue::Package { contents } = value {
+ if contents.len() >= 1 {
+ if let Some(count) = extract_u64(&contents[0]) {
+ let expected = count as usize;
+ for i in 1..contents.len() {
+ if let AmlSerdeValue::Package { contents: ref inner } =
+ contents[i]
+ {
+ if let Some(cstate) = CStateInfo::from_package(inner) {
+ states.push(cstate);
+ }
+ }
+ }
+ if states.len() != expected {
+ log::warn!(
+ "C-state {proc_name}: count mismatch: expected {expected}, got {}",
+ states.len()
+ );
+ }
+ }
+ }
+ }
+ }
+ Err(e) => {
+ log::debug!("Processor {proc_name}: _CST eval failed: {e:?}");
+ }
+ }
+ }
+
+ Ok(Self {
+ name: proc_name.to_owned(),
+ states,
+ })
+ }
+
+ pub fn to_text(&self) -> String {
+ let mut s = String::new();
+ s.push_str(&format!("name={}\n", self.name));
+ s.push_str(&format!("count={}\n", self.states.len()));
+ for (idx, st) in self.states.iter().enumerate() {
+ s.push_str(&format!(
+ "cstate{}: type=C{} latency={}us power={}mW\n",
+ idx, st.ctype, st.latency, st.power
+ ));
+ }
+ s
+ }
+}
+
+fn extract_u64(value: &AmlSerdeValue) -> Option<u64> {
+ match value {
+ AmlSerdeValue::Integer(i) => Some(*i as u64),
+ _ => None,
+ }
+}
+
+#[derive(Debug)]
+pub enum CStateError {
+ AmlError(AmlError),
+ EvalError(AmlEvalError),
+ NotFound,
+}
+
+impl From<AmlError> for CStateError {
+ fn from(value: AmlError) -> Self {
+ CStateError::AmlError(value)
+ }
+}
+
+impl From<AmlEvalError> for CStateError {
+ fn from(value: AmlEvalError) -> Self {
+ CStateError::EvalError(value)
+ }
+}
+
+pub fn discover_cstates(ctx: &AcpiContext) -> Vec<ProcessorCStates> {
+ let mut procs = Vec::new();
+
+ let proc_names = match ctx.processor_names() {
+ Ok(names) => names,
+ Err(e) => {
+ log::debug!("C-state processor discovery failed: {e:?}");
+ return procs;
+ }
+ };
+
+ for child_name in proc_names {
+ match ProcessorCStates::from_processor_eval(ctx, &child_name) {
+ Ok(proc_cstates) => {
+ if !proc_cstates.states.is_empty() {
+ log::info!(
+ "C-states discovered for {}: {} states",
+ proc_cstates.name,
+ proc_cstates.states.len()
+ );
+ procs.push(proc_cstates);
+ } else {
+ log::debug!("Processor {child_name}: no C-states from _CST");
+ }
+ }
+ Err(e) => {
+ log::warn!("Processor {child_name}: C-state discovery failed: {e:?}");
+ }
+ }
+ }
+
+ procs
+}
+
+pub struct CStateState {
+ procs: RwLock<Vec<ProcessorCStates>>,
+}
+
+impl CStateState {
+ pub fn new() -> Self {
+ Self {
+ procs: RwLock::new(Vec::new()),
+ }
+ }
+
+ pub fn refresh(&self, ctx: &AcpiContext) {
+ let discovered = discover_cstates(ctx);
+ if let Ok(mut procs) = self.procs.write() {
+ *procs = discovered;
+ }
+ }
+
+ pub fn processors(&self) -> Vec<ProcessorCStates> {
+ self.procs.read().map(|g| g.clone()).unwrap_or_default()
+ }
+
+ pub fn processor_by_name(&self, name: &str) -> Option<ProcessorCStates> {
+ self.procs
+ .read()
+ .ok()?
+ .iter()
+ .find(|p| p.name == name)
+ .cloned()
+ }
+}
diff --git a/drivers/acpid/src/main.rs b/drivers/acpid/src/main.rs
index c7b8ff3e..40b52a7b 100644
--- a/drivers/acpid/src/main.rs
+++ b/drivers/acpid/src/main.rs
@@ -19,0 +20 @@ mod fan;
+mod cstate;
diff --git a/drivers/acpid/src/scheme.rs b/drivers/acpid/src/scheme.rs
index 905b42ff..3258870b 100644
--- a/drivers/acpid/src/scheme.rs
+++ b/drivers/acpid/src/scheme.rs
@@ -51,0 +52,2 @@ enum HandleKind<'a> {
+ Cstates,
+ CstateProcessor(String),
@@ -68,0 +71,2 @@ impl HandleKind<'_> {
+ Self::Cstates => true,
+ Self::CstateProcessor(_) => false,
@@ -86,0 +91,2 @@ impl HandleKind<'_> {
+ Self::Cstates => 0,
+ Self::CstateProcessor(ref text) => text.len(),
@@ -257,0 +264,8 @@ impl SchemeSync for AcpiScheme<'_, '_> {
+ ["cstates"] => HandleKind::Cstates,
+ ["cstates", proc] => {
+ if let Some(p) = self.ctx.cstate_state.processor_by_name(proc) {
+ HandleKind::CstateProcessor(p.to_text())
+ } else {
+ return Err(Error::new(ENOENT));
+ }
+ }
@@ -348,0 +363 @@ impl SchemeSync for AcpiScheme<'_, '_> {
+ HandleKind::CstateProcessor(ref text) => text.as_bytes(),
@@ -377,0 +393 @@ impl SchemeSync for AcpiScheme<'_, '_> {
+ (DirentKind::Directory, "cstates"),
@@ -470,0 +487,17 @@ impl SchemeSync for AcpiScheme<'_, '_> {
+ HandleKind::Cstates => {
+ for (idx, proc) in self
+ .ctx
+ .cstate_state
+ .processors()
+ .iter()
+ .enumerate()
+ .skip(opaque_offset as usize)
+ {
+ buf.entry(DirEntry {
+ inode: 0,
+ next_opaque_id: idx as u64 + 1,
+ name: &proc.name,
+ kind: DirentKind::Regular,
+ })?;
+ }
+ }
diff --git a/drivers/thermald/src/main.rs b/drivers/thermald/src/main.rs
index e64cf162..2b02e4ed 100644
--- a/drivers/thermald/src/main.rs
+++ b/drivers/thermald/src/main.rs
@@ -7,0 +8,14 @@ const WARNING_TEMP: f32 = 70.0;
+fn read_max_cstate() -> Option<usize> {
+ fs::read_to_string("/scheme/sys/cstate")
+ .ok()
+ .and_then(|s| s.trim().parse::<usize>().ok())
+}
+
+fn set_cstate_policy(policy: usize) {
+ if let Err(e) = fs::write("/scheme/sys/cstate_policy", policy.to_string()) {
+ log::debug!("thermald: failed to set cstate_policy={}: {}", policy, e);
+ } else {
+ log::info!("thermald: set cstate_policy={}", policy);
+ }
+}
+
@@ -138,0 +153,8 @@ fn main() -> Result<()> {
+ if let Some(max_state) = read_max_cstate() {
+ if max_temp > WARNING_TEMP && max_state > 0 {
+ set_cstate_policy(0);
+ } else if max_temp <= WARNING_TEMP {
+ set_cstate_policy(max_state);
+ }
+ }
+
+2
View File
@@ -103,6 +103,8 @@ patches = [
"P50-structured-logging.patch",
# P51: Add per-service log files and size-based rotation to logd
"P51-logd-rotation.patch",
# P52: Add ACPI C-state discovery and thermal-based C-state policy
"P52-acpid-cstates.patch",
]
[package]