315 lines · c
1// SPDX-License-Identifier: GPL-2.0-only2//3// ALSA SoC glue to use IIO devices as audio components4//5// Copyright 2023 CS GROUP France6//7// Author: Herve Codina <herve.codina@bootlin.com>8 9#include <linux/cleanup.h>10#include <linux/iio/consumer.h>11#include <linux/minmax.h>12#include <linux/mod_devicetable.h>13#include <linux/platform_device.h>14#include <linux/slab.h>15#include <linux/string_helpers.h>16 17#include <sound/soc.h>18#include <sound/tlv.h>19 20struct audio_iio_aux_chan {21 struct iio_channel *iio_chan;22 const char *name;23 int max;24 int min;25 bool is_invert_range;26};27 28struct audio_iio_aux {29 struct device *dev;30 unsigned int num_chans;31 struct audio_iio_aux_chan chans[] __counted_by(num_chans);32};33 34static int audio_iio_aux_info_volsw(struct snd_kcontrol *kcontrol,35 struct snd_ctl_elem_info *uinfo)36{37 struct audio_iio_aux_chan *chan = (struct audio_iio_aux_chan *)kcontrol->private_value;38 39 uinfo->count = 1;40 uinfo->value.integer.min = 0;41 uinfo->value.integer.max = chan->max - chan->min;42 uinfo->type = (uinfo->value.integer.max == 1) ?43 SNDRV_CTL_ELEM_TYPE_BOOLEAN : SNDRV_CTL_ELEM_TYPE_INTEGER;44 return 0;45}46 47static int audio_iio_aux_get_volsw(struct snd_kcontrol *kcontrol,48 struct snd_ctl_elem_value *ucontrol)49{50 struct audio_iio_aux_chan *chan = (struct audio_iio_aux_chan *)kcontrol->private_value;51 int max = chan->max;52 int min = chan->min;53 bool invert_range = chan->is_invert_range;54 int ret;55 int val;56 57 ret = iio_read_channel_raw(chan->iio_chan, &val);58 if (ret < 0)59 return ret;60 61 ucontrol->value.integer.value[0] = val - min;62 if (invert_range)63 ucontrol->value.integer.value[0] = max - ucontrol->value.integer.value[0];64 65 return 0;66}67 68static int audio_iio_aux_put_volsw(struct snd_kcontrol *kcontrol,69 struct snd_ctl_elem_value *ucontrol)70{71 struct audio_iio_aux_chan *chan = (struct audio_iio_aux_chan *)kcontrol->private_value;72 int max = chan->max;73 int min = chan->min;74 bool invert_range = chan->is_invert_range;75 int val;76 int ret;77 int tmp;78 79 val = ucontrol->value.integer.value[0];80 if (val < 0)81 return -EINVAL;82 if (val > max - min)83 return -EINVAL;84 85 val = val + min;86 if (invert_range)87 val = max - val;88 89 ret = iio_read_channel_raw(chan->iio_chan, &tmp);90 if (ret < 0)91 return ret;92 93 if (tmp == val)94 return 0;95 96 ret = iio_write_channel_raw(chan->iio_chan, val);97 if (ret)98 return ret;99 100 return 1; /* The value changed */101}102 103static int audio_iio_aux_add_controls(struct snd_soc_component *component,104 struct audio_iio_aux_chan *chan)105{106 struct snd_kcontrol_new control = {107 .iface = SNDRV_CTL_ELEM_IFACE_MIXER,108 .name = chan->name,109 .info = audio_iio_aux_info_volsw,110 .get = audio_iio_aux_get_volsw,111 .put = audio_iio_aux_put_volsw,112 .private_value = (unsigned long)chan,113 };114 115 return snd_soc_add_component_controls(component, &control, 1);116}117 118/*119 * These data could be on stack but they are pretty big.120 * As ASoC internally copy them and protect them against concurrent accesses121 * (snd_soc_bind_card() protects using client_mutex), keep them in the global122 * data area.123 */124static struct snd_soc_dapm_widget widgets[3];125static struct snd_soc_dapm_route routes[2];126 127/* Be sure sizes are correct (need 3 widgets and 2 routes) */128static_assert(ARRAY_SIZE(widgets) >= 3, "3 widgets are needed");129static_assert(ARRAY_SIZE(routes) >= 2, "2 routes are needed");130 131static int audio_iio_aux_add_dapms(struct snd_soc_component *component,132 struct audio_iio_aux_chan *chan)133{134 struct snd_soc_dapm_context *dapm = snd_soc_component_get_dapm(component);135 int ret;136 137 /* Allocated names are not needed afterwards (duplicated in ASoC internals) */138 char *input_name __free(kfree) = kasprintf(GFP_KERNEL, "%s IN", chan->name);139 if (!input_name)140 return -ENOMEM;141 142 char *output_name __free(kfree) = kasprintf(GFP_KERNEL, "%s OUT", chan->name);143 if (!output_name)144 return -ENOMEM;145 146 char *pga_name __free(kfree) = kasprintf(GFP_KERNEL, "%s PGA", chan->name);147 if (!pga_name)148 return -ENOMEM;149 150 widgets[0] = SND_SOC_DAPM_INPUT(input_name);151 widgets[1] = SND_SOC_DAPM_OUTPUT(output_name);152 widgets[2] = SND_SOC_DAPM_PGA(pga_name, SND_SOC_NOPM, 0, 0, NULL, 0);153 ret = snd_soc_dapm_new_controls(dapm, widgets, 3);154 if (ret)155 return ret;156 157 routes[0].sink = pga_name;158 routes[0].control = NULL;159 routes[0].source = input_name;160 routes[1].sink = output_name;161 routes[1].control = NULL;162 routes[1].source = pga_name;163 164 return snd_soc_dapm_add_routes(dapm, routes, 2);165}166 167static int audio_iio_aux_component_probe(struct snd_soc_component *component)168{169 struct audio_iio_aux *iio_aux = snd_soc_component_get_drvdata(component);170 struct audio_iio_aux_chan *chan;171 int ret;172 int i;173 174 for (i = 0; i < iio_aux->num_chans; i++) {175 chan = iio_aux->chans + i;176 177 ret = iio_read_max_channel_raw(chan->iio_chan, &chan->max);178 if (ret)179 return dev_err_probe(component->dev, ret,180 "chan[%d] %s: Cannot get max raw value\n",181 i, chan->name);182 183 ret = iio_read_min_channel_raw(chan->iio_chan, &chan->min);184 if (ret)185 return dev_err_probe(component->dev, ret,186 "chan[%d] %s: Cannot get min raw value\n",187 i, chan->name);188 189 if (chan->min > chan->max) {190 /*191 * This should never happen but to avoid any check192 * later, just swap values here to ensure that the193 * minimum value is lower than the maximum value.194 */195 dev_dbg(component->dev, "chan[%d] %s: Swap min and max\n",196 i, chan->name);197 swap(chan->min, chan->max);198 }199 200 /* Set initial value */201 ret = iio_write_channel_raw(chan->iio_chan,202 chan->is_invert_range ? chan->max : chan->min);203 if (ret)204 return dev_err_probe(component->dev, ret,205 "chan[%d] %s: Cannot set initial value\n",206 i, chan->name);207 208 ret = audio_iio_aux_add_controls(component, chan);209 if (ret)210 return ret;211 212 ret = audio_iio_aux_add_dapms(component, chan);213 if (ret)214 return ret;215 216 dev_dbg(component->dev, "chan[%d]: Added %s (min=%d, max=%d, invert=%s)\n",217 i, chan->name, chan->min, chan->max,218 str_on_off(chan->is_invert_range));219 }220 221 return 0;222}223 224static const struct snd_soc_component_driver audio_iio_aux_component_driver = {225 .probe = audio_iio_aux_component_probe,226};227 228static int audio_iio_aux_probe(struct platform_device *pdev)229{230 struct audio_iio_aux_chan *iio_aux_chan;231 struct device *dev = &pdev->dev;232 struct audio_iio_aux *iio_aux;233 int count;234 int ret;235 int i;236 237 count = device_property_string_array_count(dev, "io-channel-names");238 if (count < 0)239 return dev_err_probe(dev, count, "failed to count io-channel-names\n");240 241 iio_aux = devm_kzalloc(dev, struct_size(iio_aux, chans, count), GFP_KERNEL);242 if (!iio_aux)243 return -ENOMEM;244 245 iio_aux->dev = dev;246 247 iio_aux->num_chans = count;248 249 const char **names __free(kfree) = kcalloc(iio_aux->num_chans,250 sizeof(*names),251 GFP_KERNEL);252 if (!names)253 return -ENOMEM;254 255 u32 *invert_ranges __free(kfree) = kcalloc(iio_aux->num_chans,256 sizeof(*invert_ranges),257 GFP_KERNEL);258 if (!invert_ranges)259 return -ENOMEM;260 261 ret = device_property_read_string_array(dev, "io-channel-names",262 names, iio_aux->num_chans);263 if (ret < 0)264 return dev_err_probe(dev, ret, "failed to read io-channel-names\n");265 266 /*267 * snd-control-invert-range is optional and can contain fewer items268 * than the number of channels. Unset values default to 0.269 */270 count = device_property_count_u32(dev, "snd-control-invert-range");271 if (count > 0) {272 count = min_t(unsigned int, count, iio_aux->num_chans);273 ret = device_property_read_u32_array(dev, "snd-control-invert-range",274 invert_ranges, count);275 if (ret < 0)276 return dev_err_probe(dev, ret, "failed to read snd-control-invert-range\n");277 }278 279 for (i = 0; i < iio_aux->num_chans; i++) {280 iio_aux_chan = iio_aux->chans + i;281 iio_aux_chan->name = names[i];282 iio_aux_chan->is_invert_range = invert_ranges[i];283 284 iio_aux_chan->iio_chan = devm_iio_channel_get(dev, iio_aux_chan->name);285 if (IS_ERR(iio_aux_chan->iio_chan))286 return dev_err_probe(dev, PTR_ERR(iio_aux_chan->iio_chan),287 "get IIO channel '%s' failed\n",288 iio_aux_chan->name);289 }290 291 platform_set_drvdata(pdev, iio_aux);292 293 return devm_snd_soc_register_component(dev, &audio_iio_aux_component_driver,294 NULL, 0);295}296 297static const struct of_device_id audio_iio_aux_ids[] = {298 { .compatible = "audio-iio-aux" },299 { }300};301MODULE_DEVICE_TABLE(of, audio_iio_aux_ids);302 303static struct platform_driver audio_iio_aux_driver = {304 .driver = {305 .name = "audio-iio-aux",306 .of_match_table = audio_iio_aux_ids,307 },308 .probe = audio_iio_aux_probe,309};310module_platform_driver(audio_iio_aux_driver);311 312MODULE_AUTHOR("Herve Codina <herve.codina@bootlin.com>");313MODULE_DESCRIPTION("IIO ALSA SoC aux driver");314MODULE_LICENSE("GPL");315