mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2024-08-27 03:10:12 +00:00
![Stephen Warren](/assets/img/avatar_default.png)
The I2S controllers are programmed with an "attention" level of 4 DWORDs. This must match the configuration passed to the DMA driver, so that when they burst in data, they don't overflow the available FIFO space. Also, the burst size is relevant to the destination for playback, and source for capture, not vice-versa as originally written. Signed-off-by: Stephen Warren <swarren@nvidia.com> Signed-off-by: Mark Brown <broonie@opensource.wolfsonmicro.com> Cc: stable@vger.kernel.org
502 lines
13 KiB
C
502 lines
13 KiB
C
/*
|
|
* tegra_pcm.c - Tegra PCM driver
|
|
*
|
|
* Author: Stephen Warren <swarren@nvidia.com>
|
|
* Copyright (C) 2010,2012 - NVIDIA, Inc.
|
|
*
|
|
* Based on code copyright/by:
|
|
*
|
|
* Copyright (c) 2009-2010, NVIDIA Corporation.
|
|
* Scott Peterson <speterson@nvidia.com>
|
|
* Vijay Mali <vmali@nvidia.com>
|
|
*
|
|
* Copyright (C) 2010 Google, Inc.
|
|
* Iliyan Malchev <malchev@google.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.
|
|
*
|
|
* 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.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
|
* 02110-1301 USA
|
|
*
|
|
*/
|
|
|
|
#include <linux/dma-mapping.h>
|
|
#include <linux/module.h>
|
|
#include <linux/slab.h>
|
|
#include <sound/core.h>
|
|
#include <sound/pcm.h>
|
|
#include <sound/pcm_params.h>
|
|
#include <sound/soc.h>
|
|
#include <sound/dmaengine_pcm.h>
|
|
|
|
#include "tegra_pcm.h"
|
|
|
|
static const struct snd_pcm_hardware tegra_pcm_hardware = {
|
|
.info = SNDRV_PCM_INFO_MMAP |
|
|
SNDRV_PCM_INFO_MMAP_VALID |
|
|
SNDRV_PCM_INFO_PAUSE |
|
|
SNDRV_PCM_INFO_RESUME |
|
|
SNDRV_PCM_INFO_INTERLEAVED,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE,
|
|
.channels_min = 2,
|
|
.channels_max = 2,
|
|
.period_bytes_min = 1024,
|
|
.period_bytes_max = PAGE_SIZE,
|
|
.periods_min = 2,
|
|
.periods_max = 8,
|
|
.buffer_bytes_max = PAGE_SIZE * 8,
|
|
.fifo_size = 4,
|
|
};
|
|
|
|
#if defined(CONFIG_TEGRA_SYSTEM_DMA)
|
|
static void tegra_pcm_queue_dma(struct tegra_runtime_data *prtd)
|
|
{
|
|
struct snd_pcm_substream *substream = prtd->substream;
|
|
struct snd_dma_buffer *buf = &substream->dma_buffer;
|
|
struct tegra_dma_req *dma_req;
|
|
unsigned long addr;
|
|
|
|
dma_req = &prtd->dma_req[prtd->dma_req_idx];
|
|
prtd->dma_req_idx = 1 - prtd->dma_req_idx;
|
|
|
|
addr = buf->addr + prtd->dma_pos;
|
|
prtd->dma_pos += dma_req->size;
|
|
if (prtd->dma_pos >= prtd->dma_pos_end)
|
|
prtd->dma_pos = 0;
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
|
|
dma_req->source_addr = addr;
|
|
else
|
|
dma_req->dest_addr = addr;
|
|
|
|
tegra_dma_enqueue_req(prtd->dma_chan, dma_req);
|
|
}
|
|
|
|
static void dma_complete_callback(struct tegra_dma_req *req)
|
|
{
|
|
struct tegra_runtime_data *prtd = (struct tegra_runtime_data *)req->dev;
|
|
struct snd_pcm_substream *substream = prtd->substream;
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
|
|
spin_lock(&prtd->lock);
|
|
|
|
if (!prtd->running) {
|
|
spin_unlock(&prtd->lock);
|
|
return;
|
|
}
|
|
|
|
if (++prtd->period_index >= runtime->periods)
|
|
prtd->period_index = 0;
|
|
|
|
tegra_pcm_queue_dma(prtd);
|
|
|
|
spin_unlock(&prtd->lock);
|
|
|
|
snd_pcm_period_elapsed(substream);
|
|
}
|
|
|
|
static void setup_dma_tx_request(struct tegra_dma_req *req,
|
|
struct tegra_pcm_dma_params * dmap)
|
|
{
|
|
req->complete = dma_complete_callback;
|
|
req->to_memory = false;
|
|
req->dest_addr = dmap->addr;
|
|
req->dest_wrap = dmap->wrap;
|
|
req->source_bus_width = 32;
|
|
req->source_wrap = 0;
|
|
req->dest_bus_width = dmap->width;
|
|
req->req_sel = dmap->req_sel;
|
|
}
|
|
|
|
static void setup_dma_rx_request(struct tegra_dma_req *req,
|
|
struct tegra_pcm_dma_params * dmap)
|
|
{
|
|
req->complete = dma_complete_callback;
|
|
req->to_memory = true;
|
|
req->source_addr = dmap->addr;
|
|
req->dest_wrap = 0;
|
|
req->source_bus_width = dmap->width;
|
|
req->source_wrap = dmap->wrap;
|
|
req->dest_bus_width = 32;
|
|
req->req_sel = dmap->req_sel;
|
|
}
|
|
|
|
static int tegra_pcm_open(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct tegra_runtime_data *prtd;
|
|
struct snd_soc_pcm_runtime *rtd = substream->private_data;
|
|
struct tegra_pcm_dma_params * dmap;
|
|
int ret = 0;
|
|
|
|
prtd = kzalloc(sizeof(struct tegra_runtime_data), GFP_KERNEL);
|
|
if (prtd == NULL)
|
|
return -ENOMEM;
|
|
|
|
runtime->private_data = prtd;
|
|
prtd->substream = substream;
|
|
|
|
spin_lock_init(&prtd->lock);
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
dmap = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
|
|
setup_dma_tx_request(&prtd->dma_req[0], dmap);
|
|
setup_dma_tx_request(&prtd->dma_req[1], dmap);
|
|
} else {
|
|
dmap = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
|
|
setup_dma_rx_request(&prtd->dma_req[0], dmap);
|
|
setup_dma_rx_request(&prtd->dma_req[1], dmap);
|
|
}
|
|
|
|
prtd->dma_req[0].dev = prtd;
|
|
prtd->dma_req[1].dev = prtd;
|
|
|
|
prtd->dma_chan = tegra_dma_allocate_channel(TEGRA_DMA_MODE_ONESHOT);
|
|
if (prtd->dma_chan == NULL) {
|
|
ret = -ENOMEM;
|
|
goto err;
|
|
}
|
|
|
|
/* Set HW params now that initialization is complete */
|
|
snd_soc_set_runtime_hwparams(substream, &tegra_pcm_hardware);
|
|
|
|
/* 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)
|
|
goto err;
|
|
|
|
return 0;
|
|
|
|
err:
|
|
if (prtd->dma_chan) {
|
|
tegra_dma_free_channel(prtd->dma_chan);
|
|
}
|
|
|
|
kfree(prtd);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int tegra_pcm_close(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct tegra_runtime_data *prtd = runtime->private_data;
|
|
|
|
tegra_dma_free_channel(prtd->dma_chan);
|
|
|
|
kfree(prtd);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_pcm_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *params)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct tegra_runtime_data *prtd = runtime->private_data;
|
|
|
|
snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);
|
|
|
|
prtd->dma_req[0].size = params_period_bytes(params);
|
|
prtd->dma_req[1].size = prtd->dma_req[0].size;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_pcm_hw_free(struct snd_pcm_substream *substream)
|
|
{
|
|
snd_pcm_set_runtime_buffer(substream, NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct tegra_runtime_data *prtd = runtime->private_data;
|
|
unsigned long flags;
|
|
|
|
switch (cmd) {
|
|
case SNDRV_PCM_TRIGGER_START:
|
|
prtd->dma_pos = 0;
|
|
prtd->dma_pos_end = frames_to_bytes(runtime, runtime->periods * runtime->period_size);
|
|
prtd->period_index = 0;
|
|
prtd->dma_req_idx = 0;
|
|
/* Fall-through */
|
|
case SNDRV_PCM_TRIGGER_RESUME:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
|
|
spin_lock_irqsave(&prtd->lock, flags);
|
|
prtd->running = 1;
|
|
spin_unlock_irqrestore(&prtd->lock, flags);
|
|
tegra_pcm_queue_dma(prtd);
|
|
tegra_pcm_queue_dma(prtd);
|
|
break;
|
|
case SNDRV_PCM_TRIGGER_STOP:
|
|
case SNDRV_PCM_TRIGGER_SUSPEND:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
|
|
spin_lock_irqsave(&prtd->lock, flags);
|
|
prtd->running = 0;
|
|
spin_unlock_irqrestore(&prtd->lock, flags);
|
|
tegra_dma_dequeue_req(prtd->dma_chan, &prtd->dma_req[0]);
|
|
tegra_dma_dequeue_req(prtd->dma_chan, &prtd->dma_req[1]);
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static snd_pcm_uframes_t tegra_pcm_pointer(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
struct tegra_runtime_data *prtd = runtime->private_data;
|
|
|
|
return prtd->period_index * runtime->period_size;
|
|
}
|
|
|
|
|
|
static int tegra_pcm_mmap(struct snd_pcm_substream *substream,
|
|
struct vm_area_struct *vma)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
|
|
return dma_mmap_writecombine(substream->pcm->card->dev, vma,
|
|
runtime->dma_area,
|
|
runtime->dma_addr,
|
|
runtime->dma_bytes);
|
|
}
|
|
|
|
static struct snd_pcm_ops tegra_pcm_ops = {
|
|
.open = tegra_pcm_open,
|
|
.close = tegra_pcm_close,
|
|
.ioctl = snd_pcm_lib_ioctl,
|
|
.hw_params = tegra_pcm_hw_params,
|
|
.hw_free = tegra_pcm_hw_free,
|
|
.trigger = tegra_pcm_trigger,
|
|
.pointer = tegra_pcm_pointer,
|
|
.mmap = tegra_pcm_mmap,
|
|
};
|
|
#else
|
|
static int tegra_pcm_open(struct snd_pcm_substream *substream)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = substream->private_data;
|
|
struct device *dev = rtd->platform->dev;
|
|
int ret;
|
|
|
|
/* Set HW params now that initialization is complete */
|
|
snd_soc_set_runtime_hwparams(substream, &tegra_pcm_hardware);
|
|
|
|
ret = snd_dmaengine_pcm_open(substream, NULL, NULL);
|
|
if (ret) {
|
|
dev_err(dev, "dmaengine pcm open failed with err %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_pcm_close(struct snd_pcm_substream *substream)
|
|
{
|
|
snd_dmaengine_pcm_close(substream);
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_pcm_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *params)
|
|
{
|
|
struct snd_soc_pcm_runtime *rtd = substream->private_data;
|
|
struct device *dev = rtd->platform->dev;
|
|
struct dma_chan *chan = snd_dmaengine_pcm_get_chan(substream);
|
|
struct tegra_pcm_dma_params *dmap;
|
|
struct dma_slave_config slave_config;
|
|
int ret;
|
|
|
|
dmap = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
|
|
|
|
ret = snd_hwparams_to_dma_slave_config(substream, params,
|
|
&slave_config);
|
|
if (ret) {
|
|
dev_err(dev, "hw params config failed with err %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
slave_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
|
|
slave_config.dst_addr = dmap->addr;
|
|
slave_config.dst_maxburst = 4;
|
|
} else {
|
|
slave_config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
|
|
slave_config.src_addr = dmap->addr;
|
|
slave_config.src_maxburst = 4;
|
|
}
|
|
slave_config.slave_id = dmap->req_sel;
|
|
|
|
ret = dmaengine_slave_config(chan, &slave_config);
|
|
if (ret < 0) {
|
|
dev_err(dev, "dma slave config failed with err %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_pcm_hw_free(struct snd_pcm_substream *substream)
|
|
{
|
|
snd_pcm_set_runtime_buffer(substream, NULL);
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
|
|
{
|
|
switch (cmd) {
|
|
case SNDRV_PCM_TRIGGER_START:
|
|
case SNDRV_PCM_TRIGGER_RESUME:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
|
|
return snd_dmaengine_pcm_trigger(substream,
|
|
SNDRV_PCM_TRIGGER_START);
|
|
|
|
case SNDRV_PCM_TRIGGER_STOP:
|
|
case SNDRV_PCM_TRIGGER_SUSPEND:
|
|
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
|
|
return snd_dmaengine_pcm_trigger(substream,
|
|
SNDRV_PCM_TRIGGER_STOP);
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_pcm_mmap(struct snd_pcm_substream *substream,
|
|
struct vm_area_struct *vma)
|
|
{
|
|
struct snd_pcm_runtime *runtime = substream->runtime;
|
|
|
|
return dma_mmap_writecombine(substream->pcm->card->dev, vma,
|
|
runtime->dma_area,
|
|
runtime->dma_addr,
|
|
runtime->dma_bytes);
|
|
}
|
|
|
|
static struct snd_pcm_ops tegra_pcm_ops = {
|
|
.open = tegra_pcm_open,
|
|
.close = tegra_pcm_close,
|
|
.ioctl = snd_pcm_lib_ioctl,
|
|
.hw_params = tegra_pcm_hw_params,
|
|
.hw_free = tegra_pcm_hw_free,
|
|
.trigger = tegra_pcm_trigger,
|
|
.pointer = snd_dmaengine_pcm_pointer,
|
|
.mmap = tegra_pcm_mmap,
|
|
};
|
|
#endif
|
|
|
|
static int tegra_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 = tegra_pcm_hardware.buffer_bytes_max;
|
|
|
|
buf->area = dma_alloc_writecombine(pcm->card->dev, size,
|
|
&buf->addr, GFP_KERNEL);
|
|
if (!buf->area)
|
|
return -ENOMEM;
|
|
|
|
buf->dev.type = SNDRV_DMA_TYPE_DEV;
|
|
buf->dev.dev = pcm->card->dev;
|
|
buf->private_data = NULL;
|
|
buf->bytes = size;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void tegra_pcm_deallocate_dma_buffer(struct snd_pcm *pcm, int stream)
|
|
{
|
|
struct snd_pcm_substream *substream;
|
|
struct snd_dma_buffer *buf;
|
|
|
|
substream = pcm->streams[stream].substream;
|
|
if (!substream)
|
|
return;
|
|
|
|
buf = &substream->dma_buffer;
|
|
if (!buf->area)
|
|
return;
|
|
|
|
dma_free_writecombine(pcm->card->dev, buf->bytes,
|
|
buf->area, buf->addr);
|
|
buf->area = NULL;
|
|
}
|
|
|
|
static u64 tegra_dma_mask = DMA_BIT_MASK(32);
|
|
|
|
static int tegra_pcm_new(struct snd_soc_pcm_runtime *rtd)
|
|
{
|
|
struct snd_card *card = rtd->card->snd_card;
|
|
struct snd_pcm *pcm = rtd->pcm;
|
|
int ret = 0;
|
|
|
|
if (!card->dev->dma_mask)
|
|
card->dev->dma_mask = &tegra_dma_mask;
|
|
if (!card->dev->coherent_dma_mask)
|
|
card->dev->coherent_dma_mask = DMA_BIT_MASK(32);
|
|
|
|
if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {
|
|
ret = tegra_pcm_preallocate_dma_buffer(pcm,
|
|
SNDRV_PCM_STREAM_PLAYBACK);
|
|
if (ret)
|
|
goto err;
|
|
}
|
|
|
|
if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) {
|
|
ret = tegra_pcm_preallocate_dma_buffer(pcm,
|
|
SNDRV_PCM_STREAM_CAPTURE);
|
|
if (ret)
|
|
goto err_free_play;
|
|
}
|
|
|
|
return 0;
|
|
|
|
err_free_play:
|
|
tegra_pcm_deallocate_dma_buffer(pcm, SNDRV_PCM_STREAM_PLAYBACK);
|
|
err:
|
|
return ret;
|
|
}
|
|
|
|
static void tegra_pcm_free(struct snd_pcm *pcm)
|
|
{
|
|
tegra_pcm_deallocate_dma_buffer(pcm, SNDRV_PCM_STREAM_CAPTURE);
|
|
tegra_pcm_deallocate_dma_buffer(pcm, SNDRV_PCM_STREAM_PLAYBACK);
|
|
}
|
|
|
|
static struct snd_soc_platform_driver tegra_pcm_platform = {
|
|
.ops = &tegra_pcm_ops,
|
|
.pcm_new = tegra_pcm_new,
|
|
.pcm_free = tegra_pcm_free,
|
|
};
|
|
|
|
int __devinit tegra_pcm_platform_register(struct device *dev)
|
|
{
|
|
return snd_soc_register_platform(dev, &tegra_pcm_platform);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tegra_pcm_platform_register);
|
|
|
|
void __devexit tegra_pcm_platform_unregister(struct device *dev)
|
|
{
|
|
snd_soc_unregister_platform(dev);
|
|
}
|
|
EXPORT_SYMBOL_GPL(tegra_pcm_platform_unregister);
|
|
|
|
MODULE_AUTHOR("Stephen Warren <swarren@nvidia.com>");
|
|
MODULE_DESCRIPTION("Tegra PCM ASoC driver");
|
|
MODULE_LICENSE("GPL");
|