| // 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"); |