[ARM] tegra: generic driver for i2s audio (initial implementation)
authorIliyan Malchev <malchev@google.com>
Thu, 29 Jul 2010 21:00:54 +0000 (14:00 -0700)
committerColin Cross <ccross@android.com>
Wed, 6 Oct 2010 23:27:22 +0000 (16:27 -0700)
-- i2s settings are passed through the board file
-- supports playback (no recording yet)
-- works in DMA and PIO (non-DMA) modes (toggle through debugfs)
-- does NOT perform volume and audio-path control
-- exports /dev/audio<n>_{in, out}, where <n> is the i2s interface
-- assumes that i2s is used such that fifo1 is TX and fifo2 is RX

Signed-off-by: Iliyan Malchev <malchev@google.com>
arch/arm/mach-tegra/Makefile
arch/arm/mach-tegra/include/mach/audio.h [new file with mode: 0644]
arch/arm/mach-tegra/tegra_i2s_audio.c [new file with mode: 0644]

index 45fd48880f652aa96cb5ab409e0a078f0f29a404..c51686343e1a03e905fca13451c284ce1f617452 100644 (file)
@@ -10,6 +10,7 @@ obj-y                                   += delay.o
 obj-y                                   += powergate.o
 obj-y                                  += suspend.o
 obj-y                                  += fuse.o
+obj-y                                  += tegra_i2s_audio.o
 obj-$(CONFIG_FIQ)                       += fiq.o
 obj-y                                   += usb_phy.o
 
diff --git a/arch/arm/mach-tegra/include/mach/audio.h b/arch/arm/mach-tegra/include/mach/audio.h
new file mode 100644 (file)
index 0000000..1e65b7c
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * arch/arm/mach-tegra/include/mach/audio.h
+ *
+ * Copyright (C) 2010 Google, Inc.
+ *
+ * Author:
+ *     Iliyan Malchev <malchev@google.com>
+ *
+ * This software is licensed under the terms of the GNU General Public
+ * License version 2, as published by the Free Software Foundation, and
+ * may be copied, distributed, and modified under those terms.
+ *
+ * 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.
+ *
+ */
+
+#ifndef __ARCH_ARM_MACH_TEGRA_AUDIO_H
+#define __ARCH_ARM_MACH_TEGRA_AUDIO_H
+
+#include <linux/kernel.h>
+#include <linux/types.h>
+#include <mach/i2s.h>
+
+#define FIFO1          0
+#define FIFO2          1
+
+/* FIXME: this is not enforced by the hardware. */
+#define I2S_FIFO_TX             FIFO1
+#define I2S_FIFO_RX             FIFO2
+
+struct tegra_audio_platform_data {
+       bool master;
+       bool dma_on;
+       unsigned long i2s_clk_rate;
+       const char *dap_clk;
+       const char *audio_sync_clk;
+
+       int mode; /* I2S, LJM, RJM, etc. */
+       int fifo_fmt;
+       int bit_size;
+};
+
+#endif /* __ARCH_ARM_MACH_TEGRA_AUDIO_H */
diff --git a/arch/arm/mach-tegra/tegra_i2s_audio.c b/arch/arm/mach-tegra/tegra_i2s_audio.c
new file mode 100644 (file)
index 0000000..84928e6
--- /dev/null
@@ -0,0 +1,1099 @@
+/*
+ * arch/arm/mach-tegra/tegra_i2s_audio.c
+ *
+ * Copyright (C) 2010 Google, Inc.
+ *
+ * Author:
+ *      Iliyan Malchev <malchev@google.com>
+ *
+ * This software is licensed under the terms of the GNU General Public
+ * License version 2, as published by the Free Software Foundation, and
+ * may be copied, distributed, and modified under those terms.
+ *
+ * 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.
+ *
+ */
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/miscdevice.h>
+#include <linux/fs.h>
+#include <linux/mutex.h>
+#include <linux/clk.h>
+#include <linux/interrupt.h>
+#include <linux/slab.h>
+#include <linux/list.h>
+#include <linux/spinlock.h>
+#include <linux/uaccess.h>
+#include <linux/dma-mapping.h>
+#include <linux/dmapool.h>
+#include <linux/err.h>
+#include <linux/spi/spi.h>
+#include <linux/kfifo.h>
+#include <linux/debugfs.h>
+#include <linux/completion.h>
+#include <linux/platform_device.h>
+#include <linux/io.h>
+
+#include <mach/dma.h>
+#include <mach/iomap.h>
+#include <mach/i2s.h>
+#include <mach/audio.h>
+#include <mach/irqs.h>
+
+#include "clock.h"
+
+/* per stream (input/output) */
+struct audio_out_stream {
+       int opened;
+       struct mutex lock;
+
+       bool playback;
+       spinlock_t pcm_out_lock;
+       void *buffer;
+       dma_addr_t buf_phys;
+       struct kfifo fifo;
+       struct completion fifo_completion;
+
+       struct tegra_dma_channel *tx_dma;
+       struct tegra_dma_req tx_dma_req;
+};
+
+struct i2s_pio_stats {
+       u32 i2s_interrupt_count;
+       u32 tx_fifo_errors;
+       u32 rx_fifo_errors;
+       u32 tx_fifo_written;
+       u32 rx_fifo_read;
+};
+
+/* per i2s controller */
+struct audio_driver_state {
+       struct list_head next;
+
+       struct platform_device *pdev;
+       struct tegra_audio_platform_data *pdata;
+       phys_addr_t i2s_phys;
+       unsigned long i2s_base;
+
+       bool using_dma;
+       unsigned long dma_req_sel;
+
+       int irq; /* for pio mode */
+       struct i2s_pio_stats pio_stats;
+
+       struct miscdevice misc_out;
+       struct audio_out_stream out;
+
+       struct miscdevice misc_in;
+/*     struct audio_in_stream in; */
+};
+
+static inline struct audio_driver_state *ads_from_misc(struct file *file)
+{
+       struct miscdevice *m = file->private_data;
+       return container_of(m, struct audio_driver_state, misc_out);
+}
+
+static inline struct audio_driver_state *ads_from_out(
+                       struct audio_out_stream *aos)
+{
+       return container_of(aos, struct audio_driver_state, out);
+}
+
+#define I2S_I2S_FIFO_TX_BUSY   I2S_I2S_STATUS_FIFO1_BSY
+#define I2S_I2S_FIFO_TX_QS     I2S_I2S_STATUS_QS_FIFO1
+#define I2S_I2S_FIFO_TX_ERR    I2S_I2S_STATUS_FIFO1_ERR
+
+#define I2S_I2S_FIFO_RX_BUSY   I2S_I2S_STATUS_FIFO2_BSY
+#define I2S_I2S_FIFO_RX_QS     I2S_I2S_STATUS_QS_FIFO2
+#define I2S_I2S_FIFO_RX_ERR    I2S_I2S_STATUS_FIFO2_ERR
+
+#define I2S_FIFO_ERR (I2S_I2S_STATUS_FIFO1_ERR | I2S_I2S_STATUS_FIFO2_ERR)
+
+static inline void i2s_writel(unsigned long base, u32 val, u32 reg)
+{
+       writel(val, base + reg);
+}
+
+static inline u32 i2s_readl(unsigned long base, u32 reg)
+{
+       return readl(base + reg);
+}
+
+static inline void i2s_fifo_write(unsigned long base, int fifo, u32 data)
+{
+       i2s_writel(base, data, fifo ? I2S_I2S_FIFO2_0 : I2S_I2S_FIFO1_0);
+}
+
+static inline u32 i2s_fifo_read(unsigned long base, int fifo)
+{
+       return i2s_readl(base, fifo ? I2S_I2S_FIFO2_0 : I2S_I2S_FIFO1_0);
+}
+
+static int i2s_set_channel_bit_count(unsigned long base,
+                       int sampling, int bitclk)
+{
+       u32 val;
+       int bitcnt = bitclk / (2 * sampling) - 1;
+
+       if (bitcnt < 0 || bitcnt >= 1<<11) {
+               pr_err("%s: bit count %d is out of bounds\n", __func__,
+                       bitcnt);
+               return -EINVAL;
+       }
+
+       val = bitcnt;
+       if (bitclk % (2 * sampling)) {
+               pr_info("%s: enabling non-symmetric mode\n", __func__);
+               val |= I2S_I2S_TIMING_NON_SYM_ENABLE;
+       }
+
+       pr_debug("%s: I2S_I2S_TIMING_0 = %08x\n", __func__, val);
+       i2s_writel(base, val, I2S_I2S_TIMING_0);
+       return 0;
+}
+
+static void i2s_set_fifo_mode(unsigned long base, int fifo, int tx)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       if (fifo == 0) {
+               val &= ~I2S_I2S_CTRL_FIFO1_RX_ENABLE;
+               val |= (!tx) ? I2S_I2S_CTRL_FIFO1_RX_ENABLE : 0;
+       } else {
+               val &= ~I2S_I2S_CTRL_FIFO2_TX_ENABLE;
+               val |= tx ? I2S_I2S_CTRL_FIFO2_TX_ENABLE : 0;
+       }
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+}
+
+static int i2s_fifo_set_attention_level(unsigned long base,
+                       int fifo, unsigned level)
+{
+       u32 val;
+
+       if (level > I2S_FIFO_ATN_LVL_TWELVE_SLOTS) {
+               pr_err("%s: invalid fifo level selector %d\n", __func__,
+                       level);
+               return -EINVAL;
+       }
+
+       val = i2s_readl(base, I2S_I2S_FIFO_SCR_0);
+
+       if (!fifo) {
+               val &= ~I2S_I2S_FIFO_SCR_FIFO1_ATN_LVL_MASK;
+               val |= level << I2S_FIFO1_ATN_LVL_SHIFT;
+       } else {
+               val &= ~I2S_I2S_FIFO_SCR_FIFO2_ATN_LVL_MASK;
+               val |= level << I2S_FIFO2_ATN_LVL_SHIFT;
+       }
+
+       i2s_writel(base, val, I2S_I2S_FIFO_SCR_0);
+       return 0;
+}
+
+static void i2s_fifo_enable(unsigned long base, int fifo, int on)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       if (!fifo) {
+               val &= ~I2S_I2S_CTRL_FIFO1_ENABLE;
+               val |= on ? I2S_I2S_CTRL_FIFO1_ENABLE : 0;
+       } else {
+               val &= ~I2S_I2S_CTRL_FIFO2_ENABLE;
+               val |= on ? I2S_I2S_CTRL_FIFO2_ENABLE : 0;
+       }
+
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+}
+
+static void i2s_fifo_clear(unsigned long base, int fifo)
+{
+       u32 val = i2s_readl(base, I2S_I2S_FIFO_SCR_0);
+       if (!fifo) {
+               val &= ~I2S_I2S_FIFO_SCR_FIFO1_CLR;
+               val |= I2S_I2S_FIFO_SCR_FIFO1_CLR;
+#if 0
+               /* Per Nvidia, reduces pop on the next run. */
+               if (!(val & I2S_I2S_CTRL_FIFO1_RX_ENABLE)) {
+                       int cnt = 16;
+                       while (cnt--)
+                               i2s_writel(base, 0, I2S_I2S_FIFO1_0);
+               }
+#endif
+       } else {
+               val &= ~I2S_I2S_FIFO_SCR_FIFO2_CLR;
+               val |= I2S_I2S_FIFO_SCR_FIFO2_CLR;
+       }
+
+       i2s_writel(base, val, I2S_I2S_FIFO_SCR_0);
+}
+
+static void i2s_set_master(unsigned long base, int master)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       val &= ~I2S_I2S_CTRL_MASTER_ENABLE;
+       val |= master ? I2S_I2S_CTRL_MASTER_ENABLE : 0;
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+}
+
+static int i2s_set_bit_format(unsigned long base, unsigned fmt)
+{
+       u32 val;
+
+       if (fmt > I2S_BIT_FORMAT_DSP) {
+               pr_err("%s: invalid bit-format selector %d\n", __func__, fmt);
+               return -EINVAL;
+       }
+
+       val = i2s_readl(base, I2S_I2S_CTRL_0);
+       val &= ~I2S_I2S_CTRL_BIT_FORMAT_MASK;
+       val |= fmt << I2S_BIT_FORMAT_SHIFT;
+
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+       return 0;
+}
+
+static int i2s_set_bit_size(unsigned long base, unsigned bit_size)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       val &= ~I2S_I2S_CTRL_BIT_SIZE_MASK;
+
+       if (bit_size > I2S_BIT_SIZE_32) {
+               pr_err("%s: invalid bit_size selector %d\n", __func__,
+                       bit_size);
+               return -EINVAL;
+       }
+
+       val |= bit_size << I2S_BIT_SIZE_SHIFT;
+
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+       return 0;
+}
+
+static int i2s_set_fifo_format(unsigned long base, unsigned fmt)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       val &= ~I2S_I2S_CTRL_FIFO_FORMAT_MASK;
+
+       if (fmt > I2S_FIFO_32 && fmt != I2S_FIFO_PACKED) {
+               pr_err("%s: invalid fmt selector %d\n", __func__, fmt);
+               return -EINVAL;
+       }
+
+       val |= fmt << I2S_FIFO_SHIFT;
+
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+       return 0;
+}
+
+static void i2s_set_left_right_control_polarity(unsigned long base,
+               int high_low)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       val &= ~I2S_I2S_CTRL_L_R_CTRL;
+       val |= high_low ? I2S_I2S_CTRL_L_R_CTRL : 0;
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+}
+
+static void i2s_set_fifo_irq_on_err(unsigned long base, int fifo, int on)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       if (!fifo) {
+               val &= ~I2S_I2S_IE_FIFO1_ERR;
+               val |= on ? I2S_I2S_IE_FIFO1_ERR : 0;
+       } else {
+               val &= ~I2S_I2S_IE_FIFO2_ERR;
+               val |= on ? I2S_I2S_IE_FIFO2_ERR : 0;
+       }
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+}
+
+static void i2s_set_fifo_irq_on_qe(unsigned long base, int fifo, int on)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       if (!fifo) {
+               val &= ~I2S_I2S_QE_FIFO1;
+               val |= on ? I2S_I2S_QE_FIFO1 : 0;
+       } else {
+               val &= ~I2S_I2S_QE_FIFO2;
+               val |= on ? I2S_I2S_QE_FIFO2 : 0;
+       }
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+}
+
+static void i2s_enable_fifos(unsigned long base, int on)
+{
+       u32 val = i2s_readl(base, I2S_I2S_CTRL_0);
+       if (on)
+               val |= I2S_I2S_QE_FIFO1 | I2S_I2S_QE_FIFO2 |
+                      I2S_I2S_IE_FIFO1_ERR | I2S_I2S_IE_FIFO2_ERR;
+       else
+               val &= ~(I2S_I2S_QE_FIFO1 | I2S_I2S_QE_FIFO2 |
+                        I2S_I2S_IE_FIFO1_ERR | I2S_I2S_IE_FIFO2_ERR);
+
+       i2s_writel(base, val, I2S_I2S_CTRL_0);
+}
+
+static inline u32 i2s_get_status(unsigned long base)
+{
+       return i2s_readl(base, I2S_I2S_STATUS_0);
+}
+
+static inline u32 i2s_get_control(unsigned long base)
+{
+       return i2s_readl(base, I2S_I2S_CTRL_0);
+}
+
+static inline void i2s_ack_status(unsigned long base)
+{
+       return i2s_writel(base, i2s_readl(base, I2S_I2S_STATUS_0),
+                               I2S_I2S_STATUS_0);
+}
+
+static inline u32 i2s_get_fifo_scr(unsigned long base)
+{
+       return i2s_readl(base, I2S_I2S_FIFO_SCR_0);
+}
+
+static inline phys_addr_t i2s_get_fifo_phy_base(unsigned long phy_base,
+               int fifo)
+{
+       return phy_base + (fifo ? I2S_I2S_FIFO2_0 : I2S_I2S_FIFO1_0);
+}
+
+static inline u32 i2s_get_fifo_full_empty_count(unsigned long base, int fifo)
+{
+       u32 val = i2s_readl(base, I2S_I2S_FIFO_SCR_0);
+
+       if (!fifo)
+               val = val >> I2S_I2S_FIFO_SCR_FIFO1_FULL_EMPTY_COUNT_SHIFT;
+       else
+               val = val >> I2S_I2S_FIFO_SCR_FIFO2_FULL_EMPTY_COUNT_SHIFT;
+
+       return val & I2S_I2S_FIFO_SCR_FIFO_FULL_EMPTY_COUNT_MASK;
+}
+
+/* FIXME: add an ioctl to report the buffer sizes */
+
+#define PCM_OUT_BUFFER_SIZE    (PAGE_SIZE*4)
+#define PCM_OUT_DMA_CHUNK      (PAGE_SIZE)
+#define PCM_OUT_THRESHOLD      (PAGE_SIZE*2)
+
+#define PLAYBACK_STARTED true
+#define PLAYBACK_STOPPED false
+
+static int setup_dma(struct audio_driver_state *);
+static void tear_down_dma(struct audio_driver_state *);
+static void start_dma_playback(struct audio_out_stream *);
+static void stop_dma_playback(struct audio_out_stream *);
+
+static int setup_pio(struct audio_driver_state *);
+static void tear_down_pio(struct audio_driver_state *);
+static void start_pio_playback(struct audio_out_stream *);
+static void stop_pio_playback(struct audio_out_stream *);
+
+struct sound_ops {
+       int (*setup)(struct audio_driver_state *);
+       void (*tear_down)(struct audio_driver_state *);
+       void (*start_playback)(struct audio_out_stream *);
+       void (*stop_playback)(struct audio_out_stream *);
+};
+
+static const struct sound_ops dma_sound_ops = {
+       .setup = setup_dma,
+       .tear_down = tear_down_dma,
+       .start_playback = start_dma_playback,
+       .stop_playback = stop_dma_playback
+};
+
+static const struct sound_ops pio_sound_ops = {
+       .setup = setup_pio,
+       .tear_down = tear_down_pio,
+       .start_playback = start_pio_playback,
+       .stop_playback = stop_pio_playback
+};
+
+static const struct sound_ops *sound_ops = &dma_sound_ops;
+
+static void start_playback(struct audio_out_stream *aos)
+{
+       sound_ops->start_playback(aos);
+       aos->playback = PLAYBACK_STARTED;
+}
+
+static void start_playback_if_necessary(struct audio_out_stream *aos)
+{
+       unsigned long flags;
+       spin_lock_irqsave(&aos->pcm_out_lock, flags);
+       if (aos->playback == PLAYBACK_STOPPED) {
+               pr_debug("%s: starting playback\n", __func__);
+               start_playback(aos);
+       } else
+               pr_debug("%s: playback already started\n", __func__);
+       spin_unlock_irqrestore(&aos->pcm_out_lock, flags);
+}
+
+static void stop_playback(struct audio_out_stream *aos)
+{
+       aos->playback = PLAYBACK_STOPPED;
+       sound_ops->stop_playback(aos);
+}
+
+static bool stop_playback_if_necessary(struct audio_out_stream *aos)
+{
+       unsigned long flags;
+       spin_lock_irqsave(&aos->pcm_out_lock, flags);
+       if (kfifo_is_empty(&aos->fifo)) {
+               stop_playback(aos);
+               spin_unlock_irqrestore(&aos->pcm_out_lock, flags);
+               return true;
+       }
+       spin_unlock_irqrestore(&aos->pcm_out_lock, flags);
+
+       return false;
+}
+
+static void toggle_dma(struct audio_driver_state *ads)
+{
+       pr_info("%s: %s\n", __func__, ads->using_dma ? "pio" : "dma");
+       sound_ops->tear_down(ads);
+       sound_ops = ads->using_dma ? &pio_sound_ops : &dma_sound_ops;
+       sound_ops->setup(ads);
+       ads->using_dma = !ads->using_dma;
+}
+
+/* DMA */
+
+static void resume_dma_playback(struct audio_out_stream *aos);
+
+static void setup_dma_tx_request(struct tegra_dma_req *req,
+               struct audio_out_stream *aos);
+
+static int setup_dma(struct audio_driver_state *ads)
+{
+       pr_info("%s\n", __func__);
+
+       ads->out.buf_phys = dma_map_single(NULL, ads->out.buffer,
+                               PCM_OUT_BUFFER_SIZE, DMA_TO_DEVICE);
+       BUG_ON(!ads->out.buf_phys);
+
+       /* setup audio playback */
+       setup_dma_tx_request(&ads->out.tx_dma_req, &ads->out);
+       ads->out.tx_dma = tegra_dma_allocate_channel(TEGRA_DMA_MODE_ONESHOT);
+       if (IS_ERR(ads->out.tx_dma)) {
+               pr_err("%s: could not allocate DMA channel for I2S: %ld\n",
+                       __func__, PTR_ERR(ads->out.tx_dma));
+               return PTR_ERR(ads->out.tx_dma);
+       }
+
+       /* TODO: setup recording */
+
+       return 0;
+}
+
+static void tear_down_dma(struct audio_driver_state *ads)
+{
+       pr_info("%s\n", __func__);
+       tegra_dma_free_channel(ads->out.tx_dma);
+       ads->out.tx_dma = NULL;
+       dma_unmap_single(&ads->pdev->dev, ads->out.buf_phys,
+                               PCM_OUT_BUFFER_SIZE,
+                               DMA_TO_DEVICE);
+       ads->out.buf_phys = 0;
+}
+
+static void dma_tx_complete_callback(struct tegra_dma_req *req)
+{
+       struct audio_out_stream *aos = req->dev;
+       int count = req->bytes_transferred;
+
+       pr_debug("%s bytes transferred %d\n", __func__, count);
+
+       kfifo_skip(&aos->fifo, count);
+
+       if (kfifo_avail(&aos->fifo) > PCM_OUT_THRESHOLD &&
+                       !completion_done(&aos->fifo_completion)) {
+               pr_debug("%s: complete (%d avail)\n", __func__,
+                               kfifo_avail(&aos->fifo));
+               complete(&aos->fifo_completion);
+       }
+
+       if (stop_playback_if_necessary(aos))
+               return;
+       resume_dma_playback(aos);
+}
+
+static void setup_dma_tx_request(struct tegra_dma_req *req,
+               struct audio_out_stream *aos)
+{
+       struct audio_driver_state *ads = ads_from_out(aos);
+
+       memset(req, 0, sizeof(*req));
+
+       /* FIXME:  need to set instance? */
+       req->complete = dma_tx_complete_callback;
+       req->dev = aos;
+       req->to_memory = false;
+       req->dest_addr = i2s_get_fifo_phy_base(ads->i2s_phys, I2S_FIFO_TX);
+       req->dest_wrap = 4;
+       req->dest_bus_width = 16;
+       req->source_bus_width = 32;
+       req->source_wrap = 0;
+       req->req_sel = ads->dma_req_sel;
+}
+
+static void resume_dma_playback(struct audio_out_stream *aos)
+{
+       struct audio_driver_state *ads = ads_from_out(aos);
+       struct tegra_dma_req *req = &aos->tx_dma_req;
+
+       unsigned out = __kfifo_off(&aos->fifo, aos->fifo.out);
+       unsigned in = __kfifo_off(&aos->fifo, aos->fifo.in);
+
+       BUG_ON(!kfifo_len(&aos->fifo));
+
+       req->source_addr = aos->buf_phys + out;
+       if (out < in)
+               req->size = in - out;
+       else
+               req->size = kfifo_size(&aos->fifo) - out;
+
+       dma_sync_single_for_device(NULL,
+                       aos->buf_phys + out, req->size, DMA_TO_DEVICE);
+
+       /* Don't send all the data yet. */
+       if (req->size > PCM_OUT_DMA_CHUNK)
+               req->size = PCM_OUT_DMA_CHUNK;
+       pr_debug("%s resume playback (%d in fifo, writing %d, in %d out %d)\n",
+                       __func__, kfifo_len(&aos->fifo), req->size, in, out);
+
+       i2s_fifo_set_attention_level(ads->i2s_base,
+                       I2S_FIFO_TX, I2S_FIFO_ATN_LVL_FOUR_SLOTS);
+       i2s_fifo_enable(ads->i2s_base, I2S_FIFO_TX, 1);
+       tegra_dma_enqueue_req(aos->tx_dma, req);
+}
+
+static void start_dma_playback(struct audio_out_stream *aos)
+{
+       resume_dma_playback(aos);
+}
+
+static void stop_dma_playback(struct audio_out_stream *aos)
+{
+       struct audio_driver_state *ads = ads_from_out(aos);
+       pr_debug("%s\n", __func__);
+       i2s_fifo_enable(ads->i2s_base, I2S_FIFO_TX, 0);
+       while (i2s_get_status(ads->i2s_base) & I2S_I2S_FIFO_TX_BUSY)
+               /* spin */;
+}
+
+/* PIO (non-DMA) */
+
+static irqreturn_t i2s_interrupt(int irq, void *data);
+
+static int setup_pio(struct audio_driver_state *ads)
+{
+       pr_info("%s\n", __func__);
+       enable_irq(ads->irq);
+       return 0;
+}
+
+static void tear_down_pio(struct audio_driver_state *ads)
+{
+       pr_info("%s\n", __func__);
+       disable_irq(ads->irq);
+}
+
+static void start_pio_playback(struct audio_out_stream *aos)
+{
+       struct audio_driver_state *ads = ads_from_out(aos);
+
+       pr_debug("%s\n", __func__);
+
+       i2s_fifo_set_attention_level(ads->i2s_base,
+                       I2S_FIFO_TX, I2S_FIFO_ATN_LVL_ONE_SLOT);
+       i2s_fifo_clear(ads->i2s_base, I2S_FIFO_TX);
+
+       i2s_set_fifo_irq_on_err(ads->i2s_base, I2S_FIFO_TX, 1);
+       i2s_set_fifo_irq_on_qe(ads->i2s_base, I2S_FIFO_TX, 1);
+}
+
+static void stop_pio_playback(struct audio_out_stream *aos)
+{
+       struct audio_driver_state *ads = ads_from_out(aos);
+
+       i2s_set_fifo_irq_on_err(ads->i2s_base, I2S_FIFO_TX, 0);
+       i2s_set_fifo_irq_on_qe(ads->i2s_base, I2S_FIFO_TX, 0);
+       i2s_fifo_enable(ads->i2s_base, I2S_FIFO_TX, 0);
+       while (i2s_get_status(ads->i2s_base) & I2S_I2S_FIFO_TX_BUSY)
+               /* spin */;
+
+       pr_info("%s: interrupts %d\n", __func__,
+                       ads->pio_stats.i2s_interrupt_count);
+       pr_info("%s: sent       %d\n", __func__,
+                       ads->pio_stats.tx_fifo_written);
+       pr_info("%s: received   %d\n", __func__,
+                       ads->pio_stats.rx_fifo_read);
+       pr_info("%s: tx errors  %d\n", __func__,
+                       ads->pio_stats.tx_fifo_errors);
+       pr_info("%s: rx errors  %d\n", __func__,
+                       ads->pio_stats.rx_fifo_errors);
+
+       memset(&ads->pio_stats, 0, sizeof(ads->pio_stats));
+}
+
+static irqreturn_t i2s_interrupt(int irq, void *data)
+{
+       struct audio_driver_state *ads = data;
+       u32 status = i2s_get_status(ads->i2s_base);
+
+       pr_debug("%s: %08x\n", __func__, status);
+
+       ads->pio_stats.i2s_interrupt_count++;
+
+       if (status & I2S_I2S_FIFO_TX_ERR)
+               ads->pio_stats.tx_fifo_errors++;
+
+       if (status & I2S_I2S_FIFO_RX_ERR)
+               ads->pio_stats.rx_fifo_errors++;
+
+       if (status & I2S_FIFO_ERR)
+               i2s_ack_status(ads->i2s_base);
+
+       if (status & I2S_I2S_FIFO_TX_QS) {
+               int written;
+               int empty;
+               int len;
+               u16 fifo_buffer[32];
+
+               struct audio_out_stream *out = &ads->out;
+
+               pr_debug("%s fifo is ready\n", __func__);
+
+               if (kfifo_avail(&out->fifo) > PCM_OUT_THRESHOLD &&
+                               !completion_done(&out->fifo_completion)) {
+                       pr_debug("%s: complete (%d avail)\n", __func__,
+                                       kfifo_avail(&out->fifo));
+                       complete(&out->fifo_completion);
+               }
+
+               if (stop_playback_if_necessary(out))
+                       goto done;
+
+               empty = i2s_get_fifo_full_empty_count(ads->i2s_base,
+                               I2S_FIFO_TX);
+
+               len = kfifo_out(&out->fifo, fifo_buffer,
+                               empty * sizeof(u16));
+               len /= sizeof(u16);
+
+               written = 0;
+               while (empty-- && written < len) {
+                       ads->pio_stats.tx_fifo_written += written * sizeof(u16);
+                       i2s_fifo_write(ads->i2s_base,
+                                       I2S_FIFO_TX, fifo_buffer[written++]);
+               }
+
+               /* TODO: Should we check to see if we wrote less than the
+                * FIFO threshold and adjust it if so?
+                */
+
+               if (written) {
+                       /* start the transaction */
+                       pr_debug("%s: enabling fifo (%d samples written)\n",
+                                       __func__, written);
+                       i2s_fifo_enable(ads->i2s_base, I2S_FIFO_TX, 1);
+               }
+       }
+
+       if (status & I2S_I2S_FIFO_RX_QS) {
+               int read = 0;
+               while (i2s_get_fifo_full_empty_count(ads->i2s_base,
+                               I2S_FIFO_RX)) {
+                       i2s_fifo_read(ads->i2s_base, I2S_FIFO_RX);
+                       read++;
+               }
+               ads->pio_stats.rx_fifo_read += read;
+               pr_debug("%s: rx fifo empty (%d read)\n", __func__, read);
+       }
+
+done:
+       pr_debug("%s: done %08x\n", __func__, i2s_get_status(ads->i2s_base));
+       return IRQ_HANDLED;
+}
+
+static ssize_t tegra_audio_write(struct file *file,
+               const char __user *buf, size_t size, loff_t *off)
+{
+       ssize_t rc, total = 0;
+       unsigned nw = 0;
+
+       struct audio_driver_state *ads = ads_from_misc(file);
+
+       mutex_lock(&ads->out.lock);
+
+       if (!IS_ALIGNED(size, 4)) {
+               pr_err("%s: user size request %d not aligned to 4\n",
+                       __func__, size);
+               rc = -EINVAL;
+       }
+
+       pr_debug("%s: write %d bytes, %d available\n", __func__,
+                       size, kfifo_avail(&ads->out.fifo));
+
+again:
+       rc = kfifo_from_user(&ads->out.fifo, buf + total, size - total, &nw);
+       if (rc < 0) {
+               pr_err("%s: error copying from user\n", __func__);
+               goto done;
+       }
+
+       total += nw;
+       if (total < size) {
+               pr_debug("%s: sleep (user %d total %d nw %d)\n", __func__,
+                               size, total, nw);
+               mutex_unlock(&ads->out.lock);
+               rc = wait_for_completion_interruptible(
+                               &ads->out.fifo_completion);
+               mutex_lock(&ads->out.lock);
+               if (rc == -ERESTARTSYS) {
+                       pr_warn("%s: interrupted\n", __func__);
+                       goto done;
+               }
+               pr_debug("%s: awake\n", __func__);
+               goto again;
+       }
+
+       rc = total;
+       *off += total;
+
+       start_playback_if_necessary(&ads->out);
+done:
+       mutex_unlock(&ads->out.lock);
+       return rc;
+}
+
+static int tegra_audio_out_open(struct inode *inode, struct file *file)
+{
+       struct audio_driver_state *ads = ads_from_misc(file);
+
+       mutex_lock(&ads->out.lock);
+       if (!ads->out.opened++)
+               kfifo_reset(&ads->out.fifo);
+       mutex_unlock(&ads->out.lock);
+
+       return 0;
+}
+
+static int tegra_audio_out_release(struct inode *inode, struct file *file)
+{
+       struct audio_driver_state *ads = ads_from_misc(file);
+
+       mutex_lock(&ads->out.lock);
+       if (ads->out.opened)
+               ads->out.opened--;
+       mutex_unlock(&ads->out.lock);
+
+       return 0;
+}
+
+static int tegra_audio_in_open(struct inode *inode, struct file *file)
+{
+       return 0;
+}
+
+static int tegra_audio_in_release(struct inode *inode, struct file *file)
+{
+       return 0;
+}
+
+#ifdef CONFIG_DEBUG_FS
+static int debugfs_open(struct inode *inode, struct file *file)
+{
+       file->private_data = inode->i_private;
+       return 0;
+}
+
+static DEFINE_MUTEX(debugfs_lock);
+
+ssize_t debugfs_read(struct file *file, char __user *buf,
+               size_t size, loff_t *off)
+{
+       int rc = 0;
+       struct audio_driver_state *ads = file->private_data;
+       static bool r;
+
+       mutex_lock(&debugfs_lock);
+
+       if (r) {
+               r = false;
+               goto done;
+       }
+
+       if (size < 5) {
+               rc = -ETOOSMALL;
+               goto done;
+       }
+
+       if (copy_to_user(buf, ads->using_dma ? "dma\n" : "pio\n", 5)) {
+               rc = -EFAULT;
+               goto done;
+       }
+
+       r = true;
+       rc = *off = 5;
+done:
+       mutex_unlock(&debugfs_lock);
+       return rc;
+}
+
+ssize_t debugfs_write(struct file *file,
+               const char __user *buf, size_t size, loff_t *off)
+{
+       char cmd[5];
+       int use_dma;
+
+       struct audio_driver_state *ads = file->private_data;
+
+       if (size < 4) {
+               pr_err("%s: buffer size %d too small\n", __func__, size);
+               return -ETOOSMALL;
+       }
+
+       if (copy_from_user(cmd, buf, 4)) {
+               pr_err("%s: could not copy from user\n", __func__);
+               return -EFAULT;
+       }
+       cmd[3] = 0;
+
+       use_dma = 0;
+       if (!strcmp(cmd, "dma"))
+               use_dma = 1;
+       else if (strcmp(cmd, "pio")) {
+               pr_err("%s: invalid string [%s]\n", __func__, cmd);
+               return -EINVAL;
+       }
+
+       /* FIXME: lock the input stream here as well. */
+       mutex_lock(&ads->out.lock);
+       if (ads->out.playback == PLAYBACK_STARTED) {
+               pr_err("%s: playback in progress, try again.\n", __func__);
+               mutex_unlock(&ads->out.lock);
+               return -EBUSY;
+       }
+       if (!!use_dma ^ !!ads->using_dma)
+               toggle_dma(ads);
+       else
+               pr_info("%s: no change\n", __func__);
+       mutex_unlock(&ads->out.lock);
+
+       return 5;
+}
+
+static const struct file_operations debugfs_ops = {
+       .read = debugfs_read,
+       .write = debugfs_write,
+       .open = debugfs_open,
+};
+
+static void setup_tegra_audio_debugfs(struct audio_driver_state *ads)
+{
+       struct dentry *dent;
+
+       dent = debugfs_create_dir("tegra_audio", 0);
+       if (IS_ERR(dent)) {
+               pr_err("%s: could not create dentry\n", __func__);
+               return;
+       }
+
+       debugfs_create_file("dma", 0666, dent, ads, &debugfs_ops);
+}
+#else
+static inline void setup_tegra_audio_debugfs(struct audio_driver_state *ads) {}
+#endif
+
+static const struct file_operations tegra_audio_out_fops = {
+       .owner = THIS_MODULE,
+       .open = tegra_audio_out_open,
+       .release = tegra_audio_out_release,
+       .write = tegra_audio_write,
+};
+
+static const struct file_operations tegra_audio_in_fops = {
+       .owner = THIS_MODULE,
+       .open = tegra_audio_in_open,
+       .release = tegra_audio_in_release,
+};
+
+static int tegra_audio_probe(struct platform_device *pdev)
+{
+       struct resource *res;
+       struct clk *i2s_clk, *audio_sync_clk, *dap_mclk;
+       struct audio_driver_state *state;
+
+       pr_info("%s\n", __func__);
+
+       state = kmalloc(sizeof(*state), GFP_KERNEL);
+       if (!state)
+               return -ENOMEM;
+
+       state->pdev = pdev;
+       state->pdata = pdev->dev.platform_data;
+       BUG_ON(!state->pdata);
+
+       res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+       if (!res) {
+               dev_err(&pdev->dev, "no mem resource!\n");
+               return -ENODEV;
+       }
+
+       if (!request_mem_region(res->start, resource_size(res), pdev->name)) {
+               dev_err(&pdev->dev, "memory region already claimed!\n");
+               return -ENOMEM;
+       }
+
+       state->i2s_phys = res->start;
+       state->i2s_base = (unsigned long)ioremap(res->start,
+                       res->end - res->start + 1);
+       if (!state->i2s_base) {
+               dev_err(&pdev->dev, "cannot remap iomem!\n");
+               return -EIO;
+       }
+
+       res = platform_get_resource(pdev, IORESOURCE_DMA, 0);
+       if (!res) {
+               dev_err(&pdev->dev, "no dma resource!\n");
+               return -ENODEV;
+       }
+       state->dma_req_sel = res->start;
+
+       res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
+       if (!res) {
+               dev_err(&pdev->dev, "no irq resource!\n");
+               return -ENODEV;
+       }
+       state->irq = res->start;
+
+       memset(&state->pio_stats, 0, sizeof(state->pio_stats));
+
+       i2s_clk = clk_get(&pdev->dev, NULL);
+       if (!i2s_clk) {
+               pr_err("%s: could not get i2s1 clock\n", __func__);
+               return -EIO;
+       }
+
+       clk_set_rate(i2s_clk, state->pdata->i2s_clk_rate);
+       if (clk_enable(i2s_clk)) {
+               pr_err("%s: failed to enable i2s1 clock\n", __func__);
+               return -EIO;
+       }
+       pr_info("%s: i2s_clk rate %ld\n", __func__, clk_get_rate(i2s_clk));
+
+       dap_mclk = tegra_get_clock_by_name(state->pdata->dap_clk);
+       if (!dap_mclk) {
+               pr_err("%s: could not get DAP clock\n", __func__);
+               return -EIO;
+       }
+       clk_enable(dap_mclk);
+
+       audio_sync_clk = tegra_get_clock_by_name(state->pdata->audio_sync_clk);
+       if (!audio_sync_clk) {
+               pr_err("%s: could not get audio_2x clock\n", __func__);
+               return -EIO;
+       }
+       clk_enable(audio_sync_clk);
+
+       /* disable interrupts from I2S */
+       i2s_enable_fifos(state->i2s_base, 0);
+
+       i2s_set_left_right_control_polarity(state->i2s_base, 0); /* default */
+
+       if (state->pdata->master)
+               i2s_set_channel_bit_count(state->i2s_base, 44100,
+                               clk_get_rate(i2s_clk));
+       i2s_set_master(state->i2s_base, state->pdata->master);
+
+       i2s_set_fifo_mode(state->i2s_base, FIFO1, 1); /* FIFO1 is TX */
+       i2s_set_fifo_mode(state->i2s_base, FIFO2, 0); /* FIFO2 is RX */
+
+       i2s_set_bit_format(state->i2s_base, state->pdata->mode);
+       i2s_set_bit_size(state->i2s_base, state->pdata->bit_size);
+       i2s_set_fifo_format(state->i2s_base, state->pdata->fifo_fmt);
+
+       state->out.opened = 0;
+       state->out.playback = PLAYBACK_STOPPED;
+       mutex_init(&state->out.lock);
+       init_completion(&state->out.fifo_completion);
+       spin_lock_init(&state->out.pcm_out_lock);
+       state->out.buf_phys = 0;
+       state->out.tx_dma = NULL;
+
+       state->out.buffer = kmalloc(PCM_OUT_BUFFER_SIZE,
+                       GFP_KERNEL | GFP_DMA);
+       if (!state->out.buffer) {
+               pr_err("%s: could not allocate buffer\n", __func__);
+               return -ENOMEM;
+       }
+
+       if (request_irq(state->irq, i2s_interrupt,
+                       IRQF_DISABLED, state->pdev->name, state) < 0) {
+               pr_err("%s: could not register handler for irq %d\n",
+                       __func__, state->irq);
+               return -EIO;
+       }
+
+       kfifo_init(&state->out.fifo, state->out.buffer,
+                       PCM_OUT_BUFFER_SIZE);
+
+       state->using_dma = state->pdata->dma_on;
+       sound_ops->setup(state);
+
+       memset(&state->misc_out, 0, sizeof(state->misc_out));
+       state->misc_out.minor = MISC_DYNAMIC_MINOR;
+       state->misc_out.name  = kmalloc(sizeof("audio_out") + 1, GFP_KERNEL);
+       if (!state->misc_out.name)
+               return -ENOMEM;
+       snprintf((char *)state->misc_out.name, sizeof("audio_out") + 1,
+                       "audio%d_out", state->pdev->id);
+       state->misc_out.fops = &tegra_audio_out_fops;
+       if (misc_register(&state->misc_out)) {
+               pr_err("%s: could not register audio_out\n", __func__);
+               return -EIO;
+       }
+
+       memset(&state->misc_in, 0, sizeof(state->misc_in));
+       state->misc_in.minor = MISC_DYNAMIC_MINOR;
+       state->misc_in.name  = kmalloc(sizeof("audio_in") + 1, GFP_KERNEL);
+       if (!state->misc_in.name)
+               return -ENOMEM;
+       snprintf((char *)state->misc_in.name, sizeof("audio_in") + 1,
+                       "audio%d_in", state->pdev->id);
+       state->misc_in.fops = &tegra_audio_in_fops;
+       if (misc_register(&state->misc_in)) {
+               pr_err("%s: could not register audio_in\n", __func__);
+               return -EIO;
+       }
+
+       setup_tegra_audio_debugfs(state);
+
+       return 0;
+}
+
+static struct platform_driver tegra_audio_driver = {
+       .driver = {
+               .name = "i2s",
+               .owner = THIS_MODULE,
+       },
+       .probe = tegra_audio_probe,
+};
+
+static int __init tegra_audio_init(void)
+{
+       return platform_driver_register(&tegra_audio_driver);
+}
+
+module_init(tegra_audio_init);
+MODULE_LICENSE("GPL");