diff --git a/share/man/man4/Makefile b/share/man/man4/Makefile --- a/share/man/man4/Makefile +++ b/share/man/man4/Makefile @@ -179,6 +179,7 @@ gpioths.4 \ gre.4 \ h_ertt.4 \ + hidbus.4 \ hifn.4 \ hpet.4 \ ${_hpt27xx.4} \ diff --git a/share/man/man4/hidbus.4 b/share/man/man4/hidbus.4 new file mode 100644 --- /dev/null +++ b/share/man/man4/hidbus.4 @@ -0,0 +1,102 @@ +.\" Copyright (c) 2020 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. +.\" +.\" $FreeBSD$ +.\" +.Dd September 14, 2020 +.Dt HIDBUS 4 +.Os +.Sh NAME +.Nm hidbus +.Nd generic HID bus 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 hidbus" +.Cd "device hid" +.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 +hidbus_load="YES" +.Ed +.Sh DESCRIPTION +The +.Nm +driver provides support for multiple HID driver attachments to single HID +transport backend. +See +.Xr iichid 4 +or +.Xr usbhid 4 . +.Pp +Each HID device can have several components, e.g., a keyboard and +a mouse. +These components use different report identifiers (a byte) combined into +groups called collections to distinguish which one data is coming from. +The +.Nm +driver has other drivers attached that handle particular +kinds of devices and +.Nm +broadcasts data to all of them. +.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.hidbus.debug +Debug output level, where 0 is debugging disabled and larger values increase +debug message verbosity. +Default is 0. +.El +.Sh SEE ALSO +.Xr hconf 4 , +.Xr hcons 4 , +.Xr hgame 4 , +.Xr hidraw 4 , +.Xr hkbd 4 , +.Xr hms 4 , +.Xr hmt 4 , +.Xr hpen 4 , +.Xr hsctrl 4 , +.Xr hskbd 4 , +.Xr iichid 4 , +.Xr usbhid 4 +.Sh HISTORY +The +.Nm +driver first appeared in +.Fx 13.0. +.Sh AUTHORS +.An -nosplit +The +.Nm +driver was written by +.An Vladimir Kondratyev Aq Mt wulf@FreeBSD.org . diff --git a/sys/conf/files b/sys/conf/files --- a/sys/conf/files +++ b/sys/conf/files @@ -1817,6 +1817,7 @@ dev/gpio/ofw_gpiobus.c optional fdt gpio dev/hid/hid.c optional hid dev/hid/hid_if.m optional hid +dev/hid/hidbus.c optional hidbus dev/hifn/hifn7751.c optional hifn dev/hptiop/hptiop.c optional hptiop scbus dev/hwpmc/hwpmc_logging.c optional hwpmc diff --git a/sys/dev/hid/hidbus.h b/sys/dev/hid/hidbus.h new file mode 100644 --- /dev/null +++ b/sys/dev/hid/hidbus.h @@ -0,0 +1,176 @@ +/*- + * Copyright (c) 2019 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 _HID_HIDBUS_H_ +#define _HID_HIDBUS_H_ + +enum { + HIDBUS_IVAR_USAGE, + HIDBUS_IVAR_INDEX, + HIDBUS_IVAR_FLAGS, +#define HIDBUS_FLAG_AUTOCHILD (0<<1) /* Child is autodiscovered */ +#define HIDBUS_FLAG_CAN_POLL (1<<1) /* Child can work during panic */ + HIDBUS_IVAR_DRIVER_INFO, + HIDBUS_IVAR_LOCK, +}; + +#define HIDBUS_ACCESSOR(A, B, T) \ + __BUS_ACCESSOR(hidbus, A, HIDBUS, B, T) + +HIDBUS_ACCESSOR(usage, USAGE, int32_t) +HIDBUS_ACCESSOR(index, INDEX, uint8_t) +HIDBUS_ACCESSOR(flags, FLAGS, uint32_t) +HIDBUS_ACCESSOR(driver_info, DRIVER_INFO, uintptr_t) +HIDBUS_ACCESSOR(lock, LOCK, struct mtx *) + +/* + * The following structure is used when looking up an HID driver for + * an HID device. It is inspired by the structure called "usb_device_id". + * which is originated in Linux and ported to FreeBSD. + */ +struct hid_device_id { + + /* Select which fields to match against */ +#if BYTE_ORDER == LITTLE_ENDIAN + uint16_t + match_flag_page:1, + match_flag_usage:1, + match_flag_bus:1, + match_flag_vendor:1, + match_flag_product:1, + match_flag_ver_lo:1, + match_flag_ver_hi:1, + match_flag_pnp:1, + match_flag_unused:8; +#else + uint16_t + match_flag_unused:8, + match_flag_pnp:1, + match_flag_ver_hi:1, + match_flag_ver_lo:1, + match_flag_product:1, + match_flag_vendor:1, + match_flag_bus:1, + match_flag_usage:1, + match_flag_page:1; +#endif + + /* Used for top level collection usage matches */ + uint16_t page; + uint16_t usage; + + /* Used for product specific matches; the Version range is inclusive */ + uint8_t idBus; + uint16_t idVendor; + uint16_t idProduct; + uint16_t idVersion_lo; + uint16_t idVersion_hi; + char *idPnP; + + /* Hook for driver specific information */ + uintptr_t driver_info; +}; + +#define HID_STD_PNP_INFO \ + "M16:mask;U16:page;U16:usage;U8:bus;U16:vendor;U16:product;" \ + "L16:version;G16:version;Z:_HID" +#define HID_PNP_INFO(table) \ + MODULE_PNP_INFO(HID_STD_PNP_INFO, hidbus, table, table, nitems(table)) + +#define HID_TLC(pg,usg) \ + .match_flag_page = 1, .match_flag_usage = 1, .page = (pg), .usage = (usg) + +#define HID_BUS(bus) \ + .match_flag_bus = 1, .idBus = (bus) + +#define HID_VENDOR(vend) \ + .match_flag_vendor = 1, .idVendor = (vend) + +#define HID_PRODUCT(prod) \ + .match_flag_product = 1, .idProduct = (prod) + +#define HID_VP(vend,prod) \ + HID_VENDOR(vend), HID_PRODUCT(prod) + +#define HID_BVP(bus,vend,prod) \ + HID_BUS(bus), HID_VENDOR(vend), HID_PRODUCT(prod) + +#define HID_BVPI(bus,vend,prod,info) \ + HID_BUS(bus), HID_VENDOR(vend), HID_PRODUCT(prod), HID_DRIVER_INFO(info) + +#define HID_VERSION_GTEQ(lo) /* greater than or equal */ \ + .match_flag_ver_lo = 1, .idVersion_lo = (lo) + +#define HID_VERSION_LTEQ(hi) /* less than or equal */ \ + .match_flag_ver_hi = 1, .idVersion_hi = (hi) + +#define HID_PNP(pnp) \ + .match_flag_pnp = 1, .idPnP = (pnp) + +#define HID_DRIVER_INFO(n) \ + .driver_info = (n) + +#define HID_GET_DRIVER_INFO(did) \ + (did)->driver_info + +#define HIDBUS_LOOKUP_ID(d, h) hidbus_lookup_id((d), (h), nitems(h)) +#define HIDBUS_LOOKUP_DRIVER_INFO(d, h) \ + hidbus_lookup_driver_info((d), (h), nitems(h)) + +/* + * Walk through all HID items hi belonging Top Level Collection #tlc_index + */ +#define HIDBUS_FOREACH_ITEM(hd, hi, tlc_index) \ + for (uint8_t _iter = 0; \ + _iter <= (tlc_index) && hid_get_item((hd), (hi)); \ + _iter += (hi)->kind == hid_endcollection && (hi)->collevel == 0) \ + if (_iter == (tlc_index)) + +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); + +const struct hid_device_id *hidbus_lookup_id(device_t, + const struct hid_device_id *, int); +struct hid_rdesc_info *hidbus_get_rdesc_info(device_t); +int hidbus_lookup_driver_info(device_t, + const struct hid_device_id *, int); +void hidbus_set_intr(device_t, hid_intr_t*, void *); +int hidbus_intr_start(device_t); +int hidbus_intr_stop(device_t); +void hidbus_intr_poll(device_t); +void hidbus_set_desc(device_t, const char *); +device_t hidbus_find_child(device_t, int32_t); + +/* hidbus HID interface */ +int hid_get_report_descr(device_t, void **, hid_size_t *); +int hid_set_report_descr(device_t, const void *, hid_size_t); + +const struct hid_device_info *hid_get_device_info(device_t); + +extern devclass_t hidbus_devclass; + +#endif /* _HID_HIDBUS_H_ */ diff --git a/sys/dev/hid/hidbus.c b/sys/dev/hid/hidbus.c new file mode 100644 --- /dev/null +++ b/sys/dev/hid/hidbus.c @@ -0,0 +1,905 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2019-2020 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 +__FBSDID("$FreeBSD$"); + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HID_DEBUG_VAR hid_debug +#include +#include +#include + +#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); +} + +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), 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) == hidbus_devclass; + bus = is_bus ? dev : device_get_parent(dev); + + KASSERT(device_get_devclass(bus) == hidbus_devclass, + ("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)); + + 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; + 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_str(device_t bus, device_t child, char *buf, + size_t buflen) +{ + struct hidbus_ivars *tlc = device_get_ivars(child); + + snprintf(buf, buflen, "index=%hhu", tlc->index); + return (0); +} + +/* PnP information for devctl(8) */ +static int +hidbus_child_pnpinfo_str(device_t bus, device_t child, char *buf, + size_t buflen) +{ + struct hidbus_ivars *tlc = device_get_ivars(child); + struct hid_device_info *devinfo = device_get_ivars(bus); + + snprintf(buf, buflen, "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; + + GIANT_REQUIRED; + + /* 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; +} + +int +hidbus_intr_start(device_t child) +{ + device_t 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; + int refcnt = 0; + int error; + + if (sx_xlock_sig(&sc->sx) != 0) + return (EINTR); + CK_STAILQ_FOREACH(tlc, &sc->tlcs, link) { + refcnt += tlc->refcnt; + if (tlc == ivar) { + mtx_lock(tlc->mtx); + ++tlc->refcnt; + mtx_unlock(tlc->mtx); + } + } + error = refcnt != 0 ? 0 : HID_INTR_START(device_get_parent(bus)); + sx_unlock(&sc->sx); + + return (error); +} + +int +hidbus_intr_stop(device_t child) +{ + device_t 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 refcnt = 0; + 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); + } + refcnt += tlc->refcnt; + } + error = refcnt != 0 ? 0 : HID_INTR_STOP(device_get_parent(bus)); + sx_unlock(&sc->sx); + + return (error); +} + +void +hidbus_intr_poll(device_t child) +{ + device_t bus = device_get_parent(child); + + HID_INTR_POLL(device_get_parent(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) == hidbus_devclass ? + 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; + + GIANT_REQUIRED; + + is_bus = device_get_devclass(dev) == hidbus_devclass; + 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_write(device_t dev, 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)); +} + +/*------------------------------------------------------------------------* + * 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) != hidbus_devclass; + 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) == hidbus_devclass ? + 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_str,hidbus_child_pnpinfo_str), + DEVMETHOD(bus_child_location_str,hidbus_child_location_str), + + /* hid interface */ + DEVMETHOD(hid_get_rdesc, hid_get_rdesc), + DEVMETHOD(hid_read, hid_read), + DEVMETHOD(hid_write, hidbus_write), + DEVMETHOD(hid_get_report, hid_get_report), + DEVMETHOD(hid_set_report, hid_set_report), + DEVMETHOD(hid_set_idle, hid_set_idle), + DEVMETHOD(hid_set_protocol, hid_set_protocol), + + DEVMETHOD_END +}; + +devclass_t hidbus_devclass; +driver_t hidbus_driver = { + "hidbus", + hidbus_methods, + sizeof(struct hidbus_softc), +}; + +MODULE_DEPEND(hidbus, hid, 1, 1, 1); +MODULE_VERSION(hidbus, 1); diff --git a/sys/modules/hid/Makefile b/sys/modules/hid/Makefile --- a/sys/modules/hid/Makefile +++ b/sys/modules/hid/Makefile @@ -1,6 +1,7 @@ # $FreeBSD$ SUBDIR = \ - hid + hid \ + hidbus .include diff --git a/sys/modules/hid/hidbus/Makefile b/sys/modules/hid/hidbus/Makefile new file mode 100644 --- /dev/null +++ b/sys/modules/hid/hidbus/Makefile @@ -0,0 +1,9 @@ +# $FreeBSD$ + +.PATH: ${SRCTOP}/sys/dev/hid + +KMOD= hidbus +SRCS= hidbus.c +SRCS+= bus_if.h device_if.h hid_if.h + +.include