/* $NetBSD: smbus_acpi.c,v 1.18 2022/10/24 10:17:27 riastradh Exp $ */

/*-
* Copyright (c) 2009 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Paul Goyette
*
* 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.
*/

/*
* ACPI SMBus Controller driver
*
* See http://smbus.org/specs/smbus_cmi10.pdf for specifications
*/

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: smbus_acpi.c,v 1.18 2022/10/24 10:17:27 riastradh Exp $");

#include <sys/param.h>
#include <sys/device.h>
#include <sys/callout.h>
#include <sys/kernel.h>
#include <sys/mutex.h>
#include <sys/systm.h>

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

#include <dev/i2c/i2cvar.h>

#define _COMPONENT              ACPI_BUS_COMPONENT
ACPI_MODULE_NAME                ("smbus_acpi")

/*
* ACPI SMBus CMI protocol codes.
*/
#define ACPI_SMBUS_RD_QUICK     0x03
#define ACPI_SMBUS_RCV_BYTE     0x05
#define ACPI_SMBUS_RD_BYTE      0x07
#define ACPI_SMBUS_RD_WORD      0x09
#define ACPI_SMBUS_RD_BLOCK     0x0B
#define ACPI_SMBUS_WR_QUICK     0x02
#define ACPI_SMBUS_SND_BYTE     0x04
#define ACPI_SMBUS_WR_BYTE      0x06
#define ACPI_SMBUS_WR_WORD      0x08
#define ACPI_SMBUS_WR_BLOCK     0x0A
#define ACPI_SMBUS_PROCESS_CALL 0x0C

struct acpi_smbus_softc {
       struct acpi_devnode     *sc_devnode;
       struct callout          sc_callout;
       struct i2c_controller   sc_i2c_tag;
       device_t                sc_dv;
       int                     sc_poll_alert;
};

static int      acpi_smbus_match(device_t, cfdata_t, void *);
static void     acpi_smbus_attach(device_t, device_t, void *);
static int      acpi_smbus_detach(device_t, int);
static int      acpi_smbus_poll_alert(ACPI_HANDLE, int *);
static int      acpi_smbus_exec(void *, i2c_op_t, i2c_addr_t, const void *,
                               size_t, void *, size_t, int);
static void     acpi_smbus_alerts(struct acpi_smbus_softc *);
static void     acpi_smbus_tick(void *);
static void     acpi_smbus_notify_handler(ACPI_HANDLE, uint32_t, void *);

struct SMB_UDID {
       uint8_t         dev_cap;
       uint8_t         vers_rev;
       uint16_t        vendor;
       uint16_t        device;
       uint16_t        interface;
       uint16_t        subsys_vendor;
       uint16_t        subsys_device;
       uint8_t         reserved[4];
};

struct SMB_DEVICE {
       uint8_t         slave_addr;
       uint8_t         reserved;
       struct SMB_UDID dev_id;
};

struct SMB_INFO {
       uint8_t         struct_ver;
       uint8_t         spec_ver;
       uint8_t         hw_cap;
       uint8_t         poll_int;
       uint8_t         dev_count;
       struct SMB_DEVICE device[1];
};

static const struct device_compatible_entry compat_data[] = {
       { .compat = "SMBUS01" },        /* SMBus CMI v1.0 */
       DEVICE_COMPAT_EOL
};

CFATTACH_DECL_NEW(acpismbus, sizeof(struct acpi_smbus_softc),
   acpi_smbus_match, acpi_smbus_attach, acpi_smbus_detach, NULL);

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

       ret = acpi_compatible_match(aa, compat_data);
       if (ret == 0)
               return 0;

       return acpi_smbus_poll_alert(aa->aa_node->ad_handle, NULL) ? ret : 0;
}

static void
acpi_smbus_attach(device_t parent, device_t self, void *aux)
{
       struct acpi_smbus_softc *sc = device_private(self);
       struct acpi_attach_args *aa = aux;
       struct i2cbus_attach_args iba;

       aprint_naive("\n");

       sc->sc_devnode = aa->aa_node;
       sc->sc_dv = self;
       sc->sc_poll_alert = 2;

       /* Attach I2C bus. */
       iic_tag_init(&sc->sc_i2c_tag);
       sc->sc_i2c_tag.ic_cookie = sc;
       sc->sc_i2c_tag.ic_exec = acpi_smbus_exec;

       (void)acpi_smbus_poll_alert(aa->aa_node->ad_handle,&sc->sc_poll_alert);

       /* If failed, fall-back to polling. */
       if (acpi_register_notify(sc->sc_devnode,
               acpi_smbus_notify_handler) != true)
               sc->sc_poll_alert = 2;

       callout_init(&sc->sc_callout, 0);
       callout_setfunc(&sc->sc_callout, acpi_smbus_tick, self);

       if (sc->sc_poll_alert != 0) {
               aprint_debug(": alert_poll %d sec", sc->sc_poll_alert);
               callout_schedule(&sc->sc_callout, sc->sc_poll_alert * hz);
       }

       aprint_normal("\n");

       (void)memset(&iba, 0, sizeof(iba));
       (void)pmf_device_register(self, NULL, NULL);

       iba.iba_tag = &sc->sc_i2c_tag;

       config_found(self, &iba, iicbus_print, CFARGS_NONE);
}

static int
acpi_smbus_detach(device_t self, int flags)
{
       struct acpi_smbus_softc *sc = device_private(self);

       pmf_device_deregister(self);
       acpi_deregister_notify(sc->sc_devnode);

       callout_halt(&sc->sc_callout, NULL);
       callout_destroy(&sc->sc_callout);

       iic_tag_fini(&sc->sc_i2c_tag);

       return 0;
}

static int
acpi_smbus_poll_alert(ACPI_HANDLE hdl, int *alert)
{
       struct SMB_INFO *info;
       ACPI_BUFFER smi_buf;
       ACPI_OBJECT *e, *p;
       ACPI_STATUS rv;

       /*
        * Retrieve polling interval for SMBus Alerts.
        */
       rv = acpi_eval_struct(hdl, "_SBI", &smi_buf);

       if (ACPI_FAILURE(rv))
               return 0;

       p = smi_buf.Pointer;

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

       if (p->Package.Count == 0) {
               rv = AE_LIMIT;
               goto out;
       }

       e = p->Package.Elements;

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

       /* Verify CMI version. */
       if (e[0].Integer.Value != 0x10) {
               rv = AE_SUPPORT;
               goto out;
       }

       if (alert != NULL) {

               if (p->Package.Count < 2)
                       goto out;

               if (e[1].Type != ACPI_TYPE_BUFFER)
                       goto out;

               info = (struct SMB_INFO *)(e[1].Buffer.Pointer);
               *alert = info->poll_int;
       }

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

       return (ACPI_FAILURE(rv)) ? 0 : 1;
}

static int
acpi_smbus_exec(void *cookie, i2c_op_t op, i2c_addr_t addr,
       const void *cmdbuf, size_t cmdlen, void *buf, size_t len, int flags)
{
       struct acpi_smbus_softc *sc = cookie;
       const uint8_t *c = cmdbuf;
       uint8_t *b = buf, *xb;
       const char *path;
       ACPI_OBJECT_LIST args;
       ACPI_OBJECT arg[5];
       ACPI_OBJECT *p, *e;
       ACPI_BUFFER smbuf;
       ACPI_STATUS rv;
       int i, r, xlen;

       /*
        *      arg[0] : protocol
        *      arg[1] : slave address
        *      arg[2] : command
        *      arg[3] : data length
        *      arg[4] : data
        */
       for (i = r = 0; i < __arraycount(arg); i++)
               arg[i].Type = ACPI_TYPE_INTEGER;

       args.Pointer = arg;

       smbuf.Pointer = NULL;
       smbuf.Length = ACPI_ALLOCATE_LOCAL_BUFFER;

       arg[1].Integer.Value = addr;

       if (I2C_OP_READ_P(op)) {

               path = "_SBR";
               args.Count = 3;

               switch (len) {

               case 0:
                       arg[0].Integer.Value = (cmdlen != 0) ?
                           ACPI_SMBUS_RCV_BYTE : ACPI_SMBUS_RD_QUICK;

                       arg[2].Integer.Value = 0;
                       break;

               case 1:
                       arg[0].Integer.Value = ACPI_SMBUS_RD_BYTE;
                       arg[2].Integer.Value = *c;
                       break;

               case 2:
                       arg[0].Integer.Value = ACPI_SMBUS_RD_WORD;
                       arg[2].Integer.Value = *c;
                       break;

               default:
                       arg[0].Integer.Value = ACPI_SMBUS_RD_BLOCK;
                       arg[2].Integer.Value = *c;
                       break;
               }

       } else {

               path = "_SBW";
               args.Count = 5;

               arg[3].Integer.Value = len;

               switch (len) {

               case 0:
                       if (cmdlen == 0) {
                               arg[2].Integer.Value = 0;
                               arg[0].Integer.Value = ACPI_SMBUS_WR_QUICK;
                       } else {
                               arg[2].Integer.Value = *c;
                               arg[0].Integer.Value = ACPI_SMBUS_SND_BYTE;
                       }

                       arg[4].Integer.Value = 0;
                       break;

               case 1:
                       arg[0].Integer.Value = ACPI_SMBUS_WR_BYTE;
                       arg[2].Integer.Value = *c;
                       arg[4].Integer.Value = *b;
                       break;

               case 2:
                       arg[0].Integer.Value = ACPI_SMBUS_WR_WORD;
                       arg[2].Integer.Value = *c;
                       arg[4].Integer.Value = *b++;
                       arg[4].Integer.Value += (*b--) << 8;
                       break;

               default:
                       arg[0].Integer.Value = ACPI_SMBUS_WR_BLOCK;
                       arg[2].Integer.Value = *c;
                       arg[4].Type = ACPI_TYPE_BUFFER;
                       arg[4].Buffer.Pointer = buf;
                       arg[4].Buffer.Length = (len < 32) ? len : 32;
                       break;
               }
       }

       rv = AcpiEvaluateObject(sc->sc_devnode->ad_handle, path, &args,&smbuf);

       if (ACPI_FAILURE(rv))
               goto out;

       p = smbuf.Pointer;

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

       if (p->Package.Count < 1) {
               rv = AE_LIMIT;
               goto out;
       }

       e = p->Package.Elements;

       if (e->Type != ACPI_TYPE_INTEGER) {
               rv = AE_TYPE;
               goto out;
       }

       ACPI_DEBUG_PRINT((ACPI_DB_DEBUG_OBJECT,
               "return status: %"PRIu64"\n", e[0].Integer.Value));

       if (e[0].Integer.Value != 0) {
               rv = AE_BAD_VALUE;
               goto out;
       }

       /*
        * For read operations, copy data to user buffer.
        */
       if (I2C_OP_READ_P(op)) {

               if (p->Package.Count < 3) {
                       rv = AE_LIMIT;
                       goto out;
               }

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

               xlen = e[1].Integer.Value;

               if (xlen > len)
                       xlen = len;

               switch (e[2].Type) {

               case ACPI_TYPE_BUFFER:

                       if (xlen == 0) {
                               rv = AE_LIMIT;
                               goto out;
                       }

                       xb = e[2].Buffer.Pointer;

                       if (xb == NULL) {
                               rv = AE_NULL_OBJECT;
                               goto out;
                       }

                       (void)memcpy(b, xb, xlen);
                       break;

               case ACPI_TYPE_INTEGER:

                       if (xlen > 0)
                               *b++ = e[2].Integer.Value & 0xff;

                       if (xlen > 1)
                               *b = e[2].Integer.Value >> 8;

                       break;

               default:
                       rv = AE_TYPE;
                       goto out;
               }
       }

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

       if (ACPI_SUCCESS(rv))
               return 0;

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

       return 1;
}

/*
* Whether triggered by periodic polling or a Notify(),
* retrieve all pending SMBus device alerts.
*/
static void
acpi_smbus_alerts(struct acpi_smbus_softc *sc)
{
       const ACPI_HANDLE hdl = sc->sc_devnode->ad_handle;
       ACPI_OBJECT *e, *p;
       ACPI_BUFFER alert;
       ACPI_STATUS rv;
       int status = 0;
       uint8_t addr;

       do {
               rv = acpi_eval_struct(hdl, "_SBA", &alert);

               if (ACPI_FAILURE(rv)) {
                       status = 1;
                       goto done;
               }

               p = alert.Pointer;

               if (p->Type == ACPI_TYPE_PACKAGE && p->Package.Count >= 2) {

                       status = 1;

                       e = p->Package.Elements;

                       if (e[0].Type == ACPI_TYPE_INTEGER)
                               status = e[0].Integer.Value;

                       if (status == 0 && e[1].Type == ACPI_TYPE_INTEGER) {
                               addr = e[1].Integer.Value;

                               aprint_debug_dev(sc->sc_dv,
                                   "alert for 0x%x\n", addr);
                       }
               }
done:
               if (alert.Pointer != NULL)
                       ACPI_FREE(alert.Pointer);

       } while (status == 0);
}

static void
acpi_smbus_tick(void *opaque)
{
       device_t dv = opaque;
       struct acpi_smbus_softc *sc = device_private(dv);

       acpi_smbus_alerts(sc);

       callout_schedule(&sc->sc_callout, sc->sc_poll_alert * hz);
}

static void
acpi_smbus_notify_handler(ACPI_HANDLE hdl, uint32_t notify, void *opaque)
{
       device_t dv = opaque;
       struct acpi_smbus_softc *sc = device_private(dv);

       aprint_debug_dev(dv, "received notify message 0x%x\n", notify);

       acpi_smbus_alerts(sc);
}