[v3,6/7] iio: adc: palmas: add support for iio threshold events

Message ID 20230405212233.4167986-7-risca@dalakolonin.se
State New
Headers
Series iio: adc: palmas_gpadc: add iio events |

Commit Message

Patrik Dahlström April 5, 2023, 9:22 p.m. UTC
  The palmas gpadc block has support for monitoring up to 2 ADC channels
and issue an interrupt if they reach past a set threshold. This change
hooks into the IIO events system and exposes to userspace the ability to
configure these threshold values for each channel, but only allow up to
2 such thresholds to be enabled at any given time. Trying to enable a
third channel will result in an error.

Userspace is expected to input calibrated, as opposed to raw, values as
threshold. However, it is not enough to do the opposite of what is done
when converting the other way around. To account for tolerances in the
ADC, the calculated raw threshold should be adjusted based on the ADC
specifications for the device. These specifications include the integral
nonlinearity (INL), offset, and gain error. To adjust the high
threshold, use the following equation:

  (calibrated value + INL) * Gain error + offset = maximum value  [1]

Likewise, use the following equation for the low threshold:

  (calibrated value - INL) * Gain error - offset = minimum value

The gain error is a combination of gain error, as listed in the
datasheet, and gain error drift due to temperature and supply. The exact
values for these specifications vary between palmas devices. This patch
sets the values found in TWL6035, TWL6037 datasheet.

[1] TI Application Report, SLIA087A, Guide to Using the GPADC in
    TPS65903x, TPS65917-Q1, TPS65919-Q1, and TPS65916 Devices.

Signed-off-by: Patrik Dahlström <risca@dalakolonin.se>
---
V2 -> V3: avoid reconfiguring channels on error and when old == new.

 drivers/iio/adc/palmas_gpadc.c | 447 +++++++++++++++++++++++++++++++--
 1 file changed, 423 insertions(+), 24 deletions(-)
  

Comments

Jonathan Cameron April 7, 2023, 5:19 p.m. UTC | #1
On Wed,  5 Apr 2023 23:22:32 +0200
Patrik Dahlström <risca@dalakolonin.se> wrote:

> The palmas gpadc block has support for monitoring up to 2 ADC channels
> and issue an interrupt if they reach past a set threshold. This change
> hooks into the IIO events system and exposes to userspace the ability to
> configure these threshold values for each channel, but only allow up to
> 2 such thresholds to be enabled at any given time. Trying to enable a
> third channel will result in an error.
> 
> Userspace is expected to input calibrated, as opposed to raw, values as
> threshold. However, it is not enough to do the opposite of what is done
> when converting the other way around. To account for tolerances in the
> ADC, the calculated raw threshold should be adjusted based on the ADC
> specifications for the device. These specifications include the integral
> nonlinearity (INL), offset, and gain error. To adjust the high
> threshold, use the following equation:
> 
>   (calibrated value + INL) * Gain error + offset = maximum value  [1]
> 
> Likewise, use the following equation for the low threshold:
> 
>   (calibrated value - INL) * Gain error - offset = minimum value
> 
> The gain error is a combination of gain error, as listed in the
> datasheet, and gain error drift due to temperature and supply. The exact
> values for these specifications vary between palmas devices. This patch
> sets the values found in TWL6035, TWL6037 datasheet.
> 
> [1] TI Application Report, SLIA087A, Guide to Using the GPADC in
>     TPS65903x, TPS65917-Q1, TPS65919-Q1, and TPS65916 Devices.
> 
> Signed-off-by: Patrik Dahlström <risca@dalakolonin.se>
Hi Patrik,

A few really trivial formatting things inline. If we don't end up
with a v4 for other reasons I can tidy this stuff up whilst applying.

Jonathan


>  
> +/**

Not kernel-doc so /* only
Even if it were the indent for the following should align the * with the first * not
the second one.

> +  * The high and low threshold values are calculated based on the advice given
> +  * in TI Application Report SLIA087A, "Guide to Using the GPADC in PS65903x,
> +  * TPS65917-Q1, TPS65919-Q1, and TPS65916 Devices". This document recommend
> +  * taking ADC tolerances into account and is based on the device integral non-
> +  * linearity (INL), offset error and gain error:
> +  *
> +  *   raw high threshold = (ideal threshold + INL) * gain error + offset error
> +  *
> +  * The gain error include both gain error, as specified in the datasheet, and
> +  * the gain error drift. These paramenters vary depending on device and whether
> +  * the the channel is calibrated (trimmed) or not.
> +  */
> +static int palmas_gpadc_threshold_with_tolerance(int val, const int INL,
> +						 const int gain_error,
> +						 const int offset_error)
> +{
> +	val = ((val + INL) * (1000 + gain_error)) / 1000 + offset_error;
> +
> +	return clamp(val, 0, 0xFFF);
> +}
> +
> +/**

/*

> +  * The values below are taken from the datasheet of TWL6035, TWL6037.
> +  * todo: get max INL, gain error, and offset error from OF.
> +  */
> +static int palmas_gpadc_get_high_threshold_raw(struct palmas_gpadc *adc,
> +					       struct palmas_adc_event *ev)
> +{
> +	const int adc_chan = ev->channel;
> +	int val = adc->thresholds[adc_chan].high;
> +	/* integral nonlinearity, measured in LSB */
> +	const int max_INL = 2;
> +	/* measured in LSB */
> +	int max_offset_error;
> +	/* 0.2% when calibrated */
> +	int max_gain_error = 2;
> +
> +	val = (val * 1000) / adc->adc_info[adc_chan].gain;
> +
> +	if (adc->adc_info[adc_chan].is_uncalibrated) {
> +		/* 2% worse */
> +		max_gain_error += 20;
> +		max_offset_error = 36;
> +	} else {
> +		val = (val * adc->adc_info[adc_chan].gain_error +
> +		       adc->adc_info[adc_chan].offset) /
> +			1000;
> +		max_offset_error = 2;
> +	}
> +
> +	return palmas_gpadc_threshold_with_tolerance(val,
> +						     max_INL,
> +						     max_gain_error,
> +						     max_offset_error);
> +}
> +
> +/**

This isn't kernel-doc so just /* 

> +  * The values below are taken from the datasheet of TWL6035, TWL6037.
> +  * todo: get min INL, gain error, and offset error from OF.
> +  */
> +static int palmas_gpadc_get_low_threshold_raw(struct palmas_gpadc *adc,
> +					      struct palmas_adc_event *ev)
> +{
> +	const int adc_chan = ev->channel;
> +	int val = adc->thresholds[adc_chan].low;
> +	/* integral nonlinearity, measured in LSB */
> +	const int min_INL = -2;
> +	/* measured in LSB */
> +	int min_offset_error;
> +	/* -0.6% when calibrated */
> +	int min_gain_error = -6;
> +
> +	val = (val * 1000) / adc->adc_info[adc_chan].gain;
> +
> +        if (adc->adc_info[adc_chan].is_uncalibrated) {
> +		/* 2% worse */
> +		min_gain_error -= 20;
> +		min_offset_error = -36;
> +        } else {
> +		val = (val * adc->adc_info[adc_chan].gain_error -
> +		       adc->adc_info[adc_chan].offset) /
> +			1000;
> +		min_offset_error = -2;
> +        }
> +
> +	return palmas_gpadc_threshold_with_tolerance(val,
> +						     min_INL,
> +						     min_gain_error,
> +						     min_offset_error);
> +}
> +
>  static int palmas_gpadc_read_raw(struct iio_dev *indio_dev,
>  	struct iio_chan_spec const *chan, int *val, int *val2, long mask)
>  {
> @@ -437,8 +586,221 @@ static int palmas_gpadc_read_raw(struct iio_dev *indio_dev,
>  	return ret;
>  }
>  
> +static int palmas_gpadc_read_event_config(struct iio_dev *indio_dev,
> +					  const struct iio_chan_spec *chan,
> +					  enum iio_event_type type,
> +					  enum iio_event_direction dir)
> +{
> +	struct palmas_gpadc *adc = iio_priv(indio_dev);
> +	int adc_chan = chan->channel;
> +	int ret = 0;
> +
> +	if (adc_chan > PALMAS_ADC_CH_MAX || type != IIO_EV_TYPE_THRESH)
> +		return -EINVAL;
> +
> +	mutex_lock(&adc->lock);
> +
> +	if (palmas_gpadc_get_event(adc, adc_chan, dir)) {
> +		ret = 1;

Trivial: No brackets needed here for kernel style.

> +	}
> +
> +	mutex_unlock(&adc->lock);
> +
> +	return ret;
> +}
...

> +static int palmas_gpadc_write_event_config(struct iio_dev *indio_dev,
> +					   const struct iio_chan_spec *chan,
> +					   enum iio_event_type type,
> +					   enum iio_event_direction dir,
> +					   int state)
> +{
> +	struct palmas_gpadc *adc = iio_priv(indio_dev);
> +	int adc_chan = chan->channel;
> +	int ret = 0;

This initial value isn't used so shouldn't be set.
One of the static analysis tools will spot this so if we don't tidy it up
now chances of it getting 'fixed' later is high.  Better to avoid the
overhead of such a patch.

> +
> +	if (adc_chan > PALMAS_ADC_CH_MAX || type != IIO_EV_TYPE_THRESH)
> +		return -EINVAL;
> +
> +	mutex_lock(&adc->lock);
> +
> +	if (state)
> +		ret = palmas_gpadc_enable_event_config(adc, chan, dir);
> +	else
> +		ret = palmas_gpadc_disable_event_config(adc, chan, dir);
> +
> +	mutex_unlock(&adc->lock);
> +
> +	return ret;
> +}
> +
> +static int palmas_gpadc_read_event_value(struct iio_dev *indio_dev,
> +					 const struct iio_chan_spec *chan,
> +					 enum iio_event_type type,
> +					 enum iio_event_direction dir,
> +					 enum iio_event_info info,
> +					 int *val, int *val2)
> +{
> +	struct palmas_gpadc *adc = iio_priv(indio_dev);
> +	int adc_chan = chan->channel;
> +	int ret = 0;

Trivial: I can't see a path where this initial value is used so it
shouldn't be initialized here.

> +
> +	if (adc_chan > PALMAS_ADC_CH_MAX || type != IIO_EV_TYPE_THRESH)
> +		return -EINVAL;
> +
> +	mutex_lock(&adc->lock);
> +
> +	switch (info) {
> +	case IIO_EV_INFO_VALUE:
> +		*val = (dir == IIO_EV_DIR_RISING) ?
> +			adc->thresholds[adc_chan].high :
> +			adc->thresholds[adc_chan].low;
> +		ret = IIO_VAL_INT;
> +		break;
> +	default:
> +		ret = -EINVAL;
> +		break;
> +	}
> +
> +	mutex_unlock(&adc->lock);
> +
> +	return ret;
> +}
> +
  
Patrik Dahlström April 8, 2023, 11:31 a.m. UTC | #2
On Fri, Apr 07, 2023 at 06:19:47PM +0100, Jonathan Cameron wrote:
> On Wed,  5 Apr 2023 23:22:32 +0200
> Patrik Dahlström <risca@dalakolonin.se> wrote:
> 
> > The palmas gpadc block has support for monitoring up to 2 ADC channels
> > and issue an interrupt if they reach past a set threshold. This change
> > hooks into the IIO events system and exposes to userspace the ability to
> > configure these threshold values for each channel, but only allow up to
> > 2 such thresholds to be enabled at any given time. Trying to enable a
> > third channel will result in an error.
> > 
> > Userspace is expected to input calibrated, as opposed to raw, values as
> > threshold. However, it is not enough to do the opposite of what is done
> > when converting the other way around. To account for tolerances in the
> > ADC, the calculated raw threshold should be adjusted based on the ADC
> > specifications for the device. These specifications include the integral
> > nonlinearity (INL), offset, and gain error. To adjust the high
> > threshold, use the following equation:
> > 
> >   (calibrated value + INL) * Gain error + offset = maximum value  [1]
> > 
> > Likewise, use the following equation for the low threshold:
> > 
> >   (calibrated value - INL) * Gain error - offset = minimum value
> > 
> > The gain error is a combination of gain error, as listed in the
> > datasheet, and gain error drift due to temperature and supply. The exact
> > values for these specifications vary between palmas devices. This patch
> > sets the values found in TWL6035, TWL6037 datasheet.
> > 
> > [1] TI Application Report, SLIA087A, Guide to Using the GPADC in
> >     TPS65903x, TPS65917-Q1, TPS65919-Q1, and TPS65916 Devices.
> > 
> > Signed-off-by: Patrik Dahlström <risca@dalakolonin.se>
> Hi Patrik,
> 
> A few really trivial formatting things inline. If we don't end up
> with a v4 for other reasons I can tidy this stuff up whilst applying.

I'll send in a v4 later today :)

> 
> Jonathan
> 
>
  

Patch

diff --git a/drivers/iio/adc/palmas_gpadc.c b/drivers/iio/adc/palmas_gpadc.c
index da4908608a27..2e0755e9e3a4 100644
--- a/drivers/iio/adc/palmas_gpadc.c
+++ b/drivers/iio/adc/palmas_gpadc.c
@@ -20,6 +20,7 @@ 
 #include <linux/completion.h>
 #include <linux/of.h>
 #include <linux/of_device.h>
+#include <linux/iio/events.h>
 #include <linux/iio/iio.h>
 #include <linux/iio/machine.h>
 #include <linux/iio/driver.h>
@@ -79,10 +80,14 @@  static struct palmas_gpadc_info palmas_gpadc_info[] = {
 struct palmas_adc_event {
 	bool enabled;
 	int channel;
-	int raw_thresh;
 	enum iio_event_direction direction;
 };
 
+struct palmas_gpadc_thresholds {
+	int high;
+	int low;
+};
+
 /*
  * struct palmas_gpadc - the palmas_gpadc structure
  * @ch0_current:	channel 0 current source setting
@@ -120,10 +125,31 @@  struct palmas_gpadc {
 	struct completion		conv_completion;
 	struct palmas_adc_event		event0;
 	struct palmas_adc_event		event1;
+	struct palmas_gpadc_thresholds	thresholds[PALMAS_ADC_CH_MAX];
 	int				auto_conversion_period;
 	struct mutex			lock;
 };
 
+static struct palmas_adc_event *palmas_gpadc_get_event(struct palmas_gpadc *adc,
+						       int adc_chan,
+						       enum iio_event_direction dir)
+{
+	if (adc_chan == adc->event0.channel && dir == adc->event0.direction)
+		return &adc->event0;
+
+	if (adc_chan == adc->event1.channel && dir == adc->event1.direction)
+		return &adc->event1;
+
+	return NULL;
+}
+
+static bool palmas_gpadc_channel_is_freerunning(struct palmas_gpadc *adc,
+						int adc_chan)
+{
+	return palmas_gpadc_get_event(adc, adc_chan, IIO_EV_DIR_RISING) ||
+		palmas_gpadc_get_event(adc, adc_chan, IIO_EV_DIR_FALLING);
+}
+
 /*
  * GPADC lock issue in AUTO mode.
  * Impact: In AUTO mode, GPADC conversion can be locked after disabling AUTO
@@ -193,11 +219,24 @@  static irqreturn_t palmas_gpadc_irq(int irq, void *data)
 
 static irqreturn_t palmas_gpadc_irq_auto(int irq, void *data)
 {
-	struct palmas_gpadc *adc = data;
+	struct iio_dev *indio_dev = data;
+	struct palmas_gpadc *adc = iio_priv(indio_dev);
+	struct palmas_adc_event *ev;
 
 	dev_dbg(adc->dev, "Threshold interrupt %d occurs\n", irq);
 	palmas_disable_auto_conversion(adc);
 
+	ev = (irq == adc->irq_auto_0) ? &adc->event0 : &adc->event1;
+	if (ev->channel != -1) {
+		enum iio_event_direction dir;
+		u64 code;
+
+		dir = ev->direction;
+		code = IIO_UNMOD_EVENT_CODE(IIO_VOLTAGE, ev->channel,
+					    IIO_EV_TYPE_THRESH, dir);
+		iio_push_event(indio_dev, code, iio_get_time_ns(indio_dev));
+	}
+
 	return IRQ_HANDLED;
 }
 
@@ -285,6 +324,9 @@  static int palmas_gpadc_read_prepare(struct palmas_gpadc *adc, int adc_chan)
 {
 	int ret;
 
+	if (palmas_gpadc_channel_is_freerunning(adc, adc_chan))
+		return 0; /* ADC already running */
+
 	ret = palmas_gpadc_enable(adc, adc_chan, true);
 	if (ret < 0)
 		return ret;
@@ -344,28 +386,43 @@  static int palmas_gpadc_start_conversion(struct palmas_gpadc *adc, int adc_chan)
 	unsigned int val;
 	int ret;
 
-	init_completion(&adc->conv_completion);
-	ret = palmas_update_bits(adc->palmas, PALMAS_GPADC_BASE,
-				PALMAS_GPADC_SW_SELECT,
-				PALMAS_GPADC_SW_SELECT_SW_START_CONV0,
-				PALMAS_GPADC_SW_SELECT_SW_START_CONV0);
-	if (ret < 0) {
-		dev_err(adc->dev, "SELECT_SW_START write failed: %d\n", ret);
-		return ret;
-	}
+	if (palmas_gpadc_channel_is_freerunning(adc, adc_chan)) {
+		int event = (adc_chan == adc->event0.channel) ? 0 : 1;
+		unsigned int reg = (event == 0) ?
+			PALMAS_GPADC_AUTO_CONV0_LSB :
+			PALMAS_GPADC_AUTO_CONV1_LSB;
 
-	ret = wait_for_completion_timeout(&adc->conv_completion,
-				PALMAS_ADC_CONVERSION_TIMEOUT);
-	if (ret == 0) {
-		dev_err(adc->dev, "conversion not completed\n");
-		return -ETIMEDOUT;
-	}
+		ret = palmas_bulk_read(adc->palmas, PALMAS_GPADC_BASE,
+					reg, &val, 2);
+		if (ret < 0) {
+			dev_err(adc->dev, "AUTO_CONV%x_LSB read failed: %d\n",
+				event, ret);
+			return ret;
+		}
+	} else {
+		init_completion(&adc->conv_completion);
+		ret = palmas_update_bits(adc->palmas, PALMAS_GPADC_BASE,
+					PALMAS_GPADC_SW_SELECT,
+					PALMAS_GPADC_SW_SELECT_SW_START_CONV0,
+					PALMAS_GPADC_SW_SELECT_SW_START_CONV0);
+		if (ret < 0) {
+			dev_err(adc->dev, "SELECT_SW_START write failed: %d\n", ret);
+			return ret;
+		}
 
-	ret = palmas_bulk_read(adc->palmas, PALMAS_GPADC_BASE,
-				PALMAS_GPADC_SW_CONV0_LSB, &val, 2);
-	if (ret < 0) {
-		dev_err(adc->dev, "SW_CONV0_LSB read failed: %d\n", ret);
-		return ret;
+		ret = wait_for_completion_timeout(&adc->conv_completion,
+					PALMAS_ADC_CONVERSION_TIMEOUT);
+		if (ret == 0) {
+			dev_err(adc->dev, "conversion not completed\n");
+			return -ETIMEDOUT;
+		}
+
+		ret = palmas_bulk_read(adc->palmas, PALMAS_GPADC_BASE,
+					PALMAS_GPADC_SW_CONV0_LSB, &val, 2);
+		if (ret < 0) {
+			dev_err(adc->dev, "SW_CONV0_LSB read failed: %d\n", ret);
+			return ret;
+		}
 	}
 
 	ret = val & 0xFFF;
@@ -391,6 +448,98 @@  static int palmas_gpadc_get_calibrated_code(struct palmas_gpadc *adc,
 	return val;
 }
 
+/**
+  * The high and low threshold values are calculated based on the advice given
+  * in TI Application Report SLIA087A, "Guide to Using the GPADC in PS65903x,
+  * TPS65917-Q1, TPS65919-Q1, and TPS65916 Devices". This document recommend
+  * taking ADC tolerances into account and is based on the device integral non-
+  * linearity (INL), offset error and gain error:
+  *
+  *   raw high threshold = (ideal threshold + INL) * gain error + offset error
+  *
+  * The gain error include both gain error, as specified in the datasheet, and
+  * the gain error drift. These paramenters vary depending on device and whether
+  * the the channel is calibrated (trimmed) or not.
+  */
+static int palmas_gpadc_threshold_with_tolerance(int val, const int INL,
+						 const int gain_error,
+						 const int offset_error)
+{
+	val = ((val + INL) * (1000 + gain_error)) / 1000 + offset_error;
+
+	return clamp(val, 0, 0xFFF);
+}
+
+/**
+  * The values below are taken from the datasheet of TWL6035, TWL6037.
+  * todo: get max INL, gain error, and offset error from OF.
+  */
+static int palmas_gpadc_get_high_threshold_raw(struct palmas_gpadc *adc,
+					       struct palmas_adc_event *ev)
+{
+	const int adc_chan = ev->channel;
+	int val = adc->thresholds[adc_chan].high;
+	/* integral nonlinearity, measured in LSB */
+	const int max_INL = 2;
+	/* measured in LSB */
+	int max_offset_error;
+	/* 0.2% when calibrated */
+	int max_gain_error = 2;
+
+	val = (val * 1000) / adc->adc_info[adc_chan].gain;
+
+	if (adc->adc_info[adc_chan].is_uncalibrated) {
+		/* 2% worse */
+		max_gain_error += 20;
+		max_offset_error = 36;
+	} else {
+		val = (val * adc->adc_info[adc_chan].gain_error +
+		       adc->adc_info[adc_chan].offset) /
+			1000;
+		max_offset_error = 2;
+	}
+
+	return palmas_gpadc_threshold_with_tolerance(val,
+						     max_INL,
+						     max_gain_error,
+						     max_offset_error);
+}
+
+/**
+  * The values below are taken from the datasheet of TWL6035, TWL6037.
+  * todo: get min INL, gain error, and offset error from OF.
+  */
+static int palmas_gpadc_get_low_threshold_raw(struct palmas_gpadc *adc,
+					      struct palmas_adc_event *ev)
+{
+	const int adc_chan = ev->channel;
+	int val = adc->thresholds[adc_chan].low;
+	/* integral nonlinearity, measured in LSB */
+	const int min_INL = -2;
+	/* measured in LSB */
+	int min_offset_error;
+	/* -0.6% when calibrated */
+	int min_gain_error = -6;
+
+	val = (val * 1000) / adc->adc_info[adc_chan].gain;
+
+        if (adc->adc_info[adc_chan].is_uncalibrated) {
+		/* 2% worse */
+		min_gain_error -= 20;
+		min_offset_error = -36;
+        } else {
+		val = (val * adc->adc_info[adc_chan].gain_error -
+		       adc->adc_info[adc_chan].offset) /
+			1000;
+		min_offset_error = -2;
+        }
+
+	return palmas_gpadc_threshold_with_tolerance(val,
+						     min_INL,
+						     min_gain_error,
+						     min_offset_error);
+}
+
 static int palmas_gpadc_read_raw(struct iio_dev *indio_dev,
 	struct iio_chan_spec const *chan, int *val, int *val2, long mask)
 {
@@ -437,8 +586,221 @@  static int palmas_gpadc_read_raw(struct iio_dev *indio_dev,
 	return ret;
 }
 
+static int palmas_gpadc_read_event_config(struct iio_dev *indio_dev,
+					  const struct iio_chan_spec *chan,
+					  enum iio_event_type type,
+					  enum iio_event_direction dir)
+{
+	struct palmas_gpadc *adc = iio_priv(indio_dev);
+	int adc_chan = chan->channel;
+	int ret = 0;
+
+	if (adc_chan > PALMAS_ADC_CH_MAX || type != IIO_EV_TYPE_THRESH)
+		return -EINVAL;
+
+	mutex_lock(&adc->lock);
+
+	if (palmas_gpadc_get_event(adc, adc_chan, dir)) {
+		ret = 1;
+	}
+
+	mutex_unlock(&adc->lock);
+
+	return ret;
+}
+
+static int palmas_adc_configure_events(struct palmas_gpadc *adc);
+static int palmas_adc_reset_events(struct palmas_gpadc *adc);
+
+static int palmas_gpadc_reconfigure_event_channels(struct palmas_gpadc *adc)
+{
+	return (adc->event0.enabled || adc->event1.enabled) ?
+		palmas_adc_configure_events(adc) :
+		palmas_adc_reset_events(adc);
+}
+
+static int palmas_gpadc_enable_event_config(struct palmas_gpadc *adc,
+					    const struct iio_chan_spec *chan,
+					    enum iio_event_direction dir)
+{
+	struct palmas_adc_event *ev;
+	int adc_chan = chan->channel;
+
+	if (palmas_gpadc_get_event(adc, adc_chan, dir))
+		/* already enabled */
+		return 0;
+
+	if (adc->event0.channel == -1) {
+		ev = &adc->event0;
+	} else if (adc->event1.channel == -1) {
+		/* event0 has to be the lowest channel */
+		if (adc_chan < adc->event0.channel) {
+			adc->event1 = adc->event0;
+			ev = &adc->event0;
+		} else {
+			ev = &adc->event1;
+		}
+	} else { /* both AUTO channels already in use */
+		dev_warn(adc->dev, "event0 - %d, event1 - %d\n",
+			 adc->event0.channel, adc->event1.channel);
+		return -EBUSY;
+	}
+
+	ev->enabled = true;
+	ev->channel = adc_chan;
+	ev->direction = dir;
+
+	return palmas_gpadc_reconfigure_event_channels(adc);
+}
+
+static int palmas_gpadc_disable_event_config(struct palmas_gpadc *adc,
+					     const struct iio_chan_spec *chan,
+					     enum iio_event_direction dir)
+{
+	int adc_chan = chan->channel;
+	struct palmas_adc_event *ev = palmas_gpadc_get_event(adc, adc_chan, dir);
+
+	if (!ev)
+		return 0;
+
+	if (ev == &adc->event0) {
+		adc->event0 = adc->event1;
+		ev = &adc->event1;
+	}
+
+	ev->enabled = false;
+	ev->channel = -1;
+	ev->direction = IIO_EV_DIR_NONE;
+
+	return palmas_gpadc_reconfigure_event_channels(adc);
+}
+
+static int palmas_gpadc_write_event_config(struct iio_dev *indio_dev,
+					   const struct iio_chan_spec *chan,
+					   enum iio_event_type type,
+					   enum iio_event_direction dir,
+					   int state)
+{
+	struct palmas_gpadc *adc = iio_priv(indio_dev);
+	int adc_chan = chan->channel;
+	int ret = 0;
+
+	if (adc_chan > PALMAS_ADC_CH_MAX || type != IIO_EV_TYPE_THRESH)
+		return -EINVAL;
+
+	mutex_lock(&adc->lock);
+
+	if (state)
+		ret = palmas_gpadc_enable_event_config(adc, chan, dir);
+	else
+		ret = palmas_gpadc_disable_event_config(adc, chan, dir);
+
+	mutex_unlock(&adc->lock);
+
+	return ret;
+}
+
+static int palmas_gpadc_read_event_value(struct iio_dev *indio_dev,
+					 const struct iio_chan_spec *chan,
+					 enum iio_event_type type,
+					 enum iio_event_direction dir,
+					 enum iio_event_info info,
+					 int *val, int *val2)
+{
+	struct palmas_gpadc *adc = iio_priv(indio_dev);
+	int adc_chan = chan->channel;
+	int ret = 0;
+
+	if (adc_chan > PALMAS_ADC_CH_MAX || type != IIO_EV_TYPE_THRESH)
+		return -EINVAL;
+
+	mutex_lock(&adc->lock);
+
+	switch (info) {
+	case IIO_EV_INFO_VALUE:
+		*val = (dir == IIO_EV_DIR_RISING) ?
+			adc->thresholds[adc_chan].high :
+			adc->thresholds[adc_chan].low;
+		ret = IIO_VAL_INT;
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	mutex_unlock(&adc->lock);
+
+	return ret;
+}
+
+static int palmas_gpadc_write_event_value(struct iio_dev *indio_dev,
+					  const struct iio_chan_spec *chan,
+					  enum iio_event_type type,
+					  enum iio_event_direction dir,
+					  enum iio_event_info info,
+					  int val, int val2)
+{
+	struct palmas_gpadc *adc = iio_priv(indio_dev);
+	int adc_chan = chan->channel;
+	int old;
+	int ret = 0;
+
+	if (adc_chan > PALMAS_ADC_CH_MAX || type != IIO_EV_TYPE_THRESH)
+		return -EINVAL;
+
+	mutex_lock(&adc->lock);
+	switch (info) {
+	case IIO_EV_INFO_VALUE:
+		if (val < 0 || val > 0xFFF) {
+			ret = -EINVAL;
+			break;
+		}
+		if (dir == IIO_EV_DIR_RISING) {
+			old = adc->thresholds[adc_chan].high;
+			adc->thresholds[adc_chan].high = val;
+		}
+		else {
+			old = adc->thresholds[adc_chan].low;
+			adc->thresholds[adc_chan].low = val;
+		}
+		break;
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	if (ret)
+		goto out_unlock;
+
+	if (val != old && palmas_gpadc_get_event(adc, adc_chan, dir))
+		ret = palmas_gpadc_reconfigure_event_channels(adc);
+
+out_unlock:
+	mutex_unlock(&adc->lock);
+
+	return ret;
+}
+
 static const struct iio_info palmas_gpadc_iio_info = {
 	.read_raw = palmas_gpadc_read_raw,
+	.read_event_config = palmas_gpadc_read_event_config,
+	.write_event_config = palmas_gpadc_write_event_config,
+	.read_event_value = palmas_gpadc_read_event_value,
+	.write_event_value = palmas_gpadc_write_event_value,
+};
+
+static const struct iio_event_spec palmas_gpadc_events[] = {
+	{
+		.type = IIO_EV_TYPE_THRESH,
+		.dir = IIO_EV_DIR_RISING,
+		.mask_separate = BIT(IIO_EV_INFO_VALUE) |
+				BIT(IIO_EV_INFO_ENABLE),
+	}, {
+		.type = IIO_EV_TYPE_THRESH,
+		.dir = IIO_EV_DIR_FALLING,
+		.mask_separate = BIT(IIO_EV_INFO_VALUE) |
+				BIT(IIO_EV_INFO_ENABLE),
+	},
 };
 
 #define PALMAS_ADC_CHAN_IIO(chan, _type, chan_info)	\
@@ -449,6 +811,8 @@  static const struct iio_info palmas_gpadc_iio_info = {
 			BIT(chan_info),			\
 	.indexed = 1,					\
 	.channel = PALMAS_ADC_CH_##chan,		\
+	.event_spec = palmas_gpadc_events,		\
+	.num_event_specs = ARRAY_SIZE(palmas_gpadc_events)	\
 }
 
 static const struct iio_chan_spec palmas_gpadc_iio_channel[] = {
@@ -555,6 +919,39 @@  static int palmas_gpadc_probe(struct platform_device *pdev)
 		return dev_err_probe(adc->dev, ret,
 				     "request irq %d failed\n", adc->irq);
 
+	adc->irq_auto_0 = platform_get_irq(pdev, 1);
+	if (adc->irq_auto_0 < 0)
+		return dev_err_probe(adc->dev, adc->irq_auto_0,
+				     "get auto0 irq failed\n");
+
+	ret = devm_request_threaded_irq(&pdev->dev, adc->irq_auto_0, NULL,
+					palmas_gpadc_irq_auto, IRQF_ONESHOT,
+					"palmas-adc-auto-0", indio_dev);
+	if (ret < 0)
+		return dev_err_probe(adc->dev, ret,
+				     "request auto0 irq %d failed\n",
+				     adc->irq_auto_0);
+
+	adc->irq_auto_1 = platform_get_irq(pdev, 2);
+	if (adc->irq_auto_1 < 0)
+		return dev_err_probe(adc->dev, adc->irq_auto_1,
+				     "get auto1 irq failed\n");
+
+	ret = devm_request_threaded_irq(&pdev->dev, adc->irq_auto_1, NULL,
+					palmas_gpadc_irq_auto, IRQF_ONESHOT,
+					"palmas-adc-auto-1", indio_dev);
+	if (ret < 0)
+		return dev_err_probe(adc->dev, ret,
+				     "request auto1 irq %d failed\n",
+				     adc->irq_auto_1);
+
+	adc->event0.enabled = false;
+	adc->event0.channel = -1;
+	adc->event0.direction = IIO_EV_DIR_NONE;
+	adc->event1.enabled = false;
+	adc->event1.channel = -1;
+	adc->event1.direction = IIO_EV_DIR_NONE;
+
 	/* set the current source 0 (value 0/5/15/20 uA => 0..3) */
 	if (gpadc_pdata->ch0_current <= 1)
 		adc->ch0_current = PALMAS_ADC_CH0_CURRENT_SRC_0;
@@ -632,13 +1029,14 @@  static int palmas_adc_configure_events(struct palmas_gpadc *adc)
 		int polarity;
 
 		ch0 = ev->channel;
-		thres = ev->raw_thresh;
 		conv |= PALMAS_GPADC_AUTO_CTRL_AUTO_CONV0_EN;
 		switch (ev->direction) {
 		case IIO_EV_DIR_RISING:
+			thres = palmas_gpadc_get_high_threshold_raw(adc, ev);
 			polarity = 0;
 			break;
 		case IIO_EV_DIR_FALLING:
+			thres = palmas_gpadc_get_low_threshold_raw(adc, ev);
 			polarity = PALMAS_GPADC_THRES_CONV0_MSB_THRES_CONV0_POL;
 			break;
 		default:
@@ -668,13 +1066,14 @@  static int palmas_adc_configure_events(struct palmas_gpadc *adc)
 		int polarity;
 
 		ch1 = ev->channel;
-		thres = ev->raw_thresh;
 		conv |= PALMAS_GPADC_AUTO_CTRL_AUTO_CONV1_EN;
 		switch (ev->direction) {
 		case IIO_EV_DIR_RISING:
+			thres = palmas_gpadc_get_high_threshold_raw(adc, ev);
 			polarity = 0;
 			break;
 		case IIO_EV_DIR_FALLING:
+			thres = palmas_gpadc_get_low_threshold_raw(adc, ev);
 			polarity = PALMAS_GPADC_THRES_CONV1_MSB_THRES_CONV1_POL;
 			break;
 		default: