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
980 lines
24 KiB
C
980 lines
24 KiB
C
/*-
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*
|
|
* Copyright (c) 2019-2020 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 <sys/cdefs.h>
|
|
#include <sys/param.h>
|
|
#include <sys/bus.h>
|
|
#include <sys/ck.h>
|
|
#include <sys/epoch.h>
|
|
#include <sys/kdb.h>
|
|
#include <sys/kernel.h>
|
|
#include <sys/libkern.h>
|
|
#include <sys/lock.h>
|
|
#include <sys/malloc.h>
|
|
#include <sys/module.h>
|
|
#include <sys/mutex.h>
|
|
#include <sys/proc.h>
|
|
#include <sys/sbuf.h>
|
|
#include <sys/sx.h>
|
|
#include <sys/systm.h>
|
|
|
|
#define HID_DEBUG_VAR hid_debug
|
|
#include <dev/hid/hid.h>
|
|
#include <dev/hid/hidbus.h>
|
|
#include <dev/hid/hidquirk.h>
|
|
|
|
#include "hid_if.h"
|
|
|
|
#define INPUT_EPOCH global_epoch_preempt
|
|
#define HID_RSIZE_MAX 1024
|
|
|
|
static hid_intr_t hidbus_intr;
|
|
|
|
static device_probe_t hidbus_probe;
|
|
static device_attach_t hidbus_attach;
|
|
static device_detach_t hidbus_detach;
|
|
|
|
struct hidbus_ivars {
|
|
int32_t usage;
|
|
uint8_t index;
|
|
uint32_t flags;
|
|
uintptr_t driver_info; /* for internal use */
|
|
struct mtx *mtx; /* child intr mtx */
|
|
hid_intr_t *intr_handler; /* executed under mtx*/
|
|
void *intr_ctx;
|
|
unsigned int refcnt; /* protected by mtx */
|
|
struct epoch_context epoch_ctx;
|
|
CK_STAILQ_ENTRY(hidbus_ivars) link;
|
|
};
|
|
|
|
struct hidbus_softc {
|
|
device_t dev;
|
|
struct sx sx;
|
|
struct mtx mtx;
|
|
|
|
bool nowrite;
|
|
|
|
struct hid_rdesc_info rdesc;
|
|
bool overloaded;
|
|
int nest; /* Child attach nesting lvl */
|
|
int nauto; /* Number of autochildren */
|
|
|
|
CK_STAILQ_HEAD(, hidbus_ivars) tlcs;
|
|
};
|
|
|
|
static int
|
|
hidbus_fill_rdesc_info(struct hid_rdesc_info *hri, const void *data,
|
|
hid_size_t len)
|
|
{
|
|
int error = 0;
|
|
|
|
hri->data = __DECONST(void *, data);
|
|
hri->len = len;
|
|
|
|
/*
|
|
* If report descriptor is not available yet, set maximal
|
|
* report sizes high enough to allow hidraw to work.
|
|
*/
|
|
hri->isize = len == 0 ? HID_RSIZE_MAX :
|
|
hid_report_size_max(data, len, hid_input, &hri->iid);
|
|
hri->osize = len == 0 ? HID_RSIZE_MAX :
|
|
hid_report_size_max(data, len, hid_output, &hri->oid);
|
|
hri->fsize = len == 0 ? HID_RSIZE_MAX :
|
|
hid_report_size_max(data, len, hid_feature, &hri->fid);
|
|
|
|
if (hri->isize > HID_RSIZE_MAX) {
|
|
DPRINTF("input size is too large, %u bytes (truncating)\n",
|
|
hri->isize);
|
|
hri->isize = HID_RSIZE_MAX;
|
|
error = EOVERFLOW;
|
|
}
|
|
if (hri->osize > HID_RSIZE_MAX) {
|
|
DPRINTF("output size is too large, %u bytes (truncating)\n",
|
|
hri->osize);
|
|
hri->osize = HID_RSIZE_MAX;
|
|
error = EOVERFLOW;
|
|
}
|
|
if (hri->fsize > HID_RSIZE_MAX) {
|
|
DPRINTF("feature size is too large, %u bytes (truncating)\n",
|
|
hri->fsize);
|
|
hri->fsize = HID_RSIZE_MAX;
|
|
error = EOVERFLOW;
|
|
}
|
|
|
|
return (error);
|
|
}
|
|
|
|
int
|
|
hidbus_locate(const void *desc, hid_size_t size, int32_t u, enum hid_kind k,
|
|
uint8_t tlc_index, uint8_t index, struct hid_location *loc,
|
|
uint32_t *flags, uint8_t *id, struct hid_absinfo *ai)
|
|
{
|
|
struct hid_data *d;
|
|
struct hid_item h;
|
|
int i;
|
|
|
|
d = hid_start_parse(desc, size, 1 << k);
|
|
HIDBUS_FOREACH_ITEM(d, &h, tlc_index) {
|
|
for (i = 0; i < h.nusages; i++) {
|
|
if (h.kind == k && h.usages[i] == u) {
|
|
if (index--)
|
|
break;
|
|
if (loc != NULL)
|
|
*loc = h.loc;
|
|
if (flags != NULL)
|
|
*flags = h.flags;
|
|
if (id != NULL)
|
|
*id = h.report_ID;
|
|
if (ai != NULL && (h.flags&HIO_RELATIVE) == 0)
|
|
*ai = (struct hid_absinfo) {
|
|
.max = h.logical_maximum,
|
|
.min = h.logical_minimum,
|
|
.res = hid_item_resolution(&h),
|
|
};
|
|
hid_end_parse(d);
|
|
return (1);
|
|
}
|
|
}
|
|
}
|
|
if (loc != NULL)
|
|
loc->size = 0;
|
|
if (flags != NULL)
|
|
*flags = 0;
|
|
if (id != NULL)
|
|
*id = 0;
|
|
hid_end_parse(d);
|
|
return (0);
|
|
}
|
|
|
|
bool
|
|
hidbus_is_collection(const void *desc, hid_size_t size, int32_t usage,
|
|
uint8_t tlc_index)
|
|
{
|
|
struct hid_data *d;
|
|
struct hid_item h;
|
|
bool ret = false;
|
|
|
|
d = hid_start_parse(desc, size, 0);
|
|
HIDBUS_FOREACH_ITEM(d, &h, tlc_index) {
|
|
if (h.kind == hid_collection && h.usage == usage) {
|
|
ret = true;
|
|
break;
|
|
}
|
|
}
|
|
hid_end_parse(d);
|
|
return (ret);
|
|
}
|
|
|
|
static device_t
|
|
hidbus_add_child(device_t dev, u_int order, const char *name, int unit)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(dev);
|
|
struct hidbus_ivars *tlc;
|
|
device_t child;
|
|
|
|
child = device_add_child_ordered(dev, order, name, unit);
|
|
if (child == NULL)
|
|
return (child);
|
|
|
|
tlc = malloc(sizeof(struct hidbus_ivars), M_DEVBUF, M_WAITOK | M_ZERO);
|
|
tlc->mtx = &sc->mtx;
|
|
device_set_ivars(child, tlc);
|
|
sx_xlock(&sc->sx);
|
|
CK_STAILQ_INSERT_TAIL(&sc->tlcs, tlc, link);
|
|
sx_unlock(&sc->sx);
|
|
|
|
return (child);
|
|
}
|
|
|
|
static int
|
|
hidbus_enumerate_children(device_t dev, const void* data, hid_size_t len)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(dev);
|
|
struct hid_data *hd;
|
|
struct hid_item hi;
|
|
device_t child;
|
|
uint8_t index = 0;
|
|
|
|
if (data == NULL || len == 0)
|
|
return (ENXIO);
|
|
|
|
/* Add a child for each top level collection */
|
|
hd = hid_start_parse(data, len, 1 << hid_input);
|
|
while (hid_get_item(hd, &hi)) {
|
|
if (hi.kind != hid_collection || hi.collevel != 1)
|
|
continue;
|
|
child = BUS_ADD_CHILD(dev, 0, NULL, -1);
|
|
if (child == NULL) {
|
|
device_printf(dev, "Could not add HID device\n");
|
|
continue;
|
|
}
|
|
hidbus_set_index(child, index);
|
|
hidbus_set_usage(child, hi.usage);
|
|
hidbus_set_flags(child, HIDBUS_FLAG_AUTOCHILD);
|
|
index++;
|
|
DPRINTF("Add child TLC: 0x%04x:0x%04x\n",
|
|
HID_GET_USAGE_PAGE(hi.usage), HID_GET_USAGE(hi.usage));
|
|
}
|
|
hid_end_parse(hd);
|
|
|
|
if (index == 0)
|
|
return (ENXIO);
|
|
|
|
sc->nauto = index;
|
|
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
hidbus_attach_children(device_t dev)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(dev);
|
|
int error;
|
|
|
|
HID_INTR_SETUP(device_get_parent(dev), dev, hidbus_intr, sc,
|
|
&sc->rdesc);
|
|
|
|
error = hidbus_enumerate_children(dev, sc->rdesc.data, sc->rdesc.len);
|
|
if (error != 0)
|
|
DPRINTF("failed to enumerate children: error %d\n", error);
|
|
|
|
/*
|
|
* hidbus_attach_children() can recurse through device_identify->
|
|
* hid_set_report_descr() call sequence. Do not perform children
|
|
* attach twice in that case.
|
|
*/
|
|
sc->nest++;
|
|
bus_generic_probe(dev);
|
|
sc->nest--;
|
|
if (sc->nest != 0)
|
|
return (0);
|
|
|
|
if (hid_is_keyboard(sc->rdesc.data, sc->rdesc.len) != 0)
|
|
error = bus_generic_attach(dev);
|
|
else
|
|
error = bus_delayed_attach_children(dev);
|
|
if (error != 0)
|
|
device_printf(dev, "failed to attach child: error %d\n", error);
|
|
|
|
return (error);
|
|
}
|
|
|
|
static int
|
|
hidbus_detach_children(device_t dev)
|
|
{
|
|
device_t *children, bus;
|
|
bool is_bus;
|
|
int i, error;
|
|
|
|
error = 0;
|
|
|
|
is_bus = device_get_devclass(dev) == devclass_find("hidbus");
|
|
bus = is_bus ? dev : device_get_parent(dev);
|
|
|
|
KASSERT(device_get_devclass(bus) == devclass_find("hidbus"),
|
|
("Device is not hidbus or it's child"));
|
|
|
|
if (is_bus) {
|
|
/* If hidbus is passed, delete all children. */
|
|
bus_generic_detach(bus);
|
|
device_delete_children(bus);
|
|
} else {
|
|
/*
|
|
* If hidbus child is passed, delete all hidbus children
|
|
* except caller. Deleting the caller may result in deadlock.
|
|
*/
|
|
error = device_get_children(bus, &children, &i);
|
|
if (error != 0)
|
|
return (error);
|
|
while (i-- > 0) {
|
|
if (children[i] == dev)
|
|
continue;
|
|
DPRINTF("Delete child. index=%d (%s)\n",
|
|
hidbus_get_index(children[i]),
|
|
device_get_nameunit(children[i]));
|
|
error = device_delete_child(bus, children[i]);
|
|
if (error) {
|
|
DPRINTF("Failed deleting %s\n",
|
|
device_get_nameunit(children[i]));
|
|
break;
|
|
}
|
|
}
|
|
free(children, M_TEMP);
|
|
}
|
|
|
|
HID_INTR_UNSETUP(device_get_parent(bus), bus);
|
|
|
|
return (error);
|
|
}
|
|
|
|
static int
|
|
hidbus_probe(device_t dev)
|
|
{
|
|
|
|
device_set_desc(dev, "HID bus");
|
|
|
|
/* Allow other subclasses to override this driver. */
|
|
return (BUS_PROBE_GENERIC);
|
|
}
|
|
|
|
static int
|
|
hidbus_attach(device_t dev)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(dev);
|
|
struct hid_device_info *devinfo = device_get_ivars(dev);
|
|
void *d_ptr = NULL;
|
|
hid_size_t d_len;
|
|
int error;
|
|
|
|
sc->dev = dev;
|
|
CK_STAILQ_INIT(&sc->tlcs);
|
|
mtx_init(&sc->mtx, "hidbus ivar lock", NULL, MTX_DEF);
|
|
sx_init(&sc->sx, "hidbus ivar list lock");
|
|
|
|
/*
|
|
* Ignore error. It is possible for non-HID device e.g. XBox360 gamepad
|
|
* to emulate HID through overloading of report descriptor.
|
|
*/
|
|
d_len = devinfo->rdescsize;
|
|
if (d_len != 0) {
|
|
d_ptr = malloc(d_len, M_DEVBUF, M_ZERO | M_WAITOK);
|
|
error = hid_get_rdesc(dev, d_ptr, d_len);
|
|
if (error != 0) {
|
|
free(d_ptr, M_DEVBUF);
|
|
d_len = 0;
|
|
d_ptr = NULL;
|
|
}
|
|
}
|
|
|
|
hidbus_fill_rdesc_info(&sc->rdesc, d_ptr, d_len);
|
|
|
|
sc->nowrite = hid_test_quirk(devinfo, HQ_NOWRITE);
|
|
|
|
error = hidbus_attach_children(dev);
|
|
if (error != 0) {
|
|
hidbus_detach(dev);
|
|
return (ENXIO);
|
|
}
|
|
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
hidbus_detach(device_t dev)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(dev);
|
|
|
|
hidbus_detach_children(dev);
|
|
sx_destroy(&sc->sx);
|
|
mtx_destroy(&sc->mtx);
|
|
free(sc->rdesc.data, M_DEVBUF);
|
|
|
|
return (0);
|
|
}
|
|
|
|
static void
|
|
hidbus_child_detached(device_t bus, device_t child)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(bus);
|
|
struct hidbus_ivars *tlc = device_get_ivars(child);
|
|
|
|
KASSERT(tlc->refcnt == 0, ("Child device is running"));
|
|
tlc->mtx = &sc->mtx;
|
|
tlc->intr_handler = NULL;
|
|
tlc->flags &= ~HIDBUS_FLAG_CAN_POLL;
|
|
}
|
|
|
|
/*
|
|
* Epoch callback indicating tlc is safe to destroy
|
|
*/
|
|
static void
|
|
hidbus_ivar_dtor(epoch_context_t ctx)
|
|
{
|
|
struct hidbus_ivars *tlc;
|
|
|
|
tlc = __containerof(ctx, struct hidbus_ivars, epoch_ctx);
|
|
free(tlc, M_DEVBUF);
|
|
}
|
|
|
|
static void
|
|
hidbus_child_deleted(device_t bus, device_t child)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(bus);
|
|
struct hidbus_ivars *tlc = device_get_ivars(child);
|
|
|
|
sx_xlock(&sc->sx);
|
|
KASSERT(tlc->refcnt == 0, ("Child device is running"));
|
|
CK_STAILQ_REMOVE(&sc->tlcs, tlc, hidbus_ivars, link);
|
|
sx_unlock(&sc->sx);
|
|
epoch_call(INPUT_EPOCH, hidbus_ivar_dtor, &tlc->epoch_ctx);
|
|
}
|
|
|
|
static int
|
|
hidbus_read_ivar(device_t bus, device_t child, int which, uintptr_t *result)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(bus);
|
|
struct hidbus_ivars *tlc = device_get_ivars(child);
|
|
|
|
switch (which) {
|
|
case HIDBUS_IVAR_INDEX:
|
|
*result = tlc->index;
|
|
break;
|
|
case HIDBUS_IVAR_USAGE:
|
|
*result = tlc->usage;
|
|
break;
|
|
case HIDBUS_IVAR_FLAGS:
|
|
*result = tlc->flags;
|
|
break;
|
|
case HIDBUS_IVAR_DRIVER_INFO:
|
|
*result = tlc->driver_info;
|
|
break;
|
|
case HIDBUS_IVAR_LOCK:
|
|
*result = (uintptr_t)(tlc->mtx == &sc->mtx ? NULL : tlc->mtx);
|
|
break;
|
|
default:
|
|
return (EINVAL);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
hidbus_write_ivar(device_t bus, device_t child, int which, uintptr_t value)
|
|
{
|
|
struct hidbus_softc *sc = device_get_softc(bus);
|
|
struct hidbus_ivars *tlc = device_get_ivars(child);
|
|
|
|
switch (which) {
|
|
case HIDBUS_IVAR_INDEX:
|
|
tlc->index = value;
|
|
break;
|
|
case HIDBUS_IVAR_USAGE:
|
|
tlc->usage = value;
|
|
break;
|
|
case HIDBUS_IVAR_FLAGS:
|
|
tlc->flags = value;
|
|
if ((value & HIDBUS_FLAG_CAN_POLL) != 0)
|
|
HID_INTR_SETUP(
|
|
device_get_parent(bus), bus, NULL, NULL, NULL);
|
|
break;
|
|
case HIDBUS_IVAR_DRIVER_INFO:
|
|
tlc->driver_info = value;
|
|
break;
|
|
case HIDBUS_IVAR_LOCK:
|
|
tlc->mtx = (struct mtx *)value == NULL ?
|
|
&sc->mtx : (struct mtx *)value;
|
|
break;
|
|
default:
|
|
return (EINVAL);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
/* Location hint for devctl(8) */
|
|
static int
|
|
hidbus_child_location(device_t bus, device_t child, struct sbuf *sb)
|
|
{
|
|
struct hidbus_ivars *tlc = device_get_ivars(child);
|
|
|
|
sbuf_printf(sb, "index=%hhu", tlc->index);
|
|
return (0);
|
|
}
|
|
|
|
/* PnP information for devctl(8) */
|
|
static int
|
|
hidbus_child_pnpinfo(device_t bus, device_t child, struct sbuf *sb)
|
|
{
|
|
struct hidbus_ivars *tlc = device_get_ivars(child);
|
|
struct hid_device_info *devinfo = device_get_ivars(bus);
|
|
|
|
sbuf_printf(sb, "page=0x%04x usage=0x%04x bus=0x%02hx "
|
|
"vendor=0x%04hx product=0x%04hx version=0x%04hx%s%s",
|
|
HID_GET_USAGE_PAGE(tlc->usage), HID_GET_USAGE(tlc->usage),
|
|
devinfo->idBus, devinfo->idVendor, devinfo->idProduct,
|
|
devinfo->idVersion, devinfo->idPnP[0] == '\0' ? "" : " _HID=",
|
|
devinfo->idPnP[0] == '\0' ? "" : devinfo->idPnP);
|
|
return (0);
|
|
}
|
|
|
|
void
|
|
hidbus_set_desc(device_t child, const char *suffix)
|
|
{
|
|
device_t bus = device_get_parent(child);
|
|
struct hidbus_softc *sc = device_get_softc(bus);
|
|
struct hid_device_info *devinfo = device_get_ivars(bus);
|
|
struct hidbus_ivars *tlc = device_get_ivars(child);
|
|
char buf[80];
|
|
|
|
/* Do not add NULL suffix or if device name already contains it. */
|
|
if (suffix != NULL && strcasestr(devinfo->name, suffix) == NULL &&
|
|
(sc->nauto > 1 || (tlc->flags & HIDBUS_FLAG_AUTOCHILD) == 0)) {
|
|
snprintf(buf, sizeof(buf), "%s %s", devinfo->name, suffix);
|
|
device_set_desc_copy(child, buf);
|
|
} else
|
|
device_set_desc(child, devinfo->name);
|
|
}
|
|
|
|
device_t
|
|
hidbus_find_child(device_t bus, int32_t usage)
|
|
{
|
|
device_t *children, child;
|
|
int ccount, i;
|
|
|
|
bus_topo_assert();
|
|
|
|
/* Get a list of all hidbus children */
|
|
if (device_get_children(bus, &children, &ccount) != 0)
|
|
return (NULL);
|
|
|
|
/* Scan through to find required TLC */
|
|
for (i = 0, child = NULL; i < ccount; i++) {
|
|
if (hidbus_get_usage(children[i]) == usage) {
|
|
child = children[i];
|
|
break;
|
|
}
|
|
}
|
|
free(children, M_TEMP);
|
|
|
|
return (child);
|
|
}
|
|
|
|
void
|
|
hidbus_intr(void *context, void *buf, hid_size_t len)
|
|
{
|
|
struct hidbus_softc *sc = context;
|
|
struct hidbus_ivars *tlc;
|
|
struct epoch_tracker et;
|
|
|
|
/*
|
|
* Broadcast input report to all subscribers.
|
|
* TODO: Add check for input report ID.
|
|
*
|
|
* Relock mutex on every TLC item as we can't hold any locks over whole
|
|
* TLC list here due to LOR with open()/close() handlers.
|
|
*/
|
|
if (!HID_IN_POLLING_MODE())
|
|
epoch_enter_preempt(INPUT_EPOCH, &et);
|
|
CK_STAILQ_FOREACH(tlc, &sc->tlcs, link) {
|
|
if (tlc->refcnt == 0 || tlc->intr_handler == NULL)
|
|
continue;
|
|
if (HID_IN_POLLING_MODE()) {
|
|
if ((tlc->flags & HIDBUS_FLAG_CAN_POLL) != 0)
|
|
tlc->intr_handler(tlc->intr_ctx, buf, len);
|
|
} else {
|
|
mtx_lock(tlc->mtx);
|
|
tlc->intr_handler(tlc->intr_ctx, buf, len);
|
|
mtx_unlock(tlc->mtx);
|
|
}
|
|
}
|
|
if (!HID_IN_POLLING_MODE())
|
|
epoch_exit_preempt(INPUT_EPOCH, &et);
|
|
}
|
|
|
|
void
|
|
hidbus_set_intr(device_t child, hid_intr_t *handler, void *context)
|
|
{
|
|
struct hidbus_ivars *tlc = device_get_ivars(child);
|
|
|
|
tlc->intr_handler = handler;
|
|
tlc->intr_ctx = context;
|
|
}
|
|
|
|
static int
|
|
hidbus_intr_start(device_t bus, device_t child)
|
|
{
|
|
MPASS(bus = device_get_parent(child));
|
|
struct hidbus_softc *sc = device_get_softc(bus);
|
|
struct hidbus_ivars *ivar = device_get_ivars(child);
|
|
struct hidbus_ivars *tlc;
|
|
bool refcnted = false;
|
|
int error;
|
|
|
|
if (sx_xlock_sig(&sc->sx) != 0)
|
|
return (EINTR);
|
|
CK_STAILQ_FOREACH(tlc, &sc->tlcs, link) {
|
|
refcnted |= (tlc->refcnt != 0);
|
|
if (tlc == ivar) {
|
|
mtx_lock(tlc->mtx);
|
|
++tlc->refcnt;
|
|
mtx_unlock(tlc->mtx);
|
|
}
|
|
}
|
|
error = refcnted ? 0 : hid_intr_start(bus);
|
|
sx_unlock(&sc->sx);
|
|
|
|
return (error);
|
|
}
|
|
|
|
static int
|
|
hidbus_intr_stop(device_t bus, device_t child)
|
|
{
|
|
MPASS(bus = device_get_parent(child));
|
|
struct hidbus_softc *sc = device_get_softc(bus);
|
|
struct hidbus_ivars *ivar = device_get_ivars(child);
|
|
struct hidbus_ivars *tlc;
|
|
bool refcnted = false;
|
|
int error;
|
|
|
|
if (sx_xlock_sig(&sc->sx) != 0)
|
|
return (EINTR);
|
|
CK_STAILQ_FOREACH(tlc, &sc->tlcs, link) {
|
|
if (tlc == ivar) {
|
|
mtx_lock(tlc->mtx);
|
|
MPASS(tlc->refcnt != 0);
|
|
--tlc->refcnt;
|
|
mtx_unlock(tlc->mtx);
|
|
}
|
|
refcnted |= (tlc->refcnt != 0);
|
|
}
|
|
error = refcnted ? 0 : hid_intr_stop(bus);
|
|
sx_unlock(&sc->sx);
|
|
|
|
return (error);
|
|
}
|
|
|
|
static void
|
|
hidbus_intr_poll(device_t bus, device_t child __unused)
|
|
{
|
|
hid_intr_poll(bus);
|
|
}
|
|
|
|
struct hid_rdesc_info *
|
|
hidbus_get_rdesc_info(device_t child)
|
|
{
|
|
device_t bus = device_get_parent(child);
|
|
struct hidbus_softc *sc = device_get_softc(bus);
|
|
|
|
return (&sc->rdesc);
|
|
}
|
|
|
|
/*
|
|
* HID interface.
|
|
*
|
|
* Hidbus as well as any hidbus child can be passed as first arg.
|
|
*/
|
|
|
|
/* Read cached report descriptor */
|
|
int
|
|
hid_get_report_descr(device_t dev, void **data, hid_size_t *len)
|
|
{
|
|
device_t bus;
|
|
struct hidbus_softc *sc;
|
|
|
|
bus = device_get_devclass(dev) == devclass_find("hidbus") ?
|
|
dev : device_get_parent(dev);
|
|
sc = device_get_softc(bus);
|
|
|
|
/*
|
|
* Do not send request to a transport backend.
|
|
* Use cached report descriptor instead of it.
|
|
*/
|
|
if (sc->rdesc.data == NULL || sc->rdesc.len == 0)
|
|
return (ENXIO);
|
|
|
|
if (data != NULL)
|
|
*data = sc->rdesc.data;
|
|
if (len != NULL)
|
|
*len = sc->rdesc.len;
|
|
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Replace cached report descriptor with top level driver provided one.
|
|
*
|
|
* It deletes all hidbus children except caller and enumerates them again after
|
|
* new descriptor has been registered. Currently it can not be called from
|
|
* autoenumerated (by report's TLC) child device context as it results in child
|
|
* duplication. To overcome this limitation hid_set_report_descr() should be
|
|
* called from device_identify driver's handler with hidbus itself passed as
|
|
* 'device_t dev' parameter.
|
|
*/
|
|
int
|
|
hid_set_report_descr(device_t dev, const void *data, hid_size_t len)
|
|
{
|
|
struct hid_rdesc_info rdesc;
|
|
device_t bus;
|
|
struct hidbus_softc *sc;
|
|
bool is_bus;
|
|
int error;
|
|
|
|
bus_topo_assert();
|
|
|
|
is_bus = device_get_devclass(dev) == devclass_find("hidbus");
|
|
bus = is_bus ? dev : device_get_parent(dev);
|
|
sc = device_get_softc(bus);
|
|
|
|
/*
|
|
* Do not overload already overloaded report descriptor in
|
|
* device_identify handler. It causes infinite recursion loop.
|
|
*/
|
|
if (is_bus && sc->overloaded)
|
|
return(0);
|
|
|
|
DPRINTFN(5, "len=%d\n", len);
|
|
DPRINTFN(5, "data = %*D\n", len, data, " ");
|
|
|
|
error = hidbus_fill_rdesc_info(&rdesc, data, len);
|
|
if (error != 0)
|
|
return (error);
|
|
|
|
error = hidbus_detach_children(dev);
|
|
if (error != 0)
|
|
return(error);
|
|
|
|
/* Make private copy to handle a case of dynamicaly allocated data. */
|
|
rdesc.data = malloc(len, M_DEVBUF, M_ZERO | M_WAITOK);
|
|
bcopy(data, rdesc.data, len);
|
|
sc->overloaded = true;
|
|
free(sc->rdesc.data, M_DEVBUF);
|
|
bcopy(&rdesc, &sc->rdesc, sizeof(struct hid_rdesc_info));
|
|
|
|
error = hidbus_attach_children(bus);
|
|
|
|
return (error);
|
|
}
|
|
|
|
static int
|
|
hidbus_get_rdesc(device_t dev, device_t child __unused, void *data,
|
|
hid_size_t len)
|
|
{
|
|
return (hid_get_rdesc(dev, data, len));
|
|
}
|
|
|
|
static int
|
|
hidbus_read(device_t dev, device_t child __unused, void *data,
|
|
hid_size_t maxlen, hid_size_t *actlen)
|
|
{
|
|
return (hid_read(dev, data, maxlen, actlen));
|
|
}
|
|
|
|
static int
|
|
hidbus_write(device_t dev, device_t child __unused, const void *data,
|
|
hid_size_t len)
|
|
{
|
|
struct hidbus_softc *sc;
|
|
uint8_t id;
|
|
|
|
sc = device_get_softc(dev);
|
|
/*
|
|
* Output interrupt endpoint is often optional. If HID device
|
|
* does not provide it, send reports via control pipe.
|
|
*/
|
|
if (sc->nowrite) {
|
|
/* try to extract the ID byte */
|
|
id = (sc->rdesc.oid & (len > 0)) ? *(const uint8_t*)data : 0;
|
|
return (hid_set_report(dev, data, len, HID_OUTPUT_REPORT, id));
|
|
}
|
|
|
|
return (hid_write(dev, data, len));
|
|
}
|
|
|
|
static int
|
|
hidbus_get_report(device_t dev, device_t child __unused, void *data,
|
|
hid_size_t maxlen, hid_size_t *actlen, uint8_t type, uint8_t id)
|
|
{
|
|
return (hid_get_report(dev, data, maxlen, actlen, type, id));
|
|
}
|
|
|
|
static int
|
|
hidbus_set_report(device_t dev, device_t child __unused, const void *data,
|
|
hid_size_t len, uint8_t type, uint8_t id)
|
|
{
|
|
return (hid_set_report(dev, data, len, type, id));
|
|
}
|
|
|
|
static int
|
|
hidbus_set_idle(device_t dev, device_t child __unused, uint16_t duration,
|
|
uint8_t id)
|
|
{
|
|
return (hid_set_idle(dev, duration, id));
|
|
}
|
|
|
|
static int
|
|
hidbus_set_protocol(device_t dev, device_t child __unused, uint16_t protocol)
|
|
{
|
|
return (hid_set_protocol(dev, protocol));
|
|
}
|
|
|
|
static int
|
|
hidbus_ioctl(device_t dev, device_t child __unused, unsigned long cmd,
|
|
uintptr_t data)
|
|
{
|
|
return (hid_ioctl(dev, cmd, data));
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*
|
|
* hidbus_lookup_id
|
|
*
|
|
* This functions takes an array of "struct hid_device_id" and tries
|
|
* to match the entries with the information in "struct hid_device_info".
|
|
*
|
|
* Return values:
|
|
* NULL: No match found.
|
|
* Else: Pointer to matching entry.
|
|
*------------------------------------------------------------------------*/
|
|
const struct hid_device_id *
|
|
hidbus_lookup_id(device_t dev, const struct hid_device_id *id, int nitems_id)
|
|
{
|
|
const struct hid_device_id *id_end;
|
|
const struct hid_device_info *info;
|
|
int32_t usage;
|
|
bool is_child;
|
|
|
|
if (id == NULL) {
|
|
goto done;
|
|
}
|
|
|
|
id_end = id + nitems_id;
|
|
info = hid_get_device_info(dev);
|
|
is_child = device_get_devclass(dev) != devclass_find("hidbus");
|
|
if (is_child)
|
|
usage = hidbus_get_usage(dev);
|
|
|
|
/*
|
|
* Keep on matching array entries until we find a match or
|
|
* until we reach the end of the matching array:
|
|
*/
|
|
for (; id != id_end; id++) {
|
|
|
|
if (is_child && (id->match_flag_page) &&
|
|
(id->page != HID_GET_USAGE_PAGE(usage))) {
|
|
continue;
|
|
}
|
|
if (is_child && (id->match_flag_usage) &&
|
|
(id->usage != HID_GET_USAGE(usage))) {
|
|
continue;
|
|
}
|
|
if ((id->match_flag_bus) &&
|
|
(id->idBus != info->idBus)) {
|
|
continue;
|
|
}
|
|
if ((id->match_flag_vendor) &&
|
|
(id->idVendor != info->idVendor)) {
|
|
continue;
|
|
}
|
|
if ((id->match_flag_product) &&
|
|
(id->idProduct != info->idProduct)) {
|
|
continue;
|
|
}
|
|
if ((id->match_flag_ver_lo) &&
|
|
(id->idVersion_lo > info->idVersion)) {
|
|
continue;
|
|
}
|
|
if ((id->match_flag_ver_hi) &&
|
|
(id->idVersion_hi < info->idVersion)) {
|
|
continue;
|
|
}
|
|
if (id->match_flag_pnp &&
|
|
strncmp(id->idPnP, info->idPnP, HID_PNP_ID_SIZE) != 0) {
|
|
continue;
|
|
}
|
|
/* We found a match! */
|
|
return (id);
|
|
}
|
|
|
|
done:
|
|
return (NULL);
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*
|
|
* hidbus_lookup_driver_info - factored out code
|
|
*
|
|
* Return values:
|
|
* 0: Success
|
|
* Else: Failure
|
|
*------------------------------------------------------------------------*/
|
|
int
|
|
hidbus_lookup_driver_info(device_t child, const struct hid_device_id *id,
|
|
int nitems_id)
|
|
{
|
|
|
|
id = hidbus_lookup_id(child, id, nitems_id);
|
|
if (id) {
|
|
/* copy driver info */
|
|
hidbus_set_driver_info(child, id->driver_info);
|
|
return (0);
|
|
}
|
|
return (ENXIO);
|
|
}
|
|
|
|
const struct hid_device_info *
|
|
hid_get_device_info(device_t dev)
|
|
{
|
|
device_t bus;
|
|
|
|
bus = device_get_devclass(dev) == devclass_find("hidbus") ?
|
|
dev : device_get_parent(dev);
|
|
|
|
return (device_get_ivars(bus));
|
|
}
|
|
|
|
static device_method_t hidbus_methods[] = {
|
|
/* device interface */
|
|
DEVMETHOD(device_probe, hidbus_probe),
|
|
DEVMETHOD(device_attach, hidbus_attach),
|
|
DEVMETHOD(device_detach, hidbus_detach),
|
|
DEVMETHOD(device_suspend, bus_generic_suspend),
|
|
DEVMETHOD(device_resume, bus_generic_resume),
|
|
|
|
/* bus interface */
|
|
DEVMETHOD(bus_add_child, hidbus_add_child),
|
|
DEVMETHOD(bus_child_detached, hidbus_child_detached),
|
|
DEVMETHOD(bus_child_deleted, hidbus_child_deleted),
|
|
DEVMETHOD(bus_read_ivar, hidbus_read_ivar),
|
|
DEVMETHOD(bus_write_ivar, hidbus_write_ivar),
|
|
DEVMETHOD(bus_child_pnpinfo, hidbus_child_pnpinfo),
|
|
DEVMETHOD(bus_child_location, hidbus_child_location),
|
|
|
|
/* hid interface */
|
|
DEVMETHOD(hid_intr_start, hidbus_intr_start),
|
|
DEVMETHOD(hid_intr_stop, hidbus_intr_stop),
|
|
DEVMETHOD(hid_intr_poll, hidbus_intr_poll),
|
|
DEVMETHOD(hid_get_rdesc, hidbus_get_rdesc),
|
|
DEVMETHOD(hid_read, hidbus_read),
|
|
DEVMETHOD(hid_write, hidbus_write),
|
|
DEVMETHOD(hid_get_report, hidbus_get_report),
|
|
DEVMETHOD(hid_set_report, hidbus_set_report),
|
|
DEVMETHOD(hid_set_idle, hidbus_set_idle),
|
|
DEVMETHOD(hid_set_protocol, hidbus_set_protocol),
|
|
DEVMETHOD(hid_ioctl, hidbus_ioctl),
|
|
|
|
DEVMETHOD_END
|
|
};
|
|
|
|
driver_t hidbus_driver = {
|
|
"hidbus",
|
|
hidbus_methods,
|
|
sizeof(struct hidbus_softc),
|
|
};
|
|
|
|
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);
|