Skip to main content

strat9_kernel/hardware/
thermal.rs

1//! ACPI thermal management : temperature reading and fan control.
2//!
3//! On Lenovo ThinkPads (X13, T14, etc.), the Embedded Controller (EC)
4//! exposes temperature sensors and fan control registers at known offsets.
5//! This driver reads the CPU temperature and controls the fan via the EC.
6//!
7//! ## ThinkPad EC register map (verified for X13 Gen 1/2)
8//!
9//! | Register | Description                     |
10//! |----------|---------------------------------|
11//! | 0x78     | CPU temperature (°C, unsigned)  |
12//! | 0x79     | GPU temperature (°C, unsigned)  |
13//! | 0x7A     | Battery temperature (°C)        |
14//! | 0x93     | Fan control: bit 0 = enable,    |
15//! |          |   bits 1–7 = speed level (0=max)|
16//! | 0x94     | Fan speed (RPM, 16-bit LE)      |
17//! | 0x84     | EC query: thermal event         |
18
19#![allow(dead_code)]
20
21use crate::hardware::ec;
22use core::sync::atomic::{AtomicBool, AtomicU16, AtomicU8, Ordering};
23use spin::Mutex;
24
25/// EC register: CPU temperature in degrees Celsius.
26const EC_REG_TEMP_CPU: u8 = 0x78;
27
28/// EC register: GPU temperature in degrees Celsius.
29const EC_REG_TEMP_GPU: u8 = 0x79;
30
31/// EC register: battery temperature in degrees Celsius.
32const EC_REG_TEMP_BAT: u8 = 0x7A;
33
34/// EC register: fan control byte.
35/// - Bit 0: fan enabled
36/// - Bits 1–7: speed level (0 = max speed, 127 = minimum speed)
37const EC_REG_FAN_CTRL: u8 = 0x93;
38
39/// EC register: fan speed in RPM (16-bit little-endian).
40const EC_REG_FAN_SPEED: u8 = 0x94;
41
42/// Fan speed levels for the ThinkPad manual fan control.
43/// Level 0 = maximum speed, 7 = off. The EC uses an inverted scale.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45#[repr(u8)]
46pub enum FanSpeed {
47    /// Maximum speed (loudest, best cooling).
48    Max = 0,
49    /// High speed.
50    High = 2,
51    /// Medium speed.
52    Medium = 4,
53    /// Low speed (quiet).
54    Low = 6,
55    /// Off.
56    Off = 7,
57}
58
59/// Thermal state snapshot, read atomically from EC registers.
60#[derive(Clone, Copy, Debug, Default)]
61pub struct ThermalState {
62    pub cpu_temp_c: u8,
63    pub gpu_temp_c: u8,
64    pub bat_temp_c: u8,
65    pub fan_rpm: u16,
66    pub fan_enabled: bool,
67    pub fan_level: u8,
68}
69
70/// Latest thermal snapshot (updated by periodic polling).
71static THERMAL_STATE: Mutex<ThermalState> = Mutex::new(ThermalState {
72    cpu_temp_c: 0,
73    gpu_temp_c: 0,
74    bat_temp_c: 0,
75    fan_rpm: 0,
76    fan_enabled: false,
77    fan_level: 0,
78});
79
80/// Whether thermal management is initialized.
81static THERMAL_INITIALIZED: AtomicBool = AtomicBool::new(false);
82
83/// Last read CPU temperature (for quick non-locking access).
84static LAST_CPU_TEMP: AtomicU8 = AtomicU8::new(0);
85
86/// Last read fan RPM (for quick non-locking access).
87static LAST_FAN_RPM: AtomicU16 = AtomicU16::new(0);
88
89/// Temperature thresholds for fan control (in °C).
90const FAN_ON_THRESHOLD: u8 = 55;
91const FAN_MAX_THRESHOLD: u8 = 85;
92const FAN_OFF_THRESHOLD: u8 = 45;
93
94/// Whether automatic fan control is enabled.
95static AUTO_FAN_CONTROL: AtomicBool = AtomicBool::new(true);
96
97/// Initialize the thermal subsystem.
98pub fn init() {
99    if !ec::is_available() {
100        log::warn!("[Thermal] EC not available, thermal management disabled");
101        return;
102    }
103
104    let state = read_thermal_state();
105    if state.cpu_temp_c > 0 && state.cpu_temp_c < 130 {
106        THERMAL_INITIALIZED.store(true, Ordering::Release);
107        LAST_CPU_TEMP.store(state.cpu_temp_c, Ordering::Relaxed);
108        LAST_FAN_RPM.store(state.fan_rpm, Ordering::Relaxed);
109
110        log::info!(
111            "[Thermal] CPU={}°C GPU={}°C BAT={}°C Fan={}RPM (enabled={})",
112            state.cpu_temp_c,
113            state.gpu_temp_c,
114            state.bat_temp_c,
115            state.fan_rpm,
116            state.fan_enabled,
117        );
118    } else {
119        log::warn!(
120            "[Thermal] Failed to read temperature (got {}°C), EC may not be ThinkPad-compatible",
121            state.cpu_temp_c
122        );
123    }
124}
125
126/// Read the current thermal state from EC registers.
127pub fn read_thermal_state() -> ThermalState {
128    let cpu_temp = ec::read(EC_REG_TEMP_CPU).unwrap_or(0);
129    let gpu_temp = ec::read(EC_REG_TEMP_GPU).unwrap_or(0);
130    let bat_temp = ec::read(EC_REG_TEMP_BAT).unwrap_or(0);
131
132    let fan_lo = ec::read(EC_REG_FAN_SPEED).unwrap_or(0);
133    let fan_hi = ec::read(EC_REG_FAN_SPEED.wrapping_add(1)).unwrap_or(0);
134    let fan_rpm = u16::from_le_bytes([fan_lo, fan_hi]);
135
136    let fan_ctrl = ec::read(EC_REG_FAN_CTRL).unwrap_or(0x87);
137    let fan_enabled = (fan_ctrl & 1) == 0;
138    let fan_level = (fan_ctrl >> 1) & 0x7F;
139
140    ThermalState {
141        cpu_temp_c: cpu_temp,
142        gpu_temp_c: gpu_temp,
143        bat_temp_c: bat_temp,
144        fan_rpm,
145        fan_enabled,
146        fan_level,
147    }
148}
149
150/// Update the cached thermal state. Call periodically (e.g., from timer tick).
151pub fn poll() {
152    if !THERMAL_INITIALIZED.load(Ordering::Acquire) {
153        return;
154    }
155
156    let state = read_thermal_state();
157    LAST_CPU_TEMP.store(state.cpu_temp_c, Ordering::Relaxed);
158    LAST_FAN_RPM.store(state.fan_rpm, Ordering::Relaxed);
159
160    let mut current = THERMAL_STATE.lock();
161    *current = state;
162
163    if AUTO_FAN_CONTROL.load(Ordering::Relaxed) {
164        drop(current);
165        auto_fan_control(state.cpu_temp_c);
166    }
167}
168
169/// Get the last known CPU temperature (lock-free).
170pub fn cpu_temperature() -> u8 {
171    LAST_CPU_TEMP.load(Ordering::Relaxed)
172}
173
174/// Get the last known fan RPM (lock-free).
175pub fn fan_rpm() -> u16 {
176    LAST_FAN_RPM.load(Ordering::Relaxed)
177}
178
179/// Enable automatic fan control based on CPU temperature.
180pub fn set_auto_fan_control(enabled: bool) {
181    AUTO_FAN_CONTROL.store(enabled, Ordering::Relaxed);
182}
183
184/// Set the fan to a specific speed level.
185///
186/// `level` maps to the ThinkPad EC fan control register:
187/// - 0 = maximum speed
188/// - 7 = off
189/// - Intermediate values scale between max and off
190pub fn set_fan_speed(speed: FanSpeed) {
191    let ctrl: u8 = ((speed as u8) << 1) | 0; // bit 0 = 0 means manual mode
192    ec::write(EC_REG_FAN_CTRL, ctrl);
193}
194
195/// Enable or disable the fan entirely.
196pub fn set_fan_enabled(enabled: bool) {
197    let current = ec::read(EC_REG_FAN_CTRL).unwrap_or(0x87);
198    let new_val = if enabled {
199        current & !1 // bit 0 = 0: fan enabled (active-low on some models)
200    } else {
201        current | 1 // bit 0 = 1: fan disabled
202    };
203    ec::write(EC_REG_FAN_CTRL, new_val);
204}
205
206/// Automatic fan control logic based on CPU temperature.
207fn auto_fan_control(cpu_temp: u8) {
208    if cpu_temp >= FAN_MAX_THRESHOLD {
209        set_fan_speed(FanSpeed::Max);
210    } else if cpu_temp >= FAN_ON_THRESHOLD {
211        set_fan_speed(FanSpeed::High);
212    } else if cpu_temp <= FAN_OFF_THRESHOLD {
213        set_fan_speed(FanSpeed::Off);
214    }
215    // Between FAN_OFF_THRESHOLD and FAN_ON_THRESHOLD: maintain current fan state
216}
217
218/// Dump thermal status to serial console.
219pub fn dump_status() {
220    let state = read_thermal_state();
221    log::info!(
222        "[Thermal] CPU={}°C GPU={}°C BAT={}°C Fan={}RPM level={} auto={}",
223        state.cpu_temp_c,
224        state.gpu_temp_c,
225        state.bat_temp_c,
226        state.fan_rpm,
227        state.fan_level,
228        AUTO_FAN_CONTROL.load(Ordering::Relaxed),
229    );
230}