From 9975bc50f3258f87b851f0c37715aca020124e08 Mon Sep 17 00:00:00 2001 From: Sugar Zhang Date: Mon, 4 Jul 2022 18:49:47 +0800 Subject: [PATCH] ASoC: rockchip: Add support for Digital Loopback This patch add support for DMA-based digital loopback. BACKGROUND Audio Products with AEC require loopback for echo cancellation. the hardware LP is not always available on some products, maybe the HW limitation(such as internal acodec) or HW Cost-down. This patch add support software DLP for such products. Enable: CONFIG_SND_SOC_ROCKCHIP_DLP &i2s { rockchip,digital-loopback; }; Mode List: amixer contents numid=2,iface=MIXER,name='Software Digital Loopback Mode' ; type=ENUMERATED,access=rw------,values=1,items=7 ; Item #0 'Disabled' ; Item #1 '2CH: 1 Loopback + 1 Mic' ; Item #2 '2CH: 1 Mic + 1 Loopback' ; Item #3 '2CH: 1 Mic + 1 Loopback-mixed' ; Item #4 '2CH: 2 Loopbacks' ; Item #5 '4CH: 2 Mics + 2 Loopbacks' ; Item #6 '4CH: 2 Mics + 1 Loopback-mixed' : values=0 Testenv: wired SDO0 --> SDI0 directly to get external digital loopback as reference. Testcase: dlp.sh /#!/bin/sh item=0 id=`amixer contents | grep "Software Digital Loopback" | \ awk -F ',' '{print $1}'` items=`amixer contents | grep -A 1 "Software Digital Loopback" | \ grep items | awk -F 'items=' '{print $2}'` echo "Software Digital Loopback: $id, items: $items" mode_chs() { case $1 in [0-4]) echo "2" ;; [5-6]) echo "4" ;; *) echo "2" ;; esac } while true do ch=`mode_chs $item` amixer -c 0 cset $id $item arecord -D hw:0,0 --period-size=1024 --buffer-size=4096 -r 48000 -c $ch -f s16_le \ -d 15 sine/dlp_$item.wav & sleep 2 for i in $(seq 1 10) do aplay -D hw:0,0 --period-size=1024 --buffer-size=8192 $((ch))ch.wav -d 1 done pid=$(ps | egrep "aplay|arecord" | grep -v grep | awk '{print $1}' | sort -r) for p in $pid do wait $p 2>/dev/null done item=$((item+1)) if [ $item -ge $items ]; then sleep 1 break fi done echo "Done" Result: do shell test and verify dlp_x.wav: * Alignment: ~1 samples shift (loopback <-> mics). * Integrity: no giltch, no data lost. * AEC: align loopback and mics sample and do simple AEC, get clean waveform. Logs: ... numid=2,iface=MIXER,name='Software Digital Loopback Mode' ; type=ENUMERATED,access=rw------,values=1,items=7 ; Item #0 'Disabled' ; Item #1 '2CH: 1 Loopback + 1 Mic' ; Item #2 '2CH: 1 Mic + 1 Loopback' ; Item #3 '2CH: 1 Mic + 1 Loopback-mixed' ; Item #4 '2CH: 2 Loopbacks' ; Item #5 '4CH: 2 Mics + 2 Loopbacks' ; Item #6 '4CH: 2 Mics + 1 Loopback-mixed' : values=2 Recording WAVE 'sine/dlp_2.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo Playing WAVE '2ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo ... numid=2,iface=MIXER,name='Software Digital Loopback Mode' ; type=ENUMERATED,access=rw------,values=1,items=7 ; Item #0 'Disabled' ; Item #1 '2CH: 1 Loopback + 1 Mic' ; Item #2 '2CH: 1 Mic + 1 Loopback' ; Item #3 '2CH: 1 Mic + 1 Loopback-mixed' ; Item #4 '2CH: 2 Loopbacks' ; Item #5 '4CH: 2 Mics + 2 Loopbacks' ; Item #6 '4CH: 2 Mics + 1 Loopback-mixed' : values=6 Recording WAVE 'sine/dlp_6.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Playing WAVE '4ch.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Channels 4 Done Signed-off-by: Sugar Zhang Change-Id: I5772f0694f7a14a0f0bd1f0777b6c4cdbd781a64 --- sound/soc/rockchip/Kconfig | 7 + sound/soc/rockchip/Makefile | 2 + sound/soc/rockchip/rockchip_dlp.c | 1079 +++++++++++++++++++++++++++++ sound/soc/rockchip/rockchip_dlp.h | 28 + 4 files changed, 1116 insertions(+) create mode 100644 sound/soc/rockchip/rockchip_dlp.c create mode 100644 sound/soc/rockchip/rockchip_dlp.h 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