Sensors on Framework Laptops (#1681)

* Begin CrOS implementation for Framework Laptops

* CrOS: Talk directly to the EC, instead of through ACPI

* Add a few Framework models

* Remove early Framework models

These have a different EC, and don't currently work

* Migrate CrOS ECs to PawnIO

* Implement EcCommand and read sensor names

* Add more Framework models

* Add some error handling

* Misc

---------

Co-authored-by: PhyxionNL <7643972+PhyxionNL@users.noreply.github.com>
This commit is contained in:
Stephen Horvath
2025-11-19 10:57:57 +01:00
committed by GitHub
co-authored by PhyxionNL
parent 0ed17c77ce
commit b02fc4c4cf
9 changed files with 371 additions and 21 deletions
@@ -52,6 +52,8 @@ internal class Identification
return Manufacturer.FIC;
case var _ when name.Equals("Foxconn", StringComparison.OrdinalIgnoreCase):
return Manufacturer.Foxconn;
case var _ when name.StartsWith("Framework", StringComparison.OrdinalIgnoreCase):
return Manufacturer.Framework;
case var _ when name.StartsWith("Fujitsu", StringComparison.OrdinalIgnoreCase):
return Manufacturer.Fujitsu;
case var _ when name.StartsWith("Gigabyte", StringComparison.OrdinalIgnoreCase):
@@ -726,6 +728,64 @@ internal class Identification
return Model.PROART_B760_CREATOR_D4;
case var _ when name.Equals("TUF GAMING B450-PLUS II", StringComparison.OrdinalIgnoreCase):
return Model.TUF_GAMING_B450_PLUS_II;
case var _ when name.Equals("FRANBMCP03", StringComparison.OrdinalIgnoreCase):
return Model.FRANBMCP03;
case var _ when name.Equals("FRANBMCP06", StringComparison.OrdinalIgnoreCase):
return Model.FRANBMCP06;
case var _ when name.Equals("FRANBMCP08", StringComparison.OrdinalIgnoreCase):
return Model.FRANBMCP08;
case var _ when name.Equals("FRANBMCP0A", StringComparison.OrdinalIgnoreCase):
return Model.FRANBMCP0A;
case var _ when name.Equals("FRANBMCP0B", StringComparison.OrdinalIgnoreCase):
return Model.FRANBMCP0B;
case var _ when name.Equals("FRANBMCP0C", StringComparison.OrdinalIgnoreCase):
return Model.FRANBMCP0C;
case var _ when name.Equals("FRANGACP04", StringComparison.OrdinalIgnoreCase):
return Model.FRANGACP04;
case var _ when name.Equals("FRANGACP06", StringComparison.OrdinalIgnoreCase):
return Model.FRANGACP06;
case var _ when name.Equals("FRANGACP08", StringComparison.OrdinalIgnoreCase):
return Model.FRANGACP08;
case var _ when name.Equals("FRANMACP04", StringComparison.OrdinalIgnoreCase):
return Model.FRANMACP04;
case var _ when name.Equals("FRANMACP06", StringComparison.OrdinalIgnoreCase):
return Model.FRANMACP06;
case var _ when name.Equals("FRANMACP08", StringComparison.OrdinalIgnoreCase):
return Model.FRANMACP08;
case var _ when name.Equals("FRANMCCP04", StringComparison.OrdinalIgnoreCase):
return Model.FRANMCCP04;
case var _ when name.Equals("FRANMCCP06", StringComparison.OrdinalIgnoreCase):
return Model.FRANMCCP06;
case var _ when name.Equals("FRANMCCP07", StringComparison.OrdinalIgnoreCase):
return Model.FRANMCCP07;
case var _ when name.Equals("FRANMDCP05", StringComparison.OrdinalIgnoreCase):
return Model.FRANMDCP05;
case var _ when name.Equals("FRANMDCP07", StringComparison.OrdinalIgnoreCase):
return Model.FRANMDCP07;
case var _ when name.Equals("FRANMECP02", StringComparison.OrdinalIgnoreCase):
return Model.FRANMECP02;
case var _ when name.Equals("FRANMECP05", StringComparison.OrdinalIgnoreCase):
return Model.FRANMECP05;
case var _ when name.Equals("FRANMECP06", StringComparison.OrdinalIgnoreCase):
return Model.FRANMECP06;
case var _ when name.Equals("FRANMZCP07", StringComparison.OrdinalIgnoreCase):
return Model.FRANMZCP07;
case var _ when name.Equals("FRANMZCP09", StringComparison.OrdinalIgnoreCase):
return Model.FRANMZCP09;
case var _ when name.Equals("FRANMFCP02", StringComparison.OrdinalIgnoreCase):
return Model.FRANMFCP02;
case var _ when name.Equals("FRANMFCP06", StringComparison.OrdinalIgnoreCase):
return Model.FRANMFCP06;
case var _ when name.Equals("FRAPMACP03", StringComparison.OrdinalIgnoreCase):
return Model.FRAPMACP03;
case var _ when name.Equals("FRAPMACP05", StringComparison.OrdinalIgnoreCase):
return Model.FRAPMACP05;
case var _ when name.Equals("FRANMGCP05", StringComparison.OrdinalIgnoreCase):
return Model.FRANMGCP05;
case var _ when name.Equals("FRANMGCP07", StringComparison.OrdinalIgnoreCase):
return Model.FRANMGCP07;
case var _ when name.Equals("FRANMGCP09", StringComparison.OrdinalIgnoreCase):
return Model.FRANMGCP09;
case var _ when name.Equals("Base Board Product Name", StringComparison.OrdinalIgnoreCase):
case var _ when name.Equals("To be filled by O.E.M.", StringComparison.OrdinalIgnoreCase):
return Model.Unknown;
@@ -0,0 +1,96 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// All Rights Reserved.
using System.Collections.Generic;
namespace LibreHardwareMonitor.Hardware.Motherboard.Lpc.EC;
public class ChromeOSEmbeddedController : EmbeddedController
{
private const byte EC_FAN_SPEED_ENTRIES = 4;
private const ushort EC_FAN_SPEED_NOT_PRESENT = 0xffff;
private const byte EC_MEMMAP_FAN = 0x10;
private const byte EC_MEMMAP_TEMP_SENSOR = 0x00;
private const byte EC_MEMMAP_TEMP_SENSOR_B = 0x18;
private const byte EC_TEMP_SENSOR_B_ENTRIES = 8;
private const byte EC_TEMP_SENSOR_ENTRIES = 16;
private const byte EC_TEMP_SENSOR_NOT_PRESENT = 0xff;
/*
* The offset of temperature value stored in mapped memory. This allows
* reporting a temperature range of 200K to 454K = -73C to 181C.
*/
private const byte EC_TEMP_SENSOR_OFFSET = 200;
public ChromeOSEmbeddedController(IEnumerable<EmbeddedControllerSource> sources, ISettings settings) : base(sources, settings)
{ }
public static ChromeOSEmbeddedController Create(ISettings settings)
{
List<EmbeddedControllerSource> sources = [];
using ChromeOSEmbeddedControllerIO embeddedControllerIO = new();
// Copy the first 0x20 bytes of the EC memory map
byte[] data = embeddedControllerIO.ReadMemmap(0x00, 0x20);
for (int i = 0; i < EC_TEMP_SENSOR_ENTRIES; i++)
{
byte temp = data[EC_MEMMAP_TEMP_SENSOR + i];
if (temp == EC_TEMP_SENSOR_NOT_PRESENT)
{
break;
}
string sensorName = embeddedControllerIO.TempSensorGetName((byte)i);
sources.Add(new EmbeddedControllerSource(sensorName,
SensorType.Temperature,
(ushort)(EC_MEMMAP_TEMP_SENSOR + i),
offset: EC_TEMP_SENSOR_OFFSET - 273,
blank: EC_TEMP_SENSOR_NOT_PRESENT));
}
for (int i = 0; i < EC_FAN_SPEED_ENTRIES; i++)
{
ushort fan = (ushort)(data[EC_MEMMAP_FAN + i * 2] | (data[EC_MEMMAP_FAN + i * 2 + 1] << 8));
if (fan == EC_FAN_SPEED_NOT_PRESENT)
{
break;
}
sources.Add(new EmbeddedControllerSource("Fan " + (i + 1),
SensorType.Fan,
(ushort)(EC_MEMMAP_FAN + i * 2),
2,
blank: EC_FAN_SPEED_NOT_PRESENT,
isLittleEndian: true));
}
for (int i = 0; i < EC_TEMP_SENSOR_B_ENTRIES; i++)
{
byte temp = data[EC_MEMMAP_TEMP_SENSOR_B + i];
if (temp == EC_TEMP_SENSOR_NOT_PRESENT)
{
break;
}
string sensorName = embeddedControllerIO.TempSensorGetName((byte)(i + EC_TEMP_SENSOR_ENTRIES));
sources.Add(new EmbeddedControllerSource(sensorName,
SensorType.Temperature,
(ushort)(EC_MEMMAP_TEMP_SENSOR_B + i),
offset: EC_TEMP_SENSOR_OFFSET - 273,
blank: EC_TEMP_SENSOR_NOT_PRESENT));
}
return new ChromeOSEmbeddedController(sources, settings);
}
protected override IEmbeddedControllerIO AcquireIOInterface()
{
return new ChromeOSEmbeddedControllerIO();
}
}
@@ -0,0 +1,74 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// All Rights Reserved.
using System.Diagnostics;
using System.Text;
using LibreHardwareMonitor.PawnIo;
namespace LibreHardwareMonitor.Hardware.Motherboard.Lpc.EC;
public class ChromeOSEmbeddedControllerIO : IEmbeddedControllerIO
{
private const short EC_CMD_TEMP_SENSOR_GET_INFO = 0x0070;
private readonly LpcCrOSEc _pawnModule;
private bool _disposed;
public ChromeOSEmbeddedControllerIO()
{
_pawnModule = new LpcCrOSEc();
if (!Mutexes.WaitEc(10))
{
throw new BusMutexLockingFailedException();
}
}
public void Read(ushort[] registers, byte[] data)
{
Trace.Assert(registers.Length <= data.Length,
"data buffer length has to be greater or equal to the registers array length");
for (int i = 0; i < registers.Length; i++)
{
data[i] = ReadMemmap((byte)registers[i], 1)[0];
}
}
public byte[] ReadMemmap(byte offset, byte bytes)
{
return _pawnModule.ReadMemmap(offset, bytes);
}
public string TempSensorGetName(byte index)
{
try
{
byte[] resp = _pawnModule.EcCommand(0, EC_CMD_TEMP_SENSOR_GET_INFO, 1, 33, [index]);
//byte sensorType = resp[33];
return Encoding.ASCII.GetString(resp, 0, 32).TrimEnd('\0');
}
catch (System.Exception)
{
return "Temp " + index;
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
Mutexes.ReleaseEc();
}
}
public class BusMutexLockingFailedException : EmbeddedController.IOException
{
public BusMutexLockingFailedException()
: base("could not lock ISA bus mutex")
{ }
}
}
@@ -285,6 +285,48 @@ public abstract class EmbeddedController : Hardware
ECSensor.TempCPUPackage,
ECSensor.TempMB,
ECSensor.TempVrm),
new(Model.FRANMDCP05,
BoardFamily.CrOS,
null),
new(Model.FRANMDCP07,
BoardFamily.CrOS,
null),
new(Model.FRANMECP02,
BoardFamily.CrOS,
null),
new(Model.FRANMECP05,
BoardFamily.CrOS,
null),
new(Model.FRANMECP06,
BoardFamily.CrOS,
null),
new(Model.FRANMZCP07,
BoardFamily.CrOS,
null),
new(Model.FRANMZCP09,
BoardFamily.CrOS,
null),
new(Model.FRANMFCP02,
BoardFamily.CrOS,
null),
new(Model.FRANMFCP06,
BoardFamily.CrOS,
null),
new(Model.FRAPMACP03,
BoardFamily.CrOS,
null),
new(Model.FRAPMACP05,
BoardFamily.CrOS,
null),
new(Model.FRANMGCP05,
BoardFamily.CrOS,
null),
new(Model.FRANMGCP07,
BoardFamily.CrOS,
null),
new(Model.FRANMGCP09,
BoardFamily.CrOS,
null)
};
private static readonly Dictionary<BoardFamily, Dictionary<ECSensor, EmbeddedControllerSource>> _knownSensors = new()
@@ -454,6 +496,12 @@ public abstract class EmbeddedController : Hardware
}
BoardInfo board = boards[0];
if (board.Family == BoardFamily.CrOS)
{
return ChromeOSEmbeddedController.Create(settings);
}
IEnumerable<EmbeddedControllerSource> sources = board.Sensors.Select(ecs => _knownSensors[board.Family][ecs]);
return Environment.OSVersion.Platform switch
@@ -474,16 +522,19 @@ public abstract class EmbeddedController : Hardware
int readRegister = 0;
for (int si = 0; si < _sensors.Count; ++si)
{
int littleEndian = _sources[si].IsLittleEndian ? 1 : 0;
int bigEndian = _sources[si].IsLittleEndian ? 0 : 1;
int val = _sources[si].Size switch
{
1 => _sources[si].Type switch { SensorType.Temperature => unchecked((sbyte)_data[readRegister]), _ => _data[readRegister] },
2 => unchecked((short)((_data[readRegister] << 8) + _data[readRegister + 1])),
2 => unchecked((short)((_data[readRegister + littleEndian] << 8) + _data[readRegister + bigEndian])),
_ => 0
};
readRegister += _sources[si].Size;
_sensors[si].Value = val != _sources[si].Blank ? val * _sources[si].Factor : null;
_sensors[si].Value = val != _sources[si].Blank ? (val * _sources[si].Factor) + _sources[si].Offset : null;
}
}
@@ -612,7 +663,8 @@ public abstract class EmbeddedController : Hardware
Intel300,
Intel400,
Intel600,
Intel700
Intel700,
CrOS
}
private struct BoardInfo
@@ -5,27 +5,21 @@
namespace LibreHardwareMonitor.Hardware.Motherboard.Lpc.EC;
public class EmbeddedControllerSource
public class EmbeddedControllerSource(string name, SensorType type, ushort register, byte size = 1, float factor = 1.0f, float offset = 0.0f, int blank = int.MaxValue, bool isLittleEndian = false)
{
public EmbeddedControllerSource(string name, SensorType type, ushort register, byte size = 1, float factor = 1.0f, int blank = int.MaxValue)
{
Name = name;
public int Blank { get; } = blank;
Register = register;
Size = size;
Type = type;
Factor = factor;
Blank = blank;
}
public float Factor { get; } = factor;
public string Name { get; }
public ushort Register { get; }
public byte Size { get; }
public float Factor { get; }
public bool IsLittleEndian { get; } = isLittleEndian;
public int Blank { get; }
public string Name { get; } = name;
public EmbeddedControllerReader Reader { get; }
public float Offset { get; } = offset;
public SensorType Type { get; }
}
public ushort Register { get; } = register;
public byte Size { get; } = size;
public SensorType Type { get; } = type;
}
@@ -30,6 +30,7 @@ public enum Manufacturer
EVGA,
FIC,
Foxconn,
Framework,
Fujitsu,
Gateway,
Gigabyte,
@@ -314,6 +314,37 @@ public enum Model
//Supermicro
X11SWN_E,
// Framework
FRANBMCP03, // Intel Core i5-1135G7
FRANBMCP06, // Intel Core i7-1165G7
FRANBMCP08, // Intel Core i7-1185G7
FRANBMCP0A, // Intel Core i5-1135G7
FRANBMCP0B, // Intel Core i7-1165G7
FRANBMCP0C, // Intel Core i7-1185G7
FRANGACP04, // Intel Core i5-1240P
FRANGACP06, // Intel Core i7-1260P
FRANGACP08, // Intel Core i7-1280P
FRANMACP04, // Intel Core i5-1240P
FRANMACP06, // Intel Core i7-1260P
FRANMACP08, // Intel Core i7-1280P
FRANMCCP04, // Intel Core i5-1340P
FRANMCCP06, // Intel Core i7-1360P
FRANMCCP07, // Intel Core i7-1370P
FRANMDCP05, // FW13 AMD Ryzen 5 7640U
FRANMDCP07, // FW13 AMD Ryzen 7 7840U
FRANMECP02, // FW13 Intel Core Ultra 5 125H
FRANMECP05, // FW13 Intel Core Ultra 7 155H
FRANMECP06, // FW13 Intel Core Ultra 7 165H
FRANMZCP07, // FW16 AMD Ryzen 7 7840HS
FRANMZCP09, // FW16 AMD Ryzen 9 7940HS
FRANMFCP02, // Desktop AMD RYZEN AI MAX 385
FRANMFCP06, // Desktop AMD RYZEN AI MAX+ 395
FRAPMACP03, // FW12 Intel Core i3-1315U
FRAPMACP05, // FW12 Intel Core i5-1334U
FRANMGCP05, // FW13 AMD Ryzen AI 5 340
FRANMGCP07, // FW13 AMD Ryzen AI 7 350
FRANMGCP09, // FW13 AMD Ryzen AI 9 HX 370
// Unknown
Unknown
}
+42
View File
@@ -0,0 +1,42 @@
using System;
namespace LibreHardwareMonitor.PawnIo;
public class LpcCrOSEc
{
private readonly PawnIo _pawnIO = PawnIo.LoadModuleFromResource(typeof(LpcCrOSEc).Assembly, $"{nameof(LibreHardwareMonitor)}.Resources.PawnIo.LpcCrOSEC.bin");
public byte[] EcCommand(int version, int command, int outsize, int insize, byte[] data)
{
long[] inArray = new long[38];
inArray[0] = version;
inArray[1] = command;
inArray[2] = outsize;
inArray[3] = insize;
// Start packing data into inArray at the 4th long (8 bytes)
Buffer.BlockCopy(data, 0, inArray, 4 * 8, data.Length);
long[] outArray = _pawnIO.Execute("ioctl_ec_command", inArray, 1 + (int)Math.Ceiling(insize / 8.0));
if (outArray[0] < 0)
{
throw new Exception("EC returned error code " + -outArray[0]);
}
byte[] retArray = new byte[insize];
// Unpack the data skipping the first long
Buffer.BlockCopy(outArray, 8, retArray, 0, insize);
return retArray;
}
public byte[] ReadMemmap(byte offset, byte bytes)
{
long[] inArray = [offset, bytes];
long[] outArray = _pawnIO.Execute("ioctl_ec_readmem", inArray, (int)Math.Ceiling(bytes / 8.0));
byte[] retArray = new byte[bytes];
Buffer.BlockCopy(outArray, 0, retArray, 0, bytes);
return retArray;
}
public void Close() => _pawnIO.Close();
}