/* sound/soc/at32/at32-pcm.c * ASoC PCM interface for Atmel AT32 SoC * * Copyright (C) 2008 Long Range Systems * Geoffrey Wossum <gwossum@acm.org> * * 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. * * Note that this is basically a port of the sound/soc/at91-pcm.c to * the AVR32 kernel. Thanks to Frank Mandarino for that code. */ #include <linux/module.h> #include <linux/init.h> #include <linux/platform_device.h> #include <linux/slab.h> #include <linux/dma-mapping.h> #include <linux/atmel_pdc.h> #include <sound/core.h> #include <sound/pcm.h> #include <sound/pcm_params.h> #include <sound/soc.h> #include "at32-pcm.h" /*--------------------------------------------------------------------------*\ * Hardware definition \*--------------------------------------------------------------------------*/ /* TODO: These values were taken from the AT91 platform driver, check * them against real values for AT32 */ static const struct snd_pcm_hardware at32_pcm_hardware = { .info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_PAUSE), .formats = SNDRV_PCM_FMTBIT_S16, .period_bytes_min = 32, .period_bytes_max = 8192, /* 512 frames * 16 bytes / frame */ .periods_min = 2, .periods_max = 1024, .buffer_bytes_max = 32 * 1024, }; /*--------------------------------------------------------------------------*\ * Data types \*--------------------------------------------------------------------------*/ struct at32_runtime_data { struct at32_pcm_dma_params *params; dma_addr_t dma_buffer; /* physical address of DMA buffer */ dma_addr_t dma_buffer_end; /* first address beyond DMA buffer */ size_t period_size; dma_addr_t period_ptr; /* physical address of next period */ int periods; /* period index of period_ptr */ /* Save PDC registers (for power management) */ u32 pdc_xpr_save; u32 pdc_xcr_save; u32 pdc_xnpr_save; u32 pdc_xncr_save; }; /*--------------------------------------------------------------------------*\ * Helper functions \*--------------------------------------------------------------------------*/ static int at32_pcm_preallocate_dma_buffer(struct snd_pcm *pcm, int stream) { struct snd_pcm_substream *substream = pcm->streams[stream].substream; struct snd_dma_buffer *dmabuf = &substream->dma_buffer; size_t size = at32_pcm_hardware.buffer_bytes_max; dmabuf->dev.type = SNDRV_DMA_TYPE_DEV; dmabuf->dev.dev = pcm->card->dev; dmabuf->private_data = NULL; dmabuf->area = dma_alloc_coherent(pcm->card->dev, size, &dmabuf->addr, GFP_KERNEL); pr_debug("at32_pcm: preallocate_dma_buffer: " "area=%p, addr=%p, size=%ld\n", (void *)dmabuf->area, (void *)dmabuf->addr, size); if (!dmabuf->area) return -ENOMEM; dmabuf->bytes = size; return 0; } /*--------------------------------------------------------------------------*\ * ISR \*--------------------------------------------------------------------------*/ static void at32_pcm_dma_irq(u32 ssc_sr, struct snd_pcm_substream *substream) { struct snd_pcm_runtime *rtd = substream->runtime; struct at32_runtime_data *prtd = rtd->private_data; struct at32_pcm_dma_params *params = prtd->params; static int count; count++; if (ssc_sr & params->mask->ssc_endbuf) { pr_warning("at32-pcm: buffer %s on %s (SSC_SR=%#x, count=%d)\n", substream->stream == SNDRV_PCM_STREAM_PLAYBACK ? "underrun" : "overrun", params->name, ssc_sr, count); /* re-start the PDC */ ssc_writex(params->ssc->regs, ATMEL_PDC_PTCR, params->mask->pdc_disable); prtd->period_ptr += prtd->period_size; if (prtd->period_ptr >= prtd->dma_buffer_end) prtd->period_ptr = prtd->dma_buffer; ssc_writex(params->ssc->regs, params->pdc->xpr, prtd->period_ptr); ssc_writex(params->ssc->regs, params->pdc->xcr, prtd->period_size / params->pdc_xfer_size); ssc_writex(params->ssc->regs, ATMEL_PDC_PTCR, params->mask->pdc_enable); } if (ssc_sr & params->mask->ssc_endx) { /* Load the PDC next pointer and counter registers */ prtd->period_ptr += prtd->period_size; if (prtd->period_ptr >= prtd->dma_buffer_end) prtd->period_ptr = prtd->dma_buffer; ssc_writex(params->ssc->regs, params->pdc->xnpr, prtd->period_ptr); ssc_writex(params->ssc->regs, params->pdc->xncr, prtd->period_size / params->pdc_xfer_size); } snd_pcm_period_elapsed(substream); } /*--------------------------------------------------------------------------*\ * PCM operations \*--------------------------------------------------------------------------*/ static int at32_pcm_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { struct snd_pcm_runtime *runtime = substream->runtime; struct at32_runtime_data *prtd = runtime->private_data; struct snd_soc_pcm_runtime *rtd = substream->private_data; /* this may get called several times by oss emulation * with different params */ snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); runtime->dma_bytes = params_buffer_bytes(params); prtd->params = rtd->dai->cpu_dai->dma_data; prtd->params->dma_intr_handler = at32_pcm_dma_irq; prtd->dma_buffer = runtime->dma_addr; prtd->dma_buffer_end = runtime->dma_addr + runtime->dma_bytes; prtd->period_size = params_period_bytes(params); pr_debug("hw_params: DMA for %s initialized " "(dma_bytes=%ld, period_size=%ld)\n", prtd->params->name, runtime->dma_bytes, prtd->period_size); return 0; } static int at32_pcm_hw_free(struct snd_pcm_substream *substream) { struct at32_runtime_data *prtd = substream->runtime->private_data; struct at32_pcm_dma_params *params = prtd->params; if (params != NULL) { ssc_writex(params->ssc->regs, SSC_PDC_PTCR, params->mask->pdc_disable); prtd->params->dma_intr_handler = NULL; } return 0; } static int at32_pcm_prepare(struct snd_pcm_substream *substream) { struct at32_runtime_data *prtd = substream->runtime->private_data; struct at32_pcm_dma_params *params = prtd->params; ssc_writex(params->ssc->regs, SSC_IDR, params->mask->ssc_endx | params->mask->ssc_endbuf); ssc_writex(params->ssc->regs, ATMEL_PDC_PTCR, params->mask->pdc_disable); return 0; } static int at32_pcm_trigger(struct snd_pcm_substream *substream, int cmd) { struct snd_pcm_runtime *rtd = substream->runtime; struct at32_runtime_data *prtd = rtd->private_data; struct at32_pcm_dma_params *params = prtd->params; int ret = 0; pr_debug("at32_pcm_trigger: buffer_size = %ld, " "dma_area = %p, dma_bytes = %ld\n", rtd->buffer_size, rtd->dma_area, rtd->dma_bytes); switch (cmd) { case SNDRV_PCM_TRIGGER_START: prtd->period_ptr = prtd->dma_buffer; ssc_writex(params->ssc->regs, params->pdc->xpr, prtd->period_ptr); ssc_writex(params->ssc->regs, params->pdc->xcr, prtd->period_size / params->pdc_xfer_size); prtd->period_ptr += prtd->period_size; ssc_writex(params->ssc->regs, params->pdc->xnpr, prtd->period_ptr); ssc_writex(params->ssc->regs, params->pdc->xncr, prtd->period_size / params->pdc_xfer_size); pr_debug("trigger: period_ptr=%lx, xpr=%x, " "xcr=%d, xnpr=%x, xncr=%d\n", (unsigned long)prtd->period_ptr, ssc_readx(params->ssc->regs, params->pdc->xpr), ssc_readx(params->ssc->regs, params->pdc->xcr), ssc_readx(params->ssc->regs, params->pdc->xnpr), ssc_readx(params->ssc->regs, params->pdc->xncr)); ssc_writex(params->ssc->regs, SSC_IER, params->mask->ssc_endx | params->mask->ssc_endbuf); ssc_writex(params->ssc->regs, SSC_PDC_PTCR, params->mask->pdc_enable); pr_debug("sr=%x, imr=%x\n", ssc_readx(params->ssc->regs, SSC_SR), ssc_readx(params->ssc->regs, SSC_IER)); break; /* SNDRV_PCM_TRIGGER_START */ case SNDRV_PCM_TRIGGER_STOP: case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_PAUSE_PUSH: ssc_writex(params->ssc->regs, ATMEL_PDC_PTCR, params->mask->pdc_disable); break; case SNDRV_PCM_TRIGGER_RESUME: case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: ssc_writex(params->ssc->regs, ATMEL_PDC_PTCR, params->mask->pdc_enable); break; default: ret = -EINVAL; } return ret; } static snd_pcm_uframes_t at32_pcm_pointer(struct snd_pcm_substream *substream) { struct snd_pcm_runtime *runtime = substream->runtime; struct at32_runtime_data *prtd = runtime->private_data; struct at32_pcm_dma_params *params = prtd->params; dma_addr_t ptr; snd_pcm_uframes_t x; ptr = (dma_addr_t) ssc_readx(params->ssc->regs, params->pdc->xpr); x = bytes_to_frames(runtime, ptr - prtd->dma_buffer); if (x == runtime->buffer_size) x = 0; return x; } static int at32_pcm_open(struct snd_pcm_substream *substream) { struct snd_pcm_runtime *runtime = substream->runtime; struct at32_runtime_data *prtd; int ret = 0; snd_soc_set_runtime_hwparams(substream, &at32_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 out; prtd = kzalloc(sizeof(*prtd), GFP_KERNEL); if (prtd == NULL) { ret = -ENOMEM; goto out; } runtime->private_data = prtd; out: return ret; } static int at32_pcm_close(struct snd_pcm_substream *substream) { struct at32_runtime_data *prtd = substream->runtime->private_data; kfree(prtd); return 0; } static int at32_pcm_mmap(struct snd_pcm_substream *substream, struct vm_area_struct *vma) { return remap_pfn_range(vma, vma->vm_start, substream->dma_buffer.addr >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot); } static struct snd_pcm_ops at32_pcm_ops = { .open = at32_pcm_open, .close = at32_pcm_close, .ioctl = snd_pcm_lib_ioctl, .hw_params = at32_pcm_hw_params, .hw_free = at32_pcm_hw_free, .prepare = at32_pcm_prepare, .trigger = at32_pcm_trigger, .pointer = at32_pcm_pointer, .mmap = at32_pcm_mmap, }; /*--------------------------------------------------------------------------*\ * ASoC platform driver \*--------------------------------------------------------------------------*/ static u64 at32_pcm_dmamask = 0xffffffff; static int at32_pcm_new(struct snd_card *card, struct snd_soc_dai *dai, struct snd_pcm *pcm) { int ret = 0; if (!card->dev->dma_mask) card->dev->dma_mask = &at32_pcm_dmamask; if (!card->dev->coherent_dma_mask) card->dev->coherent_dma_mask = 0xffffffff; if (dai->playback.channels_min) { ret = at32_pcm_preallocate_dma_buffer( pcm, SNDRV_PCM_STREAM_PLAYBACK); if (ret) goto out; } if (dai->capture.channels_min) { pr_debug("at32-pcm: Allocating PCM capture DMA buffer\n"); ret = at32_pcm_preallocate_dma_buffer( pcm, SNDRV_PCM_STREAM_CAPTURE); if (ret) goto out; } out: return ret; } static void at32_pcm_free_dma_buffers(struct snd_pcm *pcm) { struct snd_pcm_substream *substream; struct snd_dma_buffer *buf; int stream; for (stream = 0; stream < 2; stream++) { substream = pcm->streams[stream].substream; if (substream == NULL) continue; buf = &substream->dma_buffer; if (!buf->area) continue; dma_free_coherent(pcm->card->dev, buf->bytes, buf->area, buf->addr); buf->area = NULL; } } #ifdef CONFIG_PM static int at32_pcm_suspend(struct platform_device *pdev, struct snd_soc_dai *dai) { struct snd_pcm_runtime *runtime = dai->runtime; struct at32_runtime_data *prtd; struct at32_pcm_dma_params *params; if (runtime == NULL) return 0; prtd = runtime->private_data; params = prtd->params; /* Disable the PDC and save the PDC registers */ ssc_writex(params->ssc->regs, PDC_PTCR, params->mask->pdc_disable); prtd->pdc_xpr_save = ssc_readx(params->ssc->regs, params->pdc->xpr); prtd->pdc_xcr_save = ssc_readx(params->ssc->regs, params->pdc->xcr); prtd->pdc_xnpr_save = ssc_readx(params->ssc->regs, params->pdc->xnpr); prtd->pdc_xncr_save = ssc_readx(params->ssc->regs, params->pdc->xncr); return 0; } static int at32_pcm_resume(struct platform_device *pdev, struct snd_soc_dai *dai) { struct snd_pcm_runtime *runtime = dai->runtime; struct at32_runtime_data *prtd; struct at32_pcm_dma_params *params; if (runtime == NULL) return 0; prtd = runtime->private_data; params = prtd->params; /* Restore the PDC registers and enable the PDC */ ssc_writex(params->ssc->regs, params->pdc->xpr, prtd->pdc_xpr_save); ssc_writex(params->ssc->regs, params->pdc->xcr, prtd->pdc_xcr_save); ssc_writex(params->ssc->regs, params->pdc->xnpr, prtd->pdc_xnpr_save); ssc_writex(params->ssc->regs, params->pdc->xncr, prtd->pdc_xncr_save); ssc_writex(params->ssc->regs, PDC_PTCR, params->mask->pdc_enable); return 0; } #else /* CONFIG_PM */ # define at32_pcm_suspend NULL # define at32_pcm_resume NULL #endif /* CONFIG_PM */ struct snd_soc_platform at32_soc_platform = { .name = "at32-audio", .pcm_ops = &at32_pcm_ops, .pcm_new = at32_pcm_new, .pcm_free = at32_pcm_free_dma_buffers, .suspend = at32_pcm_suspend, .resume = at32_pcm_resume, }; EXPORT_SYMBOL_GPL(at32_soc_platform); MODULE_AUTHOR("Geoffrey Wossum <gwossum@acm.org>"); MODULE_DESCRIPTION("Atmel AT32 PCM module"); MODULE_LICENSE("GPL");