ACPI, DMI, SMP, xHCI, USB, I2C, NVMe, r8169 and ramdisk progress lines are kinfo now: in dmesg and on a verbose boot, off the screen of a working machine. Failures stay cprintf. vt_repaint also paints the strip right of the last column and below the last row, which kept the bottom of the X desktop after Ctrl+Alt+F-key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
620 lines
15 KiB
C
620 lines
15 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// What uACPI asks of the kernel it runs in: memory, mapping, port and PCI
|
|
// configuration I/O, time, locks, the SCI, and somewhere to run deferred
|
|
// work. Every function here is one uacpi_kernel_* entry point from
|
|
// <uacpi/kernel_api.h>; the interpreter calls nothing else.
|
|
//
|
|
// Two things make this harder than it looks.
|
|
//
|
|
// The namespace is loaded from kmain(), before the scheduler, with no
|
|
// process to sleep in -- and AML sleeps, takes mutexes and waits on events
|
|
// while it initializes devices. So every blocking primitive here checks
|
|
// myproc() and spins when there is nothing to put to sleep, the same rule
|
|
// the AHCI and NVMe drivers follow.
|
|
//
|
|
// And the SCI handler runs AML's notifications and GPE methods through
|
|
// uacpi_kernel_schedule_work(), from interrupt context, where AML must not
|
|
// run. That is the "acpi" kernel thread's whole job.
|
|
|
|
#include "core/types.h"
|
|
#include "core/defs.h"
|
|
#include "core/param.h"
|
|
#include "arch/memlayout.h"
|
|
#include "arch/mmu.h"
|
|
#include "arch/x86.h"
|
|
#include "core/spinlock.h"
|
|
#include "core/proc.h"
|
|
#include "core/irq.h"
|
|
#include "core/dma.h"
|
|
#include "core/kmem.h"
|
|
#include "dev/acpi.h"
|
|
|
|
#include <uacpi/kernel_api.h>
|
|
|
|
// --- memory ------------------------------------------------------------------
|
|
//
|
|
// kmalloc stops at 2 KB and kalloc is one page, and uACPI's allocations range
|
|
// from a few bytes to buffers AML sizes however it likes. So each allocation
|
|
// carries a header saying where it came from: the slab caches, a page, or --
|
|
// for the rare one bigger than a page -- the contiguous DMA arena, which is
|
|
// the only multi-page allocator the kernel has.
|
|
|
|
#define HDR 16
|
|
enum { FROM_SLAB = 1, FROM_PAGE, FROM_DMA };
|
|
|
|
void *
|
|
uacpi_kernel_alloc(uacpi_size size)
|
|
{
|
|
uint64 n = size + HDR;
|
|
uint64 *h;
|
|
int kind;
|
|
|
|
if (n <= 2048) {
|
|
h = kmalloc(n);
|
|
kind = FROM_SLAB;
|
|
} else if (n <= PGSIZE) {
|
|
h = (uint64 *)kalloc();
|
|
kind = FROM_PAGE;
|
|
} else {
|
|
h = dma_alloc(n, 16);
|
|
kind = FROM_DMA;
|
|
}
|
|
if (h == 0)
|
|
return 0;
|
|
h[0] = n;
|
|
h[1] = kind;
|
|
return (uchar *)h + HDR;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_free(void *mem)
|
|
{
|
|
uint64 *h;
|
|
|
|
if (mem == 0)
|
|
return;
|
|
h = (uint64 *)((uchar *)mem - HDR);
|
|
switch (h[1]) {
|
|
case FROM_SLAB: kmfree(h); break;
|
|
case FROM_PAGE: kfree((char *)h); break;
|
|
case FROM_DMA: dma_free(h, h[0]); break;
|
|
default: panic("uacpi_kernel_free: not ours");
|
|
}
|
|
}
|
|
|
|
// Physical memory: tables, and AML's SystemMemory regions. The direct map
|
|
// covers the first 4 GB; above that, ioremap().
|
|
void *
|
|
uacpi_kernel_map(uacpi_phys_addr addr, uacpi_size len)
|
|
{
|
|
if (addr + len <= 4ULL * 1024 * 1024 * 1024)
|
|
return P2V(addr);
|
|
void *p = ioremap(addr, len);
|
|
return p ? p : UACPI_MAP_FAILED;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_unmap(void *addr, uacpi_size len)
|
|
{
|
|
// Nothing to undo: the direct map is permanent, and ioremap's 1 GB windows
|
|
// are shared and never torn down.
|
|
(void)addr;
|
|
(void)len;
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_get_rsdp(uacpi_phys_addr *out)
|
|
{
|
|
uint64 pa = acpi_rsdp_phys();
|
|
if (pa == 0)
|
|
return UACPI_STATUS_NOT_FOUND;
|
|
*out = pa;
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
// --- logging -----------------------------------------------------------------
|
|
|
|
void
|
|
uacpi_kernel_log(uacpi_log_level level, const uacpi_char *msg)
|
|
{
|
|
switch (level) {
|
|
case UACPI_LOG_ERROR:
|
|
case UACPI_LOG_WARN:
|
|
kinfo("acpi: %s", msg);
|
|
break;
|
|
case UACPI_LOG_INFO:
|
|
kinfo("acpi: %s", msg);
|
|
break;
|
|
default:
|
|
break; // trace and debug: per-opcode noise
|
|
}
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_handle_firmware_request(uacpi_firmware_request *req)
|
|
{
|
|
if (req->type == UACPI_FIRMWARE_REQUEST_TYPE_FATAL)
|
|
cprintf("acpi: firmware reports a fatal error (type %x code %x arg %p)\n",
|
|
req->fatal.type, req->fatal.code, req->fatal.arg);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
// --- port I/O ----------------------------------------------------------------
|
|
//
|
|
// The handle is the base port itself; there is nothing to map.
|
|
|
|
uacpi_status
|
|
uacpi_kernel_io_map(uacpi_io_addr base, uacpi_size len, uacpi_handle *out)
|
|
{
|
|
(void)len;
|
|
*out = (uacpi_handle)(addr_t)base;
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
void uacpi_kernel_io_unmap(uacpi_handle h) { (void)h; }
|
|
|
|
#define PORT(h, off) ((ushort)((addr_t)(h) + (off)))
|
|
|
|
uacpi_status uacpi_kernel_io_read8(uacpi_handle h, uacpi_size o, uacpi_u8 *v) { *v = inb(PORT(h, o)); return UACPI_STATUS_OK; }
|
|
uacpi_status uacpi_kernel_io_read16(uacpi_handle h, uacpi_size o, uacpi_u16 *v) { *v = inw(PORT(h, o)); return UACPI_STATUS_OK; }
|
|
uacpi_status uacpi_kernel_io_read32(uacpi_handle h, uacpi_size o, uacpi_u32 *v) { *v = inl(PORT(h, o)); return UACPI_STATUS_OK; }
|
|
uacpi_status uacpi_kernel_io_write8(uacpi_handle h, uacpi_size o, uacpi_u8 v) { outb(PORT(h, o), v); return UACPI_STATUS_OK; }
|
|
uacpi_status uacpi_kernel_io_write16(uacpi_handle h, uacpi_size o, uacpi_u16 v) { outw(PORT(h, o), v); return UACPI_STATUS_OK; }
|
|
uacpi_status uacpi_kernel_io_write32(uacpi_handle h, uacpi_size o, uacpi_u32 v) { outl(PORT(h, o), v); return UACPI_STATUS_OK; }
|
|
|
|
// --- PCI configuration space -------------------------------------------------
|
|
//
|
|
// Through the 0xCF8 mechanism, which reaches segment 0 and the first 256
|
|
// bytes of each function. AML that wants PCIe extended space is refused
|
|
// rather than handed bytes from somewhere else.
|
|
|
|
struct pcihandle {
|
|
uint8 bus, dev, func;
|
|
};
|
|
|
|
uacpi_status
|
|
uacpi_kernel_pci_device_open(uacpi_pci_address a, uacpi_handle *out)
|
|
{
|
|
struct pcihandle *p;
|
|
|
|
if (a.segment != 0)
|
|
return UACPI_STATUS_UNIMPLEMENTED;
|
|
p = uacpi_kernel_alloc(sizeof(*p));
|
|
if (p == 0)
|
|
return UACPI_STATUS_OUT_OF_MEMORY;
|
|
p->bus = a.bus;
|
|
p->dev = a.device;
|
|
p->func = a.function;
|
|
*out = p;
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_pci_device_close(uacpi_handle h)
|
|
{
|
|
uacpi_kernel_free(h);
|
|
}
|
|
|
|
#define PCIH ((struct pcihandle *)h)
|
|
#define CHECK(o, w) if ((o) + (w) > 256) return UACPI_STATUS_UNIMPLEMENTED
|
|
|
|
uacpi_status
|
|
uacpi_kernel_pci_read8(uacpi_handle h, uacpi_size o, uacpi_u8 *v)
|
|
{
|
|
CHECK(o, 1);
|
|
*v = pci_read8(PCIH->bus, PCIH->dev, PCIH->func, o);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_pci_read16(uacpi_handle h, uacpi_size o, uacpi_u16 *v)
|
|
{
|
|
CHECK(o, 2);
|
|
*v = pci_read16(PCIH->bus, PCIH->dev, PCIH->func, o);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_pci_read32(uacpi_handle h, uacpi_size o, uacpi_u32 *v)
|
|
{
|
|
CHECK(o, 4);
|
|
*v = pci_read32(PCIH->bus, PCIH->dev, PCIH->func, o);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
// Narrow writes as read-modify-write of the dword: the kernel has only a
|
|
// 32-bit config write, which is all 0xCF8 strictly offers anyway.
|
|
static void
|
|
pci_write_part(uacpi_handle h, uacpi_size o, uint32 v, int width)
|
|
{
|
|
uint32 shift = (o & 3) * 8;
|
|
uint32 mask = (width == 1 ? 0xFFu : 0xFFFFu) << shift;
|
|
uint32 d = pci_read32(PCIH->bus, PCIH->dev, PCIH->func, o & ~3);
|
|
d = (d & ~mask) | ((v << shift) & mask);
|
|
pci_write32(PCIH->bus, PCIH->dev, PCIH->func, o & ~3, d);
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_pci_write8(uacpi_handle h, uacpi_size o, uacpi_u8 v)
|
|
{
|
|
CHECK(o, 1);
|
|
pci_write_part(h, o, v, 1);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_pci_write16(uacpi_handle h, uacpi_size o, uacpi_u16 v)
|
|
{
|
|
CHECK(o, 2);
|
|
pci_write_part(h, o, v, 2);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_pci_write32(uacpi_handle h, uacpi_size o, uacpi_u32 v)
|
|
{
|
|
CHECK(o, 4);
|
|
pci_write32(PCIH->bus, PCIH->dev, PCIH->func, o, v);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
// --- time --------------------------------------------------------------------
|
|
|
|
uacpi_u64
|
|
uacpi_kernel_get_nanoseconds_since_boot(void)
|
|
{
|
|
uint64 t = rdtsc(), hz = tsc_hz ? tsc_hz : 1000000000ULL;
|
|
// Split so that the multiplication cannot overflow after a few hours.
|
|
return (t / hz) * 1000000000ULL + (t % hz) * 1000000000ULL / hz;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_stall(uacpi_u8 usec)
|
|
{
|
|
microdelay(usec);
|
|
}
|
|
|
|
// Wait for up to `ms`, or until *cond is nonzero when cond is given, sleeping
|
|
// a tick at a time when there is a process to sleep and spinning otherwise.
|
|
// Caller holds lk, which is released while waiting. Returns 0 if cond became
|
|
// true, -1 on timeout. 0xFFFF is "for ever", as uACPI spells it.
|
|
static int
|
|
wait_until(volatile int *cond, struct spinlock *lk, uint ms)
|
|
{
|
|
uint64 start = rdtsc();
|
|
uint64 limit = tsc_hz / 1000 * ms;
|
|
uint t0 = ticks;
|
|
|
|
while (!cond || !*cond) {
|
|
if (ms != 0xFFFF) {
|
|
if (myproc() ? (ticks - t0) * MSEC_PER_TICK >= ms
|
|
: rdtsc() - start >= limit)
|
|
return cond ? -1 : 0;
|
|
}
|
|
if (myproc()) {
|
|
sleep(&ticks, lk);
|
|
} else {
|
|
release(lk);
|
|
microdelay(10);
|
|
acquire(lk);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_sleep(uacpi_u64 msec)
|
|
{
|
|
static struct spinlock lk;
|
|
static int init;
|
|
|
|
if (!init) {
|
|
initlock(&lk, "acpisleep");
|
|
init = 1;
|
|
}
|
|
if (msec >= 0xFFFF)
|
|
msec = 0xFFFE;
|
|
acquire(&lk);
|
|
wait_until(0, &lk, msec);
|
|
release(&lk);
|
|
}
|
|
|
|
// --- locks and events --------------------------------------------------------
|
|
|
|
struct kmutex {
|
|
struct spinlock lk;
|
|
volatile int free; // 1 when nobody holds it
|
|
};
|
|
|
|
uacpi_handle
|
|
uacpi_kernel_create_mutex(void)
|
|
{
|
|
struct kmutex *m = uacpi_kernel_alloc(sizeof(*m));
|
|
if (m) {
|
|
initlock(&m->lk, "acpimutex");
|
|
m->free = 1;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
void uacpi_kernel_free_mutex(uacpi_handle h) { uacpi_kernel_free(h); }
|
|
|
|
uacpi_status
|
|
uacpi_kernel_acquire_mutex(uacpi_handle h, uacpi_u16 timeout)
|
|
{
|
|
struct kmutex *m = h;
|
|
int rc = 0;
|
|
|
|
acquire(&m->lk);
|
|
if (!m->free)
|
|
rc = timeout == 0 ? -1 : wait_until(&m->free, &m->lk, timeout);
|
|
if (rc == 0)
|
|
m->free = 0;
|
|
release(&m->lk);
|
|
return rc == 0 ? UACPI_STATUS_OK : UACPI_STATUS_TIMEOUT;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_release_mutex(uacpi_handle h)
|
|
{
|
|
struct kmutex *m = h;
|
|
acquire(&m->lk);
|
|
m->free = 1;
|
|
release(&m->lk);
|
|
}
|
|
|
|
struct kevent {
|
|
struct spinlock lk;
|
|
volatile int count;
|
|
};
|
|
|
|
uacpi_handle
|
|
uacpi_kernel_create_event(void)
|
|
{
|
|
struct kevent *e = uacpi_kernel_alloc(sizeof(*e));
|
|
if (e) {
|
|
initlock(&e->lk, "acpievent");
|
|
e->count = 0;
|
|
}
|
|
return e;
|
|
}
|
|
|
|
void uacpi_kernel_free_event(uacpi_handle h) { uacpi_kernel_free(h); }
|
|
|
|
uacpi_bool
|
|
uacpi_kernel_wait_for_event(uacpi_handle h, uacpi_u16 timeout)
|
|
{
|
|
struct kevent *e = h;
|
|
int rc = 0;
|
|
|
|
acquire(&e->lk);
|
|
if (e->count == 0)
|
|
rc = timeout == 0 ? -1 : wait_until(&e->count, &e->lk, timeout);
|
|
if (rc == 0)
|
|
e->count--;
|
|
release(&e->lk);
|
|
return rc == 0;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_signal_event(uacpi_handle h)
|
|
{
|
|
struct kevent *e = h;
|
|
acquire(&e->lk);
|
|
e->count++;
|
|
release(&e->lk);
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_reset_event(uacpi_handle h)
|
|
{
|
|
struct kevent *e = h;
|
|
acquire(&e->lk);
|
|
e->count = 0;
|
|
release(&e->lk);
|
|
}
|
|
|
|
uacpi_thread_id
|
|
uacpi_kernel_get_thread_id(void)
|
|
{
|
|
// A process, or -- before there are any -- the CPU, numbered from 1 so that
|
|
// it can never look like a null pointer.
|
|
struct proc *p = myproc();
|
|
return p ? (uacpi_thread_id)p : (uacpi_thread_id)(addr_t)(cpunum() + 1);
|
|
}
|
|
|
|
uacpi_interrupt_state
|
|
uacpi_kernel_disable_interrupts(void)
|
|
{
|
|
uacpi_interrupt_state s = readeflags() & FL_IF;
|
|
cli();
|
|
return s;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_restore_interrupts(uacpi_interrupt_state s)
|
|
{
|
|
if (s)
|
|
sti();
|
|
}
|
|
|
|
uacpi_handle
|
|
uacpi_kernel_create_spinlock(void)
|
|
{
|
|
struct spinlock *l = uacpi_kernel_alloc(sizeof(*l));
|
|
if (l)
|
|
initlock(l, "acpispin");
|
|
return l;
|
|
}
|
|
|
|
void uacpi_kernel_free_spinlock(uacpi_handle h) { uacpi_kernel_free(h); }
|
|
|
|
uacpi_cpu_flags
|
|
uacpi_kernel_lock_spinlock(uacpi_handle h)
|
|
{
|
|
acquire(h); // which disables interrupts, as asked
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
uacpi_kernel_unlock_spinlock(uacpi_handle h, uacpi_cpu_flags f)
|
|
{
|
|
(void)f;
|
|
release(h);
|
|
}
|
|
|
|
// --- the SCI -----------------------------------------------------------------
|
|
|
|
struct sci {
|
|
uacpi_interrupt_handler fn;
|
|
uacpi_handle ctx;
|
|
int irq;
|
|
};
|
|
|
|
static void
|
|
sci_trampoline(void *arg)
|
|
{
|
|
struct sci *s = arg;
|
|
s->fn(s->ctx);
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_install_interrupt_handler(uacpi_u32 irq,
|
|
uacpi_interrupt_handler fn,
|
|
uacpi_handle ctx, uacpi_handle *out)
|
|
{
|
|
struct sci *s;
|
|
int mode, gsi;
|
|
|
|
// The FADT names the SCI as an ISA IRQ. ACPI says it is level/low unless
|
|
// an override says otherwise -- and on most machines one does, making it
|
|
// level/high, which is the case that went wrong when only ISA's and PCI's
|
|
// signalling could be expressed.
|
|
gsi = acpi_isa_gsi(irq, IRQ_LEVEL_LOW, &mode);
|
|
if (gsi < 0 || gsi >= NIRQ_IOAPIC)
|
|
return UACPI_STATUS_UNIMPLEMENTED;
|
|
|
|
s = uacpi_kernel_alloc(sizeof(*s));
|
|
if (s == 0)
|
|
return UACPI_STATUS_OUT_OF_MEMORY;
|
|
s->fn = fn;
|
|
s->ctx = ctx;
|
|
s->irq = gsi;
|
|
if (irq_register(gsi, sci_trampoline, s, "acpi", mode | IRQ_QUIET) < 0) {
|
|
uacpi_kernel_free(s);
|
|
return UACPI_STATUS_INTERNAL_ERROR;
|
|
}
|
|
kinfo("acpi: SCI on irq %d (gsi %d, %s)\n", irq, gsi,
|
|
IRQ_IS_LEVEL(mode) ? "level" : "edge");
|
|
*out = s;
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_uninstall_interrupt_handler(uacpi_interrupt_handler fn,
|
|
uacpi_handle h)
|
|
{
|
|
struct sci *s = h;
|
|
(void)fn;
|
|
irq_unregister(s->irq, sci_trampoline, s);
|
|
uacpi_kernel_free(s);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
// --- deferred work -----------------------------------------------------------
|
|
//
|
|
// A ring of pending calls and one thread to make them. Work queued before the
|
|
// thread exists waits for it; nothing is queued before interrupts are on.
|
|
|
|
#define NWORK 64
|
|
|
|
static struct {
|
|
struct spinlock lk;
|
|
int init;
|
|
struct { uacpi_work_handler fn; uacpi_handle ctx; } q[NWORK];
|
|
uint r, w;
|
|
int busy;
|
|
} work;
|
|
|
|
static void
|
|
work_init(void)
|
|
{
|
|
if (!work.init) {
|
|
initlock(&work.lk, "acpiwork");
|
|
work.init = 1;
|
|
}
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_schedule_work(uacpi_work_type type, uacpi_work_handler fn,
|
|
uacpi_handle ctx)
|
|
{
|
|
(void)type; // GPEs and notifications alike: one CPU's worth
|
|
work_init();
|
|
acquire(&work.lk);
|
|
if (work.w - work.r >= NWORK) {
|
|
release(&work.lk);
|
|
cprintf("acpi: work queue full; an event was dropped\n");
|
|
return UACPI_STATUS_OUT_OF_MEMORY;
|
|
}
|
|
work.q[work.w % NWORK].fn = fn;
|
|
work.q[work.w % NWORK].ctx = ctx;
|
|
work.w++;
|
|
release(&work.lk);
|
|
wakeup(&work);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
uacpi_status
|
|
uacpi_kernel_wait_for_work_completion(void)
|
|
{
|
|
work_init();
|
|
acquire(&work.lk);
|
|
while (work.r != work.w || work.busy) {
|
|
if (myproc()) {
|
|
sleep(&ticks, &work.lk);
|
|
} else {
|
|
// No thread yet, so nothing would ever drain it: run it here.
|
|
uacpi_work_handler fn = work.q[work.r % NWORK].fn;
|
|
uacpi_handle ctx = work.q[work.r % NWORK].ctx;
|
|
work.r++;
|
|
release(&work.lk);
|
|
fn(ctx);
|
|
acquire(&work.lk);
|
|
}
|
|
}
|
|
release(&work.lk);
|
|
return UACPI_STATUS_OK;
|
|
}
|
|
|
|
static void
|
|
acpi_worker(void)
|
|
{
|
|
work_init();
|
|
for (;;) {
|
|
acquire(&work.lk);
|
|
while (work.r == work.w)
|
|
sleep(&work, &work.lk);
|
|
uacpi_work_handler fn = work.q[work.r % NWORK].fn;
|
|
uacpi_handle ctx = work.q[work.r % NWORK].ctx;
|
|
work.r++;
|
|
work.busy = 1;
|
|
release(&work.lk);
|
|
|
|
fn(ctx);
|
|
|
|
acquire(&work.lk);
|
|
work.busy = 0;
|
|
release(&work.lk);
|
|
}
|
|
}
|
|
|
|
void
|
|
acpi_thread_start(void)
|
|
{
|
|
if (acpi_rsdp_phys() == 0)
|
|
return;
|
|
if (kthread_create(acpi_worker, "acpi") < 0)
|
|
cprintf("acpi: no worker thread; the power button will not work\n");
|
|
}
|