blob: 48e872c3c507991d779caad7a897b56b96b25e78 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stdio.h>
#include <threads.h>
#include <zircon/syscalls.h>
#include <zircon/types.h>
#include <ddk/debug.h>
#include <ddk/driver.h>
#include <fbl/auto_call.h>
#include <hw/inout.h>
#include "acpi-private.h"
#include "dev.h"
#include "errors.h"
#define xprintf(fmt...) zxlogf(TRACE, fmt)
/* EC commands */
#define EC_CMD_READ 0x80
#define EC_CMD_WRITE 0x81
#define EC_CMD_QUERY 0x84
/* EC status register bits */
#define EC_SC_SCI_EVT (1 << 5)
#define EC_SC_IBF (1 << 1)
#define EC_SC_OBF (1 << 0)
/* Thread signals */
#define IRQ_RECEIVED ZX_EVENT_SIGNALED
#define EC_THREAD_SHUTDOWN ZX_USER_SIGNAL_0
#define EC_THREAD_SHUTDOWN_DONE ZX_USER_SIGNAL_1
typedef struct acpi_ec_device {
zx_device_t* zxdev;
ACPI_HANDLE acpi_handle;
// PIO addresses for EC device
uint16_t cmd_port;
uint16_t data_port;
// GPE for EC events
ACPI_HANDLE gpe_block;
UINT32 gpe;
// thread for processing events from the EC
thrd_t evt_thread;
zx_handle_t interrupt_event;
bool gpe_setup : 1;
bool thread_setup : 1;
bool ec_space_setup : 1;
} acpi_ec_device_t;
static ACPI_STATUS get_ec_handle(ACPI_HANDLE, UINT32, void*, void**);
static ACPI_STATUS get_ec_gpe_info(ACPI_HANDLE, ACPI_HANDLE*, UINT32*);
static ACPI_STATUS get_ec_ports(ACPI_HANDLE, uint16_t*, uint16_t*);
static ACPI_STATUS ec_space_setup_handler(ACPI_HANDLE Region, UINT32 Function, void* HandlerContext,
void** ReturnContext);
static ACPI_STATUS ec_space_request_handler(UINT32 Function, ACPI_PHYSICAL_ADDRESS Address,
UINT32 BitWidth, UINT64* Value, void* HandlerContext,
void* RegionContext);
static zx_status_t wait_for_interrupt(acpi_ec_device_t* dev);
static zx_status_t execute_read_op(acpi_ec_device_t* dev, uint8_t addr, uint8_t* val);
static zx_status_t execute_write_op(acpi_ec_device_t* dev, uint8_t addr, uint8_t val);
static zx_status_t execute_query_op(acpi_ec_device_t* dev, uint8_t* val);
// Execute the EC_CMD_READ operation. Requires the ACPI global lock be held.
static zx_status_t execute_read_op(acpi_ec_device_t* dev, uint8_t addr, uint8_t* val) {
// Issue EC command
outp(dev->cmd_port, EC_CMD_READ);
// Wait for EC to read the command so we can write the address
while (inp(dev->cmd_port) & EC_SC_IBF) {
zx_status_t status = wait_for_interrupt(dev);
if (status != ZX_OK) {
return status;
}
}
// Specify the address
outp(dev->data_port, addr);
// Wait for EC to read the address and write a response
while ((inp(dev->cmd_port) & (EC_SC_OBF | EC_SC_IBF)) != EC_SC_OBF) {
zx_status_t status = wait_for_interrupt(dev);
if (status != ZX_OK) {
return status;
}
}
// Read the response
*val = inp(dev->data_port);
return ZX_OK;
}
// Execute the EC_CMD_WRITE operation. Requires the ACPI global lock be held.
static zx_status_t execute_write_op(acpi_ec_device_t* dev, uint8_t addr, uint8_t val) {
// Issue EC command
outp(dev->cmd_port, EC_CMD_WRITE);
// Wait for EC to read the command so we can write the address
while (inp(dev->cmd_port) & EC_SC_IBF) {
zx_status_t status = wait_for_interrupt(dev);
if (status != ZX_OK) {
return status;
}
}
// Specify the address
outp(dev->data_port, addr);
// Wait for EC to read the address
while (inp(dev->cmd_port) & EC_SC_IBF) {
zx_status_t status = wait_for_interrupt(dev);
if (status != ZX_OK) {
return status;
}
}
// Write the data
outp(dev->data_port, val);
// Wait for EC to read the data
while (inp(dev->cmd_port) & EC_SC_IBF) {
zx_status_t status = wait_for_interrupt(dev);
if (status != ZX_OK) {
return status;
}
}
return ZX_OK;
}
// Execute the EC_CMD_QUERY operation. Requires the ACPI global lock be held.
static zx_status_t execute_query_op(acpi_ec_device_t* dev, uint8_t* event) {
// Query EC command
outp(dev->cmd_port, EC_CMD_QUERY);
// Wait for EC to respond
while ((inp(dev->cmd_port) & (EC_SC_OBF | EC_SC_IBF)) != EC_SC_OBF) {
zx_status_t status = wait_for_interrupt(dev);
if (status != ZX_OK) {
return status;
}
}
*event = inp(dev->data_port);
return ZX_OK;
}
static ACPI_STATUS ec_space_setup_handler(ACPI_HANDLE Region, UINT32 Function, void* HandlerContext,
void** ReturnContext) {
acpi_ec_device_t* dev = static_cast<acpi_ec_device_t*>(HandlerContext);
*ReturnContext = dev;
if (Function == ACPI_REGION_ACTIVATE) {
xprintf("acpi-ec: Setting up EC region");
return AE_OK;
} else if (Function == ACPI_REGION_DEACTIVATE) {
xprintf("acpi-ec: Tearing down EC region");
return AE_OK;
} else {
return AE_SUPPORT;
}
}
static ACPI_STATUS ec_space_request_handler(UINT32 Function, ACPI_PHYSICAL_ADDRESS Address,
UINT32 BitWidth, UINT64* Value, void* HandlerContext,
void* RegionContext) {
acpi_ec_device_t* dev = static_cast<acpi_ec_device_t*>(HandlerContext);
if (BitWidth != 8 && BitWidth != 16 && BitWidth != 32 && BitWidth != 64) {
return AE_BAD_PARAMETER;
}
if (Address > UINT8_MAX || Address - 1 + BitWidth / 8 > UINT8_MAX) {
return AE_BAD_PARAMETER;
}
UINT32 global_lock;
while (AcpiAcquireGlobalLock(0xFFFF, &global_lock) != AE_OK)
;
// NB: The processing of the read/write ops below will generate interrupts,
// which will unfortunately cause spurious wakeups on the event thread. One
// design that would avoid this is to have that thread responsible for
// processing these EC address space requests, but an attempt at an
// implementation failed due to apparent deadlocks against the Global Lock.
const size_t bytes = BitWidth / 8;
ACPI_STATUS status = AE_OK;
uint8_t* value_bytes = (uint8_t*)Value;
if (Function == ACPI_WRITE) {
for (size_t i = 0; i < bytes; ++i) {
zx_status_t zx_status =
execute_write_op(dev, static_cast<uint8_t>(Address + i), value_bytes[i]);
if (zx_status != ZX_OK) {
status = AE_ERROR;
goto finish;
}
}
} else {
*Value = 0;
for (size_t i = 0; i < bytes; ++i) {
zx_status_t zx_status =
execute_read_op(dev, static_cast<uint8_t>(Address + i), value_bytes + i);
if (zx_status != ZX_OK) {
status = AE_ERROR;
goto finish;
}
}
}
finish:
AcpiReleaseGlobalLock(global_lock);
return status;
}
static zx_status_t wait_for_interrupt(acpi_ec_device_t* dev) {
uint32_t pending;
zx_status_t status = zx_object_wait_one(dev->interrupt_event, IRQ_RECEIVED | EC_THREAD_SHUTDOWN,
ZX_TIME_INFINITE, &pending);
if (status != ZX_OK) {
printf("acpi-ec: thread wait failed: %d\n", status);
zx_object_signal(dev->interrupt_event, 0, EC_THREAD_SHUTDOWN_DONE);
return status;
}
if (pending & EC_THREAD_SHUTDOWN) {
zx_object_signal(dev->interrupt_event, 0, EC_THREAD_SHUTDOWN_DONE);
return ZX_ERR_STOP;
}
/* Clear interrupt */
zx_object_signal(dev->interrupt_event, IRQ_RECEIVED, 0);
return ZX_OK;
}
static int acpi_ec_thread(void* arg) {
acpi_ec_device_t* dev = static_cast<acpi_ec_device_t*>(arg);
UINT32 global_lock;
while (1) {
zx_status_t zx_status = wait_for_interrupt(dev);
if (zx_status != ZX_OK) {
goto exiting_without_lock;
}
while (AcpiAcquireGlobalLock(0xFFFF, &global_lock) != AE_OK)
;
uint8_t status;
bool processed_evt = false;
while ((status = inp(dev->cmd_port)) & EC_SC_SCI_EVT) {
uint8_t event_code;
zx_status_t zx_status = execute_query_op(dev, &event_code);
if (zx_status != ZX_OK) {
goto exiting_with_lock;
}
if (event_code != 0) {
char method[5] = {0};
snprintf(method, sizeof(method), "_Q%02x", event_code);
xprintf("acpi-ec: Invoking method %s", method);
AcpiEvaluateObject(dev->acpi_handle, method, NULL, NULL);
xprintf("acpi-ec: Invoked method %s", method);
} else {
xprintf("acpi-ec: Spurious event?");
}
processed_evt = true;
/* Clear interrupt before we check EVT again, to prevent a spurious
* interrupt later. There could be two sources of that spurious
* wakeup: Either we handled two events back-to-back, or we didn't
* wait for the OBF interrupt above. */
zx_object_signal(dev->interrupt_event, IRQ_RECEIVED, 0);
}
if (!processed_evt) {
xprintf("acpi-ec: Spurious wakeup, no evt: %#x", status);
}
AcpiReleaseGlobalLock(global_lock);
}
exiting_with_lock:
AcpiReleaseGlobalLock(global_lock);
exiting_without_lock:
xprintf("acpi-ec: thread terminated");
return 0;
}
static uint32_t raw_ec_event_gpe_handler(ACPI_HANDLE gpe_dev, uint32_t gpe_num, void* ctx) {
acpi_ec_device_t* dev = static_cast<acpi_ec_device_t*>(ctx);
zx_object_signal(dev->interrupt_event, 0, IRQ_RECEIVED);
return ACPI_REENABLE_GPE;
}
__UNUSED
static ACPI_STATUS get_ec_handle(ACPI_HANDLE object, UINT32 nesting_level, void* context,
void** ret) {
*(ACPI_HANDLE*)context = object;
return AE_OK;
}
static ACPI_STATUS get_ec_gpe_info(ACPI_HANDLE ec_handle, ACPI_HANDLE* gpe_block, UINT32* gpe) {
acpi::UniquePtr<ACPI_OBJECT> gpe_obj;
{
ACPI_BUFFER buffer = {
.Length = ACPI_ALLOCATE_BUFFER,
.Pointer = NULL,
};
ACPI_STATUS status = AcpiEvaluateObject(ec_handle, (char*)"_GPE", NULL, &buffer);
gpe_obj.reset(static_cast<ACPI_OBJECT*>(buffer.Pointer));
if (status != AE_OK) {
return status;
}
}
auto cleanup = fbl::MakeAutoCall([]() { xprintf("Failed to intepret EC GPE number"); });
/* According to section 12.11 of ACPI v6.1, a _GPE object on this device
* evaluates to either an integer specifying bit in the GPEx_STS blocks
* to use, or a package specifying which GPE block and which bit inside
* that block to use. */
if (gpe_obj->Type == ACPI_TYPE_INTEGER) {
*gpe_block = NULL;
*gpe = static_cast<uint32_t>(gpe_obj->Integer.Value);
} else if (gpe_obj->Type == ACPI_TYPE_PACKAGE) {
if (gpe_obj->Package.Count != 2) {
return AE_BAD_DATA;
}
ACPI_OBJECT* block_obj = &gpe_obj->Package.Elements[0];
ACPI_OBJECT* gpe_num_obj = &gpe_obj->Package.Elements[1];
if (block_obj->Type != ACPI_TYPE_LOCAL_REFERENCE) {
return AE_BAD_DATA;
}
if (gpe_num_obj->Type != ACPI_TYPE_INTEGER) {
return AE_BAD_DATA;
}
*gpe_block = block_obj->Reference.Handle;
*gpe = static_cast<uint32_t>(gpe_num_obj->Integer.Value);
} else {
return AE_BAD_DATA;
}
cleanup.cancel();
return AE_OK;
}
struct ec_ports_callback_ctx {
uint16_t* data_port;
uint16_t* cmd_port;
unsigned int resource_num;
};
static ACPI_STATUS get_ec_ports_callback(ACPI_RESOURCE* Resource, void* Context) {
struct ec_ports_callback_ctx* ctx = static_cast<ec_ports_callback_ctx*>(Context);
if (Resource->Type == ACPI_RESOURCE_TYPE_END_TAG) {
return AE_OK;
}
/* The spec says there will be at most 3 resources */
if (ctx->resource_num >= 3) {
return AE_BAD_DATA;
}
/* The third resource only exists on HW-Reduced platforms, which we don't
* support at the moment. */
if (ctx->resource_num == 2) {
xprintf("RESOURCE TYPE %d", Resource->Type);
return AE_NOT_IMPLEMENTED;
}
/* The two resources we're expecting are both address regions. First the
* data one, then the command one. We assume they're single IO ports. */
if (Resource->Type != ACPI_RESOURCE_TYPE_IO) {
return AE_SUPPORT;
}
if (Resource->Data.Io.Maximum != Resource->Data.Io.Minimum) {
return AE_SUPPORT;
}
uint16_t port = Resource->Data.Io.Minimum;
if (ctx->resource_num == 0) {
*ctx->data_port = port;
} else {
*ctx->cmd_port = port;
}
ctx->resource_num++;
return AE_OK;
}
static ACPI_STATUS get_ec_ports(ACPI_HANDLE ec_handle, uint16_t* data_port, uint16_t* cmd_port) {
struct ec_ports_callback_ctx ctx = {
.data_port = data_port,
.cmd_port = cmd_port,
.resource_num = 0,
};
return AcpiWalkResources(ec_handle, (char*)"_CRS", get_ec_ports_callback, &ctx);
}
static void acpi_ec_release(void* ctx) {
acpi_ec_device_t* dev = static_cast<acpi_ec_device_t*>(ctx);
if (dev->ec_space_setup) {
AcpiRemoveAddressSpaceHandler(ACPI_ROOT_OBJECT, ACPI_ADR_SPACE_EC, ec_space_request_handler);
}
if (dev->gpe_setup) {
AcpiDisableGpe(dev->gpe_block, dev->gpe);
AcpiRemoveGpeHandler(dev->gpe_block, dev->gpe, raw_ec_event_gpe_handler);
}
if (dev->interrupt_event != ZX_HANDLE_INVALID) {
if (dev->thread_setup) {
/* Shutdown the EC thread */
zx_object_signal(dev->interrupt_event, 0, EC_THREAD_SHUTDOWN);
zx_object_wait_one(dev->interrupt_event, EC_THREAD_SHUTDOWN_DONE, ZX_TIME_INFINITE, NULL);
thrd_join(dev->evt_thread, NULL);
}
zx_handle_close(dev->interrupt_event);
}
free(dev);
}
static void acpi_ec_suspend(void* ctx, uint8_t requested_state, bool enable_wake,
uint8_t suspend_reason) {
acpi_ec_device_t* dev = static_cast<acpi_ec_device_t*>(ctx);
if (suspend_reason != DEVICE_SUSPEND_REASON_MEXEC) {
device_suspend_reply(dev->zxdev, ZX_OK, requested_state);
return;
}
AcpiRemoveAddressSpaceHandler(ACPI_ROOT_OBJECT, ACPI_ADR_SPACE_EC, ec_space_request_handler);
dev->ec_space_setup = false;
AcpiDisableGpe(dev->gpe_block, dev->gpe);
AcpiRemoveGpeHandler(dev->gpe_block, dev->gpe, raw_ec_event_gpe_handler);
dev->gpe_setup = false;
zx_object_signal(dev->interrupt_event, 0, EC_THREAD_SHUTDOWN);
zx_object_wait_one(dev->interrupt_event, EC_THREAD_SHUTDOWN_DONE, ZX_TIME_INFINITE, NULL);
thrd_join(dev->evt_thread, NULL);
zx_handle_close(dev->interrupt_event);
dev->interrupt_event = ZX_HANDLE_INVALID;
device_suspend_reply(dev->zxdev, ZX_OK, requested_state);
}
static zx_protocol_device_t acpi_ec_device_proto = [] {
zx_protocol_device_t ops = {};
ops.version = DEVICE_OPS_VERSION;
ops.release = acpi_ec_release;
ops.suspend = acpi_ec_suspend;
return ops;
}();
zx_status_t ec_init(zx_device_t* parent, ACPI_HANDLE acpi_handle) {
xprintf("acpi-ec: init");
acpi_ec_device_t* dev = static_cast<acpi_ec_device_t*>(calloc(1, sizeof(acpi_ec_device_t)));
if (!dev) {
return ZX_ERR_NO_MEMORY;
}
dev->acpi_handle = acpi_handle;
zx_status_t err = zx_event_create(0, &dev->interrupt_event);
if (err != ZX_OK) {
xprintf("acpi-ec: Failed to create event: %d", err);
acpi_ec_release(dev);
return err;
}
int ret;
ACPI_STATUS status = get_ec_gpe_info(acpi_handle, &dev->gpe_block, &dev->gpe);
if (status != AE_OK) {
xprintf("acpi-ec: Failed to decode GPE info: %d", status);
goto acpi_error;
}
status = get_ec_ports(acpi_handle, &dev->data_port, &dev->cmd_port);
if (status != AE_OK) {
xprintf("acpi-ec: Failed to decode comm info: %d", status);
goto acpi_error;
}
// Please do not use get_root_resource() in new code. See fxbug.dev/31358.
status = zx_ioports_request(get_root_resource(), dev->data_port, 1);
if (status != ZX_OK) {
xprintf("acpi-ec: Failed to map ec data port: %d", status);
goto acpi_error;
}
status = zx_ioports_request(get_root_resource(), dev->cmd_port, 1);
if (status != ZX_OK) {
xprintf("acpi-ec: Failed to map ec cmd port: %d", status);
goto acpi_error;
}
/* Setup GPE handling */
status = AcpiInstallGpeHandler(dev->gpe_block, dev->gpe, ACPI_GPE_EDGE_TRIGGERED,
raw_ec_event_gpe_handler, dev);
if (status != AE_OK) {
xprintf("acpi-ec: Failed to install GPE %d: %x", dev->gpe, status);
goto acpi_error;
}
status = AcpiEnableGpe(dev->gpe_block, dev->gpe);
if (status != AE_OK) {
xprintf("acpi-ec: Failed to enable GPE %d: %x", dev->gpe, status);
AcpiRemoveGpeHandler(dev->gpe_block, dev->gpe, raw_ec_event_gpe_handler);
goto acpi_error;
}
dev->gpe_setup = true;
/* TODO(teisenbe): This thread should ideally be at a high priority, since
it takes the ACPI global lock which is shared with SMM. */
ret = thrd_create_with_name(&dev->evt_thread, acpi_ec_thread, dev, "acpi-ec-evt");
if (ret != thrd_success) {
xprintf("acpi-ec: Failed to create thread");
acpi_ec_release(dev);
return ZX_ERR_INTERNAL;
}
dev->thread_setup = true;
status = AcpiInstallAddressSpaceHandler(ACPI_ROOT_OBJECT, ACPI_ADR_SPACE_EC,
ec_space_request_handler, ec_space_setup_handler, dev);
if (status != AE_OK) {
xprintf("acpi-ec: Failed to install ec space handler");
acpi_ec_release(dev);
return acpi_to_zx_status(status);
}
dev->ec_space_setup = true;
{
device_add_args_t args = {};
args.version = DEVICE_ADD_ARGS_VERSION;
args.name = "acpi-ec";
args.ctx = dev;
args.ops = &acpi_ec_device_proto;
args.proto_id = ZX_PROTOCOL_MISC;
status = device_add(parent, &args, &dev->zxdev);
}
if (status != ZX_OK) {
xprintf("acpi-ec: could not add device! err=%d", status);
acpi_ec_release(dev);
return status;
}
printf("acpi-ec: initialized\n");
return ZX_OK;
acpi_error:
acpi_ec_release(dev);
return acpi_to_zx_status(status);
}