diff --git a/sound/soc/rockchip/Kconfig b/sound/soc/rockchip/Kconfig index d6315a745c065..b4ce270505688 100644 --- a/sound/soc/rockchip/Kconfig +++ b/sound/soc/rockchip/Kconfig @@ -7,6 +7,13 @@ config SND_SOC_ROCKCHIP the Rockchip SoCs' Audio interfaces. You will also need to select the audio interfaces to support below. +config SND_SOC_ROCKCHIP_DLP + tristate "Rockchip Digital Loopback Driver" + depends on SND_SOC_ROCKCHIP + help + Say Y or M if you want to add support for DLP driver for + Rockchip DMA-Based Digital Loopback. + config SND_SOC_ROCKCHIP_I2S tristate "Rockchip I2S Device Driver" depends on CLKDEV_LOOKUP && SND_SOC_ROCKCHIP diff --git a/sound/soc/rockchip/Makefile b/sound/soc/rockchip/Makefile index b45fa0103c334..ec971f569bc3a 100644 --- a/sound/soc/rockchip/Makefile +++ b/sound/soc/rockchip/Makefile @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 # ROCKCHIP Platform Support +snd-soc-rockchip-dlp-objs := rockchip_dlp.o snd-soc-rockchip-i2s-objs := rockchip_i2s.o snd-soc-rockchip-i2s-tdm-objs := rockchip_i2s_tdm.o snd-soc-rockchip-multi-dais-objs := rockchip_multi_dais.o rockchip_multi_dais_pcm.o @@ -14,6 +15,7 @@ snd-soc-rockchip-vad-$(CONFIG_ARM64) += vad_preprocess_arm64.o snd-soc-rockchip-vad-$(CONFIG_ARM) += vad_preprocess_arm.o endif +obj-$(CONFIG_SND_SOC_ROCKCHIP_DLP) += snd-soc-rockchip-dlp.o obj-$(CONFIG_SND_SOC_ROCKCHIP_I2S) += snd-soc-rockchip-i2s.o obj-$(CONFIG_SND_SOC_ROCKCHIP_I2S_TDM) += snd-soc-rockchip-i2s-tdm.o obj-$(CONFIG_SND_SOC_ROCKCHIP_MULTI_DAIS) += snd-soc-rockchip-multi-dais.o diff --git a/sound/soc/rockchip/rockchip_dlp.c b/sound/soc/rockchip/rockchip_dlp.c new file mode 100644 index 0000000000000..27a514785f772 --- /dev/null +++ b/sound/soc/rockchip/rockchip_dlp.c @@ -0,0 +1,1079 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Rockchip DLP (Digital Loopback) Driver + * + * Copyright (c) 2022 Rockchip Electronics Co. Ltd. + * Author: Sugar Zhang + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "rockchip_dlp.h" + +#ifdef DLP_DBG +#define dlp_info(args...) pr_info(args) +#else +#define dlp_info(args...) no_printk(args) +#endif + +#define SND_DMAENGINE_DLP_DRV_NAME "snd_dmaengine_dlp" +#define PBUF_CNT 2 + +static unsigned int prealloc_buffer_size_kbytes = 512; +module_param(prealloc_buffer_size_kbytes, uint, 0444); +MODULE_PARM_DESC(prealloc_buffer_size_kbytes, "Preallocate DMA buffer size (KB)."); + +/* MUST: dlp_text should be match to enum dlp_mode */ +static const char *const dlp_text[] = { + "Disabled", + "2CH: 1 Loopback + 1 Mic", + "2CH: 1 Mic + 1 Loopback", + "2CH: 1 Mic + 1 Loopback-mixed", + "2CH: 2 Loopbacks", + "4CH: 2 Mics + 2 Loopbacks", + "4CH: 2 Mics + 1 Loopback-mixed", +}; + +enum dlp_mode { + DLP_MODE_DISABLED, + DLP_MODE_2CH_1LP_1MIC, /* replace cap-ch-0 with play-ch-0 */ + DLP_MODE_2CH_1MIC_1LP, /* replace cap-ch-1 with play-ch-1 */ + DLP_MODE_2CH_1MIC_1LP_MIX, /* replace cap-ch-1 with play-ch-all-mix */ + DLP_MODE_2CH_2LP, /* replace cap-ch-01 with play-ch-01 */ + DLP_MODE_4CH_2MIC_2LP, /* replace cap-ch-34 with play-ch-01 */ + DLP_MODE_4CH_2MIC_1LP_MIX, /* replace cap-ch-3 with play-ch-all-mix */ +}; + +struct dmaengine_dlp { + struct device *dev; + struct dma_chan *chan[SNDRV_PCM_STREAM_LAST + 1]; + const struct snd_dlp_config *config; + struct snd_soc_component component; + struct list_head ref_list; + enum dlp_mode mode; + spinlock_t lock; +}; + +struct dmaengine_dlp_runtime_data { + struct dmaengine_dlp *parent; + struct dmaengine_dlp_runtime_data *ref; + struct dma_chan *dma_chan; + struct kref refcount; + struct list_head node; + dma_cookie_t cookie; + + char *buf; + snd_pcm_uframes_t buf_sz; + snd_pcm_uframes_t period_sz; + snd_pcm_uframes_t hw_ptr; + snd_pcm_sframes_t hw_ptr_delta; /* play-ptr - cap-ptr */ + unsigned long period_elapsed; + unsigned int frame_bytes; + unsigned int channels; + unsigned int buf_ofs; + int stream; +}; + +static inline void dlp_activate(struct dmaengine_dlp *dlp) +{ + spin_lock(&dlp->lock); + dlp->component.active++; + spin_unlock(&dlp->lock); +} + +static inline void dlp_deactivate(struct dmaengine_dlp *dlp) +{ + spin_lock(&dlp->lock); + dlp->component.active--; + spin_unlock(&dlp->lock); +} + +static inline bool dlp_mode_channels_match(struct dmaengine_dlp *dlp, + int ch, int *expected) +{ + *expected = 0; + + switch (dlp->mode) { + case DLP_MODE_DISABLED: + return true; + case DLP_MODE_2CH_1LP_1MIC: + case DLP_MODE_2CH_1MIC_1LP: + case DLP_MODE_2CH_1MIC_1LP_MIX: + case DLP_MODE_2CH_2LP: + *expected = 2; + return (ch == 2); + case DLP_MODE_4CH_2MIC_2LP: + case DLP_MODE_4CH_2MIC_1LP_MIX: + *expected = 4; + return (ch == 4); + default: + return false; + } +} + +static inline ssize_t dlp_channels_to_bytes(struct dmaengine_dlp_runtime_data *prtd, + int channels) +{ + return (prtd->frame_bytes / prtd->channels) * channels; +} + +static inline ssize_t dlp_frames_to_bytes(struct dmaengine_dlp_runtime_data *prtd, + snd_pcm_sframes_t size) +{ + return size * prtd->frame_bytes; +} + +static inline snd_pcm_sframes_t dlp_bytes_to_frames(struct dmaengine_dlp_runtime_data *prtd, + ssize_t size) +{ + return size / prtd->frame_bytes; +} + +static inline struct dmaengine_dlp *soc_component_to_dlp(struct snd_soc_component *p) +{ + return container_of(p, struct dmaengine_dlp, component); +} + +static inline struct dmaengine_dlp_runtime_data *substream_to_prtd( + const struct snd_pcm_substream *substream) +{ + if (!substream->runtime) + return NULL; + + return substream->runtime->private_data; +} + +static struct dma_chan *snd_dmaengine_dlp_get_chan(struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + + return prtd->dma_chan; +} + +static struct device *dmaengine_dma_dev(struct dmaengine_dlp *dlp, + struct snd_pcm_substream *substream) +{ + if (!dlp->chan[substream->stream]) + return NULL; + + return dlp->chan[substream->stream]->device->dev; +} + +static int dlp_get_offset_size(struct dmaengine_dlp_runtime_data *prtd, + enum dlp_mode mode, int *ofs, int *size, bool *mix) +{ + bool is_playback = prtd->stream == SNDRV_PCM_STREAM_PLAYBACK; + int ret = 0; + + switch (mode) { + case DLP_MODE_2CH_1LP_1MIC: + *ofs = 0; + *size = dlp_channels_to_bytes(prtd, 1); + break; + case DLP_MODE_2CH_1MIC_1LP: + *ofs = dlp_channels_to_bytes(prtd, 1); + *size = dlp_channels_to_bytes(prtd, 1); + break; + case DLP_MODE_2CH_1MIC_1LP_MIX: + if (is_playback) { + *ofs = 0; + *size = dlp_frames_to_bytes(prtd, 1); + if (mix) + *mix = true; + } else { + *ofs = dlp_channels_to_bytes(prtd, 1); + *size = dlp_channels_to_bytes(prtd, 1); + } + break; + case DLP_MODE_2CH_2LP: + *ofs = 0; + *size = dlp_channels_to_bytes(prtd, 2); + break; + case DLP_MODE_4CH_2MIC_2LP: + if (is_playback) { + *ofs = 0; + *size = dlp_channels_to_bytes(prtd, 2); + } else { + *ofs = dlp_channels_to_bytes(prtd, 2); + *size = dlp_channels_to_bytes(prtd, 2); + } + break; + case DLP_MODE_4CH_2MIC_1LP_MIX: + if (is_playback) { + *ofs = 0; + *size = dlp_frames_to_bytes(prtd, 1); + if (mix) + *mix = true; + } else { + *ofs = dlp_channels_to_bytes(prtd, 2); + *size = dlp_channels_to_bytes(prtd, 1); + } + break; + default: + *ofs = 0; + *size = 0; + if (mix) + *mix = false; + ret = -EINVAL; + } + + return ret; +} + +static int dlp_mix_frame_buffer(struct dmaengine_dlp_runtime_data *prtd, void *buf) +{ + int sample_bytes = dlp_channels_to_bytes(prtd, 1); + int16_t *p16 = (int16_t *)buf, v16 = 0; + int32_t *p32 = (int32_t *)buf, v32 = 0; + int i = 0; + + switch (sample_bytes) { + case 2: + for (i = 0; i < prtd->channels; i++) + v16 += (p16[i] / prtd->channels); + p16[0] = v16; + break; + case 4: + for (i = 0; i < prtd->channels; i++) + v32 += (p32[i] / prtd->channels); + p32[0] = v32; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int dmaengine_dlp_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct dma_chan *chan = snd_dmaengine_dlp_get_chan(substream); + struct dma_slave_config slave_config; + bool is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + unsigned int buf_bytes; + int ch_req = params_channels(params), ch_exp = 0; + int ret; + + /* mode should match to channels */ + if (!is_playback && !dlp_mode_channels_match(dlp, ch_req, &ch_exp)) { + dev_err(dlp->dev, + "capture %d ch, expected: %d ch for loopback mode-%d\n", + ch_req, ch_exp, dlp->mode); + return -EINVAL; + } + + memset(&slave_config, 0, sizeof(slave_config)); + + ret = snd_dmaengine_pcm_prepare_slave_config(substream, params, &slave_config); + if (ret) + return ret; + + ret = dmaengine_slave_config(chan, &slave_config); + if (ret) + return ret; + + prtd->frame_bytes = snd_pcm_format_size(params_format(params), + params_channels(params)); + prtd->period_sz = params_period_size(params); + prtd->buf_sz = params_buffer_size(params); + prtd->channels = params_channels(params); + + buf_bytes = prtd->frame_bytes * params_buffer_size(params); + + if (is_playback) { + buf_bytes *= PBUF_CNT; + prtd->buf_sz *= PBUF_CNT; + } + + prtd->buf = kzalloc(buf_bytes, GFP_KERNEL); + if (!prtd->buf) + return -ENOMEM; + + return 0; +} + +static int +dmaengine_pcm_set_runtime_hwparams(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = asoc_substream_to_rtd(substream); + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct device *dma_dev = dmaengine_dma_dev(dlp, substream); + struct dma_chan *chan = dlp->chan[substream->stream]; + struct snd_dmaengine_dai_dma_data *dma_data; + struct snd_pcm_hardware hw; + + if (rtd->num_cpus > 1) { + dev_err(rtd->dev, + "%s doesn't support Multi CPU yet\n", __func__); + return -EINVAL; + } + + dma_data = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream); + + memset(&hw, 0, sizeof(hw)); + hw.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED; + hw.periods_min = 2; + hw.periods_max = UINT_MAX; + hw.period_bytes_min = 256; + hw.period_bytes_max = dma_get_max_seg_size(dma_dev); + hw.buffer_bytes_max = SIZE_MAX; + hw.fifo_size = dma_data->fifo_size; + + /** + * FIXME: Remove the return value check to align with the code + * before adding snd_dmaengine_pcm_refine_runtime_hwparams + * function. + */ + snd_dmaengine_pcm_refine_runtime_hwparams(substream, + dma_data, + &hw, + chan); + + return snd_soc_set_runtime_hwparams(substream, &hw); +} + +static int dmaengine_dlp_open(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct dma_chan *chan = dlp->chan[substream->stream]; + struct dmaengine_dlp_runtime_data *prtd; + int ret; + + if (!chan) + return -ENXIO; + + ret = dmaengine_pcm_set_runtime_hwparams(component, substream); + if (ret) + return ret; + + ret = snd_pcm_hw_constraint_integer(substream->runtime, + SNDRV_PCM_HW_PARAM_PERIODS); + if (ret < 0) + return ret; + + prtd = kzalloc(sizeof(*prtd), GFP_KERNEL); + if (!prtd) + return -ENOMEM; + + dlp_info("PRTD-CREATE: 0x%px (%s)\n", + prtd, substream->stream ? "C" : "P"); + + kref_init(&prtd->refcount); + prtd->parent = dlp; + prtd->stream = substream->stream; + prtd->dma_chan = chan; + + substream->runtime->private_data = prtd; + + dlp_activate(dlp); + + return 0; +} + +static void dmaengine_free_prtd(struct kref *ref) +{ + struct dmaengine_dlp_runtime_data *prtd = + container_of(ref, struct dmaengine_dlp_runtime_data, refcount); + + dlp_info("PRTD-FREE: 0x%px\n", prtd); + + kfree(prtd->buf); + kfree(prtd); +} + +static void free_ref_list(struct snd_soc_component *component) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct dmaengine_dlp_runtime_data *prtd, *_pt; + + spin_lock(&dlp->lock); + list_for_each_entry_safe(prtd, _pt, &dlp->ref_list, node) { + list_del(&prtd->node); + kref_put(&prtd->refcount, dmaengine_free_prtd); + } + spin_unlock(&dlp->lock); +} + +static int dmaengine_dlp_close(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + + dmaengine_synchronize(prtd->dma_chan); + + /* + * free residue playback ref list for capture when close + */ + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) + free_ref_list(component); + + /* + * kref put should be after hw_ptr updated when stop, + * ops->trigger: SNDRV_PCM_TRIGGER_STOP -> ops->close + * obviously, it is! + */ + kref_put(&prtd->refcount, dmaengine_free_prtd); + + dlp_deactivate(dlp); + + return 0; +} + +static snd_pcm_uframes_t dmaengine_dlp_pointer( + struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct dma_tx_state state; + unsigned int buf_size; + unsigned int pos = 0; + + dmaengine_tx_status(prtd->dma_chan, prtd->cookie, &state); + buf_size = snd_pcm_lib_buffer_bytes(substream); + if (state.residue > 0 && state.residue <= buf_size) + pos = buf_size - state.residue; + + return dlp_bytes_to_frames(prtd, pos); +} + +static void dmaengine_dlp_dma_complete(void *arg) +{ + struct snd_pcm_substream *substream = arg; + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct dmaengine_dlp *dlp = prtd->parent; + + if (!substream->runtime) + return; + + spin_lock(&dlp->lock); + prtd->period_elapsed++; + prtd->hw_ptr = prtd->period_elapsed * prtd->period_sz; + spin_unlock(&dlp->lock); + snd_pcm_period_elapsed(substream); +} + +static int dmaengine_dlp_prepare_and_submit(struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct dma_chan *chan = prtd->dma_chan; + struct dma_async_tx_descriptor *desc; + enum dma_transfer_direction direction; + unsigned long flags = DMA_CTRL_ACK; + + direction = snd_pcm_substream_to_dma_direction(substream); + + if (!substream->runtime->no_period_wakeup) + flags |= DMA_PREP_INTERRUPT; + + desc = dmaengine_prep_dma_cyclic(chan, + substream->runtime->dma_addr, + snd_pcm_lib_buffer_bytes(substream), + snd_pcm_lib_period_bytes(substream), direction, flags); + + if (!desc) + return -ENOMEM; + + desc->callback = dmaengine_dlp_dma_complete; + desc->callback_param = substream; + prtd->cookie = dmaengine_submit(desc); + + return 0; +} + +static int dmaengine_pcm_lp_setup(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + int bstream = SNDRV_PCM_STREAM_LAST - substream->stream; + struct snd_pcm_str *bro = &substream->pcm->streams[bstream]; + struct snd_pcm_substream *bsubstream = bro->substream; + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct dmaengine_dlp_runtime_data *brtd = substream_to_prtd(bsubstream); + bool is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + snd_pcm_uframes_t a = 0, b = 0, fifo_a = 0, fifo_b = 0; + snd_pcm_sframes_t delta = 0; + + if (dlp->mode == DLP_MODE_DISABLED) + return -EINVAL; + + fifo_a = dlp->config->get_fifo_count(dlp->dev, substream->stream); + a = dmaengine_dlp_pointer(component, substream); + + if (bsubstream->runtime && snd_pcm_running(bsubstream)) { + fifo_b = dlp->config->get_fifo_count(dlp->dev, bstream); + b = dmaengine_dlp_pointer(component, bsubstream); + + a = (prtd->period_elapsed * prtd->period_sz) + (a % prtd->period_sz); + b = (brtd->period_elapsed * brtd->period_sz) + (b % brtd->period_sz); + + fifo_a = dlp_bytes_to_frames(prtd, fifo_a * 4); + fifo_b = dlp_bytes_to_frames(brtd, fifo_b * 4); + + delta = is_playback ? (a - fifo_a) - (b + fifo_b) : (b - fifo_b) - (a + fifo_a); + + prtd->hw_ptr_delta = delta; + + /* push valid playback into ref list */ + spin_lock(&dlp->lock); + if (is_playback) { + kref_get(&prtd->refcount); + list_add_tail(&prtd->node, &dlp->ref_list); + } else { + kref_get(&brtd->refcount); + list_add_tail(&brtd->node, &dlp->ref_list); + } + spin_unlock(&dlp->lock); + } + + if (is_playback) + dlp_info("START-P: DMA-P: %lu, DMA-C: %lu, FIFO-P: %lu, FIFO-C: %lu, DELTA: %ld\n", + a, b, fifo_a, fifo_b, delta); + else + dlp_info("START-C: DMA-P: %lu, DMA-C: %lu, FIFO-P: %lu, FIFO-C: %lu, DELTA: %ld\n", + b, a, fifo_b, fifo_a, delta); + + return 0; +} + +static void dmaengine_pcm_lp_release(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + snd_pcm_uframes_t appl_ptr, hw_ptr; + + if (dlp->mode == DLP_MODE_DISABLED) + return; + + /* any data in FIFOs will be gone ,so don't care */ + appl_ptr = READ_ONCE(runtime->control->appl_ptr); + hw_ptr = dmaengine_dlp_pointer(component, substream); + spin_lock(&dlp->lock); + hw_ptr = (prtd->period_elapsed * prtd->period_sz) + (hw_ptr % prtd->period_sz); + prtd->hw_ptr = hw_ptr; + spin_unlock(&dlp->lock); + + /* + * playback: + * + * snd_pcm_drop: hw_ptr will be smaller than appl_ptr + * snd_pcm_drain, hw_ptr will be equal to appl_ptr + * + * anyway, we should use the smaller one, obviously, it's hw_ptr. + */ + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + dlp_info("STOP-P: applptr: %lu, hwptr: %lu\n", appl_ptr, hw_ptr); + else + dlp_info("STOP-C: applptr: %lu, hwptr: %lu\n", appl_ptr, hw_ptr); +} + +static int dmaengine_dlp_trigger(struct snd_soc_component *component, + struct snd_pcm_substream *substream, int cmd) +{ + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + int ret; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + ret = dmaengine_dlp_prepare_and_submit(substream); + if (ret) + return ret; + dma_async_issue_pending(prtd->dma_chan); + dmaengine_pcm_lp_setup(component, substream); + break; + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + dmaengine_resume(prtd->dma_chan); + break; + case SNDRV_PCM_TRIGGER_SUSPEND: + if (runtime->info & SNDRV_PCM_INFO_PAUSE) { + dmaengine_pause(prtd->dma_chan); + } else { + dmaengine_pcm_lp_release(component, substream); + dmaengine_terminate_async(prtd->dma_chan); + } + break; + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + dmaengine_pause(prtd->dma_chan); + break; + case SNDRV_PCM_TRIGGER_STOP: + dmaengine_pcm_lp_release(component, substream); + dmaengine_terminate_async(prtd->dma_chan); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int dmaengine_dlp_new(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct snd_pcm_substream *substream; + size_t prealloc_buffer_size; + size_t max_buffer_size; + unsigned int i; + + prealloc_buffer_size = prealloc_buffer_size_kbytes * 1024; + max_buffer_size = SIZE_MAX; + + for_each_pcm_streams(i) { + substream = rtd->pcm->streams[i].substream; + if (!substream) + continue; + + if (!dlp->chan[i]) { + dev_err(component->dev, + "Missing dma channel for stream: %d\n", i); + return -EINVAL; + } + + snd_pcm_set_managed_buffer(substream, + SNDRV_DMA_TYPE_DEV_IRAM, + dmaengine_dma_dev(dlp, substream), + prealloc_buffer_size, + max_buffer_size); + + if (rtd->pcm->streams[i].pcm->name[0] == '\0') { + strscpy_pad(rtd->pcm->streams[i].pcm->name, + rtd->pcm->streams[i].pcm->id, + sizeof(rtd->pcm->streams[i].pcm->name)); + } + } + + return 0; +} + +static struct dmaengine_dlp_runtime_data *get_ref(struct snd_soc_component *component) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct dmaengine_dlp_runtime_data *pref = NULL; + + spin_lock(&dlp->lock); + if (!list_empty(&dlp->ref_list)) { + pref = list_first_entry(&dlp->ref_list, struct dmaengine_dlp_runtime_data, node); + list_del(&pref->node); + } + spin_unlock(&dlp->lock); + + return pref; +} + +static int process_capture(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + unsigned long hwoff, + void __user *buf, unsigned long bytes) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + struct snd_pcm_runtime *runtime = substream->runtime; + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct dmaengine_dlp_runtime_data *pref = NULL; + void *dma_ptr = runtime->dma_area + hwoff; + snd_pcm_sframes_t frames = dlp_bytes_to_frames(prtd, bytes); + snd_pcm_sframes_t frames_consumed = 0, frames_residue = 0, frames_tmp = 0; + snd_pcm_sframes_t ofs = 0; + snd_pcm_uframes_t appl_ptr; + char *cbuf = prtd->buf, *pbuf = NULL; + int ofs_cap, ofs_play, size_cap, size_play; + int i = 0, j = 0, ret = 0; + bool free_ref = false, mix = false; + + appl_ptr = READ_ONCE(runtime->control->appl_ptr); + + memcpy(cbuf, dma_ptr, bytes); +#ifdef DLP_DBG + /* DBG: mark STUB in ch-REC for trace each read */ + memset(cbuf, 0x22, dlp_channels_to_bytes(prtd, 1)); +#endif + ret = dlp_get_offset_size(prtd, dlp->mode, &ofs_cap, &size_cap, NULL); + if (ret) { + dlp_info("fail to get dlp cap offset\n"); + return -EINVAL; + } + + /* clear channel-LP_CHN */ + for (i = 0; i < frames; i++) { + cbuf = prtd->buf + dlp_frames_to_bytes(prtd, i) + ofs_cap; + memset(cbuf, 0x0, size_cap); + } + +start: + if (!prtd->ref) + prtd->ref = get_ref(component); + pref = prtd->ref; + + /* do nothing if play stop */ + if (!pref) + return 0; + + ret = dlp_get_offset_size(pref, dlp->mode, &ofs_play, &size_play, &mix); + if (ret) { + dlp_info("fail to get dlp play offset\n"); + return 0; + } + + ofs = appl_ptr + pref->hw_ptr_delta; + + /* + * if playback stop, kref_put ref, and we can check this to + * know if playback stopped, then free prtd->ref if data consumed. + * + */ + if (kref_read(&pref->refcount) == 1) { + if (ofs >= pref->hw_ptr) { + kref_put(&pref->refcount, dmaengine_free_prtd); + prtd->ref = NULL; + return 0; + } else if ((ofs + frames) > pref->hw_ptr) { + dlp_info("applptr: %8lu, ofs': %7ld, refhwptr: %lu, frames: %lu (*)\n", + appl_ptr, ofs, pref->hw_ptr, frames); + /* + * should ignore the data that after play stop + * and care about if the next ref start in the + * same window + */ + frames_tmp = pref->hw_ptr - ofs; + frames_residue = frames - frames_tmp; + frames = frames_tmp; + free_ref = true; + } + } + + /* + * should ignore the data that before play start: + * + * frames: + * +---------------------------------------------+ + * | ofs<0 | ofs>0 | + * +---------------------------------------------+ + * + */ + if ((ofs + frames) <= 0) + return 0; + + /* skip if ofs < 0 and fixup ofs */ + j = 0; + if (ofs < 0) { + dlp_info("applptr: %8lu, ofs: %8ld, frames: %lu (*)\n", + appl_ptr, ofs, frames); + j = -ofs; + frames += ofs; + ofs = 0; + } + + ofs %= pref->buf_sz; + + dlp_info("applptr: %8lu, ofs: %8ld, frames: %lu\n", appl_ptr, ofs, frames); + + for (i = 0; i < frames; i++, j++) { + cbuf = prtd->buf + dlp_frames_to_bytes(prtd, j + frames_consumed) + ofs_cap; + pbuf = pref->buf + dlp_frames_to_bytes(pref, ((i + ofs) % pref->buf_sz)) + ofs_play; + if (mix) + dlp_mix_frame_buffer(pref, pbuf); + memcpy(cbuf, pbuf, size_cap); + } + + appl_ptr += frames; + frames_consumed += frames; + + if (free_ref) { + kref_put(&pref->refcount, dmaengine_free_prtd); + prtd->ref = NULL; + free_ref = false; + if (frames_residue) { + frames = frames_residue; + frames_residue = 0; + goto start; + } + } + + return 0; +} + +static int process_playback(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + unsigned long hwoff, + void __user *buf, unsigned long bytes) +{ + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + char *pbuf = prtd->buf + prtd->buf_ofs; + + if (copy_from_user(pbuf, buf, bytes)) + return -EFAULT; + + prtd->buf_ofs += bytes; + prtd->buf_ofs %= dlp_frames_to_bytes(prtd, prtd->buf_sz); + + return 0; +} + +static int dmaengine_process(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + unsigned long hwoff, + void __user *buf, unsigned long bytes) +{ + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + int ret = 0; + + if (dlp->mode == DLP_MODE_DISABLED) + return -EINVAL; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + ret = process_playback(component, substream, hwoff, buf, bytes); + else + ret = process_capture(component, substream, hwoff, buf, bytes); + + return ret; +} + +static int dmaengine_copy_user(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + int channel, unsigned long hwoff, + void __user *buf, unsigned long bytes) +{ + struct dmaengine_dlp_runtime_data *prtd = substream_to_prtd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + bool is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + void *dma_ptr = runtime->dma_area + hwoff + + channel * (runtime->dma_bytes / runtime->channels); + int ret; + + if (is_playback) + if (copy_from_user(dma_ptr, buf, bytes)) + return -EFAULT; + + ret = dmaengine_process(component, substream, hwoff, buf, bytes); + if (!ret) + dma_ptr = prtd->buf; + + if (!is_playback) + if (copy_to_user(buf, dma_ptr, bytes)) + return -EFAULT; + + return 0; +} + +static SOC_ENUM_SINGLE_EXT_DECL(dlp_mode, dlp_text); + +static int dmaengine_dlp_mode_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_soc_kcontrol_component(kcontrol); + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + + ucontrol->value.enumerated.item[0] = dlp->mode; + + return 0; +} + +static int dmaengine_dlp_mode_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_soc_kcontrol_component(kcontrol); + struct dmaengine_dlp *dlp = soc_component_to_dlp(component); + unsigned int mode = ucontrol->value.enumerated.item[0]; + + /* MUST: do not update mode while stream is running */ + if (snd_soc_component_active(component)) + return -EPERM; + + if (mode == dlp->mode) + return 0; + + dlp->mode = mode; + + return 1; +} + +static const struct snd_kcontrol_new dmaengine_dlp_controls[] = { + SOC_ENUM_EXT("Software Digital Loopback Mode", dlp_mode, + dmaengine_dlp_mode_get, + dmaengine_dlp_mode_put), +}; + +static const struct snd_soc_component_driver dmaengine_dlp_component = { + .name = SND_DMAENGINE_DLP_DRV_NAME, + .probe_order = SND_SOC_COMP_ORDER_LATE, + .open = dmaengine_dlp_open, + .close = dmaengine_dlp_close, + .hw_params = dmaengine_dlp_hw_params, + .trigger = dmaengine_dlp_trigger, + .pointer = dmaengine_dlp_pointer, + .copy_user = dmaengine_copy_user, + .pcm_construct = dmaengine_dlp_new, + .controls = dmaengine_dlp_controls, + .num_controls = ARRAY_SIZE(dmaengine_dlp_controls), +}; + +static const char * const dmaengine_pcm_dma_channel_names[] = { + [SNDRV_PCM_STREAM_PLAYBACK] = "tx", + [SNDRV_PCM_STREAM_CAPTURE] = "rx", +}; + +static int dmaengine_pcm_request_chan_of(struct dmaengine_dlp *dlp, + struct device *dev, const struct snd_dmaengine_pcm_config *config) +{ + unsigned int i; + const char *name; + struct dma_chan *chan; + + for_each_pcm_streams(i) { + name = dmaengine_pcm_dma_channel_names[i]; + chan = dma_request_chan(dev, name); + if (IS_ERR(chan)) { + /* + * Only report probe deferral errors, channels + * might not be present for devices that + * support only TX or only RX. + */ + if (PTR_ERR(chan) == -EPROBE_DEFER) + return -EPROBE_DEFER; + dlp->chan[i] = NULL; + } else { + dlp->chan[i] = chan; + } + } + + return 0; +} + +static void dmaengine_pcm_release_chan(struct dmaengine_dlp *dlp) +{ + unsigned int i; + + for_each_pcm_streams(i) { + if (!dlp->chan[i]) + continue; + dma_release_channel(dlp->chan[i]); + } +} + +/** + * snd_dmaengine_dlp_register - Register a dmaengine based DLP device + * @dev: The parent device for the DLP device + * @config: Platform specific DLP configuration + */ +static int snd_dmaengine_dlp_register(struct device *dev, + const struct snd_dlp_config *config) +{ + const struct snd_soc_component_driver *driver; + struct dmaengine_dlp *dlp; + int ret; + + dlp = kzalloc(sizeof(*dlp), GFP_KERNEL); + if (!dlp) + return -ENOMEM; + + dlp->dev = dev; + dlp->config = config; + + INIT_LIST_HEAD(&dlp->ref_list); + spin_lock_init(&dlp->lock); + +#ifdef CONFIG_DEBUG_FS + dlp->component.debugfs_prefix = "dma"; +#endif + ret = dmaengine_pcm_request_chan_of(dlp, dev, NULL); + if (ret) + goto err_free_dma; + + driver = &dmaengine_dlp_component; + + ret = snd_soc_component_initialize(&dlp->component, driver, dev); + if (ret) + goto err_free_dma; + + ret = snd_soc_add_component(&dlp->component, NULL, 0); + if (ret) + goto err_free_dma; + + return 0; + +err_free_dma: + dmaengine_pcm_release_chan(dlp); + kfree(dlp); + return ret; +} + +/** + * snd_dmaengine_dlp_unregister - Removes a dmaengine based DLP device + * @dev: Parent device the DLP was register with + * + * Removes a dmaengine based DLP device previously registered with + * snd_dmaengine_pcm_register. + */ +static void snd_dmaengine_dlp_unregister(struct device *dev) +{ + struct snd_soc_component *component; + struct dmaengine_dlp *dlp; + + component = snd_soc_lookup_component(dev, SND_DMAENGINE_DLP_DRV_NAME); + if (!component) + return; + + dlp = soc_component_to_dlp(component); + + snd_soc_unregister_component_by_driver(dev, component->driver); + dmaengine_pcm_release_chan(dlp); + kfree(dlp); +} + +static void devm_dmaengine_dlp_release(struct device *dev, void *res) +{ + snd_dmaengine_dlp_unregister(*(struct device **)res); +} + +/** + * devm_snd_dmaengine_dlp_register - resource managed dmaengine DLP registration + * @dev: The parent device for the DLP device + * @config: Platform specific DLP configuration + * + * Register a dmaengine based DLP device with automatic unregistration when the + * device is unregistered. + */ +int devm_snd_dmaengine_dlp_register(struct device *dev, + const struct snd_dlp_config *config) +{ + struct device **ptr; + int ret; + + ptr = devres_alloc(devm_dmaengine_dlp_release, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return -ENOMEM; + + ret = snd_dmaengine_dlp_register(dev, config); + if (ret == 0) { + *ptr = dev; + devres_add(dev, ptr); + } else { + devres_free(ptr); + } + + return ret; +} +EXPORT_SYMBOL_GPL(devm_snd_dmaengine_dlp_register); + +MODULE_LICENSE("GPL"); diff --git a/sound/soc/rockchip/rockchip_dlp.h b/sound/soc/rockchip/rockchip_dlp.h new file mode 100644 index 0000000000000..6235482b5253d --- /dev/null +++ b/sound/soc/rockchip/rockchip_dlp.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Rockchip DLP (Digital Loopback) driver + * + * Copyright (C) 2022 Rockchip Electronics Co., Ltd + * Author: Sugar Zhang + * + */ + +#ifndef _ROCKCHIP_DLP_H +#define _ROCKCHIP_DLP_H + +struct snd_dlp_config { + int (*get_fifo_count)(struct device *dev, int stream); +}; + +#if IS_REACHABLE(CONFIG_SND_SOC_ROCKCHIP_DLP) +int devm_snd_dmaengine_dlp_register(struct device *dev, + const struct snd_dlp_config *config); +#else +static inline int devm_snd_dmaengine_dlp_register(struct device *dev, + const struct snd_dlp_config *config) +{ + return -ENOSYS; +} +#endif + +#endif