diff --git a/share/man/man4/Makefile b/share/man/man4/Makefile --- a/share/man/man4/Makefile +++ b/share/man/man4/Makefile @@ -57,6 +57,7 @@ ath_hal.4 \ atkbd.4 \ atkbdc.4 \ + ${_atopcase.4} \ atp.4 \ ${_atf_test_case.4} \ ${_atrtc.4} \ @@ -794,6 +795,7 @@ _amdsmn.4= amdsmn.4 _amdtemp.4= amdtemp.4 _asmc.4= asmc.4 +_atopcase.4= atopcase.4 _bxe.4= bxe.4 _bytgpio.4= bytgpio.4 _chvgpio.4= chvgpio.4 diff --git a/share/man/man4/atopcase.4 b/share/man/man4/atopcase.4 new file mode 100644 --- /dev/null +++ b/share/man/man4/atopcase.4 @@ -0,0 +1,134 @@ +.\" Copyright (c) 2023 Vladimir Kondratyev +.\" +.\" 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. +.\" +.Dd August 17, 2023 +.Dt ATOPCASE 4 +.Os +.Sh NAME +.Nm atopcase +.Nd Apple HID-over-SPI transport driver +.Sh SYNOPSIS +To compile this driver into the kernel, +place the following lines in your +kernel configuration file: +.Bd -ragged -offset indent +.Cd "device atopcase" +.Cd "device intelspi" +.Cd "device spibus" +.Cd "device hidbus" +.Cd "device hkbd" +.Ed +.Pp +Alternatively, to load the driver as a +module at boot time, place the following line in +.Xr loader.conf 5 : +.Bd -literal -offset indent +atopcase_load="YES" +hkbd_load="YES" +.Ed +.Sh DESCRIPTION +The +.Nm +driver provides support for Human Interface Devices (HID) on +Serial Peripheral Interface (SPI) buses on Apple Intel Macs. +.Sh HARDWARE +The +.Nm +driver supports the following MacBooks produced in 2015-2018 years: +.Pp +.Bl -bullet -compact +.It +Macbook8,1 +.It +Macbook9,1 +.It +Macbook10,1 +.It +MacbookPro11,4 +.It +MacbookPro12,1 +.It +MacbookPro13,1 +.It +MacbookPro13,2 +.It +MacbookPro13,3 +.It +MacbookPro14,1 +.It +MacbookPro14,2 +.It +MacbookPro14,3 +.El +.Sh SYSCTL VARIABLES +The following variables are available as both +.Xr sysctl 8 +variables and +.Xr loader 8 +tunables: +.Bl -tag -width indent +.It Va hw.hid.atopcase.debug +Debug output level, where 0 is debugging disabled and larger values increase +debug message verbosity. +Default is 0. +.El +.Sh FILES +.Bl -tag -width ".Pa /dev/backlight/atopcase0" -compact +.It Pa /dev/backlight/atopcase0 +Keyboard +.Xr backlight 8 +device node. +.El +.Sh SEE ALSO +.Xr acpi 4 , +.Xr backlight 8 , +.Xr loader 8 , +.Xr loader.conf 5 . +.Sh HISTORY +The +.Nm +driver first appeared in +.Fx 14.0. +.Sh AUTHORS +.An -nosplit +The +.Nm +driver was originally written by +.An Val Packett Aq Mt val@packett.cool +and marginally improved upon by +.An Vladimir Kondratyev Aq Mt wulf@FreeBSD.org . +.Pp +This manual page was written by +.An Vladimir Kondratyev Aq Mt wulf@FreeBSD.org . +.Sh BUGS +Device interrupts are not acknowledged on some hardware that results in +interrupt storm. +Installation of Darwin OSI in +.Xr acpi 4 +driver fixes the issue. +To install Darwin OSI add following lines to +.Xr loader.conf 5 : +.Bl -tag -width indent +.It Va hw.acpi.install_interface="Darwin" +.It Va hw.acpi.remove_interface="Windows 2009, Windows 2012" +.El diff --git a/sys/conf/files.x86 b/sys/conf/files.x86 --- a/sys/conf/files.x86 +++ b/sys/conf/files.x86 @@ -69,6 +69,8 @@ dev/atkbdc/atkbdc_isa.c optional atkbdc isa dev/atkbdc/atkbdc_subr.c optional atkbdc dev/atkbdc/psm.c optional psm atkbdc +dev/atopcase/atopcase.c optional atopcase acpi hid spibus +dev/atopcase/atopcase_acpi.c optional atopcase acpi hid spibus dev/bxe/bxe.c optional bxe pci dev/bxe/bxe_stats.c optional bxe pci dev/bxe/bxe_debug.c optional bxe pci diff --git a/sys/dev/atopcase/atopcase.c b/sys/dev/atopcase/atopcase.c new file mode 100644 --- /dev/null +++ b/sys/dev/atopcase/atopcase.c @@ -0,0 +1,722 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2021-2023 Val Packett + * Copyright (c) 2023 Vladimir Kondratyev + * + * 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_hid.h" +#include "opt_spi.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#define HID_DEBUG_VAR atopcase_debug +#include +#include + +#include +#include + +#include "spibus_if.h" + +#include "atopcase_reg.h" +#include "atopcase_var.h" + +#define ATOPCASE_IN_KDB() (SCHEDULER_STOPPED() || kdb_active) +#define ATOPCASE_IN_POLLING_MODE(sc) \ + (((sc)->sc_gpe_bit == 0 && ((sc)->sc_irq_ih == NULL)) || cold ||\ + ATOPCASE_IN_KDB()) +#define ATOPCASE_WAKEUP(sc, chan) do { \ + if (!ATOPCASE_IN_POLLING_MODE(sc)) { \ + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "wakeup: %p\n", chan); \ + wakeup(chan); \ + } \ +} while (0) +#define ATOPCASE_SPI_PAUSE() DELAY(100) +#define ATOPCASE_SPI_NO_SLEEP_FLAG(sc) \ + ((sc)->sc_irq_ih != NULL ? SPI_FLAG_NO_SLEEP : 0) + +/* Tunables */ +static SYSCTL_NODE(_hw_hid, OID_AUTO, atopcase, CTLFLAG_RW | CTLFLAG_MPSAFE, 0, + "Apple MacBook Topcase HID driver"); + +#ifdef HID_DEBUG +enum atopcase_log_level atopcase_debug = ATOPCASE_LLEVEL_DISABLED; + +SYSCTL_INT(_hw_hid_atopcase, OID_AUTO, debug, CTLFLAG_RWTUN, + &atopcase_debug, ATOPCASE_LLEVEL_DISABLED, "atopcase log level"); +#endif /* !HID_DEBUG */ + +static const uint8_t booted[] = { 0xa0, 0x80, 0x00, 0x00 }; +static const uint8_t status_ok[] = { 0xac, 0x27, 0x68, 0xd5 }; + +static inline struct atopcase_child * +atopcase_get_child_by_device(struct atopcase_softc *sc, uint8_t device) +{ + switch (device) { + case ATOPCASE_DEV_KBRD: + return (&sc->sc_kb); + case ATOPCASE_DEV_TPAD: + return (&sc->sc_tp); + default: + return (NULL); + } +} + +static int +atopcase_receive_status(struct atopcase_softc *sc) +{ + struct spi_command cmd = SPI_COMMAND_INITIALIZER; + uint8_t dummy_buffer[4] = { 0 }; + uint8_t status_buffer[4] = { 0 }; + int err; + + cmd.tx_cmd = dummy_buffer; + cmd.tx_cmd_sz = sizeof(dummy_buffer); + cmd.rx_cmd = status_buffer; + cmd.rx_cmd_sz = sizeof(status_buffer); + cmd.flags = ATOPCASE_SPI_NO_SLEEP_FLAG(sc); + + err = SPIBUS_TRANSFER(device_get_parent(sc->sc_dev), sc->sc_dev, &cmd); + ATOPCASE_SPI_PAUSE(); + if (err) { + device_printf(sc->sc_dev, "SPI error: %d\n", err); + return (err); + } + + DPRINTFN(ATOPCASE_LLEVEL_TRACE, "Status: %*D\n", 4, status_buffer, " "); + + if (memcmp(status_buffer, status_ok, sizeof(status_ok)) == 0) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "Wrote command\n"); + ATOPCASE_WAKEUP(sc, sc->sc_dev); + } else { + device_printf(sc->sc_dev, "Failed to write command\n"); + return (EIO); + } + + return (0); +} + +static int +atopcase_process_message(struct atopcase_softc *sc, uint8_t device, void *msg, + uint16_t msg_len) +{ + struct atopcase_header *hdr = msg; + struct atopcase_child *ac; + void *payload; + uint16_t pl_len, crc; + + payload = (uint8_t *)msg + sizeof(*hdr); + pl_len = le16toh(hdr->len); + + if (pl_len + sizeof(*hdr) + sizeof(crc) != msg_len) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, + "message with length overflow\n"); + return (EIO); + } + + crc = le16toh(*(uint16_t *)((uint8_t *)payload + pl_len)); + if (crc != crc16(0, msg, msg_len - sizeof(crc))) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, + "message with failed checksum\n"); + return (EIO); + } + +#define CPOFF(dst, len, off) do { \ + unsigned _len = le16toh(len); \ + unsigned _off = le16toh(off); \ + if (pl_len >= _len + _off) { \ + memcpy(dst, (uint8_t*)payload + _off, MIN(_len, sizeof(dst)));\ + (dst)[MIN(_len, sizeof(dst) - 1)] = '\0'; \ + }} while (0); + + if ((ac = atopcase_get_child_by_device(sc, device)) != NULL + && hdr->type == ATOPCASE_MSG_TYPE_REPORT(device)) { + if (ac->open) + ac->intr_handler(ac->intr_ctx, payload, pl_len); + } else if (device == ATOPCASE_DEV_INFO + && hdr->type == ATOPCASE_MSG_TYPE_INFO(ATOPCASE_INFO_IFACE) + && (ac = atopcase_get_child_by_device(sc, hdr->type_arg)) != NULL) { + struct atopcase_iface_info_payload *iface = payload; + CPOFF(ac->name, iface->name_len, iface->name_off); + DPRINTF("Interface #%d name: %s\n", ac->device, ac->name); + } else if (device == ATOPCASE_DEV_INFO + && hdr->type == ATOPCASE_MSG_TYPE_INFO(ATOPCASE_INFO_DESCRIPTOR) + && (ac = atopcase_get_child_by_device(sc, hdr->type_arg)) != NULL) { + memcpy(ac->rdesc, payload, pl_len); + ac->rdesc_len = ac->hw.rdescsize = pl_len; + DPRINTF("%s HID report descriptor: %*D\n", ac->name, + (int) ac->hw.rdescsize, ac->rdesc, " "); + } else if (device == ATOPCASE_DEV_INFO + && hdr->type == ATOPCASE_MSG_TYPE_INFO(ATOPCASE_INFO_DEVICE) + && hdr->type_arg == ATOPCASE_INFO_DEVICE) { + struct atopcase_device_info_payload *dev = payload; + sc->sc_vid = le16toh(dev->vid); + sc->sc_pid = le16toh(dev->pid); + sc->sc_ver = le16toh(dev->ver); + CPOFF(sc->sc_vendor, dev->vendor_len, dev->vendor_off); + CPOFF(sc->sc_product, dev->product_len, dev->product_off); + CPOFF(sc->sc_serial, dev->serial_len, dev->serial_off); + if (bootverbose) { + device_printf(sc->sc_dev, "Device info descriptor:\n"); + printf(" Vendor: %s\n", sc->sc_vendor); + printf(" Product: %s\n", sc->sc_product); + printf(" Serial: %s\n", sc->sc_serial); + } + } + + return (0); +} + +int +atopcase_receive_packet(struct atopcase_softc *sc) +{ + struct atopcase_packet pkt = { 0 }; + struct spi_command cmd = SPI_COMMAND_INITIALIZER; + void *msg; + int err; + uint16_t length, remaining, offset, msg_len; + + bzero(&sc->sc_junk, sizeof(struct atopcase_packet)); + cmd.tx_cmd = &sc->sc_junk; + cmd.tx_cmd_sz = sizeof(struct atopcase_packet); + cmd.rx_cmd = &pkt; + cmd.rx_cmd_sz = sizeof(struct atopcase_packet); + cmd.flags = ATOPCASE_SPI_NO_SLEEP_FLAG(sc); + err = SPIBUS_TRANSFER(device_get_parent(sc->sc_dev), sc->sc_dev, &cmd); + ATOPCASE_SPI_PAUSE(); + if (err) { + device_printf(sc->sc_dev, "SPI error: %d\n", err); + return (err); + } + + DPRINTFN(ATOPCASE_LLEVEL_TRACE, "Response: %*D\n", 256, &pkt, " "); + + if (le16toh(pkt.checksum) != crc16(0, &pkt, sizeof(pkt) - 2)) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "packet with failed checksum\n"); + return (EIO); + } + + /* + * When we poll and nothing has arrived we get a particular packet + * starting with '80 11 00 01' + */ + if (pkt.direction == ATOPCASE_DIR_NOTHING) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "'Nothing' packet: %*D\n", 4, + &pkt, " "); + return (EAGAIN); + } + + if (pkt.direction != ATOPCASE_DIR_READ && + pkt.direction != ATOPCASE_DIR_WRITE) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, + "unknown message direction 0x%x\n", pkt.direction); + return (EIO); + } + + length = le16toh(pkt.length); + remaining = le16toh(pkt.remaining); + offset = le16toh(pkt.offset); + + if (length > sizeof(pkt.data)) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, + "packet with length overflow: %u\n", length); + return (EIO); + } + + if (pkt.direction == ATOPCASE_DIR_READ && + pkt.device == ATOPCASE_DEV_INFO && + length == sizeof(booted) && + memcmp(pkt.data, booted, length) == 0) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "GPE boot packet\n"); + sc->sc_booted = true; + ATOPCASE_WAKEUP(sc, sc); + return (0); + } + + /* handle multi-packet messages */ + if (remaining != 0 || offset != 0) { + if (offset != sc->sc_msg_len) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, + "Unexpected offset (got %u, expected %u)\n", + offset, sc->sc_msg_len); + sc->sc_msg_len = 0; + return (EIO); + } + + if ((size_t)remaining + length + offset > sizeof(sc->sc_msg)) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, + "Message with length overflow: %zu\n", + (size_t)remaining + length + offset); + sc->sc_msg_len = 0; + return (EIO); + } + + memcpy(sc->sc_msg + offset, &pkt.data, length); + sc->sc_msg_len += length; + + if (remaining != 0) + return (0); + + msg = sc->sc_msg; + msg_len = sc->sc_msg_len; + } else { + msg = pkt.data; + msg_len = length; + } + sc->sc_msg_len = 0; + + err = atopcase_process_message(sc, pkt.device, msg, msg_len); + if (err == 0 && pkt.direction == ATOPCASE_DIR_WRITE) { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "Write ack\n"); + ATOPCASE_WAKEUP(sc, sc); + } + + return (err); +} + +static int +atopcase_send(struct atopcase_softc *sc, struct atopcase_packet *pkt) +{ + struct spi_command cmd = SPI_COMMAND_INITIALIZER; + int err, retries; + + cmd.tx_cmd = pkt; + cmd.tx_cmd_sz = sizeof(struct atopcase_packet); + cmd.rx_cmd = &sc->sc_junk; + cmd.rx_cmd_sz = sizeof(struct atopcase_packet); + cmd.flags = SPI_FLAG_KEEP_CS | ATOPCASE_SPI_NO_SLEEP_FLAG(sc); + + DPRINTFN(ATOPCASE_LLEVEL_TRACE, "Request: %*D\n", + (int)sizeof(struct atopcase_packet), cmd.tx_cmd, " "); + + if (!ATOPCASE_IN_POLLING_MODE(sc)) { + if (sc->sc_irq_ih != NULL) + mtx_lock(&sc->sc_mtx); + else + sx_xlock(&sc->sc_sx); + } + sc->sc_wait_for_status = true; + err = SPIBUS_TRANSFER(device_get_parent(sc->sc_dev), sc->sc_dev, &cmd); + ATOPCASE_SPI_PAUSE(); + if (!ATOPCASE_IN_POLLING_MODE(sc)) { + if (sc->sc_irq_ih != NULL) + mtx_unlock(&sc->sc_mtx); + else + sx_xunlock(&sc->sc_sx); + } + if (err != 0) { + device_printf(sc->sc_dev, "SPI error: %d\n", err); + goto exit; + } + + if (ATOPCASE_IN_POLLING_MODE(sc)) { + err = atopcase_receive_status(sc); + } else { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "wait for: %p\n", sc->sc_dev); + err = tsleep(sc->sc_dev, 0, "atcstat", hz / 10); + } + sc->sc_wait_for_status = false; + if (err != 0) { + DPRINTF("Write status read failed: %d\n", err); + goto exit; + } + + if (ATOPCASE_IN_POLLING_MODE(sc)) { + /* Backlight setting may require a lot of time */ + retries = 20; + while ((err = atopcase_receive_packet(sc)) == EAGAIN && + --retries != 0) + DELAY(1000); + } else { + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "wait for: %p\n", sc); + err = tsleep(sc, 0, "atcack", hz / 10); + } + if (err != 0) + DPRINTF("Write ack read failed: %d\n", err); + +exit: + if (err == EWOULDBLOCK) + err = EIO; + + return (err); +} + +static void +atopcase_create_message(struct atopcase_packet *pkt, uint8_t device, + uint16_t type, uint8_t type_arg, const void *payload, uint8_t len, + uint16_t resp_len) +{ + struct atopcase_header *hdr = (struct atopcase_header *)pkt->data; + uint16_t msg_checksum; + static uint8_t seq_no; + + KASSERT(len <= ATOPCASE_DATA_SIZE - sizeof(struct atopcase_header), + ("outgoing msg must be 1 packet")); + + bzero(pkt, sizeof(struct atopcase_packet)); + pkt->direction = ATOPCASE_DIR_WRITE; + pkt->device = device; + pkt->length = htole16(sizeof(*hdr) + len + 2); + + hdr->type = htole16(type); + hdr->type_arg = type_arg; + hdr->seq_no = seq_no++; + hdr->resp_len = htole16((resp_len == 0) ? len : resp_len); + hdr->len = htole16(len); + + memcpy(pkt->data + sizeof(*hdr), payload, len); + msg_checksum = htole16(crc16(0, pkt->data, pkt->length - 2)); + memcpy(pkt->data + sizeof(*hdr) + len, &msg_checksum, 2); + pkt->checksum = htole16(crc16(0, (uint8_t*)pkt, sizeof(*pkt) - 2)); + + return; +} + +static int +atopcase_request_desc(struct atopcase_softc *sc, uint16_t type, uint8_t device) +{ + atopcase_create_message( + &sc->sc_buf, ATOPCASE_DEV_INFO, type, device, NULL, 0, 0x200); + return (atopcase_send(sc, &sc->sc_buf)); +} + +int +atopcase_intr(struct atopcase_softc *sc) +{ + int err; + + DPRINTFN(ATOPCASE_LLEVEL_DEBUG, "Interrupt event\n"); + + if (sc->sc_wait_for_status) { + err = atopcase_receive_status(sc); + sc->sc_wait_for_status = false; + } else + err = atopcase_receive_packet(sc); + + return (err); +} + +static int +atopcase_add_child(struct atopcase_softc *sc, struct atopcase_child *ac, + uint8_t device) +{ + device_t hidbus; + int err = 0; + + ac->device = device; + + /* fill device info */ + strlcpy(ac->hw.name, "Apple MacBook", sizeof(ac->hw.name)); + ac->hw.idBus = BUS_SPI; + ac->hw.idVendor = sc->sc_vid; + ac->hw.idProduct = sc->sc_pid; + ac->hw.idVersion = sc->sc_ver; + strlcpy(ac->hw.idPnP, sc->sc_hid, sizeof(ac->hw.idPnP)); + strlcpy(ac->hw.serial, sc->sc_serial, sizeof(ac->hw.serial)); + /* + * HID write and set_report methods executed on Apple SPI topcase + * hardware do the same request on SPI layer. Set HQ_NOWRITE quirk to + * force hidmap to convert writes to set_reports. That makes HID bus + * write handler unnecessary and reduces code duplication. + */ + hid_add_dynamic_quirk(&ac->hw, HQ_NOWRITE); + + DPRINTF("Get the interface #%d descriptor\n", device); + err = atopcase_request_desc(sc, + ATOPCASE_MSG_TYPE_INFO(ATOPCASE_INFO_IFACE), device); + if (err) { + device_printf(sc->sc_dev, "can't receive iface descriptor\n"); + goto exit; + } + + DPRINTF("Get the \"%s\" HID report descriptor\n", ac->name); + err = atopcase_request_desc(sc, + ATOPCASE_MSG_TYPE_INFO(ATOPCASE_INFO_DESCRIPTOR), device); + if (err) { + device_printf(sc->sc_dev, "can't receive report descriptor\n"); + goto exit; + } + + hidbus = device_add_child(sc->sc_dev, "hidbus", -1); + if (hidbus == NULL) { + device_printf(sc->sc_dev, "can't add child\n"); + err = ENOMEM; + goto exit; + } + device_set_ivars(hidbus, &ac->hw); + ac->hidbus = hidbus; + +exit: + return (err); +} + +int +atopcase_init(struct atopcase_softc *sc) +{ + int err; + + /* Wait until we know we're getting reasonable responses */ + if(!sc->sc_booted && tsleep(sc, 0, "atcboot", hz / 20) != 0) { + device_printf(sc->sc_dev, "can't establish communication\n"); + err = EIO; + goto err; + } + + /* + * Management device may send a message on first boot after power off. + * Let interrupt handler to read and discard it. + */ + DELAY(2000); + + DPRINTF("Get the device descriptor\n"); + err = atopcase_request_desc(sc, + ATOPCASE_MSG_TYPE_INFO(ATOPCASE_INFO_DEVICE), + ATOPCASE_INFO_DEVICE); + if (err) { + device_printf(sc->sc_dev, "can't receive device descriptor\n"); + goto err; + } + + err = atopcase_add_child(sc, &sc->sc_kb, ATOPCASE_DEV_KBRD); + if (err != 0) + goto err; + err = atopcase_add_child(sc, &sc->sc_tp, ATOPCASE_DEV_TPAD); + if (err != 0) + goto err; + + /* TODO: skip on 2015 models where it's controlled by asmc */ + sc->sc_backlight = backlight_register("atopcase", sc->sc_dev); + if (!sc->sc_backlight) { + device_printf(sc->sc_dev, "can't register backlight\n"); + err = ENOMEM; + } + + if (sc->sc_tq != NULL) + taskqueue_enqueue_timeout(sc->sc_tq, &sc->sc_task, hz / 120); + + return (bus_generic_attach(sc->sc_dev)); + +err: + return (err); +} + +int +atopcase_destroy(struct atopcase_softc *sc) +{ + int err; + + err = device_delete_children(sc->sc_dev); + if (err) + return (err); + + if (sc->sc_backlight) + backlight_destroy(sc->sc_backlight); + + return (0); +} + +static struct atopcase_child * +atopcase_get_child_by_hidbus(device_t child) +{ + device_t parent = device_get_parent(child); + struct atopcase_softc *sc = device_get_softc(parent); + + if (child == sc->sc_kb.hidbus) + return (&sc->sc_kb); + if (child == sc->sc_tp.hidbus) + return (&sc->sc_tp); + panic("unknown child"); +} + +void +atopcase_intr_setup(device_t dev, device_t child, hid_intr_t intr, + void *context, struct hid_rdesc_info *rdesc) +{ + struct atopcase_child *ac = atopcase_get_child_by_hidbus(child); + + if (intr == NULL) + return; + + rdesc->rdsize = ATOPCASE_MSG_SIZE - sizeof(struct atopcase_header) - 2; + rdesc->grsize = 0; + rdesc->srsize = ATOPCASE_DATA_SIZE - sizeof(struct atopcase_header) - 2; + rdesc->wrsize = 0; + + ac->intr_handler = intr; + ac->intr_ctx = context; +} + +void +atopcase_intr_unsetup(device_t dev, device_t child) +{ +} + +int +atopcase_intr_start(device_t dev, device_t child) +{ + struct atopcase_softc *sc = device_get_softc(dev); + struct atopcase_child *ac = atopcase_get_child_by_hidbus(child); + + if (ATOPCASE_IN_POLLING_MODE(sc)) + sx_xlock(&sc->sc_write_sx); + else if (sc->sc_irq_ih != NULL) + mtx_lock(&sc->sc_mtx); + else + sx_xlock(&sc->sc_sx); + ac->open = true; + if (ATOPCASE_IN_POLLING_MODE(sc)) + sx_xunlock(&sc->sc_write_sx); + else if (sc->sc_irq_ih != NULL) + mtx_unlock(&sc->sc_mtx); + else + sx_xunlock(&sc->sc_sx); + + return (0); +} + +int +atopcase_intr_stop(device_t dev, device_t child) +{ + struct atopcase_softc *sc = device_get_softc(dev); + struct atopcase_child *ac = atopcase_get_child_by_hidbus(child); + + if (ATOPCASE_IN_POLLING_MODE(sc)) + sx_xlock(&sc->sc_write_sx); + else if (sc->sc_irq_ih != NULL) + mtx_lock(&sc->sc_mtx); + else + sx_xlock(&sc->sc_sx); + ac->open = false; + if (ATOPCASE_IN_POLLING_MODE(sc)) + sx_xunlock(&sc->sc_write_sx); + else if (sc->sc_irq_ih != NULL) + mtx_unlock(&sc->sc_mtx); + else + sx_xunlock(&sc->sc_sx); + + return (0); +} + +void +atopcase_intr_poll(device_t dev, device_t child) +{ + struct atopcase_softc *sc = device_get_softc(dev); + + (void)atopcase_receive_packet(sc); +} + +int +atopcase_get_rdesc(device_t dev, device_t child, void *buf, hid_size_t len) +{ + struct atopcase_child *ac = atopcase_get_child_by_hidbus(child); + + if (ac->rdesc_len != len) + return (ENXIO); + memcpy(buf, ac->rdesc, len); + + return (0); +} + +int +atopcase_set_report(device_t dev, device_t child, const void *buf, + hid_size_t len, uint8_t type __unused, uint8_t id) +{ + struct atopcase_softc *sc = device_get_softc(dev); + struct atopcase_child *ac = atopcase_get_child_by_hidbus(child); + int err; + + if (len >= ATOPCASE_DATA_SIZE - sizeof(struct atopcase_header) - 2) + return (EINVAL); + + DPRINTF("%s HID command SET_REPORT %d (len %d): %*D\n", + ac->name, id, len, len, buf, " "); + + if (!ATOPCASE_IN_KDB()) + sx_xlock(&sc->sc_write_sx); + atopcase_create_message(&sc->sc_buf, ac->device, + ATOPCASE_MSG_TYPE_SET_REPORT(ac->device, id), 0, buf, len, 0); + err = atopcase_send(sc, &sc->sc_buf); + if (!ATOPCASE_IN_KDB()) + sx_xunlock(&sc->sc_write_sx); + + return (err); +} + +int +atopcase_backlight_update_status(device_t dev, struct backlight_props *props) +{ + struct atopcase_softc *sc = device_get_softc(dev); + struct atopcase_bl_payload payload = { 0 }; + + payload.report_id = ATOPCASE_BKL_REPORT_ID; + payload.device = ATOPCASE_DEV_KBRD; + /* + * Hardware range is 32-255 for visible backlight, + * convert from percentages + */ + payload.level = (props->brightness == 0) ? 0 : + (32 + (223 * props->brightness / 100)); + payload.status = (payload.level > 0) ? 0x01F4 : 0x1; + + return (atopcase_set_report(dev, sc->sc_kb.hidbus, &payload, + sizeof(payload), HID_OUTPUT_REPORT, ATOPCASE_BKL_REPORT_ID)); +} + +int +atopcase_backlight_get_status(device_t dev, struct backlight_props *props) +{ + struct atopcase_softc *sc = device_get_softc(dev); + + props->brightness = sc->sc_backlight_level; + props->nlevels = 0; + + return (0); +} + +int +atopcase_backlight_get_info(device_t dev, struct backlight_info *info) +{ + info->type = BACKLIGHT_TYPE_KEYBOARD; + strlcpy(info->name, "Apple MacBook Keyboard", BACKLIGHTMAXNAMELENGTH); + + return (0); +} diff --git a/sys/dev/atopcase/atopcase_acpi.c b/sys/dev/atopcase/atopcase_acpi.c new file mode 100644 --- /dev/null +++ b/sys/dev/atopcase/atopcase_acpi.c @@ -0,0 +1,458 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2021-2023 Val Packett + * Copyright (c) 2023 Vladimir Kondratyev + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#define HID_DEBUG_VAR atopcase_debug +#include +#include + +#include +#include + +#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); diff --git a/sys/dev/atopcase/atopcase_reg.h b/sys/dev/atopcase/atopcase_reg.h new file mode 100644 --- /dev/null +++ b/sys/dev/atopcase/atopcase_reg.h @@ -0,0 +1,110 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2021-2023 Val Packett + * Copyright (c) 2023 Vladimir Kondratyev + * + * 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. + */ + +#ifndef _ATOPCASE_REG_H_ +#define _ATOPCASE_REG_H_ + +#include + +#define ATOPCASE_PACKET_SIZE 256 +#define ATOPCASE_DATA_SIZE 246 +#define ATOPCASE_PKT_PER_MSG 2 +#define ATOPCASE_MSG_SIZE (ATOPCASE_DATA_SIZE * ATOPCASE_PKT_PER_MSG) + +/* Read == device-initiated, Write == host-initiated or reply to that */ +#define ATOPCASE_DIR_READ 0x20 +#define ATOPCASE_DIR_WRITE 0x40 +#define ATOPCASE_DIR_NOTHING 0x80 + +#define ATOPCASE_DEV_MGMT 0x00 +#define ATOPCASE_DEV_KBRD 0x01 +#define ATOPCASE_DEV_TPAD 0x02 +#define ATOPCASE_DEV_INFO 0xD0 + +#define ATOPCASE_BKL_REPORT_ID 0xB0 + +#define ATOPCASE_INFO_DEVICE 0x01 +#define ATOPCASE_INFO_IFACE 0x02 +#define ATOPCASE_INFO_DESCRIPTOR 0x10 + +#define ATOPCASE_MSG_TYPE_SET_REPORT(dev,rid) ((rid << 8) | 0x50 | dev) +#define ATOPCASE_MSG_TYPE_REPORT(dev) ((dev << 8) | 0x10) +#define ATOPCASE_MSG_TYPE_INFO(inf) ((inf << 8) | 0x20) + +struct atopcase_bl_payload { + uint8_t report_id; + uint8_t device; + uint16_t level; + uint16_t status; +} __packed; + +struct atopcase_device_info_payload { + uint16_t unknown[2]; + uint16_t num_devs; + uint16_t vid; + uint16_t pid; + uint16_t ver; + uint16_t vendor_off; + uint16_t vendor_len; + uint16_t product_off; + uint16_t product_len; + uint16_t serial_off; + uint16_t serial_len; +} __packed; + +struct atopcase_iface_info_payload { + uint8_t unknown0; + uint8_t iface_num; + uint8_t unknown1[3]; + uint8_t country_code; + uint16_t max_input_report_len; + uint16_t max_output_report_len; + uint16_t max_control_report_len; + uint16_t name_off; + uint16_t name_len; +} __packed; + +struct atopcase_header { + uint16_t type; + uint8_t type_arg; /* means "device" for ATOPCASE_MSG_TYPE_DESCRIPTOR */ + uint8_t seq_no; + uint16_t resp_len; + uint16_t len; +} __packed; + +struct atopcase_packet { + uint8_t direction; + uint8_t device; + uint16_t offset; + uint16_t remaining; + uint16_t length; + uint8_t data[ATOPCASE_DATA_SIZE]; + uint16_t checksum; +} __packed; + +#endif /* _ATOPCASE_REG_H_ */ diff --git a/sys/dev/atopcase/atopcase_var.h b/sys/dev/atopcase/atopcase_var.h new file mode 100644 --- /dev/null +++ b/sys/dev/atopcase/atopcase_var.h @@ -0,0 +1,142 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2021-2023 Val Packett + * Copyright (c) 2023 Vladimir Kondratyev + * + * 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. + */ + +#ifndef _ATOPCASE_VAR_H_ +#define _ATOPCASE_VAR_H_ + +#include "opt_hid.h" + +#include +#include +#include + +#include +#include + +#include + +#include + +struct atopcase_child { + device_t hidbus; + + struct hid_device_info hw; + + uint8_t device; + uint8_t name[80]; + + uint8_t rdesc[ATOPCASE_MSG_SIZE]; + size_t rdesc_len; + + hid_intr_t *intr_handler; + void *intr_ctx; + + bool open; +}; + +struct atopcase_softc { + device_t sc_dev; + + ACPI_HANDLE sc_handle; + int sc_gpe_bit; + + int sc_irq_rid; + struct resource *sc_irq_res; + void *sc_irq_ih; + volatile unsigned int sc_intr_cnt; + + struct timeout_task sc_task; + struct taskqueue *sc_tq; + + bool sc_booted; + bool sc_wait_for_status; + + uint8_t sc_hid[HID_PNP_ID_SIZE]; + uint8_t sc_vendor[80]; + uint8_t sc_product[80]; + uint8_t sc_serial[80]; + uint16_t sc_vid; + uint16_t sc_pid; + uint16_t sc_ver; + + /* + * Writes are complex and async (i.e. 2 responses arrive via interrupt) + * and cannot be interleaved (no new writes until responses arrive). + * they are serialized with sc_write_sx lock. + */ + struct sx sc_write_sx; + /* + * SPI transfers must be separated by a small pause. As they can be + * initiated by both interrupts and users, do ATOPCASE_SPI_PAUSE() + * after each transfer and serialize them with sc_sx or sc_mtx locks + * depending on interupt source (GPE or PIC). Still use sc_write_sx + * lock while polling. + */ + struct sx sc_sx; + struct mtx sc_mtx; + + struct atopcase_child sc_kb; + struct atopcase_child sc_tp; + + struct cdev *sc_backlight; + uint32_t sc_backlight_level; + + uint16_t sc_msg_len; + uint8_t sc_msg[ATOPCASE_MSG_SIZE]; + struct atopcase_packet sc_buf; + struct atopcase_packet sc_junk; +}; + +#ifdef HID_DEBUG +enum atopcase_log_level { + ATOPCASE_LLEVEL_DISABLED = 0, + ATOPCASE_LLEVEL_INFO, + ATOPCASE_LLEVEL_DEBUG, /* for troubleshooting */ + ATOPCASE_LLEVEL_TRACE, /* log every packet */ +}; +extern enum atopcase_log_level atopcase_debug; +#endif + +int atopcase_receive_packet(struct atopcase_softc *); +int atopcase_init(struct atopcase_softc *); +int atopcase_destroy(struct atopcase_softc *sc); +int atopcase_intr(struct atopcase_softc *); +void atopcase_intr_setup(device_t, device_t, hid_intr_t, void *, + struct hid_rdesc_info *); +void atopcase_intr_unsetup(device_t, device_t); +int atopcase_intr_start(device_t, device_t); +int atopcase_intr_stop(device_t, device_t); +void atopcase_intr_poll(device_t, device_t); +int atopcase_get_rdesc(device_t, device_t, void *, hid_size_t); +int atopcase_set_report(device_t, device_t, const void *, hid_size_t, uint8_t, + uint8_t); +int atopcase_backlight_update_status(device_t, struct backlight_props *); +int atopcase_backlight_get_status(device_t, struct backlight_props *); +int atopcase_backlight_get_info(device_t, struct backlight_info *); + +#endif /* _ATOPCASE_VAR_H_ */ diff --git a/sys/dev/hid/hidbus.c b/sys/dev/hid/hidbus.c --- a/sys/dev/hid/hidbus.c +++ b/sys/dev/hid/hidbus.c @@ -973,6 +973,7 @@ MODULE_DEPEND(hidbus, hid, 1, 1, 1); MODULE_VERSION(hidbus, 1); +DRIVER_MODULE(hidbus, atopcase, hidbus_driver, 0, 0); DRIVER_MODULE(hidbus, hvhid, hidbus_driver, 0, 0); DRIVER_MODULE(hidbus, iichid, hidbus_driver, 0, 0); DRIVER_MODULE(hidbus, usbhid, hidbus_driver, 0, 0); diff --git a/sys/modules/spi/Makefile b/sys/modules/spi/Makefile --- a/sys/modules/spi/Makefile +++ b/sys/modules/spi/Makefile @@ -2,7 +2,12 @@ SUBDIR = \ ../spigen \ at45d \ + ${_atopcase} \ mx25l \ spibus \ +.if ${MACHINE_CPUARCH} == "i386" || ${MACHINE_CPUARCH} == "amd64" +_atopcase=atopcase +.endif + .include diff --git a/sys/modules/spi/atopcase/Makefile b/sys/modules/spi/atopcase/Makefile new file mode 100644 --- /dev/null +++ b/sys/modules/spi/atopcase/Makefile @@ -0,0 +1,7 @@ +.PATH: ${SRCTOP}/sys/dev/atopcase +KMOD= atopcase +SRCS= atopcase.c atopcase_acpi.c +SRCS+= acpi_if.h backlight_if.h bus_if.h device_if.h hid_if.h opt_acpi.h \ + opt_hid.h opt_spi.h spibus_if.h usbdevs.h + +.include