root/sys/dev/acpica/acpi_smbat.c
/*-
 * Copyright (c) 2005 Hans Petter Selasky
 * 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>
#include "opt_acpi.h"
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/bus.h>

#include <contrib/dev/acpica/include/acpi.h>

#include <dev/acpica/acpivar.h>
#include <dev/acpica/acpiio.h>
#include <dev/acpica/acpi_smbus.h>

/* Transactions have failed after 500 ms. */
#define SMBUS_TIMEOUT   50

struct acpi_smbat_softc {
        uint8_t         sb_base_addr;
        device_t        ec_dev;

        struct acpi_bix bix;
        struct acpi_bst bst;
        struct timespec bix_lastupdated;
        struct timespec bst_lastupdated;
};

static int      acpi_smbat_probe(device_t dev);
static int      acpi_smbat_attach(device_t dev);
static int      acpi_smbat_shutdown(device_t dev);
static int      acpi_smbat_info_expired(struct timespec *lastupdated);
static void     acpi_smbat_info_updated(struct timespec *lastupdated);
static int      acpi_smbat_get_bix(device_t dev, void *, size_t);
static int      acpi_smbat_get_bst(device_t dev, struct acpi_bst *bst);

ACPI_SERIAL_DECL(smbat, "ACPI Smart Battery");

static SYSCTL_NODE(_debug_acpi, OID_AUTO, batt,
    CTLFLAG_RD | CTLFLAG_MPSAFE, NULL,
    "Battery debugging");

/* On some laptops with smart batteries, enabling battery monitoring
 * software causes keystrokes from atkbd to be lost.  This has also been
 * reported on Linux, and is apparently due to the keyboard and I2C line
 * for the battery being routed through the same chip.  Whether that's
 * accurate or not, adding extra sleeps to the status checking code
 * causes the problem to go away.
 *
 * If you experience that problem, try a value of 10ms and move up
 * from there.
 */
static int      batt_sleep_ms;
SYSCTL_INT(_debug_acpi_batt, OID_AUTO, batt_sleep_ms, CTLFLAG_RW, &batt_sleep_ms, 0,
    "Sleep during battery status updates to prevent keystroke loss.");

static device_method_t acpi_smbat_methods[] = {
        /* device interface */
        DEVMETHOD(device_probe, acpi_smbat_probe),
        DEVMETHOD(device_attach, acpi_smbat_attach),
        DEVMETHOD(device_shutdown, acpi_smbat_shutdown),

        /* ACPI battery interface */
        DEVMETHOD(acpi_batt_get_status, acpi_smbat_get_bst),
        DEVMETHOD(acpi_batt_get_info, acpi_smbat_get_bix),

        DEVMETHOD_END
};

static driver_t acpi_smbat_driver = {
        "battery",
        acpi_smbat_methods,
        sizeof(struct acpi_smbat_softc),
};

DRIVER_MODULE(acpi_smbat, acpi, acpi_smbat_driver, 0, 0);
MODULE_DEPEND(acpi_smbat, acpi, 1, 1, 1);

static int
acpi_smbat_probe(device_t dev)
{
        static char *smbat_ids[] = {"ACPI0001", "ACPI0005", NULL};
        ACPI_STATUS status;
        int rv;

        if (acpi_disabled("smbat"))
                return (ENXIO);
        rv = ACPI_ID_PROBE(device_get_parent(dev), dev, smbat_ids, NULL);
        if (rv > 0)
          return (rv);
        status = AcpiEvaluateObject(acpi_get_handle(dev), "_EC", NULL, NULL);
        if (ACPI_FAILURE(status))
                return (ENXIO);
        device_set_desc(dev, "ACPI Smart Battery");
        return (rv);
}

static int
acpi_smbat_attach(device_t dev)
{
        struct acpi_smbat_softc *sc;
        uint32_t base;

        sc = device_get_softc(dev);
        if (ACPI_FAILURE(acpi_GetInteger(acpi_get_handle(dev), "_EC", &base))) {
                device_printf(dev, "cannot get EC base address\n");
                return (ENXIO);
        }
        sc->sb_base_addr = (base >> 8) & 0xff;

        /* XXX Only works with one EC, but nearly all systems only have one. */
        sc->ec_dev = devclass_get_device(devclass_find("acpi_ec"), 0);
        if (sc->ec_dev == NULL) {
                device_printf(dev, "cannot find EC device\n");
                return (ENXIO);
        }

        timespecclear(&sc->bix_lastupdated);
        timespecclear(&sc->bst_lastupdated);

        if (acpi_battery_register(dev) != 0) {
                device_printf(dev, "cannot register battery\n");
                return (ENXIO);
        }
        return (0);
}

static int
acpi_smbat_shutdown(device_t dev)
{

        acpi_battery_remove(dev);
        return (0);
}

static int
acpi_smbat_info_expired(struct timespec *lastupdated)
{
        struct timespec curtime;

        ACPI_SERIAL_ASSERT(smbat);

        if (lastupdated == NULL)
                return (TRUE);
        if (!timespecisset(lastupdated))
                return (TRUE);

        getnanotime(&curtime);
        timespecsub(&curtime, lastupdated, &curtime);
        return (curtime.tv_sec < 0 ||
            curtime.tv_sec > acpi_battery_get_info_expire());
}

static void
acpi_smbat_info_updated(struct timespec *lastupdated)
{

        ACPI_SERIAL_ASSERT(smbat);

        if (lastupdated != NULL)
                getnanotime(lastupdated);
}

static int
acpi_smbus_read_2(struct acpi_smbat_softc *sc, uint8_t addr, uint8_t cmd,
    uint16_t *ptr)
{
        int error, to;
        UINT64 val;

        ACPI_SERIAL_ASSERT(smbat);

        if (batt_sleep_ms)
            AcpiOsSleep(batt_sleep_ms);

        val = addr;
        error = ACPI_EC_WRITE(sc->ec_dev, sc->sb_base_addr + SMBUS_ADDR,
            val, 1);
        if (error)
                goto out;

        val = cmd;
        error = ACPI_EC_WRITE(sc->ec_dev, sc->sb_base_addr + SMBUS_CMD,
            val, 1);
        if (error)
                goto out;

        val = 0x09; /* | 0x80 if PEC */
        error = ACPI_EC_WRITE(sc->ec_dev, sc->sb_base_addr + SMBUS_PRTCL,
            val, 1);
        if (error)
                goto out;

        if (batt_sleep_ms)
            AcpiOsSleep(batt_sleep_ms);

        for (to = SMBUS_TIMEOUT; to != 0; to--) {
                error = ACPI_EC_READ(sc->ec_dev, sc->sb_base_addr + SMBUS_PRTCL,
                    &val, 1);
                if (error)
                        goto out;
                if (val == 0)
                        break;
                AcpiOsSleep(10);
        }
        if (to == 0) {
                error = ETIMEDOUT;
                goto out;
        }

        error = ACPI_EC_READ(sc->ec_dev, sc->sb_base_addr + SMBUS_STS, &val, 1);
        if (error)
                goto out;
        if (val & SMBUS_STS_MASK) {
                printf("%s: AE_ERROR 0x%x\n",
                       __FUNCTION__, (int)(val & SMBUS_STS_MASK));
                error = EIO;
                goto out;
        }

        error = ACPI_EC_READ(sc->ec_dev, sc->sb_base_addr + SMBUS_DATA,
            &val, 2);
        if (error)
                goto out;

        *ptr = val;

out:
        return (error);
}

static int
acpi_smbus_read_multi_1(struct acpi_smbat_softc *sc, uint8_t addr, uint8_t cmd,
    uint8_t *ptr, uint16_t len)
{
        UINT64 val;
        uint8_t to;
        int error;

        ACPI_SERIAL_ASSERT(smbat);

        if (batt_sleep_ms)
            AcpiOsSleep(batt_sleep_ms);

        val = addr;
        error = ACPI_EC_WRITE(sc->ec_dev, sc->sb_base_addr + SMBUS_ADDR,
            val, 1);
        if (error)
                goto out;

        val = cmd;
        error = ACPI_EC_WRITE(sc->ec_dev, sc->sb_base_addr + SMBUS_CMD,
            val, 1);
        if (error)
                goto out;

        val = 0x0B /* | 0x80 if PEC */ ;
        error = ACPI_EC_WRITE(sc->ec_dev, sc->sb_base_addr + SMBUS_PRTCL,
            val, 1);
        if (error)
                goto out;

        if (batt_sleep_ms)
            AcpiOsSleep(batt_sleep_ms);

        for (to = SMBUS_TIMEOUT; to != 0; to--) {
                error = ACPI_EC_READ(sc->ec_dev, sc->sb_base_addr + SMBUS_PRTCL,
                    &val, 1);
                if (error)
                        goto out;
                if (val == 0)
                        break;
                AcpiOsSleep(10);
        }
        if (to == 0) {
                error = ETIMEDOUT;
                goto out;
        }

        error = ACPI_EC_READ(sc->ec_dev, sc->sb_base_addr + SMBUS_STS, &val, 1);
        if (error)
                goto out;
        if (val & SMBUS_STS_MASK) {
                printf("%s: AE_ERROR 0x%x\n",
                       __FUNCTION__, (int)(val & SMBUS_STS_MASK));
                error = EIO;
                goto out;
        }

        /* get length */
        error = ACPI_EC_READ(sc->ec_dev, sc->sb_base_addr + SMBUS_BCNT,
            &val, 1);
        if (error)
                goto out;
        val = (val & 0x1f) + 1;

        bzero(ptr, len);
        if (len > val)
                len = val;

        if (batt_sleep_ms)
            AcpiOsSleep(batt_sleep_ms);

        while (len--) {
                error = ACPI_EC_READ(sc->ec_dev, sc->sb_base_addr + SMBUS_DATA
                    + len, &val, 1);
                if (error)
                        goto out;

                ptr[len] = val;
                if (batt_sleep_ms)
                    AcpiOsSleep(batt_sleep_ms);
        }

out:
        return (error);
}

static int
acpi_smbat_get_bst(device_t dev, struct acpi_bst *bst)
{
        struct acpi_smbat_softc *sc;
        int error;
        uint32_t factor;
        int16_t val;
        uint8_t addr;

        ACPI_SERIAL_BEGIN(smbat);

        addr = SMBATT_ADDRESS;
        error = ENXIO;
        sc = device_get_softc(dev);

        if (!acpi_smbat_info_expired(&sc->bst_lastupdated)) {
                error = 0;
                goto out;
        }

        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_BATTERY_MODE, &val))
                goto out;
        if (val & SMBATT_BM_CAPACITY_MODE)
                factor = 10;
        else
                factor = 1;

        /* get battery status */
        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_BATTERY_STATUS, &val))
                goto out;

        sc->bst.state = 0;
        if (val & SMBATT_BS_DISCHARGING)
                sc->bst.state |= ACPI_BATT_STAT_DISCHARG;

        if (val & SMBATT_BS_REMAINING_CAPACITY_ALARM)
                sc->bst.state |= ACPI_BATT_STAT_CRITICAL;

        /*
         * If the rate is negative, it is discharging.  Otherwise,
         * it is charging.
         */
        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_CURRENT, &val))
                goto out;

        if (val > 0) {
                sc->bst.rate = val * factor;
                sc->bst.state &= ~SMBATT_BS_DISCHARGING;
                sc->bst.state |= ACPI_BATT_STAT_CHARGING;
        } else if (val < 0)
                sc->bst.rate = (-val) * factor;
        else
                sc->bst.rate = 0;

        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_REMAINING_CAPACITY, &val))
                goto out;
        sc->bst.cap = val * factor;

        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_VOLTAGE, &val))
                goto out;
        sc->bst.volt = val;

        acpi_smbat_info_updated(&sc->bst_lastupdated);
        error = 0;

out:
        if (error == 0)
                memcpy(bst, &sc->bst, sizeof(sc->bst));
        ACPI_SERIAL_END(smbat);
        return (error);
}

static int
acpi_smbat_get_bix(device_t dev, void *bix, size_t len)
{
        struct acpi_smbat_softc *sc;
        int error;
        uint32_t factor;
        uint16_t val;
        uint8_t addr;

        if (len != sizeof(struct acpi_bix) &&
            len != sizeof(struct acpi_bif))
                return (-1);

        ACPI_SERIAL_BEGIN(smbat);

        addr = SMBATT_ADDRESS;
        error = ENXIO;
        sc = device_get_softc(dev);

        if (!acpi_smbat_info_expired(&sc->bix_lastupdated)) {
                error = 0;
                goto out;
        }

        sc->bix.rev = ACPI_BIX_REV_BIF;

        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_BATTERY_MODE, &val))
                goto out;
        if (val & SMBATT_BM_CAPACITY_MODE) {
                factor = 10;
                sc->bix.units = ACPI_BIX_UNITS_MW;
        } else {
                factor = 1;
                sc->bix.units = ACPI_BIX_UNITS_MA;
        }

        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_DESIGN_CAPACITY, &val))
                goto out;
        sc->bix.dcap = val * factor;

        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_FULL_CHARGE_CAPACITY, &val))
                goto out;
        sc->bix.lfcap = val * factor;
        sc->bix.btech = 1;              /* secondary (rechargeable) */

        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_DESIGN_VOLTAGE, &val))
                goto out;
        sc->bix.dvol = val;

        sc->bix.wcap = sc->bix.dcap / 10;
        sc->bix.lcap = sc->bix.dcap / 10;

        sc->bix.gra1 = factor;  /* not supported */
        sc->bix.gra2 = factor;  /* not supported */

        if (acpi_smbus_read_multi_1(sc, addr, SMBATT_CMD_DEVICE_NAME,
            sc->bix.model, sizeof(sc->bix.model)))
                goto out;

        if (acpi_smbus_read_2(sc, addr, SMBATT_CMD_SERIAL_NUMBER, &val))
                goto out;
        snprintf(sc->bix.serial, sizeof(sc->bix.serial), "0x%04x", val);

        if (acpi_smbus_read_multi_1(sc, addr, SMBATT_CMD_DEVICE_CHEMISTRY,
            sc->bix.type, sizeof(sc->bix.type)))
                goto out;

        if (acpi_smbus_read_multi_1(sc, addr, SMBATT_CMD_MANUFACTURER_DATA,
            sc->bix.oeminfo, sizeof(sc->bix.oeminfo)))
                goto out;

        /* XXX check if device was replugged during read? */

        acpi_smbat_info_updated(&sc->bix_lastupdated);
        error = 0;

out:
        if (error == 0)
            memcpy(bix, &sc->bix, len);
        ACPI_SERIAL_END(smbat);
        return (error);
}