LDLS: every MDIO read was RFLAGS; ACPI: an Embedded Controller
On the TUF A15 Linux's r8169 found its PHY and read its ID as 0x02460246. mdio-bus keeps the register it has just read in r11 across preempt_enable(), which on x86 is `decl %gs:__preempt_count; je` to a call that preserves every register. LDLS gave __preempt_count zeroes, so the count reached zero and the call was made -- to a logging stub, whose syscall left RFLAGS (0x246) in r11. __preempt_count now starts at PREEMPT_ENABLED (x86 keeps NEED_RESCHED inverted), and preempt_schedule and its notrace twin are a bare ret. The same laptop reported its battery full and on AC, unplugged. The EC owns both, and with no EmbeddedControl handler _REG never told the firmware the EC was usable, so _BST and _PSR answered their fallbacks. The EC is found by PNP0C09 or the ECDT and served by the ACPI 12.3 byte protocol, installed before _INI. SCI queries are not handled yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
3676036762
commit
d437e42b49
3 changed files with 179 additions and 1 deletions
|
|
@ -264,7 +264,7 @@ resolve(void *ctx, const char *name, unsigned long long *out)
|
||||||
// bargain as the logging stubs, and the same caveat -- see the memcpy note in
|
// bargain as the logging stubs, and the same caveat -- see the memcpy note in
|
||||||
// docs/architecture/ldls.md.
|
// docs/architecture/ldls.md.
|
||||||
|
|
||||||
#define PERCPU_SLOTS 8
|
#define PERCPU_SLOTS 16
|
||||||
#define PERCPU_SIZE (16 * 1024)
|
#define PERCPU_SIZE (16 * 1024)
|
||||||
|
|
||||||
static char percpu_mem[PERCPU_SLOTS][PERCPU_SIZE];
|
static char percpu_mem[PERCPU_SLOTS][PERCPU_SIZE];
|
||||||
|
|
@ -294,6 +294,14 @@ percpu_for(void *ctx, const char *name)
|
||||||
*(unsigned long long *)percpu_mem[i] =
|
*(unsigned long long *)percpu_mem[i] =
|
||||||
(unsigned long long)(void *)(percpu_mem[i] + 64);
|
(unsigned long long)(void *)(percpu_mem[i] + 64);
|
||||||
puts_(" per-CPU "); puts_(name); puts_(" -- a task of zeroes\n");
|
puts_(" per-CPU "); puts_(name); puts_(" -- a task of zeroes\n");
|
||||||
|
} else if (streq(name, "__preempt_count")) {
|
||||||
|
// x86 keeps PREEMPT_NEED_RESCHED inverted in this word, so that
|
||||||
|
// `decl %gs:__preempt_count; je resched` is one instruction. Zero
|
||||||
|
// means "count zero *and* a reschedule wanted", and every
|
||||||
|
// preempt_enable() then called preempt_schedule -- see the note on
|
||||||
|
// __SCT__preempt_schedule in ldls/kpi/base.c for what that cost.
|
||||||
|
*(unsigned int *)percpu_mem[i] = 0x80000000u;
|
||||||
|
puts_(" per-CPU "); puts_(name); puts_(" -- PREEMPT_ENABLED\n");
|
||||||
} else {
|
} else {
|
||||||
puts_(" per-CPU "); puts_(name); puts_(" -- 16K of zeroes\n");
|
puts_(" per-CPU "); puts_(name); puts_(" -- 16K of zeroes\n");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@
|
||||||
#include <uacpi/event.h>
|
#include <uacpi/event.h>
|
||||||
#include <uacpi/sleep.h>
|
#include <uacpi/sleep.h>
|
||||||
#include <uacpi/registers.h>
|
#include <uacpi/registers.h>
|
||||||
|
#include <uacpi/opregion.h>
|
||||||
|
#include <uacpi/tables.h>
|
||||||
|
#include <uacpi/acpi.h>
|
||||||
|
|
||||||
static int ready;
|
static int ready;
|
||||||
|
|
||||||
|
|
@ -42,6 +45,7 @@ static int s5_typ_a = -1, s5_typ_b = -1;
|
||||||
|
|
||||||
static uacpi_interrupt_ret power_button(uacpi_handle ctx);
|
static uacpi_interrupt_ret power_button(uacpi_handle ctx);
|
||||||
static void cstate_find(void);
|
static void cstate_find(void);
|
||||||
|
static void ec_init(void);
|
||||||
|
|
||||||
void
|
void
|
||||||
acpi_aml_init(void)
|
acpi_aml_init(void)
|
||||||
|
|
@ -62,6 +66,10 @@ acpi_aml_init(void)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Embedded Controller, before _INI: methods that run there may already
|
||||||
|
// read it, and on laptops it is where the battery and the adapter live.
|
||||||
|
ec_init();
|
||||||
|
|
||||||
// \_PIC(1): tell the firmware the I/O APIC is in use, so that _PRT answers
|
// \_PIC(1): tell the firmware the I/O APIC is in use, so that _PRT answers
|
||||||
// with I/O APIC inputs rather than PIC lines. Before _INI runs, which on
|
// with I/O APIC inputs rather than PIC lines. Before _INI runs, which on
|
||||||
// some boards reads the answer.
|
// some boards reads the answer.
|
||||||
|
|
@ -102,6 +110,151 @@ acpi_aml_init(void)
|
||||||
cstate_find();
|
cstate_find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the Embedded Controller ----------------------------------------------------
|
||||||
|
//
|
||||||
|
// A laptop's EC is a microcontroller behind two I/O ports that owns the
|
||||||
|
// battery gauge, the charger, the lid and the fans; AML reaches it through an
|
||||||
|
// OperationRegion in the EmbeddedControl space, and only the OS can serve
|
||||||
|
// that space (ACPI 12). Without a handler every EC field read fails -- and
|
||||||
|
// firmware guards against that: _REG(EmbeddedControl, 1) sets a flag (ASUS
|
||||||
|
// calls it ECAV), and until it is set _BST answers "full" and _PSR "on AC".
|
||||||
|
// That is what the TUF A15 reported, unplugged.
|
||||||
|
//
|
||||||
|
// The protocol (ACPI 12.3): wait for the input buffer to empty, write a
|
||||||
|
// command to the command port, then the address -- and for a read wait for
|
||||||
|
// the output buffer and take the byte from the data port. Ports come from
|
||||||
|
// the EC device's _CRS, data first; or from the ECDT when the DSDT has no
|
||||||
|
// PNP0C09 early enough. SCI events (_Qxx) are not taken yet: nothing here
|
||||||
|
// needs to be told the adapter changed, because everything reads on demand.
|
||||||
|
|
||||||
|
#define EC_OBF 0x01
|
||||||
|
#define EC_IBF 0x02
|
||||||
|
#define EC_RD 0x80
|
||||||
|
#define EC_WR 0x81
|
||||||
|
|
||||||
|
static uint16 ec_data, ec_cmd;
|
||||||
|
static uacpi_namespace_node *ec_node;
|
||||||
|
static struct spinlock eclock;
|
||||||
|
|
||||||
|
static int
|
||||||
|
ec_wait(int want_obf)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 100000; i++) { // ~100 ms; an EC answers in tens of µs
|
||||||
|
uint8 st = inb(ec_cmd);
|
||||||
|
if (want_obf ? (st & EC_OBF) : !(st & EC_IBF))
|
||||||
|
return 0;
|
||||||
|
microdelay(1);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
ec_byte(int write, uint8 addr, uint8 *v)
|
||||||
|
{
|
||||||
|
int r = -1;
|
||||||
|
acquire(&eclock);
|
||||||
|
if (ec_wait(0) < 0) goto out;
|
||||||
|
outb(ec_cmd, write ? EC_WR : EC_RD);
|
||||||
|
if (ec_wait(0) < 0) goto out;
|
||||||
|
outb(ec_data, addr);
|
||||||
|
if (write) {
|
||||||
|
if (ec_wait(0) < 0) goto out;
|
||||||
|
outb(ec_data, *v);
|
||||||
|
if (ec_wait(0) < 0) goto out;
|
||||||
|
} else {
|
||||||
|
if (ec_wait(1) < 0) goto out;
|
||||||
|
*v = inb(ec_data);
|
||||||
|
}
|
||||||
|
r = 0;
|
||||||
|
out:
|
||||||
|
release(&eclock);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uacpi_status
|
||||||
|
ec_handler(uacpi_region_op op, uacpi_handle opdata)
|
||||||
|
{
|
||||||
|
if (op == UACPI_REGION_OP_ATTACH || op == UACPI_REGION_OP_DETACH)
|
||||||
|
return UACPI_STATUS_OK;
|
||||||
|
if (op != UACPI_REGION_OP_READ && op != UACPI_REGION_OP_WRITE)
|
||||||
|
return UACPI_STATUS_INVALID_ARGUMENT;
|
||||||
|
|
||||||
|
uacpi_region_rw_data *d = opdata;
|
||||||
|
int write = op == UACPI_REGION_OP_WRITE;
|
||||||
|
if (!write)
|
||||||
|
d->value = 0;
|
||||||
|
// Wider fields are consecutive bytes, low first.
|
||||||
|
for (int i = 0; i < d->byte_width; i++) {
|
||||||
|
uint8 b = write ? (uint8)(d->value >> (8 * i)) : 0;
|
||||||
|
if (d->address + i > 0xFF || ec_byte(write, (uint8)(d->address + i), &b) < 0)
|
||||||
|
return UACPI_STATUS_HARDWARE_TIMEOUT;
|
||||||
|
if (!write)
|
||||||
|
d->value |= (uacpi_u64)b << (8 * i);
|
||||||
|
}
|
||||||
|
return UACPI_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ec_ports { int n; uint16 p[2]; };
|
||||||
|
|
||||||
|
static uacpi_iteration_decision
|
||||||
|
ec_port(void *user, uacpi_resource *r)
|
||||||
|
{
|
||||||
|
struct ec_ports *e = user;
|
||||||
|
if (e->n < 2 && r->type == UACPI_RESOURCE_TYPE_IO)
|
||||||
|
e->p[e->n++] = r->io.minimum;
|
||||||
|
else if (e->n < 2 && r->type == UACPI_RESOURCE_TYPE_FIXED_IO)
|
||||||
|
e->p[e->n++] = r->fixed_io.address;
|
||||||
|
return UACPI_ITERATION_DECISION_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uacpi_iteration_decision
|
||||||
|
found_ec(void *user, uacpi_namespace_node *node, uacpi_u32 depth)
|
||||||
|
{
|
||||||
|
(void)user; (void)depth;
|
||||||
|
struct ec_ports e = { 0, { 0, 0 } };
|
||||||
|
uacpi_for_each_device_resource(node, "_CRS", ec_port, &e);
|
||||||
|
if (e.n == 2) {
|
||||||
|
ec_node = node;
|
||||||
|
ec_data = e.p[0];
|
||||||
|
ec_cmd = e.p[1];
|
||||||
|
return UACPI_ITERATION_DECISION_BREAK;
|
||||||
|
}
|
||||||
|
return UACPI_ITERATION_DECISION_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
ec_init(void)
|
||||||
|
{
|
||||||
|
uacpi_table t;
|
||||||
|
|
||||||
|
initlock(&eclock, "ec");
|
||||||
|
uacpi_find_devices("PNP0C09", found_ec, 0);
|
||||||
|
if (!ec_node &&
|
||||||
|
uacpi_table_find_by_signature("ECDT", &t) == UACPI_STATUS_OK) {
|
||||||
|
struct acpi_ecdt *e = t.ptr;
|
||||||
|
if (e->ec_control.address && e->ec_data.address) {
|
||||||
|
ec_cmd = (uint16)e->ec_control.address;
|
||||||
|
ec_data = (uint16)e->ec_data.address;
|
||||||
|
ec_node = uacpi_namespace_root();
|
||||||
|
}
|
||||||
|
uacpi_table_unref(&t);
|
||||||
|
}
|
||||||
|
if (!ec_node)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Installing runs _REG(3, 1) for the regions under it.
|
||||||
|
uacpi_status st = uacpi_install_address_space_handler(
|
||||||
|
ec_node, UACPI_ADDRESS_SPACE_EMBEDDED_CONTROLLER, ec_handler, 0);
|
||||||
|
if (uacpi_unlikely_error(st)) {
|
||||||
|
kinfo("acpi: EC at 0x%x/0x%x, handler refused: %s\n", ec_data, ec_cmd,
|
||||||
|
uacpi_status_to_string(st));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint8 probe = 0;
|
||||||
|
kinfo("acpi: EC at data 0x%x, command 0x%x%s\n", ec_data, ec_cmd,
|
||||||
|
ec_byte(0, 0, &probe) < 0 ? " -- not answering" : "");
|
||||||
|
}
|
||||||
|
|
||||||
// --- power -------------------------------------------------------------------
|
// --- power -------------------------------------------------------------------
|
||||||
|
|
||||||
// Run in the worker thread, not the SCI handler: syncing the filesystems
|
// Run in the worker thread, not the SCI handler: syncing the filesystems
|
||||||
|
|
|
||||||
|
|
@ -835,6 +835,17 @@ static int kpi_system_state = 3; // SYSTEM_RUNNING
|
||||||
// A list_head: empty means pointing at itself, not zero.
|
// A list_head: empty means pointing at itself, not zero.
|
||||||
static void *kpi_leds_list[2] = { kpi_leds_list, kpi_leds_list };
|
static void *kpi_leds_list[2] = { kpi_leds_list, kpi_leds_list };
|
||||||
|
|
||||||
|
// --- calls that must not touch a register ---------------------------------------------
|
||||||
|
//
|
||||||
|
// preempt_schedule is reached through a thunk that saves every register, so
|
||||||
|
// its callers keep live values in caller-saved ones across the call. A
|
||||||
|
// logging stub makes a syscall, and `syscall` writes RFLAGS into r11: mdio-
|
||||||
|
// bus kept the PHY register it had just read in r11, and every MDIO read on
|
||||||
|
// the RTL8168H came back 0x0246 -- the PHY ID read as 0x02460246 and no
|
||||||
|
// driver matched it. Nothing to schedule here anyway; a bare ret.
|
||||||
|
__asm__(".text\n.globl kpi_ret\nkpi_ret:\n\tret\n");
|
||||||
|
extern void kpi_ret(void);
|
||||||
|
|
||||||
// --- the table ------------------------------------------------------------------------------------
|
// --- the table ------------------------------------------------------------------------------------
|
||||||
|
|
||||||
extern int kpi_queue_delayed_work_on(int cpu, void *q, void *dwork, unsigned long delay);
|
extern int kpi_queue_delayed_work_on(int cpu, void *q, void *dwork, unsigned long delay);
|
||||||
|
|
@ -937,6 +948,12 @@ const struct kpi_sym kpi_base_table[] = {
|
||||||
{ "request_firmware", (void *)kpi_request_firmware },
|
{ "request_firmware", (void *)kpi_request_firmware },
|
||||||
{ "release_firmware", (void *)kpi_nop },
|
{ "release_firmware", (void *)kpi_nop },
|
||||||
|
|
||||||
|
// must preserve every register
|
||||||
|
{ "__SCT__preempt_schedule", (void *)kpi_ret },
|
||||||
|
{ "__SCT__preempt_schedule_notrace", (void *)kpi_ret },
|
||||||
|
{ "preempt_schedule_thunk", (void *)kpi_ret },
|
||||||
|
{ "preempt_schedule_notrace_thunk", (void *)kpi_ret },
|
||||||
|
|
||||||
// must not be zero
|
// must not be zero
|
||||||
{ "__list_add_valid_or_report", (void *)kpi_true },
|
{ "__list_add_valid_or_report", (void *)kpi_true },
|
||||||
{ "__list_del_entry_valid_or_report", (void *)kpi_true },
|
{ "__list_del_entry_valid_or_report", (void *)kpi_true },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue