3c6b59567f
MFC after: 1 week
1066 lines
27 KiB
C
1066 lines
27 KiB
C
/*-
|
|
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
|
|
*
|
|
* 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/cdefs.h>
|
|
__FBSDID("$FreeBSD$");
|
|
|
|
#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 "gpio_if.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; /* gpioX 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;
|
|
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_pin_intr*, uint32_t);
|
|
static int gpioc_release_pin_intr(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 struct filterops gpioc_read_filterops = {
|
|
.f_isfd = true,
|
|
.f_attach = NULL,
|
|
.f_detach = gpioc_kqdetach,
|
|
.f_event = gpioc_kqread,
|
|
.f_touch = NULL
|
|
};
|
|
|
|
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_pin_intr *intr_conf, uint32_t flags)
|
|
{
|
|
int err;
|
|
|
|
intr_conf->config_locked = true;
|
|
mtx_unlock(&intr_conf->mtx);
|
|
|
|
intr_conf->intr_res = gpio_alloc_intr_resource(intr_conf->pin->dev,
|
|
&intr_conf->intr_rid, RF_ACTIVE, intr_conf->pin, flags);
|
|
if (intr_conf->intr_res == NULL) {
|
|
err = ENXIO;
|
|
goto error_exit;
|
|
}
|
|
|
|
err = bus_setup_intr(intr_conf->pin->dev, intr_conf->intr_res,
|
|
INTR_TYPE_MISC | INTR_MPSAFE, NULL, gpioc_interrupt_handler,
|
|
intr_conf, &intr_conf->intr_cookie);
|
|
if (err != 0)
|
|
goto error_exit;
|
|
|
|
intr_conf->pin->flags = flags;
|
|
|
|
error_exit:
|
|
mtx_lock(&intr_conf->mtx);
|
|
intr_conf->config_locked = false;
|
|
wakeup(&intr_conf->config_locked);
|
|
|
|
return (err);
|
|
}
|
|
|
|
static int
|
|
gpioc_release_pin_intr(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(intr_conf->pin->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(intr_conf->pin->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;
|
|
}
|
|
}
|
|
|
|
intr_conf->pin->flags = 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, consistency_b;
|
|
|
|
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, consistency_b;
|
|
|
|
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->pin->flags;
|
|
|
|
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->pin->flags == 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->pin->flags == 0 && flags != 0) {
|
|
/*
|
|
* No interrupt is configured, but one is requested: Allocate
|
|
* and setup interrupt on the according pin.
|
|
*/
|
|
res = gpioc_allocate_pin_intr(intr_conf, flags);
|
|
if (res == 0)
|
|
res = gpioc_attach_priv_pin(priv, intr_conf);
|
|
if (res == EEXIST)
|
|
res = 0;
|
|
} else if (intr_conf->pin->flags == 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->pin->flags != 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(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(intr_conf);
|
|
if (res == 0)
|
|
res = gpioc_allocate_pin_intr(intr_conf, 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;
|
|
struct gpioc_softc *sc;
|
|
sbintime_t evtime;
|
|
uint32_t pin_state;
|
|
|
|
intr_conf = arg;
|
|
sc = intr_conf->sc;
|
|
|
|
/* Capture time and pin state first. */
|
|
evtime = sbinuptime();
|
|
if (intr_conf->pin->flags & GPIO_INTR_EDGE_BOTH)
|
|
GPIO_PIN_GET(sc->sc_pdev, intr_conf->pin->pin, &pin_state);
|
|
else if (intr_conf->pin->flags & 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);
|
|
|
|
err = GPIO_PIN_MAX(sc->sc_pdev, &sc->sc_npins);
|
|
sc->sc_npins++; /* Number of pins is one more than max pin number. */
|
|
if (err != 0)
|
|
return (err);
|
|
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].pin = malloc(sizeof(struct gpiobus_pin),
|
|
M_GPIOC, M_WAITOK | M_ZERO);
|
|
sc->sc_pin_intr[i].sc = sc;
|
|
sc->sc_pin_intr[i].pin->pin = i;
|
|
sc->sc_pin_intr[i].pin->dev = sc->sc_pdev;
|
|
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);
|
|
int err;
|
|
|
|
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);
|
|
free(sc->sc_pin_intr[i].pin, M_GPIOC);
|
|
}
|
|
free(sc->sc_pin_intr, M_GPIOC);
|
|
|
|
if ((err = bus_generic_detach(dev)) != 0)
|
|
return (err);
|
|
|
|
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;
|
|
|
|
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(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;
|
|
|
|
priv = malloc(sizeof(*priv), M_GPIOC, M_WAITOK | M_ZERO);
|
|
priv->sc = dev->si_drv1;
|
|
priv->report_option = GPIO_EVENT_REPORT_DETAIL;
|
|
err = devfs_set_cdevpriv(priv, gpioc_cdevpriv_dtor);
|
|
if (err != 0) {
|
|
gpioc_cdevpriv_dtor(priv);
|
|
return (err);
|
|
}
|
|
mtx_init(&priv->mtx, "gpioc priv", NULL, MTX_DEF);
|
|
knlist_init_mtx(&priv->selinfo.si_note, &priv->mtx);
|
|
|
|
/*
|
|
* 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 gpio_event_detail),
|
|
M_GPIOC, M_WAITOK | M_ZERO);
|
|
|
|
return (0);
|
|
}
|
|
|
|
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)
|
|
{
|
|
device_t bus;
|
|
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;
|
|
uint32_t caps, intrflags;
|
|
|
|
bus = GPIO_GET_BUS(sc->sc_pdev);
|
|
if (bus == NULL)
|
|
return (EINVAL);
|
|
switch (cmd) {
|
|
case GPIOMAXPIN:
|
|
max_pin = -1;
|
|
res = GPIO_PIN_MAX(sc->sc_pdev, &max_pin);
|
|
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 = GPIO_PIN_GETFLAGS(sc->sc_pdev, pin.gp_pin,
|
|
&pin.gp_flags);
|
|
/* Fail early */
|
|
if (res)
|
|
break;
|
|
res = devfs_get_cdevpriv((void **)&priv);
|
|
if (res)
|
|
break;
|
|
pin.gp_flags |= gpioc_get_intr_config(sc, priv,
|
|
pin.gp_pin);
|
|
GPIO_PIN_GETCAPS(sc->sc_pdev, pin.gp_pin, &pin.gp_caps);
|
|
GPIOBUS_PIN_GETNAME(bus, pin.gp_pin, pin.gp_name);
|
|
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 = GPIO_PIN_GETCAPS(sc->sc_pdev, 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 = GPIO_PIN_SETFLAGS(sc->sc_pdev, 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 = GPIO_PIN_GET(sc->sc_pdev, req.gp_pin,
|
|
&req.gp_value);
|
|
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 = GPIO_PIN_SET(sc->sc_pdev, 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 = GPIO_PIN_TOGGLE(sc->sc_pdev, 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(bus, pin.gp_pin,
|
|
pin.gp_name);
|
|
break;
|
|
case GPIOACCESS32:
|
|
a32 = (struct gpio_access_32 *)arg;
|
|
res = GPIO_PIN_ACCESS_32(sc->sc_pdev, a32->first_pin,
|
|
a32->clear_pins, a32->change_pins, &a32->orig_pins);
|
|
break;
|
|
case GPIOCONFIG32:
|
|
c32 = (struct gpio_config_32 *)arg;
|
|
res = GPIO_PIN_CONFIG_32(sc->sc_pdev, 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 any pins have been configured, changes aren't allowed. */
|
|
if (!SLIST_EMPTY(&priv->pins)) {
|
|
res = EINVAL;
|
|
break;
|
|
}
|
|
if (evcfg->gp_report_type != GPIO_EVENT_REPORT_DETAIL &&
|
|
evcfg->gp_report_type != GPIO_EVENT_REPORT_SUMMARY) {
|
|
res = EINVAL;
|
|
break;
|
|
}
|
|
priv->report_option = evcfg->gp_report_type;
|
|
/* Reallocate the events buffer if the user wants it bigger. */
|
|
if (priv->report_option == GPIO_EVENT_REPORT_DETAIL &&
|
|
priv->numevents < evcfg->gp_fifo_size) {
|
|
free(priv->events, M_GPIOC);
|
|
priv->numevents = evcfg->gp_fifo_size;
|
|
priv->events = malloc(priv->numevents *
|
|
sizeof(struct gpio_event_detail), M_GPIOC,
|
|
M_WAITOK | M_ZERO);
|
|
priv->evidx_head = priv->evidx_tail = 0;
|
|
}
|
|
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(device_shutdown, bus_generic_shutdown),
|
|
DEVMETHOD(device_suspend, bus_generic_suspend),
|
|
DEVMETHOD(device_resume, bus_generic_resume),
|
|
|
|
DEVMETHOD_END
|
|
};
|
|
|
|
driver_t gpioc_driver = {
|
|
"gpioc",
|
|
gpioc_methods,
|
|
sizeof(struct gpioc_softc)
|
|
};
|
|
|
|
devclass_t gpioc_devclass;
|
|
|
|
DRIVER_MODULE(gpioc, gpio, gpioc_driver, gpioc_devclass, 0, 0);
|
|
MODULE_VERSION(gpioc, 1);
|