diff options
Diffstat (limited to 'sound/soc/fsl/fsl_qmc_audio.c')
| -rw-r--r-- | sound/soc/fsl/fsl_qmc_audio.c | 974 |
1 files changed, 974 insertions, 0 deletions
diff --git a/sound/soc/fsl/fsl_qmc_audio.c b/sound/soc/fsl/fsl_qmc_audio.c new file mode 100644 index 000000000000..3de448ef724c --- /dev/null +++ b/sound/soc/fsl/fsl_qmc_audio.c @@ -0,0 +1,974 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ALSA SoC using the QUICC Multichannel Controller (QMC) + * + * Copyright 2022 CS GROUP France + * + * Author: Herve Codina <herve.codina@bootlin.com> + */ + +#include <linux/dma-mapping.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <soc/fsl/qe/qmc.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> + +struct qmc_dai { + char *name; + int id; + struct device *dev; + unsigned int nb_tx_ts; + unsigned int nb_rx_ts; + + unsigned int nb_chans_avail; + unsigned int nb_chans_used_tx; + unsigned int nb_chans_used_rx; + struct qmc_chan **qmc_chans; +}; + +struct qmc_audio { + struct device *dev; + unsigned int num_dais; + struct qmc_dai *dais; + struct snd_soc_dai_driver *dai_drivers; +}; + +struct qmc_dai_prtd { + struct qmc_dai *qmc_dai; + + snd_pcm_uframes_t buffer_ended; + snd_pcm_uframes_t buffer_size; + snd_pcm_uframes_t period_size; + + dma_addr_t ch_dma_addr_start; + dma_addr_t ch_dma_addr_current; + dma_addr_t ch_dma_addr_end; + size_t ch_dma_size; + size_t ch_dma_offset; + + unsigned int channels; + struct snd_pcm_substream *substream; +}; + +static int qmc_audio_pcm_construct(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd) +{ + struct snd_card *card = rtd->card->snd_card; + int ret; + + ret = dma_coerce_mask_and_coherent(card->dev, DMA_BIT_MASK(32)); + if (ret) + return ret; + + snd_pcm_set_managed_buffer_all(rtd->pcm, SNDRV_DMA_TYPE_DEV, card->dev, + 64 * 1024, 64 * 1024); + return 0; +} + +static bool qmc_audio_access_is_interleaved(snd_pcm_access_t access) +{ + switch (access) { + case SNDRV_PCM_ACCESS_MMAP_INTERLEAVED: + case SNDRV_PCM_ACCESS_RW_INTERLEAVED: + return true; + default: + break; + } + return false; +} + +static int qmc_audio_pcm_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct qmc_dai_prtd *prtd = substream->runtime->private_data; + + /* + * In interleaved mode, the driver uses one QMC channel for all audio + * channels whereas in non-interleaved mode, it uses one QMC channel per + * audio channel. + */ + prtd->channels = qmc_audio_access_is_interleaved(params_access(params)) ? + 1 : params_channels(params); + + prtd->substream = substream; + + prtd->buffer_ended = 0; + prtd->buffer_size = params_buffer_size(params); + prtd->period_size = params_period_size(params); + + prtd->ch_dma_addr_start = runtime->dma_addr; + prtd->ch_dma_offset = params_buffer_bytes(params) / prtd->channels; + prtd->ch_dma_addr_end = runtime->dma_addr + prtd->ch_dma_offset; + prtd->ch_dma_addr_current = prtd->ch_dma_addr_start; + prtd->ch_dma_size = params_period_bytes(params) / prtd->channels; + + return 0; +} + +static void qmc_audio_pcm_write_complete(void *context); + +static int qmc_audio_pcm_write_submit(struct qmc_dai_prtd *prtd) +{ + unsigned int i; + int ret; + + for (i = 0; i < prtd->channels; i++) { + ret = qmc_chan_write_submit(prtd->qmc_dai->qmc_chans[i], + prtd->ch_dma_addr_current + i * prtd->ch_dma_offset, + prtd->ch_dma_size, + i == prtd->channels - 1 ? qmc_audio_pcm_write_complete : + NULL, prtd); + if (ret) { + dev_err(prtd->qmc_dai->dev, "write_submit %u failed %d\n", + i, ret); + return ret; + } + } + + return 0; +} + +static void qmc_audio_pcm_write_complete(void *context) +{ + struct qmc_dai_prtd *prtd = context; + + prtd->buffer_ended += prtd->period_size; + if (prtd->buffer_ended >= prtd->buffer_size) + prtd->buffer_ended = 0; + + prtd->ch_dma_addr_current += prtd->ch_dma_size; + if (prtd->ch_dma_addr_current >= prtd->ch_dma_addr_end) + prtd->ch_dma_addr_current = prtd->ch_dma_addr_start; + + qmc_audio_pcm_write_submit(prtd); + + snd_pcm_period_elapsed(prtd->substream); +} + +static void qmc_audio_pcm_read_complete(void *context, size_t length, unsigned int flags); + +static int qmc_audio_pcm_read_submit(struct qmc_dai_prtd *prtd) +{ + unsigned int i; + int ret; + + for (i = 0; i < prtd->channels; i++) { + ret = qmc_chan_read_submit(prtd->qmc_dai->qmc_chans[i], + prtd->ch_dma_addr_current + i * prtd->ch_dma_offset, + prtd->ch_dma_size, + i == prtd->channels - 1 ? qmc_audio_pcm_read_complete : + NULL, prtd); + if (ret) { + dev_err(prtd->qmc_dai->dev, "read_submit %u failed %d\n", + i, ret); + return ret; + } + } + + return 0; +} + +static void qmc_audio_pcm_read_complete(void *context, size_t length, unsigned int flags) +{ + struct qmc_dai_prtd *prtd = context; + + if (length != prtd->ch_dma_size) { + dev_err(prtd->qmc_dai->dev, "read complete length = %zu, exp %zu\n", + length, prtd->ch_dma_size); + } + + prtd->buffer_ended += prtd->period_size; + if (prtd->buffer_ended >= prtd->buffer_size) + prtd->buffer_ended = 0; + + prtd->ch_dma_addr_current += prtd->ch_dma_size; + if (prtd->ch_dma_addr_current >= prtd->ch_dma_addr_end) + prtd->ch_dma_addr_current = prtd->ch_dma_addr_start; + + qmc_audio_pcm_read_submit(prtd); + + snd_pcm_period_elapsed(prtd->substream); +} + +static int qmc_audio_pcm_trigger(struct snd_soc_component *component, + struct snd_pcm_substream *substream, int cmd) +{ + struct qmc_dai_prtd *prtd = substream->runtime->private_data; + int ret; + + if (!prtd->qmc_dai) { + dev_err(component->dev, "qmc_dai is not set\n"); + return -EINVAL; + } + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + prtd->buffer_ended = 0; + prtd->ch_dma_addr_current = prtd->ch_dma_addr_start; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + /* Submit first chunk ... */ + ret = qmc_audio_pcm_write_submit(prtd); + if (ret) + return ret; + + /* ... prepare next one ... */ + prtd->ch_dma_addr_current += prtd->ch_dma_size; + if (prtd->ch_dma_addr_current >= prtd->ch_dma_addr_end) + prtd->ch_dma_addr_current = prtd->ch_dma_addr_start; + + /* ... and send it */ + ret = qmc_audio_pcm_write_submit(prtd); + if (ret) + return ret; + } else { + /* Submit first chunk ... */ + ret = qmc_audio_pcm_read_submit(prtd); + if (ret) + return ret; + + /* ... prepare next one ... */ + prtd->ch_dma_addr_current += prtd->ch_dma_size; + if (prtd->ch_dma_addr_current >= prtd->ch_dma_addr_end) + prtd->ch_dma_addr_current = prtd->ch_dma_addr_start; + + /* ... and send it */ + ret = qmc_audio_pcm_read_submit(prtd); + if (ret) + return ret; + } + break; + + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + break; + + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + break; + + default: + return -EINVAL; + } + + return 0; +} + +static snd_pcm_uframes_t qmc_audio_pcm_pointer(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct qmc_dai_prtd *prtd = substream->runtime->private_data; + + return prtd->buffer_ended; +} + +static int qmc_audio_of_xlate_dai_name(struct snd_soc_component *component, + const struct of_phandle_args *args, + const char **dai_name) +{ + struct qmc_audio *qmc_audio = dev_get_drvdata(component->dev); + struct snd_soc_dai_driver *dai_driver; + int id = args->args[0]; + int i; + + for (i = 0; i < qmc_audio->num_dais; i++) { + dai_driver = qmc_audio->dai_drivers + i; + if (dai_driver->id == id) { + *dai_name = dai_driver->name; + return 0; + } + } + + return -EINVAL; +} + +static const struct snd_pcm_hardware qmc_audio_pcm_hardware = { + .info = SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_NONINTERLEAVED | + SNDRV_PCM_INFO_PAUSE, + .period_bytes_min = 32, + .period_bytes_max = 64 * 1024, + .periods_min = 2, + .periods_max = 2 * 1024, + .buffer_bytes_max = 64 * 1024, +}; + +static int qmc_audio_pcm_open(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_pcm_runtime *runtime = substream->runtime; + struct qmc_dai_prtd *prtd; + int ret; + + snd_soc_set_runtime_hwparams(substream, &qmc_audio_pcm_hardware); + + /* ensure that buffer size is a multiple of period size */ + ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS); + if (ret < 0) + return ret; + + prtd = kzalloc(sizeof(*prtd), GFP_KERNEL); + if (!prtd) + return -ENOMEM; + + runtime->private_data = prtd; + + return 0; +} + +static int qmc_audio_pcm_close(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct qmc_dai_prtd *prtd = substream->runtime->private_data; + + kfree(prtd); + return 0; +} + +static const struct snd_soc_component_driver qmc_audio_soc_platform = { + .open = qmc_audio_pcm_open, + .close = qmc_audio_pcm_close, + .hw_params = qmc_audio_pcm_hw_params, + .trigger = qmc_audio_pcm_trigger, + .pointer = qmc_audio_pcm_pointer, + .pcm_construct = qmc_audio_pcm_construct, + .of_xlate_dai_name = qmc_audio_of_xlate_dai_name, +}; + +static unsigned int qmc_dai_get_index(struct snd_soc_dai *dai) +{ + struct qmc_audio *qmc_audio = snd_soc_dai_get_drvdata(dai); + + return dai->driver - qmc_audio->dai_drivers; +} + +static struct qmc_dai *qmc_dai_get_data(struct snd_soc_dai *dai) +{ + struct qmc_audio *qmc_audio = snd_soc_dai_get_drvdata(dai); + unsigned int index; + + index = qmc_dai_get_index(dai); + if (index > qmc_audio->num_dais) + return NULL; + + return qmc_audio->dais + index; +} + +/* + * The constraints for format/channel is to match with the number of 8bit + * time-slots available. + */ +static int qmc_dai_hw_rule_channels_by_format(struct qmc_dai *qmc_dai, + struct snd_pcm_hw_params *params, + unsigned int nb_ts) +{ + struct snd_interval *c = hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS); + snd_pcm_format_t format = params_format(params); + struct snd_interval ch = {0}; + + switch (snd_pcm_format_physical_width(format)) { + case 8: + ch.max = nb_ts; + break; + case 16: + ch.max = nb_ts / 2; + break; + case 32: + ch.max = nb_ts / 4; + break; + case 64: + ch.max = nb_ts / 8; + break; + default: + dev_err(qmc_dai->dev, "format physical width %u not supported\n", + snd_pcm_format_physical_width(format)); + return -EINVAL; + } + + ch.min = ch.max ? 1 : 0; + + return snd_interval_refine(c, &ch); +} + +static int qmc_dai_hw_rule_playback_channels_by_format(struct snd_pcm_hw_params *params, + struct snd_pcm_hw_rule *rule) +{ + struct qmc_dai *qmc_dai = rule->private; + + return qmc_dai_hw_rule_channels_by_format(qmc_dai, params, qmc_dai->nb_tx_ts); +} + +static int qmc_dai_hw_rule_capture_channels_by_format(struct snd_pcm_hw_params *params, + struct snd_pcm_hw_rule *rule) +{ + struct qmc_dai *qmc_dai = rule->private; + + return qmc_dai_hw_rule_channels_by_format(qmc_dai, params, qmc_dai->nb_rx_ts); +} + +static int qmc_dai_hw_rule_format_by_channels(struct qmc_dai *qmc_dai, + struct snd_pcm_hw_params *params, + unsigned int nb_ts) +{ + struct snd_mask *f_old = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT); + unsigned int channels = params_channels(params); + unsigned int slot_width; + snd_pcm_format_t format; + struct snd_mask f_new; + + if (!channels || channels > nb_ts) { + dev_err(qmc_dai->dev, "channels %u not supported\n", + nb_ts); + return -EINVAL; + } + + slot_width = (nb_ts / channels) * 8; + + snd_mask_none(&f_new); + pcm_for_each_format(format) { + if (snd_mask_test_format(f_old, format)) { + if (snd_pcm_format_physical_width(format) <= slot_width) + snd_mask_set_format(&f_new, format); + } + } + + return snd_mask_refine(f_old, &f_new); +} + +static int qmc_dai_hw_rule_playback_format_by_channels(struct snd_pcm_hw_params *params, + struct snd_pcm_hw_rule *rule) +{ + struct qmc_dai *qmc_dai = rule->private; + + return qmc_dai_hw_rule_format_by_channels(qmc_dai, params, qmc_dai->nb_tx_ts); +} + +static int qmc_dai_hw_rule_capture_format_by_channels(struct snd_pcm_hw_params *params, + struct snd_pcm_hw_rule *rule) +{ + struct qmc_dai *qmc_dai = rule->private; + + return qmc_dai_hw_rule_format_by_channels(qmc_dai, params, qmc_dai->nb_rx_ts); +} + +static int qmc_dai_constraints_interleaved(struct snd_pcm_substream *substream, + struct qmc_dai *qmc_dai) +{ + snd_pcm_hw_rule_func_t hw_rule_channels_by_format; + snd_pcm_hw_rule_func_t hw_rule_format_by_channels; + unsigned int frame_bits; + u64 access; + int ret; + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) { + hw_rule_channels_by_format = qmc_dai_hw_rule_capture_channels_by_format; + hw_rule_format_by_channels = qmc_dai_hw_rule_capture_format_by_channels; + frame_bits = qmc_dai->nb_rx_ts * 8; + } else { + hw_rule_channels_by_format = qmc_dai_hw_rule_playback_channels_by_format; + hw_rule_format_by_channels = qmc_dai_hw_rule_playback_format_by_channels; + frame_bits = qmc_dai->nb_tx_ts * 8; + } + + ret = snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS, + hw_rule_channels_by_format, qmc_dai, + SNDRV_PCM_HW_PARAM_FORMAT, -1); + if (ret) { + dev_err(qmc_dai->dev, "Failed to add channels rule (%d)\n", ret); + return ret; + } + + ret = snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_FORMAT, + hw_rule_format_by_channels, qmc_dai, + SNDRV_PCM_HW_PARAM_CHANNELS, -1); + if (ret) { + dev_err(qmc_dai->dev, "Failed to add format rule (%d)\n", ret); + return ret; + } + + ret = snd_pcm_hw_constraint_single(substream->runtime, + SNDRV_PCM_HW_PARAM_FRAME_BITS, + frame_bits); + if (ret < 0) { + dev_err(qmc_dai->dev, "Failed to add frame_bits constraint (%d)\n", ret); + return ret; + } + + access = 1ULL << (__force int)SNDRV_PCM_ACCESS_MMAP_INTERLEAVED | + 1ULL << (__force int)SNDRV_PCM_ACCESS_RW_INTERLEAVED; + ret = snd_pcm_hw_constraint_mask64(substream->runtime, SNDRV_PCM_HW_PARAM_ACCESS, + access); + if (ret) { + dev_err(qmc_dai->dev, "Failed to add hw_param_access constraint (%d)\n", ret); + return ret; + } + + return 0; +} + +static int qmc_dai_constraints_noninterleaved(struct snd_pcm_substream *substream, + struct qmc_dai *qmc_dai) +{ + unsigned int frame_bits; + u64 access; + int ret; + + frame_bits = (substream->stream == SNDRV_PCM_STREAM_CAPTURE) ? + qmc_dai->nb_rx_ts * 8 : qmc_dai->nb_tx_ts * 8; + ret = snd_pcm_hw_constraint_single(substream->runtime, + SNDRV_PCM_HW_PARAM_FRAME_BITS, + frame_bits); + if (ret < 0) { + dev_err(qmc_dai->dev, "Failed to add frame_bits constraint (%d)\n", ret); + return ret; + } + + access = 1ULL << (__force int)SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED | + 1ULL << (__force int)SNDRV_PCM_ACCESS_RW_NONINTERLEAVED; + ret = snd_pcm_hw_constraint_mask64(substream->runtime, SNDRV_PCM_HW_PARAM_ACCESS, + access); + if (ret) { + dev_err(qmc_dai->dev, "Failed to add hw_param_access constraint (%d)\n", ret); + return ret; + } + + return 0; +} + +static int qmc_dai_startup(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct qmc_dai_prtd *prtd = substream->runtime->private_data; + struct qmc_dai *qmc_dai; + + qmc_dai = qmc_dai_get_data(dai); + if (!qmc_dai) { + dev_err(dai->dev, "Invalid dai\n"); + return -EINVAL; + } + + prtd->qmc_dai = qmc_dai; + + return qmc_dai->nb_chans_avail > 1 ? + qmc_dai_constraints_noninterleaved(substream, qmc_dai) : + qmc_dai_constraints_interleaved(substream, qmc_dai); +} + +static int qmc_dai_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct qmc_chan_param chan_param = {0}; + unsigned int nb_chans_used; + struct qmc_dai *qmc_dai; + unsigned int i; + int ret; + + qmc_dai = qmc_dai_get_data(dai); + if (!qmc_dai) { + dev_err(dai->dev, "Invalid dai\n"); + return -EINVAL; + } + + /* + * In interleaved mode, the driver uses one QMC channel for all audio + * channels whereas in non-interleaved mode, it uses one QMC channel per + * audio channel. + */ + nb_chans_used = qmc_audio_access_is_interleaved(params_access(params)) ? + 1 : params_channels(params); + + if (nb_chans_used > qmc_dai->nb_chans_avail) { + dev_err(dai->dev, "Not enough qmc_chans. Need %u, avail %u\n", + nb_chans_used, qmc_dai->nb_chans_avail); + return -EINVAL; + } + + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) { + chan_param.mode = QMC_TRANSPARENT; + chan_param.transp.max_rx_buf_size = params_period_bytes(params) / nb_chans_used; + for (i = 0; i < nb_chans_used; i++) { + ret = qmc_chan_set_param(qmc_dai->qmc_chans[i], &chan_param); + if (ret) { + dev_err(dai->dev, "qmc_chans[%u], set param failed %d\n", + i, ret); + return ret; + } + } + qmc_dai->nb_chans_used_rx = nb_chans_used; + } else { + qmc_dai->nb_chans_used_tx = nb_chans_used; + } + + return 0; +} + +static int qmc_dai_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + unsigned int nb_chans_used; + struct qmc_dai *qmc_dai; + unsigned int i; + int direction; + int ret = 0; + int ret_tmp; + + qmc_dai = qmc_dai_get_data(dai); + if (!qmc_dai) { + dev_err(dai->dev, "Invalid dai\n"); + return -EINVAL; + } + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + direction = QMC_CHAN_WRITE; + nb_chans_used = qmc_dai->nb_chans_used_tx; + } else { + direction = QMC_CHAN_READ; + nb_chans_used = qmc_dai->nb_chans_used_rx; + } + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + for (i = 0; i < nb_chans_used; i++) { + ret = qmc_chan_start(qmc_dai->qmc_chans[i], direction); + if (ret) + goto err_stop; + } + break; + + case SNDRV_PCM_TRIGGER_STOP: + /* Stop and reset all QMC channels and return the first error encountered */ + for (i = 0; i < nb_chans_used; i++) { + ret_tmp = qmc_chan_stop(qmc_dai->qmc_chans[i], direction); + if (!ret) + ret = ret_tmp; + if (ret_tmp) + continue; + + ret_tmp = qmc_chan_reset(qmc_dai->qmc_chans[i], direction); + if (!ret) + ret = ret_tmp; + } + if (ret) + return ret; + break; + + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + /* Stop all QMC channels and return the first error encountered */ + for (i = 0; i < nb_chans_used; i++) { + ret_tmp = qmc_chan_stop(qmc_dai->qmc_chans[i], direction); + if (!ret) + ret = ret_tmp; + } + if (ret) + return ret; + break; + + default: + return -EINVAL; + } + + return 0; + +err_stop: + while (i--) { + qmc_chan_stop(qmc_dai->qmc_chans[i], direction); + qmc_chan_reset(qmc_dai->qmc_chans[i], direction); + } + return ret; +} + +static const struct snd_soc_dai_ops qmc_dai_ops = { + .startup = qmc_dai_startup, + .trigger = qmc_dai_trigger, + .hw_params = qmc_dai_hw_params, +}; + +static u64 qmc_audio_formats(u8 nb_ts, bool is_noninterleaved) +{ + unsigned int format_width; + unsigned int chan_width; + snd_pcm_format_t format; + u64 formats_mask; + + if (!nb_ts) + return 0; + + formats_mask = 0; + chan_width = nb_ts * 8; + pcm_for_each_format(format) { + /* + * Support format other than little-endian (ie big-endian or + * without endianness such as 8bit formats) + */ + if (snd_pcm_format_little_endian(format) == 1) + continue; + + /* Support physical width multiple of 8bit */ + format_width = snd_pcm_format_physical_width(format); + if (format_width == 0 || format_width % 8) + continue; + + /* + * And support physical width that can fit N times in the + * channel + */ + if (format_width > chan_width || chan_width % format_width) + continue; + + /* + * In non interleaved mode, we can only support formats that + * can fit only 1 time in the channel + */ + if (is_noninterleaved && format_width != chan_width) + continue; + + formats_mask |= pcm_format_to_bits(format); + } + return formats_mask; +} + +static int qmc_audio_dai_parse(struct qmc_audio *qmc_audio, struct device_node *np, + struct qmc_dai *qmc_dai, + struct snd_soc_dai_driver *qmc_soc_dai_driver) +{ + struct qmc_chan_ts_info ts_info; + struct qmc_chan_info info; + unsigned long rx_fs_rate; + unsigned long tx_fs_rate; + int prev_last_rx_ts = 0; + int prev_last_tx_ts = 0; + unsigned int nb_tx_ts; + unsigned int nb_rx_ts; + unsigned int i; + int last_rx_ts; + int last_tx_ts; + int count; + u32 val; + int ret; + + qmc_dai->dev = qmc_audio->dev; + + ret = of_property_read_u32(np, "reg", &val); + if (ret) { + dev_err(qmc_audio->dev, "%pOF: failed to read reg\n", np); + return ret; + } + qmc_dai->id = val; + + qmc_dai->name = devm_kasprintf(qmc_audio->dev, GFP_KERNEL, "%s.%d", + np->parent->name, qmc_dai->id); + if (!qmc_dai->name) + return -ENOMEM; + + count = qmc_chan_count_phandles(np, "fsl,qmc-chan"); + if (count < 0) + return dev_err_probe(qmc_audio->dev, count, + "dai %d get number of QMC channel failed\n", qmc_dai->id); + if (!count) + return dev_err_probe(qmc_audio->dev, -EINVAL, + "dai %d no QMC channel defined\n", qmc_dai->id); + + qmc_dai->qmc_chans = devm_kcalloc(qmc_audio->dev, count, sizeof(*qmc_dai->qmc_chans), + GFP_KERNEL); + if (!qmc_dai->qmc_chans) + return -ENOMEM; + + for (i = 0; i < count; i++) { + qmc_dai->qmc_chans[i] = devm_qmc_chan_get_byphandles_index(qmc_audio->dev, np, + "fsl,qmc-chan", i); + if (IS_ERR(qmc_dai->qmc_chans[i])) { + return dev_err_probe(qmc_audio->dev, PTR_ERR(qmc_dai->qmc_chans[i]), + "dai %d get QMC channel %d failed\n", qmc_dai->id, i); + } + + ret = qmc_chan_get_info(qmc_dai->qmc_chans[i], &info); + if (ret) { + dev_err(qmc_audio->dev, "dai %d get QMC %d channel info failed %d\n", + qmc_dai->id, i, ret); + return ret; + } + + if (info.mode != QMC_TRANSPARENT) { + dev_err(qmc_audio->dev, "dai %d QMC chan %d mode %d is not QMC_TRANSPARENT\n", + qmc_dai->id, i, info.mode); + return -EINVAL; + } + + /* + * All channels must have the same number of Tx slots and the + * same numbers of Rx slots. + */ + if (i == 0) { + nb_tx_ts = info.nb_tx_ts; + nb_rx_ts = info.nb_rx_ts; + tx_fs_rate = info.tx_fs_rate; + rx_fs_rate = info.rx_fs_rate; + } else { + if (nb_tx_ts != info.nb_tx_ts) { + dev_err(qmc_audio->dev, "dai %d QMC chan %d inconsistent number of Tx timeslots (%u instead of %u)\n", + qmc_dai->id, i, info.nb_tx_ts, nb_tx_ts); + return -EINVAL; + } + if (nb_rx_ts != info.nb_rx_ts) { + dev_err(qmc_audio->dev, "dai %d QMC chan %d inconsistent number of Rx timeslots (%u instead of %u)\n", + qmc_dai->id, i, info.nb_rx_ts, nb_rx_ts); + return -EINVAL; + } + if (tx_fs_rate != info.tx_fs_rate) { + dev_err(qmc_audio->dev, "dai %d QMC chan %d inconsistent Tx frame sample rate (%lu instead of %lu)\n", + qmc_dai->id, i, info.tx_fs_rate, tx_fs_rate); + return -EINVAL; + } + if (rx_fs_rate != info.rx_fs_rate) { + dev_err(qmc_audio->dev, "dai %d QMC chan %d inconsistent Rx frame sample rate (%lu instead of %lu)\n", + qmc_dai->id, i, info.rx_fs_rate, rx_fs_rate); + return -EINVAL; + } + } + + ret = qmc_chan_get_ts_info(qmc_dai->qmc_chans[i], &ts_info); + if (ret) { + dev_err(qmc_audio->dev, "dai %d get QMC %d channel TS info failed %d\n", + qmc_dai->id, i, ret); + return ret; + } + + last_rx_ts = fls64(ts_info.rx_ts_mask); + last_tx_ts = fls64(ts_info.tx_ts_mask); + + if (prev_last_rx_ts > last_rx_ts) { + dev_err(qmc_audio->dev, "dai %d QMC chan %d unordered channels (RX timeslot %d before %d)\n", + qmc_dai->id, i, prev_last_rx_ts, last_rx_ts); + return -EINVAL; + } + if (prev_last_tx_ts > last_tx_ts) { + dev_err(qmc_audio->dev, "dai %d QMC chan %d unordered channels (TX timeslot %d before %d)\n", + qmc_dai->id, i, prev_last_tx_ts, last_tx_ts); + return -EINVAL; + } + + prev_last_rx_ts = last_rx_ts; + prev_last_tx_ts = last_tx_ts; + } + + qmc_dai->nb_chans_avail = count; + qmc_dai->nb_tx_ts = nb_tx_ts * count; + qmc_dai->nb_rx_ts = nb_rx_ts * count; + + qmc_soc_dai_driver->id = qmc_dai->id; + qmc_soc_dai_driver->name = qmc_dai->name; + + qmc_soc_dai_driver->playback.channels_min = 0; + qmc_soc_dai_driver->playback.channels_max = 0; + if (nb_tx_ts) { + qmc_soc_dai_driver->playback.channels_min = 1; + qmc_soc_dai_driver->playback.channels_max = count > 1 ? count : nb_tx_ts; + } + qmc_soc_dai_driver->playback.formats = qmc_audio_formats(nb_tx_ts, + count > 1); + + qmc_soc_dai_driver->capture.channels_min = 0; + qmc_soc_dai_driver->capture.channels_max = 0; + if (nb_rx_ts) { + qmc_soc_dai_driver->capture.channels_min = 1; + qmc_soc_dai_driver->capture.channels_max = count > 1 ? count : nb_rx_ts; + } + qmc_soc_dai_driver->capture.formats = qmc_audio_formats(nb_rx_ts, + count > 1); + + qmc_soc_dai_driver->playback.rates = snd_pcm_rate_to_rate_bit(tx_fs_rate); + qmc_soc_dai_driver->playback.rate_min = tx_fs_rate; + qmc_soc_dai_driver->playback.rate_max = tx_fs_rate; + qmc_soc_dai_driver->capture.rates = snd_pcm_rate_to_rate_bit(rx_fs_rate); + qmc_soc_dai_driver->capture.rate_min = rx_fs_rate; + qmc_soc_dai_driver->capture.rate_max = rx_fs_rate; + + qmc_soc_dai_driver->ops = &qmc_dai_ops; + + return 0; +} + +static int qmc_audio_probe(struct platform_device *pdev) +{ + struct device_node *np = pdev->dev.of_node; + struct qmc_audio *qmc_audio; + struct device_node *child; + unsigned int i; + int ret; + + qmc_audio = devm_kzalloc(&pdev->dev, sizeof(*qmc_audio), GFP_KERNEL); + if (!qmc_audio) + return -ENOMEM; + + qmc_audio->dev = &pdev->dev; + + qmc_audio->num_dais = of_get_available_child_count(np); + if (qmc_audio->num_dais) { + qmc_audio->dais = devm_kcalloc(&pdev->dev, qmc_audio->num_dais, + sizeof(*qmc_audio->dais), + GFP_KERNEL); + if (!qmc_audio->dais) + return -ENOMEM; + + qmc_audio->dai_drivers = devm_kcalloc(&pdev->dev, qmc_audio->num_dais, + sizeof(*qmc_audio->dai_drivers), + GFP_KERNEL); + if (!qmc_audio->dai_drivers) + return -ENOMEM; + } + + i = 0; + for_each_available_child_of_node(np, child) { + ret = qmc_audio_dai_parse(qmc_audio, child, + qmc_audio->dais + i, + qmc_audio->dai_drivers + i); + if (ret) { + of_node_put(child); + return ret; + } + i++; + } + + platform_set_drvdata(pdev, qmc_audio); + + ret = devm_snd_soc_register_component(qmc_audio->dev, + &qmc_audio_soc_platform, + qmc_audio->dai_drivers, + qmc_audio->num_dais); + if (ret) + return ret; + + return 0; +} + +static const struct of_device_id qmc_audio_id_table[] = { + { .compatible = "fsl,qmc-audio" }, + {} /* sentinel */ +}; +MODULE_DEVICE_TABLE(of, qmc_audio_id_table); + +static struct platform_driver qmc_audio_driver = { + .driver = { + .name = "fsl-qmc-audio", + .of_match_table = of_match_ptr(qmc_audio_id_table), + }, + .probe = qmc_audio_probe, +}; +module_platform_driver(qmc_audio_driver); + +MODULE_AUTHOR("Herve Codina <herve.codina@bootlin.com>"); +MODULE_DESCRIPTION("CPM/QE QMC audio driver"); +MODULE_LICENSE("GPL"); |
