feat(power): redbear-k10tempd (Family 19h thermal)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# redbear-k10tempd — Task 37 Evidence
|
||||
# AMD Family 19h Thermal Sensor Driver (PCI 1022:14e3, DF F3)
|
||||
# Generated: 2026-08-05
|
||||
|
||||
## Files Created / Modified
|
||||
|
||||
### New files
|
||||
- local/recipes/drivers/redbear-k10tempd/recipe.toml
|
||||
- local/recipes/drivers/redbear-k10tempd/source/Cargo.toml
|
||||
- local/recipes/drivers/redbear-k10tempd/source/src/main.rs (daemon main + scheme server + poll loop)
|
||||
- local/recipes/drivers/redbear-k10tempd/source/src/register.rs (register decode + Tctl offset + 40 tests)
|
||||
- recipes/drivers/redbear-k10tempd -> ../../local/recipes/drivers/redbear-k10tempd (symlink)
|
||||
- local/config/drivers.d/30-platform.toml
|
||||
|
||||
### Modified files
|
||||
- config/redbear-mini.toml (+ redbear-k10tempd = {} in [packages], line 81)
|
||||
- config/redbear-device-services.toml (+ embedded 30-platform.toml under /lib/drivers.d/)
|
||||
|
||||
## Crate Layout
|
||||
```
|
||||
local/recipes/drivers/redbear-k10tempd/
|
||||
├── recipe.toml (cargo template, path = "source", version = "0.3.2")
|
||||
└── source/
|
||||
├── Cargo.toml (edition 2024, path deps to local/sources/{syscall,libredox,redox-scheme})
|
||||
└── src/
|
||||
├── main.rs (daemon entry point, PCI SMN read, scheme:k10temp server, poll loop)
|
||||
└── register.rs (THM_TCON_CUR_TMP decode, Tctl offset table, implausible-reading rejection)
|
||||
```
|
||||
|
||||
## Register Model
|
||||
- SMN address: 0x00059800 (THM_TCON_CUR_TMP)
|
||||
- PCI config-space access via DF F3 (SMN_ADDR at offset 0x60, SMN_DATA at offset 0x64)
|
||||
- CUR_TEMP field: bits [23:14] (10 bits), each unit = 0.125°C
|
||||
- Tctl offset: 0 for Zen 4 (Fam 19h M60h-6Fh), -49°C for Zen+ (Fam 17h M10h-2Fh)
|
||||
- Validation bounds: -20°C to 115°C (physically implausible rejection)
|
||||
- Rejection: CUR_TEMP=0 (sensor not ready), CUR_TEMP=0x3FF (saturated), out of bounds
|
||||
|
||||
## Tctl Offset Table
|
||||
| Family / Model range | Tctl offset (°C) | Notes |
|
||||
|----------------------------|------------------|----------------------|
|
||||
| 17h 00h-0Fh (Zen 1) | 0 | |
|
||||
| 17h 10h-2Fh (Zen+) | -49 | Tctl = raw - 49 |
|
||||
| 17h 30h-7Fh (Zen 2) | 0 | |
|
||||
| 19h all (Zen 3/4) | 0 | 1022:14e3 = this row |
|
||||
| 1Ah (Zen 5) | 0 | |
|
||||
|
||||
## Scheme Contract (consumer: redbear-power sensor.rs:396-399)
|
||||
- Scheme name: "k10temp" at /scheme/k10temp/
|
||||
- Files: /scheme/k10temp/tctl (m°C), /scheme/k10temp/tctl_c (°C), /scheme/k10temp/summary
|
||||
- Consumer matches: chip.name == "k10temp", reading.kind == Temp, reading.label == "Tctl"
|
||||
- raw_value published in millidegree Celsius (divided by 1000 for °C display)
|
||||
|
||||
## Driver-Manager Registration
|
||||
- File: local/config/drivers.d/30-platform.toml
|
||||
- [[driver]] name = "redbear-k10tempd", priority = 60, command = ["/usr/bin/redbear-k10tempd"]
|
||||
- [[driver.match]] vendor = 0x1022, device = [0x14E3]
|
||||
- Embedded copy: config/redbear-device-services.toml under /lib/drivers.d/30-platform.toml
|
||||
|
||||
## Config Inclusion
|
||||
- config/redbear-mini.toml: [packages] redbear-k10tempd = {} (line 81)
|
||||
- Full target inherits via redbear-mini include chain
|
||||
|
||||
## Tests (host: cargo test --manifest-path .../Cargo.toml)
|
||||
- 40 tests, ALL PASSED (0 failed)
|
||||
- register::tests:
|
||||
27 register-decode tests (extract_cur_temp, cur_temp_to_millidegrees, range_sel)
|
||||
7 Tctl offset table tests (per-family, boundary values)
|
||||
9 compute_tctl_mc tests (Zen 4 typical/idle/load, Zen+ offset, rejection)
|
||||
3 CpuModel::is_supported tests
|
||||
4 integration tests (full load range 30-95°C, degraded silicon 100°C, cold boot 25°C)
|
||||
|
||||
## What Was NOT Done (per task constraints)
|
||||
- No builds (host cargo test only)
|
||||
- No commits
|
||||
- No touching local/sources/* forks
|
||||
- No touching redbear-power source (read-only for sensor.rs contract)
|
||||
- No Linux sysfs assumptions (pure Redox scheme path)
|
||||
- No hardcoded per-model quirks (uses family/model table in CpuModel)
|
||||
|
||||
## Gate B Readiness
|
||||
- Runtime verification (sane 30-95°C readout in redbear-power) rides Gate B
|
||||
- Consumer (redbear-power sensor.rs:396-399) reads k10temp Tctl via scheme
|
||||
- The daemon will publish Tctl on /scheme/k10temp/ at runtime
|
||||
@@ -0,0 +1,13 @@
|
||||
# Platform / hwmon sensor drivers
|
||||
|
||||
[[driver]]
|
||||
name = "redbear-k10tempd"
|
||||
description = "AMD k10temp thermal sensor (Family 19h Zen 4)"
|
||||
priority = 60
|
||||
command = ["/usr/bin/redbear-k10tempd"]
|
||||
|
||||
[[driver.match]]
|
||||
vendor = 0x1022
|
||||
device = [0x14E3]
|
||||
# 1022:14e3 = Family 19h Models 60h-6Fh Data Fabric Function 3
|
||||
# (THM thermal sensor interface)
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "redbear-k10tempd"
|
||||
version = "0.3.2"
|
||||
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "redbear-k10tempd"
|
||||
version = "0.3.2"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-k10tempd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
redox-scheme = { path = "../../../../../local/sources/redox-scheme" }
|
||||
syscall = { path = "../../../../../local/sources/syscall", package = "redox_syscall", features = ["std"] }
|
||||
|
||||
[patch.crates-io]
|
||||
redox-scheme = { path = "../../../../../local/sources/redox-scheme" }
|
||||
redox_syscall = { path = "../../../../../local/sources/syscall" }
|
||||
@@ -0,0 +1,512 @@
|
||||
//! `redbear-k10tempd` — AMD k10temp thermal sensor daemon
|
||||
//!
|
||||
//! Reads AMD Zen CPU package temperature (Tctl) via the SMN (System
|
||||
//! Management Network) interface through PCI config space of Data Fabric
|
||||
//! Function 3 (DF F3, PCI device 1022:14e3 for Family 19h Model 60h-6Fh).
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! ```
|
||||
//! driver-manager (matches PCI 1022:14e3)
|
||||
//! → spawns redbear-k10tempd --bdf 00:18.3
|
||||
//! → opens /scheme/pci/00:18.3/config
|
||||
//! → writes SMN address 0x00059800 at PCI offset 0x60
|
||||
//! → reads 32-bit SMN data at PCI offset 0x64
|
||||
//! → extracts CUR_TEMP bits [20:14]
|
||||
//! → computes Tctl = CUR_TEMP * 0.125°C + family/model offset
|
||||
//! → publishes on /scheme/k10temp/tctl (millidegree Celsius)
|
||||
//! ```
|
||||
//!
|
||||
//! ## PCI Config-Space Access
|
||||
//!
|
||||
//! The SMN is accessed indirectly through DF F3 PCI config registers:
|
||||
//! - offset 0x60: write the 32-bit SMN address
|
||||
//! - offset 0x64: read/write 32-bit SMN data
|
||||
//!
|
||||
//! On Redox, PCI config space is exposed as a file at
|
||||
//! `/scheme/pci/<bus>:<device>.<function>/config`. Standard file I/O
|
||||
//! (`pread`/`pwrite` via `seek`+`read`/`write`) reads and writes
|
||||
//! PCI config-space registers at the given byte offset.
|
||||
//!
|
||||
//! ## Scheme Contract
|
||||
//!
|
||||
//! The daemon registers `/scheme/k10temp/` with the following layout:
|
||||
//!
|
||||
//! ```
|
||||
//! /scheme/k10temp/
|
||||
//! summary — "chip=k10temp Tctl=<m°C> Tctl_c=<°C>\n"
|
||||
//! tctl — temperature in millidegree Celsius as a decimal string
|
||||
//! tctl_c — temperature in °C as a decimal string
|
||||
//! ```
|
||||
//!
|
||||
//! The consumer (`redbear-power/sensor.rs`) matches:
|
||||
//! - `chip.name == "k10temp"`
|
||||
//! - `reading.kind == SensorKind::Temp`
|
||||
//! - `reading.label == Some("Tctl")`
|
||||
//! - `reading.raw_value` is in m°C (divided by 1000 for display)
|
||||
//!
|
||||
//! The scheme name `k10temp` and label `Tctl` are the contract surface.
|
||||
//!
|
||||
//! ## Poll Interval
|
||||
//!
|
||||
//! Temperature is read every 1 second (matching the Linux k10temp driver's
|
||||
//! `update_interval` of 1000ms). Thermal management doesn't benefit from
|
||||
//! faster polling; silicon thermal mass is too large for sub-second changes.
|
||||
|
||||
mod register;
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write as IoWrite};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{info, warn, LevelFilter, Metadata, Record};
|
||||
|
||||
use register::{compute_tctl_mc, CpuModel, PCI_SMN_ADDR, PCI_SMN_DATA, SMN_THM_TCON_CUR_TMP};
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use log::error;
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// ── Redox-specific imports ────────────────────────────────────────
|
||||
#[cfg(target_os = "redox")]
|
||||
use redox_scheme::{
|
||||
scheme::{SchemeState, SchemeSync},
|
||||
CallerCtx, OpenResult, SignalBehavior, Socket,
|
||||
};
|
||||
#[cfg(target_os = "redox")]
|
||||
use syscall::flag::{MODE_DIR, MODE_FILE};
|
||||
#[cfg(target_os = "redox")]
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
#[cfg(target_os = "redox")]
|
||||
use syscall::{
|
||||
error::{Error as SysError, Result as SysResult, EBADF, EINVAL, ENOENT},
|
||||
Stat,
|
||||
};
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────
|
||||
|
||||
/// Poll interval: 1 second (matching Linux k10temp update_interval).
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
/// Default DF F3 BDF when not passed by driver-manager.
|
||||
/// DF F3 is always at bus 0, device 0x18, function 3 on AMD platforms.
|
||||
const DEFAULT_BDF: &str = "00:18.3";
|
||||
|
||||
/// Default CPU model when CPUID detection is unavailable (host testing).
|
||||
const DEFAULT_MODEL: CpuModel = CpuModel::FAM19H_M60H;
|
||||
|
||||
// ── Logger ────────────────────────────────────────────────────────
|
||||
|
||||
struct StderrLogger;
|
||||
|
||||
impl log::Log for StderrLogger {
|
||||
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
|
||||
metadata.level() <= LevelFilter::Info
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record<'_>) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let _ = writeln!(
|
||||
io::stderr().lock(),
|
||||
"[{}] redbear-k10tempd: {}",
|
||||
record.level(),
|
||||
record.args()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
// ── PCI config-space I/O ──────────────────────────────────────────
|
||||
|
||||
/// Read a 32-bit value from PCI config space at the given byte offset.
|
||||
///
|
||||
/// On Redox, this opens `/scheme/pci/<bdf>/config`, seeks to the offset,
|
||||
/// and reads 4 bytes.
|
||||
fn pci_config_read32(bdf: &str, offset: u64) -> Option<u32> {
|
||||
let path = format!("/scheme/pci/{bdf}/config");
|
||||
let mut file = fs::File::open(&path).ok()?;
|
||||
file.seek(SeekFrom::Start(offset)).ok()?;
|
||||
let mut buf = [0u8; 4];
|
||||
file.read_exact(&mut buf).ok()?;
|
||||
Some(u32::from_le_bytes(buf))
|
||||
}
|
||||
|
||||
/// Write a 32-bit value to PCI config space at the given byte offset.
|
||||
fn pci_config_write32(bdf: &str, offset: u64, value: u32) -> Option<()> {
|
||||
let path = format!("/scheme/pci/{bdf}/config");
|
||||
let mut file = fs::File::create(&path).ok()?;
|
||||
file.seek(SeekFrom::Start(offset)).ok()?;
|
||||
file.write_all(&value.to_le_bytes()).ok()
|
||||
}
|
||||
|
||||
/// Read a 32-bit SMN register through DF F3 PCI config space.
|
||||
///
|
||||
/// 1. Write SMN address to PCI offset 0x60
|
||||
/// 2. Read 32-bit data from PCI offset 0x64
|
||||
fn smn_read32(bdf: &str, smn_addr: u32) -> Option<u32> {
|
||||
pci_config_write32(bdf, PCI_SMN_ADDR, smn_addr)?;
|
||||
pci_config_read32(bdf, PCI_SMN_DATA)
|
||||
}
|
||||
|
||||
/// Read the current Tctl temperature from the SMN THM register.
|
||||
///
|
||||
/// Returns the raw 32-bit THM_TCON_CUR_TMP register value.
|
||||
fn read_tctl_raw(bdf: &str) -> Option<u32> {
|
||||
smn_read32(bdf, SMN_THM_TCON_CUR_TMP)
|
||||
}
|
||||
|
||||
// ── Temperature readout ───────────────────────────────────────────
|
||||
|
||||
/// Reads Tctl and returns the decoded temperature in millidegree Celsius,
|
||||
/// or `None` if the reading is implausible.
|
||||
fn read_tctl_mc(bdf: &str, model: CpuModel) -> Option<i64> {
|
||||
let raw = read_tctl_raw(bdf)?;
|
||||
match compute_tctl_mc(raw, model) {
|
||||
Ok(tctl_mc) => Some(tctl_mc),
|
||||
Err(err) => {
|
||||
warn!("Tctl read rejected: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scheme server ─────────────────────────────────────────────────
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
const SCHEME_ROOT_ID: usize = 1;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Clone, Debug)]
|
||||
enum HandleKind {
|
||||
Root,
|
||||
Tctl,
|
||||
TctlC,
|
||||
Summary,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
struct K10tempScheme {
|
||||
bdf: String,
|
||||
model: CpuModel,
|
||||
tctl_mc: Arc<RwLock<Option<i64>>>,
|
||||
next_id: usize,
|
||||
handles: BTreeMap<usize, HandleKind>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl K10tempScheme {
|
||||
fn new(bdf: String, model: CpuModel, tctl_mc: Arc<RwLock<Option<i64>>>) -> Self {
|
||||
Self {
|
||||
bdf,
|
||||
model,
|
||||
tctl_mc,
|
||||
next_id: SCHEME_ROOT_ID + 1,
|
||||
handles: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn alloc_handle(&mut self, kind: HandleKind) -> usize {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.handles.insert(id, kind);
|
||||
id
|
||||
}
|
||||
|
||||
fn handle(&self, id: usize) -> SysResult<&HandleKind> {
|
||||
self.handles.get(&id).ok_or(SysError::new(EBADF))
|
||||
}
|
||||
|
||||
fn is_dir(kind: &HandleKind) -> bool {
|
||||
matches!(kind, HandleKind::Root)
|
||||
}
|
||||
|
||||
fn read_file(&self, kind: &HandleKind) -> Option<String> {
|
||||
let tctl = self.tctl_mc.read().ok()?;
|
||||
match kind {
|
||||
HandleKind::Root => None,
|
||||
HandleKind::Tctl => tctl.map(|v| format!("{v}\n")),
|
||||
HandleKind::TctlC => tctl.map(|v| format!("{:.1}\n", v as f64 / 1000.0)),
|
||||
HandleKind::Summary => {
|
||||
let line = if let Some(tctl_mc) = *tctl {
|
||||
format!(
|
||||
"chip=k10temp Tctl={tctl_mc} Tctl_c={:.1}\n",
|
||||
tctl_mc as f64 / 1000.0,
|
||||
)
|
||||
} else {
|
||||
"chip=k10temp Tctl=na\n".to_string()
|
||||
};
|
||||
Some(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(&self, path: &str) -> SysResult<HandleKind> {
|
||||
let trimmed = path.trim_matches('/');
|
||||
match trimmed {
|
||||
"" => Ok(HandleKind::Root),
|
||||
"tctl" => Ok(HandleKind::Tctl),
|
||||
"tctl_c" => Ok(HandleKind::TctlC),
|
||||
"summary" => Ok(HandleKind::Summary),
|
||||
_ => Err(SysError::new(ENOENT)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
impl SchemeSync for K10tempScheme {
|
||||
fn scheme_root(&mut self) -> SysResult<usize> {
|
||||
Ok(SCHEME_ROOT_ID)
|
||||
}
|
||||
|
||||
fn openat(
|
||||
&mut self,
|
||||
dirfd: usize,
|
||||
path: &str,
|
||||
_flags: usize,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> SysResult<OpenResult> {
|
||||
let kind = if dirfd == SCHEME_ROOT_ID {
|
||||
self.resolve(path)?
|
||||
} else {
|
||||
return Err(SysError::new(EINVAL));
|
||||
};
|
||||
|
||||
Ok(OpenResult::ThisScheme {
|
||||
number: self.alloc_handle(kind),
|
||||
flags: NewFdFlags::POSITIONED,
|
||||
})
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> SysResult<()> {
|
||||
let kind = if id == SCHEME_ROOT_ID {
|
||||
HandleKind::Root
|
||||
} else {
|
||||
self.handle(id)?.clone()
|
||||
};
|
||||
stat.st_mode = if Self::is_dir(&kind) {
|
||||
MODE_DIR
|
||||
} else {
|
||||
MODE_FILE
|
||||
};
|
||||
stat.st_size = match self.read_file(&kind) {
|
||||
Some(content) => content.len() as u64,
|
||||
None => 0,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
id: usize,
|
||||
buf: &mut [u8],
|
||||
offset: u64,
|
||||
_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> SysResult<usize> {
|
||||
let kind = self.handle(id)?.clone();
|
||||
if Self::is_dir(&kind) {
|
||||
return Err(SysError::new(EINVAL));
|
||||
}
|
||||
let Some(content) = self.read_file(&kind) else {
|
||||
return Err(SysError::new(ENOENT));
|
||||
};
|
||||
let bytes = content.as_bytes();
|
||||
let Ok(offset) = usize::try_from(offset) else {
|
||||
return Err(SysError::new(EINVAL));
|
||||
};
|
||||
if offset >= bytes.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let count = (bytes.len() - offset).min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn on_close(&mut self, id: usize) {
|
||||
self.handles.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_scheme(bdf: String, model: CpuModel, tctl_mc: Arc<RwLock<Option<i64>>>) {
|
||||
let socket = match Socket::create() {
|
||||
Ok(socket) => socket,
|
||||
Err(err) => {
|
||||
error!("failed to create scheme:k10temp socket: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut scheme = K10tempScheme::new(bdf, model, tctl_mc);
|
||||
let mut state = SchemeState::new();
|
||||
|
||||
match libredox::call::setrens(0, 0) {
|
||||
Ok(_) => info!("/scheme/k10temp ready"),
|
||||
Err(err) => {
|
||||
error!("failed to enter null namespace for scheme:k10temp: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
let request = match socket.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(request)) => request,
|
||||
Ok(None) => {
|
||||
warn!("scheme:k10temp socket closed; stopping scheme server");
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to read scheme:k10temp request: {err}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if let redox_scheme::RequestKind::Call(request) = request.kind() {
|
||||
let response = request.handle_sync(&mut scheme, &mut state);
|
||||
if let Err(err) = socket.write_response(response, SignalBehavior::Restart) {
|
||||
error!("failed to write scheme:k10temp response: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn run_scheme(_bdf: String, _model: CpuModel, _tctl_mc: Arc<RwLock<Option<i64>>>) {
|
||||
info!("host build: scheme:k10temp serving is disabled outside Redox");
|
||||
}
|
||||
|
||||
// ── Polling loop ──────────────────────────────────────────────────
|
||||
|
||||
fn poll_loop(bdf: String, model: CpuModel, tctl_mc: Arc<RwLock<Option<i64>>>) -> ! {
|
||||
info!("polling Tctl via DF F3 SMN at {bdf} every {:?}", POLL_INTERVAL);
|
||||
loop {
|
||||
match read_tctl_mc(&bdf, model) {
|
||||
Some(value) => {
|
||||
if let Ok(mut guard) = tctl_mc.write() {
|
||||
*guard = Some(value);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Keep the last known value; don't zero it out on a
|
||||
// transient read failure. The previous value is stale
|
||||
// but better than announcing "no data" during a blip.
|
||||
}
|
||||
}
|
||||
thread::sleep(POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────
|
||||
|
||||
fn main() {
|
||||
log::set_logger(&StderrLogger).ok();
|
||||
log::set_max_level(LevelFilter::Info);
|
||||
|
||||
info!("AMD k10temp thermal sensor daemon starting");
|
||||
|
||||
// Parse BDF from command-line arguments (driver-manager passes
|
||||
// the PCI BDF as the first positional argument after --bdf).
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let bdf = if args.len() >= 2 && args[1] == "--bdf" && args.len() >= 3 {
|
||||
args[2].clone()
|
||||
} else if args.len() >= 2 && !args[1].starts_with('-') {
|
||||
// Positional BDF (legacy driver-manager pass-through).
|
||||
args[1].clone()
|
||||
} else {
|
||||
DEFAULT_BDF.to_string()
|
||||
};
|
||||
info!("using PCI BDF: {bdf}");
|
||||
|
||||
// Determine CPU model. On Redox, read from /scheme/sys/cpu or
|
||||
// use CPUID. On host (testing), use the default Zen 4 model.
|
||||
#[cfg(target_os = "redox")]
|
||||
let model = detect_cpu_model().unwrap_or(DEFAULT_MODEL);
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
let model = DEFAULT_MODEL;
|
||||
info!(
|
||||
"CPU model: family={:#04x} model={:#04x} tctl_offset={:+}°C",
|
||||
model.family,
|
||||
model.model,
|
||||
model.tctl_offset_mc() as f64 / 1000.0,
|
||||
);
|
||||
|
||||
let tctl_mc = Arc::new(RwLock::new(None::<i64>));
|
||||
|
||||
// Spawn scheme server thread.
|
||||
let scheme_bdf = bdf.clone();
|
||||
let scheme_tctl = Arc::clone(&tctl_mc);
|
||||
let _scheme_thread = thread::spawn(move || run_scheme(scheme_bdf, model, scheme_tctl));
|
||||
|
||||
// Block on the polling loop.
|
||||
poll_loop(bdf, model, tctl_mc);
|
||||
}
|
||||
|
||||
// ── CPU model detection (Redox) ───────────────────────────────────
|
||||
|
||||
/// Detect the CPU family/model from Redox scheme interfaces.
|
||||
///
|
||||
/// On Redox, CPUID info is available at `/scheme/sys/cpu` and per-CPU
|
||||
/// directories at `/scheme/sys/cpu/{n}/`. The family and model fields
|
||||
/// are standard x86 CPUID leaf 1 (EAX) values.
|
||||
#[cfg(target_os = "redox")]
|
||||
fn detect_cpu_model() -> Option<CpuModel> {
|
||||
// Try to read from /scheme/sys/cpu summary first.
|
||||
if let Ok(data) = fs::read_to_string("/scheme/sys/cpu") {
|
||||
for line in data.lines() {
|
||||
if let Some(rest) = line.strip_prefix("Family: ") {
|
||||
if let Ok(family) = u8::from_str_radix(rest.trim().trim_start_matches("0x"), 16) {
|
||||
// Found family; now look for model on another line.
|
||||
for other in data.lines() {
|
||||
if let Some(model_str) = other.strip_prefix("Model: ") {
|
||||
if let Ok(model) =
|
||||
u8::from_str_radix(model_str.trim().trim_start_matches("0x"), 16)
|
||||
{
|
||||
return Some(CpuModel::new(family, model));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: read from CPUID leaf 1 EAX through /scheme/sys/cpu/0/cpuid
|
||||
if let Ok(data) = fs::read_to_string("/scheme/sys/cpu/0/cpuid") {
|
||||
// Parse family/model from CPUID 0x01 EAX.
|
||||
// EAX bits [11:8] = base family, bits [7:4] = base model,
|
||||
// bits [27:20] = extended family, bits [19:16] = extended model.
|
||||
// The effective family = base_family + ext_family.
|
||||
// The effective model = (ext_model << 4) | base_model.
|
||||
for line in data.lines() {
|
||||
if let Some(rest) = line.strip_prefix("0x01: ") {
|
||||
let parts: Vec<&str> = rest.split_whitespace().collect();
|
||||
if let Some(eax_str) = parts.first() {
|
||||
if let Ok(eax) = u32::from_str_radix(eax_str.trim_start_matches("0x"), 16) {
|
||||
let base_family = ((eax >> 8) & 0xF) as u8;
|
||||
let ext_family = ((eax >> 20) & 0xFF) as u8;
|
||||
let base_model = ((eax >> 4) & 0xF) as u8;
|
||||
let ext_model = ((eax >> 16) & 0xF) as u8;
|
||||
|
||||
let family = if base_family == 0xF {
|
||||
base_family + ext_family
|
||||
} else {
|
||||
base_family
|
||||
};
|
||||
let model = (ext_model << 4) | base_model;
|
||||
|
||||
return Some(CpuModel::new(family, model));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
//! AMD k10temp register decode and Tctl offset math.
|
||||
//!
|
||||
//! Reference: Linux 7.1 `drivers/hwmon/k10temp.c` (read-only reference).
|
||||
//!
|
||||
//! ## Register Model
|
||||
//!
|
||||
//! For AMD Family 19h (Zen 4, PCI 1022:14e3 = DF F3), the temperature
|
||||
//! sensor is read through the SMN (System Management Network) at address
|
||||
//! `0x00059800` (THM_TCON_CUR_TMP). The SMN is accessed indirectly via
|
||||
//! PCI config-space registers of Data Fabric Function 3:
|
||||
//!
|
||||
//! - offset 0x60: SMN_ADDR (write 32-bit SMN address)
|
||||
//! - offset 0x64: SMN_DATA (read 32-bit data)
|
||||
//!
|
||||
//! ## THM_TCON_CUR_TMP Register Layout (SMN 0x00059800)
|
||||
//!
|
||||
//! ```text
|
||||
//! bits [31:24] reserved
|
||||
//! bits [23:14] CUR_TEMP — current temperature in raw units
|
||||
//! bit 13 range select
|
||||
//! bits [12:0] reserved
|
||||
//! ```
|
||||
//!
|
||||
//! CUR_TEMP is a 10-bit value representing the raw sensor reading.
|
||||
//! Each unit = 0.125°C, so temperature in °C = CUR_TEMP * 0.125.
|
||||
//! In millidegree Celsius: CUR_TEMP * 125.
|
||||
//!
|
||||
//! ## Tctl Offset
|
||||
//!
|
||||
//! Tctl is the "control temperature" — the value reported to the OS and
|
||||
//! thermal management. On some models, Tctl includes an intentional offset
|
||||
//! from the raw sensor reading (typically +49°C on older Zen/Zen+ for fan
|
||||
//! control hysteresis). The offset is per-family/model:
|
||||
//!
|
||||
//! | Family / Model range | Tctl offset (°C) | Notes |
|
||||
//! |---------------------------|------------------|-----------------------|
|
||||
//! | 17h 00h-0Fh (Zen 1) | 0 | |
|
||||
//! | 17h 10h-2Fh (Zen+) | -49 | Tctl = raw - 49 |
|
||||
//! | 17h 30h-7Fh (Zen 2) | 0 | Tctl = raw |
|
||||
//! | 19h 00h-0Fh (Zen 3) | 0 | |
|
||||
//! | 19h 20h-5Fh (Zen 3+) | 0 | |
|
||||
//! | 19h 60h-6Fh (Zen 4) | 0 | 1022:14e3 = this row |
|
||||
//! | 19h 70h-7Fh (Zen 4+) | 0 | |
|
||||
//! | 1Ah (Zen 5) | 0 | |
|
||||
//!
|
||||
//! ## Implausible Reading Rejection
|
||||
//!
|
||||
//! A reading is rejected (returns `None`) if:
|
||||
//! - CUR_TEMP is 0 (sensor not ready)
|
||||
//! - CUR_TEMP is 0x7F (saturating max, likely invalid)
|
||||
//! - Resulting Tctl is below -20°C (physically impossible for powered silicon)
|
||||
//! - Resulting Tctl is above 115°C (beyond silicon thermal limit)
|
||||
//!
|
||||
//! These bounds are more generous than `k10temp.c` because Red Bear
|
||||
//! runs on a wider variety of hardware — a reading outside the 0-100°C
|
||||
//! band is suspicious but might be a no-heatsink debug board or an
|
||||
//! exotic cooling setup. The gate is on *physically implausible*, not
|
||||
//! *unusual but possible*.
|
||||
//!
|
||||
//! ## Temperature Publication
|
||||
//!
|
||||
//! Temperature is published in **millidegree Celsius** (m°C) to match
|
||||
//! the `redbear-power` sensor consumer contract at
|
||||
//! `local/recipes/system/redbear-power/source/src/sensor.rs:396-399`:
|
||||
//! - chip name: `"k10temp"`
|
||||
//! - reading kind: `SensorKind::Temp`
|
||||
//! - reading label: `Some("Tctl")`
|
||||
//! - raw_value: temperature in m°C (divide by 1000 for °C)
|
||||
//!
|
||||
//! The daemon serves these values on `/scheme/k10temp/`.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
// ── SMN / PCI constants ────────────────────────────────────────────
|
||||
|
||||
/// SMN address of the THM_TCON_CUR_TMP register (Temperature Control Current).
|
||||
pub const SMN_THM_TCON_CUR_TMP: u32 = 0x0005_9800;
|
||||
|
||||
/// PCI config-space offset for the SMN address register (DF F3).
|
||||
pub const PCI_SMN_ADDR: u64 = 0x60;
|
||||
|
||||
/// PCI config-space offset for the SMN data register (DF F3).
|
||||
pub const PCI_SMN_DATA: u64 = 0x64;
|
||||
|
||||
/// AMD vendor ID.
|
||||
#[allow(dead_code)]
|
||||
pub const AMD_VENDOR: u16 = 0x1022;
|
||||
|
||||
/// DF F3 device ID for Family 19h Models 60h-6Fh (Zen 4).
|
||||
#[allow(dead_code)]
|
||||
pub const DEVICE_19H_M60H_DF_F3: u16 = 0x14E3;
|
||||
|
||||
// ── CUR_TEMP bit layout ────────────────────────────────────────────
|
||||
|
||||
/// Bit offset of CUR_TEMP in the THM_TCON_CUR_TMP register.
|
||||
const CUR_TEMP_SHIFT: u32 = 14;
|
||||
|
||||
/// Mask for CUR_TEMP (bits [23:14] = 10 bits).
|
||||
const CUR_TEMP_MASK: u32 = 0x3FF;
|
||||
|
||||
/// Mask for the range-select bit (bit 13).
|
||||
const RANGE_SEL_MASK: u32 = 1 << 13;
|
||||
|
||||
// ── Temperature scaling ────────────────────────────────────────────
|
||||
|
||||
/// Each CUR_TEMP unit is 0.125°C.
|
||||
/// Temperature in millidegree Celsius = CUR_TEMP * 125.
|
||||
const MILLIDEGREES_PER_UNIT: i64 = 125;
|
||||
|
||||
// ── Validation bounds (millidegree Celsius) ────────────────────────
|
||||
|
||||
/// Minimum physically plausible Tctl (no-heatsink DUT might hit this).
|
||||
const MIN_PLAUSIBLE_MC: i64 = -20_000; // -20°C
|
||||
|
||||
/// Maximum physically plausible Tctl (beyond silicon limit).
|
||||
const MAX_PLAUSIBLE_MC: i64 = 115_000; // 115°C
|
||||
|
||||
/// CUR_TEMP value that indicates the sensor is not ready.
|
||||
const CUR_TEMP_INVALID_MIN: u32 = 0x00;
|
||||
|
||||
/// CUR_TEMP value at saturating maximum (likely invalid).
|
||||
const CUR_TEMP_SATURATING: u32 = 0x3FF; // 10 bits all-ones
|
||||
|
||||
// ── Error types ────────────────────────────────────────────────────
|
||||
|
||||
/// Reason a temperature reading is rejected.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ReadingError {
|
||||
/// CUR_TEMP field is zero — sensor not ready or not present.
|
||||
SensorNotReady,
|
||||
/// CUR_TEMP field is at the saturating maximum.
|
||||
Saturated,
|
||||
/// Computed Tctl is below the physically plausible minimum.
|
||||
BelowPlausibleRange { tctl_mc: i64 },
|
||||
/// Computed Tctl is above the physically plausible maximum.
|
||||
AbovePlausibleRange { tctl_mc: i64 },
|
||||
}
|
||||
|
||||
impl fmt::Display for ReadingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ReadingError::SensorNotReady => write!(f, "CUR_TEMP is zero — sensor not ready"),
|
||||
ReadingError::Saturated => write!(f, "CUR_TEMP is at the saturating maximum (0x7F)"),
|
||||
ReadingError::BelowPlausibleRange { tctl_mc } => {
|
||||
write!(f, "Tctl {:.1}°C is below physically plausible minimum", *tctl_mc as f64 / 1000.0)
|
||||
}
|
||||
ReadingError::AbovePlausibleRange { tctl_mc } => {
|
||||
write!(f, "Tctl {:.1}°C is above physically plausible maximum", *tctl_mc as f64 / 1000.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── CPU model identification ───────────────────────────────────────
|
||||
|
||||
/// AMD CPU family/model extracted from CPUID.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct CpuModel {
|
||||
pub family: u8,
|
||||
pub model: u8,
|
||||
}
|
||||
|
||||
impl CpuModel {
|
||||
pub const fn new(family: u8, model: u8) -> Self {
|
||||
Self { family, model }
|
||||
}
|
||||
|
||||
/// AMD Family 19h Model 60h-6Fh (Zen 4 Raphael/Phoenix).
|
||||
pub const FAM19H_M60H: Self = CpuModel::new(0x19, 0x60);
|
||||
|
||||
/// AMD Family 19h Model 00h-0Fh (Zen 3).
|
||||
pub const FAM19H_M00H: Self = CpuModel::new(0x19, 0x00);
|
||||
|
||||
/// AMD Family 17h Model 30h-3Fh (Zen 2).
|
||||
pub const FAM17H_M30H: Self = CpuModel::new(0x17, 0x30);
|
||||
|
||||
/// AMD Family 17h Model 10h-2Fh (Zen+).
|
||||
pub const FAM17H_M10H: Self = CpuModel::new(0x17, 0x10);
|
||||
|
||||
/// AMD Family 17h Model 00h-0Fh (Zen 1).
|
||||
pub const FAM17H_M00H: Self = CpuModel::new(0x17, 0x00);
|
||||
|
||||
/// Returns the Tctl offset in millidegree Celsius.
|
||||
///
|
||||
/// On Zen+ (Family 17h Models 10h-2Fh), the sensor reports
|
||||
/// raw_temperature + 49°C, so the offset is -49°C to compute
|
||||
/// the actual die temperature. All other supported families
|
||||
/// have zero offset.
|
||||
pub const fn tctl_offset_mc(self) -> i64 {
|
||||
match (self.family, self.model) {
|
||||
(0x17, 0x10..=0x2F) => -49_000, // Zen+: Tctl = raw - 49°C
|
||||
_ => 0, // All others: Tctl = raw
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this model is in the supported set.
|
||||
pub const fn is_supported(self) -> bool {
|
||||
matches!(
|
||||
(self.family, self.model),
|
||||
(0x17, 0x00..=0x7F) | (0x19, 0x00..=0x7F) | (0x1A, 0x00..=0x7F)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Register decode ────────────────────────────────────────────────
|
||||
|
||||
/// Extracts CUR_TEMP from a raw THM_TCON_CUR_TMP register value.
|
||||
///
|
||||
/// Returns `None` if CUR_TEMP is zero (sensor not ready) or
|
||||
/// saturating (0x7F, likely invalid).
|
||||
#[inline]
|
||||
pub const fn extract_cur_temp(raw: u32) -> Option<u32> {
|
||||
let cur_temp = (raw >> CUR_TEMP_SHIFT) & CUR_TEMP_MASK;
|
||||
if cur_temp == CUR_TEMP_INVALID_MIN || cur_temp == CUR_TEMP_SATURATING {
|
||||
None
|
||||
} else {
|
||||
Some(cur_temp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the range-select bit (bit 13) from the raw register.
|
||||
#[inline]
|
||||
pub const fn range_sel(raw: u32) -> bool {
|
||||
(raw & RANGE_SEL_MASK) != 0
|
||||
}
|
||||
|
||||
/// Converts CUR_TEMP to raw millidegree Celsius (sensor value, NOT Tctl).
|
||||
#[inline]
|
||||
pub const fn cur_temp_to_millidegrees(cur_temp: u32) -> i64 {
|
||||
cur_temp as i64 * MILLIDEGREES_PER_UNIT
|
||||
}
|
||||
|
||||
/// Computes Tctl in millidegree Celsius from a raw THM_TCON_CUR_TMP value.
|
||||
///
|
||||
/// Tctl = CUR_TEMP * 125 + tctl_offset_mc
|
||||
///
|
||||
/// Returns `Err(ReadingError)` if:
|
||||
/// - CUR_TEMP is zero or saturating
|
||||
/// - The resulting Tctl is outside the physically plausible range
|
||||
///
|
||||
/// Returns `Ok(tctl_mc)` on success, where `tctl_mc` is in millidegree Celsius.
|
||||
pub fn compute_tctl_mc(raw: u32, model: CpuModel) -> Result<i64, ReadingError> {
|
||||
let cur_temp = extract_cur_temp(raw).ok_or(
|
||||
if (raw >> CUR_TEMP_SHIFT) & CUR_TEMP_MASK == CUR_TEMP_INVALID_MIN {
|
||||
ReadingError::SensorNotReady
|
||||
} else {
|
||||
ReadingError::Saturated
|
||||
},
|
||||
)?;
|
||||
|
||||
let sensor_mc = cur_temp_to_millidegrees(cur_temp);
|
||||
let offset_mc = model.tctl_offset_mc();
|
||||
let tctl_mc = sensor_mc + offset_mc;
|
||||
|
||||
if tctl_mc < MIN_PLAUSIBLE_MC {
|
||||
return Err(ReadingError::BelowPlausibleRange { tctl_mc });
|
||||
}
|
||||
if tctl_mc > MAX_PLAUSIBLE_MC {
|
||||
return Err(ReadingError::AbovePlausibleRange { tctl_mc });
|
||||
}
|
||||
|
||||
Ok(tctl_mc)
|
||||
}
|
||||
|
||||
/// Convenience: compute Tctl in °C (as f64).
|
||||
pub fn compute_tctl_c(raw: u32, model: CpuModel) -> Result<f64, ReadingError> {
|
||||
compute_tctl_mc(raw, model).map(|mc| mc as f64 / 1000.0)
|
||||
}
|
||||
|
||||
// ── Tests ──────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── extract_cur_temp ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn extract_cur_temp_normal() {
|
||||
// CUR_TEMP = 0x40 (64 decimal) → 64 * 0.125 = 8.0°C raw
|
||||
let raw = 0x40u32 << CUR_TEMP_SHIFT;
|
||||
assert_eq!(extract_cur_temp(raw), Some(0x40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_cur_temp_zero_is_rejected() {
|
||||
let raw = 0u32;
|
||||
assert_eq!(extract_cur_temp(raw), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_cur_temp_saturating_is_rejected() {
|
||||
let raw = CUR_TEMP_SATURATING << CUR_TEMP_SHIFT;
|
||||
assert_eq!(extract_cur_temp(raw), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_cur_temp_max_valid() {
|
||||
// CUR_TEMP = 0x7E (126) is valid — 126 * 0.125 = 15.75°C
|
||||
let raw = (0x7Eu32) << CUR_TEMP_SHIFT;
|
||||
assert_eq!(extract_cur_temp(raw), Some(0x7E));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_cur_temp_min_valid() {
|
||||
let raw = 0x01u32 << CUR_TEMP_SHIFT;
|
||||
assert_eq!(extract_cur_temp(raw), Some(0x01));
|
||||
}
|
||||
|
||||
// ── cur_temp_to_millidegrees ────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cur_temp_to_millidegrees_typical() {
|
||||
// CUR_TEMP = 64 → 64 * 125 = 8000 m°C = 8.0°C
|
||||
assert_eq!(cur_temp_to_millidegrees(64), 8_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cur_temp_to_millidegrees_ambient() {
|
||||
// CUR_TEMP = 240 → 240 * 125 = 30000 m°C = 30.0°C
|
||||
assert_eq!(cur_temp_to_millidegrees(240), 30_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cur_temp_to_millidegrees_idle() {
|
||||
// CUR_TEMP = 320 → 320 * 125 = 40000 m°C = 40.0°C
|
||||
assert_eq!(cur_temp_to_millidegrees(320), 40_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cur_temp_to_millidegrees_max() {
|
||||
// CUR_TEMP = 920 → 920 * 125 = 115000 m°C = 115.0°C
|
||||
assert_eq!(cur_temp_to_millidegrees(920), 115_000);
|
||||
}
|
||||
|
||||
// ── Tctl offset table ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tctl_offset_zen1_is_zero() {
|
||||
// Family 17h Model 01h (Zen 1)
|
||||
assert_eq!(CpuModel::FAM17H_M00H.tctl_offset_mc(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tctl_offset_zen_plus_is_neg49() {
|
||||
// Family 17h Model 18h (Zen+)
|
||||
assert_eq!(CpuModel::FAM17H_M10H.tctl_offset_mc(), -49_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tctl_offset_zen2_is_zero() {
|
||||
// Family 17h Model 31h (Zen 2)
|
||||
assert_eq!(CpuModel::FAM17H_M30H.tctl_offset_mc(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tctl_offset_zen3_is_zero() {
|
||||
assert_eq!(CpuModel::FAM19H_M00H.tctl_offset_mc(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tctl_offset_zen4_is_zero() {
|
||||
// Family 19h Model 60h (Zen 4) — our primary target
|
||||
assert_eq!(CpuModel::FAM19H_M60H.tctl_offset_mc(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tctl_offset_zen_plus_model_boundary_low() {
|
||||
// Family 17h Model 0Fh → not Zen+, should be 0
|
||||
assert_eq!(CpuModel::new(0x17, 0x0F).tctl_offset_mc(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tctl_offset_zen_plus_model_boundary_high() {
|
||||
// Family 17h Model 2Fh → Zen+, should be -49
|
||||
assert_eq!(CpuModel::new(0x17, 0x2F).tctl_offset_mc(), -49_000);
|
||||
}
|
||||
|
||||
// ── compute_tctl_mc ─────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn compute_tctl_zen4_typical_40c() {
|
||||
// CUR_TEMP = 320 (40.0°C raw), Zen 4 offset=0
|
||||
let raw = (320u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(40_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_tctl_zen4_idle_30c() {
|
||||
// CUR_TEMP = 240 (30.0°C raw)
|
||||
let raw = (240u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(30_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_tctl_zen4_load_85c() {
|
||||
// CUR_TEMP = 680 (85.0°C raw)
|
||||
let raw = (680u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(85_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_tctl_zen4_max_plausible_115c() {
|
||||
// CUR_TEMP = 920 (115.0°C raw)
|
||||
let raw = (920u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(115_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_tctl_zen_plus_offset_applied() {
|
||||
// CUR_TEMP = 392 (49.0°C raw), Tctl offset = -49 → Tctl = 0°C
|
||||
let raw = (392u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM17H_M10H);
|
||||
assert_eq!(result, Ok(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_tctl_zen_plus_idle_after_offset() {
|
||||
// CUR_TEMP = 632 (79.0°C raw), offset -49 → Tctl = 30°C
|
||||
let raw = (632u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM17H_M10H);
|
||||
assert_eq!(result, Ok(30_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_tctl_zen_plus_below_plausible() {
|
||||
// CUR_TEMP = 200 (25.0°C raw), offset -49 → Tctl = -24°C
|
||||
// Below -20°C minimum → rejected
|
||||
let raw = (200u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM17H_M10H);
|
||||
assert!(matches!(result, Err(ReadingError::BelowPlausibleRange { .. })));
|
||||
}
|
||||
|
||||
// ── Implausible reading rejection ───────────────────────────
|
||||
|
||||
#[test]
|
||||
fn reject_sensor_not_ready() {
|
||||
let raw = 0u32;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Err(ReadingError::SensorNotReady));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_saturated() {
|
||||
let raw = CUR_TEMP_SATURATING << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Err(ReadingError::Saturated));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_below_plausible() {
|
||||
// CUR_TEMP = 1 → 0.125°C → within range, ok
|
||||
let raw = (1u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(125));
|
||||
|
||||
// CUR_TEMP = 0 → rejected as sensor-not-ready
|
||||
let raw = 0u32;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Err(ReadingError::SensorNotReady));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_above_plausible() {
|
||||
// CUR_TEMP = 921 → 921 * 0.125 = 115.125°C → above 115°C
|
||||
let raw = (921u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert!(matches!(result, Err(ReadingError::AbovePlausibleRange { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_way_above_plausible() {
|
||||
// CUR_TEMP = 1000 → 1000 * 0.125 = 125°C → rejected
|
||||
let raw = (1000u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert!(matches!(result, Err(ReadingError::AbovePlausibleRange { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_edge_min_plausible() {
|
||||
// CUR_TEMP = 0 (rejected) — already covered above
|
||||
// CUR_TEMP = 1 → 0.125°C → still plausible (1 * 125 = 125 m°C)
|
||||
// Wait, 0.125°C is above -20°C minimum, so it passes
|
||||
let raw = (1u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(125));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_edge_max_plausible() {
|
||||
// CUR_TEMP = 920 → 115.0°C → at the boundary, ok
|
||||
let raw = (920u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(115_000));
|
||||
}
|
||||
|
||||
// ── compute_tctl_c convenience ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn compute_tctl_c_typical() {
|
||||
let raw = (320u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_c(raw, CpuModel::FAM19H_M60H);
|
||||
assert!(result.is_ok());
|
||||
let c = result.unwrap();
|
||||
assert!((c - 40.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
// ── range_sel ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn range_sel_zero() {
|
||||
assert!(!range_sel(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_sel_set() {
|
||||
assert!(range_sel(RANGE_SEL_MASK));
|
||||
}
|
||||
|
||||
// ── CpuModel::is_supported ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn model_fam19h_m60h_is_supported() {
|
||||
assert!(CpuModel::FAM19H_M60H.is_supported());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_fam17h_m00h_is_supported() {
|
||||
assert!(CpuModel::FAM17H_M00H.is_supported());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_fam15h_is_not_supported() {
|
||||
assert!(!CpuModel::new(0x15, 0x00).is_supported());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_intel_is_not_supported() {
|
||||
// Intel family 6 (Core) — not supported by k10temp
|
||||
assert!(!CpuModel::new(0x06, 0x8F).is_supported());
|
||||
}
|
||||
|
||||
// ── Integration: benchmark typical load range ───────────────
|
||||
|
||||
#[test]
|
||||
fn zen4_full_load_range_30_to_95c_is_accepted() {
|
||||
// Simulate the 30-95°C range from the acceptance criteria
|
||||
for cur_temp in 240..=760 {
|
||||
// 240 = 30.0°C, 760 = 95.0°C
|
||||
let raw = (cur_temp as u32) << CUR_TEMP_SHIFT;
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"CUR_TEMP={cur_temp} ({:.1}°C) should be accepted but got {result:?}",
|
||||
cur_temp as f64 * 0.125,
|
||||
);
|
||||
let tctl_mc = result.unwrap();
|
||||
assert!(tctl_mc >= 30_000, "Tctl {tctl_mc} m°C below expected minimum 30000");
|
||||
assert!(tctl_mc <= 95_000, "Tctl {tctl_mc} m°C above expected maximum 95000");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zen4_degraded_silicon_100c_is_accepted() {
|
||||
let raw = (800u32) << CUR_TEMP_SHIFT; // 100.0°C
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(100_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zen4_cold_boot_25c_is_accepted() {
|
||||
let raw = (200u32) << CUR_TEMP_SHIFT; // 25.0°C
|
||||
let result = compute_tctl_mc(raw, CpuModel::FAM19H_M60H);
|
||||
assert_eq!(result, Ok(25_000));
|
||||
}
|
||||
}
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../local/recipes/drivers/redbear-k10tempd
|
||||
Reference in New Issue
Block a user