diff options
Diffstat (limited to 'drivers/misc/ti_fpc202.c')
| -rw-r--r-- | drivers/misc/ti_fpc202.c | 739 |
1 files changed, 739 insertions, 0 deletions
diff --git a/drivers/misc/ti_fpc202.c b/drivers/misc/ti_fpc202.c new file mode 100644 index 000000000..2aac83ec4 --- /dev/null +++ b/drivers/misc/ti_fpc202.c @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ti_fpc202.c - FPC202 Dual Port Controller driver + * + * Copyright (C) 2024 Bootlin + * + */ + +#include <linux/cleanup.h> +#include <linux/device/devres.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/i2c-atr.h> +#include <linux/gpio/consumer.h> +#include <linux/gpio/driver.h> +#include <linux/gpio/machine.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/math.h> +#include <linux/types.h> + +#define FPC202_NUM_PORTS 2 +#define FPC202_ALIASES_PER_PORT 2 + +/* + * GPIO: port mapping + * + * 0: P0_S0_IN_A + * 1: P0_S1_IN_A + * 2: P1_S0_IN_A + * 3: P1_S1_IN_A + * 4: P0_S0_IN_B + * ... + * 8: P0_S0_IN_C + * ... + * 12: P0_S0_OUT_A + * ... + * 16: P0_S0_OUT_B + * ... + * 19: P1_S1_OUT_B + * + * Ports with optional LED control: + * + * 20: P0_S0_OUT_C (P0_S0_LED1) + * ... + * 23: P1_S1_OUT_C (P1_S1_LED1) + * 24: P0_S0_OUT_D (P0_S0_LED2 + * ... + * 27: P1_S1_OUT_D (P1_S1_LED2) + * + */ + +#define FPC202_GPIO_COUNT 28 +#define FPC202_GPIO_P0_S0_IN_B 4 +#define FPC202_GPIO_P0_S0_OUT_A 12 +#define FPC202_GPIO_P0_S0_OUT_C 20 +#define FPC202_GPIO_P0_S0_OUT_D 24 + +#define FPC202_REG_IN_A_INT 0x6 +#define FPC202_REG_IN_C_IN_B 0x7 +#define FPC202_REG_OUT_A_OUT_B 0x8 +#define FPC202_REG_OUT_C_OUT_D 0x9 + +#define FPC202_REG_OUT_A_OUT_B_VAL 0xa + +#define FPC202_LED_COUNT 8 + +/* There are four LED GPIO mode registers which manage two GPIOs each. */ +#define FPC202_REG_LED_MODE(offset) (0x1a + 0x20 * ((offset) % 4)) + +/* LED1 GPIOs (*_OUT_C) are configured in bits 1:0, LED2 GPIOs (*_OUT_D) in bits 3:2. */ +#define FPC202_LED_MODE_SHIFT(offset) ((offset) < FPC202_GPIO_P0_S0_OUT_D ? 0 : 2) +#define FPC202_LED_MODE_MASK(offset) (GENMASK(1, 0) << FPC202_LED_MODE_SHIFT(offset)) + +/* There is one PWM control register for each GPIO LED */ +#define FPC202_REG_LED_PWM(offset) \ + (((offset) < FPC202_GPIO_P0_S0_OUT_D ? 0x14 : 0x15) + 0x20 * ((offset) % 4)) + +/* There are two blink delay registers (on/off time) for each GPIO LED */ +#define FPC202_REG_LED_BLINK_ON(offset) \ + (((offset) < FPC202_GPIO_P0_S0_OUT_D ? 0x16 : 0x18) + 0x20 * ((offset) % 4)) +#define FPC202_REG_LED_BLINK_OFF(offset) (FPC202_REG_LED_BLINK_ON(offset) + 1) + +/* The actual hardware precision is 2.5ms but since the LED API doesn't handle sub-millisecond + * timesteps this is rounded up to 5ms + */ +#define FPC202_LED_BLINK_PRECISION 5UL + +#define FPC202_LED_MAX_BRIGHTNESS 255 + +#define FPC202_REG_MOD_DEV(port, dev) (0xb4 + ((port) * 4) + (dev)) +#define FPC202_REG_AUX_DEV(port, dev) (0xb6 + ((port) * 4) + (dev)) + +/* + * The FPC202 doesn't support turning off address translation on a single port. + * So just set an invalid I2C address as the translation target when no client + * address is attached. + */ +#define FPC202_REG_DEV_INVALID 0 + +/* Even aliases are assigned to device 0 and odd aliases to device 1 */ +#define fpc202_dev_num_from_alias(alias) ((alias) % 2) + +enum fpc202_led_mode { + FPC202_LED_MODE_OFF = 0, + FPC202_LED_MODE_ON = 1, + FPC202_LED_MODE_PWM = 2, + FPC202_LED_MODE_BLINK = 3, +}; + +struct fpc202_led { + int offset; + struct led_classdev led_cdev; + struct fpc202_priv *priv; + struct gpio_desc *gpio; + enum fpc202_led_mode mode; +}; + +struct fpc202_priv { + struct i2c_client *client; + struct i2c_atr *atr; + struct gpio_desc *en_gpio; + struct gpio_chip gpio; + struct fpc202_led leds[FPC202_LED_COUNT]; + + /* Lock REG_MOD/AUX_DEV and addr_caches during attach/detach */ + struct mutex reg_dev_lock; + + /* Lock LED mode select register during accesses */ + struct mutex led_mode_lock; + + /* Cached device addresses for both ports and their devices */ + u8 addr_caches[2][2]; + + /* Keep track of which ports were probed */ + DECLARE_BITMAP(probed_ports, FPC202_NUM_PORTS); +}; + +static void fpc202_fill_alias_table(struct i2c_client *client, u16 *aliases, int port_id) +{ + u16 first_alias; + int i; + + /* + * There is a predefined list of aliases for each FPC202 I2C + * self-address. This allows daisy-chained FPC202 units to + * automatically take on different sets of aliases. + * Each port of an FPC202 unit is assigned two aliases from this list. + */ + first_alias = 0x10 + 4 * port_id + 8 * ((u16)client->addr - 2); + + for (i = 0; i < FPC202_ALIASES_PER_PORT; i++) + aliases[i] = first_alias + i; +} + +static int fpc202_gpio_get_dir(int offset) +{ + return offset < FPC202_GPIO_P0_S0_OUT_A ? GPIO_LINE_DIRECTION_IN : GPIO_LINE_DIRECTION_OUT; +} + +static int fpc202_gpio_has_led_caps(int offset) +{ + return offset >= FPC202_GPIO_P0_S0_OUT_C; +} + +static int fpc202_read(struct fpc202_priv *priv, u8 reg) +{ + int val; + + val = i2c_smbus_read_byte_data(priv->client, reg); + return val; +} + +static int fpc202_write(struct fpc202_priv *priv, u8 reg, u8 value) +{ + return i2c_smbus_write_byte_data(priv->client, reg, value); +} + +static void fpc202_set_enable(struct fpc202_priv *priv, int enable) +{ + if (!priv->en_gpio) + return; + + gpiod_set_value(priv->en_gpio, enable); +} + +static int fpc202_led_mode_write(struct fpc202_priv *priv, + int offset, + enum fpc202_led_mode mode) +{ + u8 val, reg = FPC202_REG_LED_MODE(offset); + int ret; + + guard(mutex)(&priv->led_mode_lock); + + ret = fpc202_read(priv, reg); + if (ret < 0) { + dev_err(&priv->client->dev, "failed to read LED mode %d! err %d\n", + offset, ret); + return ret; + } + + val = (u8)ret & ~FPC202_LED_MODE_MASK(offset); + val |= mode << FPC202_LED_MODE_SHIFT(offset); + + return fpc202_write(priv, reg, val); +} + +static int fpc202_led_mode_set(struct fpc202_led *led, enum fpc202_led_mode mode) +{ + struct fpc202_priv *priv = led->priv; + + led->mode = mode; + + return fpc202_led_mode_write(priv, led->offset, mode); +} + +static int fpc202_gpio_set(struct gpio_chip *chip, unsigned int offset, + int value) +{ + struct fpc202_priv *priv = gpiochip_get_data(chip); + int ret; + u8 val; + + if (fpc202_gpio_has_led_caps(offset)) { + ret = fpc202_led_mode_write(priv, offset, + value ? FPC202_LED_MODE_ON : FPC202_LED_MODE_OFF); + if (ret < 0) + dev_err(&priv->client->dev, "Failed to set GPIO %d LED mode! err %d\n", + offset, ret); + + return ret; + } + + ret = fpc202_read(priv, FPC202_REG_OUT_A_OUT_B_VAL); + if (ret < 0) { + dev_err(&priv->client->dev, "Failed to set GPIO %d value! err %d\n", offset, ret); + return ret; + } + + val = (u8)ret; + + if (value) + val |= BIT(offset - FPC202_GPIO_P0_S0_OUT_A); + else + val &= ~BIT(offset - FPC202_GPIO_P0_S0_OUT_A); + + return fpc202_write(priv, FPC202_REG_OUT_A_OUT_B_VAL, val); +} + +static int fpc202_gpio_get(struct gpio_chip *chip, unsigned int offset) +{ + struct fpc202_priv *priv = gpiochip_get_data(chip); + u8 reg, bit; + int ret; + + if (offset < FPC202_GPIO_P0_S0_IN_B) { + reg = FPC202_REG_IN_A_INT; + bit = BIT(4 + offset); + } else if (offset < FPC202_GPIO_P0_S0_OUT_A) { + reg = FPC202_REG_IN_C_IN_B; + bit = BIT(offset - FPC202_GPIO_P0_S0_IN_B); + } else if (!fpc202_gpio_has_led_caps(offset)) { + reg = FPC202_REG_OUT_A_OUT_B_VAL; + bit = BIT(offset - FPC202_GPIO_P0_S0_OUT_A); + } else { + return -EOPNOTSUPP; + } + + ret = fpc202_read(priv, reg); + if (ret < 0) + return ret; + + return !!(((u8)ret) & bit); +} + +static int fpc202_gpio_direction_input(struct gpio_chip *chip, unsigned int offset) +{ + if (fpc202_gpio_get_dir(offset) == GPIO_LINE_DIRECTION_OUT) + return -EINVAL; + + return 0; +} + +static int fpc202_gpio_direction_output(struct gpio_chip *chip, unsigned int offset, + int value) +{ + struct fpc202_priv *priv = gpiochip_get_data(chip); + u8 reg, val, bit; + int ret; + + if (fpc202_gpio_get_dir(offset) == GPIO_LINE_DIRECTION_IN) + return -EINVAL; + + fpc202_gpio_set(chip, offset, value); + + if (fpc202_gpio_has_led_caps(offset)) { + reg = FPC202_REG_OUT_C_OUT_D; + bit = BIT(offset - FPC202_GPIO_P0_S0_OUT_C); + } else { + reg = FPC202_REG_OUT_A_OUT_B; + bit = BIT(offset - FPC202_GPIO_P0_S0_OUT_A); + } + + ret = fpc202_read(priv, reg); + if (ret < 0) + return ret; + + val = (u8)ret | bit; + + return fpc202_write(priv, reg, val); +} + +/* + * Set the translation table entry associated with a port and device number. + * + * Each downstream port of the FPC202 has two fixed aliases corresponding to + * device numbers 0 and 1. If one of these aliases is found in an incoming I2C + * transfer, it will be translated to the address given by the corresponding + * translation table entry. + */ +static int fpc202_write_dev_addr(struct fpc202_priv *priv, u32 port_id, int dev_num, u16 addr) +{ + int ret, reg_mod, reg_aux; + u8 val; + + guard(mutex)(&priv->reg_dev_lock); + + reg_mod = FPC202_REG_MOD_DEV(port_id, dev_num); + reg_aux = FPC202_REG_AUX_DEV(port_id, dev_num); + val = addr & 0x7f; + + ret = fpc202_write(priv, reg_mod, val); + if (ret) + return ret; + + /* + * The FPC202 datasheet is unclear about the role of the AUX registers. + * Empirically, writing to them as well seems to be necessary for + * address translation to function properly. + */ + ret = fpc202_write(priv, reg_aux, val); + + priv->addr_caches[port_id][dev_num] = val; + + return ret; +} + +static int fpc202_attach_addr(struct i2c_atr *atr, u32 chan_id, + u16 addr, u16 alias) +{ + struct fpc202_priv *priv = i2c_atr_get_driver_data(atr); + + dev_dbg(&priv->client->dev, "attaching address 0x%02x to alias 0x%02x\n", addr, alias); + + return fpc202_write_dev_addr(priv, chan_id, fpc202_dev_num_from_alias(alias), addr); +} + +static void fpc202_detach_addr(struct i2c_atr *atr, u32 chan_id, + u16 addr) +{ + struct fpc202_priv *priv = i2c_atr_get_driver_data(atr); + int dev_num, val; + + for (dev_num = 0; dev_num < 2; dev_num++) { + mutex_lock(&priv->reg_dev_lock); + + val = priv->addr_caches[chan_id][dev_num]; + + mutex_unlock(&priv->reg_dev_lock); + + if (val == (addr & 0x7f)) { + fpc202_write_dev_addr(priv, chan_id, dev_num, FPC202_REG_DEV_INVALID); + return; + } + } +} + +static const struct i2c_atr_ops fpc202_atr_ops = { + .attach_addr = fpc202_attach_addr, + .detach_addr = fpc202_detach_addr, +}; + +static struct fpc202_led *fpc202_cdev_to_led(struct led_classdev *cdev) +{ + return container_of(cdev, struct fpc202_led, led_cdev); +} + +static struct fpc202_led *fpc202_led_get(struct fpc202_priv *priv, int offset) +{ + return &priv->leds[offset - FPC202_GPIO_P0_S0_OUT_C]; +} + +static int fpc202_led_blink_set(struct led_classdev *cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct fpc202_led *led = fpc202_cdev_to_led(cdev); + struct fpc202_priv *priv = led->priv; + unsigned long val; + int ret; + + if (*delay_on == 0 && *delay_off == 0) { + *delay_on = 250; + *delay_off = 250; + } else { + if (*delay_on % FPC202_LED_BLINK_PRECISION) + *delay_on = roundup(*delay_on, FPC202_LED_BLINK_PRECISION); + + if (*delay_off % FPC202_LED_BLINK_PRECISION) + *delay_off = roundup(*delay_off, FPC202_LED_BLINK_PRECISION); + } + + /* Multiply the duration by two, since the actual precision is 2.5ms not 5ms*/ + val = 2 * (*delay_on / FPC202_LED_BLINK_PRECISION); + if (val > 255) { + val = 255; + *delay_on = (val / 2) * FPC202_LED_BLINK_PRECISION; + } + + ret = fpc202_write(priv, FPC202_REG_LED_BLINK_ON(led->offset), val); + if (ret) { + dev_err(&priv->client->dev, + "Failed to set blink on duration for LED %d, err %d\n", + led->offset, ret); + return ret; + } + + val = 2 * (*delay_off / FPC202_LED_BLINK_PRECISION); + if (val > 255) { + val = 255; + *delay_off = (val / 2) * FPC202_LED_BLINK_PRECISION; + } + + ret = fpc202_write(priv, FPC202_REG_LED_BLINK_OFF(led->offset), val); + if (ret) { + dev_err(&priv->client->dev, + "Failed to set blink off duration for LED %d, err %d\n", + led->offset, ret); + return ret; + } + + return fpc202_led_mode_set(led, FPC202_LED_MODE_BLINK); +} + +static enum led_brightness fpc202_led_brightness_get(struct led_classdev *cdev) +{ + struct fpc202_led *led = fpc202_cdev_to_led(cdev); + + if (led->mode == FPC202_LED_MODE_OFF) + return LED_OFF; + + return LED_ON; +} + +static int fpc202_led_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct fpc202_led *led = fpc202_cdev_to_led(cdev); + struct fpc202_priv *priv = led->priv; + int ret; + + if (!brightness) + return fpc202_led_mode_set(led, FPC202_LED_MODE_OFF); + + if (led->mode != FPC202_LED_MODE_BLINK) { + if (brightness == FPC202_LED_MAX_BRIGHTNESS) + return fpc202_led_mode_set(led, FPC202_LED_MODE_ON); + + ret = fpc202_led_mode_set(led, FPC202_LED_MODE_PWM); + if (ret) { + dev_err(&priv->client->dev, "Failed to set LED %d mode, err %d\n", + led->offset, ret); + return ret; + } + } + + return fpc202_write(priv, FPC202_REG_LED_PWM(led->offset), brightness); +} + +static int fpc202_register_led(struct fpc202_priv *priv, int offset, + struct device_node *led_handle) +{ + struct fpc202_led *led = fpc202_led_get(priv, offset); + struct device *dev = &priv->client->dev; + struct led_init_data init_data = { }; + int ret = 0; + + led->priv = priv; + led->offset = offset; + led->led_cdev.max_brightness = FPC202_LED_MAX_BRIGHTNESS; + led->led_cdev.brightness_set_blocking = fpc202_led_brightness_set; + led->led_cdev.brightness_get = fpc202_led_brightness_get; + led->led_cdev.blink_set = fpc202_led_blink_set; + + init_data.fwnode = of_fwnode_handle(led_handle); + init_data.default_label = NULL; + init_data.devicename = NULL; + init_data.devname_mandatory = false; + + ret = fpc202_led_mode_set(led, FPC202_LED_MODE_OFF); + if (ret) { + dev_err(dev, "Failed to set LED %d mode, err %d\n", offset, ret); + return ret; + } + + ret = devm_led_classdev_register_ext(dev, &led->led_cdev, &init_data); + if (ret) { + dev_err(dev, "Failed to register LED %d cdev, err %d\n", offset, ret); + return ret; + } + + /* Claim corresponding GPIO line so that it cannot be interfered with */ + led->gpio = gpiochip_request_own_desc(&priv->gpio, offset, led->led_cdev.name, + GPIO_ACTIVE_HIGH, GPIOD_ASIS); + if (IS_ERR(led->gpio)) { + ret = PTR_ERR(led->gpio); + dev_err(dev, "Failed to register LED %d cdev, err %d\n", offset, ret); + } + + return ret; +} + +static int fpc202_register_leds(struct fpc202_priv *priv) +{ + struct device *dev = &priv->client->dev; + int offset, ret = 0; + + if (!devres_open_group(dev, fpc202_register_leds, GFP_KERNEL)) + return -ENOMEM; + + for_each_child_of_node_scoped(dev->of_node, led_handle) { + ret = of_property_read_u32(led_handle, "reg", &offset); + if (ret) { + dev_err(dev, "Failed to read 'reg' property of child node, err %d\n", ret); + return ret; + } + + if (offset < FPC202_GPIO_P0_S0_OUT_C || offset > FPC202_GPIO_COUNT) + continue; + + ret = fpc202_register_led(priv, offset, led_handle); + if (ret) { + dev_err(dev, "Failed to register LED %d, err %d\n", offset, + ret); + goto free_own_gpios; + } + } + + devres_close_group(dev, fpc202_register_leds); + + return 0; + +free_own_gpios: + for (offset = 0; offset < FPC202_LED_COUNT; offset++) + if (priv->leds[offset].gpio) + gpiochip_free_own_desc(priv->leds[offset].gpio); + return ret; +} + +static int fpc202_probe_port(struct fpc202_priv *priv, struct device_node *i2c_handle, int port_id) +{ + u16 aliases[FPC202_ALIASES_PER_PORT] = { }; + struct device *dev = &priv->client->dev; + struct i2c_atr_adap_desc desc = { }; + int ret = 0; + + desc.chan_id = port_id; + desc.parent = dev; + desc.bus_handle = of_fwnode_handle(i2c_handle); + desc.num_aliases = FPC202_ALIASES_PER_PORT; + + fpc202_fill_alias_table(priv->client, aliases, port_id); + desc.aliases = aliases; + + ret = i2c_atr_add_adapter(priv->atr, &desc); + if (ret) + return ret; + + set_bit(port_id, priv->probed_ports); + + ret = fpc202_write_dev_addr(priv, port_id, 0, FPC202_REG_DEV_INVALID); + if (ret) + return ret; + + return fpc202_write_dev_addr(priv, port_id, 1, FPC202_REG_DEV_INVALID); +} + +static void fpc202_remove_port(struct fpc202_priv *priv, int port_id) +{ + i2c_atr_del_adapter(priv->atr, port_id); + clear_bit(port_id, priv->probed_ports); +} + +static int fpc202_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct fpc202_priv *priv; + int ret, port_id, led_id; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + mutex_init(&priv->reg_dev_lock); + mutex_init(&priv->led_mode_lock); + + priv->client = client; + i2c_set_clientdata(client, priv); + + priv->en_gpio = devm_gpiod_get_optional(dev, "enable", GPIOD_OUT_HIGH); + if (IS_ERR(priv->en_gpio)) { + ret = PTR_ERR(priv->en_gpio); + dev_err(dev, "failed to fetch enable GPIO! err %d\n", ret); + goto destroy_mutex; + } + + priv->gpio.label = "gpio-fpc202"; + priv->gpio.base = -1; + priv->gpio.direction_input = fpc202_gpio_direction_input; + priv->gpio.direction_output = fpc202_gpio_direction_output; + priv->gpio.set = fpc202_gpio_set; + priv->gpio.get = fpc202_gpio_get; + priv->gpio.ngpio = FPC202_GPIO_COUNT; + priv->gpio.parent = dev; + priv->gpio.owner = THIS_MODULE; + + ret = gpiochip_add_data(&priv->gpio, priv); + if (ret) { + priv->gpio.parent = NULL; + dev_err(dev, "failed to add gpiochip err %d\n", ret); + goto disable_gpio; + } + + priv->atr = i2c_atr_new(client->adapter, dev, &fpc202_atr_ops, 2, 0); + if (IS_ERR(priv->atr)) { + ret = PTR_ERR(priv->atr); + dev_err(dev, "failed to create i2c atr err %d\n", ret); + goto disable_gpio; + } + + i2c_atr_set_driver_data(priv->atr, priv); + + ret = fpc202_register_leds(priv); + if (ret) { + dev_err(dev, "Failed to register LEDs, err %d\n", ret); + goto delete_atr; + } + + bitmap_zero(priv->probed_ports, FPC202_NUM_PORTS); + + for_each_child_of_node_scoped(dev->of_node, i2c_handle) { + ret = of_property_read_u32(i2c_handle, "reg", &port_id); + if (ret) { + if (ret == -EINVAL) + continue; + + dev_err(dev, "failed to read 'reg' property of child node, err %d\n", ret); + goto unregister_chans; + } + + if (port_id >= FPC202_NUM_PORTS) + continue; + + ret = fpc202_probe_port(priv, i2c_handle, port_id); + if (ret) { + dev_err(dev, "Failed to probe port %d, err %d\n", port_id, ret); + goto unregister_chans; + } + } + + goto out; + +unregister_chans: + for_each_set_bit(port_id, priv->probed_ports, FPC202_NUM_PORTS) + fpc202_remove_port(priv, port_id); + + for (led_id = 0; led_id < FPC202_LED_COUNT; led_id++) + if (priv->leds[led_id].gpio) + gpiochip_free_own_desc(priv->leds[led_id].gpio); + + devres_release_group(&client->dev, fpc202_register_leds); +delete_atr: + i2c_atr_delete(priv->atr); +disable_gpio: + fpc202_set_enable(priv, 0); + gpiochip_remove(&priv->gpio); +destroy_mutex: + mutex_destroy(&priv->led_mode_lock); + mutex_destroy(&priv->reg_dev_lock); +out: + return ret; +} + +static void fpc202_remove(struct i2c_client *client) +{ + struct fpc202_priv *priv = i2c_get_clientdata(client); + int port_id, led_id; + + for_each_set_bit(port_id, priv->probed_ports, FPC202_NUM_PORTS) + fpc202_remove_port(priv, port_id); + + for (led_id = 0; led_id < FPC202_LED_COUNT; led_id++) + if (priv->leds[led_id].gpio) + gpiochip_free_own_desc(priv->leds[led_id].gpio); + + /* Release led devices early so that blink handlers don't trigger. */ + devres_release_group(&client->dev, fpc202_register_leds); + + mutex_destroy(&priv->led_mode_lock); + mutex_destroy(&priv->reg_dev_lock); + + i2c_atr_delete(priv->atr); + + fpc202_set_enable(priv, 0); + gpiochip_remove(&priv->gpio); +} + +static const struct of_device_id fpc202_of_match[] = { + { .compatible = "ti,fpc202" }, + {} +}; +MODULE_DEVICE_TABLE(of, fpc202_of_match); + +static struct i2c_driver fpc202_driver = { + .driver = { + .name = "fpc202", + .of_match_table = fpc202_of_match, + }, + .probe = fpc202_probe, + .remove = fpc202_remove, +}; + +module_i2c_driver(fpc202_driver); + +MODULE_AUTHOR("Romain Gantois <romain.gantois@bootlin.com>"); +MODULE_DESCRIPTION("TI FPC202 Dual Port Controller driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("I2C_ATR"); |
