diff mbox series

[4/4] ASoC: codecs: Add support for the generic IIO auxiliary devices

Message ID 20230421124122.324820-5-herve.codina@bootlin.com
State New
Headers show
Series Add support for IIO devices in ASoC | expand

Commit Message

Herve Codina April 21, 2023, 12:41 p.m. UTC
Industrial I/O devices can be present in the audio path.
These devices needs to be used as audio components in order to be fully
integrated in the audio path.

This support allows to consider these Industrial I/O devices as auxliary
audio devices and allows to control them using mixer controls.

Signed-off-by: Herve Codina <herve.codina@bootlin.com>
---
 sound/soc/codecs/Kconfig          |  12 ++
 sound/soc/codecs/Makefile         |   2 +
 sound/soc/codecs/simple-iio-aux.c | 307 ++++++++++++++++++++++++++++++
 3 files changed, 321 insertions(+)
 create mode 100644 sound/soc/codecs/simple-iio-aux.c

Comments

Jonathan Cameron April 22, 2023, 5:08 p.m. UTC | #1
On Fri, 21 Apr 2023 14:41:22 +0200
Herve Codina <herve.codina@bootlin.com> wrote:

> Industrial I/O devices can be present in the audio path.
> These devices needs to be used as audio components in order to be fully
> integrated in the audio path.
> 
> This support allows to consider these Industrial I/O devices as auxliary
> audio devices and allows to control them using mixer controls.
> 
> Signed-off-by: Herve Codina <herve.codina@bootlin.com>

Hi Herve,

There are some other IIO devices that might turn up in audio paths. In theory
someone might put an IIO supported amplifier in there (though current ones are
far to high frequency and expensive for that to make sense).  For now it
probably makes sense to support potentiometers as you are doing here,
though I'm guessing that in many cases they would be used with some other
analog components. Does the transfer function matter at all?

Been many years since I last touched anything in ASoC so questions may
be silly ;)

A few comments inline.

Jonathan

> +static int simple_iio_aux_get_volsw(struct snd_kcontrol *kcontrol,
> +				    struct snd_ctl_elem_value *ucontrol)
> +{
> +	struct simple_iio_aux_chan *chan = (struct simple_iio_aux_chan *)kcontrol->private_value;
> +	int max = chan->max;
> +	int min = chan->min;
> +	unsigned int mask = (1 << fls(max)) - 1;

As below. I'm not following reason for use of mask

> +	unsigned int invert = chan->is_inverted;
> +	int ret;
> +	int val;
> +
> +	ret = iio_read_channel_raw(chan->iio_chan, &val);
> +	if (ret < 0)
> +		return ret;
> +
> +	ucontrol->value.integer.value[0] = (val & mask) - min;
> +	if (invert)
> +		ucontrol->value.integer.value[0] = max - ucontrol->value.integer.value[0];
> +
> +	return 0;
> +}
> +
> +static int simple_iio_aux_put_volsw(struct snd_kcontrol *kcontrol,
> +				    struct snd_ctl_elem_value *ucontrol)
> +{
> +	struct simple_iio_aux_chan *chan = (struct simple_iio_aux_chan *)kcontrol->private_value;
> +	int max = chan->max;
> +	int min = chan->min;
> +	unsigned int mask = (1 << fls(max)) - 1;

Why is mask needed?  Also seems like handling is making
some strong assumptions on form of max and min.
So at minimum some comments on reasoning needed.

> +	unsigned int invert = chan->is_inverted;
> +	int val;
> +	int ret;
> +	int tmp;
> +
> +	val = ucontrol->value.integer.value[0];
> +	if (val < 0)
> +		return -EINVAL;
> +	if (val > max - min)
> +		return -EINVAL;
> +
> +	val = (val + min) & mask;
> +	if (invert)
> +		val = max - val;
> +
> +	ret = iio_read_channel_raw(chan->iio_chan, &tmp);
> +	if (ret < 0)
> +		return ret;
> +
> +	if (tmp == val)
> +		return 0;
> +
> +	ret = iio_write_channel_raw(chan->iio_chan, val);
> +	if (ret)
> +		return ret;
> +
> +	return 1; /* The value changed */
> +}
> +

...



> +static int simple_iio_aux_probe(struct platform_device *pdev)
> +{
> +	struct device_node *np = pdev->dev.of_node;
> +	struct simple_iio_aux_chan *iio_aux_chan;
> +	struct simple_iio_aux *iio_aux;
> +	int count;
> +	u32 tmp;
> +	int ret;
> +	int i;
> +
> +	iio_aux = devm_kzalloc(&pdev->dev, sizeof(*iio_aux), GFP_KERNEL);
> +	if (!iio_aux)
> +		return -ENOMEM;
> +
> +	iio_aux->dev = &pdev->dev;
> +
> +	count = of_property_count_strings(np, "io-channel-names");
> +	if (count < 0) {
> +		dev_err(iio_aux->dev, "%pOF: failed to read io-channel-names\n", np);
> +		return count;
> +	}
> +
> +	iio_aux->chans = devm_kmalloc_array(&pdev->dev, count,
> +					    sizeof(*iio_aux->chans), GFP_KERNEL);
> +	if (!iio_aux->chans)
> +		return -ENOMEM;
> +	iio_aux->num_chans = count;
> +
> +	for (i = 0; i < iio_aux->num_chans; i++) {
> +		iio_aux_chan = iio_aux->chans + i;
> +
> +		ret = of_property_read_string_index(np, "io-channel-names", i,
> +						    &iio_aux_chan->name);

Whilst today this will be tightly couple with of, if you can use generic firmware
handling where possible (from linux/property.h) it will reduce what needs
to be tidied up if anyone fills in the gaps for IIO consumer bindings in ACPI
and then someone uses PRP0001 based ACPI bindings.

> +		if (ret < 0) {
> +			dev_err(iio_aux->dev, "%pOF: failed to read io-channel-names[%d]\n", np, i);

dev_err_probe() would simplify these cases a little.  Not sure on ASOC view on using
that for cases that won't defer.  I tend to take the view it's nicer everywhere
for calls in probe() functions.


> +			return ret;
> +		}
> +
> +		iio_aux_chan->iio_chan = devm_iio_channel_get(iio_aux->dev, iio_aux_chan->name);
> +		if (IS_ERR(iio_aux_chan->iio_chan)) {
> +			ret = PTR_ERR(iio_aux_chan->iio_chan);

Put that inline instead of setting ret here.

> +			return dev_err_probe(iio_aux->dev, ret,
> +					     "get IIO channel '%s' failed (%d)\n",
> +					     iio_aux_chan->name, ret);
> +		}
> +
> +		tmp = 0;
> +		of_property_read_u32_index(np, "invert", i, &tmp);
> +		iio_aux_chan->is_inverted = !!tmp;

As it's a bool this is the same as 
		iio_aux_chan->is_inverted = tmp;

> +	}
> +
> +	platform_set_drvdata(pdev, iio_aux);
> +
> +	return devm_snd_soc_register_component(iio_aux->dev,
> +					       &simple_iio_aux_component_driver,
> +					       NULL, 0);
> +}
> +
> +#ifdef CONFIG_OF
> +static const struct of_device_id simple_iio_aux_ids[] = {
> +	{ .compatible = "simple-iio-aux", },
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(of, simple_iio_aux_ids);
> +#endif
> +
> +static struct platform_driver simple_iio_aux_driver = {
> +	.driver = {
> +		.name = "simple-iio-aux",
> +		.of_match_table = of_match_ptr(simple_iio_aux_ids),

I'd just drop the of_match_ptr()  Whilst this won't work today with other
firmwares, we might enable the missing parts at some stage. Also the
driver is somewhat pointless without DT so I'd just assume it's always
built with it.  Cost is a tiny array on systems with a weird
.config

> +	},
> +	.probe = simple_iio_aux_probe,
> +};
> +
> +module_platform_driver(simple_iio_aux_driver);
> +
> +MODULE_AUTHOR("Herve Codina <herve.codina@bootlin.com>");
> +MODULE_DESCRIPTION("IIO ALSA SoC aux driver");
> +MODULE_LICENSE("GPL");
Herve Codina April 24, 2023, 10:52 a.m. UTC | #2
Hi Jonathan, Mark,

On Sat, 22 Apr 2023 18:08:14 +0100
Jonathan Cameron <jic23@kernel.org> wrote:

> On Fri, 21 Apr 2023 14:41:22 +0200
> Herve Codina <herve.codina@bootlin.com> wrote:
> 
> > Industrial I/O devices can be present in the audio path.
> > These devices needs to be used as audio components in order to be fully
> > integrated in the audio path.
> > 
> > This support allows to consider these Industrial I/O devices as auxliary
> > audio devices and allows to control them using mixer controls.
> > 
> > Signed-off-by: Herve Codina <herve.codina@bootlin.com>  
> 
> Hi Herve,
> 
> There are some other IIO devices that might turn up in audio paths. In theory
> someone might put an IIO supported amplifier in there (though current ones are
> far to high frequency and expensive for that to make sense).  For now it
> probably makes sense to support potentiometers as you are doing here,
> though I'm guessing that in many cases they would be used with some other
> analog components. Does the transfer function matter at all?
> 
> Been many years since I last touched anything in ASoC so questions may
> be silly ;)
> 
> A few comments inline.
> 
> Jonathan
> 
> > +static int simple_iio_aux_get_volsw(struct snd_kcontrol *kcontrol,
> > +				    struct snd_ctl_elem_value *ucontrol)
> > +{
> > +	struct simple_iio_aux_chan *chan = (struct simple_iio_aux_chan *)kcontrol->private_value;
> > +	int max = chan->max;
> > +	int min = chan->min;
> > +	unsigned int mask = (1 << fls(max)) - 1;  
> 
> As below. I'm not following reason for use of mask
> 
> > +	unsigned int invert = chan->is_inverted;
> > +	int ret;
> > +	int val;
> > +
> > +	ret = iio_read_channel_raw(chan->iio_chan, &val);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	ucontrol->value.integer.value[0] = (val & mask) - min;
> > +	if (invert)
> > +		ucontrol->value.integer.value[0] = max - ucontrol->value.integer.value[0];
> > +
> > +	return 0;
> > +}
> > +
> > +static int simple_iio_aux_put_volsw(struct snd_kcontrol *kcontrol,
> > +				    struct snd_ctl_elem_value *ucontrol)
> > +{
> > +	struct simple_iio_aux_chan *chan = (struct simple_iio_aux_chan *)kcontrol->private_value;
> > +	int max = chan->max;
> > +	int min = chan->min;
> > +	unsigned int mask = (1 << fls(max)) - 1;  
> 
> Why is mask needed?  Also seems like handling is making
> some strong assumptions on form of max and min.
> So at minimum some comments on reasoning needed.

This mask was present in the internal ASoC helpers used when
devices can be accessed using regmap.
The IIO accesses done by simple_iio_aux_get_volsw() and 
simple_iio_aux_put_volsw() were based on these internal helpers.
Not sure about the exact reason to this mask. Maybe Mark can answer.

For these particular use-cases using an IIO channel, the mask present in
simple_iio_aux_get_volsw() and simple_iio_aux_put_volsw() can be removed.

I will remove in the next iteration except if Mark tell me to keep them.

> 
> > +	unsigned int invert = chan->is_inverted;
> > +	int val;
> > +	int ret;
> > +	int tmp;
> > +
> > +	val = ucontrol->value.integer.value[0];
> > +	if (val < 0)
> > +		return -EINVAL;
> > +	if (val > max - min)
> > +		return -EINVAL;
> > +
> > +	val = (val + min) & mask;
> > +	if (invert)
> > +		val = max - val;
> > +
> > +	ret = iio_read_channel_raw(chan->iio_chan, &tmp);
> > +	if (ret < 0)
> > +		return ret;
> > +
> > +	if (tmp == val)
> > +		return 0;
> > +
> > +	ret = iio_write_channel_raw(chan->iio_chan, val);
> > +	if (ret)
> > +		return ret;
> > +
> > +	return 1; /* The value changed */
> > +}
> > +  
> 
> ...
> 
> 
> 
> > +static int simple_iio_aux_probe(struct platform_device *pdev)
> > +{
> > +	struct device_node *np = pdev->dev.of_node;
> > +	struct simple_iio_aux_chan *iio_aux_chan;
> > +	struct simple_iio_aux *iio_aux;
> > +	int count;
> > +	u32 tmp;
> > +	int ret;
> > +	int i;
> > +
> > +	iio_aux = devm_kzalloc(&pdev->dev, sizeof(*iio_aux), GFP_KERNEL);
> > +	if (!iio_aux)
> > +		return -ENOMEM;
> > +
> > +	iio_aux->dev = &pdev->dev;
> > +
> > +	count = of_property_count_strings(np, "io-channel-names");
> > +	if (count < 0) {
> > +		dev_err(iio_aux->dev, "%pOF: failed to read io-channel-names\n", np);
> > +		return count;
> > +	}
> > +
> > +	iio_aux->chans = devm_kmalloc_array(&pdev->dev, count,
> > +					    sizeof(*iio_aux->chans), GFP_KERNEL);
> > +	if (!iio_aux->chans)
> > +		return -ENOMEM;
> > +	iio_aux->num_chans = count;
> > +
> > +	for (i = 0; i < iio_aux->num_chans; i++) {
> > +		iio_aux_chan = iio_aux->chans + i;
> > +
> > +		ret = of_property_read_string_index(np, "io-channel-names", i,
> > +						    &iio_aux_chan->name);  
> 
> Whilst today this will be tightly couple with of, if you can use generic firmware
> handling where possible (from linux/property.h) it will reduce what needs
> to be tidied up if anyone fills in the gaps for IIO consumer bindings in ACPI
> and then someone uses PRP0001 based ACPI bindings.

No device_property_read_*() function family are available to get a value
from an array using an index.

I would prefer to keep the of_property_read_*() function family I use for this
first IIO auxiliary device support.

> 
> > +		if (ret < 0) {
> > +			dev_err(iio_aux->dev, "%pOF: failed to read io-channel-names[%d]\n", np, i);  
> 
> dev_err_probe() would simplify these cases a little.  Not sure on ASOC view on using
> that for cases that won't defer.  I tend to take the view it's nicer everywhere
> for calls in probe() functions.

I have the feeling that ASoC uses dev_err_probe() for cases that can defer.
Mark, can you confirm ?

> 
> 
> > +			return ret;
> > +		}
> > +
> > +		iio_aux_chan->iio_chan = devm_iio_channel_get(iio_aux->dev, iio_aux_chan->name);
> > +		if (IS_ERR(iio_aux_chan->iio_chan)) {
> > +			ret = PTR_ERR(iio_aux_chan->iio_chan);  
> 
> Put that inline instead of setting ret here.

Will be done in the next iteration.

> 
> > +			return dev_err_probe(iio_aux->dev, ret,
> > +					     "get IIO channel '%s' failed (%d)\n",
> > +					     iio_aux_chan->name, ret);
> > +		}
> > +
> > +		tmp = 0;
> > +		of_property_read_u32_index(np, "invert", i, &tmp);
> > +		iio_aux_chan->is_inverted = !!tmp;  
> 
> As it's a bool this is the same as 
> 		iio_aux_chan->is_inverted = tmp;

I will remove the '!!' construction.


> 
> > +	}
> > +
> > +	platform_set_drvdata(pdev, iio_aux);
> > +
> > +	return devm_snd_soc_register_component(iio_aux->dev,
> > +					       &simple_iio_aux_component_driver,
> > +					       NULL, 0);
> > +}
> > +
> > +#ifdef CONFIG_OF
> > +static const struct of_device_id simple_iio_aux_ids[] = {
> > +	{ .compatible = "simple-iio-aux", },
> > +	{ }
> > +};
> > +MODULE_DEVICE_TABLE(of, simple_iio_aux_ids);
> > +#endif
> > +
> > +static struct platform_driver simple_iio_aux_driver = {
> > +	.driver = {
> > +		.name = "simple-iio-aux",
> > +		.of_match_table = of_match_ptr(simple_iio_aux_ids),  
> 
> I'd just drop the of_match_ptr()  Whilst this won't work today with other
> firmwares, we might enable the missing parts at some stage. Also the
> driver is somewhat pointless without DT so I'd just assume it's always
> built with it.  Cost is a tiny array on systems with a weird
> .config

of_match_ptr will be removed (and the #ifdef CONFIG_OF also).

> 
> > +	},
> > +	.probe = simple_iio_aux_probe,
> > +};
> > +
> > +module_platform_driver(simple_iio_aux_driver);
> > +
> > +MODULE_AUTHOR("Herve Codina <herve.codina@bootlin.com>");
> > +MODULE_DESCRIPTION("IIO ALSA SoC aux driver");
> > +MODULE_LICENSE("GPL");  
> 

Thanks for the review.

Best regards,
Hervé
diff mbox series

Patch

diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index 4f78da914fc7..ee87e0125bfd 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -211,6 +211,7 @@  config SND_SOC_ALL_CODECS
 	imply SND_SOC_SGTL5000
 	imply SND_SOC_SI476X
 	imply SND_SOC_SIMPLE_AMPLIFIER
+	imply SND_SOC_SIMPLE_IIO_AUX
 	imply SND_SOC_SIMPLE_MUX
 	imply SND_SOC_SMA1303
 	imply SND_SOC_SPDIF
@@ -1555,6 +1556,17 @@  config SND_SOC_SIGMADSP_REGMAP
 config SND_SOC_SIMPLE_AMPLIFIER
 	tristate "Simple Audio Amplifier"
 
+config SND_SOC_SIMPLE_IIO_AUX
+	tristate "Simple IIO Auxiliary device"
+	depends on IIO
+	help
+	  Enable support for Industrial I/O devices as audio auxiliary devices.
+	  This allows to have an IIO device present in the audio path and
+	  controlled using mixer controls.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called snd-soc-simple-iio-aux.
+
 config SND_SOC_SIMPLE_MUX
 	tristate "Simple Audio Mux"
 	depends on GPIOLIB
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile
index 11bd66d46f7b..f2b1ee22b57c 100644
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -359,6 +359,7 @@  snd-soc-zl38060-objs := zl38060.o
 snd-soc-max9877-objs := max9877.o
 snd-soc-max98504-objs := max98504.o
 snd-soc-simple-amplifier-objs := simple-amplifier.o
+snd-soc-simple-iio-aux-objs := simple-iio-aux.o
 snd-soc-tpa6130a2-objs := tpa6130a2.o
 snd-soc-tas2552-objs := tas2552.o
 snd-soc-tas2562-objs := tas2562.o
@@ -729,6 +730,7 @@  obj-$(CONFIG_SND_SOC_ZL38060)	+= snd-soc-zl38060.o
 obj-$(CONFIG_SND_SOC_MAX9877)	+= snd-soc-max9877.o
 obj-$(CONFIG_SND_SOC_MAX98504)	+= snd-soc-max98504.o
 obj-$(CONFIG_SND_SOC_SIMPLE_AMPLIFIER)	+= snd-soc-simple-amplifier.o
+obj-$(CONFIG_SND_SOC_SIMPLE_IIO_AUX)	+= snd-soc-simple-iio-aux.o
 obj-$(CONFIG_SND_SOC_TPA6130A2)	+= snd-soc-tpa6130a2.o
 obj-$(CONFIG_SND_SOC_LPASS_MACRO_COMMON)	+= snd-soc-lpass-macro-common.o
 obj-$(CONFIG_SND_SOC_LPASS_WSA_MACRO)	+= snd-soc-lpass-wsa-macro.o
diff --git a/sound/soc/codecs/simple-iio-aux.c b/sound/soc/codecs/simple-iio-aux.c
new file mode 100644
index 000000000000..77f7fb11e416
--- /dev/null
+++ b/sound/soc/codecs/simple-iio-aux.c
@@ -0,0 +1,307 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+//
+// simple-iio-aux.c  --  ALSA SoC glue to use IIO devices as audio components
+//
+// Copyright 2023 CS GROUP France
+//
+// Author: Herve Codina <herve.codina@bootlin.com>
+
+#include <linux/iio/consumer.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+struct simple_iio_aux_chan {
+	struct iio_channel *iio_chan;
+	const char *name;
+	bool is_inverted;
+	int max;
+	int min;
+};
+
+struct simple_iio_aux {
+	struct device *dev;
+	struct simple_iio_aux_chan *chans;
+	unsigned int num_chans;
+};
+
+static int simple_iio_aux_info_volsw(struct snd_kcontrol *kcontrol,
+				     struct snd_ctl_elem_info *uinfo)
+{
+	struct simple_iio_aux_chan *chan =
+		(struct simple_iio_aux_chan *)kcontrol->private_value;
+
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = chan->max - chan->min;
+	uinfo->type = (uinfo->value.integer.max == 1) ?
+			SNDRV_CTL_ELEM_TYPE_BOOLEAN : SNDRV_CTL_ELEM_TYPE_INTEGER;
+	return 0;
+}
+
+static int simple_iio_aux_get_volsw(struct snd_kcontrol *kcontrol,
+				    struct snd_ctl_elem_value *ucontrol)
+{
+	struct simple_iio_aux_chan *chan = (struct simple_iio_aux_chan *)kcontrol->private_value;
+	int max = chan->max;
+	int min = chan->min;
+	unsigned int mask = (1 << fls(max)) - 1;
+	unsigned int invert = chan->is_inverted;
+	int ret;
+	int val;
+
+	ret = iio_read_channel_raw(chan->iio_chan, &val);
+	if (ret < 0)
+		return ret;
+
+	ucontrol->value.integer.value[0] = (val & mask) - min;
+	if (invert)
+		ucontrol->value.integer.value[0] = max - ucontrol->value.integer.value[0];
+
+	return 0;
+}
+
+static int simple_iio_aux_put_volsw(struct snd_kcontrol *kcontrol,
+				    struct snd_ctl_elem_value *ucontrol)
+{
+	struct simple_iio_aux_chan *chan = (struct simple_iio_aux_chan *)kcontrol->private_value;
+	int max = chan->max;
+	int min = chan->min;
+	unsigned int mask = (1 << fls(max)) - 1;
+	unsigned int invert = chan->is_inverted;
+	int val;
+	int ret;
+	int tmp;
+
+	val = ucontrol->value.integer.value[0];
+	if (val < 0)
+		return -EINVAL;
+	if (val > max - min)
+		return -EINVAL;
+
+	val = (val + min) & mask;
+	if (invert)
+		val = max - val;
+
+	ret = iio_read_channel_raw(chan->iio_chan, &tmp);
+	if (ret < 0)
+		return ret;
+
+	if (tmp == val)
+		return 0;
+
+	ret = iio_write_channel_raw(chan->iio_chan, val);
+	if (ret)
+		return ret;
+
+	return 1; /* The value changed */
+}
+
+static int simple_iio_aux_add_controls(struct snd_soc_component *component,
+				       struct simple_iio_aux_chan *chan)
+{
+	struct snd_kcontrol_new control = {0};
+
+	control.iface = SNDRV_CTL_ELEM_IFACE_MIXER;
+	control.name = chan->name;
+	control.info = simple_iio_aux_info_volsw;
+	control.get = simple_iio_aux_get_volsw;
+	control.put = simple_iio_aux_put_volsw;
+	control.private_value = (unsigned long)chan;
+
+	return snd_soc_add_component_controls(component, &control, 1);
+}
+
+/*
+ * These data could be on stack but they are pretty big.
+ * As ASoC internally copy them and protect them against concurrent accesses
+ * (snd_soc_bind_card() protects using client_mutex), keep them in the global
+ * data area.
+ */
+static struct snd_soc_dapm_widget widgets[3] = {0};
+static struct snd_soc_dapm_route routes[2] = {0};
+
+static int simple_iio_aux_add_dapms(struct snd_soc_component *component,
+				    struct simple_iio_aux_chan *chan)
+{
+	struct snd_soc_dapm_context *dapm = snd_soc_component_get_dapm(component);
+	char *input_name = NULL;
+	char *output_name = NULL;
+	char *pga_name = NULL;
+	int ret;
+
+	input_name = kasprintf(GFP_KERNEL, "%s IN", chan->name);
+	if (!input_name) {
+		ret = -ENOMEM;
+		goto end;
+	}
+	output_name = kasprintf(GFP_KERNEL, "%s OUT", chan->name);
+	if (!output_name) {
+		ret = -ENOMEM;
+		goto end;
+	}
+	pga_name = kasprintf(GFP_KERNEL, "%s PGA", chan->name);
+	if (!pga_name) {
+		ret = -ENOMEM;
+		goto end;
+	}
+
+	BUILD_BUG_ON(ARRAY_SIZE(widgets) < 3);
+	widgets[0] = SND_SOC_DAPM_WIDGET(SND_SOC_DAPM_INPUT(input_name));
+	widgets[1] = SND_SOC_DAPM_WIDGET(SND_SOC_DAPM_OUTPUT(output_name));
+	widgets[2] = SND_SOC_DAPM_WIDGET(SND_SOC_DAPM_PGA(pga_name, SND_SOC_NOPM, 0, 0, NULL, 0));
+	ret = snd_soc_dapm_new_controls(dapm, widgets, 3);
+	if (ret)
+		goto end;
+
+	BUILD_BUG_ON(ARRAY_SIZE(routes) < 2);
+	routes[0].sink = pga_name;
+	routes[0].control = NULL;
+	routes[0].source = input_name;
+	routes[1].sink = output_name;
+	routes[1].control = NULL;
+	routes[1].source = pga_name;
+	ret = snd_soc_dapm_add_routes(dapm, routes, 2);
+
+end:
+	/* Allocated names are no more needed (duplicated in ASoC internals) */
+	kfree(pga_name);
+	kfree(output_name);
+	kfree(input_name);
+
+	return ret;
+}
+
+static int simple_iio_aux_component_probe(struct snd_soc_component *component)
+{
+	struct simple_iio_aux *iio_aux = snd_soc_component_get_drvdata(component);
+	struct simple_iio_aux_chan *chan;
+	int ret;
+	int i;
+
+	for (i = 0; i < iio_aux->num_chans; i++) {
+		chan = iio_aux->chans + i;
+
+		ret = iio_read_max_channel_raw(chan->iio_chan, &chan->max);
+		if (ret) {
+			dev_err(component->dev, "chan[%d] %s: Cannot get max raw value (%d)\n",
+				i, chan->name, ret);
+			return ret;
+		}
+
+		ret = iio_read_min_channel_raw(chan->iio_chan, &chan->min);
+		if (ret) {
+			dev_err(component->dev, "chan[%d] %s: Cannot get min raw value (%d)\n",
+				i, chan->name, ret);
+			return ret;
+		}
+
+		/* Set initial value */
+		ret = iio_write_channel_raw(chan->iio_chan,
+					    chan->is_inverted ? chan->max : chan->min);
+		if (ret) {
+			dev_err(component->dev, "chan[%d] %s: Cannot set initial value (%d)\n",
+				i, chan->name, ret);
+			return ret;
+		}
+
+		ret = simple_iio_aux_add_controls(component, chan);
+		if (ret)
+			return ret;
+
+		ret = simple_iio_aux_add_dapms(component, chan);
+		if (ret)
+			return ret;
+
+		dev_dbg(component->dev, "chan[%d]: Added %s (min=%d, max=%d, invert=%s)\n",
+			i, chan->name, chan->min, chan->max,
+			chan->is_inverted ? "on" : "off");
+	}
+
+	return 0;
+}
+
+static const struct snd_soc_component_driver simple_iio_aux_component_driver = {
+	.probe = simple_iio_aux_component_probe,
+};
+
+static int simple_iio_aux_probe(struct platform_device *pdev)
+{
+	struct device_node *np = pdev->dev.of_node;
+	struct simple_iio_aux_chan *iio_aux_chan;
+	struct simple_iio_aux *iio_aux;
+	int count;
+	u32 tmp;
+	int ret;
+	int i;
+
+	iio_aux = devm_kzalloc(&pdev->dev, sizeof(*iio_aux), GFP_KERNEL);
+	if (!iio_aux)
+		return -ENOMEM;
+
+	iio_aux->dev = &pdev->dev;
+
+	count = of_property_count_strings(np, "io-channel-names");
+	if (count < 0) {
+		dev_err(iio_aux->dev, "%pOF: failed to read io-channel-names\n", np);
+		return count;
+	}
+
+	iio_aux->chans = devm_kmalloc_array(&pdev->dev, count,
+					    sizeof(*iio_aux->chans), GFP_KERNEL);
+	if (!iio_aux->chans)
+		return -ENOMEM;
+	iio_aux->num_chans = count;
+
+	for (i = 0; i < iio_aux->num_chans; i++) {
+		iio_aux_chan = iio_aux->chans + i;
+
+		ret = of_property_read_string_index(np, "io-channel-names", i,
+						    &iio_aux_chan->name);
+		if (ret < 0) {
+			dev_err(iio_aux->dev, "%pOF: failed to read io-channel-names[%d]\n", np, i);
+			return ret;
+		}
+
+		iio_aux_chan->iio_chan = devm_iio_channel_get(iio_aux->dev, iio_aux_chan->name);
+		if (IS_ERR(iio_aux_chan->iio_chan)) {
+			ret = PTR_ERR(iio_aux_chan->iio_chan);
+			return dev_err_probe(iio_aux->dev, ret,
+					     "get IIO channel '%s' failed (%d)\n",
+					     iio_aux_chan->name, ret);
+		}
+
+		tmp = 0;
+		of_property_read_u32_index(np, "invert", i, &tmp);
+		iio_aux_chan->is_inverted = !!tmp;
+	}
+
+	platform_set_drvdata(pdev, iio_aux);
+
+	return devm_snd_soc_register_component(iio_aux->dev,
+					       &simple_iio_aux_component_driver,
+					       NULL, 0);
+}
+
+#ifdef CONFIG_OF
+static const struct of_device_id simple_iio_aux_ids[] = {
+	{ .compatible = "simple-iio-aux", },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, simple_iio_aux_ids);
+#endif
+
+static struct platform_driver simple_iio_aux_driver = {
+	.driver = {
+		.name = "simple-iio-aux",
+		.of_match_table = of_match_ptr(simple_iio_aux_ids),
+	},
+	.probe = simple_iio_aux_probe,
+};
+
+module_platform_driver(simple_iio_aux_driver);
+
+MODULE_AUTHOR("Herve Codina <herve.codina@bootlin.com>");
+MODULE_DESCRIPTION("IIO ALSA SoC aux driver");
+MODULE_LICENSE("GPL");