feat: add kernel ACPI support for querying battery percentage, add battery indicator to desktop, move log state to 0:/state directory
This commit is contained in:
1 parent
0a18b2cf2d
commit
e641500b40
34 files changed
+2461
-100
No files matched your search
@@ -6,10 +6,13 @@
|
||||
|
||||
#include "AmlInterpreter.hpp"
|
||||
#include <ACPI/ACPI.hpp>
|
||||
#include <ACPI/EmbeddedController.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/Heap.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
@@ -41,13 +44,78 @@ namespace Hal {
|
||||
return IsLeadNameChar(c) || (c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
// The interpreter runs on whatever stack its caller has -- the boot
|
||||
// stack early on, a thread's 16 KiB kernel stack once userspace is up.
|
||||
// m_stackFloor is anchored at the top-level entry; a frame below it has
|
||||
// spent the budget and must unwind rather than keep recursing.
|
||||
bool Interpreter::HaveStackHeadroom(const void* frame) const {
|
||||
if (m_stackFloor == 0) return true; // no evaluation in progress
|
||||
if ((uint64_t)frame > m_stackFloor) return true;
|
||||
|
||||
// Say so once. A method that quietly gives up here returns a
|
||||
// default value that looks like a hardware answer -- a battery
|
||||
// reporting itself absent, say -- and nothing upstream can tell
|
||||
// the difference.
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
KernelLogStream(WARNING, "AML")
|
||||
<< "Evaluation stopped: spent the " << base::dec
|
||||
<< (uint64_t)(StackBudget / 1024) << " KiB stack allowance";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Interpreter::ReportDepthLimit() {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
KernelLogStream(WARNING, "AML") << "Evaluation stopped: nesting past "
|
||||
<< base::dec << (uint64_t)MaxExprDepth << " levels";
|
||||
}
|
||||
}
|
||||
|
||||
// True when [address, address+bytes) is inside the contiguous HHDM
|
||||
// mapping and so safe to touch at phys+hhdmBase.
|
||||
static bool HhdmReadable(uint64_t address, uint32_t bytes) {
|
||||
uint64_t limit = Memory::VMM::Paging::HhdmLimit();
|
||||
if (limit == 0) return false;
|
||||
if (address >= limit) return false;
|
||||
return (limit - address) >= bytes;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Constructor
|
||||
|
||||
// ============================================================================
|
||||
Interpreter::Interpreter()
|
||||
: m_dsdt(nullptr), m_dsdtLength(0), m_initialized(false) {}
|
||||
: m_dsdt(nullptr), m_dsdtLength(0), m_initialized(false),
|
||||
m_ctxPool(nullptr), m_ctxInUse(0), m_evalNesting(0), m_stackFloor(0),
|
||||
m_traceBudget(0) {}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Execution context pool
|
||||
|
||||
// ============================================================================
|
||||
Interpreter::ExecContext* Interpreter::AcquireContext() {
|
||||
if (!m_ctxPool) {
|
||||
m_ctxPool = (ExecContext*)Memory::g_heap->Request(
|
||||
sizeof(ExecContext) * (MaxCallDepth + 1));
|
||||
if (!m_ctxPool) return nullptr;
|
||||
}
|
||||
|
||||
if (m_ctxInUse > MaxCallDepth) return nullptr;
|
||||
|
||||
ExecContext* ctx = &m_ctxPool[m_ctxInUse++];
|
||||
*ctx = ExecContext{};
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void Interpreter::ReleaseContext() {
|
||||
if (m_ctxInUse > 0) m_ctxInUse--;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -148,7 +216,9 @@ namespace Hal {
|
||||
// Reads a NameString (which may include root prefix, parent prefixes,
|
||||
// dual/multi name prefix) and produces an absolute path.
|
||||
int Interpreter::ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode,
|
||||
char* outPath, int maxLen) {
|
||||
char* outPath, int maxLen,
|
||||
char* outRelative, int relLen,
|
||||
bool* outSearchable) {
|
||||
int pathPos = 0;
|
||||
bool isAbsolute = false;
|
||||
int parentPrefixCount = 0;
|
||||
@@ -220,6 +290,22 @@ namespace Hal {
|
||||
}
|
||||
|
||||
outPath[pathPos] = '\0';
|
||||
|
||||
// Hand back the name as written, for callers that must apply the
|
||||
// ACPI search rule when the rebased path does not exist.
|
||||
if (outSearchable)
|
||||
*outSearchable = !isAbsolute && parentPrefixCount == 0 && segCount > 0;
|
||||
|
||||
if (outRelative && relLen > 0) {
|
||||
int rp = 0;
|
||||
for (int i = 0; i < segCount; i++) {
|
||||
if (i > 0 && rp < relLen - 1) outRelative[rp++] = '.';
|
||||
for (int j = 0; j < 4 && rp < relLen - 1; j++)
|
||||
outRelative[rp++] = segments[i][j];
|
||||
}
|
||||
outRelative[rp] = '\0';
|
||||
}
|
||||
|
||||
return pathPos;
|
||||
}
|
||||
|
||||
@@ -582,13 +668,17 @@ namespace Hal {
|
||||
uint32_t fieldEnd = pkgStart + pkgLen;
|
||||
if (fieldEnd > endOffset) fieldEnd = endOffset;
|
||||
|
||||
// Region name
|
||||
// Region name. A Field almost always names a region
|
||||
// declared in an enclosing scope, so the search rule is not
|
||||
// optional here.
|
||||
char regionPath[256];
|
||||
ReadNameString(aml, pos, scopeNode, regionPath, 256);
|
||||
char regionRel[64];
|
||||
bool regionSearchable = false;
|
||||
ReadNameString(aml, pos, scopeNode, regionPath, 256,
|
||||
regionRel, sizeof(regionRel), ®ionSearchable);
|
||||
int32_t regionNode = m_ns.FindNode(regionPath);
|
||||
if (regionNode < 0) {
|
||||
regionNode = m_ns.ResolveName(regionPath, scopeNode);
|
||||
}
|
||||
if (regionNode < 0 && regionSearchable)
|
||||
regionNode = m_ns.SearchUpward(regionRel, scopeNode);
|
||||
|
||||
// Field flags
|
||||
uint8_t fieldFlags = aml[*pos];
|
||||
@@ -833,34 +923,74 @@ namespace Hal {
|
||||
auto* nsNode = m_ns.GetNode(node);
|
||||
if (!nsNode || nsNode->Obj.Type != ObjectType::Method) return false;
|
||||
|
||||
ExecContext ctx{};
|
||||
ctx.Aml = m_dsdt;
|
||||
ctx.AmlBase = nsNode->Obj.Method.AmlOffset;
|
||||
ctx.AmlLength = nsNode->Obj.Method.AmlLength;
|
||||
ctx.ScopeNode = node;
|
||||
ctx.Returned = false;
|
||||
ctx.Broken = false;
|
||||
ctx.Depth = 0;
|
||||
// Packages built inside the method come out of the transient ring.
|
||||
// Rewinding it here bounds the store to one evaluation's worth,
|
||||
// which is what makes repeated polling (_BST every few seconds)
|
||||
// safe. The caller must read the returned package before the next
|
||||
// evaluation, which every caller in the kernel does.
|
||||
// Evaluation is serialized: the execution contexts come from a
|
||||
// shared pool, and the battery indicator now polls from a user
|
||||
// thread while other subsystems may evaluate AML of their own.
|
||||
bool topLevel = false;
|
||||
m_evalLock.Acquire();
|
||||
if (m_evalNesting == 0) {
|
||||
topLevel = true;
|
||||
PackageStore::ResetTransient();
|
||||
|
||||
// Anchor the stack budget to this frame. Everything below
|
||||
// measures against it, so the same limit works whether we were
|
||||
// called on the large boot stack or a thread's 16 KiB one.
|
||||
char stackMarker = 0;
|
||||
m_stackFloor = (uint64_t)&stackMarker - StackBudget;
|
||||
}
|
||||
m_evalNesting++;
|
||||
|
||||
ExecContext* ctx = AcquireContext();
|
||||
if (!ctx) {
|
||||
m_evalNesting--;
|
||||
m_evalLock.Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx->Aml = m_dsdt;
|
||||
ctx->AmlBase = nsNode->Obj.Method.AmlOffset;
|
||||
ctx->AmlLength = nsNode->Obj.Method.AmlLength;
|
||||
ctx->ScopeNode = node;
|
||||
ctx->Returned = false;
|
||||
ctx->Broken = false;
|
||||
ctx->Depth = 0;
|
||||
ctx->ExprDepth = 0;
|
||||
|
||||
// Set up arguments
|
||||
for (int i = 0; i < argCount && i < MaxMethodArgs; i++) {
|
||||
ctx.Args[i] = args[i];
|
||||
ctx->Args[i] = args[i];
|
||||
}
|
||||
|
||||
// Initialize locals to zero
|
||||
for (int i = 0; i < MaxMethodLocals; i++) {
|
||||
ctx.Locals[i].Type = ObjectType::Integer;
|
||||
ctx.Locals[i].Integer = 0;
|
||||
ctx->Locals[i].Type = ObjectType::Integer;
|
||||
ctx->Locals[i].Integer = 0;
|
||||
}
|
||||
|
||||
bool ok = ExecuteBlock(ctx, ctx.AmlBase, ctx.AmlBase + ctx.AmlLength);
|
||||
bool ok = ExecuteBlock(*ctx, ctx->AmlBase, ctx->AmlBase + ctx->AmlLength);
|
||||
|
||||
result = ctx.ReturnValue;
|
||||
result = ctx->ReturnValue;
|
||||
if (result.Type == ObjectType::None) {
|
||||
result.Type = ObjectType::Integer;
|
||||
result.Integer = 0;
|
||||
}
|
||||
|
||||
ReleaseContext();
|
||||
m_evalNesting--;
|
||||
|
||||
// The floor refers to a frame on this call's stack. Leaving it set
|
||||
// would have the next evaluation -- possibly on a different thread's
|
||||
// stack entirely -- compare against an address that no longer means
|
||||
// anything, and bail out for no reason.
|
||||
if (topLevel) m_stackFloor = 0;
|
||||
|
||||
m_evalLock.Release();
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -870,6 +1000,12 @@ namespace Hal {
|
||||
|
||||
// ============================================================================
|
||||
bool Interpreter::ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset) {
|
||||
char stackMarker = 0;
|
||||
if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit();
|
||||
if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker))
|
||||
return false;
|
||||
DepthGuard depthGuard(ctx.ExprDepth);
|
||||
|
||||
uint32_t pos = offset;
|
||||
|
||||
while (pos < endOffset && !ctx.Returned && !ctx.Broken) {
|
||||
@@ -890,6 +1026,19 @@ namespace Hal {
|
||||
|
||||
uint8_t op = ctx.Aml[*pos];
|
||||
|
||||
if (m_traceBudget > 0) {
|
||||
m_traceBudget--;
|
||||
// Offsets are absolute into the DSDT, so they line up with a
|
||||
// disassembly of the same table.
|
||||
if (op == ExtOpPrefix && *pos + 1 < endOffset) {
|
||||
KernelLogStream(INFO, "AML") << " @" << base::hex << (uint64_t)*pos
|
||||
<< " ext op " << (uint64_t)ctx.Aml[*pos + 1];
|
||||
} else {
|
||||
KernelLogStream(INFO, "AML") << " @" << base::hex << (uint64_t)*pos
|
||||
<< " op " << (uint64_t)op;
|
||||
}
|
||||
}
|
||||
|
||||
// Return
|
||||
if (op == ReturnOp) {
|
||||
(*pos)++;
|
||||
@@ -988,8 +1137,11 @@ namespace Hal {
|
||||
int32_t targetNode = -1;
|
||||
bool isLocal = false, isArg = false;
|
||||
int localIdx = 0, argIdx = 0;
|
||||
EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx);
|
||||
StoreToTarget(ctx, value, targetNode, isLocal, localIdx, isArg, argIdx);
|
||||
int32_t pkgStore = -1; int pkgElement = 0;
|
||||
EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx,
|
||||
pkgStore, pkgElement);
|
||||
StoreToTarget(ctx, value, targetNode, isLocal, localIdx, isArg, argIdx,
|
||||
pkgStore, pkgElement);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -999,7 +1151,9 @@ namespace Hal {
|
||||
int32_t targetNode = -1;
|
||||
bool isLocal = false, isArg = false;
|
||||
int localIdx = 0, argIdx = 0;
|
||||
EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx);
|
||||
int32_t pkgStore = -1; int pkgElement = 0;
|
||||
EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx,
|
||||
pkgStore, pkgElement);
|
||||
|
||||
Object current{};
|
||||
if (isLocal) current = ctx.Locals[localIdx];
|
||||
@@ -1012,7 +1166,8 @@ namespace Hal {
|
||||
if (current.Type == ObjectType::Integer) {
|
||||
current.Integer += (op == IncrementOp) ? 1 : -1;
|
||||
}
|
||||
StoreToTarget(ctx, current, targetNode, isLocal, localIdx, isArg, argIdx);
|
||||
StoreToTarget(ctx, current, targetNode, isLocal, localIdx, isArg, argIdx,
|
||||
pkgStore, pkgElement);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1083,6 +1238,16 @@ namespace Hal {
|
||||
// Evaluate an AML term that produces a value.
|
||||
bool Interpreter::EvalTerm(ExecContext& ctx, uint32_t* pos, uint32_t endOffset,
|
||||
Object& result) {
|
||||
char stackMarker = 0;
|
||||
if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit();
|
||||
if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker)) {
|
||||
result = Object{};
|
||||
result.Type = ObjectType::Integer;
|
||||
result.Integer = 0;
|
||||
return false;
|
||||
}
|
||||
DepthGuard depthGuard(ctx.ExprDepth);
|
||||
|
||||
if (*pos >= endOffset) return false;
|
||||
|
||||
uint8_t op = ctx.Aml[*pos];
|
||||
@@ -1136,6 +1301,14 @@ namespace Hal {
|
||||
return true;
|
||||
}
|
||||
|
||||
// DerefOf. Index() already yields the element's value here, so a
|
||||
// dereference of one is just the value again; anything else passes
|
||||
// through unchanged.
|
||||
if (op == DerefOfOp) {
|
||||
(*pos)++;
|
||||
return EvalTerm(ctx, pos, endOffset, result);
|
||||
}
|
||||
|
||||
// Package
|
||||
if (op == PackageOp) {
|
||||
(*pos)++;
|
||||
@@ -1144,13 +1317,38 @@ namespace Hal {
|
||||
uint32_t pkgEnd = pkgStart + pkgLen;
|
||||
if (pkgEnd > endOffset) pkgEnd = endOffset;
|
||||
|
||||
// Store raw package as buffer for now
|
||||
// Retain the raw bytes (constant elements are read straight
|
||||
// out of them) and, in parallel, evaluate each element into the
|
||||
// package store so computed ones -- Package(){Local0, Local1},
|
||||
// the shape _BST and friends are built with -- survive.
|
||||
result.Type = ObjectType::Package;
|
||||
uint32_t dataLen = pkgEnd - *pos;
|
||||
if (dataLen > MaxBufferLen) dataLen = MaxBufferLen;
|
||||
result.Buffer.Length = dataLen;
|
||||
memcpy(result.Buffer.Data, &ctx.Aml[*pos], dataLen);
|
||||
|
||||
uint32_t elemPos = *pos;
|
||||
int count = (elemPos < pkgEnd) ? (int)ctx.Aml[elemPos] : 0;
|
||||
elemPos++;
|
||||
if (count > MaxPackageElements) count = MaxPackageElements;
|
||||
|
||||
if (count > 0) {
|
||||
int32_t store = PackageStore::AllocTransient(count);
|
||||
if (store > 0) {
|
||||
for (int i = 0; i < count && elemPos < pkgEnd; i++) {
|
||||
Object elem{};
|
||||
uint32_t before = elemPos;
|
||||
if (!EvalTerm(ctx, &elemPos, pkgEnd, elem)) break;
|
||||
// An element that consumed nothing would spin here
|
||||
// forever on a stream we decoded wrongly.
|
||||
if (elemPos <= before) break;
|
||||
Object* slot = PackageStore::Element(store, i);
|
||||
if (slot) *slot = elem;
|
||||
}
|
||||
result.PkgIndex = store;
|
||||
}
|
||||
}
|
||||
|
||||
*pos = pkgEnd;
|
||||
return true;
|
||||
}
|
||||
@@ -1259,14 +1457,16 @@ namespace Hal {
|
||||
int32_t tn = -1;
|
||||
bool il = false, ia = false;
|
||||
int li = 0, ai = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai);
|
||||
int32_t ps = -1; int pe = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
|
||||
} else if (IsLeadNameChar(next) || next == '\\' || next == '^') {
|
||||
int32_t tn = -1;
|
||||
bool il = false, ia = false;
|
||||
int li = 0, ai = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai);
|
||||
int32_t ps = -1; int pe = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
|
||||
}
|
||||
// ZeroOp as "no target" — skip
|
||||
else if (next == ZeroOp) {
|
||||
@@ -1289,8 +1489,9 @@ namespace Hal {
|
||||
int32_t tn = -1;
|
||||
bool il = false, ia = false;
|
||||
int li = 0, ai = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai);
|
||||
int32_t ps = -1; int pe = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
|
||||
} else if (*pos < endOffset && ctx.Aml[*pos] == ZeroOp) {
|
||||
(*pos)++;
|
||||
}
|
||||
@@ -1321,16 +1522,18 @@ namespace Hal {
|
||||
int32_t tn = -1;
|
||||
bool il = false, ia = false;
|
||||
int li = 0, ai = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai);
|
||||
StoreToTarget(ctx, remainder, tn, il, li, ia, ai);
|
||||
int32_t ps = -1; int pe = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
|
||||
StoreToTarget(ctx, remainder, tn, il, li, ia, ai, ps, pe);
|
||||
}
|
||||
// Result target
|
||||
if (*pos < endOffset) {
|
||||
int32_t tn = -1;
|
||||
bool il = false, ia = false;
|
||||
int li = 0, ai = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai);
|
||||
int32_t ps = -1; int pe = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1345,6 +1548,8 @@ namespace Hal {
|
||||
result.Integer = obj.String.Length;
|
||||
else if (obj.Type == ObjectType::Buffer)
|
||||
result.Integer = obj.Buffer.Length;
|
||||
else if (obj.Type == ObjectType::Package)
|
||||
result.Integer = (uint64_t)GetPackageCount(obj);
|
||||
else
|
||||
result.Integer = 0;
|
||||
return true;
|
||||
@@ -1357,12 +1562,23 @@ namespace Hal {
|
||||
EvalTerm(ctx, pos, endOffset, source);
|
||||
EvalTerm(ctx, pos, endOffset, index);
|
||||
|
||||
result.Type = ObjectType::Integer;
|
||||
uint64_t idx = index.Integer;
|
||||
|
||||
if (source.Type == ObjectType::Buffer && idx < source.Buffer.Length) {
|
||||
if (source.Type == ObjectType::Package) {
|
||||
// ACPI defines Index() as producing a reference; we hand
|
||||
// back the element's value instead, which is what every
|
||||
// operand context here goes on to use. Writes through
|
||||
// Index() are handled separately, in EvalTarget.
|
||||
if (!GetPackageElement(source, (int)idx, result)) {
|
||||
result = Object{};
|
||||
result.Type = ObjectType::Integer;
|
||||
result.Integer = 0;
|
||||
}
|
||||
} else if (source.Type == ObjectType::Buffer && idx < source.Buffer.Length) {
|
||||
result.Type = ObjectType::Integer;
|
||||
result.Integer = source.Buffer.Data[idx];
|
||||
} else {
|
||||
result.Type = ObjectType::Integer;
|
||||
result.Integer = 0;
|
||||
}
|
||||
|
||||
@@ -1371,8 +1587,9 @@ namespace Hal {
|
||||
int32_t tn = -1;
|
||||
bool il = false, ia = false;
|
||||
int li = 0, ai = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai);
|
||||
int32_t ps = -1; int pe = 0;
|
||||
EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe);
|
||||
StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe);
|
||||
} else if (*pos < endOffset && ctx.Aml[*pos] == ZeroOp) {
|
||||
(*pos)++;
|
||||
}
|
||||
@@ -1405,12 +1622,14 @@ namespace Hal {
|
||||
if (IsLeadNameChar(op) || op == '\\' || op == '^' ||
|
||||
op == DualNamePrefix || op == MultiNamePrefix) {
|
||||
char path[256];
|
||||
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256);
|
||||
char relative[64];
|
||||
bool searchable = false;
|
||||
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256,
|
||||
relative, sizeof(relative), &searchable);
|
||||
|
||||
int32_t node = m_ns.FindNode(path);
|
||||
if (node < 0) {
|
||||
node = m_ns.ResolveName(path, ctx.ScopeNode);
|
||||
}
|
||||
if (node < 0 && searchable)
|
||||
node = m_ns.SearchUpward(relative, ctx.ScopeNode);
|
||||
|
||||
if (node >= 0) {
|
||||
auto* nsNode = m_ns.GetNode(node);
|
||||
@@ -1430,7 +1649,13 @@ namespace Hal {
|
||||
return true;
|
||||
}
|
||||
|
||||
ExecContext subCtx{};
|
||||
ExecContext* sub = AcquireContext();
|
||||
if (!sub) {
|
||||
result.Type = ObjectType::Integer;
|
||||
result.Integer = 0;
|
||||
return true;
|
||||
}
|
||||
ExecContext& subCtx = *sub;
|
||||
subCtx.Aml = ctx.Aml;
|
||||
subCtx.AmlBase = nsNode->Obj.Method.AmlOffset;
|
||||
subCtx.AmlLength = nsNode->Obj.Method.AmlLength;
|
||||
@@ -1438,6 +1663,7 @@ namespace Hal {
|
||||
subCtx.Returned = false;
|
||||
subCtx.Broken = false;
|
||||
subCtx.Depth = ctx.Depth + 1;
|
||||
subCtx.ExprDepth = ctx.ExprDepth;
|
||||
for (int i = 0; i < argc; i++)
|
||||
subCtx.Args[i] = args[i];
|
||||
for (int i = 0; i < MaxMethodLocals; i++) {
|
||||
@@ -1451,6 +1677,7 @@ namespace Hal {
|
||||
result.Type = ObjectType::Integer;
|
||||
result.Integer = 0;
|
||||
}
|
||||
ReleaseContext();
|
||||
} else if (nsNode->Obj.Type == ObjectType::Field) {
|
||||
uint64_t val = 0;
|
||||
ReadField(node, val);
|
||||
@@ -1483,12 +1710,25 @@ namespace Hal {
|
||||
// ============================================================================
|
||||
bool Interpreter::EvalTarget(ExecContext& ctx, uint32_t* pos,
|
||||
int32_t& nodeIndex, bool& isLocal, int& localIdx,
|
||||
bool& isArg, int& argIdx) {
|
||||
bool& isArg, int& argIdx,
|
||||
int32_t& pkgStore, int& pkgElement) {
|
||||
nodeIndex = -1;
|
||||
char stackMarker = 0;
|
||||
if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit();
|
||||
if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker)) {
|
||||
isLocal = false; isArg = false;
|
||||
localIdx = 0; argIdx = 0;
|
||||
pkgStore = -1; pkgElement = 0;
|
||||
return false;
|
||||
}
|
||||
DepthGuard depthGuard(ctx.ExprDepth);
|
||||
|
||||
isLocal = false;
|
||||
isArg = false;
|
||||
localIdx = 0;
|
||||
argIdx = 0;
|
||||
pkgStore = -1;
|
||||
pkgElement = 0;
|
||||
|
||||
if (*pos >= ctx.AmlBase + ctx.AmlLength) return false;
|
||||
|
||||
@@ -1520,10 +1760,73 @@ namespace Hal {
|
||||
if (IsLeadNameChar(op) || op == '\\' || op == '^' ||
|
||||
op == DualNamePrefix || op == MultiNamePrefix) {
|
||||
char path[256];
|
||||
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256);
|
||||
char relative[64];
|
||||
bool searchable = false;
|
||||
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256,
|
||||
relative, sizeof(relative), &searchable);
|
||||
nodeIndex = m_ns.FindNode(path);
|
||||
if (nodeIndex < 0)
|
||||
nodeIndex = m_ns.ResolveName(path, ctx.ScopeNode);
|
||||
if (nodeIndex < 0 && searchable)
|
||||
nodeIndex = m_ns.SearchUpward(relative, ctx.ScopeNode);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Index(source, n) names a package element. This is how firmware
|
||||
// fills a result package -- Store(x, Index(BST0, 1)) -- so the
|
||||
// element has to be addressable, not just readable.
|
||||
if (op == IndexOp) {
|
||||
(*pos)++;
|
||||
|
||||
// Resolve the source by name where we can, so the write lands
|
||||
// in the namespace object rather than in a copy of it.
|
||||
int32_t srcNode = -1;
|
||||
uint8_t srcOp = (*pos < ctx.AmlBase + ctx.AmlLength) ? ctx.Aml[*pos] : 0;
|
||||
if (IsLeadNameChar(srcOp) || srcOp == '\\' || srcOp == '^' ||
|
||||
srcOp == DualNamePrefix || srcOp == MultiNamePrefix) {
|
||||
char path[256];
|
||||
char relative[64];
|
||||
bool searchable = false;
|
||||
ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256,
|
||||
relative, sizeof(relative), &searchable);
|
||||
srcNode = m_ns.FindNode(path);
|
||||
if (srcNode < 0 && searchable)
|
||||
srcNode = m_ns.SearchUpward(relative, ctx.ScopeNode);
|
||||
} else {
|
||||
// Locals and arguments can hold a package too.
|
||||
Object src{};
|
||||
EvalTerm(ctx, pos, ctx.AmlBase + ctx.AmlLength, src);
|
||||
if (src.Type == ObjectType::Package && src.PkgIndex > 0)
|
||||
pkgStore = src.PkgIndex;
|
||||
}
|
||||
|
||||
Object idx{};
|
||||
EvalTerm(ctx, pos, ctx.AmlBase + ctx.AmlLength, idx);
|
||||
pkgElement = (int)idx.Integer;
|
||||
|
||||
if (srcNode >= 0) {
|
||||
auto* srcObj = m_ns.GetNode(srcNode);
|
||||
if (srcObj && srcObj->Obj.Type == ObjectType::Package)
|
||||
pkgStore = MaterializeNodePackage(srcNode);
|
||||
}
|
||||
|
||||
// Index() always carries a target operand of its own -- ZeroOp
|
||||
// when there is none. It has to be consumed either way, or the
|
||||
// rest of the method decodes off by however long it was.
|
||||
if (*pos < ctx.AmlBase + ctx.AmlLength) {
|
||||
int32_t innerNode = -1;
|
||||
bool innerLocal = false, innerArg = false;
|
||||
int innerLocalIdx = 0, innerArgIdx = 0;
|
||||
int32_t innerStore = -1; int innerElement = 0;
|
||||
EvalTarget(ctx, pos, innerNode, innerLocal, innerLocalIdx,
|
||||
innerArg, innerArgIdx, innerStore, innerElement);
|
||||
}
|
||||
|
||||
if (pkgStore <= 0) {
|
||||
// Not a package we can write through (a buffer, or an
|
||||
// unresolvable source): discard the store rather than
|
||||
// corrupting an unrelated object.
|
||||
pkgStore = -1;
|
||||
nodeIndex = -1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1539,7 +1842,14 @@ namespace Hal {
|
||||
// ============================================================================
|
||||
void Interpreter::StoreToTarget(ExecContext& ctx, const Object& value,
|
||||
int32_t nodeIndex, bool isLocal, int localIdx,
|
||||
bool isArg, int argIdx) {
|
||||
bool isArg, int argIdx,
|
||||
int32_t pkgStore, int pkgElement) {
|
||||
if (pkgStore > 0) {
|
||||
Object* slot = PackageStore::Element(pkgStore, pkgElement);
|
||||
if (slot) *slot = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLocal && localIdx >= 0 && localIdx < MaxMethodLocals) {
|
||||
ctx.Locals[localIdx] = value;
|
||||
} else if (isArg && argIdx >= 0 && argIdx < MaxMethodArgs) {
|
||||
@@ -1558,6 +1868,171 @@ namespace Hal {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Package elements
|
||||
|
||||
// ============================================================================
|
||||
bool Interpreter::DecodeConstElement(const uint8_t* data, uint32_t length,
|
||||
uint32_t* pos, Object& out) {
|
||||
out = Object{};
|
||||
if (*pos >= length) return false;
|
||||
|
||||
uint8_t op = data[(*pos)++];
|
||||
|
||||
switch (op) {
|
||||
case ZeroOp:
|
||||
out.Type = ObjectType::Integer;
|
||||
out.Integer = 0;
|
||||
return true;
|
||||
case OneOp:
|
||||
out.Type = ObjectType::Integer;
|
||||
out.Integer = 1;
|
||||
return true;
|
||||
case OnesOp:
|
||||
out.Type = ObjectType::Integer;
|
||||
out.Integer = ~0ULL;
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Fixed-width integer prefixes
|
||||
int width = 0;
|
||||
if (op == BytePrefix) width = 1;
|
||||
else if (op == WordPrefix) width = 2;
|
||||
else if (op == DWordPrefix) width = 4;
|
||||
else if (op == QWordPrefix) width = 8;
|
||||
|
||||
if (width > 0) {
|
||||
if (*pos + (uint32_t)width > length) return false;
|
||||
uint64_t value = 0;
|
||||
for (int i = 0; i < width; i++)
|
||||
value |= (uint64_t)data[*pos + i] << (i * 8);
|
||||
*pos += (uint32_t)width;
|
||||
out.Type = ObjectType::Integer;
|
||||
out.Integer = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (op == StringPrefix) {
|
||||
out.Type = ObjectType::String;
|
||||
uint16_t n = 0;
|
||||
while (*pos < length && data[*pos] != 0) {
|
||||
if (n < MaxStringLen - 1)
|
||||
out.String.Data[n++] = (char)data[*pos];
|
||||
(*pos)++;
|
||||
}
|
||||
if (*pos < length) (*pos)++; // trailing NUL
|
||||
out.String.Data[n] = '\0';
|
||||
out.String.Length = n;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (op == BufferOp || op == PackageOp || op == VarPackageOp) {
|
||||
uint32_t start = *pos;
|
||||
uint32_t pkgLen = DecodePkgLength(data, pos);
|
||||
uint32_t end = start + pkgLen;
|
||||
if (end > length) end = length;
|
||||
if (end < *pos) return false;
|
||||
|
||||
uint32_t dataLen = end - *pos;
|
||||
if (dataLen > MaxBufferLen) dataLen = MaxBufferLen;
|
||||
|
||||
out.Type = (op == BufferOp) ? ObjectType::Buffer : ObjectType::Package;
|
||||
out.Buffer.Length = dataLen;
|
||||
memcpy(out.Buffer.Data, &data[*pos], dataLen);
|
||||
*pos = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
// A name reference, local, or anything else we cannot resolve from
|
||||
// the bytes alone. Skip it and hand back zero.
|
||||
if (IsLeadNameChar(op) || op == '\\' || op == '^' ||
|
||||
op == DualNamePrefix || op == MultiNamePrefix) {
|
||||
(*pos)--;
|
||||
uint32_t p = *pos;
|
||||
// Walk the NameString by hand: prefixes, then 4-char segments.
|
||||
while (p < length && (data[p] == '\\' || data[p] == '^')) p++;
|
||||
if (p < length && data[p] == DualNamePrefix) {
|
||||
p += 1 + 8;
|
||||
} else if (p < length && data[p] == MultiNamePrefix) {
|
||||
uint8_t segs = (p + 1 < length) ? data[p + 1] : 0;
|
||||
p += 2 + (uint32_t)segs * 4;
|
||||
} else if (p < length && data[p] == 0x00) {
|
||||
p += 1; // NullName
|
||||
} else {
|
||||
p += 4;
|
||||
}
|
||||
*pos = (p > length) ? length : p;
|
||||
}
|
||||
|
||||
out.Type = ObjectType::Integer;
|
||||
out.Integer = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
int Interpreter::GetPackageCount(const Object& package) {
|
||||
if (package.Type != ObjectType::Package) return 0;
|
||||
if (package.PkgIndex > 0)
|
||||
return PackageStore::Count(package.PkgIndex);
|
||||
// Raw form: the first byte of the retained AML is NumElements.
|
||||
if (package.Buffer.Length < 1) return 0;
|
||||
return (int)package.Buffer.Data[0];
|
||||
}
|
||||
|
||||
bool Interpreter::GetPackageElement(const Object& package, int index, Object& result) {
|
||||
result = Object{};
|
||||
if (package.Type != ObjectType::Package || index < 0) return false;
|
||||
|
||||
if (package.PkgIndex > 0) {
|
||||
Object* elem = PackageStore::Element(package.PkgIndex, index);
|
||||
if (!elem) return false;
|
||||
result = *elem;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t length = package.Buffer.Length;
|
||||
if (length < 1) return false;
|
||||
if (index >= (int)package.Buffer.Data[0]) return false;
|
||||
|
||||
uint32_t pos = 1; // past NumElements
|
||||
for (int i = 0; i <= index; i++) {
|
||||
if (!DecodeConstElement(package.Buffer.Data, length, &pos, result))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t Interpreter::MaterializeNodePackage(int32_t nodeIndex) {
|
||||
auto* node = m_ns.GetNode(nodeIndex);
|
||||
if (!node || node->Obj.Type != ObjectType::Package) return -1;
|
||||
if (node->Obj.PkgIndex > 0) return node->Obj.PkgIndex;
|
||||
|
||||
int count = GetPackageCount(node->Obj);
|
||||
if (count <= 0) return -1;
|
||||
if (count > MaxPackageElements) count = MaxPackageElements;
|
||||
|
||||
int32_t store = PackageStore::AllocPersistent(count);
|
||||
if (store < 0) return -1;
|
||||
|
||||
// Seed the elements from the constant bytes the package was
|
||||
// declared with, so a package that is only partly written through
|
||||
// Index() keeps its declared values everywhere else.
|
||||
uint32_t pos = 1;
|
||||
for (int i = 0; i < count; i++) {
|
||||
Object elem{};
|
||||
if (!DecodeConstElement(node->Obj.Buffer.Data, node->Obj.Buffer.Length,
|
||||
&pos, elem))
|
||||
break;
|
||||
Object* slot = PackageStore::Element(store, i);
|
||||
if (slot) *slot = elem;
|
||||
}
|
||||
|
||||
node->Obj.PkgIndex = store;
|
||||
return store;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// ReadField
|
||||
|
||||
// ============================================================================
|
||||
@@ -1578,7 +2053,19 @@ namespace Hal {
|
||||
uint64_t byteAddr = regBase + (bitOff / 8);
|
||||
uint32_t bitShift = bitOff % 8;
|
||||
|
||||
return ReadRegion(regionNode->Obj.Region.Space, byteAddr, bitLen + bitShift, value);
|
||||
uint64_t raw = 0;
|
||||
if (!ReadRegion(regionNode->Obj.Region.Space, byteAddr, bitLen + bitShift, raw))
|
||||
return false;
|
||||
|
||||
// ReadRegion hands back whole access units starting at byteAddr;
|
||||
// narrow that to the field's own bits so sub-byte flags (common in
|
||||
// EC and PM register layouts) don't carry their neighbours along.
|
||||
raw >>= bitShift;
|
||||
if (bitLen < 64)
|
||||
raw &= (1ULL << bitLen) - 1;
|
||||
|
||||
value = raw;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1639,6 +2126,13 @@ namespace Hal {
|
||||
}
|
||||
|
||||
if (space == RegionSpace::SystemMemory) {
|
||||
// The offset comes from firmware AML and may be nonsense: a
|
||||
// region we mis-decoded, or one whose base is computed by code
|
||||
// we do not fully execute. Dereferencing that through the HHDM
|
||||
// is how a bad table takes the kernel down with a #GP, so the
|
||||
// address has to be inside what Init() actually mapped.
|
||||
if (!HhdmReadable(address, accessBytes)) return false;
|
||||
|
||||
volatile uint8_t* mmio = (volatile uint8_t*)Memory::HHDM(address);
|
||||
if (accessBytes <= 1)
|
||||
value = *mmio;
|
||||
@@ -1651,6 +2145,19 @@ namespace Hal {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (space == RegionSpace::EmbeddedControl) {
|
||||
// The EC address space is a flat 256-byte array reached one
|
||||
// byte at a time over the command/data port pair, so a wider
|
||||
// field is assembled little-endian from consecutive reads.
|
||||
for (uint32_t i = 0; i < accessBytes; i++) {
|
||||
uint8_t byte = 0;
|
||||
if (!EmbeddedController::Read((uint8_t)(address + i), byte))
|
||||
return false;
|
||||
value |= (uint64_t)byte << (i * 8);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (space == RegionSpace::PciConfig) {
|
||||
// PCI config space access -- not yet implemented
|
||||
return false;
|
||||
@@ -1681,6 +2188,8 @@ namespace Hal {
|
||||
}
|
||||
|
||||
if (space == RegionSpace::SystemMemory) {
|
||||
if (!HhdmReadable(address, accessBytes)) return false;
|
||||
|
||||
volatile uint8_t* mmio = (volatile uint8_t*)Memory::HHDM(address);
|
||||
if (accessBytes <= 1)
|
||||
*mmio = (uint8_t)value;
|
||||
@@ -1693,6 +2202,15 @@ namespace Hal {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (space == RegionSpace::EmbeddedControl) {
|
||||
for (uint32_t i = 0; i < accessBytes; i++) {
|
||||
if (!EmbeddedController::Write((uint8_t)(address + i),
|
||||
(uint8_t)(value >> (i * 8))))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "AmlNamespace.hpp"
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
@@ -105,6 +106,24 @@ namespace Hal {
|
||||
static constexpr int MaxCallDepth = 16;
|
||||
static constexpr int MaxLoopIterations = 1024;
|
||||
|
||||
// Backstop cap on evaluation nesting. This counter is shared by
|
||||
// expression evaluation AND block execution, and block nesting is
|
||||
// legitimately deep -- every If/Else/While body is another level, so a
|
||||
// long firmware method reaches well past a handful. The real protection
|
||||
// against running off the stack is HaveStackHeadroom(), which measures
|
||||
// actual usage; this only catches a decode that recurses without
|
||||
// consuming input.
|
||||
static constexpr int MaxExprDepth = 48;
|
||||
|
||||
// How much stack the interpreter may consume before it gives up. Note
|
||||
// this is an ALLOWANCE, not a reserve: frames here are expensive (a
|
||||
// name-resolving EvalTerm frame carries a 256-byte path buffer plus
|
||||
// 272-byte Object temporaries, so ~800 bytes each), and a real
|
||||
// firmware _BIF nests far enough to need tens of them. Sized against
|
||||
// Sched::StackPages -- keep well under it, since the syscall path below
|
||||
// has already spent some of that stack.
|
||||
static constexpr uint64_t StackBudget = 32768;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Interpreter
|
||||
@@ -138,6 +157,31 @@ namespace Hal {
|
||||
// Check if the interpreter has been initialized
|
||||
bool IsInitialized() const { return m_initialized; }
|
||||
|
||||
// Log every opcode executed, with its offset, for the next
|
||||
// `budget` opcodes. The only way to see where a method that runs to
|
||||
// completion without returning a value actually goes.
|
||||
void SetTrace(int budget) { m_traceBudget = budget; }
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Packages
|
||||
|
||||
// ============================================================================
|
||||
// Read one element of a Package object. Works for both a package
|
||||
// whose elements were computed at run time (materialized in the
|
||||
// package store) and one that is still just the constant AML bytes
|
||||
// it was declared with. Returns false when the object is not a
|
||||
// package or the index is out of range.
|
||||
bool GetPackageElement(const Object& package, int index, Object& result);
|
||||
|
||||
// Number of elements a Package declares.
|
||||
int GetPackageCount(const Object& package);
|
||||
|
||||
// Give a package stored in a namespace node real per-element
|
||||
// storage so Index() can be written through. Returns the store
|
||||
// index, or -1 if the node does not hold a package.
|
||||
int32_t MaterializeNodePackage(int32_t nodeIndex);
|
||||
|
||||
private:
|
||||
// ============================================================================
|
||||
// Parsing (table load)
|
||||
@@ -158,8 +202,17 @@ namespace Hal {
|
||||
// ============================================================================
|
||||
// Read a NameString from AML and produce an absolute path.
|
||||
// Advances *pos past the name.
|
||||
// Produces the absolute path (rebased onto scopeNode for a
|
||||
// relative name). outRelative, when given, receives the name as it
|
||||
// actually appeared in the AML, and *outSearchable says whether the
|
||||
// ACPI search rule applies to it -- true only when the name carried
|
||||
// no root or parent prefix. Resolution must fall back to
|
||||
// Namespace::SearchUpward() in that case: the absolute path is a
|
||||
// guess at one scope, and the object usually lives in a parent.
|
||||
int ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode,
|
||||
char* outPath, int maxLen);
|
||||
char* outPath, int maxLen,
|
||||
char* outRelative = nullptr, int relLen = 0,
|
||||
bool* outSearchable = nullptr);
|
||||
|
||||
// Read a single 4-char NameSeg from AML. Advances *pos.
|
||||
void ReadNameSeg(const uint8_t* aml, uint32_t* pos, char* outSeg);
|
||||
@@ -170,6 +223,13 @@ namespace Hal {
|
||||
|
||||
// ============================================================================
|
||||
uint32_t DecodePkgLength(const uint8_t* aml, uint32_t* pos);
|
||||
|
||||
// Decode one constant package element out of raw AML bytes and
|
||||
// advance *pos past it. Non-constant elements (name references,
|
||||
// locals) decode to integer 0, which is what they were worth
|
||||
// before the package store existed.
|
||||
bool DecodeConstElement(const uint8_t* data, uint32_t length,
|
||||
uint32_t* pos, Object& out);
|
||||
uint64_t DecodeInteger(const uint8_t* aml, uint32_t* pos);
|
||||
|
||||
// ============================================================================
|
||||
@@ -187,7 +247,16 @@ namespace Hal {
|
||||
Object ReturnValue;
|
||||
bool Returned;
|
||||
bool Broken;
|
||||
int Depth;
|
||||
int Depth; // method-call nesting
|
||||
int ExprDepth; // expression/block nesting
|
||||
};
|
||||
|
||||
// Increments a depth counter for the duration of a scope; pairs
|
||||
// with the MaxExprDepth check at each recursive entry point.
|
||||
struct DepthGuard {
|
||||
int& Value;
|
||||
explicit DepthGuard(int& v) : Value(v) { Value++; }
|
||||
~DepthGuard() { Value--; }
|
||||
};
|
||||
|
||||
bool ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset);
|
||||
@@ -199,14 +268,19 @@ namespace Hal {
|
||||
// Evaluate a "SuperName" target for Store operations.
|
||||
// Returns the node index for named targets, or handles locals/args.
|
||||
// If isLocal/isArg is set, localIdx/argIdx contains the index.
|
||||
// A target may also be Index(package, n), which names a package
|
||||
// element rather than a node, local or arg. pkgStore/pkgElement
|
||||
// carry that case (pkgStore > 0 when it applies).
|
||||
bool EvalTarget(ExecContext& ctx, uint32_t* pos,
|
||||
int32_t& nodeIndex, bool& isLocal, int& localIdx,
|
||||
bool& isArg, int& argIdx);
|
||||
bool& isArg, int& argIdx,
|
||||
int32_t& pkgStore, int& pkgElement);
|
||||
|
||||
// Store a value to a target (node, local, or arg).
|
||||
void StoreToTarget(ExecContext& ctx, const Object& value,
|
||||
int32_t nodeIndex, bool isLocal, int localIdx,
|
||||
bool isArg, int argIdx);
|
||||
bool isArg, int argIdx,
|
||||
int32_t pkgStore = -1, int pkgElement = 0);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -221,10 +295,32 @@ namespace Hal {
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
// ExecContext is ~4.4 KB (sixteen Objects, each carrying a
|
||||
// 256-byte buffer inline). One per nested method call on a 16 KiB
|
||||
// kernel stack overflows after three levels, so they come from a
|
||||
// pool instead. The pool is only safe because evaluation is
|
||||
// serialized -- see m_evalLock.
|
||||
// Stack-exhaustion check; `frame` is the address of a local in
|
||||
// the calling frame.
|
||||
bool HaveStackHeadroom(const void* frame) const;
|
||||
|
||||
// One-shot warning when the nesting backstop trips.
|
||||
static void ReportDepthLimit();
|
||||
|
||||
ExecContext* AcquireContext();
|
||||
void ReleaseContext();
|
||||
|
||||
Namespace m_ns;
|
||||
const uint8_t* m_dsdt;
|
||||
uint32_t m_dsdtLength;
|
||||
bool m_initialized;
|
||||
|
||||
ExecContext* m_ctxPool; // MaxCallDepth + 1 entries, heap
|
||||
int m_ctxInUse;
|
||||
int m_evalNesting; // top-level evaluations in progress
|
||||
uint64_t m_stackFloor; // stack address the interpreter stops at
|
||||
int m_traceBudget; // opcodes left to log, 0 = off
|
||||
kcp::Mutex m_evalLock;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -7,10 +7,114 @@
|
||||
#include "AmlNamespace.hpp"
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Memory/Heap.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace AML {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Package element store
|
||||
|
||||
// ============================================================================
|
||||
namespace PackageStore {
|
||||
|
||||
struct Entry {
|
||||
Object* Elements;
|
||||
int Count;
|
||||
};
|
||||
|
||||
static Entry s_persistent[MaxPersistent];
|
||||
static Entry s_transient[MaxTransient];
|
||||
static int s_persistentUsed = 0;
|
||||
static int s_transientNext = 0;
|
||||
|
||||
// Indices handed out are 1-based so that a zeroed Object (nodes are
|
||||
// memset before Clear() runs, and callers zero-init freely) never
|
||||
// looks like a reference to entry 0.
|
||||
static constexpr int32_t TransientBase = MaxPersistent;
|
||||
|
||||
static Object* AllocElements(int count) {
|
||||
auto* mem = (Object*)Memory::g_heap->Request(sizeof(Object) * count);
|
||||
if (!mem) return nullptr;
|
||||
for (int i = 0; i < count; i++)
|
||||
mem[i] = Object{};
|
||||
return mem;
|
||||
}
|
||||
|
||||
int32_t AllocPersistent(int count) {
|
||||
if (count <= 0 || count > MaxPackageElements) return -1;
|
||||
if (s_persistentUsed >= MaxPersistent) return -1;
|
||||
|
||||
Entry& e = s_persistent[s_persistentUsed];
|
||||
e.Elements = AllocElements(count);
|
||||
if (!e.Elements) return -1;
|
||||
e.Count = count;
|
||||
return (int32_t)(s_persistentUsed++) + 1;
|
||||
}
|
||||
|
||||
int32_t AllocTransient(int count) {
|
||||
if (count <= 0 || count > MaxPackageElements) return -1;
|
||||
|
||||
int slot = s_transientNext;
|
||||
if (slot >= MaxTransient) {
|
||||
// The ring is only rewound between top-level evaluations, so
|
||||
// a method that builds more packages than this reuses the
|
||||
// oldest slots rather than failing outright.
|
||||
slot = 0;
|
||||
s_transientNext = 0;
|
||||
}
|
||||
s_transientNext++;
|
||||
|
||||
Entry& e = s_transient[slot];
|
||||
// Element arrays are sized on first use and reused afterwards;
|
||||
// a larger request reallocates.
|
||||
if (e.Elements == nullptr || e.Count < count) {
|
||||
Object* mem = AllocElements(count);
|
||||
if (!mem) return -1;
|
||||
if (e.Elements) Memory::g_heap->Free(e.Elements);
|
||||
e.Elements = mem;
|
||||
} else {
|
||||
for (int i = 0; i < count; i++)
|
||||
e.Elements[i] = Object{};
|
||||
}
|
||||
e.Count = count;
|
||||
return TransientBase + slot + 1;
|
||||
}
|
||||
|
||||
void ResetTransient() {
|
||||
s_transientNext = 0;
|
||||
}
|
||||
|
||||
static Entry* Lookup(int32_t index) {
|
||||
if (index <= 0) return nullptr;
|
||||
int32_t i = index - 1;
|
||||
|
||||
if (i < MaxPersistent) {
|
||||
if (i >= s_persistentUsed) return nullptr;
|
||||
return s_persistent[i].Elements ? &s_persistent[i] : nullptr;
|
||||
}
|
||||
|
||||
i -= TransientBase;
|
||||
if (i < 0 || i >= MaxTransient) return nullptr;
|
||||
return s_transient[i].Elements ? &s_transient[i] : nullptr;
|
||||
}
|
||||
|
||||
Object* Element(int32_t index, int element) {
|
||||
Entry* e = Lookup(index);
|
||||
if (!e || element < 0 || element >= e->Count) return nullptr;
|
||||
return &e->Elements[element];
|
||||
}
|
||||
|
||||
int Count(int32_t index) {
|
||||
Entry* e = Lookup(index);
|
||||
return e ? e->Count : 0;
|
||||
}
|
||||
};
|
||||
|
||||
Namespace::Namespace() : m_chunkCount(0), m_nodeCount(0) {
|
||||
for (int i = 0; i < MaxChunks; i++)
|
||||
m_chunks[i] = nullptr;
|
||||
@@ -30,7 +134,17 @@ namespace Hal {
|
||||
}
|
||||
|
||||
bool Namespace::AllocChunk() {
|
||||
if (m_chunkCount >= MaxChunks) return false;
|
||||
if (m_chunkCount >= MaxChunks) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
KernelLogStream(WARNING, "AML")
|
||||
<< "Namespace node pool full at " << base::dec
|
||||
<< (uint64_t)(MaxChunks * NodesPerChunk)
|
||||
<< " nodes; part of the namespace will be missing";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* chunk = (NamespaceNode*)Memory::g_heap->Request(
|
||||
sizeof(NamespaceNode) * NodesPerChunk);
|
||||
@@ -45,6 +159,41 @@ namespace Hal {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Namespace::AppendChild(NamespaceNode* parent, int32_t child) {
|
||||
if (parent->ChildCount >= parent->ChildCapacity) {
|
||||
int32_t newCap = parent->ChildCapacity ? parent->ChildCapacity * 2
|
||||
: InitialChildren;
|
||||
|
||||
auto* grown = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * newCap);
|
||||
if (!grown) {
|
||||
// Losing a child silently costs whole devices, so say it
|
||||
// once rather than leaving a hole nobody can see.
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
KernelLogStream(WARNING, "AML")
|
||||
<< "Out of heap growing a namespace node's child list;"
|
||||
<< " part of the namespace will be missing";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < parent->ChildCount; i++)
|
||||
grown[i] = parent->ChildIndices[i];
|
||||
for (int32_t i = parent->ChildCount; i < newCap; i++)
|
||||
grown[i] = -1;
|
||||
|
||||
if (parent->ChildIndices)
|
||||
Memory::g_heap->Free(parent->ChildIndices);
|
||||
|
||||
parent->ChildIndices = grown;
|
||||
parent->ChildCapacity = newCap;
|
||||
}
|
||||
|
||||
parent->ChildIndices[parent->ChildCount++] = child;
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t Namespace::AllocNode() {
|
||||
// Check if we need a new chunk
|
||||
int32_t capacity = m_chunkCount * NodesPerChunk;
|
||||
@@ -165,11 +314,7 @@ namespace Hal {
|
||||
// Add to parent's children
|
||||
auto* parent = GetNode(current);
|
||||
if (!parent) return -1;
|
||||
if (parent->ChildCount < MaxChildren) {
|
||||
parent->ChildIndices[parent->ChildCount++] = child;
|
||||
} else {
|
||||
return -1; // too many children
|
||||
}
|
||||
if (!AppendChild(parent, child)) return -1;
|
||||
}
|
||||
current = child;
|
||||
}
|
||||
@@ -188,6 +333,51 @@ namespace Hal {
|
||||
return current;
|
||||
}
|
||||
|
||||
int32_t Namespace::SearchUpward(const char* relative, int32_t scopeNodeIndex) const {
|
||||
char segments[MaxPathDepth][MaxNameSegLen + 1];
|
||||
int segCount = 0;
|
||||
|
||||
// Split on '.', padding each segment to four characters the way
|
||||
// node names are stored.
|
||||
int i = 0;
|
||||
while (relative[i] && segCount < MaxPathDepth) {
|
||||
char seg[MaxNameSegLen + 1];
|
||||
int n = 0;
|
||||
while (relative[i] && relative[i] != '.' && n < MaxNameSegLen)
|
||||
seg[n++] = relative[i++];
|
||||
seg[n] = '\0';
|
||||
while (relative[i] && relative[i] != '.') i++; // overlong segment
|
||||
if (relative[i] == '.') i++;
|
||||
|
||||
PadSegment(seg, segments[segCount]);
|
||||
segCount++;
|
||||
}
|
||||
|
||||
if (segCount == 0) return -1;
|
||||
|
||||
// Walk up from the current scope looking for the first segment.
|
||||
int32_t scope = scopeNodeIndex;
|
||||
while (scope >= 0) {
|
||||
int32_t found = FindChildByName(scope, segments[0]);
|
||||
if (found >= 0) {
|
||||
// Descend the rest. A scope that matches the first segment
|
||||
// but not the remainder does not win -- keep walking up.
|
||||
int32_t cur = found;
|
||||
bool ok = true;
|
||||
for (int k = 1; k < segCount; k++) {
|
||||
cur = FindChildByName(cur, segments[k]);
|
||||
if (cur < 0) { ok = false; break; }
|
||||
}
|
||||
if (ok) return cur;
|
||||
}
|
||||
|
||||
auto* node = GetNode(scope);
|
||||
scope = node ? node->ParentIndex : -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t Namespace::ResolveName(const char* name, int32_t scopeNodeIndex) const {
|
||||
// If it starts with '\', it's absolute
|
||||
if (name[0] == '\\')
|
||||
|
||||
@@ -55,10 +55,15 @@ namespace Hal {
|
||||
// ============================================================================
|
||||
static constexpr int MaxNameSegLen = 4;
|
||||
static constexpr int MaxPathDepth = 16;
|
||||
static constexpr int MaxChildren = 64;
|
||||
// Children are held in a heap array that grows on demand, starting at
|
||||
// this many entries. A fixed ceiling here is not survivable: an EC or
|
||||
// GPE scope in a modern laptop DSDT carries well over a hundred named
|
||||
// objects, and dropping the overflow loses whole devices (the battery
|
||||
// among them) with no way to tell from the outside.
|
||||
static constexpr int InitialChildren = 8;
|
||||
static constexpr int MaxStringLen = 64;
|
||||
static constexpr int MaxBufferLen = 256;
|
||||
static constexpr int MaxPackageElements = 16;
|
||||
static constexpr int MaxPackageElements = 32;
|
||||
static constexpr int MaxMethodArgs = 7;
|
||||
static constexpr int MaxMethodLocals = 8;
|
||||
|
||||
@@ -76,6 +81,14 @@ namespace Hal {
|
||||
struct Object {
|
||||
ObjectType Type = ObjectType::None;
|
||||
|
||||
// Packages keep their raw AML bytes in Buffer (constant elements can
|
||||
// be decoded straight from there), but a package whose elements are
|
||||
// computed or written through Index() needs real per-element
|
||||
// storage. PkgIndex names that storage once it is materialized;
|
||||
// -1 means "not materialized yet". It lives outside the union so a
|
||||
// package can carry both representations at once.
|
||||
int32_t PkgIndex = -1;
|
||||
|
||||
union {
|
||||
uint64_t Integer;
|
||||
|
||||
@@ -116,7 +129,36 @@ namespace Hal {
|
||||
} Processor;
|
||||
};
|
||||
|
||||
Object() : Type(ObjectType::None), Integer(0) {}
|
||||
Object() : Type(ObjectType::None), PkgIndex(-1), Integer(0) {}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Package element store
|
||||
|
||||
// ============================================================================
|
||||
// Element arrays for materialized packages, kept outside Object so the
|
||||
// tagged union stays small. Entries are split in two: a persistent
|
||||
// region for packages declared with Name() (materialized once and kept
|
||||
// for the life of the namespace) and a transient ring for packages
|
||||
// built inside a method, which is rewound at each top-level evaluation
|
||||
// so repeated calls -- polling _BST, say -- cannot exhaust it.
|
||||
namespace PackageStore {
|
||||
static constexpr int MaxPersistent = 32;
|
||||
static constexpr int MaxTransient = 32;
|
||||
|
||||
// Allocate an element array of `count` objects. Returns -1 when the
|
||||
// store is full or the count is out of range.
|
||||
int32_t AllocPersistent(int count);
|
||||
int32_t AllocTransient(int count);
|
||||
|
||||
// Rewind the transient ring. Called once per top-level evaluation.
|
||||
void ResetTransient();
|
||||
|
||||
// Element access. Returns nullptr for an unknown entry or an
|
||||
// out-of-range element.
|
||||
Object* Element(int32_t index, int element);
|
||||
int Count(int32_t index);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -129,16 +171,19 @@ namespace Hal {
|
||||
char Name[MaxNameSegLen + 1]; // null-terminated 4-char segment
|
||||
Object Obj;
|
||||
int32_t ParentIndex; // -1 for root
|
||||
int32_t ChildIndices[MaxChildren];
|
||||
int32_t* ChildIndices; // heap array, ChildCapacity long
|
||||
int32_t ChildCount;
|
||||
int32_t ChildCapacity;
|
||||
|
||||
// Only ever called on a freshly allocated (zeroed) node, so there is
|
||||
// no child array to release here.
|
||||
void Clear() {
|
||||
Name[0] = 0;
|
||||
Obj = Object{};
|
||||
ParentIndex = -1;
|
||||
ChildIndices = nullptr;
|
||||
ChildCount = 0;
|
||||
for (int i = 0; i < MaxChildren; i++)
|
||||
ChildIndices[i] = -1;
|
||||
ChildCapacity = 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -166,6 +211,14 @@ namespace Hal {
|
||||
// 3. Root scope
|
||||
int32_t ResolveName(const char* name, int32_t scopeNodeIndex) const;
|
||||
|
||||
// The ACPI name search rule: for a name written WITHOUT a root or
|
||||
// parent prefix, search the current scope for its first segment,
|
||||
// then each parent scope up to the root, and descend the remaining
|
||||
// segments from wherever it matched. `relative` is the name as it
|
||||
// appeared in the AML ("PBIF", "BAT0.PBIF"), never a path already
|
||||
// rebased onto a scope. Returns -1 if no scope contains it.
|
||||
int32_t SearchUpward(const char* relative, int32_t scopeNodeIndex) const;
|
||||
|
||||
// Get a node by index.
|
||||
NamespaceNode* GetNode(int32_t index);
|
||||
const NamespaceNode* GetNode(int32_t index) const;
|
||||
@@ -217,6 +270,11 @@ namespace Hal {
|
||||
private:
|
||||
int32_t AllocNode();
|
||||
bool AllocChunk();
|
||||
|
||||
// Append a child index to a parent, growing its array as needed.
|
||||
// Returns false only when the heap is exhausted.
|
||||
bool AppendChild(NamespaceNode* parent, int32_t child);
|
||||
|
||||
void EnsureRoot();
|
||||
int32_t FindChildByName(int32_t parentIndex, const char* seg) const;
|
||||
|
||||
|
||||
@@ -319,6 +319,58 @@ namespace Hal {
|
||||
|
||||
// EnumerateAll
|
||||
|
||||
// ============================================================================
|
||||
// Fill one DeviceInfo from a namespace node.
|
||||
static void DescribeDevice(AML::Namespace& ns, int32_t idx, DeviceInfo& dev) {
|
||||
dev.NodeIndex = idx;
|
||||
ns.GetNodePath(idx, dev.Path, 128);
|
||||
|
||||
dev.Status = EvaluateSta(idx);
|
||||
dev.IsPresent = (dev.Status & STA_PRESENT) && (dev.Status & STA_FUNCTIONAL);
|
||||
|
||||
if (!EvaluateHid(idx, dev.HardwareId, 16))
|
||||
dev.HardwareId[0] = '\0';
|
||||
if (!EvaluateUid(idx, dev.UniqueId, 16))
|
||||
dev.UniqueId[0] = '\0';
|
||||
|
||||
dev.Address = EvaluateAdr(idx);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// FindDeviceByHid
|
||||
|
||||
// ============================================================================
|
||||
bool FindDeviceByHid(const char* hid, DeviceInfo& out) {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return false;
|
||||
|
||||
auto& ns = interp.GetNamespace();
|
||||
int32_t found = -1;
|
||||
|
||||
// Only _HID is evaluated per candidate; the full description is
|
||||
// built once, for the match. WalkDescendants has no early exit, so
|
||||
// the first match wins and later ones are ignored.
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||
[&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) {
|
||||
if (found >= 0) return;
|
||||
|
||||
char candidate[16];
|
||||
if (!EvaluateHid(idx, candidate, 16)) return;
|
||||
if (StrEqual(candidate, hid)) found = idx;
|
||||
});
|
||||
|
||||
if (found < 0) return false;
|
||||
|
||||
out = DeviceInfo{};
|
||||
DescribeDevice(ns, found, out);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// EnumerateAll
|
||||
|
||||
// ============================================================================
|
||||
void EnumerateAll(DeviceList& result) {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
@@ -326,39 +378,32 @@ namespace Hal {
|
||||
|
||||
auto& ns = interp.GetNamespace();
|
||||
result.Count = 0;
|
||||
result.Truncated = false;
|
||||
|
||||
// Walk all namespace nodes looking for Device objects
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||
[&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) {
|
||||
if (result.Count >= MaxDevices) return;
|
||||
|
||||
auto& dev = result.Devices[result.Count];
|
||||
dev.NodeIndex = idx;
|
||||
|
||||
// Get the path
|
||||
ns.GetNodePath(idx, dev.Path, 128);
|
||||
|
||||
// Evaluate _STA
|
||||
dev.Status = EvaluateSta(idx);
|
||||
dev.IsPresent = (dev.Status & STA_PRESENT) && (dev.Status & STA_FUNCTIONAL);
|
||||
|
||||
// Evaluate _HID
|
||||
if (!EvaluateHid(idx, dev.HardwareId, 16))
|
||||
dev.HardwareId[0] = '\0';
|
||||
|
||||
// Evaluate _UID
|
||||
if (!EvaluateUid(idx, dev.UniqueId, 16))
|
||||
dev.UniqueId[0] = '\0';
|
||||
|
||||
// Evaluate _ADR
|
||||
dev.Address = EvaluateAdr(idx);
|
||||
if (result.Count >= MaxDevices) {
|
||||
result.Truncated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
DescribeDevice(ns, idx, result.Devices[result.Count]);
|
||||
result.Count++;
|
||||
});
|
||||
|
||||
KernelLogStream(OK, "ACPI") << "Enumerated " << base::dec
|
||||
<< (uint64_t)result.Count << " ACPI devices";
|
||||
|
||||
// The list is a fixed-size stack object, so a large namespace runs
|
||||
// past it. Say so: a device missing from the log below may simply
|
||||
// be one that did not fit, and looking it up by HID still works.
|
||||
if (result.Truncated) {
|
||||
KernelLogStream(WARNING, "ACPI") << "Device list truncated at "
|
||||
<< base::dec << (uint64_t)MaxDevices
|
||||
<< "; use FindDeviceByHid to reach the rest";
|
||||
}
|
||||
|
||||
// Log discovered devices
|
||||
for (int i = 0; i < result.Count; i++) {
|
||||
auto& dev = result.Devices[i];
|
||||
|
||||
@@ -45,8 +45,9 @@ namespace Hal {
|
||||
struct DeviceList {
|
||||
DeviceInfo Devices[MaxDevices];
|
||||
int Count;
|
||||
bool Truncated; // the namespace held more than MaxDevices
|
||||
|
||||
DeviceList() : Count(0) {}
|
||||
DeviceList() : Count(0), Truncated(false) {}
|
||||
|
||||
// Find a device by HID. Returns nullptr if not found.
|
||||
const DeviceInfo* FindByHid(const char* hid) const;
|
||||
@@ -64,6 +65,12 @@ namespace Hal {
|
||||
// The interpreter must be initialized first (LoadTable called).
|
||||
void EnumerateAll(DeviceList& result);
|
||||
|
||||
// Find the first device with the given _HID, walking the namespace
|
||||
// directly. Unlike EnumerateAll this has no device-count ceiling and
|
||||
// needs no multi-kilobyte list on the caller's stack, so it is the way
|
||||
// to reach a specific device on a machine whose namespace is large.
|
||||
bool FindDeviceByHid(const char* hid, DeviceInfo& out);
|
||||
|
||||
// Evaluate _STA for a device node. Returns STA_DEFAULT if no _STA exists.
|
||||
uint32_t EvaluateSta(int32_t deviceNodeIndex);
|
||||
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* Battery.cpp
|
||||
* ACPI Control Method Battery: _STA / _BIF / _BIX / _BST, plus _PSR on the
|
||||
* AC adapter
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Battery.hpp"
|
||||
#include "AcpiDevices.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace Battery {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
// ACPI reports an unreadable field as all-ones rather than zero.
|
||||
static constexpr uint32_t Unknown32 = 0xFFFFFFFFu;
|
||||
|
||||
// The EC transactions behind _BST take a few milliseconds, and the
|
||||
// value only moves on a scale of minutes, so a short cache keeps a
|
||||
// one-second panel repaint from becoming a bus workload.
|
||||
static constexpr uint64_t CacheMs = 2000;
|
||||
|
||||
static bool g_initialized = false;
|
||||
static bool g_haveBattery = false;
|
||||
static bool g_haveAdapter = false;
|
||||
static char g_batteryPath[128];
|
||||
static char g_adapterPath[128];
|
||||
static int32_t g_batteryNode = -1;
|
||||
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
// Set for the boot-path probe only, so the per-method tracing does not
|
||||
// repeat on every poll once the system is up.
|
||||
static bool g_tracing = false;
|
||||
|
||||
static bool Refresh(Status& out);
|
||||
static void Probe();
|
||||
static Status g_cached{};
|
||||
static bool g_cacheValid = false;
|
||||
static uint64_t g_cacheStamp = 0;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Path helpers
|
||||
|
||||
// ============================================================================
|
||||
static void AppendMethod(const char* devicePath, const char* method, char* out, int maxLen) {
|
||||
int i = 0;
|
||||
while (devicePath[i] && i < maxLen - 6) {
|
||||
out[i] = devicePath[i];
|
||||
i++;
|
||||
}
|
||||
// The root scope is written "\" with no separator before the first
|
||||
// segment; every other path takes a dot.
|
||||
if (!(i == 1 && out[0] == '\\'))
|
||||
out[i++] = '.';
|
||||
for (int j = 0; j < 4 && i < maxLen - 1; j++)
|
||||
out[i++] = method[j];
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
static void CopyPath(char* dst, const char* src, int maxLen) {
|
||||
int i = 0;
|
||||
while (src[i] && i < maxLen - 1) {
|
||||
dst[i] = src[i];
|
||||
i++;
|
||||
}
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Initialize
|
||||
|
||||
// ============================================================================
|
||||
void Initialize() {
|
||||
if (g_initialized) return;
|
||||
g_initialized = true;
|
||||
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return;
|
||||
|
||||
// Searched by HID rather than off an enumerated list: a laptop
|
||||
// namespace runs well past the list's fixed size, and the battery
|
||||
// sits deep under the EC where it would be cut off.
|
||||
AcpiDevices::DeviceInfo bat{};
|
||||
if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_BATTERY, bat)) {
|
||||
CopyPath(g_batteryPath, bat.Path, sizeof(g_batteryPath));
|
||||
g_batteryNode = bat.NodeIndex;
|
||||
g_haveBattery = true;
|
||||
}
|
||||
|
||||
AcpiDevices::DeviceInfo ac{};
|
||||
if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_AC_ADAPTER, ac)) {
|
||||
CopyPath(g_adapterPath, ac.Path, sizeof(g_adapterPath));
|
||||
g_haveAdapter = true;
|
||||
}
|
||||
|
||||
if (!g_haveBattery) {
|
||||
// Distinguish "this machine has no battery" from "the namespace
|
||||
// walk found nothing at all", which would point at the walk
|
||||
// rather than the hardware.
|
||||
auto& ns = interp.GetNamespace();
|
||||
int deviceCount = 0;
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||
[&](int32_t, const AML::NamespaceNode*) { deviceCount++; });
|
||||
|
||||
KernelLogStream(INFO, "Battery") << "No ACPI battery device (PNP0C0A) among "
|
||||
<< base::dec << (uint64_t)deviceCount << " ACPI devices";
|
||||
return;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "Battery") << "ACPI battery at " << g_batteryPath
|
||||
<< (g_haveAdapter ? " (AC adapter present)" : " (no AC adapter device)");
|
||||
|
||||
Probe();
|
||||
}
|
||||
|
||||
// Evaluate the battery methods once, here on the boot path, announcing
|
||||
// each before it runs. These are the first firmware methods this kernel
|
||||
// executes that reach the embedded controller, and if one of them takes
|
||||
// the machine down the last line printed says which -- far better than
|
||||
// discovering it when the desktop makes its first query.
|
||||
static void Probe() {
|
||||
KernelLogStream(INFO, "Battery") << "Probing battery methods";
|
||||
|
||||
g_tracing = true;
|
||||
Status s{};
|
||||
bool ok = Refresh(s);
|
||||
g_tracing = false;
|
||||
|
||||
if (!ok) {
|
||||
KernelLogStream(WARNING, "Battery") << "Probe failed; battery status unavailable";
|
||||
g_haveBattery = false;
|
||||
return;
|
||||
}
|
||||
|
||||
g_cached = s;
|
||||
g_cacheStamp = 0; // let the first real query refresh
|
||||
g_cacheValid = true;
|
||||
|
||||
if (!s.Present) {
|
||||
KernelLogStream(INFO, "Battery") << "Battery slot empty"
|
||||
<< (s.AcOnline ? " (on AC)" : "");
|
||||
return;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "Battery") << "Battery at " << base::dec
|
||||
<< (uint64_t)s.Percent << "% ("
|
||||
<< (s.Charging ? "charging" : (s.Discharging ? "discharging" : "idle"))
|
||||
<< ", " << (uint64_t)s.RemainingCapacity << " of "
|
||||
<< (uint64_t)s.FullCapacity << (s.PowerUnitMilliAmps ? " mAh)" : " mWh)");
|
||||
}
|
||||
|
||||
bool IsPresent() {
|
||||
return g_haveBattery;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Method evaluation
|
||||
|
||||
// ============================================================================
|
||||
static const char* TypeName(AML::ObjectType t) {
|
||||
switch (t) {
|
||||
case AML::ObjectType::None: return "none";
|
||||
case AML::ObjectType::Integer: return "integer";
|
||||
case AML::ObjectType::String: return "string";
|
||||
case AML::ObjectType::Buffer: return "buffer";
|
||||
case AML::ObjectType::Package: return "package";
|
||||
case AML::ObjectType::Device: return "device";
|
||||
case AML::ObjectType::Method: return "method";
|
||||
case AML::ObjectType::OperationRegion: return "opregion";
|
||||
case AML::ObjectType::Field: return "field";
|
||||
default: return "other";
|
||||
}
|
||||
}
|
||||
|
||||
static bool EvalPackage(const char* devicePath, const char* method, AML::Object& out) {
|
||||
char path[160];
|
||||
AppendMethod(devicePath, method, path, sizeof(path));
|
||||
|
||||
auto& interp = AML::GetInterpreter();
|
||||
|
||||
// Separate "the method is not there" from "it ran and gave us
|
||||
// something that is not a package": those need opposite fixes, and
|
||||
// the failure looks identical from the caller.
|
||||
int32_t node = interp.GetNamespace().FindNode(path);
|
||||
if (node < 0) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " " << path << ": absent";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trace the opcodes of the one method whose behaviour we cannot
|
||||
// otherwise explain: it runs to completion, trips no limit, and
|
||||
// still yields no value.
|
||||
bool traceThis = g_tracing && method[1] == 'B';
|
||||
if (traceThis) interp.SetTrace(120);
|
||||
|
||||
bool ok = interp.EvaluateObject(path, out);
|
||||
|
||||
if (traceThis) interp.SetTrace(0);
|
||||
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " " << path << ": "
|
||||
<< (ok ? "ok" : "FAILED") << ", returned " << TypeName(out.Type)
|
||||
<< " with " << base::dec
|
||||
<< (uint64_t)interp.GetPackageCount(out) << " element(s)";
|
||||
}
|
||||
|
||||
return ok && out.Type == AML::ObjectType::Package;
|
||||
}
|
||||
|
||||
static uint32_t ElementInteger(const AML::Object& pkg, int index) {
|
||||
AML::Object elem{};
|
||||
if (!AML::GetInterpreter().GetPackageElement(pkg, index, elem)) return Unknown32;
|
||||
if (elem.Type != AML::ObjectType::Integer) return Unknown32;
|
||||
return (uint32_t)elem.Integer;
|
||||
}
|
||||
|
||||
// _BIX supersedes _BIF and prepends a Revision field, shifting every
|
||||
// shared field by one. Both are read into the same three numbers.
|
||||
struct BatteryInfo {
|
||||
bool Valid;
|
||||
bool PowerUnitMilliAmps;
|
||||
uint32_t DesignCapacity;
|
||||
uint32_t FullCapacity;
|
||||
};
|
||||
|
||||
static bool ReadInfo(BatteryInfo& info) {
|
||||
info = BatteryInfo{};
|
||||
|
||||
AML::Object pkg{};
|
||||
int base = 0;
|
||||
|
||||
if (EvalPackage(g_batteryPath, "_BIX", pkg)) {
|
||||
// _BIX revision 0 starts with Revision; later revisions keep it
|
||||
// in the same place, so the offset is fixed at one.
|
||||
base = 1;
|
||||
} else if (EvalPackage(g_batteryPath, "_BIF", pkg)) {
|
||||
base = 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t unit = ElementInteger(pkg, base + 0);
|
||||
uint32_t design = ElementInteger(pkg, base + 1);
|
||||
uint32_t full = ElementInteger(pkg, base + 2);
|
||||
|
||||
info.PowerUnitMilliAmps = (unit != Unknown32) && (unit & 1);
|
||||
info.DesignCapacity = (design == Unknown32) ? 0 : design;
|
||||
info.FullCapacity = (full == Unknown32) ? 0 : full;
|
||||
info.Valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool AcOnline() {
|
||||
if (!g_haveAdapter) return false;
|
||||
|
||||
char path[160];
|
||||
AppendMethod(g_adapterPath, "_PSR", path, sizeof(path));
|
||||
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " evaluating " << path;
|
||||
|
||||
AML::Object result{};
|
||||
if (!AML::GetInterpreter().EvaluateObject(path, result)) return false;
|
||||
return result.Type == AML::ObjectType::Integer && result.Integer != 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// GetStatus
|
||||
|
||||
// ============================================================================
|
||||
static bool Refresh(Status& out) {
|
||||
out = Status{};
|
||||
out.MinutesRemaining = -1;
|
||||
out.HasAcAdapter = g_haveAdapter;
|
||||
|
||||
// _STA bit 4 says whether a battery is actually in the slot. A
|
||||
// laptop with the pack removed still has the device.
|
||||
uint32_t sta = (g_batteryNode >= 0)
|
||||
? AcpiDevices::EvaluateSta(g_batteryNode)
|
||||
: AcpiDevices::STA_DEFAULT;
|
||||
|
||||
// Bit 4 is the one that decides everything below, and firmware
|
||||
// clears it for reasons that have nothing to do with the pack
|
||||
// (an EC that has not been told its region handler is live, most
|
||||
// often), so log the raw value rather than just the verdict.
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " _STA = " << base::hex
|
||||
<< (uint64_t)sta << (((sta & AcpiDevices::STA_BATTERY) != 0)
|
||||
? " (battery present)" : " (battery bit clear)");
|
||||
}
|
||||
|
||||
out.AcOnline = AcOnline();
|
||||
|
||||
// _STA bit 4 is the spec's answer for "is a pack installed", but
|
||||
// it is only as good as the firmware path behind it. Read the
|
||||
// battery methods either way and let the capacity data have the
|
||||
// final say -- see the staSaysAbsent handling below.
|
||||
bool staSaysAbsent = (sta & AcpiDevices::STA_BATTERY) == 0;
|
||||
|
||||
BatteryInfo info{};
|
||||
if (!ReadInfo(info)) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " no _BIX/_BIF package";
|
||||
return true;
|
||||
}
|
||||
|
||||
AML::Object bst{};
|
||||
if (!EvalPackage(g_batteryPath, "_BST", bst)) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " no _BST package";
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t state = ElementInteger(bst, 0);
|
||||
uint32_t rate = ElementInteger(bst, 1);
|
||||
uint32_t remaining = ElementInteger(bst, 2);
|
||||
uint32_t voltage = ElementInteger(bst, 3);
|
||||
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " _BIF/_BIX: design="
|
||||
<< base::dec << (uint64_t)info.DesignCapacity
|
||||
<< " lastFull=" << (uint64_t)info.FullCapacity
|
||||
<< (info.PowerUnitMilliAmps ? " (mAh)" : " (mWh)");
|
||||
KernelLogStream(INFO, "Battery") << " _BST: state=" << base::hex
|
||||
<< (uint64_t)state << " rate=" << base::dec << (uint64_t)rate
|
||||
<< " remaining=" << (uint64_t)remaining
|
||||
<< " voltage=" << (uint64_t)voltage << "mV";
|
||||
}
|
||||
|
||||
// Trust real capacity data over a status bit that says otherwise:
|
||||
// firmware that reports a pack's charge is describing a pack that
|
||||
// is there.
|
||||
if (staSaysAbsent) {
|
||||
bool looksReal = info.FullCapacity > 0 && info.FullCapacity != Unknown32
|
||||
&& remaining != Unknown32;
|
||||
if (!looksReal) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery")
|
||||
<< " _STA says absent and the methods agree";
|
||||
return true;
|
||||
}
|
||||
if (g_tracing)
|
||||
KernelLogStream(WARNING, "Battery")
|
||||
<< " _STA says absent but _BST reports charge; trusting _BST";
|
||||
}
|
||||
|
||||
if (state == Unknown32) state = 0;
|
||||
|
||||
out.Present = true;
|
||||
out.Charging = (state & StateCharging) != 0;
|
||||
out.Discharging = (state & StateDischarging) != 0;
|
||||
out.Critical = (state & StateCritical) != 0;
|
||||
|
||||
out.PowerUnitMilliAmps = info.PowerUnitMilliAmps;
|
||||
out.DesignCapacity = info.DesignCapacity;
|
||||
out.FullCapacity = info.FullCapacity;
|
||||
out.RemainingCapacity = (remaining == Unknown32) ? 0 : remaining;
|
||||
out.PresentRate = (rate == Unknown32) ? 0 : rate;
|
||||
out.VoltageMv = (voltage == Unknown32) ? 0 : voltage;
|
||||
|
||||
// Some firmware leaves last-full at zero or below the current
|
||||
// reading after a swap; fall back to the design capacity, and
|
||||
// clamp so the panel can never show 143%.
|
||||
uint32_t full = out.FullCapacity;
|
||||
if (full == 0) full = out.DesignCapacity;
|
||||
|
||||
if (full > 0 && out.RemainingCapacity > 0) {
|
||||
uint64_t pct = ((uint64_t)out.RemainingCapacity * 100) / full;
|
||||
out.Percent = (pct > 100) ? 100 : (uint8_t)pct;
|
||||
}
|
||||
|
||||
if (out.PresentRate > 0 && out.Discharging) {
|
||||
out.MinutesRemaining =
|
||||
(int32_t)(((uint64_t)out.RemainingCapacity * 60) / out.PresentRate);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetStatus(Status& out) {
|
||||
if (!g_haveBattery) return false;
|
||||
|
||||
g_lock.Acquire();
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_cacheValid && now - g_cacheStamp < CacheMs) {
|
||||
out = g_cached;
|
||||
g_lock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
Status fresh{};
|
||||
bool ok = Refresh(fresh);
|
||||
if (ok) {
|
||||
g_cached = fresh;
|
||||
g_cacheStamp = now;
|
||||
g_cacheValid = true;
|
||||
}
|
||||
|
||||
out = ok ? fresh : g_cached;
|
||||
g_lock.Release();
|
||||
return ok || g_cacheValid;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Battery.hpp
|
||||
* ACPI Control Method Battery (PNP0C0A) and AC adapter (ACPI0003)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
namespace Battery {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Battery state
|
||||
|
||||
// ============================================================================
|
||||
// _BST battery state bits.
|
||||
static constexpr uint32_t StateDischarging = (1 << 0);
|
||||
static constexpr uint32_t StateCharging = (1 << 1);
|
||||
static constexpr uint32_t StateCritical = (1 << 2);
|
||||
|
||||
struct Status {
|
||||
bool Present; // a battery slot exists and reports a battery in it
|
||||
bool Charging;
|
||||
bool Discharging;
|
||||
bool Critical;
|
||||
bool AcOnline; // AC adapter connected (false when no adapter device)
|
||||
bool HasAcAdapter; // an ACPI0003 device exists
|
||||
|
||||
uint8_t Percent; // 0..100, clamped; 0 when capacity is unknown
|
||||
uint32_t RemainingCapacity; // mWh or mAh, per PowerUnitMilliAmps
|
||||
uint32_t FullCapacity; // last full charge capacity, same unit
|
||||
uint32_t DesignCapacity;
|
||||
uint32_t PresentRate; // mW or mA, 0 when unknown
|
||||
uint32_t VoltageMv; // present voltage in mV, 0 when unknown
|
||||
bool PowerUnitMilliAmps; // capacities are mAh/mA rather than mWh/mW
|
||||
|
||||
// Minutes of runtime left at the present rate, or -1 when the rate
|
||||
// is unknown/zero (which includes every charging battery).
|
||||
int32_t MinutesRemaining;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// API
|
||||
|
||||
// ============================================================================
|
||||
// Discover battery and AC adapter devices in the ACPI namespace. Safe to
|
||||
// call more than once; only the first call does work.
|
||||
void Initialize();
|
||||
|
||||
// True when at least one battery device was found, whether or not a
|
||||
// battery is currently installed in it.
|
||||
bool IsPresent();
|
||||
|
||||
// Read the current status. Results are cached briefly so that polling
|
||||
// from a panel indicator does not hammer the embedded controller.
|
||||
// Returns false when the machine has no battery device at all.
|
||||
bool GetStatus(Status& out);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* EmbeddedController.cpp
|
||||
* ACPI Embedded Controller transport (polled, no burst mode)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "EmbeddedController.hpp"
|
||||
#include "ACPI.hpp"
|
||||
#include "AcpiDevices.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace EmbeddedController {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// EC register protocol
|
||||
|
||||
// ============================================================================
|
||||
// EC_SC (status, read from the command port)
|
||||
static constexpr uint8_t SC_OBF = (1 << 0); // output buffer full: data is readable
|
||||
static constexpr uint8_t SC_IBF = (1 << 1); // input buffer full: EC has not consumed our byte
|
||||
|
||||
// EC_SC commands (written to the command port)
|
||||
static constexpr uint8_t CMD_READ = 0x80;
|
||||
static constexpr uint8_t CMD_WRITE = 0x81;
|
||||
|
||||
// The EC is a slow microcontroller; a byte transaction is typically a
|
||||
// few hundred microseconds but firmware is allowed to take much longer.
|
||||
// One "tick" is a port-wait (~1us), so this caps a wait near 500ms.
|
||||
static constexpr uint32_t PollTicks = 500000;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
static uint16_t g_dataPort = 0;
|
||||
static uint16_t g_cmdPort = 0;
|
||||
static bool g_available = false;
|
||||
static bool g_initialized = false;
|
||||
// A Mutex, not a Spinlock: EC transactions run only from process and
|
||||
// syscall context, and can spin for milliseconds waiting on the
|
||||
// controller. A Spinlock would hold interrupts off for all of it.
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// ECDT
|
||||
|
||||
// ============================================================================
|
||||
// The ECDT gives the EC's ports without needing the AML namespace. It is
|
||||
// optional, and some firmware ships one with zeroed ports, so treat a
|
||||
// zero port as "not described here" and fall through to the namespace.
|
||||
struct EcdtTable {
|
||||
ACPI::CommonSDTHeader Header;
|
||||
uint8_t ControlGas[12];
|
||||
uint8_t DataGas[12];
|
||||
uint32_t Uid;
|
||||
uint8_t GpeBit;
|
||||
// followed by a null-terminated namespace path
|
||||
} __attribute__((packed));
|
||||
|
||||
static bool SignatureMatch(const char* sig, const char* target) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (sig[i] != target[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decode a Generic Address Structure that must describe a 16-bit I/O port.
|
||||
static uint16_t GasIoPort(const uint8_t gas[12]) {
|
||||
if (gas[0] != 1) return 0; // AddressSpaceId 1 == SystemIO
|
||||
|
||||
uint64_t address = 0;
|
||||
for (int i = 0; i < 8; i++)
|
||||
address |= (uint64_t)gas[4 + i] << (i * 8);
|
||||
return address <= 0xFFFF ? (uint16_t)address : 0;
|
||||
}
|
||||
|
||||
static bool FromEcdt(ACPI::CommonSDTHeader* xsdt) {
|
||||
if (xsdt == nullptr) return false;
|
||||
|
||||
uint32_t entryCount = (xsdt->Length - sizeof(ACPI::CommonSDTHeader)) / 8;
|
||||
uint64_t* entries = (uint64_t*)((uint64_t)xsdt + sizeof(ACPI::CommonSDTHeader));
|
||||
|
||||
for (uint32_t i = 0; i < entryCount; i++) {
|
||||
auto* header = (ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
|
||||
if (!SignatureMatch(header->Signature, "ECDT")) continue;
|
||||
if (header->Length < sizeof(EcdtTable)) continue;
|
||||
|
||||
auto* ecdt = (EcdtTable*)header;
|
||||
uint16_t cmd = GasIoPort(ecdt->ControlGas);
|
||||
uint16_t data = GasIoPort(ecdt->DataGas);
|
||||
if (cmd == 0 || data == 0) continue;
|
||||
|
||||
g_cmdPort = cmd;
|
||||
g_dataPort = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Namespace fallback
|
||||
|
||||
// ============================================================================
|
||||
// _CRS on an EC device lists two I/O resources in a fixed order: data
|
||||
// register first, then command/status. Anything else is not an EC we
|
||||
// know how to drive.
|
||||
static bool FromNamespace() {
|
||||
if (!AML::GetInterpreter().IsInitialized()) return false;
|
||||
|
||||
AcpiDevices::DeviceInfo ec{};
|
||||
if (!AcpiDevices::FindDeviceByHid(AcpiDevices::HID_EC, ec)) return false;
|
||||
|
||||
AML::ResourceList crs{};
|
||||
if (!AcpiDevices::EvaluateCrs(ec.NodeIndex, crs)) return false;
|
||||
|
||||
uint16_t ports[2] = {0, 0};
|
||||
int found = 0;
|
||||
for (int i = 0; i < crs.Count && found < 2; i++) {
|
||||
if (crs.Resources[i].Type == AML::ResourceType::IoPort)
|
||||
ports[found++] = crs.Resources[i].IoPort.Base;
|
||||
else if (crs.Resources[i].Type == AML::ResourceType::FixedIoPort)
|
||||
ports[found++] = crs.Resources[i].FixedIoPort.Base;
|
||||
}
|
||||
|
||||
if (found < 2 || ports[0] == 0 || ports[1] == 0) return false;
|
||||
|
||||
g_dataPort = ports[0];
|
||||
g_cmdPort = ports[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Initialize
|
||||
|
||||
// ============================================================================
|
||||
void Initialize(void* xsdt) {
|
||||
if (g_initialized) return;
|
||||
g_initialized = true;
|
||||
|
||||
if (!FromEcdt((ACPI::CommonSDTHeader*)xsdt) && !FromNamespace()) {
|
||||
KernelLogStream(INFO, "EC") << "No ACPI Embedded Controller present";
|
||||
return;
|
||||
}
|
||||
|
||||
g_available = true;
|
||||
KernelLogStream(OK, "EC") << "Embedded Controller at cmd=" << base::hex
|
||||
<< (uint64_t)g_cmdPort << " data=" << (uint64_t)g_dataPort;
|
||||
|
||||
RunRegMethods();
|
||||
|
||||
KernelLogStream(INFO, "EC") << "EC space dump:";
|
||||
DumpSpace(0x00, 32);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// _REG
|
||||
|
||||
// ============================================================================
|
||||
void RunRegMethods() {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return;
|
||||
|
||||
auto& ns = interp.GetNamespace();
|
||||
|
||||
// Collect first, evaluate second. Executing AML can create namespace
|
||||
// nodes, which reallocates a parent's child array -- walking the
|
||||
// tree while methods run would be iterating a freed array.
|
||||
static constexpr int MaxRegMethods = 64;
|
||||
int32_t nodes[MaxRegMethods];
|
||||
int count = 0;
|
||||
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Method,
|
||||
[&](int32_t idx, const AML::NamespaceNode* node) {
|
||||
if (count >= MaxRegMethods) return;
|
||||
if (node->Name[0] == '_' && node->Name[1] == 'R' &&
|
||||
node->Name[2] == 'E' && node->Name[3] == 'G')
|
||||
nodes[count++] = idx;
|
||||
});
|
||||
|
||||
// Arg0 = region space (3, EmbeddedControl), Arg1 = 1 (handler is
|
||||
// connected). A _REG that handles some other space checks Arg0 and
|
||||
// ignores the call, so this is safe to issue namespace-wide.
|
||||
AML::Object args[2]{};
|
||||
args[0].Type = AML::ObjectType::Integer;
|
||||
args[0].Integer = (uint64_t)AML::RegionSpace::EmbeddedControl;
|
||||
args[1].Type = AML::ObjectType::Integer;
|
||||
args[1].Integer = 1;
|
||||
|
||||
int ran = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
char path[256];
|
||||
ns.GetNodePath(nodes[i], path, sizeof(path));
|
||||
|
||||
AML::Object result{};
|
||||
bool ok = interp.EvaluateMethod(path, args, 2, result);
|
||||
if (ok) ran++;
|
||||
|
||||
KernelLogStream(INFO, "EC") << " _REG " << path
|
||||
<< (ok ? " ok" : " FAILED");
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "EC") << "Ran " << base::dec << (uint64_t)ran
|
||||
<< " of " << (uint64_t)count << " _REG method(s)";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Diagnostics
|
||||
|
||||
// ============================================================================
|
||||
void DumpSpace(uint8_t base_, int bytes) {
|
||||
if (!g_available) return;
|
||||
|
||||
// Whether the transport works at all is not otherwise observable:
|
||||
// a failed read and a genuinely zero register look identical by the
|
||||
// time firmware AML has turned them into a status value. All-00 or
|
||||
// all-FF here means the handshake is wrong, not that the battery is
|
||||
// missing.
|
||||
for (int row = 0; row < bytes; row += 16) {
|
||||
char line[64];
|
||||
int n = 0;
|
||||
bool anyFailed = false;
|
||||
|
||||
for (int i = 0; i < 16 && (row + i) < bytes; i++) {
|
||||
uint8_t v = 0;
|
||||
if (!Read((uint8_t)(base_ + row + i), v)) anyFailed = true;
|
||||
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
line[n++] = hex[(v >> 4) & 0xF];
|
||||
line[n++] = hex[v & 0xF];
|
||||
line[n++] = ' ';
|
||||
}
|
||||
line[n] = '\0';
|
||||
|
||||
KernelLogStream(INFO, "EC") << " " << base::hex
|
||||
<< (uint64_t)(base_ + row) << ": " << line
|
||||
<< (anyFailed ? " (read timeout)" : "");
|
||||
}
|
||||
}
|
||||
|
||||
bool IsAvailable() {
|
||||
return g_available;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Transport
|
||||
|
||||
// ============================================================================
|
||||
// Spin until the named status bit reaches the wanted state.
|
||||
static bool WaitStatus(uint8_t mask, bool wantSet) {
|
||||
for (uint32_t i = 0; i < PollTicks; i++) {
|
||||
uint8_t sc = Io::In8(g_cmdPort);
|
||||
if (((sc & mask) != 0) == wantSet) return true;
|
||||
Io::IoPortWait();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Drain a byte the EC left in the output buffer from an earlier aborted
|
||||
// transaction. Without this the next read returns the stale value.
|
||||
static void DrainOutput() {
|
||||
if (Io::In8(g_cmdPort) & SC_OBF)
|
||||
(void)Io::In8(g_dataPort);
|
||||
}
|
||||
|
||||
bool Read(uint8_t address, uint8_t& value) {
|
||||
if (!g_available) return false;
|
||||
|
||||
g_lock.Acquire();
|
||||
DrainOutput();
|
||||
|
||||
bool ok = false;
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(CMD_READ, g_cmdPort);
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(address, g_dataPort);
|
||||
if (WaitStatus(SC_OBF, true)) {
|
||||
value = Io::In8(g_dataPort);
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_lock.Release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Write(uint8_t address, uint8_t value) {
|
||||
if (!g_available) return false;
|
||||
|
||||
g_lock.Acquire();
|
||||
DrainOutput();
|
||||
|
||||
bool ok = false;
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(CMD_WRITE, g_cmdPort);
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(address, g_dataPort);
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(value, g_dataPort);
|
||||
ok = WaitStatus(SC_IBF, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_lock.Release();
|
||||
return ok;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* EmbeddedController.hpp
|
||||
* ACPI Embedded Controller (PNP0C09) transport
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
namespace EmbeddedController {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Bring-up
|
||||
|
||||
// ============================================================================
|
||||
// Locate the EC in the ACPI namespace and latch its command/data ports.
|
||||
// Prefers the ECDT table when present (it is valid before the namespace
|
||||
// is loaded); otherwise walks the namespace for PNP0C09 and reads _CRS.
|
||||
// Safe to call more than once; only the first call does work.
|
||||
void Initialize(void* xsdt);
|
||||
|
||||
// True once a usable command/data port pair has been found.
|
||||
bool IsAvailable();
|
||||
|
||||
// Run the namespace's _REG(EmbeddedControl, 1) methods, telling firmware
|
||||
// that EC region access is live. ACPI requires this once a handler for
|
||||
// an address space is installed, and firmware leans on it hard: EC
|
||||
// methods are typically guarded by a flag that _REG sets, and until it
|
||||
// is set they return safe do-nothing answers (a battery reports itself
|
||||
// absent, a thermal zone reports no temperature). Called by Initialize.
|
||||
void RunRegMethods();
|
||||
|
||||
// Log `bytes` bytes of EC address space starting at `base_`. Purely a
|
||||
// diagnostic: it is the only way to tell a broken handshake from a
|
||||
// register that genuinely reads zero.
|
||||
void DumpSpace(uint8_t base_, int bytes);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Transport
|
||||
|
||||
// ============================================================================
|
||||
// Read/write one byte of the EC's 256-byte address space. Both return
|
||||
// false on timeout or when no EC is present, leaving *value untouched.
|
||||
bool Read(uint8_t address, uint8_t& value);
|
||||
bool Write(uint8_t address, uint8_t value);
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user