From: Iliyan Malchev Date: Thu, 29 Jul 2010 21:00:54 +0000 (-0700) Subject: [ARM] tegra: generic driver for i2s audio (initial implementation) X-Git-Tag: firefly_0821_release~9833^2~302 X-Git-Url: http://demsky.eecs.uci.edu/git/?a=commitdiff_plain;h=80a4c65c42369016e41272279aec8944f22b88ed;p=firefly-linux-kernel-4.4.55.git [ARM] tegra: generic driver for i2s audio (initial implementation) -- 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_{in, out}, where is the i2s interface -- assumes that i2s is used such that fifo1 is TX and fifo2 is RX Signed-off-by: Iliyan Malchev --- diff --git a/arch/arm/mach-tegra/Makefile b/arch/arm/mach-tegra/Makefile index 45fd48880f65..c51686343e1a 100644 --- a/arch/arm/mach-tegra/Makefile +++ b/arch/arm/mach-tegra/Makefile @@ -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 index 000000000000..1e65b7c300d5 --- /dev/null +++ b/arch/arm/mach-tegra/include/mach/audio.h @@ -0,0 +1,46 @@ +/* + * arch/arm/mach-tegra/include/mach/audio.h + * + * Copyright (C) 2010 Google, Inc. + * + * Author: + * Iliyan Malchev + * + * 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 +#include +#include + +#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 index 000000000000..84928e667884 --- /dev/null +++ b/arch/arm/mach-tegra/tegra_i2s_audio.c @@ -0,0 +1,1099 @@ +/* + * arch/arm/mach-tegra/tegra_i2s_audio.c + * + * Copyright (C) 2010 Google, Inc. + * + * Author: + * Iliyan Malchev + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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");