This patch allows for multiple Analog Devices ADCs to be placed on the same SPI bus. While it's not possible for them to share interrupts arbitrarily, a special restricted form of sharing for this special case is implemented here. The first instance of an ADC using a given interrupt will acquire the IRQ and register an object in a global list. Any subsequent instances will increment a reference count on this object. During a conversion, the active instance indicates that it is the recipient of the interrupt by setting a pointer on the object shared among it and the other instances. The existing CS locking mechanism guarantees that no more than one instance per bus will be expecting the interrupt at any time. Signed-off-by: Daniel Beer <daniel.beer@xxxxxxxxxxxxxxxxx> --- drivers/iio/adc/ad_sigma_delta.c | 191 +++++++++++++++++++++++-- include/linux/iio/adc/ad_sigma_delta.h | 4 + 2 files changed, 181 insertions(+), 14 deletions(-) diff --git a/drivers/iio/adc/ad_sigma_delta.c b/drivers/iio/adc/ad_sigma_delta.c index cd418bd8bd87..cd593af6ef3a 100644 --- a/drivers/iio/adc/ad_sigma_delta.c +++ b/drivers/iio/adc/ad_sigma_delta.c @@ -13,6 +13,7 @@ #include <linux/spi/spi.h> #include <linux/err.h> #include <linux/module.h> +#include <linux/list.h> #include <linux/iio/iio.h> #include <linux/iio/sysfs.h> @@ -24,6 +25,161 @@ #include <asm/unaligned.h> +static irqreturn_t ad_sd_data_rdy_trig_poll(int irq, void *private); + +struct ad_sigma_delta_interrupt { + /* Constant from time of creation */ + int irq; + struct spi_master *master; + + /* Protected by global lock */ + struct list_head list; + int refcnt; + + /* Protected by lock on corresponding SPI bus */ + struct ad_sigma_delta *active; +}; + +static DEFINE_MUTEX(interrupt_lock); +static LIST_HEAD(interrupt_list); + +static void adsdi_enable(struct ad_sigma_delta *a) +{ + struct ad_sigma_delta_interrupt *intr = a->interrupt; + + WARN_ON(intr->active); + intr->active = a; + pr_debug("ad_sigma_delta: enable %d for %p\n", intr->irq, a); + enable_irq(intr->irq); +} + +static void adsdi_disable(struct ad_sigma_delta *a, int nosync) +{ + struct ad_sigma_delta_interrupt *intr = a->interrupt; + + if (nosync) + disable_irq_nosync(intr->irq); + else + disable_irq(intr->irq); + + pr_debug("ad_sigma_delta: disable %d for %p\n", intr->irq, intr->active); + + /* In the case of a timeout, it's possible for adsdi_disable to + * be called twice by the same instance (if the interrupt runs + * between the call to check and the call to disable). + * + * We still need to disable first before checking intr->active. + * Then we can roll back if we've done it twice. + */ + if (intr->active != a) { + WARN_ON(intr->active); + pr_debug("ad_sigma_delta: double-disable\n"); + enable_irq(intr->irq); + } + + intr->active = NULL; +} + +static int adsdi_get(struct ad_sigma_delta_interrupt **intr_ret, + int irq, struct spi_master *master, + int flags) +{ + struct ad_sigma_delta_interrupt *intr = NULL; + struct list_head *ptr; + int ret = 0; + + mutex_lock(&interrupt_lock); + + /* Try to find an existing instance */ + list_for_each(ptr, &interrupt_list) { + struct ad_sigma_delta_interrupt *i = list_entry(ptr, + struct ad_sigma_delta_interrupt, list); + + if (i->irq == irq) { + /* No instance will attempt to wait for the + * interrupt without the SPI bus locked, which + * we can rely on to ensure correct operation. + * However, we would like to detect + * misconfiguration that would lead to unsafe + * access. + */ + if (i->master != master) { + pr_err( + "ad_sigma_delta: SPI master mismatch on IRQ %d\n", + irq); + ret = -EINVAL; + goto fail_search; + } + + intr = i; + break; + } + } + + /* Allocate a new one if necessary */ + if (!intr) { + intr = kmalloc(sizeof(*intr), GFP_KERNEL); + if (!intr) { + ret = -ENOMEM; + pr_err("ad_sigma_delta: can't allocate memory\n"); + goto fail_search; + } + + intr->irq = irq; + intr->refcnt = 0; + intr->active = NULL; + intr->master = master; + + ret = request_irq(irq, + ad_sd_data_rdy_trig_poll, + flags | IRQF_NO_AUTOEN, + "ad_sigma_delta", + intr); + if (ret) + goto fail_search; + + pr_debug("ad_sigma_delta: sharing interrupt %d\n", irq); + list_add(&intr->list, &interrupt_list); + } + + intr->refcnt++; + *intr_ret = intr; + +fail_search: + mutex_unlock(&interrupt_lock); + return ret; +} + +static void adsdi_put(struct ad_sigma_delta_interrupt *intr) +{ + mutex_lock(&interrupt_lock); + if (!--intr->refcnt) { + pr_debug("ad_sigma_delta: interrupt %d deallocated\n", + intr->irq); + free_irq(intr->irq, intr); + list_del(&intr->list); + kfree(intr); + } + mutex_unlock(&interrupt_lock); +} + +static void devm_adsdi_release(void *arg) +{ + adsdi_put(arg); +} + +static int devm_adsdi_get(struct device *dev, + struct ad_sigma_delta_interrupt **intr_ret, + int irq, struct spi_master *master, + int flags) +{ + const int ret = adsdi_get(intr_ret, irq, master, flags); + + if (ret < 0) + return ret; + + return devm_add_action_or_reset(dev, devm_adsdi_release, *intr_ret); +} #define AD_SD_COMM_CHAN_MASK 0x3 @@ -221,11 +377,11 @@ int ad_sd_calibrate(struct ad_sigma_delta *sigma_delta, goto out; sigma_delta->irq_dis = false; - enable_irq(sigma_delta->spi->irq); + adsdi_enable(sigma_delta); timeout = wait_for_completion_timeout(&sigma_delta->completion, 2 * HZ); if (timeout == 0) { sigma_delta->irq_dis = true; - disable_irq_nosync(sigma_delta->spi->irq); + adsdi_disable(sigma_delta, 0); ret = -EIO; } else { ret = 0; @@ -294,7 +450,7 @@ int ad_sigma_delta_single_conversion(struct iio_dev *indio_dev, ad_sigma_delta_set_mode(sigma_delta, AD_SD_MODE_SINGLE); sigma_delta->irq_dis = false; - enable_irq(sigma_delta->spi->irq); + adsdi_enable(sigma_delta); ret = wait_for_completion_interruptible_timeout( &sigma_delta->completion, HZ); @@ -314,7 +470,7 @@ int ad_sigma_delta_single_conversion(struct iio_dev *indio_dev, out: if (!sigma_delta->irq_dis) { - disable_irq_nosync(sigma_delta->spi->irq); + adsdi_disable(sigma_delta, 0); sigma_delta->irq_dis = true; } @@ -361,7 +517,7 @@ static int ad_sd_buffer_postenable(struct iio_dev *indio_dev) goto err_unlock; sigma_delta->irq_dis = false; - enable_irq(sigma_delta->spi->irq); + adsdi_enable(sigma_delta); return 0; @@ -379,7 +535,7 @@ static int ad_sd_buffer_postdisable(struct iio_dev *indio_dev) wait_for_completion_timeout(&sigma_delta->completion, HZ); if (!sigma_delta->irq_dis) { - disable_irq_nosync(sigma_delta->spi->irq); + adsdi_disable(sigma_delta, 0); sigma_delta->irq_dis = true; } @@ -425,7 +581,7 @@ static irqreturn_t ad_sd_trigger_handler(int irq, void *p) iio_trigger_notify_done(indio_dev->trig); sigma_delta->irq_dis = false; - enable_irq(sigma_delta->spi->irq); + adsdi_enable(sigma_delta); return IRQ_HANDLED; } @@ -438,10 +594,17 @@ static const struct iio_buffer_setup_ops ad_sd_buffer_setup_ops = { static irqreturn_t ad_sd_data_rdy_trig_poll(int irq, void *private) { - struct ad_sigma_delta *sigma_delta = private; + struct ad_sigma_delta_interrupt *intr = private; + struct ad_sigma_delta *sigma_delta = intr->active; + + WARN_ON(!sigma_delta); + if (!sigma_delta) + return IRQ_NONE; + pr_debug("ad_sigma_delta: interrupt %d fired for %p\n", + intr->irq, sigma_delta); complete(&sigma_delta->completion); - disable_irq_nosync(irq); + adsdi_disable(sigma_delta, 1); sigma_delta->irq_dis = true; iio_trigger_poll(sigma_delta->trig); @@ -486,11 +649,11 @@ static int devm_ad_sd_probe_trigger(struct device *dev, struct iio_dev *indio_de init_completion(&sigma_delta->completion); sigma_delta->irq_dis = true; - ret = devm_request_irq(dev, sigma_delta->spi->irq, - ad_sd_data_rdy_trig_poll, - sigma_delta->info->irq_flags | IRQF_NO_AUTOEN, - indio_dev->name, - sigma_delta); + ret = devm_adsdi_get(dev, + &sigma_delta->interrupt, + sigma_delta->spi->irq, + sigma_delta->spi->master, + sigma_delta->info->irq_flags); if (ret) return ret; diff --git a/include/linux/iio/adc/ad_sigma_delta.h b/include/linux/iio/adc/ad_sigma_delta.h index c525fd51652f..62f38cfe807b 100644 --- a/include/linux/iio/adc/ad_sigma_delta.h +++ b/include/linux/iio/adc/ad_sigma_delta.h @@ -54,6 +54,9 @@ struct ad_sigma_delta_info { unsigned long irq_flags; }; +/* Data relating to interrupt sharing */ +struct ad_sigma_delta_interrupt; + /** * struct ad_sigma_delta - Sigma Delta device struct * @spi: The spi device associated with the Sigma Delta device. @@ -76,6 +79,7 @@ struct ad_sigma_delta { uint8_t comm; const struct ad_sigma_delta_info *info; + struct ad_sigma_delta_interrupt *interrupt; /* * DMA (thus cache coherency maintenance) requires the -- 2.30.2