diff --git a/framework_lib/src/lib.rs b/framework_lib/src/lib.rs index 64d6cb5..2aceadb 100644 --- a/framework_lib/src/lib.rs +++ b/framework_lib/src/lib.rs @@ -58,8 +58,10 @@ pub mod ec_binary; pub mod esrt; #[cfg(feature = "uefi")] pub mod fw_uefi; +pub mod msr; mod os_specific; pub mod parade_retimer; +pub mod pcode; pub mod power; #[cfg(not(feature = "uefi"))] pub mod smart_battery; diff --git a/framework_lib/src/msr.rs b/framework_lib/src/msr.rs new file mode 100644 index 0000000..49e639e --- /dev/null +++ b/framework_lib/src/msr.rs @@ -0,0 +1,1363 @@ +//! Read Intel thermal and performance limiting MSRs +//! +//! These tell us *why* the CPU is running slower than requested. Most +//! interesting on our systems is PROCHOT, which the platform can assert to +//! throttle the CPU, and the frequency clipping reasons, which record both the +//! currently active and the (sticky) previously seen limiting reasons. +//! +//! Beware that the two disagree on Wildcat Lake: IA_PERF_LIMIT_REASONS reports +//! PROCHOT as an active *and* logged clipping reason across boots and repeated +//! reads, while PACKAGE_THERM_STATUS and IA32_THERM_STATUS show neither a +//! PROCHOT status nor a log bit. Which of the two is wrong is not established, +//! so we print both and point out the contradiction instead of picking one. +//! +//! Only Intel processors are supported. AMD does not expose comparable +//! information through MSRs. +//! +//! References: +//! - Intel SDM Volume 4 (Model-Specific Registers) +//! - Panther Lake reference code, `Include/Register/Ptl/Msr/MsrRegs.h`, +//! `PeiCpuPowerManagementLib` and `CpuCommonLib.c` for the tau encoding +//! - coreboot `src/soc/intel/common/block/cpu/cpulib.c` and `power_limit.c` +//! - Linux `arch/x86/include/asm/msr-index.h` and `tools/power/x86/turbostat` + +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +/// Maximum non-turbo and maximum efficiency ratio (MSR_PLATFORM_INFO) +pub const MSR_PLATFORM_INFO: u32 = 0x000000CE; +/// Counts at a fixed rate while the core is active (IA32_MPERF) +pub const MSR_IA32_MPERF: u32 = 0x000000E7; +/// Counts proportional to the actual frequency while active (IA32_APERF) +pub const MSR_IA32_APERF: u32 = 0x000000E8; +/// Per core thermal status (IA32_THERM_STATUS) +pub const MSR_IA32_THERM_STATUS: u32 = 0x0000019C; +/// Thermal monitor reference temperature and offsets (IA32_TEMPERATURE_TARGET) +pub const MSR_IA32_TEMPERATURE_TARGET: u32 = 0x000001A2; +/// Package thermal status (IA32_PACKAGE_THERM_STATUS) +pub const MSR_IA32_PACKAGE_THERM_STATUS: u32 = 0x000001B1; +/// Power control, contains the PROCHOT configuration (MSR_POWER_CTL) +pub const MSR_POWER_CTL: u32 = 0x000001FC; +/// Indicator of frequency clipping in the processor cores +/// +/// Sandy Bridge to Broadwell use 0x690 instead. All Intel based Framework +/// systems are Tiger Lake or newer, so we only implement the modern address. +pub const MSR_IA_PERF_LIMIT_REASONS: u32 = 0x0000064F; +/// Indicator of frequency clipping in the integrated graphics +pub const MSR_GT_PERF_LIMIT_REASONS: u32 = 0x000006B0; +/// Indicator of frequency clipping in the ring interconnect (a.k.a. CLR) +pub const MSR_RING_PERF_LIMIT_REASONS: u32 = 0x000006B1; +/// PL4, the instantaneous peak limit (MSR_VR_CURRENT_CONFIG) +pub const MSR_VR_CURRENT_CONFIG: u32 = 0x00000601; +/// Unit multipliers for the RAPL registers (MSR_PACKAGE_POWER_SKU_UNIT) +pub const MSR_PACKAGE_POWER_SKU_UNIT: u32 = 0x00000606; +/// PL1 and PL2 package power limits (MSR_PACKAGE_RAPL_LIMIT) +pub const MSR_PACKAGE_RAPL_LIMIT: u32 = 0x00000610; +/// TDP and the power range of this SKU (MSR_PACKAGE_POWER_SKU) +pub const MSR_PACKAGE_POWER_SKU: u32 = 0x00000614; +/// PL3, how often the package may exceed a peak power (MSR_PL3_CONTROL) +pub const MSR_PL3_CONTROL: u32 = 0x00000615; +/// PSys PL1 and PL2, limiting the whole platform instead of the package +pub const MSR_PLATFORM_POWER_LIMIT: u32 = 0x0000065C; + +/// Status bits of the *_PERF_LIMIT_REASONS MSRs +/// +/// The matching sticky log bit is always 16 bits higher. +const PLR_CORE_BITS: &[(u32, &str)] = &[ + (0, "PROCHOT"), + (1, "Thermal"), + (4, "ResidencyStateRegulation"), + (5, "RunningAvgThermalLimit"), + (6, "VR-ThermalAlert"), + (7, "VR-ThermalDesignCurrent"), + (8, "Other"), + (10, "PkgPwrPL1"), + (11, "PkgPwrPL2"), + (12, "MaxTurboLimit"), + (13, "TurboTransitionAttenuation"), +]; +const PLR_GT_BITS: &[(u32, &str)] = &[ + (0, "PROCHOT"), + (1, "Thermal"), + (4, "ResidencyStateRegulation"), + (5, "RunningAvgThermalLimit"), + (6, "VR-ThermalAlert"), + (7, "VR-ThermalDesignCurrent"), + (8, "Other"), + (10, "PkgPwrPL1"), + (11, "PkgPwrPL2"), + (12, "InefficientOperation"), +]; +const PLR_RING_BITS: &[(u32, &str)] = &[ + (0, "PROCHOT"), + (1, "Thermal"), + (4, "ResidencyStateRegulation"), + (5, "RunningAvgThermalLimit"), + (6, "VR-ThermalAlert"), + (7, "VR-ThermalDesignCurrent"), + (8, "Other"), + (10, "PkgPwrPL1"), + (11, "PkgPwrPL2"), +]; + +/// Status/log bit pairs shared by IA32_THERM_STATUS and +/// IA32_PACKAGE_THERM_STATUS. The log bit is always one bit higher. +const THERM_STATUS_BITS: &[(u32, &str)] = &[ + (0, "Thermal Monitor"), + (2, "PROCHOT/FORCEPR"), + (4, "Critical Temp"), + (6, "Threshold #1"), + (8, "Threshold #2"), + (10, "Power Limit"), +]; +/// Bits above 11 that only exist in the per core IA32_THERM_STATUS +const THERM_STATUS_CORE_BITS: &[(u32, &str)] = &[(12, "Current Limit"), (14, "Cross Domain Limit")]; +/// Bits above 11 that only exist in IA32_PACKAGE_THERM_STATUS +const THERM_STATUS_PKG_BITS: &[(u32, &str)] = &[(12, "Pmax Limit")]; + +fn bit(value: u64, bit: u32) -> bool { + (value >> bit) & 1 == 1 +} + +fn yes_no(value: bool) -> &'static str { + if value { + "Yes" + } else { + "No" + } +} + +/// Decode the set bits of a *_PERF_LIMIT_REASONS MSR +/// +/// `shift` is 0 for the currently active reasons and 16 for the sticky log. +fn decode_reasons(value: u64, bits: &[(u32, &str)], shift: u32) -> String { + let set: Vec<&str> = bits + .iter() + .filter(|(b, _)| bit(value, b + shift)) + .map(|(_, name)| *name) + .collect(); + if set.is_empty() { + "None".to_string() + } else { + set.join(", ") + } +} + +// ------------------------------------------------------------------------- +// CPUID +// ------------------------------------------------------------------------- + +/// Vendor, family and model from CPUID +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CpuId { + pub is_intel: bool, + pub family: u32, + pub model: u32, + /// CPUID.06H:EAX[0] Digital Thermal Sensor + pub has_dts: bool, + /// CPUID.06H:EAX[6] Package Thermal Management + pub has_ptm: bool, + /// CPUID.06H:ECX[0] APERF/MPERF hardware coordination feedback + pub has_aperf: bool, +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +// __cpuid is safe since Rust 1.87, but our MSRV is 1.81 +#[allow(unused_unsafe)] +pub fn cpuid() -> Option { + #[cfg(target_arch = "x86")] + use core::arch::x86::__cpuid; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::__cpuid; + + // SAFETY: CPUID leaf 0 is available on every CPU we can be running on + let leaf_0 = unsafe { __cpuid(0) }; + // "GenuineIntel" in EBX, EDX, ECX + let is_intel = leaf_0.ebx == 0x756E6547 && leaf_0.edx == 0x49656E69 && leaf_0.ecx == 0x6C65746E; + + // SAFETY: Leaf 1 is available whenever leaf 0 reports at least 1 + let leaf_1 = unsafe { __cpuid(1) }; + let base_family = (leaf_1.eax >> 8) & 0xF; + let base_model = (leaf_1.eax >> 4) & 0xF; + let family = if base_family == 0xF { + base_family + ((leaf_1.eax >> 20) & 0xFF) + } else { + base_family + }; + let model = if base_family == 0x6 || base_family == 0xF { + base_model + (((leaf_1.eax >> 16) & 0xF) << 4) + } else { + base_model + }; + + // Leaf 6 (Thermal and Power Management) tells us whether the thermal + // status MSRs exist. Only query it if the CPU supports that leaf. + let (has_dts, has_ptm, has_aperf) = if leaf_0.eax >= 6 { + // SAFETY: Guarded by the maximum leaf reported above + let leaf_6 = unsafe { __cpuid(6) }; + ( + leaf_6.eax & 1 == 1, + (leaf_6.eax >> 6) & 1 == 1, + leaf_6.ecx & 1 == 1, + ) + } else { + (false, false, false) + }; + + Some(CpuId { + is_intel, + family, + model, + has_dts, + has_ptm, + has_aperf, + }) +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +pub fn cpuid() -> Option { + None +} + +/// Frequency the TSC and MPERF count at, in MHz +/// +/// MPERF ticks at this rate whenever the core is active, so it is the scale +/// factor for turning an APERF/MPERF ratio into a frequency. +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +// __cpuid is safe since Rust 1.87, but our MSRV is 1.81 +#[allow(unused_unsafe)] +pub fn tsc_mhz() -> Option { + #[cfg(target_arch = "x86")] + use core::arch::x86::__cpuid; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::__cpuid; + + // SAFETY: CPUID leaf 0 is available on every CPU we can be running on + let max_leaf = unsafe { __cpuid(0) }.eax; + + // Leaf 0x15 gives the TSC as crystal_hz * numerator / denominator + if max_leaf >= 0x15 { + // SAFETY: Guarded by the maximum leaf + let leaf = unsafe { __cpuid(0x15) }; + let (denominator, numerator, crystal_hz) = (leaf.eax, leaf.ebx, leaf.ecx); + if denominator != 0 && numerator != 0 && crystal_hz != 0 { + let hz = crystal_hz as u64 * numerator as u64 / denominator as u64; + return Some((hz / 1_000_000) as u32); + } + } + + // Otherwise the TSC runs at the maximum non-turbo (base) frequency + let base = PlatformInfo::from(read_msr(0, MSR_PLATFORM_INFO)?).base_mhz(); + if base > 0 { + Some(base) + } else { + None + } +} + +#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] +pub fn tsc_mhz() -> Option { + None +} + +impl CpuId { + /// Intel Skylake (model 0x4E) or newer + /// + /// Most of the MSRs we read are not architectural, so we check this before + /// touching them. Reading a reserved MSR raises a general protection + /// fault, which is fatal in UEFI. + pub fn skylake_or_newer(&self) -> bool { + self.is_intel && self.family == 6 && self.model >= 0x4E + } + + /// Whether the *_PERF_LIMIT_REASONS MSRs at their modern addresses exist + pub fn has_perf_limit_reasons(&self) -> bool { + self.skylake_or_newer() + } + + /// Whether the RAPL power limit MSRs exist + /// + /// RAPL was introduced with Sandy Bridge (model 0x2A). + pub fn has_rapl(&self) -> bool { + self.is_intel && self.family == 6 && self.model >= 0x2A + } +} + +// ------------------------------------------------------------------------- +// MSR access, one implementation per OS +// ------------------------------------------------------------------------- + +#[cfg(all(target_os = "linux", any(target_arch = "x86", target_arch = "x86_64")))] +mod imp { + use std::fs::File; + use std::io::{Read, Seek, SeekFrom}; + + pub fn read_msr(cpu: u32, msr: u32) -> Option { + let path = format!("/dev/cpu/{}/msr", cpu); + let mut file = File::open(&path) + .map_err(|err| debug!("Failed to open {}: {:?}", path, err)) + .ok()?; + file.seek(SeekFrom::Start(msr as u64)) + .map_err(|err| debug!("Failed to seek {} to {:#X}: {:?}", path, msr, err)) + .ok()?; + let mut buf = [0u8; 8]; + file.read_exact(&mut buf) + .map_err(|err| debug!("Failed to read MSR {:#X}: {:?}", msr, err)) + .ok()?; + Some(u64::from_le_bytes(buf)) + } + + /// Enumerate the directory instead of probing sequentially, so that an + /// offline CPU in the middle doesn't cut the enumeration short + pub fn cpu_count() -> u32 { + let Ok(dir) = std::fs::read_dir("/dev/cpu") else { + return 0; + }; + dir.flatten() + .filter_map(|entry| entry.file_name().to_str()?.parse::().ok()) + .max() + .map_or(0, |max| max + 1) + } + + pub fn unavailable_hint() -> &'static str { + "Cannot access /dev/cpu/0/msr. Run as root and load the msr module (modprobe msr)" + } +} + +#[cfg(all( + target_os = "freebsd", + any(target_arch = "x86", target_arch = "x86_64") +))] +mod imp { + use nix::ioctl_readwrite; + use std::fs::File; + use std::os::fd::AsRawFd; + + /// `cpuctl_msr_args_t` from `sys/sys/cpuctl.h` + #[repr(C)] + struct CpuctlMsrArgs { + msr: i32, + data: u64, + } + ioctl_readwrite!(cpuctl_rdmsr, b'c', 1, CpuctlMsrArgs); + + pub fn read_msr(cpu: u32, msr: u32) -> Option { + let path = format!("/dev/cpuctl{}", cpu); + let file = File::open(&path) + .map_err(|err| debug!("Failed to open {}: {:?}", path, err)) + .ok()?; + let mut args = CpuctlMsrArgs { + msr: msr as i32, + data: 0, + }; + // SAFETY: args matches the struct the ioctl expects + unsafe { cpuctl_rdmsr(file.as_raw_fd(), &mut args) } + .map_err(|err| debug!("Failed to read MSR {:#X}: {:?}", msr, err)) + .ok()?; + Some(args.data) + } + + /// Enumerate the devices instead of probing sequentially, so that an + /// offline CPU in the middle doesn't cut the enumeration short + pub fn cpu_count() -> u32 { + let Ok(dir) = std::fs::read_dir("/dev") else { + return 0; + }; + dir.flatten() + .filter_map(|entry| { + entry + .file_name() + .to_str()? + .strip_prefix("cpuctl")? + .parse::() + .ok() + }) + .max() + .map_or(0, |max| max + 1) + } + + pub fn unavailable_hint() -> &'static str { + "Cannot access /dev/cpuctl0. Run as root and load the cpuctl module (kldload cpuctl)" + } +} + +#[cfg(all(target_os = "uefi", any(target_arch = "x86", target_arch = "x86_64")))] +mod imp { + use core::arch::asm; + + /// Read an MSR on the processor we're currently executing on + /// + /// UEFI applications run single threaded on the bootstrap processor, so we + /// can only ever read the BSP. Package scoped MSRs are unaffected by that. + pub fn read_msr(_cpu: u32, msr: u32) -> Option { + let lo: u32; + let hi: u32; + // SAFETY: Callers must make sure the MSR exists on this processor, + // otherwise this raises a general protection fault + unsafe { + asm!( + "rdmsr", + in("ecx") msr, + out("eax") lo, + out("edx") hi, + options(nomem, nostack, preserves_flags), + ); + } + Some(((hi as u64) << 32) | (lo as u64)) + } + + pub fn cpu_count() -> u32 { + 1 + } + + pub fn unavailable_hint() -> &'static str { + "Cannot read MSRs" + } +} + +// Windows needs a kernel driver to access MSRs and we don't ship one. +// Also covers non-x86, where these MSRs don't exist at all. +#[cfg(not(all( + any(target_os = "linux", target_os = "freebsd", target_os = "uefi"), + any(target_arch = "x86", target_arch = "x86_64") +)))] +mod imp { + pub fn read_msr(_cpu: u32, _msr: u32) -> Option { + None + } + + pub fn cpu_count() -> u32 { + 0 + } + + pub fn unavailable_hint() -> &'static str { + "Reading MSRs is not supported on this platform" + } +} + +/// Read a 64bit MSR of a logical processor +pub fn read_msr(cpu: u32, msr: u32) -> Option { + let value = imp::read_msr(cpu, msr); + debug!("CPU {} MSR {:#X}: {:#018X?}", cpu, msr, value); + value +} + +/// Number of logical processors we can read MSRs of +pub fn cpu_count() -> u32 { + imp::cpu_count() +} + +// ------------------------------------------------------------------------- +// Decoded structures +// ------------------------------------------------------------------------- + +/// IA32_TEMPERATURE_TARGET (0x1A2) +#[derive(Debug, Clone, Copy)] +pub struct TemperatureTarget { + /// Lowest temperature at which PROCHOT# is asserted, in degrees C + pub ref_temp: u8, + /// Offset below ref_temp at which TCC activates, in degrees C + pub tcc_offset: u8, + /// Offset below ref_temp at which fans should be engaged (T-Control) + pub fan_temp_offset: u8, + /// Allow RATL throttling below P1 + pub tcc_offset_clamping: bool, + /// Raw 7 bit tau field of the running average temperature limit (RATL) + /// + /// Zero means RATL is off. coreboot programs 0x66 here, the closest the tau + /// format gets to the 100 ms it aims for, so RATL is on for us whenever the + /// board configures a TCC offset at all. + pub ratl_tau: u64, + /// The whole MSR is read-only + pub locked: bool, +} + +impl From for TemperatureTarget { + fn from(msr: u64) -> Self { + Self { + ref_temp: ((msr >> 16) & 0xFF) as u8, + // 7 bits [30:24] on Panther Lake, 6 bits since Skylake. + // Offsets that large don't occur, so mask conservatively. + tcc_offset: ((msr >> 24) & 0x3F) as u8, + fan_temp_offset: ((msr >> 8) & 0xFF) as u8, + tcc_offset_clamping: bit(msr, 7), + ratl_tau: msr & 0x7F, + locked: bit(msr, 31), + } + } +} + +impl TemperatureTarget { + /// Whether the running average temperature limit is enabled + /// + /// RATL lets the die exceed the TCC activation temperature in bursts as + /// long as the average over the tau stays below it. The reference code + /// decides this the same way, by the tau field being nonzero. + pub fn ratl(&self) -> bool { + self.ratl_tau != 0 + } + + /// Temperature at which PROCHOT# is asserted, in degrees C + /// + /// With RATL enabled the TCC offset only applies to the running average, so + /// the instantaneous trip point is the reference temperature itself. That's + /// what the reference code's `CpuGetCrossThrottlingTripPoint` returns. + pub fn tcc_activation(&self) -> u8 { + if self.ratl() { + self.ref_temp + } else { + self.ref_temp.saturating_sub(self.tcc_offset) + } + } +} + +/// IA32_THERM_STATUS (0x19C) and IA32_PACKAGE_THERM_STATUS (0x1B1) +#[derive(Debug, Clone, Copy)] +pub struct ThermStatus { + pub raw: u64, + /// Temperature is valid + pub valid: bool, + /// Degrees C below the reference temperature + pub readout: u8, + /// Resolution of the readout in degrees C + pub resolution: u8, +} + +impl From for ThermStatus { + fn from(msr: u64) -> Self { + Self { + raw: msr, + valid: bit(msr, 31), + readout: ((msr >> 16) & 0x7F) as u8, + resolution: ((msr >> 27) & 0xF) as u8, + } + } +} + +impl ThermStatus { + /// Temperature in degrees C, needs the reference temperature from + /// IA32_TEMPERATURE_TARGET + pub fn temp(&self, ref_temp: u8) -> Option { + if !self.valid { + return None; + } + Some(ref_temp as i32 - self.readout as i32) + } + + /// Whether the core or package is currently being thermally throttled + pub fn throttling(&self) -> bool { + bit(self.raw, 0) || bit(self.raw, 2) + } +} + +/// The ratios reported by MSR_PLATFORM_INFO (0xCE) +/// +/// All modern Intel client processors use a 100 MHz bus clock. +#[derive(Debug, Clone, Copy)] +pub struct PlatformInfo { + /// Maximum non-turbo ratio, a.k.a. the base frequency + pub base_ratio: u8, + /// Maximum efficiency ratio, a.k.a. LFM, the lowest the cores will run at + pub max_efficiency_ratio: u8, +} + +impl From for PlatformInfo { + fn from(msr: u64) -> Self { + Self { + base_ratio: ((msr >> 8) & 0xFF) as u8, + max_efficiency_ratio: ((msr >> 40) & 0xFF) as u8, + } + } +} + +impl PlatformInfo { + pub fn base_mhz(&self) -> u32 { + self.base_ratio as u32 * 100 + } + + pub fn lfm_mhz(&self) -> u32 { + self.max_efficiency_ratio as u32 * 100 + } +} + +/// Sample APERF and MPERF on every core and return the frequency each was +/// running at while it was active, in MHz +/// +/// This is the same "busy" frequency turbostat reports as Bzy_MHz, not the +/// requested or the average-over-wall-clock frequency. A core that stayed idle +/// for the whole interval reports None. +pub fn core_frequencies(tsc_mhz: u32, interval_micros: u64) -> Vec<(u32, Option)> { + let cpus = cpu_count(); + let sample = |cpu| { + Some(( + read_msr(cpu, MSR_IA32_APERF)?, + read_msr(cpu, MSR_IA32_MPERF)?, + )) + }; + + let first: Vec> = (0..cpus).map(sample).collect(); + crate::os_specific::sleep(interval_micros); + + (0..cpus) + .map(|cpu| { + let mhz = match (first[cpu as usize], sample(cpu)) { + (Some((aperf_0, mperf_0)), Some((aperf_1, mperf_1))) => { + let aperf = aperf_1.wrapping_sub(aperf_0); + let mperf = mperf_1.wrapping_sub(mperf_0); + // The ratio is self normalizing, so it doesn't matter that + // we read each core at a slightly different time. A core + // that never woke up doesn't advance MPERF at all, which + // checked_div turns into None. + (tsc_mhz as u64 * aperf) + .checked_div(mperf) + .map(|mhz| mhz as u32) + } + _ => None, + }; + (cpu, mhz) + }) + .collect() +} + +/// Unit multipliers from MSR_PACKAGE_POWER_SKU_UNIT (0x606) +#[derive(Debug, Clone, Copy)] +pub struct RaplUnits { + /// Watts per LSB of the power fields, 1/8 W by default + pub power: f32, + /// Seconds per LSB of the time window fields, 1/1024 s by default + pub time: f32, +} + +impl From for RaplUnits { + fn from(msr: u64) -> Self { + Self { + power: 1.0 / (1u32 << (msr & 0xF)) as f32, + time: 1.0 / (1u32 << ((msr >> 16) & 0xF)) as f32, + } + } +} + +/// One power limit out of a RAPL limit register +#[derive(Debug, Clone, Copy)] +pub struct PowerLimit { + pub watts: f32, + pub enabled: bool, + /// Processor may go below the OS requested P-State to hold the limit + pub clamping: bool, + /// Averaging window in seconds, None if this limit has no tau field + pub tau: Option, +} + +/// Decode a 7 bit tau (averaging time window) field +/// +/// Tau = (1 + X/4) * 2^Y in units of `unit` seconds, where Y is bits 4:0 and X +/// is bits 6:5 of the field. Every averaging window the processor has is +/// encoded this way: the RAPL power limits, the platform current limits, PL3 +/// and the running average temperature limit. +fn tau(field: u64, unit: f32) -> f32 { + let y = field & 0x1F; + let x = (field >> 5) & 0x3; + (1.0 + x as f32 / 4.0) * (1u64 << y) as f32 * unit +} + +/// The time unit of the tau fields that have no unit register of their own +/// +/// The RAPL limits scale their tau by `RaplUnits::time`, which is 1/1024 s on +/// every processor we've seen. RATL and PL3 have no such register and the +/// reference code assumes 1/1024 s for them, which is what its seconds and +/// milliseconds conversion tables are built from. +const DEFAULT_TIME_UNIT: f32 = 1.0 / 1024.0; + +/// Decode one PL1/PL2 style limit out of a RAPL limit register +/// +/// The fields repeat every 32 bits, so `shift` is 0 for PL1 and 32 for PL2. +fn decode_limit(raw: u64, shift: u32, has_time: bool, units: &RaplUnits) -> PowerLimit { + PowerLimit { + watts: ((raw >> shift) & 0x7FFF) as f32 * units.power, + enabled: bit(raw, shift + 15), + clamping: bit(raw, shift + 16), + tau: if has_time { + Some(tau((raw >> (shift + 17)) & 0x7F, units.time)) + } else { + None + }, + } +} + +// ------------------------------------------------------------------------- +// Printing +// ------------------------------------------------------------------------- + +/// Print PROCHOT status, frequency limiting reasons and thermal MSR details +/// +/// Silently does nothing if the platform doesn't have these MSRs or we can't +/// read them. Reasons are logged. +pub fn print_thermal_msrs() { + let Some(cpuid) = cpuid() else { + debug!("No CPUID, not reading thermal MSRs"); + return; + }; + if !cpuid.is_intel { + info!("Thermal MSRs are only implemented for Intel processors"); + return; + } + if !cpuid.has_dts && !cpuid.has_ptm { + info!("Processor has no digital thermal sensor, not reading thermal MSRs"); + return; + } + + let Some(target) = read_msr(0, MSR_IA32_TEMPERATURE_TARGET).map(TemperatureTarget::from) else { + info!("{}", imp::unavailable_hint()); + return; + }; + + println!(" Intel Thermal MSRs"); + println!(" TjMax: {:>4} C", target.ref_temp); + let clamping = if target.tcc_offset_clamping { + ", clamping" + } else { + "" + }; + if target.ratl() { + // The offset limits the average temperature over the tau instead of the + // instantaneous one, so the die may run hotter than TjMax minus offset + println!( + " TCC Activation: {:>4} C ({} C average over {:.3} s tau{})", + target.tcc_activation(), + target.ref_temp.saturating_sub(target.tcc_offset), + tau(target.ratl_tau, DEFAULT_TIME_UNIT), + clamping + ); + } else { + debug!("No RATL tau, the TCC offset limits the instantaneous temperature"); + println!( + " TCC Activation: {:>4} C (Offset {} C{})", + target.tcc_activation(), + target.tcc_offset, + clamping + ); + } + // Our EC does its own fan control, so this is usually left at 0 (unused) + if target.fan_temp_offset > 0 { + println!( + " Fan Temp Target: {:>4} C (Offset {} C)", + target.ref_temp - target.fan_temp_offset, + target.fan_temp_offset + ); + } else { + debug!("Fan temperature target offset (T-Control) not programmed"); + } + debug!("TEMPERATURE_TARGET locked: {}", target.locked); + + // Collect the PROCHOT status and log bits to cross check the frequency + // limit reasons against further down + let mut therm_status = 0; + + if cpuid.has_ptm { + if let Some(pkg) = read_msr(0, MSR_IA32_PACKAGE_THERM_STATUS).map(ThermStatus::from) { + if let Some(temp) = pkg.temp(target.ref_temp) { + println!( + " Package Temp: {:>4} C (Resolution {} C)", + temp, pkg.resolution + ); + } + println!(" Package Thermal Status ({:#010X})", pkg.raw); + print_therm_status_table(pkg.raw, THERM_STATUS_PKG_BITS); + therm_status |= pkg.raw & 0xFFFF; + } + } + + if cpuid.has_dts { + therm_status |= print_core_therm_status(target.ref_temp); + } + + if cpuid.has_aperf { + print_core_frequencies(); + } else { + debug!("No APERF/MPERF support, skipping core frequencies"); + } + + print_power_limits(&cpuid); + + if cpuid.has_perf_limit_reasons() { + // Bit 2 is the PROCHOT/FORCEPR status and bit 3 its sticky log + print_perf_limit_reasons(bit(therm_status, 2) || bit(therm_status, 3)); + } else { + debug!( + "No PERF_LIMIT_REASONS MSRs on family {:#X} model {:#X}", + cpuid.family, cpuid.model + ); + } + + print_power_ctl(); +} + +/// Print the Active/Logged table of a THERM_STATUS style MSR +/// +/// The bits above 11 differ between the per core and the package register, so +/// the caller passes those in. +fn print_therm_status_table(raw: u64, extra: &[(u32, &str)]) { + println!(" {:<24} {:>6} {:>6}", "Condition", "Active", "Logged"); + for (b, name) in THERM_STATUS_BITS.iter().chain(extra) { + println!( + " {:<24} {:>6} {:>6}", + format!("{}:", name), + yes_no(bit(raw, *b)), + yes_no(bit(raw, b + 1)) + ); + } +} + +/// Print the hottest core and the thermal status across all cores +/// +/// Returns the status bits OR'd across every core we could read. +fn print_core_therm_status(ref_temp: u8) -> u64 { + let cpus = cpu_count(); + let mut hottest: Option<(u32, i32)> = None; + let mut throttling = Vec::new(); + // The status bits are per core, so OR them together to see whether any + // core ever hit a condition. Reading every core individually is far too + // much output on a system with dozens of them. + let mut any = 0; + let mut read = 0; + + for cpu in 0..cpus { + let Some(status) = read_msr(cpu, MSR_IA32_THERM_STATUS).map(ThermStatus::from) else { + continue; + }; + read += 1; + // Mask off temperature, resolution and valid, they're not status bits + any |= status.raw & 0xFFFF; + if let Some(temp) = status.temp(ref_temp) { + match hottest { + Some((_, hottest_temp)) if temp <= hottest_temp => {} + _ => hottest = Some((cpu, temp)), + } + } + if status.throttling() { + throttling.push(format!("{}", cpu)); + } + } + + if read == 0 { + return 0; + } + if let Some((cpu, temp)) = hottest { + println!(" Hottest Core Temp: {:>4} C (CPU {})", temp, cpu); + } + println!( + " Cores Throttling: {:>4}", + if throttling.is_empty() { + "None".to_string() + } else { + throttling.join(", ") + } + ); + println!( + " Core Thermal Status, any of {} cores ({:#06X})", + read, any + ); + print_therm_status_table(any, THERM_STATUS_CORE_BITS); + any +} + +/// Print which limits are clipping the core, graphics and ring frequency +/// +/// `prochot_seen` is whether IA32_THERM_STATUS or IA32_PACKAGE_THERM_STATUS +/// corroborate a PROCHOT assertion, either now or since the log was cleared. +fn print_perf_limit_reasons(prochot_seen: bool) { + let mut prochot_claimed = false; + for (msr, name, bits) in [ + (MSR_IA_PERF_LIMIT_REASONS, "Core", PLR_CORE_BITS), + (MSR_GT_PERF_LIMIT_REASONS, "Graphics", PLR_GT_BITS), + (MSR_RING_PERF_LIMIT_REASONS, "Ring", PLR_RING_BITS), + ] { + let Some(value) = read_msr(0, msr) else { + continue; + }; + println!( + " {} Frequency Limit Reasons ({:#X}: {:#010X})", + name, msr, value + ); + println!(" Active: {}", decode_reasons(value, bits, 0)); + println!( + " Logged: {}", + decode_reasons(value, bits, 16) + ); + // Bit 0 is the active and bit 16 the logged PROCHOT reason + prochot_claimed |= bit(value, 0) || bit(value, 16); + } + + // These MSRs and the THERM_STATUS ones should agree about PROCHOT. On + // Wildcat Lake they don't, so say so rather than let one of them be + // believed on its own. Which one is wrong is not established. + if prochot_claimed && !prochot_seen { + println!( + " Note: PROCHOT reported above is not corroborated by \ + THERM_STATUS ({:#X}/{:#X}), which shows no PROCHOT status or log", + MSR_IA32_THERM_STATUS, MSR_IA32_PACKAGE_THERM_STATUS + ); + } +} + +/// How long to sample APERF/MPERF over. Long enough to be stable, short +/// enough not to make --thermal feel sluggish. +const FREQ_SAMPLE_MICROS: u64 = 100_000; + +/// Print the frequency every core is currently running at +fn print_core_frequencies() { + let Some(tsc_mhz) = tsc_mhz() else { + debug!("Could not determine TSC frequency, skipping core frequencies"); + return; + }; + + let info = read_msr(0, MSR_PLATFORM_INFO).map(PlatformInfo::from); + let freqs = core_frequencies(tsc_mhz, FREQ_SAMPLE_MICROS); + if freqs.is_empty() { + return; + } + + println!( + " CPU Frequency (busy, {} ms sample)", + FREQ_SAMPLE_MICROS / 1000 + ); + if let Some(info) = info { + // Knowing where LFM and base are makes the numbers below meaningful. + // A core sitting at LFM is idle, not necessarily throttled. + println!( + " {:<20} {:>5} / {} MHz", + "LFM / Base:", + info.lfm_mhz(), + info.base_mhz() + ); + } + for (cpu, mhz) in freqs { + match mhz { + Some(mhz) => println!(" {:<20} {:>5} MHz", format!("CPU {}:", cpu), mhz), + None => println!(" {:<20} {:>5}", format!("CPU {}:", cpu), "Idle"), + } + } +} + +/// Print one PL1/PL2 style limit +fn print_limit(name: &str, limit: &PowerLimit) { + let mut notes = Vec::new(); + if !limit.enabled { + notes.push("Disabled".to_string()); + } + if limit.clamping { + notes.push("Clamping".to_string()); + } + if let Some(tau) = limit.tau { + notes.push(format!("{:.3} s tau", tau)); + } + println!( + " {:<20} {:>6.1} W {}", + format!("{}:", name), + limit.watts, + notes.join(", ") + ); +} + +/// Print the RAPL power limits, which is what the PkgPwr limiting reasons refer to +fn print_power_limits(cpuid: &CpuId) { + if !cpuid.has_rapl() { + debug!( + "No RAPL MSRs on family {:#X} model {:#X}", + cpuid.family, cpuid.model + ); + return; + } + let Some(units) = read_msr(0, MSR_PACKAGE_POWER_SKU_UNIT).map(RaplUnits::from) else { + return; + }; + + // Read once, the limits and the platform configuration below both need it + let platform = crate::pcode::platform_power(); + + println!(" Power Limits"); + if let Some(sku) = read_msr(0, MSR_PACKAGE_POWER_SKU) { + println!( + " {:<20} {:>6.1} W", + "TDP (base power):", + (sku & 0x7FFF) as f32 * units.power + ); + // Not every SKU reports the power range + let min = ((sku >> 16) & 0x7FFF) as f32 * units.power; + let max = ((sku >> 32) & 0x7FFF) as f32 * units.power; + if max > 0.0 { + println!( + " {:<20} {:>6.1} W (Min {:.1} W)", + "SKU Max Power:", max, min + ); + } + // The ceiling on the PL1 tau below. Anything longer gets clamped to it, + // so a tau that looks too short may simply be all this SKU allows. + let max_win = (sku >> 48) & 0x7F; + if max_win != 0 { + println!( + " {:<20} {:>6.1} s Longest averaging window this SKU allows", + "Max Tau:", + tau(max_win, units.time) + ); + } + } + + if let Some(raw) = read_msr(0, MSR_PACKAGE_RAPL_LIMIT) { + print_limit("PL1 (sustained)", &decode_limit(raw, 0, true, &units)); + print_limit("PL2 (burst)", &decode_limit(raw, 32, true, &units)); + if bit(raw, 63) { + println!(" {:<20} {:>6}", "PL1/PL2 Locked:", "Yes"); + } + } + + // These are not on pre-Skylake processors + if !cpuid.skylake_or_newer() { + return; + } + + if let Some(raw) = read_msr(0, MSR_PL3_CONTROL) { + print_pl3(raw, &units); + } + + if let Some(raw) = read_msr(0, MSR_VR_CURRENT_CONFIG) { + // The reference code describes the field in 0.125 A increments, but + // coreboot programs it as Watts scaled by the RAPL power unit (which + // is the same 1/8 by default) and so does the register description. + println!( + " {:<20} {:>6.1} W {}", + "PL4 (peak):", + (raw & 0xFFFF) as f32 * units.power, + if bit(raw, 31) { "Locked" } else { "" } + ); + } + + // PL1 and PL2 exist a second time in MCHBAR, in the same layout. Both + // copies are live and hold their own tau: Intel's Dual Tau Boost feature + // deliberately programs a higher PL1 with a shorter tau in MCHBAR and a + // lower PL1 with a longer tau in the MSR. On Linux this copy is also the one + // the OS gets at, as powercap's intel-rapl-mmio, so the MSR alone doesn't + // necessarily say what the package is being held to. + match platform.and_then(|power| power.package_rapl_limit) { + // Firmware programs this register alongside the MSR, so all zeroes means + // nothing is being limited through it + Some(0) => debug!("No power limits programmed in MCHBAR"), + Some(raw) => { + print_limit("MMIO PL1", &decode_limit(raw, 0, true, &units)); + print_limit("MMIO PL2", &decode_limit(raw, 32, true, &units)); + if bit(raw, 63) { + println!(" {:<20} {:>6}", "MMIO Locked:", "Yes"); + } + } + None => (), + } + + if let Some(raw) = read_msr(0, MSR_PLATFORM_POWER_LIMIT) { + // Whether PSys is enabled matters: it is the only limit that accounts + // for total platform power instead of just the package. With it off, + // nothing keeps the system inside the adapter budget proactively and + // the charger has to assert PROCHOT instead. So always report it, even + // (especially) when it's disabled. + print_limit("PSys PL1", &decode_limit(raw, 0, true, &units)); + // Bits 62:49 are reserved, PSys PL2 has no time window + print_limit("PSys PL2", &decode_limit(raw, 32, false, &units)); + if bit(raw, 63) { + println!(" {:<20} {:>6}", "PSys Locked:", "Yes"); + } + } + + // The full scale of all of the above PSys numbers, and the platform's + // current limits, aren't in MSRs. Report them next to the limits anyway, + // because the full scale is what decides whether they mean anything. + print_platform_power(platform, &units); +} + +/// Print PL3, the limit on how often the package may exceed a peak power +/// +/// PL3 doesn't cap power the way the other limits do. It allows the package to +/// exceed its power level for a fraction of the averaging window, so it takes a +/// duty cycle as well as a tau. Firmware leaves it disabled on our systems. +fn print_pl3(raw: u64, units: &RaplUnits) { + let mut notes = Vec::new(); + if bit(raw, 15) { + // Programmed in milliseconds, but encoded like every other tau + notes.push(format!( + "{:.3} s tau", + tau((raw >> 17) & 0x7F, DEFAULT_TIME_UNIT) + )); + notes.push(format!("{} % duty cycle", (raw >> 24) & 0x7F)); + // The reference code only documents this as how quickly pcode brings + // power back down, gradually or aggressively + notes.push( + if bit(raw, 16) { + "aggressive response" + } else { + "gradual response" + } + .to_string(), + ); + } else { + notes.push("Disabled".to_string()); + } + if bit(raw, 31) { + notes.push("Locked".to_string()); + } + println!( + " {:<20} {:>6.1} W {}", + "PL3 (occurrence):", + (raw & 0x7FFF) as f32 * units.power, + notes.join(", ") + ); +} + +/// Print the platform power delivery configuration that isn't in any MSR +/// +/// `units` is only needed for the Isys tau, which is encoded like a RAPL one. +fn print_platform_power(power: Option, units: &RaplUnits) { + let Some(power) = power else { + info!("{}", crate::pcode::unavailable_hint()); + return; + }; + + match power.psys { + Some(psys) if psys.pmax > 0.0 => { + println!( + " {:<20} {:>6.1} W Full scale of the PSys readings", + "PSys Pmax:", psys.pmax + ); + // Boards that don't need a correction leave both at Auto, which + // reads back as pcode's own no offset and unity slope + if let Some(correction) = psys.correction { + println!( + " {:<20} {:>6.2} x Offset {:.2} W", + "PSys Correction:", correction.slope, correction.offset + ); + } + } + Some(_) => println!( + " {:<20} {:>6} Firmware left it at the pcode default", + "PSys Pmax:", "Unset" + ), + None => debug!("pcode does not report a PSys full scale"), + } + + // Vsys Max and the Isys limits are only programmed when the platform uses + // the ThETA Ibatt feature to limit battery discharge current + match power.vsys_max { + Some(volts) if volts > 0.0 => { + println!(" {:<20} {:>6.1} V", "Vsys Max:", volts) + } + _ => debug!("No maximum system voltage programmed"), + } + + if let Some(isys) = power.isys { + if isys.l1_amps > 0.0 || isys.l2_amps > 0.0 { + print_isys_limit("Isys Limit L1", isys.l1_amps, isys.l1_enabled, { + Some(tau(isys.l1_tau, units.time)) + }); + print_isys_limit("Isys Limit L2", isys.l2_amps, isys.l2_enabled, None); + } else { + debug!("No Isys current limits programmed"); + } + } +} + +/// Print one of the two Isys current limits, in the style of [print_limit] +fn print_isys_limit(name: &str, amps: f32, enabled: bool, tau: Option) { + let mut notes = Vec::new(); + if !enabled { + notes.push("Disabled".to_string()); + } + if let Some(tau) = tau { + notes.push(format!("{:.3} s tau", tau)); + } + println!( + " {:<20} {:>6.1} A {}", + format!("{}:", name), + amps, + notes.join(", ") + ); +} + +/// Print how the processor is configured to react to PROCHOT +fn print_power_ctl() { + let Some(value) = read_msr(0, MSR_POWER_CTL) else { + return; + }; + println!(" PROCHOT Config ({:#X}: {:#010X})", MSR_POWER_CTL, value); + // Bidirectional PROCHOT lets the EC throttle the CPU by asserting PROCHOT# + println!(" Bidirectional: {}", yes_no(bit(value, 0))); + println!(" Output Enabled: {}", yes_no(!bit(value, 21))); + // The reference code only documents this as "Prochot Configurable + // Response Enable", so don't claim to know what the response is + println!( + " Response: {}", + if bit(value, 22) { + "Configurable" + } else { + "Throttle to minimum" + } + ); + println!(" VR Therm Alert: {}", yes_no(!bit(value, 24))); + println!(" Locked: {}", yes_no(bit(value, 23))); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + // The default units from the reference code are 1/8 W and 1/1024 s + fn rapl_units() { + let units = RaplUnits::from(0x000A_0E03); + assert_eq!(units.power, 0.125); + assert_eq!(units.time, 1.0 / 1024.0); + // 1/2^0 for both + let units = RaplUnits::from(0); + assert_eq!(units.power, 1.0); + assert_eq!(units.time, 1.0); + } + + #[test] + // Tau = (1 + X/4) * 2^Y, Y is bits 4:0 and X is bits 6:5 + fn rapl_tau() { + let units = RaplUnits::from(0x000A_0E03); + // The reset default of 0xA is Y=10, X=0, so 1024 * 1/1024 s + assert_eq!(tau(0x0A, units.time), 1.0); + // Y=10, X=2 gives 1.5 * 1024 * 1/1024 s + assert_eq!(tau(0x4A, units.time), 1.5); + assert_eq!(tau(0, units.time), 1.0 / 1024.0); + // The entries of the reference code's seconds conversion table, which + // only lines up with 1/1024 s units + assert_eq!(tau(0x4B, DEFAULT_TIME_UNIT), 3.0); + assert_eq!(tau(0x6E, DEFAULT_TIME_UNIT), 28.0); + assert_eq!(tau(0x71, DEFAULT_TIME_UNIT), 224.0); + // And of its milliseconds table, used for PL3 and RATL + assert_eq!(tau(0x41, DEFAULT_TIME_UNIT), 3.0 / 1024.0); + assert_eq!(tau(0x09, DEFAULT_TIME_UNIT), 0.5); + assert_eq!(tau(0x49, DEFAULT_TIME_UNIT), 0.75); + // The maximum time window MSR_PACKAGE_POWER_SKU defaults to + assert_eq!(tau(0x12, DEFAULT_TIME_UNIT), 256.0); + } + + #[test] + // PL1 28 W with a 1 s window and clamping, PL2 64 W with a 2.44 ms window + fn rapl_limit() { + let units = RaplUnits::from(0x000A_0E03); + let raw = 0x8042_8200_0015_80E0; + let pl1 = decode_limit(raw, 0, true, &units); + assert_eq!(pl1.watts, 28.0); + assert!(pl1.enabled); + assert!(pl1.clamping); + assert_eq!(pl1.tau, Some(1.0)); + + let pl2 = decode_limit(raw, 32, true, &units); + assert_eq!(pl2.watts, 64.0); + assert!(pl2.enabled); + assert!(!pl2.clamping); + // Y=1, X=1 gives 1.25 * 2 * 1/1024 s + assert_eq!(pl2.tau, Some(1.25 * 2.0 / 1024.0)); + + // Bit 63 is the lock + assert!(bit(raw, 63)); + // A limit without a tau field + assert_eq!(decode_limit(raw, 32, false, &units).tau, None); + } + + #[test] + // The Wildcat Lake value from a Framework 12: LFM 400, base 2000 MHz + fn platform_info() { + let info = PlatformInfo::from(0x0804043df8801400); + assert_eq!(info.base_ratio, 20); + assert_eq!(info.base_mhz(), 2000); + assert_eq!(info.max_efficiency_ratio, 4); + assert_eq!(info.lfm_mhz(), 400); + } + + #[test] + // PL4 is scaled by the RAPL power unit, like PL1 and PL2. 0x02E8 is the + // 93 W a Framework 13 reports. + fn rapl_pl4() { + let units = RaplUnits::from(0x000A_0E03); + assert_eq!((0x02E8 & 0xFFFF) as f32 * units.power, 93.0); + } + + #[test] + // TjMax 100 C with an 8 C TCC offset, as seen on a Framework 13 + fn temperature_target() { + let target = TemperatureTarget::from(0x0864_0000); + assert_eq!(target.ref_temp, 100); + assert_eq!(target.tcc_offset, 8); + assert_eq!(target.fan_temp_offset, 0); + assert!(!target.locked); + // Without a tau, RATL is off and the offset trips PROCHOT directly + assert!(!target.ratl()); + assert_eq!(target.tcc_activation(), 92); + } + + #[test] + // What coreboot leaves behind: the same 8 C offset, but with the low byte + // written as 0xE6, which is a 0.109 s tau (its "100 ms") plus clamping. The + // offset then only limits the average, so PROCHOT# trips at TjMax. + fn temperature_target_ratl() { + let target = TemperatureTarget::from(0x0864_00E6); + assert_eq!(target.ref_temp, 100); + assert_eq!(target.tcc_offset, 8); + assert!(target.ratl()); + assert!(target.tcc_offset_clamping); + assert_eq!(tau(target.ratl_tau, DEFAULT_TIME_UNIT), 112.0 / 1024.0); + assert_eq!(target.tcc_activation(), 100); + } + + #[test] + // PL3 25 W with a 4 ms tau at a 90 % duty cycle, enabled and unlocked + fn pl3() { + let units = RaplUnits::from(0x000A_0E03); + let raw = 0x5A04_80C8; + assert_eq!((raw & 0x7FFF) as f32 * units.power, 25.0); + assert!(bit(raw, 15)); + assert!(!bit(raw, 16)); + assert_eq!(tau((raw >> 17) & 0x7F, DEFAULT_TIME_UNIT), 4.0 / 1024.0); + assert_eq!((raw >> 24) & 0x7F, 90); + assert!(!bit(raw, 31)); + } + + #[test] + // A real package status read: 46 C, only the power limit log bit set + fn therm_status() { + let status = ThermStatus::from(0x8836_0800); + assert!(status.valid); + assert_eq!(status.readout, 54); + assert_eq!(status.resolution, 1); + assert_eq!(status.temp(100), Some(46)); + assert!(!status.throttling()); + // Bit 11 is the power limitation log + assert!(bit(status.raw, 11)); + + // Without the valid bit there is no temperature + assert_eq!(ThermStatus::from(0x0836_0800).temp(100), None); + // Bit 0 thermal monitor and bit 2 PROCHOT both mean throttling + assert!(ThermStatus::from(0x8000_0001).throttling()); + assert!(ThermStatus::from(0x8000_0004).throttling()); + // Log bits alone are not current throttling + assert!(!ThermStatus::from(0x8000_000A).throttling()); + } + + #[test] + // The values from a Framework 13 with PROCHOT logged on all three domains + fn perf_limit_reasons() { + assert_eq!(decode_reasons(0x1803_0000, PLR_CORE_BITS, 0), "None"); + assert_eq!( + decode_reasons(0x1803_0000, PLR_CORE_BITS, 16), + "PROCHOT, Thermal, PkgPwrPL2, MaxTurboLimit" + ); + assert_eq!( + decode_reasons(0x1001_0000, PLR_GT_BITS, 16), + "PROCHOT, InefficientOperation" + ); + assert_eq!(decode_reasons(0x0001_0000, PLR_RING_BITS, 16), "PROCHOT"); + // The ring domain has no bit 12/13, so those must not be decoded + assert_eq!(decode_reasons(0x3000_0000, PLR_RING_BITS, 16), "None"); + } +} diff --git a/framework_lib/src/pcode.rs b/framework_lib/src/pcode.rs new file mode 100644 index 0000000..88fe3d6 --- /dev/null +++ b/framework_lib/src/pcode.rs @@ -0,0 +1,583 @@ +//! Read platform power delivery configuration out of MCHBAR and pcode +//! +//! Some power management settings live neither in an MSR nor in a normal +//! register, but only inside pcode, which firmware configures through the BIOS +//! to pcode mailbox in MCHBAR. The interesting one for us is PMON PMAX, the +//! platform power that a full scale reading of the SoC's PSYS input stands for. +//! +//! The charger drives PSYS proportionally to total platform power (adapter plus +//! battery) and pcode maps that signal linearly onto PMAX. So PMAX is not the +//! AC adapter rating, even though the two are easily confused: it follows from +//! the charger's PSYS gain and the board's PSYS resistor. Get it wrong and +//! every platform power reading and every PSys power limit is off by the same +//! factor, in the direction that lets the platform draw more than the adapter +//! can supply until the charger asserts PROCHOT. +//! +//! coreboot programs it from the `psys_pmax_watts` devicetree register via the +//! FSP-M `PsysPmax` UPD. Left at 0, the FSP falls back to the value in the +//! SKU's PPM profile, which is 350 W for every Panther Lake profile and 0 +//! (keep whatever pcode defaults to) for Wildcat Lake. Reading it back is the +//! only way to tell which of those actually took effect. +//! +//! The PSys power limits themselves are in MSR_PLATFORM_POWER_LIMIT, see +//! [crate::msr], and so is PL4. What's left over and only reachable here is the +//! rest of the PSYS calibration (offset and slope), the maximum system voltage, +//! the Isys battery current limits, and the second copy of the package power +//! limits that lives in MCHBAR rather than in an MSR. +//! +//! Only Intel processors have any of this. +//! +//! References: +//! - Panther Lake reference code, `Library/PeiDxeSmmCpuMailboxLib`, +//! `Include/Register/B2pMailbox.h`, `PeiVrLib.c` and `PowerLimits.c` +//! - coreboot `src/soc/intel/pantherlake/systemagent.c` + +use crate::os_specific; + +/// MCHBAR, the base address register of the host bridge's MMIO range +const HOST_BRIDGE_MCHBAR: u32 = 0x48; +/// Bit 0 of MCHBAR, set while the range decodes +const MCHBAR_ENABLE: u64 = 1; +/// Address bits of MCHBAR +/// +/// The range is 32 KB on Skylake and 128 KB since Tiger Lake, so the low 15 +/// bits are reserved (and read as zero) on every processor we support. +const MCHBAR_ADDRESS_MASK: u64 = 0x0000_007F_FFFF_8000; + +/// The MCHBAR page we map, holding every register below +const WINDOW_BASE: u64 = 0x5000; +const WINDOW_LEN: usize = 0x1000; + +/// Data register of the BIOS to pcode mailbox +const PCODE_MAILBOX_DATA: u64 = 0x5DA0; +/// Interface register of the BIOS to pcode mailbox +const PCODE_MAILBOX_INTERFACE: u64 = 0x5DA4; +/// Isys (battery) current limits, the ThETA Ibatt feature. 64 bit. +const ISYS_CONTROL: u64 = 0x5E90; +/// PL1 and PL2 again, in the same layout as MSR_PACKAGE_RAPL_LIMIT. 64 bit. +const PACKAGE_RAPL_LIMIT: u64 = 0x59A0; + +/// Set by the caller to hand a command over, cleared by pcode when it's done +const MAILBOX_RUN_BUSY: u32 = 1 << 31; +/// How long to wait for pcode, in microseconds +/// +/// The same timeout that coreboot and the reference code use. +const MAILBOX_TIMEOUT_US: u32 = 1000; + +/// Read and write the SVID voltage regulator configuration +const MAILBOX_CMD_SVID_VR_HANDLER: u32 = 0x18; +/// Read the PSYS offset and slope correction +const MAILBOX_SUBCMD_GET_PMON_CONFIG: u32 = 0x19; +/// Read the PSYS full scale power +const MAILBOX_SUBCMD_GET_PMON_PMAX: u32 = 0x0A; +/// Read the maximum system voltage +const MAILBOX_SUBCMD_GET_VSYS_MAX: u32 = 0x28; + +/// What pcode leaves in the command field of the interface register when done +fn completion_code(code: u32) -> &'static str { + match code { + 0x0 => "Success", + 0x1 => "Illegal command", + 0x2 => "Timeout", + 0x3 => "Illegal data", + 0x5 => "Illegal VR ID", + 0x6 => "Locked", + 0x7 => "VR error", + 0x8 => "Illegal subcommand", + _ => "Unknown", + } +} + +// ------------------------------------------------------------------------- +// Decoding +// ------------------------------------------------------------------------- + +/// Decode unsigned fixed point with `fraction_bits` fractional bits +/// +/// The mailbox describes its fields as U16.x.y, so U10.6 is 10 integer and 6 +/// fractional bits out of the 16 bit field. +fn unsigned_fixed_point(field: u32, fraction_bits: u32) -> f32 { + (field & 0xFFFF) as f32 / (1u32 << fraction_bits) as f32 +} + +/// Decode signed (2's complement) fixed point with `fraction_bits` fractional bits +fn signed_fixed_point(field: u32, fraction_bits: u32) -> f32 { + ((field & 0xFFFF) as u16) as i16 as f32 / (1u32 << fraction_bits) as f32 +} + +/// The PSYS calibration that pcode applies to the charger's PSYS signal +#[derive(Debug, Clone, Copy)] +pub struct PsysConfig { + /// Platform power at full scale PSYS, in Watts. 0 if pcode has none. + /// + /// PMON PMAX, U10.6 fixed point, so up to 1024 W in 1/64 W steps. + pub pmax: f32, + /// Offset and slope correction of the PSYS reading + /// + /// `None` if pcode wouldn't tell us. Note that these read back as pcode's + /// own defaults (no offset, unity slope) rather than as unprogrammed when + /// firmware leaves both at Auto, in which case the FSP skips the write + /// altogether. + pub correction: Option, +} + +/// The offset and slope pcode applies to the PSYS reading before scaling it +#[derive(Debug, Clone, Copy)] +pub struct PsysCorrection { + /// Offset correction in Watts, S7.8 fixed point + pub offset: f32, + /// Slope correction, 1.0 being none, U1.15 fixed point + pub slope: f32, +} + +/// The Isys (battery) current limits of the ThETA Ibatt feature +/// +/// Two levels, each with a current limit in Amps, only the first one with a +/// time window. Firmware only programs these while on battery, so they read +/// back disabled on AC. +#[derive(Debug, Clone, Copy)] +pub struct IsysControl { + /// Level 1 limit in Amps, the field is in 1/8 A + pub l1_amps: f32, + pub l1_enabled: bool, + /// Raw 7 bit time window field of the level 1 limit + /// + /// Encoded like a RAPL time window, so decode it with the time unit from + /// MSR_PACKAGE_POWER_SKU_UNIT. + pub l1_tau: u64, + /// Level 2 limit in Amps, the field is in 1/8 A + pub l2_amps: f32, + pub l2_enabled: bool, +} + +/// Platform power delivery configuration, as firmware left it +#[derive(Debug, Clone, Copy)] +pub struct PlatformPower { + pub psys: Option, + /// Maximum system voltage in Volts, U10.6 fixed point + /// + /// Only programmed together with [IsysControl]. + pub vsys_max: Option, + pub isys: Option, + /// Raw MCHBAR copy of the package power limits + /// + /// The same fields as MSR_PACKAGE_RAPL_LIMIT, so decode it with + /// [crate::msr]. `None` if the register didn't read back plausibly. + pub package_rapl_limit: Option, +} + +// ------------------------------------------------------------------------- +// MCHBAR and the pcode mailbox +// ------------------------------------------------------------------------- + +/// A mapping of the MCHBAR page holding the registers we're after +struct Mchbar { + map: imp::Mapping, +} + +impl Mchbar { + fn open() -> Option { + let mchbar = imp::host_bridge_read64(HOST_BRIDGE_MCHBAR)?; + if mchbar & MCHBAR_ENABLE == 0 { + info!("MCHBAR is not enabled ({:#X}), cannot reach pcode", mchbar); + return None; + } + let base = (mchbar & MCHBAR_ADDRESS_MASK) + WINDOW_BASE; + debug!("MCHBAR {:#X}, mapping {:#X}", mchbar, base); + Some(Self { + map: imp::Mapping::new(base, WINDOW_LEN)?, + }) + } + + fn read32(&self, offset: u64) -> u32 { + self.map.read32((offset - WINDOW_BASE) as usize) + } + + fn write32(&self, offset: u64, value: u32) { + self.map.write32((offset - WINDOW_BASE) as usize, value) + } + + /// Read a 64 bit register as its two halves + /// + /// Everything we read this way is static configuration, so it doesn't + /// matter that the two halves aren't sampled at the same instant. + fn read64(&self, offset: u64) -> u64 { + ((self.read32(offset + 4) as u64) << 32) | (self.read32(offset) as u64) + } + + /// Wait for pcode to release the mailbox + fn poll_mailbox_ready(&self) -> bool { + for _ in 0..MAILBOX_TIMEOUT_US { + if self.read32(PCODE_MAILBOX_INTERFACE) & MAILBOX_RUN_BUSY == 0 { + return true; + } + os_specific::sleep(1); + } + false + } + + /// Run one pcode mailbox read command and return the data register + /// + /// Read commands only need the interface register written, pcode puts the + /// result in the data register. That write is a doorbell though, so this is + /// not a passive read: it hands a command to pcode the same way firmware + /// does. Nothing in the OS uses this mailbox, but we still follow the + /// firmware protocol and wait for it to go idle before claiming it. + /// + /// The reference code only runs mailbox commands on the bootstrap + /// processor. MCHBAR is package scoped, so it doesn't matter which core we + /// happen to be on. That rule is about serializing users of the one mailbox. + fn mailbox_read(&self, command: u32, param1: u32, param2: u32) -> Option { + if !self.poll_mailbox_ready() { + error!("pcode mailbox is busy"); + return None; + } + + let interface = (command & 0xFF) + | ((param1 & 0xFF) << 8) + | ((param2 & 0x1FFF) << 16) + | MAILBOX_RUN_BUSY; + debug!("pcode mailbox command {:#010X}", interface); + self.write32(PCODE_MAILBOX_INTERFACE, interface); + + if !self.poll_mailbox_ready() { + error!("pcode mailbox command {:#010X} did not complete", interface); + return None; + } + + // pcode replaces the command field with the completion code + let code = self.read32(PCODE_MAILBOX_INTERFACE) & 0xFF; + if code != 0 { + info!( + "pcode rejected mailbox command {:#010X}: {} ({:#X})", + interface, + completion_code(code), + code + ); + return None; + } + + let data = self.read32(PCODE_MAILBOX_DATA); + debug!("pcode mailbox data {:#010X}", data); + Some(data) + } + + fn vr(&self, subcommand: u32) -> Option { + self.mailbox_read(MAILBOX_CMD_SVID_VR_HANDLER, subcommand, 0) + } +} + +/// Read back the platform power delivery configuration firmware programmed +/// +/// `None` if we can't reach MCHBAR at all, see [unavailable_hint]. The +/// individual values are `None` when pcode refused the command, which is how a +/// processor without that particular knob answers. +pub fn platform_power() -> Option { + let mchbar = Mchbar::open()?; + + let psys = mchbar + .vr(MAILBOX_SUBCMD_GET_PMON_PMAX) + .map(|pmax| PsysConfig { + pmax: unsigned_fixed_point(pmax, 6), + // A separate command, and only interesting alongside a full scale, so + // don't fail the whole thing over it + correction: mchbar + .vr(MAILBOX_SUBCMD_GET_PMON_CONFIG) + .map(|config| PsysCorrection { + offset: signed_fixed_point(config, 8), + slope: unsigned_fixed_point(config >> 16, 15), + }), + }); + + let vsys_max = mchbar + .vr(MAILBOX_SUBCMD_GET_VSYS_MAX) + .map(|data| unsigned_fixed_point(data, 6)); + + let raw = mchbar.read64(ISYS_CONTROL); + debug!("ISYS_CONTROL ({:#X}): {:#018X}", ISYS_CONTROL, raw); + let isys = Some(IsysControl { + l1_amps: (raw & 0x7FFF) as f32 / 8.0, + l1_enabled: raw & (1 << 15) != 0, + l1_tau: (raw >> 16) & 0x7F, + l2_amps: ((raw >> 32) & 0x7FFF) as f32 / 8.0, + l2_enabled: raw & (1 << 47) != 0, + }); + + let raw = mchbar.read64(PACKAGE_RAPL_LIMIT); + debug!( + "PACKAGE_RAPL_LIMIT ({:#X}): {:#018X}", + PACKAGE_RAPL_LIMIT, raw + ); + // An MMIO read that doesn't decode comes back as all ones, which is not a + // limit register anyone could have programmed + let package_rapl_limit = if raw == u64::MAX { None } else { Some(raw) }; + + Some(PlatformPower { + psys, + vsys_max, + isys, + package_rapl_limit, + }) +} + +/// Why we can't reach MCHBAR on this platform, for logging +pub fn unavailable_hint() -> &'static str { + imp::unavailable_hint() +} + +// Linux gives us the host bridge's config space through sysfs and physical +// memory through /dev/mem. Both need root. +#[cfg(all(target_os = "linux", any(target_arch = "x86", target_arch = "x86_64")))] +mod imp { + use std::fs::{File, OpenOptions}; + use std::io::{Read, Seek, SeekFrom}; + use std::os::unix::io::AsRawFd; + use std::ptr; + + const HOST_BRIDGE_CONFIG: &str = "/sys/bus/pci/devices/0000:00:00.0/config"; + + /// Read a 64 bit register of the host bridge's PCI configuration space + /// + /// Anything above the standard 64 byte header needs CAP_SYS_ADMIN, so this + /// only works as root. + pub fn host_bridge_read64(offset: u32) -> Option { + let mut file = match File::open(HOST_BRIDGE_CONFIG) { + Ok(file) => file, + Err(err) => { + info!("Cannot open {}: {}", HOST_BRIDGE_CONFIG, err); + return None; + } + }; + let mut buf = [0u8; 8]; + if let Err(err) = file + .seek(SeekFrom::Start(offset as u64)) + .and_then(|_| file.read_exact(&mut buf)) + { + info!("Cannot read host bridge config {:#X}: {}", offset, err); + return None; + } + Some(u64::from_le_bytes(buf)) + } + + /// A mapping of physical memory, for volatile 32 bit register access + pub struct Mapping { + page: *mut libc::c_void, + page_len: usize, + /// Where the requested address ended up inside the mapping + offset: usize, + len: usize, + } + + impl Mapping { + /// Map `len` bytes of physical memory starting at `phys` + pub fn new(phys: u64, len: usize) -> Option { + // SAFETY: sysconf() with a valid name has no preconditions + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64; + let page_base = phys & !(page_size - 1); + let offset = (phys - page_base) as usize; + let page_len = (((offset + len) as u64).div_ceil(page_size) * page_size) as usize; + + let file = match OpenOptions::new().read(true).write(true).open("/dev/mem") { + Ok(file) => file, + Err(err) => { + info!("Cannot open /dev/mem: {}", err); + return None; + } + }; + + // SAFETY: A null hint lets the kernel pick the address and the + // length is a multiple of the page size. The mapping is only used + // through self.reg(), which stays inside it. + let page = unsafe { + libc::mmap( + ptr::null_mut(), + page_len, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + file.as_raw_fd(), + page_base as libc::off_t, + ) + }; + if page == libc::MAP_FAILED { + error!( + "Cannot map {:#X} from /dev/mem: {}", + page_base, + std::io::Error::last_os_error() + ); + return None; + } + + Some(Self { + page, + page_len, + offset, + len, + }) + } + + fn reg(&self, offset: usize) -> *mut u32 { + assert!(offset + 4 <= self.len); + // SAFETY: Asserted to be inside the mapping + unsafe { (self.page as *mut u8).add(self.offset + offset) as *mut u32 } + } + + pub fn read32(&self, offset: usize) -> u32 { + // SAFETY: reg() returns an aligned pointer into the mapping + unsafe { ptr::read_volatile(self.reg(offset)) } + } + + pub fn write32(&self, offset: usize, value: u32) { + // SAFETY: reg() returns an aligned pointer into the mapping + unsafe { ptr::write_volatile(self.reg(offset), value) } + } + } + + impl Drop for Mapping { + fn drop(&mut self) { + // SAFETY: Unmapping exactly what new() mapped + unsafe { + libc::munmap(self.page, self.page_len); + } + } + } + + pub fn unavailable_hint() -> &'static str { + "Must be root to read MCHBAR" + } +} + +#[cfg(all(target_os = "uefi", any(target_arch = "x86", target_arch = "x86_64")))] +mod imp { + use core::arch::asm; + use core::ptr; + + /// Address and data port of legacy PCI configuration space access + const PCI_CONFIG_ADDRESS: u16 = 0xCF8; + const PCI_CONFIG_DATA: u16 = 0xCFC; + + /// Read a 32 bit register of the host bridge (bus 0, device 0, function 0) + fn host_bridge_read32(offset: u32) -> u32 { + // Bit 31 enables the configuration cycle, bus/device/function are 0 + let address = 0x8000_0000 | (offset & 0xFC); + let value: u32; + // SAFETY: Reading PCI configuration space has no side effects + unsafe { + asm!( + "out dx, eax", + in("dx") PCI_CONFIG_ADDRESS, + in("eax") address, + options(nomem, nostack, preserves_flags), + ); + asm!( + "in eax, dx", + in("dx") PCI_CONFIG_DATA, + out("eax") value, + options(nomem, nostack, preserves_flags), + ); + } + value + } + + pub fn host_bridge_read64(offset: u32) -> Option { + let lo = host_bridge_read32(offset); + let hi = host_bridge_read32(offset + 4); + Some(((hi as u64) << 32) | (lo as u64)) + } + + /// Physical memory is identity mapped in UEFI, so there's nothing to map + pub struct Mapping { + base: u64, + len: usize, + } + + impl Mapping { + pub fn new(phys: u64, len: usize) -> Option { + Some(Self { base: phys, len }) + } + + fn reg(&self, offset: usize) -> *mut u32 { + assert!(offset + 4 <= self.len); + (self.base as usize + offset) as *mut u32 + } + + pub fn read32(&self, offset: usize) -> u32 { + // SAFETY: MCHBAR is identity mapped MMIO, so the address is valid + unsafe { ptr::read_volatile(self.reg(offset)) } + } + + pub fn write32(&self, offset: usize, value: u32) { + // SAFETY: MCHBAR is identity mapped MMIO, so the address is valid + unsafe { ptr::write_volatile(self.reg(offset), value) } + } + } + + pub fn unavailable_hint() -> &'static str { + "Cannot reach MCHBAR" + } +} + +// Windows needs a kernel driver for MMIO and we don't ship one. FreeBSD has no +// sysfs to get MCHBAR from. Also covers non-x86, which has no MCHBAR at all. +#[cfg(not(any( + all(target_os = "linux", any(target_arch = "x86", target_arch = "x86_64")), + all(target_os = "uefi", any(target_arch = "x86", target_arch = "x86_64")), +)))] +mod imp { + pub fn host_bridge_read64(_offset: u32) -> Option { + None + } + + pub struct Mapping; + + impl Mapping { + pub fn new(_phys: u64, _len: usize) -> Option { + None + } + + pub fn read32(&self, _offset: usize) -> u32 { + 0 + } + + pub fn write32(&self, _offset: usize, _value: u32) {} + } + + pub fn unavailable_hint() -> &'static str { + "Reading MCHBAR is not supported on this platform" + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pmon_pmax() { + // What coreboot's psys_pmax_watts = 218 ends up as: 218 W in 1/8 W + // units through the FSP, then U10.6 fixed point in the mailbox + assert_eq!(unsigned_fixed_point(218 * 64, 6), 218.0); + // The 350 W that every Panther Lake PPM profile defaults to + assert_eq!(unsigned_fixed_point(2800 * 8, 6), 350.0); + // Fractional and unprogrammed + assert_eq!(unsigned_fixed_point(0x0020, 6), 0.5); + assert_eq!(unsigned_fixed_point(0, 6), 0.0); + // Reserved upper bits are ignored, the field is 16 bits + assert_eq!(unsigned_fixed_point(0xFFFF_0000 | (100 * 64), 6), 100.0); + } + + #[test] + fn test_vsys_max() { + // The 24000 mV that every Panther Lake PPM profile defaults to + assert_eq!(unsigned_fixed_point(24000 * 64 / 1000, 6), 24.0); + } + + #[test] + fn test_pmon_config() { + // U1.15 slope correction, the reference code's example of 125 (1.25) + assert_eq!(unsigned_fixed_point(125 * 32768 / 100, 15), 1.25); + assert_eq!(unsigned_fixed_point(0, 15), 0.0); + // S7.8 offset, the reference code's example of 25348 (25.348 W), which + // 8 fractional bits can only get within 1/256 of + assert_eq!(signed_fixed_point(25348 * 256 / 1000, 8), 25.347656); + assert_eq!(signed_fixed_point(0xFF00, 8), -1.0); + } +} diff --git a/framework_lib/src/power.rs b/framework_lib/src/power.rs index 0453165..e7ea461 100644 --- a/framework_lib/src/power.rs +++ b/framework_lib/src/power.rs @@ -13,6 +13,7 @@ use crate::ccgx::{AppVersion, Application, BaseVersion, ControllerVersion, MainP use crate::chromium_ec::command::EcRequestRaw; use crate::chromium_ec::commands::*; use crate::chromium_ec::*; +use crate::msr; use crate::smbios; use crate::util::PlatformFamily; @@ -530,6 +531,8 @@ pub fn print_thermal(ec: &CrosEc) { } else { println!(" Unknown"); } + + msr::print_thermal_msrs(); } pub fn get_fan_num(ec: &CrosEc) -> EcResult {