forked from Minki/linux
3cec159cfb
The ASoC core assumes that the PCM component of the ASoC card transparently moves data around and does not impose any restrictions on the memory layout or the transfer speed. It ignores all fields from the snd_pcm_hardware struct for the PCM driver that are related to this. Setting these fields in the PCM driver might suggest otherwise though, so rather not set them. Signed-off-by: Lars-Peter Clausen <lars@metafoo.de> Acked-by: Daniel Glöckner <daniel-gl@gmx.net> Signed-off-by: Mark Brown <broonie@linaro.org>
522 lines
14 KiB
C
522 lines
14 KiB
C
/*
|
|
* ALSA PCM interface for the Stetch s6000 family
|
|
*
|
|
* Author: Daniel Gloeckner, <dg@emlix.com>
|
|
* Copyright: (C) 2009 emlix GmbH <info@emlix.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/init.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/dma-mapping.h>
|
|
#include <linux/interrupt.h>
|
|
|
|
#include <sound/core.h>
|
|
#include <sound/pcm.h>
|
|
#include <sound/pcm_params.h>
|
|
#include <sound/soc.h>
|
|
|
|
#include <asm/dma.h>
|
|
#include <variant/dmac.h>
|
|
|
|
#include "s6000-pcm.h"
|
|
|
|
#define S6_PCM_PREALLOCATE_SIZE (96 * 1024)
|
|
#define S6_PCM_PREALLOCATE_MAX (2048 * 1024)
|
|
|
|
static struct snd_pcm_hardware s6000_pcm_hardware = {
|
|
.info = (SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER |
|
|
SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID |
|
|
SNDRV_PCM_INFO_PAUSE | SNDRV_PCM_INFO_JOINT_DUPLEX),
|
|
.buffer_bytes_max = 0x7ffffff0,
|
|
.period_bytes_min = 16,
|
|
.period_bytes_max = 0xfffff0,
|
|
.periods_min = 2,
|
|
.periods_max = 1024, /* no limit */
|
|
.fifo_size = 0,
|
|
};
|
|
|
|
struct s6000_runtime_data {
|
|
spinlock_t lock;
|
|
int period; /* current DMA period */
|
|
};
|
|
|
|
static void s6000_pcm_enqueue_dma(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct s6000_runtime_data *prtd = runtime->private_data;
|
|
struct snd_soc_pcm_runtime *soc_runtime = substream->private_data;
|
|
struct s6000_pcm_dma_params *par;
|
|
int channel;
|
|
unsigned int period_size;
|
|
unsigned int dma_offset;
|
|
dma_addr_t dma_pos;
|
|
dma_addr_t src, dst;
|
|
|
|
par = snd_soc_dai_get_dma_data(soc_runtime->cpu_dai, substream);
|
|
|
|
period_size = snd_pcm_lib_period_bytes(substream);
|
|
dma_offset = prtd->period * period_size;
|
|
dma_pos = runtime->dma_addr + dma_offset;
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
src = dma_pos;
|
|
dst = par->sif_out;
|
|
channel = par->dma_out;
|
|
} else {
|
|
src = par->sif_in;
|
|
dst = dma_pos;
|
|
channel = par->dma_in;
|
|
}
|
|
|
|
if (!s6dmac_channel_enabled(DMA_MASK_DMAC(channel),
|
|
DMA_INDEX_CHNL(channel)))
|
|
return;
|
|
|
|
if (s6dmac_fifo_full(DMA_MASK_DMAC(channel), DMA_INDEX_CHNL(channel))) {
|
|
printk(KERN_ERR "s6000-pcm: fifo full\n");
|
|
return;
|
|
}
|
|
|
|
if (WARN_ON(period_size & 15))
|
|
return;
|
|
s6dmac_put_fifo(DMA_MASK_DMAC(channel), DMA_INDEX_CHNL(channel),
|
|
src, dst, period_size);
|
|
|
|
prtd->period++;
|
|
if (unlikely(prtd->period >= runtime->periods))
|
|
prtd->period = 0;
|
|
}
|
|
|
|
static irqreturn_t s6000_pcm_irq(int irq, void *data)
|
|
{
|
|
struct snd_pcm *pcm = data;
|
|
struct snd_soc_pcm_runtime *runtime = pcm->private_data;
|
|
struct s6000_runtime_data *prtd;
|
|
unsigned int has_xrun;
|
|
int i, ret = IRQ_NONE;
|
|
|
|
for (i = 0; i < 2; ++i) {
|
|
struct snd_pcm_substream *substream = pcm->streams[i].substream;
|
|
struct s6000_pcm_dma_params *params =
|
|
snd_soc_dai_get_dma_data(runtime->cpu_dai, substream);
|
|
u32 channel;
|
|
unsigned int pending;
|
|
|
|
if (substream == SNDRV_PCM_STREAM_PLAYBACK)
|
|
channel = params->dma_out;
|
|
else
|
|
channel = params->dma_in;
|
|
|
|
has_xrun = params->check_xrun(runtime->cpu_dai);
|
|
|
|
if (!channel)
|
|
continue;
|
|
|
|
if (unlikely(has_xrun & (1 << i)) &&
|
|
substream->runtime &&
|
|
snd_pcm_running(substream)) {
|
|
dev_dbg(pcm->dev, "xrun\n");
|
|
snd_pcm_stream_lock(substream);
|
|
snd_pcm_stop(substream, SNDRV_PCM_STATE_XRUN);
|
|
snd_pcm_stream_unlock(substream);
|
|
ret = IRQ_HANDLED;
|
|
}
|
|
|
|
pending = s6dmac_int_sources(DMA_MASK_DMAC(channel),
|
|
DMA_INDEX_CHNL(channel));
|
|
|
|
if (pending & 1) {
|
|
ret = IRQ_HANDLED;
|
|
if (likely(substream->runtime &&
|
|
snd_pcm_running(substream))) {
|
|
snd_pcm_period_elapsed(substream);
|
|
dev_dbg(pcm->dev, "period elapsed %x %x\n",
|
|
s6dmac_cur_src(DMA_MASK_DMAC(channel),
|
|
DMA_INDEX_CHNL(channel)),
|
|
s6dmac_cur_dst(DMA_MASK_DMAC(channel),
|
|
DMA_INDEX_CHNL(channel)));
|
|
prtd = substream->runtime->private_data;
|
|
spin_lock(&prtd->lock);
|
|
s6000_pcm_enqueue_dma(substream);
|
|
spin_unlock(&prtd->lock);
|
|
}
|
|
}
|
|
|
|
if (unlikely(pending & ~7)) {
|
|
if (pending & (1 << 3))
|
|
printk(KERN_WARNING
|
|
"s6000-pcm: DMA %x Underflow\n",
|
|
channel);
|
|
if (pending & (1 << 4))
|
|
printk(KERN_WARNING
|
|
"s6000-pcm: DMA %x Overflow\n",
|
|
channel);
|
|
if (pending & 0x1e0)
|
|
printk(KERN_WARNING
|
|
"s6000-pcm: DMA %x Master Error "
|
|
"(mask %x)\n",
|
|
channel, pending >> 5);
|
|
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int s6000_pcm_start(struct snd_pcm_substream *substream)
|
|
{
|
|
struct s6000_runtime_data *prtd = substream->runtime->private_data;
|
|
struct snd_soc_pcm_runtime *soc_runtime = substream->private_data;
|
|
struct s6000_pcm_dma_params *par;
|
|
unsigned long flags;
|
|
int srcinc;
|
|
u32 dma;
|
|
|
|
par = snd_soc_dai_get_dma_data(soc_runtime->cpu_dai, substream);
|
|
|
|
spin_lock_irqsave(&prtd->lock, flags);
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
srcinc = 1;
|
|
dma = par->dma_out;
|
|
} else {
|
|
srcinc = 0;
|
|
dma = par->dma_in;
|
|
}
|
|
s6dmac_enable_chan(DMA_MASK_DMAC(dma), DMA_INDEX_CHNL(dma),
|
|
1 /* priority 1 (0 is max) */,
|
|
0 /* peripheral requests w/o xfer length mode */,
|
|
srcinc /* source address increment */,
|
|
srcinc^1 /* destination address increment */,
|
|
0 /* chunksize 0 (skip impossible on this dma) */,
|
|
0 /* source skip after chunk (impossible) */,
|
|
0 /* destination skip after chunk (impossible) */,
|
|
4 /* 16 byte burst size */,
|
|
-1 /* don't conserve bandwidth */,
|
|
0 /* low watermark irq descriptor threshold */,
|
|
0 /* disable hardware timestamps */,
|
|
1 /* enable channel */);
|
|
|
|
s6000_pcm_enqueue_dma(substream);
|
|
s6000_pcm_enqueue_dma(substream);
|
|
|
|
spin_unlock_irqrestore(&prtd->lock, flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s6000_pcm_stop(struct snd_pcm_substream *substream)
|
|
{
|
|
struct s6000_runtime_data *prtd = substream->runtime->private_data;
|
|
struct snd_soc_pcm_runtime *soc_runtime = substream->private_data;
|
|
struct s6000_pcm_dma_params *par;
|
|
unsigned long flags;
|
|
u32 channel;
|
|
|
|
par = snd_soc_dai_get_dma_data(soc_runtime->cpu_dai, substream);
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
|
|
channel = par->dma_out;
|
|
else
|
|
channel = par->dma_in;
|
|
|
|
s6dmac_set_terminal_count(DMA_MASK_DMAC(channel),
|
|
DMA_INDEX_CHNL(channel), 0);
|
|
|
|
spin_lock_irqsave(&prtd->lock, flags);
|
|
|
|
s6dmac_disable_chan(DMA_MASK_DMAC(channel), DMA_INDEX_CHNL(channel));
|
|
|
|
spin_unlock_irqrestore(&prtd->lock, flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s6000_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
|
|
{
|
|
struct snd_soc_pcm_runtime *soc_runtime = substream->private_data;
|
|
struct s6000_pcm_dma_params *par;
|
|
int ret;
|
|
|
|
par = snd_soc_dai_get_dma_data(soc_runtime->cpu_dai, substream);
|
|
|
|
ret = par->trigger(substream, cmd, 0);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
switch (cmd) {
|
|
case SNDRV_PCM_TRIGGER_START:
|
|
case SNDRV_PCM_TRIGGER_RESUME:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
|
|
ret = s6000_pcm_start(substream);
|
|
break;
|
|
case SNDRV_PCM_TRIGGER_STOP:
|
|
case SNDRV_PCM_TRIGGER_SUSPEND:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
|
|
ret = s6000_pcm_stop(substream);
|
|
break;
|
|
default:
|
|
ret = -EINVAL;
|
|
}
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return par->trigger(substream, cmd, 1);
|
|
}
|
|
|
|
static int s6000_pcm_prepare(struct snd_pcm_substream *substream)
|
|
{
|
|
struct s6000_runtime_data *prtd = substream->runtime->private_data;
|
|
|
|
prtd->period = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static snd_pcm_uframes_t s6000_pcm_pointer(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_soc_pcm_runtime *soc_runtime = substream->private_data;
|
|
struct s6000_pcm_dma_params *par;
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct s6000_runtime_data *prtd = runtime->private_data;
|
|
unsigned long flags;
|
|
unsigned int offset;
|
|
dma_addr_t count;
|
|
|
|
par = snd_soc_dai_get_dma_data(soc_runtime->cpu_dai, substream);
|
|
|
|
spin_lock_irqsave(&prtd->lock, flags);
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
|
|
count = s6dmac_cur_src(DMA_MASK_DMAC(par->dma_out),
|
|
DMA_INDEX_CHNL(par->dma_out));
|
|
else
|
|
count = s6dmac_cur_dst(DMA_MASK_DMAC(par->dma_in),
|
|
DMA_INDEX_CHNL(par->dma_in));
|
|
|
|
count -= runtime->dma_addr;
|
|
|
|
spin_unlock_irqrestore(&prtd->lock, flags);
|
|
|
|
offset = bytes_to_frames(runtime, count);
|
|
if (unlikely(offset >= runtime->buffer_size))
|
|
offset = 0;
|
|
|
|
return offset;
|
|
}
|
|
|
|
static int s6000_pcm_open(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_soc_pcm_runtime *soc_runtime = substream->private_data;
|
|
struct s6000_pcm_dma_params *par;
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct s6000_runtime_data *prtd;
|
|
int ret;
|
|
|
|
par = snd_soc_dai_get_dma_data(soc_runtime->cpu_dai, substream);
|
|
snd_soc_set_runtime_hwparams(substream, &s6000_pcm_hardware);
|
|
|
|
ret = snd_pcm_hw_constraint_step(runtime, 0,
|
|
SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 16);
|
|
if (ret < 0)
|
|
return ret;
|
|
ret = snd_pcm_hw_constraint_step(runtime, 0,
|
|
SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 16);
|
|
if (ret < 0)
|
|
return ret;
|
|
ret = snd_pcm_hw_constraint_integer(runtime,
|
|
SNDRV_PCM_HW_PARAM_PERIODS);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
if (par->same_rate) {
|
|
int rate;
|
|
spin_lock(&par->lock); /* needed? */
|
|
rate = par->rate;
|
|
spin_unlock(&par->lock);
|
|
if (rate != -1) {
|
|
ret = snd_pcm_hw_constraint_minmax(runtime,
|
|
SNDRV_PCM_HW_PARAM_RATE,
|
|
rate, rate);
|
|
if (ret < 0)
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
prtd = kzalloc(sizeof(struct s6000_runtime_data), GFP_KERNEL);
|
|
if (prtd == NULL)
|
|
return -ENOMEM;
|
|
|
|
spin_lock_init(&prtd->lock);
|
|
|
|
runtime->private_data = prtd;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s6000_pcm_close(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct s6000_runtime_data *prtd = runtime->private_data;
|
|
|
|
kfree(prtd);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int s6000_pcm_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *hw_params)
|
|
{
|
|
struct snd_soc_pcm_runtime *soc_runtime = substream->private_data;
|
|
struct s6000_pcm_dma_params *par;
|
|
int ret;
|
|
ret = snd_pcm_lib_malloc_pages(substream,
|
|
params_buffer_bytes(hw_params));
|
|
if (ret < 0) {
|
|
printk(KERN_WARNING "s6000-pcm: allocation of memory failed\n");
|
|
return ret;
|
|
}
|
|
|
|
par = snd_soc_dai_get_dma_data(soc_runtime->cpu_dai, substream);
|
|
|
|
if (par->same_rate) {
|
|
spin_lock(&par->lock);
|
|
if (par->rate == -1 ||
|
|
!(par->in_use & ~(1 << substream->stream))) {
|
|
par->rate = params_rate(hw_params);
|
|
par->in_use |= 1 << substream->stream;
|
|
} else if (params_rate(hw_params) != par->rate) {
|
|
snd_pcm_lib_free_pages(substream);
|
|
par->in_use &= ~(1 << substream->stream);
|
|
ret = -EBUSY;
|
|
}
|
|
spin_unlock(&par->lock);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static int s6000_pcm_hw_free(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_soc_pcm_runtime *soc_runtime = substream->private_data;
|
|
struct s6000_pcm_dma_params *par =
|
|
snd_soc_dai_get_dma_data(soc_runtime->cpu_dai, substream);
|
|
|
|
spin_lock(&par->lock);
|
|
par->in_use &= ~(1 << substream->stream);
|
|
if (!par->in_use)
|
|
par->rate = -1;
|
|
spin_unlock(&par->lock);
|
|
|
|
return snd_pcm_lib_free_pages(substream);
|
|
}
|
|
|
|
static struct snd_pcm_ops s6000_pcm_ops = {
|
|
.open = s6000_pcm_open,
|
|
.close = s6000_pcm_close,
|
|
.ioctl = snd_pcm_lib_ioctl,
|
|
.hw_params = s6000_pcm_hw_params,
|
|
.hw_free = s6000_pcm_hw_free,
|
|
.trigger = s6000_pcm_trigger,
|
|
.prepare = s6000_pcm_prepare,
|
|
.pointer = s6000_pcm_pointer,
|
|
};
|
|
|
|
static void s6000_pcm_free(struct snd_pcm *pcm)
|
|
{
|
|
struct snd_soc_pcm_runtime *runtime = pcm->private_data;
|
|
struct s6000_pcm_dma_params *params =
|
|
snd_soc_dai_get_dma_data(runtime->cpu_dai,
|
|
pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream);
|
|
|
|
free_irq(params->irq, pcm);
|
|
snd_pcm_lib_preallocate_free_for_all(pcm);
|
|
}
|
|
|
|
static int s6000_pcm_new(struct snd_soc_pcm_runtime *runtime)
|
|
{
|
|
struct snd_card *card = runtime->card->snd_card;
|
|
struct snd_pcm *pcm = runtime->pcm;
|
|
struct s6000_pcm_dma_params *params;
|
|
int res;
|
|
|
|
params = snd_soc_dai_get_dma_data(runtime->cpu_dai,
|
|
pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream);
|
|
|
|
res = dma_coerce_mask_and_coherent(card->dev, DMA_BIT_MASK(32));
|
|
if (res)
|
|
return res;
|
|
|
|
if (params->dma_in) {
|
|
s6dmac_disable_chan(DMA_MASK_DMAC(params->dma_in),
|
|
DMA_INDEX_CHNL(params->dma_in));
|
|
s6dmac_int_sources(DMA_MASK_DMAC(params->dma_in),
|
|
DMA_INDEX_CHNL(params->dma_in));
|
|
}
|
|
|
|
if (params->dma_out) {
|
|
s6dmac_disable_chan(DMA_MASK_DMAC(params->dma_out),
|
|
DMA_INDEX_CHNL(params->dma_out));
|
|
s6dmac_int_sources(DMA_MASK_DMAC(params->dma_out),
|
|
DMA_INDEX_CHNL(params->dma_out));
|
|
}
|
|
|
|
res = request_irq(params->irq, s6000_pcm_irq, IRQF_SHARED,
|
|
"s6000-audio", pcm);
|
|
if (res) {
|
|
printk(KERN_ERR "s6000-pcm couldn't get IRQ\n");
|
|
return res;
|
|
}
|
|
|
|
res = snd_pcm_lib_preallocate_pages_for_all(pcm,
|
|
SNDRV_DMA_TYPE_DEV,
|
|
card->dev,
|
|
S6_PCM_PREALLOCATE_SIZE,
|
|
S6_PCM_PREALLOCATE_MAX);
|
|
if (res)
|
|
printk(KERN_WARNING "s6000-pcm: preallocation failed\n");
|
|
|
|
spin_lock_init(¶ms->lock);
|
|
params->in_use = 0;
|
|
params->rate = -1;
|
|
return 0;
|
|
}
|
|
|
|
static struct snd_soc_platform_driver s6000_soc_platform = {
|
|
.ops = &s6000_pcm_ops,
|
|
.pcm_new = s6000_pcm_new,
|
|
.pcm_free = s6000_pcm_free,
|
|
};
|
|
|
|
static int s6000_soc_platform_probe(struct platform_device *pdev)
|
|
{
|
|
return snd_soc_register_platform(&pdev->dev, &s6000_soc_platform);
|
|
}
|
|
|
|
static int s6000_soc_platform_remove(struct platform_device *pdev)
|
|
{
|
|
snd_soc_unregister_platform(&pdev->dev);
|
|
return 0;
|
|
}
|
|
|
|
static struct platform_driver s6000_pcm_driver = {
|
|
.driver = {
|
|
.name = "s6000-pcm-audio",
|
|
.owner = THIS_MODULE,
|
|
},
|
|
|
|
.probe = s6000_soc_platform_probe,
|
|
.remove = s6000_soc_platform_remove,
|
|
};
|
|
|
|
module_platform_driver(s6000_pcm_driver);
|
|
|
|
MODULE_AUTHOR("Daniel Gloeckner");
|
|
MODULE_DESCRIPTION("Stretch s6000 family PCM DMA module");
|
|
MODULE_LICENSE("GPL");
|