root/sys/dev/acpi/aibs_acpi.c
/* $NetBSD: aibs_acpi.c,v 1.7 2021/01/29 15:49:55 thorpej Exp $ */

/*-
 * Copyright (c) 2011 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Jukka Ruohonen.
 *
 * 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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.
 */

/*      $OpenBSD: atk0110.c,v 1.1 2009/07/23 01:38:16 cnst Exp $        */
/*
 * Copyright (c) 2009 Constantine A. Murenin <cnst+netbsd@bugmail.mojo.ru>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: aibs_acpi.c,v 1.7 2021/01/29 15:49:55 thorpej Exp $");

#include <sys/param.h>
#include <sys/kmem.h>
#include <sys/module.h>

#include <dev/acpi/acpireg.h>
#include <dev/acpi/acpivar.h>

/*
 * ASUSTeK AI Booster (ACPI ASOC ATK0110).
 *
 * This code was originally written for OpenBSD after the techniques
 * described in the Linux's asus_atk0110.c and FreeBSD's acpi_aiboost.c
 * were verified to be accurate on the actual hardware kindly provided by
 * Sam Fourman Jr.  It was subsequently ported from OpenBSD to DragonFly BSD,
 * and then to the NetBSD's sysmon_envsys(9) framework.
 *
 *                                -- Constantine A. Murenin <http://cnst.su/>
 */

#define _COMPONENT               ACPI_RESOURCE_COMPONENT
ACPI_MODULE_NAME                 ("acpi_aibs")

#define AIBS_MUX_HWMON           0x00000006
#define AIBS_MUX_MGMT            0x00000011

#define AIBS_TYPE(x)             (((x) >> 16) & 0xff)
#define AIBS_TYPE_VOLT           2
#define AIBS_TYPE_TEMP           3
#define AIBS_TYPE_FAN            4

struct aibs_sensor {
        envsys_data_t                    as_sensor;
        uint64_t                         as_type;
        uint64_t                         as_liml;
        uint64_t                         as_limh;

        SIMPLEQ_ENTRY(aibs_sensor)       as_list;
};

struct aibs_softc {
        device_t                         sc_dev;
        struct acpi_devnode             *sc_node;
        struct sysmon_envsys            *sc_sme;
        bool                             sc_model;      /* new model = true */

        SIMPLEQ_HEAD(, aibs_sensor)      as_head;
};

static int      aibs_match(device_t, cfdata_t, void *);
static void     aibs_attach(device_t, device_t, void *);
static int      aibs_detach(device_t, int);

static void     aibs_init(device_t);
static void     aibs_init_new(device_t);
static void     aibs_init_old(device_t, int);

static void     aibs_sensor_add(device_t, ACPI_OBJECT *);
static bool     aibs_sensor_value(device_t, struct aibs_sensor *, uint64_t *);
static void     aibs_sensor_refresh(struct sysmon_envsys *, envsys_data_t *);
static void     aibs_sensor_limits(struct sysmon_envsys *, envsys_data_t *,
                                   sysmon_envsys_lim_t *, uint32_t *);

CFATTACH_DECL_NEW(aibs, sizeof(struct aibs_softc),
    aibs_match, aibs_attach, aibs_detach, NULL);

static const struct device_compatible_entry compat_data[] = {
        { .compat = "ATK0110" },
        DEVICE_COMPAT_EOL
};

static int
aibs_match(device_t parent, cfdata_t match, void *aux)
{
        struct acpi_attach_args *aa = aux;

        return acpi_compatible_match(aa, compat_data);
}

static void
aibs_attach(device_t parent, device_t self, void *aux)
{
        struct aibs_softc *sc = device_private(self);
        struct acpi_attach_args *aa = aux;

        sc->sc_dev = self;
        sc->sc_node = aa->aa_node;

        aprint_naive("\n");
        aprint_normal(": ASUSTeK AI Booster\n");

        sc->sc_sme = sysmon_envsys_create();

        sc->sc_sme->sme_cookie = sc;
        sc->sc_sme->sme_name = device_xname(self);
        sc->sc_sme->sme_refresh = aibs_sensor_refresh;
        sc->sc_sme->sme_get_limits = aibs_sensor_limits;

        aibs_init(self);
        SIMPLEQ_INIT(&sc->as_head);

        if (sc->sc_model != false)
                aibs_init_new(self);
        else {
                aibs_init_old(self, AIBS_TYPE_FAN);
                aibs_init_old(self, AIBS_TYPE_TEMP);
                aibs_init_old(self, AIBS_TYPE_VOLT);
        }

        (void)pmf_device_register(self, NULL, NULL);

        if (sc->sc_sme->sme_nsensors == 0) {
                aprint_error_dev(self, "no sensors found\n");
                sysmon_envsys_destroy(sc->sc_sme);
                sc->sc_sme = NULL;
                return;
        }

        if (sysmon_envsys_register(sc->sc_sme) != 0)
                aprint_error_dev(self, "failed to register with sysmon\n");
}

static int
aibs_detach(device_t self, int flags)
{
        struct aibs_softc *sc = device_private(self);
        struct aibs_sensor *as;

        pmf_device_deregister(self);

        if (sc->sc_sme != NULL)
                sysmon_envsys_unregister(sc->sc_sme);

        while (SIMPLEQ_FIRST(&sc->as_head) != NULL) {
                as = SIMPLEQ_FIRST(&sc->as_head);
                SIMPLEQ_REMOVE_HEAD(&sc->as_head, as_list);
                kmem_free(as, sizeof(*as));
        }

        return 0;
}

static void
aibs_init(device_t self)
{
        struct aibs_softc *sc = device_private(self);
        ACPI_HANDLE tmp;
        ACPI_STATUS rv;

        /*
         * Old model uses the tuple { TSIF, VSIF, FSIF } to
         * enumerate the sensors and { RTMP, RVLT, RFAN }
         * to obtain the values. New mode uses GGRP for the
         * enumeration and { GITM, SITM } as accessors.
         */
        rv = AcpiGetHandle(sc->sc_node->ad_handle, "GGRP", &tmp);

        if (ACPI_FAILURE(rv)) {
                sc->sc_model = false;
                return;
        }

        rv = AcpiGetHandle(sc->sc_node->ad_handle, "GITM", &tmp);

        if (ACPI_FAILURE(rv)) {
                sc->sc_model = false;
                return;
        }

        rv = AcpiGetHandle(sc->sc_node->ad_handle, "SITM", &tmp);

        if (ACPI_FAILURE(rv)) {
                sc->sc_model = false;
                return;
        }

        sc->sc_model = true;

        /*
         * If both the new and the old methods are present, prefer
         * the old one; GGRP/GITM may not be functional in this case.
         */
        rv = AcpiGetHandle(sc->sc_node->ad_handle, "FSIF", &tmp);

        if (ACPI_FAILURE(rv))
                return;

        rv = AcpiGetHandle(sc->sc_node->ad_handle, "TSIF", &tmp);

        if (ACPI_FAILURE(rv))
                return;

        rv = AcpiGetHandle(sc->sc_node->ad_handle, "VSIF", &tmp);

        if (ACPI_FAILURE(rv))
                return;

        rv = AcpiGetHandle(sc->sc_node->ad_handle, "RFAN", &tmp);

        if (ACPI_FAILURE(rv))
                return;

        rv = AcpiGetHandle(sc->sc_node->ad_handle, "RTMP", &tmp);

        if (ACPI_FAILURE(rv))
                return;

        rv = AcpiGetHandle(sc->sc_node->ad_handle, "RVLT", &tmp);

        if (ACPI_FAILURE(rv))
                return;

        sc->sc_model = false;
}

static void
aibs_init_new(device_t self)
{
        struct aibs_softc *sc = device_private(self);
        ACPI_OBJECT_LIST arg;
        ACPI_OBJECT id, *obj;
        ACPI_BUFFER buf;
        ACPI_STATUS rv;
        uint32_t i, n;

        arg.Count = 1;
        arg.Pointer = &id;

        id.Type = ACPI_TYPE_INTEGER;
        id.Integer.Value = AIBS_MUX_HWMON;

        buf.Pointer = NULL;
        buf.Length = ACPI_ALLOCATE_LOCAL_BUFFER;

        rv = AcpiEvaluateObject(sc->sc_node->ad_handle, "GGRP", &arg, &buf);

        if (ACPI_FAILURE(rv))
                goto out;

        obj = buf.Pointer;

        if (obj->Type != ACPI_TYPE_PACKAGE) {
                rv = AE_TYPE;
                goto out;
        }

        if (obj->Package.Count > UINT32_MAX) {
                rv = AE_AML_NUMERIC_OVERFLOW;
                goto out;
        }

        n = obj->Package.Count;

        if (n == 0) {
                rv = AE_NOT_EXIST;
                goto out;
        }

        for (i = 0; i < n; i++)
                aibs_sensor_add(self, &obj->Package.Elements[i]);

out:
        if (buf.Pointer != NULL)
                ACPI_FREE(buf.Pointer);

        if (ACPI_FAILURE(rv)) {

                aprint_error_dev(self, "failed to evaluate "
                    "GGRP: %s\n", AcpiFormatException(rv));
        }
}

static void
aibs_init_old(device_t self, int type)
{
        struct aibs_softc *sc = device_private(self);
        char path[] = "?SIF";
        ACPI_OBJECT *elm, *obj;
        ACPI_BUFFER buf;
        ACPI_STATUS rv;
        uint32_t i, n;

        switch (type) {

        case AIBS_TYPE_FAN:
                path[0] = 'F';
                break;

        case AIBS_TYPE_TEMP:
                path[0] = 'T';
                break;

        case AIBS_TYPE_VOLT:
                path[0] = 'V';
                break;

        default:
                return;
        }

        rv = acpi_eval_struct(sc->sc_node->ad_handle, path, &buf);

        if (ACPI_FAILURE(rv))
                goto out;

        obj = buf.Pointer;

        if (obj->Type != ACPI_TYPE_PACKAGE) {
                rv = AE_TYPE;
                goto out;
        }

        elm = obj->Package.Elements;

        if (elm[0].Type != ACPI_TYPE_INTEGER) {
                rv = AE_TYPE;
                goto out;
        }

        if (elm[0].Integer.Value > UINT32_MAX) {
                rv = AE_AML_NUMERIC_OVERFLOW;
                goto out;
        }

        n = elm[0].Integer.Value;

        if (n == 0) {
                rv = AE_NOT_EXIST;
                goto out;
        }

        if (obj->Package.Count - 1 != n) {
                rv = AE_BAD_VALUE;
                goto out;
        }

        for (i = 1; i < obj->Package.Count; i++) {

                if (elm[i].Type != ACPI_TYPE_PACKAGE)
                        continue;

                aibs_sensor_add(self, &elm[i]);
        }

out:
        if (buf.Pointer != NULL)
                ACPI_FREE(buf.Pointer);

        if (ACPI_FAILURE(rv)) {

                aprint_error_dev(self, "failed to evaluate "
                    "%s: %s\n", path, AcpiFormatException(rv));
        }
}

static void
aibs_sensor_add(device_t self, ACPI_OBJECT *obj)
{
        struct aibs_softc *sc = device_private(self);
        struct aibs_sensor *as;
        int ena, len, lhi, llo;
        const char *name;
        ACPI_STATUS rv;

        as = NULL;
        rv = AE_OK;

        if (obj->Type != ACPI_TYPE_PACKAGE) {
                rv = AE_TYPE;
                goto out;
        }

        /*
         * The known formats are:
         *
         *      index           type            old             new
         *      -----           ----            ---             ---
         *      0               integer         flags           flags
         *      1               string          name            name
         *      2               integer         limit1          unknown
         *      3               integer         limit2          unknown
         *      4               integer         enable          limit1
         *      5               integer         -               limit2
         *      6               integer         -               enable
         */
        if (sc->sc_model != false) {
                len = 7;
                llo = 4;
                lhi = 5;
                ena = 6;
        } else {
                len = 5;
                llo = 2;
                lhi = 3;
                ena = 4;
        }

        if (obj->Package.Count != (uint32_t)len) {
                rv = AE_LIMIT;
                goto out;
        }

        if (obj->Package.Elements[0].Type != ACPI_TYPE_INTEGER ||
            obj->Package.Elements[1].Type != ACPI_TYPE_STRING ||
            obj->Package.Elements[llo].Type != ACPI_TYPE_INTEGER ||
            obj->Package.Elements[lhi].Type != ACPI_TYPE_INTEGER ||
            obj->Package.Elements[ena].Type != ACPI_TYPE_INTEGER) {
                rv = AE_TYPE;
                goto out;
        }

        as = kmem_zalloc(sizeof(*as), KM_SLEEP);

        name = obj->Package.Elements[1].String.Pointer;

        as->as_type = obj->Package.Elements[0].Integer.Value;
        as->as_liml = obj->Package.Elements[llo].Integer.Value;
        as->as_limh = obj->Package.Elements[lhi].Integer.Value;

        if (sc->sc_model != false)
                as->as_limh += as->as_liml;     /* A range in the new model. */

        as->as_sensor.state = ENVSYS_SINVALID;

        switch (AIBS_TYPE(as->as_type)) {

        case AIBS_TYPE_FAN:
                as->as_sensor.units = ENVSYS_SFANRPM;
                as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
                break;

        case AIBS_TYPE_TEMP:
                as->as_sensor.units = ENVSYS_STEMP;
                as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
                break;

        case AIBS_TYPE_VOLT:
                as->as_sensor.units = ENVSYS_SVOLTS_DC;
                as->as_sensor.flags = ENVSYS_FMONLIMITS | ENVSYS_FHAS_ENTROPY;
                break;

        default:
                rv = AE_TYPE;
                goto out;
        }

        (void)strlcpy(as->as_sensor.desc, name, sizeof(as->as_sensor.desc));

        if (sysmon_envsys_sensor_attach(sc->sc_sme, &as->as_sensor) != 0) {
                rv = AE_AML_INTERNAL;
                goto out;
        }

        SIMPLEQ_INSERT_TAIL(&sc->as_head, as, as_list);

out:
        if (ACPI_FAILURE(rv)) {

                if (as != NULL)
                        kmem_free(as, sizeof(*as));

                aprint_error_dev(self, "failed to add "
                    "sensor: %s\n",  AcpiFormatException(rv));
        }
}

static bool
aibs_sensor_value(device_t self, struct aibs_sensor *as, uint64_t *val)
{
        struct aibs_softc *sc = device_private(self);
        uint32_t type, *ret, cmb[3];
        ACPI_OBJECT_LIST arg;
        ACPI_OBJECT cmi, tmp;
        ACPI_OBJECT *obj;
        ACPI_BUFFER buf;
        ACPI_STATUS rv;
        const char *path;

        if (sc->sc_model != false) {

                path = "GITM";

                cmb[0] = as->as_type;
                cmb[1] = 0;
                cmb[2] = 0;

                arg.Count = 1;
                arg.Pointer = &tmp;

                tmp.Buffer.Length = sizeof(cmb);
                tmp.Buffer.Pointer = (uint8_t *)cmb;
                tmp.Type = type = ACPI_TYPE_BUFFER;

        } else {

                arg.Count = 1;
                arg.Pointer = &cmi;

                cmi.Integer.Value = as->as_type;
                cmi.Type = type = ACPI_TYPE_INTEGER;

                switch (AIBS_TYPE(as->as_type)) {

                case AIBS_TYPE_FAN:
                        path = "RFAN";
                        break;

                case AIBS_TYPE_TEMP:
                        path = "RTMP";
                        break;

                case AIBS_TYPE_VOLT:
                        path = "RVLT";
                        break;

                default:
                        return false;
                }
        }

        buf.Pointer = NULL;
        buf.Length = ACPI_ALLOCATE_LOCAL_BUFFER;

        rv = AcpiEvaluateObject(sc->sc_node->ad_handle, path, &arg, &buf);

        if (ACPI_FAILURE(rv))
                goto out;

        obj = buf.Pointer;

        if (obj->Type != type) {
                rv = AE_TYPE;
                goto out;
        }

        if (sc->sc_model != true)
                *val = obj->Integer.Value;
        else {
                /*
                 * The return buffer contains at least:
                 *
                 *      uint32_t buf[0]  flags
                 *      uint32_t buf[1]  return value
                 *      uint8_t  buf[2-] unknown
                 */
                if (obj->Buffer.Length < 8) {
                        rv = AE_BUFFER_OVERFLOW;
                        goto out;
                }

                ret = (uint32_t *)obj->Buffer.Pointer;

                if (ret[0] == 0) {
                        rv = AE_BAD_VALUE;
                        goto out;
                }

                *val = ret[1];
        }

out:
        if (buf.Pointer != NULL)
                ACPI_FREE(buf.Pointer);

        if (ACPI_FAILURE(rv)) {

                aprint_error_dev(self, "failed to evaluate "
                    "%s: %s\n", path, AcpiFormatException(rv));

                return false;
        }

        return true;
}

static void
aibs_sensor_refresh(struct sysmon_envsys *sme, envsys_data_t *edata)
{
        struct aibs_softc *sc = sme->sme_cookie;
        struct aibs_sensor *tmp, *as = NULL;
        envsys_data_t *s = edata;
        uint64_t val = 0;

        SIMPLEQ_FOREACH(tmp, &sc->as_head, as_list) {

                if (tmp->as_sensor.sensor == s->sensor) {
                        as = tmp;
                        break;
                }
        }

        if (as == NULL) {
                aprint_debug_dev(sc->sc_dev, "failed to find sensor\n");
                return;
        }

        as->as_sensor.state = ENVSYS_SINVALID;
        as->as_sensor.flags |= ENVSYS_FMONNOTSUPP;

        if (aibs_sensor_value(sc->sc_dev, as, &val) != true)
                return;

        switch (as->as_sensor.units) {

        case ENVSYS_SFANRPM:
                as->as_sensor.value_cur = val;
                break;

        case ENVSYS_STEMP:

                if (val == 0)
                        return;

                as->as_sensor.value_cur = val * 100 * 1000 + 273150000;
                break;

        case ENVSYS_SVOLTS_DC:
                as->as_sensor.value_cur = val * 1000;
                break;

        default:
                return;
        }

        as->as_sensor.state = ENVSYS_SVALID;
        as->as_sensor.flags &= ~ENVSYS_FMONNOTSUPP;
}

static void
aibs_sensor_limits(struct sysmon_envsys *sme, envsys_data_t *edata,
    sysmon_envsys_lim_t *limits, uint32_t *props)
{
        struct aibs_softc *sc = sme->sme_cookie;
        struct aibs_sensor *tmp, *as = NULL;
        sysmon_envsys_lim_t *lim = limits;
        envsys_data_t *s = edata;

        SIMPLEQ_FOREACH(tmp, &sc->as_head, as_list) {

                if (tmp->as_sensor.sensor == s->sensor) {
                        as = tmp;
                        break;
                }
        }

        if (as == NULL) {
                aprint_debug_dev(sc->sc_dev, "failed to find sensor\n");
                return;
        }

        switch (as->as_sensor.units) {

        case ENVSYS_SFANRPM:

                /*
                 * Some boards have strange limits for fans.
                 */
                if (as->as_liml == 0) {
                        lim->sel_warnmin = as->as_limh;
                        *props = PROP_WARNMIN;

                } else {
                        lim->sel_warnmin = as->as_liml;
                        lim->sel_warnmax = as->as_limh;
                        *props = PROP_WARNMIN | PROP_WARNMAX;
                }

                break;

        case ENVSYS_STEMP:
                lim->sel_critmax = as->as_limh * 100 * 1000 + 273150000;
                lim->sel_warnmax = as->as_liml * 100 * 1000 + 273150000;

                *props = PROP_CRITMAX | PROP_WARNMAX;
                break;

        case ENVSYS_SVOLTS_DC:
                lim->sel_critmin = as->as_liml * 1000;
                lim->sel_critmax = as->as_limh * 1000;
                *props = PROP_CRITMIN | PROP_CRITMAX;
                break;

        default:
                return;
        }
}

MODULE(MODULE_CLASS_DRIVER, aibs, "sysmon_envsys");

#ifdef _MODULE
#include "ioconf.c"
#endif

static int
aibs_modcmd(modcmd_t cmd, void *aux)
{
        int rv = 0;

        switch (cmd) {

        case MODULE_CMD_INIT:

#ifdef _MODULE
                rv = config_init_component(cfdriver_ioconf_aibs,
                    cfattach_ioconf_aibs, cfdata_ioconf_aibs);
#endif
                break;

        case MODULE_CMD_FINI:

#ifdef _MODULE
                rv = config_fini_component(cfdriver_ioconf_aibs,
                    cfattach_ioconf_aibs, cfdata_ioconf_aibs);
#endif
                break;

        default:
                rv = ENOTTY;
        }

        return rv;
}