|
@@ -1,5 +1,13 @@
|
|
|
/*
|
|
|
- * MCP23S08 SPI/GPIO gpio expander driver
|
|
|
+ * MCP23S08 SPI/I2C GPIO gpio expander driver
|
|
|
+ *
|
|
|
+ * The inputs and outputs of the mcp23s08, mcp23s17, mcp23008 and mcp23017 are
|
|
|
+ * supported.
|
|
|
+ * For the I2C versions of the chips (mcp23008 and mcp23017) generation of
|
|
|
+ * interrupts is also supported.
|
|
|
+ * The hardware of the SPI versions of the chips (mcp23s08 and mcp23s17) is
|
|
|
+ * also capable of generating interrupts, but the linux driver does not
|
|
|
+ * support that yet.
|
|
|
*/
|
|
|
|
|
|
#include <linux/kernel.h>
|
|
@@ -12,7 +20,8 @@
|
|
|
#include <linux/spi/mcp23s08.h>
|
|
|
#include <linux/slab.h>
|
|
|
#include <asm/byteorder.h>
|
|
|
-#include <linux/of.h>
|
|
|
+#include <linux/interrupt.h>
|
|
|
+#include <linux/of_irq.h>
|
|
|
#include <linux/of_device.h>
|
|
|
|
|
|
/**
|
|
@@ -34,6 +43,7 @@
|
|
|
#define MCP_DEFVAL 0x03
|
|
|
#define MCP_INTCON 0x04
|
|
|
#define MCP_IOCON 0x05
|
|
|
+# define IOCON_MIRROR (1 << 6)
|
|
|
# define IOCON_SEQOP (1 << 5)
|
|
|
# define IOCON_HAEN (1 << 3)
|
|
|
# define IOCON_ODR (1 << 2)
|
|
@@ -57,8 +67,14 @@ struct mcp23s08 {
|
|
|
u8 addr;
|
|
|
|
|
|
u16 cache[11];
|
|
|
+ u16 irq_rise;
|
|
|
+ u16 irq_fall;
|
|
|
+ int irq;
|
|
|
+ bool irq_controller;
|
|
|
/* lock protects the cached values */
|
|
|
struct mutex lock;
|
|
|
+ struct mutex irq_lock;
|
|
|
+ struct irq_domain *irq_domain;
|
|
|
|
|
|
struct gpio_chip chip;
|
|
|
|
|
@@ -77,6 +93,11 @@ struct mcp23s08_driver_data {
|
|
|
struct mcp23s08 chip[];
|
|
|
};
|
|
|
|
|
|
+/* This lock class tells lockdep that GPIO irqs are in a different
|
|
|
+ * category than their parents, so it won't report false recursion.
|
|
|
+ */
|
|
|
+static struct lock_class_key gpio_lock_class;
|
|
|
+
|
|
|
/*----------------------------------------------------------------------*/
|
|
|
|
|
|
#if IS_ENABLED(CONFIG_I2C)
|
|
@@ -315,6 +336,195 @@ mcp23s08_direction_output(struct gpio_chip *chip, unsigned offset, int value)
|
|
|
return status;
|
|
|
}
|
|
|
|
|
|
+/*----------------------------------------------------------------------*/
|
|
|
+static irqreturn_t mcp23s08_irq(int irq, void *data)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = data;
|
|
|
+ int intcap, intf, i;
|
|
|
+ unsigned int child_irq;
|
|
|
+
|
|
|
+ mutex_lock(&mcp->lock);
|
|
|
+ intf = mcp->ops->read(mcp, MCP_INTF);
|
|
|
+ if (intf < 0) {
|
|
|
+ mutex_unlock(&mcp->lock);
|
|
|
+ return IRQ_HANDLED;
|
|
|
+ }
|
|
|
+
|
|
|
+ mcp->cache[MCP_INTF] = intf;
|
|
|
+
|
|
|
+ intcap = mcp->ops->read(mcp, MCP_INTCAP);
|
|
|
+ if (intcap < 0) {
|
|
|
+ mutex_unlock(&mcp->lock);
|
|
|
+ return IRQ_HANDLED;
|
|
|
+ }
|
|
|
+
|
|
|
+ mcp->cache[MCP_INTCAP] = intcap;
|
|
|
+ mutex_unlock(&mcp->lock);
|
|
|
+
|
|
|
+
|
|
|
+ for (i = 0; i < mcp->chip.ngpio; i++) {
|
|
|
+ if ((BIT(i) & mcp->cache[MCP_INTF]) &&
|
|
|
+ ((BIT(i) & intcap & mcp->irq_rise) ||
|
|
|
+ (mcp->irq_fall & ~intcap & BIT(i)))) {
|
|
|
+ child_irq = irq_find_mapping(mcp->irq_domain, i);
|
|
|
+ handle_nested_irq(child_irq);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return IRQ_HANDLED;
|
|
|
+}
|
|
|
+
|
|
|
+static int mcp23s08_gpio_to_irq(struct gpio_chip *chip, unsigned offset)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = container_of(chip, struct mcp23s08, chip);
|
|
|
+
|
|
|
+ return irq_find_mapping(mcp->irq_domain, offset);
|
|
|
+}
|
|
|
+
|
|
|
+static void mcp23s08_irq_mask(struct irq_data *data)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = irq_data_get_irq_chip_data(data);
|
|
|
+ unsigned int pos = data->hwirq;
|
|
|
+
|
|
|
+ mcp->cache[MCP_GPINTEN] &= ~BIT(pos);
|
|
|
+}
|
|
|
+
|
|
|
+static void mcp23s08_irq_unmask(struct irq_data *data)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = irq_data_get_irq_chip_data(data);
|
|
|
+ unsigned int pos = data->hwirq;
|
|
|
+
|
|
|
+ mcp->cache[MCP_GPINTEN] |= BIT(pos);
|
|
|
+}
|
|
|
+
|
|
|
+static int mcp23s08_irq_set_type(struct irq_data *data, unsigned int type)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = irq_data_get_irq_chip_data(data);
|
|
|
+ unsigned int pos = data->hwirq;
|
|
|
+ int status = 0;
|
|
|
+
|
|
|
+ if ((type & IRQ_TYPE_EDGE_BOTH) == IRQ_TYPE_EDGE_BOTH) {
|
|
|
+ mcp->cache[MCP_INTCON] &= ~BIT(pos);
|
|
|
+ mcp->irq_rise |= BIT(pos);
|
|
|
+ mcp->irq_fall |= BIT(pos);
|
|
|
+ } else if (type & IRQ_TYPE_EDGE_RISING) {
|
|
|
+ mcp->cache[MCP_INTCON] &= ~BIT(pos);
|
|
|
+ mcp->irq_rise |= BIT(pos);
|
|
|
+ mcp->irq_fall &= ~BIT(pos);
|
|
|
+ } else if (type & IRQ_TYPE_EDGE_FALLING) {
|
|
|
+ mcp->cache[MCP_INTCON] &= ~BIT(pos);
|
|
|
+ mcp->irq_rise &= ~BIT(pos);
|
|
|
+ mcp->irq_fall |= BIT(pos);
|
|
|
+ } else
|
|
|
+ return -EINVAL;
|
|
|
+
|
|
|
+ return status;
|
|
|
+}
|
|
|
+
|
|
|
+static void mcp23s08_irq_bus_lock(struct irq_data *data)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = irq_data_get_irq_chip_data(data);
|
|
|
+
|
|
|
+ mutex_lock(&mcp->irq_lock);
|
|
|
+}
|
|
|
+
|
|
|
+static void mcp23s08_irq_bus_unlock(struct irq_data *data)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = irq_data_get_irq_chip_data(data);
|
|
|
+
|
|
|
+ mutex_lock(&mcp->lock);
|
|
|
+ mcp->ops->write(mcp, MCP_GPINTEN, mcp->cache[MCP_GPINTEN]);
|
|
|
+ mcp->ops->write(mcp, MCP_DEFVAL, mcp->cache[MCP_DEFVAL]);
|
|
|
+ mcp->ops->write(mcp, MCP_INTCON, mcp->cache[MCP_INTCON]);
|
|
|
+ mutex_unlock(&mcp->lock);
|
|
|
+ mutex_unlock(&mcp->irq_lock);
|
|
|
+}
|
|
|
+
|
|
|
+static unsigned int mcp23s08_irq_startup(struct irq_data *data)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = irq_data_get_irq_chip_data(data);
|
|
|
+
|
|
|
+ if (gpio_lock_as_irq(&mcp->chip, data->hwirq))
|
|
|
+ dev_err(mcp->chip.dev,
|
|
|
+ "unable to lock HW IRQ %lu for IRQ usage\n",
|
|
|
+ data->hwirq);
|
|
|
+
|
|
|
+ mcp23s08_irq_unmask(data);
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static void mcp23s08_irq_shutdown(struct irq_data *data)
|
|
|
+{
|
|
|
+ struct mcp23s08 *mcp = irq_data_get_irq_chip_data(data);
|
|
|
+
|
|
|
+ mcp23s08_irq_mask(data);
|
|
|
+ gpio_unlock_as_irq(&mcp->chip, data->hwirq);
|
|
|
+}
|
|
|
+
|
|
|
+static struct irq_chip mcp23s08_irq_chip = {
|
|
|
+ .name = "gpio-mcp23xxx",
|
|
|
+ .irq_mask = mcp23s08_irq_mask,
|
|
|
+ .irq_unmask = mcp23s08_irq_unmask,
|
|
|
+ .irq_set_type = mcp23s08_irq_set_type,
|
|
|
+ .irq_bus_lock = mcp23s08_irq_bus_lock,
|
|
|
+ .irq_bus_sync_unlock = mcp23s08_irq_bus_unlock,
|
|
|
+ .irq_startup = mcp23s08_irq_startup,
|
|
|
+ .irq_shutdown = mcp23s08_irq_shutdown,
|
|
|
+};
|
|
|
+
|
|
|
+static int mcp23s08_irq_setup(struct mcp23s08 *mcp)
|
|
|
+{
|
|
|
+ struct gpio_chip *chip = &mcp->chip;
|
|
|
+ int err, irq, j;
|
|
|
+
|
|
|
+ mutex_init(&mcp->irq_lock);
|
|
|
+
|
|
|
+ mcp->irq_domain = irq_domain_add_linear(chip->of_node, chip->ngpio,
|
|
|
+ &irq_domain_simple_ops, mcp);
|
|
|
+ if (!mcp->irq_domain)
|
|
|
+ return -ENODEV;
|
|
|
+
|
|
|
+ err = devm_request_threaded_irq(chip->dev, mcp->irq, NULL, mcp23s08_irq,
|
|
|
+ IRQF_TRIGGER_LOW | IRQF_ONESHOT,
|
|
|
+ dev_name(chip->dev), mcp);
|
|
|
+ if (err != 0) {
|
|
|
+ dev_err(chip->dev, "unable to request IRQ#%d: %d\n",
|
|
|
+ mcp->irq, err);
|
|
|
+ return err;
|
|
|
+ }
|
|
|
+
|
|
|
+ chip->to_irq = mcp23s08_gpio_to_irq;
|
|
|
+
|
|
|
+ for (j = 0; j < mcp->chip.ngpio; j++) {
|
|
|
+ irq = irq_create_mapping(mcp->irq_domain, j);
|
|
|
+ irq_set_lockdep_class(irq, &gpio_lock_class);
|
|
|
+ irq_set_chip_data(irq, mcp);
|
|
|
+ irq_set_chip(irq, &mcp23s08_irq_chip);
|
|
|
+ irq_set_nested_thread(irq, true);
|
|
|
+#ifdef CONFIG_ARM
|
|
|
+ set_irq_flags(irq, IRQF_VALID);
|
|
|
+#else
|
|
|
+ irq_set_noprobe(irq);
|
|
|
+#endif
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static void mcp23s08_irq_teardown(struct mcp23s08 *mcp)
|
|
|
+{
|
|
|
+ unsigned int irq, i;
|
|
|
+
|
|
|
+ free_irq(mcp->irq, mcp);
|
|
|
+
|
|
|
+ for (i = 0; i < mcp->chip.ngpio; i++) {
|
|
|
+ irq = irq_find_mapping(mcp->irq_domain, i);
|
|
|
+ if (irq > 0)
|
|
|
+ irq_dispose_mapping(irq);
|
|
|
+ }
|
|
|
+
|
|
|
+ irq_domain_remove(mcp->irq_domain);
|
|
|
+}
|
|
|
+
|
|
|
/*----------------------------------------------------------------------*/
|
|
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
@@ -370,10 +580,11 @@ done:
|
|
|
/*----------------------------------------------------------------------*/
|
|
|
|
|
|
static int mcp23s08_probe_one(struct mcp23s08 *mcp, struct device *dev,
|
|
|
- void *data, unsigned addr,
|
|
|
- unsigned type, unsigned base, unsigned pullups)
|
|
|
+ void *data, unsigned addr, unsigned type,
|
|
|
+ unsigned base, unsigned pullups)
|
|
|
{
|
|
|
int status;
|
|
|
+ bool mirror = false;
|
|
|
|
|
|
mutex_init(&mcp->lock);
|
|
|
|
|
@@ -425,20 +636,32 @@ static int mcp23s08_probe_one(struct mcp23s08 *mcp, struct device *dev,
|
|
|
}
|
|
|
|
|
|
mcp->chip.base = base;
|
|
|
- mcp->chip.can_sleep = 1;
|
|
|
+ mcp->chip.can_sleep = true;
|
|
|
mcp->chip.dev = dev;
|
|
|
mcp->chip.owner = THIS_MODULE;
|
|
|
|
|
|
/* verify MCP_IOCON.SEQOP = 0, so sequential reads work,
|
|
|
* and MCP_IOCON.HAEN = 1, so we work with all chips.
|
|
|
*/
|
|
|
+
|
|
|
status = mcp->ops->read(mcp, MCP_IOCON);
|
|
|
if (status < 0)
|
|
|
goto fail;
|
|
|
- if ((status & IOCON_SEQOP) || !(status & IOCON_HAEN)) {
|
|
|
+
|
|
|
+ mcp->irq_controller = of_property_read_bool(mcp->chip.of_node,
|
|
|
+ "interrupt-controller");
|
|
|
+ if (mcp->irq && mcp->irq_controller && (type == MCP_TYPE_017))
|
|
|
+ mirror = of_property_read_bool(mcp->chip.of_node,
|
|
|
+ "microchip,irq-mirror");
|
|
|
+
|
|
|
+ if ((status & IOCON_SEQOP) || !(status & IOCON_HAEN) || mirror) {
|
|
|
/* mcp23s17 has IOCON twice, make sure they are in sync */
|
|
|
status &= ~(IOCON_SEQOP | (IOCON_SEQOP << 8));
|
|
|
status |= IOCON_HAEN | (IOCON_HAEN << 8);
|
|
|
+ status &= ~(IOCON_INTPOL | (IOCON_INTPOL << 8));
|
|
|
+ if (mirror)
|
|
|
+ status |= IOCON_MIRROR | (IOCON_MIRROR << 8);
|
|
|
+
|
|
|
status = mcp->ops->write(mcp, MCP_IOCON, status);
|
|
|
if (status < 0)
|
|
|
goto fail;
|
|
@@ -470,6 +693,16 @@ static int mcp23s08_probe_one(struct mcp23s08 *mcp, struct device *dev,
|
|
|
}
|
|
|
|
|
|
status = gpiochip_add(&mcp->chip);
|
|
|
+ if (status < 0)
|
|
|
+ goto fail;
|
|
|
+
|
|
|
+ if (mcp->irq && mcp->irq_controller) {
|
|
|
+ status = mcp23s08_irq_setup(mcp);
|
|
|
+ if (status) {
|
|
|
+ mcp23s08_irq_teardown(mcp);
|
|
|
+ goto fail;
|
|
|
+ }
|
|
|
+ }
|
|
|
fail:
|
|
|
if (status < 0)
|
|
|
dev_dbg(dev, "can't setup chip %d, --> %d\n",
|
|
@@ -546,6 +779,7 @@ static int mcp230xx_probe(struct i2c_client *client,
|
|
|
if (match || !pdata) {
|
|
|
base = -1;
|
|
|
pullups = 0;
|
|
|
+ client->irq = irq_of_parse_and_map(client->dev.of_node, 0);
|
|
|
} else {
|
|
|
if (!gpio_is_valid(pdata->base)) {
|
|
|
dev_dbg(&client->dev, "invalid platform data\n");
|
|
@@ -559,6 +793,7 @@ static int mcp230xx_probe(struct i2c_client *client,
|
|
|
if (!mcp)
|
|
|
return -ENOMEM;
|
|
|
|
|
|
+ mcp->irq = client->irq;
|
|
|
status = mcp23s08_probe_one(mcp, &client->dev, client, client->addr,
|
|
|
id->driver_data, base, pullups);
|
|
|
if (status)
|
|
@@ -579,6 +814,9 @@ static int mcp230xx_remove(struct i2c_client *client)
|
|
|
struct mcp23s08 *mcp = i2c_get_clientdata(client);
|
|
|
int status;
|
|
|
|
|
|
+ if (client->irq && mcp->irq_controller)
|
|
|
+ mcp23s08_irq_teardown(mcp);
|
|
|
+
|
|
|
status = gpiochip_remove(&mcp->chip);
|
|
|
if (status == 0)
|
|
|
kfree(mcp);
|
|
@@ -640,7 +878,7 @@ static int mcp23s08_probe(struct spi_device *spi)
|
|
|
|
|
|
match = of_match_device(of_match_ptr(mcp23s08_spi_of_match), &spi->dev);
|
|
|
if (match) {
|
|
|
- type = (int)match->data;
|
|
|
+ type = (int)(uintptr_t)match->data;
|
|
|
status = of_property_read_u32(spi->dev.of_node,
|
|
|
"microchip,spi-present-mask", &spi_present_mask);
|
|
|
if (status) {
|