Changeset View
Changeset View
Standalone View
Standalone View
sys/dev/atopcase/atopcase_acpi.c
- This file was added.
/*- | |||||
* SPDX-License-Identifier: BSD-2-Clause | |||||
* | |||||
* Copyright (c) 2021-2023 Val Packett <val@packett.cool> | |||||
* Copyright (c) 2023 Vladimir Kondratyev <wulf@FreeBSD.org> | |||||
* | |||||
* Redistribution and use in source and binary forms, with or without | |||||
* modification, are permitted provided that the following conditions | |||||
* are met: | |||||
* 1. Redistributions of source code must retain the above copyright | |||||
* notice, this list of conditions and the following disclaimer. | |||||
* 2. Redistributions in binary form must reproduce the above copyright | |||||
* notice, this list of conditions and the following disclaimer in the | |||||
* documentation and/or other materials provided with the distribution. | |||||
* | |||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND | |||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE | |||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |||||
* SUCH DAMAGE. | |||||
*/ | |||||
#include "opt_acpi.h" | |||||
#include "opt_hid.h" | |||||
#include <sys/param.h> | |||||
#include <sys/bus.h> | |||||
#include <sys/kernel.h> | |||||
#include <sys/malloc.h> | |||||
#include <sys/mutex.h> | |||||
#include <sys/module.h> | |||||
#include <sys/rman.h> | |||||
#include <sys/sx.h> | |||||
#include <sys/taskqueue.h> | |||||
#include <contrib/dev/acpica/include/acpi.h> | |||||
#include <contrib/dev/acpica/include/accommon.h> | |||||
#include <contrib/dev/acpica/include/acevents.h> | |||||
#include <dev/acpica/acpivar.h> | |||||
#include <dev/acpica/acpiio.h> | |||||
#include <dev/backlight/backlight.h> | |||||
#include <dev/evdev/input.h> | |||||
#define HID_DEBUG_VAR atopcase_debug | |||||
#include <dev/hid/hid.h> | |||||
#include <dev/hid/hidquirk.h> | |||||
#include <dev/spibus/spi.h> | |||||
#include <dev/spibus/spibusvar.h> | |||||
#include "backlight_if.h" | |||||
#include "hid_if.h" | |||||
#include "atopcase_reg.h" | |||||
#include "atopcase_var.h" | |||||
/* | |||||
* XXX: The Linux driver only supports ACPI GPEs, but we only receive | |||||
* interrupts in this driver on a MacBookPro 12,1 and 14,1. This is because | |||||
* Linux responds to _OSI("Darwin") while we don't! | |||||
* | |||||
* ACPI GPE is enabled on FreeBSD by addition of following lines to | |||||
* /boot/loader.conf: | |||||
* hw.acpi.install_interface="Darwin" | |||||
* hw.acpi.remove_interface="Windows 2009, Windows 2012" | |||||
*/ | |||||
static const char *atopcase_ids[] = { "APP000D", NULL }; | |||||
static device_probe_t atopcase_acpi_probe; | |||||
static device_attach_t atopcase_acpi_attach; | |||||
static device_detach_t atopcase_acpi_detach; | |||||
static device_suspend_t atopcase_acpi_suspend; | |||||
static device_resume_t atopcase_acpi_resume; | |||||
static bool | |||||
acpi_is_atopcase(ACPI_HANDLE handle) | |||||
{ | |||||
const char **ids; | |||||
UINT32 sta; | |||||
for (ids = atopcase_ids; *ids != NULL; ids++) { | |||||
if (acpi_MatchHid(handle, *ids)) | |||||
break; | |||||
} | |||||
if (*ids == NULL) | |||||
return (false); | |||||
/* | |||||
* If no _STA method or if it failed, then assume that | |||||
* the device is present. | |||||
*/ | |||||
if (ACPI_FAILURE(acpi_GetInteger(handle, "_STA", &sta)) || | |||||
ACPI_DEVICE_PRESENT(sta)) | |||||
return (true); | |||||
return (false); | |||||
} | |||||
static int | |||||
atopcase_acpi_set_comm_enabled(struct atopcase_softc *sc, char *prop, | |||||
const bool on) | |||||
{ | |||||
ACPI_OBJECT argobj; | |||||
ACPI_OBJECT_LIST args; | |||||
argobj.Type = ACPI_TYPE_INTEGER; | |||||
argobj.Integer.Value = on; | |||||
args.Count = 1; | |||||
args.Pointer = &argobj; | |||||
if (ACPI_FAILURE( | |||||
AcpiEvaluateObject(sc->sc_handle, prop, &args, NULL))) | |||||
return (ENXIO); | |||||
DELAY(100); | |||||
return (0); | |||||
} | |||||
static int | |||||
atopcase_acpi_test_comm_enabled(ACPI_HANDLE handle, char *prop, int *enabled) | |||||
{ | |||||
if (ACPI_FAILURE(acpi_GetInteger(handle, prop, enabled))) | |||||
return (ENXIO); | |||||
return (0); | |||||
} | |||||
static void | |||||
atopcase_acpi_task(void *ctx, int pending __unused) | |||||
{ | |||||
struct atopcase_softc *sc = ctx; | |||||
int err = EAGAIN; | |||||
DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "Timer event\n"); | |||||
sx_xlock(&sc->sc_write_sx); | |||||
err = atopcase_receive_packet(sc); | |||||
sx_xunlock(&sc->sc_write_sx); | |||||
/* Rearm timer */ | |||||
taskqueue_enqueue_timeout(sc->sc_tq, &sc->sc_task, | |||||
hz / (err == EAGAIN ? 10 : 120)); | |||||
} | |||||
static void | |||||
atopcase_acpi_gpe_task(void *ctx) | |||||
{ | |||||
struct atopcase_softc *sc = ctx; | |||||
DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "GPE event\n"); | |||||
sx_xlock(&sc->sc_sx); | |||||
(void)atopcase_intr(sc); | |||||
sx_xunlock(&sc->sc_sx); | |||||
/* Rearm GPE */ | |||||
if (ACPI_FAILURE(AcpiFinishGpe(NULL, sc->sc_gpe_bit))) | |||||
device_printf(sc->sc_dev, "GPE rearm failed\n"); | |||||
} | |||||
static UINT32 | |||||
atopcase_acpi_notify(ACPI_HANDLE h __unused, UINT32 notify __unused, void *ctx) | |||||
{ | |||||
AcpiOsExecute(OSL_GPE_HANDLER, atopcase_acpi_gpe_task, ctx); | |||||
return (0); | |||||
} | |||||
static void | |||||
atopcase_acpi_intr(void *ctx) | |||||
{ | |||||
struct atopcase_softc *sc = ctx; | |||||
DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "Interrupt event\n"); | |||||
mtx_lock(&sc->sc_mtx); | |||||
sc->sc_intr_cnt++; | |||||
(void)atopcase_intr(sc); | |||||
mtx_unlock(&sc->sc_mtx); | |||||
} | |||||
static int | |||||
atopcase_acpi_probe(device_t dev) | |||||
{ | |||||
ACPI_HANDLE handle; | |||||
int usb_enabled; | |||||
if (acpi_disabled("atopcase")) | |||||
return (ENXIO); | |||||
handle = acpi_get_handle(dev); | |||||
if (handle == NULL) | |||||
return (ENXIO); | |||||
if (!acpi_is_atopcase(handle)) | |||||
return (ENXIO); | |||||
/* If USB interface exists and is enabled, use USB driver */ | |||||
if (atopcase_acpi_test_comm_enabled(handle, "UIST", &usb_enabled) == 0 | |||||
&& usb_enabled != 0) | |||||
return (ENXIO); | |||||
device_set_desc(dev, "Apple MacBook SPI Topcase"); | |||||
return (BUS_PROBE_DEFAULT); | |||||
} | |||||
static int | |||||
atopcase_acpi_attach(device_t dev) | |||||
{ | |||||
struct atopcase_softc *sc = device_get_softc(dev); | |||||
ACPI_DEVICE_INFO *device_info; | |||||
uint32_t cs_delay; | |||||
int spi_enabled, err; | |||||
sc->sc_dev = dev; | |||||
sc->sc_handle = acpi_get_handle(dev); | |||||
if (atopcase_acpi_test_comm_enabled(sc->sc_handle, "SIST", | |||||
&spi_enabled) != 0) { | |||||
device_printf(dev, "can't test SPI communication\n"); | |||||
return (ENXIO); | |||||
} | |||||
/* Turn SPI off if enabled to force "booted" packet to appear */ | |||||
if (spi_enabled != 0 && | |||||
atopcase_acpi_set_comm_enabled(sc, "SIEN", false) != 0) { | |||||
device_printf(dev, "can't disable SPI communication\n"); | |||||
return (ENXIO); | |||||
} | |||||
if (atopcase_acpi_set_comm_enabled(sc, "SIEN", true) != 0) { | |||||
device_printf(dev, "can't enable SPI communication\n"); | |||||
return (ENXIO); | |||||
} | |||||
/* | |||||
* Apple encodes a CS delay in ACPI properties, but | |||||
* - they're encoded in a non-standard way that predates _DSD, and | |||||
* - they're only exported if you respond to _OSI(Darwin) which we don't | |||||
* - because that has more side effects than we're prepared to handle | |||||
* - Apple makes a Windows driver and Windows is not Darwin | |||||
* - so presumably that one uses hardcoded values too | |||||
*/ | |||||
spibus_get_cs_delay(sc->sc_dev, &cs_delay); | |||||
if (cs_delay == 0) | |||||
spibus_set_cs_delay(sc->sc_dev, 10); | |||||
/* Retrieve ACPI _HID */ | |||||
if (ACPI_FAILURE(AcpiGetObjectInfo(sc->sc_handle, &device_info))) | |||||
return (ENXIO); | |||||
if (device_info->Valid & ACPI_VALID_HID) | |||||
strlcpy(sc->sc_hid, device_info->HardwareId.String, | |||||
sizeof(sc->sc_hid)); | |||||
AcpiOsFree(device_info); | |||||
sx_init(&sc->sc_write_sx, "atc_wr"); | |||||
sx_init(&sc->sc_sx, "atc_sx"); | |||||
mtx_init(&sc->sc_mtx, device_get_nameunit(dev), NULL, MTX_DEF); | |||||
err = ENXIO; | |||||
sc->sc_irq_res = bus_alloc_resource_any(sc->sc_dev, | |||||
SYS_RES_IRQ, &sc->sc_irq_rid, RF_ACTIVE); | |||||
if (sc->sc_irq_res != NULL) { | |||||
if (bus_setup_intr(dev, sc->sc_irq_res, | |||||
INTR_TYPE_MISC | INTR_MPSAFE, NULL, | |||||
atopcase_acpi_intr, sc, &sc->sc_irq_ih) != 0) { | |||||
device_printf(dev, "can't setup interrupt handler\n"); | |||||
goto err; | |||||
} | |||||
device_printf(dev, "Using interrupts.\n"); | |||||
/* | |||||
* On some hardware interrupts are not acked by SPI read for | |||||
* unknown reasons that leads to interrupt storm due to level | |||||
* triggering. GPE does not suffer from this problem. | |||||
* | |||||
* TODO: Find out what Windows driver does to ack IRQ. | |||||
*/ | |||||
pause("atopcase", hz / 5); | |||||
DPRINTF("interrupts asserted: %u\n", sc->sc_intr_cnt); | |||||
if (sc->sc_intr_cnt > 2 || sc->sc_intr_cnt == 0) { | |||||
bus_teardown_intr(dev, sc->sc_irq_res, sc->sc_irq_ih); | |||||
sc->sc_irq_ih = NULL; | |||||
device_printf(dev, "Interrupt storm detected. " | |||||
"Falling back to polling\n"); | |||||
sc->sc_tq = taskqueue_create("atc_tq", M_WAITOK|M_ZERO, | |||||
taskqueue_thread_enqueue, &sc->sc_tq); | |||||
TIMEOUT_TASK_INIT(sc->sc_tq, &sc->sc_task, 0, | |||||
atopcase_acpi_task, sc); | |||||
taskqueue_start_threads(&sc->sc_tq, 1, PI_TTY, | |||||
"%s taskq", device_get_nameunit(dev)); | |||||
} | |||||
/* | |||||
* Interrupts does not work at all. It may happen if kernel | |||||
* erroneously detected stray irq at bus_teardown_intr() and | |||||
* completelly disabled it after than. | |||||
* Fetch "booted" packet manually to pass communication check. | |||||
*/ | |||||
if (sc->sc_intr_cnt == 0) | |||||
atopcase_receive_packet(sc); | |||||
} else { | |||||
if (bootverbose) | |||||
device_printf(dev, "can't allocate IRQ resource\n"); | |||||
if (ACPI_FAILURE(acpi_GetInteger(sc->sc_handle, "_GPE", | |||||
&sc->sc_gpe_bit))) { | |||||
device_printf(dev, "can't allocate nor IRQ nor GPE\n"); | |||||
goto err; | |||||
} | |||||
if (ACPI_FAILURE(AcpiInstallGpeHandler(NULL, sc->sc_gpe_bit, | |||||
ACPI_GPE_LEVEL_TRIGGERED, atopcase_acpi_notify, sc))) { | |||||
device_printf(dev, "can't install ACPI GPE handler\n"); | |||||
goto err; | |||||
} | |||||
if (ACPI_FAILURE(AcpiEnableGpe(NULL, sc->sc_gpe_bit))) { | |||||
device_printf(dev, "can't enable ACPI notification\n"); | |||||
goto err; | |||||
} | |||||
device_printf(dev, "Using ACPI GPE.\n"); | |||||
if (bootverbose) | |||||
device_printf(dev, "GPE int %d\n", sc->sc_gpe_bit); | |||||
} | |||||
err = atopcase_init(sc); | |||||
err: | |||||
if (err != 0) | |||||
atopcase_acpi_detach(dev); | |||||
return (err); | |||||
} | |||||
static int | |||||
atopcase_acpi_detach(device_t dev) | |||||
{ | |||||
struct atopcase_softc *sc = device_get_softc(dev); | |||||
int err; | |||||
err = atopcase_destroy(sc); | |||||
if (err != 0) | |||||
return (err); | |||||
if (sc->sc_irq_ih) | |||||
bus_teardown_intr(dev, sc->sc_irq_res, sc->sc_irq_ih); | |||||
if (sc->sc_irq_res != NULL) | |||||
bus_release_resource(dev, SYS_RES_IRQ, | |||||
sc->sc_irq_rid, sc->sc_irq_res); | |||||
if (sc->sc_tq != NULL) { | |||||
while (taskqueue_cancel_timeout(sc->sc_tq, &sc->sc_task, NULL)) | |||||
taskqueue_drain_timeout(sc->sc_tq, &sc->sc_task); | |||||
taskqueue_free(sc->sc_tq); | |||||
} | |||||
if (sc->sc_gpe_bit != 0 && ACPI_FAILURE(AcpiRemoveGpeHandler(NULL, | |||||
sc->sc_gpe_bit, atopcase_acpi_notify))) | |||||
device_printf(dev, "can't remove ACPI GPE handler\n"); | |||||
if (atopcase_acpi_set_comm_enabled(sc, "SIEN", false) != 0) | |||||
device_printf(dev, "can't disable SPI communication\n"); | |||||
mtx_destroy(&sc->sc_mtx); | |||||
sx_destroy(&sc->sc_sx); | |||||
sx_destroy(&sc->sc_write_sx); | |||||
return (0); | |||||
} | |||||
static int | |||||
atopcase_acpi_suspend(device_t dev) | |||||
{ | |||||
struct atopcase_softc *sc = device_get_softc(dev); | |||||
int err; | |||||
err = bus_generic_suspend(dev); | |||||
if (err) | |||||
return (err); | |||||
if (sc->sc_gpe_bit != 0) | |||||
AcpiDisableGpe(NULL, sc->sc_gpe_bit); | |||||
if (sc->sc_tq != NULL) | |||||
while (taskqueue_cancel_timeout(sc->sc_tq, &sc->sc_task, NULL)) | |||||
taskqueue_drain_timeout(sc->sc_tq, &sc->sc_task); | |||||
if (atopcase_acpi_set_comm_enabled(sc, "SIEN", false) != 0) | |||||
device_printf(dev, "can't disable SPI communication\n"); | |||||
return (0); | |||||
} | |||||
static int | |||||
atopcase_acpi_resume(device_t dev) | |||||
{ | |||||
struct atopcase_softc *sc = device_get_softc(dev); | |||||
if (sc->sc_gpe_bit != 0) | |||||
AcpiEnableGpe(NULL, sc->sc_gpe_bit); | |||||
if (sc->sc_tq != NULL) | |||||
taskqueue_enqueue_timeout(sc->sc_tq, &sc->sc_task, hz / 120); | |||||
if (atopcase_acpi_set_comm_enabled(sc, "SIEN", true) != 0) { | |||||
device_printf(dev, "can't enable SPI communication\n"); | |||||
return (ENXIO); | |||||
} | |||||
return (bus_generic_resume(dev)); | |||||
} | |||||
static device_method_t atopcase_methods[] = { | |||||
/* Device interface */ | |||||
DEVMETHOD(device_probe, atopcase_acpi_probe), | |||||
DEVMETHOD(device_attach, atopcase_acpi_attach), | |||||
DEVMETHOD(device_detach, atopcase_acpi_detach), | |||||
DEVMETHOD(device_suspend, atopcase_acpi_suspend), | |||||
DEVMETHOD(device_resume, atopcase_acpi_resume), | |||||
/* HID interrupt interface */ | |||||
DEVMETHOD(hid_intr_setup, atopcase_intr_setup), | |||||
DEVMETHOD(hid_intr_unsetup, atopcase_intr_unsetup), | |||||
DEVMETHOD(hid_intr_start, atopcase_intr_start), | |||||
DEVMETHOD(hid_intr_stop, atopcase_intr_stop), | |||||
DEVMETHOD(hid_intr_poll, atopcase_intr_poll), | |||||
/* HID interface */ | |||||
DEVMETHOD(hid_get_rdesc, atopcase_get_rdesc), | |||||
DEVMETHOD(hid_set_report, atopcase_set_report), | |||||
/* Backlight interface */ | |||||
DEVMETHOD(backlight_update_status, atopcase_backlight_update_status), | |||||
DEVMETHOD(backlight_get_status, atopcase_backlight_get_status), | |||||
DEVMETHOD(backlight_get_info, atopcase_backlight_get_info), | |||||
DEVMETHOD_END | |||||
}; | |||||
static driver_t atopcase_driver = { | |||||
"atopcase", | |||||
atopcase_methods, | |||||
sizeof(struct atopcase_softc), | |||||
}; | |||||
DRIVER_MODULE(atopcase, spibus, atopcase_driver, 0, 0); | |||||
MODULE_DEPEND(atopcase, acpi, 1, 1, 1); | |||||
MODULE_DEPEND(atopcase, backlight, 1, 1, 1); | |||||
MODULE_DEPEND(atopcase, hid, 1, 1, 1); | |||||
MODULE_DEPEND(atopcase, hidbus, 1, 1, 1); | |||||
MODULE_DEPEND(atopcase, spibus, 1, 1, 1); | |||||
MODULE_VERSION(atopcase, 1); | |||||
SPIBUS_ACPI_PNP_INFO(atopcase_ids); |