diff options
Diffstat (limited to 'drivers/gpio/gpio-mcp23s08.c')
| -rw-r--r-- | drivers/gpio/gpio-mcp23s08.c | 291 | 
1 files changed, 266 insertions, 25 deletions
diff --git a/drivers/gpio/gpio-mcp23s08.c b/drivers/gpio/gpio-mcp23s08.c index 2deb0c5e54a..57adbc90fda 100644 --- a/drivers/gpio/gpio-mcp23s08.c +++ b/drivers/gpio/gpio-mcp23s08.c @@ -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) @@ -152,7 +173,7 @@ static int mcp23s08_read(struct mcp23s08 *mcp, unsigned reg)  	tx[0] = mcp->addr | 0x01;  	tx[1] = reg; -	status = spi_write_then_read(mcp->data, tx, sizeof tx, rx, sizeof rx); +	status = spi_write_then_read(mcp->data, tx, sizeof(tx), rx, sizeof(rx));  	return (status < 0) ? status : rx[0];  } @@ -163,7 +184,7 @@ static int mcp23s08_write(struct mcp23s08 *mcp, unsigned reg, unsigned val)  	tx[0] = mcp->addr;  	tx[1] = reg;  	tx[2] = val; -	return spi_write_then_read(mcp->data, tx, sizeof tx, NULL, 0); +	return spi_write_then_read(mcp->data, tx, sizeof(tx), NULL, 0);  }  static int @@ -172,13 +193,13 @@ mcp23s08_read_regs(struct mcp23s08 *mcp, unsigned reg, u16 *vals, unsigned n)  	u8	tx[2], *tmp;  	int	status; -	if ((n + reg) > sizeof mcp->cache) +	if ((n + reg) > sizeof(mcp->cache))  		return -EINVAL;  	tx[0] = mcp->addr | 0x01;  	tx[1] = reg;  	tmp = (u8 *)vals; -	status = spi_write_then_read(mcp->data, tx, sizeof tx, tmp, n); +	status = spi_write_then_read(mcp->data, tx, sizeof(tx), tmp, n);  	if (status >= 0) {  		while (n--)  			vals[n] = tmp[n]; /* expand to 16bit */ @@ -193,7 +214,7 @@ static int mcp23s17_read(struct mcp23s08 *mcp, unsigned reg)  	tx[0] = mcp->addr | 0x01;  	tx[1] = reg << 1; -	status = spi_write_then_read(mcp->data, tx, sizeof tx, rx, sizeof rx); +	status = spi_write_then_read(mcp->data, tx, sizeof(tx), rx, sizeof(rx));  	return (status < 0) ? status : (rx[0] | (rx[1] << 8));  } @@ -205,7 +226,7 @@ static int mcp23s17_write(struct mcp23s08 *mcp, unsigned reg, unsigned val)  	tx[1] = reg << 1;  	tx[2] = val;  	tx[3] = val >> 8; -	return spi_write_then_read(mcp->data, tx, sizeof tx, NULL, 0); +	return spi_write_then_read(mcp->data, tx, sizeof(tx), NULL, 0);  }  static int @@ -214,12 +235,12 @@ mcp23s17_read_regs(struct mcp23s08 *mcp, unsigned reg, u16 *vals, unsigned n)  	u8	tx[2];  	int	status; -	if ((n + reg) > sizeof mcp->cache) +	if ((n + reg) > sizeof(mcp->cache))  		return -EINVAL;  	tx[0] = mcp->addr | 0x01;  	tx[1] = reg << 1; -	status = spi_write_then_read(mcp->data, tx, sizeof tx, +	status = spi_write_then_read(mcp->data, tx, sizeof(tx),  				     (u8 *)vals, n * 2);  	if (status >= 0) {  		while (n--) @@ -316,6 +337,195 @@ mcp23s08_direction_output(struct gpio_chip *chip, unsigned offset, int value)  }  /*----------------------------------------------------------------------*/ +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 int mcp23s08_irq_reqres(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); +		return -EINVAL; +	} + +	return 0; +} + +static void mcp23s08_irq_relres(struct irq_data *data) +{ +	struct mcp23s08 *mcp = irq_data_get_irq_chip_data(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_request_resources = mcp23s08_irq_reqres, +	.irq_release_resources = mcp23s08_irq_relres, +}; + +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 @@ -357,7 +567,7 @@ static void mcp23s08_dbg_show(struct seq_file *s, struct gpio_chip *chip)  			(mcp->cache[MCP_GPIO] & mask) ? "hi" : "lo",  			(mcp->cache[MCP_GPPU] & mask) ? "up" : "  ");  		/* NOTE:  ignoring the irq-related registers */ -		seq_printf(s, "\n"); +		seq_puts(s, "\n");  	}  done:  	mutex_unlock(&mcp->lock); @@ -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", @@ -481,7 +714,7 @@ fail:  #ifdef CONFIG_OF  #ifdef CONFIG_SPI_MASTER -static struct of_device_id mcp23s08_spi_of_match[] = { +static const struct of_device_id mcp23s08_spi_of_match[] = {  	{  		.compatible = "microchip,mcp23s08",  		.data = (void *) MCP_TYPE_S08, @@ -505,7 +738,7 @@ MODULE_DEVICE_TABLE(of, mcp23s08_spi_of_match);  #endif  #if IS_ENABLED(CONFIG_I2C) -static struct of_device_id mcp23s08_i2c_of_match[] = { +static const struct of_device_id mcp23s08_i2c_of_match[] = {  	{  		.compatible = "microchip,mcp23008",  		.data = (void *) MCP_TYPE_008, @@ -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"); @@ -555,10 +789,11 @@ static int mcp230xx_probe(struct i2c_client *client,  		pullups = pdata->chip[0].pullups;  	} -	mcp = kzalloc(sizeof *mcp, GFP_KERNEL); +	mcp = kzalloc(sizeof(*mcp), GFP_KERNEL);  	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); @@ -629,7 +867,7 @@ static int mcp23s08_probe(struct spi_device *spi)  {  	struct mcp23s08_platform_data	*pdata;  	unsigned			addr; -	unsigned			chips = 0; +	int				chips = 0;  	struct mcp23s08_driver_data	*data;  	int				status, type;  	unsigned			base = -1, @@ -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) { @@ -657,8 +895,11 @@ static int mcp23s08_probe(struct spi_device *spi)  			return -ENODEV;  		} -		for (addr = 0; addr < ARRAY_SIZE(pdata->chip); addr++) +		for (addr = 0; addr < ARRAY_SIZE(pdata->chip); addr++) {  			pullups[addr] = 0; +			if (spi_present_mask & (1 << addr)) +				chips++; +		}  	} else {  		type = spi_get_device_id(spi)->driver_data;  		pdata = dev_get_platdata(&spi->dev); @@ -681,13 +922,13 @@ static int mcp23s08_probe(struct spi_device *spi)  			pullups[addr] = pdata->chip[addr].pullups;  		} -		if (!chips) -			return -ENODEV; -  		base = pdata->base;  	} -	data = kzalloc(sizeof *data + chips * sizeof(struct mcp23s08), +	if (!chips) +		return -ENODEV; + +	data = kzalloc(sizeof(*data) + chips * sizeof(struct mcp23s08),  			GFP_KERNEL);  	if (!data)  		return -ENOMEM;  | 
