| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Driver for the Pericom PI4IOE5V6408 GPIO Expander. |
| * |
| * Copyright (C) 2018 Google, LLC. |
| */ |
| |
| #include <linux/gpio.h> |
| #include <linux/i2c.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/regmap.h> |
| |
| #define PI4IO_CHIP_ID 0x1 |
| #define PI4IO_IO_DIRECTION 0x3 |
| #define PI4IO_OUTPUT 0x5 |
| #define PI4IO_OUTPUT_HI_IMPEDANCE 0x7 |
| #define PI4IO_INPUT_STATUS 0xF |
| #define PI4IO_INTERRUPT_STATUS 0x13 |
| |
| #define PI4IO_CHIP_ID_VAL 0xA0 |
| #define PI4IO_CHIP_ID_MASK 0xFC |
| |
| #define PI4IO_DIRECTION_TO_GPIOD(x) ((x) ? GPIOF_DIR_OUT : GPIOF_DIR_IN) |
| #define GPIOD_DIRECTION_TO_PI4IO(x) ((x) == GPIOF_DIR_OUT ? 1 : 0) |
| |
| #define GPIO_OUT_LOW 0 |
| #define GPIO_OUT_HIGH 1 |
| |
| static bool pi4io_readable_reg(struct device *dev, unsigned int reg) |
| { |
| // All readable registers are odd-numbered. |
| return (reg % 2) == 1; |
| } |
| |
| static bool pi4io_writeable_reg(struct device *dev, unsigned int reg) |
| { |
| // All odd-numbered registers are writable except for 0xF. |
| if ((reg % 2) == 1) { |
| if (reg != PI4IO_INPUT_STATUS) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static bool pi4io_volatile_reg(struct device *dev, unsigned int reg) |
| { |
| if (reg == PI4IO_INPUT_STATUS || reg == PI4IO_INTERRUPT_STATUS) { |
| return true; |
| } |
| return false; |
| } |
| |
| static const struct regmap_config pi4io_regmap = { |
| .reg_bits = 8, |
| .val_bits = 8, |
| .max_register = 0x13, |
| .writeable_reg = pi4io_writeable_reg, |
| .readable_reg = pi4io_readable_reg, |
| .volatile_reg = pi4io_volatile_reg, |
| }; |
| |
| struct pi4io_priv { |
| struct i2c_client *i2c; |
| struct regmap *regmap; |
| struct gpio_chip gpio; |
| }; |
| |
| static int pi4io_gpio_get_direction(struct gpio_chip *chip, unsigned offset) |
| { |
| int ret, io_dir; |
| struct pi4io_priv *pi4io = gpiochip_get_data(chip); |
| struct device *dev = &pi4io->i2c->dev; |
| |
| ret = regmap_read(pi4io->regmap, PI4IO_IO_DIRECTION, &io_dir); |
| if (ret) { |
| dev_err(dev, "Failed to read I/O direction: %d", ret); |
| return ret; |
| } |
| |
| return PI4IO_DIRECTION_TO_GPIOD((io_dir >> offset) & 1); |
| } |
| |
| static int pi4io_gpio_set_direction( |
| struct gpio_chip *chip, unsigned offset, int direction) |
| { |
| int ret; |
| struct pi4io_priv *pi4io = gpiochip_get_data(chip); |
| struct device *dev = &pi4io->i2c->dev; |
| |
| ret = regmap_update_bits(pi4io->regmap, PI4IO_IO_DIRECTION, 1 << offset, |
| GPIOD_DIRECTION_TO_PI4IO(direction) << offset); |
| if (ret) { |
| dev_err(dev, "Failed to set direction: %d", ret); |
| return ret; |
| } |
| |
| // We desire the hi-impedance state to track the output state. |
| ret = regmap_update_bits(pi4io->regmap, PI4IO_OUTPUT_HI_IMPEDANCE, |
| 1 << offset, direction << offset); |
| |
| return ret; |
| } |
| |
| static int pi4io_gpio_get(struct gpio_chip *chip, unsigned offset) |
| { |
| int ret, out; |
| struct pi4io_priv *pi4io = gpiochip_get_data(chip); |
| struct device *dev = &pi4io->i2c->dev; |
| |
| ret = regmap_read(pi4io->regmap, PI4IO_OUTPUT, &out); |
| if (ret) { |
| dev_err(dev, "Failed to read output: %d", ret); |
| return ret; |
| } |
| |
| if (out & (1 << offset)) { |
| return 1; |
| } |
| return 0; |
| } |
| |
| static void pi4io_gpio_set(struct gpio_chip *chip, unsigned offset, int value) |
| { |
| int ret; |
| struct pi4io_priv *pi4io = gpiochip_get_data(chip); |
| struct device *dev = &pi4io->i2c->dev; |
| |
| ret = regmap_update_bits( |
| pi4io->regmap, PI4IO_OUTPUT, 1 << offset, value << offset); |
| if (ret) { |
| dev_err(dev, "Failed to write output: %d", ret); |
| } |
| } |
| |
| static int pi4io_gpio_direction_input(struct gpio_chip *chip, unsigned offset) |
| { |
| return pi4io_gpio_set_direction(chip, offset, GPIOF_DIR_IN); |
| } |
| |
| static int pi4io_gpio_direction_output( |
| struct gpio_chip *chip, unsigned offset, int value) |
| { |
| int ret; |
| struct pi4io_priv *pi4io = gpiochip_get_data(chip); |
| struct device *dev = &pi4io->i2c->dev; |
| ret = pi4io_gpio_set_direction(chip, offset, GPIOF_DIR_OUT); |
| if (ret) { |
| dev_err(dev, "Failed to set direction: %d", ret); |
| return ret; |
| } |
| pi4io_gpio_set(chip, offset, value); |
| return 0; |
| } |
| |
| static int pi4io_gpio_setup(struct pi4io_priv *pi4io) |
| { |
| int ret; |
| struct device *dev = &pi4io->i2c->dev; |
| struct gpio_chip *gc = &pi4io->gpio; |
| gc->ngpio = 8; |
| gc->label = pi4io->i2c->name; |
| gc->parent = &pi4io->i2c->dev; |
| gc->owner = THIS_MODULE; |
| gc->base = -1; |
| gc->can_sleep = true; |
| |
| gc->get_direction = pi4io_gpio_get_direction; |
| gc->direction_input = pi4io_gpio_direction_input; |
| gc->direction_output = pi4io_gpio_direction_output; |
| gc->get = pi4io_gpio_get; |
| gc->set = pi4io_gpio_set; |
| |
| ret = devm_gpiochip_add_data(dev, gc, pi4io); |
| if (ret) { |
| dev_err(dev, "devm_gpiochip_add_data failed: %d", ret); |
| return ret; |
| } |
| return 0; |
| } |
| |
| static int pi4io_probe( |
| struct i2c_client *client, const struct i2c_device_id *id) |
| { |
| int ret, chip_id; |
| struct device *dev = &client->dev; |
| struct pi4io_priv *pi4io; |
| |
| pi4io = devm_kzalloc(dev, sizeof(struct pi4io_priv), GFP_KERNEL); |
| if (!pi4io) { |
| return -ENOMEM; |
| } |
| |
| i2c_set_clientdata(client, pi4io); |
| pi4io->i2c = client; |
| |
| pi4io->regmap = devm_regmap_init_i2c(client, &pi4io_regmap); |
| ret = regmap_read(pi4io->regmap, PI4IO_CHIP_ID, &chip_id); |
| if (ret < 0) { |
| dev_err(dev, "Failed to read Chip ID: %d", ret); |
| return ret; |
| } |
| |
| if ((chip_id & PI4IO_CHIP_ID_MASK) != PI4IO_CHIP_ID_VAL) { |
| dev_err(dev, "Invalid Chip ID!"); |
| return -EINVAL; |
| } |
| |
| ret = pi4io_gpio_setup(pi4io); |
| if (ret < 0) { |
| dev_err(dev, "Failed to setup GPIOs: %d", ret); |
| return ret; |
| } |
| |
| dev_dbg(dev, "PI4IO probe finished"); |
| return 0; |
| } |
| |
| static const struct i2c_device_id pi4io_id_table[] = { { "pi4io", 0 }, {} }; |
| MODULE_DEVICE_TABLE(i2c, pi4io_id_table); |
| |
| static struct i2c_driver pi4io_driver = { |
| .driver = { |
| .name = "pi4io-gpio", |
| }, |
| .probe = pi4io_probe, |
| .id_table = pi4io_id_table, |
| }; |
| module_i2c_driver(pi4io_driver); |
| |
| MODULE_AUTHOR("Alex Van Damme <atv@google.com>"); |
| MODULE_DESCRIPTION("PI4IOE5V6408"); |
| MODULE_LICENSE("GPL v2"); |