| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) 2021 Toradex |
| * Copyright (C) 2016 Broadcom |
| */ |
| |
| /** |
| * DOC: FXL6408 I2C to GPIO expander. |
| * |
| * This chip has 8 GPIO lines out of it, and is controlled by an I2C |
| * bus (a pair of lines), providing 4x expansion of GPIO lines. It |
| * also provides an interrupt line out for notifying of state changes. |
| * |
| * Any preconfigured state will be left in place until the GPIO lines |
| * get activated. At power on, everything is treated as an input, |
| * default input is HIGH and pulled-up, all interrupts are masked. |
| * |
| * Documentation can be found at: |
| * ------------------------------ |
| * |
| * https://www.fairchildsemi.com/datasheets/FX/FXL6408.pdf |
| * |
| * This driver bases on: |
| * --------------------- |
| * |
| * - the original driver by Eric Anholt <eric@anholt.net>: |
| * https://patchwork.kernel.org/patch/9148419/ |
| * - the Toradex version by Max Krummenacher <max.krummenacher@toradex.com>: |
| * http://git.toradex.com/cgit/linux-toradex.git/tree/drivers/gpio/gpio-fxl6408.c?h=toradex_5.4-2.3.x-imx |
| * - the U-boot PCA953x driver by Peng Fan <van.freenix@gmail.com>: |
| * drivers/gpio/pca953x_gpio.c |
| * |
| * TODO: |
| * - Add interrupts support |
| * - Replace deprecated callbacks direction_input/output() with set_flags() |
| */ |
| |
| #include <asm-generic/gpio.h> |
| #include <asm/global_data.h> |
| #include <common.h> |
| #include <dm.h> |
| #include <dm/device_compat.h> |
| #include <dt-bindings/gpio/gpio.h> |
| #include <i2c.h> |
| #include <linux/bitops.h> |
| #include <log.h> |
| |
| #define REG_DEVID_CTRL 0x1 |
| # define SW_RST BIT(0) |
| # define RST_INT BIT(1) |
| /** 0b101 is the Manufacturer's ID assigned to Fairchild by Nokia */ |
| # define MF_ID_FAIRCHILD 5 |
| |
| /** Bits set here indicate that the GPIO is an output */ |
| #define REG_IO_DIR 0x3 |
| |
| /** |
| * REG_OUT_STATE - a high-output state register address |
| * |
| * Bits set here, when the corresponding bit of REG_IO_DIR is set, |
| * drive the output high instead of low. |
| */ |
| #define REG_OUT_STATE 0x5 |
| |
| /** Bits here make the output High-Z, instead of the OUTPUT value */ |
| #define REG_OUT_HIGH_Z 0x7 |
| |
| /** |
| * REG_IN_DEFAULT_STATE - an interrupt state register address |
| * |
| * Bits here define the expected input state of the GPIO. |
| * INTERRUPT_STATUS bits will be set when the INPUT transitions away |
| * from this value. |
| */ |
| #define REG_IN_DEFAULT_STATE 0x9 |
| |
| /** |
| * REG_PULL_ENABLE - a pull-up/down enable state register address |
| * |
| * Bits here enable either pull up or pull down according to |
| * REG_PULL_MODE. |
| */ |
| #define REG_PULL_ENABLE 0xb |
| |
| /** |
| * REG_PULL_MODE - a pull-up/pull-down mode state register address |
| * |
| * Bits set here selects a pull-up/pull-down state of pin, which |
| * is configured as Input and the corresponding REG_PULL_ENABLE bit is |
| * set. |
| */ |
| #define REG_PULL_MODE 0xd |
| |
| /** Returns the current status (1 = HIGH) of the input pins */ |
| #define REG_IN_STATUS 0xf |
| |
| /** Mask of pins which can generate interrupts */ |
| #define REG_INT_MASK 0x11 |
| |
| /** Mask of pins which have generated an interrupt. Cleared on read */ |
| #define REG_INT_STATUS 0x13 |
| |
| /* Manufacturer's ID getting from Device ID & Ctrl register */ |
| enum { |
| MF_ID_MASK = GENMASK(7, 5), |
| MF_ID_SHIFT = 5, |
| }; |
| |
| /* Firmware revision getting from Device ID & Ctrl register */ |
| enum { |
| FW_REV_MASK = GENMASK(4, 2), |
| FW_REV_SHIFT = 2, |
| }; |
| |
| enum io_direction { |
| DIR_IN = 0, |
| DIR_OUT = 1, |
| }; |
| |
| /** |
| * struct fxl6408_info - Data for fxl6408 |
| * |
| * @dev: udevice structure for the device |
| * @addr: i2c slave address |
| * @device_id: hold the value of device id register |
| * @reg_io_dir: hold the value of direction register |
| * @reg_output: hold the value of output register |
| */ |
| struct fxl6408_info { |
| struct udevice *dev; |
| int addr; |
| u8 device_id; |
| u8 reg_io_dir; |
| u8 reg_output; |
| }; |
| |
| static inline int fxl6408_write(struct udevice *dev, int reg, u8 val) |
| { |
| return dm_i2c_write(dev, reg, &val, 1); |
| } |
| |
| static int fxl6408_read(struct udevice *dev, int reg) |
| { |
| int ret; |
| u8 tmp; |
| |
| ret = dm_i2c_read(dev, reg, &tmp, 1); |
| if (!ret) |
| ret = tmp; |
| |
| return ret; |
| } |
| |
| /** |
| * fxl6408_is_output() - check whether the gpio configures as either |
| * output or input. |
| * |
| * @dev: an instance of a driver |
| * @offset: a gpio offset |
| * |
| * Return: false - input, true - output. |
| */ |
| static bool fxl6408_is_output(struct udevice *dev, int offset) |
| { |
| struct fxl6408_info *info = dev_get_plat(dev); |
| |
| return info->reg_io_dir & BIT(offset); |
| } |
| |
| static int fxl6408_get_value(struct udevice *dev, uint offset) |
| { |
| int ret, reg = fxl6408_is_output(dev, offset) ? REG_OUT_STATE : REG_IN_STATUS; |
| |
| ret = fxl6408_read(dev, reg); |
| if (ret < 0) |
| return ret; |
| |
| return !!(ret & BIT(offset)); |
| } |
| |
| static int fxl6408_set_value(struct udevice *dev, uint offset, int value) |
| { |
| struct fxl6408_info *info = dev_get_plat(dev); |
| u8 val; |
| int ret; |
| |
| if (value) |
| val = info->reg_output | BIT(offset); |
| else |
| val = info->reg_output & ~BIT(offset); |
| |
| ret = fxl6408_write(dev, REG_OUT_STATE, val); |
| if (ret < 0) |
| return ret; |
| |
| info->reg_output = val; |
| |
| return 0; |
| } |
| |
| static int fxl6408_set_direction(struct udevice *dev, uint offset, |
| enum io_direction dir) |
| { |
| struct fxl6408_info *info = dev_get_plat(dev); |
| u8 val; |
| int ret; |
| |
| if (dir == DIR_IN) |
| val = info->reg_io_dir & ~BIT(offset); |
| else |
| val = info->reg_io_dir | BIT(offset); |
| |
| ret = fxl6408_write(dev, REG_IO_DIR, val); |
| if (ret < 0) |
| return ret; |
| |
| info->reg_io_dir = val; |
| |
| return 0; |
| } |
| |
| static int fxl6408_direction_input(struct udevice *dev, uint offset) |
| { |
| return fxl6408_set_direction(dev, offset, DIR_IN); |
| } |
| |
| static int fxl6408_direction_output(struct udevice *dev, uint offset, int value) |
| { |
| int ret; |
| |
| /* Configure output value */ |
| ret = fxl6408_set_value(dev, offset, value); |
| if (ret < 0) |
| return ret; |
| |
| /* Configure direction as output */ |
| fxl6408_set_direction(dev, offset, DIR_OUT); |
| |
| return 0; |
| } |
| |
| static int fxl6408_get_function(struct udevice *dev, uint offset) |
| { |
| if (fxl6408_is_output(dev, offset)) |
| return GPIOF_OUTPUT; |
| |
| return GPIOF_INPUT; |
| } |
| |
| static int fxl6408_xlate(struct udevice *dev, struct gpio_desc *desc, |
| struct ofnode_phandle_args *args) |
| { |
| desc->offset = args->args[0]; |
| desc->flags = args->args[1] & GPIO_ACTIVE_LOW ? GPIOD_ACTIVE_LOW : 0; |
| |
| return 0; |
| } |
| |
| static const struct dm_gpio_ops fxl6408_ops = { |
| .direction_input = fxl6408_direction_input, |
| .direction_output = fxl6408_direction_output, |
| .get_value = fxl6408_get_value, |
| .set_value = fxl6408_set_value, |
| .get_function = fxl6408_get_function, |
| .xlate = fxl6408_xlate, |
| }; |
| |
| static int fxl6408_probe(struct udevice *dev) |
| { |
| struct fxl6408_info *info = dev_get_plat(dev); |
| struct gpio_dev_priv *uc_priv = dev_get_uclass_priv(dev); |
| char bank_name[32], *tmp_str; |
| int addr, ret, size; |
| u32 val32; |
| |
| addr = dev_read_addr(dev); |
| if (addr == 0) |
| return -EINVAL; |
| |
| info->addr = addr; |
| |
| /* |
| * Check the device ID register to see if it's responding. |
| * This also clears RST_INT as a side effect, so we won't get |
| * the "we've been power cycled" interrupt once interrupts |
| * being enabled. |
| */ |
| ret = fxl6408_read(dev, REG_DEVID_CTRL); |
| if (ret < 0) { |
| dev_err(dev, "FXL6408 probe returned %d\n", ret); |
| return ret; |
| } |
| |
| if ((ret & MF_ID_MASK) >> MF_ID_SHIFT != MF_ID_FAIRCHILD) { |
| dev_err(dev, "FXL6408 probe: wrong Manufacturer's ID: 0x%02x\n", ret); |
| return -ENXIO; |
| } |
| info->device_id = ret; |
| |
| /* |
| * Disable High-Z of outputs, so that the OUTPUT updates |
| * actually take effect. |
| */ |
| ret = fxl6408_write(dev, REG_OUT_HIGH_Z, (u8)0); |
| if (ret < 0) { |
| dev_err(dev, "Error writing High-Z register\n"); |
| return ret; |
| } |
| |
| /* |
| * If configured, set initial output state and direction, |
| * otherwise read them from the chip. |
| */ |
| if (dev_read_u32(dev, "initial_io_dir", &val32)) { |
| ret = fxl6408_read(dev, REG_IO_DIR); |
| if (ret < 0) { |
| dev_err(dev, "Error reading direction register\n"); |
| return ret; |
| } |
| info->reg_io_dir = ret; |
| } else { |
| info->reg_io_dir = val32 & 0xFF; |
| ret = fxl6408_write(dev, REG_IO_DIR, info->reg_io_dir); |
| if (ret < 0) { |
| dev_err(dev, "Error setting direction register\n"); |
| return ret; |
| } |
| } |
| |
| if (dev_read_u32(dev, "initial_output", &val32)) { |
| ret = fxl6408_read(dev, REG_OUT_STATE); |
| if (ret < 0) { |
| dev_err(dev, "Error reading output register\n"); |
| return ret; |
| } |
| info->reg_output = ret; |
| } else { |
| info->reg_output = val32 & 0xFF; |
| ret = fxl6408_write(dev, REG_OUT_STATE, info->reg_output); |
| if (ret < 0) { |
| dev_err(dev, "Error setting output register\n"); |
| return ret; |
| } |
| } |
| |
| tmp_str = (char *)dev_read_prop(dev, "bank-name", &size); |
| if (tmp_str) { |
| snprintf(bank_name, sizeof(bank_name), "%s@%x_", tmp_str, |
| info->addr); |
| } else { |
| snprintf(bank_name, sizeof(bank_name), "gpio@%x_", info->addr); |
| } |
| |
| tmp_str = strdup(bank_name); |
| if (!tmp_str) |
| return -ENOMEM; |
| |
| uc_priv->bank_name = tmp_str; |
| uc_priv->gpio_count = dev_get_driver_data(dev); |
| uc_priv->gpio_base = -1; |
| |
| dev_dbg(dev, "%s (FW rev. %d) is ready\n", bank_name, |
| (info->device_id & FW_REV_MASK) >> FW_REV_SHIFT); |
| |
| return 0; |
| } |
| |
| static const struct udevice_id fxl6408_ids[] = { |
| { .compatible = "fcs,fxl6408", .data = 8 }, |
| { } |
| }; |
| |
| U_BOOT_DRIVER(fxl6408_gpio) = { |
| .name = "fxl6408_gpio", |
| .id = UCLASS_GPIO, |
| .ops = &fxl6408_ops, |
| .probe = fxl6408_probe, |
| .of_match = fxl6408_ids, |
| .plat_auto = sizeof(struct fxl6408_info), |
| }; |