From: Herve Codina <herve.codina@bootlin.com>
To: Herve Codina <herve.codina@bootlin.com>,
Bartosz Golaszewski <brgl@kernel.org>,
Linus Walleij <linusw@kernel.org>,
Liam Girdwood <lgirdwood@gmail.com>,
Mark Brown <broonie@kernel.org>, Rob Herring <robh@kernel.org>,
Krzysztof Kozlowski <krzk+dt@kernel.org>,
Conor Dooley <conor+dt@kernel.org>,
Saravana Kannan <saravanak@kernel.org>,
Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.com>
Cc: linux-sound@vger.kernel.org, linux-gpio@vger.kernel.org,
devicetree@vger.kernel.org, linux-kernel@vger.kernel.org,
Christophe Leroy <christophe.leroy@csgroup.eu>,
Thomas Petazzoni <thomas.petazzoni@bootlin.com>
Subject: [PATCH v3 14/17] ASoC: simple-amplifier: gpio-audio-amp: Add support for gain-ranges
Date: Wed, 13 May 2026 10:16:58 +0200 [thread overview]
Message-ID: <20260513081702.317117-15-herve.codina@bootlin.com> (raw)
In-Reply-To: <20260513081702.317117-1-herve.codina@bootlin.com>
The mapping between physical gain values and gpio values can be
expressed using ranges described in the gain-ranges property.
This gain-ranges property is an array of ranges.
Each range in the array is defined by the first point and last point in
the range. Those points are a pair of values, the gpios value and the
related gain (dB) value.
With that, a given range defines N possible items (from the first point
gpios value to the last point gpios value) in order to set a gain from
the first point gain value to the last point gain value.
Handle this description and the related kcontrol.
Signed-off-by: Herve Codina <herve.codina@bootlin.com>
---
sound/soc/codecs/simple-amplifier.c | 317 +++++++++++++++++++++++++++-
1 file changed, 312 insertions(+), 5 deletions(-)
diff --git a/sound/soc/codecs/simple-amplifier.c b/sound/soc/codecs/simple-amplifier.c
index 5b172e520dcd..5759f9bc2f4f 100644
--- a/sound/soc/codecs/simple-amplifier.c
+++ b/sound/soc/codecs/simple-amplifier.c
@@ -7,11 +7,16 @@
#include <linux/bitmap.h>
#include <linux/bits.h>
#include <linux/gpio/consumer.h>
+#include <linux/math.h>
+#include <linux/minmax.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/regulator/consumer.h>
+#include <linux/slab.h>
#include <sound/soc.h>
+#include <linux/sort.h>
+#include <sound/tlv.h>
struct simple_amp_single {
struct gpio_desc *gpio;
@@ -20,11 +25,35 @@ struct simple_amp_single {
const char *control_name;
};
+struct simple_amp_point {
+ u32 gpio_val;
+ int gain_db;
+};
+
+struct simple_amp_range {
+ unsigned int nb_points;
+ struct simple_amp_point min;
+ struct simple_amp_point max;
+};
+
+struct simple_amp_ranges {
+ unsigned int nb_ranges;
+ struct simple_amp_range *tab_ranges;
+};
+
+enum simple_amp_mode {
+ SIMPLE_AMP_MODE_NONE,
+ SIMPLE_AMP_MODE_RANGES,
+};
+
struct simple_amp_multi {
struct gpio_descs *gpios;
u32 kctrl_val;
u32 kctrl_max;
const char *control_name;
+ unsigned int *tlv_array;
+ enum simple_amp_mode mode;
+ struct simple_amp_ranges ranges;
};
struct simple_amp_data {
@@ -195,6 +224,32 @@ static int simple_amp_single_add_kcontrol(struct snd_soc_component *component,
return snd_soc_add_component_controls(component, &control, 1);
}
+static u32 simple_amp_multi_ranges_kctrl_to_gpio(u32 kctrl_val,
+ struct simple_amp_ranges *ranges)
+{
+ struct simple_amp_range *range;
+ u32 index = kctrl_val;
+ unsigned int i;
+
+ for (i = 0; i < ranges->nb_ranges; i++) {
+ range = &ranges->tab_ranges[i];
+
+ if (index < range->nb_points)
+ return (range->max.gpio_val >= range->min.gpio_val) ?
+ range->min.gpio_val + index :
+ range->min.gpio_val - index;
+
+ index -= range->nb_points;
+ }
+
+ /*
+ * Given index out of possible ranges. This is shouldn't happen.
+ * Signal the issue and return the maximum value
+ */
+ WARN(1, "kctrl_val %u out of ranges\n", kctrl_val);
+ return ranges->tab_ranges[ranges->nb_ranges - 1].max.gpio_val;
+}
+
static int simple_amp_multi_kctrl_write_gpios(struct simple_amp_multi *multi,
u32 kctrl_val)
{
@@ -204,7 +259,12 @@ static int simple_amp_multi_kctrl_write_gpios(struct simple_amp_multi *multi,
if (kctrl_val > multi->kctrl_max)
return -EINVAL;
- gpio_val = kctrl_val;
+ if (multi->mode == SIMPLE_AMP_MODE_RANGES)
+ gpio_val = simple_amp_multi_ranges_kctrl_to_gpio(kctrl_val,
+ &multi->ranges);
+ else
+ gpio_val = kctrl_val;
+
bitmap_from_arr32(bm, &gpio_val, multi->gpios->ndescs);
return gpiod_multi_set_value_cansleep(multi->gpios, bm);
@@ -252,6 +312,38 @@ static int simple_amp_multi_kctrl_int_put(struct snd_kcontrol *kcontrol,
return 1; /* The value changed */
}
+static unsigned int *simple_amp_alloc_tlv_ranges(const struct simple_amp_ranges *ranges)
+{
+ unsigned int index;
+ unsigned int *tlv;
+ unsigned int *t;
+ unsigned int i;
+
+ tlv = kzalloc_objs(*tlv, 2 + ranges->nb_ranges * 6, GFP_KERNEL);
+ if (!tlv)
+ return NULL;
+
+ t = tlv;
+
+ /* Fill first TLV */
+ *t++ = SNDRV_CTL_TLVT_DB_RANGE; /* Tag */
+ *t++ = ranges->nb_ranges * 6 * sizeof(*tlv); /* Len */
+ /* Ranges are sorted from lower to higher value */
+ index = 0;
+ for (i = 0; i < ranges->nb_ranges; i++) {
+ /* Fill range item i */
+ *t++ = index; /* min */
+ index += ranges->tab_ranges[i].nb_points;
+ *t++ = index - 1; /* max */
+ *t++ = SNDRV_CTL_TLVT_DB_MINMAX; /* Tag */
+ *t++ = 2 * sizeof(*tlv); /* Len */
+ *t++ = ranges->tab_ranges[i].min.gain_db; /* min_dB */
+ *t++ = ranges->tab_ranges[i].max.gain_db; /* max_dB */
+ }
+
+ return tlv;
+}
+
static int simple_amp_multi_add_kcontrol(struct snd_soc_component *component,
struct simple_amp_multi *multi)
{
@@ -265,12 +357,39 @@ static int simple_amp_multi_add_kcontrol(struct snd_soc_component *component,
};
int ret;
+ switch (multi->mode) {
+ case SIMPLE_AMP_MODE_RANGES:
+ multi->tlv_array = simple_amp_alloc_tlv_ranges(&multi->ranges);
+ if (!multi->tlv_array)
+ return -ENOMEM;
+
+ control.access = SNDRV_CTL_ELEM_ACCESS_TLV_READ |
+ SNDRV_CTL_ELEM_ACCESS_READWRITE;
+ control.tlv.p = multi->tlv_array;
+ break;
+
+ case SIMPLE_AMP_MODE_NONE:
+ /* Already set control configuration is enough */
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
/* Be consistent between multi->kctrl_val value and the GPIOs value */
ret = simple_amp_multi_kctrl_write_gpios(multi, multi->kctrl_val);
if (ret)
- return ret;
+ goto err_free_tlv_array;
- return snd_soc_add_component_controls(component, &control, 1);
+ ret = snd_soc_add_component_controls(component, &control, 1);
+ if (ret)
+ goto err_free_tlv_array;
+
+ return 0;
+
+err_free_tlv_array:
+ kfree(multi->tlv_array);
+ return ret;
}
static int simple_amp_add_basic_dapm(struct snd_soc_component *component)
@@ -407,8 +526,17 @@ static int simple_amp_component_probe(struct snd_soc_component *component)
return 0;
}
+static void simple_amp_component_remove(struct snd_soc_component *component)
+{
+ struct simple_amp *simple_amp = snd_soc_component_get_drvdata(component);
+
+ kfree(simple_amp->gain.tlv_array);
+ simple_amp->gain.tlv_array = NULL;
+}
+
static const struct snd_soc_component_driver simple_amp_component_driver = {
.probe = simple_amp_component_probe,
+ .remove = simple_amp_component_remove,
};
static int simple_amp_parse_single_gpio(struct device *dev,
@@ -426,10 +554,179 @@ static int simple_amp_parse_single_gpio(struct device *dev,
return 0;
}
+static int simple_amp_cmp_ranges(const void *a, const void *b)
+{
+ const struct simple_amp_range *a_range = a;
+ const struct simple_amp_range *b_range = b;
+
+ /* Ranges a and b don't overlap. This has been already checked */
+
+ return a_range->min.gain_db - b_range->max.gain_db;
+}
+
+static int simple_amp_check_new_range(const struct simple_amp_range *new_range,
+ const struct simple_amp_range *tab_ranges,
+ unsigned int nb_ranges)
+{
+ unsigned int i;
+
+ for (i = 0; i < nb_ranges; i++) {
+ /* Check for range overlaps */
+ if (new_range->min.gain_db >= tab_ranges[i].min.gain_db &&
+ new_range->min.gain_db <= tab_ranges[i].max.gain_db)
+ return -EINVAL;
+
+ if (new_range->max.gain_db >= tab_ranges[i].min.gain_db &&
+ new_range->max.gain_db <= tab_ranges[i].max.gain_db)
+ return -EINVAL;
+
+ if (new_range->min.gain_db <= tab_ranges[i].min.gain_db &&
+ new_range->max.gain_db >= tab_ranges[i].max.gain_db)
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int simple_amp_parse_ranges(struct device *dev,
+ struct simple_amp_multi *multi,
+ const char *ranges_property)
+{
+ struct simple_amp_ranges *ranges = &multi->ranges;
+ struct simple_amp_range *range;
+ struct device_node *np = dev->of_node;
+ struct simple_amp_point first_point;
+ unsigned int max_gpio_val;
+ unsigned int i;
+ int ret;
+ u32 u;
+ s32 s;
+
+ max_gpio_val = (1 << multi->gpios->ndescs) - 1;
+
+ ret = of_property_count_u32_elems(np, ranges_property);
+ if (ret < 0)
+ return ret;
+
+ /* The ranges array cannot be empty */
+ if (ret == 0)
+ return -EINVAL;
+ /*
+ * One range item is composed of 2 points and each point is composed of
+ * 2 values.
+ */
+ if (ret % 4)
+ return -EINVAL;
+
+ ranges->nb_ranges = ret / 4;
+
+ /* The worst case is one range per possible gpio value */
+ if (ranges->nb_ranges > max_gpio_val + 1)
+ return -EINVAL;
+
+ ranges->tab_ranges = devm_kcalloc(dev, ranges->nb_ranges,
+ sizeof(*ranges->tab_ranges),
+ GFP_KERNEL);
+ if (!ranges->tab_ranges)
+ return -ENOMEM;
+
+ multi->kctrl_max = 0;
+ for (i = 0; i < ranges->nb_ranges; i++) {
+ range = &ranges->tab_ranges[i];
+
+ /* First gpios value */
+ ret = of_property_read_u32_index(np, ranges_property, i * 4, &u);
+ if (ret)
+ return ret;
+ if (u > max_gpio_val)
+ return -EINVAL;
+
+ range->min.gpio_val = u;
+
+ /* First Gain value */
+ ret = of_property_read_s32_index(np, ranges_property, i * 4 + 1, &s);
+ if (ret)
+ return ret;
+
+ range->min.gain_db = s;
+
+ /* Second gpios value */
+ ret = of_property_read_u32_index(np, ranges_property, i * 4 + 2, &u);
+ if (ret)
+ return ret;
+ if (u > max_gpio_val)
+ return -EINVAL;
+
+ range->max.gpio_val = u;
+
+ /* Second Gain value */
+ ret = of_property_read_s32_index(np, ranges_property, i * 4 + 3, &s);
+ if (ret)
+ return ret;
+
+ range->max.gain_db = s;
+
+ /* Save the first point for later usage */
+ if (i == 0)
+ first_point = range->min;
+
+ /* Fix min and max if needed */
+ if (range->min.gain_db > range->max.gain_db)
+ swap(range->min, range->max);
+
+ ret = simple_amp_check_new_range(range, ranges->tab_ranges, i);
+ if (ret)
+ return ret;
+
+ range->nb_points = abs_diff(range->min.gpio_val,
+ range->max.gpio_val) + 1;
+
+ multi->kctrl_max += range->nb_points;
+ }
+
+ multi->kctrl_max -= 1;
+
+ /* Sort the tab_range array by gain_db value */
+ sort(ranges->tab_ranges, ranges->nb_ranges, sizeof(*ranges->tab_ranges),
+ simple_amp_cmp_ranges, NULL);
+
+ /*
+ * multi->kctrl_val is the index in tab_ranges.
+ *
+ * Choose to have the initial amplification value set to the first point
+ * available in the first range available in the tab_ranges array before
+ * sorting.
+ *
+ * This first point has been identified before sorting. Search for it in
+ * the sorted array in order to set the multi->kctrl_val initial value.
+ */
+ multi->kctrl_val = 0;
+ for (i = 0; i < ranges->nb_ranges; i++) {
+ range = &ranges->tab_ranges[i];
+
+ if (range->min.gpio_val == first_point.gpio_val &&
+ range->min.gain_db == first_point.gain_db)
+ break;
+
+ multi->kctrl_val += range->nb_points;
+
+ if (range->max.gpio_val == first_point.gpio_val &&
+ range->max.gain_db == first_point.gain_db) {
+ multi->kctrl_val--;
+ break;
+ }
+ }
+
+ return 0;
+}
+
static int simple_amp_parse_multi_gpio(struct device *dev,
struct simple_amp_multi *multi,
- const char *gpios_property)
+ const char *gpios_property,
+ const char *ranges_property)
{
+ struct device_node *np = dev->of_node;
+ int ret;
+
/* Start with the value 0 (GPIO inactive). Can be changed later */
multi->kctrl_val = 0;
multi->gpios = devm_gpiod_get_array_optional(dev, gpios_property, GPIOD_OUT_LOW);
@@ -448,6 +745,15 @@ static int simple_amp_parse_multi_gpio(struct device *dev,
/* Set default value for the kctrl_max. Can be changed later */
multi->kctrl_max = (1 << multi->gpios->ndescs) - 1;
+ multi->mode = SIMPLE_AMP_MODE_NONE;
+ if (of_property_present(np, ranges_property)) {
+ ret = simple_amp_parse_ranges(dev, multi, ranges_property);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "Failed to parse '%s'\n",
+ ranges_property);
+ multi->mode = SIMPLE_AMP_MODE_RANGES;
+ }
+
return 0;
}
@@ -485,7 +791,8 @@ static int simple_amp_probe(struct platform_device *pdev)
}
if (simple_amp->data->supports & SIMPLE_AUDIO_SUPPORT_PGA) {
- ret = simple_amp_parse_multi_gpio(dev, &simple_amp->gain, "gain");
+ ret = simple_amp_parse_multi_gpio(dev, &simple_amp->gain, "gain",
+ "gain-ranges");
if (ret)
return ret;
}
--
2.54.0
next prev parent reply other threads:[~2026-05-13 8:17 UTC|newest]
Thread overview: 23+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-13 8:16 [PATCH v3 00/17] ASoC: Add support for GPIOs driven amplifiers Herve Codina
2026-05-13 8:16 ` [PATCH v3 01/17] of: Introduce of_property_read_s32_index() Herve Codina
2026-05-13 8:16 ` [PATCH v3 02/17] ASoC: dt-bindings: Add support for the GPIOs driven amplifier Herve Codina
2026-05-13 8:16 ` [PATCH v3 03/17] ASoC: simple-amplifier: Remove DRV_NAME defined value Herve Codina
2026-05-13 8:16 ` [PATCH v3 04/17] ASoC: simple-amplifier: Add missing headers Herve Codina
2026-05-13 8:16 ` [PATCH v3 05/17] ASoC: simple-amplifier: Remove CONFIG_OF flag and of_match_ptr() Herve Codina
2026-05-13 8:16 ` [PATCH v3 06/17] ASoC: simple-amplifier: Rename drv_event() function Herve Codina
2026-05-13 8:16 ` [PATCH v3 07/17] ASoC: simple-amplifier: Use 'simple_amp' variable name instead of 'priv' Herve Codina
2026-05-13 8:16 ` [PATCH v3 08/17] ASoC: simple-amplifier: Remove DAPM widgets and routes from the ASoC component driver Herve Codina
2026-05-14 3:32 ` sashiko-bot
2026-05-13 8:16 ` [PATCH v3 09/17] ASoC: simple-amplifier: Introduce support for gpio-audio-amp Herve Codina
2026-05-14 3:51 ` sashiko-bot
2026-05-13 8:16 ` [PATCH v3 10/17] ASoC: simple-amplifier: gpio-audio-amp: Add support for extra power supplies Herve Codina
2026-05-13 8:16 ` [PATCH v3 11/17] ASoC: simple-amplifier: gpio-audio-amp: Add support for mute gpio Herve Codina
2026-05-13 8:16 ` [PATCH v3 12/17] ASoC: simple-amplifier: gpio-audio-amp: Add support for bypass gpio Herve Codina
2026-05-14 4:54 ` sashiko-bot
2026-05-13 8:16 ` [PATCH v3 13/17] ASoC: simple-amplifier: gpio-audio-amp: Add support for basic gain Herve Codina
2026-05-13 8:16 ` Herve Codina [this message]
2026-05-14 6:10 ` [PATCH v3 14/17] ASoC: simple-amplifier: gpio-audio-amp: Add support for gain-ranges sashiko-bot
2026-05-13 8:16 ` [PATCH v3 15/17] ASoC: simple-amplifier: gpio-audio-amp: Add support for gain-labels Herve Codina
2026-05-13 8:17 ` [PATCH v3 16/17] ASoC: simple-amplifier: Update author and copyright Herve Codina
2026-05-14 6:49 ` sashiko-bot
2026-05-13 8:17 ` [PATCH v3 17/17] MAINTAINERS: Add the ASoC gpio audio amplifier entry Herve Codina
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260513081702.317117-15-herve.codina@bootlin.com \
--to=herve.codina@bootlin.com \
--cc=brgl@kernel.org \
--cc=broonie@kernel.org \
--cc=christophe.leroy@csgroup.eu \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=krzk+dt@kernel.org \
--cc=lgirdwood@gmail.com \
--cc=linusw@kernel.org \
--cc=linux-gpio@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-sound@vger.kernel.org \
--cc=perex@perex.cz \
--cc=robh@kernel.org \
--cc=saravanak@kernel.org \
--cc=thomas.petazzoni@bootlin.com \
--cc=tiwai@suse.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.