blob: 89df53d12706b1351d1fb06f0428a6c6f0a408e7 [file] [log] [blame]
/*
* sound/soc/amlogic/meson/pcm.c
*
* Copyright (C) 2017 Amlogic, Inc. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
*/
#undef pr_fmt
#define pr_fmt(fmt) "snd_pcm: " fmt
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/platform_device.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/dma-mapping.h>
#include <linux/soundcard.h>
#include <linux/timer.h>
#include <linux/debugfs.h>
#include <linux/major.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/initval.h>
#include <sound/control.h>
#include <sound/soc.h>
#include <sound/pcm_params.h>
#include <linux/amlogic/media/sound/audin_regs.h>
#include "pcm.h"
#include "audio_hw_pcm.h"
/*--------------------------------------------------------------------------
* Hardware definition
*--------------------------------------------------------------------------
* TODO: These values were taken from the AML platform driver, check
* them against real values for AML
*/
static const struct snd_pcm_hardware aml_pcm_hardware = {
.info = SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_PAUSE,
.formats =
SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
.period_bytes_min = 32,
.period_bytes_max = 32 * 1024 * 2,
.periods_min = 2,
.periods_max = 1024,
.buffer_bytes_max = 512 * 1024,
.rate_min = 8000,
.rate_max = 192000,
.channels_min = 1,
.channels_max = 16,
};
static const struct snd_pcm_hardware aml_pcm_capture = {
.info = SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_MMAP_VALID |
SNDRV_PCM_INFO_PAUSE,
.formats =
SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
.period_bytes_min = 64,
.period_bytes_max = 32 * 1024,
.periods_min = 2,
.periods_max = 1024,
.buffer_bytes_max = 512 * 1024,
.rate_min = 8000,
.rate_max = 192000,
.channels_min = 1,
.channels_max = 16,
.fifo_size = 0,
};
static unsigned int period_sizes[] = {
64, 128, 256, 512, 1024, 2048, 4096,
8192, 16384, 32768, 65536, 65536 * 2, 65536 * 4
};
static struct snd_pcm_hw_constraint_list hw_constraints_period_sizes = {
.count = ARRAY_SIZE(period_sizes),
.list = period_sizes,
.mask = 0
};
static unsigned int aml_pcm_offset_tx(struct aml_pcm_runtime_data *prtd)
{
unsigned int value = 0;
signed int diff = 0;
value = pcm_out_rd_ptr();
diff = value - prtd->buffer_start;
if (diff < 0)
diff = 0;
else if (diff >= prtd->buffer_size)
diff = prtd->buffer_size;
pr_debug("%s value: 0x%08x offset: 0x%08x\n", __func__,
value, diff);
return (unsigned int)diff;
}
static unsigned int aml_pcm_offset_rx(struct aml_pcm_runtime_data *prtd)
{
unsigned int value = 0;
signed int diff = 0;
value = pcm_in_wr_ptr();
diff = value - prtd->buffer_start;
if (diff < 0)
diff = 0;
else if (diff >= prtd->buffer_size)
diff = prtd->buffer_size;
pr_debug("%s value: 0x%08x offset: 0x%08x\n", __func__,
value, diff);
return (unsigned int)diff;
}
static void aml_pcm_timer_update(struct aml_pcm_runtime_data *prtd)
{
struct snd_pcm_substream *substream = prtd->substream;
struct snd_pcm_runtime *runtime = substream->runtime;
unsigned int offset = 0;
unsigned int size = 0;
if (prtd->running && snd_pcm_running(substream)) {
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
offset = aml_pcm_offset_tx(prtd);
if (offset < prtd->buffer_offset) {
size =
prtd->buffer_size + offset -
prtd->buffer_offset;
} else {
size = offset - prtd->buffer_offset;
}
} else {
int rx_overflow = 0;
offset = aml_pcm_offset_rx(prtd);
if (offset < prtd->buffer_offset) {
size =
prtd->buffer_size + offset -
prtd->buffer_offset;
} else
size = offset - prtd->buffer_offset;
rx_overflow = pcm_in_fifo_int() & (1 << 2);
if (rx_overflow) {
pr_info("%s AUDIN_FIFO overflow !!\n",
__func__);
}
}
}
prtd->buffer_offset = offset;
prtd->data_size += size;
if (prtd->data_size >= frames_to_bytes(runtime, runtime->period_size))
prtd->period_elapsed++;
}
static void aml_pcm_timer_rearm(struct aml_pcm_runtime_data *prtd)
{
prtd->timer.expires = jiffies + prtd->timer_period;
add_timer(&prtd->timer);
}
static int aml_pcm_timer_start(struct aml_pcm_runtime_data *prtd)
{
pr_info("%s\n", __func__);
spin_lock(&prtd->lock);
aml_pcm_timer_rearm(prtd);
prtd->running = 1;
spin_unlock(&prtd->lock);
return 0;
}
static int aml_pcm_timer_stop(struct aml_pcm_runtime_data *prtd)
{
pr_info("%s\n", __func__);
spin_lock(&prtd->lock);
prtd->running = 0;
del_timer(&prtd->timer);
spin_unlock(&prtd->lock);
return 0;
}
static void aml_pcm_timer_callback(unsigned long data)
{
struct snd_pcm_substream *substream = (struct snd_pcm_substream *)data;
struct snd_pcm_runtime *runtime = substream->runtime;
struct aml_pcm_runtime_data *prtd = runtime->private_data;
unsigned long flags;
unsigned int elapsed = 0;
unsigned int datasize = 0;
spin_lock_irqsave(&prtd->lock, flags);
aml_pcm_timer_update(prtd);
aml_pcm_timer_rearm(prtd);
elapsed = prtd->period_elapsed;
datasize = prtd->data_size;
if (elapsed) {
prtd->period_elapsed--;
prtd->data_size -=
frames_to_bytes(runtime, runtime->period_size);
}
spin_unlock_irqrestore(&prtd->lock, flags);
if (elapsed) {
if (elapsed > 1)
pr_info("PCM timer callback not fast enough!\n");
snd_pcm_period_elapsed(prtd->substream);
}
}
static int aml_pcm_timer_create(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct aml_pcm_runtime_data *prtd = runtime->private_data;
pr_info("%s\n", __func__);
init_timer(&prtd->timer);
prtd->timer_period = 1;
prtd->timer.data = (unsigned long)substream;
prtd->timer.function = aml_pcm_timer_callback;
prtd->running = 0;
return 0;
}
static int aml_pcm_hw_params(
struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct aml_pcm_runtime_data *prtd = runtime->private_data;
int ret = 0;
struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct snd_soc_dai *codec_dai = rtd->codec_dai;
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
int channels, width;
pr_info("enter %s cpu_dai->name: %s cpu_dai->id: %d\n", __func__,
cpu_dai->name, cpu_dai->id);
pr_info("enter %s codec_dai->name: %s codec_dai->id: %d\n", __func__,
codec_dai->name, codec_dai->id);
snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);
runtime->dma_bytes = params_buffer_bytes(params);
prtd->buffer_start = runtime->dma_addr;
prtd->buffer_size = runtime->dma_bytes;
channels = params_channels(params);
width = snd_pcm_format_physical_width(params_format(params));
pr_info("%s:channels=0x%04x, width:0x%04x\n",
__func__, channels, width);
/* set cpu DAI configuration */
ret = snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_DSP_A |
SND_SOC_DAIFMT_NB_IF | SND_SOC_DAIFMT_CBS_CFS);
if (ret < 0) {
pr_err("set cpu dai fmt wrong\n");
return ret;
}
/* set codec DAI configuration */
ret = snd_soc_dai_set_fmt(codec_dai, SND_SOC_DAIFMT_DSP_A |
SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS);
if (ret < 0) {
pr_err("set codec dai fmt wrong\n");
return ret;
}
/* each codec set its slot offset itself */
ret = snd_soc_dai_set_tdm_slot(codec_dai, 0xff, 0xff, channels, width);
if (ret < 0) {
pr_err("set codec dai tdm slot wrong\n");
return ret;
}
return 0;
}
static int aml_pcm_hw_free(struct snd_pcm_substream *substream)
{
pr_info("enter %s\n", __func__);
snd_pcm_lib_free_pages(substream);
return 0;
}
static int aml_pcm_prepare(struct snd_pcm_substream *substream)
{
pr_info("enter %s\n", __func__);
return 0;
}
static int aml_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct aml_pcm_runtime_data *prtd = runtime->private_data;
int ret = 0;
pr_info("enter %s\n", __func__);
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
aml_pcm_timer_start(prtd);
break;
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
aml_pcm_timer_stop(prtd);
break;
default:
ret = -EINVAL;
}
return ret;
}
static snd_pcm_uframes_t aml_pcm_pointer(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct aml_pcm_runtime_data *prtd = runtime->private_data;
snd_pcm_uframes_t frames;
/* pr_info("enter %s\n", __func__); */
frames = bytes_to_frames(runtime, (ssize_t) prtd->buffer_offset);
return frames;
}
static int aml_pcm_open(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct aml_pcm_runtime_data *prtd = NULL;
int ret;
pr_info("enter %s, stream:%d\n", __func__, substream->stream);
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
snd_soc_set_runtime_hwparams(substream, &aml_pcm_hardware);
else
snd_soc_set_runtime_hwparams(substream, &aml_pcm_capture);
/* Ensure that period size is a multiple of 32bytes */
ret =
snd_pcm_hw_constraint_list(runtime, 0,
SNDRV_PCM_HW_PARAM_PERIOD_BYTES,
&hw_constraints_period_sizes);
if (ret < 0) {
pr_err("set period bytes constraint error\n");
goto out;
}
/* Ensure that buffer size is a multiple of period size */
ret =
snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
if (ret < 0) {
pr_err("set periods constraint error\n");
goto out;
}
prtd = kzalloc(sizeof(*prtd), GFP_KERNEL);
if (prtd == NULL) {
/*pr_err("out of memory\n");*/
ret = -ENOMEM;
goto out;
}
runtime->private_data = prtd;
aml_pcm_timer_create(substream);
prtd->substream = substream;
spin_lock_init(&prtd->lock);
return 0;
out:
return ret;
}
static int aml_pcm_close(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct aml_pcm_runtime_data *prtd = runtime->private_data;
pr_info("enter %s type: %d\n", __func__, substream->stream);
if (prtd)
kfree(runtime->private_data);
return 0;
}
static int aml_pcm_copy_playback(
struct snd_pcm_runtime *runtime,
int channel, snd_pcm_uframes_t pos,
void __user *buf, snd_pcm_uframes_t count)
{
struct aml_pcm_runtime_data *prtd = runtime->private_data;
unsigned char *hwbuf =
runtime->dma_area + frames_to_bytes(runtime, pos);
unsigned int wrptr = 0;
int ret = 0;
/* pr_info("enter %s channel: %d pos: %ld count: %ld\n",
* __func__, channel, pos, count);
*/
if (copy_from_user(hwbuf, buf, frames_to_bytes(runtime, count))) {
pr_err("%s copy from user failed!\n", __func__);
return -EFAULT;
}
{
wrptr = prtd->buffer_start + frames_to_bytes(runtime, pos)
+ frames_to_bytes(runtime, count);
if (wrptr >= (prtd->buffer_start + prtd->buffer_size))
wrptr = prtd->buffer_start + prtd->buffer_size;
pcm_out_set_wr_ptr(wrptr);
}
return ret;
}
static int aml_pcm_copy_capture(
struct snd_pcm_runtime *runtime,
int channel, snd_pcm_uframes_t pos,
void __user *buf, snd_pcm_uframes_t count)
{
struct aml_pcm_runtime_data *prtd = runtime->private_data;
signed short *hwbuf =
(signed short *)(runtime->dma_area + frames_to_bytes(runtime, pos));
unsigned int rdptr = 0;
int ret = 0;
/* pr_info("enter %s channel: %d pos: %ld count: %ld\n",
* __func__, channel, pos, count);
*/
if (copy_to_user(buf, hwbuf, frames_to_bytes(runtime, count))) {
pr_err("%s copy to user failed!\n", __func__);
return -EFAULT;
}
{
/* memset(hwbuf, 0xff, frames_to_bytes(runtime, count)); */
rdptr =
prtd->buffer_start + frames_to_bytes(runtime,
pos) +
frames_to_bytes(runtime, count);
if (rdptr >= (prtd->buffer_start + prtd->buffer_size))
rdptr = prtd->buffer_start + prtd->buffer_size;
pcm_in_set_rd_ptr(rdptr);
}
return ret;
}
static int aml_pcm_copy(struct snd_pcm_substream *substream,
int channel, snd_pcm_uframes_t pos,
void __user *buf, snd_pcm_uframes_t count)
{
struct snd_pcm_runtime *runtime = substream->runtime;
int ret = 0;
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
ret =
aml_pcm_copy_playback(runtime, channel, pos, buf, count);
} else {
ret =
aml_pcm_copy_capture(runtime, channel, pos, buf, count);
}
return ret;
}
static int aml_pcm_silence(struct snd_pcm_substream *substream,
int channel, snd_pcm_uframes_t pos, snd_pcm_uframes_t count)
{
struct snd_pcm_runtime *runtime = substream->runtime;
unsigned char *ppos = NULL;
ssize_t n;
pr_info("enter %s\n", __func__);
n = frames_to_bytes(runtime, count);
ppos = runtime->dma_area + frames_to_bytes(runtime, pos);
memset(ppos, 0, n);
return 0;
}
static struct snd_pcm_ops aml_pcm_ops = {
.open = aml_pcm_open,
.close = aml_pcm_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = aml_pcm_hw_params,
.hw_free = aml_pcm_hw_free,
.prepare = aml_pcm_prepare,
.trigger = aml_pcm_trigger,
.pointer = aml_pcm_pointer,
.copy = aml_pcm_copy,
.silence = aml_pcm_silence,
};
/* --------------------------------------------------------------------------
* ASoC platform driver
* --------------------------------------------------------------------------
*/
static u64 aml_pcm_dmamask = DMA_BIT_MASK(32);
static int aml_pcm_preallocate_dma_buffer(struct snd_pcm *pcm, int stream)
{
struct snd_pcm_substream *substream = pcm->streams[stream].substream;
struct snd_dma_buffer *buf = &substream->dma_buffer;
size_t size;
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
size = aml_pcm_hardware.buffer_bytes_max;
buf->dev.type = SNDRV_DMA_TYPE_DEV;
buf->dev.dev = pcm->card->dev;
buf->private_data = NULL;
buf->area = dma_alloc_coherent(pcm->card->dev, size,
&buf->addr, GFP_KERNEL);
if (!buf->area) {
pr_info("%s dma_alloc_coherent failed!\n", __func__);
return -ENOMEM;
}
} else {
size = aml_pcm_capture.buffer_bytes_max;
buf->dev.type = SNDRV_DMA_TYPE_DEV;
buf->dev.dev = pcm->card->dev;
buf->private_data = NULL;
buf->area = dma_alloc_coherent(pcm->card->dev, size,
&buf->addr, GFP_KERNEL);
if (!buf->area) {
pr_info("%s dma_alloc_coherent failed!\n", __func__);
return -ENOMEM;
}
}
buf->bytes = size;
return 0;
}
static int aml_pcm_new(struct snd_soc_pcm_runtime *rtd)
{
struct snd_soc_card *card = rtd->card;
struct snd_pcm *pcm = rtd->pcm;
struct snd_soc_dai *dai;
int ret = 0;
dai = rtd->cpu_dai;
pr_info("enter %s dai->name: %s dai->id: %d\n", __func__,
dai->name, dai->id);
if (!card->dev->dma_mask)
card->dev->dma_mask = &aml_pcm_dmamask;
if (!card->dev->coherent_dma_mask)
card->dev->coherent_dma_mask = DMA_BIT_MASK(32);
if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {
ret = aml_pcm_preallocate_dma_buffer(pcm,
SNDRV_PCM_STREAM_PLAYBACK);
if (ret)
goto out;
}
if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) {
ret = aml_pcm_preallocate_dma_buffer(pcm,
SNDRV_PCM_STREAM_CAPTURE);
if (ret)
goto out;
}
out:
return ret;
}
static void aml_pcm_free(struct snd_pcm *pcm)
{
struct snd_pcm_substream *substream;
struct snd_dma_buffer *buf;
int stream;
pr_info("enter %s\n", __func__);
for (stream = 0; stream < 2; stream++) {
substream = pcm->streams[stream].substream;
if (!substream)
continue;
buf = &substream->dma_buffer;
if (!buf->area)
continue;
dma_free_coherent(pcm->card->dev, buf->bytes, buf->area,
buf->addr);
buf->area = NULL;
}
}
struct snd_soc_platform_driver aml_soc_platform_pcm = {
.ops = &aml_pcm_ops,
.pcm_new = aml_pcm_new,
.pcm_free = aml_pcm_free,
};
EXPORT_SYMBOL_GPL(aml_soc_platform_pcm);
static int aml_soc_platform_pcm_probe(struct platform_device *pdev)
{
return snd_soc_register_platform(&pdev->dev, &aml_soc_platform_pcm);
}
static int aml_soc_platform_pcm_remove(struct platform_device *pdev)
{
snd_soc_unregister_platform(&pdev->dev);
return 0;
}
#ifdef CONFIG_OF
static const struct of_device_id amlogic_audio_dt_match[] = {
{.compatible = "amlogic, aml-pcm",},
{},
};
#else
#define amlogic_audio_dt_match NULL
#endif
static struct platform_driver aml_platform_pcm_driver = {
.driver = {
.name = "aml-pcm",
.owner = THIS_MODULE,
.of_match_table = amlogic_audio_dt_match,
},
.probe = aml_soc_platform_pcm_probe,
.remove = aml_soc_platform_pcm_remove,
};
static int __init aml_alsa_pcm_init(void)
{
return platform_driver_register(&aml_platform_pcm_driver);
}
static void __exit aml_alsa_pcm_exit(void)
{
platform_driver_unregister(&aml_platform_pcm_driver);
}
module_init(aml_alsa_pcm_init);
module_exit(aml_alsa_pcm_exit);
MODULE_AUTHOR("AMLogic, Inc.");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("AML audio driver for ALSA");