From ff2d7d8e9e4b57caaa80964cfa06843820b2b636 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 | 11 + sound/soc/rockchip/Makefile | 4 + sound/soc/rockchip/rockchip_dlp.c | 1060 +++++++++++++++++++++++++ sound/soc/rockchip/rockchip_dlp.h | 133 ++++ sound/soc/rockchip/rockchip_dlp_pcm.c | 604 ++++++++++++++ sound/soc/rockchip/rockchip_dlp_pcm.h | 28 + 6 files changed, 1840 insertions(+) create mode 100644 sound/soc/rockchip/rockchip_dlp.c create mode 100644 sound/soc/rockchip/rockchip_dlp.h create mode 100644 sound/soc/rockchip/rockchip_dlp_pcm.c create mode 100644 sound/soc/rockchip/rockchip_dlp_pcm.h diff --git a/sound/soc/rockchip/Kconfig b/sound/soc/rockchip/Kconfig index bce211454659e..92889bb12b237 100644 --- a/sound/soc/rockchip/Kconfig +++ b/sound/soc/rockchip/Kconfig @@ -14,6 +14,17 @@ config SND_SOC_ROCKCHIP_AUDIO_PWM Say Y or M if you want to add support for Audio PWM driver for Rockchip Audio PWM Controller. +config SND_SOC_ROCKCHIP_DLP + tristate + +config SND_SOC_ROCKCHIP_DLP_PCM + tristate "Rockchip Digital Loopback PCM Driver" + depends on SND_SOC_ROCKCHIP + select SND_SOC_ROCKCHIP_DLP + 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 4273b55c19327..e1a754a308c74 100644 --- a/sound/soc/rockchip/Makefile +++ b/sound/soc/rockchip/Makefile @@ -1,6 +1,8 @@ # SPDX-License-Identifier: GPL-2.0 # ROCKCHIP Platform Support snd-soc-rockchip-audio-pwm-objs := rockchip_audio_pwm.o +snd-soc-rockchip-dlp-objs := rockchip_dlp.o +snd-soc-rockchip-dlp-pcm-objs := rockchip_dlp_pcm.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 @@ -16,6 +18,8 @@ snd-soc-rockchip-vad-$(CONFIG_ARM) += vad_preprocess_arm.o endif obj-$(CONFIG_SND_SOC_ROCKCHIP_AUDIO_PWM) += snd-soc-rockchip-audio-pwm.o +obj-$(CONFIG_SND_SOC_ROCKCHIP_DLP) += snd-soc-rockchip-dlp.o +obj-$(CONFIG_SND_SOC_ROCKCHIP_DLP_PCM) += snd-soc-rockchip-dlp-pcm.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..ee7fe6f9743c3 --- /dev/null +++ b/sound/soc/rockchip/rockchip_dlp.c @@ -0,0 +1,1060 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 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 "rockchip_dlp.h" + +#define PBUF_CNT 2 + +/* 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", + "4CH: 4 Loopbacks", + "6CH: 4 Mics + 2 Loopbacks", + "6CH: 4 Mics + 1 Loopback-mixed", + "6CH: 6 Loopbacks", + "8CH: 6 Mics + 2 Loopbacks", + "8CH: 6 Mics + 1 Loopback-mixed", + "8CH: 8 Loopbacks", + "10CH: 8 Mics + 2 Loopbacks", + "10CH: 8 Mics + 1 Loopback-mixed", + "16CH: 8 Mics + 8 Loopbacks", +}; + +static inline void drd_buf_free(struct dlp_runtime_data *drd) +{ + if (drd && drd->buf) { + dev_dbg(drd->parent->dev, "%s: stream[%d]: 0x%px\n", + __func__, drd->stream, drd->buf); + kvfree(drd->buf); + drd->buf = NULL; + } +} + +static inline int drd_buf_alloc(struct dlp_runtime_data *drd, int size) +{ + if (drd) { + if (snd_BUG_ON(drd->buf)) + return -EINVAL; + + drd->buf = kvzalloc(size, GFP_KERNEL); + if (!drd->buf) + return -ENOMEM; + dev_dbg(drd->parent->dev, "%s: stream[%d]: 0x%px\n", + __func__, drd->stream, drd->buf); + } + + return 0; +} + +static inline void dlp_activate(struct dlp *dlp) +{ + atomic_inc(&dlp->active); +} + +static inline void dlp_deactivate(struct dlp *dlp) +{ + atomic_dec(&dlp->active); +} + +static inline bool dlp_mode_channels_match(struct 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: + case DLP_MODE_4CH_4LP: + *expected = 4; + return (ch == 4); + case DLP_MODE_6CH_4MIC_2LP: + case DLP_MODE_6CH_4MIC_1LP_MIX: + case DLP_MODE_6CH_6LP: + *expected = 6; + return (ch == 6); + case DLP_MODE_8CH_6MIC_2LP: + case DLP_MODE_8CH_6MIC_1LP_MIX: + case DLP_MODE_8CH_8LP: + *expected = 8; + return (ch == 8); + case DLP_MODE_10CH_8MIC_2LP: + case DLP_MODE_10CH_8MIC_1LP_MIX: + *expected = 10; + return (ch == 10); + case DLP_MODE_16CH_8MIC_8LP: + *expected = 16; + return (ch == 16); + default: + return false; + } +} + +static int dlp_get_offset_size(struct dlp_runtime_data *drd, + enum dlp_mode mode, int *ofs, int *size, bool *mix) +{ + bool is_playback = drd->stream == SNDRV_PCM_STREAM_PLAYBACK; + int ret = 0; + + switch (mode) { + case DLP_MODE_2CH_1LP_1MIC: + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 1); + break; + case DLP_MODE_2CH_1MIC_1LP: + *ofs = dlp_channels_to_bytes(drd, 1); + *size = dlp_channels_to_bytes(drd, 1); + break; + case DLP_MODE_2CH_1MIC_1LP_MIX: + if (is_playback) { + *ofs = 0; + *size = dlp_frames_to_bytes(drd, 1); + if (mix) + *mix = true; + } else { + *ofs = dlp_channels_to_bytes(drd, 1); + *size = dlp_channels_to_bytes(drd, 1); + } + break; + case DLP_MODE_2CH_2LP: + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 2); + break; + case DLP_MODE_4CH_2MIC_2LP: + if (is_playback) { + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 2); + } else { + *ofs = dlp_channels_to_bytes(drd, 2); + *size = dlp_channels_to_bytes(drd, 2); + } + break; + case DLP_MODE_4CH_2MIC_1LP_MIX: + if (is_playback) { + *ofs = 0; + *size = dlp_frames_to_bytes(drd, 1); + if (mix) + *mix = true; + } else { + *ofs = dlp_channels_to_bytes(drd, 2); + *size = dlp_channels_to_bytes(drd, 1); + } + break; + case DLP_MODE_4CH_4LP: + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 4); + break; + case DLP_MODE_6CH_4MIC_2LP: + if (is_playback) { + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 2); + } else { + *ofs = dlp_channels_to_bytes(drd, 4); + *size = dlp_channels_to_bytes(drd, 2); + } + break; + case DLP_MODE_6CH_4MIC_1LP_MIX: + if (is_playback) { + *ofs = 0; + *size = dlp_frames_to_bytes(drd, 1); + if (mix) + *mix = true; + } else { + *ofs = dlp_channels_to_bytes(drd, 4); + *size = dlp_channels_to_bytes(drd, 1); + } + break; + case DLP_MODE_6CH_6LP: + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 6); + break; + case DLP_MODE_8CH_6MIC_2LP: + if (is_playback) { + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 2); + } else { + *ofs = dlp_channels_to_bytes(drd, 6); + *size = dlp_channels_to_bytes(drd, 2); + } + break; + case DLP_MODE_8CH_6MIC_1LP_MIX: + if (is_playback) { + *ofs = 0; + *size = dlp_frames_to_bytes(drd, 1); + if (mix) + *mix = true; + } else { + *ofs = dlp_channels_to_bytes(drd, 6); + *size = dlp_channels_to_bytes(drd, 1); + } + break; + case DLP_MODE_8CH_8LP: + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 8); + break; + case DLP_MODE_10CH_8MIC_2LP: + if (is_playback) { + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 2); + } else { + *ofs = dlp_channels_to_bytes(drd, 8); + *size = dlp_channels_to_bytes(drd, 2); + } + break; + case DLP_MODE_10CH_8MIC_1LP_MIX: + if (is_playback) { + *ofs = 0; + *size = dlp_frames_to_bytes(drd, 1); + if (mix) + *mix = true; + } else { + *ofs = dlp_channels_to_bytes(drd, 8); + *size = dlp_channels_to_bytes(drd, 1); + } + break; + case DLP_MODE_16CH_8MIC_8LP: + if (is_playback) { + *ofs = 0; + *size = dlp_channels_to_bytes(drd, 8); + } else { + *ofs = dlp_channels_to_bytes(drd, 8); + *size = dlp_channels_to_bytes(drd, 8); + } + break; + default: + *ofs = 0; + *size = 0; + if (mix) + *mix = false; + ret = -EINVAL; + } + + return ret; +} + +static int dlp_mix_frame_buffer(struct dlp_runtime_data *drd, void *buf) +{ + int sample_bytes = dlp_channels_to_bytes(drd, 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 < drd->channels; i++) + v16 += (p16[i] / drd->channels); + p16[0] = v16; + break; + case 4: + for (i = 0; i < drd->channels; i++) + v32 += (p32[i] / drd->channels); + p32[0] = v32; + break; + default: + return -EINVAL; + } + + return 0; +} + +static inline int drd_init_from(struct dlp_runtime_data *drd, struct dlp_runtime_data *src) +{ + memset(drd, 0x0, sizeof(*drd)); + + drd->parent = src->parent; + drd->buf_sz = src->buf_sz; + drd->period_sz = src->period_sz; + drd->frame_bytes = src->frame_bytes; + drd->channels = src->channels; + drd->stream = src->stream; + + INIT_LIST_HEAD(&drd->node); + kref_init(&drd->refcount); + + dev_dbg(drd->parent->dev, "%s: drd: 0x%px\n", __func__, drd); + + return 0; +} + +static void drd_avl_list_add(struct dlp *dlp, struct dlp_runtime_data *drd) +{ + unsigned long flags; + + spin_lock_irqsave(&dlp->lock, flags); + list_add(&drd->node, &dlp->drd_avl_list); + dlp->drd_avl_count++; + spin_unlock_irqrestore(&dlp->lock, flags); +} + +static struct dlp_runtime_data *drd_avl_list_get(struct dlp *dlp) +{ + struct dlp_runtime_data *drd = NULL; + unsigned long flags; + + spin_lock_irqsave(&dlp->lock, flags); + if (!list_empty(&dlp->drd_avl_list)) { + drd = list_first_entry(&dlp->drd_avl_list, struct dlp_runtime_data, node); + list_del(&drd->node); + dlp->drd_avl_count--; + } + spin_unlock_irqrestore(&dlp->lock, flags); + + return drd; +} + +static void drd_release(struct kref *ref) +{ + struct dlp_runtime_data *drd = + container_of(ref, struct dlp_runtime_data, refcount); + + dev_dbg(drd->parent->dev, "%s: drd: 0x%px\n", __func__, drd); + + drd_buf_free(drd); + /* move to available list */ + drd_avl_list_add(drd->parent, drd); +} + +static inline struct dlp_runtime_data *drd_get(struct dlp_runtime_data *drd) +{ + if (!drd) + return NULL; + + return kref_get_unless_zero(&drd->refcount) ? drd : NULL; +} + +static inline void drd_put(struct dlp_runtime_data *drd) +{ + if (!drd) + return; + + kref_put(&drd->refcount, drd_release); +} + +static void drd_rdy_list_add(struct dlp *dlp, struct dlp_runtime_data *drd) +{ + unsigned long flags; + + spin_lock_irqsave(&dlp->lock, flags); + list_add(&drd->node, &dlp->drd_rdy_list); + spin_unlock_irqrestore(&dlp->lock, flags); +} + +static struct dlp_runtime_data *drd_rdy_list_get(struct dlp *dlp) +{ + struct dlp_runtime_data *drd = NULL; + unsigned long flags; + + spin_lock_irqsave(&dlp->lock, flags); + if (!list_empty(&dlp->drd_rdy_list)) { + /* the newest one */ + drd = list_first_entry(&dlp->drd_rdy_list, struct dlp_runtime_data, node); + list_del(&drd->node); + } + spin_unlock_irqrestore(&dlp->lock, flags); + + return drd; +} + +static bool drd_rdy_list_found(struct dlp *dlp, struct dlp_runtime_data *drd) +{ + struct dlp_runtime_data *_drd; + unsigned long flags; + bool found = false; + + if (!drd) + return false; + + spin_lock_irqsave(&dlp->lock, flags); + list_for_each_entry(_drd, &dlp->drd_rdy_list, node) { + if (_drd == drd) { + found = true; + break; + } + } + spin_unlock_irqrestore(&dlp->lock, flags); + + return found; +} + +static void drd_rdy_list_free(struct dlp *dlp) +{ + struct list_head drd_list; + struct dlp_runtime_data *drd; + unsigned long flags; + + spin_lock_irqsave(&dlp->lock, flags); + list_replace_init(&dlp->drd_rdy_list, &drd_list); + spin_unlock_irqrestore(&dlp->lock, flags); + + while (!list_empty(&drd_list)) { + drd = list_first_entry(&drd_list, struct dlp_runtime_data, node); + list_del(&drd->node); + drd_put(drd); + } +} + +static void drd_ref_list_add(struct dlp *dlp, struct dlp_runtime_data *drd) +{ + unsigned long flags; + + /* push valid playback into ref list */ + spin_lock_irqsave(&dlp->lock, flags); + list_add_tail(&drd->node, &dlp->drd_ref_list); + spin_unlock_irqrestore(&dlp->lock, flags); +} + +static struct dlp_runtime_data *drd_ref_list_first(struct dlp *dlp) +{ + struct dlp_runtime_data *drd = NULL; + unsigned long flags; + + spin_lock_irqsave(&dlp->lock, flags); + if (!list_empty(&dlp->drd_ref_list)) + drd = list_first_entry(&dlp->drd_ref_list, struct dlp_runtime_data, node); + spin_unlock_irqrestore(&dlp->lock, flags); + + return drd; +} + +static struct dlp_runtime_data *drd_ref_list_del(struct dlp *dlp, + struct dlp_runtime_data *drd) +{ + unsigned long flags; + + spin_lock_irqsave(&dlp->lock, flags); + list_del(&drd->node); + spin_unlock_irqrestore(&dlp->lock, flags); + + return drd; +} + +static void drd_ref_list_free(struct dlp *dlp) +{ + struct list_head drd_list; + struct dlp_runtime_data *drd; + unsigned long flags; + + spin_lock_irqsave(&dlp->lock, flags); + list_replace_init(&dlp->drd_ref_list, &drd_list); + spin_unlock_irqrestore(&dlp->lock, flags); + + while (!list_empty(&drd_list)) { + drd = list_first_entry(&drd_list, struct dlp_runtime_data, node); + list_del(&drd->node); + + if (!atomic_read(&drd->stop)) + drd_rdy_list_add(dlp, drd); + else + drd_put(drd); + } +} + +int dlp_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct dlp *dlp = soc_component_to_dlp(component); + struct dlp_runtime_data *drd = substream_to_drd(substream); + bool is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + int ch_req = params_channels(params), ch_exp = 0; + + if (unlikely(!dlp || !drd)) + return -EINVAL; + + /* 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; + } + + drd->frame_bytes = snd_pcm_format_size(params_format(params), + params_channels(params)); + drd->period_sz = params_period_size(params); + drd->buf_sz = params_buffer_size(params); + drd->channels = params_channels(params); + + if (is_playback) + drd->buf_sz *= PBUF_CNT; + + return 0; +} +EXPORT_SYMBOL_GPL(dlp_hw_params); + +int dlp_open(struct dlp *dlp, struct dlp_runtime_data *drd, + struct snd_pcm_substream *substream) +{ + if (unlikely(!dlp || !drd)) + return -EINVAL; + + drd->parent = dlp; + drd->stream = substream->stream; + + substream->runtime->private_data = drd; + + dlp_activate(dlp); + + return 0; +} +EXPORT_SYMBOL_GPL(dlp_open); + +int dlp_close(struct dlp *dlp, struct dlp_runtime_data *drd, + struct snd_pcm_substream *substream) +{ + if (unlikely(!dlp || !drd)) + return -EINVAL; + + /* + * In case: open -> hw_params -> prepare -> close flow + * should check and free all. + */ + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + drd_put(dlp->drd_pb_shadow); + dlp->drd_pb_shadow = NULL; + } else { + drd_buf_free(drd); + } + + dlp_deactivate(dlp); + + return 0; +} +EXPORT_SYMBOL_GPL(dlp_close); + +void dlp_dma_complete(struct dlp *dlp, struct dlp_runtime_data *drd) +{ + if (unlikely(!dlp || !drd)) + return; + + atomic64_inc(&drd->period_elapsed); +} +EXPORT_SYMBOL_GPL(dlp_dma_complete); + +int dlp_start(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct device *dev, + dma_pointer_f dma_pointer) +{ + struct 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 dlp_runtime_data *adrd = substream_to_drd(substream); + struct dlp_runtime_data *bdrd = substream_to_drd(bsubstream); + struct dlp_runtime_data *drd_ref; + bool is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + uint64_t a = 0, b = 0; + snd_pcm_uframes_t fifo_a = 0, fifo_b = 0; + snd_pcm_sframes_t delta = 0; + + if (unlikely(!dlp || !adrd || !dma_pointer)) + return -EINVAL; + + if (dlp->mode == DLP_MODE_DISABLED) + return -EINVAL; + + fifo_a = dlp->config->get_fifo_count(dev, substream); + a = dma_pointer(substream) % adrd->period_sz; + + if (bsubstream->runtime && snd_pcm_running(bsubstream)) { + if (unlikely(!bdrd)) + return -EINVAL; + + fifo_b = dlp->config->get_fifo_count(dev, bsubstream); + b = dma_pointer(bsubstream) % bdrd->period_sz; + + drd_ref = drd_rdy_list_get(dlp); + if (unlikely(!drd_ref)) { + dev_err(dev, "Failed to get rdy drd\n"); + return -EINVAL; + } + + a += (atomic64_read(&adrd->period_elapsed) * adrd->period_sz); + b += (atomic64_read(&bdrd->period_elapsed) * bdrd->period_sz); + + fifo_a = dlp_bytes_to_frames(adrd, fifo_a * 4); + fifo_b = dlp_bytes_to_frames(bdrd, fifo_b * 4); + + delta = is_playback ? (a - fifo_a) - (b + fifo_b) : (b - fifo_b) - (a + fifo_a); + + drd_ref->hw_ptr_delta = delta; + + drd_ref_list_add(dlp, drd_ref); + } + + if (is_playback) + dev_dbg(dev, "START-P: DMA-P: %llu, DMA-C: %llu, FIFO-P: %lu, FIFO-C: %lu, DELTA: %ld\n", + a, b, fifo_a, fifo_b, delta); + else + dev_dbg(dev, "START-C: DMA-P: %llu, DMA-C: %llu, FIFO-P: %lu, FIFO-C: %lu, DELTA: %ld\n", + b, a, fifo_b, fifo_a, delta); + + return 0; +} +EXPORT_SYMBOL_GPL(dlp_start); + +void dlp_stop(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + dma_pointer_f dma_pointer) +{ + struct dlp *dlp = soc_component_to_dlp(component); + struct dlp_runtime_data *drd = substream_to_drd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + uint64_t appl_ptr, hw_ptr; + + if (unlikely(!dlp || !drd || !runtime || !dma_pointer)) + return; + + 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 = dma_pointer(substream) % drd->period_sz; + hw_ptr += (atomic64_read(&drd->period_elapsed) * drd->period_sz); + + /* + * 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) { + if (dlp->drd_pb_shadow) { + dlp->drd_pb_shadow->hw_ptr = min(hw_ptr, appl_ptr); + atomic_set(&dlp->drd_pb_shadow->stop, 1); + } + drd_rdy_list_free(dlp); + } else { + /* free residue playback ref list for capture when stop */ + drd_ref_list_free(dlp); + } + + atomic64_set(&drd->period_elapsed, 0); + + dev_dbg(dlp->dev, "STOP-%s: applptr: %llu, hwptr: %llu\n", + substream->stream ? "C" : "P", appl_ptr, hw_ptr); +} +EXPORT_SYMBOL_GPL(dlp_stop); + +static int process_capture(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + unsigned long hwoff, + void __user *buf, unsigned long bytes) +{ + struct dlp *dlp = soc_component_to_dlp(component); + struct snd_pcm_runtime *runtime = substream->runtime; + struct dlp_runtime_data *drd = substream_to_drd(substream); + struct dlp_runtime_data *drd_ref = NULL; + snd_pcm_sframes_t frames = 0; + 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; + int ofs_cap, ofs_play, size_cap, size_play; + int i = 0, j = 0, ret = 0; + bool free_ref = false, mix = false; + char *cbuf = NULL, *pbuf = NULL; + void *dma_ptr; + + if (unlikely(!drd || !runtime || !buf)) + return -EINVAL; + + frames = dlp_bytes_to_frames(drd, bytes); + dma_ptr = runtime->dma_area + hwoff; + cbuf = drd->buf; + + 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(drd, 1)); +#endif + ret = dlp_get_offset_size(drd, dlp->mode, &ofs_cap, &size_cap, NULL); + if (ret) { + dev_err(dlp->dev, "Failed to get dlp cap offset\n"); + return -EINVAL; + } + + /* clear channel-LP_CHN */ + for (i = 0; i < frames; i++) { + cbuf = drd->buf + dlp_frames_to_bytes(drd, i) + ofs_cap; + memset(cbuf, 0x0, size_cap); + } + +start: + drd_ref = drd_get(drd_ref_list_first(dlp)); + if (!drd_ref) + return 0; + + ret = dlp_get_offset_size(drd_ref, dlp->mode, &ofs_play, &size_play, &mix); + if (ret) { + dev_err(dlp->dev, "Failed to get dlp play offset\n"); + goto _drd_put; + } + + ofs = appl_ptr + drd_ref->hw_ptr_delta; + + /* + * if playback stop, process the data tail and then + * free drd_ref if data consumed. + */ + if (atomic_read(&drd_ref->stop)) { + if (ofs >= drd_ref->hw_ptr) { + drd_put(drd_ref_list_del(dlp, drd_ref)); + goto _drd_put; + } else if ((ofs + frames) > drd_ref->hw_ptr) { + dev_dbg(dlp->dev, "applptr: %8lu, ofs': %7ld, refhwptr: %lld, frames: %ld (*)\n", + appl_ptr, ofs, drd_ref->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 = drd_ref->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) + goto _drd_put; + + /* skip if ofs < 0 and fixup ofs */ + j = 0; + if (ofs < 0) { + dev_dbg(dlp->dev, "applptr: %8lu, ofs: %8ld, frames: %ld (*)\n", + appl_ptr, ofs, frames); + j = -ofs; + frames += ofs; + ofs = 0; + appl_ptr += j; + } + + ofs %= drd_ref->buf_sz; + + dev_dbg(dlp->dev, "applptr: %8lu, ofs: %8ld, frames: %5ld, refc: %u\n", + appl_ptr, ofs, frames, kref_read(&drd_ref->refcount)); + + for (i = 0; i < frames; i++, j++) { + cbuf = drd->buf + dlp_frames_to_bytes(drd, j + frames_consumed) + ofs_cap; + pbuf = drd_ref->buf + dlp_frames_to_bytes(drd_ref, ((i + ofs) % drd_ref->buf_sz)) + ofs_play; + if (mix) + dlp_mix_frame_buffer(drd_ref, pbuf); + memcpy(cbuf, pbuf, size_cap); + } + + appl_ptr += frames; + frames_consumed += frames; + + if (free_ref) { + drd_put(drd_ref_list_del(dlp, drd_ref)); + drd_put(drd_ref); + drd_ref = NULL; + free_ref = false; + if (frames_residue) { + frames = frames_residue; + frames_residue = 0; + goto start; + } + } + +_drd_put: + drd_put(drd_ref); + drd_ref = NULL; + + 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 dlp *dlp = soc_component_to_dlp(component); + struct dlp_runtime_data *drd; + char *pbuf; + int ret = 0; + + drd = drd_get(dlp->drd_pb_shadow); + if (!drd) + return 0; + + pbuf = drd->buf + drd->buf_ofs; + + if (copy_from_user(pbuf, buf, bytes)) { + ret = -EFAULT; + goto err_put; + } + + drd->buf_ofs += bytes; + drd->buf_ofs %= dlp_frames_to_bytes(drd, drd->buf_sz); + +err_put: + drd_put(drd); + + return ret; +} + +static int dlp_process(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + unsigned long hwoff, + void __user *buf, unsigned long bytes) +{ + struct 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; +} + +int dlp_copy_user(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + int channel, unsigned long hwoff, + void __user *buf, unsigned long bytes) +{ + struct dlp_runtime_data *drd = substream_to_drd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + bool is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + void *dma_ptr; + int ret; + + if (unlikely(!drd || !runtime || !buf)) + return -EINVAL; + + dma_ptr = runtime->dma_area + hwoff + + channel * (runtime->dma_bytes / runtime->channels); + + if (is_playback) + if (copy_from_user(dma_ptr, buf, bytes)) + return -EFAULT; + + ret = dlp_process(component, substream, hwoff, buf, bytes); + if (!ret) + dma_ptr = drd->buf; + + if (!is_playback) + if (copy_to_user(buf, dma_ptr, bytes)) + return -EFAULT; + + return 0; +} +EXPORT_SYMBOL_GPL(dlp_copy_user); + +static SOC_ENUM_SINGLE_EXT_DECL(dlp_mode, dlp_text); + +static int dlp_mode_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_soc_kcontrol_component(kcontrol); + struct dlp *dlp = soc_component_to_dlp(component); + + ucontrol->value.enumerated.item[0] = dlp->mode; + + return 0; +} + +static int dlp_mode_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *component = snd_soc_kcontrol_component(kcontrol); + struct 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 (atomic_read(&dlp->active)) { + dev_err(dlp->dev, "Should set this mode before pcm open\n"); + return -EPERM; + } + + if (mode == dlp->mode) + return 0; + + dlp->mode = mode; + + return 1; +} + +static const struct snd_kcontrol_new dlp_controls[] = { + SOC_ENUM_EXT("Software Digital Loopback Mode", dlp_mode, + dlp_mode_get, dlp_mode_put), +}; + +int dlp_prepare(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct dlp *dlp = soc_component_to_dlp(component); + struct dlp_runtime_data *drd = substream_to_drd(substream); + struct dlp_runtime_data *drd_new = NULL; + int buf_bytes, last_buf_bytes; + int ret; + + if (unlikely(!dlp || !drd)) + return -EINVAL; + + if (dlp->mode == DLP_MODE_DISABLED) + return 0; + + buf_bytes = dlp_frames_to_bytes(drd, drd->buf_sz); + last_buf_bytes = dlp_frames_to_bytes(drd, drd->last_buf_sz); + + if (substream->runtime->status->state == SNDRV_PCM_STATE_XRUN) + dev_dbg(dlp->dev, "stream[%d]: prepare from XRUN\n", + substream->stream); + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + dev_dbg(dlp->dev, "avl count: %d\n", dlp->drd_avl_count); + if (snd_BUG_ON(!dlp->drd_avl_count)) + return -EINVAL; + + /* + * There might be multiple calls hw_params -> prepare + * before start stream, so, should check buf size status + * to determine whether to re-create buf or do nothing. + */ + if (drd_rdy_list_found(dlp, dlp->drd_pb_shadow)) { + if (buf_bytes == last_buf_bytes) + return 0; + + drd_rdy_list_free(dlp); + } + + /* release the old one, re-create for new params */ + drd_put(dlp->drd_pb_shadow); + dlp->drd_pb_shadow = NULL; + + drd_new = drd_avl_list_get(dlp); + if (!drd_new) + return -ENOMEM; + + drd_init_from(drd_new, drd); + + ret = drd_buf_alloc(drd_new, buf_bytes); + if (ret) + return -ENOMEM; + + if (snd_BUG_ON(!drd_get(drd_new))) + return -EINVAL; + + drd_rdy_list_add(dlp, drd_new); + + dlp->drd_pb_shadow = drd_new; + } else { + /* + * There might be multiple calls hw_params -> prepare + * before start stream, so, should check buf size status + * to determine whether to re-create buf or do nothing. + */ + if (drd->buf && buf_bytes == last_buf_bytes) + return 0; + + drd_buf_free(drd); + + ret = drd_buf_alloc(drd, buf_bytes); + if (ret) + return ret; + } + + /* update last after all done success */ + drd->last_buf_sz = drd->buf_sz; + + return 0; +} +EXPORT_SYMBOL_GPL(dlp_prepare); + +int dlp_probe(struct snd_soc_component *component) +{ + snd_soc_add_component_controls(component, dlp_controls, + ARRAY_SIZE(dlp_controls)); + return 0; +} +EXPORT_SYMBOL_GPL(dlp_probe); + +int dlp_register(struct dlp *dlp, struct device *dev, + const struct snd_soc_component_driver *driver, + const struct snd_dlp_config *config) +{ + struct dlp_runtime_data *drd; + int ret = 0, i = 0; + + if (unlikely(!dlp || !dev || !driver || !config)) + return -EINVAL; + + dlp->dev = dev; + dlp->config = config; + +#ifdef CONFIG_DEBUG_FS + dlp->component.debugfs_prefix = "dma"; +#endif + INIT_LIST_HEAD(&dlp->drd_avl_list); + INIT_LIST_HEAD(&dlp->drd_rdy_list); + INIT_LIST_HEAD(&dlp->drd_ref_list); + + dlp->drd_avl_count = ARRAY_SIZE(dlp->drds); + + for (i = 0; i < dlp->drd_avl_count; i++) { + drd = &dlp->drds[i]; + list_add_tail(&drd->node, &dlp->drd_avl_list); + } + + spin_lock_init(&dlp->lock); + atomic_set(&dlp->active, 0); + + ret = snd_soc_add_component(dev, &dlp->component, driver, NULL, 0); + + return ret; +} +EXPORT_SYMBOL_GPL(dlp_register); + +MODULE_DESCRIPTION("Rockchip Digital Loopback Core Driver"); +MODULE_AUTHOR("Sugar Zhang "); +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..507bb9e604f66 --- /dev/null +++ b/sound/soc/rockchip/rockchip_dlp.h @@ -0,0 +1,133 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Rockchip DLP (Digital Loopback) driver + * + * Copyright (C) 2022 Rockchip Electronics Co., Ltd + * Author: Sugar Zhang + * + */ + +#ifndef _ROCKCHIP_DLP_H +#define _ROCKCHIP_DLP_H + +#include "rockchip_dlp_pcm.h" + +#define DLP_MAX_DRDS 8 + +/* MUST: enum dlp_mode should be match to dlp_text */ +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-0~1 with play-ch-0~1 */ + DLP_MODE_4CH_2MIC_2LP, /* replace cap-ch-2~3 with play-ch-0~1 */ + DLP_MODE_4CH_2MIC_1LP_MIX, /* replace cap-ch-3 with play-ch-all-mix */ + DLP_MODE_4CH_4LP, /* replace cap-ch-0~3 with play-ch-0~3 */ + DLP_MODE_6CH_4MIC_2LP, /* replace cap-ch-4~5 with play-ch-0~1 */ + DLP_MODE_6CH_4MIC_1LP_MIX, /* replace cap-ch-4 with play-ch-all-mix */ + DLP_MODE_6CH_6LP, /* replace cap-ch-0~5 with play-ch-0~5 */ + DLP_MODE_8CH_6MIC_2LP, /* replace cap-ch-6~7 with play-ch-0~1 */ + DLP_MODE_8CH_6MIC_1LP_MIX, /* replace cap-ch-6 with play-ch-all-mix */ + DLP_MODE_8CH_8LP, /* replace cap-ch-0~7 with play-ch-0~7 */ + DLP_MODE_10CH_8MIC_2LP, /* replace cap-ch-8~9 with play-ch-0~1 */ + DLP_MODE_10CH_8MIC_1LP_MIX, /* replace cap-ch-8 with play-ch-all-mix */ + DLP_MODE_16CH_8MIC_8LP, /* replace cap-ch-8~f with play-ch-8~f */ +}; + +struct dlp; + +struct dlp_runtime_data { + struct dlp *parent; + struct kref refcount; + struct list_head node; + char *buf; + snd_pcm_uframes_t buf_sz; + snd_pcm_uframes_t last_buf_sz; + snd_pcm_uframes_t period_sz; + int64_t hw_ptr; + int64_t hw_ptr_delta; /* play-ptr - cap-ptr */ + atomic64_t period_elapsed; + atomic_t stop; + unsigned int frame_bytes; + unsigned int channels; + unsigned int buf_ofs; + int stream; +}; + +struct dlp { + struct device *dev; + struct list_head drd_avl_list; + struct list_head drd_rdy_list; + struct list_head drd_ref_list; + struct dlp_runtime_data drds[DLP_MAX_DRDS]; + struct dlp_runtime_data *drd_pb_shadow; + struct snd_soc_component component; + const struct snd_dlp_config *config; + enum dlp_mode mode; + int drd_avl_count; + atomic_t active; + spinlock_t lock; +}; + +typedef snd_pcm_uframes_t (*dma_pointer_f)(struct snd_pcm_substream *substream); + +static inline struct dlp *soc_component_to_dlp(struct snd_soc_component *p) +{ + return container_of(p, struct dlp, component); +} + +static inline struct dlp_runtime_data *substream_to_drd( + const struct snd_pcm_substream *substream) +{ + if (!substream->runtime) + return NULL; + + return substream->runtime->private_data; +} + +static inline ssize_t dlp_channels_to_bytes(struct dlp_runtime_data *drd, + int channels) +{ + return (drd->frame_bytes / drd->channels) * channels; +} + +static inline ssize_t dlp_frames_to_bytes(struct dlp_runtime_data *drd, + snd_pcm_sframes_t size) +{ + return size * drd->frame_bytes; +} + +static inline snd_pcm_sframes_t dlp_bytes_to_frames(struct dlp_runtime_data *drd, + ssize_t size) +{ + return size / drd->frame_bytes; +} + +void dlp_dma_complete(struct dlp *dlp, struct dlp_runtime_data *drd); +int dlp_open(struct dlp *dlp, struct dlp_runtime_data *drd, + struct snd_pcm_substream *substream); +int dlp_close(struct dlp *dlp, struct dlp_runtime_data *drd, + struct snd_pcm_substream *substream); +int dlp_hw_params(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params); +int dlp_start(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct device *dev, + dma_pointer_f dma_pointer); +void dlp_stop(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + dma_pointer_f dma_pointer); +int dlp_copy_user(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + int channel, unsigned long hwoff, + void __user *buf, unsigned long bytes); +int dlp_prepare(struct snd_soc_component *component, + struct snd_pcm_substream *substream); +int dlp_probe(struct snd_soc_component *component); +int dlp_register(struct dlp *dlp, struct device *dev, + const struct snd_soc_component_driver *driver, + const struct snd_dlp_config *config); + +#endif diff --git a/sound/soc/rockchip/rockchip_dlp_pcm.c b/sound/soc/rockchip/rockchip_dlp_pcm.c new file mode 100644 index 0000000000000..e68c0dccaf1ac --- /dev/null +++ b/sound/soc/rockchip/rockchip_dlp_pcm.c @@ -0,0 +1,604 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 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 "rockchip_dlp.h" + +#define SND_DMAENGINE_DLP_DRV_NAME "snd_dmaengine_dlp" + +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)."); + +struct dmaengine_dlp { + struct dlp dlp; + struct dma_chan *chan[SNDRV_PCM_STREAM_LAST + 1]; +}; + +struct dmaengine_dlp_runtime_data { + struct dlp_runtime_data drd; + struct dma_chan *dma_chan; + dma_cookie_t cookie; +}; + +static inline struct dmaengine_dlp *soc_component_to_ddlp(struct snd_soc_component *p) +{ + return container_of(soc_component_to_dlp(p), struct dmaengine_dlp, dlp); +} + +static inline struct dmaengine_dlp_runtime_data *substream_to_ddrd( + const struct snd_pcm_substream *substream) +{ + struct dlp_runtime_data *drd = substream_to_drd(substream); + + if (!drd) + return NULL; + + return container_of(drd, struct dmaengine_dlp_runtime_data, drd); +} + +static struct dma_chan *snd_dmaengine_dlp_get_chan(struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp_runtime_data *ddrd = substream_to_ddrd(substream); + + return ddrd ? ddrd->dma_chan : NULL; +} + +static struct device *dmaengine_dma_dev(struct dmaengine_dlp *ddlp, + struct snd_pcm_substream *substream) +{ + if (!ddlp->chan[substream->stream]) + return NULL; + + return ddlp->chan[substream->stream]->device->dev; +} + +static int dmaengine_dlp_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + struct dma_chan *chan = snd_dmaengine_dlp_get_chan(substream); + struct dma_slave_config slave_config; + int ret; + + 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; + + ret = dlp_hw_params(component, substream, params); + if (ret) + return ret; + + return snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(params)); +} + +static int +dmaengine_pcm_set_runtime_hwparams(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + struct dmaengine_dlp *ddlp = soc_component_to_ddlp(component); + struct device *dma_dev = dmaengine_dma_dev(ddlp, substream); + struct dma_chan *chan = ddlp->chan[substream->stream]; + struct snd_dmaengine_dai_dma_data *dma_data; + struct dma_slave_caps dma_caps; + struct snd_pcm_hardware hw; + u32 addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) | + BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) | + BIT(DMA_SLAVE_BUSWIDTH_4_BYTES); + snd_pcm_format_t i; + int ret; + + dma_data = snd_soc_dai_get_dma_data(rtd->cpu_dai, 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; + + ret = dma_get_slave_caps(chan, &dma_caps); + if (ret == 0) { + if (dma_caps.cmd_pause && dma_caps.cmd_resume) + hw.info |= SNDRV_PCM_INFO_PAUSE | SNDRV_PCM_INFO_RESUME; + if (dma_caps.residue_granularity <= DMA_RESIDUE_GRANULARITY_SEGMENT) + hw.info |= SNDRV_PCM_INFO_BATCH; + + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) + addr_widths = dma_caps.dst_addr_widths; + else + addr_widths = dma_caps.src_addr_widths; + } + + /* + * If SND_DMAENGINE_PCM_DAI_FLAG_PACK is set keep + * hw.formats set to 0, meaning no restrictions are in place. + * In this case it's the responsibility of the DAI driver to + * provide the supported format information. + */ + if (!(dma_data->flags & SND_DMAENGINE_PCM_DAI_FLAG_PACK)) + /* + * Prepare formats mask for valid/allowed sample types. If the + * dma does not have support for the given physical word size, + * it needs to be masked out so user space can not use the + * format which produces corrupted audio. + * In case the dma driver does not implement the slave_caps the + * default assumption is that it supports 1, 2 and 4 bytes + * widths. + */ + for (i = SNDRV_PCM_FORMAT_FIRST; i <= SNDRV_PCM_FORMAT_LAST; i++) { + int bits = snd_pcm_format_physical_width(i); + + /* + * Enable only samples with DMA supported physical + * widths + */ + switch (bits) { + case 8: + case 16: + case 24: + case 32: + case 64: + if (addr_widths & (1 << (bits / 8))) + hw.formats |= pcm_format_to_bits(i); + break; + default: + /* Unsupported types */ + break; + } + } + + return snd_soc_set_runtime_hwparams(substream, &hw); +} + +static int dmaengine_dlp_open(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + struct dmaengine_dlp *ddlp = soc_component_to_ddlp(component); + struct dma_chan *chan = ddlp->chan[substream->stream]; + struct dmaengine_dlp_runtime_data *ddrd; + int ret; + + if (!chan) + return -ENXIO; + + ret = dmaengine_pcm_set_runtime_hwparams(substream); + if (ret) + return ret; + + ret = snd_pcm_hw_constraint_integer(substream->runtime, + SNDRV_PCM_HW_PARAM_PERIODS); + if (ret < 0) + return ret; + + ddrd = kzalloc(sizeof(*ddrd), GFP_KERNEL); + if (!ddrd) + return -ENOMEM; + + ddrd->dma_chan = chan; + + dlp_open(&ddlp->dlp, &ddrd->drd, substream); + + return 0; +} + +static int dmaengine_dlp_close(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + struct dmaengine_dlp *ddlp = soc_component_to_ddlp(component); + struct dmaengine_dlp_runtime_data *ddrd = substream_to_ddrd(substream); + + if (unlikely(!ddlp || !ddrd)) + return -EINVAL; + + dmaengine_synchronize(ddrd->dma_chan); + + dlp_close(&ddlp->dlp, &ddrd->drd, substream); + + kfree(ddrd); + + return 0; +} + +static snd_pcm_uframes_t dmaengine_dlp_pointer(struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp_runtime_data *ddrd = substream_to_ddrd(substream); + struct dma_tx_state state; + unsigned int buf_size; + unsigned int pos = 0; + + if (unlikely(!ddrd)) + return 0; + + dmaengine_tx_status(ddrd->dma_chan, ddrd->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(&ddrd->drd, pos); +} + +static void dmaengine_dlp_dma_complete(void *arg) +{ + struct snd_pcm_substream *substream = arg; + struct dlp_runtime_data *drd; + struct dlp *dlp; + + snd_pcm_stream_lock_irq(substream); + if (!substream->runtime) { + snd_pcm_stream_unlock_irq(substream); + return; + } + + drd = substream_to_drd(substream); + dlp = drd->parent; + + dlp_dma_complete(dlp, drd); + snd_pcm_stream_unlock_irq(substream); + + snd_pcm_period_elapsed(substream); +} + +static int dmaengine_dlp_prepare_and_submit(struct snd_pcm_substream *substream) +{ + struct dmaengine_dlp_runtime_data *ddrd = substream_to_ddrd(substream); + struct dma_chan *chan; + struct dma_async_tx_descriptor *desc; + enum dma_transfer_direction direction; + unsigned long flags = DMA_CTRL_ACK; + + if (unlikely(!ddrd)) + return -EINVAL; + + chan = ddrd->dma_chan; + 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; + ddrd->cookie = dmaengine_submit(desc); + + return 0; +} + +static int dmaengine_dlp_start(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + struct dlp *dlp = soc_component_to_dlp(component); + + return dlp_start(component, substream, dlp->dev, dmaengine_dlp_pointer); +} + +static void dmaengine_dlp_stop(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + + dlp_stop(component, substream, dmaengine_dlp_pointer); +} + +static int dmaengine_dlp_trigger(struct snd_pcm_substream *substream, int cmd) +{ + struct dmaengine_dlp_runtime_data *ddrd = substream_to_ddrd(substream); + struct snd_pcm_runtime *runtime = substream->runtime; + int ret; + + if (unlikely(!ddrd)) + return -EINVAL; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + ret = dmaengine_dlp_prepare_and_submit(substream); + if (ret) + return ret; + dma_async_issue_pending(ddrd->dma_chan); + dmaengine_dlp_start(substream); + break; + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + dmaengine_resume(ddrd->dma_chan); + break; + case SNDRV_PCM_TRIGGER_SUSPEND: + if (runtime->info & SNDRV_PCM_INFO_PAUSE) { + dmaengine_pause(ddrd->dma_chan); + } else { + dmaengine_dlp_stop(substream); + dmaengine_terminate_async(ddrd->dma_chan); + } + break; + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + dmaengine_pause(ddrd->dma_chan); + break; + case SNDRV_PCM_TRIGGER_STOP: + dmaengine_dlp_stop(substream); + dmaengine_terminate_async(ddrd->dma_chan); + break; + default: + return -EINVAL; + } + + return 0; +} + +static int dmaengine_dlp_new(struct snd_soc_pcm_runtime *rtd) +{ + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + struct dmaengine_dlp *ddlp = soc_component_to_ddlp(component); + struct snd_pcm_substream *substream; + size_t prealloc_buffer_size; + size_t max_buffer_size; + unsigned int i; + int ret; + + prealloc_buffer_size = prealloc_buffer_size_kbytes * 1024; + max_buffer_size = SIZE_MAX; + + for (i = SNDRV_PCM_STREAM_PLAYBACK; i <= SNDRV_PCM_STREAM_CAPTURE; i++) { + substream = rtd->pcm->streams[i].substream; + if (!substream) + continue; + + if (!ddlp->chan[i]) { + dev_err(component->dev, + "Missing dma channel for stream: %d\n", i); + return -EINVAL; + } + + ret = snd_pcm_lib_preallocate_pages(substream, + SNDRV_DMA_TYPE_DEV_IRAM, + dmaengine_dma_dev(ddlp, substream), + prealloc_buffer_size, + max_buffer_size); + if (ret) + return ret; + + 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 int dmaengine_dlp_copy_user(struct snd_pcm_substream *substream, + int channel, unsigned long hwoff, + void __user *buf, unsigned long bytes) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + + return dlp_copy_user(component, substream, channel, hwoff, buf, bytes); +} + +static int dmaengine_dlp_prepare(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_component *component = + snd_soc_rtdcom_lookup(rtd, SND_DMAENGINE_DLP_DRV_NAME); + + return dlp_prepare(component, substream); +} + +static int dmaengine_dlp_ioctl(struct snd_pcm_substream *substream, + unsigned int cmd, void *arg) +{ + return snd_pcm_lib_ioctl(substream, cmd, arg); +} + +static int dmaengine_dlp_hw_free(struct snd_pcm_substream *substream) +{ + return snd_pcm_lib_free_pages(substream); +} + +static int dmaengine_dlp_probe(struct snd_soc_component *component) +{ + return dlp_probe(component); +} + +static const struct snd_pcm_ops dmaengine_dlp_ops = { + .open = dmaengine_dlp_open, + .close = dmaengine_dlp_close, + .ioctl = dmaengine_dlp_ioctl, + .hw_params = dmaengine_dlp_hw_params, + .hw_free = dmaengine_dlp_hw_free, + .prepare = dmaengine_dlp_prepare, + .trigger = dmaengine_dlp_trigger, + .pointer = dmaengine_dlp_pointer, + .copy_user = dmaengine_dlp_copy_user, +}; + +static const struct snd_soc_component_driver dmaengine_dlp_component = { + .name = SND_DMAENGINE_DLP_DRV_NAME, + .probe_order = SND_SOC_COMP_ORDER_LATE, + .probe = dmaengine_dlp_probe, + .ops = &dmaengine_dlp_ops, + .pcm_new = dmaengine_dlp_new, +}; + +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 *ddlp, + struct device *dev, const struct snd_dmaengine_pcm_config *config) +{ + unsigned int i; + const char *name; + struct dma_chan *chan; + + for (i = SNDRV_PCM_STREAM_PLAYBACK; i <= SNDRV_PCM_STREAM_CAPTURE; 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; + ddlp->chan[i] = NULL; + } else { + ddlp->chan[i] = chan; + } + } + + return 0; +} + +static void dmaengine_pcm_release_chan(struct dmaengine_dlp *ddlp) +{ + unsigned int i; + + for (i = SNDRV_PCM_STREAM_PLAYBACK; i <= SNDRV_PCM_STREAM_CAPTURE; i++) { + if (!ddlp->chan[i]) + continue; + dma_release_channel(ddlp->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) +{ + struct dmaengine_dlp *ddlp; + int ret; + + ddlp = kzalloc(sizeof(*ddlp), GFP_KERNEL); + if (!ddlp) + return -ENOMEM; + + ret = dmaengine_pcm_request_chan_of(ddlp, dev, NULL); + if (ret) + goto err_free_dma; + + ret = dlp_register(&ddlp->dlp, dev, &dmaengine_dlp_component, config); + if (ret) + goto err_free_dma; + + return 0; + +err_free_dma: + dmaengine_pcm_release_chan(ddlp); + kfree(ddlp); + 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_dlp_register. + */ +static void snd_dmaengine_dlp_unregister(struct device *dev) +{ + struct snd_soc_component *component; + struct dmaengine_dlp *ddlp; + + component = snd_soc_lookup_component(dev, SND_DMAENGINE_DLP_DRV_NAME); + if (!component) + return; + + ddlp = soc_component_to_ddlp(component); + + snd_soc_unregister_component(dev); + dmaengine_pcm_release_chan(ddlp); + + kfree(ddlp); +} + +static void devm_dmaengine_dlp_stop(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_stop, 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_DESCRIPTION("Rockchip Digital Loopback PCM Driver"); +MODULE_AUTHOR("Sugar Zhang "); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/rockchip/rockchip_dlp_pcm.h b/sound/soc/rockchip/rockchip_dlp_pcm.h new file mode 100644 index 0000000000000..3fa80bae8f467 --- /dev/null +++ b/sound/soc/rockchip/rockchip_dlp_pcm.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Rockchip DLP (Digital Loopback) PCM driver + * + * Copyright (C) 2022 Rockchip Electronics Co., Ltd + * Author: Sugar Zhang + * + */ + +#ifndef _ROCKCHIP_DLP_PCM_H +#define _ROCKCHIP_DLP_PCM_H + +struct snd_dlp_config { + int (*get_fifo_count)(struct device *dev, struct snd_pcm_substream *substream); +}; + +#if IS_REACHABLE(CONFIG_SND_SOC_ROCKCHIP_DLP_PCM) +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