diff --git a/drivers/gpio/Kconfig.pca95xx b/drivers/gpio/Kconfig.pca95xx index f2513bc3a33..7711b8b956a 100644 --- a/drivers/gpio/Kconfig.pca95xx +++ b/drivers/gpio/Kconfig.pca95xx @@ -15,3 +15,12 @@ config GPIO_PCA95XX_INIT_PRIORITY depends on GPIO_PCA95XX help Device driver initialization priority. + +config GPIO_PCA95XX_INTERRUPT + bool "Interrupt enable" + default n + depends on GPIO_PCA95XX + help + Enable interrupt support in PCA95XX driver. + Note that the PCA95XX cannot reliably detect + short-pulse interrupts due to its design. diff --git a/drivers/gpio/gpio_pca95xx.c b/drivers/gpio/gpio_pca95xx.c index 426981a619c..70a0a1c84b1 100644 --- a/drivers/gpio/gpio_pca95xx.c +++ b/drivers/gpio/gpio_pca95xx.c @@ -1,5 +1,6 @@ /* * Copyright (c) 2015 Intel Corporation. + * Copyright (c) 2020 Norbit ODM AS * * SPDX-License-Identifier: Apache-2.0 */ @@ -53,6 +54,7 @@ LOG_MODULE_REGISTER(gpio_pca95xx); /* Driver flags */ #define PCA_HAS_PUD BIT(0) +#define PCA_HAS_INTERRUPT BIT(1) /** Configuration data */ struct gpio_pca95xx_config { @@ -66,6 +68,15 @@ struct gpio_pca95xx_config { uint16_t i2c_slave_addr; uint8_t capabilities; + +#ifdef CONFIG_GPIO_PCA95XX_INTERRUPT + /* Interrupt pin definition */ + const char *int_gpio_port; + + gpio_pin_t int_gpio_pin; + + gpio_flags_t int_gpio_flags; +#endif }; /** Runtime driver data */ @@ -77,6 +88,7 @@ struct gpio_pca95xx_drv_data { const struct device *i2c_master; struct { + uint16_t input; uint16_t output; uint16_t dir; uint16_t pud_en; @@ -84,6 +96,28 @@ struct gpio_pca95xx_drv_data { } reg_cache; struct k_sem lock; + +#ifdef CONFIG_GPIO_PCA95XX_INTERRUPT + /* Self-reference to the driver instance */ + const struct device *instance; + + /* port ISR callback routine address */ + sys_slist_t callbacks; + + /* interrupt triggering pin masks */ + struct { + uint16_t edge_rising; + uint16_t edge_falling; + uint16_t level_high; + uint16_t level_low; + } interrupts; + + struct gpio_callback gpio_callback; + + struct k_work interrupt_worker; + + bool interrupt_active; +#endif }; /** @@ -93,19 +127,20 @@ struct gpio_pca95xx_drv_data { * * @param dev Device struct of the PCA95XX. * @param reg Register to read (the PORT0 of the pair of registers). + * @param cache Pointer to the cache to be updated after successful read. * @param buf Buffer to read data into. * * @return 0 if successful, failed otherwise. */ static int read_port_regs(const struct device *dev, uint8_t reg, - uint16_t *buf) + uint16_t *cache, uint16_t *buf) { const struct gpio_pca95xx_config * const config = dev->config; struct gpio_pca95xx_drv_data * const drv_data = (struct gpio_pca95xx_drv_data * const)dev->data; const struct device *i2c_master = drv_data->i2c_master; uint16_t i2c_addr = config->i2c_slave_addr; - uint16_t port_data; + uint16_t port_data, value; int ret; ret = i2c_burst_read(i2c_master, i2c_addr, reg, @@ -116,7 +151,9 @@ static int read_port_regs(const struct device *dev, uint8_t reg, return ret; } - *buf = sys_le16_to_cpu(port_data); + value = sys_le16_to_cpu(port_data); + *cache = value; + *buf = value; LOG_DBG("PCA95XX[0x%X]: Read: REG[0x%X] = 0x%X, REG[0x%X] = 0x%X", i2c_addr, reg, (*buf & 0xFF), (reg + 1), (*buf >> 8)); @@ -165,6 +202,15 @@ static int write_port_regs(const struct device *dev, uint8_t reg, return ret; } +static inline int update_input_regs(const struct device *dev, uint16_t *buf) +{ + struct gpio_pca95xx_drv_data * const drv_data = + (struct gpio_pca95xx_drv_data * const)dev->data; + + return read_port_regs(dev, REG_INPUT_PORT0, + &drv_data->reg_cache.input, buf); +} + static inline int update_output_regs(const struct device *dev, uint16_t value) { struct gpio_pca95xx_drv_data * const drv_data = @@ -374,7 +420,7 @@ static int gpio_pca95xx_port_get_raw(const struct device *dev, k_sem_take(&drv_data->lock, K_FOREVER); - ret = read_port_regs(dev, REG_INPUT_PORT0, &buf); + ret = update_input_regs(dev, &buf); if (ret != 0) { goto done; } @@ -448,14 +494,173 @@ static int gpio_pca95xx_port_toggle_bits(const struct device *dev, return ret; } +#ifdef CONFIG_GPIO_PCA95XX_INTERRUPT +static void gpio_pca95xx_interrupt_worker(struct k_work *work) +{ + struct gpio_pca95xx_drv_data * const drv_data = CONTAINER_OF( + work, struct gpio_pca95xx_drv_data, interrupt_worker); + uint16_t input_new, input_cache, changed_pins, trig_edge; + uint16_t trig_level = 0; + uint32_t triggered_int = 0; + int ret; + + k_sem_take(&drv_data->lock, K_FOREVER); + + input_cache = drv_data->reg_cache.input; + + ret = update_input_regs(drv_data->instance, &input_new); + if (ret == 0) { + /* Note: PCA Interrupt status is cleared by reading inputs */ + + changed_pins = (input_cache ^ input_new); + + trig_edge = (changed_pins & input_new & + drv_data->interrupts.edge_rising); + trig_edge |= (changed_pins & input_cache & + drv_data->interrupts.edge_falling); + trig_level = (input_new & drv_data->interrupts.level_high); + trig_level |= (~input_new & drv_data->interrupts.level_low); + + triggered_int = (trig_edge | trig_level); + } + + k_sem_give(&drv_data->lock); + + if (triggered_int != 0) { + gpio_fire_callbacks(&drv_data->callbacks, drv_data->instance, + triggered_int); + } + + /* Emulate level triggering */ + if (trig_level != 0) { + /* Reschedule worker */ + k_work_submit(&drv_data->interrupt_worker); + } +} + +static void gpio_pca95xx_interrupt_callback(const struct device *dev, + struct gpio_callback *cb, + gpio_port_pins_t pins) +{ + struct gpio_pca95xx_drv_data * const drv_data = + CONTAINER_OF(cb, struct gpio_pca95xx_drv_data, gpio_callback); + + ARG_UNUSED(pins); + + /* Cannot read PCA95xx registers from ISR context, queue worker */ + k_work_submit(&drv_data->interrupt_worker); +} +#endif /* CONFIG_GPIO_PCA95XX_INTERRUPT */ + static int gpio_pca95xx_pin_interrupt_configure(const struct device *dev, gpio_pin_t pin, enum gpio_int_mode mode, enum gpio_int_trig trig) { - return -ENOTSUP; + int ret = 0; + + if (!IS_ENABLED(CONFIG_GPIO_PCA95XX_INTERRUPT) + && (mode != GPIO_INT_MODE_DISABLED)) { + return -ENOTSUP; + } + +#ifdef CONFIG_GPIO_PCA95XX_INTERRUPT + const struct gpio_pca95xx_config * const config = dev->config; + struct gpio_pca95xx_drv_data * const drv_data = + (struct gpio_pca95xx_drv_data * const)dev->data; + const struct device *int_gpio_dev; + uint16_t reg; + bool enabled, edge, level, active; + + /* Check if GPIO port supports interrupts */ + if ((config->capabilities & PCA_HAS_INTERRUPT) == 0U) { + return -ENOTSUP; + } + + /* Check for an invalid pin number */ + if (BIT(pin) > config->common.port_pin_mask) { + return -EINVAL; + } + + /* Check configured pin direction */ + if ((mode != GPIO_INT_MODE_DISABLED) && + (BIT(pin) & drv_data->reg_cache.dir) != BIT(pin)) { + LOG_ERR("PCA95XX[0x%X]: output pin cannot trigger interrupt", + config->i2c_slave_addr); + return -ENOTSUP; + } + + k_sem_take(&drv_data->lock, K_FOREVER); + + /* Update interrupt masks */ + enabled = ((mode & GPIO_INT_MODE_DISABLED) == 0U); + edge = (mode == GPIO_INT_MODE_EDGE); + level = (mode == GPIO_INT_MODE_LEVEL); + WRITE_BIT(drv_data->interrupts.edge_rising, pin, (enabled && + edge && ((trig & GPIO_INT_TRIG_HIGH) == GPIO_INT_TRIG_HIGH))); + WRITE_BIT(drv_data->interrupts.edge_falling, pin, (enabled && + edge && ((trig & GPIO_INT_TRIG_LOW) == GPIO_INT_TRIG_LOW))); + WRITE_BIT(drv_data->interrupts.level_high, pin, (enabled && + level && ((trig & GPIO_INT_TRIG_HIGH) == GPIO_INT_TRIG_HIGH))); + WRITE_BIT(drv_data->interrupts.level_low, pin, (enabled && + level && ((trig & GPIO_INT_TRIG_LOW) == GPIO_INT_TRIG_LOW))); + + active = ((drv_data->interrupts.edge_rising || + drv_data->interrupts.edge_falling || + drv_data->interrupts.level_high || + drv_data->interrupts.level_low) > 0); + + /* Enable / disable interrupt as needed */ + if (active != drv_data->interrupt_active) { + int_gpio_dev = device_get_binding(config->int_gpio_port); + ret = gpio_pin_interrupt_configure(int_gpio_dev, + config->int_gpio_pin, (active ? + GPIO_INT_EDGE_TO_ACTIVE : + GPIO_INT_MODE_DISABLED)); + if (ret != 0) { + LOG_ERR("PCA95XX[0x%X]: failed to configure interrupt " + "on pin %d (%d)", config->i2c_slave_addr, + config->int_gpio_pin, ret); + goto err; + } + drv_data->interrupt_active = active; + + if (active) { + /* Read current status to reset any + * active signal on INT line + */ + update_input_regs(dev, ®); + } + } + +err: + k_sem_give(&drv_data->lock); +#endif /* CONFIG_GPIO_PCA95XX_INTERRUPT */ + return ret; } +#ifdef CONFIG_GPIO_PCA95XX_INTERRUPT +static int gpio_pca95xx_manage_callback(const struct device *dev, + struct gpio_callback *callback, + bool set) +{ + const struct gpio_pca95xx_config * const config = dev->config; + struct gpio_pca95xx_drv_data * const drv_data = + (struct gpio_pca95xx_drv_data * const)dev->data; + + if ((config->capabilities & PCA_HAS_INTERRUPT) == 0U) { + return -ENOTSUP; + } + + k_sem_take(&drv_data->lock, K_FOREVER); + + gpio_manage_callback(&drv_data->callbacks, callback, set); + + k_sem_give(&drv_data->lock); + return 0; +} +#endif + static const struct gpio_driver_api gpio_pca95xx_drv_api_funcs = { .pin_configure = gpio_pca95xx_config, .port_get_raw = gpio_pca95xx_port_get_raw, @@ -464,6 +669,9 @@ static const struct gpio_driver_api gpio_pca95xx_drv_api_funcs = { .port_clear_bits_raw = gpio_pca95xx_port_clear_bits_raw, .port_toggle_bits = gpio_pca95xx_port_toggle_bits, .pin_interrupt_configure = gpio_pca95xx_pin_interrupt_configure, +#ifdef CONFIG_GPIO_PCA95XX_INTERRUPT + .manage_callback = gpio_pca95xx_manage_callback, +#endif }; /** @@ -488,6 +696,45 @@ static int gpio_pca95xx_init(const struct device *dev) k_sem_init(&drv_data->lock, 1, 1); +#ifdef CONFIG_GPIO_PCA95XX_INTERRUPT + /* Check if GPIO port supports interrupts */ + if ((config->capabilities & PCA_HAS_INTERRUPT) != 0) { + const struct device *int_gpio_dev; + int ret; + + /* Store self-reference for interrupt handling */ + drv_data->instance = dev; + + /* Prepare interrupt worker */ + k_work_init(&drv_data->interrupt_worker, + gpio_pca95xx_interrupt_worker); + + /* Configure GPIO interrupt pin */ + int_gpio_dev = device_get_binding(config->int_gpio_port); + if (int_gpio_dev == NULL) { + LOG_ERR("PCA95XX[0x%X]: error getting interrupt GPIO" + " device (%s)", config->i2c_slave_addr, + config->int_gpio_port); + return -ENODEV; + } + + ret = gpio_pin_configure(int_gpio_dev, config->int_gpio_pin, + (config->int_gpio_flags | GPIO_INPUT)); + if (ret != 0) { + LOG_ERR("PCA95XX[0x%X]: failed to configure interrupt" + " pin %d (%d)", config->i2c_slave_addr, + config->int_gpio_pin, ret); + return ret; + } + + /* Prepare GPIO callback for interrupt pin */ + gpio_init_callback(&drv_data->gpio_callback, + gpio_pca95xx_interrupt_callback, + BIT(config->int_gpio_pin)); + gpio_add_callback(int_gpio_dev, &drv_data->gpio_callback); + } +#endif + return 0; } @@ -501,14 +748,28 @@ static const struct gpio_pca95xx_config gpio_pca95xx_##inst##_cfg = { \ .capabilities = \ (DT_INST_PROP(inst, has_pud) ? \ PCA_HAS_PUD : 0) | \ + IF_ENABLED(CONFIG_GPIO_PCA95XX_INTERRUPT, ( \ + (DT_INST_NODE_HAS_PROP(inst, interrupt_gpios) ? \ + PCA_HAS_INTERRUPT : 0) | \ + )) \ 0, \ + IF_ENABLED(CONFIG_GPIO_PCA95XX_INTERRUPT, ( \ + IF_ENABLED(DT_INST_NODE_HAS_PROP(inst, interrupt_gpios), ( \ + .int_gpio_port = DT_INST_GPIO_LABEL(inst, interrupt_gpios), \ + .int_gpio_pin = DT_INST_GPIO_PIN(inst, interrupt_gpios), \ + .int_gpio_flags = DT_INST_GPIO_FLAGS(inst, interrupt_gpios), \ + )))) \ }; \ \ static struct gpio_pca95xx_drv_data gpio_pca95xx_##inst##_drvdata = { \ + .reg_cache.input = 0x0, \ .reg_cache.output = 0xFFFF, \ .reg_cache.dir = 0xFFFF, \ .reg_cache.pud_en = 0x0, \ .reg_cache.pud_sel = 0xFFFF, \ + IF_ENABLED(CONFIG_GPIO_PCA95XX_INTERRUPT, ( \ + .interrupt_active = false, \ + )) \ }; \ \ DEVICE_AND_API_INIT(gpio_pca95xx_##inst, \ diff --git a/dts/bindings/gpio/nxp,pca95xx.yaml b/dts/bindings/gpio/nxp,pca95xx.yaml index 50b64feecf5..4039f823161 100644 --- a/dts/bindings/gpio/nxp,pca95xx.yaml +++ b/dts/bindings/gpio/nxp,pca95xx.yaml @@ -16,6 +16,11 @@ properties: required: false description: Supports pull-up/pull-down + interrupt-gpios: + type: phandle-array + required: false + description: Interrupt GPIO pin (active-low open-drain) + "#gpio-cells": const: 2