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