freebsd-src/sys/dev/atopcase/atopcase_acpi.c
Val Packett 64fbda90da Add atopcase, the Apple HID over SPI input driver
The driver provides support for Human Interface Devices (HID) on
Serial Peripheral Interface (SPI) buses on Apple Intel Macs
produced in 2015-2018.

The driver appears to work more stable after installation of Darwin OSI
in acpi(4) driver.
To install Darwin OSI insert following lines into /boot/loader.conf:

hw.acpi.install_interface="Darwin"
hw.acpi.remove_interface="Windows 2009, Windows 2012"

Reviewed by:	wulf
Differential revision:	https://reviews.freebsd.org/D39863
2023-08-20 12:53:32 +03:00

459 lines
13 KiB
C

/*-
* 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);