| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * regmap based generic GPIO driver |
| * |
| * Copyright 2020 Michael Walle <michael@walle.cc> |
| */ |
| |
| #include <linux/bits.h> |
| #include <linux/device.h> |
| #include <linux/err.h> |
| #include <linux/io.h> |
| #include <linux/module.h> |
| #include <linux/regmap.h> |
| #include <linux/slab.h> |
| #include <linux/types.h> |
| |
| #include <linux/gpio/driver.h> |
| #include <linux/gpio/regmap.h> |
| |
| struct gpio_regmap { |
| struct device *parent; |
| struct regmap *regmap; |
| struct gpio_chip gpio_chip; |
| |
| int reg_stride; |
| int ngpio_per_reg; |
| unsigned int reg_dat_base; |
| unsigned int reg_set_base; |
| unsigned int reg_clr_base; |
| unsigned int reg_dir_in_base; |
| unsigned int reg_dir_out_base; |
| |
| int (*reg_mask_xlate)(struct gpio_regmap *gpio, unsigned int base, |
| unsigned int offset, unsigned int *reg, |
| unsigned int *mask); |
| |
| void *driver_data; |
| }; |
| |
| static unsigned int gpio_regmap_addr(unsigned int addr) |
| { |
| if (addr == GPIO_REGMAP_ADDR_ZERO) |
| return 0; |
| |
| return addr; |
| } |
| |
| static int gpio_regmap_simple_xlate(struct gpio_regmap *gpio, |
| unsigned int base, unsigned int offset, |
| unsigned int *reg, unsigned int *mask) |
| { |
| unsigned int line = offset % gpio->ngpio_per_reg; |
| unsigned int stride = offset / gpio->ngpio_per_reg; |
| |
| *reg = base + stride * gpio->reg_stride; |
| *mask = BIT(line); |
| |
| return 0; |
| } |
| |
| static int gpio_regmap_get(struct gpio_chip *chip, unsigned int offset) |
| { |
| struct gpio_regmap *gpio = gpiochip_get_data(chip); |
| unsigned int base, val, reg, mask; |
| int ret; |
| |
| /* we might not have an output register if we are input only */ |
| if (gpio->reg_dat_base) |
| base = gpio_regmap_addr(gpio->reg_dat_base); |
| else |
| base = gpio_regmap_addr(gpio->reg_set_base); |
| |
| ret = gpio->reg_mask_xlate(gpio, base, offset, ®, &mask); |
| if (ret) |
| return ret; |
| |
| ret = regmap_read(gpio->regmap, reg, &val); |
| if (ret) |
| return ret; |
| |
| return !!(val & mask); |
| } |
| |
| static void gpio_regmap_set(struct gpio_chip *chip, unsigned int offset, |
| int val) |
| { |
| struct gpio_regmap *gpio = gpiochip_get_data(chip); |
| unsigned int base = gpio_regmap_addr(gpio->reg_set_base); |
| unsigned int reg, mask; |
| |
| gpio->reg_mask_xlate(gpio, base, offset, ®, &mask); |
| if (val) |
| regmap_update_bits(gpio->regmap, reg, mask, mask); |
| else |
| regmap_update_bits(gpio->regmap, reg, mask, 0); |
| } |
| |
| static void gpio_regmap_set_with_clear(struct gpio_chip *chip, |
| unsigned int offset, int val) |
| { |
| struct gpio_regmap *gpio = gpiochip_get_data(chip); |
| unsigned int base, reg, mask; |
| |
| if (val) |
| base = gpio_regmap_addr(gpio->reg_set_base); |
| else |
| base = gpio_regmap_addr(gpio->reg_clr_base); |
| |
| gpio->reg_mask_xlate(gpio, base, offset, ®, &mask); |
| regmap_write(gpio->regmap, reg, mask); |
| } |
| |
| static int gpio_regmap_get_direction(struct gpio_chip *chip, |
| unsigned int offset) |
| { |
| struct gpio_regmap *gpio = gpiochip_get_data(chip); |
| unsigned int base, val, reg, mask; |
| int invert, ret; |
| |
| if (gpio->reg_dat_base && !gpio->reg_set_base) |
| return GPIO_LINE_DIRECTION_IN; |
| if (gpio->reg_set_base && !gpio->reg_dat_base) |
| return GPIO_LINE_DIRECTION_OUT; |
| |
| if (gpio->reg_dir_out_base) { |
| base = gpio_regmap_addr(gpio->reg_dir_out_base); |
| invert = 0; |
| } else if (gpio->reg_dir_in_base) { |
| base = gpio_regmap_addr(gpio->reg_dir_in_base); |
| invert = 1; |
| } else { |
| return -ENOTSUPP; |
| } |
| |
| ret = gpio->reg_mask_xlate(gpio, base, offset, ®, &mask); |
| if (ret) |
| return ret; |
| |
| ret = regmap_read(gpio->regmap, reg, &val); |
| if (ret) |
| return ret; |
| |
| if (!!(val & mask) ^ invert) |
| return GPIO_LINE_DIRECTION_OUT; |
| else |
| return GPIO_LINE_DIRECTION_IN; |
| } |
| |
| static int gpio_regmap_set_direction(struct gpio_chip *chip, |
| unsigned int offset, bool output) |
| { |
| struct gpio_regmap *gpio = gpiochip_get_data(chip); |
| unsigned int base, val, reg, mask; |
| int invert, ret; |
| |
| if (gpio->reg_dir_out_base) { |
| base = gpio_regmap_addr(gpio->reg_dir_out_base); |
| invert = 0; |
| } else if (gpio->reg_dir_in_base) { |
| base = gpio_regmap_addr(gpio->reg_dir_in_base); |
| invert = 1; |
| } else { |
| return -ENOTSUPP; |
| } |
| |
| ret = gpio->reg_mask_xlate(gpio, base, offset, ®, &mask); |
| if (ret) |
| return ret; |
| |
| if (invert) |
| val = output ? 0 : mask; |
| else |
| val = output ? mask : 0; |
| |
| return regmap_update_bits(gpio->regmap, reg, mask, val); |
| } |
| |
| static int gpio_regmap_direction_input(struct gpio_chip *chip, |
| unsigned int offset) |
| { |
| return gpio_regmap_set_direction(chip, offset, false); |
| } |
| |
| static int gpio_regmap_direction_output(struct gpio_chip *chip, |
| unsigned int offset, int value) |
| { |
| gpio_regmap_set(chip, offset, value); |
| |
| return gpio_regmap_set_direction(chip, offset, true); |
| } |
| |
| void *gpio_regmap_get_drvdata(struct gpio_regmap *gpio) |
| { |
| return gpio->driver_data; |
| } |
| EXPORT_SYMBOL_GPL(gpio_regmap_get_drvdata); |
| |
| /** |
| * gpio_regmap_register() - Register a generic regmap GPIO controller |
| * @config: configuration for gpio_regmap |
| * |
| * Return: A pointer to the registered gpio_regmap or ERR_PTR error value. |
| */ |
| struct gpio_regmap *gpio_regmap_register(const struct gpio_regmap_config *config) |
| { |
| struct gpio_regmap *gpio; |
| struct gpio_chip *chip; |
| int ret; |
| |
| if (!config->parent) |
| return ERR_PTR(-EINVAL); |
| |
| if (!config->ngpio) |
| return ERR_PTR(-EINVAL); |
| |
| /* we need at least one */ |
| if (!config->reg_dat_base && !config->reg_set_base) |
| return ERR_PTR(-EINVAL); |
| |
| /* if we have a direction register we need both input and output */ |
| if ((config->reg_dir_out_base || config->reg_dir_in_base) && |
| (!config->reg_dat_base || !config->reg_set_base)) |
| return ERR_PTR(-EINVAL); |
| |
| /* we don't support having both registers simultaneously for now */ |
| if (config->reg_dir_out_base && config->reg_dir_in_base) |
| return ERR_PTR(-EINVAL); |
| |
| gpio = kzalloc(sizeof(*gpio), GFP_KERNEL); |
| if (!gpio) |
| return ERR_PTR(-ENOMEM); |
| |
| gpio->parent = config->parent; |
| gpio->driver_data = config->drvdata; |
| gpio->regmap = config->regmap; |
| gpio->ngpio_per_reg = config->ngpio_per_reg; |
| gpio->reg_stride = config->reg_stride; |
| gpio->reg_mask_xlate = config->reg_mask_xlate; |
| gpio->reg_dat_base = config->reg_dat_base; |
| gpio->reg_set_base = config->reg_set_base; |
| gpio->reg_clr_base = config->reg_clr_base; |
| gpio->reg_dir_in_base = config->reg_dir_in_base; |
| gpio->reg_dir_out_base = config->reg_dir_out_base; |
| |
| /* if not set, assume there is only one register */ |
| if (!gpio->ngpio_per_reg) |
| gpio->ngpio_per_reg = config->ngpio; |
| |
| /* if not set, assume they are consecutive */ |
| if (!gpio->reg_stride) |
| gpio->reg_stride = 1; |
| |
| if (!gpio->reg_mask_xlate) |
| gpio->reg_mask_xlate = gpio_regmap_simple_xlate; |
| |
| chip = &gpio->gpio_chip; |
| chip->parent = config->parent; |
| chip->fwnode = config->fwnode; |
| chip->base = -1; |
| chip->ngpio = config->ngpio; |
| chip->names = config->names; |
| chip->label = config->label ?: dev_name(config->parent); |
| chip->can_sleep = regmap_might_sleep(config->regmap); |
| |
| chip->get = gpio_regmap_get; |
| if (gpio->reg_set_base && gpio->reg_clr_base) |
| chip->set = gpio_regmap_set_with_clear; |
| else if (gpio->reg_set_base) |
| chip->set = gpio_regmap_set; |
| |
| chip->get_direction = gpio_regmap_get_direction; |
| if (gpio->reg_dir_in_base || gpio->reg_dir_out_base) { |
| chip->direction_input = gpio_regmap_direction_input; |
| chip->direction_output = gpio_regmap_direction_output; |
| } |
| |
| ret = gpiochip_add_data(chip, gpio); |
| if (ret < 0) |
| goto err_free_gpio; |
| |
| if (config->irq_domain) { |
| ret = gpiochip_irqchip_add_domain(chip, config->irq_domain); |
| if (ret) |
| goto err_remove_gpiochip; |
| } |
| |
| return gpio; |
| |
| err_remove_gpiochip: |
| gpiochip_remove(chip); |
| err_free_gpio: |
| kfree(gpio); |
| return ERR_PTR(ret); |
| } |
| EXPORT_SYMBOL_GPL(gpio_regmap_register); |
| |
| /** |
| * gpio_regmap_unregister() - Unregister a generic regmap GPIO controller |
| * @gpio: gpio_regmap device to unregister |
| */ |
| void gpio_regmap_unregister(struct gpio_regmap *gpio) |
| { |
| gpiochip_remove(&gpio->gpio_chip); |
| kfree(gpio); |
| } |
| EXPORT_SYMBOL_GPL(gpio_regmap_unregister); |
| |
| static void devm_gpio_regmap_unregister(void *res) |
| { |
| gpio_regmap_unregister(res); |
| } |
| |
| /** |
| * devm_gpio_regmap_register() - resource managed gpio_regmap_register() |
| * @dev: device that is registering this GPIO device |
| * @config: configuration for gpio_regmap |
| * |
| * Managed gpio_regmap_register(). For generic regmap GPIO device registered by |
| * this function, gpio_regmap_unregister() is automatically called on driver |
| * detach. See gpio_regmap_register() for more information. |
| * |
| * Return: A pointer to the registered gpio_regmap or ERR_PTR error value. |
| */ |
| struct gpio_regmap *devm_gpio_regmap_register(struct device *dev, |
| const struct gpio_regmap_config *config) |
| { |
| struct gpio_regmap *gpio; |
| int ret; |
| |
| gpio = gpio_regmap_register(config); |
| |
| if (IS_ERR(gpio)) |
| return gpio; |
| |
| ret = devm_add_action_or_reset(dev, devm_gpio_regmap_unregister, gpio); |
| if (ret) |
| return ERR_PTR(ret); |
| |
| return gpio; |
| } |
| EXPORT_SYMBOL_GPL(devm_gpio_regmap_register); |
| |
| MODULE_AUTHOR("Michael Walle <michael@walle.cc>"); |
| MODULE_DESCRIPTION("GPIO generic regmap driver core"); |
| MODULE_LICENSE("GPL"); |