zephyr/drivers/gpio/gpio_mcp23xxx.c
Armin Brauns 6a41a7abba drivers: mcp23xxx: explain more common causes for spurious interrupts
Interrupt handling in this chip is broken beyond repair, anyone unfortunate
enough to have to use it will probably come across this error and wonder
what's up.

Signed-off-by: Armin Brauns <armin.brauns@embedded-solutions.at>
2024-01-30 08:46:39 +01:00

556 lines
13 KiB
C

/*
*
* Copyright (c) 2021 metraTec GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file Driver for MPC23xxx I2C/SPI-based GPIO driver.
*/
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/gpio/gpio_utils.h>
#include "gpio_mcp23xxx.h"
#define LOG_LEVEL CONFIG_GPIO_LOG_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(gpio_mcp23xxx);
#define MCP23XXX_RESET_TIME_US 1
/**
* @brief Reads given register from mcp23xxx.
*
* The registers of the mcp23x0x consist of one 8 bit port.
* The registers of the mcp23x1x consist of two 8 bit ports.
*
* @param dev The mcp23xxx device.
* @param reg The register to be read.
* @param buf The buffer to read data to.
* @return 0 if successful.
* Otherwise <0 will be returned.
*/
static int read_port_regs(const struct device *dev, uint8_t reg, uint16_t *buf)
{
const struct mcp23xxx_config *config = dev->config;
if (config->ngpios == 16U) {
reg *= 2;
}
return config->read_fn(dev, reg, buf);
}
/**
* @brief Writes registers of the mcp23xxx.
*
* On the mcp23x08 one 8 bit port will be written.
* On the mcp23x17 two 8 bit ports will be written.
*
* @param dev The mcp23xxx device.
* @param reg Register to be written.
* @param buf The new register value.
*
* @return 0 if successful. Otherwise <0 will be returned.
*/
static int write_port_regs(const struct device *dev, uint8_t reg, uint16_t value)
{
const struct mcp23xxx_config *config = dev->config;
if (config->ngpios == 16U) {
reg *= 2;
}
return config->write_fn(dev, reg, value);
}
/**
* @brief Writes to the IOCON register of the mcp23xxx.
*
* IOCON is the only register that is not 16 bits wide on 16-pin devices; instead, it is mirrored in
* two adjacent memory locations. Because the underlying `write_fn` always does a 16-bit write for
* 16-pin devices, make sure we write the same value to both IOCON locations.
*
* @param dev The mcp23xxx device.
* @param value the IOCON value to write
*
* @return 0 if successful. Otherwise <0 will be returned.
*/
static int write_iocon(const struct device *dev, uint8_t value)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
uint16_t extended_value = value | (value << 8);
int ret = write_port_regs(dev, REG_IOCON, extended_value);
if (ret == 0) {
drv_data->reg_cache.iocon = extended_value;
}
return ret;
}
/**
* @brief Setup the pin direction.
*
* @param dev The mcp23xxx device.
* @param pin The pin number.
* @param flags Flags of pin or port.
* @return 0 if successful. Otherwise <0 will be returned.
*/
static int setup_pin_dir(const struct device *dev, uint32_t pin, int flags)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
uint16_t dir = drv_data->reg_cache.iodir;
uint16_t output = drv_data->reg_cache.gpio;
int ret;
if ((flags & GPIO_OUTPUT) != 0U) {
if ((flags & GPIO_OUTPUT_INIT_HIGH) != 0U) {
output |= BIT(pin);
} else if ((flags & GPIO_OUTPUT_INIT_LOW) != 0U) {
output &= ~BIT(pin);
}
dir &= ~BIT(pin);
} else {
dir |= BIT(pin);
}
ret = write_port_regs(dev, REG_GPIO, output);
if (ret != 0) {
return ret;
}
drv_data->reg_cache.gpio = output;
ret = write_port_regs(dev, REG_IODIR, dir);
if (ret == 0) {
drv_data->reg_cache.iodir = dir;
}
return ret;
}
/**
* @brief Setup pin pull up/pull down.
*
* @param dev The mcp23xxx device.
* @param pin The pin number.
* @param flags Flags of pin or port.
* @return 0 if successful. Otherwise <0 will be returned.
*/
static int setup_pin_pull(const struct device *dev, uint32_t pin, int flags)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
uint16_t port;
int ret;
port = drv_data->reg_cache.gppu;
if ((flags & GPIO_PULL_DOWN) != 0U) {
return -ENOTSUP;
}
WRITE_BIT(port, pin, (flags & GPIO_PULL_UP) != 0);
ret = write_port_regs(dev, REG_GPPU, port);
if (ret == 0) {
drv_data->reg_cache.gppu = port;
}
return ret;
}
static int mcp23xxx_pin_cfg(const struct device *dev, gpio_pin_t pin, gpio_flags_t flags)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
int ret;
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
if ((flags & GPIO_SINGLE_ENDED) != 0U) {
ret = -ENOTSUP;
goto done;
}
ret = setup_pin_dir(dev, pin, flags);
if (ret < 0) {
LOG_ERR("Error setting pin direction (%d)", ret);
goto done;
}
ret = setup_pin_pull(dev, pin, flags);
if (ret < 0) {
LOG_ERR("Error setting pin pull up/pull down (%d)", ret);
goto done;
}
done:
k_sem_give(&drv_data->lock);
return ret;
}
static int mcp23xxx_port_get_raw(const struct device *dev, uint32_t *value)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
uint16_t buf;
int ret;
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
ret = read_port_regs(dev, REG_GPIO, &buf);
if (ret == 0) {
*value = buf;
}
k_sem_give(&drv_data->lock);
return ret;
}
static int mcp23xxx_port_set_masked_raw(const struct device *dev, uint32_t mask, uint32_t value)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
uint16_t buf;
int ret;
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
buf = drv_data->reg_cache.gpio;
buf = (buf & ~mask) | (mask & value);
ret = write_port_regs(dev, REG_GPIO, buf);
if (ret == 0) {
drv_data->reg_cache.gpio = buf;
}
k_sem_give(&drv_data->lock);
return ret;
}
static int mcp23xxx_port_set_bits_raw(const struct device *dev, uint32_t mask)
{
return mcp23xxx_port_set_masked_raw(dev, mask, mask);
}
static int mcp23xxx_port_clear_bits_raw(const struct device *dev, uint32_t mask)
{
return mcp23xxx_port_set_masked_raw(dev, mask, 0);
}
static int mcp23xxx_port_toggle_bits(const struct device *dev, uint32_t mask)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
uint16_t buf;
int ret;
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
buf = drv_data->reg_cache.gpio;
buf ^= mask;
ret = write_port_regs(dev, REG_GPIO, buf);
if (ret == 0) {
drv_data->reg_cache.gpio = buf;
}
k_sem_give(&drv_data->lock);
return ret;
}
static int mcp23xxx_pin_interrupt_configure(const struct device *dev, gpio_pin_t pin,
enum gpio_int_mode mode, enum gpio_int_trig trig)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
const struct mcp23xxx_config *config = dev->config;
if (!config->gpio_int.port) {
return -ENOTSUP;
}
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
uint16_t gpinten = drv_data->reg_cache.gpinten;
uint16_t defval = drv_data->reg_cache.defval;
uint16_t intcon = drv_data->reg_cache.intcon;
int ret;
switch (mode) {
case GPIO_INT_MODE_DISABLED:
gpinten &= ~BIT(pin);
break;
case GPIO_INT_MODE_LEVEL:
gpinten |= BIT(pin);
intcon |= BIT(pin);
switch (trig) {
case GPIO_INT_TRIG_LOW:
defval |= BIT(pin);
break;
case GPIO_INT_TRIG_HIGH:
defval &= ~BIT(pin);
break;
case GPIO_INT_TRIG_BOTH:
/* can't happen */
ret = -ENOTSUP;
goto done;
}
break;
case GPIO_INT_MODE_EDGE:
gpinten |= BIT(pin);
intcon &= ~BIT(pin);
switch (trig) {
case GPIO_INT_TRIG_LOW:
drv_data->rising_edge_ints &= ~BIT(pin);
drv_data->falling_edge_ints |= BIT(pin);
break;
case GPIO_INT_TRIG_HIGH:
drv_data->rising_edge_ints |= BIT(pin);
drv_data->falling_edge_ints &= ~BIT(pin);
break;
case GPIO_INT_TRIG_BOTH:
drv_data->rising_edge_ints |= BIT(pin);
drv_data->falling_edge_ints |= BIT(pin);
break;
}
break;
}
ret = write_port_regs(dev, REG_GPINTEN, gpinten);
if (ret != 0) {
goto done;
}
drv_data->reg_cache.gpinten = gpinten;
ret = write_port_regs(dev, REG_DEFVAL, defval);
if (ret != 0) {
goto done;
}
drv_data->reg_cache.defval = defval;
ret = write_port_regs(dev, REG_INTCON, intcon);
if (ret != 0) {
goto done;
}
drv_data->reg_cache.intcon = intcon;
done:
k_sem_give(&drv_data->lock);
return ret;
}
static int mcp23xxx_manage_callback(const struct device *dev, struct gpio_callback *callback,
bool set)
{
struct mcp23xxx_drv_data *drv_data = dev->data;
const struct mcp23xxx_config *config = dev->config;
if (!config->gpio_int.port) {
return -ENOTSUP;
}
if (k_is_in_isr()) {
return -EWOULDBLOCK;
}
k_sem_take(&drv_data->lock, K_FOREVER);
int ret = gpio_manage_callback(&drv_data->callbacks, callback, set);
k_sem_give(&drv_data->lock);
return ret;
}
static void mcp23xxx_work_handler(struct k_work *work)
{
struct mcp23xxx_drv_data *drv_data = CONTAINER_OF(work, struct mcp23xxx_drv_data, work);
const struct device *dev = drv_data->dev;
int ret;
k_sem_take(&drv_data->lock, K_FOREVER);
uint16_t intf;
ret = read_port_regs(dev, REG_INTF, &intf);
if (ret != 0) {
LOG_ERR("Failed to read INTF");
goto fail;
}
if (!intf) {
/* Probable causes:
* - REG_GPIO was read from somewhere else before the interrupt handler had a chance
* to run
* - Even though the datasheet says differently, reading INTCAP while a level
* interrupt is active briefly (~2ns) causes the interrupt line to go high and
* low again. This causes a second ISR to be scheduled, which then won't
* find any active interrupts if the callback has disabled the level interrupt.
*/
LOG_ERR("Spurious interrupt");
goto fail;
}
uint16_t intcap;
/* Read INTCAP to acknowledge the interrupt */
ret = read_port_regs(dev, REG_INTCAP, &intcap);
if (ret != 0) {
LOG_ERR("Failed to read INTCAP");
goto fail;
}
/* mcp23xxx does not support single-edge interrupts in hardware, filter them out manually */
uint16_t level_ints = drv_data->reg_cache.gpinten & drv_data->reg_cache.intcon;
intf &= level_ints | (intcap & drv_data->rising_edge_ints) |
(~intcap & drv_data->falling_edge_ints);
k_sem_give(&drv_data->lock);
gpio_fire_callbacks(&drv_data->callbacks, dev, intf);
return;
fail:
k_sem_give(&drv_data->lock);
}
static void mcp23xxx_int_gpio_handler(const struct device *port, struct gpio_callback *cb,
gpio_port_pins_t pins)
{
struct mcp23xxx_drv_data *drv_data =
CONTAINER_OF(cb, struct mcp23xxx_drv_data, int_gpio_cb);
k_work_submit(&drv_data->work);
}
const struct gpio_driver_api gpio_mcp23xxx_api_table = {
.pin_configure = mcp23xxx_pin_cfg,
.port_get_raw = mcp23xxx_port_get_raw,
.port_set_masked_raw = mcp23xxx_port_set_masked_raw,
.port_set_bits_raw = mcp23xxx_port_set_bits_raw,
.port_clear_bits_raw = mcp23xxx_port_clear_bits_raw,
.port_toggle_bits = mcp23xxx_port_toggle_bits,
.pin_interrupt_configure = mcp23xxx_pin_interrupt_configure,
.manage_callback = mcp23xxx_manage_callback,
};
/**
* @brief Initialization function of MCP23XXX
*
* @param dev Device struct.
* @return 0 if successful. Otherwise <0 is returned.
*/
int gpio_mcp23xxx_init(const struct device *dev)
{
const struct mcp23xxx_config *config = dev->config;
struct mcp23xxx_drv_data *drv_data = dev->data;
int err;
if (config->ngpios != 8U && config->ngpios != 16U) {
LOG_ERR("Invalid value ngpios=%u. Expected 8 or 16!", config->ngpios);
return -EINVAL;
}
err = config->bus_fn(dev);
if (err < 0) {
return err;
}
k_sem_init(&drv_data->lock, 0, 1);
/* If the RESET line is available, pulse it. */
if (config->gpio_reset.port) {
err = gpio_pin_configure_dt(&config->gpio_reset, GPIO_OUTPUT_ACTIVE);
if (err != 0) {
LOG_ERR("Failed to configure RESET line: %d", err);
return -EIO;
}
k_usleep(MCP23XXX_RESET_TIME_US);
err = gpio_pin_set_dt(&config->gpio_reset, 0);
if (err != 0) {
LOG_ERR("Failed to deactivate RESET line: %d", err);
return -EIO;
}
}
/* If the INT line is available, configure the callback for it. */
if (config->gpio_int.port) {
if (config->ngpios == 16) {
/* send both ports' interrupts through one IRQ pin */
err = write_iocon(dev, REG_IOCON_MIRROR);
if (err != 0) {
LOG_ERR("Failed to enable mirrored IRQ pins: %d", err);
return -EIO;
}
}
if (!gpio_is_ready_dt(&config->gpio_int)) {
LOG_ERR("INT port is not ready");
return -ENODEV;
}
drv_data->dev = dev;
k_work_init(&drv_data->work, mcp23xxx_work_handler);
err = gpio_pin_configure_dt(&config->gpio_int, GPIO_INPUT);
if (err != 0) {
LOG_ERR("Failed to configure INT line: %d", err);
return -EIO;
}
gpio_init_callback(&drv_data->int_gpio_cb, mcp23xxx_int_gpio_handler,
BIT(config->gpio_int.pin));
err = gpio_add_callback(config->gpio_int.port, &drv_data->int_gpio_cb);
if (err != 0) {
LOG_ERR("Failed to add INT callback: %d", err);
return -EIO;
}
err = gpio_pin_interrupt_configure_dt(&config->gpio_int, GPIO_INT_EDGE_TO_ACTIVE);
if (err != 0) {
LOG_ERR("Failed to configure INT interrupt: %d", err);
return -EIO;
}
}
k_sem_give(&drv_data->lock);
return 0;
}