root/drivers/gpio/gpio-macsmc.c
// SPDX-License-Identifier: GPL-2.0-only OR MIT
/*
 * Apple SMC GPIO driver
 * Copyright The Asahi Linux Contributors
 *
 * This driver implements basic SMC PMU GPIO support that can read inputs
 * and write outputs. Mode changes and IRQ config are not yet implemented.
 */

#include <linux/bitmap.h>
#include <linux/device.h>
#include <linux/gpio/driver.h>
#include <linux/hex.h>
#include <linux/mfd/core.h>
#include <linux/mfd/macsmc.h>

#define MAX_GPIO 64

/*
 * Commands 0-6 are, presumably, the intended API.
 * Command 0xff lets you get/set the pin configuration in detail directly,
 * but the bit meanings seem not to be stable between devices/PMU hardware
 * versions.
 *
 * We're going to try to make do with the low commands for now.
 * We don't implement pin mode changes at this time.
 */

#define CMD_ACTION      (0 << 24)
#define CMD_OUTPUT      (1 << 24)
#define CMD_INPUT       (2 << 24)
#define CMD_PINMODE     (3 << 24)
#define CMD_IRQ_ENABLE  (4 << 24)
#define CMD_IRQ_ACK     (5 << 24)
#define CMD_IRQ_MODE    (6 << 24)
#define CMD_CONFIG      (0xff << 24)

#define MODE_INPUT      0
#define MODE_OUTPUT     1
#define MODE_VALUE_0    0
#define MODE_VALUE_1    2

#define IRQ_MODE_HIGH           0
#define IRQ_MODE_LOW            1
#define IRQ_MODE_RISING         2
#define IRQ_MODE_FALLING        3
#define IRQ_MODE_BOTH           4

#define CONFIG_MASK     GENMASK(23, 16)
#define CONFIG_VAL      GENMASK(7, 0)

#define CONFIG_OUTMODE  GENMASK(7, 6)
#define CONFIG_IRQMODE  GENMASK(5, 3)
#define CONFIG_PULLDOWN BIT(2)
#define CONFIG_PULLUP   BIT(1)
#define CONFIG_OUTVAL   BIT(0)

/*
 * Output modes seem to differ depending on the PMU in use... ?
 * j274 / M1 (Sera PMU):
 *   0 = input
 *   1 = output
 *   2 = open drain
 *   3 = disable
 * j314 / M1Pro (Maverick PMU):
 *   0 = input
 *   1 = open drain
 *   2 = output
 *   3 = ?
 */

struct macsmc_gpio {
        struct device *dev;
        struct apple_smc *smc;
        struct gpio_chip gc;

        int first_index;
};

static int macsmc_gpio_nr(smc_key key)
{
        int low = hex_to_bin(key & 0xff);
        int high = hex_to_bin((key >> 8) & 0xff);

        if (low < 0 || high < 0)
                return -1;

        return low | (high << 4);
}

static int macsmc_gpio_key(unsigned int offset)
{
        return _SMC_KEY("gP\0\0") | hex_asc_hi(offset) << 8 | hex_asc_lo(offset);
}

static int macsmc_gpio_find_first_gpio_index(struct macsmc_gpio *smcgp)
{
        struct apple_smc *smc = smcgp->smc;
        smc_key key = macsmc_gpio_key(0);
        smc_key first_key, last_key;
        int start, count, ret;

        /* Return early if the key is out of bounds */
        ret = apple_smc_get_key_by_index(smc, 0, &first_key);
        if (ret)
                return ret;
        if (key <= first_key)
                return -ENODEV;

        ret = apple_smc_get_key_by_index(smc, smc->key_count - 1, &last_key);
        if (ret)
                return ret;
        if (key > last_key)
                return -ENODEV;

        /* Binary search to find index of first SMC key bigger or equal to key */
        start = 0;
        count = smc->key_count;
        while (count > 1) {
                smc_key pkey;
                int pivot = start + ((count - 1) >> 1);

                ret = apple_smc_get_key_by_index(smc, pivot, &pkey);
                if (ret < 0)
                        return ret;

                if (pkey == key)
                        return pivot;

                pivot++;

                if (pkey < key) {
                        count -= pivot - start;
                        start = pivot;
                } else {
                        count = pivot - start;
                }
        }

        return start;
}

static int macsmc_gpio_get_direction(struct gpio_chip *gc, unsigned int offset)
{
        struct macsmc_gpio *smcgp = gpiochip_get_data(gc);
        smc_key key = macsmc_gpio_key(offset);
        u32 val;
        int ret;

        /* First try reading the explicit pin mode register */
        ret = apple_smc_rw_u32(smcgp->smc, key, CMD_PINMODE, &val);
        if (!ret)
                return (val & MODE_OUTPUT) ? GPIO_LINE_DIRECTION_OUT : GPIO_LINE_DIRECTION_IN;

        /*
         * Less common IRQ configs cause CMD_PINMODE to fail, and so does open drain mode.
         * Fall back to reading IRQ mode, which will only succeed for inputs.
         */
        ret = apple_smc_rw_u32(smcgp->smc, key, CMD_IRQ_MODE, &val);
        return ret ? GPIO_LINE_DIRECTION_OUT : GPIO_LINE_DIRECTION_IN;
}

static int macsmc_gpio_get(struct gpio_chip *gc, unsigned int offset)
{
        struct macsmc_gpio *smcgp = gpiochip_get_data(gc);
        smc_key key = macsmc_gpio_key(offset);
        u32 cmd, val;
        int ret;

        ret = macsmc_gpio_get_direction(gc, offset);
        if (ret < 0)
                return ret;

        if (ret == GPIO_LINE_DIRECTION_OUT)
                cmd = CMD_OUTPUT;
        else
                cmd = CMD_INPUT;

        ret = apple_smc_rw_u32(smcgp->smc, key, cmd, &val);
        if (ret < 0)
                return ret;

        return val ? 1 : 0;
}

static int macsmc_gpio_set(struct gpio_chip *gc, unsigned int offset, int value)
{
        struct macsmc_gpio *smcgp = gpiochip_get_data(gc);
        smc_key key = macsmc_gpio_key(offset);
        int ret;

        value |= CMD_OUTPUT;
        ret = apple_smc_write_u32(smcgp->smc, key, CMD_OUTPUT | value);
        if (ret < 0)
                dev_err(smcgp->dev, "GPIO set failed %p4ch = 0x%x\n",
                        &key, value);

        return ret;
}

static int macsmc_gpio_init_valid_mask(struct gpio_chip *gc,
                                       unsigned long *valid_mask, unsigned int ngpios)
{
        struct macsmc_gpio *smcgp = gpiochip_get_data(gc);
        int count;
        int i;

        count = min(smcgp->smc->key_count, MAX_GPIO);

        bitmap_zero(valid_mask, ngpios);

        for (i = 0; i < count; i++) {
                int ret, gpio_nr;
                smc_key key;

                ret = apple_smc_get_key_by_index(smcgp->smc, smcgp->first_index + i, &key);
                if (ret < 0)
                        return ret;

                if (key > SMC_KEY(gPff))
                        break;

                gpio_nr = macsmc_gpio_nr(key);
                if (gpio_nr < 0 || gpio_nr > MAX_GPIO) {
                        dev_err(smcgp->dev, "Bad GPIO key %p4ch\n", &key);
                        continue;
                }

                set_bit(gpio_nr, valid_mask);
        }

        return 0;
}

static int macsmc_gpio_probe(struct platform_device *pdev)
{
        struct macsmc_gpio *smcgp;
        struct apple_smc *smc = dev_get_drvdata(pdev->dev.parent);
        smc_key key;
        int ret;

        smcgp = devm_kzalloc(&pdev->dev, sizeof(*smcgp), GFP_KERNEL);
        if (!smcgp)
                return -ENOMEM;

        smcgp->dev = &pdev->dev;
        smcgp->smc = smc;

        smcgp->first_index = macsmc_gpio_find_first_gpio_index(smcgp);
        if (smcgp->first_index < 0)
                return smcgp->first_index;

        ret = apple_smc_get_key_by_index(smc, smcgp->first_index, &key);
        if (ret < 0)
                return ret;

        if (key > macsmc_gpio_key(MAX_GPIO - 1))
                return -ENODEV;

        dev_info(smcgp->dev, "First GPIO key: %p4ch\n", &key);

        smcgp->gc.label = "macsmc-pmu-gpio";
        smcgp->gc.owner = THIS_MODULE;
        smcgp->gc.get = macsmc_gpio_get;
        smcgp->gc.set = macsmc_gpio_set;
        smcgp->gc.get_direction = macsmc_gpio_get_direction;
        smcgp->gc.init_valid_mask = macsmc_gpio_init_valid_mask;
        smcgp->gc.can_sleep = true;
        smcgp->gc.ngpio = MAX_GPIO;
        smcgp->gc.base = -1;
        smcgp->gc.parent = &pdev->dev;

        return devm_gpiochip_add_data(&pdev->dev, &smcgp->gc, smcgp);
}

static const struct of_device_id macsmc_gpio_of_table[] = {
        { .compatible = "apple,smc-gpio", },
        {}
};
MODULE_DEVICE_TABLE(of, macsmc_gpio_of_table);

static struct platform_driver macsmc_gpio_driver = {
        .driver = {
                .name = "macsmc-gpio",
                .of_match_table = macsmc_gpio_of_table,
        },
        .probe = macsmc_gpio_probe,
};
module_platform_driver(macsmc_gpio_driver);

MODULE_AUTHOR("Hector Martin <marcan@marcan.st>");
MODULE_LICENSE("Dual MIT/GPL");
MODULE_DESCRIPTION("Apple SMC GPIO driver");