diff options
| author | Mark Brown <broonie@kernel.org> | 2026-07-27 14:39:26 +0100 |
|---|---|---|
| committer | Mark Brown <broonie@kernel.org> | 2026-07-27 14:39:26 +0100 |
| commit | 28f67059bb1abc655eb74615f91c59054a52749f (patch) | |
| tree | 8d0b37bbdb0ce4e65e8beb848c3cc311e2fa847b | |
| parent | fad15cabb760ae0fd552f9bc98b0b1307970c57e (diff) | |
| parent | e82159384a5052212e7d2f2aa1de39827f1bed3c (diff) | |
| download | linux-next-28f67059bb1abc655eb74615f91c59054a52749f.tar.gz linux-next-28f67059bb1abc655eb74615f91c59054a52749f.zip | |
ASoC: meson: aiu-encoder-i2s: improve hw constraints checks
Valerio Setti <vsetti@baylibre.com> says:
This is a follow-up to the recently merged series reshaping the AIU
driver following the same design as AXG [1]. During the review,
Jerome pointed out that failing in hw_params() when two streams have
incompatible requirements is too late from userspace's point of view [2].
This series addresses that comment:
- Patch 1 fixes the existing bclk quirk check first: it only rejected
one direction of the mismatch, and its interface-wide flag was
cleared too late, making a legal reconfiguration of a single stream
fail. This is a standalone fix and it's preparing for patch 2.
- Patch 2 is the actual answer to the review comment: the quirk
incompatibility is expressed as hw rules on channels and sample
bits, refined against the committed configuration of the opposite
stream, so it becomes visible during parameter refinement. The
hw_params() check is kept as the last backstop in case of concurrent
refinement.
- Patch 3 applies the same philosophy to the interface-wide rate
symmetry. Instead of manually reimplementing the check, take advantage of
the core's 'symmetric_rate' handling.
Jerome's comment also mentioned the error returned when 'bs' is not a
multiple of 2. That one intentionally remains a hw_params() time
error: bs depends on the mclk rate, which the DAI only learns via
set_sysclk() from the machine driver's hw_params(), after parameter
refinement has already run. I don't think that can easily be expressed as
a constraint.
Final note. [1] has not been mainlined yet and it's only present in
'broonie/sound.git#for-7.3'. Therefore this series depends on commit
c7852d2dcf66 ("ASoC: meson: aiu: align I2S design to the AXG one").
[1] https://lore.kernel.org/linux-sound/20260610-reshape-aiu-as-axg-v2-0-cac3663a8b51@baylibre.com/
[2] https://lore.kernel.org/linux-sound/1jik7pebk7.fsf@starbuckisacylon.baylibre.com/
Link: https://patch.msgid.link/20260710-aiu-improve-quirk-check-v1-0-2fdd1b6f8896@baylibre.com
| -rw-r--r-- | sound/soc/meson/aiu-encoder-i2s.c | 154 | ||||
| -rw-r--r-- | sound/soc/meson/aiu.c | 1 | ||||
| -rw-r--r-- | sound/soc/meson/gx-interface.h | 6 |
3 files changed, 113 insertions, 48 deletions
diff --git a/sound/soc/meson/aiu-encoder-i2s.c b/sound/soc/meson/aiu-encoder-i2s.c index 83b579e98f1c..58dce9f08c9d 100644 --- a/sound/soc/meson/aiu-encoder-i2s.c +++ b/sound/soc/meson/aiu-encoder-i2s.c @@ -62,13 +62,36 @@ static int aiu_encoder_i2s_set_legacy_div(struct snd_soc_component *component, return 0; } +/* + * Return true if the given combination of channels and sample width requires + * the bs quirk. Return false otherwise. + */ +static bool aiu_encoder_is_bs_quirk(unsigned int channels, int width) +{ + return (channels == 8) && (width == 16); +} + +static int aiu_encoder_check_bs_quirk(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct gx_stream *other_stream = snd_soc_dai_dma_data_get(dai, !substream->stream); + + /* Nothing to do if the other stream doesn't exist or it's not configured yet. */ + if (!other_stream || !other_stream->channels) + return 0; + + if (aiu_encoder_is_bs_quirk(other_stream->channels, other_stream->width) != + aiu_encoder_is_bs_quirk(params_channels(params), params_width(params))) + return -EINVAL; + + return 0; +} + static int aiu_encoder_i2s_set_more_div(struct snd_soc_component *component, struct snd_pcm_hw_params *params, unsigned int bs) { - struct aiu *aiu = snd_soc_component_get_drvdata(component); - struct gx_iface *iface = &aiu->i2s.iface; - /* * NOTE: this HW is odd. * In most configuration, the i2s divider is 'mclk / blck'. @@ -76,25 +99,13 @@ static int aiu_encoder_i2s_set_more_div(struct snd_soc_component *component, * increased by 50% to get the correct output rate. * No idea why ! */ - if (params_width(params) == 16 && params_channels(params) == 8) { + if (aiu_encoder_is_bs_quirk(params_channels(params), params_width(params))) { if (bs % 2) { dev_err(component->dev, "Cannot increase i2s divider by 50%%\n"); return -EINVAL; } bs += bs / 2; - iface->bs_quirk = true; - } else { - /* - * If the bs quirk is currently applied for one stream and another - * ones tries to setup a configuration for which the quirk is - * not required, then fail. - */ - if (iface->bs_quirk) { - dev_err(component->dev, - "bclk requirements are incompatible with active stream\n"); - return -EINVAL; - } } /* Use CLK_MORE for mclk to bclk divider */ @@ -110,9 +121,11 @@ static int aiu_encoder_i2s_set_more_div(struct snd_soc_component *component, return 0; } -static int aiu_encoder_i2s_set_clocks(struct snd_soc_component *component, - struct snd_pcm_hw_params *params) +static int aiu_encoder_i2s_set_clocks(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) { + struct snd_soc_component *component = dai->component; struct aiu *aiu = snd_soc_component_get_drvdata(component); struct gx_iface *iface = &aiu->i2s.iface; unsigned int srate = params_rate(params); @@ -133,10 +146,22 @@ static int aiu_encoder_i2s_set_clocks(struct snd_soc_component *component, bs = fs / 64; - if (aiu->platform->has_clk_ctrl_more_i2s_div) + if (aiu->platform->has_clk_ctrl_more_i2s_div) { + /* + * The hw rules added in startup() make this unreachable in the + * sequential case, but both streams may be refined concurrently + * before either commits its config, since only ops->hw_params + * runs under the card's pcm_mutex. Re-check against the committed + * state of the other stream, which is stable under that mutex. + */ + if (aiu_encoder_check_bs_quirk(substream, params, dai)) { + dev_err(dai->dev, "bclk requirements incompatible with other stream\n"); + return -EINVAL; + } ret = aiu_encoder_i2s_set_more_div(component, params, bs); - else + } else { ret = aiu_encoder_i2s_set_legacy_div(component, params, bs); + } if (ret) return ret; @@ -154,29 +179,14 @@ static int aiu_encoder_i2s_hw_params(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) { struct gx_stream *ts = snd_soc_dai_get_dma_data(dai, substream); - struct gx_iface *iface = ts->iface; - struct snd_soc_component *component = dai->component; int ret; - /* - * Enforce interface wide rate symmetry only if there is more than - * 1 stream active. - */ - if (snd_soc_dai_active(dai) > 1) { - if (iface->rate && iface->rate != params_rate(params)) { - dev_err(dai->dev, "can't set iface rate (%d != %d)\n", - iface->rate, params_rate(params)); - return -EINVAL; - } - } - - ret = aiu_encoder_i2s_set_clocks(component, params); + ret = aiu_encoder_i2s_set_clocks(substream, params, dai); if (ret) { dev_err(dai->dev, "setting i2s clocks failed: %d\n", ret); return ret; } - iface->rate = params_rate(params); ts->physical_width = params_physical_width(params); ts->width = params_width(params); ts->channels = params_channels(params); @@ -209,24 +219,24 @@ static int aiu_encoder_i2s_hw_free(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) { struct gx_stream *ts = snd_soc_dai_get_dma_data(dai, substream); - struct gx_iface *iface = ts->iface; struct snd_soc_component *component = dai->component; /* * If this is the last substream being closed then disable the i2s - * clock divider and clear 'iface->rate'. + * clock divider. */ - if (snd_soc_dai_active(dai) <= 1) { + if (snd_soc_dai_active(dai) <= 1) aiu_encoder_i2s_divider_enable(component, 0); - iface->rate = 0; - iface->bs_quirk = false; - } if (ts->clk_enabled) { clk_disable_unprepare(ts->iface->mclk); ts->clk_enabled = false; } + ts->channels = 0; + ts->width = 0; + ts->physical_width = 0; + return 0; } @@ -313,10 +323,45 @@ static const struct snd_pcm_hw_constraint_list hw_channel_constraints = { .mask = 0, }; +static int aiu_encoder_i2s_pcm_hw_rule(struct snd_pcm_hw_params *params, + struct snd_pcm_hw_rule *rule) +{ + struct gx_stream *other = rule->private; + struct snd_interval *ch = hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS); + /* + * The quirk is technically based on the significant bits whereas here + * we're using the physical width for simplicity. This works because + * S16_LE is the only format supported by this encoder that has: + * significant bits = physical width = 16-bits + */ + struct snd_interval *phys_width = hw_param_interval(params, SNDRV_PCM_HW_PARAM_SAMPLE_BITS); + struct snd_interval new_i; + + if (other->channels == 0) + return 0; + + snd_interval_any(&new_i); + + if (rule->var == SNDRV_PCM_HW_PARAM_CHANNELS) { + if (aiu_encoder_is_bs_quirk(other->channels, other->width)) + new_i.min = new_i.max = 8; + else if (snd_interval_single(phys_width) && phys_width->min == 16) + new_i.max = 2; /* Force 2ch */ + } else { /* SNDRV_PCM_HW_PARAM_SAMPLE_BITS */ + if (aiu_encoder_is_bs_quirk(other->channels, other->width)) + new_i.min = new_i.max = 16; + else if (snd_interval_single(ch) && ch->min == 8) + new_i.min = 17; /* Request physical width > 16 bits */ + } + + return snd_interval_refine(hw_param_interval(params, rule->var), &new_i); +} + static int aiu_encoder_i2s_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) { struct aiu *aiu = snd_soc_component_get_drvdata(dai->component); + struct gx_stream *other_stream = snd_soc_dai_dma_data_get(dai, !substream->stream); int ret; /* Make sure the encoder gets either 2 or 8 channels */ @@ -329,6 +374,31 @@ static int aiu_encoder_i2s_startup(struct snd_pcm_substream *substream, } /* + * If DAI supports both playback and capture streams ensure the bs-quirk is + * handled correctly. + * This is only valid for GX platforms (has_clk_ctrl_more_i2s_div=true). + */ + if (aiu->platform->has_clk_ctrl_more_i2s_div && other_stream) { + ret = snd_pcm_hw_rule_add(substream->runtime, 0, + SNDRV_PCM_HW_PARAM_CHANNELS, + aiu_encoder_i2s_pcm_hw_rule, + other_stream, + SNDRV_PCM_HW_PARAM_CHANNELS, + SNDRV_PCM_HW_PARAM_SAMPLE_BITS, -1); + if (ret) + return ret; + + ret = snd_pcm_hw_rule_add(substream->runtime, 0, + SNDRV_PCM_HW_PARAM_SAMPLE_BITS, + aiu_encoder_i2s_pcm_hw_rule, + other_stream, + SNDRV_PCM_HW_PARAM_CHANNELS, + SNDRV_PCM_HW_PARAM_SAMPLE_BITS, -1); + if (ret) + return ret; + } + + /* * Enable only clocks which are required for the interface internal * logic. MCLK is enabled/disabled from the formatter and the I2S * divider is enabled/disabled in "hw_params"/"hw_free", respectively. diff --git a/sound/soc/meson/aiu.c b/sound/soc/meson/aiu.c index 64ace4d25d92..2668646e3597 100644 --- a/sound/soc/meson/aiu.c +++ b/sound/soc/meson/aiu.c @@ -154,6 +154,7 @@ static struct snd_soc_dai_driver aiu_cpu_dai_drv[] = { .formats = AIU_FORMATS, }, .ops = &aiu_encoder_i2s_dai_ops, + .symmetric_rate = 1, }, [CPU_SPDIF_ENCODER] = { .name = "SPDIF Encoder", diff --git a/sound/soc/meson/gx-interface.h b/sound/soc/meson/gx-interface.h index 65c46dcce32a..2a6207e393e8 100644 --- a/sound/soc/meson/gx-interface.h +++ b/sound/soc/meson/gx-interface.h @@ -19,12 +19,6 @@ struct gx_iface { /* format is common to all the DAIs of the iface */ unsigned int fmt; - - /* For component wide symmetry */ - int rate; - - /* Only for GX platform */ - int bs_quirk; }; struct gx_stream { |
