root/sys/dev/gpio/gpioths.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2016 Michael Zhilin <mizhka@freebsd.org> All rights reserved.
 * Copyright (c) 2019 Ian Lepore <ian@freebsd.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * GPIOTHS - Temp/Humidity sensor over GPIO.
 *
 * This is driver for Temperature & Humidity sensor which provides digital
 * output over single-wire protocol from embedded 8-bit microcontroller.
 * Note that it uses a custom single-wire protocol, it is not 1-wire(tm).
 * 
 * This driver supports the following chips:
 *   DHT11:  Temp   0c to 50c +-2.0c, Humidity 20% to  90% +-5%
 *   DHT12:  Temp -20c to 60c +-0.5c, Humidity 20% to  95% +-5%
 *   DHT21:  Temp -40c to 80c +-0.3c, Humidity  0% to 100% +-3%
 *   DHT22:  Temp -40c to 80c +-0.3c, Humidity  0% to 100% +-2%
 *   AM2301: Same as DHT21, but also supports i2c interface.
 *   AM2302: Same as DHT22, but also supports i2c interface.
 *
 * Temp/Humidity sensor can't be discovered automatically, please specify hints
 * as part of loader or kernel configuration:
 *      hint.gpioths.0.at="gpiobus0"
 *      hint.gpioths.0.pins=<PIN>
 *
 * Or configure via FDT data.
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/bus.h>
#include <sys/gpio.h>
#include <sys/module.h>
#include <sys/errno.h>
#include <sys/systm.h>
#include <sys/sysctl.h>
#include <sys/taskqueue.h>

#include <dev/gpio/gpiobusvar.h>

#ifdef FDT
#include <dev/ofw/ofw_bus.h>
#include <dev/ofw/ofw_bus_subr.h>

static struct ofw_compat_data compat_data[] = {
        {"dht11",  true},
        {NULL,     false}
};
OFWBUS_PNP_INFO(compat_data);
SIMPLEBUS_PNP_INFO(compat_data);
#endif /* FDT */

#define PIN_IDX 0                       /* Use the first/only configured pin. */

#define GPIOTHS_POLLTIME        5       /* in seconds */

#define GPIOTHS_DHT_STARTCYCLE  20000   /* 20ms = 20000us */
#define GPIOTHS_DHT_TIMEOUT     1000    /* 1ms = 1000us */
#define GPIOTHS_DHT_CYCLES      41
#define GPIOTHS_DHT_ONEBYTEMASK 0xFF

struct gpioths_softc {
        device_t                 dev;
        gpio_pin_t               pin;
        int                      temp;
        int                      hum;
        int                      fails;
        struct timeout_task      task;
        bool                     detaching;
};

static int
gpioths_probe(device_t dev)
{
        int rv;

        /*
         * By default we only bid to attach if specifically added by our parent
         * (usually via hint.gpioths.#.at=busname).  On FDT systems we bid as
         * the default driver based on being configured in the FDT data.
         */
        rv = BUS_PROBE_NOWILDCARD;

#ifdef FDT
        if (ofw_bus_status_okay(dev) &&
            ofw_bus_search_compatible(dev, compat_data)->ocd_data)
                rv = BUS_PROBE_DEFAULT;
#endif

        device_set_desc(dev, "DHT11/DHT22 Temperature and Humidity Sensor");

        return (rv);
}

static int
gpioths_dht_timeuntil(struct gpioths_softc *sc, bool lev, uint32_t *time)
{
        bool            cur_level;
        int             i;

        for (i = 0; i < GPIOTHS_DHT_TIMEOUT; i++) {
                gpio_pin_is_active(sc->pin, &cur_level);
                if (cur_level == lev) {
                        if (time != NULL)
                                *time = i;
                        return (0);
                }
                DELAY(1);
        }

        /* Timeout */
        return (ETIMEDOUT);
}

static void
gpioths_dht_initread(struct gpioths_softc *sc)
{

        /*
         * According to specifications we need to drive the data line low for at
         * least 20ms then drive it high, to wake up the chip and signal it to
         * send a measurement. After sending this start signal, we switch the
         * pin back to input so the device can begin talking to us.
         */
        gpio_pin_setflags(sc->pin, GPIO_PIN_OUTPUT);
        gpio_pin_set_active(sc->pin, false);
        pause_sbt("gpioths", ustosbt(GPIOTHS_DHT_STARTCYCLE), C_PREL(2), 0);
        gpio_pin_set_active(sc->pin, true);
        gpio_pin_setflags(sc->pin, GPIO_PIN_INPUT);
}

static int
gpioths_dht_readbytes(struct gpioths_softc *sc)
{
        uint32_t                 calibrations[GPIOTHS_DHT_CYCLES];
        uint32_t                 intervals[GPIOTHS_DHT_CYCLES];
        uint32_t                 err, avglen, value;
        uint8_t                  crc, calc;
        int                      i, negmul, offset, size, tmphi, tmplo;

        gpioths_dht_initread(sc);
        
        err = gpioths_dht_timeuntil(sc, false, NULL);
        if (err) {
                device_printf(sc->dev, "err(START) = %d\n", err);
                goto error;
        }

        /* reading - 41 cycles */
        for (i = 0; i < GPIOTHS_DHT_CYCLES; i++) {
                err = gpioths_dht_timeuntil(sc, true, &calibrations[i]);
                if (err) {
                        device_printf(sc->dev, "err(CAL, %d) = %d\n", i, err);
                        goto error;
                }
                err = gpioths_dht_timeuntil(sc, false, &intervals[i]);
                if (err) {
                        device_printf(sc->dev, "err(INTERVAL, %d) = %d\n", i, err);
                        goto error;
                }
        }

        /* Calculate average data calibration cycle length */
        avglen = 0;
        for (i = 1; i < GPIOTHS_DHT_CYCLES; i++)
                avglen += calibrations[i];

        avglen = avglen / (GPIOTHS_DHT_CYCLES - 1);

        /* Calculate data */
        value = 0;
        offset = 1;
        size = sizeof(value) * 8;
        for (i = offset; i < size + offset; i++) {
                value <<= 1;
                if (intervals[i] > avglen)
                        value += 1;
        }

        /* Calculate CRC */
        crc = 0;
        offset = sizeof(value) * 8 + 1;
        size = sizeof(crc) * 8;
        for (i = offset;  i < size + offset; i++) {
                crc <<= 1;
                if (intervals[i] > avglen)
                        crc += 1;
        }

        calc = 0;
        for (i = 0; i < sizeof(value); i++)
                calc += (value >> (8*i)) & GPIOTHS_DHT_ONEBYTEMASK;

#ifdef GPIOTHS_DEBUG
        /* Debug bits */
        for (i = 0; i < GPIOTHS_DHT_CYCLES; i++)
                device_printf(sc->dev, "%d: %d %d\n", i, calibrations[i],
                    intervals[i]);

        device_printf(sc->dev, "len=%d, data=%x, crc=%x/%x\n", avglen, value, crc,
            calc);
#endif /* GPIOTHS_DEBUG */

        /* CRC check */
        if (calc != crc) {
                err = -1;
                goto error;
        }

        /*
         * For DHT11/12, the values are split into 8 bits of integer and 8 bits
         * of fractional tenths.  On DHT11 the fraction bytes are always zero.
         * On DHT12 the sign bit is in the high bit of the fraction byte.
         *  - DHT11: 0HHHHHHH 00000000 00TTTTTT 00000000
         *  - DHT12: 0HHHHHHH 0000hhhh 00TTTTTT s000tttt
         *
         * For DHT21/21, the values are are encoded in 16 bits each, with the
         * temperature sign bit in the high bit.  The values are tenths of a
         * degree C and tenths of a percent RH.
         *  - DHT21: 000000HH HHHHHHHH s00000TT TTTTTTTT
         *  - DHT22: 000000HH HHHHHHHH s00000TT TTTTTTTT
         *
         * For all devices, some bits are always zero because of the range of
         * values supported by the device.
         *
         * We figure out how to decode things based on the high byte of the
         * humidity.  A DHT21/22 cannot report a value greater than 3 in
         * the upper bits of its 16-bit humidity.  A DHT11/12 should not report
         * a value lower than 20.  To allow for the possibility that a device
         * could report a value slightly out of its sensitivity range, we split
         * the difference and say if the value is greater than 10 it must be a
         * DHT11/12 (that would be a humidity over 256% on a DHT21/22).
         */
#define DK_OFFSET 2731 /* Offset between K and C, in decikelvins. */
        if ((value >> 24) > 10) {
                /* DHT11 or DHT12 */
                tmphi = (value >> 8) & 0x3f;
                tmplo = value & 0x0f;
                negmul = (value & 0x80) ? -1 : 1;
                sc->temp = DK_OFFSET + (negmul * (tmphi * 10 + tmplo));
                sc->hum = (value >> 24) & 0x7f;
        } else {
                /* DHT21 or DHT22 */
                negmul = (value & 0x8000) ? -1 : 1;
                sc->temp = DK_OFFSET + (negmul * (value & 0x03ff));
                sc->hum = ((value >> 16) & 0x03ff) / 10;
        }

        sc->fails = 0;

#ifdef GPIOTHS_DEBUG
        /* Debug bits */
        device_printf(dev, "fails=%d, temp=%d, hum=%d\n", sc->fails,
            sc->temp, sc->hum);
#endif /* GPIOTHS_DEBUG */

        return (0);
error:
        sc->fails++;
        return (err);
}

static void
gpioths_poll(void *arg, int pending __unused)
{
        struct gpioths_softc    *sc;

        sc = (struct gpioths_softc *)arg;

        gpioths_dht_readbytes(sc);
        if (!sc->detaching)
                taskqueue_enqueue_timeout_sbt(taskqueue_thread, &sc->task,
                    GPIOTHS_POLLTIME * SBT_1S, 0, C_PREL(3));
}

static int
gpioths_attach(device_t dev)
{
        struct gpioths_softc    *sc;
        struct sysctl_ctx_list  *ctx;
        struct sysctl_oid       *tree;
        int err;

        sc = device_get_softc(dev);
        ctx = device_get_sysctl_ctx(dev);
        tree = device_get_sysctl_tree(dev);

        sc->dev = dev;

        TIMEOUT_TASK_INIT(taskqueue_thread, &sc->task, 0, gpioths_poll, sc);

#ifdef FDT
        /* Try to configure our pin from fdt data on fdt-based systems. */
        err = gpio_pin_get_by_ofw_idx(dev, ofw_bus_get_node(dev), PIN_IDX,
            &sc->pin);
#else
        err = ENOENT;
#endif
        /*
         * If we didn't get configured by fdt data and our parent is gpiobus,
         * see if we can be configured by the bus (allows hinted attachment even
         * on fdt-based systems).
         */
        if (err != 0 &&
            strcmp("gpiobus", device_get_name(device_get_parent(dev))) == 0)
                err = gpio_pin_get_by_child_index(dev, PIN_IDX, &sc->pin);

        /* If we didn't get configured by either method, whine and punt. */
        if (err != 0) {
                device_printf(sc->dev,
                    "cannot acquire gpio pin (config error)\n");
                return (err);
        }

        /*
         * Ensure we have control of our pin, and preset the data line to its
         * idle condition (high).  Leave the line in input mode, relying on the
         * external pullup to keep the line high while idle.
         */
        err = gpio_pin_setflags(sc->pin, GPIO_PIN_OUTPUT);
        if (err != 0) {
                device_printf(dev, "gpio_pin_setflags(OUT) = %d\n", err);
                return (err);
        }
        err = gpio_pin_set_active(sc->pin, true);
        if (err != 0) {
                device_printf(dev, "gpio_pin_set_active(false) = %d\n", err);
                return (err);
        }
        err = gpio_pin_setflags(sc->pin, GPIO_PIN_INPUT);
        if (err != 0) {
                device_printf(dev, "gpio_pin_setflags(IN) = %d\n", err);
                return (err);
        }

        /* 
         * Do an initial read so we have correct values for reporting before
         * registering the sysctls that can access those values.  This also
         * schedules the periodic polling the driver does every few seconds to
         * update the sysctl variables.
         */
        gpioths_poll(sc, 0);

        sysctl_add_oid(ctx, SYSCTL_CHILDREN(tree), OID_AUTO, "temperature",                             \
            CTLFLAG_RD | CTLTYPE_INT | CTLFLAG_MPSAFE,
            &sc->temp, 0, sysctl_handle_int, "IK", "temperature", NULL);

        SYSCTL_ADD_INT(ctx, SYSCTL_CHILDREN(tree), OID_AUTO, "humidity",
            CTLFLAG_RD, &sc->hum, 0, "relative humidity(%)");

        SYSCTL_ADD_INT(ctx, SYSCTL_CHILDREN(tree), OID_AUTO, "fails",
            CTLFLAG_RD, &sc->fails, 0,
            "failures since last successful read");

        return (0);
}

static int
gpioths_detach(device_t dev)
{
        struct gpioths_softc    *sc;

        sc = device_get_softc(dev);
        gpio_pin_release(sc->pin);
        sc->detaching = true;
        while (taskqueue_cancel_timeout(taskqueue_thread, &sc->task, NULL) != 0)
                taskqueue_drain_timeout(taskqueue_thread, &sc->task);

        return (0);
}

/* Driver bits */
static device_method_t gpioths_methods[] = {
        /* Device interface */
        DEVMETHOD(device_probe,                 gpioths_probe),
        DEVMETHOD(device_attach,                gpioths_attach),
        DEVMETHOD(device_detach,                gpioths_detach),

        DEVMETHOD_END
};

DEFINE_CLASS_0(gpioths, gpioths_driver, gpioths_methods, sizeof(struct gpioths_softc));

#ifdef FDT
DRIVER_MODULE(gpioths, simplebus, gpioths_driver, 0, 0);
#endif

DRIVER_MODULE(gpioths, gpiobus, gpioths_driver, 0, 0);
MODULE_DEPEND(gpioths, gpiobus, 1, 1, 1);