root/sys/dev/gpio/gpioc.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2009 Oleksandr Tymoshenko <gonzo@freebsd.org>
 * All rights reserved.
 *
 * 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/param.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/conf.h>
#include <sys/gpio.h>
#include <sys/ioccom.h>
#include <sys/filio.h>
#include <sys/fcntl.h>
#include <sys/sigio.h>
#include <sys/signalvar.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/uio.h>
#include <sys/poll.h>
#include <sys/selinfo.h>
#include <sys/module.h>

#include <dev/gpio/gpiobusvar.h>

#include "gpiobus_if.h"

#undef GPIOC_DEBUG
#ifdef GPIOC_DEBUG
#define dprintf printf
#define ddevice_printf device_printf
#else
#define dprintf(x, arg...)
#define ddevice_printf(dev, x, arg...)
#endif

struct gpioc_softc {
        device_t                sc_dev;         /* gpiocX dev */
        device_t                sc_pdev;        /* gpiobusX dev */
        struct cdev             *sc_ctl_dev;    /* controller device */
        int                     sc_unit;
        int                     sc_npins;
        struct gpioc_pin_intr   *sc_pin_intr;
};

struct gpioc_pin_intr {
        struct gpioc_softc                              *sc;
        gpio_pin_t                                      pin;
        uint32_t                                        intr_mode;
        bool                                            config_locked;
        int                                             intr_rid;
        struct resource                                 *intr_res;
        void                                            *intr_cookie;
        struct mtx                                      mtx;
        SLIST_HEAD(gpioc_privs_list, gpioc_privs)       privs;
};


struct gpioc_cdevpriv {
        struct gpioc_softc                      *sc;
        struct selinfo                          selinfo;
        bool                                    async;
        uint8_t                                 report_option;
        struct sigio                            *sigio;
        struct mtx                              mtx;
        struct gpioc_pin_event                  *events;
        int                                     numevents;
        int                                     evidx_head;
        int                                     evidx_tail;
        SLIST_HEAD(gpioc_pins_list, gpioc_pins) pins;
};

struct gpioc_privs {
        struct gpioc_cdevpriv           *priv;
        SLIST_ENTRY(gpioc_privs)        next;
};

struct gpioc_pins {
        struct gpioc_pin_intr   *pin;
        int                     eventcount;
        int                     firstevent;
        SLIST_ENTRY(gpioc_pins) next;
};

struct gpioc_pin_event {
        struct gpioc_pins       *privpin;
        sbintime_t              event_time;
        bool                    event_pin_state;
};

static MALLOC_DEFINE(M_GPIOC, "gpioc", "gpioc device data");

static int      gpioc_allocate_pin_intr(struct gpioc_softc*,
                    struct gpioc_pin_intr*, uint32_t, uint32_t);
static int      gpioc_release_pin_intr(struct gpioc_softc*,
                    struct gpioc_pin_intr*);
static int      gpioc_attach_priv_pin(struct gpioc_cdevpriv*,
                    struct gpioc_pin_intr*);
static int      gpioc_detach_priv_pin(struct gpioc_cdevpriv*,
                    struct gpioc_pin_intr*);
static bool     gpioc_intr_reconfig_allowed(struct gpioc_cdevpriv*,
                    struct gpioc_pin_intr *intr_conf);
static uint32_t gpioc_get_intr_config(struct gpioc_softc*,
                    struct gpioc_cdevpriv*, uint32_t pin);
static int      gpioc_set_intr_config(struct gpioc_softc*,
                    struct gpioc_cdevpriv*, uint32_t, uint32_t);
static void     gpioc_interrupt_handler(void*);

static int      gpioc_kqread(struct knote*, long);
static void     gpioc_kqdetach(struct knote*);

static int      gpioc_probe(device_t dev);
static int      gpioc_attach(device_t dev);
static int      gpioc_detach(device_t dev);

static void     gpioc_cdevpriv_dtor(void*);

static d_open_t         gpioc_open;
static d_read_t         gpioc_read;
static d_ioctl_t        gpioc_ioctl;
static d_poll_t         gpioc_poll;
static d_kqfilter_t     gpioc_kqfilter;

static struct cdevsw gpioc_cdevsw = {
        .d_version      = D_VERSION,
        .d_open         = gpioc_open,
        .d_read         = gpioc_read,
        .d_ioctl        = gpioc_ioctl,
        .d_poll         = gpioc_poll,
        .d_kqfilter     = gpioc_kqfilter,
        .d_name         = "gpioc",
};

static const struct filterops gpioc_read_filterops = {
        .f_isfd =       true,
        .f_attach =     NULL,
        .f_detach =     gpioc_kqdetach,
        .f_event =      gpioc_kqread,
        .f_touch =      NULL,
        .f_copy =       knote_triv_copy,
};

static struct gpioc_pin_event *
next_head_event(struct gpioc_cdevpriv *priv)
{
        struct gpioc_pin_event *rv;

        rv = &priv->events[priv->evidx_head++];
        if (priv->evidx_head == priv->numevents)
                priv->evidx_head = 0;
        return (rv);
}

static struct gpioc_pin_event *
next_tail_event(struct gpioc_cdevpriv *priv)
{
        struct gpioc_pin_event *rv;

        rv = &priv->events[priv->evidx_tail++];
        if (priv->evidx_tail == priv->numevents)
                priv->evidx_tail = 0;
        return (rv);
}

static size_t
number_of_events(struct gpioc_cdevpriv *priv)
{
        if (priv->evidx_head >= priv->evidx_tail)
                return (priv->evidx_head - priv->evidx_tail);
        else
                return (priv->numevents + priv->evidx_head - priv->evidx_tail);
}

static int
gpioc_allocate_pin_intr(struct gpioc_softc *sc,
    struct gpioc_pin_intr *intr_conf, uint32_t pin, uint32_t flags)
{
        int err;

        intr_conf->config_locked = true;
        mtx_unlock(&intr_conf->mtx);

        MPASS(intr_conf->pin == NULL);
        err = gpio_pin_get_by_bus_pinnum(sc->sc_pdev, pin, &intr_conf->pin);
        if (err != 0)
                goto error_exit;

        intr_conf->intr_res = gpio_alloc_intr_resource(sc->sc_dev,
            intr_conf->intr_rid, RF_ACTIVE, intr_conf->pin, flags);
        if (intr_conf->intr_res == NULL) {
                err = ENXIO;
                goto error_pin;
        }

        err = bus_setup_intr(sc->sc_dev, intr_conf->intr_res,
            INTR_TYPE_MISC | INTR_MPSAFE, NULL, gpioc_interrupt_handler,
            intr_conf, &intr_conf->intr_cookie);
        if (err != 0) {
                bus_release_resource(sc->sc_dev, intr_conf->intr_res);
                intr_conf->intr_res = NULL;
                goto error_pin;
        }

        intr_conf->intr_mode = flags;

error_exit:
        mtx_lock(&intr_conf->mtx);
        intr_conf->config_locked = false;
        wakeup(&intr_conf->config_locked);

        return (err);

error_pin:
        gpio_pin_release(intr_conf->pin);
        intr_conf->pin = NULL;
        goto error_exit;
}

static int
gpioc_release_pin_intr(struct gpioc_softc *sc, struct gpioc_pin_intr *intr_conf)
{
        int err;

        intr_conf->config_locked = true;
        mtx_unlock(&intr_conf->mtx);

        if (intr_conf->intr_cookie != NULL) {
                err = bus_teardown_intr(sc->sc_dev, intr_conf->intr_res,
                    intr_conf->intr_cookie);
                if (err != 0)
                        goto error_exit;
                else
                        intr_conf->intr_cookie = NULL;
        }

        if (intr_conf->intr_res != NULL) {
                err = bus_release_resource(sc->sc_dev, SYS_RES_IRQ,
                    intr_conf->intr_rid, intr_conf->intr_res);
                if (err != 0)
                        goto error_exit;
                else {
                        intr_conf->intr_rid = 0;
                        intr_conf->intr_res = NULL;
                }
        }

        gpio_pin_release(intr_conf->pin);
        intr_conf->pin = NULL;

        intr_conf->intr_mode = 0;
        err = 0;

error_exit:
        mtx_lock(&intr_conf->mtx);
        intr_conf->config_locked = false;
        wakeup(&intr_conf->config_locked);

        return (err);
}

static int
gpioc_attach_priv_pin(struct gpioc_cdevpriv *priv,
    struct gpioc_pin_intr *intr_conf)
{
        struct gpioc_privs      *priv_link;
        struct gpioc_pins       *pin_link;
        unsigned int            consistency_a __diagused;
        unsigned int            consistency_b __diagused;

        consistency_a = 0;
        consistency_b = 0;
        mtx_assert(&intr_conf->mtx, MA_OWNED);
        mtx_lock(&priv->mtx);
        SLIST_FOREACH(priv_link, &intr_conf->privs, next) {
                if (priv_link->priv == priv)
                        consistency_a++;
        }
        KASSERT(consistency_a <= 1,
            ("inconsistent links between pin config and cdevpriv"));
        SLIST_FOREACH(pin_link, &priv->pins, next) {
                if (pin_link->pin == intr_conf)
                        consistency_b++;
        }
        KASSERT(consistency_a == consistency_b,
            ("inconsistent links between pin config and cdevpriv"));
        if (consistency_a == 1 && consistency_b == 1) {
                mtx_unlock(&priv->mtx);
                return (EEXIST);
        }
        priv_link = malloc(sizeof(struct gpioc_privs), M_GPIOC,
            M_NOWAIT | M_ZERO);
        if (priv_link == NULL)
        {
                mtx_unlock(&priv->mtx);
                return (ENOMEM);
        }
        pin_link = malloc(sizeof(struct gpioc_pins), M_GPIOC,
            M_NOWAIT | M_ZERO);
        if (pin_link == NULL) {
                mtx_unlock(&priv->mtx);
                return (ENOMEM);
        }
        priv_link->priv = priv;
        pin_link->pin = intr_conf;
        SLIST_INSERT_HEAD(&intr_conf->privs, priv_link, next);
        SLIST_INSERT_HEAD(&priv->pins, pin_link, next);
        mtx_unlock(&priv->mtx);

        return (0);
}

static int
gpioc_detach_priv_pin(struct gpioc_cdevpriv *priv,
    struct gpioc_pin_intr *intr_conf)
{
        struct gpioc_privs      *priv_link, *priv_link_temp;
        struct gpioc_pins       *pin_link, *pin_link_temp;
        unsigned int            consistency_a __diagused;
        unsigned int            consistency_b __diagused;

        consistency_a = 0;
        consistency_b = 0;
        mtx_assert(&intr_conf->mtx, MA_OWNED);
        mtx_lock(&priv->mtx);
        SLIST_FOREACH_SAFE(priv_link, &intr_conf->privs, next, priv_link_temp) {
                if (priv_link->priv == priv) {
                        SLIST_REMOVE(&intr_conf->privs, priv_link, gpioc_privs,
                            next);
                        free(priv_link, M_GPIOC);
                        consistency_a++;
                }
        }
        KASSERT(consistency_a <= 1,
            ("inconsistent links between pin config and cdevpriv"));
        SLIST_FOREACH_SAFE(pin_link, &priv->pins, next, pin_link_temp) {
                if (pin_link->pin == intr_conf) {
                        /*
                         * If the pin we're removing has events in the priv's
                         * event fifo, we can't leave dangling pointers from
                         * those events to the gpioc_pins struct we're about to
                         * free.  We also can't remove random items and leave
                         * holes in the events fifo, so just empty it out.
                         */
                        if (pin_link->eventcount > 0) {
                                priv->evidx_head = priv->evidx_tail = 0;
                        }
                        SLIST_REMOVE(&priv->pins, pin_link, gpioc_pins, next);
                        free(pin_link, M_GPIOC);
                        consistency_b++;
                }
        }
        KASSERT(consistency_a == consistency_b,
            ("inconsistent links between pin config and cdevpriv"));
        mtx_unlock(&priv->mtx);

        return (0);
}

static bool
gpioc_intr_reconfig_allowed(struct gpioc_cdevpriv *priv,
    struct gpioc_pin_intr *intr_conf)
{
        struct gpioc_privs      *priv_link;

        mtx_assert(&intr_conf->mtx, MA_OWNED);

        if (SLIST_EMPTY(&intr_conf->privs))
                return (true);

        SLIST_FOREACH(priv_link, &intr_conf->privs, next) {
                if (priv_link->priv != priv)
                        return (false);
        }

        return (true);
}


static uint32_t
gpioc_get_intr_config(struct gpioc_softc *sc, struct gpioc_cdevpriv *priv,
    uint32_t pin)
{
        struct gpioc_pin_intr   *intr_conf = &sc->sc_pin_intr[pin];
        struct gpioc_privs      *priv_link;
        uint32_t                flags;

        flags = intr_conf->intr_mode;

        if (flags == 0)
                return (0);

        mtx_lock(&intr_conf->mtx);
        SLIST_FOREACH(priv_link, &intr_conf->privs, next) {
                if (priv_link->priv == priv) {
                        flags |= GPIO_INTR_ATTACHED;
                        break;
                }
        }
        mtx_unlock(&intr_conf->mtx);

        return (flags);
}

static int
gpioc_set_intr_config(struct gpioc_softc *sc, struct gpioc_cdevpriv *priv,
    uint32_t pin, uint32_t flags)
{
        struct gpioc_pin_intr *intr_conf = &sc->sc_pin_intr[pin];
        int res;

        res = 0;
        if (intr_conf->intr_mode == 0 && flags == 0) {
                /* No interrupt configured and none requested: Do nothing. */
                return (0);
        }
        mtx_lock(&intr_conf->mtx);
        while (intr_conf->config_locked == true)
                mtx_sleep(&intr_conf->config_locked, &intr_conf->mtx, 0,
                    "gpicfg", 0);
        if (intr_conf->intr_mode == 0 && flags != 0) {
                /*
                 * No interrupt is configured, but one is requested: Allocate
                 * and setup interrupt on the according pin.
                 */
                res = gpioc_allocate_pin_intr(sc, intr_conf, pin, flags);
                if (res == 0)
                        res = gpioc_attach_priv_pin(priv, intr_conf);
                if (res == EEXIST)
                        res = 0;
        } else if (intr_conf->intr_mode == flags) {
                /*
                 * Same interrupt requested as already configured: Attach the
                 * cdevpriv to the corresponding pin.
                 */
                res = gpioc_attach_priv_pin(priv, intr_conf);
                if (res == EEXIST)
                        res = 0;
        } else if (intr_conf->intr_mode != 0 && flags == 0) {
                /*
                 * Interrupt configured, but none requested: Teardown and
                 * release the pin when no other cdevpriv is attached. Otherwise
                 * just detach pin and cdevpriv from each other.
                 */
                if (gpioc_intr_reconfig_allowed(priv, intr_conf)) {
                        res = gpioc_release_pin_intr(sc, intr_conf);
                }
                if (res == 0)
                        res = gpioc_detach_priv_pin(priv, intr_conf);
        } else {
                /*
                 * Other flag requested than configured: Reconfigure when no
                 * other cdevpriv is are attached to the pin.
                 */
                if (!gpioc_intr_reconfig_allowed(priv, intr_conf))
                        res = EBUSY;
                else {
                        res = gpioc_release_pin_intr(sc, intr_conf);
                        if (res == 0)
                                res = gpioc_allocate_pin_intr(sc, intr_conf,
                                    pin, flags);
                        if (res == 0)
                                res = gpioc_attach_priv_pin(priv, intr_conf);
                        if (res == EEXIST)
                                res = 0;
                }
        }
        mtx_unlock(&intr_conf->mtx);

        return (res);
}

static void
gpioc_interrupt_handler(void *arg)
{
        struct gpioc_pin_intr *intr_conf;
        struct gpioc_privs *privs;
        sbintime_t evtime;
        bool pin_state;

        intr_conf = arg;

        /* Capture time and pin state first. */
        evtime = sbinuptime();
        if (intr_conf->intr_mode & GPIO_INTR_EDGE_BOTH)
                gpio_pin_is_active(intr_conf->pin, &pin_state);
        else if (intr_conf->intr_mode & GPIO_INTR_EDGE_RISING)
                pin_state = true;
        else
                pin_state = false;

        mtx_lock(&intr_conf->mtx);

        if (intr_conf->config_locked == true) {
                ddevice_printf(sc->sc_dev, "Interrupt configuration in "
                    "progress. Discarding interrupt on pin %d.\n",
                    intr_conf->pin->pin);
                mtx_unlock(&intr_conf->mtx);
                return;
        }

        if (SLIST_EMPTY(&intr_conf->privs)) {
                ddevice_printf(sc->sc_dev, "No file descriptor associated with "
                    "occurred interrupt on pin %d.\n", intr_conf->pin->pin);
                mtx_unlock(&intr_conf->mtx);
                return;
        }

        SLIST_FOREACH(privs, &intr_conf->privs, next) {
                struct gpioc_cdevpriv *priv = privs->priv;
                struct gpioc_pins *privpin;
                struct gpioc_pin_event *event;
                mtx_lock(&priv->mtx);
                SLIST_FOREACH(privpin, &priv->pins, next) {
                        if (privpin->pin == intr_conf)
                                break;
                }
                if (privpin == NULL) {
                        /* Should be impossible. */
                        ddevice_printf(sc->sc_dev, "Cannot find privpin\n");
                        mtx_unlock(&priv->mtx);
                        continue;
                }

                if (priv->report_option == GPIO_EVENT_REPORT_DETAIL) {
                        event = next_head_event(priv);
                        /* If head is overtaking tail, advance tail. */
                        if (priv->evidx_head == priv->evidx_tail)
                                next_tail_event(priv);
                } else {
                        if (privpin->eventcount > 0)
                                event = &priv->events[privpin->firstevent + 1];
                        else {
                                privpin->firstevent = priv->evidx_head;
                                event = next_head_event(priv);
                                event->privpin = privpin;
                                event->event_time = evtime;
                                event->event_pin_state = pin_state;
                                event = next_head_event(priv);
                        }
                        ++privpin->eventcount;
                }
                event->privpin = privpin;
                event->event_time = evtime;
                event->event_pin_state = pin_state;
                wakeup(priv);
                selwakeup(&priv->selinfo);
                KNOTE_LOCKED(&priv->selinfo.si_note, 0);
                if (priv->async == true && priv->sigio != NULL)
                        pgsigio(&priv->sigio, SIGIO, 0);
                mtx_unlock(&priv->mtx);
        }

        mtx_unlock(&intr_conf->mtx);
}

static int
gpioc_probe(device_t dev)
{
        device_set_desc(dev, "GPIO controller");
        return (0);
}

static int
gpioc_attach(device_t dev)
{
        int err;
        struct gpioc_softc *sc;
        struct make_dev_args devargs;

        sc = device_get_softc(dev);
        sc->sc_dev = dev;
        sc->sc_pdev = device_get_parent(dev);
        sc->sc_unit = device_get_unit(dev);

        sc->sc_npins = gpiobus_get_npins(dev);
        sc->sc_pin_intr = malloc(sizeof(struct gpioc_pin_intr) * sc->sc_npins,
            M_GPIOC, M_WAITOK | M_ZERO);
        for (int i = 0; i < sc->sc_npins; i++) {
                sc->sc_pin_intr[i].sc = sc;
                mtx_init(&sc->sc_pin_intr[i].mtx, "gpioc pin", NULL, MTX_DEF);
                SLIST_INIT(&sc->sc_pin_intr[i].privs);
        }

        make_dev_args_init(&devargs);
        devargs.mda_devsw = &gpioc_cdevsw;
        devargs.mda_uid = UID_ROOT;
        devargs.mda_gid = GID_WHEEL;
        devargs.mda_mode = 0600;
        devargs.mda_si_drv1 = sc;
        err = make_dev_s(&devargs, &sc->sc_ctl_dev, "gpioc%d", sc->sc_unit);
        if (err != 0) {
                device_printf(dev, "Failed to create gpioc%d", sc->sc_unit);
                return (ENXIO);
        }

        return (0);
}

static int
gpioc_detach(device_t dev)
{
        struct gpioc_softc *sc = device_get_softc(dev);

        if (sc->sc_ctl_dev)
                destroy_dev(sc->sc_ctl_dev);

        for (int i = 0; i < sc->sc_npins; i++) {
                mtx_destroy(&sc->sc_pin_intr[i].mtx);
                MPASS(sc->sc_pin_intr[i].pin == NULL);
        }
        free(sc->sc_pin_intr, M_GPIOC);

        return (0);
}

static void
gpioc_cdevpriv_dtor(void *data)
{
        struct gpioc_cdevpriv   *priv;
        struct gpioc_privs      *priv_link, *priv_link_temp;
        struct gpioc_pins       *pin_link, *pin_link_temp;
        unsigned int            consistency __diagused;

        priv = data;

        SLIST_FOREACH_SAFE(pin_link, &priv->pins, next, pin_link_temp) {
                consistency = 0;
                mtx_lock(&pin_link->pin->mtx);
                while (pin_link->pin->config_locked == true)
                        mtx_sleep(&pin_link->pin->config_locked,
                            &pin_link->pin->mtx, 0, "gpicfg", 0);
                SLIST_FOREACH_SAFE(priv_link, &pin_link->pin->privs, next,
                    priv_link_temp) {
                        if (priv_link->priv == priv) {
                                SLIST_REMOVE(&pin_link->pin->privs, priv_link,
                                    gpioc_privs, next);
                                free(priv_link, M_GPIOC);
                                consistency++;
                        }
                }
                KASSERT(consistency == 1,
                    ("inconsistent links between pin config and cdevpriv"));
                if (gpioc_intr_reconfig_allowed(priv, pin_link->pin)) {
                        gpioc_release_pin_intr(priv->sc, pin_link->pin);
                }
                mtx_unlock(&pin_link->pin->mtx);
                SLIST_REMOVE(&priv->pins, pin_link, gpioc_pins, next);
                free(pin_link, M_GPIOC);
        }

        wakeup(&priv);
        knlist_clear(&priv->selinfo.si_note, 0);
        seldrain(&priv->selinfo);
        knlist_destroy(&priv->selinfo.si_note);
        funsetown(&priv->sigio);

        mtx_destroy(&priv->mtx);
        free(priv->events, M_GPIOC);
        free(data, M_GPIOC);
}

static int
gpioc_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct gpioc_cdevpriv *priv;
        int err = 0;

        priv = malloc(sizeof(*priv), M_GPIOC, M_WAITOK | M_ZERO);
        priv->sc = dev->si_drv1;

        mtx_init(&priv->mtx, "gpioc priv", NULL, MTX_DEF);
        knlist_init_mtx(&priv->selinfo.si_note, &priv->mtx);

        priv->async = false;
        priv->report_option = GPIO_EVENT_REPORT_DETAIL;
        priv->sigio = NULL;

        /*
         * Allocate a circular buffer for events.  The scheme we use for summary
         * reporting assumes there will always be a pair of events available to
         * record the first/last events on any pin, so we allocate 2 * npins.
         * Even though we actually default to detailed event reporting, 2 *
         * npins isn't a horrible fifo size for that either.
         */
        priv->numevents = priv->sc->sc_npins * 2;
        priv->events = malloc(priv->numevents * sizeof(struct gpioc_pin_event),
            M_GPIOC, M_WAITOK | M_ZERO);

        priv->evidx_head = priv->evidx_tail = 0;
        SLIST_INIT(&priv->pins);

        err = devfs_set_cdevpriv(priv, gpioc_cdevpriv_dtor);
        if (err != 0)
                gpioc_cdevpriv_dtor(priv);
        return (err);
}

static int
gpioc_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct gpioc_cdevpriv *priv;
        struct gpioc_pin_event *event;
        union {
                struct gpio_event_summary sum;
                struct gpio_event_detail  evt;
                uint8_t                   data[1];
        } recbuf;
        size_t recsize;
        int err;

        if ((err = devfs_get_cdevpriv((void **)&priv)) != 0)
                return (err);

        if (priv->report_option == GPIO_EVENT_REPORT_SUMMARY)
                recsize = sizeof(struct gpio_event_summary);
        else
                recsize = sizeof(struct gpio_event_detail);

        if (uio->uio_resid < recsize)
                return (EINVAL);

        mtx_lock(&priv->mtx);
        while (priv->evidx_head == priv->evidx_tail) {
                if (SLIST_EMPTY(&priv->pins)) {
                        err = ENXIO;
                        break;
                } else if (ioflag & O_NONBLOCK) {
                        err = EWOULDBLOCK;
                        break;
                } else {
                        err = mtx_sleep(priv, &priv->mtx, PCATCH, "gpintr", 0);
                        if (err != 0)
                                break;
                }
        }

        while (err == 0 && uio->uio_resid >= recsize &&
           priv->evidx_tail != priv->evidx_head) {
                event = next_tail_event(priv);
                if (priv->report_option == GPIO_EVENT_REPORT_SUMMARY) {
                        recbuf.sum.gp_first_time = event->event_time;
                        recbuf.sum.gp_pin = event->privpin->pin->pin->pin;
                        recbuf.sum.gp_count = event->privpin->eventcount;
                        recbuf.sum.gp_first_state = event->event_pin_state;
                        event = next_tail_event(priv);
                        recbuf.sum.gp_last_time = event->event_time;
                        recbuf.sum.gp_last_state = event->event_pin_state;
                        event->privpin->eventcount = 0;
                        event->privpin->firstevent = 0;
                } else {
                        recbuf.evt.gp_time = event->event_time;
                        recbuf.evt.gp_pin = event->privpin->pin->pin->pin;
                        recbuf.evt.gp_pinstate = event->event_pin_state;
                }
                mtx_unlock(&priv->mtx);
                err = uiomove(recbuf.data, recsize, uio);
                mtx_lock(&priv->mtx);
        }
        mtx_unlock(&priv->mtx);
        return (err);
}

static int 
gpioc_ioctl(struct cdev *cdev, u_long cmd, caddr_t arg, int fflag, 
    struct thread *td)
{
        int max_pin, res;
        struct gpioc_softc *sc = cdev->si_drv1;
        struct gpioc_cdevpriv *priv;
        struct gpio_pin pin;
        struct gpio_req req;
        struct gpio_access_32 *a32;
        struct gpio_config_32 *c32;
        struct gpio_event_config *evcfg;
        struct gpioc_pin_event *tmp;
        uint32_t caps, intrflags;

        switch (cmd) {
        case GPIOMAXPIN:
                res = 0;
                max_pin = sc->sc_npins - 1;
                bcopy(&max_pin, arg, sizeof(max_pin));
                break;
        case GPIOGETCONFIG:
                bcopy(arg, &pin, sizeof(pin));
                dprintf("get config pin %d\n", pin.gp_pin);
                res = GPIOBUS_PIN_GETFLAGS(sc->sc_pdev, sc->sc_dev, pin.gp_pin,
                    &pin.gp_flags);
                /* Fail early */
                if (res != 0)
                        break;
                res = devfs_get_cdevpriv((void **)&priv);
                if (res != 0)
                        break;
                pin.gp_flags |= gpioc_get_intr_config(sc, priv,
                    pin.gp_pin);
                res = GPIOBUS_PIN_GETCAPS(sc->sc_pdev, sc->sc_dev, pin.gp_pin,
                    &pin.gp_caps);
                if (res != 0)
                        break;
                res = GPIOBUS_PIN_GETNAME(sc->sc_pdev, pin.gp_pin, pin.gp_name);
                if (res != 0)
                        break;
                bcopy(&pin, arg, sizeof(pin));
                break;
        case GPIOSETCONFIG:
                bcopy(arg, &pin, sizeof(pin));
                dprintf("set config pin %d\n", pin.gp_pin);
                res = devfs_get_cdevpriv((void **)&priv);
                if (res != 0)
                        break;
                res = GPIOBUS_PIN_GETCAPS(sc->sc_pdev, sc->sc_dev,
                    pin.gp_pin, &caps);
                if (res != 0)
                        break;
                res = gpio_check_flags(caps, pin.gp_flags);
                if (res != 0)
                        break;
                intrflags = pin.gp_flags & GPIO_INTR_MASK;
                /*
                 * We can do only edge interrupts, and only if the
                 * hardware supports that interrupt type on that pin.
                 */
                switch (intrflags) {
                case GPIO_INTR_NONE:
                        break;
                case GPIO_INTR_EDGE_RISING:
                case GPIO_INTR_EDGE_FALLING:
                case GPIO_INTR_EDGE_BOTH:
                        if ((intrflags & caps) == 0)
                                res = EOPNOTSUPP;
                        break;
                default:
                        res = EINVAL;
                        break;
                }
                if (res != 0)
                        break;
                res = GPIOBUS_PIN_SETFLAGS(sc->sc_pdev, sc->sc_dev, pin.gp_pin,
                    pin.gp_flags & ~GPIO_INTR_MASK);
                if (res != 0)
                        break;
                res = gpioc_set_intr_config(sc, priv, pin.gp_pin,
                    intrflags);
                break;
        case GPIOGET:
                bcopy(arg, &req, sizeof(req));
                res = GPIOBUS_PIN_GET(sc->sc_pdev, sc->sc_dev, req.gp_pin,
                    &req.gp_value);
                if (res != 0)
                        break;
                dprintf("read pin %d -> %d\n",
                    req.gp_pin, req.gp_value);
                bcopy(&req, arg, sizeof(req));
                break;
        case GPIOSET:
                bcopy(arg, &req, sizeof(req));
                res = GPIOBUS_PIN_SET(sc->sc_pdev, sc->sc_dev, req.gp_pin,
                    req.gp_value);
                dprintf("write pin %d -> %d\n",
                    req.gp_pin, req.gp_value);
                break;
        case GPIOTOGGLE:
                bcopy(arg, &req, sizeof(req));
                dprintf("toggle pin %d\n",
                    req.gp_pin);
                res = GPIOBUS_PIN_TOGGLE(sc->sc_pdev, sc->sc_dev, req.gp_pin);
                break;
        case GPIOSETNAME:
                bcopy(arg, &pin, sizeof(pin));
                dprintf("set name on pin %d\n", pin.gp_pin);
                res = GPIOBUS_PIN_SETNAME(sc->sc_pdev, pin.gp_pin,
                    pin.gp_name);
                break;
        case GPIOACCESS32:
                a32 = (struct gpio_access_32 *)arg;
                res = GPIOBUS_PIN_ACCESS_32(sc->sc_pdev, sc->sc_dev,
                    a32->first_pin, a32->clear_pins, a32->change_pins,
                    &a32->orig_pins);
                break;
        case GPIOCONFIG32:
                c32 = (struct gpio_config_32 *)arg;
                res = GPIOBUS_PIN_CONFIG_32(sc->sc_pdev, sc->sc_dev,
                    c32->first_pin, c32->num_pins, c32->pin_flags);
                break;
        case GPIOCONFIGEVENTS:
                evcfg = (struct gpio_event_config *)arg;
                res = devfs_get_cdevpriv((void **)&priv);
                if (res != 0)
                        break;
                if (evcfg->gp_report_type != GPIO_EVENT_REPORT_DETAIL &&
                    evcfg->gp_report_type != GPIO_EVENT_REPORT_SUMMARY) {
                        res = EINVAL;
                        break;
                }
                /* Reallocate the events buffer if the user wants it bigger. */
                tmp = NULL;
                if (evcfg->gp_report_type == GPIO_EVENT_REPORT_DETAIL &&
                    priv->numevents < evcfg->gp_fifo_size) {
                        tmp = malloc(evcfg->gp_fifo_size *
                            sizeof(struct gpioc_pin_event), M_GPIOC,
                            M_WAITOK | M_ZERO);
                }
                mtx_lock(&priv->mtx);
                /* If any pins have been configured, changes aren't allowed. */
                if (!SLIST_EMPTY(&priv->pins)) {
                        mtx_unlock(&priv->mtx);
                        free(tmp, M_GPIOC);
                        res = EINVAL;
                        break;
                }
                if (tmp != NULL) {
                        free(priv->events, M_GPIOC);
                        priv->events = tmp;
                        priv->numevents = evcfg->gp_fifo_size;
                        priv->evidx_head = priv->evidx_tail = 0;
                }
                priv->report_option = evcfg->gp_report_type;
                mtx_unlock(&priv->mtx);
                break;
        case FIONBIO:
                /*
                 * This dummy handler is necessary to prevent fcntl()
                 * from failing. The actual handling of non-blocking IO
                 * is done using the O_NONBLOCK ioflag passed to the
                 * read() syscall.
                 */
                res = 0;
                break;
        case FIOASYNC:
                res = devfs_get_cdevpriv((void **)&priv);
                if (res == 0) {
                        if (*(int *)arg == FASYNC)
                                priv->async = true;
                        else
                                priv->async = false;
                }
                break;
        case FIOGETOWN:
                res = devfs_get_cdevpriv((void **)&priv);
                if (res == 0)
                        *(int *)arg = fgetown(&priv->sigio);
                break;
        case FIOSETOWN:
                res = devfs_get_cdevpriv((void **)&priv);
                if (res == 0)
                        res = fsetown(*(int *)arg, &priv->sigio);
                break;
        default:
                return (ENOTTY);
                break;
        }

        return (res);
}

static int
gpioc_poll(struct cdev *dev, int events, struct thread *td)
{
        struct gpioc_cdevpriv *priv;
        int err;
        int revents;

        revents = 0;

        err = devfs_get_cdevpriv((void **)&priv);
        if (err != 0) {
                revents = POLLERR;
                return (revents);
        }

        if (SLIST_EMPTY(&priv->pins)) {
                revents = POLLHUP;
                return (revents);
        }

        if (events & (POLLIN | POLLRDNORM)) {
                if (priv->evidx_head != priv->evidx_tail)
                        revents |= events & (POLLIN | POLLRDNORM);
                else
                        selrecord(td, &priv->selinfo);
        }

        return (revents);
}

static int
gpioc_kqfilter(struct cdev *dev, struct knote *kn)
{
        struct gpioc_cdevpriv *priv;
        struct knlist *knlist;
        int err;

        err = devfs_get_cdevpriv((void **)&priv);
        if (err != 0)
                return err;

        if (SLIST_EMPTY(&priv->pins))
                return (ENXIO);

        switch(kn->kn_filter) {
        case EVFILT_READ:
                kn->kn_fop = &gpioc_read_filterops;
                kn->kn_hook = (void *)priv;
                break;
        default:
                return (EOPNOTSUPP);
        }

        knlist = &priv->selinfo.si_note;
        knlist_add(knlist, kn, 0);

        return (0);
}

static int
gpioc_kqread(struct knote *kn, long hint)
{
        struct gpioc_cdevpriv *priv = kn->kn_hook;
        size_t recsize;


        if (SLIST_EMPTY(&priv->pins)) {
                kn->kn_flags |= EV_EOF;
                return (1);
        } else {
                if (priv->evidx_head != priv->evidx_tail) {
                        if (priv->report_option == GPIO_EVENT_REPORT_SUMMARY)
                                recsize = sizeof(struct gpio_event_summary);
                        else
                                recsize = sizeof(struct gpio_event_detail);
                        kn->kn_data = recsize * number_of_events(priv);
                        return (1);
                }
        }
        return (0);
}

static void
gpioc_kqdetach(struct knote *kn)
{
        struct gpioc_cdevpriv *priv = kn->kn_hook;
        struct knlist *knlist = &priv->selinfo.si_note;

        knlist_remove(knlist, kn, 0);
}

static device_method_t gpioc_methods[] = {
        /* Device interface */
        DEVMETHOD(device_probe,         gpioc_probe),
        DEVMETHOD(device_attach,        gpioc_attach),
        DEVMETHOD(device_detach,        gpioc_detach),

        DEVMETHOD_END
};

driver_t gpioc_driver = {
        "gpioc",
        gpioc_methods,
        sizeof(struct gpioc_softc)
};

DRIVER_MODULE(gpioc, gpiobus, gpioc_driver, 0, 0);
MODULE_VERSION(gpioc, 1);