diff --git a/cmd/ldlsd/main.c b/cmd/ldlsd/main.c index 5105b84..6f6a8e9 100644 --- a/cmd/ldlsd/main.c +++ b/cmd/ldlsd/main.c @@ -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 // docs/architecture/ldls.md. -#define PERCPU_SLOTS 8 +#define PERCPU_SLOTS 16 #define PERCPU_SIZE (16 * 1024) 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)(void *)(percpu_mem[i] + 64); 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 { puts_(" per-CPU "); puts_(name); puts_(" -- 16K of zeroes\n"); } diff --git a/kernel/acpi/aml.c b/kernel/acpi/aml.c index dfc4cc0..7368449 100644 --- a/kernel/acpi/aml.c +++ b/kernel/acpi/aml.c @@ -33,6 +33,9 @@ #include #include #include +#include +#include +#include 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 void cstate_find(void); +static void ec_init(void); void acpi_aml_init(void) @@ -62,6 +66,10 @@ acpi_aml_init(void) 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 // with I/O APIC inputs rather than PIC lines. Before _INI runs, which on // some boards reads the answer. @@ -102,6 +110,151 @@ acpi_aml_init(void) 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 ------------------------------------------------------------------- // Run in the worker thread, not the SCI handler: syncing the filesystems diff --git a/ldls/kpi/base.c b/ldls/kpi/base.c index d69466e..8baadbe 100644 --- a/ldls/kpi/base.c +++ b/ldls/kpi/base.c @@ -835,6 +835,17 @@ static int kpi_system_state = 3; // SYSTEM_RUNNING // A list_head: empty means pointing at itself, not zero. 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 ------------------------------------------------------------------------------------ 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 }, { "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 { "__list_add_valid_or_report", (void *)kpi_true }, { "__list_del_entry_valid_or_report", (void *)kpi_true },